release v1.2.0
This commit is contained in:
105
web/src/App.css
105
web/src/App.css
@ -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;
|
||||
}
|
327
web/src/App.js
327
web/src/App.js
@ -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 ?
|
||||
<> <h1>Next Terminal</h1></> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
|
||||
<> <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']} <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']} <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>
|
||||
|
@ -4,4 +4,10 @@ export const PROTOCOL_COLORS = {
|
||||
'telnet': 'geekblue',
|
||||
'vnc': 'purple',
|
||||
'kubernetes': 'volcano'
|
||||
}
|
||||
|
||||
export const MODE_COLORS = {
|
||||
'guacd': 'green',
|
||||
'naive': 'orange',
|
||||
'terminal': 'purple',
|
||||
}
|
@ -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
|
||||
})
|
||||
|
@ -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;
|
@ -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;
|
@ -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} {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;
|
23
web/src/components/access/Message.js
Normal file
23
web/src/components/access/Message.js
Normal 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;
|
5
web/src/components/access/Stats.css
Normal file
5
web/src/components/access/Stats.css
Normal file
@ -0,0 +1,5 @@
|
||||
.description-content {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
vertical-align: middle;
|
||||
}
|
232
web/src/components/access/Stats.js
Normal file
232
web/src/components/access/Stats.js
Normal 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'])} {renderSize(rxOfSeconds)}/秒
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="发送" key={'发送' + index}>
|
||||
{renderSize(network[key]['tx'])} {renderSize(txOfSeconds)}/秒
|
||||
</Descriptions.Item>
|
||||
</React.Fragment>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Descriptions>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default Stats;
|
@ -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>
|
||||
);
|
||||
|
84
web/src/components/access/TermMonitor.js
Normal file
84
web/src/components/access/TermMonitor.js
Normal 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;
|
491
web/src/components/asset/AccessGateway.js
Normal file
491
web/src/components/asset/AccessGateway.js
Normal 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;
|
114
web/src/components/asset/AccessGatewayModal.js
Normal file
114
web/src/components/asset/AccessGatewayModal.js
Normal 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;
|
@ -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;
|
||||
|
@ -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”。">
|
||||
程序 <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,此参数无效。">工作目录
|
||||
<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,此参数无效。">参数
|
||||
<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时请选择原生模式。">连接模式
|
||||
<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)时要请求的目标主机。">目标主机
|
||||
<ExclamationCircleOutlined/></Tooltip>}
|
||||
<Form.Item label='目标主机'
|
||||
tooltip='连接到VNC代理(例如UltraVNC Repeater)时要请求的目标主机。'
|
||||
name='dest-host'>
|
||||
<Input placeholder="目标主机"/>
|
||||
</Form.Item>
|
||||
<Form.Item label={<Tooltip
|
||||
title="连接到VNC代理(例如UltraVNC Repeater)时要请求的目标端口。">目标端口
|
||||
<ExclamationCircleOutlined/></Tooltip>}
|
||||
<Form.Item label='目标端口'
|
||||
tooltip='连接到VNC代理(例如UltraVNC Repeater)时要请求的目标端口。'
|
||||
name='dest-port'>
|
||||
<Input type='number' min={1} max={65535}
|
||||
placeholder='目标端口'/>
|
||||
|
324
web/src/components/asset/MyAsset.js
Normal file
324
web/src/components/asset/MyAsset.js
Normal 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;
|
72
web/src/components/asset/MyFile.js
Normal file
72
web/src/components/asset/MyFile.js
Normal 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;
|
@ -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>
|
||||
)}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
</>
|
||||
|
@ -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%;
|
||||
}
|
839
web/src/components/devops/FileSystem.js
Normal file
839
web/src/components/devops/FileSystem.js
Normal 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} {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;
|
@ -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({
|
||||
|
@ -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()
|
||||
},
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
|
348
web/src/components/devops/Storage.js
Normal file
348
web/src/components/devops/Storage.js
Normal 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;
|
62
web/src/components/devops/StorageModal.js
Normal file
62
web/src/components/devops/StorageModal.js
Normal 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;
|
@ -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={{
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
</>
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
435
web/src/components/user/Strategy.js
Normal file
435
web/src/components/user/Strategy.js
Normal 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;
|
78
web/src/components/user/StrategyModal.js
Normal file
78
web/src/components/user/StrategyModal.js
Normal 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;
|
@ -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 ?
|
||||
|
@ -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>
|
||||
</>
|
||||
|
@ -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;
|
@ -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.
Reference in New Issue
Block a user