release v1.2.0

This commit is contained in:
dushixiang
2021-10-31 17:15:35 +08:00
parent 4665ab6f78
commit 6132a05786
173 changed files with 37928 additions and 9349 deletions

View File

@ -11,8 +11,7 @@
}
.logo {
height: 32px;
margin: 16px;
margin: 24px;
text-align: center;
}
@ -35,33 +34,26 @@
.layout-header {
height: 60px;
align-items: center;
padding: 0 16px 0 0;
background: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, .08);
position: relative;
display: flex;
}
.layout-header-left {
flex: 1 1 0;
}
.layout-header-right {
align-items: center;
padding: 0 12px;
cursor: pointer;
transition: all .3s;
line-height: 60px;
height: 60px;
text-align: right;
margin-right: 12px;
}
.layout-header-right-item {
margin: 0 6px;
display: inline;
height: 60px;
}
.nickname {
line-height: 60px;
height: 60px;
width: 125px;
text-align: left;
padding: 0 5px;
float: right;
cursor: pointer;
display: inline-flex;
align-items: center;
padding: 0 8px;
}
.page-herder {
@ -85,7 +77,10 @@
.page-content {
margin: 16px;
padding: 24px;
min-height: 280px;
}
.page-content-user {
padding: 24px;
}
.page-card {
@ -114,4 +109,70 @@
.modal-no-padding .ant-modal-body {
padding: 0;
}
.disabled-icon {
cursor: not-allowed;
color: #ccc;
}
.disabled-icon:hover {
color: #ccc;
}
.km-header {
color: white;
width: 80%;
margin: 0 auto;
position: relative;
display: flex;
align-items: center;
height: 100%;
padding: 0 16px;
}
.km-header-logo {
font-size: 18px;
/*font-weight: 500;*/
font-weight: bold;
cursor: pointer;
color: white;
}
.km-header-right {
text-align: left;
height: 100%;
margin: 0 8px;
}
.km-header-right {
text-align: right;
height: 100%;
margin: 0 8px;
}
.km-header-right-item {
cursor: pointer;
/*padding: 23px 12px;*/
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 16px;
vertical-align: middle;
height: 100%;
}
.km-container {
width: 80%;
margin: 0 auto;
}
.kd-content {
margin-top: 20px;
background-color: white;
}
.kd-page-header {
background-color: white;
margin-top: 20px;
}

View File

@ -1,7 +1,7 @@
import React, {Component} from 'react';
import 'antd/dist/antd.css';
import './App.css';
import {Col, Divider, Dropdown, Layout, Menu, Popconfirm, Row, Tooltip} from "antd";
import {Button, Dropdown, Layout, Menu, Popconfirm} from "antd";
import {Link, Route, Switch} from "react-router-dom";
import Dashboard from "./components/dashboard/Dashboard";
import Asset from "./components/asset/Asset";
@ -13,6 +13,7 @@ import Login from "./components/Login";
import DynamicCommand from "./components/command/DynamicCommand";
import Credential from "./components/credential/Credential";
import {
ApiOutlined,
AuditOutlined,
BlockOutlined,
CloudServerOutlined,
@ -22,20 +23,21 @@ import {
DesktopOutlined,
DisconnectOutlined,
DownOutlined,
GithubOutlined,
FolderOutlined, GithubOutlined,
HddOutlined,
IdcardOutlined,
InsuranceOutlined,
LinkOutlined,
LoginOutlined,
LogoutOutlined,
QuestionCircleOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
SafetyCertificateOutlined,
SettingOutlined,
SolutionOutlined,
TeamOutlined,
UserOutlined,
UserSwitchOutlined,
MenuUnfoldOutlined,
MenuFoldOutlined,
UserSwitchOutlined
} from '@ant-design/icons';
import Info from "./components/user/Info";
import request from "./common/request";
@ -50,8 +52,13 @@ import Term from "./components/access/Term";
import Job from "./components/devops/Job";
import {Header} from "antd/es/layout/layout";
import Security from "./components/devops/Security";
import Storage from "./components/devops/Storage";
import MyFile from "./components/asset/MyFile";
import Strategy from "./components/user/Strategy";
import AccessGateway from "./components/asset/AccessGateway";
import MyAsset from "./components/asset/MyAsset";
const {Footer, Sider} = Layout;
const {Footer, Content, Sider} = Layout;
const {SubMenu} = Menu;
const headerHeight = 60;
@ -136,18 +143,9 @@ class App extends Component {
<Menu.Item>
<Link to={'/info'}>
<SolutionOutlined/>
个人中心
<SolutionOutlined/> 个人中心
</Link>
</Menu.Item>
<Menu.Item>
<a target='_blank' rel="noreferrer" href='https://github.com/dushixiang/next-terminal'>
<GithubOutlined/>
点个Star
</a>
</Menu.Item>
<Menu.Divider/>
<Menu.Item>
@ -160,8 +158,7 @@ class App extends Component {
cancelText="取消"
placement="left"
>
<LogoutOutlined/>
退出登录
<LogoutOutlined/> 退出登录
</Popconfirm>
</Menu.Item>
@ -178,58 +175,59 @@ class App extends Component {
<Route path="/">
<Layout className="layout" style={{minHeight: '100vh'}}>
<Sider collapsible collapsed={this.state.collapsed} trigger={null}>
<div className="logo">
<img src='logo.svg' alt='logo'/>
{
!this.state.collapsed ?
{
isAdmin() ?
<>
<Sider collapsible collapsed={this.state.collapsed} trigger={null}>
<div className="logo">
<img
src='data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxwYXRoIGQ9Ik0yNzIgMTIyLjI0aDQ4MHYxNTcuMDU2aDk2Vi40NDhoLTk2TDI3MiAwYy01Mi44IDAtOTYgLjQ0OC05NiAuNDQ4djI3OC44NDhoOTZ2LTE1Ny4xMnptNDAzLjY0OCA2MDMuMzkyTDg5NiA1MTIgNjc1LjY0OCAyOTguMzY4IDYwOCAzNjQuNDggNzYwLjEyOCA1MTIgNjA4IDY1OS41Mmw2Ny42NDggNjYuMTEyek00MTYgNjU5LjUyTDI2My44MDggNTEyIDQxNiAzNjQuNDhsLTY3LjcxMi02Ni4xMTJMMTI4IDUxMmwyMjAuMjg4IDIxMy42MzJMNDE2IDY1OS41MnptMzM2IDI0Mi4zMDRIMjcydi0xNTcuMTJoLTk2VjEwMjRoNjcyVjc0NC43MDRoLTk2djE1Ny4xMnoiIGZpbGw9IiNmZmYiLz48L3N2Zz4='
alt='logo'/>
{
!this.state.collapsed ?
<>&nbsp;<h1>Next Terminal</h1></> :
null
}
</div>
<>&nbsp;<h1>Next Terminal</h1></> :
null
}
</div>
<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'}}>
<Divider/>
<Menu.Item key="dashboard" icon={<DashboardOutlined/>}>
<Link to={'/'}>
控制面板
</Link>
</Menu.Item>
<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'}}>
<SubMenu key='resource' title='资源管理' icon={<CloudServerOutlined/>}>
<Menu.Item key="asset" icon={<DesktopOutlined/>}>
<Link to={'/asset'}>
资产列表
</Link>
</Menu.Item>
<Menu.Item key="credential" icon={<IdcardOutlined/>}>
<Link to={'/credential'}>
授权凭证
</Link>
</Menu.Item>
<Menu.Item key="dynamic-command" icon={<CodeOutlined/>}>
<Link to={'/dynamic-command'}>
动态指令
</Link>
</Menu.Item>
<Menu.Item key="access-gateway" icon={<ApiOutlined/>}>
<Link to={'/access-gateway'}>
接入网关
</Link>
</Menu.Item>
</SubMenu>
<Menu.Item key="dashboard" icon={<DashboardOutlined/>}>
<Link to={'/'}>
控制面板
</Link>
</Menu.Item>
<SubMenu key='resource' title='资源管理' icon={<CloudServerOutlined/>}>
<Menu.Item key="asset" icon={<DesktopOutlined/>}>
<Link to={'/asset'}>
资产列表
</Link>
</Menu.Item>
<Menu.Item key="credential" icon={<IdcardOutlined/>}>
<Link to={'/credential'}>
授权凭证
</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>
</SubMenu>
{
this.state.triggerMenu && isAdmin() ?
<>
<SubMenu key='audit' title='会话审计' icon={<AuditOutlined/>}>
<Menu.Item key="online-session" icon={<LinkOutlined/>}>
<Link to={'/online-session'}>
@ -243,9 +241,7 @@ class App extends Component {
</Link>
</Menu.Item>
</SubMenu>
<SubMenu key='ops' title='系统运维' icon={<ControlOutlined/>}>
<Menu.Item key="login-log" icon={<LoginOutlined/>}>
<Link to={'/login-log'}>
登录日志
@ -263,9 +259,14 @@ class App extends Component {
访问安全
</Link>
</Menu.Item>
<Menu.Item key="storage" icon={<HddOutlined/>}>
<Link to={'/storage'}>
磁盘空间
</Link>
</Menu.Item>
</SubMenu>
<SubMenu key='user-group' title='用户管理' icon={<UserSwitchOutlined/>}>
<SubMenu key='user-manage' title='用户管理' icon={<UserSwitchOutlined/>}>
<Menu.Item key="user" icon={<UserOutlined/>}>
<Link to={'/user'}>
用户管理
@ -276,86 +277,134 @@ class App extends Component {
用户组管理
</Link>
</Menu.Item>
<Menu.Item key="strategy" icon={<InsuranceOutlined/>}>
<Link to={'/strategy'}>
授权策略
</Link>
</Menu.Item>
</SubMenu>
</> : undefined
}
<Menu.Item key="info" icon={<SolutionOutlined/>}>
<Link to={'/info'}>
个人中心
</Link>
</Menu.Item>
{
this.state.triggerMenu && isAdmin() ?
<>
<Menu.Item key="my-file" icon={<FolderOutlined/>}>
<Link to={'/my-file'}>
我的文件
</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>
</> : undefined
}
</Menu>
</Sider>
</Menu>
</Sider>
<Layout className="site-layout">
<Header className="site-layout-background"
style={{padding: 0, height: headerHeight, zIndex: 20}}>
<div className='layout-header'>
<Row justify="space-around" align="middle" gutter={24} style={{height: headerHeight}}>
<Col span={4} key={1} style={{height: headerHeight}}>
{React.createElement(this.state.collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
className: 'trigger',
onClick: this.onCollapse,
})}
</Col>
<Col span={20} key={2} style={{textAlign: 'right'}}
className={'layout-header-right'}>
<div className={'layout-header-right-item'}>
<Tooltip placement="bottom" title={'使用帮助'}>
<a target='_blank' rel="noreferrer"
href='https://github.com/dushixiang/next-terminal/blob/master/docs/faq.md'>
<QuestionCircleOutlined/>
</a>
</Tooltip>
</div>
<Dropdown overlay={menu}>
<div className='nickname layout-header-right-item'>
{getCurrentUser()['nickname']} &nbsp;<DownOutlined/>
<Layout className="site-layout">
<Header className="site-layout-background"
style={{padding: 0, height: headerHeight, zIndex: 20}}>
<div className='layout-header'>
<div className='layout-header-left'>
{React.createElement(this.state.collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, {
className: 'trigger',
onClick: this.onCollapse,
})}
</div>
</Dropdown>
</Col>
</Row>
</div>
</Header>
<div className='layout-header-right'>
<div className={'layout-header-right-item'}>
<a style={{color: 'black'}} target='_blank' href='https://github.com/dushixiang/next-terminal'>
<GithubOutlined />
</a>
</div>
</div>
<Route path="/" exact component={Dashboard}/>
<Route path="/user" component={User}/>
<Route path="/user-group" component={UserGroup}/>
<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="/login-log" component={LoginLog}/>
<Route path="/info" component={Info}/>
<Route path="/setting" component={Setting}/>
<Route path="/job" component={Job}/>
<Route path="/access-security" component={Security}/>
<div className='layout-header-right'>
<Dropdown overlay={menu}>
<div className='nickname layout-header-right-item'>
{getCurrentUser()['nickname']} &nbsp;<DownOutlined/>
</div>
</Dropdown>
</div>
</div>
</Header>
<Route path="/" exact component={Dashboard}/>
<Route path="/user" component={User}/>
<Route path="/user-group" component={UserGroup}/>
<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="/login-log" component={LoginLog}/>
<Route path="/info" component={Info}/>
<Route path="/setting" component={Setting}/>
<Route path="/job" component={Job}/>
<Route path="/access-security" component={Security}/>
<Route path="/access-gateway" component={AccessGateway}/>
<Route path="/my-file" component={MyFile}/>
<Route path="/storage" component={Storage}/>
<Route path="/strategy" component={Strategy}/>
<Footer style={{textAlign: 'center'}}>
Next Terminal ©2021 dushixiang Version:{this.state.package['version']}
</Footer>
</Layout>
</> :
<>
<Header style={{padding: 0}}>
<div className='km-header'>
<div style={{flex: '1 1 0%'}}>
<Link to={'/'}>
<img
style={{paddingBottom: 4, marginRight: 5}}
src='data:image/svg+xml;base64,PHN2ZyBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiPjxwYXRoIGQ9Ik0yNzIgMTIyLjI0aDQ4MHYxNTcuMDU2aDk2Vi40NDhoLTk2TDI3MiAwYy01Mi44IDAtOTYgLjQ0OC05NiAuNDQ4djI3OC44NDhoOTZ2LTE1Ny4xMnptNDAzLjY0OCA2MDMuMzkyTDg5NiA1MTIgNjc1LjY0OCAyOTguMzY4IDYwOCAzNjQuNDggNzYwLjEyOCA1MTIgNjA4IDY1OS41Mmw2Ny42NDggNjYuMTEyek00MTYgNjU5LjUyTDI2My44MDggNTEyIDQxNiAzNjQuNDhsLTY3LjcxMi02Ni4xMTJMMTI4IDUxMmwyMjAuMjg4IDIxMy42MzJMNDE2IDY1OS41MnptMzM2IDI0Mi4zMDRIMjcydi0xNTcuMTJoLTk2VjEwMjRoNjcyVjc0NC43MDRoLTk2djE1Ny4xMnoiIGZpbGw9IiNmZmYiLz48L3N2Zz4='
alt='logo'/>
<span className='km-header-logo'>Next Terminal</span>
</Link>
<Link to={'/my-file'}>
<Button type="text" style={{color: 'white'}}
icon={<FolderOutlined/>}>
文件
</Button>
</Link>
<Link to={'/dynamic-command'}>
<Button type="text" style={{color: 'white'}}
icon={<CodeOutlined/>}>
指令
</Button>
</Link>
</div>
<div className='km-header-right'>
<Dropdown overlay={menu}>
<span className={'km-header-right-item'}>
{getCurrentUser()['nickname']}
</span>
</Dropdown>
</div>
</div>
</Header>
<Content className='km-container'>
<Layout>
<Route path="/" exact component={MyAsset}/>
<Content className={'kd-content'}>
<Route path="/info" component={Info}/>
<Route path="/my-file" component={MyFile}/>
<Route path="/dynamic-command" component={DynamicCommand}/>
</Content>
</Layout>
</Content>
<Footer style={{textAlign: 'center'}}>
Next Terminal ©2021 dushixiang Version:{this.state.package['version']}
</Footer>
</>
}
<Footer style={{textAlign: 'center'}}>
Next Terminal ©2021 dushixiang Version:{this.state.package['version']}
</Footer>
</Layout>
</Layout>
</Route>

View File

@ -4,4 +4,10 @@ export const PROTOCOL_COLORS = {
'telnet': 'geekblue',
'vnc': 'purple',
'kubernetes': 'volcano'
}
export const MODE_COLORS = {
'guacd': 'green',
'naive': 'orange',
'terminal': 'purple',
}

View File

@ -1,20 +1,24 @@
import React, {Component} from 'react';
import Guacamole from 'guacamole-common-js';
import {Affix, Button, Col, Drawer, Dropdown, Form, Input, Menu, message, Modal, Row} from 'antd'
import {Affix, Button, Drawer, Dropdown, Form, Input, Menu, message, Modal, Tooltip} from 'antd'
import qs from "qs";
import request from "../../common/request";
import {wsServer} from "../../common/env";
import {
AppstoreTwoTone,
CopyTwoTone,
DesktopOutlined,
CodeOutlined,
CopyOutlined,
ExclamationCircleOutlined,
ExpandOutlined
ExpandOutlined,
FolderOutlined,
LineChartOutlined,
WindowsOutlined
} from '@ant-design/icons';
import {exitFull, getToken, isEmpty, requestFullScreen} from "../../utils/utils";
import './Access.css'
import Draggable from 'react-draggable';
import FileSystem from "./FileSystem";
import FileSystem from "../devops/FileSystem";
import Stats from "./Stats";
import {Base64} from "js-base64";
const {TextArea} = Input;
@ -28,8 +32,11 @@ const STATE_DISCONNECTED = 5;
class Access extends Component {
clipboardFormRef = React.createRef();
statsRef = undefined;
error = false;
state = {
session: {},
sessionId: '',
client: {},
clientState: STATE_IDLE,
@ -48,7 +55,9 @@ class Access extends Component {
startTime: new Date(),
fullScreen: false,
fullScreenBtnText: '进入全屏',
sink: undefined
sink: undefined,
commands: [],
showFileSystem: false
};
async componentDidMount() {
@ -57,14 +66,20 @@ class Access extends Component {
let assetId = urlParams.get('assetId');
document.title = urlParams.get('assetName');
let protocol = urlParams.get('protocol');
let sessionId = await this.createSession(assetId);
let session = await this.createSession(assetId);
if (!session) {
return;
}
let sessionId = session['id'];
if (isEmpty(sessionId)) {
return;
}
this.setState({
session: session,
sessionId: sessionId,
protocol: protocol
protocol: protocol,
showFileSystem: session['fileSystem'] === '1'
});
this.renderDisplay(sessionId, protocol);
@ -130,6 +145,17 @@ class Access extends Component {
}
}
getCommands = async () => {
let result = await request.get('/commands');
if (result.code !== 1) {
message.error(result.message);
return;
}
this.setState({
commands: result['data']
})
}
onClientStateChange = (state) => {
this.setState({
clientState: state
@ -154,11 +180,18 @@ class Access extends Component {
// 向后台发送请求,更新会话的状态
this.updateSessionStatus(this.state.sessionId).then(_ => {
})
if (this.state.protocol === 'ssh') {
// 加载指令
this.getCommands();
}
break;
case STATE_DISCONNECTING:
break;
case STATE_DISCONNECTED:
if (!this.error) {
this.showMessage('连接已关闭');
}
break;
default:
@ -167,7 +200,7 @@ class Access extends Component {
};
onError = (status) => {
this.error = true;
console.log('通道异常。', status);
switch (status.code) {
@ -175,7 +208,7 @@ class Access extends Component {
this.showMessage('未支持的访问');
break;
case 512:
this.showMessage('远程服务异常');
this.showMessage('远程服务异常,请检查目标设备能否正常访问。');
break;
case 513:
this.showMessage('服务器忙碌');
@ -196,11 +229,7 @@ class Access extends Component {
this.showMessage('资源已关闭');
break;
case 519:
if (new Date().getTime() - this.state.startTime.getTime() <= 1000 * 30) {
this.showMessage('认证失败');
} else {
this.showMessage('远程服务未找到');
}
this.showMessage('远程服务未找到');
break;
case 520:
this.showMessage('远程服务不可用');
@ -236,13 +265,19 @@ class Access extends Component {
this.showMessage('会话不存在');
break;
case 801:
this.showMessage('创建隧道失败');
this.showMessage('创建隧道失败请检查Guacd服务是否正常。');
break;
case 802:
this.showMessage('管理员强制关闭了此会话');
break;
default:
this.showMessage('未知错误。');
if (status.message) {
// guacd 无法处理中文字符所以进行了base64编码。
this.showMessage(Base64.decode(status.message));
} else {
this.showMessage('未知错误。');
}
}
};
@ -270,10 +305,7 @@ class Access extends Component {
// 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;
@ -281,7 +313,6 @@ class Access extends Component {
// Set clipboard contents once stream is finished
reader.onend = async () => {
// message.info('您选择的内容已复制到您的粘贴板中,在右侧的输入框中可同时查看到。');
this.setState({
clipboardText: data
@ -335,18 +366,16 @@ class Access extends Component {
fullScreenBtnText: '退出全屏'
})
}
if (this.state.sink) {
this.state.sink.focus();
}
this.focus();
}
async createSession(assetsId) {
let result = await request.post(`/sessions?assetId=${assetsId}&mode=guacd`);
if (result['code'] !== 1) {
this.showMessage(result['message']);
return null;
return undefined;
}
return result['data']['id'];
return result['data'];
}
async renderDisplay(sessionId, protocol) {
@ -517,6 +546,12 @@ class Access extends Component {
}
};
focus = () => {
if (this.state.sink) {
this.state.sink.focus();
}
}
sendCombinationKey = (keys) => {
for (let i = 0; i < keys.length; i++) {
this.state.client.sendKeyEvent(1, keys[i]);
@ -526,9 +561,23 @@ class Access extends Component {
}
}
writeCommand = (command) => {
let client = this.state.client;
let outputStream = client.createPipeStream('text/plain', 'STDIN');
// Wrap output stream in writer
let writer = new Guacamole.StringWriter(outputStream);
writer.sendText(command);
writer.sendEnd();
this.focus();
}
onRef = (statsRef) => {
this.statsRef = statsRef;
}
render() {
const menu = (
const hotKeyMenu = (
<Menu>
<Menu.Item
onClick={() => this.sendCombinationKey(['65507', '65513', '65535'])}>Ctrl+Alt+Delete</Menu.Item>
@ -547,6 +596,20 @@ class Access extends Component {
</Menu>
);
const cmdMenuItems = this.state.commands.map(item => {
return <Tooltip placement="left" title={item['content']} color='blue' key={'t-' + item['id']}>
<Menu.Item onClick={() => {
this.writeCommand(item['content'])
}} key={'i-' + item['id']}>{item['name']}</Menu.Item>
</Tooltip>;
});
const cmdMenu = (
<Menu>
{cmdMenuItems}
</Menu>
);
return (
<div>
@ -559,7 +622,7 @@ class Access extends Component {
</div>
<Draggable>
<Affix style={{position: 'absolute', top: 50, right: 100}}>
<Affix style={{position: 'absolute', top: 50, right: 50}}>
<Button icon={<ExpandOutlined/>} disabled={this.state.clientState !== STATE_CONNECTED}
onClick={() => {
this.fullScreen();
@ -568,8 +631,8 @@ class Access extends Component {
</Draggable>
<Draggable>
<Affix style={{position: 'absolute', top: 50, right: 150}}>
<Button icon={<CopyTwoTone/>} disabled={this.state.clientState !== STATE_CONNECTED}
<Affix style={{position: 'absolute', top: 50, right: 100}}>
<Button icon={<CopyOutlined/>} disabled={this.state.clientState !== STATE_CONNECTED}
onClick={() => {
this.setState({
clipboardVisible: true
@ -579,11 +642,25 @@ class Access extends Component {
</Draggable>
{
this.state.protocol === 'rdp' ?
this.state.protocol === 'vnc' ?
<>
<Draggable>
<Affix style={{position: 'absolute', top: 100, right: 100}}>
<Button icon={<AppstoreTwoTone/>}
<Dropdown overlay={hotKeyMenu} trigger={['click']} placement="bottomLeft">
<Button icon={<WindowsOutlined/>}
disabled={this.state.clientState !== STATE_CONNECTED}/>
</Dropdown>
</Affix>
</Draggable>
</> : undefined
}
{
this.state.protocol === 'rdp' && this.state.showFileSystem ?
<>
<Draggable>
<Affix style={{position: 'absolute', top: 100, right: 50}}>
<Button icon={<FolderOutlined/>}
disabled={this.state.clientState !== STATE_CONNECTED} onClick={() => {
this.setState({
fileSystemVisible: true,
@ -591,11 +668,16 @@ class Access extends Component {
}}/>
</Affix>
</Draggable>
</> : undefined
}
{
this.state.protocol === 'rdp' ?
<>
<Draggable>
<Affix style={{position: 'absolute', top: 100, right: 150}}>
<Dropdown overlay={menu} trigger={['click']} placement="bottomLeft">
<Button icon={<DesktopOutlined/>}
<Affix style={{position: 'absolute', top: 100, right: 100}}>
<Dropdown overlay={hotKeyMenu} trigger={['click']} placement="bottomLeft">
<Button icon={<WindowsOutlined/>}
disabled={this.state.clientState !== STATE_CONNECTED}/>
</Dropdown>
</Affix>
@ -607,8 +689,8 @@ class Access extends Component {
this.state.protocol === 'ssh' ?
<>
<Draggable>
<Affix style={{position: 'absolute', top: 100, right: 100}}>
<Button icon={<AppstoreTwoTone/>}
<Affix style={{position: 'absolute', top: 100, right: 50}}>
<Button icon={<FolderOutlined/>}
disabled={this.state.clientState !== STATE_CONNECTED} onClick={() => {
this.setState({
fileSystemVisible: true,
@ -617,33 +699,79 @@ class Access extends Component {
</Affix>
</Draggable>
<Draggable>
<Affix style={{position: 'absolute', top: 100, right: 100}}>
<Dropdown overlay={cmdMenu} trigger={['click']} placement="bottomLeft">
<Button icon={<CodeOutlined/>}
disabled={this.state.clientState !== STATE_CONNECTED}/>
</Dropdown>
</Affix>
</Draggable>
<Draggable>
<Affix style={{
position: 'absolute',
top: 150,
right: 100,
zIndex: this.state.enterBtnIndex
}}>
<Button icon={<LineChartOutlined/>} onClick={() => {
this.setState({
statsVisible: true,
});
if (this.statsRef) {
this.statsRef.addInterval();
}
}}/>
</Affix>
</Draggable>
</> : undefined
}
<Drawer
title={'会话详情'}
title={'文件管理'}
placement="right"
width={window.innerWidth * 0.8}
closable={true}
// maskClosable={false}
onClose={() => {
if (this.state.sink) {
this.state.sink.focus();
}
this.focus();
this.setState({
fileSystemVisible: false
});
}}
visible={this.state.fileSystemVisible}
>
<FileSystem
storageId={this.state.sessionId}
storageType={'sessions'}
upload={this.state.session['upload'] === '1'}
download={this.state.session['download'] === '1'}
delete={this.state.session['delete'] === '1'}
rename={this.state.session['rename'] === '1'}
edit={this.state.session['edit'] === '1'}
minHeight={window.innerHeight - 103}/>
</Drawer>
<Row style={{marginTop: 10}}>
<Col span={24}>
<FileSystem sessionId={this.state.sessionId}/>
</Col>
</Row>
<Drawer
title={'状态信息'}
placement="right"
width={window.innerWidth * 0.8}
closable={true}
onClose={() => {
this.setState({
statsVisible: false,
});
this.focus();
if (this.statsRef) {
this.statsRef.delInterval();
}
}}
visible={this.state.statsVisible}
>
<Stats sessionId={this.state.sessionId} onRef={this.onRef}/>
</Drawer>
{
@ -675,9 +803,7 @@ class Access extends Component {
}}
confirmLoading={this.state.confirmLoading}
onCancel={() => {
if (this.state.sink) {
this.state.sink.focus();
}
this.focus();
this.setState({
clipboardVisible: false
})

View File

@ -13,7 +13,7 @@ const STATE_CONNECTED = 3;
const STATE_DISCONNECTING = 4;
const STATE_DISCONNECTED = 5;
class Monitor extends Component {
class AccessMonitor extends Component {
formRef = React.createRef()
@ -174,4 +174,4 @@ class Monitor extends Component {
}
}
export default Monitor;
export default AccessMonitor;

View File

@ -3,18 +3,16 @@ import "xterm/css/xterm.css"
import {Terminal} from "xterm";
import qs from "qs";
import {wsServer} from "../../common/env";
import "./Console.css"
import "./BatchCommandTerm.css"
import {getToken, isEmpty} from "../../utils/utils";
import {FitAddon} from 'xterm-addon-fit'
import request from "../../common/request";
import {message} from "antd";
import Message from './Message'
class Console extends Component {
class BatchCommandTerm extends Component {
state = {
containerOverflow: 'hidden',
width: 0,
height: 0,
term: undefined,
webSocket: undefined,
fitAddon: undefined
@ -24,8 +22,7 @@ class Console extends Component {
let command = this.props.command;
let assetId = this.props.assetId;
let width = this.props.width;
let height = this.props.height;
let sessionId = await this.createSession(assetId);
if (isEmpty(sessionId)) {
return;
@ -51,7 +48,7 @@ class Console extends Component {
term.onData(data => {
let webSocket = this.state.webSocket;
if (webSocket !== undefined) {
webSocket.send(JSON.stringify({type: 'data', content: data}));
webSocket.send(new Message(Message.Data, data).toString());
}
});
@ -82,16 +79,16 @@ class Console extends Component {
let executedCommand = false
webSocket.onmessage = (e) => {
let msg = JSON.parse(e.data);
let msg = Message.parse(e.data);
switch (msg['type']) {
case 'connected':
case Message.Connected:
term.clear();
this.updateSessionStatus(sessionId);
break;
case 'data':
case Message.Data:
term.write(msg['content']);
break;
case 'closed':
case Message.Closed:
term.writeln(`\x1B[1;3;31m${msg['content']}\x1B[0m `)
webSocket.close();
break;
@ -103,10 +100,7 @@ class Console extends Component {
if (command !== '') {
let webSocket = this.state.webSocket;
if (webSocket !== undefined && webSocket.readyState === WebSocket.OPEN) {
webSocket.send(JSON.stringify({
type: 'data',
content: command + String.fromCharCode(13)
}));
webSocket.send(new Message(Message.Data, command + String.fromCharCode(13)).toString());
}
}
executedCommand = true;
@ -117,8 +111,6 @@ class Console extends Component {
term: term,
fitAddon: fitAddon,
webSocket: webSocket,
width: width,
height: height
});
window.addEventListener('resize', this.onWindowResize);
@ -151,36 +143,44 @@ class Console extends Component {
let term = this.state.term;
let fitAddon = this.state.fitAddon;
let webSocket = this.state.webSocket;
if (term && fitAddon && webSocket) {
let height = term.cols;
let width = term.rows;
try {
this.setState({
width: window.innerWidth,
height: window.innerHeight,
}, () => {
if (webSocket && webSocket.readyState === WebSocket.OPEN) {
fitAddon.fit();
} catch (e) {
console.log(e);
this.focus();
let terminalSize = {
cols: term.cols,
rows: term.rows
}
webSocket.send(new Message(Message.Resize, window.btoa(JSON.stringify(terminalSize))).toString());
}
term.focus();
if (webSocket.readyState === WebSocket.OPEN) {
webSocket.send(JSON.stringify({type: 'resize', content: JSON.stringify({height, width})}));
}
}
});
};
focus = () => {
let term = this.state.term;
if (term) {
term.focus();
}
}
render() {
return (
<div>
<div ref='terminal' id='terminal' style={{
overflow: this.state.containerOverflow,
width: this.state.width,
height: this.state.height,
backgroundColor: '#1b1b1b'
}}/>
<div style={{
width: (window.innerWidth - 254) / 2,
height: 456,
}}>
<div ref='terminal' id='terminal' style={{
backgroundColor: '#1b1b1b'
}}/>
</div>
</div>
);
}
}
export default Console;
export default BatchCommandTerm;

View File

@ -1,617 +0,0 @@
import React, {Component} from 'react';
import {Button, Card, Col, Form, Input, message, Modal, Row, Space, Table, Tooltip} from "antd";
import {
CloudDownloadOutlined,
CloudUploadOutlined,
DeleteOutlined,
EditOutlined,
ExclamationCircleOutlined,
FileExcelOutlined,
FileImageOutlined,
FileMarkdownOutlined,
FileOutlined,
FilePdfOutlined,
FileTextOutlined,
FileWordOutlined,
FileZipOutlined,
FolderAddOutlined,
FolderTwoTone,
ReloadOutlined,
ThunderboltTwoTone,
UploadOutlined
} from "@ant-design/icons";
import qs from "qs";
import request from "../../common/request";
import {server} from "../../common/env";
import Upload from "antd/es/upload";
import {download, getFileName, getToken, isEmpty, renderSize} from "../../utils/utils";
import './FileSystem.css'
const {confirm} = Modal;
class FileSystem extends Component {
mkdirFormRef = React.createRef();
renameFormRef = React.createRef();
state = {
sessionId: undefined,
currentDirectory: '/',
currentDirectoryInput: '/',
files: [],
loading: false,
selectedRowKeys: [],
selectedRow: {},
dropdown: {
visible: false
},
}
componentDidMount() {
let sessionId = this.props.sessionId;
this.setState({
sessionId: sessionId
}, () => {
this.loadFiles(this.state.currentDirectory);
});
}
download = () => {
download(`${server}/sessions/${this.state.sessionId}/download?file=${this.state.selectedRow.key}`);
}
rmdir = async () => {
let selectedRowKeys = this.state.selectedRowKeys;
if (selectedRowKeys === undefined || selectedRowKeys.length === 0) {
message.warning('请至少选择一个文件或目录');
}
let title;
if (selectedRowKeys.length === 1) {
let file = getFileName(selectedRowKeys[0]);
title = <p>您确认要删除"{file}"</p>;
} else {
title = `您确认要删除所选的${selectedRowKeys.length}项目吗?`;
}
confirm({
title: title,
icon: <ExclamationCircleOutlined/>,
content: '所选项目将立即被删除。',
onOk: async () => {
for (let i = 0; i < selectedRowKeys.length; i++) {
let rowKey = selectedRowKeys[i];
if (rowKey === '..') {
continue;
}
let result = await request.post(`/sessions/${this.state.sessionId}/rm?key=${rowKey}`);
if (result['code'] !== 1) {
message.error(result['message']);
}
}
await this.loadFiles(this.state.currentDirectory);
},
onCancel() {
},
});
}
refresh = async () => {
this.loadFiles(this.state.currentDirectory);
}
loadFiles = async (key) => {
this.setState({
loading: true
})
try {
if (isEmpty(key)) {
key = '/';
}
let result = await request.get(`/sessions/${this.state.sessionId}/ls?dir=${key}`);
if (result['code'] !== 1) {
message.error(result['message']);
return;
}
let data = result['data'];
const items = data.map(item => {
return {'key': item['path'], ...item}
});
if (key !== '/') {
items.splice(0, 0, {key: '..', name: '..', path: '..', isDir: true})
}
this.setState({
files: items,
currentDirectory: key,
currentDirectoryInput: key,
selectedRow: {},
selectedRowKeys: []
})
} finally {
this.setState({
loading: false
})
}
}
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);
}
}
getNodeTreeRightClickMenu = () => {
const {pageX, pageY, visible} = {...this.state.dropdown};
if (visible) {
const tmpStyle = {
left: `${pageX}px`,
top: `${pageY}px`,
};
let disableDownload = true;
if (this.state.selectedRowKeys.length === 1
&& !this.state.selectedRow['isDir']
&& !this.state.selectedRow['isLink']) {
disableDownload = false;
}
let disableRename = true;
if (this.state.selectedRowKeys.length === 1) {
disableRename = false;
}
return (
<ul className="popup" style={tmpStyle}>
<li><Button type={'text'} size={'small'} icon={<CloudDownloadOutlined/>} onClick={this.download}
disabled={disableDownload}>下载</Button></li>
<li><Button type={'text'} size={'small'} icon={<EditOutlined/>} disabled={disableRename}
onClick={() => {
this.setState({
renameVisible: true
})
}}
>重命名</Button></li>
<li><Button type={'text'} size={'small'} icon={<DeleteOutlined/>} onClick={this.rmdir}>删除</Button>
</li>
</ul>
);
}
return undefined;
};
handleCurrentDirectoryInputChange = (event) => {
this.setState({
currentDirectoryInput: event.target.value
})
}
handleCurrentDirectoryInputPressEnter = (event) => {
this.loadFiles(event.target.value);
}
render() {
const columns = [
{
title: '名称',
dataIndex: 'name',
key: 'name',
render: (value, item) => {
let icon;
if (item['isDir']) {
icon = <FolderTwoTone/>;
} else {
if (item['isLink']) {
icon = <ThunderboltTwoTone/>;
} else {
const fileExtension = item['name'].split('.').pop().toLowerCase();
switch (fileExtension) {
case "doc":
case "docx":
icon = <FileWordOutlined/>;
break;
case "xls":
case "xlsx":
icon = <FileExcelOutlined/>;
break;
case "bmp":
case "jpg":
case "jpeg":
case "png":
case "tif":
case "gif":
case "pcx":
case "tga":
case "exif":
case "svg":
case "psd":
case "ai":
case "webp":
icon = <FileImageOutlined/>;
break;
case "md":
icon = <FileMarkdownOutlined/>;
break;
case "pdf":
icon = <FilePdfOutlined/>;
break;
case "txt":
icon = <FileTextOutlined/>;
break;
case "zip":
case "gz":
case "tar":
case "tgz":
icon = <FileZipOutlined/>;
break;
default:
icon = <FileOutlined/>;
break;
}
}
}
return <span className={'dode'}>{icon}&nbsp;&nbsp;{item['name']}</span>;
},
sorter: (a, b) => {
if (a['key'] === '..') {
return 0;
}
if (b['key'] === '..') {
return 0;
}
return a.name.localeCompare(b.name);
},
sortDirections: ['descend', 'ascend'],
},
{
title: '大小',
dataIndex: 'size',
key: 'size',
render: (value, item) => {
if (!item['isDir'] && !item['isLink']) {
return <span className={'dode'}>{renderSize(value)}</span>;
}
return <span className={'dode'}/>;
},
sorter: (a, b) => {
if (a['key'] === '..') {
return 0;
}
if (b['key'] === '..') {
return 0;
}
return a.size - b.size;
},
}, {
title: '修改日期',
dataIndex: 'modTime',
key: 'modTime',
sorter: (a, b) => {
if (a['key'] === '..') {
return 0;
}
if (b['key'] === '..') {
return 0;
}
return a.modTime.localeCompare(b.modTime);
},
sortDirections: ['descend', 'ascend'],
render: (value, item) => {
return <span className={'dode'}>{value}</span>;
},
}, {
title: '属性',
dataIndex: 'mode',
key: 'mode',
render: (value, item) => {
return <span className={'dode'}>{value}</span>;
},
}
];
const title = (
<Row justify="space-around" align="middle" gutter={24}>
<Col span={20} key={1}>
<Input value={this.state.currentDirectoryInput} onChange={this.handleCurrentDirectoryInputChange}
onPressEnter={this.handleCurrentDirectoryInputPressEnter}/>
</Col>
<Col span={4} key={2} style={{textAlign: 'right'}}>
<Space>
<Tooltip title="创建文件夹">
<Button type="primary" size="small" icon={<FolderAddOutlined/>}
onClick={() => {
this.setState({
mkdirVisible: true
})
}} ghost/>
</Tooltip>
<Tooltip title="上传">
<Button type="primary" size="small" icon={<CloudUploadOutlined/>}
onClick={() => {
this.setState({
uploadVisible: true
})
}} ghost/>
</Tooltip>
<Tooltip title="刷新">
<Button type="primary" size="small" icon={<ReloadOutlined/>} onClick={this.refresh}
ghost/>
</Tooltip>
</Space>
</Col>
</Row>
);
const {selectedRowKeys} = this.state;
const rowSelection = {
selectedRowKeys,
onChange: (selectedRowKeys) => {
selectedRowKeys = selectedRowKeys.filter(rowKey => rowKey !== '..');
this.setState({selectedRowKeys});
},
};
return (
<div>
<Card title={title} bordered={true} size="small">
<Table columns={columns}
rowSelection={rowSelection}
dataSource={this.state.files}
size={'small'}
pagination={false}
loading={this.state.loading}
onRow={record => {
return {
onClick: event => {
if (record['key'] === '..') {
return;
}
this.setState({
selectedRow: record,
selectedRowKeys: [record['key']]
});
}, // 点击行
onDoubleClick: event => {
if (record['isDir'] || record['isLink']) {
if (record['path'] === '..') {
// 获取当前目录的上级目录
let currentDirectory = this.state.currentDirectory;
let parentDirectory = currentDirectory.substring(0, currentDirectory.lastIndexOf('/'));
this.loadFiles(parentDirectory);
} else {
this.loadFiles(record['path']);
}
} else {
}
},
onContextMenu: event => {
event.preventDefault();
if (record['key'] === '..') {
return;
}
let selectedRowKeys = this.state.selectedRowKeys;
if (selectedRowKeys.length === 0) {
selectedRowKeys = [record['key']]
}
this.setState({
selectedRow: record,
selectedRowKeys: selectedRowKeys,
dropdown: {
visible: true,
pageX: event.pageX,
pageY: event.pageY,
}
});
if (!this.state.dropdown.visible) {
const that = this;
document.addEventListener(`click`, function onClickOutside() {
that.setState({dropdown: {visible: false}});
document.removeEventListener(`click`, onClickOutside);
document.querySelector('.ant-drawer-body').style.height = 'unset';
document.querySelector('.ant-drawer-body').style['overflow-y'] = 'auto';
});
document.querySelector('.ant-drawer-body').style.height = '100vh';
document.querySelector('.ant-drawer-body').style['overflow-y'] = 'hidden';
}
},
onMouseEnter: event => {
}, // 鼠标移入行
onMouseLeave: event => {
},
};
}}
rowClassName={(record) => {
return record['key'] === this.state.selectedRow['key'] ? 'selectedRow' : '';
}}
/>
</Card>
<Modal
title="上传文件"
visible={this.state.uploadVisible}
onOk={() => {
this.setState({
uploadVisible: false
})
this.loadFiles(this.state.currentDirectory);
}}
confirmLoading={this.state.uploadLoading}
onCancel={() => {
this.setState({
uploadVisible: false
})
}}
>
<Upload
action={server + '/sessions/' + this.state.sessionId + '/upload?X-Auth-Token=' + getToken() + '&dir=' + this.state.currentDirectory}>
<Button icon={<UploadOutlined/>}>上传文件</Button>
</Upload>
</Modal>
{
this.state.mkdirVisible ?
<Modal
title="创建文件夹"
visible={this.state.mkdirVisible}
onOk={() => {
this.mkdirFormRef.current
.validateFields()
.then(async values => {
this.mkdirFormRef.current.resetFields();
let params = {
'dir': this.state.currentDirectory + '/' + 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('创建成功');
this.loadFiles(this.state.currentDirectory);
} else {
message.error(result.message);
}
this.setState({
confirmLoading: false,
mkdirVisible: false
})
})
.catch(info => {
});
}}
confirmLoading={this.state.confirmLoading}
onCancel={() => {
this.setState({
mkdirVisible: false
})
}}
>
<Form ref={this.mkdirFormRef}>
<Form.Item name='dir' rules={[{required: true, message: '请输入文件夹名称'}]}>
<Input autoComplete="off" placeholder="请输入文件夹名称"/>
</Form.Item>
</Form>
</Modal> : undefined
}
{
this.state.renameVisible ?
<Modal
title="重命名"
visible={this.state.renameVisible}
onOk={() => {
this.renameFormRef.current
.validateFields()
.then(async values => {
this.renameFormRef.current.resetFields();
try {
let currentDirectory = this.state.currentDirectory;
if (!currentDirectory.endsWith("/")) {
currentDirectory += '/';
}
let params = {
'oldName': this.state.selectedRowKeys[0],
'newName': currentDirectory + values['newName'],
}
if (params['oldName'] === params['newName']) {
message.success('重命名成功');
return;
}
let paramStr = qs.stringify(params);
this.setState({
confirmLoading: true
})
let result = await request.post(`/sessions/${this.state.sessionId}/rename?${paramStr}`);
if (result['code'] === 1) {
message.success('重命名成功');
let files = this.state.files;
for (let i = 0; i < files.length; i++) {
if (files['key'] === params['oldName']) {
files[i].path = params['newName'];
files[i].key = params['newName'];
files[i].name = getFileName(params['newName']);
break;
}
}
this.setState({
files: files
})
} else {
message.error(result.message);
}
} finally {
this.setState({
confirmLoading: false,
renameVisible: false
})
}
})
.catch(info => {
});
}}
confirmLoading={this.state.confirmLoading}
onCancel={() => {
this.setState({
renameVisible: false
})
}}
>
<Form ref={this.renameFormRef}
initialValues={{newName: getFileName(this.state.selectedRowKeys[0])}}>
<Form.Item name='newName' rules={[{required: true, message: '请输入新的名称'}]}>
<Input autoComplete="off" placeholder="新的名称"/>
</Form.Item>
</Form>
</Modal> : undefined
}
{this.getNodeTreeRightClickMenu()}
</div>
);
}
}
export default FileSystem;

View File

@ -0,0 +1,23 @@
const Message = class Message {
constructor(type, content) {
this.type = type;
this.content = content;
}
toString() {
return this.type + this.content;
}
static Closed = 0;
static Connected = 1;
static Data = 2;
static Resize = 3;
static Ping = 4;
static parse(s) {
let type = parseInt(s.substring(0, 1));
let content = s.substring(1, s.length);
return new Message(type, content);
}
};
export default Message;

View File

@ -0,0 +1,5 @@
.description-content {
align-items: center;
justify-content: center;
vertical-align: middle;
}

View File

@ -0,0 +1,232 @@
import React, {Component} from 'react';
import {Col, Descriptions, Progress, Row} from "antd";
import request from "../../common/request";
import {renderSize} from "../../utils/utils";
import './Stats.css'
class Stats extends Component {
state = {
sessionId: undefined,
stats: {
uptime: 0,
load1: 0,
load5: 0,
load10: 0,
memTotal: 0,
memFree: 0,
memAvailable: 0,
memBuffers: 0,
memCached: 0,
swapTotal: 0,
swapFree: 0,
network: {},
fileSystems: [],
cpu: {
user: 0,
system: 0,
nice: 0,
idle: 0,
ioWait: 0,
irq: 0,
softIrq: 0,
guest: 0
}
},
prevStats: {},
interval: undefined
}
componentDidMount() {
this.props.onRef(this);
let sessionId = this.props.sessionId;
this.setState({
sessionId: sessionId
}, () => {
this.getStats();
});
this.addInterval();
}
getStats = async () => {
let result = await request.get(`/sessions/${this.state.sessionId}/stats`);
if (result['code'] !== 1) {
return
}
let data = result['data'];
this.setState({
stats: data,
prevStats: this.state.stats
});
}
addInterval = () => {
let interval = setInterval(this.getStats, 5000);
this.setState({
interval: interval
});
}
delInterval = () => {
if (this.state.interval) {
clearInterval(this.state.interval);
this.setState({
interval: undefined
})
}
}
render() {
const upDays = parseInt((this.state.stats.uptime / 1000 / 60 / 60 / 24).toString());
const memUsage = ((this.state.stats.memTotal - this.state.stats.memAvailable) * 100 / this.state.stats.memTotal).toFixed(2);
let network = this.state.stats.network;
let fileSystems = this.state.stats.fileSystems;
let swapUsage = 0;
if (this.state.stats.swapTotal !== 0) {
swapUsage = ((this.state.stats.swapTotal - this.state.stats.swapFree) * 100 / this.state.stats.swapTotal).toFixed(2)
}
return (
<div>
<Descriptions title="系统信息" column={4}>
<Descriptions.Item label="主机名称">{this.state.stats.hostname}</Descriptions.Item>
<Descriptions.Item label="运行时长">{upDays}</Descriptions.Item>
</Descriptions>
<Row justify="center" align="middle">
<Col>
<Descriptions title="负载" column={4}>
<Descriptions.Item label='Load1'>
<div className='description-content'>
<Progress percent={this.state.stats.load1} steps={20} size={'small'}/>
</div>
</Descriptions.Item>
<Descriptions.Item label='Load5'>
<div className='description-content'>
<Progress percent={this.state.stats.load5} steps={20} size={'small'}/>
</div>
</Descriptions.Item>
<Descriptions.Item label='Load10'>
<div className='description-content'>
<Progress percent={this.state.stats.load10} steps={20} size={'small'}/>
</div>
</Descriptions.Item>
</Descriptions>
</Col>
</Row>
<Descriptions title="CPU" column={4}>
<Descriptions.Item label="用户">
{this.state.stats.cpu['user'].toFixed(2)}%
</Descriptions.Item>
<Descriptions.Item label="系统">
{this.state.stats.cpu['system'].toFixed(2)}%
</Descriptions.Item>
<Descriptions.Item label="空闲">
{this.state.stats.cpu['idle'].toFixed(2)}%
</Descriptions.Item>
<Descriptions.Item label="IO等待">
{this.state.stats.cpu['ioWait'].toFixed(2)}%
</Descriptions.Item>
<Descriptions.Item label="硬中断">
{this.state.stats.cpu['irq'].toFixed(2)}%
</Descriptions.Item>
<Descriptions.Item label="软中断">
{this.state.stats.cpu['softIrq'].toFixed(2)}%
</Descriptions.Item>
<Descriptions.Item label="nice">
{this.state.stats.cpu['nice'].toFixed(2)}%
</Descriptions.Item>
<Descriptions.Item label="guest">
{this.state.stats.cpu['guest'].toFixed(2)}%
</Descriptions.Item>
</Descriptions>
<Descriptions title="内存" column={4}>
<Descriptions.Item label="物理内存大小">{renderSize(this.state.stats.memTotal)}</Descriptions.Item>
<Descriptions.Item label="剩余内存大小">{renderSize(this.state.stats.memFree)}</Descriptions.Item>
<Descriptions.Item label="可用内存大小">{renderSize(this.state.stats.memAvailable)}</Descriptions.Item>
<Descriptions.Item label="使用占比">
<div className='description-content'>
<Progress percent={memUsage} steps={20} size={'small'}/>
</div>
</Descriptions.Item>
<Descriptions.Item
label="Buffers/Cached">{renderSize(this.state.stats.memBuffers)} / {renderSize(this.state.stats.memCached)}</Descriptions.Item>
<Descriptions.Item
label="交换内存大小">{renderSize(this.state.stats.swapTotal)}</Descriptions.Item>
<Descriptions.Item
label="交换内存剩余">{renderSize(this.state.stats.swapFree)}</Descriptions.Item>
<Descriptions.Item label="使用占比">
<div className='description-content'>
<Progress percent={swapUsage} steps={20} size={'small'}/>
</div>
</Descriptions.Item>
</Descriptions>
<Descriptions title="磁盘" column={4}>
{
fileSystems.map((item, index) => {
return (
<React.Fragment key={'磁盘' + index}>
<Descriptions.Item label="挂载路径" key={'挂载路径' + index}>
{item['mountPoint']}
</Descriptions.Item>
<Descriptions.Item label="已经使用" key={'已经使用' + index}>
{renderSize(item['used'])}
</Descriptions.Item>
<Descriptions.Item label="剩余空间" key={'剩余空间' + index}>
{renderSize(item['free'])}
</Descriptions.Item>
<Descriptions.Item label="使用占比" key={'使用占比' + index}>
<div className='description-content'>
<Progress
percent={(item['used'] * 100 / (item['used'] + item['free'])).toFixed(2)}
steps={20} size={'small'}/>
</div>
</Descriptions.Item>
</React.Fragment>
);
})
}
</Descriptions>
<Descriptions title="网络" column={4}>
{
Object.keys(network).map((key, index) => {
let prevNetwork = this.state.prevStats.network;
let rxOfSeconds = 0, txOfSeconds = 0;
if (prevNetwork[key] !== undefined) {
rxOfSeconds = (network[key]['rx'] - prevNetwork[key]['rx']) / 5;
}
if (prevNetwork[key] !== undefined) {
txOfSeconds = (network[key]['tx'] - prevNetwork[key]['tx']) / 5;
}
return (
<React.Fragment key={'网络' + index}>
<Descriptions.Item label="网卡" key={'网卡' + index}>{key}</Descriptions.Item>
<Descriptions.Item label="IPv4" key={'IPv4' + index}>
{network[key]['ipv4']}
</Descriptions.Item>
<Descriptions.Item label="接收" key={'接收' + index}>
{renderSize(network[key]['rx'])} &nbsp; {renderSize(rxOfSeconds)}/
</Descriptions.Item>
<Descriptions.Item label="发送" key={'发送' + index}>
{renderSize(network[key]['tx'])} &nbsp; {renderSize(txOfSeconds)}/
</Descriptions.Item>
</React.Fragment>
);
})
}
</Descriptions>
</div>
);
}
}
export default Stats;

View File

@ -7,13 +7,17 @@ import {getToken, isEmpty} from "../../utils/utils";
import {FitAddon} from 'xterm-addon-fit';
import "./Access.css"
import request from "../../common/request";
import {Affix, Button, Col, Drawer, message, Modal, Row} from "antd";
import {AppstoreTwoTone, ExclamationCircleOutlined} from "@ant-design/icons";
import {Affix, Button, Drawer, Dropdown, Menu, message, Modal, Tooltip} from "antd";
import {CodeOutlined, ExclamationCircleOutlined, FolderOutlined, LineChartOutlined} from "@ant-design/icons";
import Draggable from "react-draggable";
import FileSystem from "./FileSystem";
import FileSystem from "../devops/FileSystem";
import Stats from "./Stats";
import Message from "./Message";
class Term extends Component {
statsRef = undefined;
state = {
width: window.innerWidth,
height: window.innerHeight,
@ -21,7 +25,9 @@ class Term extends Component {
webSocket: undefined,
fitAddon: undefined,
sessionId: undefined,
enterBtnIndex: 1001
session: {},
enterBtnIndex: 1001,
commands: []
};
componentDidMount = async () => {
@ -30,7 +36,11 @@ class Term extends Component {
let assetId = urlParams.get('assetId');
document.title = urlParams.get('assetName');
let sessionId = await this.createSession(assetId);
let session = await this.createSession(assetId);
if (!session) {
return;
}
let sessionId = session['id'];
if (isEmpty(sessionId)) {
return;
}
@ -38,14 +48,13 @@ class Term extends Component {
let term = new Terminal({
fontFamily: 'monaco, Consolas, "Lucida Console", monospace',
fontSize: 15,
// theme: {
// background: '#1b1b1b',
// lineHeight: 17
// },
theme: {
background: '#1b1b1b'
},
rightClickSelectsWord: true,
});
term.open(this.refs.terminal);
term.open(document.getElementById('terminal'));
const fitAddon = new FitAddon();
term.loadAddon(fitAddon);
fitAddon.fit();
@ -55,6 +64,7 @@ class Term extends Component {
term.onSelectionChange(async () => {
let selection = term.getSelection();
console.log(`selection: [${selection}]`);
this.setState({
selection: selection
})
@ -73,7 +83,7 @@ class Term extends Component {
term.onData(data => {
let webSocket = this.state.webSocket;
if (webSocket !== undefined) {
webSocket.send(JSON.stringify({type: 'data', content: data}));
webSocket.send(new Message(Message.Data, data).toString());
}
});
@ -92,8 +102,8 @@ class Term extends Component {
let pingInterval;
webSocket.onopen = (e => {
pingInterval = setInterval(() => {
webSocket.send(JSON.stringify({type: 'ping'}))
}, 5000);
webSocket.send(new Message(Message.Ping, "").toString());
}, 1000);
});
webSocket.onerror = (e) => {
@ -107,16 +117,17 @@ class Term extends Component {
}
webSocket.onmessage = (e) => {
let msg = JSON.parse(e.data);
let msg = Message.parse(e.data);
switch (msg['type']) {
case 'connected':
case Message.Connected:
term.clear();
this.updateSessionStatus(sessionId);
this.getCommands();
break;
case 'data':
case Message.Data:
term.write(msg['content']);
break;
case 'closed':
case Message.Closed:
term.writeln(`\x1B[1;3;31m${msg['content']}\x1B[0m `)
webSocket.close();
break;
@ -129,10 +140,14 @@ class Term extends Component {
term: term,
webSocket: webSocket,
fitAddon: fitAddon,
sessionId: sessionId
sessionId: sessionId,
session: session
});
window.addEventListener('resize', this.onWindowResize);
window.onunload = function () {
webSocket.close();
};
}
componentWillUnmount() {
@ -142,6 +157,17 @@ class Term extends Component {
}
}
getCommands = async () => {
let result = await request.get('/commands');
if (result.code !== 1) {
message.error(result.message);
return;
}
this.setState({
commands: result['data']
})
}
showMessage(msg) {
message.destroy();
Modal.confirm({
@ -164,9 +190,9 @@ class Term extends Component {
let result = await request.post(`/sessions?assetId=${assetsId}&mode=naive`);
if (result['code'] !== 1) {
this.showMessage(result['message']);
return null;
return undefined;
}
return result['data']['id'];
return result['data'];
}
updateSessionStatus = async (sessionId) => {
@ -194,30 +220,62 @@ class Term extends Component {
}, () => {
if (webSocket && webSocket.readyState === WebSocket.OPEN) {
fitAddon.fit();
term.focus();
this.focus();
let terminalSize = {
cols: term.cols,
rows: term.rows
}
webSocket.send(JSON.stringify({type: 'resize', content: JSON.stringify(terminalSize)}));
webSocket.send(new Message(Message.Resize, window.btoa(JSON.stringify(terminalSize))).toString());
}
});
};
writeCommand = (command) => {
let webSocket = this.state.webSocket;
if (webSocket !== undefined) {
webSocket.send(new Message(Message.Data, command));
}
this.focus();
}
focus = () => {
let term = this.state.term;
if (term) {
term.focus();
}
}
onRef = (statsRef) => {
this.statsRef = statsRef;
}
render() {
const cmdMenuItems = this.state.commands.map(item => {
return <Tooltip placement="left" title={item['content']} color='blue' key={'t-' + item['id']}>
<Menu.Item onClick={() => {
this.writeCommand(item['content'])
}} key={'i-' + item['id']}>{item['name']}</Menu.Item>
</Tooltip>;
});
const cmdMenu = (
<Menu>
{cmdMenuItems}
</Menu>
);
return (
<div>
<div ref='terminal' id='terminal' style={{
<div id='terminal' style={{
height: this.state.height,
width: this.state.width,
backgroundColor: 'black',
overflowX: 'hidden',
overflowY: 'hidden',
backgroundColor: '#1b1b1b'
}}/>
<Draggable>
<Affix style={{position: 'absolute', top: 50, right: 50, zIndex: this.state.enterBtnIndex}}>
<Button icon={<AppstoreTwoTone/>} onClick={() => {
<Button icon={<FolderOutlined/>} onClick={() => {
this.setState({
fileSystemVisible: true,
enterBtnIndex: 999, // xterm.js 输入框的zIndex是1000在弹出文件管理页面后要隐藏此按钮
@ -226,6 +284,28 @@ class Term extends Component {
</Affix>
</Draggable>
<Draggable>
<Affix style={{position: 'absolute', top: 50, right: 100, zIndex: this.state.enterBtnIndex}}>
<Dropdown overlay={cmdMenu} trigger={['click']} placement="bottomLeft">
<Button icon={<CodeOutlined/>}/>
</Dropdown>
</Affix>
</Draggable>
<Draggable>
<Affix style={{position: 'absolute', top: 100, right: 100, zIndex: this.state.enterBtnIndex}}>
<Button icon={<LineChartOutlined/>} onClick={() => {
this.setState({
statsVisible: true,
enterBtnIndex: 999, // xterm.js 输入框的zIndex是1000在弹出文件管理页面后要隐藏此按钮
});
if (this.statsRef) {
this.statsRef.addInterval();
}
}}/>
</Affix>
</Draggable>
<Drawer
title={'会话详情'}
placement="right"
@ -237,16 +317,39 @@ class Term extends Component {
fileSystemVisible: false,
enterBtnIndex: 1001, // xterm.js 输入框的zIndex是1000在隐藏文件管理页面后要显示此按钮
});
this.focus();
}}
visible={this.state.fileSystemVisible}
>
<FileSystem
storageId={this.state.sessionId}
storageType={'sessions'}
upload={this.state.session['upload'] === '1'}
download={this.state.session['download'] === '1'}
delete={this.state.session['delete'] === '1'}
rename={this.state.session['rename'] === '1'}
edit={this.state.session['edit'] === '1'}
minHeight={window.innerHeight - 103}/>
</Drawer>
<Row style={{marginTop: 10}}>
<Col span={24}>
<FileSystem sessionId={this.state.sessionId}/>
</Col>
</Row>
<Drawer
title={'状态信息'}
placement="right"
width={window.innerWidth * 0.8}
closable={true}
onClose={() => {
this.setState({
statsVisible: false,
enterBtnIndex: 1001, // xterm.js 输入框的zIndex是1000在隐藏文件管理页面后要显示此按钮
});
this.focus();
if (this.statsRef) {
this.statsRef.delInterval();
}
}}
visible={this.state.statsVisible}
>
<Stats sessionId={this.state.sessionId} onRef={this.onRef}/>
</Drawer>
</div>
);

View File

@ -0,0 +1,84 @@
import React, {Component} from 'react';
import {Terminal} from "xterm";
import Message from "./Message";
import {getToken} from "../../utils/utils";
import qs from "qs";
import {wsServer} from "../../common/env";
import {FitAddon} from "xterm-addon-fit";
class TermMonitor extends Component {
componentDidMount() {
let sessionId = this.props.sessionId;
let term = new Terminal({
fontFamily: 'monaco, Consolas, "Lucida Console", monospace',
fontSize: 14,
theme: {
background: '#1b1b1b'
},
rightClickSelectsWord: true,
});
term.open(document.getElementById('terminal'));
term.writeln("等待用户输入中...")
const fitAddon = new FitAddon();
term.loadAddon(fitAddon);
fitAddon.fit();
term.focus();
term.onData(data => {
});
let token = getToken();
let params = {
'sessionId': sessionId,
'X-Auth-Token': token
};
let paramStr = qs.stringify(params);
let webSocket = new WebSocket(wsServer + '/ssh-monitor?' + paramStr);
webSocket.onmessage = (e) => {
let msg = Message.parse(e.data);
switch (msg['type']) {
case Message.Connected:
term.clear();
break;
case Message.Data:
term.write(msg['content']);
break;
case Message.Closed:
term.writeln(`\x1B[1;3;31m${msg['content']}\x1B[0m `)
webSocket.close();
break;
default:
break;
}
}
this.setState({
term: term,
webSocket: webSocket,
});
}
componentWillUnmount() {
let webSocket = this.state.webSocket;
if (webSocket) {
webSocket.close()
}
}
render() {
return (
<div>
<div id='terminal' style={{
backgroundColor: '#1b1b1b'
}}/>
</div>
);
}
}
export default TermMonitor;

View File

@ -0,0 +1,491 @@
import React, {Component} from 'react';
import {Badge, Button, Col, Divider, Input, Layout, Modal, Row, Space, Table, Tag, Tooltip, Typography} from "antd";
import qs from "qs";
import request from "../../common/request";
import {message} from "antd/es";
import {DeleteOutlined, ExclamationCircleOutlined, PlusOutlined, SyncOutlined, UndoOutlined} from '@ant-design/icons';
import AccessGatewayModal from "./AccessGatewayModal";
import {hasPermission} from "../../service/permission";
import dayjs from "dayjs";
const confirm = Modal.confirm;
const {Content} = Layout;
const {Title, Text} = Typography;
const {Search} = Input;
class AccessGateway extends Component {
inputRefOfName = React.createRef();
inputRefOfIp = React.createRef();
state = {
items: [],
total: 0,
queryParams: {
pageIndex: 1,
pageSize: 10
},
loading: false,
modalVisible: false,
modalTitle: '',
modalConfirmLoading: false,
selectedRow: undefined,
selectedRowKeys: [],
};
componentDidMount() {
this.loadTableData();
}
async delete(id) {
const result = await request.delete('/access-gateways/' + 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('/access-gateways/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);
};
handleSearchByIp = ip => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'ip': ip,
}
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(`/access-gateways/${id}`);
if (result.code !== 1) {
message.error(result.message, 10);
return;
}
await this.showModal('更新接入网关', result.data);
}
async reconnect(id) {
message.info({content: '正在重连中...', key: id, duration: 5});
let result = await request.post(`/access-gateways/${id}/reconnect`);
if (result.code !== 1) {
message.error({content: result.message, key: id, duration: 10});
return;
}
message.success({content: '重连完成。', key: id, duration: 3});
this.loadTableData(this.state.queryParams);
}
showModal(title, obj) {
this.setState({
modalTitle: title,
modalVisible: true,
model: obj
});
};
handleCancelModal = e => {
this.setState({
modalTitle: '',
modalVisible: false
});
};
handleOk = async (formData) => {
// 弹窗 form 传来的数据
this.setState({
modalConfirmLoading: true
});
if (formData.id) {
// 向后台提交数据
const result = await request.put('/access-gateways/' + 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('/access-gateways', 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('/access-gateways/' + 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
})
}
}
handleTableChange = (pagination, filters, sorter) => {
let query = {
...this.state.queryParams,
'order': sorter.order,
'field': sorter.field
}
this.loadTableData(query);
}
render() {
const columns = [{
title: '序号',
dataIndex: 'id',
key: 'id',
render: (id, record, index) => {
return index + 1;
}
}, {
title: '名称',
dataIndex: 'name',
key: 'name',
render: (name, record) => {
let short = name;
if (short && short.length > 20) {
short = short.substring(0, 20) + " ...";
}
if (hasPermission(record['owner'])) {
return (
<Button type="link" size='small' onClick={() => this.update(record['id'])}>
<Tooltip placement="topLeft" title={name}>
{short}
</Tooltip>
</Button>
);
} else {
return (
<Tooltip placement="topLeft" title={name}>
{short}
</Tooltip>
);
}
},
sorter: true,
}, {
title: 'IP',
dataIndex: 'ip',
key: 'ip',
sorter: true,
}, {
title: '端口',
dataIndex: 'port',
key: 'port',
}, {
title: '账户类型',
dataIndex: 'accountType',
key: 'accountType',
render: (accountType) => {
if (accountType === 'private-key') {
return (
<Tag color="green">密钥</Tag>
);
} else {
return (
<Tag color="red">密码</Tag>
);
}
}
}, {
title: '授权账户',
dataIndex: 'username',
key: 'username',
}, {
title: '状态',
dataIndex: 'connected',
key: 'connected',
render: (text, record) => {
if (text) {
return (
<Tooltip title='连接成功'>
<Badge status="success" text='已连接'/>
</Tooltip>
)
} else {
return (
<Tooltip title={record['message']}>
<Badge status="error" text='已断开'/>
</Tooltip>
)
}
}
}, {
title: '创建时间',
dataIndex: 'created',
key: 'created',
render: (text, record) => {
return (
<Tooltip title={text}>
{dayjs(text).fromNow()}
</Tooltip>
)
},
sorter: true,
}, {
title: '操作',
key: 'action',
render: (text, record, index) => {
return (
<div>
<Button type="link" size='small' loading={this.state.items[index]['execLoading']}
onClick={() => this.update(record['id'])}>编辑</Button>
<Button type="link" size='small' loading={this.state.items[index]['execLoading']}
onClick={() => this.reconnect(record['id'])}>重连</Button>
<Button type="text" size='small' danger
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 (
<>
<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.inputRefOfIp}
placeholder="IP"
allowClear
onSearch={this.handleSearchByIp}
/>
<Tooltip title='重置查询'>
<Button icon={<UndoOutlined/>} onClick={() => {
this.inputRefOfName.current.setValue('');
this.inputRefOfIp.current.setValue('');
this.loadTableData({
pageIndex: 1,
pageSize: 10,
name: '',
ip: '',
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}
onChange={this.handleTableChange}
/>
{
this.state.modalVisible ?
<AccessGatewayModal
visible={this.state.modalVisible}
title={this.state.modalTitle}
handleOk={this.handleOk}
handleCancel={this.handleCancelModal}
confirmLoading={this.state.modalConfirmLoading}
model={this.state.model}
>
</AccessGatewayModal> : undefined
}
</Content>
</>
);
}
}
export default AccessGateway;

View File

@ -0,0 +1,114 @@
import React, {useState} from 'react';
import {Form, Input, InputNumber, Modal, Select} from "antd/lib/index";
const formItemLayout = {
labelCol: {span: 6},
wrapperCol: {span: 14},
};
const {TextArea} = Input;
const AccessGatewayModal = ({title, visible, handleOk, handleCancel, confirmLoading, model}) => {
const [form] = Form.useForm();
if (model['accountType'] === undefined) {
model['accountType'] = 'password';
}
if (model['port'] === undefined) {
model['port'] = 22;
}
let [accountType, setAccountType] = useState(model.accountType);
const handleAccountTypeChange = v => {
setAccountType(v);
}
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='ip' rules={[{required: true, message: '请输入网关的主机名称或者IP地址'}]}>
<Input placeholder="网关的主机名称或者IP地址"/>
</Form.Item>
<Form.Item label="端口号" name='port' rules={[{required: true, message: '请输入端口'}]}>
<InputNumber min={1} max={65535} placeholder='TCP端口'/>
</Form.Item>
<Form.Item label="账户类型" name='accountType'
rules={[{required: true, message: '请选择接账户类型'}]}>
<Select onChange={handleAccountTypeChange}>
<Select.Option key='password' value='password'>密码</Select.Option>
<Select.Option key='private-key' value='private-key'>密钥</Select.Option>
</Select>
</Form.Item>
{
accountType === 'password' ?
<>
<input type='password' hidden={true} autoComplete='new-password'/>
<Form.Item label="授权账户" name='username'
rules={[{required: true}]}>
<Input placeholder="root"/>
</Form.Item>
<Form.Item label="授权密码" name='password'
rules={[{required: true}]}>
<Input.Password placeholder="password"/>
</Form.Item>
</>
:
<>
<Form.Item label="授权账户" name='username' rules={[{required: true}]}>
<Input placeholder="输入授权账户"/>
</Form.Item>
<Form.Item label="私钥" name='privateKey'
rules={[{required: true, message: '请输入私钥'}]}>
<TextArea rows={4}/>
</Form.Item>
<Form.Item label="私钥密码" name='passphrase'>
<TextArea rows={1}/>
</Form.Item>
</>
}
<Form.Item label="本地映射地址" name='localhost' tooltip='隧道映射到本地的地址请确保Guacd可以访问到此IP'>
<Input placeholder="localhost"/>
</Form.Item>
</Form>
</Modal>
)
};
export default AccessGatewayModal;

View File

@ -19,7 +19,6 @@ import {
Table,
Tag,
Tooltip,
Transfer,
Typography
} from "antd";
import qs from "qs";
@ -40,7 +39,7 @@ import {
} from '@ant-design/icons';
import {PROTOCOL_COLORS} from "../../common/constants";
import {hasPermission, isAdmin} from "../../service/permission";
import {hasPermission} from "../../service/permission";
import Upload from "antd/es/upload";
import axios from "axios";
import {server} from "../../common/env";
@ -77,9 +76,7 @@ class Asset extends Component {
selectedRowKeys: [],
delBtnLoading: false,
changeOwnerModalVisible: false,
changeSharerModalVisible: false,
changeOwnerConfirmLoading: false,
changeSharerConfirmLoading: false,
users: [],
selected: {},
selectedSharers: [],
@ -106,7 +103,7 @@ class Asset extends Component {
message.success('删除成功');
await this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
message.error(result.message, 10);
}
}
@ -239,6 +236,21 @@ class Asset extends Component {
await this.showModal('复制资产', result.data);
}
async connTest(id) {
message.info({content: '正在测试中...', key: id, duration: 5});
let result = await request.post(`/assets/${id}/tcping`);
if (result.code !== 1) {
message.error({content: result.message, key: id, duration: 10});
return;
}
if (result['data']['active'] === true) {
message.success({content: '连通性测试完成,当前资产在线。', key: id, duration: 3});
} else {
message.warning({content: `连通性测试完成,当前资产离线,原因: ${result['data']['message']}`, key: id, duration: 10});
}
this.loadTableData(this.state.queryParams);
}
async showModal(title, asset = {}) {
// 并行请求
let getCredentials = request.get('/credentials');
@ -269,10 +281,8 @@ class Asset extends Component {
}
asset['use-ssl'] = asset['use-ssl'] === 'true';
asset['ignore-cert'] = asset['ignore-cert'] === 'true';
console.log(asset)
asset['enable-drive'] = asset['enable-drive'] === 'true';
this.setState({
modalTitle: title,
@ -296,11 +306,14 @@ class Asset extends Component {
modalConfirmLoading: true
});
console.log(formData)
if (formData['tags']) {
formData.tags = formData['tags'].join(',');
}
if (formData['accessGatewayId'] === undefined) {
formData['accessGatewayId'] = "-"
}
if (formData.id) {
// 向后台提交数据
const result = await request.put('/assets/' + formData.id, formData);
@ -312,7 +325,7 @@ class Asset extends Component {
});
await this.loadTableData(this.state.queryParams);
} else {
message.error('操作失败 :( ' + result.message, 10);
message.error(result.message, 10);
}
} else {
// 向后台提交数据
@ -325,7 +338,7 @@ class Asset extends Component {
});
await this.loadTableData(this.state.queryParams);
} else {
message.error('操作失败 :( ' + result.message, 10);
message.error(result.message, 10);
}
}
@ -334,31 +347,6 @@ class Asset extends Component {
});
};
access = async (record) => {
const id = record['id'];
const protocol = record['protocol'];
const name = record['name'];
const sshMode = record['sshMode'];
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});
if (protocol === 'ssh' && sshMode === 'naive') {
window.open(`#/term?assetId=${id}&assetName=${name}`);
} else {
window.open(`#/access?assetId=${id}&assetName=${name}&protocol=${protocol}`);
}
} else {
message.warn({content: '您访问的资产未在线,请确认网络状态。', key: id, duration: 10});
}
} else {
message.error({content: result.message, key: id, duration: 10});
}
}
batchDelete = async () => {
this.setState({
delBtnLoading: true
@ -372,7 +360,7 @@ class Asset extends Component {
})
await this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
message.error(result.message, 10);
}
} finally {
this.setState({
@ -397,43 +385,6 @@ class Asset extends Component {
})
}
handleSharersChange = async targetKeys => {
this.setState({
selectedSharers: targetKeys
})
}
handleShowSharer = async (record) => {
let r1 = this.handleSearchByNickname('');
let r2 = request.get(`/resource-sharers/sharers?resourceId=${record['id']}`);
await r1;
let result = await r2;
let selectedSharers = [];
if (result['code'] !== 1) {
message.error(result['message']);
} else {
selectedSharers = result['data'];
}
let users = this.state.users;
users = users.map(item => {
let disabled = false;
if (record['owner'] === item['id']) {
disabled = true;
}
return {...item, 'disabled': disabled}
});
this.setState({
selectedSharers: selectedSharers,
selected: record,
changeSharerModalVisible: true,
users: users
})
}
handleCancelUpdateAttr = () => {
this.setState({
attrVisible: false,
@ -467,8 +418,8 @@ class Asset extends Component {
key: 'name',
render: (name, record) => {
let short = name;
if (short && short.length > 20) {
short = short.substring(0, 20) + " ...";
if (short && short.length > 15) {
short = short.substring(0, 15) + " ...";
}
if (hasPermission(record['owner'])) {
@ -489,7 +440,7 @@ class Asset extends Component {
},
sorter: true,
}, {
title: '连接协议',
title: '协议',
dataIndex: 'protocol',
key: 'protocol',
render: (text, record) => {
@ -500,21 +451,20 @@ class Asset extends Component {
</Tooltip>
)
}
}, {
title: '网络',
dataIndex: 'network',
key: 'network',
render: (text, record) => {
return `${record['ip'] + ':' + record['port']}`;
}
}, {
title: '标签',
dataIndex: 'tags',
key: 'tags',
render: tags => {
if (!isEmpty(tags)) {
let tagDocuments = []
let tagArr = tags.split(',');
for (let i = 0; i < tagArr.length; i++) {
if (tags[i] === '-') {
continue;
}
tagDocuments.push(<Tag key={tagArr[i]}>{tagArr[i]}</Tag>)
}
return tagDocuments;
return this.renderTags(tags);
}
}
}, {
@ -522,17 +472,16 @@ class Asset extends Component {
dataIndex: 'active',
key: 'active',
render: text => {
if (text) {
return (
<Tooltip title='运行中'>
<Badge status="processing"/>
<Badge status="processing" text='运行中'/>
</Tooltip>
)
} else {
return (
<Tooltip title='不可用'>
<Badge status="error"/>
<Badge status="error" text='不可用'/>
</Tooltip>
)
}
@ -563,49 +512,38 @@ class Asset extends Component {
<Menu>
<Menu.Item key="1">
<Button type="text" size='small'
disabled={!hasPermission(record['owner'])}
onClick={() => this.update(record.id)}>编辑</Button>
</Menu.Item>
<Menu.Item key="2">
<Button type="text" size='small'
disabled={!hasPermission(record['owner'])}
onClick={() => this.copy(record.id)}>复制</Button>
</Menu.Item>
{isAdmin() ?
<Menu.Item key="4">
<Button type="text" size='small'
disabled={!hasPermission(record['owner'])}
onClick={() => {
this.handleSearchByNickname('')
.then(() => {
this.setState({
changeOwnerModalVisible: true,
selected: record,
})
this.changeOwnerFormRef
.current
.setFieldsValue({
owner: record['owner']
})
});
}}>更换所有者</Button>
</Menu.Item> : undefined
}
<Menu.Item key="5">
<Menu.Item key="3">
<Button type="text" size='small'
onClick={() => this.connTest(record.id)}>连通性测试</Button>
</Menu.Item>
<Menu.Item key="4">
<Button type="text" size='small'
disabled={!hasPermission(record['owner'])}
onClick={async () => {
await this.handleShowSharer(record);
}}>更新授权人</Button>
</Menu.Item>
onClick={() => {
this.handleSearchByNickname('')
.then(() => {
this.setState({
changeOwnerModalVisible: true,
selected: record,
})
this.changeOwnerFormRef
.current
.setFieldsValue({
owner: record['owner']
})
});
}}>更换所有者</Button>
</Menu.Item>
<Menu.Divider/>
<Menu.Item key="6">
<Menu.Item key="5">
<Button type="text" size='small' danger
disabled={!hasPermission(record['owner'])}
onClick={() => this.showDeleteConfirm(record.id, record.name)}>删除</Button>
@ -613,11 +551,20 @@ class Asset extends Component {
</Menu>
);
const id = record['id'];
const protocol = record['protocol'];
const name = record['name'];
const sshMode = record['sshMode'];
let url = '';
if (protocol === 'ssh' && sshMode === 'naive') {
url = `#/term?assetId=${id}&assetName=${name}`;
} else {
url = `#/access?assetId=${id}&assetName=${name}&protocol=${protocol}`;
}
return (
<div>
<Button type="link" size='small'
onClick={() => this.access(record)}>接入</Button>
<Button type="link" size='small' href={url} target='_blank'>接入</Button>
<Dropdown overlay={menu}>
<Button type="link" size='small'>
更多 <DownOutlined/>
@ -629,19 +576,6 @@ class Asset extends Component {
}
];
if (isAdmin()) {
columns.splice(6, 0, {
title: '授权人数',
dataIndex: 'sharerCount',
key: 'sharerCount',
render: (text, record, index) => {
return <Button type='link' onClick={async () => {
await this.handleShowSharer(record, true);
}}>{text}</Button>
}
});
}
const selectedRowKeys = this.state.selectedRowKeys;
const rowSelection = {
selectedRowKeys: this.state.selectedRowKeys,
@ -717,25 +651,19 @@ class Asset extends Component {
</Tooltip>
<Divider type="vertical"/>
{isAdmin() ?
<Tooltip title="批量导入">
<Button type="dashed" icon={<ImportOutlined/>}
onClick={() => {
this.setState({
importModalVisible: true
})
}}>
</Button>
</Tooltip> : undefined
}
<Tooltip title="批量导入">
<Button type="dashed" icon={<ImportOutlined/>}
onClick={() => {
this.setState({
importModalVisible: true
})
}}>
</Button>
</Tooltip>
<Tooltip title="新增">
<Button type="dashed" icon={<PlusOutlined/>}
onClick={() => this.showModal('新增资产', {})}>
<Button icon={<PlusOutlined/>}
onClick={() => this.showModal('新增资产', {})}
>
</Button>
</Tooltip>
@ -962,69 +890,22 @@ class Asset extends Component {
</Form>
</Modal>
{
this.state.changeSharerModalVisible ?
<Modal title={<Text>更新资源<strong
style={{color: '#1890ff'}}>{this.state.selected['name']}</strong>
</Text>}
visible={this.state.changeSharerModalVisible}
confirmLoading={this.state.changeSharerConfirmLoading}
onOk={async () => {
this.setState({
changeSharerConfirmLoading: true
});
let changeSharerModalVisible = false;
let result = await request.post(`/resource-sharers/overwrite-sharers`, {
resourceId: this.state.selected['id'],
resourceType: 'asset',
userIds: this.state.selectedSharers
});
if (result['code'] === 1) {
message.success('操作成功');
this.loadTableData();
} else {
message.error(result['message'], 10);
changeSharerModalVisible = true;
}
this.setState({
changeSharerConfirmLoading: false,
changeSharerModalVisible: changeSharerModalVisible
})
}}
onCancel={() => {
this.setState({
changeSharerModalVisible: false
})
}}
okButtonProps={{disabled: !hasPermission(this.state.selected['owner'])}}
>
<Transfer
dataSource={this.state.users}
disabled={!hasPermission(this.state.selected['owner'])}
showSearch
titles={['未授权', '已授权']}
operations={['授权', '移除']}
listStyle={{
width: 250,
height: 300,
}}
targetKeys={this.state.selectedSharers}
onChange={this.handleSharersChange}
render={item => `${item.nickname}`}
/>
</Modal> : undefined
}
</Content>
</>
);
}
renderTags(tags) {
let tagDocuments = []
let tagArr = tags.split(',');
for (let i = 0; i < tagArr.length; i++) {
if (tags[i] === '-') {
continue;
}
tagDocuments.push(<Tag key={tagArr[i]}>{tagArr[i]}</Tag>)
}
return tagDocuments;
}
}
export default Asset;

View File

@ -1,4 +1,4 @@
import React, {useState} from 'react';
import React, {useEffect, useState} from 'react';
import {
Col,
Collapse,
@ -13,8 +13,8 @@ import {
Tooltip,
Typography
} from "antd/lib/index";
import {ExclamationCircleOutlined} from "@ant-design/icons";
import {isEmpty} from "../../utils/utils";
import request from "../../common/request";
const {TextArea} = Input;
const {Option} = Select;
@ -125,6 +125,30 @@ const AssetModal = function ({title, visible, handleOk, handleCancel, confirmLoa
model.accountType = v;
}
let [enableDrive, setEnableDrive] = useState(model['enable-drive']);
let [storages, setStorages] = useState([]);
useEffect(() => {
const getStorages = async () => {
const result = await request.get('/storages/shares');
if (result.code === 1) {
setStorages(result['data']);
}
}
getStorages();
}, []);
let [accessGateways,setAccessGateways] = useState([]);
useEffect(() => {
const getAccessGateways = async () => {
const result = await request.get('/access-gateways');
if (result.code === 1) {
setAccessGateways(result['data']);
}
}
getAccessGateways();
}, []);
return (
<Modal
@ -141,6 +165,7 @@ const AssetModal = function ({title, visible, handleOk, handleCancel, confirmLoa
.catch(info => {
});
}}
centered={true}
width={1040}
onCancel={handleCancel}
confirmLoading={confirmLoading}
@ -163,7 +188,7 @@ const AssetModal = function ({title, visible, handleOk, handleCancel, confirmLoa
<Input placeholder="资产的主机名称或者IP地址"/>
</Form.Item>
<Form.Item label="接入协议" name='protocol' rules={[{required: true, message: '请选择接入协议'}]}>
<Form.Item label="协议" name='protocol' rules={[{required: true, message: '请选择接入协议'}]}>
<Radio.Group onChange={handleProtocolChange}>
<Radio value="rdp">RDP</Radio>
<Radio value="ssh">SSH</Radio>
@ -233,14 +258,13 @@ const AssetModal = function ({title, visible, handleOk, handleCancel, confirmLoa
{
accountType === 'custom' ?
<>
<Form.Item label="授权账户" name='username'
noStyle={!(accountType === 'custom')}>
<Input placeholder="输入授权账户"/>
<input type='password' hidden={true} autoComplete='new-password'/>
<Form.Item label="授权账户" name='username'>
<Input autoComplete="off" placeholder="输入授权账户"/>
</Form.Item>
<Form.Item label="授权密码" name='password'
noStyle={!(accountType === 'custom')}>
<Input.Password placeholder="输入授权密码"/>
<Form.Item label="授权密码" name='password'>
<Input.Password autoComplete="off" placeholder="输入授权密码"/>
</Form.Item>
</>
: null
@ -266,6 +290,20 @@ const AssetModal = function ({title, visible, handleOk, handleCancel, confirmLoa
</>
}
<Form.Item label="接入网关" name='accessGatewayId' tooltip={'需要从接入网关才能访问的目标机器必选'}>
<Select onChange={() => null} allowClear={true}>
{accessGateways.map(item => {
return (
<Option key={item.id} value={item.id} placeholder={'需要从接入网关才能访问的目标机器必选'}>
<Tooltip placement="topLeft" title={item.name}>
{item.name}
</Tooltip>
</Option>
);
})}
</Select>
</Form.Item>
<Form.Item label="标签" name='tags'>
<Select mode="tags" placeholder="标签可以更加方便的检索资产">
{tags.map(tag => {
@ -282,7 +320,8 @@ const AssetModal = function ({title, visible, handleOk, handleCancel, confirmLoa
</Form.Item>
</Col>
<Col span={11}>
<Collapse defaultActiveKey={['remote-app', '认证', 'VNC中继', '模式设置']} ghost>
<Collapse defaultActiveKey={['remote-app', '认证', 'VNC中继', 'storage', '模式设置', '显示设置', '控制终端行为']}
ghost>
{
protocol === 'rdp' ?
<>
@ -297,35 +336,63 @@ const AssetModal = function ({title, visible, handleOk, handleCancel, confirmLoa
<Panel header={<Text strong>Remote App</Text>} key="remote-app">
<Form.Item
name="remote-app"
label={<Tooltip title="指定在远程桌面上启动的RemoteApp。
label='程序'
tooltip="指定在远程桌面上启动的RemoteApp。
如果您的远程桌面服务器支持该应用程序,则该应用程序(且仅该应用程序)对用户可见。
Windows需要对远程应用程序的名称使用特殊的符号。
远程应用程序的名称必须以两个竖条作为前缀。
例如如果您已经在您的服务器上为notepad.exe创建了一个远程应用程序并将其命名为“notepad”则您将该参数设置为:“||notepad”。">
程序&nbsp;<ExclamationCircleOutlined/>
</Tooltip>}
例如如果您已经在您的服务器上为notepad.exe创建了一个远程应用程序并将其命名为“notepad”则您将该参数设置为:“||notepad”。"
>
<Input type='text' placeholder="remote app"/>
</Form.Item>
<Form.Item
name="remote-app-dir"
label={<Tooltip
title="remote app的工作目录如果未配置remote app此参数无效。">工作目录&nbsp;
<ExclamationCircleOutlined/></Tooltip>}
label='工作目录'
tooltip='remote app的工作目录如果未配置remote app此参数无效。'
>
<Input type='text' placeholder="remote app的工作目录"/>
</Form.Item>
<Form.Item
name="remote-app-args"
label={<Tooltip title="remote app的命令行参数如果未配置remote app此参数无效。">参数&nbsp;
<ExclamationCircleOutlined/></Tooltip>}
label='参数'
tooltip='remote app的命令行参数如果未配置remote app此参数无效。'
>
<Input type='text' placeholder="remote app的命令行参数"/>
</Form.Item>
</Panel>
<Panel header={<Text strong>网络驱动</Text>} key="storage">
<Form.Item
name="enable-drive"
label="启用设备映射"
valuePropName="checked"
>
<Switch checkedChildren="开启" unCheckedChildren="关闭"
onChange={(checked, event) => {
setEnableDrive(checked);
}}/>
</Form.Item>
{
enableDrive ?
<Form.Item
name="drive-path"
label="网络驱动"
tooltip='用于文件传输的网络驱动,为空时使用操作人的默认空间'
>
<Select onChange={null} allowClear placeholder='为空时使用操作人的默认空间'>
{
storages.map(item => {
return <Option
value={item['id']}>{item['name']}</Option>
})
}
</Select>
</Form.Item> : undefined
}
</Panel>
</> : undefined
}
@ -335,16 +402,14 @@ Windows需要对远程应用程序的名称使用特殊的符号。
<Panel header={<Text strong>模式设置</Text>} key="">
<Form.Item
name="ssh-mode"
label={<Tooltip
title="guacd对部分SSH密钥支持不完善当密钥类型为ED25519时请选择原生模式。">连接模式&nbsp;
<ExclamationCircleOutlined/></Tooltip>}
label='连接模式'
initialValue=""
tooltip='guacd对部分SSH密钥支持不完善当密钥类型为ED25519时请选择原生模式。'
>
<Select onChange={(value) => {
setSshMode(value)
}}>
<Option value="">默认</Option>
<Option value="guacd">guacd</Option>
<Option value="">guacd</Option>
<Option value="naive">原生</Option>
</Select>
</Form.Item>
@ -448,15 +513,13 @@ Windows需要对远程应用程序的名称使用特殊的符号。
</Form.Item>
</Panel>
<Panel header={<Text strong>VNC中继</Text>} key="VNC">
<Form.Item label={<Tooltip
title="连接到VNC代理例如UltraVNC Repeater时要请求的目标主机。">目标主机&nbsp;
<ExclamationCircleOutlined/></Tooltip>}
<Form.Item label='目标主机'
tooltip='连接到VNC代理例如UltraVNC Repeater时要请求的目标主机。'
name='dest-host'>
<Input placeholder="目标主机"/>
</Form.Item>
<Form.Item label={<Tooltip
title="连接到VNC代理例如UltraVNC Repeater时要请求的目标端口。">目标端口&nbsp;
<ExclamationCircleOutlined/></Tooltip>}
<Form.Item label='目标端口'
tooltip='连接到VNC代理例如UltraVNC Repeater时要请求的目标端口。'
name='dest-port'>
<Input type='number' min={1} max={65535}
placeholder='目标端口'/>

View File

@ -0,0 +1,324 @@
import React, {Component} from 'react';
import {
Button,
Card,
Col,
Descriptions,
Divider,
Input,
Layout,
List,
Row,
Select,
Space,
Tag,
Tooltip,
Typography
} from "antd";
import {
CheckCircleOutlined,
CodeOutlined,
DesktopOutlined,
ExclamationCircleOutlined,
SyncOutlined,
TagsOutlined,
UndoOutlined
} from "@ant-design/icons";
import request from "../../common/request";
import {message} from "antd/es";
import qs from "qs";
const {Content} = Layout;
const {Title} = Typography;
const {Search} = Input;
class MyAsset extends Component {
inputRefOfName = React.createRef();
inputRefOfIp = React.createRef();
state = {
items: [],
tags: [],
queryParams: {
pageIndex: 1,
pageSize: 8,
protocol: '',
tags: ''
},
loading: false,
}
async componentDidMount() {
this.loadTableData();
let result = await request.get('/tags');
if (result['code'] === 1) {
this.setState({
tags: result['data']
})
}
}
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('/account/assets?' + 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
});
}
}
renderTags(tags) {
let tagDocuments = []
let tagArr = tags.split(',');
for (let i = 0; i < tagArr.length; i++) {
if (tags[i] === '-') {
continue;
}
tagDocuments.push(<Tag key={tagArr[i]}>{tagArr[i]}</Tag>)
}
return tagDocuments;
}
handleSearchByName = name => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'name': name,
}
this.loadTableData(query);
};
handleSearchByIp = ip => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'ip': ip,
}
this.loadTableData(query);
};
handleTagsChange = tags => {
this.setState({
selectedTags: tags
})
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'tags': tags.join(','),
}
this.loadTableData(query);
}
handleSearchByProtocol = protocol => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'protocol': protocol,
}
this.loadTableData(query);
}
handleChangPage = async (pageIndex, pageSize) => {
let queryParams = this.state.queryParams;
queryParams.pageIndex = pageIndex;
queryParams.pageSize = pageSize;
this.setState({
queryParams: queryParams
});
await this.loadTableData(queryParams)
};
handleTableChange = (pagination, filters, sorter) => {
let query = {
...this.state.queryParams,
'order': sorter.order,
'field': sorter.field
}
this.loadTableData(query);
}
render() {
return (
<div style={{marginTop: 20}}>
<Content style={{background: 'white', padding: 24}}>
<div>
<Row justify="space-around" align="middle" gutter={24}>
<Col span={4} key={1}>
<Title level={3}>我的资产</Title>
</Col>
<Col span={20} key={2} style={{textAlign: 'right'}}>
<Space>
<Search
ref={this.inputRefOfName}
placeholder="资产名称"
allowClear
onSearch={this.handleSearchByName}
style={{width: 200}}
/>
<Search
ref={this.inputRefOfIp}
placeholder="资产IP"
allowClear
onSearch={this.handleSearchByIp}
style={{width: 200}}
/>
<Select mode="multiple"
allowClear
value={this.state.selectedTags}
placeholder="资产标签" onChange={this.handleTagsChange}
style={{minWidth: 150}}>
{this.state.tags.map(tag => {
if (tag === '-') {
return undefined;
}
return (<Select.Option key={tag}>{tag}</Select.Option>)
})}
</Select>
<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.Option value="kubernetes">kubernetes</Select.Option>
</Select>
<Tooltip title='重置查询'>
<Button icon={<UndoOutlined/>} onClick={() => {
this.inputRefOfName.current.setValue('');
this.inputRefOfIp.current.setValue('');
this.setState({
selectedTags: []
})
this.loadTableData({pageIndex: 1, pageSize: 10, protocol: '', tags: ''})
}}>
</Button>
</Tooltip>
<Divider type="vertical"/>
<Tooltip title="刷新列表">
<Button icon={<SyncOutlined/>} onClick={() => {
this.loadTableData(this.state.queryParams)
}}>
</Button>
</Tooltip>
</Space>
</Col>
</Row>
</div>
</Content>
<div style={{marginTop: 20}}>
<List
loading={this.state.loading}
grid={{gutter: 16, column: 4}}
dataSource={this.state.items}
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}`,
pageSizeOptions: [8, 16, 32, 64, 128]
}}
renderItem={item => {
const id = item['id'];
const protocol = item['protocol'];
const name = item['name'];
const sshMode = item['sshMode'];
let url = '';
if (protocol === 'ssh' && sshMode === 'naive') {
url = `#/term?assetId=${id}&assetName=${name}`;
} else {
url = `#/access?assetId=${id}&assetName=${name}&protocol=${protocol}`;
}
return (
<List.Item>
<a target='_blank' href={url}>
<Card title={item['name']}
hoverable
extra={item['active'] ?
<Tag icon={<CheckCircleOutlined/>} color="success">
运行中
</Tag> : <Tag icon={<ExclamationCircleOutlined/>} color="error">
不可用
</Tag>}
actions={[]}>
<Descriptions title="" column={1}>
<Descriptions.Item label={<div><CodeOutlined/> 资产协议</div>}>
<strong>{item['protocol']}</strong>
</Descriptions.Item>
<Descriptions.Item label={<div><DesktopOutlined/> 主机地址</div>}>
<strong>{item['ip'] + ':' + item['port']}</strong>
</Descriptions.Item>
<Descriptions.Item label={<div><TagsOutlined/> 标签</div>}>
<strong>{this.renderTags(item['tags'])}</strong>
</Descriptions.Item>
</Descriptions>
</Card>
</a>
</List.Item>
)
}}
/>
</div>
</div>
);
}
}
export default MyAsset;

View File

@ -0,0 +1,72 @@
import React, {Component} from 'react';
import FileSystem from "../devops/FileSystem";
import {Col, Descriptions, Layout, Row, Typography,} from "antd";
import {getCurrentUser, isAdmin} from "../../service/permission";
import {FireOutlined, HeartOutlined} from "@ant-design/icons";
import {renderSize} from "../../utils/utils";
import request from "../../common/request";
import {message} from "antd/es";
const {Content} = Layout;
const {Title} = Typography;
class MyFile extends Component {
state = {
storage: {}
}
componentDidMount() {
this.getDefaultStorage();
}
getDefaultStorage = async () => {
let result = await request.get(`/account/storage`);
if (result.code !== 1) {
message.error(result['message']);
return;
}
this.setState({
storage: result['data']
})
}
render() {
let storage = this.state.storage;
let contentClassName = isAdmin() ? 'page-content' : 'page-content-user';
return (
<div>
<Content key='page-content' className={["site-layout-background", contentClassName]}>
<div style={{marginBottom: 20}}>
<Row justify="space-around" align="middle" gutter={24}>
<Col span={16} key={1}>
<Title level={3}>我的文件</Title>
</Col>
<Col span={8} key={2} style={{textAlign: 'right'}}>
<Descriptions title="" column={2}>
<Descriptions.Item label={<div><FireOutlined/> 大小限制</div>}>
<strong>{storage['limitSize'] < 0 ? '无限制' : renderSize(storage['limitSize'])}</strong>
</Descriptions.Item>
<Descriptions.Item label={<div><HeartOutlined/> 已用大小</div>}>
<strong>{renderSize(storage['usedSize'])}</strong>
</Descriptions.Item>
</Descriptions>
</Col>
</Row>
</div>
<FileSystem storageId={getCurrentUser()['id']}
storageType={'storages'}
callback={this.getDefaultStorage}
upload={true}
download={true}
delete={true}
rename={true}
edit={true}
minHeight={window.innerHeight - 203}/>
</Content>
</div>
);
}
}
export default MyFile;

View File

@ -1,10 +1,10 @@
import React, {Component} from 'react';
import {Card, Input, List, Spin} from "antd";
import Console from "../access/Console";
import BatchCommandTerm from "../access/BatchCommandTerm";
import './Command.css'
import request from "../../common/request";
import {message} from "antd/es";
import Message from "../access/Message";
const {Search} = Input;
@ -63,10 +63,7 @@ class BatchCommand extends Component {
for (let i = 0; i < this.state.webSockets.length; i++) {
let ws = this.state.webSockets[i]['ws'];
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'data',
content: value + String.fromCharCode(13)
}));
ws.send(new Message(Message.Data, value + String.fromCharCode(13)).toString());
}
}
this.commandRef.current.setValue('');
@ -80,7 +77,7 @@ class BatchCommand extends Component {
renderItem={item => (
<List.Item>
<Card title={item.name}
className={this.state.active === item['id'] ? 'command-active' : ''}
className={['console-card',this.state.active === item['id'] ? 'command-active' : '']}
onClick={() => {
if (this.state.active === item['id']) {
this.setState({
@ -93,10 +90,9 @@ class BatchCommand extends Component {
}
}}
>
<Console assetId={item.id} command={this.state.command}
width={(window.innerWidth - 350) / 2}
height={420}
appendWebsocket={this.appendWebsocket}/>
<BatchCommandTerm assetId={item.id}
command={this.state.command}
appendWebsocket={this.appendWebsocket}/>
</Card>
</List.Item>
)}

View File

@ -20,7 +20,7 @@ import request from "../../common/request";
import {message} from "antd/es";
import {PlusOutlined, SyncOutlined, UndoOutlined} from '@ant-design/icons';
import {SyncOutlined, UndoOutlined} from '@ant-design/icons';
import {PROTOCOL_COLORS} from "../../common/constants";
import {isEmpty} from "../../utils/utils";
import dayjs from "dayjs";
@ -172,6 +172,10 @@ class ChooseAsset extends Component {
selectedRowKeys: selectedRowKeys,
totalSelectedRows: totalSelectedRows
})
if (this.checkedAssets) {
this.checkedAssets(totalSelectedRows);
}
console.log(totalSelectedRows);
}
render() {
@ -236,13 +240,13 @@ class ChooseAsset extends Component {
if (text) {
return (
<Tooltip title='运行中'>
<Badge status="processing"/>
<Badge status="processing" text='运行中'/>
</Tooltip>
)
} else {
return (
<Tooltip title='不可用'>
<Badge status="error"/>
<Badge status="error" text='不可用'/>
</Tooltip>
)
}
@ -267,26 +271,32 @@ class ChooseAsset extends Component {
const selectedRowKeys = this.state.selectedRowKeys;
const rowSelection = {
selectedRowKeys: this.state.selectedRowKeys,
selectedRowKeys: selectedRowKeys,
onChange: (selectedRowKeys, selectedRows) => {
this.setState({selectedRowKeys, selectedRows});
let totalSelectedRows = this.state.totalSelectedRows;
let totalSelectedRowKeys = totalSelectedRows.map(item => item['id']);
for (let i = 0; i < selectedRows.length; i++) {
let selectedRow = selectedRows[i];
if (totalSelectedRowKeys.includes(selectedRow['id'])) {
continue;
}
totalSelectedRows.push(selectedRow);
}
this.setState({
totalSelectedRows: totalSelectedRows
})
if (this.checkedAssets) {
this.checkedAssets(totalSelectedRows);
}
console.log(totalSelectedRows);
},
getCheckboxProps: (record) => ({
disabled: record['disabled'],
}),
};
let hasSelected = false;
if (selectedRowKeys.length > 0) {
let totalSelectedRows = this.state.totalSelectedRows;
let allSelectedRowKeys = totalSelectedRows.map(item => item['id']);
for (let i = 0; i < selectedRowKeys.length; i++) {
let selectedRowKey = selectedRowKeys[i];
if (!allSelectedRowKeys.includes(selectedRowKey)) {
hasSelected = true;
break;
}
}
}
return (
<>
@ -366,34 +376,6 @@ class ChooseAsset extends Component {
</Button>
</Tooltip>
<Tooltip title="添加选择">
<Button type="primary" disabled={!hasSelected} icon={<PlusOutlined/>}
onClick={async () => {
console.log(this.state.selectedRows)
let totalSelectedRows = this.state.totalSelectedRows;
let totalSelectedRowKeys = totalSelectedRows.map(item => item['id']);
let selectedRows = this.state.selectedRows;
let newRowKeys = []
for (let i = 0; i < selectedRows.length; i++) {
let selectedRow = selectedRows[i];
if (totalSelectedRowKeys.includes(selectedRow['id'])) {
continue;
}
totalSelectedRows.push(selectedRow);
newRowKeys.push(selectedRow['id']);
}
this.setState({
totalSelectedRows: totalSelectedRows
})
if (this.checkedAssets) {
this.checkedAssets(totalSelectedRows);
}
}}>
</Button>
</Tooltip>
</Space>
</Col>
</Row>

View File

@ -1,4 +1,4 @@
.command-active {
box-shadow: 0 0 0 2px red;
outline: 2px solid red;
box-shadow: 0 0 0 2px #1890ff;
outline: 2px solid #1890ff;
}

View File

@ -1,7 +1,6 @@
import React, {Component} from 'react';
import {
Alert,
Button,
Col,
Divider,
@ -16,7 +15,6 @@ import {
Space,
Table,
Tooltip,
Transfer,
Typography
} from "antd";
import qs from "qs";
@ -66,12 +64,9 @@ class DynamicCommand extends Component {
selectedRowKeys: [],
delBtnLoading: false,
changeOwnerModalVisible: false,
changeSharerModalVisible: false,
changeOwnerConfirmLoading: false,
changeSharerConfirmLoading: false,
users: [],
selected: {},
selectedSharers: [],
indeterminate: true,
checkAllChecked: false
};
@ -86,7 +81,7 @@ class DynamicCommand extends Component {
message.success('删除成功');
this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
message.error(result.message, 10);
}
}
@ -264,7 +259,7 @@ class DynamicCommand extends Component {
})
await this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
message.error(result.message, 10);
}
} finally {
this.setState({
@ -289,59 +284,6 @@ class DynamicCommand extends Component {
})
}
handleSharersChange = async targetKeys => {
this.setState({
selectedSharers: targetKeys
})
}
handleShowSharer = async (record) => {
let r1 = this.handleSearchByNickname('');
let r2 = request.get(`/resource-sharers/sharers?resourceId=${record['id']}`);
await r1;
let result = await r2;
let selectedSharers = [];
if (result['code'] !== 1) {
message.error(result['message']);
} else {
selectedSharers = result['data'];
}
let users = this.state.users;
users = users.map(item => {
let disabled = false;
if (record['owner'] === item['id']) {
disabled = true;
}
return {...item, 'disabled': disabled}
});
this.setState({
selectedSharers: selectedSharers,
selected: record,
changeSharerModalVisible: true,
users: users
})
}
onCheckAllChange = (event) => {
this.setState({
checkedAssets: event.target.checked ? this.state.assets.map(item => item['id']) : [],
indeterminate: false,
checkAllChecked: event.target.checked
})
}
onChange = (list) => {
this.setState({
checkedAssets: list,
indeterminate: !!list.length && list.length < this.state.assets.length,
checkAllChecked: list.length === this.state.assets.length
})
}
handleTableChange = (pagination, filters, sorter) => {
let query = {
...this.state.queryParams,
@ -426,87 +368,76 @@ class DynamicCommand extends Component {
const menu = (
<Menu>
<Menu.Item key="0">
<Menu.Item key="1">
<Button type="text" size='small'
disabled={!hasPermission(record['owner'])}
onClick={() => this.showModal('更新指令', record)}>编辑</Button>
</Menu.Item>
{isAdmin() ?
<Menu.Item key="1">
<Button type="text" size='small'
disabled={!hasPermission(record['owner'])}
onClick={() => {
this.handleSearchByNickname('')
.then(() => {
this.setState({
changeOwnerModalVisible: true,
selected: record,
})
this.changeOwnerFormRef
.current
.setFieldsValue({
owner: record['owner']
})
});
}}>更换所有者</Button>
</Menu.Item> : undefined
}
<Menu.Item key="2">
<Button type="text" size='small'
disabled={!hasPermission(record['owner'])}
onClick={async () => {
await this.handleShowSharer(record);
}}>更新授权人</Button>
</Menu.Item>
onClick={() => {
this.handleSearchByNickname('')
.then(() => {
this.setState({
changeOwnerModalVisible: true,
selected: record,
})
this.changeOwnerFormRef
.current
.setFieldsValue({
owner: record['owner']
})
});
}}>更换所有者</Button>
</Menu.Item>
<Menu.Divider/>
<Menu.Item key="3">
<Button type="text" size='small' danger
disabled={!hasPermission(record['owner'])}
onClick={() => this.showDeleteConfirm(record.id, record.name)}>删除</Button>
</Menu.Item>
</Menu>
);
return (
<div>
<Button type="link" size='small' onClick={async () => {
let action;
if (isAdmin()) {
action = (
<div>
<Button type="link" size='small' onClick={async () => {
this.setState({
assetsVisible: true,
commandId: record['id']
});
}}>执行</Button>
this.setState({
assetsVisible: true,
commandId: record['id']
});
}}>执行</Button>
<Dropdown overlay={menu}>
<Button type="link" size='small'>
更多 <DownOutlined/>
</Button>
</Dropdown>
<Dropdown overlay={menu}>
<Button type="link" size='small'>
更多 <DownOutlined/>
</Button>
</Dropdown>
</div>
)
} else {
action = (
<div>
<Button type="link" size='small'
disabled={!hasPermission(record['owner'])}
onClick={() => this.showModal('更新指令', record)}>编辑</Button>
</div>
)
<Button type="link" size='small' danger
disabled={!hasPermission(record['owner'])}
onClick={() => this.showDeleteConfirm(record.id, record.name)}>删除</Button>
</div>
)
}
return action;
},
}
];
if (isAdmin()) {
columns.splice(4, 0, {
title: '授权人数',
dataIndex: 'sharerCount',
key: 'sharerCount',
render: (text, record, index) => {
return <Button type='link' onClick={async () => {
await this.handleShowSharer(record, true);
}}>{text}</Button>
}
});
}
const selectedRowKeys = this.state.selectedRowKeys;
const rowSelection = {
selectedRowKeys: this.state.selectedRowKeys,
@ -516,9 +447,11 @@ class DynamicCommand extends Component {
};
const hasSelected = selectedRowKeys.length > 0;
let contentClassName = isAdmin() ? 'page-content' : 'page-content-user';
return (
<>
<Content className="site-layout-background page-content">
<Content className={["site-layout-background", contentClassName]}>
<div style={{marginBottom: 20}}>
<Row justify="space-around" align="middle" gutter={24}>
@ -647,7 +580,6 @@ class DynamicCommand extends Component {
<ChooseAsset
setCheckedAssets={this.setCheckedAssets}
>
</ChooseAsset>
</Modal>
@ -706,70 +638,8 @@ class DynamicCommand extends Component {
value={d.id}>{d.nickname}</Select.Option>)}
</Select>
</Form.Item>
<Alert message="更换资产所有者不会影响授权凭证的所有者" type="info" showIcon/>
</Form>
</Modal>
{
this.state.changeSharerModalVisible ?
<Modal title={<Text>更新资源<strong
style={{color: '#1890ff'}}>{this.state.selected['name']}</strong>
</Text>}
visible={this.state.changeSharerModalVisible}
confirmLoading={this.state.changeSharerConfirmLoading}
onOk={async () => {
this.setState({
changeSharerConfirmLoading: true
});
let changeSharerModalVisible = false;
let result = await request.post(`/resource-sharers/overwrite-sharers`, {
resourceId: this.state.selected['id'],
resourceType: 'command',
userIds: this.state.selectedSharers
});
if (result['code'] === 1) {
message.success('操作成功');
this.loadTableData();
} else {
message.error(result['message'], 10);
changeSharerModalVisible = true;
}
this.setState({
changeSharerConfirmLoading: false,
changeSharerModalVisible: changeSharerModalVisible
})
}}
onCancel={() => {
this.setState({
changeSharerModalVisible: false
})
}}
okButtonProps={{disabled: !hasPermission(this.state.selected['owner'])}}
>
<Transfer
dataSource={this.state.users}
disabled={!hasPermission(this.state.selected['owner'])}
showSearch
titles={['未授权', '已授权']}
operations={['授权', '移除']}
listStyle={{
width: 250,
height: 300,
}}
targetKeys={this.state.selectedSharers}
onChange={this.handleSharersChange}
render={item => `${item.nickname}`}
/>
</Modal> : undefined
}
</Content>
</>
);

View File

@ -16,7 +16,6 @@ import {
Table,
Tag,
Tooltip,
Transfer,
Typography
} from "antd";
import qs from "qs";
@ -32,7 +31,7 @@ import {
UndoOutlined
} from '@ant-design/icons';
import {hasPermission, isAdmin} from "../../service/permission";
import {hasPermission} from "../../service/permission";
import dayjs from "dayjs";
const confirm = Modal.confirm;
@ -60,12 +59,9 @@ class Credential extends Component {
selectedRowKeys: [],
delBtnLoading: false,
changeOwnerModalVisible: false,
changeSharerModalVisible: false,
changeOwnerConfirmLoading: false,
changeSharerConfirmLoading: false,
users: [],
selected: {},
selectedSharers: [],
};
componentDidMount() {
@ -78,7 +74,7 @@ class Credential extends Component {
message.success('删除成功');
await this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
message.error(result.message, 10);
}
}
@ -215,7 +211,7 @@ class Credential extends Component {
});
await this.loadTableData(this.state.queryParams);
} else {
message.error('操作失败 :( ' + result.message, 10);
message.error(result.message, 10);
}
} else {
// 向后台提交数据
@ -228,7 +224,7 @@ class Credential extends Component {
});
await this.loadTableData(this.state.queryParams);
} else {
message.error('操作失败 :( ' + result.message, 10);
message.error(result.message, 10);
}
}
@ -250,7 +246,7 @@ class Credential extends Component {
})
await this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
message.error(result.message, 10);
}
} finally {
this.setState({
@ -281,37 +277,6 @@ class Credential extends Component {
})
}
handleShowSharer = async (record) => {
let r1 = this.handleSearchByNickname('');
let r2 = request.get(`/resource-sharers/sharers?resourceId=${record['id']}`);
await r1;
let result = await r2;
let selectedSharers = [];
if (result['code'] !== 1) {
message.error(result['message']);
} else {
selectedSharers = result['data'];
}
let users = this.state.users;
users = users.map(item => {
let disabled = false;
if (record['owner'] === item['id']) {
disabled = true;
}
return {...item, 'disabled': disabled}
});
this.setState({
selectedSharers: selectedSharers,
selected: record,
changeSharerModalVisible: true,
users: users
})
}
handleTableChange = (pagination, filters, sorter) => {
let query = {
...this.state.queryParams,
@ -402,37 +367,25 @@ class Credential extends Component {
const menu = (
<Menu>
{isAdmin() ?
<Menu.Item key="1">
<Button type="text" size='small'
disabled={!hasPermission(record['owner'])}
onClick={() => {
this.handleSearchByNickname('')
.then(() => {
this.setState({
changeOwnerModalVisible: true,
selected: record,
})
this.changeOwnerFormRef
.current
.setFieldsValue({
owner: record['owner']
})
});
}}>更换所有者</Button>
</Menu.Item> : undefined
}
<Menu.Item key="2">
<Menu.Item key="1">
<Button type="text" size='small'
disabled={!hasPermission(record['owner'])}
onClick={async () => {
await this.handleShowSharer(record);
}}>更新授权人</Button>
</Menu.Item>
onClick={() => {
this.handleSearchByNickname('')
.then(() => {
this.setState({
changeOwnerModalVisible: true,
selected: record,
})
this.changeOwnerFormRef
.current
.setFieldsValue({
owner: record['owner']
})
});
}}>更换所有者</Button>
</Menu.Item>
<Menu.Divider/>
<Menu.Item key="3">
<Button type="text" size='small' danger
@ -458,19 +411,6 @@ class Credential extends Component {
}
];
if (isAdmin()) {
columns.splice(5, 0, {
title: '授权人数',
dataIndex: 'sharerCount',
key: 'sharerCount',
render: (text, record, index) => {
return <Button type='link' onClick={async () => {
await this.handleShowSharer(record);
}}>{text}</Button>
}
});
}
const selectedRowKeys = this.state.selectedRowKeys;
const rowSelection = {
selectedRowKeys: this.state.selectedRowKeys,
@ -645,64 +585,6 @@ class Credential extends Component {
</Form>
</Modal>
{
this.state.changeSharerModalVisible ?
<Modal title={<Text>更新资源<strong
style={{color: '#1890ff'}}>{this.state.selected['name']}</strong>
</Text>}
visible={this.state.changeSharerModalVisible}
confirmLoading={this.state.changeSharerConfirmLoading}
onOk={async () => {
this.setState({
changeSharerConfirmLoading: true
});
let changeSharerModalVisible = false;
let result = await request.post(`/resource-sharers/overwrite-sharers`, {
resourceId: this.state.selected['id'],
resourceType: 'credential',
userIds: this.state.selectedSharers
});
if (result['code'] === 1) {
message.success('操作成功');
this.loadTableData();
} else {
message.error(result['message'], 10);
changeSharerModalVisible = true;
}
this.setState({
changeSharerConfirmLoading: false,
changeSharerModalVisible: changeSharerModalVisible
})
}}
onCancel={() => {
this.setState({
changeSharerModalVisible: false
})
}}
okButtonProps={{disabled: !hasPermission(this.state.selected['owner'])}}
>
<Transfer
dataSource={this.state.users}
disabled={!hasPermission(this.state.selected['owner'])}
showSearch
titles={['未授权', '已授权']}
operations={['授权', '移除']}
listStyle={{
width: 250,
height: 300,
}}
targetKeys={this.state.selectedSharers}
onChange={this.handleSharersChange}
render={item => `${item.nickname}`}
/>
</Modal> : undefined
}
</Content>
</>
);

View File

@ -98,6 +98,7 @@ const CredentialModal = ({title, visible, handleOk, handleCancel, confirmLoading
</>
:
<>
<input type='password' hidden={true} autoComplete='new-password'/>
<Form.Item label="授权账户" name='username'>
<Input placeholder="输入授权账户"/>
</Form.Item>

View File

@ -1,23 +1,29 @@
import React, {Component} from 'react';
import {Card, Col, Radio, Row, Statistic} from "antd";
import {Card, Col, Row, Statistic} from "antd";
import {DesktopOutlined, IdcardOutlined, LinkOutlined, UserOutlined} from '@ant-design/icons';
import request from "../../common/request";
import './Dashboard.css'
import {Link} from "react-router-dom";
import {Area} from '@ant-design/charts';
import {isAdmin} from "../../service/permission";
import {Bar, Pie} from '@ant-design/charts';
class Dashboard extends Component {
state = {
counter: {},
d: 'w',
session: [],
asset: {
"ssh": 0,
"rdp": 0,
"vnc": 0,
"telnet": 0,
"kubernetes": 0,
},
access: []
}
componentDidMount() {
this.getCounter();
this.getD();
this.getAsset();
this.getAccess();
}
componentWillUnmount() {
@ -33,41 +39,91 @@ class Dashboard extends Component {
}
}
getD = async () => {
let result = await request.get('/overview/sessions?d=' + this.state.d);
getAsset = async () => {
let result = await request.get('/overview/asset');
if (result['code'] === 1) {
this.setState({
session: result['data']
asset: result['data']
})
}
}
handleChangeD = (e) => {
let d = e.target.value;
this.setState({
d: d
}, () => this.getD())
}
handleLinkClick = (e) => {
if (!isAdmin()) {
e.preventDefault();
getAccess = async () => {
let result = await request.get('/overview/access');
if (result['code'] === 1) {
this.setState({
access: result['data']
})
}
}
render() {
const data = [
{
type: 'RDP',
value: this.state.asset['rdp'],
},
{
type: 'SSH',
value: this.state.asset['ssh'],
},
{
type: 'TELNET',
value: this.state.asset['telnet'],
},
{
type: 'VNC',
value: this.state.asset['vnc'],
},
{
type: 'Kubernetes',
value: this.state.asset['kubernetes'],
}
];
const config = {
data: this.state.session,
xField: 'day',
yField: 'count',
seriesField: 'protocol',
appendPadding: 10,
data: data,
angleField: 'value',
colorField: 'type',
radius: 1,
innerRadius: 0.6,
label: {
type: 'inner',
offset: '-50%',
content: '{value}',
style: {
textAlign: 'center',
fontSize: 14,
},
},
interactions: [{type: 'element-selected'}, {type: 'element-active'}],
statistic: {
title: false,
content: {
formatter: () => {
return '资产类型';
},
},
},
};
const buttonRadio = <Radio.Group value={this.state.d} onChange={this.handleChangeD}>
<Radio.Button value="w">按周</Radio.Button>
<Radio.Button value="m">按月</Radio.Button>
</Radio.Group>
let accessData = this.state.access.map(item=>{
return {
title: `${item['username']}@${item['ip']}:${item['port']}`,
// title: `${item['assetId']}`,
value: item['accessCount'],
protocol: item['protocol']
}
});
const accessConfig = {
data: accessData,
xField: 'value',
yField: 'title',
seriesField: 'protocol',
legend: { position: 'top-left' },
}
return (
<>
@ -75,24 +131,24 @@ class Dashboard extends Component {
<div style={{margin: 16, marginBottom: 0}}>
<Row gutter={16}>
<Col span={6}>
<Card bordered={true}>
<Link to={'/user'} onClick={this.handleLinkClick}>
<Card bordered={true} hoverable>
<Link to={'/user'}>
<Statistic title="在线用户" value={this.state.counter['user']}
prefix={<UserOutlined/>}/>
</Link>
</Card>
</Col>
<Col span={6}>
<Card bordered={true}>
<Card bordered={true} hoverable>
<Link to={'/asset'}>
<Statistic title="存活资产" value={this.state.counter['asset']}
<Statistic title="资产数量" value={this.state.counter['asset']}
prefix={<DesktopOutlined/>}/>
</Link>
</Card>
</Col>
<Col span={6}>
<Card bordered={true}>
<Link to={'/credential'}>
<Card bordered={true} hoverable>
<Link to={'/credential'} hoverable>
<Statistic title="授权凭证" value={this.state.counter['credential']}
prefix={<IdcardOutlined/>}/>
</Link>
@ -100,8 +156,8 @@ class Dashboard extends Component {
</Card>
</Col>
<Col span={6}>
<Card bordered={true}>
<Link to={'/online-session'} onClick={this.handleLinkClick}>
<Card bordered={true} hoverable>
<Link to={'/online-session'}>
<Statistic title="在线会话" value={this.state.counter['onlineSession']}
prefix={<LinkOutlined/>}/>
</Link>
@ -111,9 +167,18 @@ class Dashboard extends Component {
</div>
<div className="page-card">
<Card title="会话统计" bordered={true} extra={buttonRadio}>
<Area {...config} />
</Card>
<Row gutter={16}>
<Col span={12}>
<Card bordered={true} title="资产类型">
<Pie {...config} />
</Card>
</Col>
<Col span={12}>
<Card bordered={true} title="使用次数Top10 资产">
<Bar {...accessConfig} />
</Card>
</Col>
</Row>
</div>
</>

View File

@ -65,4 +65,28 @@
.popup li > i {
margin-right: 8px;
}
.fs-header {
align-items: center;
position: relative;
display: flex;
}
.fs-header-left{
flex: 1 1 0;
}
.fs-header-right{
text-align: right;
margin-left: 10px;
}
.fs-header-right-item {
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
vertical-align: middle;
height: 100%;
}

View File

@ -0,0 +1,839 @@
import React, {Component} from 'react';
import {
Button,
Card,
Form,
Input,
message,
Modal,
notification,
Popconfirm,
Progress,
Space,
Table,
Tooltip,
Typography
} from "antd";
import {
CloudUploadOutlined,
DeleteOutlined,
ExclamationCircleOutlined,
FileExcelOutlined,
FileImageOutlined,
FileMarkdownOutlined,
FileOutlined,
FilePdfOutlined,
FileTextOutlined,
FileWordOutlined,
FileZipOutlined,
FolderAddOutlined,
FolderTwoTone,
LinkOutlined,
ReloadOutlined,
UploadOutlined
} from "@ant-design/icons";
import qs from "qs";
import request from "../../common/request";
import {server} from "../../common/env";
import {download, getFileName, getToken, isEmpty, renderSize} from "../../utils/utils";
import './FileSystem.css';
import MonacoEditor from "react-monaco-editor";
const {Text} = Typography;
const confirm = Modal.confirm;
class FileSystem extends Component {
mkdirFormRef = React.createRef();
renameFormRef = React.createRef();
state = {
storageType: undefined,
storageId: undefined,
currentDirectory: '/',
currentDirectoryInput: '/',
files: [],
loading: false,
currentFileKey: undefined,
selectedRowKeys: [],
uploading: {},
callback: undefined,
minHeight: 280,
upload: false,
download: false,
delete: false,
rename: false,
edit: false,
editorVisible: false,
fileName: '',
fileContent: ''
}
componentDidMount() {
if (this.props.onRef) {
this.props.onRef(this);
}
this.setState({
storageId: this.props.storageId,
storageType: this.props.storageType,
callback: this.props.callback,
minHeight: this.props.minHeight,
upload: this.props.upload,
download: this.props.download,
delete: this.props.delete,
rename: this.props.rename,
edit: this.props.edit,
}, () => {
this.loadFiles(this.state.currentDirectory);
console.log(this.state)
});
}
reSetStorageId = (storageId) => {
this.setState({
storageId: storageId
}, () => {
this.loadFiles('/');
});
}
refresh = async () => {
this.loadFiles(this.state.currentDirectory);
if (this.state.callback) {
this.state.callback();
}
}
loadFiles = async (key) => {
this.setState({
loading: true
})
try {
if (isEmpty(key)) {
key = '/';
}
let formData = new FormData();
formData.append('dir', key);
let result = await request.post(`/${this.state.storageType}/${this.state.storageId}/ls`, formData);
if (result['code'] !== 1) {
message.error(result['message']);
return;
}
let data = result['data'];
const items = data.map(item => {
return {'key': item['path'], ...item}
});
if (key !== '/') {
items.splice(0, 0, {key: '..', name: '..', path: '..', isDir: true, disabled: true})
}
this.setState({
files: items,
currentDirectory: key,
currentDirectoryInput: key
})
} finally {
this.setState({
loading: false,
selectedRowKeys: []
})
}
}
handleCurrentDirectoryInputChange = (event) => {
this.setState({
currentDirectoryInput: event.target.value
})
}
handleCurrentDirectoryInputPressEnter = (event) => {
this.loadFiles(event.target.value);
}
handleUploadDir = () => {
let files = window.document.getElementById('dir-upload').files;
let uploadEndCount = 0;
const increaseUploadEndCount = () => {
uploadEndCount++;
return uploadEndCount;
}
for (let i = 0; i < files.length; i++) {
let relativePath = files[i]['webkitRelativePath'];
let dir = relativePath.substring(0, relativePath.length - files[i].name.length);
this.uploadFile(files[i], this.state.currentDirectory + '/' + dir, () => {
if (increaseUploadEndCount() === files.length) {
this.refresh();
}
});
}
}
handleUploadFile = () => {
let files = window.document.getElementById('file-upload').files;
let uploadEndCount = 0;
const increaseUploadEndCount = () => {
uploadEndCount++;
return uploadEndCount;
}
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (!file) {
return;
}
this.uploadFile(file, this.state.currentDirectory, () => {
if (increaseUploadEndCount() === files.length) {
this.refresh();
}
});
}
}
uploadFile = (file, dir, callback) => {
const {name, size} = file;
let url = `${server}/${this.state.storageType}/${this.state.storageId}/upload?X-Auth-Token=${getToken()}&dir=${dir}`
const key = name;
const xhr = new XMLHttpRequest();
let prevPercent = 0, percent = 0;
const uploadEnd = (success, message) => {
if (success) {
let description = (
<React.Fragment>
<div>{name}</div>
<div>{renderSize(size)} / {renderSize(size)}</div>
<Progress percent={100}/>
</React.Fragment>
);
notification.success({
key,
message: `上传成功`,
duration: 5,
description: description,
placement: 'bottomRight'
});
if (callback) {
callback();
}
} else {
let description = (
<React.Fragment>
<div>{name}</div>
<Text type="danger">{message}</Text>
</React.Fragment>
);
notification.error({
key,
message: `上传失败`,
duration: 10,
description: description,
placement: 'bottomRight'
});
}
}
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
let description = (
<React.Fragment>
<div>{name}</div>
<div>{renderSize(event.loaded)}/{renderSize(size)}</div>
<Progress percent={99}/>
</React.Fragment>
);
if (event.loaded === event.total) {
notification.info({
key,
message: `向目标机器传输中...`,
duration: null,
description: description,
placement: 'bottomRight',
onClose: () => {
xhr.abort();
message.info(`您已取消上传"${name}"`, 10);
}
});
return;
}
percent = Math.min(Math.floor(event.loaded * 100 / event.total), 99);
if (prevPercent === percent) {
return;
}
description = (
<React.Fragment>
<div>{name}</div>
<div>{renderSize(event.loaded)} / {renderSize(size)}</div>
<Progress percent={percent}/>
</React.Fragment>
);
notification.info({
key,
message: `上传中...`,
duration: null,
description: description,
placement: 'bottomRight',
onClose: () => {
xhr.abort();
message.info(`您已取消上传"${name}"`, 10);
}
});
prevPercent = percent;
}
}, false)
xhr.onreadystatechange = (data) => {
if (xhr.readyState !== 4) {
return;
}
if (xhr.status >= 200 && xhr.status < 300) {
const responseText = data.currentTarget.responseText
let result;
try {
result = JSON.parse(responseText)
} catch (e) {
result = {}
}
if (result.code !== 1) {
uploadEnd(false, result['message']);
} else {
uploadEnd(true, result['message']);
}
} else if (xhr.status >= 400 && xhr.status < 500) {
uploadEnd(false, '服务器内部错误');
}
}
xhr.onerror = () => {
uploadEnd(false, '服务器内部错误');
}
xhr.open('POST', url, true);
let formData = new FormData();
formData.append("file", file, name);
xhr.send(formData);
}
delete = async (key) => {
let formData = new FormData();
formData.append('file', key);
let result = await request.post(`/${this.state.storageType}/${this.state.storageId}/rm`, formData);
if (result['code'] !== 1) {
message.error(result['message']);
}
}
showEditor = async (name, key) => {
message.loading({key: key, content: 'Loading'})
let fileContent = await request.get(`${server}/${this.state.storageType}/${this.state.storageId}/download?file=${window.encodeURIComponent(key)}`);
this.setState({
currentFileKey: key,
fileName: name,
fileContent: fileContent + "",
editorVisible: true
})
message.destroy(key);
}
hideEditor = () => {
this.setState({
editorVisible: false,
fileName: '',
fileContent: '',
currentFileKey: ''
})
}
edit = async () => {
this.setState({
confirmLoading: true
})
let url = `${server}/${this.state.storageType}/${this.state.storageId}/edit`
let formData = new FormData();
formData.append('file', this.state.currentFileKey);
formData.append('fileContent', this.state.fileContent);
let result = await request.post(url, formData);
if (result['code'] !== 1) {
message.error(result['message']);
} else {
this.setState({
confirmLoading: false
})
this.hideEditor();
}
}
render() {
const columns = [
{
title: '名称',
dataIndex: 'name',
key: 'name',
render: (value, item) => {
let icon;
if (item['isDir']) {
icon = <FolderTwoTone/>;
} else {
if (item['isLink']) {
icon = <LinkOutlined/>;
} else {
const fileExtension = item['name'].split('.').pop().toLowerCase();
switch (fileExtension) {
case "doc":
case "docx":
icon = <FileWordOutlined/>;
break;
case "xls":
case "xlsx":
icon = <FileExcelOutlined/>;
break;
case "bmp":
case "jpg":
case "jpeg":
case "png":
case "tif":
case "gif":
case "pcx":
case "tga":
case "exif":
case "svg":
case "psd":
case "ai":
case "webp":
icon = <FileImageOutlined/>;
break;
case "md":
icon = <FileMarkdownOutlined/>;
break;
case "pdf":
icon = <FilePdfOutlined/>;
break;
case "txt":
icon = <FileTextOutlined/>;
break;
case "zip":
case "gz":
case "tar":
case "tgz":
icon = <FileZipOutlined/>;
break;
default:
icon = <FileOutlined/>;
break;
}
}
}
return <span className={'dode'}>{icon}&nbsp;&nbsp;{item['name']}</span>;
},
sorter: (a, b) => {
if (a['key'] === '..') {
return 0;
}
if (b['key'] === '..') {
return 0;
}
return a.name.localeCompare(b.name);
},
sortDirections: ['descend', 'ascend'],
},
{
title: '大小',
dataIndex: 'size',
key: 'size',
render: (value, item) => {
if (!item['isDir'] && !item['isLink']) {
return <span className={'dode'}>{renderSize(value)}</span>;
}
return <span className={'dode'}/>;
},
sorter: (a, b) => {
if (a['key'] === '..') {
return 0;
}
if (b['key'] === '..') {
return 0;
}
return a.size - b.size;
},
}, {
title: '修改日期',
dataIndex: 'modTime',
key: 'modTime',
sorter: (a, b) => {
if (a['key'] === '..') {
return 0;
}
if (b['key'] === '..') {
return 0;
}
return a.modTime.localeCompare(b.modTime);
},
sortDirections: ['descend', 'ascend'],
render: (value, item) => {
return <span className={'dode'}>{value}</span>;
},
}, {
title: '属性',
dataIndex: 'mode',
key: 'mode',
render: (value, item) => {
return <span className={'dode'}>{value}</span>;
},
}, {
title: '操作',
dataIndex: 'action',
key: 'action',
width: 210,
render: (value, item) => {
if (item['key'] === '..') {
return undefined;
}
let disableDownload = !this.state.download;
let disableEdit = !this.state.edit;
if (item['isDir'] || item['isLink']) {
disableDownload = true;
disableEdit = true
}
return (
<>
<Button type="link" size='small' disabled={disableEdit}
onClick={() => this.showEditor(item['name'], item['key'])}>
编辑
</Button>
<Button type="link" size='small' disabled={disableDownload} onClick={async () => {
download(`${server}/${this.state.storageType}/${this.state.storageId}/download?file=${window.encodeURIComponent(item['key'])}&X-Auth-Token=${getToken()}`);
}}>
下载
</Button>
<Button type={'link'} size={'small'} disabled={!this.state.rename} onClick={() => {
this.setState({
renameVisible: true,
currentFileKey: item['key']
})
}}>重命名</Button>
<Popconfirm
title="您确认要删除此文件吗?"
onConfirm={async () => {
await this.delete(item['key']);
await this.refresh();
}}
okText="是"
cancelText="否"
>
<Button type={'link'} size={'small'} disabled={!this.state.delete} danger>删除</Button>
</Popconfirm>
</>
);
},
}
];
const {selectedRowKeys} = this.state;
const rowSelection = {
selectedRowKeys,
onChange: (selectedRowKeys) => {
this.setState({selectedRowKeys});
},
getCheckboxProps: (record) => ({
disabled: record['disabled'],
}),
};
let hasSelected = selectedRowKeys.length > 0;
if (hasSelected) {
if (!this.state.delete) {
hasSelected = false;
}
}
const title = (
<div className='fs-header'>
<div className='fs-header-left'>
<Input value={this.state.currentDirectoryInput} onChange={this.handleCurrentDirectoryInputChange}
onPressEnter={this.handleCurrentDirectoryInputPressEnter}/>
</div>
<div className='fs-header-right'>
<Space>
<div className='fs-header-right-item'>
<Tooltip title="创建文件夹">
<Button type="primary" size="small"
disabled={!this.state.upload}
icon={<FolderAddOutlined/>}
onClick={() => {
this.setState({
mkdirVisible: true
})
}} ghost/>
</Tooltip>
</div>
<div className='fs-header-right-item'>
<Tooltip title="上传文件">
<Button type="primary" size="small"
icon={<CloudUploadOutlined/>}
disabled={!this.state.upload}
onClick={() => {
window.document.getElementById('file-upload').click();
}} ghost/>
<input type="file" id="file-upload" style={{display: 'none'}}
onChange={this.handleUploadFile} multiple/>
</Tooltip>
</div>
<div className='fs-header-right-item'>
<Tooltip title="上传文件夹">
<Button type="primary" size="small"
icon={<UploadOutlined/>}
disabled={!this.state.upload}
onClick={() => {
window.document.getElementById('dir-upload').click();
}} ghost/>
<input type="file" id="dir-upload" style={{display: 'none'}}
onChange={this.handleUploadDir} webkitdirectory='' multiple/>
</Tooltip>
</div>
<div className='fs-header-right-item'>
<Tooltip title="刷新">
<Button type="primary" size="small"
icon={<ReloadOutlined/>}
onClick={this.refresh}
ghost/>
</Tooltip>
</div>
<div className='fs-header-right-item'>
<Tooltip title="批量删除">
<Button type="primary" size="small" ghost danger disabled={!hasSelected}
icon={<DeleteOutlined/>}
loading={this.state.delBtnLoading}
onClick={() => {
let rowKeys = this.state.selectedRowKeys;
const content = <div>
您确定要删除选中的<Text style={{color: '#1890FF'}}
strong>{rowKeys.length}</Text>
</div>;
confirm({
icon: <ExclamationCircleOutlined/>,
content: content,
onOk: async () => {
for (let i = 0; i < rowKeys.length; i++) {
if (rowKeys[i] === '..') {
continue;
}
await this.delete(rowKeys[i]);
}
this.refresh();
},
onCancel() {
},
});
}}>
</Button>
</Tooltip>
</div>
</Space>
</div>
</div>
);
return (
<div>
<Card title={title} bordered={true} size="small" style={{minHeight: this.state.minHeight}}>
<Table columns={columns}
rowSelection={rowSelection}
dataSource={this.state.files}
size={'small'}
pagination={false}
loading={this.state.loading}
onRow={record => {
return {
onDoubleClick: event => {
if (record['isDir'] || record['isLink']) {
if (record['path'] === '..') {
// 获取当前目录的上级目录
let currentDirectory = this.state.currentDirectory;
let parentDirectory = currentDirectory.substring(0, currentDirectory.lastIndexOf('/'));
this.loadFiles(parentDirectory);
} else {
this.loadFiles(record['path']);
}
} else {
}
},
};
}}
/>
</Card>
{
this.state.mkdirVisible ?
<Modal
title="创建文件夹"
visible={this.state.mkdirVisible}
okButtonProps={{form: 'mkdir-form', key: 'submit', htmlType: 'submit'}}
onOk={() => {
this.mkdirFormRef.current
.validateFields()
.then(async values => {
this.mkdirFormRef.current.resetFields();
let params = {
'dir': this.state.currentDirectory + '/' + values['dir']
}
let paramStr = qs.stringify(params);
this.setState({
confirmLoading: true
})
let result = await request.post(`/${this.state.storageType}/${this.state.storageId}/mkdir?${paramStr}`);
if (result.code === 1) {
message.success('创建成功');
this.loadFiles(this.state.currentDirectory);
} else {
message.error(result.message);
}
this.setState({
confirmLoading: false,
mkdirVisible: false
})
})
.catch(info => {
});
}}
confirmLoading={this.state.confirmLoading}
onCancel={() => {
this.setState({
mkdirVisible: false
})
}}
>
<Form ref={this.mkdirFormRef} id={'mkdir-form'}>
<Form.Item name='dir' rules={[{required: true, message: '请输入文件夹名称'}]}>
<Input autoComplete="off" placeholder="请输入文件夹名称"/>
</Form.Item>
</Form>
</Modal> : undefined
}
{
this.state.renameVisible ?
<Modal
title="重命名"
visible={this.state.renameVisible}
okButtonProps={{form: 'rename-form', key: 'submit', htmlType: 'submit'}}
onOk={() => {
this.renameFormRef.current
.validateFields()
.then(async values => {
this.renameFormRef.current.resetFields();
try {
let currentDirectory = this.state.currentDirectory;
if (!currentDirectory.endsWith("/")) {
currentDirectory += '/';
}
let params = {
'oldName': this.state.currentFileKey,
'newName': currentDirectory + values['newName'],
}
if (params['oldName'] === params['newName']) {
message.success('重命名成功');
return;
}
let paramStr = qs.stringify(params);
this.setState({
confirmLoading: true
})
let result = await request.post(`/${this.state.storageType}/${this.state.storageId}/rename?${paramStr}`);
if (result['code'] === 1) {
message.success('重命名成功');
this.refresh();
} else {
message.error(result.message);
}
} finally {
this.setState({
confirmLoading: false,
renameVisible: false
})
}
})
.catch(info => {
});
}}
confirmLoading={this.state.confirmLoading}
onCancel={() => {
this.setState({
renameVisible: false
})
}}
>
<Form id={'rename-form'}
ref={this.renameFormRef}
initialValues={{newName: getFileName(this.state.currentFileKey)}}>
<Form.Item name='newName' rules={[{required: true, message: '请输入新的名称'}]}>
<Input autoComplete="off" placeholder="新的名称"/>
</Form.Item>
</Form>
</Modal> : undefined
}
<Modal
title={"编辑 " + this.state.fileName}
className='modal-no-padding'
visible={this.state.editorVisible}
destroyOnClose={true}
width={window.innerWidth * 0.8}
centered={true}
okButtonProps={{form: 'rename-form', key: 'submit', htmlType: 'submit'}}
onOk={this.edit}
confirmLoading={this.state.confirmLoading}
onCancel={this.hideEditor}
>
<MonacoEditor
language="javascript"
height={window.innerHeight * 0.8}
theme="vs-dark"
value={this.state.fileContent}
options={{
selectOnLineNumbers: true
}}
editorDidMount={(editor, monaco) => {
console.log('editorDidMount', editor);
editor.focus();
}}
onChange={(newValue, e) => {
console.log('onChange', newValue, e);
this.setState(
{
fileContent: newValue
}
)
}}
/>
</Modal>
</div>
);
}
}
export default FileSystem;

View File

@ -70,7 +70,7 @@ class Job extends Component {
message.success('删除成功');
this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
message.error(result.message, 10);
}
}
@ -236,7 +236,7 @@ class Job extends Component {
})
await this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
message.error(result.message, 10);
}
} finally {
this.setState({

View File

@ -12,6 +12,7 @@ import {
Select,
Space,
Table,
Tag,
Tooltip,
Typography
} from "antd";
@ -47,7 +48,6 @@ class LoginLog extends Component {
componentDidMount() {
this.loadTableData();
this.handleSearchByNickname('');
}
async loadTableData(queryParams) {
@ -110,34 +110,22 @@ class LoginLog extends Component {
this.loadTableData(query);
}
handleChangeByProtocol = protocol => {
handleSearchByUsername = username => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'protocol': protocol,
'username': username,
}
this.loadTableData(query);
}
handleSearchByNickname = async nickname => {
const result = await request.get(`/users/paging?pageIndex=1&pageSize=1000&nickname=${nickname}`);
if (result.code !== 1) {
message.error(result.message, 10);
return;
}
this.setState({
users: result.data.items
})
}
handleChangeByUserId = userId => {
handleChangeByState = (state) => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'userId': userId,
'state': state,
}
this.loadTableData(query);
}
@ -155,7 +143,7 @@ class LoginLog extends Component {
})
await this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
message.error(result.message, 10);
}
} finally {
this.setState({
@ -174,13 +162,28 @@ class LoginLog extends Component {
return index + 1;
}
}, {
title: '用户昵称',
dataIndex: 'userName',
key: 'userName'
title: '登录账号',
dataIndex: 'username',
key: 'username'
}, {
title: '来源IP',
title: '登录IP',
dataIndex: 'clientIp',
key: 'clientIp'
}, {
title: '登录状态',
dataIndex: 'state',
key: 'state',
render: text => {
if (text === '0') {
return <Tag color="error">失败</Tag>
} else {
return <Tag color="success">成功</Tag>
}
}
}, {
title: '失败原因',
dataIndex: 'reason',
key: 'reason'
}, {
title: '浏览器',
dataIndex: 'clientUserAgent',
@ -243,7 +246,7 @@ class LoginLog extends Component {
} else {
notification['error']({
message: '提示',
description: '删除失败 :( ' + result.message,
description: result.message,
});
}
@ -264,9 +267,6 @@ class LoginLog extends Component {
};
const hasSelected = selectedRowKeys.length > 0;
const userOptions = this.state.users.map(d => <Select.Option key={d.id}
value={d.id}>{d.nickname}</Select.Option>);
return (
<>
<Content className="site-layout-background page-content">
@ -280,22 +280,27 @@ class LoginLog extends Component {
<Search
ref={this.inputRefOfClientIp}
placeholder="来源IP"
placeholder="登录账号"
allowClear
onSearch={this.handleSearchByUsername}
/>
<Search
ref={this.inputRefOfClientIp}
placeholder="登录IP"
allowClear
onSearch={this.handleSearchByClientIp}
/>
<Select
style={{width: 150}}
showSearch
value={this.state.queryParams.userId}
style={{width: 100}}
placeholder='用户昵称'
onSearch={this.handleSearchByNickname}
onChange={this.handleChangeByUserId}
filterOption={false}
allowClear
onChange={this.handleChangeByState}
defaultValue={''}
>
{userOptions}
<Select.Option value=''>全部状态</Select.Option>
<Select.Option value='1'>只看成功</Select.Option>
<Select.Option value='0'>只看失败</Select.Option>
</Select>
<Tooltip title='重置查询'>
@ -325,7 +330,7 @@ class LoginLog extends Component {
</Tooltip>
<Tooltip title="批量删除">
<Button type="primary" danger disabled={!hasSelected} icon={<DeleteOutlined/>}
<Button type="dashed" danger disabled={!hasSelected} icon={<DeleteOutlined/>}
loading={this.state.delBtnLoading}
onClick={() => {
const content = <div>
@ -334,7 +339,8 @@ class LoginLog extends Component {
</div>;
confirm({
icon: <ExclamationCircleOutlined/>,
content: content,
title: content,
content: '删除用户未注销的登录日志将会强制用户下线',
onOk: () => {
this.batchDelete()
},

View File

@ -5,7 +5,6 @@ import qs from "qs";
import request from "../../common/request";
import {message} from "antd/es";
import {DeleteOutlined, ExclamationCircleOutlined, PlusOutlined, SyncOutlined, UndoOutlined} from '@ant-design/icons';
import './Job.css'
import SecurityModal from "./SecurityModal";
const confirm = Modal.confirm;
@ -42,7 +41,7 @@ class Security extends Component {
message.success('删除成功');
this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
message.error(result.message, 10);
}
}
@ -188,7 +187,7 @@ class Security extends Component {
})
await this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
message.error(result.message, 10);
}
} finally {
this.setState({
@ -249,7 +248,7 @@ class Security extends Component {
return (
<div>
<Button type="link" size='small' loading={this.state.items[index]['execLoading']}
onClick={() => this.showModal('更新计划任务', record)}>编辑</Button>
onClick={() => this.showModal('更新', record)}>编辑</Button>
<Button type="text" size='small' danger
onClick={() => this.showDeleteConfirm(record.id, record.name)}>删除</Button>
@ -307,7 +306,6 @@ class Security extends Component {
</Button>
</Tooltip>
<Tooltip title="刷新列表">
<Button icon={<SyncOutlined/>} onClick={() => {
this.loadTableData(this.state.queryParams)

View File

@ -11,7 +11,7 @@ const SecurityModal = ({title, visible, handleOk, handleCancel, confirmLoading,
const [form] = Form.useForm();
if (model['priority'] === undefined) {
model['priority'] = 0;
model['priority'] = 1;
}
return (
@ -54,8 +54,8 @@ const SecurityModal = ({title, visible, handleOk, handleCancel, confirmLoading,
</Radio.Group>
</Form.Item>
<Form.Item label="优先级" name='priority' rules={[{required: true, message: '请输入优先级'}]}>
<InputNumber min={0} max={100}/>
<Form.Item label="优先级" name='priority' rules={[{required: true, message: '请输入优先级'}]} tooltip='数字越小代表优先级越高'>
<InputNumber min={1} max={100}/>
</Form.Item>
</Form>

View File

@ -0,0 +1,348 @@
import React, {Component} from 'react';
import {
Button,
Card,
Col,
Descriptions,
Divider,
Drawer,
Input,
Layout,
List,
Popconfirm,
Row,
Space,
Tooltip,
Typography
} from "antd";
import {
DeleteOutlined,
EditOutlined,
FireOutlined,
FolderOutlined,
HeartOutlined,
PlusOutlined,
SafetyCertificateOutlined,
SyncOutlined,
TeamOutlined,
UndoOutlined,
UserOutlined
} from "@ant-design/icons";
import request from "../../common/request";
import {message} from "antd/es";
import qs from "qs";
import {cloneObj, renderSize} from "../../utils/utils";
import FileSystem from "./FileSystem";
import StorageModal from "./StorageModal";
const {Content} = Layout;
const {Title} = Typography;
const {Search} = Input;
class Storage extends Component {
inputRefOfName = React.createRef();
storageRef = undefined;
state = {
items: [],
total: 0,
queryParams: {
pageIndex: 1,
pageSize: 10
},
loading: false,
modalVisible: false,
modalTitle: '',
modalConfirmLoading: false,
selectedRow: undefined,
fileSystemVisible: false,
storageId: undefined
};
componentDidMount() {
this.loadTableData();
}
async delete(id) {
const result = await request.delete('/storages/' + 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(`/storages/paging?${paramsStr}`);
if (result.code === 1) {
data = result.data;
} else {
message.error(result.message);
}
} catch (e) {
} finally {
this.setState({
items: data.items,
total: data.total,
queryParams: queryParams,
loading: false
});
}
}
onRef = (storageRef) => {
this.storageRef = storageRef;
}
handleSearchByName = name => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'name': name,
}
this.loadTableData(query);
};
showModal(title, obj = undefined) {
this.setState({
modalTitle: title,
modalVisible: true,
model: obj
});
};
handleOk = async (formData) => {
// 弹窗 form 传来的数据
this.setState({
modalConfirmLoading: true
});
if (formData.id) {
// 转换文件大小限制单位为字节
formData['limitSize'] = parseInt(formData['limitSize']) * 1024 * 1024;
// 向后台提交数据
const result = await request.put('/storages/' + 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 {
// 转换文件大小限制单位为字节
formData['limitSize'] = parseInt(formData['limitSize']) * 1024 * 1024;
// 向后台提交数据
const result = await request.post('/storages', 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
});
};
render() {
return (
<div>
<Content className="site-layout-background page-content">
<div>
<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: '', 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>
</Space>
</Col>
</Row>
</div>
</Content>
<div style={{margin: '0 16px'}}>
<List
loading={this.state.loading}
grid={{gutter: 16, column: 4}}
dataSource={this.state.items}
renderItem={item => {
let delBtn;
if (item['isDefault']) {
delBtn = <DeleteOutlined key="delete" className={'disabled-icon'}/>
} else {
delBtn = <Popconfirm
title="您确认要删除此空间吗?"
onConfirm={() => {
this.delete(item['id']);
}}
okText="是"
cancelText="否"
>
<DeleteOutlined key="delete"/>
</Popconfirm>
}
return (
<List.Item>
<Card title={item['name']}
hoverable
actions={[
<FolderOutlined key='file' onClick={() => {
this.setState({
fileSystemVisible: true,
storageId: item['id']
});
if (this.storageRef) {
this.storageRef.reSetStorageId(item['id']);
}
}}/>,
<EditOutlined key="edit" onClick={() => {
// 转换文件大小限制单位为MB
let model = cloneObj(item);
if(model['limitSize'] > 0){
model['limitSize'] = model['limitSize'] / 1024 / 1024;
}
this.showModal('修改磁盘空间', model);
}}/>,
delBtn
,
]}>
<Descriptions title="" column={1}>
<Descriptions.Item label={<div><TeamOutlined/> 是否共享</div>}>
<strong>{item['isShare'] ? '是' : '否'}</strong>
</Descriptions.Item>
<Descriptions.Item label={<div><SafetyCertificateOutlined/> 是否默认</div>}>
<strong>{item['isDefault'] ? '是' : '否'}</strong>
</Descriptions.Item>
<Descriptions.Item label={<div><FireOutlined/> 大小限制</div>}>
<strong>{item['limitSize'] < 0 ? '无限制' : renderSize(item['limitSize'])}</strong>
</Descriptions.Item>
<Descriptions.Item label={<div><HeartOutlined/> 已用大小</div>}>
<strong>{renderSize(item['usedSize'])}</strong>
</Descriptions.Item>
<Descriptions.Item label={<div><UserOutlined/> 所属用户</div>}>
<strong>{item['ownerName']}</strong>
</Descriptions.Item>
</Descriptions>
</Card>
</List.Item>
)
}}
/>
</div>
<Drawer
title={'文件管理'}
placement="right"
width={window.innerWidth * 0.8}
closable={true}
maskClosable={true}
onClose={() => {
this.setState({
fileSystemVisible: false
});
this.loadTableData(this.state.queryParams);
}}
visible={this.state.fileSystemVisible}
>
<FileSystem
storageId={this.state.storageId}
storageType={'storages'}
onRef={this.onRef}
upload={true}
download={true}
delete={true}
rename={true}
edit={true}
minHeight={window.innerHeight - 103}/>
</Drawer>
{
this.state.modalVisible ?
<StorageModal
visible={this.state.modalVisible}
title={this.state.modalTitle}
handleOk={this.handleOk}
handleCancel={() => {
this.setState({
modalTitle: '',
modalVisible: false
});
}}
confirmLoading={this.state.modalConfirmLoading}
model={this.state.model}
>
</StorageModal> : undefined
}
</div>
);
}
}
export default Storage;

View File

@ -0,0 +1,62 @@
import React from 'react';
import {Form, Input, Modal, Switch} from "antd/lib/index";
const formItemLayout = {
labelCol: {span: 6},
wrapperCol: {span: 14},
};
const StorageModal = ({title, visible, handleOk, handleCancel, confirmLoading, model}) => {
const [form] = Form.useForm();
if(!model){
model = {
isShare: false
}
}
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 autoComplete="off" placeholder="网盘的名称"/>
</Form.Item>
<Form.Item label="是否共享" name='isShare' rules={[{required: true, message: '请选择是否共享'}]} valuePropName="checked">
<Switch checkedChildren="是" unCheckedChildren="否" />
</Form.Item>
<Form.Item label="大小限制" name='limitSize' rules={[{required: true, message: '请输入大小限制'}]}>
<Input type={'number'} min={-1} suffix="MB"/>
</Form.Item>
</Form>
</Modal>
)
};
export default StorageModal;

View File

@ -21,7 +21,7 @@ import {differTime} from "../../utils/utils";
import Playback from "./Playback";
import {message} from "antd/es";
import {DeleteOutlined, ExclamationCircleOutlined, SyncOutlined, UndoOutlined} from "@ant-design/icons";
import {PROTOCOL_COLORS} from "../../common/constants";
import {MODE_COLORS, PROTOCOL_COLORS} from "../../common/constants";
import dayjs from "dayjs";
@ -204,7 +204,7 @@ class OfflineSession extends Component {
})
await this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
message.error(result.message, 10);
}
} finally {
this.setState({
@ -226,6 +226,15 @@ class OfflineSession extends Component {
title: '来源IP',
dataIndex: 'clientIp',
key: 'clientIp'
}, {
title: '接入方式',
dataIndex: 'mode',
key: 'mode',
render: (text) => {
return (
<Tag color={MODE_COLORS[text]}>{text}</Tag>
)
}
}, {
title: '用户昵称',
dataIndex: 'creatorName',
@ -298,7 +307,7 @@ class OfflineSession extends Component {
if (result.code === 1) {
message.success('禁用成功');
} else {
message.error('禁用失败 :( ' + result.message, 10);
message.error(result.message, 10);
}
}
});
@ -326,7 +335,7 @@ class OfflineSession extends Component {
} else {
notification['error']({
message: '提示',
description: '删除失败 :( ' + result.message,
description: result.message,
});
}
@ -480,7 +489,7 @@ class OfflineSession extends Component {
<Modal
className='modal-no-padding'
title={`会话回放 来源IP${this.state.selectedRow['clientIp']} 用户昵称:${this.state.selectedRow['creatorName']} 资产名称:${this.state.selectedRow['assetName']} 网络:${this.state.selectedRow['username']}@${this.state.selectedRow['ip']}:${this.state.selectedRow['port']}`}
centered={true}
visible={this.state.playbackVisible}
onCancel={this.hidePlayback}
width={window.innerWidth * 0.8}
@ -489,7 +498,7 @@ class OfflineSession extends Component {
maskClosable={false}
>
{
this.state.selectedRow['mode'] === 'naive' ?
this.state.selectedRow['mode'] === 'naive' || this.state.selectedRow['mode'] === 'terminal' ?
<iframe
title='recording'
style={{

View File

@ -20,11 +20,12 @@ import qs from "qs";
import request from "../../common/request";
import {differTime} from "../../utils/utils";
import {message} from "antd/es";
import {PROTOCOL_COLORS} from "../../common/constants";
import {MODE_COLORS, PROTOCOL_COLORS} from "../../common/constants";
import {DisconnectOutlined, ExclamationCircleOutlined, SyncOutlined, UndoOutlined} from "@ant-design/icons";
import Monitor from "../access/Monitor";
import AccessMonitor from "../access/AccessMonitor";
import dayjs from "dayjs";
import TermMonitor from "../access/TermMonitor";
const confirm = Modal.confirm;
const {Content} = Layout;
@ -53,7 +54,8 @@ class OnlineSession extends Component {
accessVisible: false,
sessionWidth: 1024,
sessionHeight: 768,
sessionProtocol: ''
sessionProtocol: '',
sessionMode: '',
};
componentDidMount() {
@ -189,7 +191,7 @@ class OnlineSession extends Component {
})
await this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
message.error(result.message, 10);
}
} finally {
this.setState({
@ -199,10 +201,11 @@ class OnlineSession extends Component {
}
showMonitor = (record) => {
this.setState({
connectionId: record.connectionId,
sessionId: record.id,
sessionProtocol: record.protocol,
sessionMode: record.mode,
accessVisible: true,
sessionWidth: record.width,
sessionHeight: record.height,
@ -223,6 +226,15 @@ class OnlineSession extends Component {
title: '来源IP',
dataIndex: 'clientIp',
key: 'clientIp'
}, {
title: '接入方式',
dataIndex: 'mode',
key: 'mode',
render: (text) => {
return (
<Tag color={MODE_COLORS[text]}>{text}</Tag>
)
}
}, {
title: '用户昵称',
dataIndex: 'creatorName',
@ -269,7 +281,7 @@ class OnlineSession extends Component {
return (
<div>
<Button type="link" size='small' disabled={record['mode'] === 'naive'} onClick={() => {
<Button type="link" size='small' onClick={() => {
this.showMonitor(record)
}}>监控</Button>
<Button type="link" size='small' onClick={async () => {
@ -296,7 +308,7 @@ class OnlineSession extends Component {
} else {
notification['success']({
message: '提示',
description: '断开失败 :( ' + result.message,
description: result.message,
});
}
}
@ -463,13 +475,23 @@ class OnlineSession extends Component {
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}>
{
this.state.sessionMode === 'guacd' ?
<AccessMonitor connectionId={this.state.connectionId}
width={this.state.sessionWidth}
height={this.state.sessionHeight}
protocol={this.state.sessionProtocol}
rate={window.innerWidth * 0.8 / this.state.sessionWidth}>
</AccessMonitor> :
<TermMonitor sessionId={this.state.sessionId}
width={this.state.sessionWidth}
height={this.state.sessionHeight}>
</TermMonitor>
}
</Monitor>
</Modal> : undefined
}

View File

@ -1,12 +1,15 @@
import React, {Component} from 'react';
import Guacamole from "guacamole-common-js";
import {server} from "../../common/env";
import {Button, Col, Row, Slider, Typography} from "antd";
import {Button, Col, Row, Select, Slider, Typography} from "antd";
import {PauseCircleOutlined, PlayCircleOutlined} from '@ant-design/icons';
import {Tooltip} from "antd/lib/index";
import {getToken} from "../../utils/utils";
const {Text} = Typography;
let timer;
class Playback extends Component {
state = {
@ -15,6 +18,7 @@ class Playback extends Component {
recording: undefined,
percent: 0,
max: 0,
speed: 1,
}
componentDidMount() {
@ -27,14 +31,14 @@ class Playback extends Component {
}
initPlayer(sessionId) {
var RECORDING_URL = `${server}/sessions/${sessionId}/recording`;
const RECORDING_URL = `${server}/sessions/${sessionId}/recording?X-Auth-Token=${getToken()}`;
var display = document.getElementById('display');
const display = document.getElementById('display');
var tunnel = new Guacamole.StaticHTTPTunnel(RECORDING_URL);
var recording = new Guacamole.SessionRecording(tunnel);
const tunnel = new Guacamole.StaticHTTPTunnel(RECORDING_URL);
const recording = new Guacamole.SessionRecording(tunnel);
var recordingDisplay = recording.getDisplay();
const recordingDisplay = recording.getDisplay();
/**
* Converts the given number to a string, adding leading zeroes as necessary
@ -50,7 +54,7 @@ class Playback extends Component {
* 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) {
const zeroPad = function zeroPad(num, minLength) {
// Convert provided number to string
var str = num.toString();
@ -74,7 +78,7 @@ class Playback extends Component {
* A human-readable string representation of the given timestamp, in
* MM:SS format.
*/
var formatTime = function formatTime(millis) {
const formatTime = function formatTime(millis) {
// Calculate total number of whole seconds
var totalSeconds = Math.floor(millis / 1000);
@ -147,6 +151,33 @@ class Playback extends Component {
});
}
startSpeedUp = () => {
let speed = this.state.speed;
if (speed === 1) {
return;
}
let recording = this.state.recording;
if (!recording.isPlaying()) {
return;
}
const add_time = 100;
let delay = 1000 / (1000 / add_time) / (speed - 1);
let max = recording.getDuration();
let current = recording.getPosition();
if (current >= max) {
this.stopSpeedUp();
return;
}
recording.seek(current + add_time, () => {
timer = setTimeout(this.startSpeedUp, delay);
});
}
stopSpeedUp = () => {
clearTimeout(timer)
}
handlePlayPause = () => {
let recording = this.state.recording;
if (recording) {
@ -157,14 +188,17 @@ class Playback extends Component {
}, () => {
recording.seek(0, () => {
recording.play();
this.startSpeedUp();
});
});
}
if (!recording.isPlaying()) {
recording.play();
this.startSpeedUp();
} else {
recording.pause();
this.stopSpeedUp();
}
}
}
@ -203,6 +237,23 @@ class Playback extends Component {
<Slider value={this.state.percent} max={this.state.max} tooltipVisible={false}
onChange={this.handleProgressChange}/>
</Col>
<Col flex='none'>
<Select size={'small'} defaultValue='1' value={this.state.speed} onChange={(value) => {
value = parseInt(value)
this.setState({
'speed': value
});
if (value === 1) {
this.stopSpeedUp();
} else {
this.startSpeedUp();
}
}}>
<Select.Option key="1">1x</Select.Option>
<Select.Option key="2">2x</Select.Option>
<Select.Option key="5">5x</Select.Option>
</Select>
</Col>
<Col flex='none'>
<Text>{this.state.position}</Text>/ <Text>{this.state.duration}</Text>
</Col>

View File

@ -30,6 +30,7 @@ class Setting extends Component {
vncSettingFormRef = React.createRef();
guacdSettingFormRef = React.createRef();
mailSettingFormRef = React.createRef();
otherSettingFormRef = React.createRef();
componentDidMount() {
this.getProperties();
@ -111,60 +112,6 @@ class Setting extends Component {
layout="vertical">
<Title level={3}>RDP配置(远程桌面)</Title>
<Form.Item
{...formItemLayout}
name="enable-drive"
label="启用设备映射"
valuePropName="checked"
rules={[
{
required: true,
},
]}
>
<Switch checkedChildren="开启" unCheckedChildren="关闭" onChange={(checked, event) => {
this.setState({
properties: {
...this.state.properties,
'enable-drive': checked,
}
})
}}/>
</Form.Item>
{
this.state.properties['enable-drive'] === true ?
<>
<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>
</> : null
}
<Form.Item
{...formItemLayout}
name="enable-wallpaper"
@ -450,39 +397,10 @@ class Setting extends Component {
</Form.Item>
</Form>
</TabPane>
<TabPane tab="Guacd服务配置" key="other">
<Title level={3}>Guacd 服务配置</Title>
<Form ref={this.guacdSettingFormRef} name="password" onFinish={this.changeProperties}
<TabPane tab="录屏配置" key="guacd">
<Title level={3}>录屏配置</Title>
<Form ref={this.guacdSettingFormRef} name="guacd" onFinish={this.changeProperties}
layout="vertical">
<Form.Item
{...formItemLayout}
name="host"
label="Guacd监听地址"
rules={[
{
required: true,
message: 'Guacd监听地址',
},
]}
>
<Input type='text' placeholder="请输入Guacd监听地址"/>
</Form.Item>
<Form.Item
{...formItemLayout}
name="port"
label="Guacd监听端口"
rules={[
{
required: true,
message: 'Guacd监听端口',
min: 1,
max: 65535
},
]}
>
<Input type='number' placeholder="请输入Guacd监听端口"/>
</Form.Item>
<Form.Item
{...formItemLayout}
@ -507,37 +425,24 @@ class Setting extends Component {
{
this.state.properties['enable-recording'] === true ?
<>
<Form.Item
{...formItemLayout}
name="recording-path"
label="录屏存放路径"
rules={[
{
required: true,
message: '请输入录屏存放路径',
},
]}
name="session-saved-limit"
label="会话录屏保存时长"
initialValue=""
>
<Input type='text' placeholder="请输入录屏存放路径"/>
<Select onChange={null}>
<Option value="">永久</Option>
<Option value="30">30</Option>
<Option value="60">60</Option>
<Option value="180">180</Option>
<Option value="360">360</Option>
</Select>
</Form.Item>
</> : null
}
<Form.Item
{...formItemLayout}
name="session-saved-limit"
label="会话录屏保存时长"
initialValue=""
>
<Select onChange={null}>
<Option value="">永久</Option>
<Option value="30">30</Option>
<Option value="60">60</Option>
<Option value="180">180</Option>
<Option value="360">360</Option>
</Select>
</Form.Item>
<Form.Item {...formTailLayout}>
<Button type="primary" htmlType="submit">
@ -548,7 +453,7 @@ class Setting extends Component {
</TabPane>
<TabPane tab="邮箱配置" key="mail">
<Title level={3}>邮箱配置</Title>
<Form ref={this.mailSettingFormRef} name="password" onFinish={this.changeProperties}
<Form ref={this.mailSettingFormRef} name='mail' onFinish={this.changeProperties}
layout="vertical">
<Form.Item
@ -572,13 +477,13 @@ class Setting extends Component {
rules={[
{
required: false,
message: '邮件服务器地址',
message: '邮件服务器端口',
min: 1,
max: 65535
},
]}
>
<Input type='number' placeholder="请输入邮件服务器地址"/>
<Input type='number' placeholder="请输入邮件服务器端口"/>
</Form.Item>
<Form.Item
@ -617,6 +522,49 @@ class Setting extends Component {
</Form.Item>
</Form>
</TabPane>
<TabPane tab="其他配置" key="other">
<Title level={3}>其他配置</Title>
<Form ref={this.guacdSettingFormRef} name="other" onFinish={this.changeProperties}
layout="vertical">
<Form.Item
{...formItemLayout}
name="login-log-saved-limit"
label="登录日志保留时长"
initialValue=""
>
<Select onChange={null}>
<Option value="">永久</Option>
<Option value="30">30</Option>
<Option value="60">60</Option>
<Option value="180">180</Option>
<Option value="360">360</Option>
</Select>
</Form.Item>
<Form.Item
{...formItemLayout}
name="cron-log-saved-limit"
label="计划任务日志保留时长"
initialValue=""
>
<Select onChange={null}>
<Option value="">永久</Option>
<Option value="30">30</Option>
<Option value="60">60</Option>
<Option value="180">180</Option>
<Option value="360">360</Option>
</Select>
</Form.Item>
<Form.Item {...formTailLayout}>
<Button type="primary" htmlType="submit">
更新
</Button>
</Form.Item>
</Form>
</TabPane>
</Tabs>
</Content>
</>

View File

@ -1,19 +1,21 @@
import React, {Component} from 'react';
import {Button, Card, Form, Image, Input, Layout, Modal, Result, Space} from "antd";
import {Button, Card, Divider, Form, Image, Input, Layout, Modal, Result, Space, Typography} from "antd";
import request from "../../common/request";
import {message} from "antd/es";
import {ExclamationCircleOutlined, ReloadOutlined} from "@ant-design/icons";
import {isAdmin} from "../../service/permission";
const {Content} = Layout;
const {Meta} = Card;
const {Title} = Typography;
const formItemLayout = {
labelCol: {span: 3},
wrapperCol: {span: 6},
labelCol: {span: 4},
wrapperCol: {span: 10},
};
const formTailLayout = {
labelCol: {span: 3},
wrapperCol: {span: 6, offset: 3},
labelCol: {span: 4},
wrapperCol: {span: 10, offset: 4},
};
const {confirm} = Modal;
@ -107,11 +109,13 @@ class Info extends Component {
}
render() {
let contentClassName = isAdmin() ? 'page-content' : 'page-content-user';
return (
<>
<Content className="site-layout-background page-content">
<h1>修改密码</h1>
<Content className={["site-layout-background", contentClassName]}>
<Title level={3}>修改密码</Title>
<Form ref={this.passwordFormRef} name="password" onFinish={this.changePassword}>
<input type='password' hidden={true} autoComplete='new-password'/>
<Form.Item
{...formItemLayout}
name="oldPassword"
@ -123,7 +127,7 @@ class Info extends Component {
},
]}
>
<Input type='password' placeholder="请输入原始密码"/>
<Input type='password' placeholder="请输入原始密码" style={{width: 240}}/>
</Form.Item>
<Form.Item
{...formItemLayout}
@ -137,7 +141,7 @@ class Info extends Component {
]}
>
<Input type='password' placeholder="新的密码"
onChange={(value) => this.onNewPasswordChange(value)}/>
onChange={(value) => this.onNewPasswordChange(value)} style={{width: 240}}/>
</Form.Item>
<Form.Item
{...formItemLayout}
@ -153,7 +157,7 @@ class Info extends Component {
help={this.state.errorMsg || ''}
>
<Input type='password' placeholder="请和上面输入新的密码保持一致"
onChange={(value) => this.onNewPassword2Change(value)}/>
onChange={(value) => this.onNewPassword2Change(value)} style={{width: 240}}/>
</Form.Item>
<Form.Item {...formTailLayout}>
<Button type="primary" htmlType="submit">
@ -161,11 +165,12 @@ class Info extends Component {
</Button>
</Form.Item>
</Form>
</Content>
<Content className="site-layout-background page-content">
<h1>双因素认证</h1>
<Divider/>
<Title level={3}>双因素认证</Title>
<Form hidden={this.state.qr}>
<Form.Item>
<Form.Item {...formItemLayout}>
{
this.state.user.enableTotp ?
<Result
@ -214,6 +219,7 @@ class Info extends Component {
</Form.Item>
</Form>
<Form hidden={!this.state.qr} onFinish={this.confirmTOTP}>
<Form.Item {...formItemLayout} label="二维码">
<Space size={12}>
@ -222,8 +228,8 @@ class Info extends Component {
hoverable
style={{width: 280}}
cover={<Image
style={{margin: 40, marginBottom: 20}}
width={200}
style={{padding: 20}}
width={280}
src={"data:image/png;base64, " + this.state.qr}
/>
}
@ -260,6 +266,7 @@ class Info extends Component {
</Button>
</Form.Item>
</Form>
</Content>
</>
);

View File

@ -0,0 +1,435 @@
import React, {Component} from 'react';
import {Button, Col, Divider, Input, Layout, Modal, Row, Space, Table, Tag, Tooltip, Typography} from "antd";
import qs from "qs";
import request from "../../common/request";
import {message} from "antd/es";
import {DeleteOutlined, ExclamationCircleOutlined, PlusOutlined, SyncOutlined, UndoOutlined} from '@ant-design/icons';
import StrategyModal from "./StrategyModal";
import {cloneObj} from "../../utils/utils";
const confirm = Modal.confirm;
const {Content} = Layout;
const {Title, Text} = Typography;
const {Search} = Input;
const keys = ['upload', 'download', 'delete', 'rename', 'edit'];
class Strategy extends Component {
inputRefOfName = React.createRef();
state = {
items: [],
total: 0,
queryParams: {
pageIndex: 1,
pageSize: 10
},
loading: false,
modalVisible: false,
modalTitle: '',
modalConfirmLoading: false,
selectedRow: undefined,
selectedRowKeys: [],
};
componentDidMount() {
this.loadTableData();
}
async delete(id) {
const result = await request.delete('/strategies/' + 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('/strategies/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);
};
showDeleteConfirm(id, content) {
let self = this;
confirm({
title: '您确定要删除此任务吗?',
content: content,
okText: '确定',
okType: 'danger',
cancelText: '取消',
onOk() {
self.delete(id);
}
});
};
showModal(title, obj = undefined) {
let model = obj;
if (model) {
model = cloneObj(obj);
for (let i = 0; i < keys.length; i++) {
let key = keys[i];
if (!model.hasOwnProperty(key)) {
continue;
}
model[key] = model[key] === '1';
}
}
this.setState({
modalTitle: title,
modalVisible: true,
model: model
});
};
handleCancelModal = e => {
this.setState({
modalTitle: '',
modalVisible: false
});
};
handleOk = async (formData) => {
// 弹窗 form 传来的数据
this.setState({
modalConfirmLoading: true
});
for (let i = 0; i < keys.length; i++) {
let key = keys[i];
if (!formData.hasOwnProperty(key)) {
continue;
}
if (formData[key] === true) {
formData[key] = '1';
} else {
formData[key] = '0';
}
}
if (formData.id) {
// 向后台提交数据
const result = await request.put('/strategies/' + 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('/strategies', 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('/strategies/' + 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
})
}
}
handleTableChange = (pagination, filters, sorter) => {
let query = {
...this.state.queryParams,
'order': sorter.order,
'field': sorter.field
}
this.loadTableData(query);
}
render() {
const renderStatus = (text) => {
if (text === '1') {
return <Tag color={'green'}>开启</Tag>
} else {
return <Tag color={'red'}>关闭</Tag>
}
}
const columns = [{
title: '序号',
dataIndex: 'id',
key: 'id',
render: (id, record, index) => {
return index + 1;
}
}, {
title: '名称',
dataIndex: 'name',
key: 'name',
sorter: true,
}, {
title: '上传',
dataIndex: 'upload',
key: 'upload',
render: (text) => {
return renderStatus(text);
}
}, {
title: '下载',
dataIndex: 'download',
key: 'download',
render: (text) => {
return renderStatus(text);
}
}, {
title: '编辑',
dataIndex: 'edit',
key: 'edit',
render: (text) => {
return renderStatus(text);
}
}, {
title: '删除',
dataIndex: 'delete',
key: 'delete',
render: (text) => {
return renderStatus(text);
}
}, {
title: '重命名',
dataIndex: 'rename',
key: 'rename',
render: (text) => {
return renderStatus(text);
}
}, {
title: '创建时间',
dataIndex: 'created',
key: 'created',
}, {
title: '操作',
key: 'action',
render: (text, record, index) => {
return (
<div>
<Button type="link" size='small' loading={this.state.items[index]['execLoading']}
onClick={() => this.showModal('更新授权策略', record)}>编辑</Button>
<Button type="text" size='small' danger
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 (
<>
<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: '', 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}
onChange={this.handleTableChange}
/>
{
this.state.modalVisible ?
<StrategyModal
visible={this.state.modalVisible}
title={this.state.modalTitle}
handleOk={this.handleOk}
handleCancel={this.handleCancelModal}
confirmLoading={this.state.modalConfirmLoading}
model={this.state.model}
>
</StrategyModal> : undefined
}
</Content>
</>
);
}
}
export default Strategy;

View File

@ -0,0 +1,78 @@
import React from 'react';
import {Form, Input, Modal, Switch} from "antd/lib/index";
const formItemLayout = {
labelCol: {span: 6},
wrapperCol: {span: 14},
};
const StrategyModal = ({title, visible, handleOk, handleCancel, confirmLoading, model}) => {
const [form] = Form.useForm();
if (model === undefined) {
model = {
'upload': false,
'download': false,
'delete': false,
'rename': false,
'edit': false,
};
}
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 autoComplete="off" placeholder="授权策略名称"/>
</Form.Item>
<Form.Item label="上传" name='upload' rules={[{required: true}]} valuePropName="checked">
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
<Form.Item label="下载" name='download' rules={[{required: true}]} valuePropName="checked">
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
<Form.Item label="编辑" name='edit' rules={[{required: true}]} valuePropName="checked" tooltip={'编辑需要先开启下载'}>
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
<Form.Item label="删除" name='delete' rules={[{required: true}]} valuePropName="checked">
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
<Form.Item label="重命名" name='rename' rules={[{required: true}]} valuePropName="checked">
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
</Form>
</Modal>
)
};
export default StrategyModal;

View File

@ -5,6 +5,7 @@ import {
Button,
Col,
Divider,
Drawer,
Dropdown,
Form,
Input,
@ -32,9 +33,9 @@ import {
SyncOutlined,
UndoOutlined
} from '@ant-design/icons';
import UserShareAsset from "./UserShareAsset";
import {hasPermission} from "../../service/permission";
import dayjs from "dayjs";
import UserShareSelectedAsset from "./UserShareSelectedAsset";
const confirm = Modal.confirm;
const {Search} = Input;
@ -78,7 +79,7 @@ class User extends Component {
message.success('操作成功', 3);
await this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
message.error(result.message, 10);
}
}
@ -178,7 +179,7 @@ class User extends Component {
});
await this.loadTableData(this.state.queryParams);
} else {
message.error('操作失败 :( ' + result.message, 10);
message.error(result.message, 10);
}
} else {
// 向后台提交数据
@ -191,7 +192,7 @@ class User extends Component {
});
await this.loadTableData(this.state.queryParams);
} else {
message.error('操作失败 :( ' + result.message, 10);
message.error(result.message, 10);
}
}
@ -267,7 +268,9 @@ class User extends Component {
changePasswordConfirmLoading: true
})
let result = await request.post(`/users/${this.state.selectedRow['id']}/change-password?password=${values['password']}`);
let formData = new FormData();
formData.append('password', values['password']);
let result = await request.post(`/users/${this.state.selectedRow['id']}/change-password`, formData);
if (result['code'] === 1) {
message.success('操作成功', 3);
} else {
@ -430,6 +433,16 @@ class User extends Component {
}}>重置双因素认证</Button>
</Menu.Item>
<Menu.Item key="3">
<Button type="text" size='small'
onClick={() => {
this.setState({
assetVisible: true,
sharer: record['id']
})
}}>资产授权</Button>
</Menu.Item>
<Menu.Divider/>
<Menu.Item key="5">
<Button type="text" size='small' danger
@ -637,24 +650,26 @@ class User extends Component {
</UserModal> : undefined
}
<Modal
width={window.innerWidth * 0.8}
title='已授权资产'
visible={this.state.assetVisible}
maskClosable={false}
<Drawer
title="资产授权"
placement="right"
closable={true}
destroyOnClose={true}
onOk={() => {
onClose={() => {
this.loadTableData(this.state.queryParams);
this.setState({
assetVisible: false
})
}}
onCancel={this.handleAssetCancel}
okText='确定'
cancelText='取消'
footer={null}
visible={this.state.assetVisible}
width={window.innerWidth * 0.8}
>
<UserShareAsset
<UserShareSelectedAsset
sharer={this.state.sharer}
/>
</Modal>
userGroupId={undefined}
>
</UserShareSelectedAsset>
</Drawer>
{
this.state.changePasswordVisible ?

View File

@ -1,13 +1,13 @@
import React, {Component} from 'react';
import {Button, Col, Divider, Input, Layout, Modal, Row, Space, Table, Tooltip, Typography,} from "antd";
import {Button, Col, Divider, Drawer, Input, Layout, Modal, Row, Space, Table, Tooltip, Typography,} from "antd";
import qs from "qs";
import request from "../../common/request";
import {message} from "antd/es";
import {DeleteOutlined, ExclamationCircleOutlined, PlusOutlined, SyncOutlined, UndoOutlined} from '@ant-design/icons';
import UserGroupModal from "./UserGroupModal";
import UserShareAsset from "./UserShareAsset";
import dayjs from "dayjs";
import UserShareSelectedAsset from "./UserShareSelectedAsset";
const confirm = Modal.confirm;
const {Search} = Input;
@ -45,7 +45,7 @@ class UserGroup extends Component {
message.success('操作成功', 3);
await this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
message.error(result.message, 10);
}
}
@ -172,7 +172,7 @@ class UserGroup extends Component {
});
await this.loadTableData(this.state.queryParams);
} else {
message.error('操作失败 :( ' + result.message, 10);
message.error(result.message, 10);
}
} else {
// 向后台提交数据
@ -185,7 +185,7 @@ class UserGroup extends Component {
});
await this.loadTableData(this.state.queryParams);
} else {
message.error('操作失败 :( ' + result.message, 10);
message.error(result.message, 10);
}
}
@ -218,7 +218,7 @@ class UserGroup extends Component {
})
await this.loadTableData(this.state.queryParams);
} else {
message.error('删除失败 :( ' + result.message, 10);
message.error(result.message, 10);
}
} finally {
this.setState({
@ -311,6 +311,13 @@ class UserGroup extends Component {
loading={this.state.items[index].updateBtnLoading}
onClick={() => this.showModal('更新用户组', record['id'], index)}>编辑</Button>
<Button type="link" size='small'
onClick={() => {
this.setState({
assetVisible: true,
userGroupId: record['id']
})
}}>资产授权</Button>
<Button type="link" size='small' danger
onClick={() => this.showDeleteConfirm(record.id, record.name)}>删除</Button>
</div>
)
@ -431,24 +438,25 @@ class UserGroup extends Component {
</UserGroupModal> : undefined
}
<Modal
width={window.innerWidth * 0.8}
title='已授权资产'
visible={this.state.assetVisible}
maskClosable={false}
<Drawer
title="资产授权"
placement="right"
closable={true}
destroyOnClose={true}
onOk={() => {
onClose={() => {
this.loadTableData(this.state.queryParams);
this.setState({
assetVisible: false
})
}}
onCancel={this.handleAssetCancel}
okText='确定'
cancelText='取消'
footer={null}
visible={this.state.assetVisible}
width={window.innerWidth * 0.8}
>
<UserShareAsset
<UserShareSelectedAsset
userGroupId={this.state.userGroupId}
/>
</Modal>
>
</UserShareSelectedAsset>
</Drawer>
</Content>
</>

View File

@ -1,443 +0,0 @@
import React, {Component} from 'react';
import {
Badge,
Button,
Col,
Divider,
Drawer,
Input,
Layout,
Modal,
Row,
Select,
Space,
Table,
Tag,
Tooltip,
Typography
} from "antd";
import qs from "qs";
import request from "../../common/request";
import {message} from "antd/es";
import {DeleteOutlined, ExclamationCircleOutlined, PlusOutlined, SyncOutlined, UndoOutlined} from '@ant-design/icons';
import {PROTOCOL_COLORS} from "../../common/constants";
import UserShareSelectedAsset from "./UserShareSelectedAsset";
import {isEmpty} from "../../utils/utils";
import dayjs from "dayjs";
const confirm = Modal.confirm;
const {Search} = Input;
const {Content} = Layout;
const {Title, Text} = Typography;
class UserShareAsset extends Component {
inputRefOfName = React.createRef();
inputRefOfIp = React.createRef();
changeOwnerFormRef = React.createRef();
state = {
items: [],
total: 0,
queryParams: {
pageIndex: 1,
pageSize: 10,
protocol: ''
},
loading: false,
tags: [],
model: {},
selectedRowKeys: [],
delBtnLoading: false,
changeOwnerModalVisible: false,
changeSharerModalVisible: false,
changeOwnerConfirmLoading: false,
changeSharerConfirmLoading: false,
users: [],
selected: {},
selectedSharers: [],
chooseAssetVisible: false
};
async componentDidMount() {
let sharer = this.props.sharer;
let userGroupId = this.props.userGroupId;
this.loadTableData({sharer: sharer, userGroupId: userGroupId});
let result = await request.get('/tags');
if (result['code'] === 1) {
this.setState({
tags: result['data']
})
}
}
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);
};
handleSearchByIp = ip => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'ip': ip,
}
this.loadTableData(query);
};
handleTagsChange = tags => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'tags': tags.join(','),
}
this.loadTableData(query);
}
handleSearchByProtocol = protocol => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'protocol': protocol,
}
this.loadTableData(query);
}
render() {
const columns = [{
title: '序号',
dataIndex: 'id',
key: 'id',
render: (id, record, index) => {
return index + 1;
}
}, {
title: '资产名称',
dataIndex: 'name',
key: 'name',
render: (name, record) => {
let short = name;
if (short && short.length > 20) {
short = short.substring(0, 20) + " ...";
}
return (
<Tooltip placement="topLeft" title={name}>
{short}
</Tooltip>
);
}
}, {
title: '连接协议',
dataIndex: 'protocol',
key: 'protocol',
render: (text, record) => {
const title = `${record['ip'] + ':' + record['port']}`
return (
<Tooltip title={title}>
<Tag color={PROTOCOL_COLORS[text]}>{text}</Tag>
</Tooltip>
)
}
}, {
title: '标签',
dataIndex: 'tags',
key: 'tags',
render: tags => {
if (!isEmpty(tags)) {
let tagDocuments = []
let tagArr = tags.split(',');
for (let i = 0; i < tagArr.length; i++) {
if (tags[i] === '-') {
continue;
}
tagDocuments.push(<Tag>{tagArr[i]}</Tag>)
}
return tagDocuments;
}
}
}, {
title: '状态',
dataIndex: 'active',
key: 'active',
render: text => {
if (text) {
return (
<Tooltip title='运行中'>
<Badge status="processing"/>
</Tooltip>
)
} else {
return (
<Tooltip title='不可用'>
<Badge status="error"/>
</Tooltip>
)
}
}
}, {
title: '所有者',
dataIndex: 'ownerName',
key: 'ownerName'
}, {
title: '创建日期',
dataIndex: 'created',
key: 'created',
render: (text, record) => {
return (
<Tooltip title={text}>
{dayjs(text).fromNow()}
</Tooltip>
)
}
},
];
const selectedRowKeys = this.state.selectedRowKeys;
const rowSelection = {
selectedRowKeys: this.state.selectedRowKeys,
onChange: (selectedRowKeys, selectedRows) => {
this.setState({selectedRowKeys});
},
};
const hasSelected = selectedRowKeys.length > 0;
return (
<>
<Content key='page-content' className="site-layout-background">
<div style={{marginBottom: 20}}>
<Row justify="space-around" align="middle" gutter={24}>
<Col span={4} key={1}>
<Title level={3}>授权资产列表</Title>
</Col>
<Col span={20} key={2} style={{textAlign: 'right'}}>
<Space>
<Search
ref={this.inputRefOfName}
placeholder="资产名称"
allowClear
onSearch={this.handleSearchByName}
style={{width: 200}}
/>
<Search
ref={this.inputRefOfIp}
placeholder="资产IP"
allowClear
onSearch={this.handleSearchByIp}
style={{width: 200}}
/>
<Select mode="multiple"
allowClear
placeholder="资产标签" onChange={this.handleTagsChange}
style={{minWidth: 150}}>
{this.state.tags.map(tag => {
if (tag === '-') {
return undefined;
}
return (<Select.Option key={tag}>{tag}</Select.Option>)
})}
</Select>
<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.inputRefOfIp.current.setValue('');
this.loadTableData({
...this.state.queryParams,
pageIndex: 1,
pageSize: 10,
protocol: ''
})
}}>
</Button>
</Tooltip>
<Divider type="vertical"/>
<Tooltip title="添加授权资产">
<Button type="dashed" icon={<PlusOutlined/>}
onClick={() => {
this.setState({
chooseAssetVisible: true
})
}}>
</Button>
</Tooltip>
<Tooltip title="刷新列表">
<Button icon={<SyncOutlined/>} onClick={() => {
this.loadTableData(this.state.queryParams)
}}>
</Button>
</Tooltip>
<Tooltip title="移除授权资产">
<Button type="dashed" 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: async () => {
let userId = this.state.queryParams.sharer;
let result = await request.post(`/resource-sharers/remove-resources`, {
userId: userId,
resourceType: 'asset',
resourceIds: this.state.selectedRowKeys
});
if (result['code'] === 1) {
message.success('操作成功', 3);
this.setState({
selectedRowKeys: []
})
await this.loadTableData();
} else {
message.error(result['message'], 10);
}
},
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.chooseAssetVisible ?
<Drawer
title="添加授权资产"
placement="right"
closable={true}
onClose={() => {
this.loadTableData()
this.setState({
chooseAssetVisible: false
})
}}
visible={this.state.chooseAssetVisible}
width={window.innerWidth * 0.8}
>
<UserShareSelectedAsset
sharer={this.state.queryParams.sharer}
userGroupId={this.state.queryParams.userGroupId}
>
</UserShareSelectedAsset>
</Drawer> : undefined
}
</Content>
</>
);
}
}
export default UserShareAsset;

View File

@ -1,6 +1,21 @@
import React, {Component} from 'react';
import {Badge, Button, Col, Divider, Input, Layout, Row, Select, Space, Table, Tag, Tooltip, Typography} from "antd";
import {
Badge,
Button,
Col,
Divider,
Input,
Layout,
Popover,
Row,
Select,
Space,
Table,
Tag,
Tooltip,
Typography
} from "antd";
import qs from "qs";
import request from "../../common/request";
import {message} from "antd/es";
@ -42,7 +57,10 @@ class UserShareSelectedAsset extends Component {
users: [],
selected: {},
totalSelectedRows: [],
sharer: ''
sharer: '',
strategies: [],
strategyId: undefined,
sharers: []
};
async componentDidMount() {
@ -65,24 +83,67 @@ class UserShareSelectedAsset extends Component {
userGroupId: userGroupId
}
let paramStr = qs.stringify(params);
let q1 = request.get('/tags');
let q2 = request.get(`/assets/paging?${paramStr}`);
let q1 = request.get(`/strategies`);
let q2 = request.get(`/resource-sharers`);
let q3 = request.get(`/assets/paging?${paramStr}`);
let q4 = request.get('/tags');
let r1 = await q1;
let r2 = await q2;
let r3 = await q3;
let r4 = await q4;
let strategies = [];
if (r1['code'] === 1) {
strategies = r1['data'];
this.setState({
tags: r1['data']
strategies: strategies
})
}
let sharers = [];
if (r2['code'] === 1) {
sharers = r2['data'];
this.setState({
totalSelectedRows: r2['data']['items']
sharers: sharers
})
}
if (r3['code'] === 1) {
let totalSelectedRows = r3['data']['items'];
for (let i = 0; i < totalSelectedRows.length; i++) {
let assetId = totalSelectedRows[i].id;
totalSelectedRows[i]['strategy'] = this.getStrategyByAssetId(strategies, sharers, assetId);
}
this.setState({
totalSelectedRows: totalSelectedRows
})
}
if (r4['code'] === 1) {
this.setState({
tags: r4['data']
})
}
}
getStrategyByAssetId = (strategies, sharers, assetId, strategyId) => {
if (strategyId === undefined) {
for (let i = 0; i < sharers.length; i++) {
if (sharers[i]['resourceId'] === assetId) {
strategyId = sharers[i]['strategyId'];
break;
}
}
}
if (strategyId) {
for (let i = 0; i < strategies.length; i++) {
if (strategies[i].id === strategyId) {
return strategies[i]
}
}
}
return undefined;
}
async loadTableData(queryParams) {
@ -227,8 +288,8 @@ class UserShareSelectedAsset extends Component {
key: 'name',
render: (name, record) => {
let short = name;
if (short && short.length > 20) {
short = short.substring(0, 20) + " ...";
if (short && short.length > 15) {
short = short.substring(0, 15) + " ...";
}
return (
<Tooltip placement="topLeft" title={name}>
@ -237,7 +298,7 @@ class UserShareSelectedAsset extends Component {
);
}
}, {
title: '连接协议',
title: '协议',
dataIndex: 'protocol',
key: 'protocol',
render: (text, record) => {
@ -260,7 +321,7 @@ class UserShareSelectedAsset extends Component {
if (tags[i] === '-') {
continue;
}
tagDocuments.push(<Tag>{tagArr[i]}</Tag>)
tagDocuments.push(<Tag key={tagArr[i]}>{tagArr[i]}</Tag>)
}
return tagDocuments;
}
@ -274,13 +335,13 @@ class UserShareSelectedAsset extends Component {
if (text) {
return (
<Tooltip title='运行中'>
<Badge status="processing"/>
<Badge status="processing" text='运行中'/>
</Tooltip>
)
} else {
return (
<Tooltip title='不可用'>
<Badge status="error"/>
<Badge status="error" text='不可用'/>
</Tooltip>
)
}
@ -326,20 +387,67 @@ class UserShareSelectedAsset extends Component {
}
}
const renderStatus = (text) => {
if (text === '1') {
return <Tag color={'green'}>允许</Tag>
} else {
return <Tag color={'red'}>禁止</Tag>
}
}
return (
<>
<Title level={3}>授权资产列表</Title>
<div>
{
this.state.totalSelectedRows.map(item => {
return <Tag color={PROTOCOL_COLORS[item['protocol']]} closable
onClose={() => this.unSelectRow(item['id'])}
key={item['id']}>{item['name']}</Tag>
})
}
</div>
<Row gutter={16}>
<Col span={6}>
<Title level={3}>授权策略</Title>
<Select style={{minWidth: 200}} onChange={(strategyId) => {
this.setState({
'strategyId': strategyId
})
}}>
{this.state.strategies.map(item => {
return (
<Select.Option key={item.id}>{item.name}</Select.Option>
);
})}
</Select>
</Col>
<Col span={18}>
<Title level={3}>已授权资产列表</Title>
<div>
{
this.state.totalSelectedRows.map(item => {
let strategyName = '「未配置策略」';
let content = '';
if (item['strategy'] !== undefined) {
strategyName = item['strategy']['name'];
content = (
<div>
<p>上传{renderStatus(item['strategy']['upload'])}</p>
<p>下载{renderStatus(item['strategy']['download'])}</p>
<p>删除{renderStatus(item['strategy']['delete'])}</p>
<p>改名{renderStatus(item['strategy']['rename'])}</p>
</div>
);
}
return (
<Popover content={content} title={strategyName}>
<Tag color={PROTOCOL_COLORS[item['protocol']]} closable
onClose={(e) => {
e.preventDefault()
this.unSelectRow(item['id'])
}}
key={item['id']}>{[item['name'], strategyName].join(':')}</Tag>
</Popover>
);
})
}
</div>
</Col>
</Row>
<Divider/>
<Content key='page-content' className="site-layout-background">
<div style={{marginBottom: 20}}>
<Row justify="space-around" align="middle" gutter={24}>
@ -416,7 +524,6 @@ class UserShareSelectedAsset extends Component {
<Tooltip title="添加授权">
<Button type="primary" disabled={!hasSelected} icon={<PlusOutlined/>}
onClick={async () => {
console.log(this.state.selectedRows)
let totalSelectedRows = this.state.totalSelectedRows;
let totalSelectedRowKeys = totalSelectedRows.map(item => item['id']);
@ -427,15 +534,18 @@ class UserShareSelectedAsset extends Component {
if (totalSelectedRowKeys.includes(selectedRow['id'])) {
continue;
}
selectedRow['strategy'] = this.getStrategyByAssetId(this.state.strategies, this.state.sharers, selectedRow['id'], this.state.strategyId);
totalSelectedRows.push(selectedRow);
newRowKeys.push(selectedRow['id']);
}
let userId = this.state.sharer;
let userGroupId = this.state.userGroupId;
let strategyId = this.state.strategyId;
let result = await request.post(`/resource-sharers/add-resources`, {
userGroupId: userGroupId,
userId: userId,
strategyId: strategyId,
resourceType: 'asset',
resourceIds: newRowKeys
});
@ -457,6 +567,7 @@ class UserShareSelectedAsset extends Component {
</div>
<Table key='assets-table'
rowSelection={rowSelection}
dataSource={this.state.items}
columns={columns}

Binary file not shown.