Compare commits

...

15 Commits

Author SHA1 Message Date
Sylvia Crowe 0cdb63bec8 fix: frontend changes for shell pref in imports
This continues the previous changes to allow the shell to be configured
after importing the ssh_config file. This allows temporary editing of
the shell preference for imported remotes as well as adding a keywork to
allow it to be set in the ssh_config file.
2024-01-31 12:56:26 -08:00
Sylvia Crowe 0417c8a158 fix: set shellpref default settings for backend
The shell preference is a new feature that was never integrated into ssh
config importing. This makes the backend changes to allow that to
happen.
2024-01-31 12:17:33 -08:00
Evan Simkowitz 5ce7b92232 Simplify logic for startResizing width calculation (#269)
* Apply Red's suggestion and move width logic to getWidth

* remove unnecessary

* a little more

* remove comment
2024-01-30 22:34:00 -08:00
Evan Simkowitz 81a9e74991 Fix onMouseDown causing width to snap back to persisted width (#268)
* Fix onMouseDown causing width to snap back to persisted width

* var name change

* remove logs

* remove redundant call
2024-01-30 22:23:00 -08:00
Evan Simkowitz 679e85a84a Fix sidebar width not persisting when un-collapsing (#267)
* Do not persist collapsed width to db

* remove console.logs

* one more

* one more

* remove setTemp from getWidth and add to the startResize call
2024-01-30 22:04:25 -08:00
Evan Simkowitz da69a0f583 Fix broken merge (#266) 2024-01-30 20:56:32 -08:00
Red J Adaya 37ab1bca90 allow resizing of left sidebar (#244)
* wip

* integrate original sidebar content

* ResizableSidebar component

* trigger toggleCollapse

* remove debugging code

* minor refactor. disable text select on mousemove

* replace icons with fontawesome icons. fix alignment issues

* fix session view width when tabs overflow

* prevent index and icon from shifting when resizing

* snap effect

* minor refactor

* apply collapsed mode to sidebar contents

* change default width to 240px

* backend implementation

* fix wrong subcmd

* save collapsed state

* retore sidebar state on reload/launch

* use collapse data form db on first load. use previously saved width on expand.

* persist width as well collapse state

* various fixes and improvements

* bind methods

* refactor

* more refactor

* fix minor bug

* fix merge issues

* various fixes

* refactor

* fixes

* fix issues

* fix all issues

* resolve undefind tempWidth

* fix toggleCollapsed

* use Promise in stopResizing method

* use tempCollapsed to for real time toggling between logos

* minor method name change

* refactor

* remove debugging code

* fix conflict

* fix setting collapsed state via CLI

* minor refactor

* remove debugging code

* create setTempWidthAndTempCollapsed method

* handle invalid width set via cli

* refactor: setbycli not actually needed

* remove unused code
2024-01-30 20:17:49 -08:00
Evan Simkowitz 40757fa7f4 Prevent the status indicator flickering for quick-returning commands (#265)
* add status-indicator-visible

* save

* save

* Prevent the status indicator flickering for quick returns

* flx regression

* reduce delay, reset spinnerVisible when there's no more running commands

* clean up code reuse

* move code around

* slight optimizations to prevent rendering before spinner is visible

* rename var

* revert shouldSync change as it broke the sync
2024-01-30 16:31:38 -08:00
Evan Simkowitz e576f7f07d Sync RotateIcon spin as well (#262)
* Sync RotateIcon spin as well

* make shouldSync a boolean value
2024-01-30 14:16:33 -08:00
Evan Simkowitz 9e804232c2 Fix SidebarItem key error (#263) 2024-01-30 11:54:35 -08:00
sawka d3dbac6e77 fix missing react key 2024-01-30 11:29:12 -08:00
Mike Sawka 198de02a65 code to restore indicator state on refresh (#260)
* code to restore indicator state on refresh

* fix style
2024-01-29 23:51:01 -08:00
Mike Sawka 8f64d6a589 set animation startTime to sync spinners for status indicators (#259)
* set animation startTime to sync spinners for status indicators

* catch animationstart (hover interaction) to restart startTime.

* use const

---------

Co-authored-by: Evan Simkowitz <esimkowitz@users.noreply.github.com>
2024-01-29 23:48:37 -08:00
Evan Simkowitz 1a51d93a54 Fix add-workspace so that only the plus button shows pointer (#258) 2024-01-29 14:10:58 -08:00
sawka 9ab3b2a5b7 only run prettier on js/ts/css/less files 2024-01-29 13:35:09 -08:00
24 changed files with 893 additions and 310 deletions
+19 -2
View File
@@ -1,4 +1,21 @@
{ {
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode" "[javascript]": {
} "editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[less]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
+12 -4
View File
@@ -30,6 +30,7 @@ type OV<V> = mobx.IObservableValue<V>;
@mobxReact.observer @mobxReact.observer
class App extends React.Component<{}, {}> { class App extends React.Component<{}, {}> {
dcWait: OV<boolean> = mobx.observable.box(false, { name: "dcWait" }); dcWait: OV<boolean> = mobx.observable.box(false, { name: "dcWait" });
mainContentRef: React.RefObject<HTMLDivElement> = React.createRef();
constructor(props: any) { constructor(props: any) {
super(props); super(props);
@@ -75,6 +76,13 @@ class App extends React.Component<{}, {}> {
let hasClientStop = GlobalModel.getHasClientStop(); let hasClientStop = GlobalModel.getHasClientStop();
let dcWait = this.dcWait.get(); let dcWait = this.dcWait.get();
let platform = GlobalModel.getPlatform(); let platform = GlobalModel.getPlatform();
let clientData = GlobalModel.clientData.get();
// Previously, this is done in sidebar.tsx but it causes flicker when clientData is null cos screen-view shifts around.
// Doing it here fixes the flicker cos app is not rendered until clientData is populated.
if (clientData == null) {
return null;
}
if (disconnected || hasClientStop) { if (disconnected || hasClientStop) {
if (!dcWait) { if (!dcWait) {
@@ -82,8 +90,8 @@ class App extends React.Component<{}, {}> {
} }
return ( return (
<div id="main" className={"platform-" + platform} onContextMenu={this.handleContextMenu}> <div id="main" className={"platform-" + platform} onContextMenu={this.handleContextMenu}>
<div className="main-content"> <div ref={this.mainContentRef} className="main-content">
<MainSideBar /> <MainSideBar parentRef={this.mainContentRef} clientData={clientData} />
<div className="session-view" /> <div className="session-view" />
</div> </div>
<If condition={dcWait}> <If condition={dcWait}>
@@ -102,8 +110,8 @@ class App extends React.Component<{}, {}> {
} }
return ( return (
<div id="main" className={"platform-" + platform} onContextMenu={this.handleContextMenu}> <div id="main" className={"platform-" + platform} onContextMenu={this.handleContextMenu}>
<div className="main-content"> <div ref={this.mainContentRef} className="main-content">
<MainSideBar /> <MainSideBar parentRef={this.mainContentRef} clientData={clientData} />
<ErrorBoundary> <ErrorBoundary>
<PluginsView /> <PluginsView />
<WorkspaceView /> <WorkspaceView />
+164 -2
View File
@@ -9,11 +9,12 @@ import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm"; 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 { RemoteType, StatusIndicatorLevel } from "../../types/types"; import { RemoteType } from "../../types/types";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { GlobalModel } from "../../model/model"; import { GlobalModel, GlobalCommandRunner } from "../../model/model";
import * as appconst from "../appconst"; import * as appconst from "../appconst";
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../../util/keyutil"; import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../../util/keyutil";
import { MagicLayout } from "../magiclayout";
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";
@@ -1265,6 +1266,166 @@ In order to use Wave's advanced features like unified history and persistent ses
}); });
} }
interface ResizableSidebarProps {
parentRef: React.RefObject<HTMLElement>;
position: "left" | "right";
enableSnap?: boolean;
className?: string;
children?: (toggleCollapsed: () => void) => React.ReactNode;
toggleCollapse?: () => void;
}
@mobxReact.observer
class ResizableSidebar extends React.Component<ResizableSidebarProps> {
resizeStartWidth: number = 0;
startX: number = 0;
prevDelta: number = 0;
prevDragDirection: string = null;
disposeReaction: any;
@boundMethod
startResizing(event: React.MouseEvent<HTMLDivElement>) {
event.preventDefault();
const { parentRef, position } = this.props;
const parentRect = parentRef.current?.getBoundingClientRect();
if (!parentRect) return;
if (position === "right") {
this.startX = parentRect.right - event.clientX;
} else {
this.startX = event.clientX - parentRect.left;
}
const mainSidebarModel = GlobalModel.mainSidebarModel;
const collapsed = mainSidebarModel.getCollapsed();
this.resizeStartWidth = mainSidebarModel.getWidth();
document.addEventListener("mousemove", this.onMouseMove);
document.addEventListener("mouseup", this.stopResizing);
document.body.style.cursor = "col-resize";
mobx.action(() => {
mainSidebarModel.setTempWidthAndTempCollapsed(this.resizeStartWidth, collapsed);
mainSidebarModel.isDragging.set(true);
})();
}
@boundMethod
onMouseMove(event: MouseEvent) {
event.preventDefault();
const { parentRef, enableSnap, position } = this.props;
const parentRect = parentRef.current?.getBoundingClientRect();
const mainSidebarModel = GlobalModel.mainSidebarModel;
if (!mainSidebarModel.isDragging.get() || !parentRect) return;
let delta: number, newWidth: number;
if (position === "right") {
delta = parentRect.right - event.clientX - this.startX;
} else {
delta = event.clientX - parentRect.left - this.startX;
}
newWidth = this.resizeStartWidth + delta;
if (enableSnap) {
const minWidth = MagicLayout.MainSidebarMinWidth;
const snapPoint = minWidth + MagicLayout.MainSidebarSnapThreshold;
const dragResistance = MagicLayout.MainSidebarDragResistance;
let dragDirection: string;
if (delta - this.prevDelta > 0) {
dragDirection = "+";
} else if (delta - this.prevDelta == 0) {
if (this.prevDragDirection == "+") {
dragDirection = "+";
} else {
dragDirection = "-";
}
} else {
dragDirection = "-";
}
this.prevDelta = delta;
this.prevDragDirection = dragDirection;
if (newWidth - dragResistance > minWidth && newWidth < snapPoint && dragDirection == "+") {
newWidth = snapPoint;
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, false);
} else if (newWidth + dragResistance < snapPoint && dragDirection == "-") {
newWidth = minWidth;
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, true);
} else if (newWidth > snapPoint) {
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, false);
}
} else {
if (newWidth <= MagicLayout.MainSidebarMinWidth) {
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, true);
} else {
mainSidebarModel.setTempWidthAndTempCollapsed(newWidth, false);
}
}
}
@boundMethod
stopResizing() {
let mainSidebarModel = GlobalModel.mainSidebarModel;
GlobalCommandRunner.clientSetSidebar(
mainSidebarModel.tempWidth.get(),
mainSidebarModel.tempCollapsed.get()
).finally(() => {
mobx.action(() => {
mainSidebarModel.isDragging.set(false);
})();
});
document.removeEventListener("mousemove", this.onMouseMove);
document.removeEventListener("mouseup", this.stopResizing);
document.body.style.cursor = "";
}
@boundMethod
toggleCollapsed() {
const mainSidebarModel = GlobalModel.mainSidebarModel;
const tempCollapsed = mainSidebarModel.getCollapsed();
const width = mainSidebarModel.getWidth(true);
mainSidebarModel.setTempWidthAndTempCollapsed(width, !tempCollapsed);
GlobalCommandRunner.clientSetSidebar(width, !tempCollapsed);
}
render() {
const { className, children } = this.props;
const mainSidebarModel = GlobalModel.mainSidebarModel;
const width = mainSidebarModel.getWidth();
const isCollapsed = mainSidebarModel.getCollapsed();
return (
<div className={cn("sidebar", className, { collapsed: isCollapsed })} style={{ width }}>
<div className="sidebar-content">{children(this.toggleCollapsed)}</div>
<div
className="sidebar-handle"
style={{
position: "absolute",
top: 0,
[this.props.position === "left" ? "right" : "left"]: 0,
bottom: 0,
width: "5px",
cursor: "col-resize",
}}
onMouseDown={this.startResizing}
onDoubleClick={this.toggleCollapsed}
></div>
</div>
);
}
}
export { export {
CmdStrCode, CmdStrCode,
Toggle, Toggle,
@@ -1286,5 +1447,6 @@ export {
LinkButton, LinkButton,
Status, Status,
Modal, Modal,
ResizableSidebar,
ShowWaveShellInstallPrompt, ShowWaveShellInstallPrompt,
}; };
+21 -1
View File
@@ -48,13 +48,33 @@
} }
} }
/*
The following accounts for a debounce in the status indicator. We will only display the status indicator icon if the parent indicates that it should be visible AND one of the following is true:
1. There is a status to be shown.
2. There is a spinner to be shown and the required delay has passed.
*/
.status-indicator-visible {
&.spinner-visible,
&.output,
&.error,
&.success {
.positional-icon-visible;
}
// This is set by the timeout in the status indicator component.
&.spinner-visible {
#spinner {
visibility: visible;
}
}
}
.status-indicator { .status-indicator {
#spinner, #spinner,
#indicator { #indicator {
visibility: hidden; visibility: hidden;
} }
.spin #spinner { .spin #spinner {
visibility: visible;
stroke: @term-white; stroke: @term-white;
} }
&.error #indicator { &.error #indicator {
+171 -10
View File
@@ -2,17 +2,27 @@ import React from "react";
import { StatusIndicatorLevel } from "../../../types/types"; import { StatusIndicatorLevel } from "../../../types/types";
import cn from "classnames"; import cn from "classnames";
import { ReactComponent as SpinnerIndicator } from "../../assets/icons/spinner-indicator.svg"; import { ReactComponent as SpinnerIndicator } from "../../assets/icons/spinner-indicator.svg";
import { boundMethod } from "autobind-decorator";
import * as mobx from "mobx";
import * as mobxReact from "mobx-react";
import { ReactComponent as RotateIconSvg } from "../../assets/icons/line/rotate.svg";
interface PositionalIconProps { interface PositionalIconProps {
children?: React.ReactNode; children?: React.ReactNode;
className?: string; className?: string;
onClick?: React.MouseEventHandler<HTMLDivElement>; onClick?: React.MouseEventHandler<HTMLDivElement>;
divRef?: React.RefObject<HTMLDivElement>;
} }
export class FrontIcon extends React.Component<PositionalIconProps> { export class FrontIcon extends React.Component<PositionalIconProps> {
render() { render() {
return ( return (
<div className={cn("front-icon", "positional-icon", this.props.className)}> <div
ref={this.props.divRef}
className={cn("front-icon", "positional-icon", this.props.className)}
onClick={this.props.onClick}
>
<div className="positional-icon-inner">{this.props.children}</div> <div className="positional-icon-inner">{this.props.children}</div>
</div> </div>
); );
@@ -22,7 +32,11 @@ export class FrontIcon extends React.Component<PositionalIconProps> {
export class CenteredIcon extends React.Component<PositionalIconProps> { export class CenteredIcon extends React.Component<PositionalIconProps> {
render() { render() {
return ( return (
<div className={cn("centered-icon", "positional-icon", this.props.className)} onClick={this.props.onClick}> <div
ref={this.props.divRef}
className={cn("centered-icon", "positional-icon", this.props.className)}
onClick={this.props.onClick}
>
<div className="positional-icon-inner">{this.props.children}</div> <div className="positional-icon-inner">{this.props.children}</div>
</div> </div>
); );
@@ -43,35 +57,182 @@ export class ActionsIcon extends React.Component<ActionsIconProps> {
} }
} }
class SyncSpin extends React.Component<{
classRef?: React.RefObject<HTMLDivElement>;
children?: React.ReactNode;
shouldSync?: boolean;
}> {
listenerAdded: boolean = false;
componentDidMount() {
this.syncSpinner();
}
componentDidUpdate() {
this.syncSpinner();
}
componentWillUnmount(): void {
const classRef = this.props.classRef;
if (classRef.current != null && this.listenerAdded) {
const elem = classRef.current;
const svgElem = elem.querySelector("svg");
if (svgElem != null) {
svgElem.removeEventListener("animationstart", this.handleAnimationStart);
}
}
}
@boundMethod
handleAnimationStart(e: AnimationEvent) {
const classRef = this.props.classRef;
if (classRef.current == null) {
return;
}
const svgElem = classRef.current.querySelector("svg");
if (svgElem == null) {
return;
}
const animArr = svgElem.getAnimations();
if (animArr == null || animArr.length == 0) {
return;
}
animArr[0].startTime = 0;
}
syncSpinner() {
const { classRef, shouldSync } = this.props;
const shouldSyncVal = shouldSync ?? true;
if (!shouldSyncVal || classRef.current == null) {
return;
}
const elem = classRef.current;
const svgElem = elem.querySelector("svg");
if (svgElem == null) {
return;
}
if (!this.listenerAdded) {
svgElem.addEventListener("animationstart", this.handleAnimationStart);
this.listenerAdded = true;
}
const animArr = svgElem.getAnimations();
if (animArr == null || animArr.length == 0) {
return;
}
animArr[0].startTime = 0;
}
render() {
return this.props.children;
}
}
interface StatusIndicatorProps { interface StatusIndicatorProps {
/**
* The level of the status indicator. This will determine the color of the status indicator.
*/
level: StatusIndicatorLevel; level: StatusIndicatorLevel;
className?: string; className?: string;
/**
* If true, a spinner will be shown around the status indicator.
*/
runningCommands?: boolean; runningCommands?: boolean;
} }
/**
* This component is used to show the status of a command. It will show a spinner around the status indicator if there are running commands. It will also delay showing the spinner for a short time to prevent flickering.
*/
@mobxReact.observer
export class StatusIndicator extends React.Component<StatusIndicatorProps> { export class StatusIndicator extends React.Component<StatusIndicatorProps> {
iconRef: React.RefObject<HTMLDivElement> = React.createRef();
spinnerVisible: mobx.IObservableValue<boolean> = mobx.observable.box(false);
timeout: NodeJS.Timeout;
clearSpinnerTimeout() {
if (this.timeout) {
clearTimeout(this.timeout);
this.timeout = null;
}
mobx.action(() => {
this.spinnerVisible.set(false);
})();
}
/**
* This will apply a delay after there is a running command before showing the spinner. This prevents flickering for commands that return quickly.
*/
updateMountCallback() {
const runningCommands = this.props.runningCommands ?? false;
if (runningCommands && !this.timeout) {
this.timeout = setTimeout(
mobx.action(() => {
this.spinnerVisible.set(true);
}),
100
);
} else if (!runningCommands) {
this.clearSpinnerTimeout();
}
}
componentDidUpdate(): void {
this.updateMountCallback();
}
componentDidMount(): void {
this.updateMountCallback();
}
componentWillUnmount(): void {
this.clearSpinnerTimeout();
}
render() { render() {
const { level, className, runningCommands } = this.props; const { level, className, runningCommands } = this.props;
const spinnerVisible = this.spinnerVisible.get();
let statusIndicator = null; let statusIndicator = null;
if (level != StatusIndicatorLevel.None || runningCommands) { if (level != StatusIndicatorLevel.None || spinnerVisible) {
let levelClass = null; let indicatorLevelClass = null;
switch (level) { switch (level) {
case StatusIndicatorLevel.Output: case StatusIndicatorLevel.Output:
levelClass = "output"; indicatorLevelClass = "output";
break; break;
case StatusIndicatorLevel.Success: case StatusIndicatorLevel.Success:
levelClass = "success"; indicatorLevelClass = "success";
break; break;
case StatusIndicatorLevel.Error: case StatusIndicatorLevel.Error:
levelClass = "error"; indicatorLevelClass = "error";
break; break;
} }
const spinnerVisibleClass = spinnerVisible ? "spinner-visible" : null;
statusIndicator = ( statusIndicator = (
<CenteredIcon className={cn(className, levelClass, "status-indicator")}> <CenteredIcon
<SpinnerIndicator className={runningCommands ? "spin" : null} /> divRef={this.iconRef}
className={cn(className, indicatorLevelClass, spinnerVisibleClass, "status-indicator")}
>
<SpinnerIndicator className={spinnerVisible ? "spin" : null} />
</CenteredIcon> </CenteredIcon>
); );
} }
return statusIndicator; return (
<SyncSpin classRef={this.iconRef} shouldSync={runningCommands}>
{statusIndicator}
</SyncSpin>
);
}
}
export class RotateIcon extends React.Component<{
className?: string;
onClick?: React.MouseEventHandler<HTMLDivElement>;
}> {
iconRef: React.RefObject<HTMLDivElement> = React.createRef();
render() {
return (
<SyncSpin classRef={this.iconRef}>
<RotateIconSvg className={this.props.className ?? ""} />
</SyncSpin>
);
} }
} }
@@ -20,6 +20,12 @@
width: 100%; width: 100%;
} }
.import-edit-warning {
display: flex;
flex-direction: row;
align-items: flex-start;
}
.name-actions-section { .name-actions-section {
margin-bottom: 10px; margin-bottom: 10px;
display: flex; display: flex;
+30 -3
View File
@@ -58,6 +58,10 @@ class EditRemoteConnModal extends React.Component<{}, {}> {
return this.selectedRemote?.local; return this.selectedRemote?.local;
} }
isImportedRemote(): boolean {
return this.selectedRemote?.sshconfigsrc == "sshconfig-import";
}
componentDidMount(): void { componentDidMount(): void {
mobx.action(() => { mobx.action(() => {
this.tempAlias.set(this.selectedRemote?.remotealias); this.tempAlias.set(this.selectedRemote?.remotealias);
@@ -259,6 +263,27 @@ class EditRemoteConnModal extends React.Component<{}, {}> {
); );
} }
renderImportedRemoteEditWarning() {
return (
<div className="import-edit-warning">
<Tooltip
message={
<span>
Most options for connections imported from an ssh config file cannot be edited. For these
changes, you must edit the config file and import it again. The shell preference can be
edited, but will return to the default if you import again. It will stay changed if you
follow <a href="https://docs.waveterm.dev/features/sshconfig-imports">this procedure</a>.
</span>
}
icon={<i className="fa-sharp fa-regular fa-fw fa-triangle-exclamation" />}
>
<i className="fa-sharp fa-regular fa-fw fa-triangle-exclamation" />
</Tooltip>
&nbsp;SSH Config Import Behavior
</div>
);
}
renderAuthMode() { renderAuthMode() {
let authMode = this.tempAuthMode.get(); let authMode = this.tempAuthMode.get();
return ( return (
@@ -344,6 +369,7 @@ class EditRemoteConnModal extends React.Component<{}, {}> {
return null; return null;
} }
let isLocal = this.isLocalRemote(); let isLocal = this.isLocalRemote();
let isImported = this.isImportedRemote();
return ( return (
<Modal className="erconn-modal"> <Modal className="erconn-modal">
<Modal.Header title="Edit Connection" onClose={this.model.closeModal} /> <Modal.Header title="Edit Connection" onClose={this.model.closeModal} />
@@ -351,9 +377,10 @@ class EditRemoteConnModal extends React.Component<{}, {}> {
<div className="name-actions-section"> <div className="name-actions-section">
<div className="name text-primary">{util.getRemoteName(this.selectedRemote)}</div> <div className="name text-primary">{util.getRemoteName(this.selectedRemote)}</div>
</div> </div>
<If condition={!isLocal}>{this.renderAlias()}</If> <If condition={!isLocal && !isImported}>{this.renderAlias()}</If>
<If condition={!isLocal}>{this.renderAuthMode()}</If> <If condition={!isLocal && !isImported}>{this.renderAuthMode()}</If>
<If condition={!isLocal}>{this.renderConnectMode()}</If> <If condition={!isLocal && !isImported}>{this.renderConnectMode()}</If>
<If condition={isImported}>{this.renderImportedRemoteEditWarning()}</If>
{this.renderShellPref()} {this.renderShellPref()}
<If condition={!util.isBlank(this.remoteEdit?.errorstr)}> <If condition={!util.isBlank(this.remoteEdit?.errorstr)}>
<div className="settings-field settings-error">Error: {this.remoteEdit?.errorstr}</div> <div className="settings-field settings-error">Error: {this.remoteEdit?.errorstr}</div>
-11
View File
@@ -1026,17 +1026,6 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
cancelInstallButton = <></>; cancelInstallButton = <></>;
} }
if (remote.sshconfigsrc == "sshconfig-import") { if (remote.sshconfigsrc == "sshconfig-import") {
updateAuthButton = (
<Button theme="secondary" disabled={true}>
Edit
<Tooltip
message={`Connections imported from an ssh config file cannot be edited inside waveterm. To edit these, you must edit the config file and import it again.`}
icon={<i className="fa-sharp fa-regular fa-fw fa-ban" />}
>
<i className="fa-sharp fa-regular fa-fw fa-ban" />
</Tooltip>
</Button>
);
archiveButton = ( archiveButton = (
<Button theme="secondary" onClick={() => this.clickArchive()}> <Button theme="secondary" onClick={() => this.clickArchive()}>
Delete Delete
@@ -209,17 +209,6 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
cancelInstallButton = <></>; cancelInstallButton = <></>;
} }
if (remote.sshconfigsrc == "sshconfig-import") { if (remote.sshconfigsrc == "sshconfig-import") {
updateAuthButton = (
<Button theme="secondary" disabled={true}>
Edit
<Tooltip
message={`Connections imported from an ssh config file cannot be edited inside waveterm. To edit these, you must edit the config file and import it again.`}
icon={<i className="fa-sharp fa-regular fa-fw fa-ban" />}
>
<i className="fa-sharp fa-regular fa-fw fa-ban" />
</Tooltip>
</Button>
);
archiveButton = ( archiveButton = (
<Button theme="secondary" onClick={() => this.clickArchive()}> <Button theme="secondary" onClick={() => this.clickArchive()}>
Delete Delete
+81 -89
View File
@@ -13,8 +13,6 @@ import { GlobalModel, GlobalCommandRunner, Cmd, getTermPtyData } from "../../mod
import { termHeightFromRows } from "../../util/textmeasure"; import { termHeightFromRows } from "../../util/textmeasure";
import type { import type {
LineType, LineType,
RemoteType,
RemotePtrType,
RenderModeType, RenderModeType,
RendererOpts, RendererOpts,
RendererPluginType, RendererPluginType,
@@ -24,11 +22,6 @@ import type {
} from "../../types/types"; } from "../../types/types";
import cn from "classnames"; import cn from "classnames";
import { ReactComponent as FavoritesIcon } from "../assets/icons/favourites.svg";
import { ReactComponent as PinIcon } from "../assets/icons/pin.svg";
import { ReactComponent as PlusIcon } from "../assets/icons/plus.svg";
import { ReactComponent as MinusIcon } from "../assets/icons/minus.svg";
import type { LineContainerModel } from "../../model/model"; import type { LineContainerModel } from "../../model/model";
import { renderCmdText } from "../common/common"; import { renderCmdText } from "../common/common";
import { SimpleBlobRenderer } from "../../plugins/core/basicrenderer"; import { SimpleBlobRenderer } from "../../plugins/core/basicrenderer";
@@ -44,12 +37,13 @@ import * as appconst from "../appconst";
import { ReactComponent as CheckIcon } from "../assets/icons/line/check.svg"; import { ReactComponent as CheckIcon } from "../assets/icons/line/check.svg";
import { ReactComponent as CommentIcon } from "../assets/icons/line/comment.svg"; import { ReactComponent as CommentIcon } from "../assets/icons/line/comment.svg";
import { ReactComponent as QuestionIcon } from "../assets/icons/line/question.svg"; import { ReactComponent as QuestionIcon } from "../assets/icons/line/question.svg";
import { ReactComponent as RotateIcon } from "../assets/icons/line/rotate.svg";
import { ReactComponent as WarningIcon } from "../assets/icons/line/triangle-exclamation.svg"; import { ReactComponent as WarningIcon } from "../assets/icons/line/triangle-exclamation.svg";
import { ReactComponent as XmarkIcon } from "../assets/icons/line/xmark.svg"; import { ReactComponent as XmarkIcon } from "../assets/icons/line/xmark.svg";
import { ReactComponent as FillIcon } from "../assets/icons/line/fill.svg"; import { ReactComponent as FillIcon } from "../assets/icons/line/fill.svg";
import { ReactComponent as GearIcon } from "../assets/icons/line/gear.svg"; import { ReactComponent as GearIcon } from "../assets/icons/line/gear.svg";
import { RotateIcon } from "../common/icons/icons";
import "./lines.less"; import "./lines.less";
dayjs.extend(localizedFormat); dayjs.extend(localizedFormat);
@@ -59,12 +53,12 @@ type OV<V> = mobx.IObservableValue<V>;
@mobxReact.observer @mobxReact.observer
class SmallLineAvatar extends React.Component<{ line: LineType; cmd: Cmd; onRightClick?: (e: any) => void }, {}> { class SmallLineAvatar extends React.Component<{ line: LineType; cmd: Cmd; onRightClick?: (e: any) => void }, {}> {
render() { render() {
let { line, cmd } = this.props; const { line, cmd } = this.props;
let lineNumStr = (line.linenumtemp ? "~" : "#") + String(line.linenum); const lineNumStr = (line.linenumtemp ? "~" : "#") + String(line.linenum);
let status = cmd != null ? cmd.getStatus() : "done"; const status = cmd != null ? cmd.getStatus() : "done";
let rtnstate = cmd != null ? cmd.getRtnState() : false; const rtnstate = cmd != null ? cmd.getRtnState() : false;
let exitcode = cmd != null ? cmd.getExitCode() : 0; const exitcode = cmd != null ? cmd.getExitCode() : 0;
let isComment = line.linetype == "text"; const isComment = line.linetype == "text";
let icon = null; let icon = null;
let iconTitle = null; let iconTitle = null;
if (isComment) { if (isComment) {
@@ -142,7 +136,7 @@ class LineCmd extends React.Component<
} }
checkStateDiffLoad(): void { checkStateDiffLoad(): void {
let { screen, line, staticRender, visible } = this.props; const { screen, line, staticRender, visible } = this.props;
if (staticRender) { if (staticRender) {
return; return;
} }
@@ -153,7 +147,7 @@ class LineCmd extends React.Component<
} }
return; return;
} }
let cmd = screen.getCmd(line); const cmd = screen.getCmd(line);
if (cmd == null || !cmd.getRtnState() || this.rtnStateDiffFetched) { if (cmd == null || !cmd.getRtnState() || this.rtnStateDiffFetched) {
return; return;
} }
@@ -167,15 +161,15 @@ class LineCmd extends React.Component<
if (this.rtnStateDiffFetched) { if (this.rtnStateDiffFetched) {
return; return;
} }
let { line } = this.props; const { line } = this.props;
this.rtnStateDiffFetched = true; this.rtnStateDiffFetched = true;
let usp = new URLSearchParams({ const usp = new URLSearchParams({
linenum: String(line.linenum), linenum: String(line.linenum),
screenid: line.screenid, screenid: line.screenid,
lineid: line.lineid, lineid: line.lineid,
}); });
let url = GlobalModel.getBaseHostPort() + "/api/rtnstate?" + usp.toString(); const url = GlobalModel.getBaseHostPort() + "/api/rtnstate?" + usp.toString();
let fetchHeaders = GlobalModel.getFetchHeaders(); const fetchHeaders = GlobalModel.getFetchHeaders();
fetch(url, { headers: fetchHeaders }) fetch(url, { headers: fetchHeaders })
.then((resp) => { .then((resp) => {
if (!resp.ok) { if (!resp.ok) {
@@ -233,7 +227,7 @@ class LineCmd extends React.Component<
</React.Fragment> </React.Fragment>
); );
} }
let isMultiLine = lineutil.isMultiLineCmdText(cmd.getCmdStr()); const isMultiLine = lineutil.isMultiLineCmdText(cmd.getCmdStr());
return ( return (
<div key="meta2" className="meta meta-line2" ref={this.cmdTextRef}> <div key="meta2" className="meta meta-line2" ref={this.cmdTextRef}>
<div className="metapart-mono cmdtext"> <div className="metapart-mono cmdtext">
@@ -252,7 +246,7 @@ class LineCmd extends React.Component<
// TODO: this might not be necessary anymore because we're using this.lastHeight // TODO: this might not be necessary anymore because we're using this.lastHeight
getSnapshotBeforeUpdate(prevProps, prevState): { height: number } { getSnapshotBeforeUpdate(prevProps, prevState): { height: number } {
let elem = this.lineRef.current; const elem = this.lineRef.current;
if (elem == null) { if (elem == null) {
return { height: 0 }; return { height: 0 };
} }
@@ -266,25 +260,25 @@ class LineCmd extends React.Component<
} }
checkCmdText() { checkCmdText() {
let metaElem = this.cmdTextRef.current; const metaElem = this.cmdTextRef.current;
if (metaElem == null || metaElem.childNodes.length == 0) { if (metaElem == null || metaElem.childNodes.length == 0) {
return; return;
} }
let metaElemWidth = metaElem.offsetWidth; const metaElemWidth = metaElem.offsetWidth;
if (metaElemWidth == 0) { if (metaElemWidth == 0) {
return; return;
} }
let metaChild = metaElem.firstChild; const metaChild = metaElem.firstChild;
if (metaChild == null) { if (metaChild == null) {
return; return;
} }
let children = metaChild.childNodes; const children = metaChild.childNodes;
let childWidth = 0; let childWidth = 0;
for (let i = 0; i < children.length; i++) { for (let i = 0; i < children.length; i++) {
let ch = children[i]; let ch = children[i];
childWidth += ch.offsetWidth; childWidth += ch.offsetWidth;
} }
let isOverflow = childWidth > metaElemWidth; const isOverflow = childWidth > metaElemWidth;
if (isOverflow && isOverflow != this.isOverflow.get()) { if (isOverflow && isOverflow != this.isOverflow.get()) {
mobx.action(() => { mobx.action(() => {
this.isOverflow.set(isOverflow); this.isOverflow.set(isOverflow);
@@ -297,16 +291,16 @@ class LineCmd extends React.Component<
if (this.props.onHeightChange == null) { if (this.props.onHeightChange == null) {
return; return;
} }
let { line } = this.props; const { line } = this.props;
let curHeight = 0; let curHeight = 0;
let elem = this.lineRef.current; const elem = this.lineRef.current;
if (elem != null) { if (elem != null) {
curHeight = elem.offsetHeight; curHeight = elem.offsetHeight;
} }
if (this.lastHeight == curHeight) { if (this.lastHeight == curHeight) {
return; return;
} }
let lastHeight = this.lastHeight; const lastHeight = this.lastHeight;
this.lastHeight = curHeight; this.lastHeight = curHeight;
this.props.onHeightChange(line.linenum, curHeight, lastHeight); this.props.onHeightChange(line.linenum, curHeight, lastHeight);
// console.log("line height change: ", line.linenum, lastHeight, "=>", curHeight); // console.log("line height change: ", line.linenum, lastHeight, "=>", curHeight);
@@ -314,13 +308,13 @@ class LineCmd extends React.Component<
@boundMethod @boundMethod
handleClick() { handleClick() {
let { line, noSelect } = this.props; const { line, noSelect } = this.props;
if (noSelect) { if (noSelect) {
return; return;
} }
let sel = window.getSelection(); const sel = window.getSelection();
if (this.lineRef.current != null) { if (this.lineRef.current != null) {
let selText = sel.toString(); const selText = sel.toString();
if (sel.anchorNode != null && this.lineRef.current.contains(sel.anchorNode) && !isBlank(selText)) { if (sel.anchorNode != null && this.lineRef.current.contains(sel.anchorNode) && !isBlank(selText)) {
return; return;
} }
@@ -330,7 +324,7 @@ class LineCmd extends React.Component<
@boundMethod @boundMethod
clickStar() { clickStar() {
let { line } = this.props; const { line } = this.props;
if (!line.star || line.star == 0) { if (!line.star || line.star == 0) {
GlobalCommandRunner.lineStar(line.lineid, 1); GlobalCommandRunner.lineStar(line.lineid, 1);
} else { } else {
@@ -340,7 +334,7 @@ class LineCmd extends React.Component<
@boundMethod @boundMethod
clickPin() { clickPin() {
let { line } = this.props; const { line } = this.props;
if (!line.pinned) { if (!line.pinned) {
GlobalCommandRunner.linePin(line.lineid, true); GlobalCommandRunner.linePin(line.lineid, true);
} else { } else {
@@ -350,19 +344,19 @@ class LineCmd extends React.Component<
@boundMethod @boundMethod
clickBookmark() { clickBookmark() {
let { line } = this.props; const { line } = this.props;
GlobalCommandRunner.lineBookmark(line.lineid); GlobalCommandRunner.lineBookmark(line.lineid);
} }
@boundMethod @boundMethod
clickDelete() { clickDelete() {
let { line } = this.props; const { line } = this.props;
GlobalCommandRunner.lineDelete(line.lineid, true); GlobalCommandRunner.lineDelete(line.lineid, true);
} }
@boundMethod @boundMethod
clickRestart() { clickRestart() {
let { line } = this.props; const { line } = this.props;
GlobalCommandRunner.lineRestart(line.lineid, true); GlobalCommandRunner.lineRestart(line.lineid, true);
} }
@@ -375,7 +369,7 @@ class LineCmd extends React.Component<
@boundMethod @boundMethod
clickMoveToSidebar() { clickMoveToSidebar() {
let { line } = this.props; const { line } = this.props;
GlobalCommandRunner.screenSidebarAddLine(line.lineid); GlobalCommandRunner.screenSidebarAddLine(line.lineid);
} }
@@ -390,20 +384,20 @@ class LineCmd extends React.Component<
} }
getIsHidePrompt(): boolean { getIsHidePrompt(): boolean {
let { line } = this.props; const { line } = this.props;
let rendererPlugin: RendererPluginType = null; let rendererPlugin: RendererPluginType = null;
let isNoneRenderer = line.renderer == "none"; const isNoneRenderer = line.renderer == "none";
if (!isBlank(line.renderer) && line.renderer != "terminal" && !isNoneRenderer) { if (!isBlank(line.renderer) && line.renderer != "terminal" && !isNoneRenderer) {
rendererPlugin = PluginModel.getRendererPluginByName(line.renderer); rendererPlugin = PluginModel.getRendererPluginByName(line.renderer);
} }
let hidePrompt = rendererPlugin != null && rendererPlugin.hidePrompt; const hidePrompt = rendererPlugin?.hidePrompt;
return hidePrompt; return hidePrompt;
} }
getTerminalRendererHeight(cmd: Cmd): number { getTerminalRendererHeight(cmd: Cmd): number {
let { screen, line, width, renderMode } = this.props; const { screen, line, width } = this.props;
let height = 45 + 24; // height of zero height terminal let height = 45 + 24; // height of zero height terminal
let usedRows = screen.getUsedRows(lineutil.getRendererContext(line), line, cmd, width); const usedRows = screen.getUsedRows(lineutil.getRendererContext(line), line, cmd, width);
if (usedRows > 0) { if (usedRows > 0) {
height = 48 + 24 + termHeightFromRows(usedRows, GlobalModel.termFontSize.get()); height = 48 + 24 + termHeightFromRows(usedRows, GlobalModel.termFontSize.get());
} }
@@ -412,7 +406,7 @@ class LineCmd extends React.Component<
@boundMethod @boundMethod
onAvatarRightClick(e: any): void { onAvatarRightClick(e: any): void {
let { line, noSelect } = this.props; const { line, noSelect } = this.props;
if (noSelect) { if (noSelect) {
return; return;
} }
@@ -426,20 +420,20 @@ class LineCmd extends React.Component<
} }
renderSimple() { renderSimple() {
let { screen, line } = this.props; const { screen, line } = this.props;
let cmd = screen.getCmd(line); const cmd = screen.getCmd(line);
let height: number = 0; let height: number = 0;
if (isBlank(line.renderer) || line.renderer == "terminal") { if (isBlank(line.renderer) || line.renderer == "terminal") {
height = this.getTerminalRendererHeight(cmd); height = this.getTerminalRendererHeight(cmd);
} else { } else {
// header is 16px tall with hide-prompt, 36px otherwise // header is 16px tall with hide-prompt, 36px otherwise
let { screen, line, width } = this.props; const { screen, line, width } = this.props;
let hidePrompt = this.getIsHidePrompt(); const hidePrompt = this.getIsHidePrompt();
let usedRows = screen.getUsedRows(lineutil.getRendererContext(line), line, cmd, width); const usedRows = screen.getUsedRows(lineutil.getRendererContext(line), line, cmd, width);
height = (hidePrompt ? 16 + 6 : 36 + 6) + usedRows; height = (hidePrompt ? 16 + 6 : 36 + 6) + usedRows;
} }
let formattedTime = lineutil.getLineDateTimeStr(line.ts); const formattedTime = lineutil.getLineDateTimeStr(line.ts);
let mainDivCn = cn("line", "line-cmd", "line-simple"); const mainDivCn = cn("line", "line-cmd", "line-simple");
return ( return (
<div <div
className={mainDivCn} className={mainDivCn}
@@ -479,15 +473,16 @@ class LineCmd extends React.Component<
if (restartTs != null && restartTs > 0) { if (restartTs != null && restartTs > 0) {
formattedTime = "restarted @ " + lineutil.getLineDateTimeStr(restartTs); formattedTime = "restarted @ " + lineutil.getLineDateTimeStr(restartTs);
timeTitle = "original start time " + lineutil.getLineDateTimeStr(line.ts); timeTitle = "original start time " + lineutil.getLineDateTimeStr(line.ts);
} } else {
else {
formattedTime = lineutil.getLineDateTimeStr(line.ts); formattedTime = lineutil.getLineDateTimeStr(line.ts);
} }
let renderer = line.renderer; let renderer = line.renderer;
return ( return (
<div key="meta1" className="meta meta-line1"> <div key="meta1" className="meta meta-line1">
<SmallLineAvatar line={line} cmd={cmd} /> <SmallLineAvatar line={line} cmd={cmd} />
<div title={timeTitle} className="ts">{formattedTime}</div> <div title={timeTitle} className="ts">
{formattedTime}
</div>
<div>&nbsp;</div> <div>&nbsp;</div>
<If condition={!isBlank(renderer) && renderer != "terminal"}> <If condition={!isBlank(renderer) && renderer != "terminal"}>
<div className="renderer"> <div className="renderer">
@@ -506,7 +501,7 @@ class LineCmd extends React.Component<
} }
getRendererOpts(cmd: Cmd): RendererOpts { getRendererOpts(cmd: Cmd): RendererOpts {
let { screen } = this.props; const { screen } = this.props;
return { return {
maxSize: screen.getMaxContentSize(), maxSize: screen.getMaxContentSize(),
idealSize: screen.getIdealContentSize(), idealSize: screen.getIdealContentSize(),
@@ -516,9 +511,9 @@ class LineCmd extends React.Component<
} }
makeRendererModelInitializeParams(): RendererModelInitializeParams { makeRendererModelInitializeParams(): RendererModelInitializeParams {
let { screen, line } = this.props; const { screen, line } = this.props;
let context = lineutil.getRendererContext(line); const context = lineutil.getRendererContext(line);
let cmd = screen.getCmd(line); // won't be null const cmd = screen.getCmd(line); // won't be null
let savedHeight = screen.getContentHeight(context); let savedHeight = screen.getContentHeight(context);
if (savedHeight == null) { if (savedHeight == null) {
if (line.contentheight != null && line.contentheight != -1) { if (line.contentheight != null && line.contentheight != -1) {
@@ -527,7 +522,7 @@ class LineCmd extends React.Component<
savedHeight = 0; savedHeight = 0;
} }
} }
let api = { const api = {
saveHeight: (height: number) => { saveHeight: (height: number) => {
screen.setContentHeight(lineutil.getRendererContext(line), height); screen.setContentHeight(lineutil.getRendererContext(line), height);
}, },
@@ -581,12 +576,12 @@ class LineCmd extends React.Component<
}; };
render() { render() {
let { screen, line, width, staticRender, visible } = this.props; const { screen, line, width, staticRender, visible } = this.props;
let isVisible = visible.get(); const isVisible = visible.get();
if (staticRender || !isVisible) { if (staticRender || !isVisible) {
return this.renderSimple(); return this.renderSimple();
} }
let cmd = screen.getCmd(line); const cmd = screen.getCmd(line);
if (cmd == null) { if (cmd == null) {
return ( return (
<div <div
@@ -600,19 +595,17 @@ class LineCmd extends React.Component<
</div> </div>
); );
} }
let status = cmd.getStatus(); const isSelected = mobx
let lineNumStr = (line.linenumtemp ? "~" : "") + String(line.linenum);
let isSelected = mobx
.computed(() => screen.getSelectedLine() == line.linenum, { .computed(() => screen.getSelectedLine() == line.linenum, {
name: "computed-isSelected", name: "computed-isSelected",
}) })
.get(); .get();
let isPhysicalFocused = mobx const isPhysicalFocused = mobx
.computed(() => screen.getIsFocused(line.linenum), { .computed(() => screen.getIsFocused(line.linenum), {
name: "computed-getIsFocused", name: "computed-getIsFocused",
}) })
.get(); .get();
let isFocused = mobx const isFocused = mobx
.computed( .computed(
() => { () => {
let screenFocusType = screen.getFocusType(); let screenFocusType = screen.getFocusType();
@@ -621,7 +614,7 @@ class LineCmd extends React.Component<
{ name: "computed-isFocused" } { name: "computed-isFocused" }
) )
.get(); .get();
let shouldCmdFocus = mobx const shouldCmdFocus = mobx
.computed( .computed(
() => { () => {
let screenFocusType = screen.getFocusType(); let screenFocusType = screen.getFocusType();
@@ -630,7 +623,7 @@ class LineCmd extends React.Component<
{ name: "computed-shouldCmdFocus" } { name: "computed-shouldCmdFocus" }
) )
.get(); .get();
let isInSidebar = mobx const isInSidebar = mobx
.computed( .computed(
() => { () => {
return screen.isSidebarOpen() && screen.isLineIdInSidebar(line.lineid); return screen.isSidebarOpen() && screen.isLineIdInSidebar(line.lineid);
@@ -638,11 +631,10 @@ class LineCmd extends React.Component<
{ name: "computed-isInSidebar" } { name: "computed-isInSidebar" }
) )
.get(); .get();
let isStatic = staticRender; const isRunning = cmd.isRunning();
let isRunning = cmd.isRunning(); const isExpanded = this.isCmdExpanded.get();
let isExpanded = this.isCmdExpanded.get(); const rsdiff = this.rtnStateDiff.get();
let rsdiff = this.rtnStateDiff.get(); const mainDivCn = cn(
let mainDivCn = cn(
"line", "line",
"line-cmd", "line-cmd",
{ selected: isSelected }, { selected: isSelected },
@@ -651,18 +643,18 @@ class LineCmd extends React.Component<
{ "has-rtnstate": cmd.getRtnState() } { "has-rtnstate": cmd.getRtnState() }
); );
let rendererPlugin: RendererPluginType = null; let rendererPlugin: RendererPluginType = null;
let isNoneRenderer = line.renderer == "none"; const isNoneRenderer = line.renderer == "none";
if (!isBlank(line.renderer) && line.renderer != "terminal" && !isNoneRenderer) { if (!isBlank(line.renderer) && line.renderer != "terminal" && !isNoneRenderer) {
rendererPlugin = PluginModel.getRendererPluginByName(line.renderer); rendererPlugin = PluginModel.getRendererPluginByName(line.renderer);
} }
let rendererType = lineutil.getRendererType(line); const rendererType = lineutil.getRendererType(line);
let hidePrompt = rendererPlugin != null && rendererPlugin.hidePrompt; const hidePrompt = rendererPlugin?.hidePrompt;
let termFontSize = GlobalModel.termFontSize.get(); const termFontSize = GlobalModel.termFontSize.get();
let rtnStateDiffSize = termFontSize - 2; let rtnStateDiffSize = termFontSize - 2;
if (rtnStateDiffSize < 10) { if (rtnStateDiffSize < 10) {
rtnStateDiffSize = Math.max(termFontSize, 10); rtnStateDiffSize = Math.max(termFontSize, 10);
} }
let containerType = screen.getContainerType(); const containerType = screen.getContainerType();
return ( return (
<div <div
className={mainDivCn} className={mainDivCn}
@@ -681,7 +673,7 @@ class LineCmd extends React.Component<
<If condition={!hidePrompt}>{this.renderCmdText(cmd)}</If> <If condition={!hidePrompt}>{this.renderCmdText(cmd)}</If>
</div> </div>
<div key="restart" title="Restart Command" className="line-icon" onClick={this.clickRestart}> <div key="restart" title="Restart Command" className="line-icon" onClick={this.clickRestart}>
<i className="fa-sharp fa-regular fa-arrows-rotate"/> <i className="fa-sharp fa-regular fa-arrows-rotate" />
</div> </div>
<div key="delete" title="Delete Line (&#x2318;D)" className="line-icon" onClick={this.clickDelete}> <div key="delete" title="Delete Line (&#x2318;D)" className="line-icon" onClick={this.clickDelete}>
<i className="fa-sharp fa-regular fa-trash" /> <i className="fa-sharp fa-regular fa-trash" />
@@ -821,7 +813,7 @@ class Line extends React.Component<
{} {}
> { > {
render() { render() {
let line = this.props.line; const line = this.props.line;
if (line.archived) { if (line.archived) {
return null; return null;
} }
@@ -847,7 +839,7 @@ class LineText extends React.Component<
> { > {
@boundMethod @boundMethod
clickHandler() { clickHandler() {
let { line, noSelect } = this.props; const { line, noSelect } = this.props;
if (noSelect) { if (noSelect) {
return; return;
} }
@@ -856,7 +848,7 @@ class LineText extends React.Component<
@boundMethod @boundMethod
onAvatarRightClick(e: any): void { onAvatarRightClick(e: any): void {
let { line, noSelect } = this.props; const { line, noSelect } = this.props;
if (noSelect) { if (noSelect) {
return; return;
} }
@@ -870,19 +862,19 @@ class LineText extends React.Component<
} }
render() { render() {
let { screen, line, renderMode } = this.props; const { screen, line } = this.props;
let formattedTime = lineutil.getLineDateTimeStr(line.ts); const formattedTime = lineutil.getLineDateTimeStr(line.ts);
let isSelected = mobx const isSelected = mobx
.computed(() => screen.getSelectedLine() == line.linenum, { .computed(() => screen.getSelectedLine() == line.linenum, {
name: "computed-isSelected", name: "computed-isSelected",
}) })
.get(); .get();
let isFocused = mobx const isFocused = mobx
.computed(() => screen.getFocusType() == "cmd", { .computed(() => screen.getFocusType() == "cmd", {
name: "computed-isFocused", name: "computed-isFocused",
}) })
.get(); .get();
let mainClass = cn("line", "line-text", "focus-parent"); const mainClass = cn("line", "line-text", "focus-parent");
return ( return (
<div <div
className={mainClass} className={mainClass}
+6
View File
@@ -27,6 +27,12 @@ let MagicLayout = {
ScreenSidebarWidthPadding: 5, ScreenSidebarWidthPadding: 5,
ScreenSidebarMinWidth: 200, ScreenSidebarMinWidth: 200,
ScreenSidebarHeaderHeight: 28, ScreenSidebarHeaderHeight: 28,
MainSidebarMinWidth: 75,
MainSidebarMaxWidth: 300,
MainSidebarSnapThreshold: 90,
MainSidebarDragResistance: 50,
MainSidebarDefaultWidth: 240,
}; };
let m = MagicLayout; let m = MagicLayout;
+23 -18
View File
@@ -3,14 +3,13 @@
.main-sidebar { .main-sidebar {
padding: 0; padding: 0;
min-width: 20rem;
max-width: 20rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: relative; position: relative;
font-size: 12.5px; font-size: 12.5px;
line-height: 20px; line-height: 20px;
backdrop-filter: blur(4px); backdrop-filter: blur(4px);
z-index: 20;
.title-bar-drag { .title-bar-drag {
-webkit-app-region: drag; -webkit-app-region: drag;
@@ -24,7 +23,6 @@
&.collapsed { &.collapsed {
width: 6em; width: 6em;
min-width: 6em; min-width: 6em;
.arrow-container, .arrow-container,
.collapse-button { .collapse-button {
transform: rotate(180deg); transform: rotate(180deg);
@@ -34,7 +32,7 @@
margin-top: 26px; margin-top: 26px;
.top, .top,
.workspaces-item, .workspaces,
.middle, .middle,
.bottom, .bottom,
.separator { .separator {
@@ -50,7 +48,7 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
.logo-container img { .logo-container {
width: 45px; width: 45px;
} }
@@ -86,10 +84,14 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
.logo-container {
flex-shrink: 0;
width: 100px;
}
.spacer { .spacer {
flex-grow: 1; flex-grow: 1;
} }
img { img {
width: 100px; width: 100px;
} }
@@ -127,9 +129,7 @@
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;
} }
.index { .index {
font-size: 0.8em; font-size: 10px;
text-align: center;
width: 2em;
} }
.hotkey { .hotkey {
float: left !important; float: left !important;
@@ -152,7 +152,6 @@
margin-left: 6px; margin-left: 6px;
border-radius: 4px; border-radius: 4px;
opacity: 1; opacity: 1;
transition: opacity 0.1s ease-in-out, visibility 0.1s step-end;
width: inherit; width: inherit;
max-width: inherit; max-width: inherit;
min-width: inherit; min-width: inherit;
@@ -179,6 +178,7 @@
float: right; float: right;
margin-right: 6px; margin-right: 6px;
letter-spacing: 6px; letter-spacing: 6px;
margin-left: auto;
} }
&:hover { &:hover {
:not(.disabled) .hotkey { :not(.disabled) .hotkey {
@@ -190,15 +190,20 @@
} }
&:not(:hover) .status-indicator { &:not(:hover) .status-indicator {
.positional-icon-visible; .status-indicator-visible;
} }
.add_workspace {
float: right; &.workspaces {
padding: 2px; cursor: default;
margin-right: 6px; .add-workspace {
transition: transform 0.3s ease-in-out; cursor: pointer;
svg { .positional-icon-visible;
fill: @base-color; float: right;
padding: 2px;
margin-right: 6px;
.fa-plus {
font-size: 13px;
}
} }
} }
+114 -98
View File
@@ -7,19 +7,19 @@ import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator"; import { boundMethod } from "autobind-decorator";
import cn from "classnames"; import cn from "classnames";
import dayjs from "dayjs"; import dayjs from "dayjs";
import type { RemoteType } from "../../types/types"; import type { ClientDataType, RemoteType } from "../../types/types";
import { If } from "tsx-control-statements/components"; import { If } from "tsx-control-statements/components";
import { compareLoose } from "semver"; import { compareLoose } from "semver";
import { ReactComponent as LeftChevronIcon } from "../assets/icons/chevron_left.svg"; import { ReactComponent as LeftChevronIcon } from "../assets/icons/chevron_left.svg";
import { ReactComponent as AppsIcon } from "../assets/icons/apps.svg"; import { ReactComponent as AppsIcon } from "../assets/icons/apps.svg";
import { ReactComponent as WorkspacesIcon } from "../assets/icons/workspaces.svg"; import { ReactComponent as WorkspacesIcon } from "../assets/icons/workspaces.svg";
import { ReactComponent as AddIcon } from "../assets/icons/add.svg";
import { ReactComponent as SettingsIcon } from "../assets/icons/settings.svg"; import { ReactComponent as SettingsIcon } from "../assets/icons/settings.svg";
import localizedFormat from "dayjs/plugin/localizedFormat"; import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel, GlobalCommandRunner, Session, VERSION } from "../../model/model"; import { GlobalModel, GlobalCommandRunner, Session, VERSION } from "../../model/model";
import { isBlank, openLink } from "../../util/util"; import { isBlank, openLink } from "../../util/util";
import { ResizableSidebar } from "../common/common";
import * as constants from "../appconst"; import * as constants from "../appconst";
import "./sidebar.less"; import "./sidebar.less";
@@ -27,8 +27,6 @@ import { ActionsIcon, CenteredIcon, FrontIcon, StatusIndicator } from "../common
dayjs.extend(localizedFormat); dayjs.extend(localizedFormat);
type OV<V> = mobx.IObservableValue<V>;
class SideBarItem extends React.Component<{ class SideBarItem extends React.Component<{
frontIcon: React.ReactNode; frontIcon: React.ReactNode;
contents: React.ReactNode | string; contents: React.ReactNode | string;
@@ -60,16 +58,14 @@ class HotKeyIcon extends React.Component<{ hotkey: string }> {
} }
} }
@mobxReact.observer interface MainSideBarProps {
class MainSideBar extends React.Component<{}, {}> { parentRef: React.RefObject<HTMLElement>;
collapsed: mobx.IObservableValue<boolean> = mobx.observable.box(false); clientData: ClientDataType;
}
@boundMethod @mobxReact.observer
toggleCollapsed() { class MainSideBar extends React.Component<MainSideBarProps, {}> {
mobx.action(() => { sidebarRef = React.createRef<HTMLDivElement>();
this.collapsed.set(!this.collapsed.get());
})();
}
handleSessionClick(sessionId: string) { handleSessionClick(sessionId: string) {
GlobalCommandRunner.switchSession(sessionId); GlobalCommandRunner.switchSession(sessionId);
@@ -176,9 +172,9 @@ class MainSideBar extends React.Component<{}, {}> {
getSessions() { getSessions() {
if (!GlobalModel.sessionListLoaded.get()) return <div className="item">loading ...</div>; if (!GlobalModel.sessionListLoaded.get()) return <div className="item">loading ...</div>;
let sessionList = []; const sessionList: Session[] = [];
let activeSessionId = GlobalModel.activeSessionId.get(); const activeSessionId = GlobalModel.activeSessionId.get();
for (let session of GlobalModel.sessionList) { for (const session of GlobalModel.sessionList) {
if (!session.archived.get() || session.sessionId == activeSessionId) { if (!session.archived.get() || session.sessionId == activeSessionId) {
sessionList.push(session); sessionList.push(session);
} }
@@ -190,6 +186,7 @@ class MainSideBar extends React.Component<{}, {}> {
const sessionRunningCommands = sessionScreens.some((screen) => screen.numRunningCmds.get() > 0); const sessionRunningCommands = sessionScreens.some((screen) => screen.numRunningCmds.get() > 0);
return ( return (
<SideBarItem <SideBarItem
key={session.sessionId}
className={`${isActive ? "active" : ""}`} className={`${isActive ? "active" : ""}`}
frontIcon={<span className="index">{index + 1}</span>} frontIcon={<span className="index">{index + 1}</span>}
contents={session.name.get()} contents={session.name.get()}
@@ -208,97 +205,116 @@ class MainSideBar extends React.Component<{}, {}> {
} }
render() { render() {
let isCollapsed = this.collapsed.get(); let clientData = this.props.clientData;
let clientData = GlobalModel.clientData.get();
let needsUpdate = false; let needsUpdate = false;
if (!clientData?.clientopts.noreleasecheck && !isBlank(clientData?.releaseinfo?.latestversion)) { if (!clientData?.clientopts.noreleasecheck && !isBlank(clientData?.releaseinfo?.latestversion)) {
needsUpdate = compareLoose(VERSION, clientData.releaseinfo.latestversion) < 0; needsUpdate = compareLoose(VERSION, clientData.releaseinfo.latestversion) < 0;
} }
let mainSidebar = GlobalModel.mainSidebarModel;
let isCollapsed = mainSidebar.getCollapsed();
return ( return (
<div className={cn("main-sidebar", { collapsed: isCollapsed }, { "is-dev": GlobalModel.isDev })}> <ResizableSidebar
<div className="title-bar-drag" /> className="main-sidebar"
<div className="contents"> position="left"
<div className="logo"> enableSnap={true}
<If condition={isCollapsed}> parentRef={this.props.parentRef}
<div className="logo-container" onClick={this.toggleCollapsed}> >
<img src="public/logos/wave-logo.png" /> {(toggleCollapse) => (
<React.Fragment>
<div className="title-bar-drag" />
<div className="contents">
<div className="logo">
<If condition={isCollapsed}>
<div className="logo-container" onClick={toggleCollapse}>
<img src="public/logos/wave-logo.png" />
</div>
</If>
<If condition={!isCollapsed}>
<div className="logo-container">
<img src="public/logos/wave-dark.png" />
</div>
<div className="spacer" />
<div className="collapse-button" onClick={toggleCollapse}>
<LeftChevronIcon className="icon" />
</div>
</If>
</div> </div>
</If> <div className="separator" />
<If condition={!isCollapsed}> <div className="top">
<div className="logo-container"> <SideBarItem
<img src="public/logos/wave-dark.png" /> key="history"
frontIcon={<i className="fa-sharp fa-regular fa-clock-rotate-left icon" />}
contents="History"
endIcons={[<HotKeyIcon key="hotkey" hotkey="H" />]}
onClick={this.handleHistoryClick}
/>
{/* <SideBarItem className="hoverEffect unselectable" frontIcon={<FavoritesIcon className="icon" />} contents="Favorites" endIcon={<span className="hotkey">&#x2318;B</span>} onClick={this.handleBookmarksClick}/> */}
<SideBarItem
key="connections"
frontIcon={<i className="fa-sharp fa-regular fa-globe icon " />}
contents="Connections"
onClick={this.handleConnectionsClick}
/>
</div> </div>
<div className="spacer" /> <div className="separator" />
<div className="collapse-button" onClick={this.toggleCollapsed}>
<LeftChevronIcon className="icon" />
</div>
</If>
</div>
<div className="separator" />
<div className="top">
<SideBarItem
frontIcon={<i className="fa-sharp fa-regular fa-clock-rotate-left icon" />}
contents="History"
endIcons={[<HotKeyIcon key="hotkey" hotkey="H" />]}
onClick={this.handleHistoryClick}
/>
{/* <SideBarItem className="hoverEffect unselectable" frontIcon={<FavoritesIcon className="icon" />} contents="Favorites" endIcon={<span className="hotkey">&#x2318;B</span>} onClick={this.handleBookmarksClick}/> */}
<SideBarItem
frontIcon={<i className="fa-sharp fa-regular fa-globe icon " />}
contents="Connections"
onClick={this.handleConnectionsClick}
/>
</div>
<div className="separator" />
<SideBarItem
frontIcon={<WorkspacesIcon className="icon" />}
contents="Workspaces"
endIcons={[
<div
key="add_workspace"
className="add_workspace hoverEffect"
onClick={this.handleNewSession}
>
<AddIcon />
</div>,
]}
/>
<div className="middle hideScrollbarUntillHover">{this.getSessions()}</div>
<div className="bottom">
<If condition={needsUpdate}>
<SideBarItem <SideBarItem
className="updateBanner" key="workspaces"
frontIcon={<i className="fa-sharp fa-regular fa-circle-up icon" />} className="workspaces"
contents="Update Available" frontIcon={<WorkspacesIcon className="icon" />}
onClick={() => openLink("https://www.waveterm.dev/download?ref=upgrade")} contents="Workspaces"
endIcons={[
<CenteredIcon
key="add-workspace"
className="add-workspace hoverEffect"
onClick={this.handleNewSession}
>
<i className="fa-sharp fa-solid fa-plus"></i>
</CenteredIcon>,
]}
/> />
</If> <div className="middle hideScrollbarUntillHover">{this.getSessions()}</div>
<If condition={GlobalModel.isDev}> <div className="bottom">
<SideBarItem <If condition={needsUpdate}>
frontIcon={<AppsIcon className="icon" />} <SideBarItem
contents="Apps" key="update-available"
onClick={this.handlePluginsClick} className="updateBanner"
endIcons={[<HotKeyIcon key="hotkey" hotkey="A" />]} frontIcon={<i className="fa-sharp fa-regular fa-circle-up icon" />}
/> contents="Update Available"
</If> onClick={() => openLink("https://www.waveterm.dev/download?ref=upgrade")}
<SideBarItem />
frontIcon={<SettingsIcon className="icon" />} </If>
contents="Settings" <If condition={GlobalModel.isDev}>
onClick={this.handleSettingsClick} <SideBarItem
/> key="apps"
<SideBarItem frontIcon={<AppsIcon className="icon" />}
frontIcon={<i className="fa-sharp fa-regular fa-circle-question icon" />} contents="Apps"
contents="Documentation" onClick={this.handlePluginsClick}
onClick={() => openLink("https://docs.waveterm.dev")} endIcons={[<HotKeyIcon key="hotkey" hotkey="A" />]}
/> />
<SideBarItem </If>
frontIcon={<i className="fa-brands fa-discord icon" />} <SideBarItem
contents="Discord" key="settings"
onClick={() => openLink("https://discord.gg/XfvZ334gwU")} frontIcon={<SettingsIcon className="icon" />}
/> contents="Settings"
</div> onClick={this.handleSettingsClick}
</div> />
</div> <SideBarItem
key="documentation"
frontIcon={<i className="fa-sharp fa-regular fa-circle-question icon" />}
contents="Documentation"
onClick={() => openLink("https://docs.waveterm.dev")}
/>
<SideBarItem
key="discord"
frontIcon={<i className="fa-brands fa-discord icon" />}
contents="Discord"
onClick={() => openLink("https://discord.gg/XfvZ334gwU")}
/>
</div>
</div>
</React.Fragment>
)}
</ResizableSidebar>
); );
} }
} }
+29 -28
View File
@@ -5,28 +5,25 @@ import * as React from "react";
import * as mobxReact from "mobx-react"; import * as mobxReact from "mobx-react";
import * as mobx from "mobx"; import * as mobx from "mobx";
import { boundMethod } from "autobind-decorator"; import { boundMethod } from "autobind-decorator";
import { If, Choose, When, Otherwise } from "tsx-control-statements/components"; import { If } from "tsx-control-statements/components";
import cn from "classnames"; import cn from "classnames";
import dayjs from "dayjs"; import dayjs from "dayjs";
import type { RemoteType, RemoteInstanceType, RemotePtrType } from "../../../types/types"; import type { RemoteType, RemoteInstanceType, RemotePtrType } from "../../../types/types";
import localizedFormat from "dayjs/plugin/localizedFormat"; import localizedFormat from "dayjs/plugin/localizedFormat";
import { GlobalModel, GlobalCommandRunner, Screen, ScreenLines } from "../../../model/model"; import { GlobalModel, GlobalCommandRunner, Screen } from "../../../model/model";
import { renderCmdText, Button } from "../../common/common"; import { renderCmdText } from "../../common/common";
import { TextAreaInput } from "./textareainput"; import { TextAreaInput } from "./textareainput";
import { InfoMsg } from "./infomsg"; import { InfoMsg } from "./infomsg";
import { HistoryInfo } from "./historyinfo"; import { HistoryInfo } from "./historyinfo";
import { Prompt } from "../../common/prompt/prompt"; import { Prompt } from "../../common/prompt/prompt";
import { ReactComponent as ExecIcon } from "../../assets/icons/exec.svg"; import { ReactComponent as ExecIcon } from "../../assets/icons/exec.svg";
import { ReactComponent as RotateIcon } from "../../assets/icons/line/rotate.svg"; import { RotateIcon } from "../../common/icons/icons";
import "./cmdinput.less";
import { AIChat } from "./aichat"; import { AIChat } from "./aichat";
import "./cmdinput.less";
dayjs.extend(localizedFormat); dayjs.extend(localizedFormat);
const TDots = "â‹®";
type OV<V> = mobx.IObservableValue<V>;
@mobxReact.observer @mobxReact.observer
class CmdInput extends React.Component<{}, {}> { class CmdInput extends React.Component<{}, {}> {
cmdInputRef: React.RefObject<any> = React.createRef(); cmdInputRef: React.RefObject<any> = React.createRef();
@@ -37,11 +34,11 @@ class CmdInput extends React.Component<{}, {}> {
} }
updateCmdInputHeight() { updateCmdInputHeight() {
let elem = this.cmdInputRef.current; const elem = this.cmdInputRef.current;
if (elem == null) { if (elem == null) {
return; return;
} }
let height = elem.offsetHeight; const height = elem.offsetHeight;
if (height == GlobalModel.inputModel.cmdInputHeight) { if (height == GlobalModel.inputModel.cmdInputHeight) {
return; return;
} }
@@ -50,7 +47,7 @@ class CmdInput extends React.Component<{}, {}> {
})(); })();
} }
componentDidUpdate(prevProps, prevState, snapshot: {}): void { componentDidUpdate(): void {
this.updateCmdInputHeight(); this.updateCmdInputHeight();
} }
@@ -87,7 +84,7 @@ class CmdInput extends React.Component<{}, {}> {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
let inputModel = GlobalModel.inputModel; const inputModel = GlobalModel.inputModel;
if (inputModel.historyShow.get()) { if (inputModel.historyShow.get()) {
inputModel.resetHistory(); inputModel.resetHistory();
} else { } else {
@@ -113,9 +110,9 @@ class CmdInput extends React.Component<{}, {}> {
} }
render() { render() {
let model = GlobalModel; const model = GlobalModel;
let inputModel = model.inputModel; const inputModel = model.inputModel;
let screen = GlobalModel.getActiveScreen(); const screen = GlobalModel.getActiveScreen();
let ri: RemoteInstanceType = null; let ri: RemoteInstanceType = null;
let rptr: RemotePtrType = null; let rptr: RemotePtrType = null;
if (screen != null) { if (screen != null) {
@@ -129,15 +126,13 @@ class CmdInput extends React.Component<{}, {}> {
feState = ri.festate; feState = ri.festate;
} }
feState = feState || {}; feState = feState || {};
let infoShow = inputModel.infoShow.get(); const infoShow = inputModel.infoShow.get();
let historyShow = !infoShow && inputModel.historyShow.get(); const historyShow = !infoShow && inputModel.historyShow.get();
let aiChatShow = inputModel.aIChatShow.get(); const aiChatShow = inputModel.aIChatShow.get();
let infoMsg = inputModel.infoMsg.get(); const focusVal = inputModel.physicalInputFocused.get();
let hasInfo = infoMsg != null; const inputMode: string = inputModel.inputMode.get();
let focusVal = inputModel.physicalInputFocused.get(); const textAreaInputKey = screen == null ? "null" : screen.screenId;
let inputMode: string = inputModel.inputMode.get(); const win = GlobalModel.getScreenLinesById(screen.screenId);
let textAreaInputKey = screen == null ? "null" : screen.screenId;
let win = GlobalModel.getScreenLinesById(screen.screenId);
let numRunningLines = 0; let numRunningLines = 0;
if (win != null) { if (win != null) {
numRunningLines = mobx.computed(() => win.getRunningCmdLines().length).get(); numRunningLines = mobx.computed(() => win.getRunningCmdLines().length).get();
@@ -232,11 +227,17 @@ class CmdInput extends React.Component<{}, {}> {
{focusVal && ( {focusVal && (
<div className="cmd-btn hoverEffect"> <div className="cmd-btn hoverEffect">
<If condition={historyShow}> <If condition={historyShow}>
<div className="hint-elem" onMouseDown={this.clickHistoryHint}>close (esc)</div> <div className="hint-elem" onMouseDown={this.clickHistoryHint}>
close (esc)
</div>
</If> </If>
<If condition={!historyShow}> <If condition={!historyShow}>
<div className="hint-elem" onMouseDown={this.clickHistoryHint}>history (ctrl-r)</div> <div className="hint-elem" onMouseDown={this.clickHistoryHint}>
<div className="hint-elem" onMouseDown={this.clickAIHint}>AI (ctrl-space)</div> history (ctrl-r)
</div>
<div className="hint-elem" onMouseDown={this.clickAIHint}>
AI (ctrl-space)
</div>
</If> </If>
</div> </div>
)} )}
+1 -1
View File
@@ -287,7 +287,7 @@
} }
&:not(:hover) .status-indicator { &:not(:hover) .status-indicator {
.positional-icon-visible; .status-indicator-visible;
} }
&:hover { &:hover {
+2 -5
View File
@@ -8,12 +8,12 @@
&.is-hidden { &.is-hidden {
display: none; display: none;
} }
max-width: calc(100% - 20.5em);
background: @background-session; background: @background-session;
border: 1px solid @base-border; border: 1px solid @base-border;
border-radius: 8px; border-radius: 8px;
transition: width 0.2s ease; // transition: width 0.2s ease;
margin-bottom: 0.5em; margin-bottom: 0.5em;
margin-right: 0.5em;
.center-message { .center-message {
display: flex; display: flex;
@@ -24,6 +24,3 @@
color: @text-secondary; color: @text-secondary;
} }
} }
.collapsed + .session-view {
max-width: calc(100% - 6.7em);
}
+17 -4
View File
@@ -27,20 +27,33 @@ class WorkspaceView extends React.Component<{}, {}> {
if (session == null) { if (session == null) {
return ( return (
<div className="session-view"> <div className="session-view">
<div className="center-message"><div>(no active workspace)</div></div> <div className="center-message">
<div>(no active workspace)</div>
</div>
</div> </div>
); );
} }
let activeScreen = session.getActiveScreen(); let activeScreen = session.getActiveScreen();
let cmdInputHeight = model.inputModel.cmdInputHeight.get(); let cmdInputHeight = model.inputModel.cmdInputHeight.get();
if (cmdInputHeight == 0) { if (cmdInputHeight == 0) {
cmdInputHeight = MagicLayout.CmdInputHeight; // this is the base size of cmdInput (measured using devtools) cmdInputHeight = MagicLayout.CmdInputHeight; // this is the base size of cmdInput (measured using devtools)
} }
cmdInputHeight += MagicLayout.CmdInputBottom; // reference to .cmd-input, bottom: 12px cmdInputHeight += MagicLayout.CmdInputBottom; // reference to .cmd-input, bottom: 12px
let isHidden = GlobalModel.activeMainView.get() != "session"; let isHidden = GlobalModel.activeMainView.get() != "session";
let mainSidebarModel = GlobalModel.mainSidebarModel;
// Has to calc manually because when tabs overflow, the width of the session view is increased for some reason causing inconsistent width.
// 6px is the right margin of session view.
let width = window.innerWidth - 6 - mainSidebarModel.getWidth();
return ( return (
<div className={cn("session-view", { "is-hidden": isHidden })} data-sessionid={session.sessionId}> <div
className={cn("session-view", { "is-hidden": isHidden })}
data-sessionid={session.sessionId}
style={{
width: `${width}px`,
}}
>
<ScreenTabs key={"tabs-" + session.sessionId} session={session} /> <ScreenTabs key={"tabs-" + session.sessionId} session={session} />
<ErrorBoundary> <ErrorBoundary>
<ScreenView key={"screenview-" + session.sessionId} session={session} screen={activeScreen} /> <ScreenView key={"screenview-" + session.sessionId} session={session} screen={activeScreen} />
+86 -7
View File
@@ -7,6 +7,7 @@ import { sprintf } from "sprintf-js";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { boundMethod } from "autobind-decorator"; import { boundMethod } from "autobind-decorator";
import { debounce } from "throttle-debounce"; import { debounce } from "throttle-debounce";
import * as mobxReact from "mobx-react";
import { import {
handleJsonFetchResponse, handleJsonFetchResponse,
base64ToString, base64ToString,
@@ -2617,6 +2618,77 @@ class ClientSettingsViewModel {
})(); })();
} }
} }
class MainSidebarModel {
tempWidth: OV<number> = mobx.observable.box(null, {
name: "MainSidebarModel-tempWidth",
});
tempCollapsed: OV<boolean> = mobx.observable.box(null, {
name: "MainSidebarModel-tempCollapsed",
});
isDragging: OV<boolean> = mobx.observable.box(false, {
name: "MainSidebarModel-isDragging",
});
setTempWidthAndTempCollapsed(newWidth: number, newCollapsed: boolean): void {
const width = Math.max(MagicLayout.MainSidebarMinWidth, Math.min(newWidth, MagicLayout.MainSidebarMaxWidth));
mobx.action(() => {
this.tempWidth.set(width);
this.tempCollapsed.set(newCollapsed);
})();
}
/**
* Gets the intended width for the sidebar. If the sidebar is being dragged, returns the tempWidth. If the sidebar is collapsed, returns the default width.
* @param ignoreCollapse If true, returns the persisted width even if the sidebar is collapsed.
* @returns The intended width for the sidebar or the default width if the sidebar is collapsed. Can be overridden using ignoreCollapse.
*/
getWidth(ignoreCollapse: boolean = false): number {
const clientData = GlobalModel.clientData.get();
let width = clientData?.clientopts?.mainsidebar?.width ?? MagicLayout.MainSidebarDefaultWidth;
if (this.isDragging.get()) {
if (this.tempWidth.get() == null && width == null) {
return MagicLayout.MainSidebarDefaultWidth;
}
if (this.tempWidth.get() == null) {
return width;
}
return this.tempWidth.get();
}
// Set by CLI and collapsed
if (this.getCollapsed()) {
if (ignoreCollapse) {
return width;
} else {
return MagicLayout.MainSidebarMinWidth;
}
} else {
if (width <= MagicLayout.MainSidebarMinWidth) {
width = MagicLayout.MainSidebarDefaultWidth;
}
const snapPoint = MagicLayout.MainSidebarMinWidth + MagicLayout.MainSidebarSnapThreshold;
if (width < snapPoint || width > MagicLayout.MainSidebarMaxWidth) {
width = MagicLayout.MainSidebarDefaultWidth;
}
}
return width;
}
getCollapsed(): boolean {
const clientData = GlobalModel.clientData.get();
const collapsed = clientData?.clientopts?.mainsidebar?.collapsed;
if (this.isDragging.get()) {
if (this.tempCollapsed.get() == null && collapsed == null) {
return false;
}
if (this.tempCollapsed.get() == null) {
return collapsed;
}
return this.tempCollapsed.get();
}
return collapsed;
}
}
class BookmarksModel { class BookmarksModel {
bookmarks: OArr<BookmarkType> = mobx.observable.array([], { bookmarks: OArr<BookmarkType> = mobx.observable.array([], {
@@ -3387,6 +3459,7 @@ class Model {
connectionViewModel: ConnectionsViewModel; connectionViewModel: ConnectionsViewModel;
clientSettingsViewModel: ClientSettingsViewModel; clientSettingsViewModel: ClientSettingsViewModel;
modalsModel: ModalsModel; modalsModel: ModalsModel;
mainSidebarModel: MainSidebarModel;
clientData: OV<ClientDataType> = mobx.observable.box(null, { clientData: OV<ClientDataType> = mobx.observable.box(null, {
name: "clientData", name: "clientData",
}); });
@@ -3413,6 +3486,7 @@ class Model {
this.remotesModalModel = new RemotesModalModel(); this.remotesModalModel = new RemotesModalModel();
this.remotesModel = new RemotesModel(); this.remotesModel = new RemotesModel();
this.modalsModel = new ModalsModel(); this.modalsModel = new ModalsModel();
this.mainSidebarModel = new MainSidebarModel();
let isWaveSrvRunning = getApi().getWaveSrvStatus(); let isWaveSrvRunning = getApi().getWaveSrvStatus();
this.waveSrvRunning = mobx.observable.box(isWaveSrvRunning, { this.waveSrvRunning = mobx.observable.box(isWaveSrvRunning, {
name: "model-wavesrv-running", name: "model-wavesrv-running",
@@ -4100,15 +4174,15 @@ class Model {
if ("openaicmdinfochat" in update) { if ("openaicmdinfochat" in update) {
this.inputModel.setOpenAICmdInfoChat(update.openaicmdinfochat); this.inputModel.setOpenAICmdInfoChat(update.openaicmdinfochat);
} }
if ("screenstatusindicator" in update) { if ("screenstatusindicators" in update) {
this.getScreenById_single(update.screenstatusindicator.screenid)?.setStatusIndicator( for (const indicator of update.screenstatusindicators) {
update.screenstatusindicator.status this.getScreenById_single(indicator.screenid)?.setStatusIndicator(indicator.status);
); }
} }
if ("screennumrunningcommands" in update) { if ("screennumrunningcommands" in update) {
this.getScreenById_single(update.screennumrunningcommands.screenid)?.setNumRunningCmds( for (const snc of update.screennumrunningcommands) {
update.screennumrunningcommands.num this.getScreenById_single(snc.screenid)?.setNumRunningCmds(snc.num);
); }
} }
} }
@@ -4980,6 +5054,11 @@ class CommandRunner {
return GlobalModel.submitCommand("client", "setconfirmflag", [flag, valueStr], kwargs, false); return GlobalModel.submitCommand("client", "setconfirmflag", [flag, valueStr], kwargs, false);
} }
clientSetSidebar(width: number, collapsed: boolean): Promise<CommandRtnType> {
let kwargs = { nohist: "1", width: `${width}`, collapsed: collapsed ? "1" : "0" };
return GlobalModel.submitCommand("client", "setsidebar", null, kwargs, false);
}
editBookmark(bookmarkId: string, desc: string, cmdstr: string) { editBookmark(bookmarkId: string, desc: string, cmdstr: string) {
let kwargs = { let kwargs = {
nohist: "1", nohist: "1",
+6 -2
View File
@@ -326,8 +326,8 @@ type ModelUpdateType = {
remoteview?: RemoteViewType; remoteview?: RemoteViewType;
openaicmdinfochat?: OpenAICmdInfoChatMessageType[]; openaicmdinfochat?: OpenAICmdInfoChatMessageType[];
alertmessage?: AlertMessageType; alertmessage?: AlertMessageType;
screenstatusindicator?: ScreenStatusIndicatorUpdateType; screenstatusindicators?: ScreenStatusIndicatorUpdateType[];
screennumrunningcommands?: ScreenNumRunningCommandsUpdateType; screennumrunningcommands?: ScreenNumRunningCommandsUpdateType[];
}; };
type HistoryViewDataType = { type HistoryViewDataType = {
@@ -524,6 +524,10 @@ type ClientOptsType = {
noreleasecheck: boolean; noreleasecheck: boolean;
acceptedtos: number; acceptedtos: number;
confirmflags: ConfirmFlagsType; confirmflags: ConfirmFlagsType;
mainsidebar: {
collapsed: boolean;
width: number;
};
}; };
type ReleaseInfoType = { type ReleaseInfoType = {
+70 -4
View File
@@ -92,6 +92,7 @@ var TabIcons = []string{"square", "sparkle", "fire", "ghost", "cloud", "compass"
var RemoteColorNames = []string{"red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"} var RemoteColorNames = []string{"red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"}
var RemoteSetArgs = []string{"alias", "connectmode", "key", "password", "autoinstall", "color"} var RemoteSetArgs = []string{"alias", "connectmode", "key", "password", "autoinstall", "color"}
var ConfirmFlags = []string{"hideshellprompt"} var ConfirmFlags = []string{"hideshellprompt"}
var SidebarNames = []string{"main"}
var ScreenCmds = []string{"run", "comment", "cd", "cr", "clear", "sw", "reset", "signal", "chat"} var ScreenCmds = []string{"run", "comment", "cd", "cr", "clear", "sw", "reset", "signal", "chat"}
var NoHistCmds = []string{"_compgen", "line", "history", "_killserver"} var NoHistCmds = []string{"_compgen", "line", "history", "_killserver"}
@@ -218,6 +219,7 @@ func init() {
registerCmdFn("client:notifyupdatewriter", ClientNotifyUpdateWriterCommand) registerCmdFn("client:notifyupdatewriter", ClientNotifyUpdateWriterCommand)
registerCmdFn("client:accepttos", ClientAcceptTosCommand) registerCmdFn("client:accepttos", ClientAcceptTosCommand)
registerCmdFn("client:setconfirmflag", ClientConfirmFlagCommand) registerCmdFn("client:setconfirmflag", ClientConfirmFlagCommand)
registerCmdFn("client:setsidebar", ClientSetSidebarCommand)
registerCmdFn("sidebar:open", SidebarOpenCommand) registerCmdFn("sidebar:open", SidebarOpenCommand)
registerCmdFn("sidebar:close", SidebarCloseCommand) registerCmdFn("sidebar:close", SidebarCloseCommand)
@@ -1418,9 +1420,6 @@ func RemoteSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss
if err != nil { if err != nil {
return nil, err return nil, err
} }
if ids.Remote.RState.SSHConfigSrc == sstore.SSHConfigSrcTypeImport {
return nil, fmt.Errorf("/remote:new cannot update imported remote")
}
visualEdit := resolveBool(pk.Kwargs["visual"], false) visualEdit := resolveBool(pk.Kwargs["visual"], false)
isSubmitted := resolveBool(pk.Kwargs["submit"], false) isSubmitted := resolveBool(pk.Kwargs["submit"], false)
editArgs, err := parseRemoteEditArgs(false, pk, ids.Remote.MShell.IsLocal()) editArgs, err := parseRemoteEditArgs(false, pk, ids.Remote.MShell.IsLocal())
@@ -1540,6 +1539,7 @@ type HostInfoType struct {
SshKeyFile string SshKeyFile string
ConnectMode string ConnectMode string
Ignore bool Ignore bool
ShellPref string
} }
func createSshImportSummary(changeList map[string][]string) string { func createSshImportSummary(changeList map[string][]string) string {
@@ -1635,6 +1635,13 @@ func NewHostInfo(hostName string) (*HostInfoType, error) {
connectMode = sstore.ConnectModeManual connectMode = sstore.ConnectModeManual
} }
shellPref := sstore.ShellTypePref_Detect
if cfgWaveOptions["shellpref"] == "bash" {
shellPref = "bash"
} else if cfgWaveOptions["shellpref"] == "zsh" {
shellPref = "zsh"
}
outHostInfo := new(HostInfoType) outHostInfo := new(HostInfoType)
outHostInfo.Host = hostName outHostInfo.Host = hostName
outHostInfo.User = userName outHostInfo.User = userName
@@ -1643,6 +1650,7 @@ func NewHostInfo(hostName string) (*HostInfoType, error) {
outHostInfo.SshKeyFile = sshKeyFile outHostInfo.SshKeyFile = sshKeyFile
outHostInfo.ConnectMode = connectMode outHostInfo.ConnectMode = connectMode
outHostInfo.Ignore = shouldIgnore outHostInfo.Ignore = shouldIgnore
outHostInfo.ShellPref = shellPref
return outHostInfo, nil return outHostInfo, nil
} }
@@ -1707,6 +1715,7 @@ func RemoteConfigParseCommand(ctx context.Context, pk *scpacket.FeCommandPacketT
if hostInfo.SshKeyFile != "" { if hostInfo.SshKeyFile != "" {
editMap[sstore.RemoteField_SSHKey] = hostInfo.SshKeyFile editMap[sstore.RemoteField_SSHKey] = hostInfo.SshKeyFile
} }
editMap[sstore.RemoteField_ShellPref] = hostInfo.ShellPref
msh := remote.GetRemoteById(previouslyImportedRemote.RemoteId) msh := remote.GetRemoteById(previouslyImportedRemote.RemoteId)
if msh == nil { if msh == nil {
remoteChangeList["updateErr"] = append(remoteChangeList["updateErr"], hostInfo.CanonicalName) remoteChangeList["updateErr"] = append(remoteChangeList["updateErr"], hostInfo.CanonicalName)
@@ -1714,7 +1723,7 @@ func RemoteConfigParseCommand(ctx context.Context, pk *scpacket.FeCommandPacketT
continue continue
} }
if msh.Remote.ConnectMode == hostInfo.ConnectMode && msh.Remote.SSHOpts.SSHIdentity == hostInfo.SshKeyFile && msh.Remote.RemoteAlias == hostInfo.Host { if msh.Remote.ConnectMode == hostInfo.ConnectMode && msh.Remote.SSHOpts.SSHIdentity == hostInfo.SshKeyFile && msh.Remote.RemoteAlias == hostInfo.Host && msh.Remote.ShellPref == hostInfo.ShellPref {
// silently skip this one. it didn't fail, but no changes were needed // silently skip this one. it didn't fail, but no changes were needed
continue continue
} }
@@ -1751,6 +1760,7 @@ func RemoteConfigParseCommand(ctx context.Context, pk *scpacket.FeCommandPacketT
AutoInstall: true, AutoInstall: true,
SSHOpts: sshOpts, SSHOpts: sshOpts,
SSHConfigSrc: sstore.SSHConfigSrcTypeImport, SSHConfigSrc: sstore.SSHConfigSrcTypeImport,
ShellPref: sstore.ShellTypePref_Detect,
} }
err := remote.AddRemote(ctx, r, false) err := remote.AddRemote(ctx, r, false)
if err != nil { if err != nil {
@@ -4463,6 +4473,62 @@ func ClientConfirmFlagCommand(ctx context.Context, pk *scpacket.FeCommandPacketT
return update, nil return update, nil
} }
func ClientSetSidebarCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
clientData, err := sstore.EnsureClientData(ctx)
if err != nil {
return nil, fmt.Errorf("cannot retrieve client data: %v", err)
}
// Handle collapsed
collapsed, ok := pk.Kwargs["collapsed"]
if !ok {
return nil, fmt.Errorf("collapsed key not provided")
}
collapsedValue := resolveBool(collapsed, false)
// Handle width
var width int
if w, exists := pk.Kwargs["width"]; exists {
width, err = resolveNonNegInt(w, 0)
if err != nil {
return nil, fmt.Errorf("error resolving width: %v", err)
}
} else if clientData.ClientOpts.MainSidebar != nil {
width = clientData.ClientOpts.MainSidebar.Width
}
// Initialize SidebarCollapsed if it's nil
if clientData.ClientOpts.MainSidebar == nil {
clientData.ClientOpts.MainSidebar = new(sstore.SidebarValueType)
}
// Set the sidebar values
var sv sstore.SidebarValueType
sv.Collapsed = collapsedValue
if width != 0 {
sv.Width = width
}
clientData.ClientOpts.MainSidebar = &sv
// Update client data
err = sstore.SetClientOpts(ctx, clientData.ClientOpts)
if err != nil {
return nil, fmt.Errorf("error updating client data: %v", err)
}
// Retrieve updated client data
clientData, err = sstore.EnsureClientData(ctx)
if err != nil {
return nil, fmt.Errorf("cannot retrieve updated client data: %v", err)
}
update := &sstore.ModelUpdate{
ClientData: clientData,
}
return update, nil
}
func validateOpenAIAPIToken(key string) error { func validateOpenAIAPIToken(key string) error {
if len(key) > MaxOpenAIAPITokenLen { if len(key) > MaxOpenAIAPITokenLen {
return fmt.Errorf("invalid openai token, too long") return fmt.Errorf("invalid openai token, too long")

Some files were not shown because too many files have changed in this diff Show More