提交 v1.3.0 beta

This commit is contained in:
dushixiang
2022-10-23 20:05:13 +08:00
parent 4ff4d37442
commit 112435199a
329 changed files with 18340 additions and 58458 deletions

8
web/config-overrides.js Normal file
View File

@ -0,0 +1,8 @@
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
module.exports = function override(config, env) {
config.plugins.push(new MonacoWebpackPlugin({
languages: ['json']
}));
return config;
}

40539
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,35 +1,43 @@
{
"name": "next-terminal",
"version": "1.2.7",
"version": "1.3.0-beta1",
"private": true,
"dependencies": {
"@ant-design/charts": "^1.2.13",
"@ant-design/icons": "^4.6.4",
"antd": "^4.16.13",
"axios": "^0.21.4",
"dayjs": "^1.10.4",
"guacamole-common-js": "^1.3.0",
"js-base64": "^3.7.2",
"qs": "^6.9.4",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"@ant-design/charts": "^1.4.2",
"@ant-design/icons": "^4.7.0",
"@ant-design/pro-components": "1.1.21",
"antd": "4.23.5",
"asciinema-player": "^3.0.1",
"axios": "0.27.2",
"dayjs": "1.11.2",
"guacamole-common-js": "1.4.0-a",
"js-base64": "3.7.2",
"monaco-editor": "^0.34.1",
"monaco-editor-webpack-plugin": "^7.0.1",
"qs": "6.10.3",
"react": "^18.2.0",
"react-app-rewired": "^2.2.1",
"react-dom": "^18.2.0",
"react-draggable": "^4.4.3",
"react-monaco-editor": "0.40.0",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"react-scripts": "^4.0.0",
"xterm": "^4.9.0",
"xterm-addon-fit": "^0.4.0",
"xterm-addon-web-links": "^0.4.0"
"react-monaco-editor": "^0.50.1",
"react-query": "^3.39.2",
"react-router-dom": "^6.3.0",
"react-scripts": "^5.0.1",
"xterm": "4.18.0",
"xterm-addon-fit": "0.5.0",
"xterm-addon-web-links": "0.5.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
"extends": "react-app",
"rules":{
"jsx-a11y/anchor-is-valid":"off"
}
},
"browserslist": [
">0.2%",

View File

@ -1,42 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" type="text/css" href="static/css/asciinema-player.css"/>
<style>
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<asciinema-player id='asciinema-player' src="" rows="42" cols="150"></asciinema-player>
<script src="static/js/asciinema-player.js"></script>
</body>
<script>
const getToken = function () {
return localStorage.getItem('X-Auth-Token');
}
// const server = '//127.0.0.1:8088';
const server = '';
function getQueryVariable(variable) {
const query = window.location.search.substring(1);
const vars = query.split("&");
for (let i = 0; i < vars.length; i++) {
const pair = vars[i].split("=");
if (pair[0] === variable) {
return pair[1];
}
}
return false;
}
let sessionId = getQueryVariable('sessionId');
document.getElementById('asciinema-player').setAttribute('src', `${server}/sessions/${sessionId}/recording?X-Auth-Token=${getToken()}`);
</script>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 175 KiB

After

Width:  |  Height:  |  Size: 134 KiB

View File

@ -12,9 +12,9 @@
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<title>Next Terminal</title>
<title></title>
</head>
<body style="background-color: #8c8c8c">
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,6 @@
@import '~antd/dist/antd.min.css';
@import '~@ant-design/pro-components/dist/components.css';
.trigger {
font-size: 18px;
line-height: 64px;
@ -23,10 +26,6 @@
display: inline-block;
}
.site-layout .site-layout-background {
background: #fff;
}
.site-page-header-ghost-wrapper {
background-color: #FFF;
}
@ -62,8 +61,6 @@
.page-search {
background-color: white;
margin: 16px 16px 0 16px;
padding: 16px;
}
.page-search label {
@ -74,12 +71,23 @@
margin-bottom: 0;
}
.page-content {
.page-container {
margin: 16px;
padding: 24px;
}
.page-content-user {
.page-container-white {
margin: 16px;
padding: 24px;
background-color: white;
}
.page-detail-warp {
margin: 16px;
padding: 0 16px 0 16px;
}
.page-detail-info {
background-color: white;
padding: 24px;
}
@ -111,6 +119,10 @@
padding: 0;
}
.modal-no-padding-bg-xterm .ant-modal-body {
background-color: #121314;
}
.disabled-icon {
cursor: not-allowed;
color: #ccc;
@ -120,6 +132,11 @@
color: #ccc;
}
.nt-container {
width: 80%;
margin: 20px auto 0;
}
.km-header {
color: white;
width: 80%;
@ -137,22 +154,6 @@
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;
@ -161,4 +162,53 @@
.kd-page-header {
background-color: white;
margin-top: 20px;
}
}
.ant-page-header {
padding: 0 !important;
}
.danger {
color: red;
}
.danger:hover {
color: red !important;
}
.xterm-viewport::-webkit-scrollbar {
background-color: transparent;
width: 12px;
}
.xterm-viewport::-webkit-scrollbar-thumb {
background-color: inherit;
border-radius: 8px;
background-clip: content-box;
border: 2px solid transparent;
}
.xterm-viewport[scroll]::-webkit-scrollbar-thumb,
.xterm-viewport::-webkit-scrollbar-thumb:hover {
background-color: #bfbfbf;
transition: 0s;
}
/*.ant-layout-sider::-webkit-scrollbar {*/
/* background-color: transparent;*/
/* !*background-color: red;*!*/
/* width: 10px;*/
/*}*/
/*.ant-layout-sider::-webkit-scrollbar-thumb {*/
/* background-color: inherit;*/
/* border-radius: 8px;*/
/* background-clip: content-box;*/
/* border: 2px solid transparent;*/
/*}*/
/*.ant-layout-sider[scroll]::-webkit-scrollbar-thumb,*/
/*.ant-layout-sider::-webkit-scrollbar-thumb:hover {*/
/* background-color: #bfbfbf;*/
/* transition: 0s;*/
/*}*/

View File

@ -1,426 +1,140 @@
import React, {Component} from 'react';
import 'antd/dist/antd.css';
import React, {Suspense} from 'react';
import {Outlet, Route, Routes, useNavigate} from "react-router-dom";
import './App.css';
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";
import Access from "./components/access/Access";
import User from "./components/user/User";
import OnlineSession from "./components/session/OnlineSession";
import OfflineSession from "./components/session/OfflineSession";
import Login from "./components/Login";
import DynamicCommand from "./components/command/DynamicCommand";
import Credential from "./components/credential/Credential";
import LogoWithName from './images/logo-with-name.svg'
import Logo from './images/logo.svg'
import {
ApiOutlined,
AuditOutlined,
BlockOutlined,
CloudServerOutlined,
CodeOutlined,
ControlOutlined,
DashboardOutlined,
DesktopOutlined,
DisconnectOutlined,
DownOutlined,
FolderOutlined,
GithubOutlined,
HddOutlined,
IdcardOutlined,
InsuranceOutlined,
LinkOutlined,
LoginOutlined,
LogoutOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
SafetyCertificateOutlined,
SettingOutlined,
SolutionOutlined,
TeamOutlined,
UserOutlined,
UserSwitchOutlined
} from '@ant-design/icons';
import Info from "./components/user/Info";
import request from "./common/request";
import {message} from "antd/es";
import Setting from "./components/setting/Setting";
import BatchCommand from "./components/command/BatchCommand";
import {isEmpty, NT_PACKAGE} from "./utils/utils";
import {getCurrentUser, isAdmin} from "./service/permission";
import UserGroup from "./components/user/UserGroup";
import LoginLog from "./components/devops/LoginLog";
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";
import './Arco.css';
import ManagerLayout from "./layout/ManagerLayout";
import UserLayout from "./layout/UserLayout";
const {Footer, Content, Sider} = Layout;
import NoMatch from "./components/NoMatch";
import Landing from "./components/Landing";
import NoPermission from "./components/NoPermission";
import {useQuery} from "react-query";
import accountApi from "./api/account";
import {setCurrentUser} from "./service/permission";
const {SubMenu} = Menu;
const headerHeight = 60;
const GuacdMonitor = React.lazy(() => import("./components/session/GuacdMonitor"));
const GuacdPlayback = React.lazy(() => import("./components/session/GuacdPlayback"));
const TermMonitor = React.lazy(() => import("./components/session/TermMonitor"));
const TermPlayback = React.lazy(() => import("./components/session/TermPlayback"));
class App extends Component {
const BatchCommand = React.lazy(() => import("./components/devops/BatchCommand"));
const LoginPolicyDetail = React.lazy(() => import("./components/security/LoginPolicyDetail"));
const Login = React.lazy(() => import("./components/Login"));
const Dashboard = React.lazy(() => import("./components/dashboard/Dashboard"));
const Monitoring = React.lazy(() => import("./components/dashboard/Monitoring"));
state = {
collapsed: false,
current: sessionStorage.getItem('current'),
openKeys: sessionStorage.getItem('openKeys') ? JSON.parse(sessionStorage.getItem('openKeys')) : [],
user: {
'nickname': '未定义'
},
package: NT_PACKAGE(),
triggerMenu: true,
logo: LogoWithName,
logoWidth: 140
};
const Asset = React.lazy(() => import("./components/asset/Asset"));
const AssetDetail = React.lazy(() => import("./components/asset/AssetDetail"));
const MyFile = React.lazy(() => import("./components/worker/MyFile"));
const AccessGateway = React.lazy(() => import("./components/asset/AccessGateway"));
const MyAsset = React.lazy(() => import("./components/worker/MyAsset"));
const MyCommand = React.lazy(() => import("./components/worker/MyCommand"));
const MyInfo = React.lazy(() => import("./components/worker/MyInfo"));
onCollapse = () => {
let collapsed = !this.state.collapsed;
if (collapsed) {
this.setState({
logo: Logo,
logoWidth: 46,
collapsed: collapsed,
});
} else {
this.setState({
logo: LogoWithName,
logoWidth: 140,
collapsed: collapsed,
});
const Access = React.lazy(() => import("./components/access/Access"));
const Term = React.lazy(() => import("./components/access/Term"));
const User = React.lazy(() => import("./components/user/user/User"));
const UserDetailPage = React.lazy(() => import("./components/user/user/UserDetailPage"));
const Role = React.lazy(() => import("./components/user/Role"));
const RoleDetail = React.lazy(() => import("./components/user/RoleDetail"));
const UserGroup = React.lazy(() => import("./components/user/UserGroup"));
const UserGroupDetail = React.lazy(() => import("./components/user/UserGroupDetail"));
const Strategy = React.lazy(() => import("./components/authorised/Strategy"));
const StrategyDetail = React.lazy(() => import("./components/authorised/StrategyDetail"));
const Info = React.lazy(() => import("./components/Info"));
const OnlineSession = React.lazy(() => import("./components/session/OnlineSession"));
const OfflineSession = React.lazy(() => import("./components/session/OfflineSession"));
const Command = React.lazy(() => import("./components/asset/Command"));
const ExecuteCommand = React.lazy(() => import("./components/devops/ExecuteCommand"));
const Credential = React.lazy(() => import("./components/asset/Credential"));
const Job = React.lazy(() => import("./components/devops/Job"));
const LoginLog = React.lazy(() => import("./components/log-audit/LoginLog"));
const StorageLog = React.lazy(() => import("./components/log-audit/StorageLog"));
const Security = React.lazy(() => import("./components/security/Security"));
const Storage = React.lazy(() => import("./components/devops/Storage"));
const Setting = React.lazy(() => import("./components/setting/Setting"));
const LoginPolicy = React.lazy(() => import("./components/security/LoginPolicy"));
const App = () => {
const navigate = useNavigate();
useQuery('getUserInfo', accountApi.getUserInfo, {
onSuccess: (data) => {
setCurrentUser(data);
if (data?.type === 'user') {
navigate('/my-asset');
}
}
};
});
componentDidMount() {
let hash = window.location.hash;
let current = hash.replace('#/', '');
if (isEmpty(current)) {
current = 'dashboard';
}
this.setCurrent(current);
this.getInfo();
}
return (
<Routes>
<Route element={
<Suspense fallback={<Landing/>}>
<Outlet/>
</Suspense>
}>
<Route path="/access" element={<Access/>}/>
<Route path="/term" element={<Term/>}/>
<Route path="/term-monitor" element={<TermMonitor/>}/>
<Route path="/term-playback" element={<TermPlayback/>}/>
<Route path="/guacd-monitor" element={<GuacdMonitor/>}/>
<Route path="/guacd-playback" element={<GuacdPlayback/>}/>
<Route path="/login" element={<Login/>}/>
<Route path="/permission-denied" element={<NoPermission/>}/>
<Route path="*" element={<NoMatch/>}/>
</Route>
async getInfo() {
<Route element={<ManagerLayout/>}>
<Route path="/" element={<Dashboard/>}/>
<Route path="/dashboard" element={<Dashboard/>}/>
<Route path="/monitoring" element={<Monitoring/>}/>
let result = await request.get('/account/info');
if (result['code'] === 1) {
sessionStorage.setItem('user', JSON.stringify(result['data']));
this.setState({
user: result['data'],
triggerMenu: true
})
} else {
message.error(result['message']);
}
}
<Route path="/user" element={<User/>}/>
<Route path="/user/:userId" element={<UserDetailPage/>}/>
<Route path="/role" element={<Role/>}/>
<Route path="/role/:roleId" element={<RoleDetail/>}/>
<Route path="/user-group" element={<UserGroup/>}/>
<Route path="/user-group/:userGroupId" element={<UserGroupDetail/>}/>
updateUser = (user) => {
this.setState({
user: user
})
}
<Route path="/asset" element={<Asset/>}/>
<Route path="/asset/:assetId" element={<AssetDetail/>}/>
<Route path="/credential" element={<Credential/>}/>
<Route path="/command" element={<Command/>}/>
<Route path="/batch-command" element={<BatchCommand/>}/>
<Route path="/execute-command" element={<ExecuteCommand/>}/>
<Route path="/online-session" element={<OnlineSession/>}/>
<Route path="/offline-session" element={<OfflineSession/>}/>
<Route path="/login-log" element={<LoginLog/>}/>
<Route path="/storage-log" element={<StorageLog/>}/>
<Route path="/info" element={<Info/>}/>
<Route path="/setting" element={<Setting/>}/>
<Route path="/job" element={<Job/>}/>
<Route path="/file" element={<MyFile/>}/>
<Route path="/access-security" element={<Security/>}/>
<Route path="/access-gateway" element={<AccessGateway/>}/>
<Route path="/storage" element={<Storage/>}/>
<Route path="/strategy" element={<Strategy/>}/>
<Route path="/strategy/:strategyId" element={<StrategyDetail/>}/>
<Route path="/login-policy" element={<LoginPolicy/>}/>
<Route path="/login-policy/:loginPolicyId" element={<LoginPolicyDetail/>}/>
</Route>
setCurrent = (key) => {
this.setState({
current: key
})
sessionStorage.setItem('current', key);
}
subMenuChange = (openKeys) => {
this.setState({
openKeys: openKeys
})
sessionStorage.setItem('openKeys', JSON.stringify(openKeys));
}
confirm = async () => {
let result = await request.post('/account/logout');
if (result['code'] !== 1) {
message.error(result['message']);
} else {
message.success('退出登录成功,即将跳转至登录页面。');
window.location.reload();
}
}
render() {
const menu = (
<Menu>
<Menu.Item>
<Link to={'/info'}>
<SolutionOutlined/> 个人中心
</Link>
</Menu.Item>
<Menu.Divider/>
<Menu.Item>
<Popconfirm
key='login-btn-pop'
title="您确定要退出登录吗?"
onConfirm={this.confirm}
okText="确定"
cancelText="取消"
placement="left"
>
<LogoutOutlined/> 退出登录
</Popconfirm>
</Menu.Item>
</Menu>
);
return (
<Switch>
<Route path="/access" component={Access}/>
<Route path="/term" component={Term}/>
<Route path="/login"><Login updateUser={this.updateUser}/></Route>
<Route path="/">
<Layout className="layout" style={{minHeight: '100vh'}}>
{
isAdmin() ?
<>
<Sider collapsible collapsed={this.state.collapsed} trigger={null}>
<div className="logo">
<img src={this.state.logo} alt='logo' width={this.state.logoWidth}/>
</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'}}>
<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>
<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>
<SubMenu key='audit' title='会话审计' icon={<AuditOutlined/>}>
<Menu.Item key="online-session" icon={<LinkOutlined/>}>
<Link to={'/online-session'}>
在线会话
</Link>
</Menu.Item>
<Menu.Item key="offline-session" icon={<DisconnectOutlined/>}>
<Link to={'/offline-session'}>
历史会话
</Link>
</Menu.Item>
</SubMenu>
<SubMenu key='ops' title='系统运维' icon={<ControlOutlined/>}>
<Menu.Item key="login-log" icon={<LoginOutlined/>}>
<Link to={'/login-log'}>
登录日志
</Link>
</Menu.Item>
<Menu.Item key="job" icon={<BlockOutlined/>}>
<Link to={'/job'}>
计划任务
</Link>
</Menu.Item>
<Menu.Item key="access-security" icon={<SafetyCertificateOutlined/>}>
<Link to={'/access-security'}>
访问安全
</Link>
</Menu.Item>
<Menu.Item key="storage" icon={<HddOutlined/>}>
<Link to={'/storage'}>
磁盘空间
</Link>
</Menu.Item>
</SubMenu>
<SubMenu key='user-manage' title='用户管理' icon={<UserSwitchOutlined/>}>
<Menu.Item key="user" icon={<UserOutlined/>}>
<Link to={'/user'}>
用户管理
</Link>
</Menu.Item>
<Menu.Item key="user-group" icon={<TeamOutlined/>}>
<Link to={'/user-group'}>
用户组管理
</Link>
</Menu.Item>
<Menu.Item key="strategy" icon={<InsuranceOutlined/>}>
<Link to={'/strategy'}>
授权策略
</Link>
</Menu.Item>
</SubMenu>
<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>
</Menu>
</Sider>
<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>
<div className='layout-header-right'>
<div className={'layout-header-right-item'}>
<a style={{color: 'black'}} target='_blank'
href='https://github.com/dushixiang/next-terminal'
rel='noreferrer noopener'>
<GithubOutlined/>
</a>
</div>
</div>
<div className='layout-header-right'>
<Dropdown overlay={menu}>
<div className='nickname layout-header-right-item'>
{getCurrentUser()['nickname']} &nbsp;<DownOutlined/>
</div>
</Dropdown>
</div>
</div>
</Header>
<Route path="/" exact component={Dashboard}/>
<Route path="/user" component={User}/>
<Route path="/user-group" component={UserGroup}/>
<Route path="/asset" component={Asset}/>
<Route path="/credential" component={Credential}/>
<Route path="/dynamic-command" component={DynamicCommand}/>
<Route path="/batch-command" component={BatchCommand}/>
<Route path="/online-session" component={OnlineSession}/>
<Route path="/offline-session" component={OfflineSession}/>
<Route path="/login-log" component={LoginLog}/>
<Route path="/info" component={Info}/>
<Route path="/setting" component={Setting}/>
<Route path="/job" component={Job}/>
<Route path="/access-security" component={Security}/>
<Route path="/access-gateway" component={AccessGateway}/>
<Route path="/my-file" component={MyFile}/>
<Route path="/storage" component={Storage}/>
<Route path="/strategy" component={Strategy}/>
<Footer style={{textAlign: 'center'}}>
Copyright © 2020-2022 dushixiang, All Rights Reserved.
Version:{this.state.package['version']}
</Footer>
</Layout>
</> :
<>
<Header style={{padding: 0}}>
<div className='km-header'>
<div style={{flex: '1 1 0%'}}>
<Link to={'/'}>
<img src={this.state.logo} alt='logo' width={120}/>
</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'}}>
Copyright © 2020-2022 dushixiang, All Rights Reserved.
Version:{this.state.package['version']}
</Footer>
</>
}
</Layout>
</Route>
</Switch>
);
}
<Route element={<UserLayout/>}>
<Route path="/my-asset" element={<MyAsset/>}/>
<Route path="/my-info" element={<MyInfo/>}/>
<Route path="/my-file" element={<MyFile/>}/>
<Route path="/my-command" element={<MyCommand/>}/>
</Route>
</Routes>
);
}
export default App;

0
web/src/Arco.css Normal file
View File

View File

@ -0,0 +1,10 @@
import Api from "./api";
class AccessGatewayApi extends Api{
constructor() {
super("access-gateways");
}
}
let accessGatewayApi = new AccessGatewayApi();
export default accessGatewayApi;

78
web/src/api/account.js Normal file
View File

@ -0,0 +1,78 @@
import request from "../common/request";
import qs from "qs";
class AccountApi {
group = 'account';
logout = async () => {
let result = await request.post('/account/logout');
return result['code'] === 1
}
getUserInfo = async () => {
let result = await request.get(`/${this.group}/info`);
if (result['code'] !== 1) {
return {};
}
return result['data'];
}
assetPaging = async (params) => {
let paramsStr = qs.stringify(params);
let result = await request.get(`/${this.group}/assets?${paramsStr}`);
if (result['code'] !== 1) {
return {};
}
return result['data'];
}
getAccessToken = async () => {
let result = await request.get(`/${this.group}/access-token`);
if (result['code'] !== 1) {
return {};
}
return result['data'];
}
createAccessToken = async () => {
let result = await request.post(`/${this.group}/access-token`);
if (result['code'] !== 1) {
return {};
}
return result['data'];
}
deleteAccessToken = async () => {
let result = await request.delete(`/${this.group}/access-token`);
return result['code'] === 1;
}
changePassword = async (values) => {
let result = await request.post(`/${this.group}/change-password`, values);
return result.code === 1;
}
reloadTotp = async () => {
let result = await request.get('/account/reload-totp');
if (result.code === 1) {
return result.data;
} else {
return {}
}
}
confirmTotp = async (values) => {
let result = await request.post(`/${this.group}/confirm-totp`, values);
return result.code === 1;
}
resetTotp = async () => {
let result = await request.post(`/${this.group}/reset-totp`);
return result.code === 1;
}
}
let accountApi = new AccountApi();
export default accountApi;

50
web/src/api/api.js Normal file
View File

@ -0,0 +1,50 @@
import request from "../common/request";
import qs from "qs";
export default class Api {
group = "";
constructor(group) {
this.group = group;
}
getById = async (id) => {
let result = await request.get(`/${this.group}/${id}`);
if (result['code'] !== 1) {
return;
}
return result['data'];
}
getPaging = async (params) => {
let paramsStr = qs.stringify(params);
let result = await request.get(`/${this.group}/paging?${paramsStr}`);
if (result['code'] !== 1) {
return {};
}
return result['data'];
}
getAll = async () => {
let result = await request.get(`/${this.group}`);
if (result['code'] !== 1) {
return [];
}
return result['data'];
}
create = async (data) => {
const result = await request.post(`/${this.group}`, data);
return result['code'] === 1;
}
updateById = async (id, data) => {
const result = await request.put(`/${this.group}/${id}`, data);
return result['code'] === 1;
}
deleteById = async (id) => {
const result = await request.delete(`/${this.group}/${id}`);
return result['code'] === 1;
}
}

42
web/src/api/asset.js Normal file
View File

@ -0,0 +1,42 @@
import Api from "./api";
import request from "../common/request";
class AssetApi extends Api {
constructor() {
super("assets");
}
GetAll = async (protocol = '') => {
let result = await request.get(`/${this.group}?protocol=${protocol}`);
if (result['code'] !== 1) {
return [];
}
return result['data'];
}
connTest = async (id) => {
let result = await request.post(`/${this.group}/${id}/tcping`);
if (result.code !== 1) {
return [false, result.message];
}
return [result['data']['active'], result['data']['message']];
}
importAsset = async (file) => {
const formData = new FormData();
formData.append("file", file,);
let result = await request.post(`/${this.group}/import`, formData, {'Content-Type': 'multipart/form-data'});
if (result.code !== 1) {
return [false, result.message];
}
return [true, result['data']];
}
changeOwner = async (id, owner) => {
let result = await request.post(`/${this.group}/${id}/change-owner?owner=${owner}`);
return result['code'] === 1;
}
}
const assetApi = new AssetApi();
export default assetApi;

66
web/src/api/authorised.js Normal file
View File

@ -0,0 +1,66 @@
import qs from "qs";
import request from "../common/request";
class AuthorisedApi {
group = "authorised";
GetAssetPaging = async (params) => {
let paramsStr = qs.stringify(params);
let result = await request.get(`/${this.group}/assets/paging?${paramsStr}`);
if (result['code'] !== 1) {
return {};
}
return result['data'];
}
GetUserPaging = async (params) => {
let paramsStr = qs.stringify(params);
let result = await request.get(`/${this.group}/users/paging?${paramsStr}`);
if (result['code'] !== 1) {
return {};
}
return result['data'];
}
GetUserGroupPaging = async (params) => {
let paramsStr = qs.stringify(params);
let result = await request.get(`/${this.group}/user-groups/paging?${paramsStr}`);
if (result['code'] !== 1) {
return {};
}
return result['data'];
}
AuthorisedAssets = async (data) => {
const result = await request.post(`/${this.group}/assets`, data);
return result['code'] === 1;
}
AuthorisedUsers = async (data) => {
const result = await request.post(`/${this.group}/users`, data);
return result['code'] === 1;
}
AuthorisedUserGroups = async (data) => {
const result = await request.post(`/${this.group}/user-groups`, data);
return result['code'] === 1;
}
GetSelected = async (params) => {
let paramsStr = qs.stringify(params);
let result = await request.get(`/${this.group}/selected?${paramsStr}`);
if (result['code'] !== 1) {
return [];
}
return result['data'];
}
DeleteById = async (id) => {
const result = await request.delete(`/${this.group}/${id}`);
return result['code'] === 1;
}
}
const authorisedApi = new AuthorisedApi();
export default authorisedApi;

15
web/src/api/branding.js Normal file
View File

@ -0,0 +1,15 @@
import request from "../common/request";
class BrandingApi {
getBranding = async () => {
let result = await request.get(`/branding`);
if (result['code'] !== 1) {
return {};
}
return result['data'];
}
}
let brandingApi = new BrandingApi();
export default brandingApi;

View File

@ -0,0 +1,10 @@
import Api from "./api";
class CommandFilterRuleApi extends Api{
constructor() {
super("command-filter-rules");
}
}
const commandFilterRuleApi = new CommandFilterRuleApi();
export default commandFilterRuleApi;

View File

@ -0,0 +1,38 @@
import request from "../common/request";
import Api from "./api";
class CommandFilterApi extends Api{
constructor() {
super("command-filters");
}
Bind = async (id, data) => {
const result = await request.post(`/${this.group}/${id}/bind`, data);
return result['code'] === 1;
}
Unbind = async (id, data) => {
const result = await request.post(`/${this.group}/${id}/unbind`, data);
return result['code'] === 1;
}
GetAssetIdByCommandFilterId = async (commandFilterId) => {
let result = await request.get(`/${this.group}/${commandFilterId}/assets/id`);
if (result['code'] !== 1) {
return [];
}
return result['data'];
}
GetAll = async () => {
let result = await request.get(`/${this.group}`);
if (result['code'] !== 1) {
return [];
}
return result['data'];
}
}
const commandFilterApi = new CommandFilterApi();
export default commandFilterApi;

16
web/src/api/command.js Normal file
View File

@ -0,0 +1,16 @@
import Api from "./api";
import request from "../common/request";
class CommandApi extends Api{
constructor() {
super("commands");
}
changeOwner = async (id, owner) => {
let result = await request.post(`/${this.group}/${id}/change-owner?owner=${owner}`);
return result['code'] === 1;
}
}
let commandApi = new CommandApi();
export default commandApi;

19
web/src/api/credential.js Normal file
View File

@ -0,0 +1,19 @@
import Api from "./api";
import request from "../common/request";
class CredentialApi extends Api{
constructor() {
super("credentials");
}
getAll = async () => {
let result = await request.get(`/${this.group}`);
if (result['code'] !== 1) {
return [];
}
return result['data'];
}
}
let credentialApi = new CredentialApi();
export default credentialApi;

36
web/src/api/job.js Normal file
View File

@ -0,0 +1,36 @@
import Api from "./api";
import request from "../common/request";
import qs from "qs";
class JobApi extends Api {
constructor() {
super("jobs");
}
changeStatus = async (id, status) => {
let result = await request.post(`/${this.group}/${id}/change-status?status=${status}`);
return result['code'] !== 1;
}
exec = async (id) => {
let result = await request.post(`/${this.group}/${id}/exec`);
return result['code'] !== 1;
}
getLogPaging = async (id, params) => {
let paramsStr = qs.stringify(params);
let result = await request.get(`/${this.group}/${id}/logs/paging?${paramsStr}`);
if (result['code'] !== 1) {
return {};
}
return result['data'];
}
deleteLogByJobId = async (id) => {
let result = await request.delete(`/${this.group}/${id}/logs`);
return result['code'] !== 1;
}
}
let jobApi = new JobApi();
export default jobApi;

17
web/src/api/license.js Normal file
View File

@ -0,0 +1,17 @@
import request from "../common/request";
export const GetLicense = async () => {
let result = await request.get('/license');
if (result['code'] !== 1) {
return;
}
return result['data'];
}
export const GetMachineId = async () => {
let result = await request.get('/license/machine-id');
if (result['code'] !== 1) {
return;
}
return result['data'];
}

16
web/src/api/login-log.js Normal file
View File

@ -0,0 +1,16 @@
import Api from "./api";
import request from "../common/request";
class LoginLogApi extends Api{
constructor() {
super("login-logs");
}
Clear = async () => {
const result = await request.post(`/${this.group}/clear`);
return result['code'] === 1;
}
}
let loginLogApi = new LoginLogApi();
export default loginLogApi;

View File

@ -0,0 +1,40 @@
import request from "../common/request";
import qs from "qs";
import Api from "./api";
class LoginPolicyApi extends Api{
constructor() {
super("login-policies");
}
Bind = async (id, data) => {
const result = await request.post(`/${this.group}/${id}/bind`, data);
return result['code'] === 1;
}
Unbind = async (id, data) => {
const result = await request.post(`/${this.group}/${id}/unbind`, data);
return result['code'] === 1;
}
GetUserPagingByForbiddenCommandId = async (id, params) => {
let paramsStr = qs.stringify(params);
let result = await request.get(`/${this.group}/${id}/users/paging?${paramsStr}`);
if (result['code'] !== 1) {
return {};
}
return result['data'];
}
GetUserIdByLoginPolicyId = async (id) => {
let result = await request.get(`/${this.group}/${id}/users/id`);
if (result['code'] !== 1) {
return [];
}
return result['data'];
}
}
const loginPolicyApi = new LoginPolicyApi();
export default loginPolicyApi;

37
web/src/api/monitor.js Normal file
View File

@ -0,0 +1,37 @@
import request from "../common/request";
class MonitorApi {
getData = async () => {
let result = await request.get('/overview/ps');
if (result['code'] !== 1) {
return {};
}
let data = result['data'];
let netIO = [];
for (let i = 0; i < data['netIO'].length; i++) {
let item = data['netIO'][i];
netIO.push({
time: item['time'],
read: item['read'] / 1024 / 1024 / 1024,
write: item['write'] / 1024 / 1024 / 1024,
});
}
data['netIO'] = netIO;
let diskIO = [];
for (let i = 0; i < data['diskIO'].length; i++) {
let item = data['diskIO'][i];
diskIO.push({
time: item['time'],
read: item['read'] / 1024 / 1024 / 1024,
write: item['write'] / 1024 / 1024 / 1024,
});
}
data['diskIO'] = diskIO;
return data
}
}
let monitorApi = new MonitorApi();
export default monitorApi;

16
web/src/api/permission.js Normal file
View File

@ -0,0 +1,16 @@
import request from "../common/request";
class PermissionApi {
group = "permissions";
getMenus = async () => {
let result = await request.get(`/menus`);
if (result['code'] !== 1) {
return [];
}
return result['data'];
}
}
let permissionApi = new PermissionApi();
export default permissionApi;

19
web/src/api/role.js Normal file
View File

@ -0,0 +1,19 @@
import request from "../common/request";
import Api from "./api";
class RoleApi extends Api {
constructor() {
super("roles");
}
GetAll = async () => {
let result = await request.get(`/${this.group}`);
if (result['code'] !== 1) {
return [];
}
return result['data'];
}
}
let roleApi = new RoleApi();
export default roleApi;

10
web/src/api/security.js Normal file
View File

@ -0,0 +1,10 @@
import Api from "./api";
class SecurityApi extends Api {
constructor() {
super("securities");
}
}
let securityApi = new SecurityApi();
export default securityApi;

52
web/src/api/session.js Normal file
View File

@ -0,0 +1,52 @@
import Api from "./api";
import qs from "qs";
import request from "../common/request";
class SessionApi extends Api {
constructor() {
super("sessions");
}
GetCommandPagingBySessionId = async (sessionId, params) => {
let paramsStr = qs.stringify(params);
let result = await request.get(`/${this.group}/${sessionId}/commands/paging?${paramsStr}`);
if (result['code'] !== 1) {
return {};
}
return result['data'];
}
create = async (assetsId, mode) => {
let result = await request.post(`/${this.group}?assetId=${assetsId}&mode=${mode}`);
if (result['code'] !== 1) {
return {};
}
return result['data'];
}
connect = async (sessionId) => {
let result = await request.post(`/${this.group}/${sessionId}/connect`);
return result['code'] === 1;
}
disconnect = async (sessionId) => {
let result = await request.post(`/${this.group}/${sessionId}/disconnect`);
return result['code'] === 1;
}
clear = async () => {
let result = await request.post(`/${this.group}/clear`);
return result['code'] === 1;
}
stats = async (sessionId) => {
let result = await request.get(`/${this.group}/${sessionId}/stats`);
if (result['code'] !== 1) {
return {};
}
return result['data'];
}
}
const sessionApi = new SessionApi();
export default sessionApi;

View File

@ -0,0 +1,23 @@
import Api from "./api";
import request from "../common/request";
class StorageLogApi extends Api {
constructor() {
super("storage-logs");
}
create = () => {
}
getById = () => {
}
updateById = () => {
}
Clear = async () => {
const result = await request.post(`/${this.group}/clear`);
return result['code'] === 1;
}
}
const storageLogApi = new StorageLogApi();
export default storageLogApi;

10
web/src/api/storage.js Normal file
View File

@ -0,0 +1,10 @@
import Api from "./api";
class StorageApi extends Api{
constructor() {
super("storages");
}
}
let storageApi = new StorageApi();
export default storageApi;

19
web/src/api/strategy.js Normal file
View File

@ -0,0 +1,19 @@
import Api from "./api";
import request from "../common/request";
class StrategyApi extends Api {
constructor() {
super("strategies");
}
GetAll = async () => {
let result = await request.get(`/${this.group}`);
if (result['code'] !== 1) {
return [];
}
return result['data'];
}
}
const strategyApi = new StrategyApi();
export default strategyApi;

15
web/src/api/tag.js Normal file
View File

@ -0,0 +1,15 @@
import request from "../common/request";
class TagApi {
getAll = async () => {
let result = await request.get(`/tags`);
if (result['code'] !== 1) {
return [];
}
return result['data'];
}
}
let tagApi = new TagApi();
export default tagApi;

19
web/src/api/user-group.js Normal file
View File

@ -0,0 +1,19 @@
import Api from "./api";
import request from "../common/request";
class UserGroupApi extends Api {
constructor() {
super("user-groups");
}
GetAll = async () => {
let result = await request.get(`/${this.group}`);
if (result['code'] !== 1) {
return [];
}
return result['data'];
}
}
const userGroupApi = new UserGroupApi();
export default userGroupApi;

28
web/src/api/user.js Normal file
View File

@ -0,0 +1,28 @@
import Api from "./api";
import request from "../common/request";
class UserApi extends Api {
constructor() {
super("users");
}
resetTotp = async (id) => {
let result = await request.post(`/${this.group}/${id}/reset-totp`);
return result['code'] === 1;
}
changePassword = async (id, password) => {
let formData = new FormData();
formData.set('password', password);
let result = await request.post(`/${this.group}/${id}/change-password`, formData);
return result['code'] === 1;
}
changeStatus = async (id, status) => {
let result = await request.patch(`/${this.group}/${id}/status?status=${status}`);
return result['code'] !== 1;
}
}
const userApi = new UserApi();
export default userApi;

View File

@ -0,0 +1,19 @@
import Api from "../api";
import request from "../../common/request";
class WorkAssetApi extends Api{
constructor() {
super("worker/assets");
}
tags = async () => {
let result = await request.get(`/${this.group}/tags`);
if (result['code'] !== 1) {
return [];
}
return result['data'];
}
}
let workAssetApi = new WorkAssetApi();
export default workAssetApi;

View File

@ -0,0 +1,10 @@
import Api from "../api";
class WorkCommandApi extends Api{
constructor() {
super("worker/commands");
}
}
let workCommandApi = new WorkCommandApi();
export default workCommandApi;

8
web/src/common/auth.js Normal file
View File

@ -0,0 +1,8 @@
export const HasPermission = (permission) => {
let permissionsStr = sessionStorage.getItem('permissions');
let permissions = JSON.parse(permissionsStr);
if (!permissions) {
return false;
}
return permissions.includes(permission);
}

View File

@ -28,6 +28,14 @@ const handleResult = (result) => {
if (result['code'] === 401) {
window.location.href = '#/login';
return false;
}if (result['code'] === 403) {
window.location.href = '#/permission-denied';
return false;
} else if (result['code'] === 100) {
return true;
} else if (result['code'] !== 1) {
message.error(result['message']);
return false;
}
return true;
}
@ -40,8 +48,9 @@ const request = {
return new Promise((resolve, reject) => {
axios.get(url, {headers: headers})
.then((response) => {
if (!handleResult(response.data)) {
return;
let contentType = response.headers['content-type'];
if (contentType !== '' && contentType.includes('application/json')) {
handleResult(response.data);
}
resolve(response.data);
})
@ -54,16 +63,20 @@ const request = {
})
},
post: function (url, params) {
post: function (url, params, header) {
const headers = getHeaders();
if (header) {
for (const k in header) {
headers[k] = header[k];
}
}
return new Promise((resolve, reject) => {
axios.post(url, params, {headers: headers})
.then((response) => {
if (!handleResult(response.data)) {
return;
}
handleResult(response.data);
resolve(response.data);
})
.catch((error) => {
@ -82,9 +95,7 @@ const request = {
return new Promise((resolve, reject) => {
axios.put(url, params, {headers: headers})
.then((response) => {
if (!handleResult(response.data)) {
return;
}
handleResult(response.data);
resolve(response.data);
})
.catch((error) => {
@ -102,9 +113,7 @@ const request = {
return new Promise((resolve, reject) => {
axios.delete(url, {headers: headers})
.then((response) => {
if (!handleResult(response.data)) {
return;
}
handleResult(response.data);
resolve(response.data);
})
.catch((error) => {
@ -122,9 +131,7 @@ const request = {
return new Promise((resolve, reject) => {
axios.patch(url, params, {headers: headers})
.then((response) => {
if (!handleResult(response.data)) {
return;
}
handleResult(response.data);
resolve(response.data);
})
.catch((error) => {

11
web/src/common/router.js Normal file
View File

@ -0,0 +1,11 @@
import {useLocation, useNavigate, useParams, useSearchParams} from "react-router-dom";
export const withRouter = (Component) => {
return (props) => {
const location = useLocation();
const navigate = useNavigate();
const params = useParams();
const searchParams = useSearchParams();
return <Component {...props} location={location} navigate={navigate} params={params} searchParams={searchParams}/>;
};
}

View File

@ -0,0 +1,51 @@
import React from 'react';
import {Button, Descriptions, Space, Typography} from "antd";
import {useQuery} from "react-query";
import accountApi from "../api/account";
const {Title, Text} = Typography;
const AccessToken = () => {
let tokenQuery = useQuery('getAccessToken', accountApi.getAccessToken);
const genAccessToken = async () => {
await accountApi.createAccessToken();
await tokenQuery.refetch();
}
const clearAccessToken = async () => {
let success = await accountApi.deleteAccessToken();
if (success) {
await tokenQuery.refetch();
}
}
return (
<div>
<Title level={4}>授权令牌</Title>
<div style={{margin: 16}}></div>
<Descriptions column={1}>
<Descriptions.Item label="授权令牌">
<Text strong copyable>{tokenQuery.data?.token}</Text>
</Descriptions.Item>
<Descriptions.Item label="生成时间">
<Text strong>{tokenQuery.data?.created}</Text>
</Descriptions.Item>
</Descriptions>
<Space>
<Button type="primary" onClick={genAccessToken}>
重新生成
</Button>
<Button type="primary" danger disabled={tokenQuery.data?.token === ''}
onClick={clearAccessToken}>
删除令牌
</Button>
</Space>
</div>
);
};
export default AccessToken;

118
web/src/components/Info.js Normal file
View File

@ -0,0 +1,118 @@
import React, {useState} from 'react';
import {Button, Form, Input, Layout, message, Tabs, Typography} from "antd";
import accountApi from "../api/account";
import Totp from "./Totp";
const {Content} = Layout;
const {Title} = Typography;
const Info = () => {
let [newPassword1, setNewPassword1] = useState('');
let [newPassword2, setNewPassword2] = useState('');
let [newPasswordStatus, setNewPasswordStatus] = useState({});
const onNewPasswordChange = (value) => {
setNewPassword1(value.target.value);
setNewPasswordStatus(validateNewPassword(value.target.value, newPassword2));
}
const onNewPassword2Change = (value) => {
setNewPassword2(value.target.value);
setNewPasswordStatus(validateNewPassword(newPassword1, value.target.value));
}
const validateNewPassword = (newPassword1, newPassword2) => {
if (newPassword2 === newPassword1) {
return {
validateStatus: 'success',
errorMsg: null,
};
}
return {
validateStatus: 'error',
errorMsg: '两次输入的密码不一致',
};
}
const changePassword = async (values) => {
let success = await accountApi.changePassword(values);
if (success) {
message.success('密码修改成功,即将跳转至登录页面');
window.location.href = '/#';
}
}
return (
<>
<Content className={'page-container-white'}>
<Tabs className={'info-tab'} tabPosition={'left'} tabBarStyle={{width: 150}}>
<Tabs.TabPane tab="修改密码" key="change-password">
<Title level={4}>修改密码</Title>
<div style={{margin: 16}}></div>
<Form name="password" onFinish={changePassword}>
<input type='password' hidden={true} autoComplete='new-password'/>
<Form.Item
name="oldPassword"
label="原始密码"
rules={[
{
required: true,
message: '原始密码',
},
]}
>
<Input type='password' placeholder="请输入原始密码" style={{width: 240}}/>
</Form.Item>
<Form.Item
name="newPassword"
label="新的密码"
rules={[
{
required: true,
message: '请输入新的密码',
},
]}
>
<Input type='password' placeholder="新的密码"
onChange={(value) => onNewPasswordChange(value)} style={{width: 240}}/>
</Form.Item>
<Form.Item
name="newPassword2"
label="确认密码"
rules={[
{
required: true,
message: '请和上面输入新的密码保持一致',
},
]}
validateStatus={newPasswordStatus.validateStatus}
help={newPasswordStatus.errorMsg || ' '}
>
<Input type='password' placeholder="请和上面输入新的密码保持一致"
onChange={(value) => onNewPassword2Change(value)} style={{width: 240}}/>
</Form.Item>
<Form.Item>
<Button disabled={newPasswordStatus.errorMsg || !newPasswordStatus.validateStatus}
type="primary"
htmlType="submit">
提交
</Button>
</Form.Item>
</Form>
</Tabs.TabPane>
{/*<Tabs.TabPane tab="授权令牌" key="token">*/}
{/* <AccessToken/>*/}
{/*</Tabs.TabPane>*/}
<Tabs.TabPane tab="两步认证" key="totp">
<Totp/>
</Tabs.TabPane>
</Tabs>
</Content>
</>
);
}
export default Info;

View File

@ -0,0 +1,20 @@
import React from 'react';
const Landing = () => {
return (
<div style={{
// width: '100vw',
// height: '100vh',
width: '100%',
height: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'white'
}}>
<div style={{fontWeight: 'bold'}}>正在努力加载中...</div>
</div>
);
};
export default Landing;

View File

@ -14,7 +14,7 @@
.login-card {
position: absolute;
left: 50%;
top: 50%;
top: 40%;
margin-left: -175px;
margin-top: -189px;
}

View File

@ -1,162 +1,124 @@
import React, {Component} from 'react';
import {Button, Card, Checkbox, Form, Input, Modal, Typography} from "antd";
import React, {useEffect, useState} from 'react';
import {Button, Card, Checkbox, Form, Input, message, Modal, Typography} from "antd";
import './Login.css'
import request from "../common/request";
import {message} from "antd/es";
import {withRouter} from "react-router-dom";
import {LockOutlined, OneToOneOutlined, UserOutlined} from '@ant-design/icons';
import Background from '../images/bg.jpg'
import {LockOutlined, LockTwoTone, UserOutlined} from '@ant-design/icons';
import {setToken} from "../utils/utils";
import accountApi from "../api/account";
import brandingApi from "../api/branding";
import strings from "../utils/strings";
const {Title} = Typography;
class LoginForm extends Component {
const LoginForm = () => {
formRef = React.createRef();
totpInputRef = React.createRef();
let [inLogin, setInLogin] = useState(false);
let [branding, setBranding] = useState({});
state = {
inLogin: false,
height: window.innerHeight,
width: window.innerWidth,
loginAccount: undefined,
totpModalVisible: false,
confirmLoading: false
};
useEffect(() => {
const x = async () => {
let branding = await brandingApi.getBranding();
document.title = branding['name'];
setBranding(branding);
}
x();
}, []);
componentDidMount() {
window.addEventListener('resize', () => {
this.setState({
height: window.innerHeight,
width: window.innerWidth
})
const afterLoginSuccess = async (token) => {
// 跳转登录
sessionStorage.removeItem('current');
sessionStorage.removeItem('openKeys');
setToken(token);
let user = await accountApi.getUserInfo();
if (user) {
if (user['type'] === 'user') {
window.location.href = "/my-asset"
} else {
window.location.href = "/"
}
}
}
const login = async (values) => {
let result = await request.post('/login', values);
if (result['code'] === 1) {
Modal.destroyAll();
await afterLoginSuccess(result['data']);
}
}
const handleOk = (loginAccount, totp) => {
if (!strings.hasText(totp)) {
message.warn("请输入双因素认证码");
return false;
}
loginAccount['totp'] = totp;
login(loginAccount);
return false;
}
const showTOTP = (loginAccount) => {
let value = '';
Modal.confirm({
title: '双因素认证',
icon: <LockTwoTone/>,
content: <Input onChange={e => value = e.target.value} onPressEnter={() => handleOk(loginAccount, value)}
placeholder="请输入双因素认证码"/>,
onOk: () => handleOk(loginAccount, value),
});
}
handleSubmit = async params => {
this.setState({
inLogin: true
});
const handleSubmit = async params => {
setInLogin(true);
try {
let result = await request.post('/login', params);
if (result.code === 0) {
// 进行双因子认证
this.setState({
loginAccount: params,
totpModalVisible: true
})
this.totpInputRef.current.focus();
if (result.code === 100) {
// 进行双因素认证
showTOTP(params);
return;
}
if (result.code !== 1) {
throw new Error(result.message);
}
// 跳转登录
sessionStorage.removeItem('current');
sessionStorage.removeItem('openKeys');
setToken(result['data']);
// this.props.history.push();
window.location.href = "/"
} catch (e) {
message.error(e.message);
} finally {
this.setState({
inLogin: false
});
}
};
handleOk = async (values) => {
this.setState({
confirmLoading: true
})
let loginAccount = this.state.loginAccount;
loginAccount['totp'] = values['totp'];
try {
let result = await request.post('/loginWithTotp', loginAccount);
if (result['code'] !== 1) {
message.error(result['message']);
return;
}
// 跳转登录
sessionStorage.removeItem('current');
sessionStorage.removeItem('openKeys');
setToken(result['data']);
// this.props.history.push();
window.location.href = "/"
afterLoginSuccess(result['data']);
} catch (e) {
message.error(e.message);
} finally {
this.setState({
confirmLoading: false
});
setInLogin(false);
}
}
};
handleCancel = () => {
this.setState({
totpModalVisible: false
})
}
return (
<div style={{width: '100vw', height: '100vh', backgroundColor: '#fafafa'}}>
<Card className='login-card' title={null}>
<div style={{textAlign: "center", margin: '15px auto 30px auto', color: '#1890ff'}}>
<Title level={1}>{branding['name']}</Title>
{/*<Text>一个轻量级的堡垒机系统</Text>*/}
</div>
<Form onFinish={handleSubmit} className="login-form">
<Form.Item name='username' rules={[{required: true, message: '请输入登录账号!'}]}>
<Input prefix={<UserOutlined/>} placeholder="登录账号"/>
</Form.Item>
<Form.Item name='password' rules={[{required: true, message: '请输入登录密码!'}]}>
<Input.Password prefix={<LockOutlined/>} placeholder="登录密码"/>
</Form.Item>
<Form.Item name='remember' valuePropName='checked' initialValue={false}>
<Checkbox>保持登录</Checkbox>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" className="login-form-button"
loading={inLogin}>
登录
</Button>
</Form.Item>
</Form>
</Card>
</div>
render() {
return (
<div className='login-bg'
style={{width: this.state.width, height: this.state.height, background: `url(${Background})`}}>
<Card className='login-card' title={null}>
<div style={{textAlign: "center", margin: '15px auto 30px auto', color: '#1890ff'}}>
<Title level={1}>Next Terminal</Title>
</div>
<Form onFinish={this.handleSubmit} className="login-form">
<Form.Item name='username' rules={[{required: true, message: '请输入登录账号!'}]}>
<Input prefix={<UserOutlined/>} placeholder="登录账号"/>
</Form.Item>
<Form.Item name='password' rules={[{required: true, message: '请输入登录密码!'}]}>
<Input.Password prefix={<LockOutlined/>} placeholder="登录密码"/>
</Form.Item>
<Form.Item name='remember' valuePropName='checked' initialValue={false}>
<Checkbox>记住登录</Checkbox>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" className="login-form-button"
loading={this.state.inLogin}>
登录
</Button>
</Form.Item>
</Form>
</Card>
<Modal title="双因素认证" visible={this.state.totpModalVisible} confirmLoading={this.state.confirmLoading}
maskClosable={false}
okButtonProps={{form:'totp-form', key: 'submit', htmlType: 'submit'}}
onOk={() => {
this.formRef.current
.validateFields()
.then(values => {
this.handleOk(values);
// this.formRef.current.resetFields();
});
}}
onCancel={this.handleCancel}>
<Form id='totp-form' ref={this.formRef}>
<Form.Item name='totp' rules={[{required: true, message: '请输入双因素认证APP中显示的授权码'}]}>
<Input ref={this.totpInputRef} prefix={<OneToOneOutlined/>} placeholder="请输入双因素认证APP中显示的授权码"/>
</Form.Item>
</Form>
</Modal>
</div>
);
}
);
}
export default withRouter(LoginForm);
export default LoginForm;

View File

@ -0,0 +1,22 @@
import React from 'react';
import {Button, Layout, Result} from "antd";
import {Link} from "react-router-dom";
const {Content} = Layout;
const NoMatch = () => {
return (
<div>
<Content>
<Result
status="404"
title="404"
subTitle="抱歉,您似乎到达了预期之外的页面。"
extra={<Button type="primary"><Link to={'/'}>回到首页</Link></Button>}
/>
</Content>
</div>
);
};
export default NoMatch;

View File

@ -0,0 +1,33 @@
import React from 'react';
import {Button, Layout, Result, Space} from "antd";
import {Link, useNavigate} from "react-router-dom";
const {Content} = Layout;
const NoPermission = () => {
const navigate = useNavigate();
return (
<div>
<Content>
<Result
status="403"
title="403"
subTitle="抱歉,您似乎没有此页面的权限。"
extra={
<Space>
<Button type="primary" onClick={() => {
navigate(-1);
}
}>返回上一页</Button>
<Button type="primary"><Link to={'/'}>回到首页</Link></Button>
</Space>
}
/>
</Content>
</div>
);
};
export default NoPermission;

131
web/src/components/Totp.js Normal file
View File

@ -0,0 +1,131 @@
import React, {useState} from 'react';
import {Button, Form, Image, Input, message, Modal, Result, Space, Typography} from "antd";
import {ExclamationCircleOutlined, ReloadOutlined} from "@ant-design/icons";
import accountApi from "../api/account";
import {useQuery} from "react-query";
const {Title} = Typography;
const Totp = () => {
let infoQuery = useQuery('infoQuery', accountApi.getUserInfo);
let [totp, setTotp] = useState({});
const resetTOTP = async () => {
let totp = await accountApi.reloadTotp();
setTotp(totp);
}
const confirmTOTP = async (values) => {
values['secret'] = totp['secret'];
let success = await accountApi.confirmTotp(values);
if (success) {
message.success('TOTP启用成功');
await infoQuery.refetch();
setTotp({});
}
}
const renderBindingTotpPage = (qr) => {
if (!qr) {
return undefined;
}
return <Form hidden={!totp.qr} onFinish={confirmTOTP}>
<Form.Item label="二维码"
extra={'有效期30秒在扫描后请尽快输入。推荐使用Google Authenticator, Authy 或者 Microsoft Authenticator。'}>
<Space size={12} direction='horizontal'>
<Image
style={{padding: 20}}
width={280}
src={"data:image/png;base64, " + totp.qr}
/>
<Button
type="primary"
icon={<ReloadOutlined/>}
onClick={resetTOTP}
>
重新加载
</Button>
</Space>
</Form.Item>
<Form.Item
name="totp"
label="TOTP"
rules={[
{
required: true,
message: '请输入双因素认证APP中显示的授权码',
},
]}
>
<Input placeholder="请输入双因素认证APP中显示的授权码"/>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
确认
</Button>
</Form.Item>
</Form>
}
return (
<div>
<Title level={4}>双因素认证</Title>
<Form hidden={totp.qr}>
<Form.Item>
{
infoQuery.data?.enableTotp ?
<Result
status="success"
title="您已成功开启双因素认证!"
subTitle="多因素认证-MFA二次认证-登录身份鉴别,访问控制更安全。"
extra={[
<Button type="primary" key="console" danger onClick={() => {
Modal.confirm({
title: '您确认要解除双因素认证吗?',
icon: <ExclamationCircleOutlined/>,
content: '解除之后可能存在系统账号被暴力破解的风险。',
okText: '确认',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
let success = await accountApi.resetTotp();
if (success) {
message.success('双因素认证解除成功');
await infoQuery.refetch();
}
},
onCancel() {
console.log('Cancel');
},
})
}}>
解除绑定
</Button>,
<Button key="re-bind" onClick={resetTOTP}>重新绑定</Button>,
]}
/> :
<Result
status="warning"
title="您还未开启双因素认证!"
subTitle="系统账号存在被暴力破解的风险。"
extra={
<Button type="primary" key="bind" onClick={resetTOTP}>
去开启
</Button>
}
/>
}
</Form.Item>
</Form>
{
renderBindingTotpPage(totp.qr)
}
</div>
);
};
export default Totp;

View File

@ -0,0 +1,13 @@
.xterm-viewport.xterm-viewport {
scrollbar-width: thin;
}
.xterm-viewport::-webkit-scrollbar {
width: 10px;
}
.xterm-viewport::-webkit-scrollbar-track {
opacity: 0;
}
.xterm-viewport::-webkit-scrollbar-thumb {
min-height: 20px;
background-color: #ffffff20;
}

View File

@ -64,9 +64,17 @@ class Access extends Component {
};
async componentDidMount() {
let urlParams = new URLSearchParams(this.props.location.search);
let href = window.location.href;
let search = href.split('?')[1];
let urlParams = new URLSearchParams(search);
let assetId = urlParams.get('assetId');
document.title = urlParams.get('assetName');
if (!assetId) {
this.showMessage('获取资产ID失败');
return;
}
let protocol = urlParams.get('protocol');
let width = urlParams.get('width');
let height = urlParams.get('height');
@ -789,7 +797,7 @@ class Access extends Component {
}}
visible={this.state.statsVisible}
>
<Stats sessionId={this.state.sessionId} onRef={this.onRef}/>
<Stats sessionId={this.state.sessionId}/>
</Drawer>
{

View File

@ -1,174 +0,0 @@
import React, {Component} from 'react';
import Guacamole from 'guacamole-common-js';
import {Modal, Result, Spin} from 'antd'
import qs from "qs";
import {wsServer} from "../../common/env";
import {getToken} from "../../utils/utils";
import './Access.css'
const STATE_IDLE = 0;
const STATE_CONNECTING = 1;
const STATE_WAITING = 2;
const STATE_CONNECTED = 3;
const STATE_DISCONNECTING = 4;
const STATE_DISCONNECTED = 5;
class AccessMonitor extends Component {
state = {
client: {},
containerOverflow: 'hidden',
width: 0,
height: 0,
rate: 1,
loading: false,
tip: '',
closed: false,
};
async componentDidMount() {
const sessionId = this.props.sessionId;
let rate = this.props.rate;
let protocol = this.props.protocol;
let width = this.props.width;
let height = this.props.height;
if (protocol === 'ssh' || protocol === 'telnet') {
rate = rate * 0.5;
width = width * 2;
height = height * 2;
}
this.setState({
width: width * rate,
height: height * rate,
rate: rate,
})
this.renderDisplay(sessionId);
}
componentWillUnmount() {
if (this.state.client) {
this.state.client.disconnect();
}
}
onTunnelStateChange = (state) => {
console.log('onTunnelStateChange', state);
if (state === Guacamole.Tunnel.State.CLOSED) {
this.setState({
loading: false,
closed: true,
});
}
};
onClientStateChange = (state) => {
switch (state) {
case STATE_IDLE:
this.setState({
loading: true,
tip: '正在初始化中...'
});
break;
case STATE_CONNECTING:
this.setState({
loading: true,
tip: '正在努力连接中...'
});
break;
case STATE_WAITING:
this.setState({
loading: true,
tip: '正在等待服务器响应...'
});
break;
case STATE_CONNECTED:
this.setState({
loading: false
});
if (this.state.client) {
this.state.client.getDisplay().scale(this.state.rate);
}
break;
case STATE_DISCONNECTING:
break;
case STATE_DISCONNECTED:
break;
default:
break;
}
};
showMessage(message) {
Modal.error({
title: '提示',
content: message,
});
}
async renderDisplay(sessionId, protocol) {
let tunnel = new Guacamole.WebSocketTunnel(`${wsServer}/sessions/${sessionId}/tunnel-monitor`);
tunnel.onstatechange = this.onTunnelStateChange;
let client = new Guacamole.Client(tunnel);
// 处理客户端的状态变化事件
client.onstatechange = this.onClientStateChange;
const display = document.getElementById("display");
// Add client to display div
const element = client.getDisplay().getElement();
display.appendChild(element);
let token = getToken();
let params = {
'X-Auth-Token': token
};
let paramStr = qs.stringify(params);
// Connect
client.connect(paramStr);
// Disconnect on close
window.onunload = function () {
client.disconnect();
};
this.setState({
client: client
})
}
render() {
return (
<Spin spinning={this.state.loading} tip={this.state.tip}>
<div>
{
this.state.closed ?
<Result
title="远程连接已关闭"
/> :
<div className="container" style={{
overflow: this.state.containerOverflow,
width: this.state.width,
height: this.state.height
}}>
<div id="display"/>
</div>
}
</div>
</Spin>
);
}
}
export default AccessMonitor;

View File

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

View File

@ -1,22 +0,0 @@
.xterm .xterm-viewport {
/* On OS X this is required in order for the scroll bar to appear fully opaque */
background-color: transparent;
overflow-y: scroll;
cursor: default;
position: absolute;
right: 0;
left: 0;
top: 0;
bottom: 0;
--scrollbar-color: var(--highlight) var(--dark);
--scrollbar-width: thin;
}
.xterm-viewport::-webkit-scrollbar {
background-color: var(--dark);
width: 5px;
}
.xterm-viewport::-webkit-scrollbar-thumb {
background: var(--highlight);
}

View File

@ -1,57 +1,117 @@
import React, {Component} from 'react';
import "xterm/css/xterm.css"
import React, {useEffect, useState} from 'react';
import {useSearchParams} from "react-router-dom";
import {Terminal} from "xterm";
import {FitAddon} from "xterm-addon-fit";
import {getToken} from "../../utils/utils";
import request from "../../common/request";
import {Affix, Button, Drawer, Dropdown, Menu, message, Select, Space, Typography} from "antd";
import Message from "./Message";
import qs from "qs";
import {wsServer} from "../../common/env";
import {getToken, isEmpty} from "../../utils/utils";
import {FitAddon} from 'xterm-addon-fit';
import "./Access.css"
import request from "../../common/request";
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 {CodeOutlined, FolderOutlined, LineChartOutlined} from "@ant-design/icons";
import FileSystem from "../devops/FileSystem";
import "xterm/css/xterm.css"
import Stats from "./Stats";
import Message from "./Message";
import {debounce} from "../../utils/fun";
import commandApi from "../../api/command";
import strings from "../../utils/strings";
import workCommandApi from "../../api/worker/command";
import {xtermScrollPretty} from "../../utils/xterm-scroll-pretty";
class Term extends Component {
const {Text} = Typography;
statsRef = undefined;
const Term = () => {
state = {
width: window.innerWidth,
height: window.innerHeight,
term: undefined,
webSocket: undefined,
fitAddon: undefined,
sessionId: undefined,
session: {},
enterBtnIndex: 1001,
commands: []
const [searchParams] = useSearchParams();
const assetId = searchParams.get('assetId');
const assetName = searchParams.get('assetName');
const isWorker = searchParams.get('isWorker');
const [box, setBox] = useState({width: window.innerWidth, height: window.innerHeight});
let [commands, setCommands] = useState([]);
let [term, setTerm] = useState();
let [fitAddon, setFitAddon] = useState();
let [websocket, setWebsocket] = useState();
let [session, setSession] = useState({});
let [fileSystemVisible, setFileSystemVisible] = useState(false);
let [statsVisible, setStatsVisible] = useState(false);
let [enterBtnZIndex, setEnterBtnZIndex] = useState(999);
let [queryInterval, setQueryInterval] = useState(5000);
const createSession = async (assetsId) => {
let result = await request.post(`/sessions?assetId=${assetsId}&mode=native`);
if (result['code'] !== 1) {
return [undefined, result['message']];
}
return [result['data'], ''];
}
const writeErrorMessage = (term, message) => {
term.writeln(`\x1B[1;3;31m${message}\x1B[0m `);
}
const updateSessionStatus = async (sessionId) => {
let result = await request.post(`/sessions/${sessionId}/connect`);
if (result['code'] !== 1) {
message.error(result['message']);
}
}
const writeCommand = (command) => {
if (websocket) {
websocket.send(new Message(Message.Data, command));
}
}
const getCommands = async () => {
if (strings.hasText(isWorker)) {
let items = await workCommandApi.getAll();
setCommands(items);
} else {
let items = await commandApi.getAll();
setCommands(items);
}
}
const focus = () => {
if (term) {
term.focus();
}
}
const fit = () => {
if (fitAddon) {
fitAddon.fit();
}
}
useEffect(() => {
if (term && websocket && fitAddon && websocket.readyState === WebSocket.OPEN) {
fit();
focus();
let terminalSize = {
cols: term.cols,
rows: term.rows
}
websocket.send(new Message(Message.Resize, window.btoa(JSON.stringify(terminalSize))).toString());
}
}, [box.width, box.height]);
const onWindowResize = () => {
setBox({width: window.innerWidth, height: window.innerHeight});
};
componentDidMount = async () => {
let urlParams = new URLSearchParams(this.props.location.search);
let assetId = urlParams.get('assetId');
document.title = urlParams.get('assetName');
let session = await this.createSession(assetId);
if (!session) {
return;
}
let sessionId = session['id'];
if (isEmpty(sessionId)) {
return;
}
const init = async (assetId) => {
let term = new Terminal({
fontFamily: 'monaco, Consolas, "Lucida Console", monospace',
fontSize: 15,
theme: {
background: '#1b1b1b'
},
rightClickSelectsWord: true,
});
let elementTerm = document.getElementById('terminal');
term.open(elementTerm);
@ -60,56 +120,41 @@ class Term extends Component {
fitAddon.fit();
term.focus();
term.writeln('Trying to connect to the server ...');
if (!assetId) {
writeErrorMessage(term, `参数缺失,请关闭此页面后重新打开。`)
return;
}
term.onSelectionChange(async () => {
let selection = term.getSelection();
this.setState({
selection: selection
})
if (navigator.clipboard) {
await navigator.clipboard.writeText(selection);
}
});
let [session, errMsg] = await createSession(assetId);
if (!session) {
writeErrorMessage(term, `创建会话失败,${errMsg}`)
return;
}
term.attachCustomKeyEventHandler((e) => {
if (e.ctrlKey && e.key === 'c' && this.state.selection) {
return false;
}
return !(e.ctrlKey && e.key === 'v');
});
let sessionId = session['id'];
term.writeln('trying to connect to the server ...');
document.body.oncopy = (event) => {
event.preventDefault();
if (this.state.session['copy'] === '0') {
// message.warn('禁止复制')
if (session['copy'] === '0') {
message.warn('禁止复制')
return false;
}else {
if (event.clipboardData) {
return event.clipboardData.setData('text', '');
} else {
// 兼容IE
return window.clipboardData.setData("text", '');
}
} else {
return true;
}
}
document.body.onpaste = (event) => {
event.preventDefault();
if (this.state.session['paste'] === '0') {
// message.warn('禁止粘贴')
if (session['paste'] === '0') {
message.warn('禁止粘贴')
return false;
} else {
return true;
}
return true;
}
term.onData(data => {
let webSocket = this.state.webSocket;
if (webSocket !== undefined) {
webSocket.send(new Message(Message.Data, data).toString());
}
});
let token = getToken();
let params = {
'cols': term.cols,
@ -125,32 +170,40 @@ class Term extends Component {
webSocket.onopen = (e => {
pingInterval = setInterval(() => {
webSocket.send(new Message(Message.Ping, "").toString());
}, 1000);
}, 10000);
xtermScrollPretty();
});
webSocket.onerror = (e) => {
term.writeln("Failed to connect to server.");
writeErrorMessage(term, `websocket error ${e.data}`)
}
webSocket.onclose = (e) => {
term.writeln("Connection is closed.");
term.writeln("connection is closed.");
if (pingInterval) {
clearInterval(pingInterval);
}
}
term.onData(data => {
if (webSocket !== undefined) {
webSocket.send(new Message(Message.Data, data).toString());
}
});
webSocket.onmessage = (e) => {
let msg = Message.parse(e.data);
switch (msg['type']) {
case Message.Connected:
term.clear();
this.updateSessionStatus(sessionId);
this.getCommands();
updateSessionStatus(sessionId);
getCommands();
break;
case Message.Data:
term.write(msg['content']);
break;
case Message.Closed:
term.writeln(`\x1B[1;3;31m${msg['content']}\x1B[0m `)
term.writeln(`\x1B[1;3;31m${msg['content']}\x1B[0m `);
webSocket.close();
break;
default:
@ -158,232 +211,147 @@ class Term extends Component {
}
}
this.setState({
term: term,
webSocket: webSocket,
fitAddon: fitAddon,
sessionId: sessionId,
session: session
});
window.addEventListener('resize', this.onWindowResize);
window.addEventListener('beforeunload', this.handleUnload);
window.onunload = function () {
webSocket.close();
};
setSession(session);
setTerm(term);
setFitAddon(fitAddon);
setWebsocket(webSocket);
}
componentWillUnmount() {
let webSocket = this.state.webSocket;
if (webSocket) {
webSocket.close()
}
window.removeEventListener('beforeunload', this.handleUnload);
}
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({
title: '提示',
icon: <ExclamationCircleOutlined/>,
content: msg,
centered: true,
okText: '重新连接',
cancelText: '关闭页面',
onOk() {
window.location.reload();
},
onCancel() {
window.close();
},
});
}
async createSession(assetsId) {
let result = await request.post(`/sessions?assetId=${assetsId}&mode=native`);
if (result['code'] !== 1) {
this.showMessage(result['message']);
return undefined;
}
return result['data'];
}
updateSessionStatus = async (sessionId) => {
let result = await request.post(`/sessions/${sessionId}/connect`);
if (result['code'] !== 1) {
message.error(result['message']);
}
}
terminalSize() {
return {
cols: Math.floor(this.state.width / 7.5),
rows: Math.floor(window.innerHeight / 17),
}
}
onWindowResize = (e) => {
let term = this.state.term;
let fitAddon = this.state.fitAddon;
let webSocket = this.state.webSocket;
this.setState({
width: window.innerWidth,
height: window.innerHeight,
}, () => {
if (webSocket && webSocket.readyState === WebSocket.OPEN) {
fitAddon.fit();
this.focus();
let terminalSize = {
cols: term.cols,
rows: term.rows
}
webSocket.send(new Message(Message.Resize, window.btoa(JSON.stringify(terminalSize))).toString());
}
});
};
handleUnload(e) {
var message = "要离开网站吗?";
const handleUnload = (e) => {
const message = "要离开网站吗?";
(e || window.event).returnValue = message; //Gecko + IE
return message;
}
writeCommand = (command) => {
let webSocket = this.state.webSocket;
if (webSocket !== undefined) {
webSocket.send(new Message(Message.Data, command));
}
this.focus();
}
useEffect(() => {
document.title = assetName;
window.addEventListener('beforeunload', handleUnload);
focus = () => {
let term = this.state.term;
if (term) {
term.focus();
}
}
init(assetId);
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>;
let resize = debounce(() => {
onWindowResize();
});
const cmdMenu = (
<Menu>
{cmdMenuItems}
</Menu>
);
window.addEventListener('resize', resize);
return (
<div>
<div id='terminal' style={{
height: this.state.height,
width: this.state.width,
backgroundColor: '#1b1b1b'
}}/>
return () => {
if (websocket) {
websocket.close();
}
window.removeEventListener('resize', resize);
window.removeEventListener('beforeunload', handleUnload);
}
}, [assetId]);
<Draggable>
<Affix style={{position: 'absolute', top: 50, right: 50, zIndex: this.state.enterBtnIndex}}>
<Button icon={<FolderOutlined/>} onClick={() => {
this.setState({
fileSystemVisible: true,
enterBtnIndex: 999, // xterm.js 输入框的zIndex是1000在弹出文件管理页面后要隐藏此按钮
});
}}/>
</Affix>
</Draggable>
const cmdMenuItems = commands.map(item => {
return {
key: item['id'],
label: item['name'],
};
});
<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"
width={window.innerWidth * 0.8}
closable={true}
// maskClosable={false}
onClose={() => {
this.setState({
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>
<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>
);
const handleCmdMenuClick = (e) => {
for (const command of commands) {
if (command['id'] === e.key) {
writeCommand(command['content']);
}
}
}
}
export default Term;
return (
<div>
<div id='terminal' style={{
overflow: 'hidden',
height: box.height,
width: box.width,
backgroundColor: '#1b1b1b'
}}/>
<Draggable>
<Affix style={{position: 'absolute', top: 50, right: 50, zIndex: enterBtnZIndex}}>
<Button icon={<FolderOutlined/>} onClick={() => {
setFileSystemVisible(true);
setEnterBtnZIndex(999); // xterm.js 输入框的zIndex是1000在弹出文件管理页面后要隐藏此按钮
}}/>
</Affix>
</Draggable>
<Draggable>
<Affix style={{position: 'absolute', top: 50, right: 100, zIndex: enterBtnZIndex}}>
<Dropdown overlay={<Menu onClick={handleCmdMenuClick} items={cmdMenuItems}/>} trigger={['click']}
placement="bottomLeft">
<Button icon={<CodeOutlined/>}/>
</Dropdown>
</Affix>
</Draggable>
<Draggable>
<Affix style={{position: 'absolute', top: 100, right: 100, zIndex: enterBtnZIndex}}>
<Button icon={<LineChartOutlined/>} onClick={() => {
setStatsVisible(true);
setEnterBtnZIndex(999);
}}/>
</Affix>
</Draggable>
<Drawer
title={'会话详情'}
placement="right"
width={window.innerWidth * 0.8}
closable={true}
// maskClosable={false}
onClose={() => {
setFileSystemVisible(false);
setEnterBtnZIndex(1001); // xterm.js 输入框的zIndex是1000在弹出文件管理页面后要隐藏此按钮
focus();
}}
visible={fileSystemVisible}
>
<FileSystem
storageId={session['id']}
storageType={'sessions'}
upload={session['upload'] === '1'}
download={session['download'] === '1'}
delete={session['delete'] === '1'}
rename={session['rename'] === '1'}
edit={session['edit'] === '1'}
minHeight={window.innerHeight - 103}/>
</Drawer>
<Drawer
title={'状态信息'}
placement="right"
width={window.innerWidth * 0.8}
closable={true}
onClose={() => {
setStatsVisible(false);
setEnterBtnZIndex(1001);
focus();
}}
visible={statsVisible}
extra={
<Space>
<div style={{width: 100}}>
<Text>查询时间间隔</Text>
</div>
<Select defaultValue="5000" style={{width: 80}} onChange={(value) => {
setQueryInterval(parseInt(value));
}}>
<Select.Option value="1000">1</Select.Option>
<Select.Option value="5000">5</Select.Option>
<Select.Option value="15000">15</Select.Option>
<Select.Option value="30000">30</Select.Option>
</Select>
</Space>
}
>
<Stats sessionId={session['id']} visible={statsVisible} queryInterval={queryInterval}/>
</Drawer>
</div>
);
};
export default Term;

View File

@ -1,83 +0,0 @@
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 = {
'X-Auth-Token': token
};
let paramStr = qs.stringify(params);
let webSocket = new WebSocket(`${wsServer}/sessions/${sessionId}/ssh-monitor?${paramStr}`);
webSocket.onmessage = (e) => {
let msg = Message.parse(e.data);
switch (msg['type']) {
case Message.Connected:
term.clear();
break;
case Message.Data:
term.write(msg['content']);
break;
case Message.Closed:
term.writeln(`\x1B[1;3;31m${msg['content']}\x1B[0m `)
webSocket.close();
break;
default:
break;
}
}
this.setState({
term: term,
webSocket: webSocket,
});
}
componentWillUnmount() {
let webSocket = this.state.webSocket;
if (webSocket) {
webSocket.close()
}
}
render() {
return (
<div>
<div id='terminal' style={{
backgroundColor: '#1b1b1b'
}}/>
</div>
);
}
}
export default TermMonitor;

View File

@ -1,283 +1,51 @@
import React, {Component} from 'react';
import React, {useState} 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 {Badge, Button, Layout, Popconfirm, Tag, Tooltip} from "antd";
import accessGatewayApi from "../../api/access-gateway";
import {ProTable} from "@ant-design/pro-components";
import AccessGatewayModal from "./AccessGatewayModal";
import {hasPermission} from "../../service/permission";
import dayjs from "dayjs";
import ColumnState, {useColumnState} from "../../hook/column-state";
import Show from "../../dd/fi/show";
const confirm = Modal.confirm;
const {Content} = Layout;
const {Title, Text} = Typography;
const {Search} = Input;
class AccessGateway extends Component {
const api = accessGatewayApi;
inputRefOfName = React.createRef();
inputRefOfIp = React.createRef();
const actionRef = React.createRef();
state = {
items: [],
total: 0,
queryParams: {
pageIndex: 1,
pageSize: 10
const AccessGateway = () => {
let [visible, setVisible] = useState(false);
let [confirmLoading, setConfirmLoading] = useState(false);
let [selectedRowKey, setSelectedRowKey] = useState(undefined);
const [columnsStateMap, setColumnsStateMap] = useColumnState(ColumnState.ACCESS_GATEWAY);
const columns = [
{
dataIndex: 'index',
valueType: 'indexBorder',
width: 48,
},
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);
}
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
});
try {
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);
}
}
} finally {
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,
hideInSearch: true
}, {
title: '端口',
dataIndex: 'port',
key: 'port',
hideInSearch: true
}, {
title: '账户类型',
dataIndex: 'accountType',
key: 'accountType',
hideInSearch: true,
render: (accountType) => {
if (accountType === 'private-key') {
return (
@ -293,10 +61,12 @@ class AccessGateway extends Component {
title: '授权账户',
dataIndex: 'username',
key: 'username',
hideInSearch: true
}, {
title: '状态',
dataIndex: 'connected',
key: 'connected',
hideInSearch: true,
render: (text, record) => {
if (text) {
return (
@ -307,173 +77,132 @@ class AccessGateway extends Component {
} else {
return (
<Tooltip title={record['message']}>
<Badge status="error" text='已断开'/>
<Badge status="default" text='已断开'/>
</Tooltip>
)
}
}
}, {
},
{
title: '创建时间',
dataIndex: 'created',
key: 'created',
render: (text, record) => {
return (
<Tooltip title={text}>
{dayjs(text).fromNow()}
</Tooltip>
)
},
sorter: true,
}, {
dataIndex: 'created',
hideInSearch: true,
},
{
title: '操作',
key: 'action',
render: (text, record, index) => {
return (
<div>
<Button type="link" size='small'
onClick={() => this.update(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}`
valueType: 'option',
key: 'option',
render: (text, record, _, action) => [
<Show menu={'access-gateway-edit'} key={'access-gateway-edit'}>
<a
key="edit"
onClick={() => {
setVisible(true);
setSelectedRowKey(record['id']);
}}
loading={this.state.loading}
onChange={this.handleTableChange}
/>
>
编辑
</a>
</Show>,
<Show menu={'access-gateway-del'} key={'access-gateway-del'}>
<Popconfirm
key={'confirm-delete'}
title="您确认要删除此行吗?"
onConfirm={async () => {
await api.deleteById(record.id);
actionRef.current.reload();
}}
okText="确认"
cancelText="取消"
>
<a key='delete' className='danger'>删除</a>
</Popconfirm>
</Show>,
],
},
];
{
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
return (<Content className="page-container">
<ProTable
columns={columns}
actionRef={actionRef}
columnsState={{
value: columnsStateMap,
onChange: setColumnsStateMap
}}
request={async (params = {}, sort, filter) => {
let field = '';
let order = '';
if (Object.keys(sort).length > 0) {
field = Object.keys(sort)[0];
order = Object.values(sort)[0];
}
let queryParams = {
pageIndex: params.current,
pageSize: params.pageSize,
name: params.name,
field: field,
order: order
}
let result = await api.getPaging(queryParams);
return {
data: result['items'],
success: true,
total: result['total']
};
}}
rowKey="id"
search={{
labelWidth: 'auto',
}}
pagination={{
defaultPageSize: 10,
}}
dateFormatter="string"
headerTitle="接入网关列表"
toolBarRender={() => [
<Show menu={'access-gateway-add'}>
<Button key="button" type="primary" onClick={() => {
setVisible(true)
}}>
新建
</Button>
</Show>,
]}
/>
<AccessGatewayModal
id={selectedRowKey}
visible={visible}
confirmLoading={confirmLoading}
handleCancel={() => {
setVisible(false);
setSelectedRowKey(undefined);
}}
handleOk={async (values) => {
setConfirmLoading(true);
try {
let success;
if (values['id']) {
success = await api.updateById(values['id'], values);
} else {
success = await api.create(values);
}
</Content>
</>
);
}
if (success) {
setVisible(false);
}
actionRef.current.reload();
} finally {
setConfirmLoading(false);
}
}}
/>
</Content>);
}
export default AccessGateway;

View File

@ -1,5 +1,6 @@
import React, {useState} from 'react';
import {Form, Input, InputNumber, Modal, Select} from "antd/lib/index";
import React, {useEffect, useState} from 'react';
import {Form, Input, InputNumber, Modal, Select} from "antd";
import accessGatewayApi from "../../api/access-gateway";
const formItemLayout = {
labelCol: {span: 6},
@ -7,46 +8,73 @@ const formItemLayout = {
};
const {TextArea} = Input;
const api = accessGatewayApi;
const AccessGatewayModal = ({title, visible, handleOk, handleCancel, confirmLoading, model}) => {
const AccessGatewayModal = ({
visible,
handleOk,
handleCancel,
confirmLoading,
id,
}) => {
const [form] = Form.useForm();
if (model['accountType'] === undefined) {
model['accountType'] = 'password';
}
if (model['port'] === undefined) {
model['port'] = 22;
}
let [accountType, setAccountType] = useState(model.accountType);
let [accountType, setAccountType] = useState('password');
const handleAccountTypeChange = v => {
setAccountType(v);
}
useEffect(() => {
const getItem = async () => {
let data = await api.getById(id);
if (data) {
form.setFieldsValue(data);
setAccountType(data['accountType']);
}
}
if (visible) {
if(id){
getItem();
}else {
form.setFieldsValue({
accountType: 'password',
port: 22,
});
}
} else {
form.resetFields();
}
}, [visible]);
return (
<Modal
title={title}
title={id ? '更新接入网关' : '新建接入网关'}
visible={visible}
maskClosable={false}
destroyOnClose={true}
onOk={() => {
form
.validateFields()
.then(values => {
form.resetFields();
handleOk(values);
.then(async values => {
let ok = await handleOk(values);
if (ok) {
form.resetFields();
}
});
}}
onCancel={handleCancel}
onCancel={() => {
form.resetFields();
handleCancel();
}}
confirmLoading={confirmLoading}
okText='确定'
cancelText='取消'
>
<Form form={form} {...formItemLayout} initialValues={model}>
<Form form={form} {...formItemLayout}>
<Form.Item name='id' noStyle>
<Input hidden={true}/>
</Form.Item>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,52 @@
import React, {useState} from 'react';
import {useParams, useSearchParams} from "react-router-dom";
import {Tabs} from "antd";
import AssetInfo from "./AssetInfo";
import AssetUser from "./AssetUser";
import AssetUserGroup from "./AssetUserGroup";
import {hasMenu} from "../../service/permission";
const {TabPane} = Tabs;
const AssetDetail = () => {
let params = useParams();
const id = params['assetId'];
const [searchParams, setSearchParams] = useSearchParams();
let key = searchParams.get('activeKey');
key = key ? key : 'info';
let [activeKey, setActiveKey] = useState(key);
const handleTagChange = (key) => {
setActiveKey(key);
setSearchParams({'activeKey': key});
}
return (
<div className="page-detail-warp">
<Tabs activeKey={activeKey} onChange={handleTagChange}>
{
hasMenu('asset-detail') &&
<TabPane tab="基本信息" key="info">
<AssetInfo active={activeKey === 'info'} id={id}/>
</TabPane>
}
{
hasMenu('asset-authorised-user') &&
<TabPane tab="授权的用户" key="bind-user">
<AssetUser active={activeKey === 'bind-user'} id={id}/>
</TabPane>
}
{
hasMenu('asset-authorised-user-group') &&
<TabPane tab="授权的用户组" key="bind-user-group">
<AssetUserGroup active={activeKey === 'bind-user-group'} id={id}/>
</TabPane>
}
</Tabs>
</div>
);
};
export default AssetDetail;

View File

@ -0,0 +1,39 @@
import React, {useEffect, useState} from 'react';
import assetApi from "../../api/asset";
import {Descriptions} from "antd";
const api = assetApi;
const AssetInfo = ({active, id}) => {
let [item, setItem] = useState({});
useEffect(() => {
const getItem = async (id) => {
let item = await api.getById(id);
if (item) {
setItem(item);
}
};
if (active && id) {
getItem(id);
}
}, [active]);
return (
<div className={'page-detail-info'}>
<Descriptions column={1}>
<Descriptions.Item label="资产名称">{item['name']}</Descriptions.Item>
<Descriptions.Item label="协议">{item['protocol']}</Descriptions.Item>
<Descriptions.Item label="IP">{item['ip']}</Descriptions.Item>
<Descriptions.Item label="端口">{item['port']}</Descriptions.Item>
<Descriptions.Item label="标签">{item['tags']}</Descriptions.Item>
{/*<Descriptions.Item label="类型">{item['type'] === 'regexp' ? '正则表达式' : '命令'}</Descriptions.Item>*/}
<Descriptions.Item label="创建时间">{item['created']}</Descriptions.Item>
</Descriptions>
</div>
);
};
export default AssetInfo;

View File

@ -0,0 +1,3 @@
.asset-modal .ant-modal-body{
padding-top: 0 !important;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,145 @@
import React, {useEffect, useState} from 'react';
import {Link} from "react-router-dom";
import authorisedApi from "../../api/authorised";
import {ProTable} from "@ant-design/pro-components";
import {Button} from "antd";
import AssetUserBind from "./AssetUserBind";
import Show from "../../dd/fi/show";
const actionRef = React.createRef();
const AssetUser = ({active, id}) => {
let [visible, setVisible] = useState(false);
let [confirmLoading, setConfirmLoading] = useState(false);
useEffect(() => {
if (active) {
actionRef.current.reload();
}
}, [active]);
const columns = [
{
dataIndex: 'index',
valueType: 'indexBorder',
width: 48,
},
{
title: '用户名称',
dataIndex: 'userName',
render: ((text, record) => {
return <Link to={`/user/${record['userId']}`}>{text}</Link>
})
},
{
title: '授权策略名称',
dataIndex: 'strategyName',
hideInSearch: true,
render: ((text, record) => {
return <Link to={`/strategy/${record['strategyId']}`}>{text}</Link>
})
},
{
title: '授权日期',
key: 'created',
dataIndex: 'created',
hideInSearch: true,
},
{
title: '操作',
valueType: 'option',
key: 'option',
width: 50,
render: (text, record, _, action) => [
<Show menu={'asset-authorised-user-del'} key={'unbind-acc'}>
<a
key="unbind"
onClick={async () => {
await authorisedApi.DeleteById(record['id']);
actionRef.current.reload();
}}
>
移除
</a>
</Show>,
],
},
];
return (
<div>
<ProTable
columns={columns}
actionRef={actionRef}
request={async (params = {}, sort, filter) => {
let field = '';
let order = '';
if (Object.keys(sort).length > 0) {
field = Object.keys(sort)[0];
order = Object.values(sort)[0];
}
let queryParams = {
pageIndex: params.current,
pageSize: params.pageSize,
name: params.name,
assetId: id,
field: field,
order: order
}
let result = await authorisedApi.GetUserPaging(queryParams);
return {
data: result['items'],
success: true,
total: result['total']
};
}}
rowKey="id"
search={{
labelWidth: 'auto',
}}
pagination={{
pageSize: 10,
}}
dateFormatter="string"
headerTitle="授权的用户列表"
toolBarRender={() => [
<Show menu={'asset-authorised-user-add'} key={'bind-acc'}>
<Button key="button" type="primary" onClick={() => {
setVisible(true);
}}>
授权
</Button>
</Show>
,
]}
/>
<AssetUserBind
id={id}
visible={visible}
confirmLoading={confirmLoading}
handleCancel={() => {
setVisible(false);
}}
handleOk={async (values) => {
setConfirmLoading(true);
values['assetId'] = id;
try {
let success = authorisedApi.AuthorisedUsers(values);
if (success) {
setVisible(false);
}
actionRef.current.reload();
} finally {
setConfirmLoading(false);
}
}}
/>
</div>
);
};
export default AssetUser;

View File

@ -0,0 +1,99 @@
import React, {useEffect, useState} from 'react';
import {Form, Modal, Select} from "antd";
import authorisedApi from "../../api/authorised";
import strategyApi from "../../api/strategy";
import userApi from "../../api/user";
const formItemLayout = {
labelCol: {span: 6},
wrapperCol: {span: 14},
};
const AssetUserBind = ({id, visible, handleOk, handleCancel, confirmLoading}) => {
const [form] = Form.useForm();
let [selectedUserIds, setSelectedUserIds] = useState([]);
let [users, setUsers] = useState([]);
let [strategies, setStrategies] = useState([]);
useEffect(() => {
async function fetchData() {
let queryParam = {'key': 'userId', 'assetId': id};
let items = await authorisedApi.GetSelected(queryParam);
setSelectedUserIds(items);
let users = await userApi.getAll();
setUsers(users);
let strategies = await strategyApi.getAll();
setStrategies(strategies);
}
if (visible) {
fetchData();
} else {
form.resetFields();
}
}, [visible])
return (
<Modal
title={'用户授权'}
visible={visible}
maskClosable={false}
destroyOnClose={true}
onOk={() => {
form
.validateFields()
.then(async values => {
let ok = await handleOk(values);
if (ok) {
form.resetFields();
}
});
}}
onCancel={() => {
form.resetFields();
handleCancel();
}}
confirmLoading={confirmLoading}
okText='确定'
cancelText='取消'
>
<Form form={form} {...formItemLayout} >
<Form.Item label="用户" name='userIds' rules={[{required: true, message: '请选择用户'}]}>
<Select
mode="multiple"
allowClear
style={{width: '100%'}}
placeholder="请选择用户"
>
{users.map(item => {
return <Select.Option key={item.id}
disabled={selectedUserIds.includes(item.id)}>{item.nickname}</Select.Option>
})}
</Select>
</Form.Item>
<Form.Item label="授权策略" name='strategyId' extra={'可控制授权用户上传下载文件等功能'}>
<Select
allowClear
style={{width: '100%'}}
placeholder="此字段不是必填的"
>
{strategies.map(item => {
return <Select.Option key={item.id}>{item.name}</Select.Option>
})}
</Select>
</Form.Item>
</Form>
</Modal>
)
};
export default AssetUserBind;

View File

@ -0,0 +1,146 @@
import React, {useEffect, useState} from 'react';
import {Button} from "antd";
import authorisedApi from "../../api/authorised";
import {Link} from "react-router-dom";
import {ProTable} from "@ant-design/pro-components";
import AssetUserGroupBind from "./AssetUserGroupBind";
import Show from "../../dd/fi/show";
const actionRef = React.createRef();
const AssetUserGroup = ({id, active}) => {
let [visible, setVisible] = useState(false);
let [confirmLoading, setConfirmLoading] = useState(false);
useEffect(() => {
if (active) {
actionRef.current.reload();
}
}, [active]);
const columns = [
{
dataIndex: 'index',
valueType: 'indexBorder',
width: 48,
},
{
title: '用户组名称',
dataIndex: 'userGroupName',
render: ((text, record) => {
return <Link to={`/user-group/${record['userGroupId']}`}>{text}</Link>
})
},
{
title: '授权策略名称',
dataIndex: 'strategyName',
hideInSearch: true,
render: ((text, record) => {
return <Link to={`/strategy/${record['strategyId']}`}>{text}</Link>
})
},
{
title: '授权日期',
key: 'created',
dataIndex: 'created',
hideInSearch: true,
},
{
title: '操作',
valueType: 'option',
key: 'option',
width: 50,
render: (text, record, _, action) => [
<Show menu={'asset-authorised-user-group-del'} key={'unbind-acc'}>
<a
key="unbind"
onClick={async () => {
await authorisedApi.DeleteById(record['id']);
actionRef.current.reload();
}}
>
移除
</a>
</Show>
,
],
},
];
return (
<div>
<ProTable
columns={columns}
actionRef={actionRef}
request={async (params = {}, sort, filter) => {
let field = '';
let order = '';
if (Object.keys(sort).length > 0) {
field = Object.keys(sort)[0];
order = Object.values(sort)[0];
}
let queryParams = {
pageIndex: params.current,
pageSize: params.pageSize,
name: params.name,
assetId: id,
field: field,
order: order
}
let result = await authorisedApi.GetUserGroupPaging(queryParams);
return {
data: result['items'],
success: true,
total: result['total']
};
}}
rowKey="id"
search={{
labelWidth: 'auto',
}}
pagination={{
pageSize: 10,
}}
dateFormatter="string"
headerTitle="授权的用户组列表"
toolBarRender={() => [
<Show menu={'asset-authorised-user-group-add'} key={'bind-acc'}>
<Button key="button" type="primary" onClick={() => {
setVisible(true);
}}>
授权
</Button>
</Show>
,
]}
/>
<AssetUserGroupBind
id={id}
visible={visible}
confirmLoading={confirmLoading}
handleCancel={() => {
setVisible(false);
}}
handleOk={async (values) => {
setConfirmLoading(true);
values['assetId'] = id;
try {
let success = authorisedApi.AuthorisedUserGroups(values);
if (success) {
setVisible(false);
}
actionRef.current.reload();
} finally {
setConfirmLoading(false);
}
}}
/>
</div>
);
};
export default AssetUserGroup;

View File

@ -0,0 +1,105 @@
import React, {useEffect, useState} from 'react';
import {Form, Modal, Select} from "antd";
import authorisedApi from "../../api/authorised";
import userGroupApi from "../../api/user-group";
import strategyApi from "../../api/strategy";
import commandFilterApi from "../../api/command-filter";
const formItemLayout = {
labelCol: {span: 6},
wrapperCol: {span: 14},
};
const AssetUserGroupBind = ({id, visible, handleOk, handleCancel, confirmLoading}) => {
const [form] = Form.useForm();
let [selectedUserGroupIds, setSelectedUserGroupIds] = useState([]);
let [userGroups, setUserGroups] = useState([]);
let [commandFilters, setCommandFilters] = useState([]);
let [strategies, setStrategies] = useState([]);
useEffect(() => {
async function fetchData() {
let queryParam = {'key': 'userGroupId', 'assetId': id};
let items = await authorisedApi.GetSelected(queryParam);
setSelectedUserGroupIds(items);
let userGroups = await userGroupApi.GetAll();
setUserGroups(userGroups);
let strategies = await strategyApi.GetAll();
setStrategies(strategies);
let commandFilters = await commandFilterApi.GetAll();
setCommandFilters(commandFilters);
}
if (visible) {
fetchData();
} else {
form.resetFields();
}
}, [visible])
return (
<Modal
title={'用户授权'}
visible={visible}
maskClosable={false}
destroyOnClose={true}
onOk={() => {
form
.validateFields()
.then(async values => {
let ok = await handleOk(values);
if (ok) {
form.resetFields();
}
});
}}
onCancel={() => {
form.resetFields();
handleCancel();
}}
confirmLoading={confirmLoading}
okText='确定'
cancelText='取消'
>
<Form form={form} {...formItemLayout} >
<Form.Item label="用户组" name='userGroupIds' rules={[{required: true, message: '请选择用户组'}]}>
<Select
mode="multiple"
allowClear
style={{width: '100%'}}
placeholder="请选择用户组"
>
{userGroups.map(item => {
return <Select.Option key={item.id}
disabled={selectedUserGroupIds.includes(item.id)}>{item.name}</Select.Option>
})}
</Select>
</Form.Item>
<Form.Item label="授权策略" name='strategyId' extra={'可控制授权用户上传下载文件等功能'}>
<Select
allowClear
style={{width: '100%'}}
placeholder="此字段不是必填的"
>
{strategies.map(item => {
return <Select.Option key={item.id}>{item.name}</Select.Option>
})}
</Select>
</Form.Item>
</Form>
</Modal>
)
};
export default AssetUserGroupBind;

View File

@ -0,0 +1,43 @@
import React, {useState} from 'react';
import {useQuery} from "react-query";
import userApi from "../../api/user";
import {Modal, Select, Spin} from "antd";
const ChangeOwner = ({lastOwner, open, handleOk, handleCancel}) => {
let [confirmLoading, setConfirmLoading] = useState(false);
let [owner, setOwner] = useState(lastOwner);
let usersQuery = useQuery('usersQuery', userApi.getAll, {
enabled: open
});
return (<div>
<Modal title="更换所有者"
confirmLoading={confirmLoading}
open={open}
onOk={async () => {
setConfirmLoading(true);
await handleOk(owner);
setConfirmLoading(false);
}}
onCancel={handleCancel}
destroyOnClose={true}
>
{/*<Alert style={{marginBottom: `8px`}} message="Informational Notes" type="info" showIcon />*/}
<Spin spinning={usersQuery.isLoading}>
<Select defaultValue={lastOwner}
style={{width: `100%`}}
onChange={(value) => {
setOwner(value);
}}>
{usersQuery.data?.map(item => {
return <Select.Option key={item.id} value={item.id}>{item.nickname}</Select.Option>
})}
</Select>
</Spin>
</Modal>
</div>);
};
export default ChangeOwner;

View File

@ -0,0 +1,233 @@
import React, {useState} from 'react';
import {Button, Layout, message, Popconfirm} from "antd";
import {ProTable} from "@ant-design/pro-components";
import commandApi from "../../api/command";
import CommandModal from "./CommandModal";
import SelectingAsset from "./SelectingAsset";
import ColumnState, {useColumnState} from "../../hook/column-state";
import Show from "../../dd/fi/show";
import ChangeOwner from "./ChangeOwner";
const {Content} = Layout;
const api = commandApi;
const actionRef = React.createRef();
const Command = () => {
let [assetVisible, setAssetVisible] = useState(false);
let [visible, setVisible] = useState(false);
let [confirmLoading, setConfirmLoading] = useState(false);
let [selectedRowKey, setSelectedRowKey] = useState(undefined);
const [columnsStateMap, setColumnsStateMap] = useColumnState(ColumnState.CREDENTIAL);
let [selectedRow, setSelectedRow] = useState(undefined);
let [changeOwnerVisible, setChangeOwnerVisible] = useState(false);
const columns = [
{
dataIndex: 'index',
valueType: 'indexBorder',
width: 48,
},
{
title: '名称',
dataIndex: 'name',
}, {
title: '内容',
dataIndex: 'content',
key: 'content',
copyable: true,
ellipsis: true
}, {
title: '所有者',
dataIndex: 'ownerName',
key: 'ownerName',
hideInSearch: true
},
{
title: '创建时间',
key: 'created',
dataIndex: 'created',
hideInSearch: true,
},
{
title: '操作',
valueType: 'option',
key: 'option',
render: (text, record, _, action) => [
<Show menu={'command-exec'} key={'command-exec'}>
<a
key="run"
onClick={() => {
setAssetVisible(true);
setSelectedRowKey(record['id']);
}}
>
执行
</a>
</Show>,
<Show menu={'command-edit'} key={'command-edit'}>
<a
key="edit"
onClick={() => {
setVisible(true);
setSelectedRowKey(record['id']);
}}
>
编辑
</a>
</Show>,
<Show menu={'command-change-owner'} key={'command-change-owner'}>
<a
key="change-owner"
onClick={() => {
handleChangeOwner(record);
}}
>
更换所有者
</a>
</Show>,
<Show menu={'command-del'} key={'command-del'}>
<Popconfirm
key={'confirm-delete'}
title="您确认要删除此行吗?"
onConfirm={async () => {
await api.deleteById(record.id);
actionRef.current.reload();
}}
okText="确认"
cancelText="取消"
>
<a key='delete' className='danger'>删除</a>
</Popconfirm>
</Show>,
],
},
];
const handleChangeOwner = (row) => {
setSelectedRow(row);
setChangeOwnerVisible(true);
}
return (<Content className="page-container">
<ProTable
columns={columns}
actionRef={actionRef}
columnsState={{
value: columnsStateMap,
onChange: setColumnsStateMap
}}
request={async (params = {}, sort, filter) => {
let field = '';
let order = '';
if (Object.keys(sort).length > 0) {
field = Object.keys(sort)[0];
order = Object.values(sort)[0];
}
let queryParams = {
pageIndex: params.current,
pageSize: params.pageSize,
name: params.name,
field: field,
order: order
}
let result = await api.getPaging(queryParams);
return {
data: result['items'],
success: true,
total: result['total']
};
}}
rowKey="id"
search={{
labelWidth: 'auto',
}}
pagination={{
defaultPageSize: 10,
}}
dateFormatter="string"
headerTitle="动态指令列表"
toolBarRender={() => [
<Show menu={'command-add'}>
<Button key="button" type="primary" onClick={() => {
setVisible(true)
}}>
新建
</Button>
</Show>,
]}
/>
<CommandModal
id={selectedRowKey}
visible={visible}
confirmLoading={confirmLoading}
handleCancel={() => {
setVisible(false);
setSelectedRowKey(undefined);
}}
handleOk={async (values) => {
setConfirmLoading(true);
try {
let success;
if (values['id']) {
success = await api.updateById(values['id'], values);
} else {
success = await api.create(values);
}
if (success) {
setVisible(false);
}
actionRef.current.reload();
} finally {
setConfirmLoading(false);
}
}}
/>
<SelectingAsset
visible={assetVisible}
handleCancel={() => {
setAssetVisible(false);
setSelectedRowKey(undefined);
}}
handleOk={(rows) => {
if (rows.length === 0) {
message.warning('请至少选择一个资产');
return;
}
let cAssets = rows.map(item => {
return {
id: item['id'],
name: item['name']
}
});
window.location.href = '#/execute-command?commandId=' + selectedRowKey + '&assets=' + JSON.stringify(cAssets);
}}
/>
<ChangeOwner
lastOwner={selectedRow?.owner}
open={changeOwnerVisible}
handleOk={async (owner) => {
let success = await api.changeOwner(selectedRow?.id, owner);
if (success) {
setChangeOwnerVisible(false);
actionRef.current.reload();
}
}}
handleCancel={() => {
setChangeOwnerVisible(false);
}}
/>
</Content>);
};
export default Command;

View File

@ -0,0 +1,93 @@
import React, {useEffect} from 'react';
import {Form, Input, Modal} from "antd";
import commandApi from "../../api/command";
import workCommandApi from "../../api/worker/command";
const api = commandApi;
const {TextArea} = Input;
const CommandModal = ({
visible,
handleOk,
handleCancel,
confirmLoading,
id,
worker
}) => {
const [form] = Form.useForm();
const formItemLayout = {
labelCol: {span: 6},
wrapperCol: {span: 14},
};
useEffect(() => {
const getItem = async () => {
let data;
if (worker === true) {
data = await workCommandApi.getById(id);
} else {
data = await api.getById(id);
}
if (data) {
form.setFieldsValue(data);
}
}
if (visible) {
if (id) {
getItem();
} else {
form.setFieldsValue({});
}
} else {
form.resetFields();
}
}, [visible]);
return (
<Modal
title={id ? '更新动态指令' : '新建动态指令'}
visible={visible}
maskClosable={false}
destroyOnClose={true}
onOk={() => {
form
.validateFields()
.then(async values => {
let ok = await handleOk(values);
if (ok) {
form.resetFields();
}
});
}}
onCancel={() => {
form.resetFields();
handleCancel();
}}
confirmLoading={confirmLoading}
okText='确定'
cancelText='取消'
>
<Form form={form} {...formItemLayout}>
<Form.Item name='id' noStyle>
<Input hidden={true}/>
</Form.Item>
<Form.Item label="指令名称" name='name' rules={[{required: true, message: '请输入指令名称'}]}>
<Input placeholder="请输入指令名称"/>
</Form.Item>
<Form.Item label="指令内容" name='content' rules={[{required: true, message: '请输入指令内容'}]}>
<TextArea autoSize={{minRows: 5, maxRows: 10}} placeholder="一行一个指令"/>
</Form.Item>
</Form>
</Modal>
)
};
export default CommandModal;

View File

@ -0,0 +1,180 @@
import React, {useState} from 'react';
import {Button, Layout, Popconfirm, Tag} from "antd";
import {ProTable} from "@ant-design/pro-components";
import credentialApi from "../../api/credential";
import CredentialModal from "./CredentialModal";
import ColumnState, {useColumnState} from "../../hook/column-state";
import Show from "../../dd/fi/show";
const {Content} = Layout;
const actionRef = React.createRef();
const api = credentialApi;
const Credential = () => {
let [visible, setVisible] = useState(false);
let [confirmLoading, setConfirmLoading] = useState(false);
let [selectedRowKey, setSelectedRowKey] = useState(undefined);
const [columnsStateMap, setColumnsStateMap] = useColumnState(ColumnState.CREDENTIAL);
const columns = [
{
dataIndex: 'index',
valueType: 'indexBorder',
width: 48,
},
{
title: '名称',
dataIndex: 'name',
}, {
title: '凭证类型',
dataIndex: 'type',
key: 'type',
hideInSearch: true,
render: (type, record) => {
if (type === 'private-key') {
return (
<Tag color="green">密钥</Tag>
);
} else {
return (
<Tag color="red">密码</Tag>
);
}
}
}, {
title: '授权账户',
dataIndex: 'username',
key: 'username',
hideInSearch: true
}, {
title: '所有者',
dataIndex: 'ownerName',
key: 'ownerName',
hideInSearch: true
},
{
title: '创建时间',
key: 'created',
dataIndex: 'created',
hideInSearch: true,
},
{
title: '操作',
valueType: 'option',
key: 'option',
render: (text, record, _, action) => [
<Show menu={'credential-edit'} key={'credential-edit'}>
<a
key="edit"
onClick={() => {
setVisible(true);
setSelectedRowKey(record['id']);
}}
>
编辑
</a>
</Show>,
<Show menu={'credential-del'} key={'credential-del'}>
<Popconfirm
key={'confirm-delete'}
title="您确认要删除此行吗?"
onConfirm={async () => {
await api.deleteById(record.id);
actionRef.current.reload();
}}
okText="确认"
cancelText="取消"
>
<a key='delete' className='danger'>删除</a>
</Popconfirm>
</Show>,
],
},
];
return (<Content className="page-container">
<ProTable
columns={columns}
actionRef={actionRef}
columnsState={{
value: columnsStateMap,
onChange: setColumnsStateMap
}}
request={async (params = {}, sort, filter) => {
let field = '';
let order = '';
if (Object.keys(sort).length > 0) {
field = Object.keys(sort)[0];
order = Object.values(sort)[0];
}
let queryParams = {
pageIndex: params.current,
pageSize: params.pageSize,
name: params.name,
field: field,
order: order
}
let result = await api.getPaging(queryParams);
return {
data: result['items'],
success: true,
total: result['total']
};
}}
rowKey="id"
search={{
labelWidth: 'auto',
}}
pagination={{
defaultPageSize: 10,
}}
dateFormatter="string"
headerTitle="授权凭证列表"
toolBarRender={() => [
<Show menu={'credential-add'}>
<Button key="button" type="primary" onClick={() => {
setVisible(true)
}}>
新建
</Button>
</Show>,
]}
/>
<CredentialModal
id={selectedRowKey}
visible={visible}
confirmLoading={confirmLoading}
handleCancel={() => {
setVisible(false);
setSelectedRowKey(undefined);
}}
handleOk={async (values) => {
setConfirmLoading(true);
try {
let success;
if (values['id']) {
success = await api.updateById(values['id'], values);
} else {
success = await api.create(values);
}
if (success) {
setVisible(false);
}
actionRef.current.reload();
} finally {
setConfirmLoading(false);
}
}}
/>
</Content>);
}
export default Credential;

View File

@ -1,15 +1,22 @@
import React, {useState} from 'react';
import {Form, Input, Modal, Select} from "antd/lib/index";
import {isEmpty} from "../../utils/utils";
import React, {useEffect, useState} from 'react';
import {Form, Input, Modal, Select} from "antd";
import credentialApi from "../../api/credential";
const {TextArea} = Input;
const api = credentialApi;
const accountTypes = [
{text: '密码', value: 'custom'},
{text: '密钥', value: 'private-key'},
];
const CredentialModal = ({title, visible, handleOk, handleCancel, confirmLoading, model}) => {
const CredentialModal = ({
visible,
handleOk,
handleCancel,
confirmLoading,
id,
}) => {
const [form] = Form.useForm();
@ -18,54 +25,63 @@ const CredentialModal = ({title, visible, handleOk, handleCancel, confirmLoading
wrapperCol: {span: 14},
};
if (model === null || model === undefined) {
model = {}
}
if (isEmpty(model.type)) {
model.type = 'custom';
}
for (let key in model) {
if (model.hasOwnProperty(key)) {
if (model[key] === '-') {
model[key] = '';
}
}
}
let [type, setType] = useState(model.type);
let [type, setType] = useState('');
const handleAccountTypeChange = v => {
setType(v);
model.type = v;
}
useEffect(() => {
const getItem = async () => {
let data = await api.getById(id);
if (data) {
form.setFieldsValue(data);
setType(data['type']);
}
}
if (visible) {
if (id) {
getItem();
}else {
form.setFieldsValue({
type: 'custom',
});
}
} else {
form.resetFields();
}
}, [visible]);
return (
<Modal
title={title}
title={id ? '更新授权凭证' : '新建授权凭证'}
visible={visible}
maskClosable={false}
destroyOnClose={true}
onOk={() => {
form
.validateFields()
.then(values => {
form.resetFields();
handleOk(values);
})
.catch(info => {
.then(async values => {
let ok = await handleOk(values);
if (ok) {
form.resetFields();
}
});
}}
onCancel={handleCancel}
onCancel={() => {
form.resetFields();
handleCancel();
}}
confirmLoading={confirmLoading}
okText='确定'
cancelText='取消'
>
<Form form={form} {...formItemLayout} initialValues={model}>
<Form form={form} {...formItemLayout}>
<Form.Item name='id' noStyle>
<Input hidden={true}/>
</Form.Item>

View File

@ -1,312 +0,0 @@
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,
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}}
/>
<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 === 'native') {
url = `#/term?assetId=${id}&assetName=${name}`;
} else {
url = `#/access?assetId=${id}&assetName=${name}&protocol=${protocol}`;
}
return (
<List.Item>
<a target='_blank' href={url} rel='noreferrer noopener'>
<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><TagsOutlined/> 标签</div>}>
<strong>{this.renderTags(item['tags'])}</strong>
</Descriptions.Item>
</Descriptions>
</Card>
</a>
</List.Item>
)
}}
/>
</div>
</div>
);
}
}
export default MyAsset;

View File

@ -0,0 +1,218 @@
import React, {useState} from 'react';
import {Badge, Modal, Space, Table, Tag, Tooltip, Typography} from "antd";
import {PROTOCOL_COLORS} from "../../common/constants";
import {isEmpty} from "../../utils/utils";
import dayjs from "dayjs";
import {ProTable} from "@ant-design/pro-components";
import assetApi from "../../api/asset";
const {Title} = Typography;
const actionRef = React.createRef();
const SelectingAsset = ({
visible,
handleOk,
handleCancel,
confirmLoading,
id,
}) => {
let [rows, setRows] = useState([]);
const addRows = (selectedRows) => {
selectedRows.forEach(selectedRow => {
let exist = rows.some(row => {
return row.id === selectedRow.id;
});
if (exist === false) {
rows.push(selectedRow);
}
});
setRows(rows.slice());
}
const removeRows = (selectedRows) => {
selectedRows.forEach(selectedRow => {
rows = rows.filter(row => row.id !== selectedRow.id);
});
setRows(rows.slice());
}
const removeRow = (rowKey) => {
let items = rows.filter(row => row.id !== rowKey);
setRows(items.slice());
}
const columns = [{
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" text='运行中'/>
</Tooltip>
)
} else {
return (
<Tooltip title='不可用'>
<Badge status="error" text='不可用'/>
</Tooltip>
)
}
}
}, {
title: '所有者',
dataIndex: 'ownerName',
key: 'ownerName'
}, {
title: '创建时间',
dataIndex: 'created',
key: 'created',
render: (text, record) => {
return (
<Tooltip title={text}>
{dayjs(text).fromNow()}
</Tooltip>
)
}
},
];
return (
<div>
<Modal
title="选择资产"
visible={visible}
width={window.innerWidth * 0.8}
centered={true}
onOk={() => {
handleOk(rows);
}}
onCancel={handleCancel}
>
<div style={{paddingLeft: 24, paddingRight: 24}}>
<Title level={5}>待执行资产列表</Title>
<div>
{
rows.map(item => {
return <Tag color={PROTOCOL_COLORS[item['protocol']]} closable
onClose={() => removeRow(item['id'])}
key={item['id']}>{item['name']}</Tag>
})
}
</div>
</div>
<ProTable
columns={columns}
actionRef={actionRef}
rowSelection={{
// 自定义选择项参考: https://ant.design/components/table-cn/#components-table-demo-row-selection-custom
// 注释该行则默认不显示下拉选项
selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT],
}}
tableAlertRender={({selectedRowKeys, selectedRows, onCleanSelected}) => (
<Space size={24}>
<span>
已选 {selectedRowKeys.length}
</span>
<span>
<a onClick={() => addRows(selectedRows)}>
加入待执行列表
</a>
</span>
<span>
<a onClick={() => removeRows(selectedRows)}>
从待执行列表移除
</a>
</span>
</Space>
)}
request={async (params = {}, sort, filter) => {
let field = '';
let order = '';
if (Object.keys(sort).length > 0) {
field = Object.keys(sort)[0];
order = Object.values(sort)[0];
}
let queryParams = {
pageIndex: params.current,
pageSize: params.pageSize,
name: params.name,
protocol: 'ssh',
field: field,
order: order
}
let result = await assetApi.getPaging(queryParams);
return {
data: result['items'],
success: true,
total: result['total']
};
}}
rowKey="id"
search={{
labelWidth: 'auto',
}}
pagination={{
pageSize: 5,
}}
dateFormatter="string"
headerTitle="资产列表"
/>
</Modal>
</div>
);
};
export default SelectingAsset;

View File

@ -0,0 +1,233 @@
import React, {useState} from 'react';
import {Button, Layout, Popconfirm, Tag} from "antd";
import StrategyModal from "./StrategyModal";
import {ProTable} from "@ant-design/pro-components";
import strategyApi from "../../api/strategy";
import {Link} from "react-router-dom";
import ColumnState, {useColumnState} from "../../hook/column-state";
import {hasMenu} from "../../service/permission";
import Show from "../../dd/fi/show";
const api = strategyApi;
const {Content} = Layout;
const actionRef = React.createRef();
const renderStatus = (text) => {
if (text === true) {
return <Tag color={'green'}>开启</Tag>
} else {
return <Tag color={'red'}>关闭</Tag>
}
}
const Strategy = () => {
let [visible, setVisible] = useState(false);
let [confirmLoading, setConfirmLoading] = useState(false);
let [selectedRowKey, setSelectedRowKey] = useState(undefined);
const [columnsStateMap, setColumnsStateMap] = useColumnState(ColumnState.STRATEGY);
const columns = [{
dataIndex: 'index',
valueType: 'indexBorder',
width: 48,
}, {
title: '名称',
dataIndex: 'name',
key: 'name',
sorter: true,
render: (text, record) => {
let view = <div>{text}</div>;
if(hasMenu('strategy-detail')){
view = <Link to={`/strategy/${record['id']}`}>{text}</Link>;
}
return view;
},
}, {
title: '上传',
dataIndex: 'upload',
key: 'upload',
hideInSearch: true,
render: (text) => {
return renderStatus(text);
}
}, {
title: '下载',
dataIndex: 'download',
key: 'download',
hideInSearch: true,
render: (text) => {
return renderStatus(text);
}
}, {
title: '编辑',
dataIndex: 'edit',
key: 'edit',
hideInSearch: true,
render: (text) => {
return renderStatus(text);
}
}, {
title: '删除',
dataIndex: 'delete',
key: 'delete',
hideInSearch: true,
render: (text) => {
return renderStatus(text);
}
}, {
title: '重命名',
dataIndex: 'rename',
key: 'rename',
hideInSearch: true,
render: (text) => {
return renderStatus(text);
}
}, {
title: '复制',
dataIndex: 'copy',
key: 'copy',
hideInSearch: true,
render: (text) => {
return renderStatus(text);
}
}, {
title: '粘贴',
dataIndex: 'paste',
key: 'paste',
hideInSearch: true,
render: (text) => {
return renderStatus(text);
}
}, {
title: '创建时间',
dataIndex: 'created',
key: 'created',
hideInSearch: true,
},
{
title: '操作',
valueType: 'option',
key: 'option',
render: (text, record, _, action) => [
<Show menu={'strategy-edit'} key={'strategy-edit'}>
<a
key="edit"
onClick={() => {
setVisible(true);
setSelectedRowKey(record['id']);
}}
>
编辑
</a>
</Show>
,
<Show menu={'strategy-del'} key={'strategy-del'}>
<Popconfirm
key={'confirm-delete'}
title="您确认要删除此行吗?"
onConfirm={async () => {
await api.deleteById(record.id);
actionRef.current.reload();
}}
okText="确认"
cancelText="取消"
>
<a key='delete' className='danger'>删除</a>
</Popconfirm>
</Show>
,
],
},
];
return (
<div>
<Content className="page-container">
<ProTable
columns={columns}
actionRef={actionRef}
columnsState={{
value: columnsStateMap,
onChange: setColumnsStateMap
}}
request={async (params = {}, sort, filter) => {
let field = '';
let order = '';
if (Object.keys(sort).length > 0) {
field = Object.keys(sort)[0];
order = Object.values(sort)[0];
}
let queryParams = {
pageIndex: params.current,
pageSize: params.pageSize,
name: params.name,
field: field,
order: order
}
let result = await api.getPaging(queryParams);
return {
data: result['items'],
success: true,
total: result['total']
};
}}
rowKey="id"
search={{
labelWidth: 'auto',
}}
pagination={{
pageSize: 10,
}}
dateFormatter="string"
headerTitle="授权策略"
toolBarRender={() => [
<Show menu={'strategy-add'}>
<Button key="button" type="primary" onClick={() => {
setVisible(true)
}}>
新建
</Button>
</Show>
,
]}
/>
<StrategyModal
id={selectedRowKey}
visible={visible}
confirmLoading={confirmLoading}
handleCancel={() => {
setVisible(false);
setSelectedRowKey(undefined);
}}
handleOk={async (values) => {
setConfirmLoading(true);
try {
let success;
if (values['id']) {
success = await api.updateById(values['id'], values);
} else {
success = await api.create(values);
}
if (success) {
setVisible(false);
}
actionRef.current.reload();
} finally {
setConfirmLoading(false);
}
}}
/>
</Content>
</div>
);
}
export default Strategy;

View File

@ -0,0 +1,33 @@
import React, {useState} from 'react';
import {useParams, useSearchParams} from "react-router-dom";
import {Layout, Tabs} from "antd";
import StrategyInfo from "./StrategyInfo";
const StrategyDetail = () => {
let params = useParams();
const id = params['strategyId'];
const [searchParams, setSearchParams] = useSearchParams();
let key = searchParams.get('activeKey');
key = key ? key : 'info';
let [activeKey, setActiveKey] = useState(key);
const handleTagChange = (key) => {
setActiveKey(key);
setSearchParams({'activeKey': key});
}
return (
<div>
<Layout.Content className="page-detail-warp">
<Tabs activeKey={activeKey} onChange={handleTagChange}>
<Tabs.TabPane tab="基本信息" key="info">
<StrategyInfo active={activeKey === 'info'} id={id}/>
</Tabs.TabPane>
</Tabs>
</Layout.Content>
</div>
);
};
export default StrategyDetail;

View File

@ -0,0 +1,47 @@
import React, {useEffect, useState} from 'react';
import {Descriptions, Tag} from "antd";
import strategyApi from "../../api/strategy";
const api = strategyApi;
const renderStatus = (text) => {
if (text === true) {
return <Tag color={'green'}>开启</Tag>
} else {
return <Tag color={'red'}>关闭</Tag>
}
}
const StrategyInfo = ({active, id}) => {
let [item, setItem] = useState({});
useEffect(() => {
const getItem = async (id) => {
let item = await api.getById(id);
if (item) {
setItem(item);
}
};
if (active && id) {
getItem(id);
}
}, [active]);
return (
<div className={'page-detail-info'}>
<Descriptions column={1}>
<Descriptions.Item label="名称">{item['name']}</Descriptions.Item>
<Descriptions.Item label="上传">{renderStatus(item['upload'])}</Descriptions.Item>
<Descriptions.Item label="下载">{renderStatus(item['download'])}</Descriptions.Item>
<Descriptions.Item label="编辑">{renderStatus(item['edit'])}</Descriptions.Item>
<Descriptions.Item label="删除">{renderStatus(item['delete'])}</Descriptions.Item>
<Descriptions.Item label="重命名">{renderStatus(item['rename'])}</Descriptions.Item>
<Descriptions.Item label="复制">{renderStatus(item['copy'])}</Descriptions.Item>
<Descriptions.Item label="粘贴">{renderStatus(item['paste'])}</Descriptions.Item>
<Descriptions.Item label="创建时间">{item['created']}</Descriptions.Item>
</Descriptions>
</div>
);
};
export default StrategyInfo;

View File

@ -1,48 +1,67 @@
import React from 'react';
import {Form, Input, Modal, Switch} from "antd/lib/index";
import React, {useEffect} from 'react';
import {Form, Input, Modal, Switch} from "antd";
import strategyApi from "../../api/strategy";
const api = strategyApi;
const formItemLayout = {
labelCol: {span: 6},
wrapperCol: {span: 14},
};
const StrategyModal = ({title, visible, handleOk, handleCancel, confirmLoading, model}) => {
const StrategyModal = ({visible, handleOk, handleCancel, confirmLoading, id}) => {
const [form] = Form.useForm();
if (model === undefined) {
model = {
'upload': false,
'download': false,
'delete': false,
'rename': false,
'edit': false,
'copy': false,
'paste': false,
};
}
useEffect(() => {
const getItem = async () => {
let data = await api.getById(id);
if (data) {
form.setFieldsValue(data);
}
}
if (visible && id) {
getItem();
} else {
form.setFieldsValue({
upload: false,
download: false,
edit: false,
delete: false,
rename: false,
copy: false,
paste: false,
});
}
}, [visible]);
return (
<Modal
title={title}
title={id ? '更新授权策略' : '新建授权策略'}
visible={visible}
maskClosable={false}
destroyOnClose={true}
onOk={() => {
form
.validateFields()
.then(values => {
form.resetFields();
handleOk(values);
.then(async values => {
let ok = await handleOk(values);
if (ok) {
form.resetFields();
}
});
}}
onCancel={handleCancel}
onCancel={() => {
form.resetFields();
handleCancel();
}}
confirmLoading={confirmLoading}
okText='确定'
cancelText='取消'
>
<Form form={form} {...formItemLayout} initialValues={model}>
<Form form={form} {...formItemLayout}>
<Form.Item name='id' noStyle>
<Input hidden={true}/>
</Form.Item>

View File

@ -1,108 +0,0 @@
import React, {Component} from 'react';
import {Card, Input, List, Spin} from "antd";
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;
class BatchCommand extends Component {
commandRef = React.createRef();
state = {
webSockets: [],
assets: [],
active: undefined,
loading: true
}
componentDidMount() {
let params = new URLSearchParams(this.props.location.search);
let assets = JSON.parse(params.get('assets'));
let commandId = params.get('commandId');
this.init(commandId, assets)
}
init = async (commandId, assets) => {
let result = await request.get(`/commands/${commandId}`);
if (result['code'] !== 1) {
message.error(result['message'], 10);
this.setState({
loading: false
})
return;
}
let command = result['data']['content'];
this.setState({
loading: false,
command: command,
assets: assets
})
}
onPaneChange = activeKey => {
this.setState({activeKey});
};
appendWebsocket = (webSocket) => {
this.state.webSockets.push(webSocket);
}
render() {
return (
<>
<Spin spinning={this.state.loading} tip='正在获取指令内容...'>
<div className="page-search">
<Search ref={this.commandRef} placeholder="请输入指令" onSearch={value => {
for (let i = 0; i < this.state.webSockets.length; i++) {
let ws = this.state.webSockets[i]['ws'];
if (ws.readyState === WebSocket.OPEN) {
ws.send(new Message(Message.Data, value + String.fromCharCode(13)).toString());
}
}
this.commandRef.current.setValue('');
}} enterButton='执行'/>
</div>
<div className="page-card">
<List
grid={{gutter: 16, column: 2}}
dataSource={this.state.assets}
renderItem={item => (
<List.Item>
<Card title={item.name}
className={['console-card',this.state.active === item['id'] ? 'command-active' : '']}
onClick={() => {
if (this.state.active === item['id']) {
this.setState({
active: undefined
})
} else {
this.setState({
active: item['id']
})
}
}}
>
<BatchCommandTerm assetId={item.id}
command={this.state.command}
appendWebsocket={this.appendWebsocket}/>
</Card>
</List.Item>
)}
/>
</div>
</Spin>
</>
);
}
}
export default BatchCommand;

View File

@ -1,407 +0,0 @@
import React, {Component} from 'react';
import {
Badge,
Button,
Col,
Divider,
Input,
Layout,
Row,
Select,
Space,
Table,
Tag,
Tooltip,
Typography
} from "antd";
import qs from "qs";
import request from "../../common/request";
import {message} from "antd/es";
import {SyncOutlined, UndoOutlined} from '@ant-design/icons';
import {PROTOCOL_COLORS} from "../../common/constants";
import {isEmpty} from "../../utils/utils";
import dayjs from "dayjs";
const {Search} = Input;
const {Content} = Layout;
const {Title} = Typography;
class ChooseAsset extends Component {
inputRefOfName = React.createRef();
inputRefOfIp = React.createRef();
changeOwnerFormRef = React.createRef();
state = {
items: [],
total: 0,
queryParams: {
pageIndex: 1,
pageSize: 10,
protocol: 'ssh'
},
loading: false,
tags: [],
model: {},
selectedRowKeys: [],
selectedRows: [],
delBtnLoading: false,
changeOwnerModalVisible: false,
changeSharerModalVisible: false,
changeOwnerConfirmLoading: false,
changeSharerConfirmLoading: false,
users: [],
selected: {},
totalSelectedRows: [],
};
checkedAssets = undefined
async componentDidMount() {
this.checkedAssets = this.props.setCheckedAssets;
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('/assets/paging?' + paramsStr);
if (result['code'] === 1) {
data = result['data'];
} else {
message.error(result['message']);
}
} catch (e) {
} finally {
let sharer = this.state.sharer;
const items = data.items.map(item => {
let disabled = false;
if (sharer === item['owner']) {
disabled = true;
}
return {...item, 'key': item['id'], 'disabled': disabled}
})
let totalSelectedRows = this.state.totalSelectedRows;
let selectedRowKeys = totalSelectedRows.map(item => item['id']);
this.setState({
items: items,
total: data.total,
queryParams: queryParams,
loading: false,
selectedRowKeys: selectedRowKeys
});
}
}
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 => {
console.log(tags)
// this.setState({
// tags: tags
// })
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'tags': tags.join(','),
}
this.loadTableData(query);
}
unSelectRow = async (assetId) => {
const selectedRowKeys = this.state.selectedRowKeys.filter(key => key !== assetId);
const totalSelectedRows = this.state.totalSelectedRows.filter(item => item['id'] !== assetId);
this.setState({
selectedRowKeys: selectedRowKeys,
totalSelectedRows: totalSelectedRows
})
if (this.checkedAssets) {
this.checkedAssets(totalSelectedRows);
}
console.log(totalSelectedRows);
}
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" text='运行中'/>
</Tooltip>
)
} else {
return (
<Tooltip title='不可用'>
<Badge status="error" text='不可用'/>
</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: 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'],
}),
};
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>
<Divider/>
<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>
<Tooltip title='重置查询'>
<Button icon={<UndoOutlined/>} onClick={() => {
this.inputRefOfName.current.setValue('');
this.inputRefOfIp.current.setValue('');
this.loadTableData({
...this.state.queryParams,
pageIndex: 1,
pageSize: 10,
protocol: 'ssh',
name: '',
ip: ''
})
}}>
</Button>
</Tooltip>
<Divider type="vertical"/>
<Tooltip title="刷新列表">
<Button icon={<SyncOutlined/>} onClick={() => {
this.loadTableData(this.state.queryParams)
}}>
</Button>
</Tooltip>
</Space>
</Col>
</Row>
</div>
<Table key='assets-table'
rowSelection={rowSelection}
dataSource={this.state.items}
columns={columns}
position={'both'}
size="middle"
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}
/>
</Content>
</>
);
}
}
export default ChooseAsset;

View File

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

View File

@ -1,649 +0,0 @@
import React, {Component} from 'react';
import {
Button,
Col,
Divider,
Dropdown,
Form,
Input,
Layout,
Menu,
Modal,
Row,
Select,
Space,
Table,
Tooltip,
Typography
} from "antd";
import qs from "qs";
import request from "../../common/request";
import {message} from "antd/es";
import DynamicCommandModal from "./DynamicCommandModal";
import {
DeleteOutlined,
DownOutlined,
ExclamationCircleOutlined,
PlusOutlined,
SyncOutlined,
UndoOutlined
} from '@ant-design/icons';
import {hasPermission, isAdmin} from "../../service/permission";
import dayjs from "dayjs";
import ChooseAsset from "./ChooseAsset";
const confirm = Modal.confirm;
const {Content} = Layout;
const {Title, Text} = Typography;
const {Search} = Input;
class DynamicCommand extends Component {
inputRefOfName = React.createRef();
inputRefOfContent = React.createRef();
changeOwnerFormRef = React.createRef();
state = {
items: [],
total: 0,
queryParams: {
pageIndex: 1,
pageSize: 10
},
loading: false,
modalVisible: false,
modalTitle: '',
modalConfirmLoading: false,
assetsVisible: false,
assets: [],
checkedAssets: [],
commandId: '',
model: null,
selectedRowKeys: [],
delBtnLoading: false,
changeOwnerModalVisible: false,
changeOwnerConfirmLoading: false,
users: [],
selected: {},
indeterminate: true,
checkAllChecked: false
};
componentDidMount() {
this.loadTableData();
}
async delete(id) {
const result = await request.delete('/commands/' + id);
if (result.code === 1) {
message.success('删除成功');
this.loadTableData(this.state.queryParams);
} else {
message.error(result.message, 10);
}
}
async loadTableData(queryParams) {
this.setState({
loading: true
});
queryParams = queryParams || this.state.queryParams;
// queryParams
let paramsStr = qs.stringify(queryParams);
let data = {
items: [],
total: 0
};
try {
let result = await request.get('/commands/paging?' + paramsStr);
if (result.code === 1) {
data = result.data;
} else {
message.error(result.message);
}
} catch (e) {
} finally {
const items = data.items.map(item => {
return {'key': item['id'], ...item}
})
this.setState({
items: items,
total: data.total,
queryParams: queryParams,
loading: false
});
}
}
handleChangPage = async (pageIndex, pageSize) => {
let queryParams = this.state.queryParams;
queryParams.pageIndex = pageIndex;
queryParams.pageSize = pageSize;
this.setState({
queryParams: queryParams
});
await this.loadTableData(queryParams)
};
handleSearchByName = name => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'name': name,
}
this.loadTableData(query);
};
handleSearchByContent = content => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'content': content,
}
this.loadTableData(query);
};
showDeleteConfirm(id, content) {
let self = this;
confirm({
title: '您确定要删除此指令吗?',
content: content,
okText: '确定',
okType: 'danger',
cancelText: '取消',
onOk() {
self.delete(id);
}
});
};
showModal(title, assets = null) {
this.setState({
modalTitle: title,
modalVisible: true,
model: assets
});
};
handleCancelModal = e => {
this.setState({
modalTitle: '',
modalVisible: false
});
};
setCheckedAssets = (checkedAssets) => {
this.setState({
checkedAssets: checkedAssets
})
}
executeCommand = e => {
let checkedAssets = this.state.checkedAssets;
if (checkedAssets.length === 0) {
message.warning('请至少选择一个资产');
return;
}
let cAssets = checkedAssets.map(item => {
return {
id: item['id'],
name: item['name']
}
});
window.location.href = '#/batch-command?commandId=' + this.state.commandId + '&assets=' + JSON.stringify(cAssets);
};
handleOk = async (formData) => {
// 弹窗 form 传来的数据
this.setState({
modalConfirmLoading: true
});
if (formData.id) {
// 向后台提交数据
const result = await request.put('/commands/' + formData.id, formData);
if (result.code === 1) {
message.success('更新成功');
this.setState({
modalVisible: false
});
this.loadTableData(this.state.queryParams);
} else {
message.error('更新失败 :( ' + result.message, 10);
}
} else {
// 向后台提交数据
const result = await request.post('/commands', formData);
if (result.code === 1) {
message.success('新增成功');
this.setState({
modalVisible: false
});
this.loadTableData(this.state.queryParams);
} else {
message.error('新增失败 :( ' + result.message, 10);
}
}
this.setState({
modalConfirmLoading: false
});
};
batchDelete = async () => {
this.setState({
delBtnLoading: true
})
try {
let result = await request.delete('/commands/' + this.state.selectedRowKeys.join(','));
if (result.code === 1) {
message.success('操作成功', 3);
this.setState({
selectedRowKeys: []
})
await this.loadTableData(this.state.queryParams);
} else {
message.error(result.message, 10);
}
} finally {
this.setState({
delBtnLoading: false
})
}
}
handleSearchByNickname = async nickname => {
const result = await request.get(`/users/paging?pageIndex=1&pageSize=100&nickname=${nickname}`);
if (result.code !== 1) {
message.error(result.message, 10);
return;
}
const items = result['data']['items'].map(item => {
return {'key': item['id'], ...item}
})
this.setState({
users: items
})
}
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.showModal('更新指令', record)}>
<Tooltip placement="topLeft" title={name}>
{short}
</Tooltip>
</Button>
);
} else {
return (
<Tooltip placement="topLeft" title={name}>
{short}
</Tooltip>
);
}
},
sorter: true,
}, {
title: '指令内容',
dataIndex: 'content',
key: 'content',
render: (content, record) => {
let short = content;
if (short && short.length > 20) {
short = short.substring(0, 20) + " ...";
}
return (
<Tooltip placement="topLeft" title={content}>
{short}
</Tooltip>
);
}
}, {
title: '所有者',
dataIndex: 'ownerName',
key: 'ownerName'
}, {
title: '创建日期',
dataIndex: 'created',
key: 'created',
render: (text, record) => {
return (
<Tooltip title={text}>
{dayjs(text).fromNow()}
</Tooltip>
)
},
sorter: true,
}, {
title: '操作',
key: 'action',
render: (text, record) => {
const menu = (
<Menu>
<Menu.Item key="1">
<Button type="text" size='small'
onClick={() => this.showModal('更新指令', record)}>编辑</Button>
</Menu.Item>
<Menu.Item key="2">
<Button type="text" size='small'
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
onClick={() => this.showDeleteConfirm(record.id, record.name)}>删除</Button>
</Menu.Item>
</Menu>
);
let action;
if (isAdmin()) {
action = (
<div>
<Button type="link" size='small' onClick={async () => {
this.setState({
assetsVisible: true,
commandId: record['id']
});
}}>执行</Button>
<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>
<Button type="link" size='small' danger
disabled={!hasPermission(record['owner'])}
onClick={() => this.showDeleteConfirm(record.id, record.name)}>删除</Button>
</div>
)
}
return action;
},
}
];
const selectedRowKeys = this.state.selectedRowKeys;
const rowSelection = {
selectedRowKeys: this.state.selectedRowKeys,
onChange: (selectedRowKeys, selectedRows) => {
this.setState({selectedRowKeys});
},
};
const hasSelected = selectedRowKeys.length > 0;
let contentClassName = isAdmin() ? 'page-content' : 'page-content-user';
return (
<>
<Content className={["site-layout-background", contentClassName]}>
<div style={{marginBottom: 20}}>
<Row justify="space-around" align="middle" gutter={24}>
<Col span={12} key={1}>
<Title level={3}>指令列表</Title>
</Col>
<Col span={12} key={2} style={{textAlign: 'right'}}>
<Space>
<Search
ref={this.inputRefOfName}
placeholder="指令名称"
allowClear
onSearch={this.handleSearchByName}
/>
<Search
ref={this.inputRefOfContent}
placeholder="指令内容"
allowClear
onSearch={this.handleSearchByContent}
/>
<Tooltip title='重置查询'>
<Button icon={<UndoOutlined/>} onClick={() => {
this.inputRefOfName.current.setValue('');
this.inputRefOfContent.current.setValue('');
this.loadTableData({pageIndex: 1, pageSize: 10, name: '', content: ''})
}}>
</Button>
</Tooltip>
<Divider type="vertical"/>
<Tooltip title="新增">
<Button type="dashed" icon={<PlusOutlined/>}
onClick={() => this.showModal('新增指令', {})}>
</Button>
</Tooltip>
<Tooltip title="刷新列表">
<Button icon={<SyncOutlined/>} onClick={() => {
this.loadTableData(this.state.queryParams)
}}>
</Button>
</Tooltip>
<Tooltip title="批量删除">
<Button type="primary" danger disabled={!hasSelected} icon={<DeleteOutlined/>}
loading={this.state.delBtnLoading}
onClick={() => {
const content = <div>
您确定要删除选中的<Text style={{color: '#1890FF'}}
strong>{this.state.selectedRowKeys.length}</Text>
</div>;
confirm({
icon: <ExclamationCircleOutlined/>,
content: content,
onOk: () => {
this.batchDelete()
},
onCancel() {
},
});
}}>
</Button>
</Tooltip>
</Space>
</Col>
</Row>
</div>
<Table
rowSelection={rowSelection}
dataSource={this.state.items}
columns={columns}
position={'both'}
pagination={{
showSizeChanger: true,
current: this.state.queryParams.pageIndex,
pageSize: this.state.queryParams.pageSize,
onChange: this.handleChangPage,
onShowSizeChange: this.handleChangPage,
total: this.state.total,
showTotal: total => `总计 ${total}`
}}
loading={this.state.loading}
onChange={this.handleTableChange}
/>
{
this.state.modalVisible ?
<DynamicCommandModal
visible={this.state.modalVisible}
title={this.state.modalTitle}
handleOk={this.handleOk}
handleCancel={this.handleCancelModal}
confirmLoading={this.state.modalConfirmLoading}
model={this.state.model}
>
</DynamicCommandModal>
: null
}
<Modal
title="选择资产"
visible={this.state.assetsVisible}
width={window.innerWidth * 0.8}
centered={true}
onOk={this.executeCommand}
onCancel={() => {
this.setState({
assetsVisible: false
});
}}
>
<ChooseAsset
setCheckedAssets={this.setCheckedAssets}
>
</ChooseAsset>
</Modal>
<Modal title={<Text>更换资源<strong style={{color: '#1890ff'}}>{this.state.selected['name']}</strong>
</Text>}
visible={this.state.changeOwnerModalVisible}
confirmLoading={this.state.changeOwnerConfirmLoading}
onOk={() => {
this.setState({
changeOwnerConfirmLoading: true
});
let changeOwnerModalVisible = false;
this.changeOwnerFormRef
.current
.validateFields()
.then(async values => {
let result = await request.post(`/commands/${this.state.selected['id']}/change-owner?owner=${values['owner']}`);
if (result['code'] === 1) {
message.success('操作成功');
this.loadTableData();
} else {
message.error(result['message'], 10);
changeOwnerModalVisible = true;
}
})
.catch(info => {
})
.finally(() => {
this.setState({
changeOwnerConfirmLoading: false,
changeOwnerModalVisible: changeOwnerModalVisible
})
});
}}
onCancel={() => {
this.setState({
changeOwnerModalVisible: false
})
}}
>
<Form ref={this.changeOwnerFormRef}>
<Form.Item name='owner' rules={[{required: true, message: '请选择所有者'}]}>
<Select
showSearch
placeholder='请选择所有者'
onSearch={this.handleSearchByNickname}
filterOption={false}
>
{this.state.users.map(d => <Select.Option key={d.id}
value={d.id}>{d.nickname}</Select.Option>)}
</Select>
</Form.Item>
</Form>
</Modal>
</Content>
</>
);
}
}
export default DynamicCommand;

View File

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

View File

@ -1,594 +0,0 @@
import React, {Component} from 'react';
import {
Button,
Col,
Divider,
Dropdown,
Form,
Input,
Layout,
Menu,
Modal,
Row,
Select,
Space,
Table,
Tag,
Tooltip,
Typography
} from "antd";
import qs from "qs";
import CredentialModal from "./CredentialModal";
import request from "../../common/request";
import {message} from "antd/es";
import {
DeleteOutlined,
DownOutlined,
ExclamationCircleOutlined,
PlusOutlined,
SyncOutlined,
UndoOutlined
} from '@ant-design/icons';
import {hasPermission} from "../../service/permission";
import dayjs from "dayjs";
const confirm = Modal.confirm;
const {Search} = Input;
const {Title, Text} = Typography;
const {Content} = Layout;
class Credential extends Component {
inputRefOfName = React.createRef();
changeOwnerFormRef = React.createRef();
state = {
items: [],
total: 0,
queryParams: {
pageIndex: 1,
pageSize: 10
},
loading: false,
modalVisible: false,
modalTitle: '',
modalConfirmLoading: false,
model: null,
selectedRowKeys: [],
delBtnLoading: false,
changeOwnerModalVisible: false,
changeOwnerConfirmLoading: false,
users: [],
selected: {},
};
componentDidMount() {
this.loadTableData();
}
async delete(id) {
const result = await request.delete('/credentials/' + id);
if (result.code === 1) {
message.success('删除成功');
await this.loadTableData(this.state.queryParams);
} else {
message.error(result.message, 10);
}
}
async loadTableData(queryParams) {
this.setState({
loading: true
});
queryParams = queryParams || this.state.queryParams;
// queryParams
let paramsStr = qs.stringify(queryParams);
let data = {
items: [],
total: 0
};
try {
let result = await request.get('/credentials/paging?' + paramsStr);
if (result.code === 1) {
data = result.data;
} else {
message.error(result.message);
}
} catch (e) {
} finally {
const items = data.items.map(item => {
return {'key': item['id'], ...item}
})
this.setState({
items: items,
total: data.total,
queryParams: queryParams,
loading: false
});
}
}
handleChangPage = (pageIndex, pageSize) => {
let queryParams = this.state.queryParams;
queryParams.pageIndex = pageIndex;
queryParams.pageSize = pageSize;
this.setState({
queryParams: queryParams
});
this.loadTableData(queryParams)
};
handleSearchByName = name => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'name': name,
}
this.loadTableData(query);
};
showDeleteConfirm(id, content) {
let self = this;
confirm({
title: '您确定要删除此记录吗?',
content: content,
okText: '确定',
okType: 'danger',
cancelText: '取消',
onOk() {
self.delete(id).then(r => {
});
}
});
};
showModal = async (title, id = null, index) => {
let items = this.state.items;
let model = {}
if (id) {
items[index].updateBtnLoading = true;
this.setState({
items: items
});
let result = await request.get('/credentials/' + id);
if (result['code'] !== 1) {
message.error(result['message']);
items[index].updateBtnLoading = false;
this.setState({
items: items
});
return;
}
items[index].updateBtnLoading = false;
model = result['data']
}
this.setState({
modalTitle: title,
modalVisible: true,
model: model,
items: items
});
};
handleCancelModal = e => {
this.setState({
modalTitle: '',
modalVisible: false
});
};
handleOk = async (formData) => {
// 弹窗 form 传来的数据
this.setState({
modalConfirmLoading: true
});
if (formData.id) {
// 向后台提交数据
const result = await request.put('/credentials/' + formData.id, formData);
if (result.code === 1) {
message.success('操作成功', 3);
this.setState({
modalVisible: false
});
await this.loadTableData(this.state.queryParams);
} else {
message.error(result.message, 10);
}
} else {
// 向后台提交数据
const result = await request.post('/credentials', formData);
if (result.code === 1) {
message.success('操作成功', 3);
this.setState({
modalVisible: false
});
await this.loadTableData(this.state.queryParams);
} else {
message.error(result.message, 10);
}
}
this.setState({
modalConfirmLoading: false
});
};
batchDelete = async () => {
this.setState({
delBtnLoading: true
})
try {
let result = await request.delete('/credentials/' + this.state.selectedRowKeys.join(','));
if (result.code === 1) {
message.success('操作成功', 3);
this.setState({
selectedRowKeys: []
})
await this.loadTableData(this.state.queryParams);
} else {
message.error(result.message, 10);
}
} finally {
this.setState({
delBtnLoading: false
})
}
}
handleSearchByNickname = async nickname => {
const result = await request.get(`/users/paging?pageIndex=1&pageSize=100&nickname=${nickname}`);
if (result.code !== 1) {
message.error(result.message, 10);
return;
}
const items = result['data']['items'].map(item => {
return {'key': item['id'], 'disabled': false, ...item}
})
this.setState({
users: items
})
}
handleSharersChange = async targetKeys => {
this.setState({
selectedSharers: targetKeys
})
}
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, index) => {
let short = name;
if (short && short.length > 20) {
short = short.substring(0, 20) + " ...";
}
if (hasPermission(record['owner'])) {
return (
<Button type="link" size='small' loading={this.state.items[index].updateBtnLoading}
onClick={() => this.showModal('更新凭证', record.id, index)}>
<Tooltip placement="topLeft" title={name}>
{short}
</Tooltip>
</Button>
);
} else {
return (
<Tooltip placement="topLeft" title={name}>
{short}
</Tooltip>
);
}
},
sorter: true,
}, {
title: '凭证类型',
dataIndex: 'type',
key: 'type',
render: (type, record) => {
if (type === 'private-key') {
return (
<Tag color="green">密钥</Tag>
);
} else {
return (
<Tag color="red">密码</Tag>
);
}
}
}, {
title: '授权账户',
dataIndex: 'username',
key: 'username',
}, {
title: '所有者',
dataIndex: 'ownerName',
key: 'ownerName',
}, {
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) => {
const menu = (
<Menu>
<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>
<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' loading={this.state.items[index].updateBtnLoading}
disabled={!hasPermission(record['owner'])}
onClick={() => this.showModal('更新凭证', record.id, index)}>编辑</Button>
<Dropdown overlay={menu}>
<Button type="link" size='small'>
更多 <DownOutlined/>
</Button>
</Dropdown>
</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: ''})
}}>
</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}
rowKey='id'
dataSource={this.state.items}
columns={columns}
position={'both'}
pagination={{
showSizeChanger: true,
current: this.state.queryParams.pageIndex,
pageSize: this.state.queryParams.pageSize,
onChange: this.handleChangPage,
onShowSizeChange: this.handleChangPage,
total: this.state.total,
showTotal: total => `总计 ${total}`
}}
loading={this.state.loading}
onChange={this.handleTableChange}
/>
{
this.state.modalVisible ?
<CredentialModal
visible={this.state.modalVisible}
title={this.state.modalTitle}
handleOk={this.handleOk}
handleCancel={this.handleCancelModal}
confirmLoading={this.state.modalConfirmLoading}
model={this.state.model}
>
</CredentialModal>
: null
}
<Modal title={<Text>更换资源<strong style={{color: '#1890ff'}}>{this.state.selected['name']}</strong>
</Text>}
visible={this.state.changeOwnerModalVisible}
confirmLoading={this.state.changeOwnerConfirmLoading}
onOk={() => {
this.setState({
changeOwnerConfirmLoading: true
});
let changeOwnerModalVisible = false;
this.changeOwnerFormRef
.current
.validateFields()
.then(async values => {
let result = await request.post(`/credentials/${this.state.selected['id']}/change-owner?owner=${values['owner']}`);
if (result['code'] === 1) {
message.success('操作成功');
this.loadTableData();
} else {
message.error(result['message'], 10);
changeOwnerModalVisible = true;
}
})
.catch(info => {
})
.finally(() => {
this.setState({
changeOwnerConfirmLoading: false,
changeOwnerModalVisible: changeOwnerModalVisible
})
});
}}
onCancel={() => {
this.setState({
changeOwnerModalVisible: false
})
}}
>
<Form ref={this.changeOwnerFormRef}>
<Form.Item name='owner' rules={[{required: true, message: '请选择所有者'}]}>
<Select
showSearch
placeholder='请选择所有者'
onSearch={this.handleSearchByNickname}
filterOption={false}
>
{this.state.users.map(d => <Select.Option key={d.id}
value={d.id}>{d.nickname}</Select.Option>)}
</Select>
</Form.Item>
</Form>
</Modal>
</Content>
</>
);
}
}
export default Credential;

View File

@ -1,7 +1,3 @@
.text-center{
width: 100px;
text-align: center;
}
.disable-link {
pointer-events: none;
.pie-card .ant-pro-card-body {
padding: 16px !important;
}

View File

@ -1,15 +1,22 @@
import React, {Component} from 'react';
import {Card, Col, Row, Statistic} from "antd";
import {DesktopOutlined, IdcardOutlined, LinkOutlined, UserOutlined} from '@ant-design/icons';
import {DesktopOutlined, DisconnectOutlined, LoginOutlined, UserOutlined} from '@ant-design/icons';
import request from "../../common/request";
import './Dashboard.css'
import {Link} from "react-router-dom";
import {Bar, Pie} from '@ant-design/charts';
import {ProCard, StatisticCard} from '@ant-design/pro-components';
import {Line, Pie} from '@ant-design/charts';
import {Segmented} from 'antd';
class Dashboard extends Component {
state = {
counter: {},
counter: {
onlineUser: 0,
totalUser: 0,
activeAsset: 0,
totalAsset: 0,
failLoginCount: 0,
offlineSession: 0,
},
asset: {
"ssh": 0,
"rdp": 0,
@ -17,13 +24,13 @@ class Dashboard extends Component {
"telnet": 0,
"kubernetes": 0,
},
access: []
dateCounter: [],
}
componentDidMount() {
this.getCounter();
this.getAsset();
this.getAccess();
this.getDateCounter('week');
}
componentWillUnmount() {
@ -39,6 +46,15 @@ class Dashboard extends Component {
}
}
getDateCounter = async (d) => {
let result = await request.get('/overview/date-counter?d=' + d);
if (result['code'] === 1) {
this.setState({
dateCounter: result['data']
})
}
}
getAsset = async () => {
let result = await request.get('/overview/asset');
if (result['code'] === 1) {
@ -48,18 +64,17 @@ class Dashboard extends Component {
}
}
getAccess = async () => {
let result = await request.get('/overview/access');
if (result['code'] === 1) {
this.setState({
access: result['data']
})
handleChangeDateCounter = (value) => {
if(value === '按周'){
this.getDateCounter('week');
}else {
this.getDateCounter('month');
}
}
render() {
const data = [
const assetData = [
{
type: 'RDP',
value: this.state.asset['rdp'],
@ -81,9 +96,11 @@ class Dashboard extends Component {
value: this.state.asset['kubernetes'],
}
];
const config = {
const assetConfig = {
width: 200,
height: 200,
appendPadding: 10,
data: data,
data: assetData,
angleField: 'value',
colorField: 'type',
radius: 1,
@ -104,85 +121,90 @@ class Dashboard extends Component {
formatter: () => {
return '资产类型';
},
style: {
fontSize: 18,
}
},
},
};
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 dateCounterConfig = {
height: 270,
data: this.state.dateCounter,
xField: 'date',
yField: 'value',
seriesField: 'type',
legend: {
position: 'top',
},
smooth: true,
animation: {
appear: {
animation: 'path-in',
duration: 5000,
},
},
};
const accessConfig = {
data: accessData,
xField: 'value',
yField: 'title',
seriesField: 'protocol',
legend: { position: 'top-left' },
}
return (<>
<div style={{margin: 16}}>
<ProCard
title="数据概览"
// extra={dayjs().format("YYYY[年]MM[月]DD[日]") + ' 星期' + weekMapping[dayjs().day()]}
split={'horizontal'}
headerBordered
bordered
>
<ProCard split={'vertical'}>
<ProCard split="horizontal">
<ProCard split='vertical'>
<StatisticCard
statistic={{
title: '在线用户',
value: this.state.counter['onlineUser'] + '/' + this.state.counter['totalUser'],
prefix: <UserOutlined/>
}}
/>
<StatisticCard
statistic={{
title: '运行中资产',
value: this.state.counter['activeAsset'] + '/' + this.state.counter['totalAsset'],
prefix: <DesktopOutlined/>
}}
/>
</ProCard>
<ProCard split='vertical'>
<StatisticCard
statistic={{
title: '登录失败次数',
value: this.state.counter['failLoginCount'],
prefix: <LoginOutlined/>
}}
/>
<StatisticCard
statistic={{
title: '历史会话总数',
value: this.state.counter['offlineSession'],
prefix: <DisconnectOutlined/>
}}
/>
</ProCard>
</ProCard>
<ProCard className='pie-card'>
<ProCard>
<Pie {...assetConfig} />
</ProCard>
</ProCard>
</ProCard>
return (
<>
</ProCard>
<div style={{margin: 16, marginBottom: 0}}>
<Row gutter={16}>
<Col span={6}>
<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} hoverable>
<Link to={'/asset'}>
<Statistic title="资产数量" value={this.state.counter['asset']}
prefix={<DesktopOutlined/>}/>
</Link>
</Card>
</Col>
<Col span={6}>
<Card bordered={true} hoverable>
<Link to={'/credential'} hoverable>
<Statistic title="授权凭证" value={this.state.counter['credential']}
prefix={<IdcardOutlined/>}/>
</Link>
</Card>
</Col>
<Col span={6}>
<Card bordered={true} hoverable>
<Link to={'/online-session'}>
<Statistic title="在线会话" value={this.state.counter['onlineSession']}
prefix={<LinkOutlined/>}/>
</Link>
</Card>
</Col>
</Row>
</div>
<div className="page-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>
</>
);
<ProCard title="会话统计" style={{marginTop: 16}}
extra={<Segmented options={['按周', '按月']} onChange={this.handleChangeDateCounter}/>}>
<Line {...dateCounterConfig} />
</ProCard>
</div>
</>);
}
}

View File

@ -0,0 +1,3 @@
.ant-pro-card-body {
padding: 16px !important;
}

View File

@ -0,0 +1,353 @@
import React from 'react';
import {Space, Tooltip} from "antd";
import {DualAxes, Liquid} from '@ant-design/plots';
import {ProCard, StatisticCard} from '@ant-design/pro-components';
import dayjs from "dayjs";
import {renderSize} from "../../utils/utils";
import {Area} from "@ant-design/charts";
import './Monitoring.css'
import {renderWeekDay} from "../../utils/week";
import {useQuery} from "react-query";
import monitorApi from "../../api/monitor";
const {Statistic} = StatisticCard;
const renderLoad = (percent) => {
if (percent >= 0.9) {
return '堵塞';
} else if (percent >= 0.8) {
return '缓慢';
} else if (percent >= 0.7) {
return '正常';
} else {
return '流畅';
}
}
const initData = {
loadStat: {
load1: 0, load5: 0, load15: 0, percent: 0
},
mem: {
total: 0,
available: 0,
usedPercent: 0
},
cpu: {
count: 0,
usedPercent: 0,
info: [{
'modelName': ''
}]
},
disk: {
total: 0,
available: 0,
usedPercent: 0
},
diskIO: [], netIO: [], cpuStat: [], memStat: [],
}
const Monitoring = () => {
let monitorQuery = useQuery('getMonitorData', monitorApi.getData, {
initialData: initData,
refetchInterval: 5
});
let loadPercent = monitorQuery.data?.loadStat['percent'];
let loadColor = '#5B8FF9';
if (loadPercent > 0.9) {
loadColor = '#F4664A';
} else if (loadPercent > 0.8) {
loadColor = '#001D70';
} else if (loadPercent > 0.7) {
loadColor = '#0047A5';
}
const loadStatConfig = {
height: 100,
width: 100,
shape: function (x, y, width, height) {
const r = width / 4;
const dx = x - width / 2;
const dy = y - height / 2;
return [
['M', dx, dy + r * 2],
['A', r, r, 0, 0, 1, x, dy + r],
['A', r, r, 0, 0, 1, dx + width, dy + r * 2],
['L', x, dy + height],
['L', dx, dy + r * 2],
['Z'],
];
},
percent: loadPercent,
outline: {
border: 4, distance: 4,
},
wave: {
length: 64,
},
theme: {
styleSheet: {
brandColor: loadColor,
},
},
statistic: {
title: false, content: false
},
pattern: {
type: 'square',
},
};
let cpuPercent = monitorQuery.data?.cpu['usedPercent'] / 100;
let cpuColor = '#5B8FF9';
if (cpuPercent > 0.9) {
cpuColor = '#F4664A';
} else if (cpuPercent > 0.8) {
cpuColor = '#001D70';
}
const cpuStatConfig = {
height: 100,
width: 100,
shape: 'diamond',
percent: cpuPercent,
outline: {
border: 4, distance: 4,
},
wave: {
length: 64,
},
theme: {
styleSheet: {
brandColor: cpuColor,
},
},
pattern: {
type: 'line',
},
statistic: {
title: false, content: false
}
};
let memPercent = monitorQuery.data?.mem['usedPercent'] / 100;
let memColor = '#5B8FF9';
if (memPercent > 0.75) {
memColor = '#F4664A';
}
const memStatConfig = {
height: 100,
width: 100,
percent: memPercent,
outline: {
border: 4, distance: 4,
},
wave: {
length: 64,
},
theme: {
styleSheet: {
brandColor: memColor,
},
},
statistic: {
title: false, content: false
},
pattern: {
type: 'dot',
},
};
let diskPercent = monitorQuery.data?.disk['usedPercent'] / 100;
let diskColor = '#5B8FF9';
if (diskPercent > 0.9) {
diskColor = '#F4664A';
} else if (diskPercent > 0.8) {
diskColor = '#001D70';
}
const diskStatConfig = {
height: 100,
width: 100,
shape: 'rect',
percent: diskPercent,
outline: {
border: 4, distance: 4,
},
wave: {
length: 64,
},
theme: {
styleSheet: {
brandColor: diskColor,
},
},
pattern: {
type: 'line',
},
statistic: {
title: false, content: false
}
};
const diskIOConfig = {
height: 150,
data: [monitorQuery.data['diskIO'], monitorQuery.data['diskIO']],
xField: 'time',
yField: ['read', 'write'],
meta: {
read: {
alias: '读取MB/s',
}, write: {
alias: '写入MB/s'
}
},
geometryOptions: [{
geometry: 'line', color: '#5B8FF9', smooth: true,
}, {
geometry: 'line', color: '#5AD8A6', smooth: true,
},],
};
const netIOConfig = {
height: 150,
data: [monitorQuery.data['netIO'], monitorQuery.data['netIO']],
xField: 'time',
yField: ['read', 'write'],
meta: {
read: {
alias: '接收MB/s',
}, write: {
alias: '发送MB/s'
}
},
geometryOptions: [{
geometry: 'line', color: '#5B8FF9', smooth: true,
}, {
geometry: 'line', color: '#5AD8A6', smooth: true,
},],
};
const cpuConfig = {
height: 150, data: monitorQuery.data['cpuStat'], xField: 'time', yField: 'value', smooth: true, areaStyle: {
fill: '#d6e3fd',
},
};
const memConfig = {
height: 150, data: monitorQuery.data['memStat'], xField: 'time', yField: 'value', smooth: true, areaStyle: {
fill: '#d6e3fd',
},
};
const cpuModelName = monitorQuery.data['cpu']['info'][0]['modelName'].length > 10 ? monitorQuery.data['cpu']['info'][0]['modelName'].substring(0, 10) + '...' : monitorQuery.data['cpu']['info'][0]['modelName'];
return (<>
<div style={{margin: 16}}>
<ProCard
title="系统监控"
extra={dayjs().format("YYYY[年]MM[月]DD[日]") + ' ' + renderWeekDay(dayjs().day())}
split={'horizontal'}
headerBordered
bordered
>
<ProCard split={'vertical'}>
<ProCard>
<StatisticCard
statistic={{
title: '负载',
value: renderLoad(monitorQuery.data['loadStat']['percent']),
description: <Space direction="vertical" size={1}>
<Statistic title="Load1" value={monitorQuery.data['loadStat']['load1'].toFixed(2)}/>
<Statistic title="Load5" value={monitorQuery.data['loadStat']['load5'].toFixed(2)}/>
<Statistic title="Load15"
value={monitorQuery.data['loadStat']['load15'].toFixed(2)}/>
</Space>,
}}
chart={<Liquid {...loadStatConfig} />}
chartPlacement="left"
/>
<StatisticCard
statistic={{
title: 'CPU',
value: monitorQuery.data['cpu']['count'],
suffix: '个',
description: <Space direction="vertical" size={1}>
<Statistic title="利用率"
value={monitorQuery.data['cpu']['usedPercent'].toFixed(2) + '%'}/>
<Statistic title="物理核数"
value={monitorQuery.data['cpu']['phyCount'] + ' 个'}/>
<Tooltip title={monitorQuery.data['cpu']['info'][0]['modelName']}>
<Statistic title="型号" value={cpuModelName}/>
</Tooltip>
</Space>,
}}
chart={<Liquid {...cpuStatConfig} />}
chartPlacement="left"
/>
</ProCard>
<ProCard>
<StatisticCard
statistic={{
title: '内存',
value: renderSize(monitorQuery.data['mem']['total']),
description: <Space direction="vertical" size={1}>
<Statistic title="利用率"
value={monitorQuery.data['mem']['usedPercent'].toFixed(2) + '%'}/>
<Statistic title="可用的"
value={renderSize(monitorQuery.data['mem']['available'])}/>
<Statistic title="已使用" value={renderSize(monitorQuery.data['mem']['used'])}/>
</Space>,
}}
chart={<Liquid {...memStatConfig} />}
chartPlacement="left"
/>
<StatisticCard
statistic={{
title: '硬盘',
value: renderSize(monitorQuery.data['disk']['total']),
description: <Space direction="vertical" size={1}>
<Statistic title="利用率"
value={monitorQuery.data['disk']['usedPercent'].toFixed(2) + '%'}/>
<Statistic title="剩余的"
value={renderSize(monitorQuery.data['disk']['available'])}/>
<Statistic title="已使用" value={renderSize(monitorQuery.data['disk']['used'])}/>
</Space>,
}}
chart={<Liquid {...diskStatConfig} />}
chartPlacement="left"
/>
</ProCard>
</ProCard>
<ProCard split={'vertical'}>
<ProCard title="CPU负载">
<Area {...cpuConfig} />
</ProCard>
<ProCard title="内存负载">
<Area {...memConfig} />
</ProCard>
</ProCard>
<ProCard split={'vertical'}>
<ProCard title="网络吞吐">
<DualAxes onlyChangeData={true} {...netIOConfig} />
</ProCard>
<ProCard title="磁盘IO">
<DualAxes onlyChangeData={true} {...diskIOConfig} />
</ProCard>
</ProCard>
</ProCard>
</div>
</>);
}
export default Monitoring;

View File

@ -0,0 +1,201 @@
import React, {useState} from 'react';
import {Badge, Col, Divider, Layout, Row, Space, Table, Tag, Tooltip, Typography} from "antd";
import {ProTable} from "@ant-design/pro-components";
import {PROTOCOL_COLORS} from "../../common/constants";
import assetApi from "../../api/asset";
import {isEmpty} from "../../utils/utils";
import dayjs from "dayjs";
const {Title} = Typography;
const {Content} = Layout;
const actionRef = React.createRef();
const BatchCommand = () => {
let [rows, setRows] = useState([]);
const addRows = (selectedRows) => {
selectedRows.forEach(selectedRow => {
let exist = rows.some(row => {
return row.id === selectedRow.id;
});
if (exist === false) {
rows.push(selectedRow);
}
});
setRows(rows.slice());
}
const removeRows = (selectedRows) => {
selectedRows.forEach(selectedRow => {
rows = rows.filter(row => row.id !== selectedRow.id);
});
setRows(rows.slice());
}
const removeRow = (rowKey) => {
let items = rows.filter(row => row.id !== rowKey);
setRows(items.slice());
}
const columns = [{
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" text='运行中'/>
</Tooltip>
)
} else {
return (
<Tooltip title='不可用'>
<Badge status="error" text='不可用'/>
</Tooltip>
)
}
}
}, {
title: '所有者',
dataIndex: 'ownerName',
key: 'ownerName'
}, {
title: '创建时间',
dataIndex: 'created',
key: 'created',
render: (text, record) => {
return (
<Tooltip title={text}>
{dayjs(text).fromNow()}
</Tooltip>
)
}
},
];
return (<Content className="page-container">
<div style={{paddingLeft: 24, paddingRight: 24}}>
<Title level={5}>待执行资产列表</Title>
<div>
{
rows.map(item => {
return <Tag color={PROTOCOL_COLORS[item['protocol']]} closable
onClose={() => removeRow(item['id'])}
key={item['id']}>{item['name']}</Tag>
})
}
</div>
<Divider/>
</div>
<ProTable
columns={columns}
actionRef={actionRef}
rowSelection={{
// 自定义选择项参考: https://ant.design/components/table-cn/#components-table-demo-row-selection-custom
// 注释该行则默认不显示下拉选项
selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT],
}}
tableAlertRender={({selectedRowKeys, selectedRows, onCleanSelected}) => (
<Space size={24}>
<span>
已选 {selectedRowKeys.length}
</span>
<span>
<a onClick={() => addRows(selectedRows)}>
加入待执行列表
</a>
</span>
<span>
<a onClick={() => removeRows(selectedRows)}>
从待执行列表移除
</a>
</span>
</Space>
)}
request={async (params = {}, sort, filter) => {
let field = '';
let order = '';
if (Object.keys(sort).length > 0) {
field = Object.keys(sort)[0];
order = Object.values(sort)[0];
}
let queryParams = {
pageIndex: params.current,
pageSize: params.pageSize,
name: params.name,
protocol: 'ssh',
field: field,
order: order
}
let result = await assetApi.getPaging(queryParams);
return {
data: result['items'],
success: true,
total: result['total']
};
}}
rowKey="id"
search={{
labelWidth: 'auto',
}}
pagination={{
pageSize: 5,
}}
dateFormatter="string"
headerTitle="资产列表"
/>
</Content>);
};
export default BatchCommand;

View File

@ -0,0 +1,24 @@
.term-container .ant-pro-card-header{
background-color: #2c2c2c;
padding-bottom: 10px;
}
.term-container .ant-pro-card-title{
color: white !important;
}
.term-container .ant-pro-card-extra{
color: white !important;
}
.term-container .ant-pro-card-body{
padding: 10px !important;
padding-right: 0 !important;
background-color: #2D2E2C;
height: 400px;
}
.term-adder .ant-pro-card-body{
padding: 0 !important;
height: 450px;
}

View File

@ -0,0 +1,224 @@
import React, {useEffect, useState} from 'react';
import {useSearchParams} from "react-router-dom";
import commandApi from "../../api/command";
import Message from "../access/Message";
import {Input, Layout, Spin} from "antd";
import {ProCard} from "@ant-design/pro-components";
import "xterm/css/xterm.css"
import "./ExecuteCommand.css"
import sessionApi from "../../api/session";
import {Terminal} from "xterm";
import {FitAddon} from "xterm-addon-fit";
import {getToken} from "../../utils/utils";
import qs from "qs";
import {wsServer} from "../../common/env";
import {CloseOutlined} from "@ant-design/icons";
import {useQuery} from "react-query";
import {xtermScrollPretty} from "../../utils/xterm-scroll-pretty";
const {Search} = Input;
const {Content} = Layout;
const ExecuteCommand = () => {
let [sessions, setSessions] = useState([]);
const [searchParams, _] = useSearchParams();
let commandId = searchParams.get('commandId');
let commandQuery = useQuery('commandQuery', () => commandApi.getById(commandId));
let [inputValue, setInputValue] = useState(commandQuery.data?.content);
let items = JSON.parse(searchParams.get('assets'));
let [assets, setAssets] = useState(items);
let readies = {};
for (let i = 0; i < items.length; i++) {
readies[items[i].id] = false;
items[i]['locked'] = false;
}
useEffect(() => {
items.forEach(item => {
console.log(getReady(item['id']));
if (getReady(item['id']) === false) {
initTerm(item['id']);
}
})
window.addEventListener('resize', handleWindowResize);
return function cleanup() {
window.removeEventListener('resize', handleWindowResize);
sessions.forEach(session => {
if (session['ws']) {
session['ws'].close();
}
if (session['term']) {
session['term'].dispose();
}
})
}
}, [commandId]);
const handleWindowResize = () => {
sessions.forEach(session => {
session['fitAddon'].fit();
let ws = session['ws'];
if (ws && ws.readyState === WebSocket.OPEN) {
let term = session['term'];
let terminalSize = {
cols: term.cols,
rows: term.rows
}
ws.send(new Message(Message.Resize, window.btoa(JSON.stringify(terminalSize))).toString());
}
})
}
const handleInputChange = (e) => {
let value = e.target.value;
setInputValue(value);
}
const handleExecuteCommand = (value) => {
sessions.forEach(session => {
let ws = session['ws'];
if (ws.readyState === WebSocket.OPEN) {
ws.send(new Message(Message.Data, value + String.fromCharCode(13)).toString());
}
})
setInputValue('');
}
const addSession = (session) => {
sessions.push(session);
setSessions(sessions.slice());
}
const setReady = (id, ready) => {
readies[id] = ready;
}
const getReady = (id) => {
return readies[id];
}
const initTerm = async (assetId) => {
let session = await sessionApi.create(assetId, 'native');
let sessionId = session['id'];
let term = new Terminal({
fontFamily: 'monaco, Consolas, "Lucida Console", monospace', fontSize: 15, theme: {
background: '#2d2f2c'
}, rightClickSelectsWord: true,
});
const fitAddon = new FitAddon();
term.loadAddon(fitAddon);
term.open(document.getElementById(assetId));
fitAddon.fit();
term.focus();
term.writeln('Trying to connect to the server ...');
xtermScrollPretty();
let token = getToken();
let params = {
'cols': term.cols, 'rows': term.rows, 'sessionId': sessionId, 'X-Auth-Token': token
};
let paramStr = qs.stringify(params);
let webSocket = new WebSocket(`${wsServer}/sessions/${sessionId}/ssh?${paramStr}`);
term.onData(data => {
if (webSocket) {
webSocket.send(new Message(Message.Data, data).toString());
}
});
webSocket.onerror = (e) => {
term.writeln("Failed to connect to server.");
}
webSocket.onclose = (e) => {
term.writeln("Connection is closed.");
}
webSocket.onmessage = (e) => {
let msg = Message.parse(e.data);
switch (msg['type']) {
case Message.Connected:
term.clear();
sessionApi.connect(sessionId);
break;
case Message.Data:
term.write(msg['content']);
break;
case Message.Closed:
term.writeln(`\x1B[1;3;31m${msg['content']}\x1B[0m `)
webSocket.close();
break;
default:
break;
}
}
addSession({'id': assetId, 'ws': webSocket, 'term': term, 'fitAddon': fitAddon});
setReady(assetId, true);
}
const handleRemoveTerm = (id) => {
let session = sessions.find(item => item.id === id);
session.ws.close();
session.term.dispose();
let result = assets.filter(item => item.id !== id);
setAssets(result);
}
return (
<div>
<Content className="page-container">
<div className="page-search">
<Search placeholder="请输入指令" value={inputValue} onChange={handleInputChange}
onSearch={handleExecuteCommand} enterButton='执行'/>
</div>
</Content>
<Spin spinning={commandQuery.isLoading} tip='正在获取指令内容...'>
<div className="page-card">
<ProCard ghost gutter={[8, 8]} wrap>
{assets.map(item => {
return <ProCard
className={'term-container'}
key={item['id']}
extra={<div style={{cursor: 'pointer'}} onClick={() => handleRemoveTerm(item['id'])}>
<CloseOutlined/></div>}
title={item['name']}
layout="center"
headerBordered
size={'small'}
colSpan={12}
bordered>
<div id={item['id']} style={{width: '100%', height: '100%'}}/>
</ProCard>
})}
{/*<ProCard*/}
{/* className={'term-adder'}*/}
{/* layout="center"*/}
{/* colSpan={12}*/}
{/* bordered>*/}
{/* <Button type="dashed" style={{width: '100%', height: '100%'}} icon={<PlusOutlined />}/>*/}
{/*</ProCard>*/}
</ProCard>
</div>
</Spin>
</div>
);
};
export default ExecuteCommand;

View File

@ -1,4 +1,4 @@
import React, {Component} from 'react';
import React, {Component, lazy, Suspense} from 'react';
import {
Button,
Card,
@ -37,7 +37,9 @@ 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";
import Landing from "../Landing";
const MonacoEditor = lazy(() => import('react-monaco-editor'));
const {Text} = Typography;
const confirm = Modal.confirm;
@ -73,6 +75,10 @@ class FileSystem extends Component {
if (this.props.onRef) {
this.props.onRef(this);
}
if (!this.props.storageId) {
return
}
this.setState({
storageId: this.props.storageId,
storageType: this.props.storageType,
@ -126,12 +132,32 @@ class FileSystem extends Component {
return {'key': item['path'], ...item}
});
const sortByName = (a, b) => {
let a1 = a['name'].toUpperCase();
let a2 = b['name'].toUpperCase();
if (a1 < a2) {
return -1;
}
if (a1 > a2) {
return 1;
}
return 0;
}
let dirs = items.filter(item => item['isDir'] === true);
dirs.sort(sortByName);
let files = items.filter(item => item['isDir'] === false);
files.sort(sortByName);
dirs.push(...files);
if (key !== '/') {
items.splice(0, 0, {key: '..', name: '..', path: '..', isDir: true, disabled: true})
dirs.splice(0, 0, {key: '..', name: '..', path: '..', isDir: true, disabled: true})
}
this.setState({
files: items,
files: dirs,
currentDirectory: key,
currentDirectoryInput: key
})
@ -618,7 +644,7 @@ class FileSystem extends Component {
let rowKeys = this.state.selectedRowKeys;
const content = <div>
您确定要删除选中的<Text style={{color: '#1890FF'}}
strong>{rowKeys.length}</Text>
strong>{rowKeys.length}</Text>
</div>;
confirm({
icon: <ExclamationCircleOutlined/>,
@ -808,27 +834,31 @@ class FileSystem extends Component {
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
}
)
}}
/>
<Suspense fallback={<Landing/>}>
<MonacoEditor
language="javascript"
height={window.innerHeight * 0.8}
theme="vs-dark"
value={this.state.fileContent}
options={{
selectOnLineNumbers: true
}}
editorDidMount={(editor, monaco) => {
editor.focus();
}}
editorWillUnmount={() => {
}}
onChange={(newValue, e) => {
this.setState(
{
fileContent: newValue
}
)
}}
/>
</Suspense>
</Modal>
</div>
);

View File

@ -1,315 +1,58 @@
import React, {Component} from 'react';
import {
Button,
Col,
Divider,
Dropdown,
Input,
Layout,
Menu,
Modal,
Row,
Space,
Spin,
Switch,
Table,
Tag,
Tooltip,
Typography
} from "antd";
import qs from "qs";
import request from "../../common/request";
import {message} from "antd/es";
import {
DeleteOutlined,
DownOutlined,
ExclamationCircleOutlined,
PlusOutlined,
SyncOutlined,
UndoOutlined
} from '@ant-design/icons';
import dayjs from "dayjs";
import JobModal from "./JobModal";
import React, {useState} from 'react';
import './Job.css'
import {Button, Layout, message, Popconfirm, Switch, Tag, Tooltip} from "antd";
import {ProTable} from "@ant-design/pro-components";
import jobApi from "../../api/job";
import JobModal from "./JobModal";
import dayjs from "dayjs";
import JobLog from "./JobLog";
import ColumnState, {useColumnState} from "../../hook/column-state";
import Show from "../../dd/fi/show";
import {hasMenu} from "../../service/permission";
const confirm = Modal.confirm;
const {Content} = Layout;
const {Title, Text} = Typography;
const {Search} = Input;
class Job extends Component {
const actionRef = React.createRef();
inputRefOfName = React.createRef();
const api = jobApi;
state = {
items: [],
total: 0,
queryParams: {
pageIndex: 1,
pageSize: 10
const Job = () => {
let [visible, setVisible] = useState(false);
let [confirmLoading, setConfirmLoading] = useState(false);
let [selectedRowKey, setSelectedRowKey] = useState(undefined);
let [logVisible, setLogVisible] = useState(false);
let [execLoading, setExecLoading] = useState([]);
const [columnsStateMap, setColumnsStateMap] = useColumnState(ColumnState.JOB);
const columns = [
{
dataIndex: 'index',
valueType: 'indexBorder',
width: 48,
},
loading: false,
modalVisible: false,
modalTitle: '',
modalConfirmLoading: false,
selectedRow: undefined,
selectedRowKeys: [],
logPending: false,
logs: []
};
componentDidMount() {
this.loadTableData();
}
async delete(id) {
const result = await request.delete('/jobs/' + 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('/jobs/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 = async (title, obj = null) => {
if (obj['id']) {
let result = await request.get(`/jobs/${obj['id']}`);
if (result.code !== 1) {
message.error(result.message);
return;
}
obj = result.data;
}
if (obj['func'] === 'shell-job') {
obj['shell'] = JSON.parse(obj['metadata'])['shell'];
}
if (obj['mode'] === 'custom') {
obj['resourceIds'] = obj['resourceIds'].split(',');
}
this.setState({
modalTitle: title,
modalVisible: true,
model: obj
});
};
handleCancelModal = e => {
this.setState({
modalTitle: '',
modalVisible: false
});
};
handleOk = async (formData) => {
// 弹窗 form 传来的数据
this.setState({
modalConfirmLoading: true
});
console.log(formData)
if (formData['func'] === 'shell-job') {
console.log(formData['shell'], JSON.stringify({'shell': formData['shell']}))
formData['metadata'] = JSON.stringify({'shell': formData['shell']});
formData['shell'] = undefined;
}
if (formData['mode'] === 'custom') {
let resourceIds = formData['resourceIds'];
formData['resourceIds'] = resourceIds.join(',');
}
if (formData.id) {
// 向后台提交数据
const result = await request.put('/jobs/' + 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('/jobs', 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('/jobs/' + 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) + " ...";
}
return (
<Tooltip placement="topLeft" title={name}>
{short}
</Tooltip>
);
},
sorter: true,
}, {
}
, {
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status, record) => {
return <Switch checkedChildren="开启" unCheckedChildren="关闭" checked={status === 'running'}
onChange={async (checked) => {
let jobStatus = checked ? 'running' : 'not-running';
let result = await request.post(`/jobs/${record['id']}/change-status?status=${jobStatus}`);
if (result['code'] === 1) {
message.success('操作成功');
await this.loadTableData();
} else {
message.error(result['message']);
}
}}
hideInSearch: true,
render: (status, record, index) => {
return <Switch disabled={!hasMenu('job-change-status')} checkedChildren="开启" unCheckedChildren="关闭" checked={status === 'running'}
onChange={(checked) => handleChangeStatus(record['id'], checked ? 'running' : 'not-running', index)}
/>
}
}, {
title: '任务类型',
dataIndex: 'func',
key: 'func',
hideInSearch: true,
render: (func, record) => {
switch (func) {
case "check-asset-status-job":
@ -323,11 +66,13 @@ class Job extends Component {
}, {
title: 'cron表达式',
dataIndex: 'cron',
key: 'cron'
key: 'cron',
hideInSearch: true,
}, {
title: '创建日期',
dataIndex: 'created',
key: 'created',
hideInSearch: true,
render: (text, record) => {
return (
<Tooltip title={text}>
@ -340,9 +85,10 @@ class Job extends Component {
title: '最后执行日期',
dataIndex: 'updated',
key: 'updated',
hideInSearch: true,
render: (text, record) => {
if (text === '0001-01-01 00:00:00') {
return '';
return '-';
}
return (
<Tooltip title={text}>
@ -351,246 +97,183 @@ class Job extends Component {
)
},
sorter: true,
}, {
},
{
title: '操作',
key: 'action',
render: (text, record, index) => {
const menu = (
<Menu>
<Menu.Item key="0">
<Button type="text" size='small'
onClick={() => this.showModal('更新计划任务', record)}>编辑</Button>
</Menu.Item>
<Menu.Item key="2">
<Button type="text" size='small'
onClick={async () => {
this.setState({
logVisible: true,
logPending: true
})
let result = await request.get(`/jobs/${record['id']}/logs`);
if (result['code'] === 1) {
this.setState({
logPending: false,
logs: result['data'],
selectedRow: record
})
}
}}>日志</Button>
</Menu.Item>
<Menu.Divider/>
<Menu.Item key="3">
<Button type="text" size='small' danger
onClick={() => this.showDeleteConfirm(record.id, record.name)}>删除</Button>
</Menu.Item>
</Menu>
);
return (
<div>
<Button type="link" size='small' loading={this.state.items[index]['execLoading']}
onClick={async () => {
let items = this.state.items;
items[index]['execLoading'] = true;
this.setState({
items: items
});
let result = await request.post(`/jobs/${record['id']}/exec`);
if (result['code'] === 1) {
message.success('执行成功');
await this.loadTableData();
} else {
message.error(result['message']);
items[index]['execLoading'] = false;
this.setState({
items: items
});
}
}}>执行</Button>
<Dropdown overlay={menu}>
<Button type="link" size='small'>
更多 <DownOutlined/>
</Button>
</Dropdown>
</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}`
valueType: 'option',
key: 'option',
render: (text, record, index, action) => [
<Show menu={'job-run'} key={'job-run'}>
<a
key="exec"
disabled={execLoading[index]}
onClick={() => handleExec(record['id'], index)}
>
执行
</a>
</Show>,
<Show menu={'job-log'} key={'job-log'}>
<a
key="logs"
onClick={() => handleShowLog(record['id'])}
>
日志
</a>
</Show>,
<Show menu={'job-edit'} key={'job-edit'}>
<a
key="edit"
onClick={() => {
setVisible(true);
setSelectedRowKey(record['id']);
}}
loading={this.state.loading}
onChange={this.handleTableChange}
/>
>
编辑
</a>
</Show>,
<Show menu={'job-del'} key={'job-del'}>
<Popconfirm
key={'confirm-delete'}
title="您确认要删除此行吗?"
onConfirm={async () => {
await api.deleteById(record.id);
actionRef.current.reload();
}}
okText="确认"
cancelText="取消"
>
<a key='delete' className='danger'>删除</a>
</Popconfirm>
</Show>,
],
},
];
{
this.state.modalVisible ?
<JobModal
visible={this.state.modalVisible}
title={this.state.modalTitle}
handleOk={this.handleOk}
handleCancel={this.handleCancelModal}
confirmLoading={this.state.modalConfirmLoading}
model={this.state.model}
>
</JobModal> : undefined
}
{
this.state.logVisible ?
<Modal
className='modal-no-padding'
width={window.innerWidth * 0.8}
title={'日志'}
visible={true}
maskClosable={false}
onOk={async () => {
let result = await request.delete(`/jobs/${this.state.selectedRow['id']}/logs`);
if (result['code'] === 1) {
this.setState({
logVisible: false,
selectedRow: undefined
})
message.success('日志清空成功');
} else {
message.error(result['message'], 10);
}
}}
onCancel={() => {
this.setState({
logVisible: false,
selectedRow: undefined
})
}}
okText='清空'
okType={'danger'}
cancelText='取消'
>
<Spin tip='加载中...' spinning={this.state.logPending}>
<pre className='cron-log'>
{
this.state.logs.map(item => {
return <><Divider
orientation="left"
style={{color: 'white'}}>{item['timestamp']}</Divider>{item['message']}</>;
})
}
</pre>
</Spin>
</Modal> : undefined
}
</Content>
</>
);
const handleChangeStatus = async (id, status, index) => {
await api.changeStatus(id, status);
actionRef.current.reload();
}
const handleExec = async (id, index) => {
message.loading({content: '正在执行...', key: id, duration: 30});
execLoading[index] = true;
setExecLoading(execLoading.slice());
await api.exec(id);
message.success({content: '执行成功', key: id});
execLoading[index] = false;
setExecLoading(execLoading.slice());
actionRef.current.reload();
}
const handleShowLog = (id) => {
setLogVisible(true);
setSelectedRowKey(id);
}
return (
<div>
<Content className="page-container">
<ProTable
columns={columns}
actionRef={actionRef}
columnsState={{
value: columnsStateMap,
onChange: setColumnsStateMap
}}
request={async (params = {}, sort, filter) => {
let field = '';
let order = '';
if (Object.keys(sort).length > 0) {
field = Object.keys(sort)[0];
order = Object.values(sort)[0];
}
let queryParams = {
pageIndex: params.current,
pageSize: params.pageSize,
name: params.name,
field: field,
order: order
}
let result = await api.getPaging(queryParams);
let items = result['items'];
for (let i = 0; i < items.length; i++) {
execLoading.push(false);
}
setExecLoading(execLoading.slice());
return {
data: items,
success: true,
total: result['total']
};
}}
rowKey="id"
search={{
labelWidth: 'auto',
}}
pagination={{
pageSize: 10,
}}
dateFormatter="string"
headerTitle="计划任务列表"
toolBarRender={() => [
<Show menu={'job-add'}>
<Button key="button" type="primary" onClick={() => {
setVisible(true)
}}>
新建
</Button>
</Show>,
]}
/>
<JobModal
id={selectedRowKey}
visible={visible}
confirmLoading={confirmLoading}
handleCancel={() => {
setVisible(false);
setSelectedRowKey(undefined);
}}
handleOk={async (values) => {
setConfirmLoading(true);
try {
let success;
if (values['id']) {
success = await api.updateById(values['id'], values);
} else {
success = await api.create(values);
}
if (success) {
setVisible(false);
}
actionRef.current.reload();
} finally {
setConfirmLoading(false);
}
}}
/>
<JobLog
id={selectedRowKey}
visible={logVisible}
handleCancel={() => {
setLogVisible(false);
setSelectedRowKey(undefined);
}}
>
</JobLog>
</Content>
</div>
);
}
export default Job;

View File

@ -0,0 +1,108 @@
import React, {useState} from 'react';
import {Button, Drawer} from "antd";
import {ProTable} from "@ant-design/pro-components";
import jobApi from "../../api/job";
const actionRef = React.createRef();
const JobLog = ({
visible,
handleCancel,
id,
}) => {
let [loading, setLoading] = useState(false);
const columns = [
{
dataIndex: 'index',
valueType: 'indexBorder',
width: 48,
},
{
title: '执行时间',
dataIndex: 'timestamp',
key: 'timestamp',
hideInSearch: true,
sorter: true,
},
{
title: '日志',
dataIndex: 'message',
key: 'message',
hideInSearch: true,
valueType: 'code',
}
]
return (
<div>
<Drawer
title={'计划任务日志'}
placement="right"
width={window.innerWidth * 0.9}
closable={true}
maskClosable={true}
onClose={handleCancel}
visible={visible}
>
{visible ?
<ProTable
columns={columns}
actionRef={actionRef}
request={async (params = {}, sort, filter) => {
let field = '';
let order = '';
if (Object.keys(sort).length > 0) {
field = Object.keys(sort)[0];
order = Object.values(sort)[0];
}
let queryParams = {
pageIndex: params.current,
pageSize: params.pageSize,
name: params.name,
field: field,
order: order
}
let result = await jobApi.getLogPaging(id, queryParams);
let items = result['items'];
return {
data: items,
success: true,
total: result['total']
};
}}
rowKey="id"
search={false}
pagination={{
pageSize: 5,
}}
dateFormatter="string"
headerTitle="计划任务日志"
toolBarRender={() => [
<Button
key="button"
type="primary"
loading={loading}
danger
onClick={async () => {
setLoading(true);
await jobApi.deleteLogByJobId(id);
actionRef.current.reload();
setLoading(false);
}}
>
清空
</Button>,
]}
/> : undefined}
</Drawer>
</div>
);
};
export default JobLog;

View File

@ -1,41 +1,54 @@
import React, {useEffect, useState} from 'react';
import {Form, Input, Modal, Radio, Select, Spin} from "antd/lib/index";
import TextArea from "antd/es/input/TextArea";
import request from "../../common/request";
import {message} from "antd";
import {Form, Input, Modal, Radio, Select, Spin} from "antd";
import jobApi from "../../api/job";
import assetApi from "../../api/asset";
const JobModal = ({title, visible, handleOk, handleCancel, confirmLoading, model}) => {
const {TextArea} = Input;
const JobModal = ({
visible,
handleOk,
handleCancel,
confirmLoading,
id,
}) => {
const [form] = Form.useForm();
if (model.func === undefined) {
model.func = 'shell-job';
}
if (model.mode === undefined) {
model.mode = 'all';
}
let [func, setFunc] = useState(model.func);
let [mode, setMode] = useState(model.mode);
let [func, setFunc] = useState('shell-job');
let [mode, setMode] = useState('all');
let [resources, setResources] = useState([]);
let [resourcesLoading, setResourcesLoading] = useState(false);
useEffect(() => {
const fetchData = async () => {
setResourcesLoading(true);
let result = await request.get('/assets?protocol=ssh');
if (result['code'] === 1) {
setResources(result['data']);
} else {
message.error(result['message'], 10);
}
let result = await assetApi.GetAll('ssh');
setResources(result);
setResourcesLoading(false);
};
fetchData();
}, []);
const getItem = async () => {
let data = await jobApi.getById(id);
if (data) {
form.setFieldsValue(data);
setMode(data['mode']);
setFunc(data['func']);
}
}
if (visible && id) {
getItem();
} else {
form.setFieldsValue({
func: 'shell-job',
mode: 'all',
});
}
let [resourcesLoading, setResourcesLoading] = useState(false);
}, [visible]);
const formItemLayout = {
labelCol: {span: 6},
@ -44,27 +57,30 @@ const JobModal = ({title, visible, handleOk, handleCancel, confirmLoading, model
return (
<Modal
title={title}
title={id ? '更新计划任务' : '新建计划任务'}
visible={visible}
maskClosable={false}
destroyOnClose={true}
onOk={() => {
form
.validateFields()
.then(values => {
form.resetFields();
handleOk(values);
})
.catch(info => {
.then(async values => {
let ok = await handleOk(values);
if (ok) {
form.resetFields();
}
});
}}
onCancel={handleCancel}
onCancel={() => {
form.resetFields();
handleCancel();
}}
confirmLoading={confirmLoading}
okText='确定'
cancelText='取消'
>
<Form form={form} {...formItemLayout} initialValues={model}>
<Form form={form} {...formItemLayout}>
<Form.Item name='id' noStyle>
<Input hidden={true}/>
</Form.Item>

View File

@ -1,425 +0,0 @@
import React, {Component} from 'react';
import {
Button,
Col,
Divider,
Input,
Layout,
Modal,
notification,
Row,
Select,
Space,
Table,
Tag,
Tooltip,
Typography
} from "antd";
import qs from "qs";
import request from "../../common/request";
import {formatDate, isEmpty} from "../../utils/utils";
import {message} from "antd/es";
import {ClearOutlined, DeleteOutlined, ExclamationCircleOutlined, SyncOutlined, UndoOutlined} from "@ant-design/icons";
const confirm = Modal.confirm;
const {Content} = Layout;
const {Search} = Input;
const {Title, Text} = Typography;
class LoginLog extends Component {
inputRefOfClientIp = React.createRef();
state = {
items: [],
total: 0,
queryParams: {
pageIndex: 1,
pageSize: 10,
userId: undefined,
},
loading: false,
selectedRowKeys: [],
delBtnLoading: false,
users: [],
};
componentDidMount() {
this.loadTableData();
}
async loadTableData(queryParams) {
queryParams = queryParams || this.state.queryParams;
this.setState({
queryParams: queryParams,
loading: true
});
// queryParams
let paramsStr = qs.stringify(queryParams);
let data = {
items: [],
total: 0
};
try {
let result = await request.get('/login-logs/paging?' + paramsStr);
if (result.code === 1) {
data = result.data;
} else {
message.error(result.message);
}
} catch (e) {
} finally {
const items = data.items.map(item => {
return {'key': item['id'], ...item}
})
this.setState({
items: items,
total: data.total,
queryParams: queryParams,
loading: false
});
}
}
handleChangPage = (pageIndex, pageSize) => {
let queryParams = this.state.queryParams;
queryParams.pageIndex = pageIndex;
queryParams.pageSize = pageSize;
this.setState({
queryParams: queryParams
});
this.loadTableData(queryParams)
};
handleSearchByClientIp = clientIp => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'clientIp': clientIp,
}
this.loadTableData(query);
}
handleSearchByUsername = username => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'username': username,
}
this.loadTableData(query);
}
handleChangeByState = (state) => {
let query = {
...this.state.queryParams,
'pageIndex': 1,
'pageSize': this.state.queryParams.pageSize,
'state': state,
}
this.loadTableData(query);
}
batchDelete = async () => {
this.setState({
delBtnLoading: true
})
try {
let result = await request.delete('/login-logs/' + 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
})
}
}
clearLoginLogs = async () => {
this.setState({
clearBtnLoading: true
})
try {
let result = await request.post('/login-logs/clear');
if (result.code === 1) {
message.success('操作成功,即将跳转至登录页面。', 3);
this.setState({
selectedRowKeys: []
})
setTimeout(function () {
window.location.reload();
}, 3000);
} else {
message.error(result.message, 10);
}
} finally {
this.setState({
clearBtnLoading: false
})
}
}
render() {
const columns = [{
title: '序号',
dataIndex: 'id',
key: 'id',
render: (id, record, index) => {
return index + 1;
}
}, {
title: '登录账号',
dataIndex: 'username',
key: 'username'
}, {
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',
key: 'clientUserAgent',
render: (text, record) => {
if (isEmpty(text)) {
return '未知';
}
return (
<Tooltip placement="topLeft" title={text}>
{text.split(' ')[0]}
</Tooltip>
)
}
}, {
title: '登录时间',
dataIndex: 'loginTime',
key: 'loginTime',
render: (text, record) => {
return formatDate(text, 'yyyy-MM-dd hh:mm:ss');
}
}, {
title: '注销时间',
dataIndex: 'logoutTime',
key: 'logoutTime',
render: (text, record) => {
if (isEmpty(text) || text === '0001-01-01 00:00:00') {
return '';
}
return text;
}
},
{
title: '操作',
key: 'action',
render: (text, record) => {
return (
<div>
<Button type="link" size='small' onClick={() => {
confirm({
title: '您确定要删除此条登录日志吗?',
content: '删除用户未注销的登录日志将会强制用户下线',
okText: '确定',
okType: 'danger',
cancelText: '取消',
onOk() {
del(record.id)
}
});
const del = async (id) => {
const result = await request.delete(`/login-logs/${id}`);
if (result.code === 1) {
notification['success']({
message: '提示',
description: '删除成功',
});
this.loadTableData();
} else {
notification['error']({
message: '提示',
description: result.message,
});
}
}
}}>删除</Button>
</div>
)
},
}
];
const selectedRowKeys = this.state.selectedRowKeys;
const rowSelection = {
selectedRowKeys: this.state.selectedRowKeys,
onChange: (selectedRowKeys, selectedRows) => {
this.setState({selectedRowKeys});
},
};
const hasSelected = selectedRowKeys.length > 0;
return (
<>
<Content className="site-layout-background page-content">
<div style={{marginBottom: 20}}>
<Row justify="space-around" align="middle" gutter={24}>
<Col span={8} key={1}>
<Title level={3}>登录日志列表</Title>
</Col>
<Col span={16} key={2} style={{textAlign: 'right'}}>
<Space>
<Search
ref={this.inputRefOfClientIp}
placeholder="登录账号"
allowClear
onSearch={this.handleSearchByUsername}
/>
<Search
ref={this.inputRefOfClientIp}
placeholder="登录IP"
allowClear
onSearch={this.handleSearchByClientIp}
/>
<Select
style={{width: 100}}
placeholder='用户昵称'
onChange={this.handleChangeByState}
defaultValue={''}
>
<Select.Option value=''>全部状态</Select.Option>
<Select.Option value='1'>只看成功</Select.Option>
<Select.Option value='0'>只看失败</Select.Option>
</Select>
<Tooltip title='重置查询'>
<Button icon={<UndoOutlined/>} onClick={() => {
this.inputRefOfClientIp.current.setValue('');
this.loadTableData({
pageIndex: 1,
pageSize: 10,
protocol: '',
userId: undefined,
assetId: undefined
})
}}>
</Button>
</Tooltip>
<Divider type="vertical"/>
<Tooltip title="刷新列表">
<Button icon={<SyncOutlined/>} onClick={() => {
this.loadTableData(this.state.queryParams)
}}>
</Button>
</Tooltip>
<Tooltip title="批量删除">
<Button type="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/>,
title: content,
content: '删除用户未注销的登录日志将会强制用户下线',
onOk: () => {
this.batchDelete()
},
onCancel() {
},
});
}}>
</Button>
</Tooltip>
<Tooltip title="清空">
<Button type="primary" danger icon={<ClearOutlined/>}
loading={this.state.clearBtnLoading}
onClick={() => {
const title = <Text style={{color: 'red'}}
strong>您确定要清空全部的登录日志吗</Text>;
confirm({
icon: <ExclamationCircleOutlined/>,
title: title,
content: '删除用户未注销的登录日志将会强制用户下线,当前登录的用户也会退出登录。',
okType: 'danger',
onOk: this.clearLoginLogs,
onCancel() {
},
});
}}>
</Button>
</Tooltip>
</Space>
</Col>
</Row>
</div>
<Table rowSelection={rowSelection}
dataSource={this.state.items}
columns={columns}
position={'both'}
pagination={{
showSizeChanger: true,
current: this.state.queryParams.pageIndex,
pageSize: this.state.queryParams.pageSize,
onChange: this.handleChangPage,
total: this.state.total,
showTotal: total => `总计 ${total}`
}}
loading={this.state.loading}
/>
</Content>
</>
);
}
}
export default LoginLog;

View File

@ -1,382 +0,0 @@
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 SecurityModal from "./SecurityModal";
const confirm = Modal.confirm;
const {Content} = Layout;
const {Title, Text} = Typography;
const {Search} = Input;
class Security 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('/securities/' + 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('/securities/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 = null) {
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('/securities/' + 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('/securities', 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('/securities/' + 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: 'IP',
dataIndex: 'ip',
key: 'ip',
sorter: true,
}, {
title: '规则',
dataIndex: 'rule',
key: 'rule',
render: (rule) => {
if (rule === 'allow') {
return <Tag color={'green'}>允许</Tag>
} else {
return <Tag color={'red'}>禁止</Tag>
}
}
}, {
title: '优先级',
dataIndex: 'priority',
key: 'priority',
sorter: true,
}, {
title: '来源',
dataIndex: 'source',
key: 'source',
}, {
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}>IP访问规则列表</Title>
</Col>
<Col span={12} key={2} style={{textAlign: 'right'}}>
<Space>
<Search
ref={this.inputRefOfName}
placeholder="IP"
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 ?
<SecurityModal
visible={this.state.modalVisible}
title={this.state.modalTitle}
handleOk={this.handleOk}
handleCancel={this.handleCancelModal}
confirmLoading={this.state.modalConfirmLoading}
model={this.state.model}
>
</SecurityModal> : undefined
}
</Content>
</>
);
}
}
export default Security;

View File

@ -1,66 +0,0 @@
import React from 'react';
import {Form, Input, InputNumber, Modal, Radio} from "antd/lib/index";
const formItemLayout = {
labelCol: {span: 6},
wrapperCol: {span: 14},
};
const SecurityModal = ({title, visible, handleOk, handleCancel, confirmLoading, model}) => {
const [form] = Form.useForm();
if (model['priority'] === undefined) {
model['priority'] = 1;
}
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="IP地址" name='ip' rules={[{required: true, message: '请输入IP地址'}]}>
<Input autoComplete="off" placeholder="支持单个IPCIDR或使用-连接的两个IP"/>
</Form.Item>
<Form.Item label="规则" name='rule' rules={[{required: true, message: '请选择规则'}]}>
<Radio.Group onChange={async (e) => {
}}>
<Radio value={'allow'}>允许</Radio>
<Radio value={'reject'}>拒绝</Radio>
</Radio.Group>
</Form.Item>
<Form.Item label="优先级" name='priority' rules={[{required: true, message: '请输入优先级'}]} tooltip='数字越小代表优先级越高'>
<InputNumber min={1} max={100}/>
</Form.Item>
</Form>
</Modal>
)
};
export default SecurityModal;

View File

@ -1,301 +1,208 @@
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 React, {useState} from 'react';
import {Button, Drawer, Layout, Popconfirm, Tag} from "antd";
import {ProTable} from "@ant-design/pro-components";
import storageApi from "../../api/storage";
import StorageModal from "./StorageModal";
import {renderSize} from "../../utils/utils";
import FileSystem from "./FileSystem";
import ColumnState, {useColumnState} from "../../hook/column-state";
import Show from "../../dd/fi/show";
const api = storageApi;
const {Content} = Layout;
const {Title} = Typography;
const {Search} = Input;
class Storage extends Component {
const actionRef = React.createRef();
inputRefOfName = React.createRef();
storageRef = undefined;
const Storage = () => {
let [visible, setVisible] = useState(false);
let [confirmLoading, setConfirmLoading] = useState(false);
let [selectedRowKey, setSelectedRowKey] = useState(undefined);
state = {
items: [],
total: 0,
queryParams: {
pageIndex: 1,
pageSize: 10
let [fileSystemVisible, setFileSystemVisible] = useState(false);
const [columnsStateMap, setColumnsStateMap] = useColumnState(ColumnState.STORAGE);
const columns = [
{
dataIndex: 'index',
valueType: 'indexBorder',
width: 48,
},
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);
{
title: '名称',
dataIndex: 'name',
key: 'name',
}, {
title: '是否共享',
dataIndex: 'isShare',
key: 'isShare',
hideInSearch: true,
render: (isShare) => {
if (isShare) {
return <Tag color={'green'}></Tag>
} else {
return <Tag color={'red'}></Tag>
}
}
} 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);
}, {
title: '是否默认',
dataIndex: 'isDefault',
key: 'isDefault',
hideInSearch: true,
render: (isDefault) => {
if (isDefault) {
return <Tag color={'green'}></Tag>
} else {
return <Tag color={'red'}></Tag>
}
}
} 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>
)
}, {
title: '大小限制',
dataIndex: 'limitSize',
key: 'limitSize',
hideInSearch: true,
render: (text => {
return text < 0 ? '无限制' : renderSize(text);
})
}, {
title: '已用大小',
dataIndex: 'usedSize',
key: 'usedSize',
hideInSearch: true,
render: (text => {
return renderSize(text);
})
}, {
title: '所属用户',
dataIndex: 'ownerName',
key: 'ownerName',
hideInSearch: true,
},
{
title: '操作',
valueType: 'option',
key: 'option',
render: (text, record, _, action) => [
<Show menu={'storage-browse'} key={'storage-browse'}>
<a
key="edit"
onClick={() => {
setFileSystemVisible(true);
setSelectedRowKey(record['id']);
}}
/>
</div>
>
浏览
</a>
</Show>,
<Show menu={'storage-edit'} key={'storage-edit'}>
<a
key="edit"
onClick={() => {
setVisible(true);
setSelectedRowKey(record['id']);
}}
>
编辑
</a>
</Show>,
<Show menu={'storage-del'} key={'storage-del'}>
<Popconfirm
key={'confirm-delete'}
title="您确认要删除此行吗?"
onConfirm={async () => {
await api.deleteById(record.id);
actionRef.current.reload();
}}
okText="确认"
cancelText="取消"
>
<a key='delete' disabled={record['isDefault']} className='danger'>删除</a>
</Popconfirm>
</Show>,
],
},
];
return (
<div>
<Content className="page-container">
<ProTable
columns={columns}
actionRef={actionRef}
columnsState={{
value: columnsStateMap,
onChange: setColumnsStateMap
}}
request={async (params = {}, sort, filter) => {
let field = '';
let order = '';
if (Object.keys(sort).length > 0) {
field = Object.keys(sort)[0];
order = Object.values(sort)[0];
}
let queryParams = {
pageIndex: params.current,
pageSize: params.pageSize,
name: params.name,
field: field,
order: order
}
let result = await api.getPaging(queryParams);
return {
data: result['items'],
success: true,
total: result['total']
};
}}
rowKey="id"
search={{
labelWidth: 'auto',
}}
pagination={{
pageSize: 10,
}}
dateFormatter="string"
headerTitle="磁盘空间列表"
toolBarRender={() => [
<Show menu={'storage-add'}>
<Button key="button" type="primary" onClick={() => {
setVisible(true)
}}>
新建
</Button>
</Show>,
]}
/>
<StorageModal
id={selectedRowKey}
visible={visible}
confirmLoading={confirmLoading}
handleCancel={() => {
setVisible(false);
setSelectedRowKey(undefined);
}}
handleOk={async (values) => {
setConfirmLoading(true);
try {
let success;
if (values['id']) {
success = await api.updateById(values['id'], values);
} else {
success = await api.create(values);
}
if (success) {
setVisible(false);
}
actionRef.current.reload();
} finally {
setConfirmLoading(false);
}
}}
/>
<Drawer
title={'文件管理'}
@ -304,45 +211,29 @@ class Storage extends Component {
closable={true}
maskClosable={true}
onClose={() => {
this.setState({
fileSystemVisible: false
});
this.loadTableData(this.state.queryParams);
setFileSystemVisible(false);
setSelectedRowKey(undefined);
actionRef.current.reload();
}}
visible={this.state.fileSystemVisible}
visible={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>
{fileSystemVisible ?
<FileSystem
storageId={selectedRowKey}
storageType={'storages'}
upload={true}
download={true}
delete={true}
rename={true}
edit={true}
minHeight={window.innerHeight - 103}/>
: undefined
}
{
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>
);
}
</Drawer>
</Content>
</div>
);
}
export default Storage;

View File

@ -1,43 +1,66 @@
import React from 'react';
import {Form, Input, Modal, Switch} from "antd/lib/index";
import React, {useEffect} from 'react';
import {Form, Input, Modal, Switch} from "antd";
import storageApi from "../../api/storage";
const formItemLayout = {
labelCol: {span: 6},
wrapperCol: {span: 14},
};
const StorageModal = ({title, visible, handleOk, handleCancel, confirmLoading, model}) => {
const StorageModal = ({
visible,
handleOk,
handleCancel,
confirmLoading,
id,
}) => {
const [form] = Form.useForm();
if (!model) {
model = {
isShare: false
useEffect(() => {
const getItem = async () => {
let data = await storageApi.getById(id);
if (data) {
form.setFieldsValue(data);
}
}
}
if (visible && id) {
getItem();
} else {
form.setFieldsValue({
isShare: false,
});
}
}, [visible])
return (
<Modal
title={title}
title={id ? '更新磁盘空间' : '新建磁盘空间'}
visible={visible}
maskClosable={false}
destroyOnClose={true}
onOk={() => {
form
.validateFields()
.then(values => {
form.resetFields();
handleOk(values);
})
.catch(info => {
.then(async values => {
let ok = await handleOk(values);
if (ok) {
form.resetFields();
}
});
}}
onCancel={handleCancel}
onCancel={() => {
form.resetFields();
handleCancel();
}}
confirmLoading={confirmLoading}
okText='确定'
cancelText='取消'
>
<Form form={form} {...formItemLayout} initialValues={model}>
<Form form={form} {...formItemLayout}>
<Form.Item name='id' noStyle>
<Input hidden={true}/>
</Form.Item>

Some files were not shown because too many files have changed in this diff Show More