next-terminal/web/src/components/access/Term.js

255 lines
7.5 KiB
JavaScript

import React, {Component} from 'react';
import "xterm/css/xterm.css"
import {Terminal} from "xterm";
import qs from "qs";
import {wsServer} from "../../common/env";
import {getToken, isEmpty} from "../../utils/utils";
import {FitAddon} from 'xterm-addon-fit';
import "./Access.css"
import request from "../../common/request";
import {Affix, Button, Col, Drawer, message, Modal, Row} from "antd";
import {AppstoreTwoTone, ExclamationCircleOutlined} from "@ant-design/icons";
import Draggable from "react-draggable";
import FileSystem from "./FileSystem";
class Term extends Component {
state = {
width: window.innerWidth,
height: window.innerHeight,
term: undefined,
webSocket: undefined,
fitAddon: undefined,
sessionId: undefined
};
componentDidMount = async () => {
let urlParams = new URLSearchParams(this.props.location.search);
let assetId = urlParams.get('assetId');
document.title = urlParams.get('assetName');
let sessionId = await this.createSession(assetId);
if (isEmpty(sessionId)) {
return;
}
let term = new Terminal({
fontFamily: 'monaco, Consolas, "Lucida Console", monospace',
fontSize: 15,
// theme: {
// background: '#1b1b1b',
// lineHeight: 17
// },
rightClickSelectsWord: true,
});
term.open(this.refs.terminal);
const fitAddon = new FitAddon();
term.loadAddon(fitAddon);
fitAddon.fit();
term.focus();
term.writeln('Trying to connect to the server ...');
term.onSelectionChange(async () => {
let selection = term.getSelection();
this.setState({
selection: selection
})
if (navigator.clipboard) {
await navigator.clipboard.writeText(selection);
}
});
term.attachCustomKeyEventHandler((e) => {
if (e.ctrlKey && e.key === 'c' && this.state.selection) {
return false;
}
return !(e.ctrlKey && e.key === 'v');
});
term.onData(data => {
let webSocket = this.state.webSocket;
if (webSocket !== undefined) {
webSocket.send(JSON.stringify({type: 'data', content: data}));
}
});
let token = getToken();
let params = {
'cols': term.cols,
'rows': term.rows,
'sessionId': sessionId,
'X-Auth-Token': token
};
let paramStr = qs.stringify(params);
let webSocket = new WebSocket(wsServer + '/ssh?' + paramStr);
let pingInterval;
webSocket.onopen = (e => {
pingInterval = setInterval(() => {
webSocket.send(JSON.stringify({type: 'ping'}))
}, 5000);
});
webSocket.onerror = (e) => {
term.writeln("Failed to connect to server.");
}
webSocket.onclose = (e) => {
term.writeln("Connection is closed.");
if (pingInterval) {
clearInterval(pingInterval);
}
}
webSocket.onmessage = (e) => {
let msg = JSON.parse(e.data);
switch (msg['type']) {
case 'connected':
term.clear();
this.updateSessionStatus(sessionId);
break;
case 'data':
term.write(msg['content']);
break;
case 'closed':
term.writeln(`\x1B[1;3;31m${msg['content']}\x1B[0m `)
webSocket.close();
break;
default:
break;
}
}
this.setState({
term: term,
webSocket: webSocket,
fitAddon: fitAddon,
sessionId: sessionId
});
window.addEventListener('resize', this.onWindowResize);
}
componentWillUnmount() {
let webSocket = this.state.webSocket;
if (webSocket) {
webSocket.close()
}
}
showMessage(msg) {
message.destroy();
Modal.confirm({
title: '提示',
icon: <ExclamationCircleOutlined/>,
content: msg,
centered: true,
okText: '重新连接',
cancelText: '关闭页面',
onOk() {
window.location.reload();
},
onCancel() {
window.close();
},
});
}
async createSession(assetsId) {
let result = await request.post(`/sessions?assetId=${assetsId}&mode=naive`);
if (result['code'] !== 1) {
this.showMessage(result['message']);
return null;
}
return result['data']['id'];
}
updateSessionStatus = async (sessionId) => {
let result = await request.post(`/sessions/${sessionId}/connect`);
if (result['code'] !== 1) {
message.error(result['message']);
}
}
terminalSize() {
return {
cols: Math.floor(this.state.width / 7.5),
rows: Math.floor(window.innerHeight / 17),
}
}
onWindowResize = (e) => {
let term = this.state.term;
let fitAddon = this.state.fitAddon;
let webSocket = this.state.webSocket;
this.setState({
width: window.innerWidth,
height: window.innerHeight,
}, () => {
if (webSocket && webSocket.readyState === WebSocket.OPEN) {
fitAddon.fit();
term.focus();
let terminalSize = {
cols: term.cols,
rows: term.rows
}
webSocket.send(JSON.stringify({type: 'resize', content: JSON.stringify(terminalSize)}));
}
});
};
render() {
return (
<div>
<div ref='terminal' id='terminal' style={{
height: this.state.height,
width: this.state.width,
backgroundColor: 'black',
overflowX: 'hidden',
overflowY: 'hidden',
}}/>
<Draggable>
<Affix style={{position: 'absolute', top: 50, right: 50, zIndex: 9999}}>
<Button icon={<AppstoreTwoTone/>} onClick={() => {
this.setState({
fileSystemVisible: true,
});
}}/>
</Affix>
</Draggable>
<Drawer
style={{zIndex: 10000}}
title={'会话详情'}
placement="right"
width={window.innerWidth * 0.8}
closable={true}
// maskClosable={false}
onClose={() => {
this.setState({
fileSystemVisible: false
});
}}
visible={this.state.fileSystemVisible}
>
<Row style={{marginTop: 10}}>
<Col span={24}>
<FileSystem sessionId={this.state.sessionId}/>
</Col>
</Row>
</Drawer>
</div>
);
}
}
export default Term;