提交 v1.3.0 beta
This commit is contained in:
8
web/config-overrides.js
Normal file
8
web/config-overrides.js
Normal 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
40539
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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%",
|
||||
|
||||
@ -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 |
@ -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
102
web/src/App.css
102
web/src/App.css
@ -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;*/
|
||||
/*}*/
|
||||
536
web/src/App.js
536
web/src/App.js
@ -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']} <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
0
web/src/Arco.css
Normal file
10
web/src/api/access-gateway.js
Normal file
10
web/src/api/access-gateway.js
Normal 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
78
web/src/api/account.js
Normal 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
50
web/src/api/api.js
Normal 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
42
web/src/api/asset.js
Normal 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
66
web/src/api/authorised.js
Normal 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
15
web/src/api/branding.js
Normal 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;
|
||||
10
web/src/api/command-filter-rule.js
Normal file
10
web/src/api/command-filter-rule.js
Normal 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;
|
||||
38
web/src/api/command-filter.js
Normal file
38
web/src/api/command-filter.js
Normal 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
16
web/src/api/command.js
Normal 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
19
web/src/api/credential.js
Normal 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
36
web/src/api/job.js
Normal 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
17
web/src/api/license.js
Normal 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
16
web/src/api/login-log.js
Normal 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;
|
||||
40
web/src/api/login-policy.js
Normal file
40
web/src/api/login-policy.js
Normal 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
37
web/src/api/monitor.js
Normal 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
16
web/src/api/permission.js
Normal 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
19
web/src/api/role.js
Normal 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
10
web/src/api/security.js
Normal 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
52
web/src/api/session.js
Normal 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;
|
||||
23
web/src/api/storage-log.js
Normal file
23
web/src/api/storage-log.js
Normal 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
10
web/src/api/storage.js
Normal 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
19
web/src/api/strategy.js
Normal 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
15
web/src/api/tag.js
Normal 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
19
web/src/api/user-group.js
Normal 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
28
web/src/api/user.js
Normal 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;
|
||||
19
web/src/api/worker/asset.js
Normal file
19
web/src/api/worker/asset.js
Normal 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;
|
||||
10
web/src/api/worker/command.js
Normal file
10
web/src/api/worker/command.js
Normal 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
8
web/src/common/auth.js
Normal 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);
|
||||
}
|
||||
@ -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
11
web/src/common/router.js
Normal 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}/>;
|
||||
};
|
||||
}
|
||||
51
web/src/components/AccessToken.js
Normal file
51
web/src/components/AccessToken.js
Normal 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
118
web/src/components/Info.js
Normal 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;
|
||||
20
web/src/components/Landing.js
Normal file
20
web/src/components/Landing.js
Normal 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;
|
||||
@ -14,7 +14,7 @@
|
||||
.login-card {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
top: 40%;
|
||||
margin-left: -175px;
|
||||
margin-top: -189px;
|
||||
}
|
||||
@ -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;
|
||||
|
||||
22
web/src/components/NoMatch.js
Normal file
22
web/src/components/NoMatch.js
Normal 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;
|
||||
33
web/src/components/NoPermission.js
Normal file
33
web/src/components/NoPermission.js
Normal 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
131
web/src/components/Totp.js
Normal 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;
|
||||
@ -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;
|
||||
}
|
||||
@ -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>
|
||||
|
||||
{
|
||||
|
||||
@ -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;
|
||||
@ -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'])} {renderSize(rxOfSeconds)}/秒
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="发送" key={'发送' + index}>
|
||||
{renderSize(network[key]['tx'])} {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'])} {renderSize(rxOfSeconds)}/秒
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="发送" key={'发送' + index}>
|
||||
{renderSize(network[key]['tx'])} {renderSize(txOfSeconds)}/秒
|
||||
</Descriptions.Item>
|
||||
</React.Fragment>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Descriptions>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Stats;
|
||||
@ -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);
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
|
||||
@ -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
52
web/src/components/asset/AssetDetail.js
Normal file
52
web/src/components/asset/AssetDetail.js
Normal 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;
|
||||
39
web/src/components/asset/AssetInfo.js
Normal file
39
web/src/components/asset/AssetInfo.js
Normal 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;
|
||||
3
web/src/components/asset/AssetModal.css
Normal file
3
web/src/components/asset/AssetModal.css
Normal file
@ -0,0 +1,3 @@
|
||||
.asset-modal .ant-modal-body{
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
145
web/src/components/asset/AssetUser.js
Normal file
145
web/src/components/asset/AssetUser.js
Normal 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;
|
||||
99
web/src/components/asset/AssetUserBind.js
Normal file
99
web/src/components/asset/AssetUserBind.js
Normal 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;
|
||||
146
web/src/components/asset/AssetUserGroup.js
Normal file
146
web/src/components/asset/AssetUserGroup.js
Normal 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;
|
||||
105
web/src/components/asset/AssetUserGroupBind.js
Normal file
105
web/src/components/asset/AssetUserGroupBind.js
Normal 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;
|
||||
43
web/src/components/asset/ChangeOwner.js
Normal file
43
web/src/components/asset/ChangeOwner.js
Normal 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;
|
||||
233
web/src/components/asset/Command.js
Normal file
233
web/src/components/asset/Command.js
Normal 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;
|
||||
93
web/src/components/asset/CommandModal.js
Normal file
93
web/src/components/asset/CommandModal.js
Normal 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;
|
||||
180
web/src/components/asset/Credential.js
Normal file
180
web/src/components/asset/Credential.js
Normal 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;
|
||||
@ -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>
|
||||
@ -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;
|
||||
218
web/src/components/asset/SelectingAsset.js
Normal file
218
web/src/components/asset/SelectingAsset.js
Normal 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;
|
||||
233
web/src/components/authorised/Strategy.js
Normal file
233
web/src/components/authorised/Strategy.js
Normal 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;
|
||||
33
web/src/components/authorised/StrategyDetail.js
Normal file
33
web/src/components/authorised/StrategyDetail.js
Normal 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;
|
||||
47
web/src/components/authorised/StrategyInfo.js
Normal file
47
web/src/components/authorised/StrategyInfo.js
Normal 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;
|
||||
@ -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>
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -1,4 +0,0 @@
|
||||
.command-active {
|
||||
box-shadow: 0 0 0 2px #1890ff;
|
||||
outline: 2px solid #1890ff;
|
||||
}
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
@ -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>
|
||||
</>);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
3
web/src/components/dashboard/Monitoring.css
Normal file
3
web/src/components/dashboard/Monitoring.css
Normal file
@ -0,0 +1,3 @@
|
||||
.ant-pro-card-body {
|
||||
padding: 16px !important;
|
||||
}
|
||||
353
web/src/components/dashboard/Monitoring.js
Normal file
353
web/src/components/dashboard/Monitoring.js
Normal 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;
|
||||
201
web/src/components/devops/BatchCommand.js
Normal file
201
web/src/components/devops/BatchCommand.js
Normal 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;
|
||||
24
web/src/components/devops/ExecuteCommand.css
Normal file
24
web/src/components/devops/ExecuteCommand.css
Normal 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;
|
||||
}
|
||||
224
web/src/components/devops/ExecuteCommand.js
Normal file
224
web/src/components/devops/ExecuteCommand.js
Normal 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;
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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;
|
||||
|
||||
108
web/src/components/devops/JobLog.js
Normal file
108
web/src/components/devops/JobLog.js
Normal 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;
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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="支持单个IP,CIDR或使用-连接的两个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;
|
||||
@ -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;
|
||||
@ -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
Reference in New Issue
Block a user