fix: 修复关闭SSH终端标签页时会话状态未更新的问题

This commit is contained in:
2026-04-18 02:35:38 +08:00
commit 6e2e2f9387
43467 changed files with 5489040 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
REACT_APP_ENV=development
ESLINT_NO_DEV_ERRORS=true
+3
View File
@@ -0,0 +1,3 @@
REACT_APP_ENV=production
GENERATE_SOURCEMAP=false
DISABLE_ESLINT_PLUGIN=true
+2
View File
@@ -0,0 +1,2 @@
# Project exclude paths
/node_modules/
+339
View File
@@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
+8
View File
@@ -0,0 +1,8 @@
# Next Terminal dashboard
just do go dashboard
## 安装依赖
```shell
npm install
```
+8
View File
@@ -0,0 +1,8 @@
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
module.exports = function override(config, env) {
config.plugins.push(new MonacoWebpackPlugin({
languages: ['json']
}));
return config;
}
+50
View File
@@ -0,0 +1,50 @@
{
"name": "next-terminal",
"version": "1.3.9",
"private": true,
"dependencies": {
"@ant-design/charts": "^1.4.2",
"@ant-design/icons": "^4.7.0",
"@ant-design/pro-components": "1.1.21",
"@turf/bbox": "^6.5.0",
"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.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-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app",
"rules": {
"jsx-a11y/anchor-is-valid": "off"
}
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
],
"homepage": "."
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

+31
View File
@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico"/>
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta name="theme-color" content="#000000"/>
<!--
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></title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
+215
View File
@@ -0,0 +1,215 @@
@import '~antd/dist/antd.min.css';
@import '~@ant-design/pro-components/dist/components.css';
.trigger {
font-size: 18px;
line-height: 64px;
padding: 0 24px;
cursor: pointer;
transition: color 0.3s;
}
.trigger:hover {
color: #1890ff;
}
.logo {
margin: 30px 17px;
text-align: center;
}
.logo > h1 {
color: white;
font-weight: bold;
line-height: 32px; /*设置line-height与父级元素的height相等*/
text-align: center; /*设置文本水平居中*/
display: inline-block;
}
.site-page-header-ghost-wrapper {
background-color: #FFF;
}
.layout-header {
height: 60px;
align-items: center;
background: #fff;
box-shadow: 0 1px 4px rgba(0, 21, 41, .08);
position: relative;
display: flex;
}
.layout-header-left {
flex: 1 1 0;
margin-left: 24px;
}
.layout-header-right {
text-align: right;
margin-right: 24px;
}
.layout-header-right-item {
cursor: pointer;
display: inline-flex;
align-items: center;
padding: 0 8px;
}
.page-herder {
margin: 16px 16px 0 16px;
}
.page-search {
background-color: white;
}
.page-search label {
font-weight: bold;
}
.page-search .ant-form-item {
margin-bottom: 0;
}
.page-container {
margin: 16px;
}
.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;
}
.page-card {
margin: 16px;
}
.user-in-menu {
align-items: center;
text-align: center;
margin: 10px auto;
color: white;
}
.user-in-menu > .nickname {
margin-top: 20px;
margin-right: auto;
margin-left: auto;
font-weight: bold;
padding: 2px 5px;
border-style: solid;
border-width: 1px;
border-color: white;
width: fit-content;
border-radius: 5%;
}
.modal-no-padding .ant-modal-body {
padding: 0;
}
.modal-no-padding-bg-xterm .ant-modal-body {
background-color: #121314;
}
.disabled-icon {
cursor: not-allowed;
color: #ccc;
}
.disabled-icon:hover {
color: #ccc;
}
.nt-container {
width: 80%;
margin: 20px auto 0;
}
.km-header {
color: white;
width: 80%;
margin: 0 auto;
position: relative;
display: flex;
align-items: center;
height: 100%;
/*padding: 0 16px;*/
}
.km-header-right {
text-align: left;
height: 100%;
margin: 0 8px;
}
.kd-content {
margin-top: 20px;
background-color: white;
}
.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;*/
/*}*/
+127
View File
@@ -0,0 +1,127 @@
import React, {Suspense} from 'react';
import {Outlet, Route, Routes} from "react-router-dom";
import './App.css';
import './Arco.css';
import ManagerLayout from "./layout/ManagerLayout";
import UserLayout from "./layout/UserLayout";
import NoMatch from "./components/NoMatch";
import Landing from "./components/Landing";
import NoPermission from "./components/NoPermission";
import Redirect from "./components/Redirect";
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"));
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"));
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"));
const Guacd = React.lazy(() => import("./components/access/Guacd"));
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 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 = () => {
return (
<Routes>
<Route path="/" element={<Redirect/>}/>
<Route element={
<Suspense fallback={<Landing/>}>
<Outlet/>
</Suspense>
}>
<Route path="/access" element={<Guacd/>}/>
<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>
<Route element={<ManagerLayout/>}>
<Route path="/dashboard" element={<Dashboard/>}/>
<Route path="/monitoring" element={<Monitoring/>}/>
<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/>}/>
<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="/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>
<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;
+9
View File
@@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
ReactDOM.unmountComponentAtNode(div);
});
View File
+10
View File
@@ -0,0 +1,10 @@
import Api from "./api";
class AccessGatewayApi extends Api{
constructor() {
super("access-gateways");
}
}
let accessGatewayApi = new AccessGatewayApi();
export default accessGatewayApi;
+78
View File
@@ -0,0 +1,78 @@
import request from "../common/request";
import qs from "qs";
class AccountApi {
group = 'account';
logout = async () => {
let result = await request.post('/account/logout');
return result['code'] === 1
}
getUserInfo = async () => {
let result = await request.get(`/${this.group}/info`);
if (result['code'] !== 1) {
return {};
}
return result['data'];
}
assetPaging = async (params) => {
let paramsStr = qs.stringify(params);
let result = await request.get(`/${this.group}/assets?${paramsStr}`);
if (result['code'] !== 1) {
return {};
}
return result['data'];
}
getAccessToken = async () => {
let result = await request.get(`/${this.group}/access-token`);
if (result['code'] !== 1) {
return {};
}
return result['data'];
}
createAccessToken = async () => {
let result = await request.post(`/${this.group}/access-token`);
if (result['code'] !== 1) {
return {};
}
return result['data'];
}
deleteAccessToken = async () => {
let result = await request.delete(`/${this.group}/access-token`);
return result['code'] === 1;
}
changePassword = async (values) => {
let result = await request.post(`/${this.group}/change-password`, values);
return result.code === 1;
}
reloadTotp = async () => {
let result = await request.get('/account/reload-totp');
if (result.code === 1) {
return result.data;
} else {
return {}
}
}
confirmTotp = async (values) => {
let result = await request.post(`/${this.group}/confirm-totp`, values);
return result.code === 1;
}
resetTotp = async () => {
let result = await request.post(`/${this.group}/reset-totp`);
return result.code === 1;
}
}
let accountApi = new AccountApi();
export default accountApi;
+50
View File
@@ -0,0 +1,50 @@
import request from "../common/request";
import qs from "qs";
export default class Api {
group = "";
constructor(group) {
this.group = group;
}
getById = async (id) => {
let result = await request.get(`/${this.group}/${id}`);
if (result['code'] !== 1) {
return;
}
return result['data'];
}
getPaging = async (params) => {
let paramsStr = qs.stringify(params);
let result = await request.get(`/${this.group}/paging?${paramsStr}`);
if (result['code'] !== 1) {
return {};
}
return result['data'];
}
getAll = async () => {
let result = await request.get(`/${this.group}`);
if (result['code'] !== 1) {
return [];
}
return result['data'];
}
create = async (data) => {
const result = await request.post(`/${this.group}`, data);
return result['code'] === 1;
}
updateById = async (id, data) => {
const result = await request.put(`/${this.group}/${id}`, data);
return result['code'] === 1;
}
deleteById = async (id) => {
const result = await request.delete(`/${this.group}/${id}`);
return result['code'] === 1;
}
}
+42
View File
@@ -0,0 +1,42 @@
import Api from "./api";
import request from "../common/request";
class AssetApi extends Api {
constructor() {
super("assets");
}
GetAll = async (protocol = '') => {
let result = await request.get(`/${this.group}?protocol=${protocol}`);
if (result['code'] !== 1) {
return [];
}
return result['data'];
}
connTest = async (id) => {
let result = await request.post(`/${this.group}/${id}/tcping`);
if (result.code !== 1) {
return [false, result.message];
}
return [result['data']['active'], result['data']['message']];
}
importAsset = async (file) => {
const formData = new FormData();
formData.append("file", file,);
let result = await request.post(`/${this.group}/import`, formData, {'Content-Type': 'multipart/form-data'});
if (result.code !== 1) {
return [false, result.message];
}
return [true, result['data']];
}
changeOwner = async (id, owner) => {
let result = await request.post(`/${this.group}/${id}/change-owner?owner=${owner}`);
return result['code'] === 1;
}
}
const assetApi = new AssetApi();
export default assetApi;
+66
View File
@@ -0,0 +1,66 @@
import qs from "qs";
import request from "../common/request";
class AuthorisedApi {
group = "authorised";
GetAssetPaging = async (params) => {
let paramsStr = qs.stringify(params);
let result = await request.get(`/${this.group}/assets/paging?${paramsStr}`);
if (result['code'] !== 1) {
return {};
}
return result['data'];
}
GetUserPaging = async (params) => {
let paramsStr = qs.stringify(params);
let result = await request.get(`/${this.group}/users/paging?${paramsStr}`);
if (result['code'] !== 1) {
return {};
}
return result['data'];
}
GetUserGroupPaging = async (params) => {
let paramsStr = qs.stringify(params);
let result = await request.get(`/${this.group}/user-groups/paging?${paramsStr}`);
if (result['code'] !== 1) {
return {};
}
return result['data'];
}
AuthorisedAssets = async (data) => {
const result = await request.post(`/${this.group}/assets`, data);
return result['code'] === 1;
}
AuthorisedUsers = async (data) => {
const result = await request.post(`/${this.group}/users`, data);
return result['code'] === 1;
}
AuthorisedUserGroups = async (data) => {
const result = await request.post(`/${this.group}/user-groups`, data);
return result['code'] === 1;
}
GetSelected = async (params) => {
let paramsStr = qs.stringify(params);
let result = await request.get(`/${this.group}/selected?${paramsStr}`);
if (result['code'] !== 1) {
return [];
}
return result['data'];
}
DeleteById = async (id) => {
const result = await request.delete(`/${this.group}/${id}`);
return result['code'] === 1;
}
}
const authorisedApi = new AuthorisedApi();
export default authorisedApi;
+15
View File
@@ -0,0 +1,15 @@
import request from "../common/request";
class BrandingApi {
getBranding = async () => {
let result = await request.get(`/branding`);
if (result['code'] !== 1) {
return {};
}
return result['data'];
}
}
let brandingApi = new BrandingApi();
export default brandingApi;
+10
View File
@@ -0,0 +1,10 @@
import Api from "./api";
class CommandFilterRuleApi extends Api{
constructor() {
super("command-filter-rules");
}
}
const commandFilterRuleApi = new CommandFilterRuleApi();
export default commandFilterRuleApi;
+38
View File
@@ -0,0 +1,38 @@
import request from "../common/request";
import Api from "./api";
class CommandFilterApi extends Api{
constructor() {
super("command-filters");
}
Bind = async (id, data) => {
const result = await request.post(`/${this.group}/${id}/bind`, data);
return result['code'] === 1;
}
Unbind = async (id, data) => {
const result = await request.post(`/${this.group}/${id}/unbind`, data);
return result['code'] === 1;
}
GetAssetIdByCommandFilterId = async (commandFilterId) => {
let result = await request.get(`/${this.group}/${commandFilterId}/assets/id`);
if (result['code'] !== 1) {
return [];
}
return result['data'];
}
GetAll = async () => {
let result = await request.get(`/${this.group}`);
if (result['code'] !== 1) {
return [];
}
return result['data'];
}
}
const commandFilterApi = new CommandFilterApi();
export default commandFilterApi;
+16
View File
@@ -0,0 +1,16 @@
import Api from "./api";
import request from "../common/request";
class CommandApi extends Api{
constructor() {
super("commands");
}
changeOwner = async (id, owner) => {
let result = await request.post(`/${this.group}/${id}/change-owner?owner=${owner}`);
return result['code'] === 1;
}
}
let commandApi = new CommandApi();
export default commandApi;
+19
View File
@@ -0,0 +1,19 @@
import Api from "./api";
import request from "../common/request";
class CredentialApi extends Api{
constructor() {
super("credentials");
}
getAll = async () => {
let result = await request.get(`/${this.group}`);
if (result['code'] !== 1) {
return [];
}
return result['data'];
}
}
let credentialApi = new CredentialApi();
export default credentialApi;
+36
View File
@@ -0,0 +1,36 @@
import Api from "./api";
import request from "../common/request";
import qs from "qs";
class JobApi extends Api {
constructor() {
super("jobs");
}
changeStatus = async (id, status) => {
let result = await request.post(`/${this.group}/${id}/change-status?status=${status}`);
return result['code'] !== 1;
}
exec = async (id) => {
let result = await request.post(`/${this.group}/${id}/exec`);
return result['code'] !== 1;
}
getLogPaging = async (id, params) => {
let paramsStr = qs.stringify(params);
let result = await request.get(`/${this.group}/${id}/logs/paging?${paramsStr}`);
if (result['code'] !== 1) {
return {};
}
return result['data'];
}
deleteLogByJobId = async (id) => {
let result = await request.delete(`/${this.group}/${id}/logs`);
return result['code'] !== 1;
}
}
let jobApi = new JobApi();
export default jobApi;
+17
View File
@@ -0,0 +1,17 @@
import request from "../common/request";
export const GetLicense = async () => {
let result = await request.get('/license');
if (result['code'] !== 1) {
return;
}
return result['data'];
}
export const GetMachineId = async () => {
let result = await request.get('/license/machine-id');
if (result['code'] !== 1) {
return;
}
return result['data'];
}
+16
View File
@@ -0,0 +1,16 @@
import Api from "./api";
import request from "../common/request";
class LoginLogApi extends Api{
constructor() {
super("login-logs");
}
Clear = async () => {
const result = await request.post(`/${this.group}/clear`);
return result['code'] === 1;
}
}
let loginLogApi = new LoginLogApi();
export default loginLogApi;
+40
View File
@@ -0,0 +1,40 @@
import request from "../common/request";
import qs from "qs";
import Api from "./api";
class LoginPolicyApi extends Api{
constructor() {
super("login-policies");
}
Bind = async (id, data) => {
const result = await request.post(`/${this.group}/${id}/bind`, data);
return result['code'] === 1;
}
Unbind = async (id, data) => {
const result = await request.post(`/${this.group}/${id}/unbind`, data);
return result['code'] === 1;
}
GetUserPagingByForbiddenCommandId = async (id, params) => {
let paramsStr = qs.stringify(params);
let result = await request.get(`/${this.group}/${id}/users/paging?${paramsStr}`);
if (result['code'] !== 1) {
return {};
}
return result['data'];
}
GetUserIdByLoginPolicyId = async (id) => {
let result = await request.get(`/${this.group}/${id}/users/id`);
if (result['code'] !== 1) {
return [];
}
return result['data'];
}
}
const loginPolicyApi = new LoginPolicyApi();
export default loginPolicyApi;
+37
View File
@@ -0,0 +1,37 @@
import request from "../common/request";
class MonitorApi {
getData = async () => {
let result = await request.get('/overview/ps');
if (result['code'] !== 1) {
return {};
}
let data = result['data'];
let netIO = [];
for (let i = 0; i < data['netIO'].length; i++) {
let item = data['netIO'][i];
netIO.push({
time: item['time'],
read: item['read'] / 1024 / 1024 / 1024,
write: item['write'] / 1024 / 1024 / 1024,
});
}
data['netIO'] = netIO;
let diskIO = [];
for (let i = 0; i < data['diskIO'].length; i++) {
let item = data['diskIO'][i];
diskIO.push({
time: item['time'],
read: item['read'] / 1024 / 1024 / 1024,
write: item['write'] / 1024 / 1024 / 1024,
});
}
data['diskIO'] = diskIO;
return data
}
}
let monitorApi = new MonitorApi();
export default monitorApi;
+16
View File
@@ -0,0 +1,16 @@
import request from "../common/request";
class PermissionApi {
group = "permissions";
getMenus = async () => {
let result = await request.get(`/menus`);
if (result['code'] !== 1) {
return [];
}
return result['data'];
}
}
let permissionApi = new PermissionApi();
export default permissionApi;
+19
View File
@@ -0,0 +1,19 @@
import request from "../common/request";
import Api from "./api";
class RoleApi extends Api {
constructor() {
super("roles");
}
GetAll = async () => {
let result = await request.get(`/${this.group}`);
if (result['code'] !== 1) {
return [];
}
return result['data'];
}
}
let roleApi = new RoleApi();
export default roleApi;
+10
View File
@@ -0,0 +1,10 @@
import Api from "./api";
class SecurityApi extends Api {
constructor() {
super("securities");
}
}
let securityApi = new SecurityApi();
export default securityApi;
+57
View File
@@ -0,0 +1,57 @@
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'];
}
resize = async (sessionId, width, height) => {
let result = await request.post(`/sessions/${sessionId}/resize?width=${width}&height=${height}`);
return result.code === 1;
}
}
const sessionApi = new SessionApi();
export default sessionApi;
+23
View File
@@ -0,0 +1,23 @@
import Api from "./api";
import request from "../common/request";
class StorageLogApi extends Api {
constructor() {
super("storage-logs");
}
create = () => {
}
getById = () => {
}
updateById = () => {
}
Clear = async () => {
const result = await request.post(`/${this.group}/clear`);
return result['code'] === 1;
}
}
const storageLogApi = new StorageLogApi();
export default storageLogApi;
+10
View File
@@ -0,0 +1,10 @@
import Api from "./api";
class StorageApi extends Api{
constructor() {
super("storages");
}
}
let storageApi = new StorageApi();
export default storageApi;
+19
View File
@@ -0,0 +1,19 @@
import Api from "./api";
import request from "../common/request";
class StrategyApi extends Api {
constructor() {
super("strategies");
}
GetAll = async () => {
let result = await request.get(`/${this.group}`);
if (result['code'] !== 1) {
return [];
}
return result['data'];
}
}
const strategyApi = new StrategyApi();
export default strategyApi;
+15
View File
@@ -0,0 +1,15 @@
import request from "../common/request";
class TagApi {
getAll = async () => {
let result = await request.get(`/tags`);
if (result['code'] !== 1) {
return [];
}
return result['data'];
}
}
let tagApi = new TagApi();
export default tagApi;
+19
View File
@@ -0,0 +1,19 @@
import Api from "./api";
import request from "../common/request";
class UserGroupApi extends Api {
constructor() {
super("user-groups");
}
GetAll = async () => {
let result = await request.get(`/${this.group}`);
if (result['code'] !== 1) {
return [];
}
return result['data'];
}
}
const userGroupApi = new UserGroupApi();
export default userGroupApi;
+28
View File
@@ -0,0 +1,28 @@
import Api from "./api";
import request from "../common/request";
class UserApi extends Api {
constructor() {
super("users");
}
resetTotp = async (id) => {
let result = await request.post(`/${this.group}/${id}/reset-totp`);
return result['code'] === 1;
}
changePassword = async (id, password) => {
let formData = new FormData();
formData.set('password', password);
let result = await request.post(`/${this.group}/${id}/change-password`, formData);
return result['code'] === 1;
}
changeStatus = async (id, status) => {
let result = await request.patch(`/${this.group}/${id}/status?status=${status}`);
return result['code'] !== 1;
}
}
const userApi = new UserApi();
export default userApi;
+19
View File
@@ -0,0 +1,19 @@
import Api from "../api";
import request from "../../common/request";
class WorkAssetApi extends Api{
constructor() {
super("worker/assets");
}
tags = async () => {
let result = await request.get(`/${this.group}/tags`);
if (result['code'] !== 1) {
return [];
}
return result['data'];
}
}
let workAssetApi = new WorkAssetApi();
export default workAssetApi;
+10
View File
@@ -0,0 +1,10 @@
import Api from "../api";
class WorkCommandApi extends Api{
constructor() {
super("worker/commands");
}
}
let workCommandApi = new WorkCommandApi();
export default workCommandApi;
+8
View File
@@ -0,0 +1,8 @@
export const HasPermission = (permission) => {
let permissionsStr = sessionStorage.getItem('permissions');
let permissions = JSON.parse(permissionsStr);
if (!permissions) {
return false;
}
return permissions.includes(permission);
}
+13
View File
@@ -0,0 +1,13 @@
export const PROTOCOL_COLORS = {
'rdp': 'cyan',
'ssh': 'blue',
'telnet': 'geekblue',
'vnc': 'purple',
'kubernetes': 'volcano'
}
export const MODE_COLORS = {
'guacd': 'green',
'native': 'orange',
'terminal': 'purple',
}
+28
View File
@@ -0,0 +1,28 @@
function env() {
if (process.env.REACT_APP_ENV === 'development') {
// 本地开发环境
return {
server: '//127.0.0.1:8088',
wsServer: 'ws://127.0.0.1:8088',
prefix: '',
}
} else {
// 生产环境
let wsPrefix;
if (window.location.protocol === 'https:') {
wsPrefix = 'wss:'
} else {
wsPrefix = 'ws:'
}
return {
server: '',
wsServer: wsPrefix + window.location.host,
prefix: window.location.protocol + '//' + window.location.host,
}
}
}
export default env();
export const server = env().server;
export const wsServer = env().wsServer;
export const prefix = env().prefix;
+146
View File
@@ -0,0 +1,146 @@
import axios from 'axios'
import {server} from "./env";
import {message} from 'antd';
import {getHeaders} from "../utils/utils";
// 测试地址
// axios.defaults.baseURL = server;
// 线上地址
axios.defaults.baseURL = server;
const handleError = (error) => {
if ("Network Error" === error.toString()) {
message.error('网络异常');
return false;
}
if (error.response !== undefined && error.response.status === 401) {
window.location.href = '#/login';
return false;
}
if (error.response !== undefined) {
message.error(error.response.data.message);
return false;
}
return true;
};
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;
}
const request = {
get: function (url) {
const headers = getHeaders();
return new Promise((resolve, reject) => {
axios.get(url, {headers: headers})
.then((response) => {
let contentType = response.headers['content-type'];
if (contentType !== '' && contentType.includes('application/json')) {
handleResult(response.data);
}
resolve(response.data);
})
.catch((error) => {
if (!handleError(error)) {
return;
}
reject(error);
});
})
},
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) => {
handleResult(response.data);
resolve(response.data);
})
.catch((error) => {
if (!handleError(error)) {
return;
}
reject(error);
});
})
},
put: function (url, params) {
const headers = getHeaders();
return new Promise((resolve, reject) => {
axios.put(url, params, {headers: headers})
.then((response) => {
handleResult(response.data);
resolve(response.data);
})
.catch((error) => {
if (!handleError(error)) {
return;
}
reject(error);
});
})
},
delete: function (url) {
const headers = getHeaders();
return new Promise((resolve, reject) => {
axios.delete(url, {headers: headers})
.then((response) => {
handleResult(response.data);
resolve(response.data);
})
.catch((error) => {
if (!handleError(error)) {
return;
}
reject(error);
});
})
},
patch: function (url, params) {
const headers = getHeaders();
return new Promise((resolve, reject) => {
axios.patch(url, params, {headers: headers})
.then((response) => {
handleResult(response.data);
resolve(response.data);
})
.catch((error) => {
if (!handleError(error)) {
return;
}
reject(error);
});
})
},
};
export default request
+11
View File
@@ -0,0 +1,11 @@
import {useLocation, useNavigate, useParams, useSearchParams} from "react-router-dom";
export const withRouter = (Component) => {
return (props) => {
const location = useLocation();
const navigate = useNavigate();
const params = useParams();
const searchParams = useSearchParams();
return <Component {...props} location={location} navigate={navigate} params={params} searchParams={searchParams}/>;
};
}
+51
View File
@@ -0,0 +1,51 @@
import React from 'react';
import {Button, Descriptions, Space, Typography} from "antd";
import {useQuery} from "react-query";
import accountApi from "../api/account";
const {Title, Text} = Typography;
const AccessToken = () => {
let tokenQuery = useQuery('getAccessToken', accountApi.getAccessToken);
const genAccessToken = async () => {
await accountApi.createAccessToken();
await tokenQuery.refetch();
}
const clearAccessToken = async () => {
let success = await accountApi.deleteAccessToken();
if (success) {
await tokenQuery.refetch();
}
}
return (
<div>
<Title level={4}>授权令牌</Title>
<div style={{margin: 16}}></div>
<Descriptions column={1}>
<Descriptions.Item label="授权令牌">
<Text strong copyable>{tokenQuery.data?.token}</Text>
</Descriptions.Item>
<Descriptions.Item label="生成时间">
<Text strong>{tokenQuery.data?.created}</Text>
</Descriptions.Item>
</Descriptions>
<Space>
<Button type="primary" onClick={genAccessToken}>
重新生成
</Button>
<Button type="primary" danger disabled={tokenQuery.data?.token === ''}
onClick={clearAccessToken}>
删除令牌
</Button>
</Space>
</div>
);
};
export default AccessToken;
+118
View File
@@ -0,0 +1,118 @@
import React, {useState} from 'react';
import {Button, Form, Input, Layout, message, Tabs, Typography} from "antd";
import accountApi from "../api/account";
import Totp from "./Totp";
const {Content} = Layout;
const {Title} = Typography;
const Info = () => {
let [newPassword1, setNewPassword1] = useState('');
let [newPassword2, setNewPassword2] = useState('');
let [newPasswordStatus, setNewPasswordStatus] = useState({});
const onNewPasswordChange = (value) => {
setNewPassword1(value.target.value);
setNewPasswordStatus(validateNewPassword(value.target.value, newPassword2));
}
const onNewPassword2Change = (value) => {
setNewPassword2(value.target.value);
setNewPasswordStatus(validateNewPassword(newPassword1, value.target.value));
}
const validateNewPassword = (newPassword1, newPassword2) => {
if (newPassword2 === newPassword1) {
return {
validateStatus: 'success',
errorMsg: null,
};
}
return {
validateStatus: 'error',
errorMsg: '两次输入的密码不一致',
};
}
const changePassword = async (values) => {
let success = await accountApi.changePassword(values);
if (success) {
message.success('密码修改成功,即将跳转至登录页面');
window.location.href = '/#';
}
}
return (
<>
<Content className={'page-container-white'}>
<Tabs className={'info-tab'} tabPosition={'left'} tabBarStyle={{width: 150}}>
<Tabs.TabPane tab="修改密码" key="change-password">
<Title level={4}>修改密码</Title>
<div style={{margin: 16}}></div>
<Form name="password" onFinish={changePassword}>
<input type='password' hidden={true} autoComplete='new-password'/>
<Form.Item
name="oldPassword"
label="原始密码"
rules={[
{
required: true,
message: '原始密码',
},
]}
>
<Input type='password' placeholder="请输入原始密码" style={{width: 240}}/>
</Form.Item>
<Form.Item
name="newPassword"
label="新的密码"
rules={[
{
required: true,
message: '请输入新的密码',
},
]}
>
<Input type='password' placeholder="新的密码"
onChange={(value) => onNewPasswordChange(value)} style={{width: 240}}/>
</Form.Item>
<Form.Item
name="newPassword2"
label="确认密码"
rules={[
{
required: true,
message: '请和上面输入新的密码保持一致',
},
]}
validateStatus={newPasswordStatus.validateStatus}
help={newPasswordStatus.errorMsg || ' '}
>
<Input type='password' placeholder="请和上面输入新的密码保持一致"
onChange={(value) => onNewPassword2Change(value)} style={{width: 240}}/>
</Form.Item>
<Form.Item>
<Button disabled={newPasswordStatus.errorMsg || !newPasswordStatus.validateStatus}
type="primary"
htmlType="submit">
提交
</Button>
</Form.Item>
</Form>
</Tabs.TabPane>
{/*<Tabs.TabPane tab="授权令牌" key="token">*/}
{/* <AccessToken/>*/}
{/*</Tabs.TabPane>*/}
<Tabs.TabPane tab="两步认证" key="totp">
<Totp/>
</Tabs.TabPane>
</Tabs>
</Content>
</>
);
}
export default Info;
+20
View File
@@ -0,0 +1,20 @@
import React from 'react';
const Landing = () => {
return (
<div style={{
// width: '100vw',
// height: '100vh',
width: '100%',
height: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'white'
}}>
<div style={{fontWeight: 'bold'}}>正在努力加载中...</div>
</div>
);
};
export default Landing;
+20
View File
@@ -0,0 +1,20 @@
.login-form {
max-width: 300px;
min-width: 300px;
}
.login-form-forgot {
float: right;
}
.login-form-button {
width: 100%;
}
.login-card {
position: absolute;
left: 50%;
top: 40%;
margin-left: -175px;
margin-top: -189px;
}
+133
View File
@@ -0,0 +1,133 @@
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 {LockOutlined, UserOutlined} from '@ant-design/icons';
import {setToken} from "../utils/utils";
import brandingApi from "../api/branding";
import strings from "../utils/strings";
import {useNavigate} from "react-router-dom";
import {setCurrentUser} from "../service/permission";
import PromptModal from "../dd/prompt-modal/prompt-modal";
const {Title, Text} = Typography;
const LoginForm = () => {
const navigate = useNavigate();
let [inLogin, setInLogin] = useState(false);
let [branding, setBranding] = useState({});
let [prompt, setPrompt] = useState(false);
let [account, setAccount] = useState({});
useEffect(() => {
const x = async () => {
let branding = await brandingApi.getBranding();
document.title = branding['name'];
setBranding(branding);
}
x();
}, []);
const afterLoginSuccess = async (data) => {
// 跳转登录
sessionStorage.removeItem('current');
sessionStorage.removeItem('openKeys');
setToken(data['token']);
let user = data['info'];
setCurrentUser(user);
if (user) {
if (user['type'] === 'user') {
navigate('/my-asset');
} else {
navigate('/');
}
}
}
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 handleSubmit = async params => {
setInLogin(true);
try {
let result = await request.post('/login', params);
if (result.code === 100) {
// 进行双因素认证
setPrompt(true);
setAccount(params);
return;
}
if (result.code !== 1) {
return;
}
afterLoginSuccess(result['data']);
} catch (e) {
message.error(e.message);
} finally {
setInLogin(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>{branding['description']}</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>
<PromptModal
title={'双因素认证'}
open={prompt}
onOk={(value) => {
handleOk(account, value)
}}
onCancel={() => setPrompt(false)}
placeholder={"请输入双因素认证码"}
>
</PromptModal>
</div>
);
}
export default LoginForm;
+31
View File
@@ -0,0 +1,31 @@
import React from 'react';
import {Button, Layout, Result, Space} from "antd";
import {Link, useNavigate} from "react-router-dom";
const {Content} = Layout;
const NoMatch = () => {
let navigate = useNavigate();
return (
<div>
<Content>
<Result
status="404"
title="404"
subTitle="抱歉,您似乎到达了预期之外的页面。"
extra={
<Space>
<Button type="primary" onClick={() => {navigate(-1);}}>返回上一页</Button>
<Button type="primary"><Link to={'/my-asset'}>我的资产</Link></Button>
<Button type="primary"><Link to={'/'}>后台首页</Link></Button>
</Space>
}
/>
</Content>
</div>
);
};
export default NoMatch;
+31
View File
@@ -0,0 +1,31 @@
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={'/my-asset'}>我的资产</Link></Button>
<Button type="primary"><Link to={'/'}>后台首页</Link></Button>
</Space>
}
/>
</Content>
</div>
);
};
export default NoPermission;
+30
View File
@@ -0,0 +1,30 @@
import React from 'react';
import {useQuery} from "react-query";
import accountApi from "../api/account";
import {setCurrentUser} from "../service/permission";
import {useNavigate} from "react-router-dom";
import Landing from "./Landing";
const Redirect = () => {
let navigate = useNavigate();
let infoQuery = useQuery('infoQuery', accountApi.getUserInfo, {
onSuccess: data => {
setCurrentUser(data);
if (data.type === 'user') {
navigate('/my-asset');
} else if (data.type === 'admin'){
navigate('/dashboard');
}
}
});
return (
<div>
<Landing/>
</div>
);
};
export default Redirect;
+131
View File
@@ -0,0 +1,131 @@
import React, {useState} from 'react';
import {Button, Form, Image, Input, message, Modal, Result, Space, Typography} from "antd";
import {ExclamationCircleOutlined, ReloadOutlined} from "@ant-design/icons";
import accountApi from "../api/account";
import {useQuery} from "react-query";
const {Title} = Typography;
const Totp = () => {
let infoQuery = useQuery('infoQuery', accountApi.getUserInfo);
let [totp, setTotp] = useState({});
const resetTOTP = async () => {
let totp = await accountApi.reloadTotp();
setTotp(totp);
}
const confirmTOTP = async (values) => {
values['secret'] = totp['secret'];
let success = await accountApi.confirmTotp(values);
if (success) {
message.success('TOTP启用成功');
await infoQuery.refetch();
setTotp({});
}
}
const renderBindingTotpPage = (qr) => {
if (!qr) {
return undefined;
}
return <Form hidden={!totp.qr} onFinish={confirmTOTP}>
<Form.Item label="二维码"
extra={'有效期30秒,在扫描后请尽快输入。推荐使用Google Authenticator, Authy 或者 Microsoft Authenticator。'}>
<Space size={12} direction='horizontal'>
<Image
style={{padding: 20}}
width={280}
src={"data:image/png;base64, " + totp.qr}
/>
<Button
type="primary"
icon={<ReloadOutlined/>}
onClick={resetTOTP}
>
重新加载
</Button>
</Space>
</Form.Item>
<Form.Item
name="totp"
label="TOTP"
rules={[
{
required: true,
message: '请输入双因素认证APP中显示的授权码',
},
]}
>
<Input placeholder="请输入双因素认证APP中显示的授权码"/>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
确认
</Button>
</Form.Item>
</Form>
}
return (
<div>
<Title level={4}>双因素认证</Title>
<Form hidden={totp.qr}>
<Form.Item>
{
infoQuery.data?.enableTotp ?
<Result
status="success"
title="您已成功开启双因素认证!"
subTitle="多因素认证-MFA二次认证-登录身份鉴别,访问控制更安全。"
extra={[
<Button type="primary" key="console" danger onClick={() => {
Modal.confirm({
title: '您确认要解除双因素认证吗?',
icon: <ExclamationCircleOutlined/>,
content: '解除之后可能存在系统账号被暴力破解的风险。',
okText: '确认',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
let success = await accountApi.resetTotp();
if (success) {
message.success('双因素认证解除成功');
await infoQuery.refetch();
}
},
onCancel() {
console.log('Cancel');
},
})
}}>
解除绑定
</Button>,
<Button key="re-bind" onClick={resetTOTP}>重新绑定</Button>,
]}
/> :
<Result
status="warning"
title="您还未开启双因素认证!"
subTitle="系统账号存在被暴力破解的风险。"
extra={
<Button type="primary" key="bind" onClick={resetTOTP}>
去开启
</Button>
}
/>
}
</Form.Item>
</Form>
{
renderBindingTotpPage(totp.qr)
}
</div>
);
};
export default Totp;
@@ -0,0 +1,3 @@
.console-card .ant-card-body {
padding: 0;
}
@@ -0,0 +1,186 @@
import React, {Component} from 'react';
import "xterm/css/xterm.css"
import {Terminal} from "xterm";
import qs from "qs";
import {wsServer} from "../../common/env";
import "./BatchCommandTerm.css"
import {getToken, isEmpty} from "../../utils/utils";
import {FitAddon} from 'xterm-addon-fit'
import request from "../../common/request";
import {message} from "antd";
import Message from './Message'
class BatchCommandTerm extends Component {
state = {
term: undefined,
webSocket: undefined,
fitAddon: undefined
};
componentDidMount = async () => {
let command = this.props.command;
let assetId = this.props.assetId;
let sessionId = await this.createSession(assetId);
if (isEmpty(sessionId)) {
return;
}
let term = new Terminal({
fontFamily: 'monaco, Consolas, "Lucida Console", monospace',
fontSize: 14,
theme: {
background: '#1b1b1b'
},
rightClickSelectsWord: true,
});
term.open(this.refs.terminal);
const fitAddon = new FitAddon();
term.loadAddon(fitAddon);
fitAddon.fit();
term.focus();
term.writeln('Trying to connect to the server ...');
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,
'rows': term.rows,
'sessionId': sessionId,
'X-Auth-Token': token
};
let paramStr = qs.stringify(params);
let webSocket = new WebSocket(`${wsServer}/sessions/${sessionId}/ssh?${paramStr}`);
this.props.appendWebsocket({'id': assetId, 'ws': webSocket});
webSocket.onopen = (e => {
this.onWindowResize();
});
webSocket.onerror = (e) => {
term.writeln("Failed to connect to server.");
}
webSocket.onclose = (e) => {
term.writeln("Connection is closed.");
}
let executedCommand = false
webSocket.onmessage = (e) => {
let msg = Message.parse(e.data);
switch (msg['type']) {
case Message.Connected:
term.clear();
this.updateSessionStatus(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;
}
if (!executedCommand) {
if (command !== '') {
let webSocket = this.state.webSocket;
if (webSocket !== undefined && webSocket.readyState === WebSocket.OPEN) {
webSocket.send(new Message(Message.Data, command + String.fromCharCode(13)).toString());
}
}
executedCommand = true;
}
}
this.setState({
term: term,
fitAddon: fitAddon,
webSocket: webSocket,
});
window.addEventListener('resize', this.onWindowResize);
}
componentWillUnmount() {
let webSocket = this.state.webSocket;
if (webSocket) {
webSocket.close()
}
}
async createSession(assetsId) {
let result = await request.post(`/sessions?assetId=${assetsId}&mode=native`);
if (result['code'] !== 1) {
this.showMessage(result['message']);
return null;
}
return result['data']['id'];
}
updateSessionStatus = async (sessionId) => {
let result = await request.post(`/sessions/${sessionId}/connect`);
if (result['code'] !== 1) {
message.error(result['message']);
}
}
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());
}
});
};
focus = () => {
let term = this.state.term;
if (term) {
term.focus();
}
}
render() {
return (
<div>
<div style={{
width: (window.innerWidth - 254) / 2,
height: 456,
}}>
<div ref='terminal' id='terminal' style={{
backgroundColor: '#1b1b1b'
}}/>
</div>
</div>
);
}
}
export default BatchCommandTerm;
@@ -0,0 +1,3 @@
#display > div {
margin: 0 auto;
}
+560
View File
@@ -0,0 +1,560 @@
import React, {useEffect, useState} from 'react';
import {useSearchParams} from "react-router-dom";
import sessionApi from "../../api/session";
import strings from "../../utils/strings";
import Guacamole from "guacamole-common-js";
import {wsServer} from "../../common/env";
import {exitFull, getToken, requestFullScreen} from "../../utils/utils";
import qs from "qs";
import {Affix, Button, Drawer, Dropdown, Menu, message, Modal} from "antd";
import {
CopyOutlined,
ExclamationCircleOutlined,
ExpandOutlined,
FolderOutlined,
WindowsOutlined
} from "@ant-design/icons";
import {Base64} from "js-base64";
import Draggable from "react-draggable";
import FileSystem from "../devops/FileSystem";
import GuacdClipboard from "./GuacdClipboard";
import {debounce} from "../../utils/fun";
import './Guacd.css';
let fixedSize = false;
const STATE_IDLE = 0;
const STATE_CONNECTING = 1;
const STATE_WAITING = 2;
const STATE_CONNECTED = 3;
const STATE_DISCONNECTING = 4;
const STATE_DISCONNECTED = 5;
const Guacd = () => {
let [searchParams] = useSearchParams();
let assetId = searchParams.get('assetId');
let assetName = searchParams.get('assetName');
let protocol = searchParams.get('protocol');
let width = searchParams.get('width');
let height = searchParams.get('height');
if (width && height) {
fixedSize = true;
} else {
width = window.innerWidth;
height = window.innerHeight;
}
let [box, setBox] = useState({width, height});
let [guacd, setGuacd] = useState({});
let [session, setSession] = useState({});
let [clipboardText, setClipboardText] = useState('');
let [fullScreened, setFullScreened] = useState(false);
let [clipboardVisible, setClipboardVisible] = useState(false);
let [fileSystemVisible, setFileSystemVisible] = useState(false);
useEffect(() => {
document.title = assetName;
createSession();
}, [assetId, assetName]);
const createSession = async () => {
let session = await sessionApi.create(assetId, 'guacd');
if (!strings.hasText(session['id'])) {
return;
}
setSession(session);
renderDisplay(session['id'], protocol, width, height);
}
const renderDisplay = (sessionId, protocol, width, height) => {
let tunnel = new Guacamole.WebSocketTunnel(`${wsServer}/sessions/${sessionId}/tunnel`);
let client = new Guacamole.Client(tunnel);
// 处理从虚拟机收到的剪贴板内容
client.onclipboard = handleClipboardReceived;
// 处理客户端的状态变化事件
client.onstatechange = (state) => {
onClientStateChange(state, sessionId);
};
client.onerror = onError;
tunnel.onerror = onError;
// Get display div from document
const displayEle = document.getElementById("display");
// Add client to display div
const element = client.getDisplay().getElement();
displayEle.appendChild(element);
let dpi = 96;
if (protocol === 'telnet') {
dpi = dpi * 2;
}
let token = getToken();
let params = {
'width': width,
'height': height,
'dpi': dpi,
'X-Auth-Token': token
};
let paramStr = qs.stringify(params);
client.connect(paramStr);
let display = client.getDisplay();
display.onresize = function (width, height) {
display.scale(Math.min(
window.innerHeight / display.getHeight(),
window.innerWidth / display.getHeight()
))
}
const sink = new Guacamole.InputSink();
displayEle.appendChild(sink.getElement());
sink.focus();
const keyboard = new Guacamole.Keyboard(sink.getElement());
keyboard.onkeydown = (keysym) => {
console.log('aaa')
client.sendKeyEvent(1, keysym);
if (keysym === 65288) {
return false;
}
};
keyboard.onkeyup = (keysym) => {
client.sendKeyEvent(0, keysym);
};
const sinkFocus = debounce(() => {
sink.focus();
});
const mouse = new Guacamole.Mouse(element);
mouse.onmousedown = mouse.onmouseup = function (mouseState) {
sinkFocus();
client.sendMouseState(mouseState);
}
mouse.onmousemove = function (mouseState) {
sinkFocus();
client.getDisplay().showCursor(false);
mouseState.x = mouseState.x / display.getScale();
mouseState.y = mouseState.y / display.getScale();
client.sendMouseState(mouseState);
};
const touch = new Guacamole.Mouse.Touchpad(element); // or Guacamole.Touchscreen
touch.onmousedown = touch.onmousemove = touch.onmouseup = function (state) {
client.sendMouseState(state);
};
setGuacd({
client,
sink,
});
}
useEffect(() => {
let resize = debounce(() => {
onWindowResize();
});
window.addEventListener('resize', resize);
window.addEventListener('beforeunload', handleUnload);
window.addEventListener('focus', handleWindowFocus);
return () => {
window.removeEventListener('resize', resize);
window.removeEventListener('beforeunload', handleUnload);
window.removeEventListener('focus', handleWindowFocus);
};
}, [guacd])
const onWindowResize = () => {
if (guacd.client && !fixedSize) {
const display = guacd.client.getDisplay();
let width = window.innerWidth;
let height = window.innerHeight;
setBox({width, height});
let scale = Math.min(
height / display.getHeight(),
width / display.getHeight()
);
display.scale(scale);
guacd.client.sendSize(width, height);
}
}
const handleUnload = (e) => {
const message = "要离开网站吗?";
(e || window.event).returnValue = message; //Gecko + IE
return message;
}
const focus = () => {
console.log(guacd.sink)
if (guacd.sink) {
guacd.sink.focus();
}
}
const handleWindowFocus = (e) => {
if (navigator.clipboard) {
try {
navigator.clipboard.readText().then((text) => {
sendClipboard({
'data': text,
'type': 'text/plain'
});
})
} catch (e) {
console.error('复制剪贴板失败', e);
}
}
};
const handleClipboardReceived = (stream, mimetype) => {
if (session['copy'] === '0') {
// message.warn('禁止复制');
return
}
if (/^text\//.exec(mimetype)) {
let reader = new Guacamole.StringReader(stream);
let data = '';
reader.ontext = function textReceived(text) {
data += text;
};
reader.onend = async () => {
setClipboardText(data);
if (navigator.clipboard) {
await navigator.clipboard.writeText(data);
}
// message.success('您选择的内容已复制到您的粘贴板中,在右侧的输入框中可同时查看到。');
};
} else {
let reader = new Guacamole.BlobReader(stream, mimetype);
reader.onend = () => {
setClipboardText(reader.getBlob());
}
}
};
const sendClipboard = (data) => {
if (!guacd.client) {
return;
}
if (session['paste'] === '0') {
message.warn('禁止粘贴');
return
}
const stream = guacd.client.createClipboardStream(data.type);
if (typeof data.data === 'string') {
let writer = new Guacamole.StringWriter(stream);
writer.sendText(data.data);
writer.sendEnd();
} else {
let writer = new Guacamole.BlobWriter(stream);
writer.oncomplete = function clipboardSent() {
writer.sendEnd();
};
writer.sendBlob(data.data);
}
if (data.data && data.data.length > 0) {
// message.info('您输入的内容已复制到远程服务器上');
}
}
const onClientStateChange = (state, sessionId) => {
const key = 'message';
switch (state) {
case STATE_IDLE:
message.destroy(key);
message.loading({content: '正在初始化中...', duration: 0, key: key});
break;
case STATE_CONNECTING:
message.destroy(key);
message.loading({content: '正在努力连接中...', duration: 0, key: key});
break;
case STATE_WAITING:
message.destroy(key);
message.loading({content: '正在等待服务器响应...', duration: 0, key: key});
break;
case STATE_CONNECTED:
Modal.destroyAll();
message.destroy(key);
message.success({content: '连接成功', duration: 3, key: key});
// 向后台发送请求,更新会话的状态
sessionApi.connect(sessionId);
break;
case STATE_DISCONNECTING:
break;
case STATE_DISCONNECTED:
message.info({content: '连接已关闭', duration: 3, key: key});
break;
default:
break;
}
};
const sendCombinationKey = (keys) => {
if (!guacd.client) {
return;
}
for (let i = 0; i < keys.length; i++) {
guacd.client.sendKeyEvent(1, keys[i]);
}
for (let j = 0; j < keys.length; j++) {
guacd.client.sendKeyEvent(0, keys[j]);
}
message.success('发送组合键成功');
}
const showMessage = (msg) => {
message.destroy();
Modal.confirm({
title: '提示',
icon: <ExclamationCircleOutlined/>,
content: msg,
centered: true,
okText: '重新连接',
cancelText: '关闭页面',
onOk() {
window.location.reload();
},
onCancel() {
window.close();
},
});
}
const onError = (status) => {
switch (status.code) {
case 256:
showMessage('未支持的访问');
break;
case 512:
showMessage('远程服务异常,请检查目标设备能否正常访问。');
break;
case 513:
showMessage('服务器忙碌');
break;
case 514:
showMessage('服务器连接超时');
break;
case 515:
showMessage('远程服务异常');
break;
case 516:
showMessage('资源未找到');
break;
case 517:
showMessage('资源冲突');
break;
case 518:
showMessage('资源已关闭');
break;
case 519:
showMessage('远程服务未找到');
break;
case 520:
showMessage('远程服务不可用');
break;
case 521:
showMessage('会话冲突');
break;
case 522:
showMessage('会话连接超时');
break;
case 523:
showMessage('会话已关闭');
break;
case 768:
showMessage('网络不可达');
break;
case 769:
showMessage('服务器密码验证失败');
break;
case 771:
showMessage('客户端被禁止');
break;
case 776:
showMessage('客户端连接超时');
break;
case 781:
showMessage('客户端异常');
break;
case 783:
showMessage('错误的请求类型');
break;
case 800:
showMessage('会话不存在');
break;
case 801:
showMessage('创建隧道失败,请检查Guacd服务是否正常。');
break;
case 802:
showMessage('管理员强制关闭了此会话');
break;
default:
if (status.message) {
// guacd 无法处理中文字符,所以进行了base64编码。
showMessage(Base64.decode(status.message));
} else {
showMessage('未知错误。');
}
}
};
const fullScreen = () => {
if (fullScreened) {
exitFull();
setFullScreened(false);
} else {
requestFullScreen(document.documentElement);
setFullScreened(true);
}
focus();
}
const hotKeyMenu = (
<Menu>
<Menu.Item key={'ctrl+alt+delete'}
onClick={() => sendCombinationKey(['65507', '65513', '65535'])}>Ctrl+Alt+Delete</Menu.Item>
<Menu.Item key={'ctrl+alt+backspace'}
onClick={() => sendCombinationKey(['65507', '65513', '65288'])}>Ctrl+Alt+Backspace</Menu.Item>
<Menu.Item key={'windows+d'}
onClick={() => sendCombinationKey(['65515', '100'])}>Windows+D</Menu.Item>
<Menu.Item key={'windows+e'}
onClick={() => sendCombinationKey(['65515', '101'])}>Windows+E</Menu.Item>
<Menu.Item key={'windows+r'}
onClick={() => sendCombinationKey(['65515', '114'])}>Windows+R</Menu.Item>
<Menu.Item key={'windows+x'}
onClick={() => sendCombinationKey(['65515', '120'])}>Windows+X</Menu.Item>
<Menu.Item key={'windows'}
onClick={() => sendCombinationKey(['65515'])}>Windows</Menu.Item>
</Menu>
);
return (
<div>
<div className="container" style={{
width: box.width,
height: box.height,
margin: '0 auto',
backgroundColor: '#1b1b1b'
}}>
<div id="display"/>
</div>
<Draggable>
<Affix style={{position: 'absolute', top: 50, right: 50}}>
<Button icon={<ExpandOutlined/>} onClick={() => {
fullScreen();
}}/>
</Affix>
</Draggable>
{
session['copy'] === '1' || session['paste'] === '1' ?
<Draggable>
<Affix style={{position: 'absolute', top: 50, right: 100}}>
<Button icon={<CopyOutlined/>}
onClick={() => {
setClipboardVisible(true);
}}/>
</Affix>
</Draggable> : undefined
}
{
protocol === 'vnc' &&
<Draggable>
<Affix style={{position: 'absolute', top: 100, right: 100}}>
<Dropdown overlay={hotKeyMenu} trigger={['click']} placement="bottomLeft">
<Button icon={<WindowsOutlined/>}/>
</Dropdown>
</Affix>
</Draggable>
}
{
(protocol === 'rdp' && session['fileSystem'] === '1') &&
<Draggable>
<Affix style={{position: 'absolute', top: 100, right: 50}}>
<Button icon={<FolderOutlined/>} onClick={() => {
setFileSystemVisible(true);
}}/>
</Affix>
</Draggable>
}
{
protocol === 'rdp' &&
<Draggable>
<Affix style={{position: 'absolute', top: 100, right: 100}}>
<Dropdown overlay={hotKeyMenu} trigger={['click']} placement="bottomLeft">
<Button icon={<WindowsOutlined/>}/>
</Dropdown>
</Affix>
</Draggable>
}
<Drawer
title={'文件管理'}
placement="right"
width={window.innerWidth * 0.8}
closable={true}
onClose={() => {
focus();
setFileSystemVisible(false);
}}
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>
<GuacdClipboard
visible={clipboardVisible}
clipboardText={clipboardText}
handleOk={(text) => {
sendClipboard({
'data': text,
'type': 'text/plain'
});
setClipboardText(text);
setClipboardVisible(false);
focus();
}}
handleCancel={() => {
setClipboardVisible(false);
focus();
}}
/>
</div>
);
};
export default Guacd;
@@ -0,0 +1,48 @@
import React, {useEffect, useState} from 'react';
import {Form, Input, Modal} from "antd";
const GuacdClipboard = ({visible, clipboardText, handleOk, handleCancel}) => {
const [form] = Form.useForm();
let [confirmLoading, setConfirmLoading] = useState(false);
useEffect(() => {
form.setFieldsValue({
'clipboard': clipboardText
})
}, [visible]);
return (
<div>
<Modal
title="剪贴板"
maskClosable={false}
visible={visible}
onOk={() => {
form.validateFields()
.then(values => {
setConfirmLoading(true);
try {
handleOk(values['clipboard']);
} finally {
setConfirmLoading(false);
}
})
.catch(info => {
});
}}
confirmLoading={confirmLoading}
onCancel={handleCancel}
>
<Form form={form}>
<Form.Item name='clipboard'>
<Input.TextArea rows={10}/>
</Form.Item>
</Form>
</Modal>
</div>
);
};
export default GuacdClipboard;
@@ -0,0 +1,23 @@
const Message = class Message {
constructor(type, content) {
this.type = type;
this.content = content;
}
toString() {
return this.type + this.content;
}
static Closed = 0;
static Connected = 1;
static Data = 2;
static Resize = 3;
static Ping = 4;
static parse(s) {
let type = parseInt(s.substring(0, 1));
let content = s.substring(1, s.length);
return new Message(type, content);
}
};
export default Message;
@@ -0,0 +1,5 @@
.description-content {
align-items: center;
justify-content: center;
vertical-align: middle;
}
+197
View File
@@ -0,0 +1,197 @@
import React, {useState} from 'react';
import {Col, Descriptions, Progress, Row} from "antd";
import {renderSize} from "../../utils/utils";
import './Stats.css'
import {useQuery} from "react-query";
import sessionApi from "../../api/session";
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
}
}
const Stats = ({sessionId, visible, queryInterval = 5000}) => {
let [stats, setStats] = useState(defaultStats);
let [prevStats, setPrevStats] = useState({});
useQuery("stats", () => sessionApi.stats(sessionId), {
refetchInterval: queryInterval,
enabled: visible,
onSuccess: (data) => {
setPrevStats(stats);
setStats(data);
}
});
const upDays = parseInt((stats.uptime / 1000 / 60 / 60 / 24).toString());
const memUsage = ((stats.memTotal - stats.memAvailable) * 100 / stats.memTotal).toFixed(2);
let network = stats.network;
let fileSystems = stats.fileSystems;
let swapUsage = 0;
if (stats.swapTotal !== 0) {
swapUsage = ((stats.swapTotal - stats.swapFree) * 100 / stats.swapTotal).toFixed(2)
}
return (
<div>
<Descriptions title="系统信息" column={4}>
<Descriptions.Item label="主机名称">{stats.hostname}</Descriptions.Item>
<Descriptions.Item label="运行时长">{upDays}</Descriptions.Item>
</Descriptions>
<Row justify="center" align="middle">
<Col>
<Descriptions title="负载" column={4}>
<Descriptions.Item label='Load1'>
<div className='description-content'>
<Progress percent={stats.load1} steps={20} size={'small'}/>
</div>
</Descriptions.Item>
<Descriptions.Item label='Load5'>
<div className='description-content'>
<Progress percent={stats.load5} steps={20} size={'small'}/>
</div>
</Descriptions.Item>
<Descriptions.Item label='Load10'>
<div className='description-content'>
<Progress percent={stats.load10} steps={20} size={'small'}/>
</div>
</Descriptions.Item>
</Descriptions>
</Col>
</Row>
<Descriptions title="CPU" column={4}>
<Descriptions.Item label="用户">
{stats.cpu['user'].toFixed(2)}%
</Descriptions.Item>
<Descriptions.Item label="系统">
{stats.cpu['system'].toFixed(2)}%
</Descriptions.Item>
<Descriptions.Item label="空闲">
{stats.cpu['idle'].toFixed(2)}%
</Descriptions.Item>
<Descriptions.Item label="IO等待">
{stats.cpu['ioWait'].toFixed(2)}%
</Descriptions.Item>
<Descriptions.Item label="硬中断">
{stats.cpu['irq'].toFixed(2)}%
</Descriptions.Item>
<Descriptions.Item label="软中断">
{stats.cpu['softIrq'].toFixed(2)}%
</Descriptions.Item>
<Descriptions.Item label="nice">
{stats.cpu['nice'].toFixed(2)}%
</Descriptions.Item>
<Descriptions.Item label="guest">
{stats.cpu['guest'].toFixed(2)}%
</Descriptions.Item>
</Descriptions>
<Descriptions title="内存" column={4}>
<Descriptions.Item label="物理内存大小">{renderSize(stats.memTotal)}</Descriptions.Item>
<Descriptions.Item label="剩余内存大小">{renderSize(stats.memFree)}</Descriptions.Item>
<Descriptions.Item label="可用内存大小">{renderSize(stats.memAvailable)}</Descriptions.Item>
<Descriptions.Item label="使用占比">
<div className='description-content'>
<Progress percent={memUsage} steps={20} size={'small'}/>
</div>
</Descriptions.Item>
<Descriptions.Item
label="Buffers/Cached">{renderSize(stats.memBuffers)} / {renderSize(stats.memCached)}</Descriptions.Item>
<Descriptions.Item
label="交换内存大小">{renderSize(stats.swapTotal)}</Descriptions.Item>
<Descriptions.Item
label="交换内存剩余">{renderSize(stats.swapFree)}</Descriptions.Item>
<Descriptions.Item label="使用占比">
<div className='description-content'>
<Progress percent={swapUsage} steps={20} size={'small'}/>
</div>
</Descriptions.Item>
</Descriptions>
<Descriptions title="磁盘" column={4}>
{
fileSystems.map((item, index) => {
return (
<React.Fragment key={'磁盘' + index}>
<Descriptions.Item label="挂载路径" key={'挂载路径' + index}>
{item['mountPoint']}
</Descriptions.Item>
<Descriptions.Item label="已经使用" key={'已经使用' + index}>
{renderSize(item['used'])}
</Descriptions.Item>
<Descriptions.Item label="剩余空间" key={'剩余空间' + index}>
{renderSize(item['free'])}
</Descriptions.Item>
<Descriptions.Item label="使用占比" key={'使用占比' + index}>
<div className='description-content'>
<Progress
percent={(item['used'] * 100 / (item['used'] + item['free'])).toFixed(2)}
steps={20} size={'small'}/>
</div>
</Descriptions.Item>
</React.Fragment>
);
})
}
</Descriptions>
<Descriptions title="网络" column={4}>
{
Object.keys(network).map((key, index) => {
let prevNetwork = prevStats.network;
let rxOfSeconds = 0, txOfSeconds = 0;
if (prevNetwork[key] !== undefined) {
rxOfSeconds = (network[key]['rx'] - prevNetwork[key]['rx']) / 5;
}
if (prevNetwork[key] !== undefined) {
txOfSeconds = (network[key]['tx'] - prevNetwork[key]['tx']) / 5;
}
return (
<React.Fragment key={'网络' + index}>
<Descriptions.Item label="网卡" key={'网卡' + index}>{key}</Descriptions.Item>
<Descriptions.Item label="IPv4" key={'IPv4' + index}>
{network[key]['ipv4']}
</Descriptions.Item>
<Descriptions.Item label="接收" key={'接收' + index}>
{renderSize(network[key]['rx'])} &nbsp; {renderSize(rxOfSeconds)}/
</Descriptions.Item>
<Descriptions.Item label="发送" key={'发送' + index}>
{renderSize(network[key]['tx'])} &nbsp; {renderSize(txOfSeconds)}/
</Descriptions.Item>
</React.Fragment>
);
})
}
</Descriptions>
</div>
);
};
export default Stats;
+357
View File
@@ -0,0 +1,357 @@
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 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 {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";
const {Text} = Typography;
const Term = () => {
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();
}
}
const onWindowResize = () => {
setBox({width: window.innerWidth, height: window.innerHeight});
};
const init = async (assetId) => {
let term = new Terminal({
fontFamily: 'monaco, Consolas, "Lucida Console", monospace',
fontSize: 15,
theme: {
background: '#1b1b1b'
},
});
let elementTerm = document.getElementById('terminal');
term.open(elementTerm);
const fitAddon = new FitAddon();
term.loadAddon(fitAddon);
fitAddon.fit();
term.focus();
if (!assetId) {
writeErrorMessage(term, `参数缺失,请关闭此页面后重新打开。`)
return;
}
let [session, errMsg] = await createSession(assetId);
if (!session) {
writeErrorMessage(term, `创建会话失败,${errMsg}`)
return;
}
let sessionId = session['id'];
term.writeln('trying to connect to the server ...');
document.body.oncopy = (event) => {
event.preventDefault();
if (session['copy'] === '0') {
message.warn('禁止复制')
return false;
} else {
return true;
}
}
document.body.onpaste = (event) => {
event.preventDefault();
if (session['paste'] === '0') {
message.warn('禁止粘贴')
return false;
} else {
return true;
}
}
let token = getToken();
let params = {
'cols': term.cols,
'rows': term.rows,
'X-Auth-Token': token
};
let paramStr = qs.stringify(params);
let webSocket = new WebSocket(`${wsServer}/sessions/${sessionId}/ssh?${paramStr}`);
let pingInterval;
webSocket.onopen = (e => {
pingInterval = setInterval(() => {
webSocket.send(new Message(Message.Ping, "").toString());
}, 10000);
xtermScrollPretty();
});
webSocket.onerror = (e) => {
writeErrorMessage(term, `websocket error ${e.data}`)
}
webSocket.onclose = (e) => {
console.log(`e`, e);
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();
updateSessionStatus(sessionId);
getCommands();
break;
case Message.Data:
term.write(msg['content']);
break;
case Message.Closed:
console.log(`服务端通知需要关闭连接`)
term.writeln(`\x1B[1;3;31m${msg['content']}\x1B[0m `);
webSocket.close();
break;
default:
break;
}
}
setSession(session);
setTerm(term);
setFitAddon(fitAddon);
setWebsocket(webSocket);
}
const handleUnload = (e) => {
const message = "要离开网站吗?";
(e || window.event).returnValue = message; //Gecko + IE
return message;
}
useEffect(() => {
document.title = assetName;
init(assetId);
}, [assetId]);
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());
}
window.addEventListener('beforeunload', handleUnload);
let resize = debounce(() => {
onWindowResize();
});
window.addEventListener('resize', resize);
return () => {
// if (websocket) {
// websocket.close();
// }
window.removeEventListener('resize', resize);
window.removeEventListener('beforeunload', handleUnload);
}
}, [box.width, box.height]);
const cmdMenuItems = commands.map(item => {
return {
key: item['id'],
label: item['name'],
};
});
const handleCmdMenuClick = (e) => {
for (const command of commands) {
if (command['id'] === e.key) {
writeCommand(command['content']);
}
}
}
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;
@@ -0,0 +1,208 @@
import React, {useState} from 'react';
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 ColumnState, {useColumnState} from "../../hook/column-state";
import Show from "../../dd/fi/show";
const {Content} = Layout;
const api = accessGatewayApi;
const actionRef = React.createRef();
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,
},
{
title: '名称',
dataIndex: 'name',
},
{
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 (
<Tag color="green">密钥</Tag>
);
} else {
return (
<Tag color="red">密码</Tag>
);
}
}
}, {
title: '授权账户',
dataIndex: 'username',
key: 'username',
hideInSearch: true
}, {
title: '状态',
dataIndex: 'connected',
key: 'connected',
hideInSearch: true,
render: (text, record) => {
if (text) {
return (
<Tooltip title='连接成功'>
<Badge status="success" text='已连接'/>
</Tooltip>
)
} else {
return (
<Tooltip title={record['message']}>
<Badge status="default" text='已断开'/>
</Tooltip>
)
}
}
},
{
title: '创建时间',
key: 'created',
dataIndex: 'created',
hideInSearch: true,
},
{
title: '操作',
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']);
}}
>
编辑
</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>,
],
},
];
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);
}
if (success) {
setVisible(false);
}
actionRef.current.reload();
} finally {
setConfirmLoading(false);
}
}}
/>
</Content>);
}
export default AccessGateway;
@@ -0,0 +1,136 @@
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},
wrapperCol: {span: 14},
};
const {TextArea} = Input;
const api = accessGatewayApi;
const AccessGatewayModal = ({
visible,
handleOk,
handleCancel,
confirmLoading,
id,
}) => {
const [form] = Form.useForm();
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={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='ip' rules={[{required: true, message: '请输入网关的主机名称或者IP地址'}]}>
<Input placeholder="网关的主机名称或者IP地址"/>
</Form.Item>
<Form.Item label="端口号" name='port' rules={[{required: true, message: '请输入端口'}]}>
<InputNumber min={1} max={65535} placeholder='TCP端口'/>
</Form.Item>
<Form.Item label="账户类型" name='accountType'
rules={[{required: true, message: '请选择接账户类型'}]}>
<Select onChange={handleAccountTypeChange}>
<Select.Option key='password' value='password'>密码</Select.Option>
<Select.Option key='private-key' value='private-key'>密钥</Select.Option>
</Select>
</Form.Item>
{
accountType === 'password' ?
<>
<input type='password' hidden={true} autoComplete='new-password'/>
<Form.Item label="授权账户" name='username'
rules={[{required: true}]}>
<Input placeholder="root"/>
</Form.Item>
<Form.Item label="授权密码" name='password'
rules={[{required: true}]}>
<Input.Password placeholder="password"/>
</Form.Item>
</>
:
<>
<Form.Item label="授权账户" name='username' rules={[{required: true}]}>
<Input placeholder="输入授权账户"/>
</Form.Item>
<Form.Item label="私钥" name='privateKey'
rules={[{required: true, message: '请输入私钥'}]}>
<TextArea rows={4}/>
</Form.Item>
<Form.Item label="私钥密码" name='passphrase'>
<TextArea rows={1}/>
</Form.Item>
</>
}
</Form>
</Modal>
)
};
export default AccessGatewayModal;
+553
View File
@@ -0,0 +1,553 @@
import React, {useState} from 'react';
import {
Badge,
Button,
Layout,
Modal,
notification,
Popconfirm,
Popover,
Select,
Table,
Tag,
Tooltip,
Upload
} from "antd";
import {Link, useNavigate} from "react-router-dom";
import {ProTable, TableDropdown} from "@ant-design/pro-components";
import assetApi from "../../api/asset";
import tagApi from "../../api/tag";
import {PROTOCOL_COLORS} from "../../common/constants";
import strings from "../../utils/strings";
import AssetModal from "./AssetModal";
import ColumnState, {useColumnState} from "../../hook/column-state";
import {useQuery} from "react-query";
import Show from "../../dd/fi/show";
import {hasMenu} from "../../service/permission";
import ChangeOwner from "./ChangeOwner";
import dayjs from "dayjs";
const api = assetApi;
const {Content} = Layout;
const actionRef = React.createRef();
function downloadImportExampleCsv() {
let csvString = 'name,ssh,127.0.0.1,22,username,password,privateKey,passphrase,description,tag1|tag2|tag3';
//前置的"\uFEFF"为“零宽不换行空格”,可处理中文乱码问题
const blob = new Blob(["\uFEFF" + csvString], {type: 'text/csv;charset=gb2312;'});
let a = document.createElement('a');
a.download = 'sample.csv';
a.href = URL.createObjectURL(blob);
a.click();
}
const importExampleContent = <>
<a onClick={downloadImportExampleCsv}>下载示例</a>
<div>导入资产时账号密码和密钥密码属于二选一都填写时优先选择私钥和密码</div>
</>
const Asset = () => {
let [visible, setVisible] = useState(false);
let [confirmLoading, setConfirmLoading] = useState(false);
let [selectedRowKey, setSelectedRowKey] = useState(undefined);
let [items, setItems] = useState([]);
let [selectedRowKeys, setSelectedRowKeys] = useState([]);
let [copied, setCopied] = useState(false);
let [selectedRow, setSelectedRow] = useState(undefined);
let [changeOwnerVisible, setChangeOwnerVisible] = useState(false);
const [columnsStateMap, setColumnsStateMap] = useColumnState(ColumnState.ASSET);
const tagQuery = useQuery('getAllTag', tagApi.getAll);
let navigate = useNavigate();
const columns = [
{
dataIndex: 'index',
valueType: 'indexBorder',
width: 48,
},
{
title: '名称',
dataIndex: 'name',
sorter: true,
render: (text, record) => {
if (record['description'] === '-') {
record['description'] = '';
}
let view = <div>{text}</div>;
if (hasMenu('asset-detail')) {
view = <Link to={`/asset/${record['id']}`}>{text}</Link>;
}
return <div>
{view}
<div style={{
color: 'rgba(0, 0, 0, 0.45)',
lineHeight: 1.45,
fontSize: '14px'
}}>{record['description']}</div>
</div>
},
}, {
title: '协议',
dataIndex: 'protocol',
key: 'protocol',
sorter: true,
render: (text, record) => {
return (
<Tag color={PROTOCOL_COLORS[text]}>{text}</Tag>
)
},
renderFormItem: (item, {type, defaultRender, ...rest}, form) => {
if (type === 'form') {
return null;
}
return (
<Select>
<Select.Option value="rdp">RDP</Select.Option>
<Select.Option value="ssh">SSH</Select.Option>
<Select.Option value="telnet">Telnet</Select.Option>
<Select.Option value="kubernetes">Kubernetes</Select.Option>
</Select>
);
},
}, {
title: '网络',
dataIndex: 'network',
key: 'network',
sorter: true,
fieldProps: {
placeholder: '示例: 127、127.0.0.1、:22、127.0.0.1:22'
},
render: (text, record) => {
return `${record['ip'] + ':' + record['port']}`;
}
}, {
title: '标签',
dataIndex: 'tags',
key: 'tags',
render: tags => {
if (strings.hasText(tags)) {
return tags.split(',').filter(tag => tag !== '-').map(tag => <Tag key={tag}>{tag}</Tag>);
}
},
renderFormItem: (item, {type, defaultRender, ...rest}, form) => {
if (type === 'form') {
return null;
}
return (
<Select mode="multiple"
allowClear>
{
tagQuery.data?.map(tag => {
if (tag === '-') {
return undefined;
}
return <Select.Option key={tag}>{tag}</Select.Option>
})
}
</Select>
);
},
}, {
title: '状态',
dataIndex: 'active',
key: 'active',
sorter: true,
render: (text, record) => {
if (record['testing'] === true) {
return (
<Tooltip title='测试中'>
<Badge status="processing" text='测试中'/>
</Tooltip>
)
}
if (text) {
return (
<Tooltip title='运行中'>
<Badge status="success" text='运行中'/>
</Tooltip>
)
} else {
return (
<Tooltip title={record['activeMessage']}>
<Badge status="error" text='不可用'/>
</Tooltip>
)
}
},
renderFormItem: (item, {type, defaultRender, ...rest}, form) => {
if (type === 'form') {
return null;
}
return (
<Select>
<Select.Option value="true">运行中</Select.Option>
<Select.Option value="false">不可用</Select.Option>
</Select>
);
},
}, {
title: '所有者',
dataIndex: 'ownerName',
key: 'ownerName',
hideInSearch: true,
},
{
title: '创建时间',
key: 'created',
sorter: true,
dataIndex: 'created',
hideInSearch: true,
},
{
title: '最后接入时间',
key: 'lastAccessTime',
sorter: true,
dataIndex: 'lastAccessTime',
hideInSearch: true,
render: (text, record) => {
if (text === '0001-01-01 00:00:00') {
return '-';
}
return (
<Tooltip title={text}>
{dayjs(text).fromNow()}
</Tooltip>
)
},
},
{
title: '操作',
valueType: 'option',
key: 'option',
render: (text, record, index, action) => {
const id = record['id'];
const protocol = record['protocol'];
const name = record['name'];
let url = '';
if (protocol === 'ssh') {
url = `#/term?assetId=${id}&assetName=${name}`;
} else {
url = `#/access?assetId=${id}&assetName=${name}&protocol=${protocol}`;
}
return [
<Show menu={'asset-access'} key={'asset-access'}>
<a
key="access"
href={url}
target='_blank'
>
接入
</a>
</Show>,
<Show menu={'asset-edit'} key={'asset-edit'}>
<a
key="edit"
onClick={() => {
setVisible(true);
setSelectedRowKey(record['id']);
}}
>
编辑
</a>
</Show>,
<Show menu={'asset-del'} key={'asset-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>,
<TableDropdown
key="actionGroup"
onSelect={(key) => {
switch (key) {
case "copy":
setCopied(true);
setVisible(true);
setSelectedRowKey(record['id']);
break;
case "test":
connTest(record['id'], index);
break;
case "change-owner":
handleChangeOwner(record);
break;
case 'asset-detail':
navigate(`/asset/${record['id']}?activeKey=info`);
break;
case 'asset-authorised-user':
navigate(`/asset/${record['id']}?activeKey=bind-user`);
break;
case 'asset-authorised-user-group':
navigate(`/asset/${record['id']}?activeKey=bind-user-group`);
break;
}
}}
menus={[
{key: 'copy', name: '复制', disabled: !hasMenu('asset-copy')},
{key: 'test', name: '连通性测试', disabled: !hasMenu('asset-conn-test')},
{key: 'change-owner', name: '更换所有者', disabled: !hasMenu('asset-change-owner')},
{key: 'asset-detail', name: '详情', disabled: !hasMenu('asset-detail')},
{
key: 'asset-authorised-user',
name: '授权用户',
disabled: !hasMenu('asset-authorised-user')
},
{
key: 'asset-authorised-user-group',
name: '授权用户组',
disabled: !hasMenu('asset-authorised-user-group')
},
]}
/>,
]
},
},
];
const connTest = async (id, index) => {
items[index]['testing'] = true;
setItems(items.slice());
let [active, msg] = await assetApi.connTest(id);
items[index]['active'] = active;
items[index]['activeMessage'] = msg;
items[index]['testing'] = false;
setItems(items.slice());
}
const connTestInBatch = async () => {
for (let i = 0; i < items.length; i++) {
let item = items[i];
if (selectedRowKeys.includes(item['id'])) {
connTest(item['id'], i);
}
}
setSelectedRowKeys([]);
}
const handleImportAsset = async (file) => {
let [success, data] = await api.importAsset(file);
if (success === false) {
notification['error']({
message: '导入资产失败',
description: data,
});
return false;
}
let successCount = data['successCount'];
let errorCount = data['errorCount'];
if (errorCount === 0) {
notification['success']({
message: '导入资产成功',
description: '共导入成功' + successCount + '条资产。',
});
} else {
notification['info']({
message: '导入资产完成',
description: `共导入成功${successCount}条资产,失败${errorCount}条资产。`,
});
}
actionRef.current.reload();
return false;
}
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];
if (field === 'network') {
field = 'ip';
}
order = Object.values(sort)[0];
}
let ip, port;
if (params.network) {
let split = params.network.split(':');
if (split.length >= 2) {
ip = split[0];
port = split[1];
} else {
ip = split[0];
}
}
let queryParams = {
pageIndex: params.current,
pageSize: params.pageSize,
name: params.name,
type: params.type,
protocol: params.protocol,
active: params.active,
'tags': params.tags?.join(','),
ip: ip,
port: port,
field: field,
order: order
}
let result = await api.getPaging(queryParams);
setItems(result['items']);
return {
data: items,
success: true,
total: result['total']
};
}}
rowSelection={{
// 自定义选择项参考: https://ant.design/components/table-cn/#components-table-demo-row-selection-custom
// 注释该行则默认不显示下拉选项
selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT],
selectedRowKeys: selectedRowKeys,
onChange: (keys) => {
setSelectedRowKeys(keys);
}
}}
dataSource={items}
rowKey="id"
search={{
labelWidth: 'auto',
}}
pagination={{
defaultPageSize: 10,
showSizeChanger: true
}}
dateFormatter="string"
headerTitle="资产列表"
toolBarRender={() => {
return [
<Show menu={'asset-add'}>
<Button key="add" type="primary" onClick={() => {
setVisible(true)
}}>
新建
</Button>
</Show>,
<Show menu={'asset-import'}>
<Popover content={importExampleContent}>
<Upload
maxCount={1}
beforeUpload={handleImportAsset}
showUploadList={false}
>
<Button key='import'>导入</Button>
</Upload>
</Popover>
</Show>,
<Show menu={'asset-del'}>
<Button key="delete" danger
type="primary"
disabled={selectedRowKeys.length === 0}
onClick={() => {
Modal.confirm({
title: '您确定要删除选中的行吗?',
content: '删除之后无法进行恢复,请慎重考虑。',
okText: '确定',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
await api.deleteById(selectedRowKeys.join(","));
actionRef.current.reload();
setSelectedRowKeys([]);
}
});
}}>
删除
</Button>
</Show>,
<Show menu={'asset-conn-test'}>
<Button key="connTest"
type="primary"
disabled={selectedRowKeys.length === 0}
onClick={connTestInBatch}>
连通性测试
</Button>
</Show>
];
}}
/>
<AssetModal
id={selectedRowKey}
copied={copied}
visible={visible}
confirmLoading={confirmLoading}
handleCancel={() => {
setVisible(false);
setSelectedRowKey(undefined);
setCopied(false);
}}
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);
setSelectedRowKey(undefined);
setCopied(false);
}
}}
/>
<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 Asset;
@@ -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;
@@ -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;
@@ -0,0 +1,3 @@
.asset-modal .ant-modal-body{
padding-top: 0 !important;
}
@@ -0,0 +1,831 @@
import React, {useEffect, useState} from 'react';
import {Collapse, Form, Input, InputNumber, Modal, Radio, Select, Switch, Tabs, Tooltip, Typography} from "antd";
import request from "../../common/request";
import assetApi from "../../api/asset";
import tagApi from "../../api/tag";
import credentialApi from "../../api/credential";
import arrays from "../../utils/array";
import strings from "../../utils/strings";
import {ControlOutlined, DesktopOutlined} from "@ant-design/icons";
import './AssetModal.css'
const {TextArea} = Input;
const {Option} = Select;
const {Text} = Typography;
const {Panel} = Collapse;
// 子级页面
// Ant form create 表单内置方法
const protocolMapping = {
'ssh': [
{text: '密码', value: 'custom'},
{text: '密钥', value: 'private-key'},
{text: '授权凭证', value: 'credential'},
],
'rdp': [{text: '密码', value: 'custom'}, {text: '授权凭证', value: 'credential'}],
'vnc': [{text: '密码', value: 'custom'}, {text: '授权凭证', value: 'credential'}],
'telnet': [{text: '密码', value: 'custom'}, {text: '授权凭证', value: 'credential'}]
}
const formLayout = {
labelCol: {span: 6},
wrapperCol: {span: 16},
};
const TELENETFormItemLayout = {
labelCol: {span: 6},
wrapperCol: {span: 14},
};
const AssetModal = function ({
visible,
handleOk,
handleCancel,
confirmLoading,
id,
copied
}) {
const [form] = Form.useForm();
let [accountType, setAccountType] = useState('custom');
let [protocol, setProtocol] = useState('rdp');
let [protocolOptions, setProtocolOptions] = useState(protocolMapping['rdp']);
let [useSSL, setUseSSL] = useState(false);
let [storages, setStorages] = useState([]);
let [enableDrive, setEnableDrive] = useState(false);
let [socksProxyEnable, setSocksProxyEnable] = useState(false);
let [accessGateways, setAccessGateways] = useState([]);
let [tags, setTags] = useState([]);
let [credentials, setCredentials] = useState([]);
const getStorages = async () => {
const result = await request.get('/storages/shares');
if (result.code === 1) {
setStorages(result['data']);
}
}
useEffect(() => {
const getItem = async () => {
let asset = await assetApi.getById(id);
if (asset) {
asset['use-ssl'] = asset['use-ssl'] === 'true';
asset['ignore-cert'] = asset['ignore-cert'] === 'true';
asset['enable-drive'] = asset['enable-drive'] === 'true';
asset['socks-proxy-enable'] = asset['socks-proxy-enable'] === 'true';
asset['force-lossless'] = asset['force-lossless'] === 'true';
for (let key in asset) {
if (asset.hasOwnProperty(key)) {
if (asset[key] === '-') {
asset[key] = '';
}
}
}
if (strings.hasText(asset['tags'])) {
asset['tags'] = asset['tags'].split(',');
} else {
asset['tags'] = [];
}
setAccountType(asset['accountType']);
if (asset['accountType'] === 'credential') {
getCredentials();
}
setProtocolOptions(protocolMapping[asset['protocol']]);
setProtocol(asset['protocol']);
setUseSSL(asset['use-ssl']);
setEnableDrive(asset['enable-drive']);
setSocksProxyEnable(asset['socks-proxy-enable']);
form.setFieldsValue(asset);
}
}
const getAccessGateways = async () => {
const result = await request.get('/access-gateways');
if (result.code === 1) {
setAccessGateways(result['data']);
}
}
const getTags = async () => {
let tags = await tagApi.getAll();
setTags(tags);
}
if (visible) {
if (id) {
getItem();
}
getTags();
getAccessGateways();
} else {
form.setFieldsValue({
'accountType': accountType,
'protocol': protocol,
'port': 3389,
'enable-drive': false,
'force-lossless': false,
'socks-proxy-enable': false,
'ignore-cert': false,
'use-ssl': false,
});
}
}, [visible]);
const handleProtocolChange = e => {
setProtocol(e.target.value)
let port;
switch (e.target.value) {
case 'ssh':
port = 22;
setProtocolOptions(protocolMapping['ssh']);
form.setFieldsValue({accountType: 'custom',});
handleAccountTypeChange('custom');
break;
case 'rdp':
port = 3389;
setProtocolOptions(protocolMapping['rdp']);
form.setFieldsValue({accountType: 'custom',});
handleAccountTypeChange('custom');
break;
case 'vnc':
port = 5900;
setProtocolOptions(protocolMapping['vnc']);
form.setFieldsValue({accountType: 'custom',});
handleAccountTypeChange('custom');
break;
case 'telnet':
port = 23;
setProtocolOptions(protocolMapping['telnet']);
form.setFieldsValue({accountType: 'custom',});
handleAccountTypeChange('custom');
break;
case 'kubernetes':
port = 6443;
break
default:
port = 65535;
}
form.setFieldsValue({
port: port,
});
};
const getCredentials = async () => {
let items = await credentialApi.getAll();
setCredentials(items);
}
const handleAccountTypeChange = v => {
setAccountType(v);
if (v === 'credential') {
getCredentials();
}
}
const basicView = <div className='basic' style={{marginTop: 16}}>
<Form.Item label="资产名称" name='name' rules={[{required: true, message: "请输入资产名称"}]}>
<Input placeholder="资产名称"/>
</Form.Item>
<Form.Item label="协议" name='protocol' rules={[{required: true, message: '请选择接入协议'}]}>
<Radio.Group onChange={handleProtocolChange}>
<Radio value="rdp">RDP</Radio>
<Radio value="ssh">SSH</Radio>
<Radio value="vnc">VNC</Radio>
<Radio value="telnet">Telnet</Radio>
<Radio value="kubernetes">Kubernetes</Radio>
</Radio.Group>
</Form.Item>
<Form.Item label="主机地址" rules={[{required: true, message: '请输入资产的主机名称和IP地址'}]}>
<Input.Group compact>
<Form.Item noStyle name='ip'>
<Input style={{width: '80%'}} placeholder="资产的主机名称或者IP地址"/>
</Form.Item>
<Form.Item noStyle name='port'>
<InputNumber style={{width: '20%'}} min={1} max={65535} placeholder='TCP端口'/>
</Form.Item>
</Input.Group>
</Form.Item>
{
protocol === 'kubernetes' ? <>
<Form.Item
name="namespace"
label="命名空间"
>
<Input type='text' placeholder="为空时默认使用default命名空间"/>
</Form.Item>
<Form.Item
name="pod"
label="pod"
rules={[{required: true, message: '请输入Pod名称'}]}
>
<Input type='text' placeholder="Kubernetes Pod的名称,其中包含与之相连的容器。"/>
</Form.Item>
<Form.Item
name="container"
label="容器"
>
<Input type='text' placeholder="为空时默认使用第一个容器"/>
</Form.Item>
</> : <>
<Form.Item label="账户类型" name='accountType'
rules={[{required: true, message: '请选择接账户类型'}]}>
<Select onChange={handleAccountTypeChange}>
{protocolOptions.map(item => {
return (
<Option key={item.value} value={item.value}>{item.text}</Option>)
})}
</Select>
</Form.Item>
{
accountType === 'credential' ?
<>
<Form.Item label="授权凭证" name='credentialId'
rules={[{required: true, message: '请选择授权凭证'}]}>
<Select onChange={() => null}>
{credentials.map(item => {
return (
<Option key={item.id} value={item.id}>
<Tooltip placement="topLeft" title={item.name}>
{item.name}
</Tooltip>
</Option>
);
})}
</Select>
</Form.Item>
</>
: null
}
{
accountType === 'custom' ?
<>
<input type='password' hidden={true} autoComplete='new-password'/>
<Form.Item label="授权账户" name='username'>
<Input autoComplete="off" placeholder="输入授权账户"/>
</Form.Item>
<Form.Item label="授权密码" name='password'>
<Input.Password autoComplete="off" placeholder="输入授权密码"/>
</Form.Item>
</>
: null
}
{
accountType === 'private-key' ?
<>
<Form.Item label="授权账户" name='username'>
<Input placeholder="输入授权账户"/>
</Form.Item>
<Form.Item label="私钥" name='privateKey'
rules={[{required: true, message: '请输入私钥'}]}>
<TextArea rows={4}/>
</Form.Item>
<Form.Item label="私钥密码" name='passphrase'>
<TextArea rows={1}/>
</Form.Item>
</>
: null
}
</>
}
<Form.Item label="接入网关" name='accessGatewayId' tooltip={'需要从接入网关才能访问的目标机器必选'}>
<Select onChange={() => null} allowClear={true}>
{accessGateways.map(item => {
return (
<Option key={item.id} value={item.id} placeholder={'需要从接入网关才能访问的目标机器必选'}>
<Tooltip placement="topLeft" title={item.name}>
{item.name}
</Tooltip>
</Option>
);
})}
</Select>
</Form.Item>
<Form.Item label="标签" name='tags'>
<Select mode="tags" placeholder="标签可以更加方便的检索资产">
{tags.map(tag => {
if (tag === '-') {
return undefined;
}
return (<Option key={tag}>{tag}</Option>)
})}
</Select>
</Form.Item>
<Form.Item label="备注" name='description'>
<TextArea rows={4} placeholder='关于资产的一些信息您可以写在这里'/>
</Form.Item>
</div>;
const advancedView = <div className='advanced'>
<Collapse
defaultActiveKey={['VNC中继', 'storage', '模式设置', '显示设置', '控制终端行为', 'socks']}
ghost>
{
protocol === 'rdp' ?
<>
<Panel header={<Text strong>显示设置</Text>} key="">
<Form.Item
name="color-depth"
label="色彩深度"
initialValue=""
>
<Select onChange={null}>
<Option value="">默认</Option>
<Option value="16">低色16</Option>
<Option value="24">真彩24</Option>
<Option value="32">真彩32</Option>
<Option value="8">256</Option>
</Select>
</Form.Item>
<Form.Item
name="force-lossless"
label="无损压缩"
valuePropName="checked"
rules={[
{
required: true,
},
]}
>
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
</Panel>
<Panel header={<Text strong>认证</Text>} key="">
<Form.Item
name="domain"
label='域'
>
<Input type='text' placeholder="身份验证时使用的域"/>
</Form.Item>
</Panel>
<Panel header={<Text strong>预连接 PDU (Hyper-V / VMConnect)</Text>} key="PDU">
<Form.Item
name="preconnection-id"
label='预连接ID'
>
<Input type='text' placeholder="RDP 源的数字 ID"/>
</Form.Item>
<Form.Item
name="preconnection-blob"
label='预连接字符'
>
<Input type='text' placeholder="标识 RDP 源的任意字符串"/>
</Form.Item>
</Panel>
<Panel header={<Text strong>Remote App</Text>} key="remote-app">
<Form.Item
name="remote-app"
label='程序'
tooltip="指定在远程桌面上启动的RemoteApp。
如果您的远程桌面服务器支持该应用程序,则该应用程序(且仅该应用程序)对用户可见。
Windows需要对远程应用程序的名称使用特殊的符号。
远程应用程序的名称必须以两个竖条作为前缀。
例如,如果您已经在您的服务器上为notepad.exe创建了一个远程应用程序,并将其命名为“notepad”,则您将该参数设置为:“||notepad”。"
>
<Input type='text' placeholder="remote app"/>
</Form.Item>
<Form.Item
name="remote-app-dir"
label='工作目录'
tooltip='remote app的工作目录,如果未配置remote app,此参数无效。'
>
<Input type='text' placeholder="remote app的工作目录"/>
</Form.Item>
<Form.Item
name="remote-app-args"
label='参数'
tooltip='remote app的命令行参数,如果未配置remote app,此参数无效。'
>
<Input type='text' placeholder="remote app的命令行参数"/>
</Form.Item>
</Panel>
<Panel header={<Text strong>映射网络驱动器</Text>} key="storage">
<Form.Item
name="enable-drive"
label="启用"
valuePropName="checked"
>
<Switch checkedChildren="开启" unCheckedChildren="关闭"
onChange={async (checked, event) => {
setEnableDrive(checked);
if (checked === true) {
getStorages();
}
}}/>
</Form.Item>
{
enableDrive ?
<Form.Item
name="drive-path"
label="映射空间"
extra='用于文件传输的映射网络驱动器,为空时使用操作人的默认空间'
>
<Select onChange={null} allowClear
placeholder='为空时使用操作人的默认空间'>
{
storages.map(item => {
return <Option
value={item['id']}>{item['name']}</Option>
})
}
</Select>
</Form.Item> : undefined
}
</Panel>
</> : undefined
}
{
protocol === 'ssh' ?
<>
<Panel header={<Text strong>Socks 代理</Text>} key="socks">
<Form.Item name='ssh-mode' noStyle>
<Input hidden={true} value={'native'}/>
</Form.Item>
<Form.Item
name="socks-proxy-enable"
label="使用Socks代理"
valuePropName="checked"
>
<Switch checkedChildren="是" unCheckedChildren="否"
onChange={(checked, event) => {
setSocksProxyEnable(checked);
}}/>
</Form.Item>
{
socksProxyEnable ? <>
<Form.Item label="代理地址" name='socks-proxy-host'
rules={[{required: true}]}>
<Input placeholder="Socks 代理的主机地址"/>
</Form.Item>
<Form.Item label="代理端口" name='socks-proxy-port'
rules={[{required: true}]}>
<InputNumber min={1} max={65535}
placeholder='Socks 代理的主机端口'/>
</Form.Item>
<input type='password' hidden={true}
autoComplete='new-password'/>
<Form.Item label="代理账号" name='socks-proxy-username'>
<Input autoComplete="off" placeholder="代理账号,没有可以不填"/>
</Form.Item>
<Form.Item label="代理密码" name='socks-proxy-password'>
<Input.Password autoComplete="off"
placeholder="代理密码,没有可以不填"/>
</Form.Item>
</> : undefined
}
</Panel>
</> : undefined
}
{
protocol === 'vnc' ?
<>
<Panel header={<Text strong>显示设置</Text>} key="">
<Form.Item
name="color-depth"
label="色彩深度"
initialValue=""
>
<Select onChange={null}>
<Option value="">默认</Option>
<Option value="16">低色16</Option>
<Option value="24">真彩24</Option>
<Option value="32">真彩32</Option>
<Option value="8">256</Option>
</Select>
</Form.Item>
<Form.Item
name="cursor"
label="光标"
initialValue=""
>
<Select onChange={null}>
<Option value="">默认</Option>
<Option value="local">本地</Option>
<Option value="remote">远程</Option>
</Select>
</Form.Item>
</Panel>
<Panel header={<Text strong>VNC中继</Text>} key="VNC">
<Form.Item label='目标主机'
tooltip='连接到VNC代理(例如UltraVNC Repeater)时要请求的目标主机。'
name='dest-host'>
<Input placeholder="目标主机"/>
</Form.Item>
<Form.Item label='目标端口'
tooltip='连接到VNC代理(例如UltraVNC Repeater)时要请求的目标端口。'
name='dest-port'>
<Input type='number' min={1} max={65535}
placeholder='目标端口'/>
</Form.Item>
</Panel>
</> : undefined
}
{
protocol === 'telnet' ?
<>
<Panel header={<Text strong>认证</Text>} key="">
<Form.Item
{...TELENETFormItemLayout}
name="username-regex"
label="用户名正则表达式"
>
<Input type='text' placeholder=""/>
</Form.Item>
<Form.Item
{...TELENETFormItemLayout}
name="password-regex"
label="密码正则表达式"
>
<Input type='text' placeholder=""/>
</Form.Item>
<Form.Item
{...TELENETFormItemLayout}
name="login-success-regex"
label="登录成功正则表达式"
>
<Input type='text' placeholder=""/>
</Form.Item>
<Form.Item
{...TELENETFormItemLayout}
name="login-failure-regex"
label="登录失败正则表达式"
>
<Input type='text' placeholder=""/>
</Form.Item>
</Panel>
<Panel header={<Text strong>显示设置</Text>} key="">
<Form.Item
name="color-scheme"
label="配色方案"
initialValue=""
>
<Select onChange={null}>
<Option value="">默认</Option>
<Option value="gray-black">黑底灰字</Option>
<Option value="green-black">黑底绿字</Option>
<Option value="white-black">黑底白字</Option>
<Option value="black-white">白底黑字</Option>
</Select>
</Form.Item>
<Form.Item
name="font-name"
label="字体名称"
>
<Input type='text' placeholder="为空时使用系统默认字体"/>
</Form.Item>
<Form.Item
name="font-size"
label="字体大小"
>
<Input type='number' placeholder="为空时使用系统默认字体大小" min={8} max={96}/>
</Form.Item>
</Panel>
<Panel header={<Text strong>控制终端行为</Text>} key="">
<Form.Item
name="backspace"
label="退格键映射"
initialValue=""
>
<Select onChange={null}>
<Option value="">默认</Option>
<Option value="127">删除键(Ctrl-?)</Option>
<Option value="8">退格键(Ctrl-H)</Option>
</Select>
</Form.Item>
<Form.Item
name="terminal-type"
label="终端类型"
initialValue=""
>
<Select onChange={null}>
<Option value="">默认</Option>
<Option value="ansi">ansi</Option>
<Option value="linux">linux</Option>
<Option value="vt100">vt100</Option>
<Option value="vt220">vt220</Option>
<Option value="xterm">xterm</Option>
<Option value="xterm-256color">xterm-256color</Option>
</Select>
</Form.Item>
</Panel>
</> : undefined
}
{
protocol === 'kubernetes' ?
<>
<Panel header={<Text strong>认证</Text>} key="">
<Form.Item
name="use-ssl"
label="使用SSL"
valuePropName="checked"
>
<Switch checkedChildren="是" unCheckedChildren="否"
onChange={(checked, event) => {
setUseSSL(checked);
}}/>
</Form.Item>
{
useSSL ?
<>
<Form.Item
name="client-cert"
label="client-cert"
>
<Input type='text' placeholder=""/>
</Form.Item>
<Form.Item
name="client-key"
label="client-key"
>
<Input type='text' placeholder=""/>
</Form.Item>
<Form.Item
name="ca-cert"
label="ca-cert"
>
<Input type='text' placeholder=""/>
</Form.Item>
</> : undefined
}
<Form.Item
name="ignore-cert"
label="忽略证书"
valuePropName="checked"
>
<Switch checkedChildren="是" unCheckedChildren="否"
onChange={(checked, event) => {
}}/>
</Form.Item>
</Panel>
<Panel header={<Text strong>显示设置</Text>} key="">
<Form.Item
name="color-scheme"
label="配色方案"
initialValue=""
>
<Select onChange={null}>
<Option value="">默认</Option>
<Option value="gray-black">黑底灰字</Option>
<Option value="green-black">黑底绿字</Option>
<Option value="white-black">黑底白字</Option>
<Option value="black-white">白底黑字</Option>
</Select>
</Form.Item>
<Form.Item
name="font-name"
label="字体名称"
>
<Input type='text' placeholder="为空时使用系统默认字体"/>
</Form.Item>
<Form.Item
name="font-size"
label="字体大小"
>
<Input type='number' placeholder="为空时使用系统默认字体大小" min={8} max={96}/>
</Form.Item>
</Panel>
<Panel header={<Text strong>控制终端行为</Text>} key="">
<Form.Item
name="backspace"
label="退格键映射"
initialValue=""
>
<Select onChange={null}>
<Option value="">默认</Option>
<Option value="127">删除键(Ctrl-?)</Option>
<Option value="8">退格键(Ctrl-H)</Option>
</Select>
</Form.Item>
<Form.Item
name="terminal-type"
label="终端类型"
initialValue=""
>
<Select onChange={null}>
<Option value="">默认</Option>
<Option value="ansi">ansi</Option>
<Option value="linux">linux</Option>
<Option value="vt100">vt100</Option>
<Option value="vt220">vt220</Option>
<Option value="xterm">xterm</Option>
<Option value="xterm-256color">xterm-256color</Option>
</Select>
</Form.Item>
</Panel>
</> : undefined
}
</Collapse>
</div>;
return (
<Modal
className={'asset-modal'}
title={id && copied === false ? '更新资产' : '新建资产'}
visible={visible}
maskClosable={false}
destroyOnClose={true}
centered
width={700}
onOk={() => {
form
.validateFields()
.then(async values => {
if (copied === true) {
values['id'] = undefined;
}
console.log(values['tags'], arrays.isEmpty(values['tags']))
if (!arrays.isEmpty(values['tags'])) {
values.tags = values['tags'].join(',');
} else {
values.tags = '';
}
form.resetFields();
await handleOk(values);
});
}}
onCancel={() => {
form.resetFields();
handleCancel();
}}
confirmLoading={confirmLoading}
okText='确定'
cancelText='取消'
>
<Form form={form} {...formLayout}>
<Form.Item name='id' noStyle>
<Input hidden={true}/>
</Form.Item>
<Tabs
defaultActiveKey="basic"
items={[
{
label: <span><DesktopOutlined/>基础信息</span>,
key: 'basic',
children: basicView,
},
{
label: <span><ControlOutlined/>高级配置</span>,
key: 'advanced',
children: advancedView,
},
]}
/>
</Form>
</Modal>
)
}
export default AssetModal;
@@ -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={{
defaultPageSize: 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;
@@ -0,0 +1,119 @@
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])
let strategyOptions = strategies.map(item => {
return {
value: item.id,
label: item.name
}
});
let userOptions = users.map(item => {
return {
value: item.id,
label: item.nickname,
disabled: selectedUserIds.includes(item.id)
}
});
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="请选择用户"
showSearch
filterOption={(input, option) =>
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
}
options={userOptions}
>
</Select>
</Form.Item>
<Form.Item label="授权策略" name='strategyId' extra={'可控制授权用户上传下载文件等功能'}>
<Select
allowClear
style={{width: '100%'}}
placeholder="此字段不是必填的"
showSearch
filterOption={(input, option) =>
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
}
options={strategyOptions}
>
</Select>
</Form.Item>
</Form>
</Modal>
)
};
export default AssetUserBind;
@@ -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={{
defaultPageSize: 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;
@@ -0,0 +1,120 @@
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";
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 [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);
}
if (visible) {
fetchData();
} else {
form.resetFields();
}
}, [visible])
let strategyOptions = strategies.map(item => {
return {
value: item.id,
label: item.name
}
});
let userGroupOptions = userGroups.map(item => {
return {
value: item.id,
label: item.name,
disabled: selectedUserGroupIds.includes(item.id)
}
});
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="请选择用户组"
showSearch
filterOption={(input, option) =>
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
}
options={userGroupOptions}
>
</Select>
</Form.Item>
<Form.Item label="授权策略" name='strategyId' extra={'可控制授权用户上传下载文件等功能'}>
<Select
allowClear
style={{width: '100%'}}
placeholder="此字段不是必填的"
showSearch
filterOption={(input, option) =>
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
}
options={strategyOptions}
>
</Select>
</Form.Item>
</Form>
</Modal>
)
};
export default AssetUserGroupBind;
@@ -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
View File
@@ -0,0 +1,233 @@
import React, {useState} from 'react';
import {Button, Layout, message, Popconfirm} from "antd";
import {ProTable} from "@ant-design/pro-components";
import commandApi from "../../api/command";
import CommandModal from "./CommandModal";
import SelectingAsset from "./SelectingAsset";
import ColumnState, {useColumnState} from "../../hook/column-state";
import Show from "../../dd/fi/show";
import ChangeOwner from "./ChangeOwner";
const {Content} = Layout;
const api = commandApi;
const actionRef = React.createRef();
const Command = () => {
let [assetVisible, setAssetVisible] = useState(false);
let [visible, setVisible] = useState(false);
let [confirmLoading, setConfirmLoading] = useState(false);
let [selectedRowKey, setSelectedRowKey] = useState(undefined);
const [columnsStateMap, setColumnsStateMap] = useColumnState(ColumnState.CREDENTIAL);
let [selectedRow, setSelectedRow] = useState(undefined);
let [changeOwnerVisible, setChangeOwnerVisible] = useState(false);
const columns = [
{
dataIndex: 'index',
valueType: 'indexBorder',
width: 48,
},
{
title: '名称',
dataIndex: 'name',
}, {
title: '内容',
dataIndex: 'content',
key: 'content',
copyable: true,
ellipsis: true
}, {
title: '所有者',
dataIndex: 'ownerName',
key: 'ownerName',
hideInSearch: true
},
{
title: '创建时间',
key: 'created',
dataIndex: 'created',
hideInSearch: true,
},
{
title: '操作',
valueType: 'option',
key: 'option',
render: (text, record, _, action) => [
<Show menu={'command-exec'} key={'command-exec'}>
<a
key="run"
onClick={() => {
setAssetVisible(true);
setSelectedRowKey(record['id']);
}}
>
执行
</a>
</Show>,
<Show menu={'command-edit'} key={'command-edit'}>
<a
key="edit"
onClick={() => {
setVisible(true);
setSelectedRowKey(record['id']);
}}
>
编辑
</a>
</Show>,
<Show menu={'command-change-owner'} key={'command-change-owner'}>
<a
key="change-owner"
onClick={() => {
handleChangeOwner(record);
}}
>
更换所有者
</a>
</Show>,
<Show menu={'command-del'} key={'command-del'}>
<Popconfirm
key={'confirm-delete'}
title="您确认要删除此行吗?"
onConfirm={async () => {
await api.deleteById(record.id);
actionRef.current.reload();
}}
okText="确认"
cancelText="取消"
>
<a key='delete' className='danger'>删除</a>
</Popconfirm>
</Show>,
],
},
];
const handleChangeOwner = (row) => {
setSelectedRow(row);
setChangeOwnerVisible(true);
}
return (<Content className="page-container">
<ProTable
columns={columns}
actionRef={actionRef}
columnsState={{
value: columnsStateMap,
onChange: setColumnsStateMap
}}
request={async (params = {}, sort, filter) => {
let field = '';
let order = '';
if (Object.keys(sort).length > 0) {
field = Object.keys(sort)[0];
order = Object.values(sort)[0];
}
let queryParams = {
pageIndex: params.current,
pageSize: params.pageSize,
name: params.name,
field: field,
order: order
}
let result = await api.getPaging(queryParams);
return {
data: result['items'],
success: true,
total: result['total']
};
}}
rowKey="id"
search={{
labelWidth: 'auto',
}}
pagination={{
defaultPageSize: 10,
}}
dateFormatter="string"
headerTitle="动态指令列表"
toolBarRender={() => [
<Show menu={'command-add'}>
<Button key="button" type="primary" onClick={() => {
setVisible(true)
}}>
新建
</Button>
</Show>,
]}
/>
<CommandModal
id={selectedRowKey}
visible={visible}
confirmLoading={confirmLoading}
handleCancel={() => {
setVisible(false);
setSelectedRowKey(undefined);
}}
handleOk={async (values) => {
setConfirmLoading(true);
try {
let success;
if (values['id']) {
success = await api.updateById(values['id'], values);
} else {
success = await api.create(values);
}
if (success) {
setVisible(false);
}
actionRef.current.reload();
} finally {
setConfirmLoading(false);
}
}}
/>
<SelectingAsset
visible={assetVisible}
handleCancel={() => {
setAssetVisible(false);
setSelectedRowKey(undefined);
}}
handleOk={(rows) => {
if (rows.length === 0) {
message.warning('请至少选择一个资产');
return;
}
let cAssets = rows.map(item => {
return {
id: item['id'],
name: item['name']
}
});
window.location.href = '#/execute-command?commandId=' + selectedRowKey + '&assets=' + JSON.stringify(cAssets);
}}
/>
<ChangeOwner
lastOwner={selectedRow?.owner}
open={changeOwnerVisible}
handleOk={async (owner) => {
let success = await api.changeOwner(selectedRow?.id, owner);
if (success) {
setChangeOwnerVisible(false);
actionRef.current.reload();
}
}}
handleCancel={() => {
setChangeOwnerVisible(false);
}}
/>
</Content>);
};
export default Command;
@@ -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;
@@ -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;
@@ -0,0 +1,134 @@
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 = ({
visible,
handleOk,
handleCancel,
confirmLoading,
id,
}) => {
const [form] = Form.useForm();
const formItemLayout = {
labelCol: {span: 6},
wrapperCol: {span: 14},
};
let [type, setType] = useState('');
const handleAccountTypeChange = v => {
setType(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={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='type' rules={[{required: true, message: '请选择接账户类型'}]}>
<Select onChange={handleAccountTypeChange}>
{accountTypes.map(item => {
return (<Select.Option key={item.value} value={item.value}>{item.text}</Select.Option>)
})}
</Select>
</Form.Item>
{
type === 'private-key' ?
<>
<Form.Item label="授权账户" name='username'>
<Input placeholder="输入授权账户"/>
</Form.Item>
<Form.Item label="私钥" name='privateKey' rules={[{required: true, message: '请输入私钥'}]}>
<TextArea rows={4}/>
</Form.Item>
<Form.Item label="私钥密码" name='passphrase'>
<TextArea rows={1}/>
</Form.Item>
</>
:
<>
<input type='password' hidden={true} autoComplete='new-password'/>
<Form.Item label="授权账户" name='username'>
<Input placeholder="输入授权账户"/>
</Form.Item>
<Form.Item label="授权密码" name='password'>
<Input.Password placeholder="输入授权密码"/>
</Form.Item>
</>
}
</Form>
</Modal>
)
};
export default CredentialModal;
@@ -0,0 +1,249 @@
import React, {useState} from 'react';
import {Badge, Modal, Select, 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";
import strings from "../../utils/strings";
import {useQuery} from "react-query";
import tagApi from "../../api/tag";
const {Title} = Typography;
const actionRef = React.createRef();
const SelectingAsset = ({
visible,
handleOk,
handleCancel,
confirmLoading,
id,
}) => {
let [rows, setRows] = useState([]);
const tagQuery = useQuery('getAllTag', tagApi.getAll);
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 (strings.hasText(tags)) {
return tags.split(',').filter(tag => tag !== '-').map(tag => <Tag key={tag}>{tag}</Tag>);
}
},
renderFormItem: (item, {type, defaultRender, ...rest}, form) => {
if (type === 'form') {
return null;
}
return (
<Select mode="multiple"
allowClear>
{
tagQuery.data?.map(tag => {
if (tag === '-') {
return undefined;
}
return <Select.Option key={tag}>{tag}</Select.Option>
})
}
</Select>
);
},
}, {
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>
)
}
},
renderFormItem: (item, {type, defaultRender, ...rest}, form) => {
if (type === 'form') {
return null;
}
return (
<Select>
<Select.Option value="true">运行中</Select.Option>
<Select.Option value="false">不可用</Select.Option>
</Select>
);
},
}, {
title: '所有者',
dataIndex: 'ownerName',
key: 'ownerName',
hideInSearch: true,
}, {
title: '创建时间',
dataIndex: 'created',
key: 'created',
hideInSearch: true,
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',
active: params.active,
'tags': params.tags?.join(','),
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;
@@ -0,0 +1,237 @@
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-detail'} key={'strategy-get'}>
<Link key="get" to={`/strategy/${record['id']}`}>详情</Link>
</Show>
,
<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={{
defaultPageSize: 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;
@@ -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;
@@ -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;
@@ -0,0 +1,106 @@
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 = ({visible, handleOk, handleCancel, confirmLoading, id}) => {
const [form] = Form.useForm();
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={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 autoComplete="off" placeholder="授权策略名称"/>
</Form.Item>
<Form.Item label="上传" name='upload' rules={[{required: true}]} valuePropName="checked">
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
<Form.Item label="下载" name='download' rules={[{required: true}]} valuePropName="checked">
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
<Form.Item label="编辑" name='edit' rules={[{required: true}]} valuePropName="checked"
tooltip={'编辑需要先开启下载'}>
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
<Form.Item label="删除" name='delete' rules={[{required: true}]} valuePropName="checked">
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
<Form.Item label="重命名" name='rename' rules={[{required: true}]} valuePropName="checked">
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
<Form.Item label="复制" name='copy' rules={[{required: true}]} valuePropName="checked">
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
<Form.Item label="粘贴" name='paste' rules={[{required: true}]} valuePropName="checked">
<Switch checkedChildren="开启" unCheckedChildren="关闭"/>
</Form.Item>
</Form>
</Modal>
)
};
export default StrategyModal;
@@ -0,0 +1,3 @@
.pie-card .ant-pro-card-body {
padding: 16px !important;
}
@@ -0,0 +1,211 @@
import React, {Component} from 'react';
import {DesktopOutlined, DisconnectOutlined, LoginOutlined, UserOutlined} from '@ant-design/icons';
import request from "../../common/request";
import './Dashboard.css'
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: {
onlineUser: 0,
totalUser: 0,
activeAsset: 0,
totalAsset: 0,
failLoginCount: 0,
offlineSession: 0,
},
asset: {
"ssh": 0,
"rdp": 0,
"vnc": 0,
"telnet": 0,
"kubernetes": 0,
},
dateCounter: [],
}
componentDidMount() {
this.getCounter();
this.getAsset();
this.getDateCounter('week');
}
componentWillUnmount() {
}
getCounter = async () => {
let result = await request.get('/overview/counter');
if (result['code'] === 1) {
this.setState({
counter: result['data']
})
}
}
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) {
this.setState({
asset: result['data']
})
}
}
handleChangeDateCounter = (value) => {
if(value === '按周'){
this.getDateCounter('week');
}else {
this.getDateCounter('month');
}
}
render() {
const assetData = [
{
type: 'RDP',
value: this.state.asset['rdp'],
},
{
type: 'SSH',
value: this.state.asset['ssh'],
},
{
type: 'TELNET',
value: this.state.asset['telnet'],
},
{
type: 'VNC',
value: this.state.asset['vnc'],
},
{
type: 'Kubernetes',
value: this.state.asset['kubernetes'],
}
];
const assetConfig = {
width: 200,
height: 200,
appendPadding: 10,
data: assetData,
angleField: 'value',
colorField: 'type',
radius: 1,
innerRadius: 0.6,
label: {
type: 'inner',
offset: '-50%',
content: '{value}',
style: {
textAlign: 'center',
fontSize: 14,
},
},
interactions: [{type: 'element-selected'}, {type: 'element-active'}],
statistic: {
title: false,
content: {
formatter: () => {
return '资产类型';
},
style: {
fontSize: 18,
}
},
},
};
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,
},
},
};
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>
</ProCard>
<ProCard title="会话统计" style={{marginTop: 16}}
extra={<Segmented options={['按周', '按月']} onChange={this.handleChangeDateCounter}/>}>
<Line {...dateCounterConfig} />
</ProCard>
</div>
</>);
}
}
export default Dashboard;
@@ -0,0 +1,3 @@
.ant-pro-card-body {
padding: 16px !important;
}
@@ -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: 5000
});
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;
@@ -0,0 +1,201 @@
import React, {useState} from 'react';
import {Badge, Divider, Layout, 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;
@@ -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;
}
@@ -0,0 +1,240 @@
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";
import strings from "../../utils/strings";
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),{
onSuccess: data => {
let commands = data.content.split('\n');
if (!commands) {
return;
}
items.forEach(item => {
if (getReady(item['id']) === false) {
initTerm(item['id'], commands);
}
})
},
refetchOnWindowFocus: false
});
let [inputValue, setInputValue] = useState('');
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(() => {
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, commands) => {
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);
for (let i = 0; i < commands.length; i++) {
let command = commands[i];
if (!strings.hasText(command)) {
continue
}
webSocket.send(new Message(Message.Data, command + String.fromCharCode(13)).toString());
}
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;
@@ -0,0 +1,92 @@
.dode {
-webkit-user-select: none;
-moz-user-select: none;
-o-user-select: none;
-ms-user-select: none;
}
@-webkit-keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.popup {
-webkit-animation-name: fadeIn;
animation-name: fadeIn;
animation-duration: 0.4s;
background-clip: padding-box;
background-color: #fff;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.75);
left: 0;
list-style-type: none;
margin: 0;
outline: none;
padding: 0;
position: fixed;
text-align: left;
top: 0;
overflow: hidden;
-webkit-box-shadow: 0 2px 8px rgba(0, 0, 0, 0.75);
}
.popup li {
clear: both;
/*color: rgba(0, 0, 0, 0.65);*/
cursor: pointer;
font-size: 14px;
font-weight: normal;
line-height: 22px;
margin: 0;
padding: 5px 12px;
transition: all .3s;
white-space: nowrap;
-webkit-transition: all .3s;
}
.popup li:hover {
background-color: #e6f7ff;
}
.popup li > i {
margin-right: 8px;
}
.fs-header {
align-items: center;
position: relative;
display: flex;
}
.fs-header-left{
flex: 1 1 0;
}
.fs-header-right{
text-align: right;
margin-left: 10px;
}
.fs-header-right-item {
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
vertical-align: middle;
height: 100%;
}
@@ -0,0 +1,883 @@
import React, {Component, lazy, Suspense} from 'react';
import {
Button,
Card,
Form,
Input,
message,
Modal,
notification,
Popconfirm,
Progress,
Space,
Table,
Tooltip,
Typography
} from "antd";
import {
CloudUploadOutlined,
DeleteOutlined,
ExclamationCircleOutlined,
FileExcelOutlined,
FileImageOutlined,
FileMarkdownOutlined,
FileOutlined,
FilePdfOutlined,
FileTextOutlined,
FileWordOutlined,
FileZipOutlined,
FolderAddOutlined,
FolderTwoTone,
LinkOutlined,
ReloadOutlined,
UploadOutlined
} from "@ant-design/icons";
import qs from "qs";
import request from "../../common/request";
import {server} from "../../common/env";
import {download, getFileName, getToken, isEmpty, renderSize} from "../../utils/utils";
import './FileSystem.css';
import Landing from "../Landing";
const MonacoEditor = lazy(() => import('react-monaco-editor'));
const {Text} = Typography;
const confirm = Modal.confirm;
class FileSystem extends Component {
mkdirFormRef = React.createRef();
renameFormRef = React.createRef();
state = {
storageType: undefined,
storageId: undefined,
currentDirectory: '/',
currentDirectoryInput: '/',
files: [],
loading: false,
currentFileKey: undefined,
selectedRowKeys: [],
uploading: {},
callback: undefined,
minHeight: 280,
upload: false,
download: false,
delete: false,
rename: false,
edit: false,
editorVisible: false,
fileName: '',
fileContent: ''
}
componentDidMount() {
if (this.props.onRef) {
this.props.onRef(this);
}
if (!this.props.storageId) {
return
}
this.setState({
storageId: this.props.storageId,
storageType: this.props.storageType,
callback: this.props.callback,
minHeight: this.props.minHeight,
upload: this.props.upload,
download: this.props.download,
delete: this.props.delete,
rename: this.props.rename,
edit: this.props.edit,
}, () => {
this.loadFiles(this.state.currentDirectory);
});
}
reSetStorageId = (storageId) => {
this.setState({
storageId: storageId
}, () => {
this.loadFiles('/');
});
}
refresh = async () => {
this.loadFiles(this.state.currentDirectory);
if (this.state.callback) {
this.state.callback();
}
}
loadFiles = async (key) => {
this.setState({
loading: true
})
try {
if (isEmpty(key)) {
key = '/';
}
let formData = new FormData();
formData.append('dir', key);
let result = await request.post(`/${this.state.storageType}/${this.state.storageId}/ls`, formData);
if (result['code'] !== 1) {
message.error(result['message']);
return;
}
let data = result['data'];
const items = data.map(item => {
return {'key': item['path'], ...item}
});
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 !== '/') {
dirs.splice(0, 0, {key: '..', name: '..', path: '..', isDir: true, disabled: true})
}
this.setState({
files: dirs,
currentDirectory: key,
currentDirectoryInput: key
})
} finally {
this.setState({
loading: false,
selectedRowKeys: []
})
}
}
handleCurrentDirectoryInputChange = (event) => {
this.setState({
currentDirectoryInput: event.target.value
})
}
handleCurrentDirectoryInputPressEnter = (event) => {
this.loadFiles(event.target.value);
}
handleUploadDir = () => {
let files = window.document.getElementById('dir-upload').files;
let uploadEndCount = 0;
const increaseUploadEndCount = () => {
uploadEndCount++;
return uploadEndCount;
}
for (let i = 0; i < files.length; i++) {
let relativePath = files[i]['webkitRelativePath'];
let dir = relativePath.substring(0, relativePath.length - files[i].name.length);
this.uploadFile(files[i], this.state.currentDirectory + '/' + dir, () => {
if (increaseUploadEndCount() === files.length) {
this.refresh();
}
});
}
}
handleUploadFile = () => {
let files = window.document.getElementById('file-upload').files;
let uploadEndCount = 0;
const increaseUploadEndCount = () => {
uploadEndCount++;
return uploadEndCount;
}
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (!file) {
return;
}
this.uploadFile(file, this.state.currentDirectory, () => {
if (increaseUploadEndCount() === files.length) {
this.refresh();
}
});
}
}
uploadFile = (file, dir, callback) => {
const {name, size} = file;
let url = `${server}/${this.state.storageType}/${this.state.storageId}/upload?X-Auth-Token=${getToken()}&dir=${dir}`
const key = name;
const xhr = new XMLHttpRequest();
let prevPercent = 0, percent = 0;
const uploadEnd = (success, message) => {
if (success) {
let description = (
<React.Fragment>
<div>{name}</div>
<div>{renderSize(size)} / {renderSize(size)}</div>
<Progress percent={100}/>
</React.Fragment>
);
notification.success({
key,
message: `上传成功`,
duration: 5,
description: description,
placement: 'bottomRight'
});
if (callback) {
callback();
}
} else {
let description = (
<React.Fragment>
<div>{name}</div>
<Text type="danger">{message}</Text>
</React.Fragment>
);
notification.error({
key,
message: `上传失败`,
duration: 10,
description: description,
placement: 'bottomRight'
});
}
}
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
let description = (
<React.Fragment>
<div>{name}</div>
<div>{renderSize(event.loaded)}/{renderSize(size)}</div>
<Progress percent={99}/>
</React.Fragment>
);
if (event.loaded === event.total) {
notification.info({
key,
message: `向目标机器传输中...`,
duration: null,
description: description,
placement: 'bottomRight',
onClose: () => {
xhr.abort();
message.info(`您已取消上传"${name}"`, 10);
}
});
return;
}
percent = Math.min(Math.floor(event.loaded * 100 / event.total), 99);
if (prevPercent === percent) {
return;
}
description = (
<React.Fragment>
<div>{name}</div>
<div>{renderSize(event.loaded)} / {renderSize(size)}</div>
<Progress percent={percent}/>
</React.Fragment>
);
notification.info({
key,
message: `上传中...`,
duration: null,
description: description,
placement: 'bottomRight',
onClose: () => {
xhr.abort();
message.info(`您已取消上传"${name}"`, 10);
}
});
prevPercent = percent;
}
}, false)
xhr.onreadystatechange = (data) => {
if (xhr.readyState !== 4) {
let responseText = data.currentTarget.responseText;
let result = responseText.split(``).filter(item => item !== '');
if (result.length > 0) {
let upload = result[result.length - 1];
let uploadToTarget = parseInt(upload);
percent = Math.min(Math.floor(uploadToTarget * 100 / size), 99);
let description = (
<React.Fragment>
<div>{name}</div>
<div>{renderSize(uploadToTarget)}/{renderSize(size)}</div>
<Progress percent={percent}/>
</React.Fragment>
);
notification.info({
key,
message: `向目标机器传输中...`,
duration: null,
description: description,
placement: 'bottomRight',
onClose: () => {
xhr.abort();
message.info(`您已取消上传"${name}"`, 10);
}
});
}
return;
}
if (xhr.status >= 200 && xhr.status < 300) {
uploadEnd(true, `上传成功`);
} else if (xhr.status >= 400 && xhr.status < 500) {
uploadEnd(false, '服务器内部错误');
}
}
xhr.onerror = () => {
uploadEnd(false, '服务器内部错误');
}
xhr.open('POST', url, true);
let formData = new FormData();
formData.append("file", file, name);
xhr.send(formData);
}
delete = async (key) => {
let formData = new FormData();
formData.append('file', key);
let result = await request.post(`/${this.state.storageType}/${this.state.storageId}/rm`, formData);
if (result['code'] !== 1) {
message.error(result['message']);
}
}
showEditor = async (name, key) => {
message.loading({key: key, content: 'Loading'})
let fileContent = await request.get(`${server}/${this.state.storageType}/${this.state.storageId}/download?file=${window.encodeURIComponent(key)}&t=${new Date().getTime()}`);
this.setState({
currentFileKey: key,
fileName: name,
fileContent: fileContent + "",
editorVisible: true
})
message.destroy(key);
}
hideEditor = () => {
this.setState({
editorVisible: false,
fileName: '',
fileContent: '',
currentFileKey: ''
})
}
edit = async () => {
this.setState({
confirmLoading: true
})
let url = `${server}/${this.state.storageType}/${this.state.storageId}/edit`
let formData = new FormData();
formData.append('file', this.state.currentFileKey);
formData.append('fileContent', this.state.fileContent);
let result = await request.post(url, formData);
if (result['code'] !== 1) {
message.error(result['message']);
}
this.setState({
confirmLoading: false
})
this.hideEditor();
}
render() {
const columns = [
{
title: '名称',
dataIndex: 'name',
key: 'name',
render: (value, item) => {
let icon;
if (item['isDir']) {
icon = <FolderTwoTone/>;
} else {
if (item['isLink']) {
icon = <LinkOutlined/>;
} else {
const fileExtension = item['name'].split('.').pop().toLowerCase();
switch (fileExtension) {
case "doc":
case "docx":
icon = <FileWordOutlined/>;
break;
case "xls":
case "xlsx":
icon = <FileExcelOutlined/>;
break;
case "bmp":
case "jpg":
case "jpeg":
case "png":
case "tif":
case "gif":
case "pcx":
case "tga":
case "exif":
case "svg":
case "psd":
case "ai":
case "webp":
icon = <FileImageOutlined/>;
break;
case "md":
icon = <FileMarkdownOutlined/>;
break;
case "pdf":
icon = <FilePdfOutlined/>;
break;
case "txt":
icon = <FileTextOutlined/>;
break;
case "zip":
case "gz":
case "tar":
case "tgz":
icon = <FileZipOutlined/>;
break;
default:
icon = <FileOutlined/>;
break;
}
}
}
return <span className={'dode'}>{icon}&nbsp;&nbsp;{item['name']}</span>;
},
sorter: (a, b) => {
if (a['key'] === '..') {
return 0;
}
if (b['key'] === '..') {
return 0;
}
return a.name.localeCompare(b.name);
},
sortDirections: ['descend', 'ascend'],
},
{
title: '大小',
dataIndex: 'size',
key: 'size',
render: (value, item) => {
if (!item['isDir'] && !item['isLink']) {
return <span className={'dode'}>{renderSize(value)}</span>;
}
return <span className={'dode'}/>;
},
sorter: (a, b) => {
if (a['key'] === '..') {
return 0;
}
if (b['key'] === '..') {
return 0;
}
return a.size - b.size;
},
}, {
title: '修改日期',
dataIndex: 'modTime',
key: 'modTime',
sorter: (a, b) => {
if (a['key'] === '..') {
return 0;
}
if (b['key'] === '..') {
return 0;
}
return a.modTime.localeCompare(b.modTime);
},
sortDirections: ['descend', 'ascend'],
render: (value, item) => {
return <span className={'dode'}>{value}</span>;
},
}, {
title: '属性',
dataIndex: 'mode',
key: 'mode',
render: (value, item) => {
return <span className={'dode'}>{value}</span>;
},
}, {
title: '操作',
dataIndex: 'action',
key: 'action',
width: 210,
render: (value, item) => {
if (item['key'] === '..') {
return undefined;
}
let disableDownload = !this.state.download;
let disableEdit = !this.state.edit;
if (item['isDir'] || item['isLink']) {
disableDownload = true;
disableEdit = true
}
return (
<>
<Button type="link" size='small' disabled={disableEdit}
onClick={() => this.showEditor(item['name'], item['key'])}>
编辑
</Button>
<Button type="link" size='small' disabled={disableDownload} onClick={async () => {
download(`${server}/${this.state.storageType}/${this.state.storageId}/download?file=${window.encodeURIComponent(item['key'])}&X-Auth-Token=${getToken()}&t=${new Date().getTime()}`);
}}>
下载
</Button>
<Button type={'link'} size={'small'} disabled={!this.state.rename} onClick={() => {
this.setState({
renameVisible: true,
currentFileKey: item['key']
})
}}>重命名</Button>
<Popconfirm
title="您确认要删除此文件吗?"
onConfirm={async () => {
await this.delete(item['key']);
await this.refresh();
}}
okText="是"
cancelText="否"
>
<Button type={'link'} size={'small'} disabled={!this.state.delete} danger>删除</Button>
</Popconfirm>
</>
);
},
}
];
const {selectedRowKeys} = this.state;
const rowSelection = {
selectedRowKeys,
onChange: (selectedRowKeys) => {
this.setState({selectedRowKeys});
},
getCheckboxProps: (record) => ({
disabled: record['disabled'],
}),
};
let hasSelected = selectedRowKeys.length > 0;
if (hasSelected) {
if (!this.state.delete) {
hasSelected = false;
}
}
const title = (
<div className='fs-header'>
<div className='fs-header-left'>
<Input value={this.state.currentDirectoryInput} onChange={this.handleCurrentDirectoryInputChange}
onPressEnter={this.handleCurrentDirectoryInputPressEnter}/>
</div>
<div className='fs-header-right'>
<Space>
<div className='fs-header-right-item'>
<Tooltip title="创建文件夹">
<Button type="primary" size="small"
disabled={!this.state.upload}
icon={<FolderAddOutlined/>}
onClick={() => {
this.setState({
mkdirVisible: true
})
}} ghost/>
</Tooltip>
</div>
<div className='fs-header-right-item'>
<Tooltip title="上传文件">
<Button type="primary" size="small"
icon={<CloudUploadOutlined/>}
disabled={!this.state.upload}
onClick={() => {
window.document.getElementById('file-upload').click();
}} ghost/>
<input type="file" id="file-upload" style={{display: 'none'}}
onChange={this.handleUploadFile} multiple/>
</Tooltip>
</div>
<div className='fs-header-right-item'>
<Tooltip title="上传文件夹">
<Button type="primary" size="small"
icon={<UploadOutlined/>}
disabled={!this.state.upload}
onClick={() => {
window.document.getElementById('dir-upload').click();
}} ghost/>
<input type="file" id="dir-upload" style={{display: 'none'}}
onChange={this.handleUploadDir} webkitdirectory='' multiple/>
</Tooltip>
</div>
<div className='fs-header-right-item'>
<Tooltip title="刷新">
<Button type="primary" size="small"
icon={<ReloadOutlined/>}
onClick={this.refresh}
ghost/>
</Tooltip>
</div>
<div className='fs-header-right-item'>
<Tooltip title="批量删除">
<Button type="primary" size="small" ghost danger disabled={!hasSelected}
icon={<DeleteOutlined/>}
loading={this.state.delBtnLoading}
onClick={() => {
let rowKeys = this.state.selectedRowKeys;
const content = <div>
您确定要删除选中的<Text style={{color: '#1890FF'}}
strong>{rowKeys.length}</Text>
</div>;
confirm({
icon: <ExclamationCircleOutlined/>,
content: content,
onOk: async () => {
for (let i = 0; i < rowKeys.length; i++) {
if (rowKeys[i] === '..') {
continue;
}
await this.delete(rowKeys[i]);
}
this.refresh();
},
onCancel() {
},
});
}}>
</Button>
</Tooltip>
</div>
</Space>
</div>
</div>
);
return (
<div>
<Card title={title} bordered={true} size="small" style={{minHeight: this.state.minHeight}}>
<Table columns={columns}
rowSelection={rowSelection}
dataSource={this.state.files}
size={'small'}
pagination={false}
loading={this.state.loading}
onRow={record => {
return {
onDoubleClick: event => {
if (record['isDir'] || record['isLink']) {
if (record['path'] === '..') {
// 获取当前目录的上级目录
let currentDirectory = this.state.currentDirectory;
let parentDirectory = currentDirectory.substring(0, currentDirectory.lastIndexOf('/'));
this.loadFiles(parentDirectory);
} else {
this.loadFiles(record['path']);
}
} else {
}
},
};
}}
/>
</Card>
{
this.state.mkdirVisible ?
<Modal
title="创建文件夹"
visible={this.state.mkdirVisible}
okButtonProps={{form: 'mkdir-form', key: 'submit', htmlType: 'submit'}}
onOk={() => {
this.mkdirFormRef.current
.validateFields()
.then(async values => {
this.mkdirFormRef.current.resetFields();
let params = {
'dir': this.state.currentDirectory + '/' + values['dir']
}
let paramStr = qs.stringify(params);
this.setState({
confirmLoading: true
})
let result = await request.post(`/${this.state.storageType}/${this.state.storageId}/mkdir?${paramStr}`);
if (result.code === 1) {
message.success('创建成功');
this.loadFiles(this.state.currentDirectory);
} else {
message.error(result.message);
}
this.setState({
confirmLoading: false,
mkdirVisible: false
})
})
.catch(info => {
});
}}
confirmLoading={this.state.confirmLoading}
onCancel={() => {
this.setState({
mkdirVisible: false
})
}}
>
<Form ref={this.mkdirFormRef} id={'mkdir-form'}>
<Form.Item name='dir' rules={[{required: true, message: '请输入文件夹名称'}]}>
<Input autoComplete="off" placeholder="请输入文件夹名称"/>
</Form.Item>
</Form>
</Modal> : undefined
}
{
this.state.renameVisible ?
<Modal
title="重命名"
visible={this.state.renameVisible}
okButtonProps={{form: 'rename-form', key: 'submit', htmlType: 'submit'}}
onOk={() => {
this.renameFormRef.current
.validateFields()
.then(async values => {
this.renameFormRef.current.resetFields();
try {
let currentDirectory = this.state.currentDirectory;
if (!currentDirectory.endsWith("/")) {
currentDirectory += '/';
}
let params = {
'oldName': this.state.currentFileKey,
'newName': currentDirectory + values['newName'],
}
if (params['oldName'] === params['newName']) {
message.success('重命名成功');
return;
}
let paramStr = qs.stringify(params);
this.setState({
confirmLoading: true
})
let result = await request.post(`/${this.state.storageType}/${this.state.storageId}/rename?${paramStr}`);
if (result['code'] === 1) {
message.success('重命名成功');
this.refresh();
} else {
message.error(result.message);
}
} finally {
this.setState({
confirmLoading: false,
renameVisible: false
})
}
})
.catch(info => {
});
}}
confirmLoading={this.state.confirmLoading}
onCancel={() => {
this.setState({
renameVisible: false
})
}}
>
<Form id={'rename-form'}
ref={this.renameFormRef}
initialValues={{newName: getFileName(this.state.currentFileKey)}}>
<Form.Item name='newName' rules={[{required: true, message: '请输入新的名称'}]}>
<Input autoComplete="off" placeholder="新的名称"/>
</Form.Item>
</Form>
</Modal> : undefined
}
<Modal
title={"编辑 " + this.state.fileName}
className='modal-no-padding'
visible={this.state.editorVisible}
destroyOnClose={true}
width={window.innerWidth * 0.8}
centered={true}
okButtonProps={{form: 'rename-form', key: 'submit', htmlType: 'submit'}}
onOk={this.edit}
confirmLoading={this.state.confirmLoading}
onCancel={this.hideEditor}
>
<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>
);
}
}
export default FileSystem;
+12
View File
@@ -0,0 +1,12 @@
.cron-log {
overflow: auto;
border: 0 none;
line-height: 23px;
padding: 15px;
margin: 0;
white-space: pre-wrap;
height: 500px;
background-color: rgb(51, 51, 51);
color: #f1f1f1;
border-radius: 0;
}
+285
View File
@@ -0,0 +1,285 @@
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 {Content} = Layout;
const actionRef = React.createRef();
const api = jobApi;
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,
},
{
title: '任务名称',
dataIndex: 'name',
key: 'name',
sorter: true,
}
, {
title: '状态',
dataIndex: 'status',
key: 'status',
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":
return <Tag color="green">资产状态检测</Tag>;
case "shell-job":
return <Tag color="volcano">Shell脚本</Tag>;
default:
return '';
}
}
}, {
title: 'cron表达式',
dataIndex: 'cron',
key: 'cron',
hideInSearch: true,
}, {
title: '创建日期',
dataIndex: 'created',
key: 'created',
hideInSearch: true,
render: (text, record) => {
return (
<Tooltip title={text}>
{dayjs(text).fromNow()}
</Tooltip>
)
},
sorter: true,
}, {
title: '最后执行日期',
dataIndex: 'updated',
key: 'updated',
hideInSearch: true,
render: (text, record) => {
if (text === '0001-01-01 00:00:00') {
return '-';
}
return (
<Tooltip title={text}>
{dayjs(text).fromNow()}
</Tooltip>
)
},
sorter: true,
},
{
title: '操作',
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']);
}}
>
编辑
</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>,
],
},
];
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={{
defaultPageSize: 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 {
if (values['func'] === 'shell-job') {
values['metadata'] = JSON.stringify({
'shell': values['shell']
});
}
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;
+110
View File
@@ -0,0 +1,110 @@
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}
open={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={{
defaultPageSize: 5,
pageSizeOptions: [5, 10, 20, 50, 100],
showSizeChanger: true,
}}
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;
@@ -0,0 +1,149 @@
import React, {useState} from 'react';
import {Form, Input, Modal, Radio, Select, Spin} from "antd";
import jobApi from "../../api/job";
import assetApi from "../../api/asset";
import {useQuery} from "react-query";
import strings from "../../utils/strings";
const {TextArea} = Input;
const JobModal = ({
visible,
handleOk,
handleCancel,
confirmLoading,
id,
}) => {
const [form] = Form.useForm();
let [func, setFunc] = useState('shell-job');
let [mode, setMode] = useState('all');
useQuery('getJobById', () => jobApi.getById(id), {
enabled: visible && strings.hasText(id),
onSuccess: data => {
if (data['func'] === 'shell-job') {
try {
data['shell'] = JSON.parse(data['metadata'])['shell'];
} catch (e) {
data['shell'] = '';
}
}
if (data.resourceIds) {
data.resourceIds = data.resourceIds.split(',');
}
form.setFieldsValue(data);
setMode(data['mode']);
setFunc(data['func']);
},
});
let resQuery = useQuery(`resQuery`, () => assetApi.GetAll('ssh'));
let resOptions = resQuery.data?.map(item => {
return {
label: item.name,
value: item.id
}
});
const formItemLayout = {
labelCol: {span: 6},
wrapperCol: {span: 14},
};
return (
<Modal
title={id ? '更新计划任务' : '新建计划任务'}
visible={visible}
maskClosable={false}
destroyOnClose={true}
onOk={() => {
form
.validateFields()
.then(async values => {
console.log(values)
if (values['resourceIds']) {
values['resourceIds'] = values['resourceIds'].join(',');
}
form.resetFields();
handleOk(values);
});
}}
onCancel={() => {
form.resetFields();
handleCancel();
}}
confirmLoading={confirmLoading}
okText='确定'
cancelText='取消'
>
<Form form={form} {...formItemLayout}
initialValues={
{
func: 'shell-job',
mode: 'all',
}
}>
<Form.Item name='id' noStyle>
<Input hidden={true}/>
</Form.Item>
<Form.Item label="任务类型" name='func' rules={[{required: true, message: '请选择任务类型'}]}>
<Select onChange={(value) => {
setFunc(value);
}}>
<Select.Option value="shell-job">Shell脚本</Select.Option>
<Select.Option value="check-asset-status-job">资产状态检测</Select.Option>
</Select>
</Form.Item>
<Form.Item label="任务名称" name='name' rules={[{required: true, message: '请输入任务名称'}]}>
<Input autoComplete="off" placeholder="请输入任务名称"/>
</Form.Item>
{
func === 'shell-job' ?
<Form.Item label="Shell脚本" name='shell'
rules={[{required: true, message: '请输入Shell脚本'}]}>
<TextArea autoSize={{minRows: 5, maxRows: 10}} placeholder="在此处填写Shell脚本内容"/>
</Form.Item> : undefined
}
<Form.Item label="cron表达式" name='cron' rules={[{required: true, message: '请输入cron表达式'}]}>
<Input placeholder="请输入cron表达式"/>
</Form.Item>
<Form.Item label="资产选择" name='mode' rules={[{required: true, message: '请选择资产'}]}>
<Radio.Group onChange={async (e) => {
setMode(e.target.value);
}}>
<Radio value={'all'}>全部资产</Radio>
<Radio value={'custom'}>自定义</Radio>
<Radio value={'self'}>本机</Radio>
</Radio.Group>
</Form.Item>
{
mode === 'custom' &&
<Spin tip='加载中...' spinning={resQuery.isLoading}>
<Form.Item label="已选择资产" name='resourceIds' rules={[{required: true}]}>
<Select
mode="multiple"
allowClear
placeholder="请选择资产"
options={resOptions}
>
</Select>
</Form.Item>
</Spin>
}
</Form>
</Modal>
)
};
export default JobModal;
+239
View File
@@ -0,0 +1,239 @@
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 actionRef = React.createRef();
const Storage = () => {
let [visible, setVisible] = useState(false);
let [confirmLoading, setConfirmLoading] = useState(false);
let [selectedRowKey, setSelectedRowKey] = useState(undefined);
let [fileSystemVisible, setFileSystemVisible] = useState(false);
const [columnsStateMap, setColumnsStateMap] = useColumnState(ColumnState.STORAGE);
const columns = [
{
dataIndex: 'index',
valueType: 'indexBorder',
width: 48,
},
{
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>
}
}
}, {
title: '是否默认',
dataIndex: 'isDefault',
key: 'isDefault',
hideInSearch: true,
render: (isDefault) => {
if (isDefault) {
return <Tag color={'green'}></Tag>
} else {
return <Tag color={'red'}></Tag>
}
}
}, {
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']);
}}
>
浏览
</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={'文件管理'}
placement="right"
width={window.innerWidth * 0.8}
closable={true}
maskClosable={true}
onClose={() => {
setFileSystemVisible(false);
setSelectedRowKey(undefined);
actionRef.current.reload();
}}
visible={fileSystemVisible}
>
{fileSystemVisible ?
<FileSystem
storageId={selectedRowKey}
storageType={'storages'}
upload={true}
download={true}
delete={true}
rename={true}
edit={true}
minHeight={window.innerHeight - 103}/>
: undefined
}
</Drawer>
</Content>
</div>
);
}
export default Storage;
@@ -0,0 +1,135 @@
import React, {useState} from 'react';
import {Form, Input, InputNumber, Modal, Select, Switch} from "antd";
import storageApi from "../../api/storage";
import {renderSize} from "../../utils/utils";
import {useQuery} from "react-query";
import strings from "../../utils/strings";
const formItemLayout = {
labelCol: {span: 6},
wrapperCol: {span: 14},
};
const StorageModal = ({
visible,
handleOk,
handleCancel,
confirmLoading,
id,
}) => {
const [form] = Form.useForm();
useQuery('getStorageById', () => storageApi.getById(id), {
enabled: visible && strings.hasText(id),
onSuccess: data => {
if (data['limitSize'] > 0) {
let limitSize = renderSize(data['limitSize']);
let ss = limitSize.split(' ');
data['limitSize'] = parseInt(ss[0]);
setUnit(ss[1]);
} else {
data['limitSize'] = -1;
}
form.setFieldsValue(data);
},
});
let [unit, setUnit] = useState('MB');
const selectAfter = (
<Select value={unit} style={{width: 65}} onChange={(value) => {
setUnit(value);
}}>
<Select.Option value="B">B</Select.Option>
<Select.Option value="KB">KB</Select.Option>
<Select.Option value="MB">MB</Select.Option>
<Select.Option value="GB">GB</Select.Option>
<Select.Option value="TB">TB</Select.Option>
<Select.Option value="PB">PB</Select.Option>
<Select.Option value="EB">EB</Select.Option>
<Select.Option value="ZB">ZB</Select.Option>
<Select.Option value="YB">YB</Select.Option>
</Select>
);
return (
<Modal
title={id ? '更新磁盘空间' : '新建磁盘空间'}
visible={visible}
maskClosable={false}
destroyOnClose={true}
onOk={() => {
form
.validateFields()
.then(async values => {
let limitSize = values['limitSize'];
switch (unit) {
case 'B':
break;
case 'KB':
limitSize = limitSize * 1024;
break;
case 'MB':
limitSize = limitSize * 1024 * 1024;
break;
case 'GB':
limitSize = limitSize * 1024 * 1024 * 1024;
break;
case 'TB':
limitSize = limitSize * 1024 * 1024 * 1024 * 1024 * 1024;
break;
case 'EB':
limitSize = limitSize * 1024 * 1024 * 1024 * 1024 * 1024 * 1024;
break;
case 'ZB':
limitSize = limitSize * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024;
break;
case 'YB':
limitSize = limitSize * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024 * 1024;
break;
default:
break;
}
values['limitSize'] = limitSize;
let ok = await handleOk(values);
if (ok) {
form.resetFields();
}
});
}}
onCancel={() => {
form.resetFields();
setUnit('MB');
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 autoComplete="off" placeholder="网盘的名称"/>
</Form.Item>
<Form.Item label="是否共享" name='isShare' rules={[{required: true, message: '请选择是否共享'}]}
valuePropName="checked">
<Switch checkedChildren="是" unCheckedChildren="否"/>
</Form.Item>
<Form.Item label="大小限制" name='limitSize' rules={[{required: true, message: '请输入大小限制'}]}
tooltip='无限制请填写-1'>
<InputNumber min={-1} addonAfter={selectAfter} style={{width: 275}}/>
</Form.Item>
</Form>
</Modal>
)
};
export default StorageModal;
@@ -0,0 +1,216 @@
import React, {useState} from 'react';
import {Button, Layout, Modal, Popconfirm, Table, Tag, Tooltip} from "antd";
import {formatDate, isEmpty} from "../../utils/utils";
import {ProTable} from "@ant-design/pro-components";
import loginLogApi from "../../api/login-log";
import ColumnState, {useColumnState} from "../../hook/column-state";
import Show from "../../dd/fi/show";
const api = loginLogApi;
const {Content} = Layout;
const actionRef = React.createRef();
const LoginLog = () => {
let [total, setTotal] = useState(0);
let [selectedRowKeys, setSelectedRowKeys] = useState([]);
const [columnsStateMap, setColumnsStateMap] = useColumnState(ColumnState.LOGIN_LOG);
const columns = [
{
dataIndex: 'index',
valueType: 'indexBorder',
width: 48,
},
{
title: '登录账号',
dataIndex: 'username',
key: 'username'
},
{
title: '登录IP',
dataIndex: 'clientIp',
key: 'clientIp'
}, {
title: '登录状态',
dataIndex: 'state',
key: 'state',
hideInSearch: true,
render: text => {
if (text === '0') {
return <Tag color="error">失败</Tag>
} else {
return <Tag color="success">成功</Tag>
}
}
}, {
title: '失败原因',
dataIndex: 'reason',
key: 'reason',
hideInSearch: true,
}, {
title: '浏览器',
dataIndex: 'clientUserAgent',
key: 'clientUserAgent',
hideInSearch: true,
render: (text, record) => {
if (isEmpty(text)) {
return '未知';
}
return (
<Tooltip placement="topLeft" title={text}>
{text.split(' ')[0]}
</Tooltip>
)
}
}, {
title: '登录时间',
dataIndex: 'loginTime',
key: 'loginTime',
hideInSearch: true,
render: (text, record) => {
return formatDate(text, 'yyyy-MM-dd hh:mm:ss');
}
}, {
title: '注销时间',
dataIndex: 'logoutTime',
key: 'logoutTime',
hideInSearch: true,
render: (text, record) => {
if (isEmpty(text) || text === '0001-01-01 00:00:00') {
return '';
}
return text;
}
},
{
title: '操作',
valueType: 'option',
key: 'option',
render: (text, record, _, action) => [
<Show menu={'login-log-del'} key={'login-log-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
}}
rowSelection={{
// 自定义选择项参考: https://ant.design/components/table-cn/#components-table-demo-row-selection-custom
// 注释该行则默认不显示下拉选项
selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT],
selectedRowKeys: selectedRowKeys,
onChange: (keys) => {
setSelectedRowKeys(keys);
}
}}
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,
username: params.username,
clientIp: params.clientIp,
field: field,
order: order
}
let result = await api.getPaging(queryParams);
setTotal(result['total']);
return {
data: result['items'],
success: true,
total: result['total']
};
}}
rowKey="id"
search={{
labelWidth: 'auto',
}}
pagination={{
pageSize: 10,
}}
dateFormatter="string"
headerTitle="登录日志列表"
toolBarRender={() => [
<Show menu={'login-log-del'}>
<Button key="delete"
danger
disabled={selectedRowKeys.length === 0}
onClick={async () => {
Modal.confirm({
title: '您确定要删除选中的登录日志吗?',
content: '删除之后无法进行恢复,请慎重考虑。',
okText: '确定',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
await api.deleteById(selectedRowKeys.join(","));
actionRef.current.reload();
setSelectedRowKeys([]);
}
});
}}>
删除
</Button>
</Show>,
<Show menu={'login-log-clear'}>
<Button key="clear"
type="primary"
danger
disabled={total === 0}
onClick={async () => {
Modal.confirm({
title: '您确定要清空全部的文件登录日志吗?',
content: '清空之后无法进行恢复,请慎重考虑。',
okText: '确定',
okType: 'danger',
cancelText: '取消',
onOk: async () => {
await api.Clear();
actionRef.current.reload();
}
});
}}>
清空
</Button>
</Show>,
]}
/>
</Content>
</div>
);
}
export default LoginLog;

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