next-terminal/web/src/components/devops/ExecuteCommand.js
dushixiang ded4dc492a - 修复mysql模式下「资产授权列表」「用户授权列表」「用户组授权列表」无法使用的问题 fixed #315
- 修复资产新增、修改无权限的缺陷 fixed #314
- 修复执行动态指令时多行失败且无法自动执行的问题 fixed #313 #310
- 修复计划任务无法选择资产的问题 fixed #312
- 修复导入导出备份无效的问题 fixed #303
- 增加「资产详情」「资产授权」「用户详情」「用户授权」「用户组详情」「用户组授权」「角色详情」「授权策略详情」按钮
- 修复资产列表使用IP搜索无效的问题
- 资产列表增加最近接入时间排序、增加修改每页数量 fixed #311
- 修复登录页面双因素认证输入框无法自动获取焦点的问题 fixed #311
- 增加普通页面资产列表最后接入时间排序 fixed #311
- 计划任务增加执行本机系统命令
2022-11-20 17:36:27 +08:00

239 lines
7.9 KiB
JavaScript

import React, {useEffect, useState} from 'react';
import {useSearchParams} from "react-router-dom";
import commandApi from "../../api/command";
import Message from "../access/Message";
import {Input, Layout, Spin} from "antd";
import {ProCard} from "@ant-design/pro-components";
import "xterm/css/xterm.css"
import "./ExecuteCommand.css"
import sessionApi from "../../api/session";
import {Terminal} from "xterm";
import {FitAddon} from "xterm-addon-fit";
import {getToken} from "../../utils/utils";
import qs from "qs";
import {wsServer} from "../../common/env";
import {CloseOutlined} from "@ant-design/icons";
import {useQuery} from "react-query";
import {xtermScrollPretty} from "../../utils/xterm-scroll-pretty";
import strings from "../../utils/strings";
const {Search} = Input;
const {Content} = Layout;
const ExecuteCommand = () => {
let [sessions, setSessions] = useState([]);
const [searchParams, _] = useSearchParams();
let commandId = searchParams.get('commandId');
let commandQuery = useQuery('commandQuery', () => commandApi.getById(commandId),{
onSuccess: data => {
let commands = data.content.split('\n');
if (!commands) {
return;
}
items.forEach(item => {
if (getReady(item['id']) === false) {
initTerm(item['id'], commands);
}
})
}
});
let [inputValue, setInputValue] = useState('');
let items = JSON.parse(searchParams.get('assets'));
let [assets, setAssets] = useState(items);
let readies = {};
for (let i = 0; i < items.length; i++) {
readies[items[i].id] = false;
items[i]['locked'] = false;
}
useEffect(() => {
window.addEventListener('resize', handleWindowResize);
return function cleanup() {
window.removeEventListener('resize', handleWindowResize);
sessions.forEach(session => {
if (session['ws']) {
session['ws'].close();
}
if (session['term']) {
session['term'].dispose();
}
})
}
}, [commandId]);
const handleWindowResize = () => {
sessions.forEach(session => {
session['fitAddon'].fit();
let ws = session['ws'];
if (ws && ws.readyState === WebSocket.OPEN) {
let term = session['term'];
let terminalSize = {
cols: term.cols,
rows: term.rows
}
ws.send(new Message(Message.Resize, window.btoa(JSON.stringify(terminalSize))).toString());
}
})
}
const handleInputChange = (e) => {
let value = e.target.value;
setInputValue(value);
}
const handleExecuteCommand = (value) => {
sessions.forEach(session => {
let ws = session['ws'];
if (ws.readyState === WebSocket.OPEN) {
ws.send(new Message(Message.Data, value + String.fromCharCode(13)).toString());
}
})
setInputValue('');
}
const addSession = (session) => {
sessions.push(session);
setSessions(sessions.slice());
}
const setReady = (id, ready) => {
readies[id] = ready;
}
const getReady = (id) => {
return readies[id];
}
const initTerm = async (assetId, commands) => {
let session = await sessionApi.create(assetId, 'native');
let sessionId = session['id'];
let term = new Terminal({
fontFamily: 'monaco, Consolas, "Lucida Console", monospace', fontSize: 15, theme: {
background: '#2d2f2c'
}, rightClickSelectsWord: true,
});
const fitAddon = new FitAddon();
term.loadAddon(fitAddon);
term.open(document.getElementById(assetId));
fitAddon.fit();
term.focus();
term.writeln('Trying to connect to the server ...');
xtermScrollPretty();
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}/sessions/${sessionId}/ssh?${paramStr}`);
term.onData(data => {
if (webSocket) {
webSocket.send(new Message(Message.Data, data).toString());
}
});
webSocket.onerror = (e) => {
term.writeln("Failed to connect to server.");
}
webSocket.onclose = (e) => {
term.writeln("Connection is closed.");
}
webSocket.onmessage = (e) => {
let msg = Message.parse(e.data);
switch (msg['type']) {
case Message.Connected:
term.clear();
sessionApi.connect(sessionId);
for (let i = 0; i < commands.length; i++) {
let command = commands[i];
if (!strings.hasText(command)) {
continue
}
webSocket.send(new Message(Message.Data, command + String.fromCharCode(13)).toString());
}
break;
case Message.Data:
term.write(msg['content']);
break;
case Message.Closed:
term.writeln(`\x1B[1;3;31m${msg['content']}\x1B[0m `)
webSocket.close();
break;
default:
break;
}
}
addSession({'id': assetId, 'ws': webSocket, 'term': term, 'fitAddon': fitAddon});
setReady(assetId, true);
}
const handleRemoveTerm = (id) => {
let session = sessions.find(item => item.id === id);
session.ws.close();
session.term.dispose();
let result = assets.filter(item => item.id !== id);
setAssets(result);
}
return (
<div>
<Content className="page-container">
<div className="page-search">
<Search placeholder="请输入指令" value={inputValue} onChange={handleInputChange}
onSearch={handleExecuteCommand} enterButton='执行'/>
</div>
</Content>
<Spin spinning={commandQuery.isLoading} tip='正在获取指令内容...'>
<div className="page-card">
<ProCard ghost gutter={[8, 8]} wrap>
{assets.map(item => {
return <ProCard
className={'term-container'}
key={item['id']}
extra={<div style={{cursor: 'pointer'}} onClick={() => handleRemoveTerm(item['id'])}>
<CloseOutlined/></div>}
title={item['name']}
layout="center"
headerBordered
size={'small'}
colSpan={12}
bordered>
<div id={item['id']} style={{width: '100%', height: '100%'}}/>
</ProCard>
})}
{/*<ProCard*/}
{/* className={'term-adder'}*/}
{/* layout="center"*/}
{/* colSpan={12}*/}
{/* bordered>*/}
{/* <Button type="dashed" style={{width: '100%', height: '100%'}} icon={<PlusOutlined />}/>*/}
{/*</ProCard>*/}
</ProCard>
</div>
</Spin>
</div>
);
};
export default ExecuteCommand;