mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-03-09 13:00:53 +01:00
merge branch 'main' into use-ssh-library--add-user-input
This commit is contained in:
commit
fa9a0bd228
@ -30,6 +30,7 @@ type OV<V> = mobx.IObservableValue<V>;
|
||||
@mobxReact.observer
|
||||
class App extends React.Component<{}, {}> {
|
||||
dcWait: OV<boolean> = mobx.observable.box(false, { name: "dcWait" });
|
||||
mainContentRef: React.RefObject<HTMLDivElement> = React.createRef();
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
@ -75,6 +76,13 @@ class App extends React.Component<{}, {}> {
|
||||
let hasClientStop = GlobalModel.getHasClientStop();
|
||||
let dcWait = this.dcWait.get();
|
||||
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 (!dcWait) {
|
||||
@ -82,8 +90,8 @@ class App extends React.Component<{}, {}> {
|
||||
}
|
||||
return (
|
||||
<div id="main" className={"platform-" + platform} onContextMenu={this.handleContextMenu}>
|
||||
<div className="main-content">
|
||||
<MainSideBar />
|
||||
<div ref={this.mainContentRef} className="main-content">
|
||||
<MainSideBar parentRef={this.mainContentRef} clientData={clientData} />
|
||||
<div className="session-view" />
|
||||
</div>
|
||||
<If condition={dcWait}>
|
||||
@ -102,8 +110,8 @@ class App extends React.Component<{}, {}> {
|
||||
}
|
||||
return (
|
||||
<div id="main" className={"platform-" + platform} onContextMenu={this.handleContextMenu}>
|
||||
<div className="main-content">
|
||||
<MainSideBar />
|
||||
<div ref={this.mainContentRef} className="main-content">
|
||||
<MainSideBar parentRef={this.mainContentRef} clientData={clientData} />
|
||||
<ErrorBoundary>
|
||||
<PluginsView />
|
||||
<WorkspaceView />
|
||||
|
@ -9,11 +9,12 @@ import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import cn from "classnames";
|
||||
import { If } from "tsx-control-statements/components";
|
||||
import { RemoteType, StatusIndicatorLevel } from "../../types/types";
|
||||
import { RemoteType } from "../../types/types";
|
||||
import ReactDOM from "react-dom";
|
||||
import { GlobalModel } from "../../model/model";
|
||||
import { GlobalModel, GlobalCommandRunner } from "../../model/model";
|
||||
import * as appconst from "../appconst";
|
||||
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "../../util/keyutil";
|
||||
import { MagicLayout } from "../magiclayout";
|
||||
|
||||
import { ReactComponent as CheckIcon } from "../assets/icons/line/check.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 {
|
||||
CmdStrCode,
|
||||
Toggle,
|
||||
@ -1286,5 +1447,6 @@ export {
|
||||
LinkButton,
|
||||
Status,
|
||||
Modal,
|
||||
ResizableSidebar,
|
||||
ShowWaveShellInstallPrompt,
|
||||
};
|
||||
|
@ -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 {
|
||||
#spinner,
|
||||
#indicator {
|
||||
visibility: hidden;
|
||||
}
|
||||
.spin #spinner {
|
||||
visibility: visible;
|
||||
stroke: @term-white;
|
||||
}
|
||||
&.error #indicator {
|
||||
|
@ -2,17 +2,27 @@ import React from "react";
|
||||
import { StatusIndicatorLevel } from "../../../types/types";
|
||||
import cn from "classnames";
|
||||
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 {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
onClick?: React.MouseEventHandler<HTMLDivElement>;
|
||||
divRef?: React.RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export class FrontIcon extends React.Component<PositionalIconProps> {
|
||||
render() {
|
||||
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>
|
||||
);
|
||||
@ -22,7 +32,11 @@ export class FrontIcon extends React.Component<PositionalIconProps> {
|
||||
export class CenteredIcon extends React.Component<PositionalIconProps> {
|
||||
render() {
|
||||
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>
|
||||
);
|
||||
@ -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 {
|
||||
/**
|
||||
* The level of the status indicator. This will determine the color of the status indicator.
|
||||
*/
|
||||
level: StatusIndicatorLevel;
|
||||
className?: string;
|
||||
/**
|
||||
* If true, a spinner will be shown around the status indicator.
|
||||
*/
|
||||
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> {
|
||||
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() {
|
||||
const { level, className, runningCommands } = this.props;
|
||||
const spinnerVisible = this.spinnerVisible.get();
|
||||
let statusIndicator = null;
|
||||
if (level != StatusIndicatorLevel.None || runningCommands) {
|
||||
let levelClass = null;
|
||||
if (level != StatusIndicatorLevel.None || spinnerVisible) {
|
||||
let indicatorLevelClass = null;
|
||||
switch (level) {
|
||||
case StatusIndicatorLevel.Output:
|
||||
levelClass = "output";
|
||||
indicatorLevelClass = "output";
|
||||
break;
|
||||
case StatusIndicatorLevel.Success:
|
||||
levelClass = "success";
|
||||
indicatorLevelClass = "success";
|
||||
break;
|
||||
case StatusIndicatorLevel.Error:
|
||||
levelClass = "error";
|
||||
indicatorLevelClass = "error";
|
||||
break;
|
||||
}
|
||||
|
||||
const spinnerVisibleClass = spinnerVisible ? "spinner-visible" : null;
|
||||
statusIndicator = (
|
||||
<CenteredIcon className={cn(className, levelClass, "status-indicator")}>
|
||||
<SpinnerIndicator className={runningCommands ? "spin" : null} />
|
||||
<CenteredIcon
|
||||
divRef={this.iconRef}
|
||||
className={cn(className, indicatorLevelClass, spinnerVisibleClass, "status-indicator")}
|
||||
>
|
||||
<SpinnerIndicator className={spinnerVisible ? "spin" : null} />
|
||||
</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%;
|
||||
}
|
||||
|
||||
.import-edit-warning {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.name-actions-section {
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
|
@ -58,6 +58,10 @@ class EditRemoteConnModal extends React.Component<{}, {}> {
|
||||
return this.selectedRemote?.local;
|
||||
}
|
||||
|
||||
isImportedRemote(): boolean {
|
||||
return this.selectedRemote?.sshconfigsrc == "sshconfig-import";
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
mobx.action(() => {
|
||||
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>
|
||||
SSH Config Import Behavior
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderAuthMode() {
|
||||
let authMode = this.tempAuthMode.get();
|
||||
return (
|
||||
@ -344,6 +369,7 @@ class EditRemoteConnModal extends React.Component<{}, {}> {
|
||||
return null;
|
||||
}
|
||||
let isLocal = this.isLocalRemote();
|
||||
let isImported = this.isImportedRemote();
|
||||
return (
|
||||
<Modal className="erconn-modal">
|
||||
<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 text-primary">{util.getRemoteName(this.selectedRemote)}</div>
|
||||
</div>
|
||||
<If condition={!isLocal}>{this.renderAlias()}</If>
|
||||
<If condition={!isLocal}>{this.renderAuthMode()}</If>
|
||||
<If condition={!isLocal}>{this.renderConnectMode()}</If>
|
||||
<If condition={!isLocal && !isImported}>{this.renderAlias()}</If>
|
||||
<If condition={!isLocal && !isImported}>{this.renderAuthMode()}</If>
|
||||
<If condition={!isLocal && !isImported}>{this.renderConnectMode()}</If>
|
||||
<If condition={isImported}>{this.renderImportedRemoteEditWarning()}</If>
|
||||
{this.renderShellPref()}
|
||||
<If condition={!util.isBlank(this.remoteEdit?.errorstr)}>
|
||||
<div className="settings-field settings-error">Error: {this.remoteEdit?.errorstr}</div>
|
||||
|
@ -1026,17 +1026,6 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
|
||||
cancelInstallButton = <></>;
|
||||
}
|
||||
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 = (
|
||||
<Button theme="secondary" onClick={() => this.clickArchive()}>
|
||||
Delete
|
||||
|
@ -209,17 +209,6 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
|
||||
cancelInstallButton = <></>;
|
||||
}
|
||||
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 = (
|
||||
<Button theme="secondary" onClick={() => this.clickArchive()}>
|
||||
Delete
|
||||
|
@ -13,8 +13,6 @@ import { GlobalModel, GlobalCommandRunner, Cmd, getTermPtyData } from "../../mod
|
||||
import { termHeightFromRows } from "../../util/textmeasure";
|
||||
import type {
|
||||
LineType,
|
||||
RemoteType,
|
||||
RemotePtrType,
|
||||
RenderModeType,
|
||||
RendererOpts,
|
||||
RendererPluginType,
|
||||
@ -24,11 +22,6 @@ import type {
|
||||
} from "../../types/types";
|
||||
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 { renderCmdText } from "../common/common";
|
||||
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 CommentIcon } from "../assets/icons/line/comment.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 XmarkIcon } from "../assets/icons/line/xmark.svg";
|
||||
import { ReactComponent as FillIcon } from "../assets/icons/line/fill.svg";
|
||||
import { ReactComponent as GearIcon } from "../assets/icons/line/gear.svg";
|
||||
|
||||
import { RotateIcon } from "../common/icons/icons";
|
||||
|
||||
import "./lines.less";
|
||||
|
||||
dayjs.extend(localizedFormat);
|
||||
@ -59,12 +53,12 @@ type OV<V> = mobx.IObservableValue<V>;
|
||||
@mobxReact.observer
|
||||
class SmallLineAvatar extends React.Component<{ line: LineType; cmd: Cmd; onRightClick?: (e: any) => void }, {}> {
|
||||
render() {
|
||||
let { line, cmd } = this.props;
|
||||
let lineNumStr = (line.linenumtemp ? "~" : "#") + String(line.linenum);
|
||||
let status = cmd != null ? cmd.getStatus() : "done";
|
||||
let rtnstate = cmd != null ? cmd.getRtnState() : false;
|
||||
let exitcode = cmd != null ? cmd.getExitCode() : 0;
|
||||
let isComment = line.linetype == "text";
|
||||
const { line, cmd } = this.props;
|
||||
const lineNumStr = (line.linenumtemp ? "~" : "#") + String(line.linenum);
|
||||
const status = cmd != null ? cmd.getStatus() : "done";
|
||||
const rtnstate = cmd != null ? cmd.getRtnState() : false;
|
||||
const exitcode = cmd != null ? cmd.getExitCode() : 0;
|
||||
const isComment = line.linetype == "text";
|
||||
let icon = null;
|
||||
let iconTitle = null;
|
||||
if (isComment) {
|
||||
@ -142,7 +136,7 @@ class LineCmd extends React.Component<
|
||||
}
|
||||
|
||||
checkStateDiffLoad(): void {
|
||||
let { screen, line, staticRender, visible } = this.props;
|
||||
const { screen, line, staticRender, visible } = this.props;
|
||||
if (staticRender) {
|
||||
return;
|
||||
}
|
||||
@ -153,7 +147,7 @@ class LineCmd extends React.Component<
|
||||
}
|
||||
return;
|
||||
}
|
||||
let cmd = screen.getCmd(line);
|
||||
const cmd = screen.getCmd(line);
|
||||
if (cmd == null || !cmd.getRtnState() || this.rtnStateDiffFetched) {
|
||||
return;
|
||||
}
|
||||
@ -167,15 +161,15 @@ class LineCmd extends React.Component<
|
||||
if (this.rtnStateDiffFetched) {
|
||||
return;
|
||||
}
|
||||
let { line } = this.props;
|
||||
const { line } = this.props;
|
||||
this.rtnStateDiffFetched = true;
|
||||
let usp = new URLSearchParams({
|
||||
const usp = new URLSearchParams({
|
||||
linenum: String(line.linenum),
|
||||
screenid: line.screenid,
|
||||
lineid: line.lineid,
|
||||
});
|
||||
let url = GlobalModel.getBaseHostPort() + "/api/rtnstate?" + usp.toString();
|
||||
let fetchHeaders = GlobalModel.getFetchHeaders();
|
||||
const url = GlobalModel.getBaseHostPort() + "/api/rtnstate?" + usp.toString();
|
||||
const fetchHeaders = GlobalModel.getFetchHeaders();
|
||||
fetch(url, { headers: fetchHeaders })
|
||||
.then((resp) => {
|
||||
if (!resp.ok) {
|
||||
@ -233,7 +227,7 @@ class LineCmd extends React.Component<
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
let isMultiLine = lineutil.isMultiLineCmdText(cmd.getCmdStr());
|
||||
const isMultiLine = lineutil.isMultiLineCmdText(cmd.getCmdStr());
|
||||
return (
|
||||
<div key="meta2" className="meta meta-line2" ref={this.cmdTextRef}>
|
||||
<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
|
||||
getSnapshotBeforeUpdate(prevProps, prevState): { height: number } {
|
||||
let elem = this.lineRef.current;
|
||||
const elem = this.lineRef.current;
|
||||
if (elem == null) {
|
||||
return { height: 0 };
|
||||
}
|
||||
@ -266,25 +260,25 @@ class LineCmd extends React.Component<
|
||||
}
|
||||
|
||||
checkCmdText() {
|
||||
let metaElem = this.cmdTextRef.current;
|
||||
const metaElem = this.cmdTextRef.current;
|
||||
if (metaElem == null || metaElem.childNodes.length == 0) {
|
||||
return;
|
||||
}
|
||||
let metaElemWidth = metaElem.offsetWidth;
|
||||
const metaElemWidth = metaElem.offsetWidth;
|
||||
if (metaElemWidth == 0) {
|
||||
return;
|
||||
}
|
||||
let metaChild = metaElem.firstChild;
|
||||
const metaChild = metaElem.firstChild;
|
||||
if (metaChild == null) {
|
||||
return;
|
||||
}
|
||||
let children = metaChild.childNodes;
|
||||
const children = metaChild.childNodes;
|
||||
let childWidth = 0;
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
let ch = children[i];
|
||||
childWidth += ch.offsetWidth;
|
||||
}
|
||||
let isOverflow = childWidth > metaElemWidth;
|
||||
const isOverflow = childWidth > metaElemWidth;
|
||||
if (isOverflow && isOverflow != this.isOverflow.get()) {
|
||||
mobx.action(() => {
|
||||
this.isOverflow.set(isOverflow);
|
||||
@ -297,16 +291,16 @@ class LineCmd extends React.Component<
|
||||
if (this.props.onHeightChange == null) {
|
||||
return;
|
||||
}
|
||||
let { line } = this.props;
|
||||
const { line } = this.props;
|
||||
let curHeight = 0;
|
||||
let elem = this.lineRef.current;
|
||||
const elem = this.lineRef.current;
|
||||
if (elem != null) {
|
||||
curHeight = elem.offsetHeight;
|
||||
}
|
||||
if (this.lastHeight == curHeight) {
|
||||
return;
|
||||
}
|
||||
let lastHeight = this.lastHeight;
|
||||
const lastHeight = this.lastHeight;
|
||||
this.lastHeight = curHeight;
|
||||
this.props.onHeightChange(line.linenum, curHeight, lastHeight);
|
||||
// console.log("line height change: ", line.linenum, lastHeight, "=>", curHeight);
|
||||
@ -314,13 +308,13 @@ class LineCmd extends React.Component<
|
||||
|
||||
@boundMethod
|
||||
handleClick() {
|
||||
let { line, noSelect } = this.props;
|
||||
const { line, noSelect } = this.props;
|
||||
if (noSelect) {
|
||||
return;
|
||||
}
|
||||
let sel = window.getSelection();
|
||||
const sel = window.getSelection();
|
||||
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)) {
|
||||
return;
|
||||
}
|
||||
@ -330,7 +324,7 @@ class LineCmd extends React.Component<
|
||||
|
||||
@boundMethod
|
||||
clickStar() {
|
||||
let { line } = this.props;
|
||||
const { line } = this.props;
|
||||
if (!line.star || line.star == 0) {
|
||||
GlobalCommandRunner.lineStar(line.lineid, 1);
|
||||
} else {
|
||||
@ -340,7 +334,7 @@ class LineCmd extends React.Component<
|
||||
|
||||
@boundMethod
|
||||
clickPin() {
|
||||
let { line } = this.props;
|
||||
const { line } = this.props;
|
||||
if (!line.pinned) {
|
||||
GlobalCommandRunner.linePin(line.lineid, true);
|
||||
} else {
|
||||
@ -350,19 +344,19 @@ class LineCmd extends React.Component<
|
||||
|
||||
@boundMethod
|
||||
clickBookmark() {
|
||||
let { line } = this.props;
|
||||
const { line } = this.props;
|
||||
GlobalCommandRunner.lineBookmark(line.lineid);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
clickDelete() {
|
||||
let { line } = this.props;
|
||||
const { line } = this.props;
|
||||
GlobalCommandRunner.lineDelete(line.lineid, true);
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
clickRestart() {
|
||||
let { line } = this.props;
|
||||
const { line } = this.props;
|
||||
GlobalCommandRunner.lineRestart(line.lineid, true);
|
||||
}
|
||||
|
||||
@ -375,7 +369,7 @@ class LineCmd extends React.Component<
|
||||
|
||||
@boundMethod
|
||||
clickMoveToSidebar() {
|
||||
let { line } = this.props;
|
||||
const { line } = this.props;
|
||||
GlobalCommandRunner.screenSidebarAddLine(line.lineid);
|
||||
}
|
||||
|
||||
@ -390,20 +384,20 @@ class LineCmd extends React.Component<
|
||||
}
|
||||
|
||||
getIsHidePrompt(): boolean {
|
||||
let { line } = this.props;
|
||||
const { line } = this.props;
|
||||
let rendererPlugin: RendererPluginType = null;
|
||||
let isNoneRenderer = line.renderer == "none";
|
||||
const isNoneRenderer = line.renderer == "none";
|
||||
if (!isBlank(line.renderer) && line.renderer != "terminal" && !isNoneRenderer) {
|
||||
rendererPlugin = PluginModel.getRendererPluginByName(line.renderer);
|
||||
}
|
||||
let hidePrompt = rendererPlugin != null && rendererPlugin.hidePrompt;
|
||||
const hidePrompt = rendererPlugin?.hidePrompt;
|
||||
return hidePrompt;
|
||||
}
|
||||
|
||||
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 usedRows = screen.getUsedRows(lineutil.getRendererContext(line), line, cmd, width);
|
||||
const usedRows = screen.getUsedRows(lineutil.getRendererContext(line), line, cmd, width);
|
||||
if (usedRows > 0) {
|
||||
height = 48 + 24 + termHeightFromRows(usedRows, GlobalModel.termFontSize.get());
|
||||
}
|
||||
@ -412,7 +406,7 @@ class LineCmd extends React.Component<
|
||||
|
||||
@boundMethod
|
||||
onAvatarRightClick(e: any): void {
|
||||
let { line, noSelect } = this.props;
|
||||
const { line, noSelect } = this.props;
|
||||
if (noSelect) {
|
||||
return;
|
||||
}
|
||||
@ -426,20 +420,20 @@ class LineCmd extends React.Component<
|
||||
}
|
||||
|
||||
renderSimple() {
|
||||
let { screen, line } = this.props;
|
||||
let cmd = screen.getCmd(line);
|
||||
const { screen, line } = this.props;
|
||||
const cmd = screen.getCmd(line);
|
||||
let height: number = 0;
|
||||
if (isBlank(line.renderer) || line.renderer == "terminal") {
|
||||
height = this.getTerminalRendererHeight(cmd);
|
||||
} else {
|
||||
// header is 16px tall with hide-prompt, 36px otherwise
|
||||
let { screen, line, width } = this.props;
|
||||
let hidePrompt = this.getIsHidePrompt();
|
||||
let usedRows = screen.getUsedRows(lineutil.getRendererContext(line), line, cmd, width);
|
||||
const { screen, line, width } = this.props;
|
||||
const hidePrompt = this.getIsHidePrompt();
|
||||
const usedRows = screen.getUsedRows(lineutil.getRendererContext(line), line, cmd, width);
|
||||
height = (hidePrompt ? 16 + 6 : 36 + 6) + usedRows;
|
||||
}
|
||||
let formattedTime = lineutil.getLineDateTimeStr(line.ts);
|
||||
let mainDivCn = cn("line", "line-cmd", "line-simple");
|
||||
const formattedTime = lineutil.getLineDateTimeStr(line.ts);
|
||||
const mainDivCn = cn("line", "line-cmd", "line-simple");
|
||||
return (
|
||||
<div
|
||||
className={mainDivCn}
|
||||
@ -479,15 +473,16 @@ class LineCmd extends React.Component<
|
||||
if (restartTs != null && restartTs > 0) {
|
||||
formattedTime = "restarted @ " + lineutil.getLineDateTimeStr(restartTs);
|
||||
timeTitle = "original start time " + lineutil.getLineDateTimeStr(line.ts);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
formattedTime = lineutil.getLineDateTimeStr(line.ts);
|
||||
}
|
||||
let renderer = line.renderer;
|
||||
return (
|
||||
<div key="meta1" className="meta meta-line1">
|
||||
<SmallLineAvatar line={line} cmd={cmd} />
|
||||
<div title={timeTitle} className="ts">{formattedTime}</div>
|
||||
<div title={timeTitle} className="ts">
|
||||
{formattedTime}
|
||||
</div>
|
||||
<div> </div>
|
||||
<If condition={!isBlank(renderer) && renderer != "terminal"}>
|
||||
<div className="renderer">
|
||||
@ -506,7 +501,7 @@ class LineCmd extends React.Component<
|
||||
}
|
||||
|
||||
getRendererOpts(cmd: Cmd): RendererOpts {
|
||||
let { screen } = this.props;
|
||||
const { screen } = this.props;
|
||||
return {
|
||||
maxSize: screen.getMaxContentSize(),
|
||||
idealSize: screen.getIdealContentSize(),
|
||||
@ -516,9 +511,9 @@ class LineCmd extends React.Component<
|
||||
}
|
||||
|
||||
makeRendererModelInitializeParams(): RendererModelInitializeParams {
|
||||
let { screen, line } = this.props;
|
||||
let context = lineutil.getRendererContext(line);
|
||||
let cmd = screen.getCmd(line); // won't be null
|
||||
const { screen, line } = this.props;
|
||||
const context = lineutil.getRendererContext(line);
|
||||
const cmd = screen.getCmd(line); // won't be null
|
||||
let savedHeight = screen.getContentHeight(context);
|
||||
if (savedHeight == null) {
|
||||
if (line.contentheight != null && line.contentheight != -1) {
|
||||
@ -527,7 +522,7 @@ class LineCmd extends React.Component<
|
||||
savedHeight = 0;
|
||||
}
|
||||
}
|
||||
let api = {
|
||||
const api = {
|
||||
saveHeight: (height: number) => {
|
||||
screen.setContentHeight(lineutil.getRendererContext(line), height);
|
||||
},
|
||||
@ -581,12 +576,12 @@ class LineCmd extends React.Component<
|
||||
};
|
||||
|
||||
render() {
|
||||
let { screen, line, width, staticRender, visible } = this.props;
|
||||
let isVisible = visible.get();
|
||||
const { screen, line, width, staticRender, visible } = this.props;
|
||||
const isVisible = visible.get();
|
||||
if (staticRender || !isVisible) {
|
||||
return this.renderSimple();
|
||||
}
|
||||
let cmd = screen.getCmd(line);
|
||||
const cmd = screen.getCmd(line);
|
||||
if (cmd == null) {
|
||||
return (
|
||||
<div
|
||||
@ -600,19 +595,17 @@ class LineCmd extends React.Component<
|
||||
</div>
|
||||
);
|
||||
}
|
||||
let status = cmd.getStatus();
|
||||
let lineNumStr = (line.linenumtemp ? "~" : "") + String(line.linenum);
|
||||
let isSelected = mobx
|
||||
const isSelected = mobx
|
||||
.computed(() => screen.getSelectedLine() == line.linenum, {
|
||||
name: "computed-isSelected",
|
||||
})
|
||||
.get();
|
||||
let isPhysicalFocused = mobx
|
||||
const isPhysicalFocused = mobx
|
||||
.computed(() => screen.getIsFocused(line.linenum), {
|
||||
name: "computed-getIsFocused",
|
||||
})
|
||||
.get();
|
||||
let isFocused = mobx
|
||||
const isFocused = mobx
|
||||
.computed(
|
||||
() => {
|
||||
let screenFocusType = screen.getFocusType();
|
||||
@ -621,7 +614,7 @@ class LineCmd extends React.Component<
|
||||
{ name: "computed-isFocused" }
|
||||
)
|
||||
.get();
|
||||
let shouldCmdFocus = mobx
|
||||
const shouldCmdFocus = mobx
|
||||
.computed(
|
||||
() => {
|
||||
let screenFocusType = screen.getFocusType();
|
||||
@ -630,7 +623,7 @@ class LineCmd extends React.Component<
|
||||
{ name: "computed-shouldCmdFocus" }
|
||||
)
|
||||
.get();
|
||||
let isInSidebar = mobx
|
||||
const isInSidebar = mobx
|
||||
.computed(
|
||||
() => {
|
||||
return screen.isSidebarOpen() && screen.isLineIdInSidebar(line.lineid);
|
||||
@ -638,11 +631,10 @@ class LineCmd extends React.Component<
|
||||
{ name: "computed-isInSidebar" }
|
||||
)
|
||||
.get();
|
||||
let isStatic = staticRender;
|
||||
let isRunning = cmd.isRunning();
|
||||
let isExpanded = this.isCmdExpanded.get();
|
||||
let rsdiff = this.rtnStateDiff.get();
|
||||
let mainDivCn = cn(
|
||||
const isRunning = cmd.isRunning();
|
||||
const isExpanded = this.isCmdExpanded.get();
|
||||
const rsdiff = this.rtnStateDiff.get();
|
||||
const mainDivCn = cn(
|
||||
"line",
|
||||
"line-cmd",
|
||||
{ selected: isSelected },
|
||||
@ -651,18 +643,18 @@ class LineCmd extends React.Component<
|
||||
{ "has-rtnstate": cmd.getRtnState() }
|
||||
);
|
||||
let rendererPlugin: RendererPluginType = null;
|
||||
let isNoneRenderer = line.renderer == "none";
|
||||
const isNoneRenderer = line.renderer == "none";
|
||||
if (!isBlank(line.renderer) && line.renderer != "terminal" && !isNoneRenderer) {
|
||||
rendererPlugin = PluginModel.getRendererPluginByName(line.renderer);
|
||||
}
|
||||
let rendererType = lineutil.getRendererType(line);
|
||||
let hidePrompt = rendererPlugin != null && rendererPlugin.hidePrompt;
|
||||
let termFontSize = GlobalModel.termFontSize.get();
|
||||
const rendererType = lineutil.getRendererType(line);
|
||||
const hidePrompt = rendererPlugin?.hidePrompt;
|
||||
const termFontSize = GlobalModel.termFontSize.get();
|
||||
let rtnStateDiffSize = termFontSize - 2;
|
||||
if (rtnStateDiffSize < 10) {
|
||||
rtnStateDiffSize = Math.max(termFontSize, 10);
|
||||
}
|
||||
let containerType = screen.getContainerType();
|
||||
const containerType = screen.getContainerType();
|
||||
return (
|
||||
<div
|
||||
className={mainDivCn}
|
||||
@ -681,7 +673,7 @@ class LineCmd extends React.Component<
|
||||
<If condition={!hidePrompt}>{this.renderCmdText(cmd)}</If>
|
||||
</div>
|
||||
<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 key="delete" title="Delete Line (⌘D)" className="line-icon" onClick={this.clickDelete}>
|
||||
<i className="fa-sharp fa-regular fa-trash" />
|
||||
@ -821,7 +813,7 @@ class Line extends React.Component<
|
||||
{}
|
||||
> {
|
||||
render() {
|
||||
let line = this.props.line;
|
||||
const line = this.props.line;
|
||||
if (line.archived) {
|
||||
return null;
|
||||
}
|
||||
@ -847,7 +839,7 @@ class LineText extends React.Component<
|
||||
> {
|
||||
@boundMethod
|
||||
clickHandler() {
|
||||
let { line, noSelect } = this.props;
|
||||
const { line, noSelect } = this.props;
|
||||
if (noSelect) {
|
||||
return;
|
||||
}
|
||||
@ -856,7 +848,7 @@ class LineText extends React.Component<
|
||||
|
||||
@boundMethod
|
||||
onAvatarRightClick(e: any): void {
|
||||
let { line, noSelect } = this.props;
|
||||
const { line, noSelect } = this.props;
|
||||
if (noSelect) {
|
||||
return;
|
||||
}
|
||||
@ -870,19 +862,19 @@ class LineText extends React.Component<
|
||||
}
|
||||
|
||||
render() {
|
||||
let { screen, line, renderMode } = this.props;
|
||||
let formattedTime = lineutil.getLineDateTimeStr(line.ts);
|
||||
let isSelected = mobx
|
||||
const { screen, line } = this.props;
|
||||
const formattedTime = lineutil.getLineDateTimeStr(line.ts);
|
||||
const isSelected = mobx
|
||||
.computed(() => screen.getSelectedLine() == line.linenum, {
|
||||
name: "computed-isSelected",
|
||||
})
|
||||
.get();
|
||||
let isFocused = mobx
|
||||
const isFocused = mobx
|
||||
.computed(() => screen.getFocusType() == "cmd", {
|
||||
name: "computed-isFocused",
|
||||
})
|
||||
.get();
|
||||
let mainClass = cn("line", "line-text", "focus-parent");
|
||||
const mainClass = cn("line", "line-text", "focus-parent");
|
||||
return (
|
||||
<div
|
||||
className={mainClass}
|
||||
|
@ -27,6 +27,12 @@ let MagicLayout = {
|
||||
ScreenSidebarWidthPadding: 5,
|
||||
ScreenSidebarMinWidth: 200,
|
||||
ScreenSidebarHeaderHeight: 28,
|
||||
|
||||
MainSidebarMinWidth: 75,
|
||||
MainSidebarMaxWidth: 300,
|
||||
MainSidebarSnapThreshold: 90,
|
||||
MainSidebarDragResistance: 50,
|
||||
MainSidebarDefaultWidth: 240,
|
||||
};
|
||||
|
||||
let m = MagicLayout;
|
||||
|
@ -3,14 +3,13 @@
|
||||
|
||||
.main-sidebar {
|
||||
padding: 0;
|
||||
min-width: 20rem;
|
||||
max-width: 20rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
font-size: 12.5px;
|
||||
line-height: 20px;
|
||||
backdrop-filter: blur(4px);
|
||||
z-index: 20;
|
||||
|
||||
.title-bar-drag {
|
||||
-webkit-app-region: drag;
|
||||
@ -24,7 +23,6 @@
|
||||
&.collapsed {
|
||||
width: 6em;
|
||||
min-width: 6em;
|
||||
|
||||
.arrow-container,
|
||||
.collapse-button {
|
||||
transform: rotate(180deg);
|
||||
@ -34,7 +32,7 @@
|
||||
margin-top: 26px;
|
||||
|
||||
.top,
|
||||
.workspaces-item,
|
||||
.workspaces,
|
||||
.middle,
|
||||
.bottom,
|
||||
.separator {
|
||||
@ -50,7 +48,7 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.logo-container img {
|
||||
.logo-container {
|
||||
width: 45px;
|
||||
}
|
||||
|
||||
@ -86,10 +84,14 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
.logo-container {
|
||||
flex-shrink: 0;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100px;
|
||||
}
|
||||
@ -150,7 +152,6 @@
|
||||
margin-left: 6px;
|
||||
border-radius: 4px;
|
||||
opacity: 1;
|
||||
transition: opacity 0.1s ease-in-out, visibility 0.1s step-end;
|
||||
width: inherit;
|
||||
max-width: inherit;
|
||||
min-width: inherit;
|
||||
@ -177,6 +178,7 @@
|
||||
float: right;
|
||||
margin-right: 6px;
|
||||
letter-spacing: 6px;
|
||||
margin-left: auto;
|
||||
}
|
||||
&:hover {
|
||||
:not(.disabled) .hotkey {
|
||||
@ -188,7 +190,7 @@
|
||||
}
|
||||
|
||||
&:not(:hover) .status-indicator {
|
||||
.positional-icon-visible;
|
||||
.status-indicator-visible;
|
||||
}
|
||||
|
||||
&.workspaces {
|
||||
|
@ -7,7 +7,7 @@ import * as mobx from "mobx";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import cn from "classnames";
|
||||
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 { compareLoose } from "semver";
|
||||
|
||||
@ -19,6 +19,7 @@ import { ReactComponent as SettingsIcon } from "../assets/icons/settings.svg";
|
||||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||
import { GlobalModel, GlobalCommandRunner, Session, VERSION } from "../../model/model";
|
||||
import { isBlank, openLink } from "../../util/util";
|
||||
import { ResizableSidebar } from "../common/common";
|
||||
import * as constants from "../appconst";
|
||||
|
||||
import "./sidebar.less";
|
||||
@ -26,8 +27,6 @@ import { ActionsIcon, CenteredIcon, FrontIcon, StatusIndicator } from "../common
|
||||
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
type OV<V> = mobx.IObservableValue<V>;
|
||||
|
||||
class SideBarItem extends React.Component<{
|
||||
frontIcon: React.ReactNode;
|
||||
contents: React.ReactNode | string;
|
||||
@ -59,16 +58,14 @@ class HotKeyIcon extends React.Component<{ hotkey: string }> {
|
||||
}
|
||||
}
|
||||
|
||||
@mobxReact.observer
|
||||
class MainSideBar extends React.Component<{}, {}> {
|
||||
collapsed: mobx.IObservableValue<boolean> = mobx.observable.box(false);
|
||||
interface MainSideBarProps {
|
||||
parentRef: React.RefObject<HTMLElement>;
|
||||
clientData: ClientDataType;
|
||||
}
|
||||
|
||||
@boundMethod
|
||||
toggleCollapsed() {
|
||||
mobx.action(() => {
|
||||
this.collapsed.set(!this.collapsed.get());
|
||||
})();
|
||||
}
|
||||
@mobxReact.observer
|
||||
class MainSideBar extends React.Component<MainSideBarProps, {}> {
|
||||
sidebarRef = React.createRef<HTMLDivElement>();
|
||||
|
||||
handleSessionClick(sessionId: string) {
|
||||
GlobalCommandRunner.switchSession(sessionId);
|
||||
@ -175,9 +172,9 @@ class MainSideBar extends React.Component<{}, {}> {
|
||||
|
||||
getSessions() {
|
||||
if (!GlobalModel.sessionListLoaded.get()) return <div className="item">loading ...</div>;
|
||||
let sessionList = [];
|
||||
let activeSessionId = GlobalModel.activeSessionId.get();
|
||||
for (let session of GlobalModel.sessionList) {
|
||||
const sessionList: Session[] = [];
|
||||
const activeSessionId = GlobalModel.activeSessionId.get();
|
||||
for (const session of GlobalModel.sessionList) {
|
||||
if (!session.archived.get() || session.sessionId == activeSessionId) {
|
||||
sessionList.push(session);
|
||||
}
|
||||
@ -189,6 +186,7 @@ class MainSideBar extends React.Component<{}, {}> {
|
||||
const sessionRunningCommands = sessionScreens.some((screen) => screen.numRunningCmds.get() > 0);
|
||||
return (
|
||||
<SideBarItem
|
||||
key={session.sessionId}
|
||||
className={`${isActive ? "active" : ""}`}
|
||||
frontIcon={<span className="index">{index + 1}</span>}
|
||||
contents={session.name.get()}
|
||||
@ -207,98 +205,116 @@ class MainSideBar extends React.Component<{}, {}> {
|
||||
}
|
||||
|
||||
render() {
|
||||
let isCollapsed = this.collapsed.get();
|
||||
let clientData = GlobalModel.clientData.get();
|
||||
let clientData = this.props.clientData;
|
||||
let needsUpdate = false;
|
||||
if (!clientData?.clientopts.noreleasecheck && !isBlank(clientData?.releaseinfo?.latestversion)) {
|
||||
needsUpdate = compareLoose(VERSION, clientData.releaseinfo.latestversion) < 0;
|
||||
}
|
||||
let mainSidebar = GlobalModel.mainSidebarModel;
|
||||
let isCollapsed = mainSidebar.getCollapsed();
|
||||
return (
|
||||
<div className={cn("main-sidebar", { collapsed: isCollapsed }, { "is-dev": GlobalModel.isDev })}>
|
||||
<div className="title-bar-drag" />
|
||||
<div className="contents">
|
||||
<div className="logo">
|
||||
<If condition={isCollapsed}>
|
||||
<div className="logo-container" onClick={this.toggleCollapsed}>
|
||||
<img src="public/logos/wave-logo.png" />
|
||||
<ResizableSidebar
|
||||
className="main-sidebar"
|
||||
position="left"
|
||||
enableSnap={true}
|
||||
parentRef={this.props.parentRef}
|
||||
>
|
||||
{(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>
|
||||
</If>
|
||||
<If condition={!isCollapsed}>
|
||||
<div className="logo-container">
|
||||
<img src="public/logos/wave-dark.png" />
|
||||
<div className="separator" />
|
||||
<div className="top">
|
||||
<SideBarItem
|
||||
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">⌘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 className="spacer" />
|
||||
<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">⌘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
|
||||
className="workspaces"
|
||||
frontIcon={<WorkspacesIcon className="icon" />}
|
||||
contents="Workspaces"
|
||||
endIcons={[
|
||||
<CenteredIcon
|
||||
key="add-workspace"
|
||||
className="add-workspace hoverEffect"
|
||||
onClick={this.handleNewSession}
|
||||
>
|
||||
<i className="fa-sharp fa-solid fa-plus"></i>
|
||||
</CenteredIcon>,
|
||||
]}
|
||||
/>
|
||||
<div className="middle hideScrollbarUntillHover">{this.getSessions()}</div>
|
||||
<div className="bottom">
|
||||
<If condition={needsUpdate}>
|
||||
<div className="separator" />
|
||||
<SideBarItem
|
||||
className="updateBanner"
|
||||
frontIcon={<i className="fa-sharp fa-regular fa-circle-up icon" />}
|
||||
contents="Update Available"
|
||||
onClick={() => openLink("https://www.waveterm.dev/download?ref=upgrade")}
|
||||
key="workspaces"
|
||||
className="workspaces"
|
||||
frontIcon={<WorkspacesIcon className="icon" />}
|
||||
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>
|
||||
<If condition={GlobalModel.isDev}>
|
||||
<SideBarItem
|
||||
frontIcon={<AppsIcon className="icon" />}
|
||||
contents="Apps"
|
||||
onClick={this.handlePluginsClick}
|
||||
endIcons={[<HotKeyIcon key="hotkey" hotkey="A" />]}
|
||||
/>
|
||||
</If>
|
||||
<SideBarItem
|
||||
frontIcon={<SettingsIcon className="icon" />}
|
||||
contents="Settings"
|
||||
onClick={this.handleSettingsClick}
|
||||
/>
|
||||
<SideBarItem
|
||||
frontIcon={<i className="fa-sharp fa-regular fa-circle-question icon" />}
|
||||
contents="Documentation"
|
||||
onClick={() => openLink("https://docs.waveterm.dev")}
|
||||
/>
|
||||
<SideBarItem
|
||||
frontIcon={<i className="fa-brands fa-discord icon" />}
|
||||
contents="Discord"
|
||||
onClick={() => openLink("https://discord.gg/XfvZ334gwU")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="middle hideScrollbarUntillHover">{this.getSessions()}</div>
|
||||
<div className="bottom">
|
||||
<If condition={needsUpdate}>
|
||||
<SideBarItem
|
||||
key="update-available"
|
||||
className="updateBanner"
|
||||
frontIcon={<i className="fa-sharp fa-regular fa-circle-up icon" />}
|
||||
contents="Update Available"
|
||||
onClick={() => openLink("https://www.waveterm.dev/download?ref=upgrade")}
|
||||
/>
|
||||
</If>
|
||||
<If condition={GlobalModel.isDev}>
|
||||
<SideBarItem
|
||||
key="apps"
|
||||
frontIcon={<AppsIcon className="icon" />}
|
||||
contents="Apps"
|
||||
onClick={this.handlePluginsClick}
|
||||
endIcons={[<HotKeyIcon key="hotkey" hotkey="A" />]}
|
||||
/>
|
||||
</If>
|
||||
<SideBarItem
|
||||
key="settings"
|
||||
frontIcon={<SettingsIcon className="icon" />}
|
||||
contents="Settings"
|
||||
onClick={this.handleSettingsClick}
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -5,28 +5,25 @@ import * as React from "react";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import * as mobx from "mobx";
|
||||
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 dayjs from "dayjs";
|
||||
import type { RemoteType, RemoteInstanceType, RemotePtrType } from "../../../types/types";
|
||||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||
import { GlobalModel, GlobalCommandRunner, Screen, ScreenLines } from "../../../model/model";
|
||||
import { renderCmdText, Button } from "../../common/common";
|
||||
import { GlobalModel, GlobalCommandRunner, Screen } from "../../../model/model";
|
||||
import { renderCmdText } from "../../common/common";
|
||||
import { TextAreaInput } from "./textareainput";
|
||||
import { InfoMsg } from "./infomsg";
|
||||
import { HistoryInfo } from "./historyinfo";
|
||||
import { Prompt } from "../../common/prompt/prompt";
|
||||
import { ReactComponent as ExecIcon } from "../../assets/icons/exec.svg";
|
||||
import { ReactComponent as RotateIcon } from "../../assets/icons/line/rotate.svg";
|
||||
import "./cmdinput.less";
|
||||
import { RotateIcon } from "../../common/icons/icons";
|
||||
import { AIChat } from "./aichat";
|
||||
|
||||
import "./cmdinput.less";
|
||||
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
const TDots = "⋮";
|
||||
|
||||
type OV<V> = mobx.IObservableValue<V>;
|
||||
|
||||
@mobxReact.observer
|
||||
class CmdInput extends React.Component<{}, {}> {
|
||||
cmdInputRef: React.RefObject<any> = React.createRef();
|
||||
@ -37,11 +34,11 @@ class CmdInput extends React.Component<{}, {}> {
|
||||
}
|
||||
|
||||
updateCmdInputHeight() {
|
||||
let elem = this.cmdInputRef.current;
|
||||
const elem = this.cmdInputRef.current;
|
||||
if (elem == null) {
|
||||
return;
|
||||
}
|
||||
let height = elem.offsetHeight;
|
||||
const height = elem.offsetHeight;
|
||||
if (height == GlobalModel.inputModel.cmdInputHeight) {
|
||||
return;
|
||||
}
|
||||
@ -50,7 +47,7 @@ class CmdInput extends React.Component<{}, {}> {
|
||||
})();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState, snapshot: {}): void {
|
||||
componentDidUpdate(): void {
|
||||
this.updateCmdInputHeight();
|
||||
}
|
||||
|
||||
@ -87,7 +84,7 @@ class CmdInput extends React.Component<{}, {}> {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
let inputModel = GlobalModel.inputModel;
|
||||
const inputModel = GlobalModel.inputModel;
|
||||
if (inputModel.historyShow.get()) {
|
||||
inputModel.resetHistory();
|
||||
} else {
|
||||
@ -113,9 +110,9 @@ class CmdInput extends React.Component<{}, {}> {
|
||||
}
|
||||
|
||||
render() {
|
||||
let model = GlobalModel;
|
||||
let inputModel = model.inputModel;
|
||||
let screen = GlobalModel.getActiveScreen();
|
||||
const model = GlobalModel;
|
||||
const inputModel = model.inputModel;
|
||||
const screen = GlobalModel.getActiveScreen();
|
||||
let ri: RemoteInstanceType = null;
|
||||
let rptr: RemotePtrType = null;
|
||||
if (screen != null) {
|
||||
@ -129,15 +126,13 @@ class CmdInput extends React.Component<{}, {}> {
|
||||
feState = ri.festate;
|
||||
}
|
||||
feState = feState || {};
|
||||
let infoShow = inputModel.infoShow.get();
|
||||
let historyShow = !infoShow && inputModel.historyShow.get();
|
||||
let aiChatShow = inputModel.aIChatShow.get();
|
||||
let infoMsg = inputModel.infoMsg.get();
|
||||
let hasInfo = infoMsg != null;
|
||||
let focusVal = inputModel.physicalInputFocused.get();
|
||||
let inputMode: string = inputModel.inputMode.get();
|
||||
let textAreaInputKey = screen == null ? "null" : screen.screenId;
|
||||
let win = GlobalModel.getScreenLinesById(screen.screenId);
|
||||
const infoShow = inputModel.infoShow.get();
|
||||
const historyShow = !infoShow && inputModel.historyShow.get();
|
||||
const aiChatShow = inputModel.aIChatShow.get();
|
||||
const focusVal = inputModel.physicalInputFocused.get();
|
||||
const inputMode: string = inputModel.inputMode.get();
|
||||
const textAreaInputKey = screen == null ? "null" : screen.screenId;
|
||||
const win = GlobalModel.getScreenLinesById(screen.screenId);
|
||||
let numRunningLines = 0;
|
||||
if (win != null) {
|
||||
numRunningLines = mobx.computed(() => win.getRunningCmdLines().length).get();
|
||||
@ -232,11 +227,17 @@ class CmdInput extends React.Component<{}, {}> {
|
||||
{focusVal && (
|
||||
<div className="cmd-btn hoverEffect">
|
||||
<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 condition={!historyShow}>
|
||||
<div className="hint-elem" onMouseDown={this.clickHistoryHint}>history (ctrl-r)</div>
|
||||
<div className="hint-elem" onMouseDown={this.clickAIHint}>AI (ctrl-space)</div>
|
||||
<div className="hint-elem" onMouseDown={this.clickHistoryHint}>
|
||||
history (ctrl-r)
|
||||
</div>
|
||||
<div className="hint-elem" onMouseDown={this.clickAIHint}>
|
||||
AI (ctrl-space)
|
||||
</div>
|
||||
</If>
|
||||
</div>
|
||||
)}
|
||||
|
@ -287,7 +287,7 @@
|
||||
}
|
||||
|
||||
&:not(:hover) .status-indicator {
|
||||
.positional-icon-visible;
|
||||
.status-indicator-visible;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
@ -8,12 +8,12 @@
|
||||
&.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
max-width: calc(100% - 20.5em);
|
||||
background: @background-session;
|
||||
border: 1px solid @base-border;
|
||||
border-radius: 8px;
|
||||
transition: width 0.2s ease;
|
||||
// transition: width 0.2s ease;
|
||||
margin-bottom: 0.5em;
|
||||
margin-right: 0.5em;
|
||||
|
||||
.center-message {
|
||||
display: flex;
|
||||
@ -24,6 +24,3 @@
|
||||
color: @text-secondary;
|
||||
}
|
||||
}
|
||||
.collapsed + .session-view {
|
||||
max-width: calc(100% - 6.7em);
|
||||
}
|
||||
|
@ -27,20 +27,33 @@ class WorkspaceView extends React.Component<{}, {}> {
|
||||
if (session == null) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
let activeScreen = session.getActiveScreen();
|
||||
let cmdInputHeight = model.inputModel.cmdInputHeight.get();
|
||||
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 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 (
|
||||
<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} />
|
||||
<ErrorBoundary>
|
||||
<ScreenView key={"screenview-" + session.sessionId} session={session} screen={activeScreen} />
|
||||
|
@ -7,6 +7,7 @@ import { sprintf } from "sprintf-js";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { boundMethod } from "autobind-decorator";
|
||||
import { debounce } from "throttle-debounce";
|
||||
import * as mobxReact from "mobx-react";
|
||||
import {
|
||||
handleJsonFetchResponse,
|
||||
base64ToString,
|
||||
@ -2619,6 +2620,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 {
|
||||
bookmarks: OArr<BookmarkType> = mobx.observable.array([], {
|
||||
@ -3389,6 +3461,7 @@ class Model {
|
||||
connectionViewModel: ConnectionsViewModel;
|
||||
clientSettingsViewModel: ClientSettingsViewModel;
|
||||
modalsModel: ModalsModel;
|
||||
mainSidebarModel: MainSidebarModel;
|
||||
clientData: OV<ClientDataType> = mobx.observable.box(null, {
|
||||
name: "clientData",
|
||||
});
|
||||
@ -3415,6 +3488,7 @@ class Model {
|
||||
this.remotesModalModel = new RemotesModalModel();
|
||||
this.remotesModel = new RemotesModel();
|
||||
this.modalsModel = new ModalsModel();
|
||||
this.mainSidebarModel = new MainSidebarModel();
|
||||
let isWaveSrvRunning = getApi().getWaveSrvStatus();
|
||||
this.waveSrvRunning = mobx.observable.box(isWaveSrvRunning, {
|
||||
name: "model-wavesrv-running",
|
||||
@ -4102,15 +4176,15 @@ class Model {
|
||||
if ("openaicmdinfochat" in update) {
|
||||
this.inputModel.setOpenAICmdInfoChat(update.openaicmdinfochat);
|
||||
}
|
||||
if ("screenstatusindicator" in update) {
|
||||
this.getScreenById_single(update.screenstatusindicator.screenid)?.setStatusIndicator(
|
||||
update.screenstatusindicator.status
|
||||
);
|
||||
if ("screenstatusindicators" in update) {
|
||||
for (const indicator of update.screenstatusindicators) {
|
||||
this.getScreenById_single(indicator.screenid)?.setStatusIndicator(indicator.status);
|
||||
}
|
||||
}
|
||||
if ("screennumrunningcommands" in update) {
|
||||
this.getScreenById_single(update.screennumrunningcommands.screenid)?.setNumRunningCmds(
|
||||
update.screennumrunningcommands.num
|
||||
);
|
||||
for (const snc of update.screennumrunningcommands) {
|
||||
this.getScreenById_single(snc.screenid)?.setNumRunningCmds(snc.num);
|
||||
}
|
||||
}
|
||||
if ("userinputrequest" in update) {
|
||||
let userInputRequest: UserInputRequest = update.userinputrequest;
|
||||
@ -4990,6 +5064,11 @@ class CommandRunner {
|
||||
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) {
|
||||
let kwargs = {
|
||||
nohist: "1",
|
||||
|
@ -326,8 +326,8 @@ type ModelUpdateType = {
|
||||
remoteview?: RemoteViewType;
|
||||
openaicmdinfochat?: OpenAICmdInfoChatMessageType[];
|
||||
alertmessage?: AlertMessageType;
|
||||
screenstatusindicator?: ScreenStatusIndicatorUpdateType;
|
||||
screennumrunningcommands?: ScreenNumRunningCommandsUpdateType;
|
||||
screenstatusindicators?: ScreenStatusIndicatorUpdateType[];
|
||||
screennumrunningcommands?: ScreenNumRunningCommandsUpdateType[];
|
||||
userinputrequest?: UserInputRequest;
|
||||
};
|
||||
|
||||
@ -525,6 +525,10 @@ type ClientOptsType = {
|
||||
noreleasecheck: boolean;
|
||||
acceptedtos: number;
|
||||
confirmflags: ConfirmFlagsType;
|
||||
mainsidebar: {
|
||||
collapsed: boolean;
|
||||
width: number;
|
||||
};
|
||||
};
|
||||
|
||||
type ReleaseInfoType = {
|
||||
|
@ -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 RemoteSetArgs = []string{"alias", "connectmode", "key", "password", "autoinstall", "color"}
|
||||
var ConfirmFlags = []string{"hideshellprompt"}
|
||||
var SidebarNames = []string{"main"}
|
||||
|
||||
var ScreenCmds = []string{"run", "comment", "cd", "cr", "clear", "sw", "reset", "signal", "chat"}
|
||||
var NoHistCmds = []string{"_compgen", "line", "history", "_killserver"}
|
||||
@ -218,6 +219,7 @@ func init() {
|
||||
registerCmdFn("client:notifyupdatewriter", ClientNotifyUpdateWriterCommand)
|
||||
registerCmdFn("client:accepttos", ClientAcceptTosCommand)
|
||||
registerCmdFn("client:setconfirmflag", ClientConfirmFlagCommand)
|
||||
registerCmdFn("client:setsidebar", ClientSetSidebarCommand)
|
||||
|
||||
registerCmdFn("sidebar:open", SidebarOpenCommand)
|
||||
registerCmdFn("sidebar:close", SidebarCloseCommand)
|
||||
@ -1418,9 +1420,6 @@ func RemoteSetCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (ss
|
||||
if err != nil {
|
||||
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)
|
||||
isSubmitted := resolveBool(pk.Kwargs["submit"], false)
|
||||
editArgs, err := parseRemoteEditArgs(false, pk, ids.Remote.MShell.IsLocal())
|
||||
@ -1540,6 +1539,7 @@ type HostInfoType struct {
|
||||
SshKeyFile string
|
||||
ConnectMode string
|
||||
Ignore bool
|
||||
ShellPref string
|
||||
}
|
||||
|
||||
func createSshImportSummary(changeList map[string][]string) string {
|
||||
@ -1635,6 +1635,13 @@ func NewHostInfo(hostName string) (*HostInfoType, error) {
|
||||
connectMode = sstore.ConnectModeManual
|
||||
}
|
||||
|
||||
shellPref := sstore.ShellTypePref_Detect
|
||||
if cfgWaveOptions["shellpref"] == "bash" {
|
||||
shellPref = "bash"
|
||||
} else if cfgWaveOptions["shellpref"] == "zsh" {
|
||||
shellPref = "zsh"
|
||||
}
|
||||
|
||||
outHostInfo := new(HostInfoType)
|
||||
outHostInfo.Host = hostName
|
||||
outHostInfo.User = userName
|
||||
@ -1643,6 +1650,7 @@ func NewHostInfo(hostName string) (*HostInfoType, error) {
|
||||
outHostInfo.SshKeyFile = sshKeyFile
|
||||
outHostInfo.ConnectMode = connectMode
|
||||
outHostInfo.Ignore = shouldIgnore
|
||||
outHostInfo.ShellPref = shellPref
|
||||
return outHostInfo, nil
|
||||
}
|
||||
|
||||
@ -1707,6 +1715,7 @@ func RemoteConfigParseCommand(ctx context.Context, pk *scpacket.FeCommandPacketT
|
||||
if hostInfo.SshKeyFile != "" {
|
||||
editMap[sstore.RemoteField_SSHKey] = hostInfo.SshKeyFile
|
||||
}
|
||||
editMap[sstore.RemoteField_ShellPref] = hostInfo.ShellPref
|
||||
msh := remote.GetRemoteById(previouslyImportedRemote.RemoteId)
|
||||
if msh == nil {
|
||||
remoteChangeList["updateErr"] = append(remoteChangeList["updateErr"], hostInfo.CanonicalName)
|
||||
@ -1714,7 +1723,7 @@ func RemoteConfigParseCommand(ctx context.Context, pk *scpacket.FeCommandPacketT
|
||||
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
|
||||
continue
|
||||
}
|
||||
@ -1751,6 +1760,7 @@ func RemoteConfigParseCommand(ctx context.Context, pk *scpacket.FeCommandPacketT
|
||||
AutoInstall: true,
|
||||
SSHOpts: sshOpts,
|
||||
SSHConfigSrc: sstore.SSHConfigSrcTypeImport,
|
||||
ShellPref: sstore.ShellTypePref_Detect,
|
||||
}
|
||||
err := remote.AddRemote(ctx, r, false)
|
||||
if err != nil {
|
||||
@ -4463,6 +4473,62 @@ func ClientConfirmFlagCommand(ctx context.Context, pk *scpacket.FeCommandPacketT
|
||||
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 {
|
||||
if len(key) > MaxOpenAIAPITokenLen {
|
||||
return fmt.Errorf("invalid openai token, too long")
|
||||
|
@ -158,6 +158,7 @@ func (ws *WSState) ReplaceShell(shell *wsshell.WSShell) {
|
||||
return
|
||||
}
|
||||
|
||||
// returns all state required to display current UI
|
||||
func (ws *WSState) handleConnection() error {
|
||||
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancelFn()
|
||||
@ -167,6 +168,8 @@ func (ws *WSState) handleConnection() error {
|
||||
}
|
||||
remotes := remote.GetAllRemoteRuntimeState()
|
||||
update.Remotes = remotes
|
||||
// restore status indicators
|
||||
update.ScreenStatusIndicators, update.ScreenNumRunningCommands = sstore.GetCurrentIndicatorState()
|
||||
update.Connect = true
|
||||
err = ws.Shell.WriteJson(update)
|
||||
if err != nil {
|
||||
|
@ -190,6 +190,22 @@ func ScreenMemSetIndicatorLevel(screenId string, level StatusIndicatorLevel) {
|
||||
ScreenMemStore[screenId].StatusIndicator = StatusIndicatorLevel_None
|
||||
}
|
||||
|
||||
func GetCurrentIndicatorState() ([]*ScreenStatusIndicatorType, []*ScreenNumRunningCommandsType) {
|
||||
MemLock.Lock()
|
||||
defer MemLock.Unlock()
|
||||
indicators := []*ScreenStatusIndicatorType{}
|
||||
numRunningCommands := []*ScreenNumRunningCommandsType{}
|
||||
for screenId, screenMem := range ScreenMemStore {
|
||||
if screenMem.StatusIndicator > 0 {
|
||||
indicators = append(indicators, &ScreenStatusIndicatorType{ScreenId: screenId, Status: screenMem.StatusIndicator})
|
||||
}
|
||||
if screenMem.NumRunningCommands > 0 {
|
||||
numRunningCommands = append(numRunningCommands, &ScreenNumRunningCommandsType{ScreenId: screenId, Num: screenMem.NumRunningCommands})
|
||||
}
|
||||
}
|
||||
return indicators, numRunningCommands
|
||||
}
|
||||
|
||||
// safe because we return a copy
|
||||
func GetScreenMemState(screenId string) *ScreenMemState {
|
||||
MemLock.Lock()
|
||||
|
@ -277,11 +277,17 @@ func (tdata *TelemetryData) Scan(val interface{}) error {
|
||||
return quickScanJson(tdata, val)
|
||||
}
|
||||
|
||||
type SidebarValueType struct {
|
||||
Collapsed bool `json:"collapsed"`
|
||||
Width int `json:"width"`
|
||||
}
|
||||
|
||||
type ClientOptsType struct {
|
||||
NoTelemetry bool `json:"notelemetry,omitempty"`
|
||||
NoReleaseCheck bool `json:"noreleasecheck,omitempty"`
|
||||
AcceptedTos int64 `json:"acceptedtos,omitempty"`
|
||||
ConfirmFlags map[string]bool `json:"confirmflags,omitempty"`
|
||||
NoTelemetry bool `json:"notelemetry,omitempty"`
|
||||
NoReleaseCheck bool `json:"noreleasecheck,omitempty"`
|
||||
AcceptedTos int64 `json:"acceptedtos,omitempty"`
|
||||
ConfirmFlags map[string]bool `json:"confirmflags,omitempty"`
|
||||
MainSidebar *SidebarValueType `json:"mainsidebar,omitempty"`
|
||||
}
|
||||
|
||||
type FeOptsType struct {
|
||||
@ -1446,10 +1452,10 @@ func SetStatusIndicatorLevel_Update(ctx context.Context, update *ModelUpdate, sc
|
||||
}
|
||||
}
|
||||
|
||||
update.ScreenStatusIndicator = &ScreenStatusIndicatorType{
|
||||
update.ScreenStatusIndicators = []*ScreenStatusIndicatorType{{
|
||||
ScreenId: screenId,
|
||||
Status: newStatus,
|
||||
}
|
||||
}}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -1479,10 +1485,10 @@ func ResetStatusIndicator(screenId string) error {
|
||||
func IncrementNumRunningCmds_Update(update *ModelUpdate, screenId string, delta int) {
|
||||
newNum := ScreenMemIncrementNumRunningCommands(screenId, delta)
|
||||
log.Printf("IncrementNumRunningCmds_Update: screenId=%s, newNum=%d\n", screenId, newNum)
|
||||
update.ScreenNumRunningCommands = &ScreenNumRunningCommandsType{
|
||||
update.ScreenNumRunningCommands = []*ScreenNumRunningCommandsType{{
|
||||
ScreenId: screenId,
|
||||
Num: newNum,
|
||||
}
|
||||
}}
|
||||
}
|
||||
|
||||
func IncrementNumRunningCmds(screenId string, delta int) {
|
||||
|
@ -66,8 +66,8 @@ type ModelUpdate struct {
|
||||
SessionTombstones []*SessionTombstoneType `json:"sessiontombstones,omitempty"`
|
||||
OpenAICmdInfoChat []*packet.OpenAICmdInfoChatMessage `json:"openaicmdinfochat,omitempty"`
|
||||
AlertMessage *AlertMessageType `json:"alertmessage,omitempty"`
|
||||
ScreenStatusIndicator *ScreenStatusIndicatorType `json:"screenstatusindicator,omitempty"`
|
||||
ScreenNumRunningCommands *ScreenNumRunningCommandsType `json:"screennumrunningcommands,omitempty"`
|
||||
ScreenStatusIndicators []*ScreenStatusIndicatorType `json:"screenstatusindicators,omitempty"`
|
||||
ScreenNumRunningCommands []*ScreenNumRunningCommandsType `json:"screennumrunningcommands,omitempty"`
|
||||
UserInputRequest *UserInputRequestType `json:"userinputrequest,omitempty"`
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user