Initial commit
This commit is contained in:
95
web/src/App.css
Normal file
95
web/src/App.css
Normal 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
241
web/src/App.js
Normal 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 ?
|
||||
|
||||
<> <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
9
web/src/App.test.js
Normal 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);
|
||||
});
|
20
web/src/common/constants.js
Normal file
20
web/src/common/constants.js
Normal 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
115
web/src/common/request.js
Normal 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
|
21
web/src/components/Login.css
Normal file
21
web/src/components/Login.css
Normal 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;
|
||||
}
|
||||
|
78
web/src/components/Login.js
Normal file
78
web/src/components/Login.js
Normal 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);
|
3
web/src/components/access/Access.css
Normal file
3
web/src/components/access/Access.css
Normal file
@ -0,0 +1,3 @@
|
||||
.container div {
|
||||
margin: 0 auto;
|
||||
}
|
889
web/src/components/access/Access.js
Normal file
889
web/src/components/access/Access.js
Normal 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>
|
||||
远程文件管理
|
||||
|
||||
|
||||
<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;
|
3
web/src/components/access/Console.css
Normal file
3
web/src/components/access/Console.css
Normal file
@ -0,0 +1,3 @@
|
||||
.console-card .ant-card-body {
|
||||
padding: 0;
|
||||
}
|
110
web/src/components/access/Console.js
Normal file
110
web/src/components/access/Console.js
Normal 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;
|
237
web/src/components/access/Monitor.js
Normal file
237
web/src/components/access/Monitor.js
Normal 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;
|
512
web/src/components/asset/Asset.js
Normal file
512
web/src/components/asset/Asset.js
Normal 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;
|
157
web/src/components/asset/AssetModal.js
Normal file
157
web/src/components/asset/AssetModal.js
Normal 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;
|
92
web/src/components/command/BatchCommand.js
Normal file
92
web/src/components/command/BatchCommand.js
Normal 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;
|
501
web/src/components/command/DynamicCommand.js
Normal file
501
web/src/components/command/DynamicCommand.js
Normal 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;
|
59
web/src/components/command/DynamicCommandModal.js
Normal file
59
web/src/components/command/DynamicCommandModal.js
Normal 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;
|
400
web/src/components/credential/Credential.js
Normal file
400
web/src/components/credential/Credential.js
Normal 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;
|
59
web/src/components/credential/CredentialModal.js
Normal file
59
web/src/components/credential/CredentialModal.js
Normal 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;
|
4
web/src/components/dashboard/Dashboard.css
Normal file
4
web/src/components/dashboard/Dashboard.css
Normal file
@ -0,0 +1,4 @@
|
||||
.text-center{
|
||||
width: 100px;
|
||||
text-align: center;
|
||||
}
|
196
web/src/components/dashboard/Dashboard.js
Normal file
196
web/src/components/dashboard/Dashboard.js
Normal 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;
|
499
web/src/components/session/OfflineSession.js
Normal file
499
web/src/components/session/OfflineSession.js
Normal 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;
|
511
web/src/components/session/OnlineSession.js
Normal file
511
web/src/components/session/OnlineSession.js
Normal 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;
|
173
web/src/components/session/Playback.js
Normal file
173
web/src/components/session/Playback.js
Normal 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;
|
315
web/src/components/setting/Setting.js
Normal file
315
web/src/components/setting/Setting.js
Normal 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;
|
144
web/src/components/user/Info.js
Normal file
144
web/src/components/user/Info.js
Normal 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;
|
431
web/src/components/user/User.js
Normal file
431
web/src/components/user/User.js
Normal 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;
|
58
web/src/components/user/UserModal.js
Normal file
58
web/src/components/user/UserModal.js
Normal 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;
|
BIN
web/src/fonts/Menlo/Menlo-Italic-4.ttf
Normal file
BIN
web/src/fonts/Menlo/Menlo-Italic-4.ttf
Normal file
Binary file not shown.
14
web/src/index.css
Normal file
14
web/src/index.css
Normal 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
23
web/src/index.js
Normal 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
7
web/src/logo.svg
Normal 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 |
7
web/src/service/TreeNodeService.js
Normal file
7
web/src/service/TreeNodeService.js
Normal file
@ -0,0 +1,7 @@
|
||||
import request from "../common/request";
|
||||
|
||||
const treeNodeService = {
|
||||
|
||||
|
||||
};
|
||||
export default treeNodeService;
|
135
web/src/serviceWorker.js
Normal file
135
web/src/serviceWorker.js
Normal 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
145
web/src/utils/utils.js
Normal 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;
|
||||
}
|
Reference in New Issue
Block a user