mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-02 18:39:05 +01:00
Merge branch 'main' into evan/newton
This commit is contained in:
commit
05707de824
57
.github/workflows/build-macos-x64.yml
vendored
Normal file
57
.github/workflows/build-macos-x64.yml
vendored
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
name: "Build MacOS x64"
|
||||||
|
on: workflow_dispatch
|
||||||
|
env:
|
||||||
|
WAVETERM_VERSION: 0.5.3
|
||||||
|
GO_VERSION: '1.21.5'
|
||||||
|
NODE_VERSION: '21.5.0'
|
||||||
|
jobs:
|
||||||
|
runbuild-x64:
|
||||||
|
name: "Build MacOS x64"
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: ${{env.GO_VERSION}}
|
||||||
|
cache-dependency-path: |
|
||||||
|
wavesrv/go.sum
|
||||||
|
waveshell/go.sum
|
||||||
|
- run: brew tap scripthaus-dev/scripthaus
|
||||||
|
- run: brew install scripthaus
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{env.NODE_VERSION}}
|
||||||
|
cache: 'yarn'
|
||||||
|
- run: yarn --frozen-lockfile
|
||||||
|
- run: scripthaus run build-package
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: waveterm-build-darwin-x64
|
||||||
|
path: out/make/zip/darwin/x64/*.zip
|
||||||
|
retention-days: 2
|
||||||
|
runbuild-arm64:
|
||||||
|
name: "Build MacOS arm64"
|
||||||
|
runs-on: macos-latest-xlarge
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: ${{env.GO_VERSION}}
|
||||||
|
cache-dependency-path: |
|
||||||
|
wavesrv/go.sum
|
||||||
|
waveshell/go.sum
|
||||||
|
- run: brew tap scripthaus-dev/scripthaus
|
||||||
|
- run: brew install scripthaus
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{env.NODE_VERSION}}
|
||||||
|
cache: 'yarn'
|
||||||
|
- run: yarn --frozen-lockfile
|
||||||
|
- run: scripthaus run build-package
|
||||||
|
- uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: waveterm-build-darwin-arm64
|
||||||
|
path: out/make/zip/darwin/arm64/*.zip
|
||||||
|
retention-days: 2
|
||||||
|
|
||||||
|
|
@ -7,6 +7,8 @@ github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF
|
|||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
|
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/wavetermdev/ssh_config v0.0.0-20240109090616-36c8da3d7376 h1:tFhJgTu7lgd+hldLfPSzDCoWUpXI8wHKR3rxq5jTLkQ=
|
||||||
|
github.com/wavetermdev/ssh_config v0.0.0-20240109090616-36c8da3d7376/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
23
package.json
23
package.json
@ -13,6 +13,7 @@
|
|||||||
"@tanstack/react-table": "^8.10.3",
|
"@tanstack/react-table": "^8.10.3",
|
||||||
"@types/semver": "^7.5.6",
|
"@types/semver": "^7.5.6",
|
||||||
"autobind-decorator": "^2.4.0",
|
"autobind-decorator": "^2.4.0",
|
||||||
|
"base64-js": "^1.5.1",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"dayjs": "^1.11.3",
|
"dayjs": "^1.11.3",
|
||||||
"dompurify": "^3.0.2",
|
"dompurify": "^3.0.2",
|
||||||
@ -26,7 +27,6 @@
|
|||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
"react": "^18.1.0",
|
"react": "^18.1.0",
|
||||||
"react-dom": "^18.1.0",
|
"react-dom": "^18.1.0",
|
||||||
"react-json-view": "^1.21.3",
|
|
||||||
"react-markdown": "^9.0.0",
|
"react-markdown": "^9.0.0",
|
||||||
"remark": "^15.0.1",
|
"remark": "^15.0.1",
|
||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.0",
|
||||||
@ -35,7 +35,8 @@
|
|||||||
"tsx-control-statements": "^4.1.1",
|
"tsx-control-statements": "^4.1.1",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"winston": "^3.8.2",
|
"winston": "^3.8.2",
|
||||||
"xterm": "^5.0.0"
|
"xterm": "^5.0.0",
|
||||||
|
"xterm-addon-web-links": "^0.9.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.17.10",
|
"@babel/cli": "^7.17.10",
|
||||||
@ -49,17 +50,17 @@
|
|||||||
"@babel/preset-env": "^7.18.2",
|
"@babel/preset-env": "^7.18.2",
|
||||||
"@babel/preset-react": "^7.23.3",
|
"@babel/preset-react": "^7.23.3",
|
||||||
"@babel/preset-typescript": "^7.17.12",
|
"@babel/preset-typescript": "^7.17.12",
|
||||||
"@electron-forge/cli": "^6.0.0-beta.70",
|
"@electron-forge/cli": "^7.2.0",
|
||||||
"@electron-forge/maker-deb": "^6.0.0-beta.70",
|
"@electron-forge/maker-deb": "^7.2.0",
|
||||||
"@electron-forge/maker-rpm": "^6.0.0-beta.70",
|
"@electron-forge/maker-rpm": "^7.2.0",
|
||||||
"@electron-forge/maker-snap": "^6.4.2",
|
"@electron-forge/maker-snap": "^7.2.0",
|
||||||
"@electron-forge/maker-squirrel": "^6.0.0-beta.70",
|
"@electron-forge/maker-squirrel": "^7.2.0",
|
||||||
"@electron-forge/maker-zip": "^6.0.0-beta.70",
|
"@electron-forge/maker-zip": "^7.2.0",
|
||||||
"@electron/rebuild": "^3.4.0",
|
"@electron/rebuild": "^3.4.0",
|
||||||
"@svgr/webpack": "^8.1.0",
|
"@svgr/webpack": "^8.1.0",
|
||||||
"@types/classnames": "^2.3.1",
|
"@types/classnames": "^2.3.1",
|
||||||
"@types/electron": "^1.6.10",
|
"@types/electron": "^1.6.10",
|
||||||
"@types/node": "20.10.3",
|
"@types/node": "20.11.0",
|
||||||
"@types/papaparse": "^5.3.10",
|
"@types/papaparse": "^5.3.10",
|
||||||
"@types/react": "^18.0.12",
|
"@types/react": "^18.0.12",
|
||||||
"@types/sprintf-js": "^1.1.3",
|
"@types/sprintf-js": "^1.1.3",
|
||||||
@ -70,7 +71,7 @@
|
|||||||
"babel-plugin-jsx-control-statements": "^4.1.2",
|
"babel-plugin-jsx-control-statements": "^4.1.2",
|
||||||
"copy-webpack-plugin": "^11.0.0",
|
"copy-webpack-plugin": "^11.0.0",
|
||||||
"css-loader": "^6.7.1",
|
"css-loader": "^6.7.1",
|
||||||
"electron": "27.1.3",
|
"electron": "28.1.3",
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
"http-server": "^14.1.1",
|
"http-server": "^14.1.1",
|
||||||
"less": "^4.1.2",
|
"less": "^4.1.2",
|
||||||
@ -81,7 +82,7 @@
|
|||||||
"raw-loader": "^4.0.2",
|
"raw-loader": "^4.0.2",
|
||||||
"react-split-it": "^2.0.0",
|
"react-split-it": "^2.0.0",
|
||||||
"style-loader": "^3.3.1",
|
"style-loader": "^3.3.1",
|
||||||
"typescript": "^4.7.3",
|
"typescript": "^5.0.0",
|
||||||
"webpack": "^5.73.0",
|
"webpack": "^5.73.0",
|
||||||
"webpack-bundle-analyzer": "^4.10.1",
|
"webpack-bundle-analyzer": "^4.10.1",
|
||||||
"webpack-cli": "^5.1.4",
|
"webpack-cli": "^5.1.4",
|
||||||
|
@ -476,17 +476,26 @@ a.a-block {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon.color-default,
|
||||||
|
.icon.color-green {
|
||||||
|
path,
|
||||||
|
circle {
|
||||||
|
fill: @tab-green;
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: @tab-green;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.icon.color-red {
|
.icon.color-red {
|
||||||
path,
|
path,
|
||||||
circle {
|
circle {
|
||||||
fill: @tab-red;
|
fill: @tab-red;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.icon.color-green {
|
i {
|
||||||
path,
|
color: @tab-red;
|
||||||
circle {
|
|
||||||
fill: @tab-green;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -495,6 +504,10 @@ a.a-block {
|
|||||||
circle {
|
circle {
|
||||||
fill: @tab-orange;
|
fill: @tab-orange;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: @tab-orange;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.color-blue {
|
.icon.color-blue {
|
||||||
@ -502,6 +515,10 @@ a.a-block {
|
|||||||
circle {
|
circle {
|
||||||
fill: @tab-blue;
|
fill: @tab-blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: @tab-blue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.color-yellow {
|
.icon.color-yellow {
|
||||||
@ -509,6 +526,10 @@ a.a-block {
|
|||||||
circle {
|
circle {
|
||||||
fill: @tab-yellow;
|
fill: @tab-yellow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: @tab-yellow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.color-pink {
|
.icon.color-pink {
|
||||||
@ -516,6 +537,10 @@ a.a-block {
|
|||||||
circle {
|
circle {
|
||||||
fill: @tab-pink;
|
fill: @tab-pink;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: @tab-pink;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.color-mint {
|
.icon.color-mint {
|
||||||
@ -523,6 +548,10 @@ a.a-block {
|
|||||||
circle {
|
circle {
|
||||||
fill: @tab-mint;
|
fill: @tab-mint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: @tab-mint;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.color-cyan {
|
.icon.color-cyan {
|
||||||
@ -530,6 +559,10 @@ a.a-block {
|
|||||||
circle {
|
circle {
|
||||||
fill: @tab-cyan;
|
fill: @tab-cyan;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: @tab-cyan;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.color-violet {
|
.icon.color-violet {
|
||||||
@ -537,6 +570,10 @@ a.a-block {
|
|||||||
circle {
|
circle {
|
||||||
fill: @tab-violet;
|
fill: @tab-violet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: @tab-violet;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.color-white {
|
.icon.color-white {
|
||||||
@ -544,6 +581,10 @@ a.a-block {
|
|||||||
circle {
|
circle {
|
||||||
fill: @tab-white;
|
fill: @tab-white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: @tab-white;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-icon.status-connected {
|
.status-icon.status-connected {
|
||||||
|
@ -16,14 +16,9 @@ import { PluginsView } from "./pluginsview/pluginsview";
|
|||||||
import { BookmarksView } from "./bookmarks/bookmarks";
|
import { BookmarksView } from "./bookmarks/bookmarks";
|
||||||
import { HistoryView } from "./history/history";
|
import { HistoryView } from "./history/history";
|
||||||
import { ConnectionsView } from "./connections/connections";
|
import { ConnectionsView } from "./connections/connections";
|
||||||
import {
|
|
||||||
ScreenSettingsModal,
|
|
||||||
SessionSettingsModal,
|
|
||||||
LineSettingsModal,
|
|
||||||
ClientSettingsModal,
|
|
||||||
} from "./common/modals/settings";
|
|
||||||
import { MainSideBar } from "./sidebar/sidebar";
|
import { MainSideBar } from "./sidebar/sidebar";
|
||||||
import { DisconnectedModal, ClientStopModal, ModalsProvider } from "./common/modals/modals";
|
import { DisconnectedModal, ClientStopModal } from "./common/modals";
|
||||||
|
import { ModalsProvider } from "./common/modals/provider";
|
||||||
import { ErrorBoundary } from "./common/error/errorboundary";
|
import { ErrorBoundary } from "./common/error/errorboundary";
|
||||||
import "./app.less";
|
import "./app.less";
|
||||||
|
|
||||||
@ -74,7 +69,6 @@ class App extends React.Component<{}, {}> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let clientSettingsModal = GlobalModel.clientSettingsModal.get();
|
|
||||||
let remotesModel = GlobalModel.remotesModel;
|
let remotesModel = GlobalModel.remotesModel;
|
||||||
let disconnected = !GlobalModel.ws.open.get() || !GlobalModel.waveSrvRunning.get();
|
let disconnected = !GlobalModel.ws.open.get() || !GlobalModel.waveSrvRunning.get();
|
||||||
let hasClientStop = GlobalModel.getHasClientStop();
|
let hasClientStop = GlobalModel.getHasClientStop();
|
||||||
|
@ -7,9 +7,12 @@ export const SCREEN_SETTINGS = "screenSettings";
|
|||||||
export const SESSION_SETTINGS = "sessionSettings";
|
export const SESSION_SETTINGS = "sessionSettings";
|
||||||
export const LINE_SETTINGS = "lineSettings";
|
export const LINE_SETTINGS = "lineSettings";
|
||||||
export const CLIENT_SETTINGS = "clientSettings";
|
export const CLIENT_SETTINGS = "clientSettings";
|
||||||
|
export const TAB_SWITCHER = "tabSwitcher";
|
||||||
|
|
||||||
export const LineContainer_Main = "main";
|
export const LineContainer_Main = "main";
|
||||||
export const LineContainer_History = "history";
|
export const LineContainer_History = "history";
|
||||||
export const LineContainer_Sidebar = "sidebar";
|
export const LineContainer_Sidebar = "sidebar";
|
||||||
|
|
||||||
|
export const ConfirmKey_HideShellPrompt = "hideshellprompt";
|
||||||
|
|
||||||
export const NoStrPos = -1;
|
export const NoStrPos = -1;
|
||||||
|
@ -188,14 +188,14 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: #9e9e9e;
|
color: @term-bright-white;
|
||||||
transition: color 250ms cubic-bezier(0.4, 0, 0.23, 1);
|
transition: color 250ms cubic-bezier(0.4, 0, 0.23, 1);
|
||||||
}
|
}
|
||||||
input[type="checkbox"] + label > span {
|
input[type="checkbox"] + label > span {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-right: 16px;
|
margin-right: 10px;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@ -205,10 +205,6 @@
|
|||||||
transition: all 250ms cubic-bezier(0.4, 0, 0.23, 1);
|
transition: all 250ms cubic-bezier(0.4, 0, 0.23, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="checkbox"] + label:hover,
|
|
||||||
input[type="checkbox"]:focus + label {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
input[type="checkbox"] + label:hover > span,
|
input[type="checkbox"] + label:hover > span,
|
||||||
input[type="checkbox"]:focus + label > span {
|
input[type="checkbox"]:focus + label > span {
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: rgba(255, 255, 255, 0.1);
|
||||||
@ -356,7 +352,7 @@
|
|||||||
background-color: @markdown-highlight;
|
background-color: @markdown-highlight;
|
||||||
color: @term-white;
|
color: @term-white;
|
||||||
font-family: @terminal-font;
|
font-family: @terminal-font;
|
||||||
display: inline-block;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
code.inline {
|
code.inline {
|
||||||
@ -407,6 +403,13 @@
|
|||||||
background-color: @markdown-highlight;
|
background-color: @markdown-highlight;
|
||||||
margin: 4px 10px 4px 10px;
|
margin: 4px 10px 4px 10px;
|
||||||
padding: 6px 6px 6px 10px;
|
padding: 6px 6px 6px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.selected {
|
||||||
|
border-style: solid;
|
||||||
|
outline-width: 2px;
|
||||||
|
border-color: @term-green;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title.is-1 {
|
.title.is-1 {
|
||||||
@ -837,6 +840,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.no-label {
|
||||||
|
height: 34px;
|
||||||
|
|
||||||
|
input {
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.wave-input-decoration {
|
.wave-input-decoration {
|
||||||
|
@ -11,6 +11,7 @@ import cn from "classnames";
|
|||||||
import { If } from "tsx-control-statements/components";
|
import { If } from "tsx-control-statements/components";
|
||||||
import type { RemoteType } from "../../types/types";
|
import type { RemoteType } from "../../types/types";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
|
import { GlobalModel } from "../../model/model";
|
||||||
|
|
||||||
import { ReactComponent as CheckIcon } from "../assets/icons/line/check.svg";
|
import { ReactComponent as CheckIcon } from "../assets/icons/line/check.svg";
|
||||||
import { ReactComponent as CopyIcon } from "../assets/icons/history/copy.svg";
|
import { ReactComponent as CopyIcon } from "../assets/icons/history/copy.svg";
|
||||||
@ -99,23 +100,57 @@ class Toggle extends React.Component<{ checked: boolean; onChange: (value: boole
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Checkbox extends React.Component<
|
class Checkbox extends React.Component<
|
||||||
{ checked: boolean; onChange: (value: boolean) => void; label: React.ReactNode; id: string },
|
{
|
||||||
{}
|
checked?: boolean;
|
||||||
|
defaultChecked?: boolean;
|
||||||
|
onChange: (value: boolean) => void;
|
||||||
|
label: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
id?: string;
|
||||||
|
},
|
||||||
|
{ checkedInternal: boolean }
|
||||||
> {
|
> {
|
||||||
|
generatedId;
|
||||||
|
static idCounter = 0;
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
checkedInternal: this.props.checked !== undefined ? this.props.checked : Boolean(this.props.defaultChecked),
|
||||||
|
};
|
||||||
|
this.generatedId = `checkbox-${Checkbox.idCounter++}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (this.props.checked !== undefined && this.props.checked !== prevProps.checked) {
|
||||||
|
this.setState({ checkedInternal: this.props.checked });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange = (e) => {
|
||||||
|
const newChecked = e.target.checked;
|
||||||
|
if (this.props.checked === undefined) {
|
||||||
|
this.setState({ checkedInternal: newChecked });
|
||||||
|
}
|
||||||
|
this.props.onChange(newChecked);
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { checked, onChange, label, id } = this.props;
|
const { label, className, id } = this.props;
|
||||||
|
const { checkedInternal } = this.state;
|
||||||
|
const checkboxId = id || this.generatedId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="checkbox">
|
<div className={cn("checkbox", className)}>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id={id}
|
id={checkboxId}
|
||||||
checked={checked}
|
checked={checkedInternal}
|
||||||
onChange={(e) => onChange(e.target.checked)}
|
onChange={this.handleChange}
|
||||||
aria-checked={checked}
|
aria-checked={checkedInternal}
|
||||||
role="checkbox"
|
role="checkbox"
|
||||||
/>
|
/>
|
||||||
<label htmlFor={id}>
|
<label htmlFor={checkboxId}>
|
||||||
<span></span>
|
<span></span>
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
@ -231,6 +266,8 @@ interface ButtonProps {
|
|||||||
rightIcon?: React.ReactNode;
|
rightIcon?: React.ReactNode;
|
||||||
color?: string;
|
color?: string;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
|
autoFocus?: boolean;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Button extends React.Component<ButtonProps> {
|
class Button extends React.Component<ButtonProps> {
|
||||||
@ -257,6 +294,7 @@ class Button extends React.Component<ButtonProps> {
|
|||||||
onClick={this.handleClick}
|
onClick={this.handleClick}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
style={style}
|
style={style}
|
||||||
|
autoFocus={this.props.autoFocus}
|
||||||
>
|
>
|
||||||
{leftIcon && <span className="icon-left">{leftIcon}</span>}
|
{leftIcon && <span className="icon-left">{leftIcon}</span>}
|
||||||
{children}
|
{children}
|
||||||
@ -283,25 +321,24 @@ export default IconButton;
|
|||||||
|
|
||||||
interface LinkButtonProps extends ButtonProps {
|
interface LinkButtonProps extends ButtonProps {
|
||||||
href: string;
|
href: string;
|
||||||
|
rel?: string;
|
||||||
target?: string;
|
target?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class LinkButton extends IconButton {
|
class LinkButton extends React.Component<LinkButtonProps> {
|
||||||
render() {
|
render() {
|
||||||
// @ts-ignore
|
const { leftIcon, rightIcon, children, className, ...rest } = this.props;
|
||||||
const { href, target, leftIcon, rightIcon, children, theme, variant }: LinkButtonProps = this.props;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a href={href} target={target} className={`wave-button link-button`}>
|
<a {...rest} className={cn(`wave-button link-button`, className)}>
|
||||||
<button {...this.props} className={`icon-button ${theme} ${variant}`}>
|
{leftIcon && <span className="icon-left">{leftIcon}</span>}
|
||||||
{leftIcon && <span className="icon-left">{leftIcon}</span>}
|
{children}
|
||||||
{children}
|
{rightIcon && <span className="icon-right">{rightIcon}</span>}
|
||||||
{rightIcon && <span className="icon-right">{rightIcon}</span>}
|
|
||||||
</button>
|
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StatusProps {
|
interface StatusProps {
|
||||||
status: "green" | "red" | "gray" | "yellow";
|
status: "green" | "red" | "gray" | "yellow";
|
||||||
text: string;
|
text: string;
|
||||||
@ -332,7 +369,7 @@ interface TextFieldDecorationProps {
|
|||||||
endDecoration?: React.ReactNode;
|
endDecoration?: React.ReactNode;
|
||||||
}
|
}
|
||||||
interface TextFieldProps {
|
interface TextFieldProps {
|
||||||
label: string;
|
label?: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
@ -445,10 +482,11 @@ class TextField extends React.Component<TextFieldProps, TextFieldState> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(`wave-textfield ${className || ""}`, {
|
className={cn("wave-textfield", className, {
|
||||||
focused: focused,
|
focused: focused,
|
||||||
error: error,
|
error: error,
|
||||||
disabled: disabled,
|
disabled: disabled,
|
||||||
|
"no-label": !label,
|
||||||
})}
|
})}
|
||||||
onFocus={this.handleComponentFocus}
|
onFocus={this.handleComponentFocus}
|
||||||
onBlur={this.handleComponentBlur}
|
onBlur={this.handleComponentBlur}
|
||||||
@ -456,15 +494,17 @@ class TextField extends React.Component<TextFieldProps, TextFieldState> {
|
|||||||
>
|
>
|
||||||
{decoration?.startDecoration && <>{decoration.startDecoration}</>}
|
{decoration?.startDecoration && <>{decoration.startDecoration}</>}
|
||||||
<div className="wave-textfield-inner">
|
<div className="wave-textfield-inner">
|
||||||
<label
|
<If condition={label}>
|
||||||
className={cn("wave-textfield-inner-label", {
|
<label
|
||||||
float: this.state.hasContent || this.state.focused || placeholder,
|
className={cn("wave-textfield-inner-label", {
|
||||||
"offset-left": decoration?.startDecoration,
|
float: this.state.hasContent || this.state.focused || placeholder,
|
||||||
})}
|
"offset-left": decoration?.startDecoration,
|
||||||
htmlFor={label}
|
})}
|
||||||
>
|
htmlFor={label}
|
||||||
{label}
|
>
|
||||||
</label>
|
{label}
|
||||||
|
</label>
|
||||||
|
</If>
|
||||||
<input
|
<input
|
||||||
className={cn("wave-textfield-inner-input", { "offset-left": decoration?.startDecoration })}
|
className={cn("wave-textfield-inner-input", { "offset-left": decoration?.startDecoration })}
|
||||||
ref={this.inputRef}
|
ref={this.inputRef}
|
||||||
@ -774,7 +814,7 @@ class InfoMessage extends React.Component<{ width: number; children: React.React
|
|||||||
function LinkRenderer(props: any): any {
|
function LinkRenderer(props: any): any {
|
||||||
let newUrl = "https://extern?" + encodeURIComponent(props.href);
|
let newUrl = "https://extern?" + encodeURIComponent(props.href);
|
||||||
return (
|
return (
|
||||||
<a href={newUrl} target="_blank" rel={"noopener"}>
|
<a href={newUrl} target="_blank" rel={"noopener"}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
@ -789,9 +829,67 @@ function CodeRenderer(props: any): any {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@mobxReact.observer
|
@mobxReact.observer
|
||||||
class Markdown extends React.Component<{ text: string; style?: any; extraClassName?: string }, {}> {
|
class CodeBlockMarkdown extends React.Component<
|
||||||
|
{ children: React.ReactNode; blockText: string; codeSelectSelectedIndex?: number },
|
||||||
|
{}
|
||||||
|
> {
|
||||||
|
blockIndex: number;
|
||||||
|
blockRef: React.RefObject<HTMLPreElement>;
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.blockRef = React.createRef();
|
||||||
|
this.blockIndex = GlobalModel.inputModel.addCodeBlockToCodeSelect(this.blockRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let codeText = this.props.blockText;
|
||||||
|
let clickHandler: (e: React.MouseEvent<HTMLElement>, blockIndex: number) => void;
|
||||||
|
let inputModel = GlobalModel.inputModel;
|
||||||
|
clickHandler = (e: React.MouseEvent<HTMLElement>, blockIndex: number) => {
|
||||||
|
inputModel.setCodeSelectSelectedCodeBlock(blockIndex);
|
||||||
|
};
|
||||||
|
let selected = this.blockIndex == this.props.codeSelectSelectedIndex;
|
||||||
|
return (
|
||||||
|
<pre
|
||||||
|
ref={this.blockRef}
|
||||||
|
className={cn({ selected: selected })}
|
||||||
|
onClick={(event) => clickHandler(event, this.blockIndex)}
|
||||||
|
>
|
||||||
|
{this.props.children}
|
||||||
|
</pre>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mobxReact.observer
|
||||||
|
class Markdown extends React.Component<
|
||||||
|
{ text: string; style?: any; extraClassName?: string; codeSelect?: boolean },
|
||||||
|
{}
|
||||||
|
> {
|
||||||
|
CodeBlockRenderer(props: any, codeSelect: boolean, codeSelectIndex: number): any {
|
||||||
|
let codeText = codeSelect ? props.node.children[0].children[0].value : props.children;
|
||||||
|
if (codeText) {
|
||||||
|
codeText = codeText.replace(/\n$/, ""); // remove trailing newline
|
||||||
|
}
|
||||||
|
if (codeSelect) {
|
||||||
|
return (
|
||||||
|
<CodeBlockMarkdown blockText={codeText} codeSelectSelectedIndex={codeSelectIndex}>
|
||||||
|
{props.children}
|
||||||
|
</CodeBlockMarkdown>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let clickHandler = (e: React.MouseEvent<HTMLElement>) => {
|
||||||
|
navigator.clipboard.writeText(codeText);
|
||||||
|
};
|
||||||
|
return <pre onClick={(event) => clickHandler(event)}>{props.children}</pre>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let text = this.props.text;
|
let text = this.props.text;
|
||||||
|
let codeSelect = this.props.codeSelect;
|
||||||
|
let curCodeSelectIndex = GlobalModel.inputModel.getCodeSelectSelectedIndex();
|
||||||
let markdownComponents = {
|
let markdownComponents = {
|
||||||
a: LinkRenderer,
|
a: LinkRenderer,
|
||||||
h1: (props) => HeaderRenderer(props, 1),
|
h1: (props) => HeaderRenderer(props, 1),
|
||||||
@ -800,7 +898,8 @@ class Markdown extends React.Component<{ text: string; style?: any; extraClassNa
|
|||||||
h4: (props) => HeaderRenderer(props, 4),
|
h4: (props) => HeaderRenderer(props, 4),
|
||||||
h5: (props) => HeaderRenderer(props, 5),
|
h5: (props) => HeaderRenderer(props, 5),
|
||||||
h6: (props) => HeaderRenderer(props, 6),
|
h6: (props) => HeaderRenderer(props, 6),
|
||||||
code: CodeRenderer,
|
code: (props) => CodeRenderer(props),
|
||||||
|
pre: (props) => this.CodeBlockRenderer(props, codeSelect, curCodeSelectIndex),
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className={cn("markdown content", this.props.extraClassName)} style={this.props.style}>
|
<div className={cn("markdown content", this.props.extraClassName)} style={this.props.style}>
|
||||||
@ -1082,16 +1181,18 @@ class Dropdown extends React.Component<DropdownProps, DropdownState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ModalHeaderProps {
|
interface ModalHeaderProps {
|
||||||
onClose: () => void;
|
onClose?: () => void;
|
||||||
title: string;
|
title: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ModalHeader: React.FC<ModalHeaderProps> = ({ onClose, title }) => (
|
const ModalHeader: React.FC<ModalHeaderProps> = ({ onClose, title }) => (
|
||||||
<div className="wave-modal-header">
|
<div className="wave-modal-header">
|
||||||
{<div className="wave-modal-title">{title}</div>}
|
{<div className="wave-modal-title">{title}</div>}
|
||||||
<IconButton theme="secondary" variant="ghost" onClick={onClose}>
|
<If condition={onClose}>
|
||||||
<i className="fa-sharp fa-solid fa-xmark"></i>
|
<IconButton theme="secondary" variant="ghost" onClick={onClose}>
|
||||||
</IconButton>
|
<i className="fa-sharp fa-solid fa-xmark"></i>
|
||||||
|
</IconButton>
|
||||||
|
</If>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1141,7 +1242,7 @@ class Modal extends React.Component<ModalProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return ReactDOM.createPortal(this.renderModal(), document.getElementById("app") );
|
return ReactDOM.createPortal(this.renderModal(), document.getElementById("app"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
114
src/app/common/modals/about.less
Normal file
114
src/app/common/modals/about.less
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
@import "../../../app/common/themes/themes.less";
|
||||||
|
|
||||||
|
.about-modal {
|
||||||
|
.wave-modal-content {
|
||||||
|
gap: 24px;
|
||||||
|
|
||||||
|
.wave-modal-body {
|
||||||
|
margin-bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 24px;
|
||||||
|
|
||||||
|
.about-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
align-self: stretch;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.logo-wrapper {
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
img {
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
align-self: stretch;
|
||||||
|
font-style: normal;
|
||||||
|
line-height: 20px;
|
||||||
|
|
||||||
|
div:first-child {
|
||||||
|
color: @term-bright-white;
|
||||||
|
font-size: 14.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div:last-child {
|
||||||
|
color: @term-white;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div:first-child + div {
|
||||||
|
color: @term-white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.updated {
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: @term-green;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.outdated {
|
||||||
|
div {
|
||||||
|
i {
|
||||||
|
color: @term-yellow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-section:nth-child(3) {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.wave-button-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-section:last-child {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
color: @term-white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
136
src/app/common/modals/about.tsx
Normal file
136
src/app/common/modals/about.tsx
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
// Copyright 2023, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as mobxReact from "mobx-react";
|
||||||
|
import * as mobx from "mobx";
|
||||||
|
import { boundMethod } from "autobind-decorator";
|
||||||
|
import { GlobalModel } from "../../../model/model";
|
||||||
|
import { Modal, LinkButton } from "../common";
|
||||||
|
import * as util from "../../../util/util";
|
||||||
|
|
||||||
|
import logo from "../../assets/waveterm-logo-with-bg.svg";
|
||||||
|
import "./about.less";
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const VERSION = __WAVETERM_VERSION__;
|
||||||
|
// @ts-ignore
|
||||||
|
let BUILD = __WAVETERM_BUILD__;
|
||||||
|
|
||||||
|
@mobxReact.observer
|
||||||
|
class AboutModal extends React.Component<{}, {}> {
|
||||||
|
@boundMethod
|
||||||
|
closeModal(): void {
|
||||||
|
mobx.action(() => {
|
||||||
|
GlobalModel.modalsModel.popModal();
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
isUpToDate(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
updateApp(): void {
|
||||||
|
// GlobalCommandRunner.updateApp();
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
getStatus(isUpToDate: boolean): JSX.Element {
|
||||||
|
// TODO no up-to-date status reporting
|
||||||
|
return (
|
||||||
|
<div className="status updated">
|
||||||
|
<div className="text-selectable">
|
||||||
|
Client Version {VERSION} ({BUILD})
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isUpToDate) {
|
||||||
|
return (
|
||||||
|
<div className="status updated">
|
||||||
|
<div>
|
||||||
|
<i className="fa-sharp fa-solid fa-circle-check" />
|
||||||
|
<span>Up to Date</span>
|
||||||
|
</div>
|
||||||
|
<div className="selectable">
|
||||||
|
Client Version {VERSION} ({BUILD})
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="status outdated">
|
||||||
|
<div>
|
||||||
|
<i className="fa-sharp fa-solid fa-triangle-exclamation" />
|
||||||
|
<span>Outdated Version</span>
|
||||||
|
</div>
|
||||||
|
<div className="selectable">
|
||||||
|
Client Version {VERSION} ({BUILD})
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button onClick={this.updateApp} className="button color-green text-secondary">
|
||||||
|
Update
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Modal className="about-modal">
|
||||||
|
<Modal.Header onClose={this.closeModal} title="About" />
|
||||||
|
<div className="wave-modal-body">
|
||||||
|
<div className="about-section">
|
||||||
|
<div className="logo-wrapper">
|
||||||
|
<img src={logo} alt="logo" />
|
||||||
|
</div>
|
||||||
|
<div className="text-wrapper">
|
||||||
|
<div>Wave Terminal</div>
|
||||||
|
<div className="text-standard">
|
||||||
|
Modern Terminal for
|
||||||
|
<br />
|
||||||
|
Seamless Workflow
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="about-section text-standard">{this.getStatus(this.isUpToDate())}</div>
|
||||||
|
<div className="about-section">
|
||||||
|
<LinkButton
|
||||||
|
className="secondary solid"
|
||||||
|
href={util.makeExternLink("https://github.com/wavetermdev/waveterm")}
|
||||||
|
target="_blank"
|
||||||
|
leftIcon={<i className="fa-brands fa-github"></i>}
|
||||||
|
>
|
||||||
|
Github
|
||||||
|
</LinkButton>
|
||||||
|
<LinkButton
|
||||||
|
className="secondary solid"
|
||||||
|
href={util.makeExternLink("https://www.waveterm.dev/")}
|
||||||
|
target="_blank"
|
||||||
|
leftIcon={<i className="fa-sharp fa-light fa-globe"></i>}
|
||||||
|
>
|
||||||
|
Website
|
||||||
|
</LinkButton>
|
||||||
|
<LinkButton
|
||||||
|
className="secondary solid"
|
||||||
|
href={util.makeExternLink(
|
||||||
|
"https://github.com/wavetermdev/waveterm/blob/main/acknowledgements/README.md"
|
||||||
|
)}
|
||||||
|
target="_blank"
|
||||||
|
rel={"noopener"}
|
||||||
|
leftIcon={<i className="fa-sharp fa-light fa-heart"></i>}
|
||||||
|
>
|
||||||
|
Acknowledgements
|
||||||
|
</LinkButton>
|
||||||
|
</div>
|
||||||
|
<div className="about-section text-standard">© 2023 Command Line Inc.</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { AboutModal };
|
11
src/app/common/modals/alert.less
Normal file
11
src/app/common/modals/alert.less
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
@import "../../../app/common/themes/themes.less";
|
||||||
|
|
||||||
|
.alert-modal {
|
||||||
|
width: 500px;
|
||||||
|
|
||||||
|
.wave-modal-content {
|
||||||
|
.wave-modal-body {
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
75
src/app/common/modals/alert.tsx
Normal file
75
src/app/common/modals/alert.tsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
// Copyright 2023, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as mobxReact from "mobx-react";
|
||||||
|
import { boundMethod } from "autobind-decorator";
|
||||||
|
import { If } from "tsx-control-statements/components";
|
||||||
|
import { Markdown, Modal, Button, Checkbox } from "../common";
|
||||||
|
import { GlobalModel, GlobalCommandRunner } from "../../../model/model";
|
||||||
|
|
||||||
|
import "./alert.less";
|
||||||
|
|
||||||
|
@mobxReact.observer
|
||||||
|
class AlertModal extends React.Component<{}, {}> {
|
||||||
|
@boundMethod
|
||||||
|
closeModal(): void {
|
||||||
|
GlobalModel.cancelAlert();
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleOK(): void {
|
||||||
|
GlobalModel.confirmAlert();
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleDontShowAgain(checked: boolean) {
|
||||||
|
let message = GlobalModel.alertMessage.get();
|
||||||
|
if (message.confirmflag == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
GlobalCommandRunner.clientSetConfirmFlag(message.confirmflag, checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let message = GlobalModel.alertMessage.get();
|
||||||
|
let title = message?.title ?? (message?.confirm ? "Confirm" : "Alert");
|
||||||
|
let isConfirm = message?.confirm ?? false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal className="alert-modal">
|
||||||
|
<Modal.Header onClose={this.closeModal} title={title} />
|
||||||
|
<div className="wave-modal-body">
|
||||||
|
<If condition={message?.markdown}>
|
||||||
|
<Markdown text={message?.message ?? ""} />
|
||||||
|
</If>
|
||||||
|
<If condition={!message?.markdown}>{message?.message}</If>
|
||||||
|
<If condition={message.confirmflag}>
|
||||||
|
<Checkbox
|
||||||
|
onChange={this.handleDontShowAgain}
|
||||||
|
label={"Don't show me this again"}
|
||||||
|
className="dontshowagain-text"
|
||||||
|
/>
|
||||||
|
</If>
|
||||||
|
</div>
|
||||||
|
<div className="wave-modal-footer">
|
||||||
|
<If condition={isConfirm}>
|
||||||
|
<Button theme="secondary" onClick={this.closeModal}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button autoFocus={true} onClick={this.handleOK}>
|
||||||
|
Ok
|
||||||
|
</Button>
|
||||||
|
</If>
|
||||||
|
<If condition={!isConfirm}>
|
||||||
|
<Button autoFocus={true} onClick={this.handleOK}>
|
||||||
|
Ok
|
||||||
|
</Button>
|
||||||
|
</If>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { AlertModal };
|
11
src/app/common/modals/clientstop.less
Normal file
11
src/app/common/modals/clientstop.less
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
@import "../../../app/common/themes/themes.less";
|
||||||
|
|
||||||
|
.clientstop-modal {
|
||||||
|
.inner-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 30px;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
49
src/app/common/modals/clientstop.tsx
Normal file
49
src/app/common/modals/clientstop.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
// Copyright 2023, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as mobxReact from "mobx-react";
|
||||||
|
import { boundMethod } from "autobind-decorator";
|
||||||
|
import { If } from "tsx-control-statements/components";
|
||||||
|
import { GlobalModel } from "../../../model/model";
|
||||||
|
import { Modal, Button } from "../common";
|
||||||
|
|
||||||
|
import "./clientstop.less";
|
||||||
|
|
||||||
|
@mobxReact.observer
|
||||||
|
class ClientStopModal extends React.Component<{}, {}> {
|
||||||
|
@boundMethod
|
||||||
|
refreshClient() {
|
||||||
|
GlobalModel.refreshClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let model = GlobalModel;
|
||||||
|
let cdata = model.clientData.get();
|
||||||
|
return (
|
||||||
|
<Modal className="clientstop-modal">
|
||||||
|
<Modal.Header title="Client Not Ready" />
|
||||||
|
<div className="wave-modal-body">
|
||||||
|
<div className="modal-content">
|
||||||
|
<div className="inner-content">
|
||||||
|
<If condition={cdata == null}>
|
||||||
|
<div>Cannot get client data.</div>
|
||||||
|
</If>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
theme="secondary"
|
||||||
|
onClick={this.refreshClient}
|
||||||
|
leftIcon={<i className="fa-sharp fa-solid fa-rotate"></i>}
|
||||||
|
>
|
||||||
|
Hard Refresh Client
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ClientStopModal };
|
24
src/app/common/modals/createremoteconn.less
Normal file
24
src/app/common/modals/createremoteconn.less
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
@import "../../../app/common/themes/themes.less";
|
||||||
|
|
||||||
|
.crconn-modal {
|
||||||
|
width: 452px;
|
||||||
|
min-height: 411px;
|
||||||
|
|
||||||
|
.wave-modal-content {
|
||||||
|
gap: 24px;
|
||||||
|
|
||||||
|
.wave-modal-body {
|
||||||
|
display: flex;
|
||||||
|
padding: 0px 20px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
align-self: stretch;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
370
src/app/common/modals/createremoteconn.tsx
Normal file
370
src/app/common/modals/createremoteconn.tsx
Normal file
@ -0,0 +1,370 @@
|
|||||||
|
// Copyright 2023, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as mobxReact from "mobx-react";
|
||||||
|
import * as mobx from "mobx";
|
||||||
|
import { boundMethod } from "autobind-decorator";
|
||||||
|
import { If } from "tsx-control-statements/components";
|
||||||
|
import { GlobalModel, GlobalCommandRunner, RemotesModel } from "../../../model/model";
|
||||||
|
import * as T from "../../../types/types";
|
||||||
|
import { Modal, TextField, NumberField, InputDecoration, Dropdown, PasswordField, Tooltip } from "../common";
|
||||||
|
import * as util from "../../../util/util";
|
||||||
|
import * as appconst from "../../appconst";
|
||||||
|
|
||||||
|
import "./createremoteconn.less";
|
||||||
|
|
||||||
|
type OV<V> = mobx.IObservableValue<V>;
|
||||||
|
|
||||||
|
@mobxReact.observer
|
||||||
|
class CreateRemoteConnModal extends React.Component<{}, {}> {
|
||||||
|
tempAlias: OV<string>;
|
||||||
|
tempHostName: OV<string>;
|
||||||
|
tempPort: OV<string>;
|
||||||
|
tempAuthMode: OV<string>;
|
||||||
|
tempConnectMode: OV<string>;
|
||||||
|
tempPassword: OV<string>;
|
||||||
|
tempKeyFile: OV<string>;
|
||||||
|
errorStr: OV<string>;
|
||||||
|
remoteEdit: T.RemoteEditType;
|
||||||
|
model: RemotesModel;
|
||||||
|
|
||||||
|
constructor(props: { remotesModel?: RemotesModel }) {
|
||||||
|
super(props);
|
||||||
|
this.model = GlobalModel.remotesModel;
|
||||||
|
this.remoteEdit = this.model.remoteEdit.get();
|
||||||
|
this.tempAlias = mobx.observable.box("", { name: "CreateRemote-alias" });
|
||||||
|
this.tempHostName = mobx.observable.box("", { name: "CreateRemote-hostName" });
|
||||||
|
this.tempPort = mobx.observable.box("", { name: "CreateRemote-port" });
|
||||||
|
this.tempAuthMode = mobx.observable.box("none", { name: "CreateRemote-authMode" });
|
||||||
|
this.tempConnectMode = mobx.observable.box("auto", { name: "CreateRemote-connectMode" });
|
||||||
|
this.tempKeyFile = mobx.observable.box("", { name: "CreateRemote-keystr" });
|
||||||
|
this.tempPassword = mobx.observable.box("", { name: "CreateRemote-password" });
|
||||||
|
this.errorStr = mobx.observable.box(this.remoteEdit?.errorstr ?? null, { name: "CreateRemote-errorStr" });
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount(): void {
|
||||||
|
GlobalModel.getClientData();
|
||||||
|
}
|
||||||
|
|
||||||
|
remoteCName(): string {
|
||||||
|
let hostName = this.tempHostName.get();
|
||||||
|
if (hostName == "") {
|
||||||
|
return "[no host]";
|
||||||
|
}
|
||||||
|
if (hostName.indexOf("@") == -1) {
|
||||||
|
hostName = "[no user]@" + hostName;
|
||||||
|
}
|
||||||
|
return hostName;
|
||||||
|
}
|
||||||
|
|
||||||
|
getErrorStr(): string {
|
||||||
|
if (this.errorStr.get() != null) {
|
||||||
|
return this.errorStr.get();
|
||||||
|
}
|
||||||
|
return this.remoteEdit?.errorstr ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleOk(): void {
|
||||||
|
this.showShellPrompt(this.submitRemote);
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
showShellPrompt(cb: () => void): void {
|
||||||
|
let prtn = GlobalModel.showAlert({
|
||||||
|
message:
|
||||||
|
"You are about to install WaveShell on a remote machine. Please be aware that WaveShell will be executed on the remote system.",
|
||||||
|
confirm: true,
|
||||||
|
confirmflag: appconst.ConfirmKey_HideShellPrompt,
|
||||||
|
});
|
||||||
|
prtn.then((confirm) => {
|
||||||
|
if (!confirm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
submitRemote(): void {
|
||||||
|
mobx.action(() => {
|
||||||
|
this.errorStr.set(null);
|
||||||
|
})();
|
||||||
|
let authMode = this.tempAuthMode.get();
|
||||||
|
let cname = this.tempHostName.get();
|
||||||
|
if (cname == "") {
|
||||||
|
this.errorStr.set("You must specify a 'user@host' value to create a new connection");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let kwargs: Record<string, string> = {};
|
||||||
|
kwargs["alias"] = this.tempAlias.get();
|
||||||
|
if (this.tempPort.get() != "" && this.tempPort.get() != "22") {
|
||||||
|
kwargs["port"] = this.tempPort.get();
|
||||||
|
}
|
||||||
|
if (authMode == "key" || authMode == "key+password") {
|
||||||
|
if (this.tempKeyFile.get() == "") {
|
||||||
|
this.errorStr.set("When AuthMode is set to 'key', you must supply a valid key file name.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
kwargs["key"] = this.tempKeyFile.get();
|
||||||
|
} else {
|
||||||
|
kwargs["key"] = "";
|
||||||
|
}
|
||||||
|
if (authMode == "password" || authMode == "key+password") {
|
||||||
|
if (this.tempPassword.get() == "") {
|
||||||
|
this.errorStr.set("When AuthMode is set to 'password', you must supply a password.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
kwargs["password"] = this.tempPassword.get();
|
||||||
|
} else {
|
||||||
|
kwargs["password"] = "";
|
||||||
|
}
|
||||||
|
kwargs["connectmode"] = this.tempConnectMode.get();
|
||||||
|
kwargs["visual"] = "1";
|
||||||
|
kwargs["submit"] = "1";
|
||||||
|
let prtn = GlobalCommandRunner.createRemote(cname, kwargs, false);
|
||||||
|
prtn.then((crtn) => {
|
||||||
|
if (crtn.success) {
|
||||||
|
this.model.setRecentConnAdded(true);
|
||||||
|
this.model.closeModal();
|
||||||
|
|
||||||
|
let crRtn = GlobalCommandRunner.screenSetRemote(cname, true, false);
|
||||||
|
crRtn.then((crcrtn) => {
|
||||||
|
if (crcrtn.success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mobx.action(() => {
|
||||||
|
this.errorStr.set(crcrtn.error);
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mobx.action(() => {
|
||||||
|
this.errorStr.set(crtn.error);
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleChangeKeyFile(value: string): void {
|
||||||
|
mobx.action(() => {
|
||||||
|
this.tempKeyFile.set(value);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleChangePassword(value: string): void {
|
||||||
|
mobx.action(() => {
|
||||||
|
this.tempPassword.set(value);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleChangeAlias(value: string): void {
|
||||||
|
mobx.action(() => {
|
||||||
|
this.tempAlias.set(value);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleChangeAuthMode(value: string): void {
|
||||||
|
mobx.action(() => {
|
||||||
|
this.tempAuthMode.set(value);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleChangePort(value: string): void {
|
||||||
|
mobx.action(() => {
|
||||||
|
this.tempPort.set(value);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleChangeHostName(value: string): void {
|
||||||
|
mobx.action(() => {
|
||||||
|
this.tempHostName.set(value);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleChangeConnectMode(value: string): void {
|
||||||
|
mobx.action(() => {
|
||||||
|
this.tempConnectMode.set(value);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let authMode = this.tempAuthMode.get();
|
||||||
|
|
||||||
|
if (this.remoteEdit == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal className="crconn-modal">
|
||||||
|
<Modal.Header title="Add Connection" onClose={this.model.closeModal} />
|
||||||
|
<div className="wave-modal-body">
|
||||||
|
<div className="user-section">
|
||||||
|
<TextField
|
||||||
|
label="user@host"
|
||||||
|
autoFocus={true}
|
||||||
|
value={this.tempHostName.get()}
|
||||||
|
onChange={this.handleChangeHostName}
|
||||||
|
required={true}
|
||||||
|
decoration={{
|
||||||
|
endDecoration: (
|
||||||
|
<InputDecoration>
|
||||||
|
<Tooltip
|
||||||
|
message={`(Required) The user and host that you want to connect with. This is in the same format as
|
||||||
|
you would pass to ssh, e.g. "ubuntu@test.mydomain.com".`}
|
||||||
|
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||||
|
>
|
||||||
|
<i className="fa-sharp fa-regular fa-circle-question" />
|
||||||
|
</Tooltip>
|
||||||
|
</InputDecoration>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="alias-section">
|
||||||
|
<TextField
|
||||||
|
label="Alias"
|
||||||
|
onChange={this.handleChangeAlias}
|
||||||
|
value={this.tempAlias.get()}
|
||||||
|
maxLength={100}
|
||||||
|
decoration={{
|
||||||
|
endDecoration: (
|
||||||
|
<InputDecoration>
|
||||||
|
<Tooltip
|
||||||
|
message={`(Optional) A short alias to use when selecting or displaying this connection.`}
|
||||||
|
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||||
|
>
|
||||||
|
<i className="fa-sharp fa-regular fa-circle-question" />
|
||||||
|
</Tooltip>
|
||||||
|
</InputDecoration>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="port-section">
|
||||||
|
<NumberField
|
||||||
|
label="Port"
|
||||||
|
placeholder="22"
|
||||||
|
value={this.tempPort.get()}
|
||||||
|
onChange={this.handleChangePort}
|
||||||
|
decoration={{
|
||||||
|
endDecoration: (
|
||||||
|
<InputDecoration>
|
||||||
|
<Tooltip
|
||||||
|
message={`(Optional) Defaults to 22. Set if the server you are connecting to listens to a non-standard
|
||||||
|
SSH port.`}
|
||||||
|
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||||
|
>
|
||||||
|
<i className="fa-sharp fa-regular fa-circle-question" />
|
||||||
|
</Tooltip>
|
||||||
|
</InputDecoration>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="authmode-section">
|
||||||
|
<Dropdown
|
||||||
|
label="Auth Mode"
|
||||||
|
options={[
|
||||||
|
{ value: "none", label: "none" },
|
||||||
|
{ value: "key", label: "key" },
|
||||||
|
{ value: "password", label: "password" },
|
||||||
|
{ value: "key+password", label: "key+password" },
|
||||||
|
]}
|
||||||
|
value={this.tempAuthMode.get()}
|
||||||
|
onChange={(val: string) => {
|
||||||
|
this.tempAuthMode.set(val);
|
||||||
|
}}
|
||||||
|
decoration={{
|
||||||
|
endDecoration: (
|
||||||
|
<InputDecoration>
|
||||||
|
<Tooltip
|
||||||
|
message={
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>none</b> - no authentication, or authentication is already
|
||||||
|
configured in your ssh config.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>key</b> - use a private key.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>password</b> - use a password.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>key+password</b> - use a key with a passphrase.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||||
|
>
|
||||||
|
<i className="fa-sharp fa-regular fa-circle-question" />
|
||||||
|
</Tooltip>
|
||||||
|
</InputDecoration>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<If condition={authMode == "key" || authMode == "key+password"}>
|
||||||
|
<TextField
|
||||||
|
label="SSH Keyfile"
|
||||||
|
placeholder="keyfile path"
|
||||||
|
onChange={this.handleChangeKeyFile}
|
||||||
|
value={this.tempKeyFile.get()}
|
||||||
|
maxLength={400}
|
||||||
|
required={true}
|
||||||
|
decoration={{
|
||||||
|
endDecoration: (
|
||||||
|
<InputDecoration>
|
||||||
|
<Tooltip
|
||||||
|
message={`(Required) The path to your ssh key file.`}
|
||||||
|
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||||
|
>
|
||||||
|
<i className="fa-sharp fa-regular fa-circle-question" />
|
||||||
|
</Tooltip>
|
||||||
|
</InputDecoration>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</If>
|
||||||
|
<If condition={authMode == "password" || authMode == "key+password"}>
|
||||||
|
<PasswordField
|
||||||
|
label={authMode == "password" ? "SSH Password" : "Key Passphrase"}
|
||||||
|
placeholder="password"
|
||||||
|
onChange={this.handleChangePassword}
|
||||||
|
value={this.tempPassword.get()}
|
||||||
|
maxLength={400}
|
||||||
|
/>
|
||||||
|
</If>
|
||||||
|
<div className="connectmode-section">
|
||||||
|
<Dropdown
|
||||||
|
label="Connect Mode"
|
||||||
|
options={[
|
||||||
|
{ value: "startup", label: "startup" },
|
||||||
|
{ value: "auto", label: "auto" },
|
||||||
|
{ value: "manual", label: "manual" },
|
||||||
|
]}
|
||||||
|
value={this.tempConnectMode.get()}
|
||||||
|
onChange={(val: string) => {
|
||||||
|
this.tempConnectMode.set(val);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<If condition={!util.isBlank(this.getErrorStr() as string)}>
|
||||||
|
<div className="settings-field settings-error">Error: {this.getErrorStr()}</div>
|
||||||
|
</If>
|
||||||
|
</div>
|
||||||
|
<Modal.Footer onCancel={this.model.closeModal} onOk={this.handleOk} okLabel="Connect" />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { CreateRemoteConnModal };
|
47
src/app/common/modals/disconnected.less
Normal file
47
src/app/common/modals/disconnected.less
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
@import "../../../app/common/themes/themes.less";
|
||||||
|
|
||||||
|
.disconnected-modal {
|
||||||
|
.wave-modal-content {
|
||||||
|
.wave-modal-body {
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
footer {
|
||||||
|
.footer-text-link {
|
||||||
|
color: @term-white;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner-content {
|
||||||
|
.log {
|
||||||
|
height: 335px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track,
|
||||||
|
&::-webkit-scrollbar-thumb,
|
||||||
|
&::-webkit-scrollbar-corner {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover::-webkit-scrollbar-thumb {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
color: @term-white;
|
||||||
|
background-color: @term-black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wave-modal-footer {
|
||||||
|
button:first-child {
|
||||||
|
color: @term-green;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
101
src/app/common/modals/disconnected.tsx
Normal file
101
src/app/common/modals/disconnected.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
// Copyright 2023, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as mobxReact from "mobx-react";
|
||||||
|
import * as mobx from "mobx";
|
||||||
|
import { boundMethod } from "autobind-decorator";
|
||||||
|
import { GlobalModel } from "../../../model/model";
|
||||||
|
import { Modal, Button } from "../common";
|
||||||
|
|
||||||
|
import "./disconnected.less";
|
||||||
|
|
||||||
|
const NumOfLines = 50;
|
||||||
|
|
||||||
|
@mobxReact.observer
|
||||||
|
class DisconnectedModal extends React.Component<{}, {}> {
|
||||||
|
logRef: any = React.createRef();
|
||||||
|
logs: mobx.IObservableValue<string> = mobx.observable.box("");
|
||||||
|
logInterval: NodeJS.Timeout = null;
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
restartServer() {
|
||||||
|
GlobalModel.restartWaveSrv();
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
tryReconnect() {
|
||||||
|
GlobalModel.ws.connectNow("manual");
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.fetchLogs();
|
||||||
|
|
||||||
|
this.logInterval = setInterval(() => {
|
||||||
|
this.fetchLogs();
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (this.logInterval) {
|
||||||
|
clearInterval(this.logInterval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
if (this.logRef.current != null) {
|
||||||
|
this.logRef.current.scrollTop = this.logRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchLogs() {
|
||||||
|
GlobalModel.getLastLogs(
|
||||||
|
NumOfLines,
|
||||||
|
mobx.action((logs) => {
|
||||||
|
this.logs.set(logs);
|
||||||
|
if (this.logRef.current != null) {
|
||||||
|
this.logRef.current.scrollTop = this.logRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Modal className="disconnected-modal">
|
||||||
|
<Modal.Header title="Wave Client Disconnected" />
|
||||||
|
<div className="wave-modal-body">
|
||||||
|
<div className="modal-content">
|
||||||
|
<div className="inner-content">
|
||||||
|
<div className="log" ref={this.logRef}>
|
||||||
|
<pre>{this.logs.get()}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="wave-modal-footer">
|
||||||
|
<Button
|
||||||
|
theme="secondary"
|
||||||
|
onClick={this.tryReconnect}
|
||||||
|
leftIcon={
|
||||||
|
<span className="icon">
|
||||||
|
<i className="fa-sharp fa-solid fa-rotate" />
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Try Reconnect
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
theme="secondary"
|
||||||
|
onClick={this.restartServer}
|
||||||
|
leftIcon={<i className="fa-sharp fa-solid fa-triangle-exclamation"></i>}
|
||||||
|
>
|
||||||
|
Restart Server
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { DisconnectedModal };
|
51
src/app/common/modals/editremoteconn.less
Normal file
51
src/app/common/modals/editremoteconn.less
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
@import "../../../app/common/themes/themes.less";
|
||||||
|
|
||||||
|
.erconn-modal {
|
||||||
|
width: 502px;
|
||||||
|
min-height: 411px;
|
||||||
|
|
||||||
|
.wave-modal-content {
|
||||||
|
gap: 20px;
|
||||||
|
|
||||||
|
.wave-modal-body {
|
||||||
|
display: flex;
|
||||||
|
padding: 0px 20px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
align-self: stretch;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-actions-section {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.name {
|
||||||
|
color: @term-bright-white;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
.wave-button {
|
||||||
|
padding: 4px 15px;
|
||||||
|
font-size: 11px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
312
src/app/common/modals/editremoteconn.tsx
Normal file
312
src/app/common/modals/editremoteconn.tsx
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
// Copyright 2023, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as mobxReact from "mobx-react";
|
||||||
|
import * as mobx from "mobx";
|
||||||
|
import { If } from "tsx-control-statements/components";
|
||||||
|
import { boundMethod } from "autobind-decorator";
|
||||||
|
import { GlobalModel, GlobalCommandRunner, RemotesModel } from "../../../model/model";
|
||||||
|
import * as T from "../../../types/types";
|
||||||
|
import { Modal, TextField, InputDecoration, Dropdown, PasswordField, Tooltip } from "../common";
|
||||||
|
import * as util from "../../../util/util";
|
||||||
|
|
||||||
|
import "./editremoteconn.less";
|
||||||
|
|
||||||
|
type OV<V> = mobx.IObservableValue<V>;
|
||||||
|
|
||||||
|
const PasswordUnchangedSentinel = "--unchanged--";
|
||||||
|
|
||||||
|
@mobxReact.observer
|
||||||
|
class EditRemoteConnModal extends React.Component<{}, {}> {
|
||||||
|
tempAlias: OV<string>;
|
||||||
|
tempKeyFile: OV<string>;
|
||||||
|
tempPassword: OV<string>;
|
||||||
|
tempConnectMode: OV<string>;
|
||||||
|
tempAuthMode: OV<string>;
|
||||||
|
model: RemotesModel;
|
||||||
|
|
||||||
|
constructor(props: { remotesModel?: RemotesModel }) {
|
||||||
|
super(props);
|
||||||
|
this.model = GlobalModel.remotesModel;
|
||||||
|
this.tempAlias = mobx.observable.box(null, { name: "EditRemoteSettings-tempAlias" });
|
||||||
|
this.tempAuthMode = mobx.observable.box(null, { name: "EditRemoteSettings-tempAuthMode" });
|
||||||
|
this.tempKeyFile = mobx.observable.box(null, { name: "EditRemoteSettings-tempKeyFile" });
|
||||||
|
this.tempPassword = mobx.observable.box(null, { name: "EditRemoteSettings-tempPassword" });
|
||||||
|
this.tempConnectMode = mobx.observable.box(null, { name: "EditRemoteSettings-tempConnectMode" });
|
||||||
|
}
|
||||||
|
|
||||||
|
get selectedRemoteId() {
|
||||||
|
return this.model.selectedRemoteId.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
get selectedRemote(): T.RemoteType {
|
||||||
|
return GlobalModel.getRemote(this.selectedRemoteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
get remoteEdit(): T.RemoteEditType {
|
||||||
|
return this.model.remoteEdit.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
get isAuthEditMode(): boolean {
|
||||||
|
return this.model.isAuthEditMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount(): void {
|
||||||
|
mobx.action(() => {
|
||||||
|
this.tempAlias.set(this.selectedRemote?.remotealias);
|
||||||
|
this.tempKeyFile.set(this.remoteEdit?.keystr);
|
||||||
|
this.tempPassword.set(this.remoteEdit?.haspassword ? PasswordUnchangedSentinel : "");
|
||||||
|
this.tempConnectMode.set(this.selectedRemote?.connectmode);
|
||||||
|
this.tempAuthMode.set(this.selectedRemote?.authtype);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
if (this.selectedRemote == null || this.selectedRemote.archived) {
|
||||||
|
this.model.deSelectRemote();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleChangeKeyFile(value: string): void {
|
||||||
|
mobx.action(() => {
|
||||||
|
this.tempKeyFile.set(value);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleChangePassword(value: string): void {
|
||||||
|
mobx.action(() => {
|
||||||
|
this.tempPassword.set(value);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleChangeAlias(value: string): void {
|
||||||
|
mobx.action(() => {
|
||||||
|
this.tempAlias.set(value);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleChangeAuthMode(value: string): void {
|
||||||
|
mobx.action(() => {
|
||||||
|
this.tempAuthMode.set(value);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleChangeConnectMode(value: string): void {
|
||||||
|
mobx.action(() => {
|
||||||
|
this.tempConnectMode.set(value);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
canResetPw(): boolean {
|
||||||
|
if (this.remoteEdit == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return Boolean(this.remoteEdit.haspassword) && this.tempPassword.get() != PasswordUnchangedSentinel;
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
resetPw(): void {
|
||||||
|
mobx.action(() => {
|
||||||
|
this.tempPassword.set(PasswordUnchangedSentinel);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
onFocusPassword(e: any) {
|
||||||
|
if (this.tempPassword.get() == PasswordUnchangedSentinel) {
|
||||||
|
e.target.select();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
submitRemote(): void {
|
||||||
|
let authMode = this.tempAuthMode.get();
|
||||||
|
let kwargs: Record<string, string> = {};
|
||||||
|
if (authMode == "key" || authMode == "key+password") {
|
||||||
|
let keyStrEq = util.isStrEq(this.tempKeyFile.get(), this.remoteEdit?.keystr);
|
||||||
|
if (!keyStrEq) {
|
||||||
|
kwargs["key"] = this.tempKeyFile.get();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!util.isBlank(this.tempKeyFile.get())) {
|
||||||
|
kwargs["key"] = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (authMode == "password" || authMode == "key+password") {
|
||||||
|
if (this.tempPassword.get() != PasswordUnchangedSentinel) {
|
||||||
|
kwargs["password"] = this.tempPassword.get();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.remoteEdit?.haspassword) {
|
||||||
|
kwargs["password"] = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!util.isStrEq(this.tempAlias.get(), this.selectedRemote?.remotealias)) {
|
||||||
|
kwargs["alias"] = this.tempAlias.get();
|
||||||
|
}
|
||||||
|
if (!util.isStrEq(this.tempConnectMode.get(), this.selectedRemote?.connectmode)) {
|
||||||
|
kwargs["connectmode"] = this.tempConnectMode.get();
|
||||||
|
}
|
||||||
|
kwargs["visual"] = "1";
|
||||||
|
kwargs["submit"] = "1";
|
||||||
|
GlobalCommandRunner.editRemote(this.selectedRemote?.remoteid, kwargs);
|
||||||
|
this.model.closeModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderAuthModeMessage(): any {
|
||||||
|
let authMode = this.tempAuthMode.get();
|
||||||
|
if (authMode == "none") {
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
This connection requires no authentication.
|
||||||
|
<br />
|
||||||
|
Or authentication is already configured in ssh_config.
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (authMode == "key") {
|
||||||
|
return <span>Use a public/private keypair.</span>;
|
||||||
|
}
|
||||||
|
if (authMode == "password") {
|
||||||
|
return <span>Use a password.</span>;
|
||||||
|
}
|
||||||
|
if (authMode == "key+password") {
|
||||||
|
return <span>Use a public/private keypair with a passphrase.</span>;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let authMode = this.tempAuthMode.get();
|
||||||
|
if (this.remoteEdit === null || !this.isAuthEditMode) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Modal className="erconn-modal">
|
||||||
|
<Modal.Header title="Edit Connection" onClose={this.model.closeModal} />
|
||||||
|
<div className="wave-modal-body">
|
||||||
|
<div className="name-actions-section">
|
||||||
|
<div className="name text-primary">{util.getRemoteName(this.selectedRemote)}</div>
|
||||||
|
</div>
|
||||||
|
<div className="alias-section">
|
||||||
|
<TextField
|
||||||
|
label="Alias"
|
||||||
|
onChange={this.handleChangeAlias}
|
||||||
|
value={this.tempAlias.get()}
|
||||||
|
maxLength={100}
|
||||||
|
decoration={{
|
||||||
|
endDecoration: (
|
||||||
|
<InputDecoration>
|
||||||
|
<Tooltip
|
||||||
|
message={`(Optional) A short alias to use when selecting or displaying this connection.`}
|
||||||
|
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||||
|
>
|
||||||
|
<i className="fa-sharp fa-regular fa-circle-question" />
|
||||||
|
</Tooltip>
|
||||||
|
</InputDecoration>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="authmode-section">
|
||||||
|
<Dropdown
|
||||||
|
label="Auth Mode"
|
||||||
|
options={[
|
||||||
|
{ value: "none", label: "none" },
|
||||||
|
{ value: "key", label: "key" },
|
||||||
|
{ value: "password", label: "password" },
|
||||||
|
{ value: "key+password", label: "key+password" },
|
||||||
|
]}
|
||||||
|
value={this.tempAuthMode.get()}
|
||||||
|
onChange={this.handleChangeAuthMode}
|
||||||
|
decoration={{
|
||||||
|
endDecoration: (
|
||||||
|
<InputDecoration>
|
||||||
|
<Tooltip
|
||||||
|
message={
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b>none</b> - no authentication, or authentication is already
|
||||||
|
configured in your ssh config.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>key</b> - use a private key.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>password</b> - use a password.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>key+password</b> - use a key with a passphrase.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||||
|
>
|
||||||
|
<i className="fa-sharp fa-regular fa-circle-question" />
|
||||||
|
</Tooltip>
|
||||||
|
</InputDecoration>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<If condition={authMode == "key" || authMode == "key+password"}>
|
||||||
|
<TextField
|
||||||
|
label="SSH Keyfile"
|
||||||
|
placeholder="keyfile path"
|
||||||
|
onChange={this.handleChangeKeyFile}
|
||||||
|
value={this.tempKeyFile.get()}
|
||||||
|
maxLength={400}
|
||||||
|
required={true}
|
||||||
|
decoration={{
|
||||||
|
endDecoration: (
|
||||||
|
<InputDecoration>
|
||||||
|
<Tooltip
|
||||||
|
message={`(Required) The path to your ssh key file.`}
|
||||||
|
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||||
|
>
|
||||||
|
<i className="fa-sharp fa-regular fa-circle-question" />
|
||||||
|
</Tooltip>
|
||||||
|
</InputDecoration>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</If>
|
||||||
|
<If condition={authMode == "password" || authMode == "key+password"}>
|
||||||
|
<PasswordField
|
||||||
|
label={authMode == "password" ? "SSH Password" : "Key Passphrase"}
|
||||||
|
placeholder="password"
|
||||||
|
onChange={this.handleChangePassword}
|
||||||
|
value={this.tempPassword.get()}
|
||||||
|
maxLength={400}
|
||||||
|
/>
|
||||||
|
</If>
|
||||||
|
<div className="connectmode-section">
|
||||||
|
<Dropdown
|
||||||
|
label="Connect Mode"
|
||||||
|
options={[
|
||||||
|
{ value: "startup", label: "startup" },
|
||||||
|
{ value: "auto", label: "auto" },
|
||||||
|
{ value: "manual", label: "manual" },
|
||||||
|
]}
|
||||||
|
value={this.tempConnectMode.get()}
|
||||||
|
onChange={this.handleChangeConnectMode}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<If condition={!util.isBlank(this.remoteEdit?.errorstr)}>
|
||||||
|
<div className="settings-field settings-error">Error: {this.remoteEdit?.errorstr}</div>
|
||||||
|
</If>
|
||||||
|
</div>
|
||||||
|
<Modal.Footer onOk={this.submitRemote} onCancel={this.model.closeModal} okLabel="Save" />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { EditRemoteConnModal };
|
8
src/app/common/modals/index.tsx
Normal file
8
src/app/common/modals/index.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export { AboutModal } from "./about";
|
||||||
|
export { DisconnectedModal } from "./disconnected";
|
||||||
|
export { ClientStopModal } from "./clientstop";
|
||||||
|
export { AlertModal } from "./alert";
|
||||||
|
export { CreateRemoteConnModal } from "./createremoteconn";
|
||||||
|
export { ViewRemoteConnDetailModal } from "./viewremoteconndetail";
|
||||||
|
export { EditRemoteConnModal } from "./editremoteconn";
|
||||||
|
export { TabSwitcherModal } from "./tabswitcher";
|
@ -17,24 +17,46 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.disconnected-modal {
|
.disconnected-modal {
|
||||||
.modal-content {
|
.wave-modal-content {
|
||||||
footer {
|
.wave-modal-body {
|
||||||
.footer-text-link {
|
padding: 0;
|
||||||
color: @term-white;
|
|
||||||
cursor: pointer;
|
.modal-content {
|
||||||
|
footer {
|
||||||
|
.footer-text-link {
|
||||||
|
color: @term-white;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner-content {
|
||||||
|
.log {
|
||||||
|
height: 335px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track,
|
||||||
|
&::-webkit-scrollbar-thumb,
|
||||||
|
&::-webkit-scrollbar-corner {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover::-webkit-scrollbar-thumb {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
color: @term-white;
|
||||||
|
background-color: @term-black;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.inner-content {
|
.wave-modal-footer {
|
||||||
.ws-log {
|
button:first-child {
|
||||||
padding: 5px;
|
color: @term-green;
|
||||||
background-color: @term-black;
|
|
||||||
height: 250px;
|
|
||||||
overflow: auto;
|
|
||||||
|
|
||||||
.ws-logline {
|
|
||||||
color: @term-white;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -453,6 +475,89 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tabswitcher-modal {
|
||||||
|
width: 452px;
|
||||||
|
min-height: 384px;
|
||||||
|
|
||||||
|
.wave-modal-content {
|
||||||
|
.wave-modal-body {
|
||||||
|
display: flex;
|
||||||
|
padding: 0px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
align-self: stretch;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.textfield-wrapper {
|
||||||
|
padding: 20px 20px 0px;
|
||||||
|
|
||||||
|
.wave-input-decoration.start-position {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.tabswitcher-search-prefix {
|
||||||
|
opacity: 0.5;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-container {
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 10px 0 20px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-container-inner {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
padding: 0 16px 0 20px;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover::-webkit-scrollbar-thumb {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options-list {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.search-option {
|
||||||
|
padding: 5px 5px 5px 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
div.tabname {
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 20px;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.focused-option {
|
||||||
|
border: 1px solid rgba(241, 246, 243, 0.15);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.screen-settings-tooltip .wave-tooltip-icon {
|
.screen-settings-tooltip .wave-tooltip-icon {
|
||||||
i {
|
i {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@ -491,6 +596,10 @@
|
|||||||
gap: 4px;
|
gap: 4px;
|
||||||
align-self: stretch;
|
align-self: stretch;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
.settings-input .hotkey {
|
||||||
|
color: @text-secondary;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -564,11 +673,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.alert-modal {
|
.alert-modal {
|
||||||
width: 500px;
|
width: 510px;
|
||||||
|
|
||||||
.wave-modal-content {
|
.wave-modal-content {
|
||||||
.wave-modal-body {
|
.wave-modal-body {
|
||||||
padding: 40px 20px;
|
padding: 40px 20px;
|
||||||
|
|
||||||
|
.dontshowagain-text {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,12 +23,15 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
Button,
|
Button,
|
||||||
Status,
|
Status,
|
||||||
|
Checkbox,
|
||||||
} from "../common";
|
} from "../common";
|
||||||
import * as util from "../../../util/util";
|
import * as util from "../../../util/util";
|
||||||
import * as textmeasure from "../../../util/textmeasure";
|
import * as textmeasure from "../../../util/textmeasure";
|
||||||
|
import * as appconst from "../../appconst";
|
||||||
import { ClientDataType } from "../../../types/types";
|
import { ClientDataType } from "../../../types/types";
|
||||||
|
import { Screen } from "../../../model/model";
|
||||||
|
import { ReactComponent as SquareIcon } from "../../assets/icons/tab/square.svg";
|
||||||
|
|
||||||
import { ReactComponent as WarningIcon } from "../../assets/icons/line/triangle-exclamation.svg";
|
|
||||||
import shield from "../../assets/icons/shield_check.svg";
|
import shield from "../../assets/icons/shield_check.svg";
|
||||||
import help from "../../assets/icons/help_filled.svg";
|
import help from "../../assets/icons/help_filled.svg";
|
||||||
import github from "../../assets/icons/github.svg";
|
import github from "../../assets/icons/github.svg";
|
||||||
@ -42,9 +45,11 @@ const VERSION = __WAVETERM_VERSION__;
|
|||||||
let BUILD = __WAVETERM_BUILD__;
|
let BUILD = __WAVETERM_BUILD__;
|
||||||
|
|
||||||
type OV<V> = mobx.IObservableValue<V>;
|
type OV<V> = mobx.IObservableValue<V>;
|
||||||
|
type OArr<V> = mobx.IObservableArray<V>;
|
||||||
|
|
||||||
const RemotePtyRows = 9;
|
const RemotePtyRows = 9;
|
||||||
const RemotePtyCols = 80;
|
const RemotePtyCols = 80;
|
||||||
|
const NumOfLines = 50;
|
||||||
const PasswordUnchangedSentinel = "--unchanged--";
|
const PasswordUnchangedSentinel = "--unchanged--";
|
||||||
|
|
||||||
@mobxReact.observer
|
@mobxReact.observer
|
||||||
@ -67,7 +72,8 @@ class ModalsProvider extends React.Component {
|
|||||||
@mobxReact.observer
|
@mobxReact.observer
|
||||||
class DisconnectedModal extends React.Component<{}, {}> {
|
class DisconnectedModal extends React.Component<{}, {}> {
|
||||||
logRef: any = React.createRef();
|
logRef: any = React.createRef();
|
||||||
showLog: mobx.IObservableValue<boolean> = mobx.observable.box(false);
|
logs: mobx.IObservableValue<string> = mobx.observable.box("");
|
||||||
|
logInterval: NodeJS.Timeout = null;
|
||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
restartServer() {
|
restartServer() {
|
||||||
@ -80,8 +86,16 @@ class DisconnectedModal extends React.Component<{}, {}> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (this.logRef.current != null) {
|
this.fetchLogs();
|
||||||
this.logRef.current.scrollTop = this.logRef.current.scrollHeight;
|
|
||||||
|
this.logInterval = setInterval(() => {
|
||||||
|
this.fetchLogs();
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (this.logInterval) {
|
||||||
|
clearInterval(this.logInterval);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,58 +105,52 @@ class DisconnectedModal extends React.Component<{}, {}> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@boundMethod
|
fetchLogs() {
|
||||||
handleShowLog(): void {
|
GlobalModel.getLastLogs(
|
||||||
mobx.action(() => {
|
NumOfLines,
|
||||||
this.showLog.set(!this.showLog.get());
|
mobx.action((logs) => {
|
||||||
})();
|
this.logs.set(logs);
|
||||||
|
if (this.logRef.current != null) {
|
||||||
|
this.logRef.current.scrollTop = this.logRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let model = GlobalModel;
|
|
||||||
let logLine: string = null;
|
|
||||||
let idx: number = 0;
|
|
||||||
return (
|
return (
|
||||||
<div className="prompt-modal disconnected-modal modal is-active">
|
<Modal className="disconnected-modal">
|
||||||
<div className="modal-background"></div>
|
<Modal.Header title="Wave Client Disconnected" />
|
||||||
<div className="modal-content">
|
<div className="wave-modal-body">
|
||||||
<div className="message-header">
|
<div className="modal-content">
|
||||||
<div className="modal-title">Wave Client Disconnected</div>
|
|
||||||
</div>
|
|
||||||
<If condition={this.showLog.get()}>
|
|
||||||
<div className="inner-content">
|
<div className="inner-content">
|
||||||
<div className="ws-log" ref={this.logRef}>
|
<div className="log" ref={this.logRef}>
|
||||||
<For each="logLine" index="idx" of={GlobalModel.ws.wsLog}>
|
<pre>{this.logs.get()}</pre>
|
||||||
<div key={idx} className="ws-logline">
|
|
||||||
{logLine}
|
|
||||||
</div>
|
|
||||||
</For>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</If>
|
</div>
|
||||||
<footer>
|
</div>
|
||||||
<div className="footer-text-link" style={{ marginLeft: 10 }} onClick={this.handleShowLog}>
|
<div className="wave-modal-footer">
|
||||||
<If condition={!this.showLog.get()}>
|
<Button
|
||||||
<i className="fa-sharp fa-solid fa-plus" /> Show Log
|
theme="secondary"
|
||||||
</If>
|
onClick={this.tryReconnect}
|
||||||
<If condition={this.showLog.get()}>
|
leftIcon={
|
||||||
<i className="fa-sharp fa-solid fa-minus" /> Hide Log
|
|
||||||
</If>
|
|
||||||
</div>
|
|
||||||
<div className="flex-spacer" />
|
|
||||||
<button onClick={this.tryReconnect} className="button">
|
|
||||||
<span className="icon">
|
<span className="icon">
|
||||||
<i className="fa-sharp fa-solid fa-rotate" />
|
<i className="fa-sharp fa-solid fa-rotate" />
|
||||||
</span>
|
</span>
|
||||||
<span>Try Reconnect</span>
|
}
|
||||||
</button>
|
>
|
||||||
<button onClick={this.restartServer} className="button is-danger" style={{ marginLeft: 10 }}>
|
Try Reconnect
|
||||||
<WarningIcon className="icon" />
|
</Button>
|
||||||
<span>Restart Server</span>
|
<Button
|
||||||
</button>
|
theme="secondary"
|
||||||
</footer>
|
onClick={this.restartServer}
|
||||||
|
leftIcon={<i className="fa-sharp fa-solid fa-triangle-exclamation"></i>}
|
||||||
|
>
|
||||||
|
Restart Server
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -210,6 +218,15 @@ class AlertModal extends React.Component<{}, {}> {
|
|||||||
GlobalModel.confirmAlert();
|
GlobalModel.confirmAlert();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleDontShowAgain(checked: boolean) {
|
||||||
|
let message = GlobalModel.alertMessage.get();
|
||||||
|
if (message.confirmflag == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
GlobalCommandRunner.clientSetConfirmFlag(message.confirmflag, checked);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let message = GlobalModel.alertMessage.get();
|
let message = GlobalModel.alertMessage.get();
|
||||||
let title = message?.title ?? (message?.confirm ? "Confirm" : "Alert");
|
let title = message?.title ?? (message?.confirm ? "Confirm" : "Alert");
|
||||||
@ -223,16 +240,27 @@ class AlertModal extends React.Component<{}, {}> {
|
|||||||
<Markdown text={message?.message ?? ""} />
|
<Markdown text={message?.message ?? ""} />
|
||||||
</If>
|
</If>
|
||||||
<If condition={!message?.markdown}>{message?.message}</If>
|
<If condition={!message?.markdown}>{message?.message}</If>
|
||||||
|
<If condition={message.confirmflag}>
|
||||||
|
<Checkbox
|
||||||
|
onChange={this.handleDontShowAgain}
|
||||||
|
label={"Don't show me this again"}
|
||||||
|
className="dontshowagain-text"
|
||||||
|
/>
|
||||||
|
</If>
|
||||||
</div>
|
</div>
|
||||||
<div className="wave-modal-footer">
|
<div className="wave-modal-footer">
|
||||||
<If condition={isConfirm}>
|
<If condition={isConfirm}>
|
||||||
<Button theme="secondary" onClick={this.closeModal}>
|
<Button theme="secondary" onClick={this.closeModal}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={this.handleOK}>Ok</Button>
|
<Button autoFocus={true} onClick={this.handleOK}>
|
||||||
|
Ok
|
||||||
|
</Button>
|
||||||
</If>
|
</If>
|
||||||
<If condition={!isConfirm}>
|
<If condition={!isConfirm}>
|
||||||
<Button onClick={this.handleOK}>Ok</Button>
|
<Button autoFocus={true} onClick={this.handleOK}>
|
||||||
|
Ok
|
||||||
|
</Button>
|
||||||
</If>
|
</If>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
@ -497,6 +525,10 @@ class CreateRemoteConnModal extends React.Component<{}, {}> {
|
|||||||
this.errorStr = mobx.observable.box(this.remoteEdit?.errorstr ?? null, { name: "CreateRemote-errorStr" });
|
this.errorStr = mobx.observable.box(this.remoteEdit?.errorstr ?? null, { name: "CreateRemote-errorStr" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount(): void {
|
||||||
|
GlobalModel.getClientData();
|
||||||
|
}
|
||||||
|
|
||||||
remoteCName(): string {
|
remoteCName(): string {
|
||||||
let hostName = this.tempHostName.get();
|
let hostName = this.tempHostName.get();
|
||||||
if (hostName == "") {
|
if (hostName == "") {
|
||||||
@ -515,6 +547,27 @@ class CreateRemoteConnModal extends React.Component<{}, {}> {
|
|||||||
return this.remoteEdit?.errorstr ?? null;
|
return this.remoteEdit?.errorstr ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleOk(): void {
|
||||||
|
this.showShellPrompt(this.submitRemote);
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
showShellPrompt(cb: () => void): void {
|
||||||
|
let prtn = GlobalModel.showAlert({
|
||||||
|
message:
|
||||||
|
"You are about to install WaveShell on a remote machine. Please be aware that WaveShell will be executed on the remote system.",
|
||||||
|
confirm: true,
|
||||||
|
confirmflag: appconst.ConfirmKey_HideShellPrompt,
|
||||||
|
});
|
||||||
|
prtn.then((confirm) => {
|
||||||
|
if (!confirm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
submitRemote(): void {
|
submitRemote(): void {
|
||||||
mobx.action(() => {
|
mobx.action(() => {
|
||||||
@ -552,26 +605,27 @@ class CreateRemoteConnModal extends React.Component<{}, {}> {
|
|||||||
kwargs["connectmode"] = this.tempConnectMode.get();
|
kwargs["connectmode"] = this.tempConnectMode.get();
|
||||||
kwargs["visual"] = "1";
|
kwargs["visual"] = "1";
|
||||||
kwargs["submit"] = "1";
|
kwargs["submit"] = "1";
|
||||||
let model = this.model;
|
|
||||||
let prtn = GlobalCommandRunner.createRemote(cname, kwargs, false);
|
let prtn = GlobalCommandRunner.createRemote(cname, kwargs, false);
|
||||||
prtn.then((crtn) => {
|
prtn.then((crtn) => {
|
||||||
if (crtn.success) {
|
if (crtn.success) {
|
||||||
|
this.model.setRecentConnAdded(true);
|
||||||
|
this.model.closeModal();
|
||||||
|
|
||||||
let crRtn = GlobalCommandRunner.screenSetRemote(cname, true, false);
|
let crRtn = GlobalCommandRunner.screenSetRemote(cname, true, false);
|
||||||
crRtn.then((crcrtn) => {
|
crRtn.then((crcrtn) => {
|
||||||
if (crcrtn.success) {
|
if (crcrtn.success) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mobx.action(() => {
|
mobx.action(() => {
|
||||||
this.errorStr.set(crcrtn.error ?? null);
|
this.errorStr.set(crcrtn.error);
|
||||||
})();
|
})();
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mobx.action(() => {
|
mobx.action(() => {
|
||||||
this.errorStr.set(crtn.error ?? null);
|
this.errorStr.set(crtn.error);
|
||||||
})();
|
})();
|
||||||
});
|
});
|
||||||
model.seRecentConnAdded(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
@ -789,7 +843,7 @@ class CreateRemoteConnModal extends React.Component<{}, {}> {
|
|||||||
<div className="settings-field settings-error">Error: {this.getErrorStr()}</div>
|
<div className="settings-field settings-error">Error: {this.getErrorStr()}</div>
|
||||||
</If>
|
</If>
|
||||||
</div>
|
</div>
|
||||||
<Modal.Footer onCancel={this.model.closeModal} onOk={this.submitRemote} okLabel="Connect" />
|
<Modal.Footer onCancel={this.model.closeModal} onOk={this.handleOk} okLabel="Connect" />
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -806,7 +860,7 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@mobx.computed
|
@mobx.computed
|
||||||
get selectedRemote(): T.RemoteType {
|
getSelectedRemote(): T.RemoteType {
|
||||||
const selectedRemoteId = this.model.selectedRemoteId.get();
|
const selectedRemoteId = this.model.selectedRemoteId.get();
|
||||||
return GlobalModel.getRemote(selectedRemoteId);
|
return GlobalModel.getRemote(selectedRemoteId);
|
||||||
}
|
}
|
||||||
@ -821,7 +875,7 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
if (this.selectedRemote == null || this.selectedRemote.archived) {
|
if (this.getSelectedRemote() == null || this.getSelectedRemote().archived) {
|
||||||
this.model.deSelectRemote();
|
this.model.deSelectRemote();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -885,7 +939,7 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
|
|||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
clickArchive(): void {
|
clickArchive(): void {
|
||||||
if (this.selectedRemote && this.selectedRemote.status == "connected") {
|
if (this.getSelectedRemote() && this.getSelectedRemote().status == "connected") {
|
||||||
GlobalModel.showAlert({ message: "Cannot delete when connected. Disconnect and try again." });
|
GlobalModel.showAlert({ message: "Cannot delete when connected. Disconnect and try again." });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -897,21 +951,22 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
|
|||||||
if (!confirm) {
|
if (!confirm) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.selectedRemote) {
|
if (this.getSelectedRemote()) {
|
||||||
GlobalCommandRunner.archiveRemote(this.selectedRemote.remoteid);
|
GlobalCommandRunner.archiveRemote(this.getSelectedRemote().remoteid);
|
||||||
}
|
}
|
||||||
|
GlobalModel.modalsModel.popModal();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
clickReinstall(): void {
|
clickReinstall(): void {
|
||||||
GlobalCommandRunner.installRemote(this.selectedRemote?.remoteid);
|
GlobalCommandRunner.installRemote(this.getSelectedRemote().remoteid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
handleClose(): void {
|
handleClose(): void {
|
||||||
this.model.closeModal();
|
this.model.closeModal();
|
||||||
this.model.seRecentConnAdded(false);
|
this.model.setRecentConnAdded(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderInstallStatus(remote: T.RemoteType): any {
|
renderInstallStatus(remote: T.RemoteType): any {
|
||||||
@ -990,7 +1045,7 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
|
|||||||
<Button theme="secondary" disabled={true}>
|
<Button theme="secondary" disabled={true}>
|
||||||
Edit
|
Edit
|
||||||
<Tooltip
|
<Tooltip
|
||||||
message={`Remotes imported from an ssh config file cannot be edited inside waveterm. To edit these, you must edit the config file and import it again.`}
|
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" />}
|
icon={<i className="fa-sharp fa-regular fa-fw fa-ban" />}
|
||||||
>
|
>
|
||||||
<i className="fa-sharp fa-regular fa-fw fa-ban" />
|
<i className="fa-sharp fa-regular fa-fw fa-ban" />
|
||||||
@ -1003,7 +1058,7 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
message={
|
message={
|
||||||
<span>
|
<span>
|
||||||
Remotes imported from an ssh config file can be deleted, but will come back upon
|
Connections imported from an ssh config file can be deleted, but will come back upon
|
||||||
importing again. They will stay removed if you follow{" "}
|
importing again. They will stay removed if you follow{" "}
|
||||||
<a href="https://docs.waveterm.dev/features/sshconfig-imports">this procedure</a>.
|
<a href="https://docs.waveterm.dev/features/sshconfig-imports">this procedure</a>.
|
||||||
</span>
|
</span>
|
||||||
@ -1072,7 +1127,7 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let remote = this.selectedRemote;
|
let remote = this.getSelectedRemote();
|
||||||
|
|
||||||
if (remote == null) {
|
if (remote == null) {
|
||||||
return null;
|
return null;
|
||||||
@ -1083,14 +1138,15 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
|
|||||||
let termFontSize = GlobalModel.termFontSize.get();
|
let termFontSize = GlobalModel.termFontSize.get();
|
||||||
let termWidth = textmeasure.termWidthFromCols(RemotePtyCols, termFontSize);
|
let termWidth = textmeasure.termWidthFromCols(RemotePtyCols, termFontSize);
|
||||||
let remoteAliasText = util.isBlank(remote.remotealias) ? "(none)" : remote.remotealias;
|
let remoteAliasText = util.isBlank(remote.remotealias) ? "(none)" : remote.remotealias;
|
||||||
|
let selectedRemoteStatus = this.getSelectedRemote().status;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal className="rconndetail-modal">
|
<Modal className="rconndetail-modal">
|
||||||
<Modal.Header title="Connection" onClose={this.model.closeModal} />
|
<Modal.Header title="Connection" onClose={this.handleClose} />
|
||||||
<div className="wave-modal-body">
|
<div className="wave-modal-body">
|
||||||
<div className="name-header-actions-wrapper">
|
<div className="name-header-actions-wrapper">
|
||||||
<div className="name text-primary name-wrapper">
|
<div className="name text-primary name-wrapper">
|
||||||
{getName(remote)} {getImportTooltip(remote)}
|
{util.getRemoteName(remote)} {getImportTooltip(remote)}
|
||||||
</div>
|
</div>
|
||||||
<div className="header-actions">{this.renderHeaderBtns(remote)}</div>
|
<div className="header-actions">{this.renderHeaderBtns(remote)}</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1161,7 +1217,18 @@ class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Modal.Footer onOk={this.model.closeModal} onCancel={this.model.closeModal} okLabel="Done" />
|
<div className="wave-modal-footer">
|
||||||
|
<Button
|
||||||
|
theme="secondary"
|
||||||
|
disabled={selectedRemoteStatus == "connecting"}
|
||||||
|
onClick={this.handleClose}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button disabled={selectedRemoteStatus == "connecting"} onClick={this.handleClose}>
|
||||||
|
Done
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1343,7 +1410,7 @@ class EditRemoteConnModal extends React.Component<{}, {}> {
|
|||||||
<Modal.Header title="Edit Connection" onClose={this.model.closeModal} />
|
<Modal.Header title="Edit Connection" onClose={this.model.closeModal} />
|
||||||
<div className="wave-modal-body">
|
<div className="wave-modal-body">
|
||||||
<div className="name-actions-section">
|
<div className="name-actions-section">
|
||||||
<div className="name text-primary">{getName(this.selectedRemote)}</div>
|
<div className="name text-primary">{util.getRemoteName(this.selectedRemote)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="alias-section">
|
<div className="alias-section">
|
||||||
<TextField
|
<TextField
|
||||||
@ -1459,14 +1526,309 @@ class EditRemoteConnModal extends React.Component<{}, {}> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getName = (remote: T.RemoteType): string => {
|
type SwitcherDataType = {
|
||||||
if (remote == null) {
|
sessionId: string;
|
||||||
return "";
|
sessionName: string;
|
||||||
}
|
sessionIdx: number;
|
||||||
const { remotealias, remotecanonicalname } = remote;
|
screenId: string;
|
||||||
return remotealias ? `${remotealias} [${remotecanonicalname}]` : remotecanonicalname;
|
screenIdx: number;
|
||||||
|
screenName: string;
|
||||||
|
icon: string;
|
||||||
|
color: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MaxOptionsToDisplay = 100;
|
||||||
|
|
||||||
|
@mobxReact.observer
|
||||||
|
class TabSwitcherModal extends React.Component<{}, {}> {
|
||||||
|
screens: Map<string, OV<string>>[];
|
||||||
|
sessions: Map<string, OV<string>>[];
|
||||||
|
options: SwitcherDataType[] = [];
|
||||||
|
sOptions: OArr<SwitcherDataType> = mobx.observable.array(null, {
|
||||||
|
name: "TabSwitcherModal-sOptions",
|
||||||
|
});
|
||||||
|
focusedIdx: OV<number> = mobx.observable.box(0, { name: "TabSwitcherModal-selectedIdx" });
|
||||||
|
activeSessionIdx: number;
|
||||||
|
optionRefs = [];
|
||||||
|
listWrapperRef = React.createRef<HTMLDivElement>();
|
||||||
|
prevFocusedIdx = 0;
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.activeSessionIdx = GlobalModel.getActiveSession().sessionIdx.get();
|
||||||
|
let oSessions = GlobalModel.sessionList;
|
||||||
|
let oScreens = GlobalModel.screenMap;
|
||||||
|
oScreens.forEach((oScreen) => {
|
||||||
|
// Find the matching session in the observable array
|
||||||
|
let foundSession = oSessions.find((s) => {
|
||||||
|
if (s.sessionId === oScreen.sessionId && s.archived.get() == false) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (foundSession) {
|
||||||
|
let data: SwitcherDataType = {
|
||||||
|
sessionName: foundSession.name.get(),
|
||||||
|
sessionId: foundSession.sessionId,
|
||||||
|
sessionIdx: foundSession.sessionIdx.get(),
|
||||||
|
screenName: oScreen.name.get(),
|
||||||
|
screenId: oScreen.screenId,
|
||||||
|
screenIdx: oScreen.screenIdx.get(),
|
||||||
|
icon: this.getTabIcon(oScreen),
|
||||||
|
color: this.getTabColor(oScreen),
|
||||||
|
};
|
||||||
|
this.options.push(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mobx.action(() => {
|
||||||
|
this.sOptions.replace(this.sortOptions(this.options).slice(0, MaxOptionsToDisplay));
|
||||||
|
})();
|
||||||
|
|
||||||
|
document.addEventListener("keydown", this.handleKeyDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
document.removeEventListener("keydown", this.handleKeyDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
let currFocusedIdx = this.focusedIdx.get();
|
||||||
|
|
||||||
|
// Check if selectedIdx has changed
|
||||||
|
if (currFocusedIdx !== this.prevFocusedIdx) {
|
||||||
|
let optionElement = this.optionRefs[currFocusedIdx]?.current;
|
||||||
|
|
||||||
|
if (optionElement) {
|
||||||
|
optionElement.scrollIntoView({ block: "nearest" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update prevFocusedIdx for the next update cycle
|
||||||
|
this.prevFocusedIdx = currFocusedIdx;
|
||||||
|
}
|
||||||
|
if (currFocusedIdx >= this.sOptions.length && this.sOptions.length > 0) {
|
||||||
|
this.setFocusedIndex(this.sOptions.length - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
getTabIcon(screen: Screen): string {
|
||||||
|
let tabIcon = "default";
|
||||||
|
let screenOpts = screen.opts.get();
|
||||||
|
if (screenOpts != null && !util.isBlank(screenOpts.tabicon)) {
|
||||||
|
tabIcon = screenOpts.tabicon;
|
||||||
|
}
|
||||||
|
return tabIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
getTabColor(screen: Screen): string {
|
||||||
|
let tabColor = "default";
|
||||||
|
let screenOpts = screen.opts.get();
|
||||||
|
if (screenOpts != null && !util.isBlank(screenOpts.tabcolor)) {
|
||||||
|
tabColor = screenOpts.tabcolor;
|
||||||
|
}
|
||||||
|
return tabColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleKeyDown(e) {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
this.closeModal();
|
||||||
|
} else if (e.key === "ArrowUp" || e.key === "ArrowDown") {
|
||||||
|
e.preventDefault();
|
||||||
|
let newIndex = this.calculateNewIndex(e.key === "ArrowUp");
|
||||||
|
this.setFocusedIndex(newIndex);
|
||||||
|
} else if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
this.handleSelect(this.focusedIdx.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
calculateNewIndex(isUpKey) {
|
||||||
|
let currentIndex = this.focusedIdx.get();
|
||||||
|
if (isUpKey) {
|
||||||
|
return Math.max(currentIndex - 1, 0);
|
||||||
|
} else {
|
||||||
|
return Math.min(currentIndex + 1, this.sOptions.length - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
setFocusedIndex(index) {
|
||||||
|
mobx.action(() => {
|
||||||
|
this.focusedIdx.set(index);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
closeModal(): void {
|
||||||
|
GlobalModel.modalsModel.popModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleSelect(index: number): void {
|
||||||
|
const selectedOption = this.sOptions[index];
|
||||||
|
if (selectedOption) {
|
||||||
|
GlobalCommandRunner.switchScreen(selectedOption.screenId, selectedOption.sessionId);
|
||||||
|
this.closeModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleSearch(val: string): void {
|
||||||
|
let sOptions: SwitcherDataType[];
|
||||||
|
if (val == "") {
|
||||||
|
sOptions = this.sortOptions(this.options).slice(0, MaxOptionsToDisplay);
|
||||||
|
} else {
|
||||||
|
sOptions = this.filterOptions(val);
|
||||||
|
sOptions = this.sortOptions(sOptions);
|
||||||
|
if (sOptions.length > MaxOptionsToDisplay) {
|
||||||
|
sOptions = sOptions.slice(0, MaxOptionsToDisplay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mobx.action(() => {
|
||||||
|
this.sOptions.replace(sOptions);
|
||||||
|
this.focusedIdx.set(0);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
@mobx.computed
|
||||||
|
@boundMethod
|
||||||
|
filterOptions(searchInput: string): SwitcherDataType[] {
|
||||||
|
let filteredScreens = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < this.options.length; i++) {
|
||||||
|
let tab = this.options[i];
|
||||||
|
let match = false;
|
||||||
|
|
||||||
|
if (searchInput.includes("/")) {
|
||||||
|
let [sessionFilter, screenFilter] = searchInput.split("/").map((s) => s.trim().toLowerCase());
|
||||||
|
match =
|
||||||
|
tab.sessionName.toLowerCase().includes(sessionFilter) &&
|
||||||
|
tab.screenName.toLowerCase().includes(screenFilter);
|
||||||
|
} else {
|
||||||
|
match =
|
||||||
|
tab.sessionName.toLowerCase().includes(searchInput) ||
|
||||||
|
tab.screenName.toLowerCase().includes(searchInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add tab to filtered list if it matches the criteria
|
||||||
|
if (match) {
|
||||||
|
filteredScreens.push(tab);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredScreens;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mobx.computed
|
||||||
|
@boundMethod
|
||||||
|
sortOptions(options: SwitcherDataType[]): SwitcherDataType[] {
|
||||||
|
return options.sort((a, b) => {
|
||||||
|
let aInCurrentSession = a.sessionIdx === this.activeSessionIdx;
|
||||||
|
let bInCurrentSession = b.sessionIdx === this.activeSessionIdx;
|
||||||
|
|
||||||
|
// Tabs in the current session are sorted by screenIdx
|
||||||
|
if (aInCurrentSession && bInCurrentSession) {
|
||||||
|
return a.screenIdx - b.screenIdx;
|
||||||
|
}
|
||||||
|
// a is in the current session and b is not, so a comes first
|
||||||
|
else if (aInCurrentSession) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
// b is in the current session and a is not, so b comes first
|
||||||
|
else if (bInCurrentSession) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
// Both are in different, non-current sessions - sort by sessionIdx and then by screenIdx
|
||||||
|
else {
|
||||||
|
if (a.sessionIdx === b.sessionIdx) {
|
||||||
|
return a.screenIdx - b.screenIdx;
|
||||||
|
} else {
|
||||||
|
return a.sessionIdx - b.sessionIdx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
renderIcon(option: SwitcherDataType): React.ReactNode {
|
||||||
|
let tabIcon = option.icon;
|
||||||
|
if (tabIcon === "default" || tabIcon === "square") {
|
||||||
|
return <SquareIcon className="left-icon" />;
|
||||||
|
}
|
||||||
|
return <i className={`fa-sharp fa-solid fa-${tabIcon}`}></i>;
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
renderOption(option: SwitcherDataType, index: number): JSX.Element {
|
||||||
|
if (!this.optionRefs[index]) {
|
||||||
|
this.optionRefs[index] = React.createRef();
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={option.sessionId + "/" + option.screenId}
|
||||||
|
ref={this.optionRefs[index]}
|
||||||
|
className={cn("search-option unselectable", {
|
||||||
|
"focused-option": this.focusedIdx.get() === index,
|
||||||
|
})}
|
||||||
|
onClick={() => this.handleSelect(index)}
|
||||||
|
>
|
||||||
|
<div className={cn("icon", "color-" + option.color)}>{this.renderIcon(option)}</div>
|
||||||
|
<div className="tabname">
|
||||||
|
#{option.sessionName} / {option.screenName}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let option: SwitcherDataType;
|
||||||
|
let index: number;
|
||||||
|
return (
|
||||||
|
<Modal className="tabswitcher-modal">
|
||||||
|
<div className="wave-modal-body">
|
||||||
|
<div className="textfield-wrapper">
|
||||||
|
<TextField
|
||||||
|
onChange={this.handleSearch}
|
||||||
|
maxLength={400}
|
||||||
|
autoFocus={true}
|
||||||
|
decoration={{
|
||||||
|
startDecoration: (
|
||||||
|
<InputDecoration position="start">
|
||||||
|
<div className="tabswitcher-search-prefix">Switch to Tab:</div>
|
||||||
|
</InputDecoration>
|
||||||
|
),
|
||||||
|
endDecoration: (
|
||||||
|
<InputDecoration>
|
||||||
|
<Tooltip
|
||||||
|
message={`Type to filter workspaces and tabs.`}
|
||||||
|
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||||
|
>
|
||||||
|
<i className="fa-sharp fa-regular fa-circle-question" />
|
||||||
|
</Tooltip>
|
||||||
|
</InputDecoration>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="list-container">
|
||||||
|
<div ref={this.listWrapperRef} className="list-container-inner">
|
||||||
|
<div className="options-list">
|
||||||
|
<For each="option" index="index" of={this.sOptions}>
|
||||||
|
{this.renderOption(option, index)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getImportTooltip = (remote: T.RemoteType): React.ReactElement<any, any> => {
|
const getImportTooltip = (remote: T.RemoteType): React.ReactElement<any, any> => {
|
||||||
if (remote.sshconfigsrc == "sshconfig-import") {
|
if (remote.sshconfigsrc == "sshconfig-import") {
|
||||||
return (
|
return (
|
||||||
@ -1493,4 +1855,5 @@ export {
|
|||||||
ViewRemoteConnDetailModal,
|
ViewRemoteConnDetailModal,
|
||||||
EditRemoteConnModal,
|
EditRemoteConnModal,
|
||||||
ModalsProvider,
|
ModalsProvider,
|
||||||
|
TabSwitcherModal,
|
||||||
};
|
};
|
||||||
|
26
src/app/common/modals/provider.tsx
Normal file
26
src/app/common/modals/provider.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// Copyright 2023, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as mobxReact from "mobx-react";
|
||||||
|
import { GlobalModel } from "../../../model/model";
|
||||||
|
import { TosModal } from "./tos";
|
||||||
|
|
||||||
|
@mobxReact.observer
|
||||||
|
class ModalsProvider extends React.Component {
|
||||||
|
render() {
|
||||||
|
let store = GlobalModel.modalsModel.store.slice();
|
||||||
|
if (GlobalModel.needsTos()) {
|
||||||
|
return <TosModal />;
|
||||||
|
}
|
||||||
|
let rtn: JSX.Element[] = [];
|
||||||
|
for (let i = 0; i < store.length; i++) {
|
||||||
|
let entry = store[i];
|
||||||
|
let Comp = entry.component;
|
||||||
|
rtn.push(<Comp key={entry.uniqueKey} />);
|
||||||
|
}
|
||||||
|
return <>{rtn}</>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ModalsProvider };
|
@ -4,11 +4,12 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {
|
import {
|
||||||
AboutModal,
|
AboutModal,
|
||||||
|
AlertModal,
|
||||||
CreateRemoteConnModal,
|
CreateRemoteConnModal,
|
||||||
ViewRemoteConnDetailModal,
|
ViewRemoteConnDetailModal,
|
||||||
EditRemoteConnModal,
|
EditRemoteConnModal,
|
||||||
AlertModal,
|
TabSwitcherModal,
|
||||||
} from "./modals";
|
} from "../modals";
|
||||||
import { ScreenSettingsModal, SessionSettingsModal, LineSettingsModal, ClientSettingsModal } from "./settings";
|
import { ScreenSettingsModal, SessionSettingsModal, LineSettingsModal, ClientSettingsModal } from "./settings";
|
||||||
import * as constants from "../../appconst";
|
import * as constants from "../../appconst";
|
||||||
|
|
||||||
@ -22,6 +23,7 @@ const modalsRegistry: { [key: string]: () => React.ReactElement } = {
|
|||||||
[constants.SESSION_SETTINGS]: () => <SessionSettingsModal />,
|
[constants.SESSION_SETTINGS]: () => <SessionSettingsModal />,
|
||||||
[constants.LINE_SETTINGS]: () => <LineSettingsModal />,
|
[constants.LINE_SETTINGS]: () => <LineSettingsModal />,
|
||||||
[constants.CLIENT_SETTINGS]: () => <ClientSettingsModal />,
|
[constants.CLIENT_SETTINGS]: () => <ClientSettingsModal />,
|
||||||
|
[constants.TAB_SWITCHER]: () => <TabSwitcherModal />,
|
||||||
};
|
};
|
||||||
|
|
||||||
export { modalsRegistry };
|
export { modalsRegistry };
|
@ -17,7 +17,16 @@ import {
|
|||||||
Screen,
|
Screen,
|
||||||
Session,
|
Session,
|
||||||
} from "../../../model/model";
|
} from "../../../model/model";
|
||||||
import { Toggle, InlineSettingsTextEdit, SettingsError, InfoMessage, Modal, Dropdown, Tooltip } from "../common";
|
import {
|
||||||
|
Toggle,
|
||||||
|
InlineSettingsTextEdit,
|
||||||
|
SettingsError,
|
||||||
|
InfoMessage,
|
||||||
|
Modal,
|
||||||
|
Dropdown,
|
||||||
|
Tooltip,
|
||||||
|
Button,
|
||||||
|
} from "../common";
|
||||||
import { LineType, RendererPluginType, ClientDataType, CommandRtnType, RemoteType } from "../../../types/types";
|
import { LineType, RendererPluginType, ClientDataType, CommandRtnType, RemoteType } from "../../../types/types";
|
||||||
import { PluginModel } from "../../../plugins/plugins";
|
import { PluginModel } from "../../../plugins/plugins";
|
||||||
import * as util from "../../../util/util";
|
import * as util from "../../../util/util";
|
||||||
@ -219,7 +228,7 @@ class ScreenSettingsModal extends React.Component<{}, {}> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.screen.getScreenLines().lines.length == 0) {
|
if (this.screen.getScreenLines().lines.length == 0) {
|
||||||
GlobalCommandRunner.screenDelete(this.screenId);
|
GlobalCommandRunner.screenDelete(this.screenId, false);
|
||||||
GlobalModel.modalsModel.popModal();
|
GlobalModel.modalsModel.popModal();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -229,7 +238,7 @@ class ScreenSettingsModal extends React.Component<{}, {}> {
|
|||||||
if (!result) {
|
if (!result) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let prtn = GlobalCommandRunner.screenDelete(this.screenId);
|
let prtn = GlobalCommandRunner.screenDelete(this.screenId, false);
|
||||||
commandRtnHandler(prtn, this.errorMessage);
|
commandRtnHandler(prtn, this.errorMessage);
|
||||||
GlobalModel.modalsModel.popModal();
|
GlobalModel.modalsModel.popModal();
|
||||||
});
|
});
|
||||||
@ -632,12 +641,6 @@ class LineSettingsModal extends React.Component<{}, {}> {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-field">
|
|
||||||
<div className="settings-label">Archived</div>
|
|
||||||
<div className="settings-input">
|
|
||||||
<Toggle checked={!!line.archived} onChange={this.handleChangeArchived} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<SettingsError errorMessage={this.errorMessage} />
|
<SettingsError errorMessage={this.errorMessage} />
|
||||||
<div style={{ height: 50 }} />
|
<div style={{ height: 50 }} />
|
||||||
</div>
|
</div>
|
||||||
|
84
src/app/common/modals/tabswitcher.less
Normal file
84
src/app/common/modals/tabswitcher.less
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
@import "../../../app/common/themes/themes.less";
|
||||||
|
|
||||||
|
.tabswitcher-modal {
|
||||||
|
width: 452px;
|
||||||
|
min-height: 384px;
|
||||||
|
|
||||||
|
.wave-modal-content {
|
||||||
|
.wave-modal-body {
|
||||||
|
display: flex;
|
||||||
|
padding: 0px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
align-self: stretch;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.textfield-wrapper {
|
||||||
|
padding: 20px 20px 0px;
|
||||||
|
|
||||||
|
.wave-input-decoration.start-position {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.tabswitcher-search-prefix {
|
||||||
|
opacity: 0.5;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-container {
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 10px 0 20px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-container-inner {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
padding: 0 16px 0 20px;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover::-webkit-scrollbar-thumb {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options-list {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.search-option {
|
||||||
|
padding: 5px 5px 5px 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
div.tabname {
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 20px;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.focused-option {
|
||||||
|
border: 1px solid rgba(241, 246, 243, 0.15);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
324
src/app/common/modals/tabswitcher.tsx
Normal file
324
src/app/common/modals/tabswitcher.tsx
Normal file
@ -0,0 +1,324 @@
|
|||||||
|
// Copyright 2023, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as mobxReact from "mobx-react";
|
||||||
|
import * as mobx from "mobx";
|
||||||
|
import { boundMethod } from "autobind-decorator";
|
||||||
|
import { For } from "tsx-control-statements/components";
|
||||||
|
import cn from "classnames";
|
||||||
|
import { GlobalModel, GlobalCommandRunner } from "../../../model/model";
|
||||||
|
import { Modal, TextField, InputDecoration, Tooltip } from "../common";
|
||||||
|
import * as util from "../../../util/util";
|
||||||
|
import { Screen } from "../../../model/model";
|
||||||
|
import { ReactComponent as SquareIcon } from "../../assets/icons/tab/square.svg";
|
||||||
|
|
||||||
|
import "./tabswitcher.less";
|
||||||
|
|
||||||
|
type OV<V> = mobx.IObservableValue<V>;
|
||||||
|
type OArr<V> = mobx.IObservableArray<V>;
|
||||||
|
|
||||||
|
type SwitcherDataType = {
|
||||||
|
sessionId: string;
|
||||||
|
sessionName: string;
|
||||||
|
sessionIdx: number;
|
||||||
|
screenId: string;
|
||||||
|
screenIdx: number;
|
||||||
|
screenName: string;
|
||||||
|
icon: string;
|
||||||
|
color: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MaxOptionsToDisplay = 100;
|
||||||
|
|
||||||
|
@mobxReact.observer
|
||||||
|
class TabSwitcherModal extends React.Component<{}, {}> {
|
||||||
|
screens: Map<string, OV<string>>[];
|
||||||
|
sessions: Map<string, OV<string>>[];
|
||||||
|
options: SwitcherDataType[] = [];
|
||||||
|
sOptions: OArr<SwitcherDataType> = mobx.observable.array(null, {
|
||||||
|
name: "TabSwitcherModal-sOptions",
|
||||||
|
});
|
||||||
|
focusedIdx: OV<number> = mobx.observable.box(0, { name: "TabSwitcherModal-selectedIdx" });
|
||||||
|
activeSessionIdx: number;
|
||||||
|
optionRefs = [];
|
||||||
|
listWrapperRef = React.createRef<HTMLDivElement>();
|
||||||
|
prevFocusedIdx = 0;
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.activeSessionIdx = GlobalModel.getActiveSession().sessionIdx.get();
|
||||||
|
let oSessions = GlobalModel.sessionList;
|
||||||
|
let oScreens = GlobalModel.screenMap;
|
||||||
|
oScreens.forEach((oScreen) => {
|
||||||
|
// Find the matching session in the observable array
|
||||||
|
let foundSession = oSessions.find((s) => {
|
||||||
|
if (s.sessionId === oScreen.sessionId && s.archived.get() == false) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (foundSession) {
|
||||||
|
let data: SwitcherDataType = {
|
||||||
|
sessionName: foundSession.name.get(),
|
||||||
|
sessionId: foundSession.sessionId,
|
||||||
|
sessionIdx: foundSession.sessionIdx.get(),
|
||||||
|
screenName: oScreen.name.get(),
|
||||||
|
screenId: oScreen.screenId,
|
||||||
|
screenIdx: oScreen.screenIdx.get(),
|
||||||
|
icon: this.getTabIcon(oScreen),
|
||||||
|
color: this.getTabColor(oScreen),
|
||||||
|
};
|
||||||
|
this.options.push(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mobx.action(() => {
|
||||||
|
this.sOptions.replace(this.sortOptions(this.options).slice(0, MaxOptionsToDisplay));
|
||||||
|
})();
|
||||||
|
|
||||||
|
document.addEventListener("keydown", this.handleKeyDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
document.removeEventListener("keydown", this.handleKeyDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
let currFocusedIdx = this.focusedIdx.get();
|
||||||
|
|
||||||
|
// Check if selectedIdx has changed
|
||||||
|
if (currFocusedIdx !== this.prevFocusedIdx) {
|
||||||
|
let optionElement = this.optionRefs[currFocusedIdx]?.current;
|
||||||
|
|
||||||
|
if (optionElement) {
|
||||||
|
optionElement.scrollIntoView({ block: "nearest" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update prevFocusedIdx for the next update cycle
|
||||||
|
this.prevFocusedIdx = currFocusedIdx;
|
||||||
|
}
|
||||||
|
if (currFocusedIdx >= this.sOptions.length && this.sOptions.length > 0) {
|
||||||
|
this.setFocusedIndex(this.sOptions.length - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
getTabIcon(screen: Screen): string {
|
||||||
|
let tabIcon = "default";
|
||||||
|
let screenOpts = screen.opts.get();
|
||||||
|
if (screenOpts != null && !util.isBlank(screenOpts.tabicon)) {
|
||||||
|
tabIcon = screenOpts.tabicon;
|
||||||
|
}
|
||||||
|
return tabIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
getTabColor(screen: Screen): string {
|
||||||
|
let tabColor = "default";
|
||||||
|
let screenOpts = screen.opts.get();
|
||||||
|
if (screenOpts != null && !util.isBlank(screenOpts.tabcolor)) {
|
||||||
|
tabColor = screenOpts.tabcolor;
|
||||||
|
}
|
||||||
|
return tabColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleKeyDown(e) {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
this.closeModal();
|
||||||
|
} else if (e.key === "ArrowUp" || e.key === "ArrowDown") {
|
||||||
|
e.preventDefault();
|
||||||
|
let newIndex = this.calculateNewIndex(e.key === "ArrowUp");
|
||||||
|
this.setFocusedIndex(newIndex);
|
||||||
|
} else if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
this.handleSelect(this.focusedIdx.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
calculateNewIndex(isUpKey) {
|
||||||
|
let currentIndex = this.focusedIdx.get();
|
||||||
|
if (isUpKey) {
|
||||||
|
return Math.max(currentIndex - 1, 0);
|
||||||
|
} else {
|
||||||
|
return Math.min(currentIndex + 1, this.sOptions.length - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
setFocusedIndex(index) {
|
||||||
|
mobx.action(() => {
|
||||||
|
this.focusedIdx.set(index);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
closeModal(): void {
|
||||||
|
GlobalModel.modalsModel.popModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleSelect(index: number): void {
|
||||||
|
const selectedOption = this.sOptions[index];
|
||||||
|
if (selectedOption) {
|
||||||
|
GlobalCommandRunner.switchScreen(selectedOption.screenId, selectedOption.sessionId);
|
||||||
|
this.closeModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleSearch(val: string): void {
|
||||||
|
let sOptions: SwitcherDataType[];
|
||||||
|
if (val == "") {
|
||||||
|
sOptions = this.sortOptions(this.options).slice(0, MaxOptionsToDisplay);
|
||||||
|
} else {
|
||||||
|
sOptions = this.filterOptions(val);
|
||||||
|
sOptions = this.sortOptions(sOptions);
|
||||||
|
if (sOptions.length > MaxOptionsToDisplay) {
|
||||||
|
sOptions = sOptions.slice(0, MaxOptionsToDisplay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mobx.action(() => {
|
||||||
|
this.sOptions.replace(sOptions);
|
||||||
|
this.focusedIdx.set(0);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
@mobx.computed
|
||||||
|
@boundMethod
|
||||||
|
filterOptions(searchInput: string): SwitcherDataType[] {
|
||||||
|
let filteredScreens = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < this.options.length; i++) {
|
||||||
|
let tab = this.options[i];
|
||||||
|
let match = false;
|
||||||
|
|
||||||
|
if (searchInput.includes("/")) {
|
||||||
|
let [sessionFilter, screenFilter] = searchInput.split("/").map((s) => s.trim().toLowerCase());
|
||||||
|
match =
|
||||||
|
tab.sessionName.toLowerCase().includes(sessionFilter) &&
|
||||||
|
tab.screenName.toLowerCase().includes(screenFilter);
|
||||||
|
} else {
|
||||||
|
match =
|
||||||
|
tab.sessionName.toLowerCase().includes(searchInput) ||
|
||||||
|
tab.screenName.toLowerCase().includes(searchInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add tab to filtered list if it matches the criteria
|
||||||
|
if (match) {
|
||||||
|
filteredScreens.push(tab);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredScreens;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mobx.computed
|
||||||
|
@boundMethod
|
||||||
|
sortOptions(options: SwitcherDataType[]): SwitcherDataType[] {
|
||||||
|
return options.sort((a, b) => {
|
||||||
|
let aInCurrentSession = a.sessionIdx === this.activeSessionIdx;
|
||||||
|
let bInCurrentSession = b.sessionIdx === this.activeSessionIdx;
|
||||||
|
|
||||||
|
// Tabs in the current session are sorted by screenIdx
|
||||||
|
if (aInCurrentSession && bInCurrentSession) {
|
||||||
|
return a.screenIdx - b.screenIdx;
|
||||||
|
}
|
||||||
|
// a is in the current session and b is not, so a comes first
|
||||||
|
else if (aInCurrentSession) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
// b is in the current session and a is not, so b comes first
|
||||||
|
else if (bInCurrentSession) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
// Both are in different, non-current sessions - sort by sessionIdx and then by screenIdx
|
||||||
|
else {
|
||||||
|
if (a.sessionIdx === b.sessionIdx) {
|
||||||
|
return a.screenIdx - b.screenIdx;
|
||||||
|
} else {
|
||||||
|
return a.sessionIdx - b.sessionIdx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
renderIcon(option: SwitcherDataType): React.ReactNode {
|
||||||
|
let tabIcon = option.icon;
|
||||||
|
if (tabIcon === "default" || tabIcon === "square") {
|
||||||
|
return <SquareIcon className="left-icon" />;
|
||||||
|
}
|
||||||
|
return <i className={`fa-sharp fa-solid fa-${tabIcon}`}></i>;
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
renderOption(option: SwitcherDataType, index: number): JSX.Element {
|
||||||
|
if (!this.optionRefs[index]) {
|
||||||
|
this.optionRefs[index] = React.createRef();
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={option.sessionId + "/" + option.screenId}
|
||||||
|
ref={this.optionRefs[index]}
|
||||||
|
className={cn("search-option unselectable", {
|
||||||
|
"focused-option": this.focusedIdx.get() === index,
|
||||||
|
})}
|
||||||
|
onClick={() => this.handleSelect(index)}
|
||||||
|
>
|
||||||
|
<div className={cn("icon", "color-" + option.color)}>{this.renderIcon(option)}</div>
|
||||||
|
<div className="tabname">
|
||||||
|
#{option.sessionName} / {option.screenName}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let option: SwitcherDataType;
|
||||||
|
let index: number;
|
||||||
|
return (
|
||||||
|
<Modal className="tabswitcher-modal">
|
||||||
|
<div className="wave-modal-body">
|
||||||
|
<div className="textfield-wrapper">
|
||||||
|
<TextField
|
||||||
|
onChange={this.handleSearch}
|
||||||
|
maxLength={400}
|
||||||
|
autoFocus={true}
|
||||||
|
decoration={{
|
||||||
|
startDecoration: (
|
||||||
|
<InputDecoration position="start">
|
||||||
|
<div className="tabswitcher-search-prefix">Switch to Tab:</div>
|
||||||
|
</InputDecoration>
|
||||||
|
),
|
||||||
|
endDecoration: (
|
||||||
|
<InputDecoration>
|
||||||
|
<Tooltip
|
||||||
|
message={`Type to filter workspaces and tabs.`}
|
||||||
|
icon={<i className="fa-sharp fa-regular fa-circle-question" />}
|
||||||
|
>
|
||||||
|
<i className="fa-sharp fa-regular fa-circle-question" />
|
||||||
|
</Tooltip>
|
||||||
|
</InputDecoration>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="list-container">
|
||||||
|
<div ref={this.listWrapperRef} className="list-container-inner">
|
||||||
|
<div className="options-list">
|
||||||
|
<For each="option" index="index" of={this.sOptions}>
|
||||||
|
{this.renderOption(option, index)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { TabSwitcherModal };
|
102
src/app/common/modals/tos.less
Normal file
102
src/app/common/modals/tos.less
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
@import "../../../app/common/themes/themes.less";
|
||||||
|
|
||||||
|
.tos-modal {
|
||||||
|
width: 640px;
|
||||||
|
|
||||||
|
.wave-modal-content .wave-modal-body {
|
||||||
|
padding: 32px 48px;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.wave-modal-body-inner {
|
||||||
|
gap: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
header.tos-header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--sizing-sm, 12px);
|
||||||
|
border-bottom: none;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-subtitle {
|
||||||
|
color: @term-white;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content.tos-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 32px;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.item-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
flex: 1 0 0;
|
||||||
|
|
||||||
|
.item-title {
|
||||||
|
color: @term-bright-white;
|
||||||
|
font-style: normal;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-text {
|
||||||
|
color: @term-white;
|
||||||
|
font-style: normal;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-field {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
.item-text {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
button {
|
||||||
|
font-size: 12.5px !important;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.disabled-button {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
130
src/app/common/modals/tos.tsx
Normal file
130
src/app/common/modals/tos.tsx
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
// Copyright 2023, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as mobxReact from "mobx-react";
|
||||||
|
import { boundMethod } from "autobind-decorator";
|
||||||
|
import { GlobalModel, GlobalCommandRunner } from "../../../model/model";
|
||||||
|
import { Toggle, Modal, Button } from "../common";
|
||||||
|
import * as util from "../../../util/util";
|
||||||
|
import { ClientDataType } from "../../../types/types";
|
||||||
|
|
||||||
|
import shield from "../../assets/icons/shield_check.svg";
|
||||||
|
import help from "../../assets/icons/help_filled.svg";
|
||||||
|
import github from "../../assets/icons/github.svg";
|
||||||
|
|
||||||
|
import "./tos.less";
|
||||||
|
|
||||||
|
@mobxReact.observer
|
||||||
|
class TosModal extends React.Component<{}, {}> {
|
||||||
|
@boundMethod
|
||||||
|
acceptTos(): void {
|
||||||
|
GlobalCommandRunner.clientAcceptTos();
|
||||||
|
GlobalModel.modalsModel.popModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleChangeTelemetry(val: boolean): void {
|
||||||
|
if (val) {
|
||||||
|
GlobalCommandRunner.telemetryOn(false);
|
||||||
|
} else {
|
||||||
|
GlobalCommandRunner.telemetryOff(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let cdata: ClientDataType = GlobalModel.clientData.get();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal className="tos-modal">
|
||||||
|
<div className="wave-modal-body">
|
||||||
|
<div className="wave-modal-body-inner">
|
||||||
|
<header className="tos-header unselectable">
|
||||||
|
<div className="modal-title">Welcome to Wave Terminal!</div>
|
||||||
|
<div className="modal-subtitle">Lets set everything for you</div>
|
||||||
|
</header>
|
||||||
|
<div className="content tos-content unselectable">
|
||||||
|
<div className="item">
|
||||||
|
<img src={shield} alt="Privacy" />
|
||||||
|
<div className="item-inner">
|
||||||
|
<div className="item-title">Telemetry</div>
|
||||||
|
<div className="item-text">
|
||||||
|
We only collect minimal <i>anonymous</i> telemetry data to help us understand
|
||||||
|
how many people are using Wave.
|
||||||
|
</div>
|
||||||
|
<div className="item-field" style={{ marginTop: 2 }}>
|
||||||
|
<Toggle
|
||||||
|
checked={!cdata.clientopts.notelemetry}
|
||||||
|
onChange={this.handleChangeTelemetry}
|
||||||
|
/>
|
||||||
|
<div className="item-label">
|
||||||
|
Telemetry {cdata.clientopts.notelemetry ? "Disabled" : "Enabled"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="item">
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
href={util.makeExternLink("https://discord.gg/XfvZ334gwU")}
|
||||||
|
rel={"noopener"}
|
||||||
|
>
|
||||||
|
<img src={help} alt="Help" />
|
||||||
|
</a>
|
||||||
|
<div className="item-inner">
|
||||||
|
<div className="item-title">Join our Community</div>
|
||||||
|
<div className="item-text">
|
||||||
|
Get help, submit feature requests, report bugs, or just chat with fellow
|
||||||
|
terminal enthusiasts.
|
||||||
|
<br />
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
href={util.makeExternLink("https://discord.gg/XfvZ334gwU")}
|
||||||
|
rel={"noopener"}
|
||||||
|
>
|
||||||
|
Join the Wave Discord Channel
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="item">
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
href={util.makeExternLink("https://github.com/wavetermdev/waveterm")}
|
||||||
|
rel={"noopener"}
|
||||||
|
>
|
||||||
|
<img src={github} alt="Github" />
|
||||||
|
</a>
|
||||||
|
<div className="item-inner">
|
||||||
|
<div className="item-title">Support us on GitHub</div>
|
||||||
|
<div className="item-text">
|
||||||
|
We're <i>open source</i> and committed to providing a free terminal for
|
||||||
|
individual users. Please show your support us by giving us a star on{" "}
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
href={util.makeExternLink("https://github.com/wavetermdev/waveterm")}
|
||||||
|
rel={"noopener"}
|
||||||
|
>
|
||||||
|
Github (wavetermdev/waveterm)
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer className="unselectable">
|
||||||
|
<div className="item-text">
|
||||||
|
By continuing, I accept the
|
||||||
|
<a href="https://www.waveterm.dev/tos">Terms of Service</a>
|
||||||
|
</div>
|
||||||
|
<div className="button-wrapper">
|
||||||
|
<Button onClick={this.acceptTos}>Continue</Button>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { TosModal };
|
121
src/app/common/modals/viewremoteconndetail.less
Normal file
121
src/app/common/modals/viewremoteconndetail.less
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
@import "../../../app/common/themes/themes.less";
|
||||||
|
|
||||||
|
.rconndetail-modal {
|
||||||
|
width: 631px;
|
||||||
|
min-height: 565px;
|
||||||
|
|
||||||
|
.wave-modal-content {
|
||||||
|
display: flex;
|
||||||
|
padding-bottom: 0px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.wave-modal-body {
|
||||||
|
display: flex;
|
||||||
|
padding: 0px 20px;
|
||||||
|
align-items: flex-start;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
align-self: stretch;
|
||||||
|
|
||||||
|
.name-header-actions-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.name-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rconndetail-name {
|
||||||
|
color: @term-bright-white;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
.wave-button {
|
||||||
|
padding: 4px 15px;
|
||||||
|
font-size: 11px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-detail {
|
||||||
|
.settings-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.settings-label {
|
||||||
|
font-weight: bold;
|
||||||
|
width: 12em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-input {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
color: @term-white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-field:not(:first-child) {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
display: flex;
|
||||||
|
height: 30px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
align-self: stretch;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(241, 246, 243, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 5px;
|
||||||
|
|
||||||
|
.terminal-connectelem {
|
||||||
|
height: 163px !important; // Needed to override plugin height
|
||||||
|
|
||||||
|
.xterm-viewport {
|
||||||
|
display: flex;
|
||||||
|
padding: 6px 10px;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: flex-start;
|
||||||
|
align-self: stretch;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--element-separator, rgba(241, 246, 243, 0.15));
|
||||||
|
background: #080a08;
|
||||||
|
height: 163px !important; // Needed to override plugin height
|
||||||
|
}
|
||||||
|
|
||||||
|
.xterm-screen {
|
||||||
|
padding: 10px;
|
||||||
|
width: 541px !important; // Needed to override plugin width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
421
src/app/common/modals/viewremoteconndetail.tsx
Normal file
421
src/app/common/modals/viewremoteconndetail.tsx
Normal file
@ -0,0 +1,421 @@
|
|||||||
|
// Copyright 2023, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as mobxReact from "mobx-react";
|
||||||
|
import * as mobx from "mobx";
|
||||||
|
import { boundMethod } from "autobind-decorator";
|
||||||
|
import { If, For } from "tsx-control-statements/components";
|
||||||
|
import cn from "classnames";
|
||||||
|
import { GlobalModel, GlobalCommandRunner, RemotesModel } from "../../../model/model";
|
||||||
|
import * as T from "../../../types/types";
|
||||||
|
import { Modal, Tooltip, Button, Status } from "../common";
|
||||||
|
import * as util from "../../../util/util";
|
||||||
|
import * as textmeasure from "../../../util/textmeasure";
|
||||||
|
|
||||||
|
import "./viewremoteconndetail.less";
|
||||||
|
|
||||||
|
const RemotePtyRows = 9;
|
||||||
|
const RemotePtyCols = 80;
|
||||||
|
|
||||||
|
@mobxReact.observer
|
||||||
|
class ViewRemoteConnDetailModal extends React.Component<{}, {}> {
|
||||||
|
termRef: React.RefObject<any> = React.createRef();
|
||||||
|
model: RemotesModel;
|
||||||
|
|
||||||
|
constructor(props: { remotesModel?: RemotesModel }) {
|
||||||
|
super(props);
|
||||||
|
this.model = GlobalModel.remotesModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mobx.computed
|
||||||
|
getSelectedRemote(): T.RemoteType {
|
||||||
|
const selectedRemoteId = this.model.selectedRemoteId.get();
|
||||||
|
return GlobalModel.getRemote(selectedRemoteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
let elem = this.termRef.current;
|
||||||
|
if (elem == null) {
|
||||||
|
console.log("ERROR null term-remote element");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.model.createTermWrap(elem);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
if (this.getSelectedRemote() == null || this.getSelectedRemote().archived) {
|
||||||
|
this.model.deSelectRemote();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.model.disposeTerm();
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
clickTermBlock(): void {
|
||||||
|
if (this.model.remoteTermWrap != null) {
|
||||||
|
this.model.remoteTermWrap.giveFocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getRemoteTypeStr(remote: T.RemoteType): string {
|
||||||
|
if (!util.isBlank(remote.uname)) {
|
||||||
|
let unameStr = remote.uname;
|
||||||
|
unameStr = unameStr.replace("|", ", ");
|
||||||
|
return remote.remotetype + " (" + unameStr + ")";
|
||||||
|
}
|
||||||
|
return remote.remotetype;
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
connectRemote(remoteId: string) {
|
||||||
|
GlobalCommandRunner.connectRemote(remoteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
disconnectRemote(remoteId: string) {
|
||||||
|
GlobalCommandRunner.disconnectRemote(remoteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
installRemote(remoteId: string) {
|
||||||
|
GlobalCommandRunner.installRemote(remoteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
cancelInstall(remoteId: string) {
|
||||||
|
GlobalCommandRunner.installCancelRemote(remoteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
openEditModal(): void {
|
||||||
|
GlobalModel.remotesModel.startEditAuth();
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
getStatus(status: string) {
|
||||||
|
switch (status) {
|
||||||
|
case "connected":
|
||||||
|
return "green";
|
||||||
|
case "disconnected":
|
||||||
|
return "gray";
|
||||||
|
default:
|
||||||
|
return "red";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
clickArchive(): void {
|
||||||
|
if (this.getSelectedRemote() && this.getSelectedRemote().status == "connected") {
|
||||||
|
GlobalModel.showAlert({ message: "Cannot delete when connected. Disconnect and try again." });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let prtn = GlobalModel.showAlert({
|
||||||
|
message: "Are you sure you want to delete this connection?",
|
||||||
|
confirm: true,
|
||||||
|
});
|
||||||
|
prtn.then((confirm) => {
|
||||||
|
if (!confirm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this.getSelectedRemote()) {
|
||||||
|
GlobalCommandRunner.archiveRemote(this.getSelectedRemote().remoteid);
|
||||||
|
}
|
||||||
|
GlobalModel.modalsModel.popModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
clickReinstall(): void {
|
||||||
|
GlobalCommandRunner.installRemote(this.getSelectedRemote().remoteid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleClose(): void {
|
||||||
|
this.model.closeModal();
|
||||||
|
this.model.setRecentConnAdded(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderInstallStatus(remote: T.RemoteType): any {
|
||||||
|
let statusStr: string = null;
|
||||||
|
if (remote.installstatus == "disconnected") {
|
||||||
|
if (remote.needsmshellupgrade) {
|
||||||
|
statusStr = "mshell " + remote.mshellversion + " - needs upgrade";
|
||||||
|
} else if (util.isBlank(remote.mshellversion)) {
|
||||||
|
statusStr = "mshell unknown";
|
||||||
|
} else {
|
||||||
|
statusStr = "mshell " + remote.mshellversion + " - current";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
statusStr = remote.installstatus;
|
||||||
|
}
|
||||||
|
if (statusStr == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div key="install-status" className="settings-field">
|
||||||
|
<div className="settings-label"> Install Status</div>
|
||||||
|
<div className="settings-input">{statusStr}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderHeaderBtns(remote: T.RemoteType): React.ReactNode {
|
||||||
|
let buttons: React.ReactNode[] = [];
|
||||||
|
const disconnectButton = (
|
||||||
|
<Button theme="secondary" onClick={() => this.disconnectRemote(remote.remoteid)}>
|
||||||
|
Disconnect Now
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
const connectButton = (
|
||||||
|
<Button theme="secondary" onClick={() => this.connectRemote(remote.remoteid)}>
|
||||||
|
Connect Now
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
const tryReconnectButton = (
|
||||||
|
<Button theme="secondary" onClick={() => this.connectRemote(remote.remoteid)}>
|
||||||
|
Try Reconnect
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
let updateAuthButton = (
|
||||||
|
<Button theme="secondary" onClick={() => this.openEditModal()}>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
let cancelInstallButton = (
|
||||||
|
<Button theme="secondary" onClick={() => this.cancelInstall(remote.remoteid)}>
|
||||||
|
Cancel Install
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
let installNowButton = (
|
||||||
|
<Button theme="secondary" onClick={() => this.installRemote(remote.remoteid)}>
|
||||||
|
Install Now
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
let archiveButton = (
|
||||||
|
<Button theme="secondary" onClick={() => this.clickArchive()}>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
const reinstallButton = (
|
||||||
|
<Button theme="secondary" onClick={this.clickReinstall}>
|
||||||
|
Reinstall
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
if (remote.local) {
|
||||||
|
installNowButton = <></>;
|
||||||
|
updateAuthButton = <></>;
|
||||||
|
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
|
||||||
|
<Tooltip
|
||||||
|
message={
|
||||||
|
<span>
|
||||||
|
Connections imported from an ssh config file can be deleted, but will come back upon
|
||||||
|
importing again. They will stay removed 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>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (remote.status == "connected" || remote.status == "connecting") {
|
||||||
|
buttons.push(disconnectButton);
|
||||||
|
} else if (remote.status == "disconnected") {
|
||||||
|
buttons.push(connectButton);
|
||||||
|
} else if (remote.status == "error") {
|
||||||
|
if (remote.needsmshellupgrade) {
|
||||||
|
if (remote.installstatus == "connecting") {
|
||||||
|
buttons.push(cancelInstallButton);
|
||||||
|
} else {
|
||||||
|
buttons.push(installNowButton);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
buttons.push(tryReconnectButton);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buttons.push(reinstallButton);
|
||||||
|
buttons.push(updateAuthButton);
|
||||||
|
buttons.push(archiveButton);
|
||||||
|
|
||||||
|
let i = 0;
|
||||||
|
let button: React.ReactNode = null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<For each="button" of={buttons} index="i">
|
||||||
|
<div key={i}>{button}</div>
|
||||||
|
</For>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getMessage(remote: T.RemoteType): string {
|
||||||
|
let message = "";
|
||||||
|
if (remote.status == "connected") {
|
||||||
|
message = "Connected and ready to run commands.";
|
||||||
|
} else if (remote.status == "connecting") {
|
||||||
|
message = remote.waitingforpassword ? "Connecting, waiting for user-input..." : "Connecting...";
|
||||||
|
let connectTimeout = remote.connecttimeout ?? 0;
|
||||||
|
message = message + " (" + connectTimeout + "s)";
|
||||||
|
} else if (remote.status == "disconnected") {
|
||||||
|
message = "Disconnected";
|
||||||
|
} else if (remote.status == "error") {
|
||||||
|
if (remote.noinitpk) {
|
||||||
|
message = "Error, could not connect.";
|
||||||
|
} else if (remote.needsmshellupgrade) {
|
||||||
|
if (remote.installstatus == "connecting") {
|
||||||
|
message = "Installing...";
|
||||||
|
} else {
|
||||||
|
message = "Error, needs install.";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message = "Error";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let remote = this.getSelectedRemote();
|
||||||
|
|
||||||
|
if (remote == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let model = this.model;
|
||||||
|
let isTermFocused = this.model.remoteTermWrapFocus.get();
|
||||||
|
let termFontSize = GlobalModel.termFontSize.get();
|
||||||
|
let termWidth = textmeasure.termWidthFromCols(RemotePtyCols, termFontSize);
|
||||||
|
let remoteAliasText = util.isBlank(remote.remotealias) ? "(none)" : remote.remotealias;
|
||||||
|
let selectedRemoteStatus = this.getSelectedRemote().status;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal className="rconndetail-modal">
|
||||||
|
<Modal.Header title="Connection" onClose={this.handleClose} />
|
||||||
|
<div className="wave-modal-body">
|
||||||
|
<div className="name-header-actions-wrapper">
|
||||||
|
<div className="name text-primary name-wrapper">
|
||||||
|
{util.getRemoteName(remote)} {getImportTooltip(remote)}
|
||||||
|
</div>
|
||||||
|
<div className="header-actions">{this.renderHeaderBtns(remote)}</div>
|
||||||
|
</div>
|
||||||
|
<div className="remote-detail" style={{ overflow: "hidden" }}>
|
||||||
|
<div className="settings-field">
|
||||||
|
<div className="settings-label">Conn Id</div>
|
||||||
|
<div className="settings-input">{remote.remoteid}</div>
|
||||||
|
</div>
|
||||||
|
<div className="settings-field">
|
||||||
|
<div className="settings-label">Type</div>
|
||||||
|
<div className="settings-input">{this.getRemoteTypeStr(remote)}</div>
|
||||||
|
</div>
|
||||||
|
<div className="settings-field">
|
||||||
|
<div className="settings-label">Canonical Name</div>
|
||||||
|
<div className="settings-input">
|
||||||
|
{remote.remotecanonicalname}
|
||||||
|
<If condition={!util.isBlank(remote.remotevars.port) && remote.remotevars.port != "22"}>
|
||||||
|
<span style={{ marginLeft: 5 }}>(port {remote.remotevars.port})</span>
|
||||||
|
</If>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="settings-field" style={{ minHeight: 24 }}>
|
||||||
|
<div className="settings-label">Alias</div>
|
||||||
|
<div className="settings-input">{remoteAliasText}</div>
|
||||||
|
</div>
|
||||||
|
<div className="settings-field">
|
||||||
|
<div className="settings-label">Auth Type</div>
|
||||||
|
<div className="settings-input">
|
||||||
|
<If condition={!remote.local}>{remote.authtype}</If>
|
||||||
|
<If condition={remote.local}>local</If>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="settings-field">
|
||||||
|
<div className="settings-label">Connect Mode</div>
|
||||||
|
<div className="settings-input">{remote.connectmode}</div>
|
||||||
|
</div>
|
||||||
|
{this.renderInstallStatus(remote)}
|
||||||
|
<div className="flex-spacer" style={{ minHeight: 20 }} />
|
||||||
|
<div className="status">
|
||||||
|
<Status status={this.getStatus(remote.status)} text={this.getMessage(remote)} />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
key="term"
|
||||||
|
className={cn(
|
||||||
|
"terminal-wrapper",
|
||||||
|
{ focus: isTermFocused },
|
||||||
|
remote != null ? "status-" + remote.status : null
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<If condition={!isTermFocused}>
|
||||||
|
<div key="termblock" className="term-block" onClick={this.clickTermBlock}></div>
|
||||||
|
</If>
|
||||||
|
<If condition={model.showNoInputMsg.get()}>
|
||||||
|
<div key="termtag" className="term-tag">
|
||||||
|
input is only allowed while status is 'connecting'
|
||||||
|
</div>
|
||||||
|
</If>
|
||||||
|
<div
|
||||||
|
key="terminal"
|
||||||
|
className="terminal-connectelem"
|
||||||
|
ref={this.termRef}
|
||||||
|
data-remoteid={remote.remoteid}
|
||||||
|
style={{
|
||||||
|
height: textmeasure.termHeightFromRows(RemotePtyRows, termFontSize),
|
||||||
|
width: termWidth,
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="wave-modal-footer">
|
||||||
|
<Button
|
||||||
|
theme="secondary"
|
||||||
|
disabled={selectedRemoteStatus == "connecting"}
|
||||||
|
onClick={this.handleClose}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button disabled={selectedRemoteStatus == "connecting"} onClick={this.handleClose}>
|
||||||
|
Done
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getImportTooltip(remote: T.RemoteType): React.ReactElement<any, any> {
|
||||||
|
if (remote.sshconfigsrc == "sshconfig-import") {
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
message={`This remote was imported from an SSH config file.`}
|
||||||
|
icon={<i className="fa-sharp fa-solid fa-file-import" />}
|
||||||
|
>
|
||||||
|
<i className="fa-sharp fa-solid fa-file-import" />
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ViewRemoteConnDetailModal };
|
@ -11,6 +11,7 @@ import { GlobalModel, RemotesModel, GlobalCommandRunner } from "../../model/mode
|
|||||||
import { Button, IconButton, Status } from "../common/common";
|
import { Button, IconButton, Status } from "../common/common";
|
||||||
import * as T from "../../types/types";
|
import * as T from "../../types/types";
|
||||||
import * as util from "../../util/util";
|
import * as util from "../../util/util";
|
||||||
|
import * as appconst from "../appconst";
|
||||||
|
|
||||||
import "./connections.less";
|
import "./connections.less";
|
||||||
|
|
||||||
@ -74,10 +75,31 @@ class ConnectionsView extends React.Component<{ model: RemotesModel }, { hovered
|
|||||||
}
|
}
|
||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
handleImportSshConfig(): void {
|
importSshConfig(): void {
|
||||||
GlobalCommandRunner.importSshConfig();
|
GlobalCommandRunner.importSshConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
handleImportSshConfig(): void {
|
||||||
|
this.showShellPrompt(this.importSshConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
showShellPrompt(cb: () => void): void {
|
||||||
|
let prtn = GlobalModel.showAlert({
|
||||||
|
message:
|
||||||
|
"You are about to install WaveShell on a remote machine. Please be aware that WaveShell will be executed on the remote system.",
|
||||||
|
confirm: true,
|
||||||
|
confirmflag: appconst.ConfirmKey_HideShellPrompt,
|
||||||
|
});
|
||||||
|
prtn.then((confirm) => {
|
||||||
|
if (!confirm) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cb();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
handleRead(remoteId: string): void {
|
handleRead(remoteId: string): void {
|
||||||
GlobalModel.remotesModel.openReadModal(remoteId);
|
GlobalModel.remotesModel.openReadModal(remoteId);
|
||||||
@ -163,8 +185,8 @@ class ConnectionsView extends React.Component<{ model: RemotesModel }, { hovered
|
|||||||
onClick={() => this.handleRead(item.remoteid)} // Moved onClick here
|
onClick={() => this.handleRead(item.remoteid)} // Moved onClick here
|
||||||
>
|
>
|
||||||
<td className="col-name">
|
<td className="col-name">
|
||||||
<Status status={this.getStatus(item.status)} text=""></Status>
|
<Status status={this.getStatus(item.status)} text=""></Status>
|
||||||
{this.getName(item)} {this.getImportSymbol(item)}
|
{this.getName(item)} {this.getImportSymbol(item)}
|
||||||
</td>
|
</td>
|
||||||
<td className="col-type">
|
<td className="col-type">
|
||||||
<div>{item.remotetype}</div>
|
<div>{item.remotetype}</div>
|
||||||
|
@ -354,6 +354,12 @@ class LineCmd extends React.Component<
|
|||||||
GlobalCommandRunner.lineBookmark(line.lineid);
|
GlobalCommandRunner.lineBookmark(line.lineid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@boundMethod
|
||||||
|
clickDelete() {
|
||||||
|
let { line } = this.props;
|
||||||
|
GlobalCommandRunner.lineDelete(line.lineid, true);
|
||||||
|
}
|
||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
clickMinimize() {
|
clickMinimize() {
|
||||||
mobx.action(() => {
|
mobx.action(() => {
|
||||||
@ -659,6 +665,9 @@ class LineCmd extends React.Component<
|
|||||||
{this.renderMeta1(cmd)}
|
{this.renderMeta1(cmd)}
|
||||||
<If condition={!hidePrompt}>{this.renderCmdText(cmd)}</If>
|
<If condition={!hidePrompt}>{this.renderCmdText(cmd)}</If>
|
||||||
</div>
|
</div>
|
||||||
|
<div key="delete" title="Delete Line (⌘D)" className="line-icon" onClick={this.clickDelete}>
|
||||||
|
<i className="fa-sharp fa-regular fa-trash" />
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
key="bookmark"
|
key="bookmark"
|
||||||
title="Bookmark"
|
title="Bookmark"
|
||||||
|
219
src/app/workspace/cmdinput/aichat.tsx
Normal file
219
src/app/workspace/cmdinput/aichat.tsx
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
// Copyright 2023, Command Line Inc.
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as mobxReact from "mobx-react";
|
||||||
|
import * as mobx from "mobx";
|
||||||
|
import { GlobalModel } from "../../../model/model";
|
||||||
|
import { isBlank } from "../../../util/util";
|
||||||
|
import { boundMethod } from "autobind-decorator";
|
||||||
|
import cn from "classnames";
|
||||||
|
import { Prompt } from "../../common/prompt/prompt";
|
||||||
|
import { TextAreaInput } from "./textareainput";
|
||||||
|
import { If, For } from "tsx-control-statements/components";
|
||||||
|
import type { OpenAICmdInfoChatMessageType } from "../../../types/types";
|
||||||
|
import { Markdown } from "../../common/common";
|
||||||
|
|
||||||
|
@mobxReact.observer
|
||||||
|
class AIChat extends React.Component<{}, {}> {
|
||||||
|
chatListKeyCount: number = 0;
|
||||||
|
textAreaNumLines: mobx.IObservableValue<number> = mobx.observable.box(1, { name: "textAreaNumLines" });
|
||||||
|
chatWindowScrollRef: React.RefObject<HTMLDivElement>;
|
||||||
|
textAreaRef: React.RefObject<HTMLTextAreaElement>;
|
||||||
|
|
||||||
|
constructor(props: any) {
|
||||||
|
super(props);
|
||||||
|
this.chatWindowScrollRef = React.createRef();
|
||||||
|
this.textAreaRef = React.createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
let model = GlobalModel;
|
||||||
|
let inputModel = model.inputModel;
|
||||||
|
if (this.chatWindowScrollRef != null && this.chatWindowScrollRef.current != null) {
|
||||||
|
this.chatWindowScrollRef.current.scrollTop = this.chatWindowScrollRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
if (this.textAreaRef.current != null) {
|
||||||
|
this.textAreaRef.current.focus();
|
||||||
|
inputModel.setCmdInfoChatRefs(this.textAreaRef, this.chatWindowScrollRef);
|
||||||
|
}
|
||||||
|
this.requestChatUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate() {
|
||||||
|
if (this.chatWindowScrollRef != null && this.chatWindowScrollRef.current != null) {
|
||||||
|
this.chatWindowScrollRef.current.scrollTop = this.chatWindowScrollRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestChatUpdate() {
|
||||||
|
this.submitChatMessage("");
|
||||||
|
}
|
||||||
|
|
||||||
|
submitChatMessage(messageStr: string) {
|
||||||
|
let model = GlobalModel;
|
||||||
|
let inputModel = model.inputModel;
|
||||||
|
let curLine = inputModel.getCurLine();
|
||||||
|
let prtn = GlobalModel.submitChatInfoCommand(messageStr, curLine, false);
|
||||||
|
prtn.then((rtn) => {
|
||||||
|
if (!rtn.success) {
|
||||||
|
console.log("submit chat command error: " + rtn.error);
|
||||||
|
}
|
||||||
|
}).catch((error) => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
getLinePos(elem: any): { numLines: number; linePos: number } {
|
||||||
|
let numLines = elem.value.split("\n").length;
|
||||||
|
let linePos = elem.value.substr(0, elem.selectionStart).split("\n").length;
|
||||||
|
return { numLines, linePos };
|
||||||
|
}
|
||||||
|
|
||||||
|
@mobx.action
|
||||||
|
@boundMethod
|
||||||
|
onKeyDown(e: any) {
|
||||||
|
mobx.action(() => {
|
||||||
|
let model = GlobalModel;
|
||||||
|
let inputModel = model.inputModel;
|
||||||
|
let ctrlMod = e.getModifierState("Control") || e.getModifierState("Meta") || e.getModifierState("Shift");
|
||||||
|
let resetCodeSelect = !ctrlMod;
|
||||||
|
|
||||||
|
if (e.code == "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!ctrlMod) {
|
||||||
|
if (inputModel.getCodeSelectSelectedIndex() == -1) {
|
||||||
|
let messageStr = e.target.value;
|
||||||
|
this.submitChatMessage(messageStr);
|
||||||
|
e.target.value = "";
|
||||||
|
} else {
|
||||||
|
inputModel.grabCodeSelectSelection();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
e.target.setRangeText("\n", e.target.selectionStart, e.target.selectionEnd, "end");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (e.code == "Escape") {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
inputModel.closeAIAssistantChat();
|
||||||
|
}
|
||||||
|
if (e.code == "KeyL" && e.getModifierState("Control")) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
inputModel.clearAIAssistantChat();
|
||||||
|
}
|
||||||
|
if (e.code == "ArrowUp") {
|
||||||
|
if (this.getLinePos(e.target).linePos > 1) {
|
||||||
|
// normal up arrow
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
inputModel.codeSelectSelectNextOldestCodeBlock();
|
||||||
|
resetCodeSelect = false;
|
||||||
|
}
|
||||||
|
if (e.code == "ArrowDown") {
|
||||||
|
if (inputModel.getCodeSelectSelectedIndex() == inputModel.codeSelectBottom) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
inputModel.codeSelectSelectNextNewestCodeBlock();
|
||||||
|
resetCodeSelect = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resetCodeSelect) {
|
||||||
|
inputModel.codeSelectDeselectAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
// set height of textarea based on number of newlines
|
||||||
|
this.textAreaNumLines.set(e.target.value.split(/\n/).length);
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
renderError(err: string): any {
|
||||||
|
return <div className="chat-msg-error">{err}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderChatMessage(chatItem: OpenAICmdInfoChatMessageType): any {
|
||||||
|
let curKey = "chatmsg-" + this.chatListKeyCount;
|
||||||
|
this.chatListKeyCount++;
|
||||||
|
let senderClassName = chatItem.isassistantresponse ? "chat-msg-assistant" : "chat-msg-user";
|
||||||
|
let msgClassName = "chat-msg " + senderClassName;
|
||||||
|
let innerHTML: React.JSX.Element = (
|
||||||
|
<span>
|
||||||
|
<span style={{ display: "flex" }}>
|
||||||
|
<i className="fa-sharp fa-solid fa-user" style={{ marginRight: "5px", marginTop: "1px" }}></i>
|
||||||
|
<p style={{ marginRight: "5px" }}>You</p>
|
||||||
|
</span>
|
||||||
|
<p className="msg-text">{chatItem.userquery}</p>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
if (chatItem.isassistantresponse) {
|
||||||
|
if (chatItem.assistantresponse.error != null && chatItem.assistantresponse.error != "") {
|
||||||
|
innerHTML = this.renderError(chatItem.assistantresponse.error);
|
||||||
|
} else {
|
||||||
|
innerHTML = (
|
||||||
|
<span>
|
||||||
|
<span style={{ display: "flex" }}>
|
||||||
|
<i
|
||||||
|
className="fa-sharp fa-solid fa-headset"
|
||||||
|
style={{ marginRight: "5px", marginTop: "1px" }}
|
||||||
|
></i>
|
||||||
|
<p style={{ marginRight: "5px" }}>ChatGPT</p>
|
||||||
|
</span>
|
||||||
|
<Markdown text={chatItem.assistantresponse.message} codeSelect />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={msgClassName} key={curKey}>
|
||||||
|
{innerHTML}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderChatWindow(): any {
|
||||||
|
let model = GlobalModel;
|
||||||
|
let inputModel = model.inputModel;
|
||||||
|
let chatMessageItems = inputModel.AICmdInfoChatItems.slice();
|
||||||
|
let chitem: OpenAICmdInfoChatMessageType = null;
|
||||||
|
return (
|
||||||
|
<div className="chat-window" ref={this.chatWindowScrollRef}>
|
||||||
|
<For each="chitem" index="idx" of={chatMessageItems}>
|
||||||
|
{this.renderChatMessage(chitem)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let model = GlobalModel;
|
||||||
|
let inputModel = model.inputModel;
|
||||||
|
|
||||||
|
const termFontSize = 14;
|
||||||
|
const textAreaMaxLines = 4;
|
||||||
|
const textAreaLineHeight = termFontSize * 1.5;
|
||||||
|
const textAreaPadding = 2 * 0.5 * termFontSize;
|
||||||
|
let textAreaMaxHeight = textAreaLineHeight * textAreaMaxLines + textAreaPadding;
|
||||||
|
let textAreaInnerHeight = this.textAreaNumLines.get() * textAreaLineHeight + textAreaPadding;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="cmd-aichat">
|
||||||
|
{this.renderChatWindow()}
|
||||||
|
<textarea
|
||||||
|
key="main"
|
||||||
|
ref={this.textAreaRef}
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
|
id="chat-cmd-input"
|
||||||
|
onKeyDown={this.onKeyDown}
|
||||||
|
style={{ height: textAreaInnerHeight, maxHeight: textAreaMaxHeight, fontSize: termFontSize }}
|
||||||
|
className={cn("chat-textarea")}
|
||||||
|
placeholder="Send a Message to ChatGPT..."
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { AIChat };
|
@ -42,6 +42,10 @@
|
|||||||
max-height: max(300px, 70%);
|
max-height: max(300px, 70%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.has-aichat {
|
||||||
|
max-height: max(300px, 70%);
|
||||||
|
}
|
||||||
|
|
||||||
.remote-status-warning {
|
.remote-status-warning {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@ -196,6 +200,72 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cmd-aichat {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-flow: column nowrap;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
flex-shrink: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.chat-window {
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
flex-shrink: 1;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-textarea {
|
||||||
|
color: @term-bright-white;
|
||||||
|
background-color: @textarea-background;
|
||||||
|
padding: 0.5em;
|
||||||
|
resize: none;
|
||||||
|
overflow: auto;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
border-color: transparent;
|
||||||
|
border: none;
|
||||||
|
font-family: @terminal-font;
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-grow: 1;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
box-shadow: none;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-msg {
|
||||||
|
margin-top:5px;
|
||||||
|
margin-bottom:5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-msg-assistant {
|
||||||
|
color: @term-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-msg-user {
|
||||||
|
|
||||||
|
.msg-text {
|
||||||
|
font-family: @markdown-font;
|
||||||
|
font-size: 14px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-msg-error {
|
||||||
|
color: @term-bright-red;
|
||||||
|
font-family: @markdown-font;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.grow-spacer {
|
||||||
|
flex: 1 0 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.cmd-history {
|
.cmd-history {
|
||||||
color: @term-white;
|
color: @term-white;
|
||||||
|
@ -19,6 +19,7 @@ import { Prompt } from "../../common/prompt/prompt";
|
|||||||
import { ReactComponent as ExecIcon } from "../../assets/icons/exec.svg";
|
import { ReactComponent as ExecIcon } from "../../assets/icons/exec.svg";
|
||||||
import { ReactComponent as RotateIcon } from "../../assets/icons/line/rotate.svg";
|
import { ReactComponent as RotateIcon } from "../../assets/icons/line/rotate.svg";
|
||||||
import "./cmdinput.less";
|
import "./cmdinput.less";
|
||||||
|
import { AIChat } from "./aichat";
|
||||||
|
|
||||||
dayjs.extend(localizedFormat);
|
dayjs.extend(localizedFormat);
|
||||||
|
|
||||||
@ -116,6 +117,7 @@ class CmdInput extends React.Component<{}, {}> {
|
|||||||
}
|
}
|
||||||
let infoShow = inputModel.infoShow.get();
|
let infoShow = inputModel.infoShow.get();
|
||||||
let historyShow = !infoShow && inputModel.historyShow.get();
|
let historyShow = !infoShow && inputModel.historyShow.get();
|
||||||
|
let aiChatShow = inputModel.aIChatShow.get();
|
||||||
let infoMsg = inputModel.infoMsg.get();
|
let infoMsg = inputModel.infoMsg.get();
|
||||||
let hasInfo = infoMsg != null;
|
let hasInfo = infoMsg != null;
|
||||||
let focusVal = inputModel.physicalInputFocused.get();
|
let focusVal = inputModel.physicalInputFocused.get();
|
||||||
@ -127,11 +129,23 @@ class CmdInput extends React.Component<{}, {}> {
|
|||||||
numRunningLines = mobx.computed(() => win.getRunningCmdLines().length).get();
|
numRunningLines = mobx.computed(() => win.getRunningCmdLines().length).get();
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div ref={this.cmdInputRef} className={cn("cmd-input", { "has-info": infoShow }, { active: focusVal })}>
|
<div
|
||||||
|
ref={this.cmdInputRef}
|
||||||
|
className={cn(
|
||||||
|
"cmd-input",
|
||||||
|
{ "has-info": infoShow },
|
||||||
|
{ "has-aichat": aiChatShow },
|
||||||
|
{ active: focusVal }
|
||||||
|
)}
|
||||||
|
>
|
||||||
<If condition={historyShow}>
|
<If condition={historyShow}>
|
||||||
<div className="cmd-input-grow-spacer"></div>
|
<div className="cmd-input-grow-spacer"></div>
|
||||||
<HistoryInfo />
|
<HistoryInfo />
|
||||||
</If>
|
</If>
|
||||||
|
<If condition={aiChatShow}>
|
||||||
|
<div className="cmd-input-grow-spacer"></div>
|
||||||
|
<AIChat />
|
||||||
|
</If>
|
||||||
<InfoMsg key="infomsg" />
|
<InfoMsg key="infomsg" />
|
||||||
<If condition={remote && remote.status != "connected"}>
|
<If condition={remote && remote.status != "connected"}>
|
||||||
<div className="remote-status-warning">
|
<div className="remote-status-warning">
|
||||||
|
@ -230,6 +230,7 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
|||||||
if (inputModel.inputMode.get() != null) {
|
if (inputModel.inputMode.get() != null) {
|
||||||
inputModel.resetInputMode();
|
inputModel.resetInputMode();
|
||||||
}
|
}
|
||||||
|
inputModel.closeAIAssistantChat();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.code == "KeyE" && e.getModifierState("Meta")) {
|
if (e.code == "KeyE" && e.getModifierState("Meta")) {
|
||||||
@ -313,6 +314,10 @@ class TextAreaInput extends React.Component<{ screen: Screen; onHeightChange: ()
|
|||||||
scrollDiv(div, e.code == "PageUp" ? -amt : amt);
|
scrollDiv(div, e.code == "PageUp" ? -amt : amt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (e.code == "Space" && e.getModifierState("Control")) {
|
||||||
|
e.preventDefault();
|
||||||
|
inputModel.openAIAssistantChat();
|
||||||
|
}
|
||||||
// console.log(e.code, e.keyCode, e.key, event.which, ctrlMod, e);
|
// console.log(e.code, e.keyCode, e.key, event.which, ctrlMod, e);
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
@ -244,11 +244,15 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
overflow-x: hidden;
|
overflow-x: scroll;
|
||||||
|
|
||||||
&:hover,
|
&::-webkit-scrollbar-thumb,
|
||||||
&:focus {
|
&::-webkit-scrollbar-track {
|
||||||
overflow-x: overlay;
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover::-webkit-scrollbar-thumb {
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.screen-tab {
|
.screen-tab {
|
||||||
@ -321,6 +325,7 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
height: 37px;
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
|
@ -140,7 +140,7 @@ function getWaveSrvCmd() {
|
|||||||
let waveSrvPath = getWaveSrvPath();
|
let waveSrvPath = getWaveSrvPath();
|
||||||
let waveHome = getWaveHomeDir();
|
let waveHome = getWaveHomeDir();
|
||||||
let logFile = path.join(waveHome, "wavesrv.log");
|
let logFile = path.join(waveHome, "wavesrv.log");
|
||||||
return `${waveSrvPath} >> "${logFile}" 2>&1`;
|
return `"${waveSrvPath}" >> "${logFile}" 2>&1`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWaveSrvCwd() {
|
function getWaveSrvCwd() {
|
||||||
@ -173,10 +173,10 @@ let menuTemplate = [
|
|||||||
role: "appMenu",
|
role: "appMenu",
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: 'About Wave Terminal',
|
label: "About Wave Terminal",
|
||||||
click: () => {
|
click: () => {
|
||||||
MainWindow?.webContents.send('menu-item-about');
|
MainWindow?.webContents.send("menu-item-about");
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{ type: "separator" },
|
{ type: "separator" },
|
||||||
{ role: "services" },
|
{ role: "services" },
|
||||||
@ -250,7 +250,7 @@ function createMainWindow(clientData) {
|
|||||||
minWidth: 800,
|
minWidth: 800,
|
||||||
minHeight: 600,
|
minHeight: 600,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
icon: (unamePlatform == "linux") ? "public/logos/wave-logo-dark.png" : undefined,
|
icon: unamePlatform == "linux" ? "public/logos/wave-logo-dark.png" : undefined,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: path.join(getAppBasePath(), DistDir, "preload.js"),
|
preload: path.join(getAppBasePath(), DistDir, "preload.js"),
|
||||||
},
|
},
|
||||||
@ -302,6 +302,11 @@ function createMainWindow(clientData) {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (input.code == "KeyP" && input.meta) {
|
||||||
|
win.webContents.send("p-cmd", mods);
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (input.meta && (input.code == "ArrowUp" || input.code == "ArrowDown")) {
|
if (input.meta && (input.code == "ArrowUp" || input.code == "ArrowDown")) {
|
||||||
if (input.code == "ArrowUp") {
|
if (input.code == "ArrowUp") {
|
||||||
win.webContents.send("meta-arrowup");
|
win.webContents.send("meta-arrowup");
|
||||||
@ -480,6 +485,41 @@ electron.ipcMain.on("reload-window", (event) => {
|
|||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
electron.ipcMain.on("open-external-link", async (_, url) => {
|
||||||
|
try {
|
||||||
|
await electron.shell.openExternal(url);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("error opening external link", err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
electron.ipcMain.on("get-last-logs", async (event, numberOfLines) => {
|
||||||
|
try {
|
||||||
|
const logPath = path.join(getWaveHomeDir(), "wavesrv.log");
|
||||||
|
const lastLines = await readLastLinesOfFile(logPath, numberOfLines);
|
||||||
|
event.reply("last-logs", lastLines);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error reading log file:", err);
|
||||||
|
event.reply("last-logs", "Error reading log file.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function readLastLinesOfFile(filePath, lineCount) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
child_process.exec(`tail -n ${lineCount} "${filePath}"`, (err, stdout, stderr) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (stderr) {
|
||||||
|
reject(stderr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(stdout);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getContextMenu(): any {
|
function getContextMenu(): any {
|
||||||
let menu = new electron.Menu();
|
let menu = new electron.Menu();
|
||||||
let menuItem = new electron.MenuItem({ label: "Testing", click: () => console.log("click testing!") });
|
let menuItem = new electron.MenuItem({ label: "Testing", click: () => console.log("click testing!") });
|
||||||
@ -535,8 +575,8 @@ function sendWSSC() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function runWaveSrv() {
|
function runWaveSrv() {
|
||||||
let pResolve = null;
|
let pResolve: (value: unknown) => void;
|
||||||
let pReject = null;
|
let pReject: (reason?: any) => void;
|
||||||
let rtnPromise = new Promise((argResolve, argReject) => {
|
let rtnPromise = new Promise((argResolve, argReject) => {
|
||||||
pResolve = argResolve;
|
pResolve = argResolve;
|
||||||
pReject = argReject;
|
pReject = argReject;
|
||||||
@ -546,8 +586,9 @@ function runWaveSrv() {
|
|||||||
if (isDev) {
|
if (isDev) {
|
||||||
envCopy[WaveDevVarName] = "1";
|
envCopy[WaveDevVarName] = "1";
|
||||||
}
|
}
|
||||||
console.log("trying to run local server", getWaveSrvPath());
|
let waveSrvCmd = getWaveSrvCmd();
|
||||||
let proc = child_process.spawn("bash", ["-c", getWaveSrvCmd()], {
|
console.log("trying to run local server", waveSrvCmd);
|
||||||
|
let proc = child_process.spawn("bash", ["-c", waveSrvCmd], {
|
||||||
cwd: getWaveSrvCwd(),
|
cwd: getWaveSrvCwd(),
|
||||||
env: envCopy,
|
env: envCopy,
|
||||||
});
|
});
|
||||||
@ -555,7 +596,7 @@ function runWaveSrv() {
|
|||||||
console.log("wavesrv exit", e);
|
console.log("wavesrv exit", e);
|
||||||
waveSrvProc = null;
|
waveSrvProc = null;
|
||||||
sendWSSC();
|
sendWSSC();
|
||||||
pReject(new Error(sprintf("failed to start local server (%s)", getWaveSrvPath())));
|
pReject(new Error(sprintf("failed to start local server (%s)", waveSrvCmd)));
|
||||||
if (waveSrvShouldRestart) {
|
if (waveSrvShouldRestart) {
|
||||||
waveSrvShouldRestart = false;
|
waveSrvShouldRestart = false;
|
||||||
this.runWaveSrv();
|
this.runWaveSrv();
|
||||||
|
@ -6,13 +6,19 @@ contextBridge.exposeInMainWorld("api", {
|
|||||||
getIsDev: () => ipcRenderer.sendSync("get-isdev"),
|
getIsDev: () => ipcRenderer.sendSync("get-isdev"),
|
||||||
getAuthKey: () => ipcRenderer.sendSync("get-authkey"),
|
getAuthKey: () => ipcRenderer.sendSync("get-authkey"),
|
||||||
getWaveSrvStatus: () => ipcRenderer.sendSync("wavesrv-status"),
|
getWaveSrvStatus: () => ipcRenderer.sendSync("wavesrv-status"),
|
||||||
|
getLastLogs: (numberOfLines, callback) => {
|
||||||
|
ipcRenderer.send("get-last-logs", numberOfLines);
|
||||||
|
ipcRenderer.once("last-logs", (event, data) => callback(data));
|
||||||
|
},
|
||||||
restartWaveSrv: () => ipcRenderer.sendSync("restart-server"),
|
restartWaveSrv: () => ipcRenderer.sendSync("restart-server"),
|
||||||
reloadWindow: () => ipcRenderer.sendSync("reload-window"),
|
reloadWindow: () => ipcRenderer.sendSync("reload-window"),
|
||||||
|
openExternalLink: (url) => ipcRenderer.send("open-external-link", url),
|
||||||
onTCmd: (callback) => ipcRenderer.on("t-cmd", callback),
|
onTCmd: (callback) => ipcRenderer.on("t-cmd", callback),
|
||||||
onICmd: (callback) => ipcRenderer.on("i-cmd", callback),
|
onICmd: (callback) => ipcRenderer.on("i-cmd", callback),
|
||||||
onLCmd: (callback) => ipcRenderer.on("l-cmd", callback),
|
onLCmd: (callback) => ipcRenderer.on("l-cmd", callback),
|
||||||
onHCmd: (callback) => ipcRenderer.on("h-cmd", callback),
|
onHCmd: (callback) => ipcRenderer.on("h-cmd", callback),
|
||||||
onWCmd: (callback) => ipcRenderer.on("w-cmd", callback),
|
onWCmd: (callback) => ipcRenderer.on("w-cmd", callback),
|
||||||
|
onPCmd: (callback) => ipcRenderer.on("p-cmd", callback),
|
||||||
onMetaArrowUp: (callback) => ipcRenderer.on("meta-arrowup", callback),
|
onMetaArrowUp: (callback) => ipcRenderer.on("meta-arrowup", callback),
|
||||||
onMetaArrowDown: (callback) => ipcRenderer.on("meta-arrowdown", callback),
|
onMetaArrowDown: (callback) => ipcRenderer.on("meta-arrowdown", callback),
|
||||||
onMetaPageUp: (callback) => ipcRenderer.on("meta-pageup", callback),
|
onMetaPageUp: (callback) => ipcRenderer.on("meta-pageup", callback),
|
||||||
|
@ -9,6 +9,8 @@ import { boundMethod } from "autobind-decorator";
|
|||||||
import { debounce } from "throttle-debounce";
|
import { debounce } from "throttle-debounce";
|
||||||
import {
|
import {
|
||||||
handleJsonFetchResponse,
|
handleJsonFetchResponse,
|
||||||
|
base64ToString,
|
||||||
|
stringToBase64,
|
||||||
base64ToArray,
|
base64ToArray,
|
||||||
genMergeData,
|
genMergeData,
|
||||||
genMergeDataMap,
|
genMergeDataMap,
|
||||||
@ -63,6 +65,7 @@ import type {
|
|||||||
CommandRtnType,
|
CommandRtnType,
|
||||||
WebCmd,
|
WebCmd,
|
||||||
WebRemote,
|
WebRemote,
|
||||||
|
OpenAICmdInfoChatMessageType,
|
||||||
} from "../types/types";
|
} from "../types/types";
|
||||||
import * as T from "../types/types";
|
import * as T from "../types/types";
|
||||||
import { WSControl } from "./ws";
|
import { WSControl } from "./ws";
|
||||||
@ -78,7 +81,7 @@ import localizedFormat from "dayjs/plugin/localizedFormat";
|
|||||||
import customParseFormat from "dayjs/plugin/customParseFormat";
|
import customParseFormat from "dayjs/plugin/customParseFormat";
|
||||||
import { getRendererContext, cmdStatusIsRunning } from "../app/line/lineutil";
|
import { getRendererContext, cmdStatusIsRunning } from "../app/line/lineutil";
|
||||||
import { MagicLayout } from "../app/magiclayout";
|
import { MagicLayout } from "../app/magiclayout";
|
||||||
import { modalsRegistry } from "../app/common/modals/modalsRegistry";
|
import { modalsRegistry } from "../app/common/modals/registry";
|
||||||
import * as appconst from "../app/appconst";
|
import * as appconst from "../app/appconst";
|
||||||
|
|
||||||
dayjs.extend(customParseFormat);
|
dayjs.extend(customParseFormat);
|
||||||
@ -193,10 +196,13 @@ type ElectronApi = {
|
|||||||
getWaveSrvStatus: () => boolean;
|
getWaveSrvStatus: () => boolean;
|
||||||
restartWaveSrv: () => boolean;
|
restartWaveSrv: () => boolean;
|
||||||
reloadWindow: () => void;
|
reloadWindow: () => void;
|
||||||
|
openExternalLink: (url: string) => void;
|
||||||
onTCmd: (callback: (mods: KeyModsType) => void) => void;
|
onTCmd: (callback: (mods: KeyModsType) => void) => void;
|
||||||
onICmd: (callback: (mods: KeyModsType) => void) => void;
|
onICmd: (callback: (mods: KeyModsType) => void) => void;
|
||||||
onLCmd: (callback: (mods: KeyModsType) => void) => void;
|
onLCmd: (callback: (mods: KeyModsType) => void) => void;
|
||||||
onHCmd: (callback: (mods: KeyModsType) => void) => void;
|
onHCmd: (callback: (mods: KeyModsType) => void) => void;
|
||||||
|
onPCmd: (callback: (mods: KeyModsType) => void) => void;
|
||||||
|
onWCmd: (callback: (mods: KeyModsType) => void) => void;
|
||||||
onMenuItemAbout: (callback: () => void) => void;
|
onMenuItemAbout: (callback: () => void) => void;
|
||||||
onMetaArrowUp: (callback: () => void) => void;
|
onMetaArrowUp: (callback: () => void) => void;
|
||||||
onMetaArrowDown: (callback: () => void) => void;
|
onMetaArrowDown: (callback: () => void) => void;
|
||||||
@ -207,6 +213,7 @@ type ElectronApi = {
|
|||||||
contextScreen: (screenOpts: { screenId: string }, position: { x: number; y: number }) => void;
|
contextScreen: (screenOpts: { screenId: string }, position: { x: number; y: number }) => void;
|
||||||
contextEditMenu: (position: { x: number; y: number }, opts: ContextMenuOpts) => void;
|
contextEditMenu: (position: { x: number; y: number }, opts: ContextMenuOpts) => void;
|
||||||
onWaveSrvStatusChange: (callback: (status: boolean, pid: number) => void) => void;
|
onWaveSrvStatusChange: (callback: (status: boolean, pid: number) => void) => void;
|
||||||
|
getLastLogs: (numOfLines: number, callback: (logs: any) => void) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getApi(): ElectronApi {
|
function getApi(): ElectronApi {
|
||||||
@ -334,7 +341,7 @@ class Cmd {
|
|||||||
type: "feinput",
|
type: "feinput",
|
||||||
ck: this.screenId + "/" + this.lineId,
|
ck: this.screenId + "/" + this.lineId,
|
||||||
remote: this.remote,
|
remote: this.remote,
|
||||||
inputdata64: btoa(data),
|
inputdata64: stringToBase64(data),
|
||||||
};
|
};
|
||||||
GlobalModel.sendInputPacket(inputPacket);
|
GlobalModel.sendInputPacket(inputPacket);
|
||||||
}
|
}
|
||||||
@ -1228,7 +1235,18 @@ function getDefaultHistoryQueryOpts(): HistoryQueryOpts {
|
|||||||
class InputModel {
|
class InputModel {
|
||||||
historyShow: OV<boolean> = mobx.observable.box(false);
|
historyShow: OV<boolean> = mobx.observable.box(false);
|
||||||
infoShow: OV<boolean> = mobx.observable.box(false);
|
infoShow: OV<boolean> = mobx.observable.box(false);
|
||||||
|
aIChatShow: OV<boolean> = mobx.observable.box(false);
|
||||||
cmdInputHeight: OV<number> = mobx.observable.box(0);
|
cmdInputHeight: OV<number> = mobx.observable.box(0);
|
||||||
|
aiChatTextAreaRef: React.RefObject<HTMLTextAreaElement>;
|
||||||
|
aiChatWindowRef: React.RefObject<HTMLDivElement>;
|
||||||
|
codeSelectBlockRefArray: Array<React.RefObject<HTMLElement>>;
|
||||||
|
codeSelectSelectedIndex: OV<number> = mobx.observable.box(-1);
|
||||||
|
|
||||||
|
AICmdInfoChatItems: mobx.IObservableArray<OpenAICmdInfoChatMessageType> = mobx.observable.array([], {
|
||||||
|
name: "aicmdinfo-chat",
|
||||||
|
});
|
||||||
|
readonly codeSelectTop: number = -2;
|
||||||
|
readonly codeSelectBottom: number = -1;
|
||||||
|
|
||||||
historyType: mobx.IObservableValue<HistoryTypeStrs> = mobx.observable.box("screen");
|
historyType: mobx.IObservableValue<HistoryTypeStrs> = mobx.observable.box("screen");
|
||||||
historyLoading: mobx.IObservableValue<boolean> = mobx.observable.box(false);
|
historyLoading: mobx.IObservableValue<boolean> = mobx.observable.box(false);
|
||||||
@ -1266,6 +1284,10 @@ class InputModel {
|
|||||||
this.filteredHistoryItems = mobx.computed(() => {
|
this.filteredHistoryItems = mobx.computed(() => {
|
||||||
return this._getFilteredHistoryItems();
|
return this._getFilteredHistoryItems();
|
||||||
});
|
});
|
||||||
|
mobx.action(() => {
|
||||||
|
this.codeSelectSelectedIndex.set(-1);
|
||||||
|
this.codeSelectBlockRefArray = [];
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
setInputMode(inputMode: null | "comment" | "global"): void {
|
setInputMode(inputMode: null | "comment" | "global"): void {
|
||||||
@ -1390,6 +1412,11 @@ class InputModel {
|
|||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setOpenAICmdInfoChat(chat: OpenAICmdInfoChatMessageType[]): void {
|
||||||
|
this.AICmdInfoChatItems.replace(chat);
|
||||||
|
this.codeSelectBlockRefArray = [];
|
||||||
|
}
|
||||||
|
|
||||||
setHistoryShow(show: boolean): void {
|
setHistoryShow(show: boolean): void {
|
||||||
if (this.historyShow.get() == show) {
|
if (this.historyShow.get() == show) {
|
||||||
return;
|
return;
|
||||||
@ -1678,6 +1705,152 @@ class InputModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setCmdInfoChatRefs(
|
||||||
|
textAreaRef: React.RefObject<HTMLTextAreaElement>,
|
||||||
|
chatWindowRef: React.RefObject<HTMLDivElement>
|
||||||
|
) {
|
||||||
|
this.aiChatTextAreaRef = textAreaRef;
|
||||||
|
this.aiChatWindowRef = chatWindowRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAIChatFocus() {
|
||||||
|
if (this.aiChatTextAreaRef != null && this.aiChatTextAreaRef.current != null) {
|
||||||
|
this.aiChatTextAreaRef.current.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
grabCodeSelectSelection() {
|
||||||
|
if (
|
||||||
|
this.codeSelectSelectedIndex.get() >= 0 &&
|
||||||
|
this.codeSelectSelectedIndex.get() < this.codeSelectBlockRefArray.length
|
||||||
|
) {
|
||||||
|
let curBlockRef = this.codeSelectBlockRefArray[this.codeSelectSelectedIndex.get()];
|
||||||
|
let codeText = curBlockRef.current.innerText;
|
||||||
|
codeText = codeText.replace(/\n$/, ""); // remove trailing newline
|
||||||
|
let newLineValue = this.getCurLine() + " " + codeText;
|
||||||
|
this.setCurLine(newLineValue);
|
||||||
|
this.giveFocus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addCodeBlockToCodeSelect(blockRef: React.RefObject<HTMLElement>): number {
|
||||||
|
let rtn = -1;
|
||||||
|
rtn = this.codeSelectBlockRefArray.length;
|
||||||
|
this.codeSelectBlockRefArray.push(blockRef);
|
||||||
|
return rtn;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCodeSelectSelectedCodeBlock(blockIndex: number) {
|
||||||
|
mobx.action(() => {
|
||||||
|
if (blockIndex >= 0 && blockIndex < this.codeSelectBlockRefArray.length) {
|
||||||
|
this.codeSelectSelectedIndex.set(blockIndex);
|
||||||
|
let currentRef = this.codeSelectBlockRefArray[blockIndex].current;
|
||||||
|
if (currentRef != null) {
|
||||||
|
if (this.aiChatWindowRef != null && this.aiChatWindowRef.current != null) {
|
||||||
|
let chatWindowTop = this.aiChatWindowRef.current.scrollTop;
|
||||||
|
let chatWindowBottom = chatWindowTop + this.aiChatWindowRef.current.clientHeight - 100;
|
||||||
|
let elemTop = currentRef.offsetTop;
|
||||||
|
let elemBottom = elemTop - currentRef.offsetHeight;
|
||||||
|
let elementIsInView = elemBottom < chatWindowBottom && elemTop > chatWindowTop;
|
||||||
|
if (!elementIsInView) {
|
||||||
|
this.aiChatWindowRef.current.scrollTop =
|
||||||
|
elemBottom - this.aiChatWindowRef.current.clientHeight / 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.codeSelectBlockRefArray = [];
|
||||||
|
this.setAIChatFocus();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
codeSelectSelectNextNewestCodeBlock() {
|
||||||
|
// oldest code block = index 0 in array
|
||||||
|
// this decrements codeSelectSelected index
|
||||||
|
mobx.action(() => {
|
||||||
|
if (this.codeSelectSelectedIndex.get() == this.codeSelectTop) {
|
||||||
|
this.codeSelectSelectedIndex.set(this.codeSelectBottom);
|
||||||
|
} else if (this.codeSelectSelectedIndex.get() == this.codeSelectBottom) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let incBlockIndex = this.codeSelectSelectedIndex.get() + 1;
|
||||||
|
if (this.codeSelectSelectedIndex.get() == this.codeSelectBlockRefArray.length - 1) {
|
||||||
|
this.codeSelectDeselectAll();
|
||||||
|
if (this.aiChatWindowRef != null && this.aiChatWindowRef.current != null) {
|
||||||
|
this.aiChatWindowRef.current.scrollTop = this.aiChatWindowRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (incBlockIndex >= 0 && incBlockIndex < this.codeSelectBlockRefArray.length) {
|
||||||
|
this.setCodeSelectSelectedCodeBlock(incBlockIndex);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
codeSelectSelectNextOldestCodeBlock() {
|
||||||
|
mobx.action(() => {
|
||||||
|
if (this.codeSelectSelectedIndex.get() == this.codeSelectBottom) {
|
||||||
|
if (this.codeSelectBlockRefArray.length > 0) {
|
||||||
|
this.codeSelectSelectedIndex.set(this.codeSelectBlockRefArray.length);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (this.codeSelectSelectedIndex.get() == this.codeSelectTop) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let decBlockIndex = this.codeSelectSelectedIndex.get() - 1;
|
||||||
|
if (decBlockIndex < 0) {
|
||||||
|
this.codeSelectDeselectAll(this.codeSelectTop);
|
||||||
|
if (this.aiChatWindowRef != null && this.aiChatWindowRef.current != null) {
|
||||||
|
this.aiChatWindowRef.current.scrollTop = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (decBlockIndex >= 0 && decBlockIndex < this.codeSelectBlockRefArray.length) {
|
||||||
|
this.setCodeSelectSelectedCodeBlock(decBlockIndex);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
getCodeSelectSelectedIndex() {
|
||||||
|
return this.codeSelectSelectedIndex.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
getCodeSelectRefArrayLength() {
|
||||||
|
return this.codeSelectBlockRefArray.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
codeBlockIsSelected(blockIndex: number): boolean {
|
||||||
|
return blockIndex == this.codeSelectSelectedIndex.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
codeSelectDeselectAll(direction: number = this.codeSelectBottom) {
|
||||||
|
mobx.action(() => {
|
||||||
|
this.codeSelectSelectedIndex.set(direction);
|
||||||
|
this.codeSelectBlockRefArray = [];
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
|
||||||
|
openAIAssistantChat(): void {
|
||||||
|
this.aIChatShow.set(true);
|
||||||
|
this.setAIChatFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
closeAIAssistantChat(): void {
|
||||||
|
this.aIChatShow.set(false);
|
||||||
|
this.giveFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
clearAIAssistantChat(): void {
|
||||||
|
let prtn = GlobalModel.submitChatInfoCommand("", "", true);
|
||||||
|
prtn.then((rtn) => {
|
||||||
|
if (rtn.success) {
|
||||||
|
} else {
|
||||||
|
console.log("submit chat command error: " + rtn.error);
|
||||||
|
}
|
||||||
|
}).catch((error) => {
|
||||||
|
console.log("submit chat command error: ", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
hasScrollingInfoMsg(): boolean {
|
hasScrollingInfoMsg(): boolean {
|
||||||
if (!this.infoShow.get()) {
|
if (!this.infoShow.get()) {
|
||||||
return false;
|
return false;
|
||||||
@ -1773,6 +1946,7 @@ class InputModel {
|
|||||||
resetInput(): void {
|
resetInput(): void {
|
||||||
mobx.action(() => {
|
mobx.action(() => {
|
||||||
this.setHistoryShow(false);
|
this.setHistoryShow(false);
|
||||||
|
this.closeAIAssistantChat();
|
||||||
this.infoShow.set(false);
|
this.infoShow.set(false);
|
||||||
this.inputMode.set(null);
|
this.inputMode.set(null);
|
||||||
this.resetHistory();
|
this.resetHistory();
|
||||||
@ -2864,7 +3038,7 @@ class RemotesModalModel {
|
|||||||
let inputPacket: RemoteInputPacketType = {
|
let inputPacket: RemoteInputPacketType = {
|
||||||
type: "remoteinput",
|
type: "remoteinput",
|
||||||
remoteid: remoteId,
|
remoteid: remoteId,
|
||||||
inputdata64: btoa(event.key),
|
inputdata64: stringToBase64(event.key),
|
||||||
};
|
};
|
||||||
GlobalModel.sendInputPacket(inputPacket);
|
GlobalModel.sendInputPacket(inputPacket);
|
||||||
}
|
}
|
||||||
@ -2922,8 +3096,11 @@ class RemotesModel {
|
|||||||
return this.recentConnAddedState.get();
|
return this.recentConnAddedState.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
seRecentConnAdded(value: boolean) {
|
@boundMethod
|
||||||
this.recentConnAddedState.set(value);
|
setRecentConnAdded(value: boolean) {
|
||||||
|
mobx.action(() => {
|
||||||
|
this.recentConnAddedState.set(value);
|
||||||
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
deSelectRemote(): void {
|
deSelectRemote(): void {
|
||||||
@ -2935,6 +3112,7 @@ class RemotesModel {
|
|||||||
|
|
||||||
openReadModal(remoteId: string): void {
|
openReadModal(remoteId: string): void {
|
||||||
mobx.action(() => {
|
mobx.action(() => {
|
||||||
|
this.setRecentConnAdded(false);
|
||||||
this.selectedRemoteId.set(remoteId);
|
this.selectedRemoteId.set(remoteId);
|
||||||
this.remoteEdit.set(null);
|
this.remoteEdit.set(null);
|
||||||
GlobalModel.modalsModel.pushModal(appconst.VIEW_REMOTE);
|
GlobalModel.modalsModel.pushModal(appconst.VIEW_REMOTE);
|
||||||
@ -3043,7 +3221,7 @@ class RemotesModel {
|
|||||||
let inputPacket: RemoteInputPacketType = {
|
let inputPacket: RemoteInputPacketType = {
|
||||||
type: "remoteinput",
|
type: "remoteinput",
|
||||||
remoteid: remoteId,
|
remoteid: remoteId,
|
||||||
inputdata64: btoa(event.key),
|
inputdata64: stringToBase64(event.key),
|
||||||
};
|
};
|
||||||
GlobalModel.sendInputPacket(inputPacket);
|
GlobalModel.sendInputPacket(inputPacket);
|
||||||
}
|
}
|
||||||
@ -3207,6 +3385,8 @@ class Model {
|
|||||||
getApi().onICmd(this.onICmd.bind(this));
|
getApi().onICmd(this.onICmd.bind(this));
|
||||||
getApi().onLCmd(this.onLCmd.bind(this));
|
getApi().onLCmd(this.onLCmd.bind(this));
|
||||||
getApi().onHCmd(this.onHCmd.bind(this));
|
getApi().onHCmd(this.onHCmd.bind(this));
|
||||||
|
getApi().onPCmd(this.onPCmd.bind(this));
|
||||||
|
getApi().onWCmd(this.onWCmd.bind(this));
|
||||||
getApi().onMenuItemAbout(this.onMenuItemAbout.bind(this));
|
getApi().onMenuItemAbout(this.onMenuItemAbout.bind(this));
|
||||||
getApi().onMetaArrowUp(this.onMetaArrowUp.bind(this));
|
getApi().onMetaArrowUp(this.onMetaArrowUp.bind(this));
|
||||||
getApi().onMetaArrowDown(this.onMetaArrowDown.bind(this));
|
getApi().onMetaArrowDown(this.onMetaArrowDown.bind(this));
|
||||||
@ -3245,6 +3425,16 @@ class Model {
|
|||||||
getApi().reloadWindow();
|
getApi().reloadWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a new default browser window to the given url
|
||||||
|
* @param {string} url The url to open
|
||||||
|
*/
|
||||||
|
openExternalLink(url: string): void {
|
||||||
|
console.log("opening external link: " + url);
|
||||||
|
getApi().openExternalLink(url);
|
||||||
|
console.log("finished opening external link");
|
||||||
|
}
|
||||||
|
|
||||||
refocus() {
|
refocus() {
|
||||||
// givefocus() give back focus to cmd or input
|
// givefocus() give back focus to cmd or input
|
||||||
let activeScreen = this.getActiveScreen();
|
let activeScreen = this.getActiveScreen();
|
||||||
@ -3276,6 +3466,13 @@ class Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showAlert(alertMessage: AlertMessageType): Promise<boolean> {
|
showAlert(alertMessage: AlertMessageType): Promise<boolean> {
|
||||||
|
if (alertMessage.confirmflag != null) {
|
||||||
|
let cdata = GlobalModel.clientData.get();
|
||||||
|
let noConfirm = cdata.clientopts?.confirmflags?.[alertMessage.confirmflag];
|
||||||
|
if (noConfirm) {
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
mobx.action(() => {
|
mobx.action(() => {
|
||||||
this.alertMessage.set(alertMessage);
|
this.alertMessage.set(alertMessage);
|
||||||
GlobalModel.modalsModel.pushModal(appconst.ALERT);
|
GlobalModel.modalsModel.pushModal(appconst.ALERT);
|
||||||
@ -3356,7 +3553,7 @@ class Model {
|
|||||||
// nothing for now
|
// nothing for now
|
||||||
}
|
}
|
||||||
|
|
||||||
docKeyDownHandler(e: any) {
|
docKeyDownHandler(e: KeyboardEvent) {
|
||||||
if (isModKeyPress(e)) {
|
if (isModKeyPress(e)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -3418,6 +3615,54 @@ class Model {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (e.code == "KeyD" && e.getModifierState("Meta")) {
|
||||||
|
let ranDelete = this.deleteActiveLine();
|
||||||
|
if (ranDelete) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteActiveLine(): boolean {
|
||||||
|
let activeScreen = this.getActiveScreen();
|
||||||
|
if (activeScreen == null || activeScreen.getFocusType() != "cmd") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let selectedLine = activeScreen.selectedLine.get();
|
||||||
|
if (selectedLine == null || selectedLine <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let line = activeScreen.getLineByNum(selectedLine);
|
||||||
|
if (line == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let cmd = activeScreen.getCmd(line);
|
||||||
|
if (cmd != null) {
|
||||||
|
if (cmd.isRunning()) {
|
||||||
|
let info: T.InfoType = { infomsg: "Cannot delete a running command" };
|
||||||
|
this.inputModel.flashInfoMsg(info, 2000);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GlobalCommandRunner.lineDelete(String(selectedLine), true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
onWCmd(e: any, mods: KeyModsType) {
|
||||||
|
let activeScreen = this.getActiveScreen();
|
||||||
|
if (activeScreen == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let rtnp = this.showAlert({
|
||||||
|
message: "Are you sure you want to delete this screen?",
|
||||||
|
confirm: true,
|
||||||
|
});
|
||||||
|
rtnp.then((result) => {
|
||||||
|
if (!result) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
GlobalCommandRunner.screenDelete(activeScreen.screenId, true);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
clearModals(): boolean {
|
clearModals(): boolean {
|
||||||
@ -3474,6 +3719,10 @@ class Model {
|
|||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getLastLogs(numbOfLines: number, cb: (logs: any) => void): void {
|
||||||
|
getApi().getLastLogs(numbOfLines, cb);
|
||||||
|
}
|
||||||
|
|
||||||
getContentHeight(context: RendererContext): number {
|
getContentHeight(context: RendererContext): number {
|
||||||
let key = context.screenId + "/" + context.lineId;
|
let key = context.screenId + "/" + context.lineId;
|
||||||
return this.termUsedRowsCache[key];
|
return this.termUsedRowsCache[key];
|
||||||
@ -3535,6 +3784,10 @@ class Model {
|
|||||||
GlobalModel.historyViewModel.reSearch();
|
GlobalModel.historyViewModel.reSearch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onPCmd(e: any, mods: KeyModsType) {
|
||||||
|
GlobalModel.modalsModel.pushModal(appconst.TAB_SWITCHER);
|
||||||
|
}
|
||||||
|
|
||||||
getFocusedLine(): LineFocusType {
|
getFocusedLine(): LineFocusType {
|
||||||
if (this.inputModel.hasFocus()) {
|
if (this.inputModel.hasFocus()) {
|
||||||
return { cmdInputFocus: true };
|
return { cmdInputFocus: true };
|
||||||
@ -3711,8 +3964,8 @@ class Model {
|
|||||||
this.remotes.clear();
|
this.remotes.clear();
|
||||||
}
|
}
|
||||||
this.updateRemotes(update.remotes);
|
this.updateRemotes(update.remotes);
|
||||||
|
// This code's purpose is to show view remote connection modal when a new connection is added
|
||||||
if (update.remotes && update.remotes.length && this.remotesModel.recentConnAddedState.get()) {
|
if (update.remotes && update.remotes.length && this.remotesModel.recentConnAddedState.get()) {
|
||||||
GlobalModel.remotesModel.closeModal();
|
|
||||||
GlobalModel.remotesModel.openReadModal(update.remotes![0].remoteid);
|
GlobalModel.remotesModel.openReadModal(update.remotes![0].remoteid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3744,6 +3997,10 @@ class Model {
|
|||||||
this.remotesModel.openEditModal({ ...rview.remoteedit });
|
this.remotesModel.openEditModal({ ...rview.remoteedit });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (interactive && "alertmessage" in update) {
|
||||||
|
let alertMessage: AlertMessageType = update.alertmessage;
|
||||||
|
this.showAlert(alertMessage);
|
||||||
|
}
|
||||||
if ("cmdline" in update) {
|
if ("cmdline" in update) {
|
||||||
this.inputModel.updateCmdLine(update.cmdline);
|
this.inputModel.updateCmdLine(update.cmdline);
|
||||||
}
|
}
|
||||||
@ -3756,6 +4013,9 @@ class Model {
|
|||||||
this.sessionListLoaded.set(true);
|
this.sessionListLoaded.set(true);
|
||||||
this.remotesLoaded.set(true);
|
this.remotesLoaded.set(true);
|
||||||
}
|
}
|
||||||
|
if ("openaicmdinfochat" in update) {
|
||||||
|
this.inputModel.setOpenAICmdInfoChat(update.openaicmdinfochat);
|
||||||
|
}
|
||||||
// console.log("run-update>", Date.now(), interactive, update);
|
// console.log("run-update>", Date.now(), interactive, update);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3990,6 +4250,28 @@ class Model {
|
|||||||
return this.submitCommandPacket(pk, interactive);
|
return this.submitCommandPacket(pk, interactive);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
submitChatInfoCommand(chatMsg: string, curLineStr: string, clear: boolean): Promise<CommandRtnType> {
|
||||||
|
let commandStr = "/chat " + chatMsg;
|
||||||
|
let interactive = false;
|
||||||
|
let pk: FeCmdPacketType = {
|
||||||
|
type: "fecmd",
|
||||||
|
metacmd: "eval",
|
||||||
|
args: [commandStr],
|
||||||
|
kwargs: {},
|
||||||
|
uicontext: this.getUIContext(),
|
||||||
|
interactive: interactive,
|
||||||
|
rawstr: chatMsg,
|
||||||
|
};
|
||||||
|
pk.kwargs["nohist"] = "1";
|
||||||
|
if (clear) {
|
||||||
|
pk.kwargs["cmdinfoclear"] = "1";
|
||||||
|
} else {
|
||||||
|
pk.kwargs["cmdinfo"] = "1";
|
||||||
|
}
|
||||||
|
pk.kwargs["curline"] = curLineStr;
|
||||||
|
return this.submitCommandPacket(pk, interactive);
|
||||||
|
}
|
||||||
|
|
||||||
submitRawCommand(cmdStr: string, addToHistory: boolean, interactive: boolean): Promise<CommandRtnType> {
|
submitRawCommand(cmdStr: string, addToHistory: boolean, interactive: boolean): Promise<CommandRtnType> {
|
||||||
let pk: FeCmdPacketType = {
|
let pk: FeCmdPacketType = {
|
||||||
type: "fecmd",
|
type: "fecmd",
|
||||||
@ -4190,7 +4472,7 @@ class Model {
|
|||||||
return resp.text() as any;
|
return resp.text() as any;
|
||||||
}
|
}
|
||||||
contentType = resp.headers.get("Content-Type");
|
contentType = resp.headers.get("Content-Type");
|
||||||
fileInfo = JSON.parse(atob(resp.headers.get("X-FileInfo")));
|
fileInfo = JSON.parse(base64ToString(resp.headers.get("X-FileInfo")));
|
||||||
return resp.blob();
|
return resp.blob();
|
||||||
})
|
})
|
||||||
.then((blobOrText: any) => {
|
.then((blobOrText: any) => {
|
||||||
@ -4268,11 +4550,15 @@ class CommandRunner {
|
|||||||
GlobalModel.submitCommand("session", null, [session], { nohist: "1" }, false);
|
GlobalModel.submitCommand("session", null, [session], { nohist: "1" }, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
switchScreen(screen: string) {
|
switchScreen(screen: string, session?: string) {
|
||||||
mobx.action(() => {
|
mobx.action(() => {
|
||||||
GlobalModel.activeMainView.set("session");
|
GlobalModel.activeMainView.set("session");
|
||||||
})();
|
})();
|
||||||
GlobalModel.submitCommand("screen", null, [screen], { nohist: "1" }, false);
|
let kwargs = { nohist: "1" };
|
||||||
|
if (session != null) {
|
||||||
|
kwargs["session"] = session;
|
||||||
|
}
|
||||||
|
GlobalModel.submitCommand("screen", null, [screen], kwargs, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
lineView(sessionId: string, screenId: string, lineNum?: number) {
|
lineView(sessionId: string, screenId: string, lineNum?: number) {
|
||||||
@ -4290,6 +4576,10 @@ class CommandRunner {
|
|||||||
return GlobalModel.submitCommand("line", "archive", [lineArg, archiveStr], kwargs, false);
|
return GlobalModel.submitCommand("line", "archive", [lineArg, archiveStr], kwargs, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lineDelete(lineArg: string, interactive: boolean): Promise<CommandRtnType> {
|
||||||
|
return GlobalModel.submitCommand("line", "delete", [lineArg], { nohist: "1" }, interactive);
|
||||||
|
}
|
||||||
|
|
||||||
lineSet(lineArg: string, opts: { renderer?: string }): Promise<CommandRtnType> {
|
lineSet(lineArg: string, opts: { renderer?: string }): Promise<CommandRtnType> {
|
||||||
let kwargs = { nohist: "1" };
|
let kwargs = { nohist: "1" };
|
||||||
if ("renderer" in opts) {
|
if ("renderer" in opts) {
|
||||||
@ -4339,8 +4629,8 @@ class CommandRunner {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
screenDelete(screenId: string): Promise<CommandRtnType> {
|
screenDelete(screenId: string, interactive: boolean): Promise<CommandRtnType> {
|
||||||
return GlobalModel.submitCommand("screen", "delete", [screenId], { nohist: "1" }, false);
|
return GlobalModel.submitCommand("screen", "delete", [screenId], { nohist: "1" }, interactive);
|
||||||
}
|
}
|
||||||
|
|
||||||
screenWebShare(screenId: string, shouldShare: boolean): Promise<CommandRtnType> {
|
screenWebShare(screenId: string, shouldShare: boolean): Promise<CommandRtnType> {
|
||||||
@ -4407,7 +4697,7 @@ class CommandRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
importSshConfig() {
|
importSshConfig() {
|
||||||
GlobalModel.submitCommand("remote", "parse", null, null, false);
|
GlobalModel.submitCommand("remote", "parse", null, { nohist: "1", visual: "1" }, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
screenSelectLine(lineArg: string, focusVal?: string) {
|
screenSelectLine(lineArg: string, focusVal?: string) {
|
||||||
@ -4573,6 +4863,12 @@ class CommandRunner {
|
|||||||
GlobalModel.submitCommand("client", "accepttos", null, { nohist: "1" }, true);
|
GlobalModel.submitCommand("client", "accepttos", null, { nohist: "1" }, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clientSetConfirmFlag(flag: string, value: boolean): Promise<CommandRtnType> {
|
||||||
|
let kwargs = { nohist: "1" };
|
||||||
|
let valueStr = value ? "1" : "0";
|
||||||
|
return GlobalModel.submitCommand("client", "setconfirmflag", [flag, valueStr], kwargs, false);
|
||||||
|
}
|
||||||
|
|
||||||
editBookmark(bookmarkId: string, desc: string, cmdstr: string) {
|
editBookmark(bookmarkId: string, desc: string, cmdstr: string) {
|
||||||
let kwargs = {
|
let kwargs = {
|
||||||
nohist: "1",
|
nohist: "1",
|
||||||
|
@ -3,7 +3,8 @@
|
|||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as T from "../../types/types";
|
import * as T from "../../types/types";
|
||||||
import Editor from "@monaco-editor/react";
|
import Editor, { Monaco } from "@monaco-editor/react";
|
||||||
|
import type * as MonacoTypes from "monaco-editor/esm/vs/editor/editor.api";
|
||||||
import { Markdown } from "../../app/common/common";
|
import { Markdown } from "../../app/common/common";
|
||||||
import { GlobalModel, GlobalCommandRunner } from "../../model/model";
|
import { GlobalModel, GlobalCommandRunner } from "../../model/model";
|
||||||
import Split from "react-split-it";
|
import Split from "react-split-it";
|
||||||
@ -146,21 +147,24 @@ class SourceCodeRenderer extends React.Component<
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
handleEditorDidMount = (editor, monaco) => {
|
handleEditorDidMount = (editor: MonacoTypes.editor.IStandaloneCodeEditor, monaco: Monaco) => {
|
||||||
this.monacoEditor = editor;
|
this.monacoEditor = editor;
|
||||||
this.setInitialLanguage(editor);
|
this.setInitialLanguage(editor);
|
||||||
this.setEditorHeight();
|
this.setEditorHeight();
|
||||||
editor.onKeyDown((e) => {
|
editor.onKeyDown((e: MonacoTypes.IKeyboardEvent) => {
|
||||||
if (e.code === "KeyS" && (e.ctrlKey || e.metaKey) && this.state.isSave) {
|
if (e.code === "KeyS" && e.metaKey && this.state.isSave) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
this.doSave();
|
this.doSave();
|
||||||
}
|
}
|
||||||
if (e.code === "KeyD" && (e.ctrlKey || e.metaKey)) {
|
if (e.code === "KeyD" && e.metaKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
this.doClose();
|
this.doClose();
|
||||||
}
|
}
|
||||||
if (e.code === "KeyP" && (e.ctrlKey || e.metaKey)) {
|
if (e.code === "KeyP" && e.metaKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
this.togglePreview();
|
this.togglePreview();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -3,10 +3,13 @@
|
|||||||
|
|
||||||
import * as mobx from "mobx";
|
import * as mobx from "mobx";
|
||||||
import { Terminal } from "xterm";
|
import { Terminal } from "xterm";
|
||||||
|
//TODO: replace with `@xterm/addon-web-links` when it's available as stable
|
||||||
|
import { WebLinksAddon } from "xterm-addon-web-links";
|
||||||
import { sprintf } from "sprintf-js";
|
import { sprintf } from "sprintf-js";
|
||||||
import { boundMethod } from "autobind-decorator";
|
import { boundMethod } from "autobind-decorator";
|
||||||
import { windowWidthToCols, windowHeightToRows } from "../../util/textmeasure";
|
import { windowWidthToCols, windowHeightToRows } from "../../util/textmeasure";
|
||||||
import { boundInt } from "../../util/util";
|
import { boundInt } from "../../util/util";
|
||||||
|
import { GlobalModel } from "../../model/model"
|
||||||
import type {
|
import type {
|
||||||
TermContextUnion,
|
TermContextUnion,
|
||||||
TermOptsType,
|
TermOptsType,
|
||||||
@ -96,6 +99,21 @@ class TermWrap {
|
|||||||
fontFamily: "JetBrains Mono",
|
fontFamily: "JetBrains Mono",
|
||||||
theme: { foreground: terminal.foreground, background: terminal.background },
|
theme: { foreground: terminal.foreground, background: terminal.background },
|
||||||
});
|
});
|
||||||
|
this.terminal.loadAddon(new WebLinksAddon((e, uri) => {
|
||||||
|
e.preventDefault();
|
||||||
|
switch (GlobalModel.platform) {
|
||||||
|
case "darwin":
|
||||||
|
if (e.metaKey) {
|
||||||
|
GlobalModel.openExternalLink(uri);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (e.ctrlKey) {
|
||||||
|
GlobalModel.openExternalLink(uri);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}));
|
||||||
this.terminal._core._inputHandler._parser.setErrorHandler((state) => {
|
this.terminal._core._inputHandler._parser.setErrorHandler((state) => {
|
||||||
this.numParseErrors++;
|
this.numParseErrors++;
|
||||||
return state;
|
return state;
|
||||||
|
@ -265,6 +265,20 @@ type ScreenLinesType = {
|
|||||||
cmds: CmdDataType[];
|
cmds: CmdDataType[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type OpenAIPacketOutputType = {
|
||||||
|
model: string;
|
||||||
|
created: number;
|
||||||
|
finish_reason: string;
|
||||||
|
message: string;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type OpenAICmdInfoChatMessageType = {
|
||||||
|
isassistantresponse?: boolean;
|
||||||
|
assistantresponse?: OpenAIPacketOutputType;
|
||||||
|
userquery?: string;
|
||||||
|
};
|
||||||
|
|
||||||
type ModelUpdateType = {
|
type ModelUpdateType = {
|
||||||
interactive: boolean;
|
interactive: boolean;
|
||||||
sessions?: SessionDataType[];
|
sessions?: SessionDataType[];
|
||||||
@ -285,6 +299,8 @@ type ModelUpdateType = {
|
|||||||
clientdata?: ClientDataType;
|
clientdata?: ClientDataType;
|
||||||
historyviewdata?: HistoryViewDataType;
|
historyviewdata?: HistoryViewDataType;
|
||||||
remoteview?: RemoteViewType;
|
remoteview?: RemoteViewType;
|
||||||
|
openaicmdinfochat?: OpenAICmdInfoChatMessageType[];
|
||||||
|
alertmessage?: AlertMessageType;
|
||||||
};
|
};
|
||||||
|
|
||||||
type HistoryViewDataType = {
|
type HistoryViewDataType = {
|
||||||
@ -472,10 +488,15 @@ type FeOptsType = {
|
|||||||
termfontsize: number;
|
termfontsize: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ConfirmFlagsType = {
|
||||||
|
[k: string]: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
type ClientOptsType = {
|
type ClientOptsType = {
|
||||||
notelemetry: boolean;
|
notelemetry: boolean;
|
||||||
noreleasecheck: boolean;
|
noreleasecheck: boolean;
|
||||||
acceptedtos: number;
|
acceptedtos: number;
|
||||||
|
confirmflags: ConfirmFlagsType;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ReleaseInfoType = {
|
type ReleaseInfoType = {
|
||||||
@ -524,6 +545,7 @@ type AlertMessageType = {
|
|||||||
message: string;
|
message: string;
|
||||||
confirm?: boolean;
|
confirm?: boolean;
|
||||||
markdown?: boolean;
|
markdown?: boolean;
|
||||||
|
confirmflag?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type HistorySearchParams = {
|
type HistorySearchParams = {
|
||||||
@ -756,4 +778,5 @@ export type {
|
|||||||
ModalStoreEntry,
|
ModalStoreEntry,
|
||||||
StrWithPos,
|
StrWithPos,
|
||||||
CmdInputTextPacketType,
|
CmdInputTextPacketType,
|
||||||
|
OpenAICmdInfoChatMessageType,
|
||||||
};
|
};
|
||||||
|
@ -6,6 +6,7 @@ import { sprintf } from "sprintf-js";
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import localizedFormat from "dayjs/plugin/localizedFormat";
|
import localizedFormat from "dayjs/plugin/localizedFormat";
|
||||||
import type { RemoteType, CommandRtnType } from "../types/types";
|
import type { RemoteType, CommandRtnType } from "../types/types";
|
||||||
|
import base64 from "base64-js";
|
||||||
|
|
||||||
type OV<V> = mobx.IObservableValue<V>;
|
type OV<V> = mobx.IObservableValue<V>;
|
||||||
|
|
||||||
@ -70,6 +71,16 @@ function handleJsonFetchResponse(url: URL, resp: any): Promise<any> {
|
|||||||
return rtnData;
|
return rtnData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function base64ToString(b64: string): string {
|
||||||
|
let stringBytes = base64.toByteArray(b64)
|
||||||
|
return new TextDecoder().decode(stringBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
function stringToBase64(input: string): string {
|
||||||
|
let stringBytes = new TextEncoder().encode(input)
|
||||||
|
return base64.fromByteArray(stringBytes)
|
||||||
|
}
|
||||||
|
|
||||||
function base64ToArray(b64: string): Uint8Array {
|
function base64ToArray(b64: string): Uint8Array {
|
||||||
let rawStr = atob(b64);
|
let rawStr = atob(b64);
|
||||||
let rtnArr = new Uint8Array(new ArrayBuffer(rawStr.length));
|
let rtnArr = new Uint8Array(new ArrayBuffer(rawStr.length));
|
||||||
@ -229,26 +240,6 @@ function genMergeDataMap<ObjType extends IObjType<DataType>, DataType extends ID
|
|||||||
return rtn;
|
return rtn;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseEnv0(envStr64: string): Map<string, string> {
|
|
||||||
let envStr = atob(envStr64);
|
|
||||||
let parts = envStr.split("\x00");
|
|
||||||
let rtn: Map<string, string> = new Map();
|
|
||||||
for (let i = 0; i < parts.length; i++) {
|
|
||||||
let part = parts[i];
|
|
||||||
if (part == "") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let eqIdx = part.indexOf("=");
|
|
||||||
if (eqIdx == -1) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let varName = part.substr(0, eqIdx);
|
|
||||||
let varVal = part.substr(eqIdx + 1);
|
|
||||||
rtn.set(varName, varVal);
|
|
||||||
}
|
|
||||||
return rtn;
|
|
||||||
}
|
|
||||||
|
|
||||||
function boundInt(ival: number, minVal: number, maxVal: number): number {
|
function boundInt(ival: number, minVal: number, maxVal: number): number {
|
||||||
if (ival < minVal) {
|
if (ival < minVal) {
|
||||||
return minVal;
|
return minVal;
|
||||||
@ -404,13 +395,22 @@ function commandRtnHandler(prtn: Promise<CommandRtnType>, errorMessage: OV<strin
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRemoteName(remote: RemoteType): string {
|
||||||
|
if (remote == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
let { remotealias, remotecanonicalname } = remote;
|
||||||
|
return remotealias ? `${remotealias} [${remotecanonicalname}]` : remotecanonicalname;
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
handleJsonFetchResponse,
|
handleJsonFetchResponse,
|
||||||
|
base64ToString,
|
||||||
|
stringToBase64,
|
||||||
base64ToArray,
|
base64ToArray,
|
||||||
genMergeData,
|
genMergeData,
|
||||||
genMergeDataMap,
|
genMergeDataMap,
|
||||||
genMergeSimpleData,
|
genMergeSimpleData,
|
||||||
parseEnv0,
|
|
||||||
boundInt,
|
boundInt,
|
||||||
isModKeyPress,
|
isModKeyPress,
|
||||||
incObs,
|
incObs,
|
||||||
@ -428,4 +428,5 @@ export {
|
|||||||
getColorRGB,
|
getColorRGB,
|
||||||
commandRtnHandler,
|
commandRtnHandler,
|
||||||
getRemoteConnVal,
|
getRemoteConnVal,
|
||||||
|
getRemoteName,
|
||||||
};
|
};
|
||||||
|
@ -70,6 +70,8 @@ const PacketEOFStr = "EOF"
|
|||||||
|
|
||||||
var TypeStrToFactory map[string]reflect.Type
|
var TypeStrToFactory map[string]reflect.Type
|
||||||
|
|
||||||
|
const OpenAICmdInfoChatGreetingMessage = "Hello, may I help you with this command? \n(Press ESC to close and Ctrl+L to clear chat buffer)"
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
TypeStrToFactory = make(map[string]reflect.Type)
|
TypeStrToFactory = make(map[string]reflect.Type)
|
||||||
TypeStrToFactory[RunPacketStr] = reflect.TypeOf(RunPacketType{})
|
TypeStrToFactory[RunPacketStr] = reflect.TypeOf(RunPacketType{})
|
||||||
@ -729,6 +731,14 @@ type OpenAIUsageType struct {
|
|||||||
TotalTokens int `json:"total_tokens,omitempty"`
|
TotalTokens int `json:"total_tokens,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OpenAICmdInfoPacketOutputType struct {
|
||||||
|
Model string `json:"model,omitempty"`
|
||||||
|
Created int64 `json:"created,omitempty"`
|
||||||
|
FinishReason string `json:"finish_reason,omitempty"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type OpenAIPacketType struct {
|
type OpenAIPacketType struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Model string `json:"model,omitempty"`
|
Model string `json:"model,omitempty"`
|
||||||
@ -843,6 +853,14 @@ func MakeWriteFileDonePacket(reqId string) *WriteFileDonePacketType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OpenAICmdInfoChatMessage struct {
|
||||||
|
MessageID int `json:"messageid"`
|
||||||
|
IsAssistantResponse bool `json:"isassistantresponse,omitempty"`
|
||||||
|
AssistantResponse *OpenAICmdInfoPacketOutputType `json:"assistantresponse,omitempty"`
|
||||||
|
UserQuery string `json:"userquery,omitempty"`
|
||||||
|
UserEngineeredQuery string `json:"userengineeredquery,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type OpenAIPromptMessageType struct {
|
type OpenAIPromptMessageType struct {
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
@ -927,14 +945,6 @@ func ParseJsonPacket(jsonBuf []byte) (PacketType, error) {
|
|||||||
return pk, nil
|
return pk, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func sanitizeBytes(buf []byte) {
|
|
||||||
for idx, b := range buf {
|
|
||||||
if b >= 127 || (b < 32 && b != 10 && b != 13) {
|
|
||||||
buf[idx] = '?'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type SendError struct {
|
type SendError struct {
|
||||||
IsWriteError bool // fatal
|
IsWriteError bool // fatal
|
||||||
IsMarshalError bool // not fatal
|
IsMarshalError bool // not fatal
|
||||||
@ -970,7 +980,6 @@ func MarshalPacket(packet PacketType) ([]byte, error) {
|
|||||||
outBuf.Write(jsonBytes)
|
outBuf.Write(jsonBytes)
|
||||||
outBuf.WriteByte('\n')
|
outBuf.WriteByte('\n')
|
||||||
outBytes := outBuf.Bytes()
|
outBytes := outBuf.Bytes()
|
||||||
sanitizeBytes(outBytes)
|
|
||||||
return outBytes, nil
|
return outBytes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
12
wavesrv/db/migrations/000029_canonicalport.down.sql
Normal file
12
wavesrv/db/migrations/000029_canonicalport.down.sql
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
UPDATE remote
|
||||||
|
SET remotecanonicalname = SUBSTR(remotecanonicalname, 1, INSTR(remotecanonicalname, ':') - 1)
|
||||||
|
WHERE INSTR(remotecanonicalname, ':');
|
||||||
|
|
||||||
|
DELETE FROM remote
|
||||||
|
WHERE remoteid NOT IN (
|
||||||
|
SELECT remoteid FROM (
|
||||||
|
SELECT MIN(archived), remoteid, remotecanonicalname
|
||||||
|
FROM remote
|
||||||
|
GROUP BY remotecanonicalname
|
||||||
|
)
|
||||||
|
);
|
3
wavesrv/db/migrations/000029_canonicalport.up.sql
Normal file
3
wavesrv/db/migrations/000029_canonicalport.up.sql
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
UPDATE remote
|
||||||
|
SET remotecanonicalname = remotecanonicalname || COALESCE( ":" || json_extract(sshopts, '$.sshport'), "")
|
||||||
|
WHERE json_extract(sshopts, '$.sshport') != 22;
|
@ -33,3 +33,4 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/wavetermdev/waveterm/waveshell => ../waveshell
|
replace github.com/wavetermdev/waveterm/waveshell => ../waveshell
|
||||||
|
replace github.com/kevinburke/ssh_config => github.com/wavetermdev/ssh_config v0.0.0-20240109090616-36c8da3d7376
|
||||||
|
@ -89,6 +89,7 @@ var ColorNames = []string{"yellow", "blue", "pink", "mint", "cyan", "violet", "o
|
|||||||
var TabIcons = []string{"square", "sparkle", "fire", "ghost", "cloud", "compass", "crown", "droplet", "graduation-cap", "heart", "file"}
|
var TabIcons = []string{"square", "sparkle", "fire", "ghost", "cloud", "compass", "crown", "droplet", "graduation-cap", "heart", "file"}
|
||||||
var RemoteColorNames = []string{"red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"}
|
var RemoteColorNames = []string{"red", "green", "yellow", "blue", "magenta", "cyan", "white", "orange"}
|
||||||
var RemoteSetArgs = []string{"alias", "connectmode", "key", "password", "autoinstall", "color"}
|
var RemoteSetArgs = []string{"alias", "connectmode", "key", "password", "autoinstall", "color"}
|
||||||
|
var ConfirmFlags = []string{"hideshellprompt"}
|
||||||
|
|
||||||
var ScreenCmds = []string{"run", "comment", "cd", "cr", "clear", "sw", "reset", "signal", "chat"}
|
var ScreenCmds = []string{"run", "comment", "cd", "cr", "clear", "sw", "reset", "signal", "chat"}
|
||||||
var NoHistCmds = []string{"_compgen", "line", "history", "_killserver"}
|
var NoHistCmds = []string{"_compgen", "line", "history", "_killserver"}
|
||||||
@ -115,8 +116,8 @@ var SetVarScopes = []SetVarScope{
|
|||||||
{ScopeName: "remote", VarNames: []string{}},
|
{ScopeName: "remote", VarNames: []string{}},
|
||||||
}
|
}
|
||||||
|
|
||||||
var userHostRe = regexp.MustCompile("^(sudo@)?([a-z][a-z0-9._@-]*)@([a-z0-9][a-z0-9.-]*)(?::([0-9]+))?$")
|
var userHostRe = regexp.MustCompile(`^(sudo@)?([a-z][a-z0-9._@\\-]*)@([a-z0-9][a-z0-9.-]*)(?::([0-9]+))?$`)
|
||||||
var remoteAliasRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_-]*$")
|
var remoteAliasRe = regexp.MustCompile("^[a-zA-Z0-9][a-zA-Z0-9._-]*$")
|
||||||
var genericNameRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_ .()<>,/\"'\\[\\]{}=+$@!*-]*$")
|
var genericNameRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_ .()<>,/\"'\\[\\]{}=+$@!*-]*$")
|
||||||
var rendererRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_.:-]*$")
|
var rendererRe = regexp.MustCompile("^[a-zA-Z][a-zA-Z0-9_.:-]*$")
|
||||||
var positionRe = regexp.MustCompile("^((S?\\+|E?-)?[0-9]+|(\\+|-|S|E))$")
|
var positionRe = regexp.MustCompile("^((S?\\+|E?-)?[0-9]+|(\\+|-|S|E))$")
|
||||||
@ -213,6 +214,7 @@ func init() {
|
|||||||
registerCmdFn("client:set", ClientSetCommand)
|
registerCmdFn("client:set", ClientSetCommand)
|
||||||
registerCmdFn("client:notifyupdatewriter", ClientNotifyUpdateWriterCommand)
|
registerCmdFn("client:notifyupdatewriter", ClientNotifyUpdateWriterCommand)
|
||||||
registerCmdFn("client:accepttos", ClientAcceptTosCommand)
|
registerCmdFn("client:accepttos", ClientAcceptTosCommand)
|
||||||
|
registerCmdFn("client:setconfirmflag", ClientConfirmFlagCommand)
|
||||||
|
|
||||||
registerCmdFn("sidebar:open", SidebarOpenCommand)
|
registerCmdFn("sidebar:open", SidebarOpenCommand)
|
||||||
registerCmdFn("sidebar:close", SidebarCloseCommand)
|
registerCmdFn("sidebar:close", SidebarCloseCommand)
|
||||||
@ -1255,8 +1257,15 @@ func parseRemoteEditArgs(isNew bool, pk *scpacket.FeCommandPacketType, isLocal b
|
|||||||
if portVal == 0 && uhPort != 0 {
|
if portVal == 0 && uhPort != 0 {
|
||||||
portVal = uhPort
|
portVal = uhPort
|
||||||
}
|
}
|
||||||
|
if portVal < 0 || portVal > 65535 {
|
||||||
|
// 0 is used as a sentinel value for the default in this case
|
||||||
|
return nil, fmt.Errorf("invalid port argument, \"%d\" is not in the range of 1 to 65535", portVal)
|
||||||
|
}
|
||||||
sshOpts.SSHPort = portVal
|
sshOpts.SSHPort = portVal
|
||||||
canonicalName = remoteUser + "@" + remoteHost
|
canonicalName = remoteUser + "@" + remoteHost
|
||||||
|
if portVal != 0 && portVal != 22 {
|
||||||
|
canonicalName = canonicalName + ":" + strconv.Itoa(portVal)
|
||||||
|
}
|
||||||
if isSudo {
|
if isSudo {
|
||||||
canonicalName = "sudo@" + canonicalName
|
canonicalName = "sudo@" + canonicalName
|
||||||
}
|
}
|
||||||
@ -1511,6 +1520,47 @@ type HostInfoType struct {
|
|||||||
Ignore bool
|
Ignore bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createSshImportSummary(changeList map[string][]string) string {
|
||||||
|
totalNumChanges := len(changeList["create"]) + len(changeList["delete"]) + len(changeList["update"]) + len(changeList["createErr"]) + len(changeList["deleteErr"]) + len(changeList["updateErr"])
|
||||||
|
if totalNumChanges == 0 {
|
||||||
|
return "No changes made from ssh config import"
|
||||||
|
}
|
||||||
|
remoteStatusMsgs := map[string]string{
|
||||||
|
"delete": "Deleted %d connection%s: %s",
|
||||||
|
"create": "Created %d connection%s: %s",
|
||||||
|
"update": "Edited %d connection%s: %s",
|
||||||
|
"deleteErr": "Error deleting %d connection%s: %s",
|
||||||
|
"createErr": "Error creating %d connection%s: %s",
|
||||||
|
"updateErr": "Error editing %d connection%s: %s",
|
||||||
|
}
|
||||||
|
|
||||||
|
changeTypeKeys := []string{"delete", "create", "update", "deleteErr", "createErr", "updateErr"}
|
||||||
|
|
||||||
|
var outMsgs []string
|
||||||
|
for _, changeTypeKey := range changeTypeKeys {
|
||||||
|
changes := changeList[changeTypeKey]
|
||||||
|
if len(changes) > 0 {
|
||||||
|
rawStatusMsg := remoteStatusMsgs[changeTypeKey]
|
||||||
|
var pluralize string
|
||||||
|
if len(changes) == 1 {
|
||||||
|
pluralize = ""
|
||||||
|
} else {
|
||||||
|
pluralize = "s"
|
||||||
|
}
|
||||||
|
newMsg := fmt.Sprintf(rawStatusMsg, len(changes), pluralize, strings.Join(changes, ", "))
|
||||||
|
outMsgs = append(outMsgs, newMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var pluralize string
|
||||||
|
if totalNumChanges == 1 {
|
||||||
|
pluralize = ""
|
||||||
|
} else {
|
||||||
|
pluralize = "s"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d connection%s changed:\n\n%s", totalNumChanges, pluralize, strings.Join(outMsgs, "\n\n"))
|
||||||
|
}
|
||||||
|
|
||||||
func NewHostInfo(hostName string) (*HostInfoType, error) {
|
func NewHostInfo(hostName string) (*HostInfoType, error) {
|
||||||
userName, _ := ssh_config.GetStrict(hostName, "User")
|
userName, _ := ssh_config.GetStrict(hostName, "User")
|
||||||
if userName == "" {
|
if userName == "" {
|
||||||
@ -1528,18 +1578,18 @@ func NewHostInfo(hostName string) (*HostInfoType, error) {
|
|||||||
|
|
||||||
portStr, _ := ssh_config.GetStrict(hostName, "Port")
|
portStr, _ := ssh_config.GetStrict(hostName, "Port")
|
||||||
var portVal int
|
var portVal int
|
||||||
if portStr != "" {
|
if portStr != "" && portStr != "22" {
|
||||||
|
canonicalName = canonicalName + ":" + portStr
|
||||||
var err error
|
var err error
|
||||||
portVal, err = strconv.Atoi(portStr)
|
portVal, err = strconv.Atoi(portStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// do not make assumptions about port if incorrectly configured
|
// do not make assumptions about port if incorrectly configured
|
||||||
return nil, fmt.Errorf("could not parse \"%s\" (%s) - %s could not be converted to a valid port\n", hostName, canonicalName, portStr)
|
return nil, fmt.Errorf("could not parse \"%s\" (%s) - %s could not be converted to a valid port\n", hostName, canonicalName, portStr)
|
||||||
}
|
}
|
||||||
if int(int16(portVal)) != portVal {
|
if portVal <= 0 || portVal > 65535 {
|
||||||
return nil, fmt.Errorf("could not parse port \"%d\": number is not valid for a port\n", portVal)
|
return nil, fmt.Errorf("could not parse port \"%d\": number is not valid for a port\n", portVal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
identityFile, _ := ssh_config.GetStrict(hostName, "IdentityFile")
|
identityFile, _ := ssh_config.GetStrict(hostName, "IdentityFile")
|
||||||
passwordAuth, _ := ssh_config.GetStrict(hostName, "PasswordAuthentication")
|
passwordAuth, _ := ssh_config.GetStrict(hostName, "PasswordAuthentication")
|
||||||
|
|
||||||
@ -1579,6 +1629,7 @@ func RemoteConfigParseCommand(ctx context.Context, pk *scpacket.FeCommandPacketT
|
|||||||
localConfig := filepath.Join(home, ".ssh", "config")
|
localConfig := filepath.Join(home, ".ssh", "config")
|
||||||
systemConfig := filepath.Join("/", "ssh", "config")
|
systemConfig := filepath.Join("/", "ssh", "config")
|
||||||
sshConfigFiles := []string{localConfig, systemConfig}
|
sshConfigFiles := []string{localConfig, systemConfig}
|
||||||
|
ssh_config.ReloadConfigs()
|
||||||
hostPatterns, hostPatternsErr := resolveSshConfigPatterns(sshConfigFiles)
|
hostPatterns, hostPatternsErr := resolveSshConfigPatterns(sshConfigFiles)
|
||||||
if hostPatternsErr != nil {
|
if hostPatternsErr != nil {
|
||||||
return nil, hostPatternsErr
|
return nil, hostPatternsErr
|
||||||
@ -1600,6 +1651,8 @@ func RemoteConfigParseCommand(ctx context.Context, pk *scpacket.FeCommandPacketT
|
|||||||
hostInfoInConfig[hostInfo.CanonicalName] = hostInfo
|
hostInfoInConfig[hostInfo.CanonicalName] = hostInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
remoteChangeList := make(map[string][]string)
|
||||||
|
|
||||||
// remove all previously imported remotes that
|
// remove all previously imported remotes that
|
||||||
// no longer have a canonical pattern in the config files
|
// no longer have a canonical pattern in the config files
|
||||||
for importedRemoteCanonicalName, importedRemote := range previouslyImportedRemotes {
|
for importedRemoteCanonicalName, importedRemote := range previouslyImportedRemotes {
|
||||||
@ -1608,17 +1661,17 @@ func RemoteConfigParseCommand(ctx context.Context, pk *scpacket.FeCommandPacketT
|
|||||||
if !importedRemote.Archived && (hostInfo == nil || hostInfo.Ignore) {
|
if !importedRemote.Archived && (hostInfo == nil || hostInfo.Ignore) {
|
||||||
err = remote.ArchiveRemote(ctx, importedRemote.RemoteId)
|
err = remote.ArchiveRemote(ctx, importedRemote.RemoteId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
remoteChangeList["deleteErr"] = append(remoteChangeList["deleteErr"], importedRemote.RemoteCanonicalName)
|
||||||
log.Printf("sshconfig-import: failed to remove remote \"%s\" (%s)\n", importedRemote.RemoteAlias, importedRemote.RemoteCanonicalName)
|
log.Printf("sshconfig-import: failed to remove remote \"%s\" (%s)\n", importedRemote.RemoteAlias, importedRemote.RemoteCanonicalName)
|
||||||
} else {
|
} else {
|
||||||
|
remoteChangeList["delete"] = append(remoteChangeList["delete"], importedRemote.RemoteCanonicalName)
|
||||||
log.Printf("sshconfig-import: archived remote \"%s\" (%s)\n", importedRemote.RemoteAlias, importedRemote.RemoteCanonicalName)
|
log.Printf("sshconfig-import: archived remote \"%s\" (%s)\n", importedRemote.RemoteAlias, importedRemote.RemoteCanonicalName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var updatedRemotes []string
|
|
||||||
for _, hostInfo := range parsedHostData {
|
for _, hostInfo := range parsedHostData {
|
||||||
previouslyImportedRemote := previouslyImportedRemotes[hostInfo.CanonicalName]
|
previouslyImportedRemote := previouslyImportedRemotes[hostInfo.CanonicalName]
|
||||||
updatedRemotes = append(updatedRemotes, hostInfo.CanonicalName)
|
|
||||||
if hostInfo.Ignore {
|
if hostInfo.Ignore {
|
||||||
log.Printf("sshconfig-import: ignore remote[%s] as specified in config file\n", hostInfo.CanonicalName)
|
log.Printf("sshconfig-import: ignore remote[%s] as specified in config file\n", hostInfo.CanonicalName)
|
||||||
continue
|
continue
|
||||||
@ -1626,27 +1679,31 @@ func RemoteConfigParseCommand(ctx context.Context, pk *scpacket.FeCommandPacketT
|
|||||||
if previouslyImportedRemote != nil && !previouslyImportedRemote.Archived {
|
if previouslyImportedRemote != nil && !previouslyImportedRemote.Archived {
|
||||||
// this already existed and was created via import
|
// this already existed and was created via import
|
||||||
// it needs to be updated instead of created
|
// it needs to be updated instead of created
|
||||||
|
|
||||||
editMap := make(map[string]interface{})
|
editMap := make(map[string]interface{})
|
||||||
editMap[sstore.RemoteField_Alias] = hostInfo.Host
|
editMap[sstore.RemoteField_Alias] = hostInfo.Host
|
||||||
editMap[sstore.RemoteField_ConnectMode] = hostInfo.ConnectMode
|
editMap[sstore.RemoteField_ConnectMode] = hostInfo.ConnectMode
|
||||||
// changing port is unique to imports because it lets us avoid conflicts
|
|
||||||
// if the port is changed in the ssh config
|
|
||||||
editMap[sstore.RemoteField_SSHPort] = hostInfo.Port
|
|
||||||
if hostInfo.SshKeyFile != "" {
|
if hostInfo.SshKeyFile != "" {
|
||||||
editMap[sstore.RemoteField_SSHKey] = hostInfo.SshKeyFile
|
editMap[sstore.RemoteField_SSHKey] = hostInfo.SshKeyFile
|
||||||
}
|
}
|
||||||
msh := remote.GetRemoteById(previouslyImportedRemote.RemoteId)
|
msh := remote.GetRemoteById(previouslyImportedRemote.RemoteId)
|
||||||
if msh == nil {
|
if msh == nil {
|
||||||
|
remoteChangeList["updateErr"] = append(remoteChangeList["updateErr"], hostInfo.CanonicalName)
|
||||||
log.Printf("strange, msh for remote %s [%s] not found\n", hostInfo.CanonicalName, previouslyImportedRemote.RemoteId)
|
log.Printf("strange, msh for remote %s [%s] not found\n", hostInfo.CanonicalName, previouslyImportedRemote.RemoteId)
|
||||||
continue
|
continue
|
||||||
} else {
|
|
||||||
err := msh.UpdateRemote(ctx, editMap)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("error updating remote[%s]: %v\n", hostInfo.CanonicalName, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if msh.Remote.ConnectMode == hostInfo.ConnectMode && msh.Remote.SSHOpts.SSHIdentity == hostInfo.SshKeyFile && msh.Remote.RemoteAlias == hostInfo.Host {
|
||||||
|
// silently skip this one. it didn't fail, but no changes were needed
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err := msh.UpdateRemote(ctx, editMap)
|
||||||
|
if err != nil {
|
||||||
|
remoteChangeList["updateErr"] = append(remoteChangeList["updateErr"], hostInfo.CanonicalName)
|
||||||
|
log.Printf("error updating remote[%s]: %v\n", hostInfo.CanonicalName, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
remoteChangeList["update"] = append(remoteChangeList["update"], hostInfo.CanonicalName)
|
||||||
log.Printf("sshconfig-import: found previously imported remote with canonical name \"%s\": it has been updated\n", hostInfo.CanonicalName)
|
log.Printf("sshconfig-import: found previously imported remote with canonical name \"%s\": it has been updated\n", hostInfo.CanonicalName)
|
||||||
} else {
|
} else {
|
||||||
sshOpts := &sstore.SSHOpts{
|
sshOpts := &sstore.SSHOpts{
|
||||||
@ -1675,21 +1732,31 @@ func RemoteConfigParseCommand(ctx context.Context, pk *scpacket.FeCommandPacketT
|
|||||||
}
|
}
|
||||||
err := remote.AddRemote(ctx, r, false)
|
err := remote.AddRemote(ctx, r, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
remoteChangeList["createErr"] = append(remoteChangeList["createErr"], hostInfo.CanonicalName)
|
||||||
log.Printf("sshconfig-import: failed to add remote \"%s\" (%s): it is being skipped\n", hostInfo.Host, hostInfo.CanonicalName)
|
log.Printf("sshconfig-import: failed to add remote \"%s\" (%s): it is being skipped\n", hostInfo.Host, hostInfo.CanonicalName)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
remoteChangeList["create"] = append(remoteChangeList["create"], hostInfo.CanonicalName)
|
||||||
log.Printf("sshconfig-import: created remote \"%s\" (%s)\n", hostInfo.Host, hostInfo.CanonicalName)
|
log.Printf("sshconfig-import: created remote \"%s\" (%s)\n", hostInfo.Host, hostInfo.CanonicalName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
update := &sstore.ModelUpdate{Remotes: remote.GetAllRemoteRuntimeState()}
|
outMsg := createSshImportSummary(remoteChangeList)
|
||||||
update.Info = &sstore.InfoMsgType{}
|
visualEdit := resolveBool(pk.Kwargs["visual"], false)
|
||||||
if len(updatedRemotes) == 0 {
|
if visualEdit {
|
||||||
update.Info.InfoMsg = "no connections imported from ssh config."
|
update := &sstore.ModelUpdate{}
|
||||||
|
update.AlertMessage = &sstore.AlertMessageType{
|
||||||
|
Title: "SSH Config Import",
|
||||||
|
Message: outMsg,
|
||||||
|
Markdown: true,
|
||||||
|
}
|
||||||
|
return update, nil
|
||||||
} else {
|
} else {
|
||||||
update.Info.InfoMsg = fmt.Sprintf("imported %d connection(s) from ssh config file: %s\n", len(updatedRemotes), strings.Join(updatedRemotes, ", "))
|
update := &sstore.ModelUpdate{}
|
||||||
|
update.Info = &sstore.InfoMsgType{}
|
||||||
|
update.Info.InfoMsg = outMsg
|
||||||
|
return update, nil
|
||||||
}
|
}
|
||||||
return update, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ScreenShowAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
func ScreenShowAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
||||||
@ -1934,6 +2001,114 @@ func doOpenAICompletion(cmd *sstore.CmdType, opts *sstore.OpenAIOptsType, prompt
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func writePacketToUpdateBus(ctx context.Context, cmd *sstore.CmdType, pk *packet.OpenAICmdInfoChatMessage) {
|
||||||
|
update, err := sstore.UpdateWithAddNewOpenAICmdInfoPacket(ctx, cmd.ScreenId, pk)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Open AI Update packet err: %v\n", err)
|
||||||
|
}
|
||||||
|
sstore.MainBus.SendScreenUpdate(cmd.ScreenId, update)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateAsstResponseAndWriteToUpdateBus(ctx context.Context, cmd *sstore.CmdType, pk *packet.OpenAICmdInfoChatMessage, messageID int) {
|
||||||
|
update, err := sstore.UpdateWithUpdateOpenAICmdInfoPacket(ctx, cmd.ScreenId, messageID, pk)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Open AI Update packet err: %v\n", err)
|
||||||
|
}
|
||||||
|
sstore.MainBus.SendScreenUpdate(cmd.ScreenId, update)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCmdInfoEngineeredPrompt(userQuery string, curLineStr string) string {
|
||||||
|
rtn := "You are an expert on the command line terminal. Your task is to help me write a command."
|
||||||
|
if curLineStr != "" {
|
||||||
|
rtn += "My current command is: " + curLineStr
|
||||||
|
}
|
||||||
|
rtn += ". My question is: " + userQuery + "."
|
||||||
|
return rtn
|
||||||
|
}
|
||||||
|
|
||||||
|
func doOpenAICmdInfoCompletion(cmd *sstore.CmdType, clientId string, opts *sstore.OpenAIOptsType, prompt []packet.OpenAIPromptMessageType, curLineStr string) {
|
||||||
|
var hadError bool
|
||||||
|
log.Println("had error: ", hadError)
|
||||||
|
ctx, cancelFn := context.WithTimeout(context.Background(), OpenAIStreamTimeout)
|
||||||
|
defer cancelFn()
|
||||||
|
defer func() {
|
||||||
|
r := recover()
|
||||||
|
if r != nil {
|
||||||
|
panicMsg := fmt.Sprintf("panic: %v", r)
|
||||||
|
log.Printf("panic in doOpenAICompletion: %s\n", panicMsg)
|
||||||
|
hadError = true
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
var ch chan *packet.OpenAIPacketType
|
||||||
|
var err error
|
||||||
|
if opts.APIToken == "" {
|
||||||
|
var conn *websocket.Conn
|
||||||
|
ch, conn, err = openai.RunCloudCompletionStream(ctx, clientId, opts, prompt)
|
||||||
|
if conn != nil {
|
||||||
|
defer conn.Close()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ch, err = openai.RunCompletionStream(ctx, opts, prompt)
|
||||||
|
}
|
||||||
|
asstOutputPk := &packet.OpenAICmdInfoPacketOutputType{
|
||||||
|
Model: "",
|
||||||
|
Created: 0,
|
||||||
|
FinishReason: "",
|
||||||
|
Message: "",
|
||||||
|
}
|
||||||
|
asstOutputMessageID := sstore.ScreenMemGetCmdInfoMessageCount(cmd.ScreenId)
|
||||||
|
asstMessagePk := &packet.OpenAICmdInfoChatMessage{IsAssistantResponse: true, AssistantResponse: asstOutputPk, MessageID: asstOutputMessageID}
|
||||||
|
if err != nil {
|
||||||
|
asstOutputPk.Error = fmt.Sprintf("Error calling OpenAI API: %v", err)
|
||||||
|
writePacketToUpdateBus(ctx, cmd, asstMessagePk)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writePacketToUpdateBus(ctx, cmd, asstMessagePk)
|
||||||
|
doneWaitingForPackets := false
|
||||||
|
for !doneWaitingForPackets {
|
||||||
|
select {
|
||||||
|
case <-time.After(OpenAIPacketTimeout):
|
||||||
|
// timeout reading from channel
|
||||||
|
hadError = true
|
||||||
|
doneWaitingForPackets = true
|
||||||
|
asstOutputPk.Error = "timeout waiting for server response"
|
||||||
|
updateAsstResponseAndWriteToUpdateBus(ctx, cmd, asstMessagePk, asstOutputMessageID)
|
||||||
|
break
|
||||||
|
case pk, ok := <-ch:
|
||||||
|
if ok {
|
||||||
|
// got a packet
|
||||||
|
if pk.Error != "" {
|
||||||
|
hadError = true
|
||||||
|
asstOutputPk.Error = pk.Error
|
||||||
|
}
|
||||||
|
if pk.Model != "" && pk.Index == 0 {
|
||||||
|
asstOutputPk.Model = pk.Model
|
||||||
|
asstOutputPk.Created = pk.Created
|
||||||
|
asstOutputPk.FinishReason = pk.FinishReason
|
||||||
|
if pk.Text != "" {
|
||||||
|
asstOutputPk.Message += pk.Text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pk.Index == 0 {
|
||||||
|
if pk.FinishReason != "" {
|
||||||
|
asstOutputPk.FinishReason = pk.FinishReason
|
||||||
|
}
|
||||||
|
if pk.Text != "" {
|
||||||
|
asstOutputPk.Message += pk.Text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
asstMessagePk.AssistantResponse = asstOutputPk
|
||||||
|
updateAsstResponseAndWriteToUpdateBus(ctx, cmd, asstMessagePk, asstOutputMessageID)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// channel closed
|
||||||
|
doneWaitingForPackets = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func doOpenAIStreamCompletion(cmd *sstore.CmdType, clientId string, opts *sstore.OpenAIOptsType, prompt []packet.OpenAIPromptMessageType) {
|
func doOpenAIStreamCompletion(cmd *sstore.CmdType, clientId string, opts *sstore.OpenAIOptsType, prompt []packet.OpenAIPromptMessageType) {
|
||||||
var outputPos int64
|
var outputPos int64
|
||||||
var hadError bool
|
var hadError bool
|
||||||
@ -2019,6 +2194,23 @@ func doOpenAIStreamCompletion(cmd *sstore.CmdType, clientId string, opts *sstore
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BuildOpenAIPromptArrayWithContext(messages []*packet.OpenAICmdInfoChatMessage) []packet.OpenAIPromptMessageType {
|
||||||
|
rtn := make([]packet.OpenAIPromptMessageType, 0)
|
||||||
|
for _, msg := range messages {
|
||||||
|
content := msg.UserEngineeredQuery
|
||||||
|
if msg.UserEngineeredQuery == "" {
|
||||||
|
content = msg.UserQuery
|
||||||
|
}
|
||||||
|
msgRole := sstore.OpenAIRoleUser
|
||||||
|
if msg.IsAssistantResponse {
|
||||||
|
msgRole = sstore.OpenAIRoleAssistant
|
||||||
|
content = msg.AssistantResponse.Message
|
||||||
|
}
|
||||||
|
rtn = append(rtn, packet.OpenAIPromptMessageType{Role: msgRole, Content: content})
|
||||||
|
}
|
||||||
|
return rtn
|
||||||
|
}
|
||||||
|
|
||||||
func OpenAICommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
func OpenAICommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
||||||
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen)
|
ids, err := resolveUiIds(ctx, pk, R_Session|R_Screen)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -2044,9 +2236,6 @@ func OpenAICommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor
|
|||||||
opts.MaxTokens = openai.DefaultMaxTokens
|
opts.MaxTokens = openai.DefaultMaxTokens
|
||||||
}
|
}
|
||||||
promptStr := firstArg(pk)
|
promptStr := firstArg(pk)
|
||||||
if promptStr == "" {
|
|
||||||
return nil, fmt.Errorf("openai error, prompt string is blank")
|
|
||||||
}
|
|
||||||
ptermVal := defaultStr(pk.Kwargs["wterm"], DefaultPTERM)
|
ptermVal := defaultStr(pk.Kwargs["wterm"], DefaultPTERM)
|
||||||
pkTermOpts, err := GetUITermOpts(pk.UIContext.WinSize, ptermVal)
|
pkTermOpts, err := GetUITermOpts(pk.UIContext.WinSize, ptermVal)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -2057,11 +2246,40 @@ func OpenAICommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstor
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("openai error, cannot make dyn cmd")
|
return nil, fmt.Errorf("openai error, cannot make dyn cmd")
|
||||||
}
|
}
|
||||||
|
if resolveBool(pk.Kwargs["cmdinfo"], false) {
|
||||||
|
if promptStr == "" {
|
||||||
|
// this is requesting an update without wanting an openai query
|
||||||
|
update, err := sstore.UpdateWithCurrentOpenAICmdInfoChat(cmd.ScreenId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error getting update for CmdInfoChat %v", err)
|
||||||
|
}
|
||||||
|
return update, nil
|
||||||
|
}
|
||||||
|
curLineStr := defaultStr(pk.Kwargs["curline"], "")
|
||||||
|
userQueryPk := &packet.OpenAICmdInfoChatMessage{UserQuery: promptStr, MessageID: sstore.ScreenMemGetCmdInfoMessageCount(cmd.ScreenId)}
|
||||||
|
engineeredQuery := getCmdInfoEngineeredPrompt(promptStr, curLineStr)
|
||||||
|
userQueryPk.UserEngineeredQuery = engineeredQuery
|
||||||
|
writePacketToUpdateBus(ctx, cmd, userQueryPk)
|
||||||
|
prompt := BuildOpenAIPromptArrayWithContext(sstore.ScreenMemGetCmdInfoChat(cmd.ScreenId).Messages)
|
||||||
|
go doOpenAICmdInfoCompletion(cmd, clientData.ClientId, opts, prompt, curLineStr)
|
||||||
|
update := &sstore.ModelUpdate{}
|
||||||
|
return update, nil
|
||||||
|
}
|
||||||
|
prompt := []packet.OpenAIPromptMessageType{{Role: sstore.OpenAIRoleUser, Content: promptStr}}
|
||||||
|
if resolveBool(pk.Kwargs["cmdinfoclear"], false) {
|
||||||
|
update, err := sstore.UpdateWithClearOpenAICmdInfo(cmd.ScreenId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error clearing CmdInfoChat: %v", err)
|
||||||
|
}
|
||||||
|
return update, nil
|
||||||
|
}
|
||||||
|
if promptStr == "" {
|
||||||
|
return nil, fmt.Errorf("openai error, prompt string is blank")
|
||||||
|
}
|
||||||
line, err := sstore.AddOpenAILine(ctx, ids.ScreenId, DefaultUserId, cmd)
|
line, err := sstore.AddOpenAILine(ctx, ids.ScreenId, DefaultUserId, cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot add new line: %v", err)
|
return nil, fmt.Errorf("cannot add new line: %v", err)
|
||||||
}
|
}
|
||||||
prompt := []packet.OpenAIPromptMessageType{{Role: sstore.OpenAIRoleUser, Content: promptStr}}
|
|
||||||
if resolveBool(pk.Kwargs["stream"], true) {
|
if resolveBool(pk.Kwargs["stream"], true) {
|
||||||
go doOpenAIStreamCompletion(cmd, clientData.ClientId, opts, prompt)
|
go doOpenAIStreamCompletion(cmd, clientData.ClientId, opts, prompt)
|
||||||
} else {
|
} else {
|
||||||
@ -3441,7 +3659,7 @@ func LineDeleteCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s
|
|||||||
}
|
}
|
||||||
err = sstore.DeleteLinesByIds(ctx, ids.ScreenId, lineIds)
|
err = sstore.DeleteLinesByIds(ctx, ids.ScreenId, lineIds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("/line:delete error purging lines: %v", err)
|
return nil, fmt.Errorf("/line:delete error deleting lines: %v", err)
|
||||||
}
|
}
|
||||||
update := &sstore.ModelUpdate{}
|
update := &sstore.ModelUpdate{}
|
||||||
for _, lineId := range lineIds {
|
for _, lineId := range lineIds {
|
||||||
@ -3452,6 +3670,11 @@ func LineDeleteCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (s
|
|||||||
}
|
}
|
||||||
update.Lines = append(update.Lines, lineObj)
|
update.Lines = append(update.Lines, lineObj)
|
||||||
}
|
}
|
||||||
|
screen, err := sstore.FixupScreenSelectedLine(ctx, ids.ScreenId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("/line:delete error fixing up screen: %v", err)
|
||||||
|
}
|
||||||
|
update.Screens = []*sstore.ScreenType{screen}
|
||||||
return update, nil
|
return update, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4019,6 +4242,57 @@ func ClientAcceptTosCommand(ctx context.Context, pk *scpacket.FeCommandPacketTyp
|
|||||||
return update, nil
|
return update, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var confirmKeyRe = regexp.MustCompile(`^[a-z][a-z0-9_]*$`)
|
||||||
|
|
||||||
|
// confirm flags must be all lowercase and only contain letters, numbers, and underscores (and start with letter)
|
||||||
|
func ClientConfirmFlagCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (sstore.UpdatePacket, error) {
|
||||||
|
// Check for valid arguments length
|
||||||
|
if len(pk.Args) < 2 {
|
||||||
|
return nil, fmt.Errorf("invalid arguments: expected at least 2, got %d", len(pk.Args))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract confirmKey and value from pk.Args
|
||||||
|
confirmKey := pk.Args[0]
|
||||||
|
if !confirmKeyRe.MatchString(confirmKey) {
|
||||||
|
return nil, fmt.Errorf("invalid confirm flag key: %s", confirmKey)
|
||||||
|
}
|
||||||
|
value := resolveBool(pk.Args[1], true)
|
||||||
|
validKey := utilfn.ContainsStr(ConfirmFlags, confirmKey)
|
||||||
|
if !validKey {
|
||||||
|
return nil, fmt.Errorf("invalid confirm flag key: %s", confirmKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
clientData, err := sstore.EnsureClientData(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot retrieve client data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize ConfirmFlags if it's nil
|
||||||
|
if clientData.ClientOpts.ConfirmFlags == nil {
|
||||||
|
clientData.ClientOpts.ConfirmFlags = make(map[string]bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the confirm flag
|
||||||
|
clientData.ClientOpts.ConfirmFlags[confirmKey] = value
|
||||||
|
|
||||||
|
err = sstore.SetClientOpts(ctx, clientData.ClientOpts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error updating client data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve updated client data
|
||||||
|
clientData, err = sstore.EnsureClientData(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot retrieve updated client data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
update := &sstore.ModelUpdate{
|
||||||
|
ClientData: clientData,
|
||||||
|
}
|
||||||
|
|
||||||
|
return update, nil
|
||||||
|
}
|
||||||
|
|
||||||
func validateOpenAIAPIToken(key string) error {
|
func validateOpenAIAPIToken(key string) error {
|
||||||
if len(key) > MaxOpenAIAPITokenLen {
|
if len(key) > MaxOpenAIAPITokenLen {
|
||||||
return fmt.Errorf("invalid openai token, too long")
|
return fmt.Errorf("invalid openai token, too long")
|
||||||
|
@ -11,10 +11,10 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/remote"
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/remote"
|
||||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/scpacket"
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/scpacket"
|
||||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/sstore"
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/sstore"
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -21,6 +21,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/alessio/shellescape"
|
||||||
"github.com/armon/circbuf"
|
"github.com/armon/circbuf"
|
||||||
"github.com/creack/pty"
|
"github.com/creack/pty"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@ -66,9 +67,9 @@ func MakeLocalMShellCommandStr(isSudo bool) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if isSudo {
|
if isSudo {
|
||||||
return fmt.Sprintf(`%s; sudo %s --server`, PrintPingPacket, mshellPath), nil
|
return fmt.Sprintf(`%s; sudo %s --server`, PrintPingPacket, shellescape.Quote(mshellPath)), nil
|
||||||
} else {
|
} else {
|
||||||
return fmt.Sprintf(`%s; %s --server`, PrintPingPacket, mshellPath), nil
|
return fmt.Sprintf(`%s; %s --server`, PrintPingPacket, shellescape.Quote(mshellPath)), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1125,6 +1126,9 @@ func addScVarsToState(state *packet.ShellState) *packet.ShellState {
|
|||||||
envMap := shexec.DeclMapFromState(&rtn)
|
envMap := shexec.DeclMapFromState(&rtn)
|
||||||
envMap["PROMPT"] = &shexec.DeclareDeclType{Name: "PROMPT", Value: "1", Args: "x"}
|
envMap["PROMPT"] = &shexec.DeclareDeclType{Name: "PROMPT", Value: "1", Args: "x"}
|
||||||
envMap["PROMPT_VERSION"] = &shexec.DeclareDeclType{Name: "PROMPT_VERSION", Value: scbase.WaveVersion, Args: "x"}
|
envMap["PROMPT_VERSION"] = &shexec.DeclareDeclType{Name: "PROMPT_VERSION", Value: scbase.WaveVersion, Args: "x"}
|
||||||
|
if _, exists := envMap["LANG"]; !exists {
|
||||||
|
envMap["LANG"] = &shexec.DeclareDeclType{Name: "LANG", Value: scbase.DetermineLang(), Args: "x"}
|
||||||
|
}
|
||||||
rtn.ShellVars = shexec.SerializeDeclMap(envMap)
|
rtn.ShellVars = shexec.SerializeDeclMap(envMap)
|
||||||
return &rtn
|
return &rtn
|
||||||
}
|
}
|
||||||
|
@ -377,3 +377,30 @@ func MacOSRelease() string {
|
|||||||
})
|
})
|
||||||
return osRelease
|
return osRelease
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var osLangOnce = &sync.Once{}
|
||||||
|
var osLang string
|
||||||
|
|
||||||
|
func determineLang() string {
|
||||||
|
ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancelFn()
|
||||||
|
if runtime.GOOS == "darwin" {
|
||||||
|
out, err := exec.CommandContext(ctx, "defaults", "read", "-g", "AppleLocale").CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error executing 'defaults read -g AppleLocale': %v\n", err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(out)) + ".UTF-8"
|
||||||
|
} else {
|
||||||
|
// this is specifically to get the wavesrv LANG so waveshell
|
||||||
|
// on a remote uses the same LANG
|
||||||
|
return os.Getenv("LANG")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func DetermineLang() string {
|
||||||
|
osLangOnce.Do(func() {
|
||||||
|
osLang = determineLang()
|
||||||
|
})
|
||||||
|
return osLang
|
||||||
|
}
|
||||||
|
@ -240,7 +240,8 @@ func (c *parseContext) tokenizeDQ() ([]*WordType, bool) {
|
|||||||
|
|
||||||
// returns (words, eofexit)
|
// returns (words, eofexit)
|
||||||
// backticks (WordTypeBQ) handle backslash in a special way, but that seems to mainly effect execution (not completion)
|
// backticks (WordTypeBQ) handle backslash in a special way, but that seems to mainly effect execution (not completion)
|
||||||
// de_backslash => removes initial backslash in \`, \\, and \$ before execution
|
//
|
||||||
|
// de_backslash => removes initial backslash in \`, \\, and \$ before execution
|
||||||
func (c *parseContext) tokenizeRaw() ([]*WordType, bool) {
|
func (c *parseContext) tokenizeRaw() ([]*WordType, bool) {
|
||||||
state := &tokenizeOutputState{}
|
state := &tokenizeOutputState{}
|
||||||
isExpSubShell := c.QC.cur() == WordTypeDP
|
isExpSubShell := c.QC.cur() == WordTypeDP
|
||||||
|
@ -748,6 +748,7 @@ func InsertScreen(ctx context.Context, sessionId string, origScreenName string,
|
|||||||
return nil, txErr
|
return nil, txErr
|
||||||
}
|
}
|
||||||
update.Sessions = []*SessionType{bareSession}
|
update.Sessions = []*SessionType{bareSession}
|
||||||
|
update.OpenAICmdInfoChat = ScreenMemGetCmdInfoChat(newScreenId).Messages
|
||||||
}
|
}
|
||||||
return update, nil
|
return update, nil
|
||||||
}
|
}
|
||||||
@ -854,6 +855,29 @@ func GetCmdByScreenId(ctx context.Context, screenId string, lineId string) (*Cmd
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func UpdateWithClearOpenAICmdInfo(screenId string) (*ModelUpdate, error) {
|
||||||
|
ScreenMemClearCmdInfoChat(screenId)
|
||||||
|
return UpdateWithCurrentOpenAICmdInfoChat(screenId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateWithAddNewOpenAICmdInfoPacket(ctx context.Context, screenId string, pk *packet.OpenAICmdInfoChatMessage) (*ModelUpdate, error) {
|
||||||
|
ScreenMemAddCmdInfoChatMessage(screenId, pk)
|
||||||
|
return UpdateWithCurrentOpenAICmdInfoChat(screenId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateWithCurrentOpenAICmdInfoChat(screenId string) (*ModelUpdate, error) {
|
||||||
|
cmdInfoUpdate := ScreenMemGetCmdInfoChat(screenId).Messages
|
||||||
|
return &ModelUpdate{OpenAICmdInfoChat: cmdInfoUpdate}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateWithUpdateOpenAICmdInfoPacket(ctx context.Context, screenId string, messageID int, pk *packet.OpenAICmdInfoChatMessage) (*ModelUpdate, error) {
|
||||||
|
err := ScreenMemUpdateCmdInfoChatMessage(screenId, messageID, pk)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return UpdateWithCurrentOpenAICmdInfoChat(screenId)
|
||||||
|
}
|
||||||
|
|
||||||
func UpdateCmdDoneInfo(ctx context.Context, ck base.CommandKey, donePk *packet.CmdDonePacketType, status string) (*ModelUpdate, error) {
|
func UpdateCmdDoneInfo(ctx context.Context, ck base.CommandKey, donePk *packet.CmdDonePacketType, status string) (*ModelUpdate, error) {
|
||||||
if donePk == nil {
|
if donePk == nil {
|
||||||
return nil, fmt.Errorf("invalid cmddone packet")
|
return nil, fmt.Errorf("invalid cmddone packet")
|
||||||
@ -1039,6 +1063,7 @@ func SwitchScreenById(ctx context.Context, sessionId string, screenId string) (*
|
|||||||
memState := GetScreenMemState(screenId)
|
memState := GetScreenMemState(screenId)
|
||||||
if memState != nil {
|
if memState != nil {
|
||||||
update.CmdLine = &memState.CmdInputText
|
update.CmdLine = &memState.CmdInputText
|
||||||
|
update.OpenAICmdInfoChat = ScreenMemGetCmdInfoChat(screenId).Messages
|
||||||
}
|
}
|
||||||
return update, nil
|
return update, nil
|
||||||
}
|
}
|
||||||
@ -1704,7 +1729,6 @@ const (
|
|||||||
RemoteField_ConnectMode = "connectmode" // string
|
RemoteField_ConnectMode = "connectmode" // string
|
||||||
RemoteField_SSHKey = "sshkey" // string
|
RemoteField_SSHKey = "sshkey" // string
|
||||||
RemoteField_SSHPassword = "sshpassword" // string
|
RemoteField_SSHPassword = "sshpassword" // string
|
||||||
RemoteField_SSHPort = "sshport" // string
|
|
||||||
RemoteField_Color = "color" // string
|
RemoteField_Color = "color" // string
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1736,10 +1760,6 @@ func UpdateRemote(ctx context.Context, remoteId string, editMap map[string]inter
|
|||||||
query = `UPDATE remote SET sshopts = json_set(sshopts, '$.sshpassword', ?) WHERE remoteid = ?`
|
query = `UPDATE remote SET sshopts = json_set(sshopts, '$.sshpassword', ?) WHERE remoteid = ?`
|
||||||
tx.Exec(query, sshPassword, remoteId)
|
tx.Exec(query, sshPassword, remoteId)
|
||||||
}
|
}
|
||||||
if sshPort, found := editMap[RemoteField_SSHPort]; found {
|
|
||||||
query = `UPDATE remote SET sshopts = json_set(sshopts, '$.sshport', ?) WHERE remoteid = ?`
|
|
||||||
tx.Exec(query, sshPort, remoteId)
|
|
||||||
}
|
|
||||||
if color, found := editMap[RemoteField_Color]; found {
|
if color, found := editMap[RemoteField_Color]; found {
|
||||||
query = `UPDATE remote SET remoteopts = json_set(remoteopts, '$.color', ?) WHERE remoteid = ?`
|
query = `UPDATE remote SET remoteopts = json_set(remoteopts, '$.color', ?) WHERE remoteid = ?`
|
||||||
tx.Exec(query, color, remoteId)
|
tx.Exec(query, color, remoteId)
|
||||||
@ -2044,6 +2064,29 @@ func SetLineArchivedById(ctx context.Context, screenId string, lineId string, ar
|
|||||||
return txErr
|
return txErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// returns updated screen (only if updated)
|
||||||
|
func FixupScreenSelectedLine(ctx context.Context, screenId string) (*ScreenType, error) {
|
||||||
|
return WithTxRtn(ctx, func(tx *TxWrap) (*ScreenType, error) {
|
||||||
|
query := `SELECT selectedline FROM screen WHERE screenid = ?`
|
||||||
|
sline := tx.GetInt(query, screenId)
|
||||||
|
query = `SELECT linenum FROM line WHERE screenid = ? AND linenum = ?`
|
||||||
|
if tx.Exists(query, screenId, sline) {
|
||||||
|
// selected line is valid
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
query = `SELECT min(linenum) FROM line WHERE screenid = ? AND linenum > ?`
|
||||||
|
newSLine := tx.GetInt(query, screenId, sline)
|
||||||
|
if newSLine == 0 {
|
||||||
|
query = `SELECT max(linenum) FROM line WHERE screenid = ? AND linenum < ?`
|
||||||
|
newSLine = tx.GetInt(query, screenId, sline)
|
||||||
|
}
|
||||||
|
// newSLine might be 0, but that's ok (because that means there are no lines)
|
||||||
|
query = `UPDATE screen SET selectedline = ? WHERE screenid = ?`
|
||||||
|
tx.Exec(query, newSLine, screenId)
|
||||||
|
return GetScreenById(tx.Context(), screenId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func DeleteLinesByIds(ctx context.Context, screenId string, lineIds []string) error {
|
func DeleteLinesByIds(ctx context.Context, screenId string, lineIds []string) error {
|
||||||
txErr := WithTx(ctx, func(tx *TxWrap) error {
|
txErr := WithTx(ctx, func(tx *TxWrap) error {
|
||||||
isWS := isWebShare(tx, screenId)
|
isWS := isWebShare(tx, screenId)
|
||||||
@ -2051,9 +2094,8 @@ func DeleteLinesByIds(ctx context.Context, screenId string, lineIds []string) er
|
|||||||
query := `SELECT status FROM cmd WHERE screenid = ? AND lineid = ?`
|
query := `SELECT status FROM cmd WHERE screenid = ? AND lineid = ?`
|
||||||
cmdStatus := tx.GetString(query, screenId, lineId)
|
cmdStatus := tx.GetString(query, screenId, lineId)
|
||||||
if cmdStatus == CmdStatusRunning {
|
if cmdStatus == CmdStatusRunning {
|
||||||
return fmt.Errorf("cannot delete line[%s:%s], cmd is running", screenId, lineId)
|
return fmt.Errorf("cannot delete line[%s], cmd is running", lineId)
|
||||||
}
|
}
|
||||||
|
|
||||||
query = `DELETE FROM line WHERE screenid = ? AND lineid = ?`
|
query = `DELETE FROM line WHERE screenid = ? AND lineid = ?`
|
||||||
tx.Exec(query, screenId, lineId)
|
tx.Exec(query, screenId, lineId)
|
||||||
query = `DELETE FROM cmd WHERE screenid = ? AND lineid = ?`
|
query = `DELETE FROM cmd WHERE screenid = ? AND lineid = ?`
|
||||||
@ -2061,7 +2103,6 @@ func DeleteLinesByIds(ctx context.Context, screenId string, lineIds []string) er
|
|||||||
// don't delete history anymore, just remove lineid reference
|
// don't delete history anymore, just remove lineid reference
|
||||||
query = `UPDATE history SET lineid = '', linenum = 0 WHERE screenid = ? AND lineid = ?`
|
query = `UPDATE history SET lineid = '', linenum = 0 WHERE screenid = ? AND lineid = ?`
|
||||||
tx.Exec(query, screenId, lineId)
|
tx.Exec(query, screenId, lineId)
|
||||||
|
|
||||||
if isWS {
|
if isWS {
|
||||||
insertScreenLineUpdate(tx, screenId, lineId, UpdateType_LineDel)
|
insertScreenLineUpdate(tx, screenId, lineId, UpdateType_LineDel)
|
||||||
}
|
}
|
||||||
|
@ -5,9 +5,11 @@
|
|||||||
package sstore
|
package sstore
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/wavetermdev/waveterm/waveshell/pkg/packet"
|
||||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/utilfn"
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/utilfn"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -43,11 +45,109 @@ func isIndicatorGreater(i1 string, i2 string) bool {
|
|||||||
return screenIndicatorLevels[i1] > screenIndicatorLevels[i2]
|
return screenIndicatorLevels[i1] > screenIndicatorLevels[i2]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OpenAICmdInfoChatStore struct {
|
||||||
|
MessageCount int `json:"messagecount"`
|
||||||
|
Messages []*packet.OpenAICmdInfoChatMessage `json:"messages"`
|
||||||
|
}
|
||||||
|
|
||||||
type ScreenMemState struct {
|
type ScreenMemState struct {
|
||||||
NumRunningCommands int `json:"numrunningcommands,omitempty"`
|
NumRunningCommands int `json:"numrunningcommands,omitempty"`
|
||||||
IndicatorType string `json:"indicatortype,omitempty"`
|
IndicatorType string `json:"indicatortype,omitempty"`
|
||||||
CmdInputText utilfn.StrWithPos `json:"cmdinputtext,omitempty"`
|
CmdInputText utilfn.StrWithPos `json:"cmdinputtext,omitempty"`
|
||||||
CmdInputSeqNum int `json:"cmdinputseqnum,omitempty"`
|
CmdInputSeqNum int `json:"cmdinputseqnum,omitempty"`
|
||||||
|
AICmdInfoChat *OpenAICmdInfoChatStore `json:"aicmdinfochat,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ScreenMemDeepCopyCmdInfoChatStore(store *OpenAICmdInfoChatStore) *OpenAICmdInfoChatStore {
|
||||||
|
rtnMessages := []*packet.OpenAICmdInfoChatMessage{}
|
||||||
|
for index := 0; index < len(store.Messages); index++ {
|
||||||
|
messageToCopy := *store.Messages[index]
|
||||||
|
if messageToCopy.AssistantResponse != nil {
|
||||||
|
assistantResponseCopy := *messageToCopy.AssistantResponse
|
||||||
|
messageToCopy.AssistantResponse = &assistantResponseCopy
|
||||||
|
}
|
||||||
|
rtnMessages = append(rtnMessages, &messageToCopy)
|
||||||
|
}
|
||||||
|
rtn := &OpenAICmdInfoChatStore{MessageCount: store.MessageCount, Messages: rtnMessages}
|
||||||
|
return rtn
|
||||||
|
}
|
||||||
|
|
||||||
|
func ScreenMemInitCmdInfoChat(screenId string) {
|
||||||
|
greetingMessagePk := &packet.OpenAICmdInfoChatMessage{
|
||||||
|
MessageID: 0,
|
||||||
|
IsAssistantResponse: true,
|
||||||
|
AssistantResponse: &packet.OpenAICmdInfoPacketOutputType{
|
||||||
|
Message: packet.OpenAICmdInfoChatGreetingMessage,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ScreenMemStore[screenId].AICmdInfoChat = &OpenAICmdInfoChatStore{MessageCount: 1, Messages: []*packet.OpenAICmdInfoChatMessage{greetingMessagePk}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ScreenMemClearCmdInfoChat(screenId string) {
|
||||||
|
MemLock.Lock()
|
||||||
|
defer MemLock.Unlock()
|
||||||
|
if ScreenMemStore[screenId] == nil {
|
||||||
|
ScreenMemStore[screenId] = &ScreenMemState{}
|
||||||
|
}
|
||||||
|
ScreenMemInitCmdInfoChat(screenId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ScreenMemAddCmdInfoChatMessage(screenId string, msg *packet.OpenAICmdInfoChatMessage) {
|
||||||
|
MemLock.Lock()
|
||||||
|
defer MemLock.Unlock()
|
||||||
|
if ScreenMemStore[screenId] == nil {
|
||||||
|
ScreenMemStore[screenId] = &ScreenMemState{}
|
||||||
|
}
|
||||||
|
if ScreenMemStore[screenId].AICmdInfoChat == nil {
|
||||||
|
log.Printf("AICmdInfoChat is null, creating")
|
||||||
|
ScreenMemInitCmdInfoChat(screenId)
|
||||||
|
}
|
||||||
|
|
||||||
|
CmdInfoChat := ScreenMemStore[screenId].AICmdInfoChat
|
||||||
|
CmdInfoChat.Messages = append(CmdInfoChat.Messages, msg)
|
||||||
|
CmdInfoChat.MessageCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
func ScreenMemGetCmdInfoMessageCount(screenId string) int {
|
||||||
|
MemLock.Lock()
|
||||||
|
defer MemLock.Unlock()
|
||||||
|
if ScreenMemStore[screenId] == nil {
|
||||||
|
ScreenMemStore[screenId] = &ScreenMemState{}
|
||||||
|
}
|
||||||
|
if ScreenMemStore[screenId].AICmdInfoChat == nil {
|
||||||
|
ScreenMemInitCmdInfoChat(screenId)
|
||||||
|
}
|
||||||
|
return ScreenMemStore[screenId].AICmdInfoChat.MessageCount
|
||||||
|
}
|
||||||
|
|
||||||
|
func ScreenMemGetCmdInfoChat(screenId string) *OpenAICmdInfoChatStore {
|
||||||
|
MemLock.Lock()
|
||||||
|
defer MemLock.Unlock()
|
||||||
|
if ScreenMemStore[screenId] == nil {
|
||||||
|
ScreenMemStore[screenId] = &ScreenMemState{}
|
||||||
|
}
|
||||||
|
if ScreenMemStore[screenId].AICmdInfoChat == nil {
|
||||||
|
ScreenMemInitCmdInfoChat(screenId)
|
||||||
|
}
|
||||||
|
return ScreenMemDeepCopyCmdInfoChatStore(ScreenMemStore[screenId].AICmdInfoChat)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ScreenMemUpdateCmdInfoChatMessage(screenId string, messageID int, msg *packet.OpenAICmdInfoChatMessage) error {
|
||||||
|
MemLock.Lock()
|
||||||
|
defer MemLock.Unlock()
|
||||||
|
if ScreenMemStore[screenId] == nil {
|
||||||
|
ScreenMemStore[screenId] = &ScreenMemState{}
|
||||||
|
}
|
||||||
|
if ScreenMemStore[screenId].AICmdInfoChat == nil {
|
||||||
|
ScreenMemInitCmdInfoChat(screenId)
|
||||||
|
}
|
||||||
|
CmdInfoChat := ScreenMemStore[screenId].AICmdInfoChat
|
||||||
|
if messageID >= 0 && messageID < len(CmdInfoChat.Messages) {
|
||||||
|
CmdInfoChat.Messages[messageID] = msg
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("ScreenMemUpdateCmdInfoChatMessage: error: Message Id out of range: %d", messageID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ScreenMemSetCmdInputText(screenId string, sp utilfn.StrWithPos, seqNum int) {
|
func ScreenMemSetCmdInputText(screenId string, sp utilfn.StrWithPos, seqNum int) {
|
||||||
|
@ -22,7 +22,7 @@ import (
|
|||||||
"github.com/golang-migrate/migrate/v4"
|
"github.com/golang-migrate/migrate/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
const MaxMigration = 28
|
const MaxMigration = 29
|
||||||
const MigratePrimaryScreenVersion = 9
|
const MigratePrimaryScreenVersion = 9
|
||||||
const CmdScreenSpecialMigration = 13
|
const CmdScreenSpecialMigration = 13
|
||||||
const CmdLineSpecialMigration = 20
|
const CmdLineSpecialMigration = 20
|
||||||
|
@ -267,9 +267,10 @@ func (tdata *TelemetryData) Scan(val interface{}) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ClientOptsType struct {
|
type ClientOptsType struct {
|
||||||
NoTelemetry bool `json:"notelemetry,omitempty"`
|
NoTelemetry bool `json:"notelemetry,omitempty"`
|
||||||
NoReleaseCheck bool `json:"noreleasecheck,omitempty"`
|
NoReleaseCheck bool `json:"noreleasecheck,omitempty"`
|
||||||
AcceptedTos int64 `json:"acceptedtos,omitempty"`
|
AcceptedTos int64 `json:"acceptedtos,omitempty"`
|
||||||
|
ConfirmFlags map[string]bool `json:"confirmflags,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FeOptsType struct {
|
type FeOptsType struct {
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/wavetermdev/waveterm/waveshell/pkg/packet"
|
||||||
"github.com/wavetermdev/waveterm/wavesrv/pkg/utilfn"
|
"github.com/wavetermdev/waveterm/wavesrv/pkg/utilfn"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -38,28 +39,30 @@ func (*PtyDataUpdate) UpdateType() string {
|
|||||||
func (pdu *PtyDataUpdate) Clean() {}
|
func (pdu *PtyDataUpdate) Clean() {}
|
||||||
|
|
||||||
type ModelUpdate struct {
|
type ModelUpdate struct {
|
||||||
Sessions []*SessionType `json:"sessions,omitempty"`
|
Sessions []*SessionType `json:"sessions,omitempty"`
|
||||||
ActiveSessionId string `json:"activesessionid,omitempty"`
|
ActiveSessionId string `json:"activesessionid,omitempty"`
|
||||||
Screens []*ScreenType `json:"screens,omitempty"`
|
Screens []*ScreenType `json:"screens,omitempty"`
|
||||||
ScreenLines *ScreenLinesType `json:"screenlines,omitempty"`
|
ScreenLines *ScreenLinesType `json:"screenlines,omitempty"`
|
||||||
Line *LineType `json:"line,omitempty"`
|
Line *LineType `json:"line,omitempty"`
|
||||||
Lines []*LineType `json:"lines,omitempty"`
|
Lines []*LineType `json:"lines,omitempty"`
|
||||||
Cmd *CmdType `json:"cmd,omitempty"`
|
Cmd *CmdType `json:"cmd,omitempty"`
|
||||||
CmdLine *utilfn.StrWithPos `json:"cmdline,omitempty"`
|
CmdLine *utilfn.StrWithPos `json:"cmdline,omitempty"`
|
||||||
Info *InfoMsgType `json:"info,omitempty"`
|
Info *InfoMsgType `json:"info,omitempty"`
|
||||||
ClearInfo bool `json:"clearinfo,omitempty"`
|
ClearInfo bool `json:"clearinfo,omitempty"`
|
||||||
Remotes []RemoteRuntimeState `json:"remotes,omitempty"`
|
Remotes []RemoteRuntimeState `json:"remotes,omitempty"`
|
||||||
History *HistoryInfoType `json:"history,omitempty"`
|
History *HistoryInfoType `json:"history,omitempty"`
|
||||||
Interactive bool `json:"interactive"`
|
Interactive bool `json:"interactive"`
|
||||||
Connect bool `json:"connect,omitempty"`
|
Connect bool `json:"connect,omitempty"`
|
||||||
MainView string `json:"mainview,omitempty"`
|
MainView string `json:"mainview,omitempty"`
|
||||||
Bookmarks []*BookmarkType `json:"bookmarks,omitempty"`
|
Bookmarks []*BookmarkType `json:"bookmarks,omitempty"`
|
||||||
SelectedBookmark string `json:"selectedbookmark,omitempty"`
|
SelectedBookmark string `json:"selectedbookmark,omitempty"`
|
||||||
HistoryViewData *HistoryViewData `json:"historyviewdata,omitempty"`
|
HistoryViewData *HistoryViewData `json:"historyviewdata,omitempty"`
|
||||||
ClientData *ClientData `json:"clientdata,omitempty"`
|
ClientData *ClientData `json:"clientdata,omitempty"`
|
||||||
RemoteView *RemoteViewType `json:"remoteview,omitempty"`
|
RemoteView *RemoteViewType `json:"remoteview,omitempty"`
|
||||||
ScreenTombstones []*ScreenTombstoneType `json:"screentombstones,omitempty"`
|
ScreenTombstones []*ScreenTombstoneType `json:"screentombstones,omitempty"`
|
||||||
SessionTombstones []*SessionTombstoneType `json:"sessiontombstones,omitempty"`
|
SessionTombstones []*SessionTombstoneType `json:"sessiontombstones,omitempty"`
|
||||||
|
OpenAICmdInfoChat []*packet.OpenAICmdInfoChatMessage `json:"openaicmdinfochat,omitempty"`
|
||||||
|
AlertMessage *AlertMessageType `json:"alertmessage,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*ModelUpdate) UpdateType() string {
|
func (*ModelUpdate) UpdateType() string {
|
||||||
@ -128,6 +131,13 @@ type RemoteEditType struct {
|
|||||||
HasPassword bool `json:"haspassword,omitempty"`
|
HasPassword bool `json:"haspassword,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AlertMessageType struct {
|
||||||
|
Title string `json:"title,omitempty"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Confirm bool `json:"confirm,omitempty"`
|
||||||
|
Markdown bool `json:"markdown,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type InfoMsgType struct {
|
type InfoMsgType struct {
|
||||||
InfoTitle string `json:"infotitle"`
|
InfoTitle string `json:"infotitle"`
|
||||||
InfoError string `json:"infoerror,omitempty"`
|
InfoError string `json:"infoerror,omitempty"`
|
||||||
|
Loading…
Reference in New Issue
Block a user