Initial commit

This commit is contained in:
dushixiang
2020-12-20 21:19:11 +08:00
commit e7f2773c77
77 changed files with 27866 additions and 0 deletions

95
web/src/App.css Normal file
View File

@ -0,0 +1,95 @@
.trigger {
font-size: 18px;
line-height: 64px;
padding: 0 24px;
cursor: pointer;
transition: color 0.3s;
}
.trigger:hover {
color: #1890ff;
}
.logo {
height: 32px;
margin: 16px;
text-align: center;
}
.logo > h1 {
color: white;
font-weight: bold;
line-height: 32px; /*设置line-height与父级元素的height相等*/
text-align: center; /*设置文本水平居中*/
display: inline-block;
}
.site-layout .site-layout-background {
background: #fff;
}
.site-page-header-ghost-wrapper {
background-color: #FFF;
}
.global-header {
position: relative;
display: flex;
align-items: center;
height: 48px;
padding: 0 16px 0 0;
background: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, .08);
}
.page-herder {
margin: 16px 16px 0 16px;
}
.page-search {
background-color: white;
margin: 16px 16px 0 16px;
padding: 16px;
}
.page-search label {
font-weight: bold;
}
.page-search .ant-form-item {
margin-bottom: 0;
}
.page-content {
margin: 16px;
padding: 24px;
min-height: 280px;
}
.page-card {
margin: 16px;
}
.user-in-menu {
align-items: center;
text-align: center;
margin: 10px auto;
color: white;
}
.user-in-menu>.nickname {
margin-top: 20px;
margin-right: auto;
margin-left: auto;
font-weight: bold;
padding: 2px 5px;
border-style: solid;
border-width: 1px;
border-color: white;
width: fit-content;
border-radius: 5%;
}
.monitor .ant-modal-body{
padding: 0;
}

241
web/src/App.js Normal file
View File

@ -0,0 +1,241 @@
import React, {Component} from 'react';
import 'antd/dist/antd.css';
import './App.css';
import {Divider, Layout} from "antd";
import {Switch, Route, Link} from "react-router-dom";
import {Menu} from 'antd';
import Dashboard from "./components/dashboard/Dashboard";
import Asset from "./components/asset/Asset";
import Access from "./components/access/Access";
import User from "./components/user/User";
import OnlineSession from "./components/session/OnlineSession";
import OfflineSession from "./components/session/OfflineSession";
import Login from "./components/Login";
import DynamicCommand from "./components/command/DynamicCommand";
import Credential from "./components/credential/Credential";
import {
DashboardOutlined,
UserOutlined,
IdcardOutlined,
CloudServerOutlined,
CodeOutlined,
BlockOutlined,
AuditOutlined,
DesktopOutlined,
DisconnectOutlined,
MenuUnfoldOutlined,
MenuFoldOutlined,
SolutionOutlined,
SettingOutlined, LinkOutlined
} from '@ant-design/icons';
import Info from "./components/user/Info";
import request from "./common/request";
import {message} from "antd/es";
import Setting from "./components/setting/Setting";
import BatchCommand from "./components/command/BatchCommand";
const {Footer, Sider} = Layout;
const {SubMenu} = Menu;
class App extends Component {
state = {
collapsed: false,
current: sessionStorage.getItem('current'),
openKeys: sessionStorage.getItem('openKeys') ? JSON.parse(sessionStorage.getItem('openKeys')) : [],
user: {
'nickname': '未定义'
}
};
toggle = () => {
this.setState({
collapsed: !this.state.collapsed,
});
};
componentDidMount() {
this.getInfo().then(r => {
});
}
async getInfo() {
if ('/login' === window.location.pathname) {
return;
}
let result = await request.get('/info');
if (result.code === 1) {
this.setState({
user: result.data
})
} else {
message.error(result.message);
}
}
updateUser = (user) => {
this.setState({
user: user
})
}
setCurrent = (key) => {
this.setState({
current: key
})
sessionStorage.setItem('current', key);
}
subMenuChange = (openKeys) => {
console.debug("current open keys");
console.table(openKeys);
this.setState({
openKeys: openKeys
})
sessionStorage.setItem('openKeys', JSON.stringify(openKeys));
}
render() {
return (
<Switch>
<Route path="/access" component={Access}/>
<Route path="/login"><Login updateUser={this.updateUser}/></Route>
<Route path="/">
<Layout className="layout" style={{minHeight: '100vh'}}>
<Sider trigger={null} collapsible collapsed={this.state.collapsed} style={{width: 256}}>
<div className="logo">
<img src='logo.svg' alt='logo'/>
{
!this.state.collapsed ?
<>&nbsp;<h1>Next Terminal</h1></> :
null
}
</div>
<Divider/>
<Menu
onClick={(e) => this.setCurrent(e.key)}
selectedKeys={[this.state.current]}
onOpenChange={this.subMenuChange}
defaultOpenKeys={this.state.openKeys}
theme="dark" mode="inline" defaultSelectedKeys={['dashboard']}
inlineCollapsed={this.state.collapsed}
style={{lineHeight: '64px'}}>
<Menu.Item key="dashboard" icon={<DashboardOutlined/>}>
<Link to={'/dashboard'}>
控制面板
</Link>
</Menu.Item>
<SubMenu key='resource' title='资源管理' icon={<CloudServerOutlined/>}>
<Menu.Item key="idcard" icon={<IdcardOutlined/>}>
<Link to={'/credential'}>
授权凭证
</Link>
</Menu.Item>
<Menu.Item key="asset" icon={<DesktopOutlined/>}>
<Link to={'/asset'}>
资产列表
</Link>
</Menu.Item>
</SubMenu>
<SubMenu key='command-manage' title='指令管理' icon={<CodeOutlined/>}>
<Menu.Item key="dynamic-command" icon={<BlockOutlined/>}>
<Link to={'/dynamic-command'}>
动态指令
</Link>
</Menu.Item>
{/*<Menu.Item key="silent-command" icon={<DeploymentUnitOutlined/>}>
<Link to={'/silent-command'}>
静默指令
</Link>
</Menu.Item>*/}
</SubMenu>
<SubMenu key='audit' title='操作审计' icon={<AuditOutlined/>}>
<Menu.Item key="online-session" icon={<LinkOutlined />}>
<Link to={'/online-session'}>
在线会话
</Link>
</Menu.Item>
<Menu.Item key="offline-session" icon={<DisconnectOutlined/>}>
<Link to={'/offline-session'}>
历史会话
</Link>
</Menu.Item>
</SubMenu>
<Menu.Item key="user" icon={<UserOutlined/>}>
<Link to={'/user'}>
用户管理
</Link>
</Menu.Item>
<Menu.Item key="info" icon={<SolutionOutlined/>}>
<Link to={'/info'}>
个人中心
</Link>
</Menu.Item>
<Menu.Item key="setting" icon={<SettingOutlined/>}>
<Link to={'/setting'}>
系统设置
</Link>
</Menu.Item>
</Menu>
<div>
{React.createElement(this.state.collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
className: 'trigger',
onClick: this.toggle,
})}
</div>
</Sider>
<Layout className="site-layout">
{/*<Header className="site-layout-background"
style={{padding: 0, height: 48, lineHeight: 48}}>
</Header>*/}
<Route path="/dashboard" component={Dashboard}/>
<Route path="/user" component={User}/>
<Route path="/asset" component={Asset}/>
<Route path="/credential" component={Credential}/>
<Route path="/dynamic-command" component={DynamicCommand}/>
<Route path="/batch-command" component={BatchCommand}/>
<Route path="/online-session" component={OnlineSession}/>
<Route path="/offline-session" component={OfflineSession}/>
<Route path="/info" component={Info}/>
<Route path="/setting" component={Setting}/>
<Footer style={{textAlign: 'center'}}>
Next Terminal ©2020 Created by 杜世翔
</Footer>
</Layout>
</Layout>
</Route>
</Switch>
);
}
}
export default App;

9
web/src/App.test.js Normal file
View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
ReactDOM.unmountComponentAtNode(div);
});

View File

@ -0,0 +1,20 @@
// prod
// export const server = '';
// export const wsServer = '';
// export const prefix = '';
// dev
export const server = '//127.0.0.1:8088';
export const wsServer = 'ws://127.0.0.1:8088';
export const prefix = '';
// export const server = '//172.16.101.32:8080';
// export const wsServer = 'ws://172.16.101.32:8080';
// export const prefix = '';
export const PROTOCOL_COLORS = {
'rdp': 'red',
'ssh': 'blue',
'telnet': 'geekblue',
'vnc': 'purple'
}

115
web/src/common/request.js Normal file
View File

@ -0,0 +1,115 @@
import axios from 'axios'
import {prefix, server} from "./constants";
import {message} from 'antd';
import {getHeaders} from "../utils/utils";
// 测试地址
// axios.defaults.baseURL = server;
// 线上地址
axios.defaults.baseURL = server + prefix;
const handleError = (error) => {
if ("Network Error" === error.toString()) {
message.error('网络异常');
return;
}
if (error.response !== undefined && error.response.status === 403) {
window.location.href = '#/login';
return;
}
if (error.response !== undefined) {
message.error(error.response.data.message);
}
};
const handleResult = (result) => {
if (result['code'] === 403) {
window.location.href = '#/login';
}
}
const request = {
get: function (url) {
const headers = getHeaders();
return new Promise((resolve, reject) => {
axios.get(url, {headers: headers})
.then((response) => {
handleResult(response.data);
resolve(response.data);
})
.catch((error) => {
handleError(error);
reject(error);
});
})
},
post: function (url, params) {
const headers = getHeaders();
return new Promise((resolve, reject) => {
axios.post(url, params, {headers: headers})
.then((response) => {
handleResult(response.data);
resolve(response.data);
})
.catch((error) => {
handleError(error);
reject(error);
});
})
},
put: function (url, params) {
const headers = getHeaders();
return new Promise((resolve, reject) => {
axios.put(url, params, {headers: headers})
.then((response) => {
handleResult(response.data);
resolve(response.data);
})
.catch((error) => {
handleError(error);
reject(error);
});
})
},
delete: function (url) {
const headers = getHeaders();
return new Promise((resolve, reject) => {
axios.delete(url, {headers: headers})
.then((response) => {
handleResult(response.data);
resolve(response.data);
})
.catch((error) => {
handleError(error);
reject(error);
});
})
},
patch: function (url, params) {
const headers = getHeaders();
return new Promise((resolve, reject) => {
axios.patch(url, params, {headers: headers})
.then((response) => {
handleResult(response.data);
resolve(response.data);
})
.catch((error) => {
handleError(error);
reject(error);
});
})
},
};
export default request

View File

@ -0,0 +1,21 @@
.login-form {
max-width: 300px;
min-width: 300px;
}
.login-form-forgot {
float: right;
}
.login-form-button {
width: 100%;
}
.login-card {
position: absolute;
left: 50%;
top: 50%;
margin-left: -175px;
margin-top: -189px;
}

View File

@ -0,0 +1,78 @@
import React, {Component} from 'react';
import {Button, Card, Checkbox, Form, Input} from "antd";
import './Login.css'
import request from "../common/request";
import {message} from "antd/es";
import {withRouter} from "react-router-dom";
import {
UserOutlined, LockOutlined
} from '@ant-design/icons';
class LoginForm extends Component {
state = {
inLogin: false
};
handleSubmit = async params => {
this.setState({
inLogin: true
});
try {
let result = await request.post('/login', params);
if (result.code !== 1) {
throw new Error(result.message);
}
// 跳转登录
sessionStorage.removeItem('current');
sessionStorage.removeItem('openKeys');
localStorage.setItem('X-Auth-Token', result.data);
// this.props.history.push();
window.location.href = "/"
let r = await request.get('/info');
if (r.code === 1) {
this.props.updateUser(r.data);
} else {
message.error(r.message);
}
} catch (e) {
message.error(e.message);
} finally {
this.setState({
inLogin: false
});
}
};
render() {
return (
<Card className='login-card' title="登录">
<Form onFinish={this.handleSubmit} className="login-form">
<Form.Item name='username' rules={[{required: true, message: '请输入登录账号!'}]}>
<Input prefix={<UserOutlined/>} placeholder="登录账号"/>
</Form.Item>
<Form.Item name='password' rules={[{required: true, message: '请输入登录密码!'}]}>
<Input.Password prefix={<LockOutlined/>} placeholder="登录密码"/>
</Form.Item>
<Form.Item name='remember' valuePropName='checked' initialValue={false}>
<Checkbox>记住登录</Checkbox>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" className="login-form-button"
loading={this.state.inLogin}>
登录
</Button>
</Form.Item>
</Form>
</Card>
);
}
}
export default withRouter(LoginForm);

View File

@ -0,0 +1,3 @@
.container div {
margin: 0 auto;
}

View File

@ -0,0 +1,889 @@
import React, {Component} from 'react';
import Guacamole from 'guacamole-common-js';
import {
Affix,
Alert,
Button,
Card,
Col,
Drawer,
Form,
Input,
message,
Modal,
Row,
Space,
Spin,
Tooltip,
Tree
} from 'antd'
import qs from "qs";
import request from "../../common/request";
import {prefix, server, wsServer} from "../../common/constants";
import {
CloudDownloadOutlined,
CloudUploadOutlined,
DeleteOutlined,
FolderAddOutlined,
LoadingOutlined,
ReloadOutlined,
UploadOutlined
} from '@ant-design/icons';
import CopyOutlined from "@ant-design/icons/lib/icons/CopyOutlined";
import FolderOpenOutlined from "@ant-design/icons/lib/icons/FolderOpenOutlined";
import Upload from "antd/es/upload";
import {download, getToken} from "../../utils/utils";
import './Access.css'
const {TextArea} = Input;
const {DirectoryTree} = Tree;
const STATE_IDLE = 0;
const STATE_CONNECTING = 1;
const STATE_WAITING = 2;
const STATE_CONNECTED = 3;
const STATE_DISCONNECTING = 4;
const STATE_DISCONNECTED = 5;
const antIcon = <LoadingOutlined/>;
const formItemLayout = {
labelCol: {span: 6},
wrapperCol: {span: 14},
};
class Access extends Component {
formRef = React.createRef()
state = {
sessionId: '',
client: {},
clipboardVisible: false,
clipboardText: '',
containerOverflow: 'hidden',
containerWidth: 0,
containerHeight: 0,
fileSystemVisible: false,
fileSystem: {
loading: false,
object: null,
currentDirectory: '/',
files: [],
},
uploadAction: '',
uploadHeaders: {},
keyboard: {},
protocol: '',
treeData: [],
selectNode: {},
confirmVisible: false,
confirmLoading: false,
uploadVisible: false,
uploadLoading: false,
};
async componentDidMount() {
let params = new URLSearchParams(this.props.location.search);
let assetsId = params.get('assetsId');
let protocol = params.get('protocol');
let sessionId = await this.createSession(assetsId);
this.setState({
sessionId: sessionId,
protocol: protocol
});
this.renderDisplay(sessionId, protocol);
window.addEventListener('resize', this.onWindowResize);
window.addEventListener('onfocus', this.onWindowFocus);
}
componentWillUnmount() {
if (this.state.client) {
this.state.client.disconnect();
}
window.removeEventListener('resize', this.onWindowResize);
document.removeEventListener("onfocus", this.onWindowFocus);
}
sendClipboard(data) {
let writer;
// Create stream with proper mimetype
const stream = this.state.client.createClipboardStream(data.type);
// Send data as a string if it is stored as a string
if (typeof data.data === 'string') {
writer = new Guacamole.StringWriter(stream);
writer.sendText(data.data);
writer.sendEnd();
} else {
// Write File/Blob asynchronously
writer = new Guacamole.BlobWriter(stream);
writer.oncomplete = function clipboardSent() {
writer.sendEnd();
};
// Begin sending data
writer.sendBlob(data.data);
}
this.setState({
clipboardText: data.data
})
if (this.state.protocol === 'ssh') {
message.success('您输入的内容已复制到远程服务器上,使用右键将自动粘贴。');
} else {
message.success('您输入的内容已复制到远程服务器上');
}
}
onTunnelStateChange = (state) => {
console.log('onTunnelStateChange', state);
};
updateSessionStatus = async (sessionId) => {
let result = await request.post(`/sessions/${sessionId}/content`);
if (result.code !== 1) {
message.error(result.message);
}
}
onClientStateChange = (state) => {
switch (state) {
case STATE_IDLE:
console.log('初始化');
message.destroy();
message.loading('正在初始化中...', 0);
break;
case STATE_CONNECTING:
console.log('正在连接...');
message.destroy();
message.loading('正在努力连接中...', 0);
break;
case STATE_WAITING:
console.log('正在等待...');
message.destroy();
message.loading('正在等待服务器响应...', 0);
break;
case STATE_CONNECTED:
console.log('连接成功。');
this.onWindowResize(null);
message.destroy();
message.success('连接成功');
// 向后台发送请求,更新会话的状态
this.updateSessionStatus(this.state.sessionId).then(_ => {
})
break;
case STATE_DISCONNECTING:
console.log('连接正在关闭中...');
message.destroy();
message.loading('正在关闭连接...', 0);
break;
case STATE_DISCONNECTED:
console.log('连接关闭。');
message.destroy();
message.error('连接关闭');
break;
default:
break;
}
};
onError = (status) => {
console.log('通道异常。', status);
switch (status.code) {
case 256:
this.showMessage('未支持的访问');
break;
case 512:
this.showMessage('远程服务异常');
break;
case 513:
this.showMessage('服务器忙碌');
break;
case 514:
this.showMessage('服务器连接超时');
break;
case 515:
this.showMessage('远程服务异常');
break;
case 516:
this.showMessage('资源未找到');
break;
case 517:
this.showMessage('资源冲突');
break;
case 518:
this.showMessage('资源已关闭');
break;
case 519:
this.showMessage('远程服务未找到');
break;
case 520:
this.showMessage('远程服务不可用');
break;
case 521:
this.showMessage('会话冲突');
break;
case 522:
this.showMessage('会话连接超时');
break;
case 523:
this.showMessage('会话已关闭');
break;
case 768:
this.showMessage('网络不可达');
break;
case 769:
this.showMessage('服务器密码验证失败');
break;
case 771:
this.showMessage('客户端被禁止');
break;
case 776:
this.showMessage('客户端连接超时');
break;
case 781:
this.showMessage('客户端异常');
break;
case 783:
this.showMessage('错误的请求类型');
break;
case 797:
this.showMessage('客户端连接数量过多');
break;
default:
this.showMessage('未知错误。');
}
};
showMessage(message) {
Modal.error({
title: '提示',
content: message,
});
}
clientClipboardReceived = (stream, mimetype) => {
let reader;
// If the received data is text, read it as a simple string
if (/^text\//.exec(mimetype)) {
reader = new Guacamole.StringReader(stream);
// Assemble received data into a single string
let data = '';
reader.ontext = function textReceived(text) {
data += text;
};
// Set clipboard contents once stream is finished
reader.onend = async () => {
message.success('您选择的内容已复制到您的粘贴板中,在右侧的输入框中可同时查看到。');
this.setState({
clipboardText: data
});
if (navigator.clipboard) {
await navigator.clipboard.writeText(data);
}
};
}
// Otherwise read the clipboard data as a Blob
else {
reader = new Guacamole.BlobReader(stream, mimetype);
reader.onend = () => {
this.setState({
clipboardText: reader.getBlob()
})
}
}
};
uploadChange = (info) => {
if (info.file.status !== 'uploading') {
}
if (info.file.status === 'done') {
message.success(`${info.file.name} 文件上传成功。`, 3);
} else if (info.file.status === 'error') {
message.error(`${info.file.name} 文件上传失败。`, 10);
}
}
onKeyDown = (keysym) => {
if (true === this.state.clipboardVisible || true === this.state.confirmVisible) {
return true;
}
this.state.client.sendKeyEvent(1, keysym);
if (keysym === 65288) {
return false;
}
};
onKeyUp = (keysym) => {
this.state.client.sendKeyEvent(0, keysym);
};
showFileSystem = () => {
this.setState({
fileSystemVisible: true,
});
this.loadDirData('/');
};
hideFileSystem = () => {
this.setState({
fileSystemVisible: false,
});
};
showClipboard = () => {
this.setState({
clipboardVisible: true
}, () => {
let element = document.getElementById('clipboard');
if (element) {
element.value = this.state.clipboardText;
}
});
};
hideClipboard = () => {
this.setState({
clipboardVisible: false
});
};
updateClipboardFormTextarea = () => {
let clipboardText = document.getElementById('clipboard').value;
this.setState({
clipboardText: clipboardText
});
this.sendClipboard({
'data': clipboardText,
'type': 'text/plain'
});
};
async createSession(assetsId) {
let result = await request.post(`/sessions?assetId=${assetsId}`);
if (result.code !== 1) {
message.error(result.message, 10);
return;
}
return result.data['id'];
}
async renderDisplay(sessionId, protocol) {
let tunnel = new Guacamole.WebSocketTunnel(wsServer + prefix + '/tunnel');
tunnel.onstatechange = this.onTunnelStateChange;
// Get new client instance
let client = new Guacamole.Client(tunnel);
// 设置虚拟机剪贴板内容
client.sendClipboard = this.sendClipboard;
// 处理从虚拟机收到的剪贴板内容
client.onclipboard = this.clientClipboardReceived;
// 处理客户端的状态变化事件
client.onstatechange = this.onClientStateChange;
client.onerror = this.onError;
// Get display div from document
const display = document.getElementById("display");
// Add client to display div
const element = client.getDisplay().getElement();
display.appendChild(element);
let width = window.innerWidth;
let height = window.innerHeight;
let token = getToken();
let params = {
'sessionId': sessionId,
'width': width,
'height': height,
'X-Auth-Token': token
};
let paramStr = qs.stringify(params);
// Connect
client.connect(paramStr);
// Disconnect on close
window.onunload = function () {
client.disconnect();
};
// Mouse
const mouse = new Guacamole.Mouse(element);
mouse.onmousedown = mouse.onmouseup = function (mouseState) {
client.sendMouseState(mouseState);
};
mouse.onmousemove = function (mouseState) {
if (protocol === 'ssh' || protocol === 'telnet') {
mouseState.x = mouseState.x * 2;
mouseState.y = mouseState.y * 2;
client.sendMouseState(mouseState);
} else {
client.sendMouseState(mouseState);
}
};
// Keyboard
const keyboard = new Guacamole.Keyboard(document);
keyboard.onkeydown = this.onKeyDown;
keyboard.onkeyup = this.onKeyUp;
this.setState({
client: client,
containerWidth: width,
containerHeight: height,
keyboard: keyboard
});
}
onWindowResize = (e) => {
if (this.state.client) {
const display = this.state.client.getDisplay();
const width = window.innerWidth;
const height = window.innerHeight;
if (this.state.protocol === 'ssh' || this.state.protocol === 'telnet') {
let r = 2;
display.scale(1 / r);
this.state.client.sendSize(width * r, height * r);
} else {
this.state.client.sendSize(width, height);
}
this.setState({
containerWidth: width,
containerHeight: height,
})
this.resize(this.state.sessionId, width, height).then(_ => {
});
}
};
resize = async (sessionId, width, height) => {
let result = await request.post(`/sessions/${sessionId}/resize?width=${width}&height=${height}`);
if (result.code !== 1) {
message.error(result.message);
}
}
onWindowFocus = (e) => {
if (navigator.clipboard) {
navigator.clipboard.readText().then((text) => {
this.sendClipboard({
'data': text,
'type': 'text/plain'
});
})
}
};
onPaste = (e) => {
const cbd = e.clipboardData;
const ua = window.navigator.userAgent;
// 如果是 Safari 直接 return
if (!(e.clipboardData && e.clipboardData.items)) {
return;
}
// Mac平台下Chrome49版本以下 复制Finder中的文件的Bug Hack掉
if (cbd.items && cbd.items.length === 2 && cbd.items[0].kind === "string" && cbd.items[1].kind === "file" &&
cbd.types && cbd.types.length === 2 && cbd.types[0] === "text/plain" && cbd.types[1] === "Files" &&
ua.match(/Macintosh/i) && Number(ua.match(/Chrome\/(\d{2})/i)[1]) < 49) {
return;
}
for (let i = 0; i < cbd.items.length; i++) {
let item = cbd.items[i];
if (item.kind === "file") {
let blob = item.getAsFile();
if (blob.size === 0) {
return;
}
// blob 就是从剪切板获得的文件 可以进行上传或其他操作
} else if (item.kind === 'string') {
item.getAsString((str) => {
this.sendClipboard({
'data': str,
'type': 'text/plain'
});
})
}
}
};
onSelect = (keys, event) => {
this.setState({
selectNode: {
key: keys[0],
isLeaf: event.node.isLeaf
}
})
};
handleOk = async (values) => {
let params = {
'dir': this.state.selectNode.key + '/' + values['dir']
}
let paramStr = qs.stringify(params);
this.setState({
confirmLoading: true
})
let result = await request.post(`/sessions/${this.state.sessionId}/mkdir?${paramStr}`);
if (result.code === 1) {
message.success('创建成功');
let parentPath = this.state.selectNode.key;
let items = await this.getTreeNodes(parentPath);
this.setState({
treeData: this.updateTreeData(this.state.treeData, parentPath, items),
selectNode: {}
});
} else {
message.error(result.message);
}
this.setState({
confirmLoading: false,
confirmVisible: false
})
}
handleConfirmCancel = () => {
this.setState({
confirmVisible: false
})
}
handleUploadCancel = () => {
this.setState({
uploadVisible: false
})
}
mkdir = () => {
if (!this.state.selectNode.key || this.state.selectNode.isLeaf) {
message.warning('请选择一个目录');
return;
}
this.setState({
confirmVisible: true
})
}
upload = () => {
if (!this.state.selectNode.key || this.state.selectNode.isLeaf) {
message.warning('请选择一个目录进行上传');
return;
}
this.setState({
uploadVisible: true
})
}
download = () => {
if (!this.state.selectNode.key || !this.state.selectNode.isLeaf) {
message.warning('当前只支持下载文件');
return;
}
download(`${server}${prefix}/sessions/${this.state.sessionId}/download?file=${this.state.selectNode.key}`);
}
rmdir = async () => {
if (!this.state.selectNode.key) {
message.warning('请选择一个文件或目录');
return;
}
let result;
if (this.state.selectNode.isLeaf) {
result = await request.delete(`/sessions/${this.state.sessionId}/rm?file=${this.state.selectNode.key}`);
} else {
result = await request.delete(`/sessions/${this.state.sessionId}/rmdir?dir=${this.state.selectNode.key}`);
}
if (result.code !== 1) {
message.error(result.message);
} else {
message.success('删除成功');
let path = this.state.selectNode.key;
let parentPath = path.substring(0, path.lastIndexOf('/'));
let items = await this.getTreeNodes(parentPath);
this.setState({
treeData: this.updateTreeData(this.state.treeData, parentPath, items),
selectNode: {}
});
}
}
refresh = async () => {
if (!this.state.selectNode.key || this.state.selectNode.isLeaf) {
await this.loadDirData('/');
} else {
let key = this.state.selectNode.key;
let items = await this.getTreeNodes(key);
this.setState({
treeData: this.updateTreeData(this.state.treeData, key, items),
});
}
message.success('刷新目录成功');
}
onRightClick = ({event, node}) => {
};
loadDirData = async (key) => {
let items = await this.getTreeNodes(key);
this.setState({
treeData: items,
});
}
getTreeNodes = async (key) => {
const url = server + prefix + '/sessions/' + this.state.sessionId + '/ls?dir=' + key;
let result = await request.get(url);
if (result.code !== 1) {
return [];
}
let data = result.data;
data = data.sort(((a, b) => a.name.localeCompare(b.name)));
return data.map(item => {
return {
title: item['name'],
key: item['path'],
isLeaf: !item['isDir'] && !item['isLink'],
}
});
}
onLoadData = ({key, children}) => {
return new Promise(async (resolve) => {
if (children) {
resolve();
return;
}
let items = await this.getTreeNodes(key);
this.setState({
treeData: this.updateTreeData(this.state.treeData, key, items),
});
resolve();
});
}
updateTreeData = (list, key, children) => {
return list.map((node) => {
if (node.key === key) {
return {...node, children};
} else if (node.children) {
return {...node, children: this.updateTreeData(node.children, key, children)};
}
return node;
});
}
render() {
const title = (
<Row>
<Space>
远程文件管理
&nbsp;
&nbsp;
<Tooltip title="创建文件夹">
<Button type="primary" size="small" icon={<FolderAddOutlined/>}
onClick={this.mkdir} ghost/>
</Tooltip>
<Tooltip title="上传">
<Button type="primary" size="small" icon={<CloudUploadOutlined/>}
onClick={this.upload} ghost/>
</Tooltip>
<Tooltip title="下载">
<Button type="primary" size="small" icon={<CloudDownloadOutlined/>}
onClick={this.download} ghost/>
</Tooltip>
<Tooltip title="删除文件">
<Button type="dashed" size="small" icon={<DeleteOutlined/>} onClick={this.rmdir}
danger/>
</Tooltip>
<Tooltip title="刷新">
<Button type="primary" size="small" icon={<ReloadOutlined/>} onClick={this.refresh}
ghost/>
</Tooltip>
</Space>
</Row>
);
return (
<div>
<div className="container" style={{
overflow: this.state.containerOverflow,
width: this.state.containerWidth,
height: this.state.containerHeight
}}>
<div id="display"/>
</div>
<Modal
title="创建文件夹"
visible={this.state.confirmVisible}
onOk={() => {
this.formRef.current
.validateFields()
.then(values => {
this.formRef.current.resetFields();
this.handleOk(values);
})
.catch(info => {
});
}}
confirmLoading={this.state.confirmLoading}
onCancel={this.handleConfirmCancel}
>
<Form ref={this.formRef} {...formItemLayout}>
<Form.Item label="文件夹名称" name='dir' rules={[{required: true, message: '请输入文件夹名称'}]}>
<Input autoComplete="off" placeholder="请输入文件夹名称"/>
</Form.Item>
</Form>
</Modal>
<Modal
title="上传文件"
visible={this.state.uploadVisible}
onOk={() => {
}}
confirmLoading={this.state.uploadLoading}
onCancel={this.handleUploadCancel}
>
<Upload
action={server + prefix + '/sessions/' + this.state.sessionId + '/upload?X-Auth-Token=' + getToken() + '&dir=' + this.state.selectNode.key}>
<Button icon={<UploadOutlined/>}>上传文件</Button>
</Upload>
</Modal>
<Affix style={{position: 'absolute', top: 50, right: 100}}>
<Button
icon={<CopyOutlined/>}
onClick={() => {
this.showClipboard();
}}
>
</Button>
</Affix>
<Affix style={{position: 'absolute', top: 50, right: 50}}>
<Button
icon={<FolderOpenOutlined/>}
onClick={() => {
this.showFileSystem();
}}
>
</Button>
</Affix>
<Drawer
title={title}
placement="right"
width={window.innerWidth * 0.3}
closable={true}
maskClosable={false}
onClose={this.hideFileSystem}
visible={this.state.fileSystemVisible}
>
<Row style={{marginTop: 10}}>
<Col span={24}>
<Card title={this.state.fileSystem.currentDirectory} bordered={true} size="small">
<Spin indicator={antIcon} spinning={this.state.fileSystem.loading}>
<DirectoryTree
// multiple
onSelect={this.onSelect}
loadData={this.onLoadData}
treeData={this.state.treeData}
onRightClick={this.onRightClick}
/>
</Spin>
</Card>
</Col>
</Row>
</Drawer>
<Drawer
title="剪贴板"
placement="right"
width={window.innerWidth * 0.3}
onClose={this.hideClipboard}
visible={this.state.clipboardVisible}
>
<Alert message="复制/剪切的文本将出现在这里。对下面文本内容所作的修改将会影响远程电脑上的剪贴板。" type="info" showIcon closable/>
<div style={{marginTop: 10, marginBottom: 10}}>
<TextArea id='clipboard' rows={10} onBlur={this.updateClipboardFormTextarea}/>
</div>
</Drawer>
</div>
);
}
}
export default Access;

View File

@ -0,0 +1,3 @@
.console-card .ant-card-body {
padding: 0;
}

View File

@ -0,0 +1,110 @@
import React, {Component} from 'react';
import "xterm/css/xterm.css"
import {Terminal} from "xterm";
import {AttachAddon} from 'xterm-addon-attach';
import qs from "qs";
import {prefix, wsServer} from "../../common/constants";
import "./Console.css"
import {getToken} from "../../utils/utils";
function getGeometry(width, height) {
const cols = Math.floor(width / 9);
const rows = Math.floor(height / 17);
return [cols, rows];
}
class Console extends Component {
state = {
containerOverflow: 'hidden',
containerWidth: 0,
containerHeight: 0,
term: null,
};
componentDidMount() {
let command = this.props.command;
let assetId = this.props.assetId;
let width = this.props.width;
let height = this.props.height;
// let width = Math.floor(window.innerWidth * scale);
// let height = Math.floor(window.innerHeight * scale);
let params = {
'width': width,
'height': height,
'assetId': assetId
};
let paramStr = qs.stringify(params);
let [cols, rows] = getGeometry(width, height);
let term = new Terminal({
cols: cols,
rows: rows,
// screenKeys: true,
// fontFamily: 'menlo',
});
// let fitAddon = new FitAddon();
// term.loadAddon(fitAddon);
term.open(this.refs.terminal);
// fitAddon.fit();
term.writeln('正在努力连接服务器中...');
term.onResize(e => {
});
let token = getToken();
let webSocket = new WebSocket(wsServer + prefix + '/ssh?X-Auth-Token=' + token + '&' + paramStr);
term.loadAddon(new AttachAddon(webSocket));
this.props.appendWebsocket(webSocket);
webSocket.onopen = (e => {
term.clear();
term.focus();
if (command !== '') {
webSocket.send(command + String.fromCharCode(13));
}
});
this.setState({
term: term,
containerWidth: width,
containerHeight: height
});
// window.addEventListener('resize', this.onWindowResize);
}
onWindowResize = (e) => {
let term = this.state.term;
if (term) {
const [cols, rows] = getGeometry(this.state.containerWidth, this.state.containerHeight);
term.resize(cols, rows);
}
};
render() {
return (
<div>
<div ref='terminal' style={{
overflow: this.state.containerOverflow,
width: this.state.containerWidth,
height: this.state.containerHeight,
backgroundColor: 'black'
}}/>
</div>
);
}
}
export default Console;

View File

@ -0,0 +1,237 @@
import React, {Component} from 'react';
import Guacamole from 'guacamole-common-js';
import {message, Modal} from 'antd'
import qs from "qs";
import {prefix, wsServer} from "../../common/constants";
import {LoadingOutlined} from '@ant-design/icons';
import {getToken} from "../../utils/utils";
import './Access.css'
const STATE_IDLE = 0;
const STATE_CONNECTING = 1;
const STATE_WAITING = 2;
const STATE_CONNECTED = 3;
const STATE_DISCONNECTING = 4;
const STATE_DISCONNECTED = 5;
const antIcon = <LoadingOutlined/>;
class Access extends Component {
formRef = React.createRef()
state = {
client: {},
containerOverflow: 'hidden',
containerWidth: 0,
containerHeight: 0,
rate: 1
};
async componentDidMount() {
const connectionId = this.props.connectionId;
let rate = this.props.rate;
let protocol = this.props.protocol;
let width = this.props.width;
let height = this.props.height;
if (protocol === 'ssh' || protocol === 'telnet') {
rate = rate * 0.5;
width = width * 2;
height = height * 2;
}
this.setState({
containerWidth: width * rate,
containerHeight: height * rate,
rate: rate,
})
this.renderDisplay(connectionId);
window.addEventListener('resize', this.onWindowResize);
window.addEventListener('onfocus', this.onWindowFocus);
}
componentWillUnmount() {
if (this.state.client) {
this.state.client.disconnect();
}
window.removeEventListener('resize', this.onWindowResize);
document.removeEventListener("onfocus", this.onWindowFocus);
}
onTunnelStateChange = (state) => {
console.log('onTunnelStateChange', state);
};
onClientStateChange = (state) => {
switch (state) {
case STATE_IDLE:
console.log('初始化');
message.destroy();
message.loading('正在初始化中...', 0);
break;
case STATE_CONNECTING:
console.log('正在连接...');
message.destroy();
message.loading('正在努力连接中...', 0);
break;
case STATE_WAITING:
console.log('正在等待...');
message.destroy();
message.loading('正在等待服务器响应...', 0);
break;
case STATE_CONNECTED:
console.log('连接成功。');
message.destroy();
message.success('连接成功');
if (this.state.client) {
this.state.client.getDisplay().scale(this.state.rate);
}
break;
case STATE_DISCONNECTING:
console.log('连接正在关闭中...');
message.destroy();
break;
case STATE_DISCONNECTED:
console.log('连接关闭。');
message.destroy();
break;
default:
break;
}
};
onError = (status) => {
console.log('通道异常。', status);
switch (status.code) {
case 256:
this.showMessage('未支持的访问');
break;
case 512:
this.showMessage('远程服务异常');
break;
case 513:
this.showMessage('服务器忙碌');
break;
case 514:
this.showMessage('服务器连接超时');
break;
case 515:
this.showMessage('远程服务异常');
break;
case 516:
this.showMessage('资源未找到');
break;
case 517:
this.showMessage('资源冲突');
break;
case 518:
this.showMessage('资源已关闭');
break;
case 519:
this.showMessage('远程服务未找到');
break;
case 520:
this.showMessage('远程服务不可用');
break;
case 521:
this.showMessage('会话冲突');
break;
case 522:
this.showMessage('会话连接超时');
break;
case 523:
this.showMessage('会话已关闭');
break;
case 768:
this.showMessage('网络不可达');
break;
case 769:
this.showMessage('服务器密码验证失败');
break;
case 771:
this.showMessage('客户端被禁止');
break;
case 776:
this.showMessage('客户端连接超时');
break;
case 781:
this.showMessage('客户端异常');
break;
case 783:
this.showMessage('错误的请求类型');
break;
case 797:
this.showMessage('客户端连接数量过多');
break;
default:
this.showMessage('未知错误。');
}
};
showMessage(message) {
Modal.error({
title: '提示',
content: message,
});
}
async renderDisplay(connectionId, protocol) {
let tunnel = new Guacamole.WebSocketTunnel(wsServer + prefix + '/tunnel');
tunnel.onstatechange = this.onTunnelStateChange;
let client = new Guacamole.Client(tunnel);
// 处理客户端的状态变化事件
client.onstatechange = this.onClientStateChange;
client.onerror = this.onError;
const display = document.getElementById("display");
// Add client to display div
const element = client.getDisplay().getElement();
display.appendChild(element);
let token = getToken();
let params = {
'connectionId': connectionId,
'X-Auth-Token': token
};
let paramStr = qs.stringify(params);
// Connect
client.connect(paramStr);
// Disconnect on close
window.onunload = function () {
client.disconnect();
};
this.setState({
client: client
})
}
render() {
return (
<div>
<div className="container" style={{
overflow: this.state.containerOverflow,
width: this.state.containerWidth,
height: this.state.containerHeight
}}>
<div id="display"/>
</div>
</div>
);
}
}
export default Access;

View File

@ -0,0 +1,512 @@
import React, {Component} from 'react';
import "video-react/dist/video-react.css";
import {
Badge,
Button,
Col,
Divider,
Input,
Layout,
Modal,
PageHeader,
Row,
Select,
Space,
Table,
Tag,
Tooltip,
Typography
} from "antd";
import qs from "qs";
import AssetModal from "./AssetModal";
import request from "../../common/request";
import {message} from "antd/es";
import {
CodeTwoTone,
CopyTwoTone,
DeleteOutlined,
DeleteTwoTone,
EditTwoTone,
ExclamationCircleOutlined,
PlusOutlined,
SyncOutlined,
UndoOutlined
} from '@ant-design/icons';
import {itemRender} from "../../utils/utils";
import {PROTOCOL_COLORS} from "../../common/constants";
const confirm = Modal.confirm;
const {Search} = Input;
const {Content} = Layout;
const {Title, Text} = Typography;
const routes = [
{
path: '',
breadcrumbName: '首页',
},
{
path: 'assets',
breadcrumbName: '资产管理',
}
];
class Asset extends Component {
inputRefOfName = React.createRef();
state = {
items: [],
total: 0,
queryParams: {
pageIndex: 1,
pageSize: 10,
protocol: ''
},
loading: false,
modalVisible: false,
modalTitle: '',
modalConfirmLoading: false,
credentials: [],
model: {},
selectedRowKeys: [],
delBtnLoading: false,
};
async componentDidMount() {
await this.loadTableData();
}
async delete(id) {
const result = await request.delete('/assets/' + id);
if (result['code'] === 1) {
message.success('删除成功');
await this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
}
}
async loadTableData(queryParams) {
this.setState({
loading: true
});
queryParams = queryParams || this.state.queryParams;
// queryParams
let paramsStr = qs.stringify(queryParams);
let data = {
items: [],
total: 0
};
try {
let result = await request.get('/assets/paging?' + paramsStr);
if (result['code'] === 1) {
data = result['data'];
} else {
message.error(result['message']);
}
} catch (e) {
} finally {
const items = data.items.map(item => {
return {'key': item['id'], ...item}
})
this.setState({
items: items,
total: data.total,
queryParams: queryParams,
loading: false
});
}
}
handleChangPage = async (pageIndex, pageSize) => {
let queryParams = this.state.queryParams;
queryParams.pageIndex = pageIndex;
queryParams.pageSize = pageSize;
this.setState({
queryParams: queryParams
});
await this.loadTableData(queryParams)
};
handleSearchByName = name => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'name': name,
}
this.loadTableData(query);
};
handleSearchByProtocol = protocol => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'protocol': protocol,
}
this.loadTableData(query);
}
showDeleteConfirm(id, content) {
let self = this;
confirm({
title: '您确定要删除此资产吗?',
content: content,
okText: '确定',
okType: 'danger',
cancelText: '取消',
onOk() {
self.delete(id);
}
});
};
async update(id) {
let result = await request.get(`/assets/${id}`);
if (result.code !== 1) {
message.error(result.message, 10);
return;
}
await this.showModal('更新资产', result.data);
}
async copy(id) {
let result = await request.get(`/assets/${id}`);
if (result.code !== 1) {
message.error(result.message, 10);
return;
}
result.data['id'] = undefined;
await this.showModal('复制资产', result.data);
}
async showModal(title, assets = {}) {
let result = await request.get('/credentials');
let credentials = [];
if (result.code === 1) {
credentials = result.data;
}
this.setState({
modalTitle: title,
modalVisible: true,
credentials: credentials,
model: assets
});
};
handleCancelModal = e => {
this.setState({
modalTitle: '',
modalVisible: false
});
};
handleOk = async (formData) => {
// 弹窗 form 传来的数据
this.setState({
modalConfirmLoading: true
});
if (formData.id) {
// 向后台提交数据
const result = await request.put('/assets/' + formData.id, formData);
if (result.code === 1) {
message.success('操作成功', 3);
this.setState({
modalVisible: false
});
await this.loadTableData(this.state.queryParams);
} else {
message.error('操作失败 :( ' + result.message, 10);
}
} else {
// 向后台提交数据
const result = await request.post('/assets', formData);
if (result.code === 1) {
message.success('操作成功', 3);
this.setState({
modalVisible: false
});
await this.loadTableData(this.state.queryParams);
} else {
message.error('操作失败 :( ' + result.message, 10);
}
}
this.setState({
modalConfirmLoading: false
});
};
access = async (id, protocol) => {
message.loading({content: '正在检测资产是否在线...', key: id});
let result = await request.post(`/assets/${id}/tcping`);
if (result.code === 1) {
if (result.data === true) {
message.success({content: '检测完成,您访问的资产在线,即将打开窗口进行访问。', key: id, duration: 3});
window.open(`#/access?assetsId=${id}&protocol=${protocol}`);
} else {
message.warn('您访问的资产未在线,请确认网络状态。', 10);
}
} else {
message.error('操作失败 :( ' + result.message, 10);
}
}
batchDelete = async () => {
this.setState({
delBtnLoading: true
})
try {
let result = await request.delete('/assets/' + this.state.selectedRowKeys.join(','));
if (result.code === 1) {
message.success('操作成功', 3);
this.setState({
selectedRowKeys: []
})
await this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
}
} finally {
this.setState({
delBtnLoading: false
})
}
}
render() {
const columns = [{
title: '序号',
dataIndex: 'id',
key: 'id',
render: (id, record, index) => {
return index + 1;
}
}, {
title: '资产名称',
dataIndex: 'name',
key: 'name',
render: (name, record) => {
if (name && name.length > 20) {
name = name.substring(0, 20) + "...";
}
return (
<Tooltip placement="topLeft" title={name}>
{name}
</Tooltip>
);
}
}, {
title: 'IP',
dataIndex: 'ip',
key: 'ip',
}, {
title: '端口',
dataIndex: 'port',
key: 'port',
}, {
title: '连接协议',
dataIndex: 'protocol',
key: 'protocol',
render: (text, record) => {
return (<Tag color={PROTOCOL_COLORS[text]}>{text}</Tag>);
}
}, {
title: '状态',
dataIndex: 'active',
key: 'active',
render: text => {
if (text) {
return (<Badge status="processing" text="运行中"/>);
} else {
return (<Badge status="error" text="不可用"/>);
}
}
}, {
title: '创建日期',
dataIndex: 'created',
key: 'created'
},
{
title: '操作',
key: 'action',
render: (text, record) => {
return (
<div>
<Button type="link" size='small' icon={<CodeTwoTone/>}
onClick={() => this.access(record.id, record.protocol)}>接入</Button>
<Button type="link" size='small' icon={<EditTwoTone/>}
onClick={() => this.update(record.id)}>编辑</Button>
<Button type="link" size='small' icon={<CopyTwoTone/>}
onClick={() => this.copy(record.id)}>复制</Button>
<Button type="link" size='small' icon={<DeleteTwoTone/>}
onClick={() => this.showDeleteConfirm(record.id, record.name)}>删除</Button>
</div>
)
},
}
];
const selectedRowKeys = this.state.selectedRowKeys;
const rowSelection = {
selectedRowKeys: this.state.selectedRowKeys,
onChange: (selectedRowKeys, selectedRows) => {
this.setState({selectedRowKeys});
},
};
const hasSelected = selectedRowKeys.length > 0;
return (
<>
<PageHeader
className="site-page-header-ghost-wrapper page-herder"
title="资产管理"
breadcrumb={{
routes: routes,
itemRender: itemRender
}}
subTitle="资产"
>
</PageHeader>
<Content key='page-content' className="site-layout-background page-content">
<div style={{marginBottom: 20}}>
<Row justify="space-around" align="middle" gutter={24}>
<Col span={12} key={1}>
<Title level={3}>资产列表</Title>
</Col>
<Col span={12} key={2} style={{textAlign: 'right'}}>
<Space>
<Search
ref={this.inputRefOfName}
placeholder="资产名称"
allowClear
onSearch={this.handleSearchByName}
/>
<Select onChange={this.handleSearchByProtocol}
value={this.state.queryParams.protocol ? this.state.queryParams.protocol : ''}
style={{width: 100}}>
<Select.Option value="">全部协议</Select.Option>
<Select.Option value="rdp">rdp</Select.Option>
<Select.Option value="ssh">ssh</Select.Option>
<Select.Option value="vnc">vnc</Select.Option>
<Select.Option value="telnet">telnet</Select.Option>
</Select>
<Tooltip title='重置查询'>
<Button icon={<UndoOutlined/>} onClick={() => {
this.inputRefOfName.current.setValue('');
this.loadTableData({pageIndex: 1, pageSize: 10, protocol: ''})
}}>
</Button>
</Tooltip>
<Divider type="vertical"/>
<Tooltip title="新增">
<Button type="dashed" icon={<PlusOutlined/>}
onClick={() => this.showModal('新增资产', {})}>
</Button>
</Tooltip>
<Tooltip title="刷新列表">
<Button icon={<SyncOutlined/>} onClick={() => {
this.loadTableData(this.state.queryParams)
}}>
</Button>
</Tooltip>
<Tooltip title="批量删除">
<Button type="primary" danger disabled={!hasSelected} icon={<DeleteOutlined/>}
loading={this.state.delBtnLoading}
onClick={() => {
const content = <div>
您确定要删除选中的<Text style={{color: '#1890FF'}}
strong>{this.state.selectedRowKeys.length}</Text>
</div>;
confirm({
icon: <ExclamationCircleOutlined/>,
content: content,
onOk: () => {
this.batchDelete()
},
onCancel() {
},
});
}}>
</Button>
</Tooltip>
</Space>
</Col>
</Row>
</div>
<Table key='assets-table'
rowSelection={rowSelection}
dataSource={this.state.items}
columns={columns}
position={'both'}
pagination={{
showSizeChanger: true,
current: this.state.queryParams.pageIndex,
pageSize: this.state.queryParams.pageSize,
onChange: this.handleChangPage,
onShowSizeChange: this.handleChangPage,
total: this.state.total,
showTotal: total => `总计 ${total}`
}}
loading={this.state.loading}
/>
{
this.state.modalVisible ?
<AssetModal
modalFormRef={this.modalFormRef}
visible={this.state.modalVisible}
title={this.state.modalTitle}
handleOk={this.handleOk}
handleCancel={this.handleCancelModal}
confirmLoading={this.state.modalConfirmLoading}
credentials={this.state.credentials}
model={this.state.model}
/>
: null
}
</Content>
</>
);
}
}
export default Asset;

View File

@ -0,0 +1,157 @@
import React, {useEffect, useState} from 'react';
import {Form, Input, InputNumber, Modal, Radio, Select, Tooltip} from "antd/lib/index";
const {TextArea} = Input;
const {Option} = Select;
// 子级页面
// Ant form create 表单内置方法
const AssetModal = function ({title, visible, handleOk, handleCancel, confirmLoading, credentials, model}) {
const [form] = Form.useForm();
let [accountType, setAccountType] = useState(model.accountType);
useEffect(() => {
setAccountType(model.accountType);
});
const formItemLayout = {
labelCol: {span: 6},
wrapperCol: {span: 14},
};
const handleProtocolChange = e => {
let port;
switch (e.target.value) {
case 'ssh':
port = 22;
break;
case 'rdp':
port = 3389;
break;
case 'vnc':
port = 5901;
form.setFieldsValue({
accountType: 'custom',
});
break;
case 'telnet':
port = 23;
break;
default:
port = 65535;
}
form.setFieldsValue({
port: port,
});
};
return (
<Modal
title={title}
visible={visible}
maskClosable={true}
onOk={() => {
form
.validateFields()
.then(values => {
form.resetFields();
handleOk(values);
})
.catch(info => {});
}}
onCancel={handleCancel}
confirmLoading={confirmLoading}
okText='确定'
cancelText='取消'
>
<Form form={form} {...formItemLayout} initialValues={model}>
<Form.Item name='id' noStyle>
<Input hidden={true}/>
</Form.Item>
<Form.Item label="资产名称" name='name' rules={[{required: true, message: "请输入资产名称"}]}>
<Input placeholder="请输入资产名称"/>
</Form.Item>
<Form.Item label="IP" name='ip' rules={[{required: true, message: '请输入资产IP'}]}>
<Input placeholder="请输入资产IP"/>
</Form.Item>
<Form.Item label="接入协议" name='protocol' rules={[{required: true, message: '请选择接入协议'}]}>
<Radio.Group onChange={handleProtocolChange}>
<Radio value="rdp">rdp</Radio>
<Radio value="ssh">ssh</Radio>
<Radio value="vnc">vnc</Radio>
<Radio value="telnet">telnet</Radio>
</Radio.Group>
</Form.Item>
<Form.Item label="端口" name='port' rules={[{required: true, message: '请输入资产端口'}]}>
<InputNumber min={1} max={65535}/>
</Form.Item>
<Form.Item label="账户类型" name='accountType' rules={[{required: true, message: '请选择接账户类型'}]}>
<Select onChange={(v) => {
setAccountType(v);
model.accountType = v;
}}>
<Option value="custom">自定义</Option>
<Option value="credential">授权凭证</Option>
<Option value="secret-key">密钥</Option>
</Select>
</Form.Item>
{
accountType === 'credential' ?
<Form.Item label="授权凭证" name='credentialId' rules={[{required: true, message: '请选择授权凭证'}]}>
<Select onChange={() => null}>
{credentials.map(item => {
return (
<Option key={item.id} value={item.id}>
<Tooltip placement="topLeft" title={item.name}>
{item.name}
</Tooltip>
</Option>
);
})}
</Select>
</Form.Item>
: null
}
{
accountType === 'custom' ?
<>
<Form.Item label="授权账户" name='username' rules={[{required: true, message: '请输入授权账户'}]}
noStyle={!(accountType === 'custom')}>
<Input placeholder="输入授权账户"/>
</Form.Item>
<Form.Item label="授权密码" name='password' rules={[{required: true, message: '请输入授权密码'}]}
noStyle={!(accountType === 'custom')}>
<Input placeholder="输入授权密码"/>
</Form.Item>
</>
: null
}
{
accountType === 'secret-key' ?
<Form.Item label="私钥" name='passphrase' rules={[{required: true, message: '请输入私钥'}]}>
<TextArea rows={4}/>
</Form.Item>
: null
}
</Form>
</Modal>
)
}
export default AssetModal;

View File

@ -0,0 +1,92 @@
import React, {Component} from 'react';
import {List, Card, Input, PageHeader} from "antd";
import Console from "../access/Console";
import {itemRender} from "../../utils/utils";
const {Search} = Input;
const routes = [
{
path: '',
breadcrumbName: '首页',
},
{
path: '/dynamic-command',
breadcrumbName: '动态指令',
},
{
path: '/batch-command',
breadcrumbName: '批量执行命令',
}
];
class BatchCommand extends Component {
commandRef = React.createRef();
state = {
webSockets: [],
assets: []
}
componentDidMount() {
let params = new URLSearchParams(this.props.location.search);
let command = params.get('command');
let assets = JSON.parse(params.get('assets'));
this.setState({
command: command,
assets: assets
})
}
onPaneChange = activeKey => {
this.setState({activeKey});
};
appendWebsocket = (webSocket) => {
this.state.webSockets.push(webSocket);
}
render() {
return (
<>
<PageHeader
className="site-page-header-ghost-wrapper page-herder"
title="批量执行命令"
breadcrumb={{
routes: routes,
itemRender: itemRender
}}
subTitle="动态指令"
>
</PageHeader>
<div className="page-search">
<Search ref={this.commandRef} placeholder="请输入指令" onSearch={value => {
for (let i = 0; i < this.state.webSockets.length; i++) {
this.state.webSockets[i].send(value + String.fromCharCode(13))
}
this.commandRef.current.setValue('');
}} enterButton='执行'/>
</div>
<div className="page-card">
<List
grid={{gutter: 16, column: 2}}
dataSource={this.state.assets}
renderItem={item => (
<List.Item>
<Card title={item.name}>
<Console assetId={item.id} command={this.state.command}
width={(window.innerWidth - 350) / 2}
height={400}
appendWebsocket={this.appendWebsocket}/>
</Card>
</List.Item>
)}
/>
</div>
</>
);
}
}
export default BatchCommand;

View File

@ -0,0 +1,501 @@
import React, {Component} from 'react';
import {
Button,
Checkbox,
Col,
Divider,
Input,
Layout,
Modal,
PageHeader,
Row,
Space,
Table,
Tooltip,
Typography
} from "antd";
import qs from "qs";
import request from "../../common/request";
import {message} from "antd/es";
import DynamicCommandModal from "./DynamicCommandModal";
import {
CodeTwoTone,
DeleteOutlined,
DeleteTwoTone,
EditTwoTone,
ExclamationCircleOutlined,
PlusOutlined,
SyncOutlined,
UndoOutlined
} from '@ant-design/icons';
import {itemRender} from "../../utils/utils";
const confirm = Modal.confirm;
const {Content} = Layout;
const {Title, Text} = Typography;
const {Search} = Input;
const routes = [
{
path: '',
breadcrumbName: '首页',
},
{
path: 'command',
breadcrumbName: '动态指令',
}
];
class DynamicCommand extends Component {
inputRefOfName = React.createRef();
inputRefOfContent = React.createRef();
state = {
items: [],
total: 0,
queryParams: {
pageIndex: 1,
pageSize: 10
},
loading: false,
modalVisible: false,
modalTitle: '',
modalConfirmLoading: false,
assetsVisible: false,
assets: [],
checkedAssets: [],
command: '',
model: null,
selectedRowKeys: [],
delBtnLoading: false,
};
componentDidMount() {
this.loadTableData();
}
async delete(id) {
const result = await request.delete('/commands/' + id);
if (result.code === 1) {
message.success('删除成功');
this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
}
}
async loadTableData(queryParams) {
this.setState({
loading: true
});
queryParams = queryParams || this.state.queryParams;
// queryParams
let paramsStr = qs.stringify(queryParams);
let data = {
items: [],
total: 0
};
try {
let result = await request.get('/commands/paging?' + paramsStr);
if (result.code === 1) {
data = result.data;
} else {
message.error(result.message);
}
} catch (e) {
} finally {
const items = data.items.map(item => {
return {'key': item['id'], ...item}
})
this.setState({
items: items,
total: data.total,
queryParams: queryParams,
loading: false
});
}
}
handleChangPage = async (pageIndex, pageSize) => {
let queryParams = this.state.queryParams;
queryParams.pageIndex = pageIndex;
queryParams.pageSize = pageSize;
this.setState({
queryParams: queryParams
});
await this.loadTableData(queryParams)
};
handleSearchByName = name => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'name': name,
}
this.loadTableData(query);
};
handleSearchByContent = content => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'content': content,
}
this.loadTableData(query);
};
showDeleteConfirm(id, content) {
let self = this;
confirm({
title: '您确定要删除此指令吗?',
content: content,
okText: '确定',
okType: 'danger',
cancelText: '取消',
onOk() {
self.delete(id);
}
});
};
showModal(title, assets = null) {
this.setState({
modalTitle: title,
modalVisible: true,
model: assets
});
};
handleCancelModal = e => {
this.setState({
modalTitle: '',
modalVisible: false
});
};
handleChecked = e => {
let checkedAssets = this.state.checkedAssets;
if (e.target.checked) {
checkedAssets.push(e.target.value);
} else {
for (let i = 0; i < checkedAssets.length; i++) {
if (checkedAssets[i].id === e.target.value.id) {
checkedAssets.splice(i, 1);
}
}
}
this.setState({
checkedAssets: checkedAssets
});
};
executeCommand = e => {
let checkedAssets = this.state.checkedAssets;
if (checkedAssets.length === 0) {
message.warning('请至少选择一个资产');
return;
}
let assets = checkedAssets.map(item => {
return {
id: item.id,
name: item.name
}
});
window.location.href = '#/batch-command?command=' + this.state.command + '&assets=' + JSON.stringify(assets);
};
handleOk = async (formData) => {
// 弹窗 form 传来的数据
this.setState({
modalConfirmLoading: true
});
if (formData.id) {
// 向后台提交数据
const result = await request.put('/commands/' + formData.id, formData);
if (result.code === 1) {
message.success('更新成功');
this.setState({
modalVisible: false
});
this.loadTableData(this.state.queryParams);
} else {
message.error('更新失败 :( ' + result.message, 10);
}
} else {
// 向后台提交数据
const result = await request.post('/commands', formData);
if (result.code === 1) {
message.success('新增成功');
this.setState({
modalVisible: false
});
this.loadTableData(this.state.queryParams);
} else {
message.error('新增失败 :( ' + result.message, 10);
}
}
this.setState({
modalConfirmLoading: false
});
};
batchDelete = async () => {
this.setState({
delBtnLoading: true
})
try {
let result = await request.delete('/commands/' + this.state.selectedRowKeys.join(','));
if (result.code === 1) {
message.success('操作成功', 3);
this.setState({
selectedRowKeys: []
})
await this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
}
} finally {
this.setState({
delBtnLoading: false
})
}
}
render() {
const columns = [{
title: '序号',
dataIndex: 'id',
key: 'id',
render: (id, record, index) => {
return index + 1;
}
}, {
title: '指令名称',
dataIndex: 'name',
key: 'name',
render: (name, record) => {
if (name && name.length > 20) {
name = name.substring(0, 20) + "...";
}
return (
<Tooltip placement="topLeft" title={name}>
{name}
</Tooltip>
);
}
}, {
title: '指令内容',
dataIndex: 'content',
key: 'content',
}, {
title: '操作',
key: 'action',
render: (text, record) => {
return (
<div>
<Button type="link" size='small' icon={<EditTwoTone/>}
onClick={() => this.showModal('更新指令', record)}>编辑</Button>
<Button type="link" size='small' icon={<DeleteTwoTone/>}
onClick={() => this.showDeleteConfirm(record.id, record.name)}>删除</Button>
<Button type="link" size='small' icon={<CodeTwoTone/>} onClick={async () => {
this.setState({
assetsVisible: true,
command: record.content
});
let result = await request.get('/assets?protocol=ssh');
if (result.code === 1) {
this.setState({
assets: result.data
});
} else {
message.error(result.message);
}
}}>执行</Button>
</div>
)
},
}
];
const selectedRowKeys = this.state.selectedRowKeys;
const rowSelection = {
selectedRowKeys: this.state.selectedRowKeys,
onChange: (selectedRowKeys, selectedRows) => {
this.setState({selectedRowKeys});
},
};
const hasSelected = selectedRowKeys.length > 0;
return (
<>
<PageHeader
className="site-page-header-ghost-wrapper page-herder"
title="动态指令"
breadcrumb={{
routes: routes,
itemRender: itemRender
}}
subTitle="批量动态指令执行"
>
</PageHeader>
<Content className="site-layout-background page-content">
<div style={{marginBottom: 20}}>
<Row justify="space-around" align="middle" gutter={24}>
<Col span={12} key={1}>
<Title level={3}>指令列表</Title>
</Col>
<Col span={12} key={2} style={{textAlign: 'right'}}>
<Space>
<Search
ref={this.inputRefOfName}
placeholder="指令名称"
allowClear
onSearch={this.handleSearchByName}
/>
<Search
ref={this.inputRefOfContent}
placeholder="指令内容"
allowClear
onSearch={this.handleSearchByContent}
/>
<Tooltip title='重置查询'>
<Button icon={<UndoOutlined/>} onClick={() => {
this.inputRefOfName.current.setValue('');
this.inputRefOfContent.current.setValue('');
this.loadTableData({pageIndex: 1, pageSize: 10, name: '', content: ''})
}}>
</Button>
</Tooltip>
<Divider type="vertical"/>
<Tooltip title="新增">
<Button type="dashed" icon={<PlusOutlined/>}
onClick={() => this.showModal('新增指令', {})}>
</Button>
</Tooltip>
<Tooltip title="刷新列表">
<Button icon={<SyncOutlined/>} onClick={() => {
this.loadTableData(this.state.queryParams)
}}>
</Button>
</Tooltip>
<Tooltip title="批量删除">
<Button type="primary" danger disabled={!hasSelected} icon={<DeleteOutlined/>}
loading={this.state.delBtnLoading}
onClick={() => {
const content = <div>
您确定要删除选中的<Text style={{color: '#1890FF'}}
strong>{this.state.selectedRowKeys.length}</Text>
</div>;
confirm({
icon: <ExclamationCircleOutlined/>,
content: content,
onOk: () => {
this.batchDelete()
},
onCancel() {
},
});
}}>
</Button>
</Tooltip>
</Space>
</Col>
</Row>
</div>
<Table
rowSelection={rowSelection}
dataSource={this.state.items}
columns={columns}
position={'both'}
pagination={{
showSizeChanger: true,
current: this.state.queryParams.pageIndex,
pageSize: this.state.queryParams.pageSize,
onChange: this.handleChangPage,
onShowSizeChange: this.handleChangPage,
total: this.state.total,
showTotal: total => `总计 ${total}`
}}
loading={this.state.loading}
/>
{
this.state.modalVisible ?
<DynamicCommandModal
visible={this.state.modalVisible}
title={this.state.modalTitle}
handleOk={this.handleOk}
handleCancel={this.handleCancelModal}
confirmLoading={this.state.modalConfirmLoading}
model={this.state.model}
>
</DynamicCommandModal>
: null
}
<Modal
title="选择资产"
visible={this.state.assetsVisible}
onOk={this.executeCommand}
onCancel={() => {
this.setState({
assetsVisible: false
});
}}
>
{this.state.assets.map(item => {
return (<Checkbox key={item.id} value={item}
onChange={this.handleChecked}>{item.name}</Checkbox>);
})}
</Modal>
</Content>
</>
);
}
}
export default DynamicCommand;

View File

@ -0,0 +1,59 @@
import React from 'react';
import {Form, Input, Modal} from "antd/lib/index";
const {TextArea} = Input;
// 子级页面
// Ant form create 表单内置方法
const DynamicCommandModal = ({title, visible, handleOk, handleCancel, confirmLoading, model}) => {
const [form] = Form.useForm();
const formItemLayout = {
labelCol: {span: 6},
wrapperCol: {span: 18},
};
return (
<Modal
title={title}
visible={visible}
maskClosable={false}
onOk={() => {
form
.validateFields()
.then(values => {
form.resetFields();
handleOk(values);
})
.catch(info => {
});
}}
onCancel={handleCancel}
confirmLoading={confirmLoading}
okText='确定'
cancelText='取消'
>
<Form form={form} {...formItemLayout} initialValues={model}>
<Form.Item name='id' noStyle>
<Input hidden={true}/>
</Form.Item>
<Form.Item label="指令名称" name='name' rules={[{required: true, message: '请输入指令名称'}]}>
<Input placeholder="请输入指令内容"/>
</Form.Item>
<Form.Item label="指令内容" name='content' rules={[{required: true, message: '请输入指令内容'}]}>
<TextArea autoSize={{ minRows: 5, maxRows: 10 }} placeholder="一行一个命令"/>
</Form.Item>
</Form>
</Modal>
)
};
export default DynamicCommandModal;

View File

@ -0,0 +1,400 @@
import React, {Component} from 'react';
import "video-react/dist/video-react.css";
import {Button, Col, Divider, Input, Layout, Modal, PageHeader, Row, Space, Table, Tooltip, Typography} from "antd";
import qs from "qs";
import CredentialModal from "./CredentialModal";
import request from "../../common/request";
import {message} from "antd/es";
import {
DeleteOutlined, DeleteTwoTone,
EditTwoTone,
ExclamationCircleOutlined,
PlusOutlined,
SyncOutlined,
UndoOutlined
} from '@ant-design/icons';
import {itemRender} from "../../utils/utils";
const confirm = Modal.confirm;
const {Search} = Input;
const {Title, Text} = Typography;
const {Content} = Layout;
const routes = [
{
path: '',
breadcrumbName: '首页',
},
{
path: 'credentials',
breadcrumbName: '授权凭证',
}
];
class Credential extends Component {
inputRefOfName = React.createRef();
state = {
items: [],
total: 0,
queryParams: {
pageIndex: 1,
pageSize: 10
},
loading: false,
modalVisible: false,
modalTitle: '',
modalConfirmLoading: false,
model: null,
selectedRowKeys: [],
delBtnLoading: false,
};
componentDidMount() {
this.loadTableData();
}
async delete(id) {
const result = await request.delete('/credentials/' + id);
if (result.code === 1) {
message.success('删除成功');
await this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
}
}
async loadTableData(queryParams) {
this.setState({
loading: true
});
queryParams = queryParams || this.state.queryParams;
// queryParams
let paramsStr = qs.stringify(queryParams);
let data = {
items: [],
total: 0
};
try {
let result = await request.get('/credentials/paging?' + paramsStr);
if (result.code === 1) {
data = result.data;
} else {
message.error(result.message);
}
} catch (e) {
} finally {
const items = data.items.map(item => {
return {'key': item['id'], ...item}
})
this.setState({
items: items,
total: data.total,
queryParams: queryParams,
loading: false
});
}
}
handleChangPage = (pageIndex, pageSize) => {
let queryParams = this.state.queryParams;
queryParams.pageIndex = pageIndex;
queryParams.pageSize = pageSize;
this.setState({
queryParams: queryParams
});
this.loadTableData(queryParams)
};
handleSearchByName = name => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'name': name,
}
this.loadTableData(query);
};
showDeleteConfirm(id, content) {
let self = this;
confirm({
title: '您确定要删除此记录吗?',
content: content,
okText: '确定',
okType: 'danger',
cancelText: '取消',
onOk() {
self.delete(id).then(r => {});
}
});
};
showModal(title, idcard = null) {
this.setState({
modalTitle: title,
modalVisible: true,
model: idcard
});
};
handleCancelModal = e => {
this.setState({
modalTitle: '',
modalVisible: false
});
};
handleOk = async (formData) => {
// 弹窗 form 传来的数据
this.setState({
modalConfirmLoading: true
});
if (formData.id) {
// 向后台提交数据
const result = await request.put('/credentials/' + formData.id, formData);
if (result.code === 1) {
message.success('操作成功', 3);
this.setState({
modalVisible: false
});
await this.loadTableData(this.state.queryParams);
} else {
message.error('操作失败 :( ' + result.message, 10);
}
} else {
// 向后台提交数据
const result = await request.post('/credentials', formData);
if (result.code === 1) {
message.success('操作成功', 3);
this.setState({
modalVisible: false
});
await this.loadTableData(this.state.queryParams);
} else {
message.error('操作失败 :( ' + result.message, 10);
}
}
this.setState({
modalConfirmLoading: false
});
};
batchDelete = async () => {
this.setState({
delBtnLoading: true
})
try {
let result = await request.delete('/credentials/' + this.state.selectedRowKeys.join(','));
if (result.code === 1) {
message.success('操作成功', 3);
this.setState({
selectedRowKeys: []
})
await this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
}
} finally {
this.setState({
delBtnLoading: false
})
}
}
render() {
const columns = [{
title: '序号',
dataIndex: 'id',
key: 'id',
render: (id, record, index) => {
return index + 1;
}
}, {
title: '凭证名称',
dataIndex: 'name',
key: 'name',
render: (name, record) => {
if (name && name.length > 20) {
name = name.substring(0, 20) + "...";
}
return (
<Tooltip placement="topLeft" title={name}>
{name}
</Tooltip>
);
}
}, {
title: '授权账户',
dataIndex: 'username',
key: 'username',
}, {
title: '授权密码',
dataIndex: 'password',
key: 'password',
},
{
title: '操作',
key: 'action',
render: (text, record) => {
return (
<div>
<Button type="link" size='small' icon={<EditTwoTone/>} onClick={() => this.showModal('更新凭证', record)}>编辑</Button>
<Button type="link" size='small' icon={<DeleteTwoTone />} onClick={() => this.showDeleteConfirm(record.id, record.name)}>删除</Button>
</div>
)
},
}
];
const selectedRowKeys = this.state.selectedRowKeys;
const rowSelection = {
selectedRowKeys: this.state.selectedRowKeys,
onChange: (selectedRowKeys, selectedRows) => {
this.setState({selectedRowKeys});
},
};
const hasSelected = selectedRowKeys.length > 0;
return (
<>
<PageHeader
className="site-page-header-ghost-wrapper page-herder"
title="授权凭证"
breadcrumb={{
routes: routes,
itemRender: itemRender
}}
subTitle="访问资产的账户、密钥等"
>
</PageHeader>
<Content className="site-layout-background page-content">
<div style={{marginBottom: 20}}>
<Row justify="space-around" align="middle" gutter={24}>
<Col span={12} key={1}>
<Title level={3}>授权凭证列表</Title>
</Col>
<Col span={12} key={2} style={{textAlign: 'right'}}>
<Space>
<Search
ref={this.inputRefOfName}
placeholder="凭证名称"
allowClear
onSearch={this.handleSearchByName}
/>
<Tooltip title='重置查询'>
<Button icon={<UndoOutlined/>} onClick={() => {
this.inputRefOfName.current.setValue('');
this.loadTableData({pageIndex: 1, pageSize: 10, name: ''})
}}>
</Button>
</Tooltip>
<Divider type="vertical"/>
<Tooltip title="新增">
<Button type="dashed" icon={<PlusOutlined/>}
onClick={() => this.showModal('新增凭证', null)}>
</Button>
</Tooltip>
<Tooltip title="刷新列表">
<Button icon={<SyncOutlined/>} onClick={() => {
this.loadTableData(this.state.queryParams)
}}>
</Button>
</Tooltip>
<Tooltip title="批量删除">
<Button type="primary" danger disabled={!hasSelected} icon={<DeleteOutlined/>}
loading={this.state.delBtnLoading}
onClick={() => {
const content = <div>
您确定要删除选中的<Text style={{color: '#1890FF'}}
strong>{this.state.selectedRowKeys.length}</Text>
</div>;
confirm({
icon: <ExclamationCircleOutlined/>,
content: content,
onOk: () => {
this.batchDelete()
},
onCancel() {
},
});
}}>
</Button>
</Tooltip>
</Space>
</Col>
</Row>
</div>
<Table
rowSelection={rowSelection}
rowKey='id'
dataSource={this.state.items}
columns={columns}
position={'both'}
pagination={{
showSizeChanger: true,
current: this.state.queryParams.pageIndex,
pageSize: this.state.queryParams.pageSize,
onChange: this.handleChangPage,
onShowSizeChange: this.handleChangPage,
total: this.state.total,
showTotal: total => `总计 ${total}`
}}
loading={this.state.loading}
/>
{
this.state.modalVisible ?
<CredentialModal
visible={this.state.modalVisible}
title={this.state.modalTitle}
handleOk={this.handleOk}
handleCancel={this.handleCancelModal}
confirmLoading={this.state.modalConfirmLoading}
model={this.state.model}
>
</CredentialModal>
: null
}
</Content>
</>
);
}
}
export default Credential;

View File

@ -0,0 +1,59 @@
import React from 'react';
import {Form, Input, Modal} from "antd/lib/index";
const CredentialModal = ({title, visible, handleOk, handleCancel, confirmLoading,model}) => {
const [form] = Form.useForm();
const formItemLayout = {
labelCol: {span: 6},
wrapperCol: {span: 14},
};
return (
<Modal
title={title}
visible={visible}
maskClosable={false}
onOk={() => {
form
.validateFields()
.then(values => {
form.resetFields();
handleOk(values);
})
.catch(info => {
});
}}
onCancel={handleCancel}
confirmLoading={confirmLoading}
okText='确定'
cancelText='取消'
>
<Form form={form} {...formItemLayout} initialValues={model}>
<Form.Item name='id' noStyle>
<Input hidden={true}/>
</Form.Item>
<Form.Item label="凭证名称" name='name' rules={[{required: true, message: '请输入凭证名称'}]}>
<Input placeholder="请输入凭证名称"/>
</Form.Item>
<Form.Item label="授权账户" name='username' rules={[{required: true, message: '请输入授权账户'}]}>
<Input placeholder="输入授权账户"/>
</Form.Item>
<Form.Item label="授权密码" name='password' rules={[{required: true, message: '请输入授权密码',}]}>
<Input placeholder="输入授权密码"/>
</Form.Item>
</Form>
</Modal>
)
};
export default CredentialModal;

View File

@ -0,0 +1,4 @@
.text-center{
width: 100px;
text-align: center;
}

View File

@ -0,0 +1,196 @@
import React, {Component} from 'react';
import {Layout, PageHeader, Card, Row, Col, Progress, Typography, Popover, Statistic} from "antd";
import {DesktopOutlined, IdcardOutlined, LikeOutlined, LinkOutlined, UserOutlined} from '@ant-design/icons';
import {itemRender} from '../../utils/utils'
import request from "../../common/request";
import './Dashboard.css'
import {Link} from "react-router-dom";
const {Content} = Layout;
const {Title, Paragraph} = Typography;
const routes = [
{
path: '',
breadcrumbName: '首页',
},
{
path: 'dashboard',
breadcrumbName: '仪表盘',
}
];
class Dashboard extends Component {
state = {
status: {
load: {
load1: 0,
load5: 0,
load15: 0,
},
cpu: {
percent: 0,
logicalCount: 0,
physicalCount: 0
},
memory: {
usedPercent: 0,
available: 0,
total: 0,
used: 0
}
},
interval: null,
counter: {}
}
componentDidMount() {
this.getCounter();
this.getStatus();
this.setState({
interval: setInterval(() => this.getStatus(), 5000)
})
}
componentWillUnmount() {
if (this.state.interval != null) {
clearInterval(this.state.interval);
}
}
getStatus = async () => {
let result = await request.get('/overview/status');
if (result.code === 1) {
this.setState({
status: result.data
})
}
}
getCounter = async () => {
let result = await request.get('/overview/counter');
if (result.code === 1) {
this.setState({
counter: result.data
})
}
}
render() {
const loadContent = (
<div>
<p>最近1分钟负载{this.state.status.load['load1'].toFixed(1)}</p>
<p>最近5分钟负载{this.state.status.load['load5'].toFixed(1)}</p>
<p>最近15分钟负载{this.state.status.load['load15'].toFixed(1)}</p>
</div>
);
const cpuContent = (
<div>
<p>CPU型号{this.state.status.cpu['modelName']}</p>
<p>物理核心{this.state.status.cpu['physicalCount']}</p>
<p>逻辑核心{this.state.status.cpu['logicalCount']}</p>
</div>
);
return (
<>
<PageHeader
className="site-page-header-ghost-wrapper page-herder"
title="dashboard"
breadcrumb={{
routes: routes,
itemRender: itemRender
}}
subTitle="仪表盘"
>
</PageHeader>
<div className="page-card">
<Row gutter={16}>
<Col span={6}>
<Card bordered={true}>
<Link to={'/user'}>
<Statistic title="在线用户" value={this.state.counter['user']}
prefix={<UserOutlined/>}/>
</Link>
</Card>
</Col>
<Col span={6}>
<Card bordered={true}>
<Link to={'/asset'}>
<Statistic title="存活资产" value={this.state.counter['asset']}
prefix={<DesktopOutlined/>}/>
</Link>
</Card>
</Col>
<Col span={6}>
<Card bordered={true}>
<Link to={'/credential'}>
<Statistic title="授权凭证" value={this.state.counter['credential']}
prefix={<IdcardOutlined/>}/>
</Link>
</Card>
</Col>
<Col span={6}>
<Card bordered={true}>
<Link to={'/online-session'}>
<Statistic title="在线会话" value={this.state.counter['onlineSession']}
prefix={<LinkOutlined/>}/>
</Link>
</Card>
</Col>
</Row>
</div>
<div className="page-card">
<Card title="状态" bordered={true}>
<Row>
<Col span={4}>
<Title level={5} className="text-center">负载状态</Title>
<Popover placement="topLeft" title={"负载详情"} content={loadContent}>
<Progress type="circle" width={100}
percent={this.state.status.load['load1'].toFixed(1)}/>
</Popover>
<Paragraph className="text-center">运行流畅</Paragraph>
</Col>
<Col span={4}>
<Title level={5} className="text-center">CPU使用率</Title>
<Popover placement="topLeft" title={"CPU详情"} content={cpuContent}>
<Progress type="circle" width={100}
percent={this.state.status.cpu['percent'].toFixed(1)}/>
</Popover>
<Paragraph className="text-center">{this.state.status.cpu['logicalCount']}核心</Paragraph>
</Col>
<Col span={4}>
<Title level={5} className="text-center">内存使用率</Title>
<Progress type="circle" width={100}
percent={this.state.status.memory['usedPercent'].toFixed(1)}/>
<Paragraph className="text-center">
{Math.floor(this.state.status.memory['used'] / 1024 / 1024)}
/
{Math.floor(this.state.status.memory['total'] / 1024 / 1024)}
(MB)
</Paragraph>
</Col>
<Col span={4}>
</Col>
</Row>
</Card>
</div>
</>
);
}
}
export default Dashboard;

View File

@ -0,0 +1,499 @@
import React, {Component} from 'react';
import "video-react/dist/video-react.css";
import {
Button,
Col,
Divider,
Input,
Layout,
Modal,
notification,
PageHeader,
Row,
Select,
Space,
Table,
Tag,
Tooltip,
Typography
} from "antd";
import qs from "qs";
import request from "../../common/request";
import {differTime, formatDate, itemRender} from "../../utils/utils";
import Playback from "./Playback";
import {message} from "antd/es";
import {
DeleteOutlined, DeleteTwoTone,
ExclamationCircleOutlined,
PlaySquareTwoTone,
SyncOutlined,
UndoOutlined
} from "@ant-design/icons";
import {PROTOCOL_COLORS} from "../../common/constants";
const confirm = Modal.confirm;
const {Content} = Layout;
const {Search} = Input;
const {Title, Text} = Typography;
const routes = [
{
path: '',
breadcrumbName: '首页',
},
{
path: 'offlineSession',
breadcrumbName: '离线会话',
}
];
class OfflineSession extends Component {
inputRefOfClientIp = React.createRef();
state = {
items: [],
total: 0,
queryParams: {
pageIndex: 1,
pageSize: 10,
protocol: '',
userId: undefined,
assetId: undefined
},
loading: false,
playbackVisible: false,
playbackSessionId: null,
videoPlayerVisible: false,
videoPlayerSource: null,
selectedRowKeys: [],
delBtnLoading: false,
users: [],
assets: [],
};
componentDidMount() {
this.loadTableData();
this.handleSearchByNickname('');
this.handleSearchByAssetName('');
}
async loadTableData(queryParams) {
queryParams = queryParams || this.state.queryParams;
queryParams['status'] = 'disconnected';
this.setState({
queryParams: queryParams,
loading: true
});
// queryParams
let paramsStr = qs.stringify(queryParams);
let data = {
items: [],
total: 0
};
try {
let result = await request.get('/sessions/paging?' + paramsStr);
if (result.code === 1) {
data = result.data;
} else {
message.error(result.message);
}
} catch (e) {
} finally {
const items = data.items.map(item => {
return {'key': item['id'], ...item}
})
this.setState({
items: items,
total: data.total,
queryParams: queryParams,
loading: false
});
}
}
handleChangPage = (pageIndex, pageSize) => {
let queryParams = this.state.queryParams;
queryParams.pageIndex = pageIndex;
queryParams.pageSize = pageSize;
this.setState({
queryParams: queryParams
});
this.loadTableData(queryParams)
};
showPlayback = (sessionId) => {
this.setState({
playbackVisible: true,
playbackSessionId: sessionId
});
};
hidePlayback = () => {
this.setState({
playbackVisible: false,
playbackSessionId: null
});
};
handleSearchByClientIp = clientIp => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'clientIp': clientIp,
}
this.loadTableData(query);
}
handleChangeByProtocol = protocol => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'protocol': protocol,
}
this.loadTableData(query);
}
handleSearchByNickname = async nickname => {
const result = await request.get(`/users/paging?pageIndex=1&pageSize=100&nickname=${nickname}`);
if (result.code !== 1) {
message.error(result.message, 10);
return;
}
this.setState({
users: result.data.items
})
}
handleChangeByUserId = userId => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'userId': userId,
}
this.loadTableData(query);
}
handleSearchByAssetName = async assetName => {
const result = await request.get(`/assets/paging?pageIndex=1&pageSize=100&name=${assetName}`);
if (result.code !== 1) {
message.error(result.message, 10);
return;
}
this.setState({
assets: result.data.items
})
}
handleChangeByAssetId = (assetId, options) => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'assetId': assetId,
}
this.loadTableData(query);
}
batchDelete = async () => {
this.setState({
delBtnLoading: true
})
try {
let result = await request.delete('/sessions/' + this.state.selectedRowKeys.join(','));
if (result.code === 1) {
message.success('操作成功', 3);
this.setState({
selectedRowKeys: []
})
await this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
}
} finally {
this.setState({
delBtnLoading: false
})
}
}
render() {
const columns = [{
title: '序号',
dataIndex: 'id',
key: 'id',
render: (id, record, index) => {
return index + 1;
}
}, {
title: '用户昵称',
dataIndex: 'creatorName',
key: 'creatorName'
}, {
title: '来源IP',
dataIndex: 'clientIp',
key: 'clientIp'
}, {
title: '资产名称',
dataIndex: 'assetName',
key: 'assetName'
}, {
title: '远程连接',
dataIndex: 'access',
key: 'access',
render: (text, record) => {
return `${record.username}@${record.ip}:${record.port}`;
}
}, {
title: '屏幕大小',
dataIndex: 'screen',
key: 'screen',
render: (text, record) => {
return `${record.width}x${record.height}`;
}
}, {
title: '连接协议',
dataIndex: 'protocol',
key: 'protocol',
render: (text, record) => {
return (<Tag color={PROTOCOL_COLORS[text]}>{text}</Tag>);
}
}, {
title: '接入时间',
dataIndex: 'connectedTime',
key: 'connectedTime',
render: (text, record) => {
return formatDate(text, 'yyyy-MM-dd hh:mm:ss');
}
}, {
title: '接入时长',
dataIndex: 'connectedTime',
key: 'connectedTime',
render: (text, record) => {
return differTime(new Date(record['connectedTime']), new Date(record['disconnectedTime']));
}
},
{
title: '操作',
key: 'action',
render: (text, record) => {
return (
<div>
<Button type="link" size='small' icon={<PlaySquareTwoTone />} onClick={() => this.showPlayback(record.id)}>回放</Button>
<Button type="link" size='small' icon={<DeleteTwoTone />} onClick={() => {
confirm({
title: '您确定要删除此会话吗?',
content: '',
okText: '确定',
okType: 'danger',
cancelText: '取消',
onOk() {
del(record.id)
}
});
const del = async (id) => {
const result = await request.delete(`/sessions/${id}`);
if (result.code === 1) {
notification['success']({
message: '提示',
description: '删除成功',
});
this.loadTableData();
} else {
notification['error']({
message: '提示',
description: '删除失败 :( ' + result.message,
});
}
}
}}>删除</Button>
</div>
)
},
}
];
const selectedRowKeys = this.state.selectedRowKeys;
const rowSelection = {
selectedRowKeys: this.state.selectedRowKeys,
onChange: (selectedRowKeys, selectedRows) => {
this.setState({selectedRowKeys});
},
};
const hasSelected = selectedRowKeys.length > 0;
const userOptions = this.state.users.map(d => <Select.Option key={d.id}
value={d.id}>{d.nickname}</Select.Option>);
const assetOptions = this.state.assets.map(d => <Select.Option key={d.id}
value={d.id}>{d.name}</Select.Option>);
return (
<>
<PageHeader
className="site-page-header-ghost-wrapper page-herder"
title="离线会话"
breadcrumb={{
routes: routes,
itemRender: itemRender
}}
subTitle="离线会话管理"
>
</PageHeader>
<Content className="site-layout-background page-content">
<div style={{marginBottom: 20}}>
<Row justify="space-around" align="middle" gutter={24}>
<Col span={8} key={1}>
<Title level={3}>离线会话列表</Title>
</Col>
<Col span={16} key={2} style={{textAlign: 'right'}}>
<Space>
<Select
style={{width: 200}}
showSearch
value={this.state.queryParams.userId}
placeholder='用户昵称'
onSearch={this.handleSearchByNickname}
onChange={this.handleChangeByUserId}
filterOption={false}
>
{userOptions}
</Select>
<Search
ref={this.inputRefOfClientIp}
placeholder="来源IP"
allowClear
onSearch={this.handleSearchByClientIp}
/>
<Select
style={{width: 200}}
showSearch
value={this.state.queryParams.assetId}
placeholder='资产名称'
onSearch={this.handleSearchByAssetName}
onChange={this.handleChangeByAssetId}
filterOption={false}
>
{assetOptions}
</Select>
<Select onChange={this.handleChangeByProtocol}
value={this.state.queryParams.protocol ? this.state.queryParams.protocol : ''}
style={{width: 100}}>
<Select.Option value="">全部协议</Select.Option>
<Select.Option value="rdp">rdp</Select.Option>
<Select.Option value="ssh">ssh</Select.Option>
<Select.Option value="vnc">vnc</Select.Option>
<Select.Option value="telnet">telnet</Select.Option>
</Select>
<Tooltip title='重置查询'>
<Button icon={<UndoOutlined/>} onClick={() => {
this.inputRefOfClientIp.current.setValue('');
this.loadTableData({
pageIndex: 1,
pageSize: 10,
protocol: '',
userId: undefined,
assetId: undefined
})
}}>
</Button>
</Tooltip>
<Divider type="vertical"/>
<Tooltip title="刷新列表">
<Button icon={<SyncOutlined/>} onClick={() => {
this.loadTableData(this.state.queryParams)
}}>
</Button>
</Tooltip>
<Tooltip title="批量删除">
<Button type="primary" danger disabled={!hasSelected} icon={<DeleteOutlined/>}
loading={this.state.delBtnLoading}
onClick={() => {
const content = <div>
您确定要删除选中的<Text style={{color: '#1890FF'}}
strong>{this.state.selectedRowKeys.length}</Text>
</div>;
confirm({
icon: <ExclamationCircleOutlined/>,
content: content,
onOk: () => {
this.batchDelete()
},
onCancel() {
},
});
}}>
</Button>
</Tooltip>
</Space>
</Col>
</Row>
</div>
<Table rowSelection={rowSelection}
dataSource={this.state.items}
columns={columns}
position={'both'}
pagination={{
showSizeChanger: true,
current: this.state.queryParams.pageIndex,
pageSize: this.state.queryParams.pageSize,
onChange: this.handleChangPage,
total: this.state.total,
showTotal: total => `总计 ${total}`
}}
loading={this.state.loading}
/>
<Modal
title="会话回放"
visible={this.state.playbackVisible}
onCancel={this.hidePlayback}
width={window.innerWidth * 0.8}
footer={null}
destroyOnClose
>
<Playback sessionId={this.state.playbackSessionId}/>
</Modal>
</Content>
</>
);
}
}
export default OfflineSession;

View File

@ -0,0 +1,511 @@
import React, {Component} from 'react';
import "video-react/dist/video-react.css";
import {
Button,
Col,
Divider,
Input,
Layout,
Modal,
notification,
PageHeader,
Row,
Select,
Space,
Table,
Tag,
Tooltip,
Typography
} from "antd";
import qs from "qs";
import request from "../../common/request";
import {differTime, formatDate, itemRender} from "../../utils/utils";
import {message} from "antd/es";
import {PROTOCOL_COLORS} from "../../common/constants";
import {
ApiTwoTone,
DisconnectOutlined,
ExclamationCircleOutlined,
EyeTwoTone,
SyncOutlined,
UndoOutlined
} from "@ant-design/icons";
import Monitor from "../access/Monitor";
const confirm = Modal.confirm;
const {Content} = Layout;
const {Search} = Input;
const {Title, Text} = Typography;
const routes = [
{
path: '',
breadcrumbName: '首页',
},
{
path: 'onlineSession',
breadcrumbName: '在线会话',
}
];
class OnlineSession extends Component {
inputRefOfClientIp = React.createRef();
state = {
items: [],
total: 0,
queryParams: {
pageIndex: 1,
pageSize: 10,
protocol: '',
userId: undefined,
assetId: undefined
},
loading: false,
selectedRowKeys: [],
delBtnLoading: false,
users: [],
assets: [],
accessVisible: false,
sessionWidth: 1024,
sessionHeight: 768,
sessionProtocol: ''
};
componentDidMount() {
this.loadTableData();
this.handleSearchByNickname('');
this.handleSearchByAssetName('');
}
async loadTableData(queryParams) {
this.setState({
loading: true
});
queryParams = queryParams || this.state.queryParams;
queryParams['status'] = 'connected';
// queryParams
let paramsStr = qs.stringify(queryParams);
let data = {
items: [],
total: 0
};
try {
let result = await request.get('/sessions/paging?' + paramsStr);
if (result.code === 1) {
data = result.data;
} else {
message.error(result.message);
}
} catch (e) {
} finally {
const items = data.items.map(item => {
return {'key': item['id'], ...item}
})
this.setState({
items: items,
total: data.total,
queryParams: queryParams,
loading: false
});
}
}
handleChangPage = (pageIndex, pageSize) => {
let queryParams = this.state.queryParams;
queryParams.pageIndex = pageIndex;
queryParams.pageSize = pageSize;
this.setState({
queryParams: queryParams
});
this.loadTableData(queryParams)
};
handleSearchByClientIp = clientIp => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'clientIp': clientIp,
}
this.loadTableData(query);
}
handleChangeByProtocol = protocol => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'protocol': protocol,
}
this.loadTableData(query);
}
handleSearchByNickname = async nickname => {
const result = await request.get(`/users/paging?pageIndex=1&pageSize=100&nickname=${nickname}`);
if (result.code !== 1) {
message.error(result.message, 10);
return;
}
this.setState({
users: result.data.items
})
}
handleChangeByUserId = userId => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'userId': userId,
}
this.loadTableData(query);
}
handleSearchByAssetName = async assetName => {
const result = await request.get(`/assets/paging?pageIndex=1&pageSize=100&name=${assetName}`);
if (result.code !== 1) {
message.error(result.message, 10);
return;
}
this.setState({
assets: result.data.items
})
}
handleChangeByAssetId = (assetId, options) => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'assetId': assetId,
}
this.loadTableData(query);
}
batchDis = async () => {
this.setState({
delBtnLoading: true
})
try {
let result = await request.post('/sessions/' + this.state.selectedRowKeys.join(',') + '/discontent');
if (result.code === 1) {
message.success('操作成功', 3);
this.setState({
selectedRowKeys: []
})
await this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
}
} finally {
this.setState({
delBtnLoading: false
})
}
}
showMonitor = (record) => {
this.setState({
connectionId: record.connectionId,
sessionProtocol: record.protocol,
accessVisible: true,
sessionWidth: record.width,
sessionHeight: record.height,
sessionTitle: `${record.username}@${record.ip}:${record.port} ${record.width}x${record.height}`
})
}
render() {
const columns = [{
title: '序号',
dataIndex: 'id',
key: 'id',
render: (id, record, index) => {
return index + 1;
}
}, {
title: '用户昵称',
dataIndex: 'creatorName',
key: 'creatorName'
}, {
title: '来源IP',
dataIndex: 'clientIp',
key: 'clientIp'
}, {
title: '资产名称',
dataIndex: 'assetName',
key: 'assetName'
}, {
title: '远程连接',
dataIndex: 'access',
key: 'access',
render: (text, record) => {
return `${record.username}@${record.ip}:${record.port}`;
}
}, {
title: '屏幕大小',
dataIndex: 'screen',
key: 'screen',
render: (text, record) => {
return `${record.width}x${record.height}`;
}
}, {
title: '连接协议',
dataIndex: 'protocol',
key: 'protocol',
render: (text, record) => {
return (<Tag color={PROTOCOL_COLORS[text]}>{text}</Tag>);
}
}, {
title: '接入时间',
dataIndex: 'connectedTime',
key: 'connectedTime',
render: (text, record) => {
return formatDate(text, 'yyyy-MM-dd hh:mm:ss');
}
}, {
title: '接入时长',
dataIndex: 'connectedTime',
key: 'connectedTime',
render: (text, record) => {
return differTime(new Date(record['connectedTime']), new Date());
}
},
{
title: '操作',
key: 'action',
render: (text, record) => {
return (
<div>
<Button type="link" size='small' icon={<EyeTwoTone/>} onClick={() => {
this.showMonitor(record)
}}>监控</Button>
<Button type="link" size='small' icon={<ApiTwoTone/>} onClick={async () => {
confirm({
title: '您确定要断开此会话吗?',
content: '',
okText: '确定',
okType: 'danger',
cancelText: '取消',
onOk() {
dis(record.id)
}
});
const dis = async (id) => {
const result = await request.post(`/sessions/${id}/discontent`);
if (result.code === 1) {
notification['success']({
message: '提示',
description: '断开成功',
});
this.loadTableData();
} else {
notification['success']({
message: '提示',
description: '断开失败 :( ' + result.message,
});
}
}
}}>断开</Button>
</div>
)
},
}
];
const selectedRowKeys = this.state.selectedRowKeys;
const rowSelection = {
selectedRowKeys: selectedRowKeys,
onChange: (selectedRowKeys, selectedRows) => {
this.setState({selectedRowKeys});
},
};
const hasSelected = selectedRowKeys.length > 0;
const userOptions = this.state.users.map(d => <Select.Option key={d.id}
value={d.id}>{d.nickname}</Select.Option>);
const assetOptions = this.state.assets.map(d => <Select.Option key={d.id}
value={d.id}>{d.name}</Select.Option>);
return (
<>
<PageHeader
className="site-page-header-ghost-wrapper page-herder"
title="在线会话"
breadcrumb={{
routes: routes,
itemRender: itemRender
}}
subTitle="查询实时在线会话"
>
</PageHeader>
<Content className="site-layout-background page-content">
<div style={{marginBottom: 20}}>
<Row justify="space-around" align="middle" gutter={24}>
<Col span={8} key={1}>
<Title level={3}>在线会话列表</Title>
</Col>
<Col span={16} key={2} style={{textAlign: 'right'}}>
<Space>
<Select
style={{width: 200}}
showSearch
value={this.state.queryParams.userId}
placeholder='用户昵称'
onSearch={this.handleSearchByNickname}
onChange={this.handleChangeByUserId}
filterOption={false}
>
{userOptions}
</Select>
<Search
ref={this.inputRefOfClientIp}
placeholder="来源IP"
allowClear
onSearch={this.handleSearchByClientIp}
/>
<Select
style={{width: 200}}
showSearch
value={this.state.queryParams.assetId}
placeholder='资产名称'
onSearch={this.handleSearchByAssetName}
onChange={this.handleChangeByAssetId}
filterOption={false}
>
{assetOptions}
</Select>
<Select onChange={this.handleChangeByProtocol}
value={this.state.queryParams.protocol ? this.state.queryParams.protocol : ''}
style={{width: 100}}>
<Select.Option value="">全部协议</Select.Option>
<Select.Option value="rdp">rdp</Select.Option>
<Select.Option value="ssh">ssh</Select.Option>
<Select.Option value="vnc">vnc</Select.Option>
<Select.Option value="telnet">telnet</Select.Option>
</Select>
<Tooltip title='重置查询'>
<Button icon={<UndoOutlined/>} onClick={() => {
this.inputRefOfClientIp.current.setValue('');
this.loadTableData({
pageIndex: 1,
pageSize: 10,
protocol: '',
userId: undefined,
assetId: undefined
})
}}>
</Button>
</Tooltip>
<Divider type="vertical"/>
<Tooltip title="刷新列表">
<Button icon={<SyncOutlined/>} onClick={() => {
this.loadTableData(this.state.queryParams)
}}>
</Button>
</Tooltip>
<Tooltip title="批量断开">
<Button type="primary" danger disabled={!hasSelected}
icon={<DisconnectOutlined/>}
loading={this.state.delBtnLoading}
onClick={() => {
const content = <div>
您确定要断开选中的<Text style={{color: '#1890FF'}}
strong>{this.state.selectedRowKeys.length}</Text>
</div>;
confirm({
icon: <ExclamationCircleOutlined/>,
content: content,
onOk: () => {
this.batchDis()
},
onCancel() {
},
});
}}>
</Button>
</Tooltip>
</Space>
</Col>
</Row>
</div>
<Table rowSelection={rowSelection}
dataSource={this.state.items}
columns={columns}
position={'both'}
pagination={{
showSizeChanger: true,
current: this.state.queryParams.pageIndex,
pageSize: this.state.queryParams.pageSize,
onChange: this.handleChangPage,
total: this.state.total,
showTotal: total => `总计 ${total}`
}}
loading={this.state.loading}
/>
<Modal
className='monitor'
title={this.state.sessionTitle}
centered
visible={this.state.accessVisible}
footer={null}
width={window.innerWidth * 0.8}
height={window.innerWidth * 0.8 / this.state.sessionWidth * this.state.sessionHeight}
onCancel={() => {
this.setState({accessVisible: false})
}}
>
<Monitor connectionId={this.state.connectionId}
width={this.state.sessionWidth}
height={this.state.sessionHeight}
protocol={this.state.sessionProtocol}
rate={window.innerWidth * 0.8 / this.state.sessionWidth}>
</Monitor>
</Modal>
</Content>
</>
);
}
}
export default OnlineSession;

View File

@ -0,0 +1,173 @@
import React, {Component} from 'react';
import Guacamole from "guacamole-common-js";
class Playback extends Component {
componentDidMount() {
let sessionId = this.props.sessionId;
this.initPlayer(sessionId);
}
componentWillMount() {
}
initPlayer(sessionId) {
var RECORDING_URL = '/recording/' + sessionId + '.guac';
var player = document.getElementById('player');
var display = document.getElementById('display');
var playPause = document.getElementById('play-pause');
var position = document.getElementById('position');
var positionSlider = document.getElementById('position-slider');
var duration = document.getElementById('duration');
var tunnel = new Guacamole.StaticHTTPTunnel(RECORDING_URL);
var recording = new Guacamole.SessionRecording(tunnel);
var recordingDisplay = recording.getDisplay();
/**
* Converts the given number to a string, adding leading zeroes as necessary
* to reach a specific minimum length.
*
* @param {Numer} num
* The number to convert to a string.
*
* @param {Number} minLength
* The minimum length of the resulting string, in characters.
*
* @returns {String}
* A string representation of the given number, with leading zeroes
* added as necessary to reach the specified minimum length.
*/
var zeroPad = function zeroPad(num, minLength) {
// Convert provided number to string
var str = num.toString();
// Add leading zeroes until string is long enough
while (str.length < minLength)
str = '0' + str;
return str;
};
/**
* Converts the given millisecond timestamp into a human-readable string in
* MM:SS format.
*
* @param {Number} millis
* An arbitrary timestamp, in milliseconds.
*
* @returns {String}
* A human-readable string representation of the given timestamp, in
* MM:SS format.
*/
var formatTime = function formatTime(millis) {
// Calculate total number of whole seconds
var totalSeconds = Math.floor(millis / 1000);
// Split into seconds and minutes
var seconds = totalSeconds % 60;
var minutes = Math.floor(totalSeconds / 60);
// Format seconds and minutes as MM:SS
return zeroPad(minutes, 2) + ':' + zeroPad(seconds, 2);
};
// Add playback display to DOM
display.appendChild(recordingDisplay.getElement());
// Begin downloading the recording
recording.connect();
// If playing, the play/pause button should read "Pause"
recording.onplay = function () {
playPause.textContent = 'Pause';
};
// If paused, the play/pause button should read "Play"
recording.onpause = function () {
playPause.textContent = 'Play';
};
// Toggle play/pause when display or button are clicked
display.onclick = playPause.onclick = function () {
if (!recording.isPlaying())
recording.play();
else
recording.pause();
};
// Fit display within containing div
recordingDisplay.onresize = function displayResized(width, height) {
// Do not scale if display has no width
if (!width)
return;
// Scale display to fit width of container
recordingDisplay.scale(display.offsetWidth / width);
};
// Update slider and status when playback position changes
recording.onseek = function positionChanged(millis) {
position.textContent = formatTime(millis);
positionSlider.value = millis;
};
// Update slider and status when duration changes
recording.onprogress = function durationChanged(millis) {
duration.textContent = formatTime(millis);
positionSlider.max = millis;
};
// Seek within recording if slider is moved
positionSlider.onchange = function sliderPositionChanged() {
// Request seek
recording.seek(positionSlider.value, function seekComplete() {
// Seek has completed
player.className = '';
});
// Seek is in progress
player.className = 'seeking';
};
}
render() {
return (
<div>
<div id="player">
<div id="display">
<div className="notification-container">
<div className="seek-notification">
</div>
</div>
</div>
<div className="controls">
<button id="play-pause">Play</button>
<input id="position-slider" type="range"/>
<span id="position">00:00</span>
<span>/</span>
<span id="duration">00:00</span>
</div>
</div>
</div>
);
}
}
export default Playback;

View File

@ -0,0 +1,315 @@
import React, {Component} from 'react';
import {Layout, PageHeader, Switch, Select} from "antd";
import {itemRender} from '../../utils/utils'
import {Form, Input, Button, Checkbox} from "antd";
import request from "../../common/request";
import {message} from "antd/es";
const {Content} = Layout;
const {Option} = Select;
const routes = [
{
path: '',
breadcrumbName: '首页',
},
{
path: 'setting',
breadcrumbName: '系统设置',
}
];
const formItemLayout = {
labelCol: {span: 3},
wrapperCol: {span: 9},
};
const formTailLayout = {
labelCol: {span: 3},
wrapperCol: {span: 9, offset: 3},
};
class Setting extends Component {
state = {}
settingFormRef = React.createRef();
componentDidMount() {
this.getProperties();
}
changeProperties = async (values) => {
let result = await request.put('/properties', values);
if (result.code === 1) {
message.success('修改成功');
} else {
message.error(result.message);
}
}
getProperties = async () => {
// eslint-disable-next-line no-extend-native
String.prototype.bool = function() {
return (/^true$/i).test(this);
};
let result = await request.get('/properties');
if (result.code === 1) {
let properties = {}
for (let i = 0; i < result.data.length; i++) {
let item = result.data[i];
if (item['name'].startsWith('enable') ||
item['name'].startsWith('disable')) {
properties[item['name']] = item['value'].bool()
}else {
properties[item['name']] = item['value']
}
}
this.settingFormRef.current.setFieldsValue(properties)
} else {
message.error(result.message);
}
}
render() {
return (
<>
<PageHeader
className="site-page-header-ghost-wrapper page-herder"
title="系统设置"
breadcrumb={{
routes: routes,
itemRender: itemRender
}}
subTitle="系统设置"
>
</PageHeader>
<Content className="site-layout-background page-content">
<Form ref={this.settingFormRef} name="password" onFinish={this.changeProperties}>
<h3>Guacd 服务配置</h3>
<Form.Item
{...formItemLayout}
name="host"
label="监听地址"
rules={[
{
required: true,
message: '监听地址',
},
]}
>
<Input type='text' placeholder="请输入监听地址"/>
</Form.Item>
<Form.Item
{...formItemLayout}
name="port"
label="监听端口"
rules={[
{
required: true,
message: '监听端口',
min: 1,
max: 65535
},
]}
>
<Input type='number' placeholder="请输入监听端口"/>
</Form.Item>
<h3>远程桌面RDP配置</h3>
<Form.Item
{...formItemLayout}
name="enable-drive"
label="启用设备映射"
valuePropName="checked"
>
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
<Form.Item
{...formItemLayout}
name="drive-name"
label="设备名称"
rules={[
{
required: true,
message: '请输入设备名称',
},
]}
>
<Input type='text' placeholder="请输入设备名称"/>
</Form.Item>
<Form.Item
{...formItemLayout}
name="drive-path"
label="设备路径"
rules={[
{
required: true,
message: '请输入设备路径',
},
]}
>
<Input type='text' placeholder="请输入设备路径"/>
</Form.Item>
<Form.Item
{...formItemLayout}
name="enable-wallpaper"
label="启用桌面墙纸"
valuePropName="checked"
>
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
<Form.Item
{...formItemLayout}
name="enable-theming"
label="启用桌面主题"
valuePropName="checked"
>
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
<Form.Item
{...formItemLayout}
name="enable-font-smoothing"
label="启用字体平滑ClearType"
valuePropName="checked"
>
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
<Form.Item
{...formItemLayout}
name="enable-full-window-drag"
label="启用全窗口拖拽"
valuePropName="checked"
>
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
<Form.Item
{...formItemLayout}
name="enable-desktop-composition"
label="启用桌面合成效果Aero"
valuePropName="checked"
>
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
<Form.Item
{...formItemLayout}
name="enable-menu-animations"
label="启用菜单动画"
valuePropName="checked"
>
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
<Form.Item
{...formItemLayout}
name="disable-bitmap-caching"
label="禁用位图缓存"
valuePropName="checked"
>
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
<Form.Item
{...formItemLayout}
name="disable-offscreen-caching"
label="禁用离屏缓存"
valuePropName="checked"
>
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
<Form.Item
{...formItemLayout}
name="disable-glyph-caching"
label="禁用字形缓存"
valuePropName="checked"
>
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
<h3>SSH配置</h3>
<Form.Item
{...formItemLayout}
name="font-name"
label="字体名称"
rules={[
{
required: true,
message: '字体名称',
},
]}
>
<Input type='text' placeholder="请输入字体名称"/>
</Form.Item>
<Form.Item
{...formItemLayout}
name="font-size"
label="字体大小"
rules={[
{
required: true,
message: '字体大小',
},
]}
>
<Input type='number' placeholder="请输入字体大小"/>
</Form.Item>
<Form.Item
{...formItemLayout}
name="color-scheme"
label="颜色主题"
rules={[
{
required: true,
message: '颜色主题',
},
]}
initialValue="gray-black"
>
<Select style={{width: 120}} onChange={null}>
<Option value="gray-black">黑底灰字</Option>
<Option value="green-black">黑底绿字</Option>
<Option value="white-black">黑底白字</Option>
<Option value="black-white">白底黑字</Option>
</Select>
</Form.Item>
<Form.Item
{...formItemLayout}
name="enable-sftp"
label="启用SFTP"
valuePropName="checked"
>
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
<Form.Item {...formTailLayout}>
<Button type="primary" htmlType="submit">
提交
</Button>
</Form.Item>
</Form>
</Content>
</>
);
}
}
export default Setting;

View File

@ -0,0 +1,144 @@
import React, {Component} from 'react';
import {Layout, PageHeader} from "antd";
import {itemRender} from '../../utils/utils'
import {Form, Input, Button, Checkbox} from "antd";
import request from "../../common/request";
import {message} from "antd/es";
const {Content} = Layout;
const routes = [
{
path: '',
breadcrumbName: '首页',
},
{
path: 'info',
breadcrumbName: '个人中心',
}
];
const formItemLayout = {
labelCol: {span: 3},
wrapperCol: {span: 6},
};
const formTailLayout = {
labelCol: {span: 3},
wrapperCol: {span: 6, offset: 3},
};
class Info extends Component {
state = {}
passwordFormRef = React.createRef();
onNewPasswordChange(value) {
this.setState({
'newPassword': value.target.value
})
}
onNewPassword2Change = (value) => {
this.setState({
...this.validateNewPassword(value.target.value),
'newPassword2': value.target.value
})
}
validateNewPassword = (newPassword2) => {
if (newPassword2 === this.state.newPassword) {
return {
validateStatus: 'success',
errorMsg: null,
};
}
return {
validateStatus: 'error',
errorMsg: '两次输入的密码不一致',
};
}
changePassword = async (values) => {
let result = await request.post('/change-password', values);
if (result.code === 1) {
message.success('密码修改成功');
} else {
message.error(result.message);
}
}
render() {
return (
<>
<PageHeader
className="site-page-header-ghost-wrapper page-herder"
title="个人中心"
breadcrumb={{
routes: routes,
itemRender: itemRender
}}
subTitle="个人中心"
>
</PageHeader>
<Content className="site-layout-background page-content">
<h1>修改密码</h1>
<Form ref={this.passwordFormRef} name="password" onFinish={this.changePassword}>
<Form.Item
{...formItemLayout}
name="oldPassword"
label="原始密码"
rules={[
{
required: true,
message: '原始密码',
},
]}
>
<Input type='password' placeholder="请输入原始密码"/>
</Form.Item>
<Form.Item
{...formItemLayout}
name="newPassword"
label="新的密码"
rules={[
{
required: true,
message: '请输入新的密码',
},
]}
>
<Input type='password' placeholder="新的密码" onChange={(value) => this.onNewPasswordChange(value)}/>
</Form.Item>
<Form.Item
{...formItemLayout}
name="newPassword2"
label="确认密码"
rules={[
{
required: true,
message: '请和上面输入新的密码保持一致',
},
]}
validateStatus={this.state.validateStatus}
help={this.state.errorMsg || ''}
>
<Input type='password' placeholder="请和上面输入新的密码保持一致" onChange={(value) => this.onNewPassword2Change(value)}/>
</Form.Item>
<Form.Item {...formTailLayout}>
<Button type="primary" htmlType="submit">
提交
</Button>
</Form.Item>
</Form>
</Content>
</>
);
}
}
export default Info;

View File

@ -0,0 +1,431 @@
import React, {Component} from 'react';
import {itemRender} from '../../utils/utils'
import {
Badge,
Button,
Col,
Divider,
Input,
Layout,
Modal,
PageHeader,
Row,
Space,
Table,
Tooltip,
Typography,
} from "antd";
import qs from "qs";
import UserModal from "./UserModal";
import request from "../../common/request";
import {message} from "antd/es";
import {
DeleteOutlined, DeleteTwoTone,
EditTwoTone,
ExclamationCircleOutlined,
PlusOutlined,
SyncOutlined,
UndoOutlined
} from '@ant-design/icons';
const confirm = Modal.confirm;
const {Search} = Input;
const {Title, Text} = Typography;
const {Content} = Layout;
const routes = [
{
path: '',
breadcrumbName: '首页',
},
{
path: 'user',
breadcrumbName: '用户',
}
];
class User extends Component {
inputRefOfNickname = React.createRef();
inputRefOfUsername = React.createRef();
state = {
items: [],
total: 0,
queryParams: {
pageIndex: 1,
pageSize: 10
},
loading: false,
modalVisible: false,
modalTitle: '',
modalConfirmLoading: false,
model: null,
selectedRowKeys: [],
delBtnLoading: false,
};
componentDidMount() {
this.loadTableData();
}
async delete(id) {
let result = await request.delete('/users/' + id);
if (result.code === 1) {
message.success('操作成功', 3);
await this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
}
}
async loadTableData(queryParams) {
this.setState({
loading: true
});
queryParams = queryParams || this.state.queryParams;
let paramsStr = qs.stringify(queryParams);
let data = {
items: [],
total: 0
};
try {
let result = await request.get('/users/paging?' + paramsStr);
if (result.code === 1) {
data = result.data;
} else {
message.error(result.message, 10);
}
} catch (e) {
} finally {
const items = data.items.map(item => {
return {'key': item['id'], ...item}
})
this.setState({
items: items,
total: data.total,
queryParams: queryParams,
loading: false
});
}
}
handleChangPage = (pageIndex, pageSize) => {
let queryParams = this.state.queryParams;
queryParams.pageIndex = pageIndex;
queryParams.pageSize = pageSize;
this.setState({
queryParams: queryParams
});
this.loadTableData(queryParams).then(r => {})
};
showDeleteConfirm(id, content) {
let self = this;
confirm({
title: '您确定要删除此用户吗?',
content: content,
okText: '确定',
okType: 'danger',
cancelText: '取消',
onOk() {
self.delete(id);
}
});
};
showModal(title, user = {}) {
this.setState({
model: user,
modalVisible: true,
modalTitle: title
});
};
handleCancelModal = e => {
this.setState({
modalVisible: false,
modalTitle: ''
});
};
handleOk = async (formData) => {
// 弹窗 form 传来的数据
this.setState({
modalConfirmLoading: true
});
if (formData.id) {
// 向后台提交数据
const result = await request.put('/users/' + formData.id, formData);
if (result.code === 1) {
message.success('操作成功', 3);
this.setState({
modalVisible: false
});
await this.loadTableData(this.state.queryParams);
} else {
message.error('操作失败 :( ' + result.message, 10);
}
} else {
// 向后台提交数据
const result = await request.post('/users', formData);
if (result.code === 1) {
message.success('操作成功', 3);
this.setState({
modalVisible: false
});
await this.loadTableData(this.state.queryParams);
} else {
message.error('操作失败 :( ' + result.message, 10);
}
}
this.setState({
modalConfirmLoading: false
});
};
handleSearchByUsername = username => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'username': username,
}
this.loadTableData(query);
};
handleSearchByNickname = nickname => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'nickname': nickname,
}
this.loadTableData(query);
};
batchDelete = async () => {
this.setState({
delBtnLoading: true
})
try {
let result = await request.delete('/users/' + this.state.selectedRowKeys.join(','));
if (result.code === 1) {
message.success('操作成功', 3);
this.setState({
selectedRowKeys: []
})
await this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
}
} finally {
this.setState({
delBtnLoading: false
})
}
}
render() {
const columns = [{
title: '序号',
dataIndex: 'id',
key: 'id',
render: (id, record, index) => {
return index + 1;
}
}, {
title: '登录账号',
dataIndex: 'username',
key: 'username',
}, {
title: '用户昵称',
dataIndex: 'nickname',
key: 'nickname',
}, {
title: '在线状态',
dataIndex: 'online',
key: 'online',
render: text => {
if (text) {
return (<Badge status="success" text="在线"/>);
} else {
return (<Badge status="default" text="离线"/>);
}
}
}, {
title: '创建日期',
dataIndex: 'created',
key: 'created'
},
{
title: '操作',
key: 'action',
render: (text, record) => {
return (
<div>
<Button type="link" size='small' icon={<EditTwoTone/>}
onClick={() => this.showModal('更新用户', record)}>编辑</Button>
<Button type="link" size='small' icon={<DeleteTwoTone/>}
onClick={() => this.showDeleteConfirm(record.id, record.name)}>删除</Button>
</div>
)
},
}
];
const selectedRowKeys = this.state.selectedRowKeys;
const rowSelection = {
selectedRowKeys: this.state.selectedRowKeys,
onChange: (selectedRowKeys, selectedRows) => {
this.setState({selectedRowKeys});
},
};
const hasSelected = selectedRowKeys.length > 0;
return (
<>
<PageHeader
className="site-page-header-ghost-wrapper page-herder"
title="用户管理"
breadcrumb={{
routes: routes,
itemRender: itemRender
}}
subTitle="平台用户管理"
>
</PageHeader>
<Content className="site-layout-background page-content">
<div style={{marginBottom: 20}}>
<Row justify="space-around" align="middle" gutter={24}>
<Col span={8} key={1}>
<Title level={3}>用户列表</Title>
</Col>
<Col span={16} key={2} style={{textAlign: 'right'}}>
<Space>
<Search
ref={this.inputRefOfNickname}
placeholder="用户昵称"
allowClear
onSearch={this.handleSearchByNickname}
/>
<Search
ref={this.inputRefOfUsername}
placeholder="登录账号"
allowClear
onSearch={this.handleSearchByUsername}
/>
<Tooltip title='重置查询'>
<Button icon={<UndoOutlined/>} onClick={() => {
this.inputRefOfUsername.current.setValue('');
this.inputRefOfNickname.current.setValue('');
this.loadTableData({pageIndex: 1, pageSize: 10, protocol: ''})
}}>
</Button>
</Tooltip>
<Divider type="vertical"/>
<Tooltip title="新增">
<Button type="dashed" icon={<PlusOutlined/>}
onClick={() => this.showModal('新增用户', {})}>
</Button>
</Tooltip>
<Tooltip title="刷新列表">
<Button icon={<SyncOutlined/>} onClick={() => {
this.loadTableData(this.state.queryParams)
}}>
</Button>
</Tooltip>
<Tooltip title="批量删除">
<Button type="primary" danger disabled={!hasSelected} icon={<DeleteOutlined/>}
loading={this.state.delBtnLoading}
onClick={() => {
const content = <div>
您确定要删除选中的<Text style={{color: '#1890FF'}}
strong>{this.state.selectedRowKeys.length}</Text>
</div>;
confirm({
icon: <ExclamationCircleOutlined/>,
content: content,
onOk: () => {
this.batchDelete()
},
onCancel() {
},
});
}}>
</Button>
</Tooltip>
</Space>
</Col>
</Row>
</div>
<Table rowSelection={rowSelection}
dataSource={this.state.items}
columns={columns}
position={'both'}
pagination={{
showSizeChanger: true,
current: this.state.queryParams.pageIndex,
pageSize: this.state.queryParams.pageSize,
onChange: this.handleChangPage,
onShowSizeChange: this.handleChangPage,
total: this.state.total,
showTotal: total => `总计 ${total}`
}}
loading={this.state.loading}
/>
{
this.state.modalVisible ?
<UserModal
visible={this.state.modalVisible}
title={this.state.modalTitle}
handleOk={this.handleOk}
handleCancel={this.handleCancelModal}
confirmLoading={this.state.modalConfirmLoading}
model={this.state.model}
>
</UserModal>
: null
}
</Content>
</>
);
}
}
export default User;

View File

@ -0,0 +1,58 @@
import React from 'react';
import {Form, Input, Modal, Radio} from "antd/lib/index";
const UserModal = ({title, visible, handleOk, handleCancel, confirmLoading, model}) => {
const [form] = Form.useForm();
const formItemLayout = {
labelCol: {span: 6},
wrapperCol: {span: 14},
};
return (
<Modal
title={title}
visible={visible}
maskClosable={false}
onOk={() => {
form
.validateFields()
.then(values => {
form.resetFields();
handleOk(values);
})
.catch(info => {});
}}
onCancel={handleCancel}
confirmLoading={confirmLoading}
okText='确定'
cancelText='取消'
>
<Form form={form} {...formItemLayout} initialValues={model}>
<Form.Item name='id' noStyle>
<Input hidden={true}/>
</Form.Item>
<Form.Item label="登录账户" name='username' rules={[{required: true, message: '请输入登录账户'}]}>
<Input autoComplete="off" placeholder="请输入登录账户"/>
</Form.Item>
<Form.Item label="用户昵称" name='nickname' rules={[{required: true, message: '请输入用户昵称'}]}>
<Input placeholder="请输入用户昵称"/>
</Form.Item>
{
title.indexOf('新增') > -1 ?
(<Form.Item label="登录密码" name='password' rules={[{required: true, message: '请输入登录密码'}]}>
<Input type="password" autoComplete="new-password" placeholder="输入登录密码"/>
</Form.Item>) : null
}
</Form>
</Modal>
)
};
export default UserModal;

Binary file not shown.

14
web/src/index.css Normal file
View File

@ -0,0 +1,14 @@
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}

23
web/src/index.js Normal file
View File

@ -0,0 +1,23 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import zhCN from 'antd/es/locale-provider/zh_CN';
import {ConfigProvider} from 'antd';
import {HashRouter as Router} from "react-router-dom";
ReactDOM.render(
<ConfigProvider locale={zhCN}>
<Router>
<App/>
</Router>
</ConfigProvider>,
document.getElementById('root')
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

7
web/src/logo.svg Normal file
View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
<g fill="#61DAFB">
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
<circle cx="420.9" cy="296.5" r="45.7"/>
<path d="M520.5 78.1z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,7 @@
import request from "../common/request";
const treeNodeService = {
};
export default treeNodeService;

135
web/src/serviceWorker.js Normal file
View File

@ -0,0 +1,135 @@
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export function register(config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve asset; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA'
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl, config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onStateChange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}

145
web/src/utils/utils.js Normal file
View File

@ -0,0 +1,145 @@
import React from "react";
import {Link} from "react-router-dom";
export const sleep = function (ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
export const getToken = function () {
return localStorage.getItem('X-Auth-Token');
}
export const getHeaders = function () {
return {'X-Auth-Token': getToken()};
}
export const itemRender = function (route, params, routes, paths) {
const last = routes.indexOf(route) === routes.length - 1;
return last ? (
<span>{route.breadcrumbName}</span>
) : (
<Link to={paths.join('/')}>{route.breadcrumbName}</Link>
);
}
export const formatDate = function (time, format) {
let date = new Date(time);
let o = {
"M+": date.getMonth() + 1,
"d+": date.getDate(),
"h+": date.getHours(),
"m+": date.getMinutes(),
"s+": date.getSeconds(),
"q+": Math.floor((date.getMonth() + 3) / 3), //quarter
"S": date.getMilliseconds() //millisecond
};
if (/(y+)/.test(format)) {
format = format.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
}
for (let k in o) {
if (new RegExp("(" + k + ")").test(format)) {
format = format.replace(RegExp.$1, RegExp.$1.length === 1 ? o[k] : ("00" + o[k]).substr(("" + o[k]).length));
}
}
return format;
};
export const isLeapYear = function (year) {
return (year % 4 === 0 && year % 100 !== 0) || (year % 100 === 0 && year % 400 === 0);
};
export const groupBy = (list, fn) => {
const groups = {};
list.forEach(x => {
let groupKey = fn(x).toString();
groups[groupKey] = groups[groupKey] || [];
groups[groupKey].push(x);
});
return groups;
};
export const cloneObj = (obj, ignoreFields) => {
let str, newObj = obj.constructor === Array ? [] : {};
if (typeof obj !== 'object') {
return;
} else if (window.JSON) {
str = JSON.stringify(obj);
newObj = JSON.parse(str);
} else {
for (const i in obj) {
newObj[i] = typeof obj[i] === 'object' ? cloneObj(obj[i]) : obj[i];
}
}
return newObj;
};
export function download(url) {
let aElement = document.createElement('a');
aElement.setAttribute('download', '');
aElement.setAttribute('target', '_blank');
aElement.setAttribute('href', url);
aElement.click();
}
export function differTime(start, end) {
//总秒数
let millisecond = Math.floor((end.getTime() - start.getTime()) / 1000);
//总天数
let allDay = Math.floor(millisecond / (24 * 60 * 60));
//注意同getYear的区别
let startYear = start.getFullYear();
let currentYear = end.getFullYear();
//闰年个数
let leapYear = 0;
for (let i = startYear; i < currentYear; i++) {
if (isLeapYear(i)) {
leapYear++;
}
}
//年数
const year = Math.floor((allDay - leapYear * 366) / 365 + leapYear);
//天数
let day;
if (allDay > 366) {
day = (allDay - leapYear * 366) % 365;
} else {
day = allDay;
}
//取余数(秒)
const remainder = millisecond % (24 * 60 * 60);
//小时数
const hour = Math.floor(remainder / (60 * 60));
//分钟数
const minute = Math.floor(remainder % (60 * 60) / 60);
//秒数
const second = remainder - hour * 60 * 60 - minute * 60;
let show = '';
if (year > 0) {
show += year + '年';
}
if (day > 0) {
show += day + '天';
}
if (hour > 0) {
show += hour + '小时';
}
if (minute > 0) {
show += minute + '分钟';
}
if (second > 0) {
show += second + '秒';
}
return show;
}