- 修复资产新增、修改无权限的缺陷 fixed #314 - 修复执行动态指令时多行失败且无法自动执行的问题 fixed #313 #310 - 修复计划任务无法选择资产的问题 fixed #312 - 修复导入导出备份无效的问题 fixed #303 - 增加「资产详情」「资产授权」「用户详情」「用户授权」「用户组详情」「用户组授权」「角色详情」「授权策略详情」按钮 - 修复资产列表使用IP搜索无效的问题 - 资产列表增加最近接入时间排序、增加修改每页数量 fixed #311 - 修复登录页面双因素认证输入框无法自动获取焦点的问题 fixed #311 - 增加普通页面资产列表最后接入时间排序 fixed #311 - 计划任务增加执行本机系统命令
239 lines
7.9 KiB
JavaScript
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; |