waveterm/src/main.tsx

581 lines
20 KiB
TypeScript
Raw Normal View History

2022-06-08 02:25:35 +02:00
import * as React from "react";
import * as mobxReact from "mobx-react";
import * as mobx from "mobx";
import {sprintf} from "sprintf-js";
import {boundMethod} from "autobind-decorator";
2022-07-05 07:37:45 +02:00
import dayjs from 'dayjs'
2022-06-08 02:25:35 +02:00
import {If, For, When, Otherwise, Choose} from "tsx-control-statements/components";
import cn from "classnames"
import {TermWrap} from "./term";
2022-07-09 10:37:19 +02:00
import type {SessionDataType, LineType, CmdDataType, RemoteType} from "./types";
import localizedFormat from 'dayjs/plugin/localizedFormat';
2022-07-12 02:55:03 +02:00
import {GlobalModel, Session, Cmd, Window} from "./model";
dayjs.extend(localizedFormat)
2022-06-13 20:12:39 +02:00
2022-07-12 02:55:03 +02:00
function getLineId(line : LineType) : string {
return sprintf("%s-%s-%s", line.sessionid, line.windowid, line.lineid);
}
2022-06-08 02:25:35 +02:00
@mobxReact.observer
class LineMeta extends React.Component<{line : LineType}, {}> {
render() {
let line = this.props.line;
return (
<div className="meta">
<div className="lineid">{line.lineid}</div>
<div className="user">{line.userid}</div>
<div className="ts">{dayjs(line.ts).format("hh:mm:ss a")}</div>
</div>
);
}
}
function getLineDateStr(ts : number) : string {
let lineDate = new Date(ts);
let nowDate = new Date();
if (nowDate.getFullYear() != lineDate.getFullYear()) {
return dayjs(lineDate).format("ddd L LTS");
}
else if (nowDate.getMonth() != lineDate.getMonth() || nowDate.getDate() != lineDate.getDate()) {
let yesterdayDate = (new Date());
yesterdayDate.setDate(yesterdayDate.getDate()-1);
if (yesterdayDate.getMonth() == lineDate.getMonth() && yesterdayDate.getDate() == lineDate.getDate()) {
return "Yesterday " + dayjs(lineDate).format("LTS");;
}
return dayjs(lineDate).format("ddd L LTS");
}
else {
return dayjs(ts).format("LTS");
}
}
2022-06-08 02:25:35 +02:00
@mobxReact.observer
2022-07-11 23:43:18 +02:00
class LineText extends React.Component<{line : LineType}, {}> {
2022-06-08 02:25:35 +02:00
render() {
let line = this.props.line;
let formattedTime = getLineDateStr(line.ts);
2022-06-08 02:25:35 +02:00
return (
<div className="line line-text">
<div className="avatar">
S
2022-06-08 02:25:35 +02:00
</div>
<div className="line-content">
<div className="meta">
<div className="user">{line.userid}</div>
<div className="ts">{formattedTime}</div>
2022-06-08 02:25:35 +02:00
</div>
<div className="text">
{line.text}
</div>
</div>
</div>
);
}
}
@mobxReact.observer
2022-07-11 23:43:18 +02:00
class LineCmd extends React.Component<{line : LineType}, {}> {
2022-06-16 09:31:54 +02:00
constructor(props) {
super(props);
}
2022-06-08 02:25:35 +02:00
componentDidMount() {
2022-07-11 23:43:18 +02:00
let {line} = this.props;
let model = GlobalModel;
2022-07-12 02:55:03 +02:00
let cmd = model.getCmd(line);
if (cmd != null) {
let termElem = document.getElementById("term-" + getLineId(line));
cmd.connectElem(termElem);
2022-07-12 02:55:03 +02:00
}
2022-06-15 01:02:20 +02:00
}
componentWillUnmount() {
let {line} = this.props;
let model = GlobalModel;
let cmd = model.getCmd(line);
if (cmd != null) {
cmd.disconnectElem();
}
}
scrollIntoView() {
let lineElem = document.getElementById("line-" + getLineId(this.props.line));
lineElem.scrollIntoView({block: "end"});
2022-06-08 02:25:35 +02:00
}
2022-06-13 20:12:39 +02:00
@boundMethod
doRefresh() {
2022-07-12 02:55:03 +02:00
let model = GlobalModel;
let cmd = model.getCmd(this.props.line);
if (cmd != null) {
cmd.reloadTerminal(500);
2022-07-12 02:55:03 +02:00
}
2022-06-15 01:02:20 +02:00
}
2022-07-07 22:27:44 +02:00
replaceHomePath(path : string, homeDir : string) : string {
if (path == homeDir) {
return "~";
}
if (path.startsWith(homeDir + "/")) {
return "~" + path.substr(homeDir.length);
}
return path;
}
2022-07-11 23:43:18 +02:00
renderCmdText(cmd : Cmd, remote : RemoteType) : any {
2022-07-07 22:27:44 +02:00
if (cmd == null) {
return (
<div className="metapart-mono cmdtext">
<span className="term-bright-green">(cmd not found)</span>
</div>
);
}
let promptStr = "";
if (remote.remotevars.local) {
promptStr = sprintf("%s@%s", remote.remotevars.remoteuser, "local")
}
else if (remote.remotevars.remotehost) {
promptStr = sprintf("%s@%s", remote.remotevars.remoteuser, remote.remotevars.remotehost)
}
else {
let host = remote.remotevars.host || "unknown";
if (remote.remotevars.user) {
promptStr = sprintf("%s@%s", remote.remotevars.user, host)
}
else {
promptStr = host;
}
}
let cwd = "(unknown)";
2022-07-12 02:55:03 +02:00
let remoteState = cmd.getRemoteState();
if (remoteState && remoteState.cwd) {
cwd = remoteState.cwd;
2022-07-07 22:27:44 +02:00
}
if (remote.remotevars.home) {
cwd = this.replaceHomePath(cwd, remote.remotevars.home)
}
return (
<div className="metapart-mono cmdtext">
2022-07-12 02:55:03 +02:00
<span className="term-bright-green">[{promptStr} {cwd}]</span> {cmd.getSingleLineCmdText()}
2022-07-07 22:27:44 +02:00
</div>
);
}
2022-06-08 02:25:35 +02:00
render() {
2022-07-12 02:55:03 +02:00
let {line} = this.props;
let model = GlobalModel;
2022-06-08 02:25:35 +02:00
let lineid = line.lineid.toString();
let formattedTime = getLineDateStr(line.ts);
2022-07-12 02:55:03 +02:00
let cmd = model.getCmd(line);
if (cmd == null) {
return <div className="line line-invalid">[cmd not found '{line.cmdid}']</div>;
2022-07-07 22:27:44 +02:00
}
2022-07-12 02:55:03 +02:00
let cellHeightPx = 17;
let totalHeight = cellHeightPx * cmd.usedRows.get();
let remote = model.getRemote(cmd.remoteId);
let status = cmd.getStatus();
let running = (status == "running");
let detached = (status == "detached");
let termOpts = cmd.getTermOpts();
2022-06-08 02:25:35 +02:00
return (
<div className="line line-cmd" id={"line-" + getLineId(line)}>
2022-07-08 06:49:15 +02:00
<div className={cn("avatar",{"num4": lineid.length == 4}, {"num5": lineid.length >= 5}, {"running": running}, {"detached": detached})}>
2022-06-08 02:25:35 +02:00
{lineid}
</div>
<div className="line-content">
<div className="meta">
<div className="user" style={{display: "none"}}>{line.userid}</div>
<div className="ts">{formattedTime}</div>
</div>
<div className="meta">
<div className="metapart-mono" style={{display: "none"}}>
{line.cmdid}
2022-07-12 02:55:03 +02:00
({termOpts.rows}x{termOpts.cols})
</div>
2022-07-07 22:27:44 +02:00
{this.renderCmdText(cmd, remote)}
2022-06-08 02:25:35 +02:00
</div>
2022-07-12 02:55:03 +02:00
<div className={cn("terminal-wrapper", {"focus": cmd.isFocused.get()})} style={{overflowY: "hidden"}}>
2022-06-21 02:49:14 +02:00
<div className="terminal" id={"term-" + getLineId(line)} data-cmdid={line.cmdid} style={{height: totalHeight}}></div>
2022-06-08 02:25:35 +02:00
</div>
</div>
2022-06-20 22:03:20 +02:00
<div onClick={this.doRefresh} className="button refresh-button has-background-black is-small">
<span className="icon"><i className="fa fa-refresh"/></span>
</div>
2022-06-08 02:25:35 +02:00
</div>
);
}
}
@mobxReact.observer
class Line extends React.Component<{line : LineType}, {}> {
2022-06-08 02:25:35 +02:00
render() {
let line = this.props.line;
if (line.linetype == "text") {
return <LineText {...this.props}/>;
}
if (line.linetype == "cmd") {
return <LineCmd {...this.props}/>;
}
return <div className="line line-invalid">[invalid line type '{line.linetype}']</div>;
}
}
@mobxReact.observer
2022-07-12 02:55:03 +02:00
class CmdInput extends React.Component<{}, {}> {
2022-06-21 01:06:37 +02:00
historyIndex : mobx.IObservableValue<number> = mobx.observable.box(0, {name: "history-index"});
modHistory : mobx.IObservableArray<string> = mobx.observable.array([""], {name: "mod-history"});
2022-06-08 02:25:35 +02:00
@mobx.action @boundMethod
onKeyDown(e : any) {
mobx.action(() => {
2022-07-11 23:43:18 +02:00
let model = GlobalModel;
2022-07-12 02:55:03 +02:00
let win = model.getActiveWindow();
2022-06-08 02:25:35 +02:00
let ctrlMod = e.getModifierState("Control") || e.getModifierState("Meta") || e.getModifierState("Shift");
if (e.code == "Enter" && !ctrlMod) {
e.preventDefault();
2022-06-13 20:12:39 +02:00
setTimeout(() => this.doSubmitCmd(), 0);
2022-06-08 02:25:35 +02:00
return;
}
2022-06-21 01:06:37 +02:00
if (e.code == "ArrowUp") {
e.preventDefault();
let hidx = this.historyIndex.get();
hidx += 1;
2022-07-11 23:43:18 +02:00
if (hidx > win.getNumHistoryItems()) {
hidx = win.getNumHistoryItems();
2022-06-21 01:06:37 +02:00
}
this.historyIndex.set(hidx);
return;
}
if (e.code == "ArrowDown") {
e.preventDefault();
let hidx = this.historyIndex.get();
hidx -= 1;
if (hidx < 0) {
hidx = 0;
}
this.historyIndex.set(hidx);
return;
}
2022-06-13 20:12:39 +02:00
// console.log(e.code, e.keyCode, e.key, event.which, ctrlMod, e);
2022-06-08 02:25:35 +02:00
})();
}
2022-06-21 01:06:37 +02:00
@boundMethod
clearCurLine() {
mobx.action(() => {
this.historyIndex.set(0);
this.modHistory.clear();
this.modHistory[0] = "";
})();
}
@boundMethod
getCurLine() : string {
2022-07-11 23:43:18 +02:00
let model = GlobalModel;
2022-06-21 01:06:37 +02:00
let hidx = this.historyIndex.get();
if (hidx < this.modHistory.length && this.modHistory[hidx] != null) {
return this.modHistory[hidx];
}
2022-07-12 02:55:03 +02:00
let win = model.getActiveWindow();
if (win == null) {
return "";
}
2022-07-11 23:43:18 +02:00
let hitem = win.getHistoryItem(-hidx);
2022-06-21 01:06:37 +02:00
if (hitem == null) {
return "";
}
return hitem.cmdtext;
}
@boundMethod
setCurLine(val : string) {
let hidx = this.historyIndex.get();
this.modHistory[hidx] = val;
}
2022-06-08 02:25:35 +02:00
@boundMethod
onChange(e : any) {
mobx.action(() => {
2022-06-21 01:06:37 +02:00
this.setCurLine(e.target.value);
2022-06-08 02:25:35 +02:00
})();
}
2022-06-13 20:12:39 +02:00
@boundMethod
doSubmitCmd() {
2022-07-11 23:43:18 +02:00
let model = GlobalModel;
2022-06-21 01:06:37 +02:00
let commandStr = this.getCurLine();
let hitem = {cmdtext: commandStr};
this.clearCurLine();
2022-07-12 02:55:03 +02:00
model.submitCommand(commandStr);
2022-06-13 20:12:39 +02:00
}
2022-06-08 02:25:35 +02:00
render() {
2022-06-21 01:06:37 +02:00
let curLine = this.getCurLine();
2022-06-08 02:25:35 +02:00
return (
<div className="box cmd-input has-background-black">
<div className="cmd-input-context">
<div className="has-text-white">
2022-06-20 22:03:20 +02:00
<span className="bold term-bright-green">[mike@local ~]</span>
2022-06-08 02:25:35 +02:00
</div>
</div>
<div className="cmd-input-field field has-addons">
<div className="control cmd-quick-context">
<div className="button is-static">mike@local</div>
</div>
<div className="control cmd-input-control is-expanded">
2022-07-08 22:45:14 +02:00
<textarea id="main-cmd-input" value={curLine} onKeyDown={this.onKeyDown} onChange={this.onChange} className="input"></textarea>
2022-06-08 02:25:35 +02:00
</div>
<div className="control cmd-exec">
2022-06-13 20:12:39 +02:00
<div onClick={this.doSubmitCmd} className="button">
2022-06-08 02:25:35 +02:00
<span className="icon">
<i className="fa fa-rocket"/>
</span>
</div>
</div>
</div>
</div>
);
}
}
@mobxReact.observer
class WindowView extends React.Component<{windowId : string}, {}> {
mutObs : any;
scrollToBottom() {
let elem = document.getElementById(this.getLinesId());
let oldST = elem.scrollTop;
elem.scrollTop = elem.scrollHeight;
// console.log("scroll-elem", oldST, elem.scrollHeight, elem.scrollTop, elem.scrollLeft, elem);
}
@boundMethod
scrollHandler(event : any) {
let target = event.target;
let atBottom = (target.scrollTop + 30 > (target.scrollHeight - target.offsetHeight));
let win = this.getWindow();
if (win.shouldFollow.get() != atBottom) {
mobx.action(() => win.shouldFollow.set(atBottom));
}
// console.log("scroll-handler>", atBottom, target.scrollTop, target.scrollHeight);
}
componentDidMount() {
let elem = document.getElementById(this.getLinesId());
if (elem == null) {
return;
}
this.mutObs = new MutationObserver(this.handleDomMutation.bind(this));
this.mutObs.observe(elem, {childList: true});
elem.addEventListener("termresize", this.handleTermResize)
}
componentWillUnmount() {
this.mutObs.disconnect();
}
handleDomMutation(mutations, mutObs) {
let win = this.getWindow();
if (win && win.shouldFollow.get()) {
setTimeout(() => this.scrollToBottom(), 0);
}
}
getWindow() : Window {
let {windowId} = this.props;
if (windowId == null) {
return null;
}
let model = GlobalModel;
let session = model.getActiveSession();
if (session == null) {
return null;
}
let win = session.getWindowById(windowId);
return win;
}
getLinesId() {
let {windowId} = this.props;
return "window-lines-" + windowId;
}
@boundMethod
handleTermResize(e : any) {
let win = this.getWindow();
if (win && win.shouldFollow.get()) {
setTimeout(() => this.scrollToBottom(), 0);
}
}
renderError(message : string) {
return (
<div className="window-view">
<div className="lines" onScroll={this.scrollHandler} id={this.getLinesId()}>
{message}
</div>
</div>
);
}
2022-06-08 02:25:35 +02:00
render() {
let win = this.getWindow();
2022-07-11 23:43:18 +02:00
if (win == null) {
return this.renderError("(no window)");
}
2022-07-11 23:43:18 +02:00
if (!win.linesLoaded.get()) {
return this.renderError("(loading)");
}
2022-07-05 07:18:36 +02:00
let idx = 0;
2022-07-05 07:37:45 +02:00
let line : LineType = null;
2022-06-08 02:25:35 +02:00
return (
<div className="window-view">
<div className="lines" onScroll={this.scrollHandler} id={this.getLinesId()}>
2022-07-11 23:43:18 +02:00
<For each="line" of={win.lines} index="idx">
<Line key={line.lineid} line={line}/>
2022-06-08 02:25:35 +02:00
</For>
</div>
</div>
);
}
}
@mobxReact.observer
class SessionView extends React.Component<{}, {}> {
render() {
let model = GlobalModel;
let session = model.getActiveSession();
if (session == null) {
return <div className="session-view">(no active session)</div>;
}
let curWindowId = session.curWindowId.get();
return (
<div className="session-view">
<WindowView windowId={curWindowId}/>
2022-07-12 02:55:03 +02:00
<CmdInput/>
2022-06-08 02:25:35 +02:00
</div>
);
}
}
2022-06-20 22:03:20 +02:00
@mobxReact.observer
class MainSideBar extends React.Component<{}, {}> {
collapsed : mobx.IObservableValue<boolean> = mobx.observable.box(false);
@boundMethod
toggleCollapsed() {
mobx.action(() => {
this.collapsed.set(!this.collapsed.get());
})();
}
handleSessionClick(sessionId : string) {
console.log("click session", sessionId);
}
2022-07-09 10:37:19 +02:00
2022-06-20 22:03:20 +02:00
render() {
2022-07-11 23:43:18 +02:00
let model = GlobalModel;
let curSessionId = model.curSessionId.get();
2022-07-12 02:55:03 +02:00
let session : Session = null;
2022-06-20 22:03:20 +02:00
return (
<div className={cn("main-sidebar", {"collapsed": this.collapsed.get()})}>
<div className="collapse-container">
<div className="arrow-container" onClick={this.toggleCollapsed}>
<If condition={!this.collapsed.get()}><i className="fa fa-arrow-left"/></If>
<If condition={this.collapsed.get()}><i className="fa fa-arrow-right"/></If>
</div>
</div>
<div className="menu">
<p className="menu-label">
Private Sessions
2022-06-20 22:03:20 +02:00
</p>
<ul className="menu-list">
2022-07-12 02:55:03 +02:00
<If condition={!model.sessionListLoaded.get()}>
2022-07-11 23:43:18 +02:00
<li><a>(loading)</a></li>
</If>
2022-07-12 02:55:03 +02:00
<If condition={model.sessionListLoaded.get()}>
2022-07-11 23:43:18 +02:00
<For each="session" of={model.sessionList}>
2022-07-12 02:55:03 +02:00
<li key={session.sessionId}><a className={cn({"is-active": curSessionId == session.sessionId})} onClick={() => this.handleSessionClick(session.sessionId)}>#{session.name.get()}</a></li>
2022-07-11 23:43:18 +02:00
</For>
<li className="new-session"><a className="new-session"><i className="fa fa-plus"/> New Session</a></li>
</If>
2022-06-20 22:03:20 +02:00
</ul>
<p className="menu-label">
Shared Sessions
2022-06-20 22:03:20 +02:00
</p>
<ul className="menu-list">
<li><a>#server-status</a></li>
<li><a className="activity">#bug-3458 <div className="tag is-link">3</div></a></li>
<li><a>#dev-build</a></li>
2022-06-20 22:03:20 +02:00
<li className="new-session"><a className="new-session"><i className="fa fa-plus"/> New Session</a></li>
</ul>
<p className="menu-label">
Direct Messages
</p>
<ul className="menu-list">
<li><a>
<i className="user-status status fa fa-circle"/>
<img className="avatar" src="https://i.pravatar.cc/48?img=4"/>
Mike S <span className="sub-label">you</span>
</a></li>
<li><a>
<i className="user-status status offline fa fa-circle"/>
<img className="avatar" src="https://i.pravatar.cc/48?img=8"/>
Matt P
</a></li>
<li><a>
<i className="user-status status offline fa fa-circle"/>
<img className="avatar" src="https://i.pravatar.cc/48?img=12"/>
Adam B
</a></li>
<li><a className="activity">
<i className="user-status status fa fa-circle"/>
2022-06-21 01:06:37 +02:00
<img className="avatar" src="https://i.pravatar.cc/48?img=5"/>
2022-06-20 22:03:20 +02:00
Michelle T <div className="tag is-link">2</div>
</a></li>
</ul>
<div className="spacer"></div>
<p className="menu-label">
Remotes
</p>
<ul className="menu-list">
<li><a><i className="status fa fa-circle"/>local</a></li>
<li><a><i className="status fa fa-circle"/>local-sudo</a></li>
<li><a><i className="status offline fa fa-circle"/>mike@app01.ec2</a></li>
<li><a><i className="status fa fa-circle"/>mike@test01.ec2</a></li>
<li><a><i className="status offline fa fa-circle"/>root@app01.ec2</a></li>
</ul>
<div className="bottom-spacer"></div>
</div>
</div>
);
}
}
2022-06-16 03:12:22 +02:00
@mobxReact.observer
2022-06-17 00:51:17 +02:00
class Main extends React.Component<{}, {}> {
2022-06-16 03:12:22 +02:00
constructor(props : any) {
super(props);
}
render() {
return (
2022-06-20 22:03:20 +02:00
<div id="main">
2022-06-16 03:12:22 +02:00
<h1 className="title scripthaus-logo-small">
<div className="title-cursor">&#9608;</div>
ScriptHaus
</h1>
2022-06-20 22:03:20 +02:00
<div className="main-content">
<MainSideBar/>
2022-07-11 23:43:18 +02:00
<SessionView/>
2022-06-20 22:03:20 +02:00
</div>
2022-06-16 03:12:22 +02:00
</div>
);
}
}
2022-06-08 02:25:35 +02:00
export {Main};
2022-07-07 22:27:44 +02:00