mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-02 18:39:05 +01:00
add connection modal (#64)
* various improvements * animation menu on close * finish custom dropdown * minor change * minor fix * add error support for dropdown * create remote modal * NumberField component and more on add remote connection modal implementation * extend TextField instead. dobounce should be handled by the user not the component * PasswordField * remove used code. fix ssh key file field issue * style buttons. fix modal height issue * use react portal for dropdown menu * remove debugging code * tooltip implementation * put back modal background * (sawka) add autofocus attribute to textfield. change label color to color-secondary. fix file menu name. fix typescript error in basicrenderer
This commit is contained in:
parent
1a566d06aa
commit
a0479c4683
@ -24,7 +24,13 @@ import {
|
|||||||
import { RemotesModal } from "./connections/connections";
|
import { RemotesModal } from "./connections/connections";
|
||||||
import { TosModal } from "./common/modals/modals";
|
import { TosModal } from "./common/modals/modals";
|
||||||
import { MainSideBar } from "./sidebar/sidebar";
|
import { MainSideBar } from "./sidebar/sidebar";
|
||||||
import { DisconnectedModal, ClientStopModal, AlertModal, AboutModal } from "./common/modals/modals";
|
import {
|
||||||
|
DisconnectedModal,
|
||||||
|
ClientStopModal,
|
||||||
|
AlertModal,
|
||||||
|
AboutModal,
|
||||||
|
CreateRemoteConnModal,
|
||||||
|
} from "./common/modals/modals";
|
||||||
import { ErrorBoundary } from "./common/error/errorboundary";
|
import { ErrorBoundary } from "./common/error/errorboundary";
|
||||||
import "./app.less";
|
import "./app.less";
|
||||||
|
|
||||||
@ -79,7 +85,10 @@ class App extends React.Component<{}, {}> {
|
|||||||
let sessionSettingsModal = GlobalModel.sessionSettingsModal.get();
|
let sessionSettingsModal = GlobalModel.sessionSettingsModal.get();
|
||||||
let lineSettingsModal = GlobalModel.lineSettingsModal.get();
|
let lineSettingsModal = GlobalModel.lineSettingsModal.get();
|
||||||
let clientSettingsModal = GlobalModel.clientSettingsModal.get();
|
let clientSettingsModal = GlobalModel.clientSettingsModal.get();
|
||||||
let remotesModal = GlobalModel.remotesModalModel.isOpen();
|
let remotesModel = GlobalModel.remotesModalModel;
|
||||||
|
let remotesModal = remotesModel.isOpen();
|
||||||
|
let selectedRemoteId = remotesModel.selectedRemoteId.get();
|
||||||
|
let remoteEdit = remotesModel.remoteEdit.get();
|
||||||
let disconnected = !GlobalModel.ws.open.get() || !GlobalModel.waveSrvRunning.get();
|
let disconnected = !GlobalModel.ws.open.get() || !GlobalModel.waveSrvRunning.get();
|
||||||
let hasClientStop = GlobalModel.getHasClientStop();
|
let hasClientStop = GlobalModel.getHasClientStop();
|
||||||
let dcWait = this.dcWait.get();
|
let dcWait = this.dcWait.get();
|
||||||
@ -127,6 +136,9 @@ class App extends React.Component<{}, {}> {
|
|||||||
<If condition={GlobalModel.aboutModalOpen.get()}>
|
<If condition={GlobalModel.aboutModalOpen.get()}>
|
||||||
<AboutModal />
|
<AboutModal />
|
||||||
</If>
|
</If>
|
||||||
|
<If condition={remoteEdit !== null && !remoteEdit.old}>
|
||||||
|
<CreateRemoteConnModal model={remotesModel} remoteEdit={remoteEdit} />
|
||||||
|
</If>
|
||||||
<If condition={screenSettingsModal != null}>
|
<If condition={screenSettingsModal != null}>
|
||||||
<ScreenSettingsModal
|
<ScreenSettingsModal
|
||||||
key={screenSettingsModal.sessionId + ":" + screenSettingsModal.screenId}
|
key={screenSettingsModal.sessionId + ":" + screenSettingsModal.screenId}
|
||||||
|
@ -257,20 +257,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.button.is-wave-green {
|
.wave-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 6px 16px !important;
|
padding: 6px 16px !important;
|
||||||
color: @term-bright-white !important;
|
color: @term-white !important;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
border-radius: 6px !important;
|
border-radius: 6px !important;
|
||||||
height: auto !important;
|
height: auto !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: @term-white !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-button.is-wave-green {
|
||||||
|
color: @term-bright-white !important;
|
||||||
background: @term-green !important;
|
background: @term-green !important;
|
||||||
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5),
|
|
||||||
0px 0px 0.5px 0px rgba(255, 255, 255, 0.8) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.6) inset;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: @term-green;
|
background-color: @term-green;
|
||||||
|
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5),
|
||||||
|
0px 0px 0.5px 0px rgba(255, 255, 255, 0.8) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.6) inset;
|
||||||
color: @term-bright-white !important;
|
color: @term-bright-white !important;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
@ -625,17 +633,173 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.textfield {
|
.wave-dropdown {
|
||||||
|
position: relative;
|
||||||
|
background-color: transparent;
|
||||||
|
height: 44px;
|
||||||
|
width: 412px;
|
||||||
|
border: 1px solid var(--element-separator, rgba(241, 246, 243, 0.15));
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--element-hover-2, rgba(255, 255, 255, 0.06));
|
||||||
|
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5),
|
||||||
|
0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||||
|
|
||||||
|
&-label {
|
||||||
|
position: absolute;
|
||||||
|
left: 16px;
|
||||||
|
top: 16px;
|
||||||
|
font-size: 12.5px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
color: @term-white;
|
||||||
|
line-height: 10px;
|
||||||
|
|
||||||
|
&.float {
|
||||||
|
font-size: 10px;
|
||||||
|
top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.offset-left {
|
||||||
|
left: 42px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-display {
|
||||||
|
position: absolute;
|
||||||
|
left: 16px;
|
||||||
|
bottom: 5px;
|
||||||
|
|
||||||
|
&.offset-left {
|
||||||
|
left: 42px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-arrow {
|
||||||
|
position: absolute;
|
||||||
|
right: 16px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
transition: transform 0.3s;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-arrow-rotate {
|
||||||
|
transform: translateY(-50%) rotate(180deg); // Rotate the arrow when dropdown is open
|
||||||
|
}
|
||||||
|
|
||||||
|
&-item {
|
||||||
|
display: flex;
|
||||||
|
min-width: 120px;
|
||||||
|
padding: 5px 8px;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
align-self: stretch;
|
||||||
|
border-radius: 6px;
|
||||||
|
|
||||||
|
&-highlighted,
|
||||||
|
&:hover {
|
||||||
|
background: var(--element-active, rgba(241, 246, 243, 0.08));
|
||||||
|
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5),
|
||||||
|
0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-input-decoration {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-input-decoration.end-position {
|
||||||
|
margin-right: 44px;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-input-decoration.start-position {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-error {
|
||||||
|
border-color: @term-red;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: @term-green;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-dropdown-menu {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 0;
|
||||||
|
margin-top: 2px;
|
||||||
|
padding: 0;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 6px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--olive-dark-1, #151715);
|
||||||
|
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.5), 0px 3px 8px 0px rgba(0, 0, 0, 0.35), 0px 0px 0.5px 0px #fff inset,
|
||||||
|
0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||||
|
animation-fill-mode: forwards;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-dropdown-menu-close {
|
||||||
|
z-index: 0;
|
||||||
|
animation: waveDropdownMenuFadeOut 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-textfield.wave-password {
|
||||||
|
.wave-textfield-inner-eye {
|
||||||
|
position: absolute;
|
||||||
|
right: 16px;
|
||||||
|
top: 52%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
transition: transform 0.3s;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-input-decoration {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-input-decoration.end-position {
|
||||||
|
margin-right: 47px;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-input-decoration.start-position {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-textfield {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border: 1px solid @term-white;
|
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-bottom: 1rem;
|
|
||||||
background-color: transparent;
|
|
||||||
height: 44px;
|
height: 44px;
|
||||||
min-width: 412px;
|
min-width: 412px;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
border: 1px solid var(--element-separator, rgba(241, 246, 243, 0.15));
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--element-hover-2, rgba(255, 255, 255, 0.06));
|
||||||
|
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.4), 0px 0px 0.5px 0px rgba(0, 0, 0, 0.5),
|
||||||
|
0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||||
|
|
||||||
&.focused {
|
&.focused {
|
||||||
border-color: @term-green;
|
border-color: @term-green;
|
||||||
@ -645,20 +809,20 @@
|
|||||||
border-color: @term-red;
|
border-color: @term-red;
|
||||||
}
|
}
|
||||||
|
|
||||||
.textfield-inner {
|
&-inner {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
.textfield-label {
|
&-label {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 16px;
|
left: 16px;
|
||||||
top: 16px;
|
top: 16px;
|
||||||
font-size: 12.5px;
|
font-size: 12.5px;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
color: @term-white;
|
color: @text-secondary;
|
||||||
line-height: 10px;
|
line-height: 10px;
|
||||||
|
|
||||||
&.float {
|
&.float {
|
||||||
@ -666,12 +830,12 @@
|
|||||||
top: 5px;
|
top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.start {
|
&.offset-left {
|
||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.textfield-input {
|
&-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
border: none;
|
border: none;
|
||||||
@ -682,23 +846,51 @@
|
|||||||
color: @term-bright-white;
|
color: @term-bright-white;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
|
|
||||||
&.start {
|
&.offset-left {
|
||||||
padding: 5px 16px 5px 0;
|
padding: 5px 16px 5px 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-input-decoration {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
i {
|
i {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-decoration {
|
.wave-input-decoration.start-position {
|
||||||
|
margin: 0 4px 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-input-decoration.end-position {
|
||||||
|
margin: 0 16px 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-tooltip {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
position: absolute;
|
||||||
justify-content: center;
|
z-index: 1000;
|
||||||
padding: 0 8px;
|
flex-direction: row;
|
||||||
margin: 8px;
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #777;
|
||||||
|
background-color: #444;
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 300px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
display: inline;
|
||||||
|
font-size: 16px;
|
||||||
|
fill: @base-color;
|
||||||
|
padding-top: 0.2em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.inline-edit {
|
.inline-edit {
|
||||||
@ -717,11 +909,11 @@
|
|||||||
|
|
||||||
&.edit-not-active {
|
&.edit-not-active {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
i.fa-pen {
|
i.fa-pen {
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
text-decoration-style: dotted;
|
text-decoration-style: dotted;
|
||||||
@ -733,9 +925,9 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
height: 20px;
|
height: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ import remarkGfm from "remark-gfm";
|
|||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
import { If } from "tsx-control-statements/components";
|
import { If } from "tsx-control-statements/components";
|
||||||
import type { RemoteType } from "../../types/types";
|
import type { RemoteType } from "../../types/types";
|
||||||
import { debounce } from "throttle-debounce";
|
import ReactDOM from "react-dom";
|
||||||
|
|
||||||
import { ReactComponent as CheckIcon } from "../assets/icons/line/check.svg";
|
import { ReactComponent as CheckIcon } from "../assets/icons/line/check.svg";
|
||||||
import { ReactComponent as CopyIcon } from "../assets/icons/history/copy.svg";
|
import { ReactComponent as CopyIcon } from "../assets/icons/history/copy.svg";
|
||||||
@ -125,14 +125,95 @@ class Checkbox extends React.Component<
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface InputDecorationProps {
|
interface InputDecorationProps {
|
||||||
|
position?: "start" | "end";
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mobxReact.observer
|
@mobxReact.observer
|
||||||
class InputDecoration extends React.Component<InputDecorationProps, {}> {
|
class InputDecoration extends React.Component<InputDecorationProps, {}> {
|
||||||
render() {
|
render() {
|
||||||
const { children } = this.props;
|
const { children, position = "end" } = this.props;
|
||||||
return <div className="input-decoration">{children}</div>;
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("wave-input-decoration", {
|
||||||
|
"start-position": position === "start",
|
||||||
|
"end-position": position === "end",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TooltipProps {
|
||||||
|
message: React.ReactNode;
|
||||||
|
icon?: React.ReactNode; // Optional icon property
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TooltipState {
|
||||||
|
isVisible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mobxReact.observer
|
||||||
|
class Tooltip extends React.Component<TooltipProps, TooltipState> {
|
||||||
|
iconRef: React.RefObject<HTMLDivElement>;
|
||||||
|
|
||||||
|
constructor(props: TooltipProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
isVisible: false,
|
||||||
|
};
|
||||||
|
this.iconRef = React.createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
showBubble() {
|
||||||
|
this.setState({ isVisible: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
hideBubble() {
|
||||||
|
this.setState({ isVisible: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
calculatePosition() {
|
||||||
|
// Get the position of the icon element
|
||||||
|
const iconElement = this.iconRef.current;
|
||||||
|
if (iconElement) {
|
||||||
|
const rect = iconElement.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
top: `${rect.bottom + window.scrollY - 29.5}px`,
|
||||||
|
left: `${rect.left + window.scrollX + rect.width / 2 - 19}px`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
renderBubble() {
|
||||||
|
if (!this.state.isVisible) return null;
|
||||||
|
|
||||||
|
const style = this.calculatePosition();
|
||||||
|
|
||||||
|
return ReactDOM.createPortal(
|
||||||
|
<div className="wave-tooltip" style={style}>
|
||||||
|
{this.props.icon && <div className="wave-tooltip-icon">{this.props.icon}</div>}
|
||||||
|
<div className="wave-tooltip-message">{this.props.message}</div>
|
||||||
|
</div>,
|
||||||
|
document.getElementById("app")!
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div onMouseEnter={this.showBubble} onMouseLeave={this.hideBubble} ref={this.iconRef}>
|
||||||
|
{this.props.children}
|
||||||
|
{this.renderBubble()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,6 +230,8 @@ interface TextFieldProps {
|
|||||||
defaultValue?: string;
|
defaultValue?: string;
|
||||||
decoration?: TextFieldDecorationProps;
|
decoration?: TextFieldDecorationProps;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
|
maxLength?: number;
|
||||||
|
autoFocus?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TextFieldState {
|
interface TextFieldState {
|
||||||
@ -207,14 +290,9 @@ class TextField extends React.Component<TextFieldProps, TextFieldState> {
|
|||||||
this.setState((prevState) => ({ showHelpText: !prevState.showHelpText }));
|
this.setState((prevState) => ({ showHelpText: !prevState.showHelpText }));
|
||||||
}
|
}
|
||||||
|
|
||||||
debouncedOnChange = debounce(300, (value) => {
|
|
||||||
const { onChange } = this.props;
|
|
||||||
onChange?.(value);
|
|
||||||
});
|
|
||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
handleInputChange(e: React.ChangeEvent<HTMLInputElement>) {
|
handleInputChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
const { required } = this.props;
|
const { required, onChange } = this.props;
|
||||||
const inputValue = e.target.value;
|
const inputValue = e.target.value;
|
||||||
|
|
||||||
// Check if value is empty and the field is required
|
// Check if value is empty and the field is required
|
||||||
@ -229,31 +307,31 @@ class TextField extends React.Component<TextFieldProps, TextFieldState> {
|
|||||||
this.setState({ internalValue: inputValue });
|
this.setState({ internalValue: inputValue });
|
||||||
}
|
}
|
||||||
|
|
||||||
this.debouncedOnChange(inputValue);
|
onChange && onChange(inputValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { label, value, placeholder, decoration, className } = this.props;
|
const { label, value, placeholder, decoration, className, maxLength, autoFocus } = this.props;
|
||||||
const { focused, internalValue, error } = this.state;
|
const { focused, internalValue, error } = this.state;
|
||||||
|
|
||||||
// Decide if the input should behave as controlled or uncontrolled
|
// Decide if the input should behave as controlled or uncontrolled
|
||||||
const inputValue = value !== undefined ? value : internalValue;
|
const inputValue = value !== undefined ? value : internalValue;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn(`textfield ${className || ""}`, { focused: focused, error: error })}>
|
<div className={cn(`wave-textfield ${className || ""}`, { focused: focused, error: error })}>
|
||||||
{decoration?.startDecoration && <>{decoration.startDecoration}</>}
|
{decoration?.startDecoration && <>{decoration.startDecoration}</>}
|
||||||
<div className="textfield-inner">
|
<div className="wave-textfield-inner">
|
||||||
<label
|
<label
|
||||||
className={cn("textfield-label", {
|
className={cn("wave-textfield-inner-label", {
|
||||||
float: this.state.hasContent || this.state.focused || placeholder,
|
float: this.state.hasContent || this.state.focused || placeholder,
|
||||||
start: decoration?.startDecoration,
|
"offset-left": decoration?.startDecoration,
|
||||||
})}
|
})}
|
||||||
htmlFor={label}
|
htmlFor={label}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
className={cn("textfield-input", { start: decoration?.startDecoration })}
|
className={cn("wave-textfield-inner-input", { "offset-left": decoration?.startDecoration })}
|
||||||
ref={this.inputRef}
|
ref={this.inputRef}
|
||||||
id={label}
|
id={label}
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
@ -261,9 +339,152 @@ class TextField extends React.Component<TextFieldProps, TextFieldState> {
|
|||||||
onFocus={this.handleFocus}
|
onFocus={this.handleFocus}
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
maxLength={maxLength}
|
||||||
|
autoFocus={autoFocus}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{decoration?.endDecoration && <div>{decoration.endDecoration}</div>}
|
{decoration?.endDecoration && <>{decoration.endDecoration}</>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NumberField extends TextField {
|
||||||
|
@boundMethod
|
||||||
|
handleInputChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
const { required, onChange } = this.props;
|
||||||
|
const inputValue = e.target.value;
|
||||||
|
|
||||||
|
// Allow only numeric input
|
||||||
|
if (inputValue === "" || /^\d*$/.test(inputValue)) {
|
||||||
|
// Update the internal state only if the component is not controlled.
|
||||||
|
if (this.props.value === undefined) {
|
||||||
|
const isError = required ? inputValue.trim() === "" : false;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
internalValue: inputValue,
|
||||||
|
error: isError,
|
||||||
|
hasContent: Boolean(inputValue),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange && onChange(inputValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
|
||||||
|
// Allow backspace, delete, tab, escape, and enter
|
||||||
|
if (
|
||||||
|
[46, 8, 9, 27, 13].includes(event.keyCode) ||
|
||||||
|
// Allow: Ctrl+A, Ctrl+C, Ctrl+X
|
||||||
|
((event.keyCode === 65 || event.keyCode === 67 || event.keyCode === 88) && event.ctrlKey === true) ||
|
||||||
|
// Allow: home, end, left, right
|
||||||
|
(event.keyCode >= 35 && event.keyCode <= 39)
|
||||||
|
) {
|
||||||
|
return; // let it happen, don't do anything
|
||||||
|
}
|
||||||
|
// Ensure that it is a number and stop the keypress
|
||||||
|
if (
|
||||||
|
(event.shiftKey || event.keyCode < 48 || event.keyCode > 57) &&
|
||||||
|
(event.keyCode < 96 || event.keyCode > 105)
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
// Use the render method from TextField but add the onKeyDown handler
|
||||||
|
const renderedTextField = super.render();
|
||||||
|
|
||||||
|
return React.cloneElement(renderedTextField, {
|
||||||
|
onKeyDown: this.handleKeyDown,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PasswordFieldState extends TextFieldState {
|
||||||
|
passwordVisible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mobxReact.observer
|
||||||
|
class PasswordField extends TextField {
|
||||||
|
state: PasswordFieldState;
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
...this.state,
|
||||||
|
passwordVisible: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
togglePasswordVisibility() {
|
||||||
|
//@ts-ignore
|
||||||
|
this.setState((prevState) => ({
|
||||||
|
//@ts-ignore
|
||||||
|
passwordVisible: !prevState.passwordVisible,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleInputChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
|
// Call the parent handleInputChange method
|
||||||
|
super.handleInputChange(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { decoration, className, placeholder, maxLength, label } = this.props;
|
||||||
|
const { focused, internalValue, error, passwordVisible } = this.state;
|
||||||
|
const inputValue = this.props.value !== undefined ? this.props.value : internalValue;
|
||||||
|
|
||||||
|
// The input should always receive the real value
|
||||||
|
const inputProps = {
|
||||||
|
className: cn("wave-textfield-inner-input", { "offset-left": decoration?.startDecoration }),
|
||||||
|
ref: this.inputRef,
|
||||||
|
id: label,
|
||||||
|
value: inputValue, // Always use the real value here
|
||||||
|
onChange: this.handleInputChange,
|
||||||
|
onFocus: this.handleFocus,
|
||||||
|
onBlur: this.handleBlur,
|
||||||
|
placeholder: placeholder,
|
||||||
|
maxLength: maxLength,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn(`wave-textfield wave-password ${className || ""}`, { focused: focused, error: error })}>
|
||||||
|
{decoration?.startDecoration && <>{decoration.startDecoration}</>}
|
||||||
|
<div className="wave-textfield-inner">
|
||||||
|
<label
|
||||||
|
className={cn("wave-textfield-inner-label", {
|
||||||
|
float: this.state.hasContent || this.state.focused || placeholder,
|
||||||
|
"offset-left": decoration?.startDecoration,
|
||||||
|
})}
|
||||||
|
htmlFor={label}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
<If condition={passwordVisible}>
|
||||||
|
<input {...inputProps} type="text" />
|
||||||
|
</If>
|
||||||
|
<If condition={!passwordVisible}>
|
||||||
|
<input {...inputProps} type="password" />
|
||||||
|
</If>
|
||||||
|
<div
|
||||||
|
className="wave-textfield-inner-eye"
|
||||||
|
onClick={this.togglePasswordVisibility}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
<If condition={passwordVisible}>
|
||||||
|
<i className="fa-sharp fa-solid fa-eye"></i>
|
||||||
|
</If>
|
||||||
|
<If condition={!passwordVisible}>
|
||||||
|
<i className="fa-sharp fa-solid fa-eye-slash"></i>
|
||||||
|
</If>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{decoration?.endDecoration && <>{decoration.endDecoration}</>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -387,7 +608,9 @@ class InlineSettingsTextEdit extends React.Component<
|
|||||||
title="Cancel (Esc)"
|
title="Cancel (Esc)"
|
||||||
className="button is-prompt-danger is-outlined is-small"
|
className="button is-prompt-danger is-outlined is-small"
|
||||||
>
|
>
|
||||||
<span className="icon is-small"><i className="fa-sharp fa-solid fa-xmark"/></span>
|
<span className="icon is-small">
|
||||||
|
<i className="fa-sharp fa-solid fa-xmark" />
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="control">
|
<div className="control">
|
||||||
@ -396,7 +619,9 @@ class InlineSettingsTextEdit extends React.Component<
|
|||||||
title="Confirm (Enter)"
|
title="Confirm (Enter)"
|
||||||
className="button is-prompt-green is-outlined is-small"
|
className="button is-prompt-green is-outlined is-small"
|
||||||
>
|
>
|
||||||
<span className="icon is-small"><i className="fa-sharp fa-solid fa-check"/></span>
|
<span className="icon is-small">
|
||||||
|
<i className="fa-sharp fa-solid fa-check" />
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -407,7 +632,7 @@ class InlineSettingsTextEdit extends React.Component<
|
|||||||
<div onClick={this.clickEdit} className={cn("settings-input inline-edit", "edit-not-active")}>
|
<div onClick={this.clickEdit} className={cn("settings-input inline-edit", "edit-not-active")}>
|
||||||
{this.props.text}
|
{this.props.text}
|
||||||
<If condition={this.props.showIcon}>
|
<If condition={this.props.showIcon}>
|
||||||
<i className="fa-sharp fa-solid fa-pen"/>
|
<i className="fa-sharp fa-solid fa-pen" />
|
||||||
</If>
|
</If>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -498,6 +723,247 @@ class SettingsError extends React.Component<{ errorMessage: OV<string> }, {}> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DropdownDecorationProps {
|
||||||
|
startDecoration?: React.ReactNode;
|
||||||
|
endDecoration?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DropdownProps {
|
||||||
|
label: string;
|
||||||
|
options: { value: string; label: string }[];
|
||||||
|
value?: string;
|
||||||
|
className?: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
decoration?: DropdownDecorationProps;
|
||||||
|
defaultValue?: string;
|
||||||
|
required?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DropdownState {
|
||||||
|
isOpen: boolean;
|
||||||
|
internalValue: string;
|
||||||
|
highlightedIndex: number;
|
||||||
|
isTouched: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mobxReact.observer
|
||||||
|
class Dropdown extends React.Component<DropdownProps, DropdownState> {
|
||||||
|
wrapperRef: React.RefObject<HTMLDivElement>;
|
||||||
|
menuRef: React.RefObject<HTMLDivElement>;
|
||||||
|
timeoutId: any;
|
||||||
|
|
||||||
|
constructor(props: DropdownProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
isOpen: false,
|
||||||
|
internalValue: props.defaultValue || "",
|
||||||
|
highlightedIndex: -1,
|
||||||
|
isTouched: false,
|
||||||
|
};
|
||||||
|
this.wrapperRef = React.createRef();
|
||||||
|
this.menuRef = React.createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
document.addEventListener("mousedown", this.handleClickOutside);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
document.removeEventListener("mousedown", this.handleClickOutside);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: Readonly<DropdownProps>, prevState: Readonly<DropdownState>, snapshot?: any): void {
|
||||||
|
// If the dropdown was open but now is closed, start the timeout
|
||||||
|
if (prevState.isOpen && !this.state.isOpen) {
|
||||||
|
this.timeoutId = setTimeout(() => {
|
||||||
|
if (this.menuRef.current) {
|
||||||
|
this.menuRef.current.style.display = "none";
|
||||||
|
}
|
||||||
|
}, 300); // Time is equal to the animation duration
|
||||||
|
}
|
||||||
|
// If the dropdown is now open, cancel any existing timeout and show the menu
|
||||||
|
else if (!prevState.isOpen && this.state.isOpen) {
|
||||||
|
if (this.timeoutId !== null) {
|
||||||
|
clearTimeout(this.timeoutId); // Cancel any existing timeout
|
||||||
|
this.timeoutId = null;
|
||||||
|
}
|
||||||
|
if (this.menuRef.current) {
|
||||||
|
this.menuRef.current.style.display = "inline-flex";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleClickOutside(event: MouseEvent) {
|
||||||
|
// Check if the click is outside both the wrapper and the menu
|
||||||
|
if (
|
||||||
|
this.wrapperRef.current &&
|
||||||
|
!this.wrapperRef.current.contains(event.target as Node) &&
|
||||||
|
this.menuRef.current &&
|
||||||
|
!this.menuRef.current.contains(event.target as Node)
|
||||||
|
) {
|
||||||
|
this.setState({ isOpen: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleClick() {
|
||||||
|
this.toggleDropdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleFocus() {
|
||||||
|
this.setState({ isTouched: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleKeyDown(event: React.KeyboardEvent) {
|
||||||
|
const { options } = this.props;
|
||||||
|
const { isOpen, highlightedIndex } = this.state;
|
||||||
|
|
||||||
|
switch (event.key) {
|
||||||
|
case "Enter":
|
||||||
|
case " ":
|
||||||
|
if (isOpen) {
|
||||||
|
const option = options[highlightedIndex];
|
||||||
|
if (option) {
|
||||||
|
this.handleSelect(option.value, undefined);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.toggleDropdown();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "Escape":
|
||||||
|
this.setState({ isOpen: false });
|
||||||
|
break;
|
||||||
|
case "ArrowUp":
|
||||||
|
if (isOpen) {
|
||||||
|
this.setState((prevState) => ({
|
||||||
|
highlightedIndex:
|
||||||
|
prevState.highlightedIndex > 0 ? prevState.highlightedIndex - 1 : options.length - 1,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "ArrowDown":
|
||||||
|
if (isOpen) {
|
||||||
|
this.setState((prevState) => ({
|
||||||
|
highlightedIndex:
|
||||||
|
prevState.highlightedIndex < options.length - 1 ? prevState.highlightedIndex + 1 : 0,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "Tab":
|
||||||
|
this.setState({ isOpen: false });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleSelect(value: string, event?: React.MouseEvent | React.KeyboardEvent) {
|
||||||
|
const { onChange } = this.props;
|
||||||
|
if (event) {
|
||||||
|
event.stopPropagation(); // This stops the event from bubbling up to the wrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!("value" in this.props)) {
|
||||||
|
this.setState({ internalValue: value });
|
||||||
|
}
|
||||||
|
onChange(value);
|
||||||
|
this.setState({ isOpen: false, isTouched: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
toggleDropdown() {
|
||||||
|
this.setState((prevState) => ({ isOpen: !prevState.isOpen, isTouched: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
calculatePosition(): React.CSSProperties {
|
||||||
|
if (this.wrapperRef.current) {
|
||||||
|
const rect = this.wrapperRef.current.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
position: "absolute",
|
||||||
|
top: `${rect.bottom + window.scrollY}px`,
|
||||||
|
left: `${rect.left + window.scrollX}px`,
|
||||||
|
width: `${rect.width}px`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { label, options, value, placeholder, decoration, className, required } = this.props;
|
||||||
|
const { isOpen, internalValue, highlightedIndex, isTouched } = this.state;
|
||||||
|
|
||||||
|
const currentValue = value !== undefined ? value : internalValue;
|
||||||
|
const selectedOptionLabel =
|
||||||
|
options.find((option) => option.value === currentValue)?.label || placeholder || internalValue;
|
||||||
|
|
||||||
|
// Determine if the dropdown should be marked as having an error
|
||||||
|
const isError =
|
||||||
|
required &&
|
||||||
|
(value === undefined || value === "") &&
|
||||||
|
(internalValue === undefined || internalValue === "") &&
|
||||||
|
isTouched;
|
||||||
|
|
||||||
|
// Determine if the label should float
|
||||||
|
const shouldLabelFloat = !!value || !!internalValue || !!placeholder || isOpen;
|
||||||
|
|
||||||
|
const dropdownMenu = isOpen
|
||||||
|
? ReactDOM.createPortal(
|
||||||
|
<div className={cn("wave-dropdown-menu")} ref={this.menuRef} style={this.calculatePosition()}>
|
||||||
|
{options.map((option, index) => (
|
||||||
|
<div
|
||||||
|
key={option.value}
|
||||||
|
className={cn("wave-dropdown-item", {
|
||||||
|
"wave-dropdown-item-highlighted": index === highlightedIndex,
|
||||||
|
})}
|
||||||
|
onClick={(e) => this.handleSelect(option.value, e)}
|
||||||
|
onMouseEnter={() => this.setState({ highlightedIndex: index })}
|
||||||
|
onMouseLeave={() => this.setState({ highlightedIndex: -1 })}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>,
|
||||||
|
document.getElementById("app")!
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(`wave-dropdown ${className || ""}`, {
|
||||||
|
"wave-dropdown-error": isError,
|
||||||
|
})}
|
||||||
|
ref={this.wrapperRef}
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={this.handleKeyDown}
|
||||||
|
onClick={this.handleClick}
|
||||||
|
onFocus={this.handleFocus}
|
||||||
|
>
|
||||||
|
{decoration?.startDecoration && <>{decoration.startDecoration}</>}
|
||||||
|
<div
|
||||||
|
className={cn("wave-dropdown-label", {
|
||||||
|
float: shouldLabelFloat,
|
||||||
|
"offset-left": decoration?.startDecoration,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
<div className={cn("wave-dropdown-display", { "offset-left": decoration?.startDecoration })}>
|
||||||
|
{selectedOptionLabel}
|
||||||
|
</div>
|
||||||
|
<div className={cn("wave-dropdown-arrow", { "wave-dropdown-arrow-rotate": isOpen })}>
|
||||||
|
<i className="fa-sharp fa-solid fa-chevron-down"></i>
|
||||||
|
</div>
|
||||||
|
{dropdownMenu}
|
||||||
|
{decoration?.endDecoration && <>{decoration.endDecoration}</>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
CmdStrCode,
|
CmdStrCode,
|
||||||
Toggle,
|
Toggle,
|
||||||
@ -508,6 +974,10 @@ export {
|
|||||||
InfoMessage,
|
InfoMessage,
|
||||||
Markdown,
|
Markdown,
|
||||||
SettingsError,
|
SettingsError,
|
||||||
|
Dropdown,
|
||||||
TextField,
|
TextField,
|
||||||
InputDecoration,
|
InputDecoration,
|
||||||
|
NumberField,
|
||||||
|
PasswordField,
|
||||||
|
Tooltip,
|
||||||
};
|
};
|
||||||
|
@ -182,7 +182,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal.wave-modal {
|
.modal.wave-modal {
|
||||||
.modal-content {
|
.wave-modal-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
@ -192,14 +192,14 @@
|
|||||||
box-shadow: 0px 3px 5px 0px rgba(0, 0, 0, 0.35), 0px 10px 24px 0px rgba(0, 0, 0, 0.45),
|
box-shadow: 0px 3px 5px 0px rgba(0, 0, 0, 0.35), 0px 10px 24px 0px rgba(0, 0, 0, 0.45),
|
||||||
0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
0px 0px 0.5px 0px rgba(255, 255, 255, 0.5) inset, 0px 0.5px 0px 0px rgba(255, 255, 255, 0.2) inset;
|
||||||
|
|
||||||
.modal-content-wrapper {
|
.wave-modal-content-inner {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
header {
|
.wave-modal-header {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -208,14 +208,14 @@
|
|||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
border-bottom: 1px solid rgba(250, 250, 250, 0.1);
|
border-bottom: 1px solid rgba(250, 250, 250, 0.1);
|
||||||
|
|
||||||
.modal-title {
|
.wave-modal-title {
|
||||||
color: @term-bright-white;
|
color: @term-bright-white;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.close-icon-wrapper {
|
.wave-modal-close {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -223,7 +223,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.wave-modal-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
@ -235,7 +235,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal.tos-modal {
|
.modal.tos-modal {
|
||||||
.modal-content.tos-modal-content {
|
.modal-content.wave-modal-content {
|
||||||
padding: 32px 48px;
|
padding: 32px 48px;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
||||||
@ -324,13 +324,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal.about-modal {
|
.modal.about-modal {
|
||||||
.modal-content.about-modal-content {
|
.about-wave-modal-content {
|
||||||
width: 401px;
|
width: 401px;
|
||||||
|
|
||||||
.about-content {
|
.about-wave-modal-body {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
|
||||||
.wave-section {
|
.wave-modal-section {
|
||||||
.logo-wrapper {
|
.logo-wrapper {
|
||||||
width: 72px;
|
width: 72px;
|
||||||
height: 72px;
|
height: 72px;
|
||||||
@ -403,7 +403,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.wave-section:nth-child(3) {
|
.wave-modal-section:nth-child(3) {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@ -418,7 +418,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.wave-section:last-child {
|
.wave-modal-section:last-child {
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
color: @term-white;
|
color: @term-white;
|
||||||
}
|
}
|
||||||
@ -426,6 +426,48 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wave-modal.crconn-modal {
|
||||||
|
.wave-modal-content.crconn-wave-modal-content {
|
||||||
|
width: 452px;
|
||||||
|
min-height: 411px;
|
||||||
|
overflow: visible;
|
||||||
|
|
||||||
|
.wave-modal-content-inner.crconn-wave-modal-content-inner {
|
||||||
|
display: flex;
|
||||||
|
padding-bottom: 0px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.crconn-wave-modal-body {
|
||||||
|
display: flex;
|
||||||
|
padding: 0px 20px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
align-self: stretch;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.crconn-wave-modal-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 20px 20px;
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
div.button {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.wave-button {
|
.wave-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 6px 16px;
|
padding: 6px 16px;
|
||||||
@ -465,7 +507,7 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wave-section {
|
.wave-modal-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
@ -9,11 +9,13 @@ import { If, For } from "tsx-control-statements/components";
|
|||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||||
import { GlobalModel, GlobalCommandRunner } from "../../../model/model";
|
import { GlobalModel, GlobalCommandRunner, RemotesModalModel } from "../../../model/model";
|
||||||
import { Markdown } from "../common";
|
import * as T from "../../../types/types";
|
||||||
|
import { Markdown, InfoMessage } from "../common";
|
||||||
import * as util from "../../../util/util";
|
import * as util from "../../../util/util";
|
||||||
import { Toggle, Checkbox } from "../common";
|
import { Toggle, Checkbox } from "../common";
|
||||||
import { ClientDataType } from "../../../types/types";
|
import { ClientDataType } from "../../../types/types";
|
||||||
|
import { TextField, NumberField, InputDecoration, Dropdown, PasswordField, Tooltip } from "../common";
|
||||||
|
|
||||||
import close from "../../assets/icons/close.svg";
|
import close from "../../assets/icons/close.svg";
|
||||||
import { ReactComponent as WarningIcon } from "../../assets/icons/line/triangle-exclamation.svg";
|
import { ReactComponent as WarningIcon } from "../../assets/icons/line/triangle-exclamation.svg";
|
||||||
@ -22,6 +24,7 @@ import shield from "../../assets/icons/shield_check.svg";
|
|||||||
import help from "../../assets/icons/help_filled.svg";
|
import help from "../../assets/icons/help_filled.svg";
|
||||||
import github from "../../assets/icons/github.svg";
|
import github from "../../assets/icons/github.svg";
|
||||||
import logo from "../../assets/waveterm-logo-with-bg.svg";
|
import logo from "../../assets/waveterm-logo-with-bg.svg";
|
||||||
|
import { ReactComponent as AngleDownIcon } from "../../assets/icons/history/angle-down.svg";
|
||||||
|
|
||||||
dayjs.extend(localizedFormat);
|
dayjs.extend(localizedFormat);
|
||||||
|
|
||||||
@ -255,9 +258,9 @@ class TosModal extends React.Component<{}, {}> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("modal tos-modal wave-modal is-active")}>
|
<div className={cn("modal tos-modal wave-modal is-active")}>
|
||||||
<div className="modal-background" />
|
<div className="modal-background wave-modal-background" />
|
||||||
<div className="modal-content tos-modal-content">
|
<div className="modal-content wave-modal-content tos-wave-modal-content">
|
||||||
<div className="modal-content-wrapper">
|
<div className="modal-content-inner wave-modal-content-inner tos-wave-modal-content-inner">
|
||||||
<header className="tos-header unselectable">
|
<header className="tos-header unselectable">
|
||||||
<div className="modal-title">Welcome to Wave Terminal!</div>
|
<div className="modal-title">Welcome to Wave Terminal!</div>
|
||||||
<div className="modal-subtitle">Lets set everything for you</div>
|
<div className="modal-subtitle">Lets set everything for you</div>
|
||||||
@ -332,7 +335,7 @@ class TosModal extends React.Component<{}, {}> {
|
|||||||
<div className="button-wrapper">
|
<div className="button-wrapper">
|
||||||
<button
|
<button
|
||||||
onClick={this.acceptTos}
|
onClick={this.acceptTos}
|
||||||
className={cn("button is-wave-green is-outlined is-small", {
|
className={cn("button wave-button is-wave-green is-outlined is-small", {
|
||||||
"disabled-button": !this.state.isChecked,
|
"disabled-button": !this.state.isChecked,
|
||||||
})}
|
})}
|
||||||
disabled={!this.state.isChecked}
|
disabled={!this.state.isChecked}
|
||||||
@ -375,7 +378,7 @@ class AboutModal extends React.Component<{}, {}> {
|
|||||||
<div className="text-selectable">Client Version v0.4.0 20231016-110014</div>
|
<div className="text-selectable">Client Version v0.4.0 20231016-110014</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isUpToDate) {
|
if (isUpToDate) {
|
||||||
return (
|
return (
|
||||||
<div className="status updated">
|
<div className="status updated">
|
||||||
@ -406,17 +409,17 @@ class AboutModal extends React.Component<{}, {}> {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className={cn("modal about-modal wave-modal is-active")}>
|
<div className={cn("modal about-modal wave-modal is-active")}>
|
||||||
<div className="modal-background" />
|
<div className="modal-background wave-modal-background" />
|
||||||
<div className="modal-content about-modal-content unselectable">
|
<div className="modal-content wave-modal-content about-wave-modal-content">
|
||||||
<div className="modal-content-wrapper">
|
<div className="modal-content-inner wave-modal-content-inner about-wave-modal-content-inner">
|
||||||
<header className="common-header">
|
<header className="wave-modal-header about-wave-modal-header">
|
||||||
<div className="modal-title">About</div>
|
<div className="wave-modal-title about-wave-modal-title">About</div>
|
||||||
<div className="close-icon-wrapper" onClick={this.closeModal}>
|
<div className="wave-modal-close about-wave-modal-close" onClick={this.closeModal}>
|
||||||
<img src={close} alt="Close (Escape)" />
|
<img src={close} alt="Close (Escape)" />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div className="content about-content">
|
<div className="wave-modal-body about-wave-modal-body">
|
||||||
<section className="wave-section about-section">
|
<section className="wave-modal-section about-section">
|
||||||
<div className="logo-wrapper">
|
<div className="logo-wrapper">
|
||||||
<img src={logo} alt="logo" />
|
<img src={logo} alt="logo" />
|
||||||
</div>
|
</div>
|
||||||
@ -425,10 +428,10 @@ class AboutModal extends React.Component<{}, {}> {
|
|||||||
<div className="text-standard">Modern Terminal for Seamless Workflow</div>
|
<div className="text-standard">Modern Terminal for Seamless Workflow</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section className="wave-section about-section text-standard">
|
<section className="wave-modal-section about-section text-standard">
|
||||||
{this.getStatus(this.isUpToDate())}
|
{this.getStatus(this.isUpToDate())}
|
||||||
</section>
|
</section>
|
||||||
<section className="wave-section about-section">
|
<section className="wave-modal-section about-section">
|
||||||
<a
|
<a
|
||||||
className="wave-button wave-button-link color-standard"
|
className="wave-button wave-button-link color-standard"
|
||||||
href={util.makeExternLink("https://github.com/wavetermdev/waveterm")}
|
href={util.makeExternLink("https://github.com/wavetermdev/waveterm")}
|
||||||
@ -456,7 +459,7 @@ class AboutModal extends React.Component<{}, {}> {
|
|||||||
License
|
License
|
||||||
</a>
|
</a>
|
||||||
</section>
|
</section>
|
||||||
<section className="wave-section about-section text-standard">
|
<section className="wave-modal-section about-section text-standard">
|
||||||
Copyright © 2023 Command Line Inc.
|
Copyright © 2023 Command Line Inc.
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@ -467,4 +470,333 @@ class AboutModal extends React.Component<{}, {}> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { LoadingSpinner, ClientStopModal, AlertModal, DisconnectedModal, TosModal, AboutModal };
|
@mobxReact.observer
|
||||||
|
class CreateRemoteConnModal extends React.Component<{ model: RemotesModalModel; remoteEdit: T.RemoteEditType }, {}> {
|
||||||
|
tempAlias: OV<string>;
|
||||||
|
tempHostName: OV<string>;
|
||||||
|
tempPort: OV<string>;
|
||||||
|
tempAuthMode: OV<string>;
|
||||||
|
tempConnectMode: OV<string>;
|
||||||
|
tempManualMode: OV<boolean>;
|
||||||
|
tempPassword: OV<string>;
|
||||||
|
tempKeyFile: OV<string>;
|
||||||
|
errorStr: OV<string>;
|
||||||
|
|
||||||
|
constructor(props: any) {
|
||||||
|
super(props);
|
||||||
|
let { remoteEdit } = this.props;
|
||||||
|
this.tempAlias = mobx.observable.box("", { name: "CreateRemote-alias" });
|
||||||
|
this.tempHostName = mobx.observable.box("", { name: "CreateRemote-hostName" });
|
||||||
|
this.tempPort = mobx.observable.box("", { name: "CreateRemote-port" });
|
||||||
|
this.tempAuthMode = mobx.observable.box("none", { name: "CreateRemote-authMode" });
|
||||||
|
this.tempConnectMode = mobx.observable.box("auto", { name: "CreateRemote-connectMode" });
|
||||||
|
this.tempKeyFile = mobx.observable.box("", { name: "CreateRemote-keystr" });
|
||||||
|
this.tempPassword = mobx.observable.box("", { name: "CreateRemote-password" });
|
||||||
|
this.errorStr = mobx.observable.box(remoteEdit.errorstr, { name: "CreateRemote-errorStr" });
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteCName(): string {
|
||||||
|
let hostName = this.tempHostName.get();
|
||||||
|
if (hostName == "") {
|
||||||
|
return "[no host]";
|
||||||
|
}
|
||||||
|
if (hostName.indexOf("@") == -1) {
|
||||||
|
hostName = "[no user]@" + hostName;
|
||||||
|
}
|
||||||
|
return hostName;
|
||||||
|
}
|
||||||
|
|
||||||
|
getErrorStr(): string {
|
||||||
|
if (this.errorStr.get() != null) {
|
||||||
|
return this.errorStr.get();
|
||||||
|
}
|
||||||
|
return this.props.remoteEdit.errorstr;
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
submitRemote(): void {
|
||||||
|
mobx.action(() => {
|
||||||
|
this.errorStr.set(null);
|
||||||
|
})();
|
||||||
|
let authMode = this.tempAuthMode.get();
|
||||||
|
let cname = this.tempHostName.get();
|
||||||
|
if (cname == "") {
|
||||||
|
this.errorStr.set("You must specify a 'user@host' value to create a new connection");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let kwargs: Record<string, string> = {};
|
||||||
|
kwargs["alias"] = this.tempAlias.get();
|
||||||
|
if (this.tempPort.get() != "" && this.tempPort.get() != "22") {
|
||||||
|
kwargs["port"] = this.tempPort.get();
|
||||||
|
}
|
||||||
|
if (authMode == "key" || authMode == "key+password") {
|
||||||
|
if (this.tempKeyFile.get() == "") {
|
||||||
|
this.errorStr.set("When AuthMode is set to 'key', you must supply a valid key file name.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
kwargs["key"] = this.tempKeyFile.get();
|
||||||
|
} else {
|
||||||
|
kwargs["key"] = "";
|
||||||
|
}
|
||||||
|
if (authMode == "password" || authMode == "key+password") {
|
||||||
|
if (this.tempPassword.get() == "") {
|
||||||
|
this.errorStr.set("When AuthMode is set to 'password', you must supply a password.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
kwargs["password"] = this.tempPassword.get();
|
||||||
|
} else {
|
||||||
|
kwargs["password"] = "";
|
||||||
|
}
|
||||||
|
kwargs["connectmode"] = this.tempConnectMode.get();
|
||||||
|
kwargs["visual"] = "1";
|
||||||
|
kwargs["submit"] = "1";
|
||||||
|
let model = this.props.model;
|
||||||
|
let prtn = GlobalCommandRunner.createRemote(cname, kwargs, false);
|
||||||
|
prtn.then((crtn) => {
|
||||||
|
if (crtn.success) {
|
||||||
|
let crRtn = GlobalCommandRunner.screenSetRemote(cname, true, false);
|
||||||
|
crRtn.then((crcrtn) => {
|
||||||
|
if (crcrtn.success) {
|
||||||
|
model.closeModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mobx.action(() => {
|
||||||
|
this.errorStr.set(crcrtn.error);
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mobx.action(() => {
|
||||||
|
this.errorStr.set(crtn.error);
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleChangeKeyFile(value: string): void {
|
||||||
|
mobx.action(() => {
|
||||||
|
this.tempKeyFile.set(value);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleChangePassword(value: string): void {
|
||||||
|
mobx.action(() => {
|
||||||
|
this.tempPassword.set(value);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleChangeAlias(value: string): void {
|
||||||
|
mobx.action(() => {
|
||||||
|
this.tempAlias.set(value);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleChangePort(value: string): void {
|
||||||
|
mobx.action(() => {
|
||||||
|
this.tempPort.set(value);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleChangeHostName(value: string): void {
|
||||||
|
mobx.action(() => {
|
||||||
|
this.tempHostName.set(value);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let { model, remoteEdit } = this.props;
|
||||||
|
let authMode = this.tempAuthMode.get();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("modal wave-modal crconn-modal is-active")}>
|
||||||
|
<div className="modal-background wave-modal-background" />
|
||||||
|
<div className="modal-content wave-modal-content crconn-wave-modal-content">
|
||||||
|
<div className="wave-modal-content-inner crconn-wave-modal-content-inner">
|
||||||
|
<header className="wave-modal-header crconn-wave-modal-header">
|
||||||
|
<div className="wave-modal-title crconn-wave-modal-title">Add Connection</div>
|
||||||
|
<div className="wave-modal-close crconn-wave-modal-close" onClick={model.cancelEditAuth}>
|
||||||
|
<img src={close} alt="Close (Escape)" />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div className="wave-modal-body crconn-wave-modal-body">
|
||||||
|
<div className="user-section">
|
||||||
|
<TextField
|
||||||
|
label="user@host"
|
||||||
|
autoFocus={true}
|
||||||
|
value={this.tempHostName.get()}
|
||||||
|
onChange={this.handleChangeHostName}
|
||||||
|
required={true}
|
||||||
|
decoration={{
|
||||||
|
endDecoration: (
|
||||||
|
<InputDecoration>
|
||||||
|
<Tooltip
|
||||||
|
message={`(Required) The user and host that you want to connect with. This is in the same format as
|
||||||
|
you would pass to ssh, e.g. "ubuntu@test.mydomain.com".`}
|
||||||
|
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||||
|
>
|
||||||
|
<i className="fa-sharp fa-regular fa-circle-question" />
|
||||||
|
</Tooltip>
|
||||||
|
</InputDecoration>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="alias-section">
|
||||||
|
<TextField
|
||||||
|
label="Alias"
|
||||||
|
onChange={this.handleChangeAlias}
|
||||||
|
value={this.tempAlias.get()}
|
||||||
|
maxLength={100}
|
||||||
|
decoration={{
|
||||||
|
endDecoration: (
|
||||||
|
<InputDecoration>
|
||||||
|
<Tooltip
|
||||||
|
message={`(Optional) A short alias to use when selecting or displaying this connection.`}
|
||||||
|
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||||
|
>
|
||||||
|
<i className="fa-sharp fa-regular fa-circle-question" />
|
||||||
|
</Tooltip>
|
||||||
|
</InputDecoration>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="port-section">
|
||||||
|
<NumberField
|
||||||
|
label="Port"
|
||||||
|
placeholder="22"
|
||||||
|
value={this.tempPort.get()}
|
||||||
|
onChange={this.handleChangePort}
|
||||||
|
decoration={{
|
||||||
|
endDecoration: (
|
||||||
|
<InputDecoration>
|
||||||
|
<Tooltip
|
||||||
|
message={`(Optional) Defaults to 22. Set if the server you are connecting to listens to a non-standard
|
||||||
|
SSH port.`}
|
||||||
|
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||||
|
>
|
||||||
|
<i className="fa-sharp fa-regular fa-circle-question" />
|
||||||
|
</Tooltip>
|
||||||
|
</InputDecoration>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="authmode-section">
|
||||||
|
<Dropdown
|
||||||
|
label="Auth Mode"
|
||||||
|
options={[
|
||||||
|
{ value: "none", label: "none" },
|
||||||
|
{ value: "key", label: "key" },
|
||||||
|
{ value: "password", label: "password" },
|
||||||
|
{ value: "key+password", label: "key+password" },
|
||||||
|
]}
|
||||||
|
value={this.tempAuthMode.get()}
|
||||||
|
onChange={(val: string) => {
|
||||||
|
this.tempAuthMode.set(val);
|
||||||
|
}}
|
||||||
|
decoration={{
|
||||||
|
endDecoration: (
|
||||||
|
<InputDecoration>
|
||||||
|
<Tooltip
|
||||||
|
message={
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>none</b> - no authentication, or authentication is
|
||||||
|
already configured in your ssh config.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>key</b> - use a private key.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>password</b> - use a password.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>key+password</b> - use a key with a passphrase.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||||
|
>
|
||||||
|
<i className="fa-sharp fa-regular fa-circle-question" />
|
||||||
|
</Tooltip>
|
||||||
|
</InputDecoration>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<If condition={authMode == "key" || authMode == "key+password"}>
|
||||||
|
<TextField
|
||||||
|
label="SSH Keyfile"
|
||||||
|
placeholder="keyfile path"
|
||||||
|
onChange={this.handleChangeKeyFile}
|
||||||
|
value={this.tempKeyFile.get()}
|
||||||
|
maxLength={400}
|
||||||
|
required={true}
|
||||||
|
decoration={{
|
||||||
|
endDecoration: (
|
||||||
|
<InputDecoration>
|
||||||
|
<Tooltip
|
||||||
|
message={`(Required) The path to your ssh key file.`}
|
||||||
|
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||||
|
>
|
||||||
|
<i className="fa-sharp fa-regular fa-circle-question" />
|
||||||
|
</Tooltip>
|
||||||
|
</InputDecoration>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</If>
|
||||||
|
<If condition={authMode == "password" || authMode == "key+password"}>
|
||||||
|
<PasswordField
|
||||||
|
label={authMode == "password" ? "SSH Password" : "Key Passphrase"}
|
||||||
|
placeholder="password"
|
||||||
|
onChange={this.handleChangePassword}
|
||||||
|
value={this.tempPassword.get()}
|
||||||
|
maxLength={400}
|
||||||
|
/>
|
||||||
|
</If>
|
||||||
|
<div className="connectmode-section">
|
||||||
|
<Dropdown
|
||||||
|
label="Connect Mode"
|
||||||
|
options={[
|
||||||
|
{ value: "startup", label: "startup" },
|
||||||
|
{ value: "key", label: "key" },
|
||||||
|
{ value: "auto", label: "auto" },
|
||||||
|
{ value: "manual", label: "manual" },
|
||||||
|
]}
|
||||||
|
value={this.tempConnectMode.get()}
|
||||||
|
onChange={(val: string) => {
|
||||||
|
this.tempConnectMode.set(val);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<If condition={!util.isBlank(this.getErrorStr())}>
|
||||||
|
<div className="settings-field settings-error">Error: {this.getErrorStr()}</div>
|
||||||
|
</If>
|
||||||
|
</div>
|
||||||
|
<footer className="wave-modal-footer crconn-wave-modal-footer">
|
||||||
|
<div className="action-buttons">
|
||||||
|
<div onClick={model.cancelEditAuth} className="button wave-button is-plain">
|
||||||
|
Cancel
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={this.submitRemote}
|
||||||
|
className="button wave-button is-wave-green text-standard"
|
||||||
|
>
|
||||||
|
Connect
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { LoadingSpinner, ClientStopModal, AlertModal, DisconnectedModal, TosModal, AboutModal, CreateRemoteConnModal };
|
||||||
|
@ -1129,6 +1129,12 @@ class RemotesModal extends React.Component<{ model: RemotesModalModel }, {}> {
|
|||||||
let selectedRemote = GlobalModel.getRemote(selectedRemoteId);
|
let selectedRemote = GlobalModel.getRemote(selectedRemoteId);
|
||||||
let remoteEdit = model.remoteEdit.get();
|
let remoteEdit = model.remoteEdit.get();
|
||||||
let onlyAddNewRemote = model.onlyAddNewRemote.get();
|
let onlyAddNewRemote = model.onlyAddNewRemote.get();
|
||||||
|
|
||||||
|
// @TODO: this is a hack to determine which create modal to show
|
||||||
|
if (remoteEdit && !remoteEdit.old) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("modal remotes-modal settings-modal prompt-modal is-active")}>
|
<div className={cn("modal remotes-modal settings-modal prompt-modal is-active")}>
|
||||||
<div className="modal-background" />
|
<div className="modal-background" />
|
||||||
@ -1179,7 +1185,15 @@ class RemotesModal extends React.Component<{ model: RemotesModalModel }, {}> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@mobxReact.observer
|
@mobxReact.observer
|
||||||
class ConnectionDropdown extends React.Component<{ curRemote: T.RemoteType, onSelectRemote?: (cname: string) => void, allowNewConn: boolean, onNewConn?: () => void }, {}> {
|
class ConnectionDropdown extends React.Component<
|
||||||
|
{
|
||||||
|
curRemote: T.RemoteType;
|
||||||
|
onSelectRemote?: (cname: string) => void;
|
||||||
|
allowNewConn: boolean;
|
||||||
|
onNewConn?: () => void;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
> {
|
||||||
connDropdownActive: OV<boolean> = mobx.observable.box(false, { name: "connDropdownActive" });
|
connDropdownActive: OV<boolean> = mobx.observable.box(false, { name: "connDropdownActive" });
|
||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
@ -1208,7 +1222,7 @@ class ConnectionDropdown extends React.Component<{ curRemote: T.RemoteType, onSe
|
|||||||
this.props.onNewConn();
|
this.props.onNewConn();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let { curRemote } = this.props;
|
let { curRemote } = this.props;
|
||||||
let remote: T.RemoteType = null;
|
let remote: T.RemoteType = null;
|
||||||
@ -1219,39 +1233,31 @@ class ConnectionDropdown extends React.Component<{ curRemote: T.RemoteType, onSe
|
|||||||
<div className="conn-dd-trigger">
|
<div className="conn-dd-trigger">
|
||||||
<If condition={curRemote != null}>
|
<If condition={curRemote != null}>
|
||||||
<div className="lefticon">
|
<div className="lefticon">
|
||||||
<GlobeIcon className="globe-icon"/>
|
<GlobeIcon className="globe-icon" />
|
||||||
<StatusCircleIcon className={cn("status-icon", "status-" + curRemote.status)}/>
|
<StatusCircleIcon className={cn("status-icon", "status-" + curRemote.status)} />
|
||||||
</div>
|
</div>
|
||||||
<div className="conntext">
|
<div className="conntext">
|
||||||
<If condition={util.isBlank(curRemote.remotealias)}>
|
<If condition={util.isBlank(curRemote.remotealias)}>
|
||||||
<div className="text-standard conntext-solo">
|
<div className="text-standard conntext-solo">{curRemote.remotecanonicalname}</div>
|
||||||
{curRemote.remotecanonicalname}
|
|
||||||
</div>
|
|
||||||
</If>
|
</If>
|
||||||
<If condition={!util.isBlank(curRemote.remotealias)}>
|
<If condition={!util.isBlank(curRemote.remotealias)}>
|
||||||
<div className="text-secondary conntext-1">
|
<div className="text-secondary conntext-1">{curRemote.remotealias}</div>
|
||||||
{curRemote.remotealias}
|
<div className="text-caption conntext-2">{curRemote.remotecanonicalname}</div>
|
||||||
</div>
|
|
||||||
<div className="text-caption conntext-2">
|
|
||||||
{curRemote.remotecanonicalname}
|
|
||||||
</div>
|
|
||||||
</If>
|
</If>
|
||||||
</div>
|
</div>
|
||||||
<div className="dd-control">
|
<div className="dd-control">
|
||||||
<ArrowsUpDownIcon className="icon"/>
|
<ArrowsUpDownIcon className="icon" />
|
||||||
</div>
|
</div>
|
||||||
</If>
|
</If>
|
||||||
<If condition={curRemote == null}>
|
<If condition={curRemote == null}>
|
||||||
<div className="lefticon">
|
<div className="lefticon">
|
||||||
<GlobeIcon className="globe-icon"/>
|
<GlobeIcon className="globe-icon" />
|
||||||
</div>
|
</div>
|
||||||
<div className="conntext">
|
<div className="conntext">
|
||||||
<div className="text-standard conntext-solo">
|
<div className="text-standard conntext-solo">(no connection)</div>
|
||||||
(no connection)
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="dd-control">
|
<div className="dd-control">
|
||||||
<ArrowsUpDownIcon className="icon"/>
|
<ArrowsUpDownIcon className="icon" />
|
||||||
</div>
|
</div>
|
||||||
</If>
|
</If>
|
||||||
</div>
|
</div>
|
||||||
@ -1259,9 +1265,13 @@ class ConnectionDropdown extends React.Component<{ curRemote: T.RemoteType, onSe
|
|||||||
<div className="dropdown-menu" role="menu">
|
<div className="dropdown-menu" role="menu">
|
||||||
<div className="dropdown-content conn-dd-menu">
|
<div className="dropdown-content conn-dd-menu">
|
||||||
<For each="remote" of={allRemotes}>
|
<For each="remote" of={allRemotes}>
|
||||||
<div className="dropdown-item" key={remote.remoteid} onClick={() => this.selectRemote(remote.remotecanonicalname)}>
|
<div
|
||||||
|
className="dropdown-item"
|
||||||
|
key={remote.remoteid}
|
||||||
|
onClick={() => this.selectRemote(remote.remotecanonicalname)}
|
||||||
|
>
|
||||||
<div className="status-div">
|
<div className="status-div">
|
||||||
<CircleIcon className={cn("status-icon", "status-" + remote.status)}/>
|
<CircleIcon className={cn("status-icon", "status-" + remote.status)} />
|
||||||
</div>
|
</div>
|
||||||
<If condition={util.isBlank(remote.remotealias)}>
|
<If condition={util.isBlank(remote.remotealias)}>
|
||||||
<div className="text-standard">{remote.remotecanonicalname}</div>
|
<div className="text-standard">{remote.remotecanonicalname}</div>
|
||||||
@ -1275,7 +1285,7 @@ class ConnectionDropdown extends React.Component<{ curRemote: T.RemoteType, onSe
|
|||||||
<If condition={this.props.allowNewConn}>
|
<If condition={this.props.allowNewConn}>
|
||||||
<div className="dropdown-item" onClick={this.clickNewConnection}>
|
<div className="dropdown-item" onClick={this.clickNewConnection}>
|
||||||
<div className="add-div">
|
<div className="add-div">
|
||||||
<AddIcon className="add-icon"/>
|
<AddIcon className="add-icon" />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-standard">New Connection</div>
|
<div className="text-standard">New Connection</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -101,7 +101,7 @@ class NewTabSettings extends React.Component<{ screen: Screen }, {}> {
|
|||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
clickNewConnection(): void {
|
clickNewConnection(): void {
|
||||||
GlobalModel.remotesModalModel.openModalForEdit({ remoteedit: true }, true);
|
GlobalModel.remotesModalModel.openModalForEdit({ remoteedit: true, old: false }, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderTabIconSelector(): React.ReactNode {
|
renderTabIconSelector(): React.ReactNode {
|
||||||
@ -171,9 +171,8 @@ class NewTabSettings extends React.Component<{ screen: Screen }, {}> {
|
|||||||
return (
|
return (
|
||||||
<div className="newtab-container">
|
<div className="newtab-container">
|
||||||
<div className="newtab-section name-section">
|
<div className="newtab-section name-section">
|
||||||
<div className="text-standard">Name</div>
|
|
||||||
<TextField
|
<TextField
|
||||||
label="Title"
|
label="Name"
|
||||||
required={true}
|
required={true}
|
||||||
defaultValue={screen.name.get() ?? ""}
|
defaultValue={screen.name.get() ?? ""}
|
||||||
onChange={this.updateName}
|
onChange={this.updateName}
|
||||||
|
@ -188,7 +188,7 @@ let menuTemplate = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Filemenu",
|
label: "File",
|
||||||
submenu: [{ role: "close" }, { role: "forceReload" }],
|
submenu: [{ role: "close" }, { role: "forceReload" }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -3290,7 +3290,7 @@ class Model {
|
|||||||
if (rview.remoteshowall) {
|
if (rview.remoteshowall) {
|
||||||
this.remotesModalModel.openModal();
|
this.remotesModalModel.openModal();
|
||||||
} else if (rview.remoteedit != null) {
|
} else if (rview.remoteedit != null) {
|
||||||
this.remotesModalModel.openModalForEdit(rview.remoteedit, false);
|
this.remotesModalModel.openModalForEdit({ ...rview.remoteedit, old: true }, false);
|
||||||
} else if (rview.ptyremoteid) {
|
} else if (rview.ptyremoteid) {
|
||||||
this.remotesModalModel.openModal(rview.ptyremoteid);
|
this.remotesModalModel.openModal(rview.ptyremoteid);
|
||||||
}
|
}
|
||||||
|
@ -56,7 +56,7 @@ class SimpleBlobRendererModel {
|
|||||||
this.savedHeight = params.savedHeight;
|
this.savedHeight = params.savedHeight;
|
||||||
this.ptyDataSource = params.ptyDataSource;
|
this.ptyDataSource = params.ptyDataSource;
|
||||||
if (this.isClosed) {
|
if (this.isClosed) {
|
||||||
this.dataBlob = new Blob();
|
this.dataBlob = (new Blob() as T.ExtBlob);
|
||||||
this.dataBlob.notFound = false; // TODO
|
this.dataBlob.notFound = false; // TODO
|
||||||
} else {
|
} else {
|
||||||
if (this.isDone.get()) {
|
if (this.isDone.get()) {
|
||||||
|
@ -309,6 +309,8 @@ type RemoteEditType = {
|
|||||||
infostr?: string;
|
infostr?: string;
|
||||||
keystr?: string;
|
keystr?: string;
|
||||||
haspassword?: boolean;
|
haspassword?: boolean;
|
||||||
|
// @TODO: this is a hack to determine which create modal to show
|
||||||
|
old?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type InfoType = {
|
type InfoType = {
|
||||||
|
Loading…
Reference in New Issue
Block a user