fix: 检查loadSshGateway返回值错误
This commit is contained in:
@@ -93,7 +93,9 @@ func (r gatewayService) LoadAll() error {
|
||||
}
|
||||
if len(sshGateways) > 0 {
|
||||
for i := range sshGateways {
|
||||
r.loadSshGateway(&sshGateways[i])
|
||||
if _, err := r.loadSshGateway(&sshGateways[i]); err != nil {
|
||||
log.Warn("loadSshGateway failed, id=" + sshGateways[i].ID + " err=" + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
REACT_APP_ENV=development
|
||||
ESLINT_NO_DEV_ERRORS=true
|
||||
@@ -1,3 +0,0 @@
|
||||
REACT_APP_ENV=production
|
||||
GENERATE_SOURCEMAP=false
|
||||
DISABLE_ESLINT_PLUGIN=true
|
||||
@@ -1,2 +0,0 @@
|
||||
# Project exclude paths
|
||||
/node_modules/
|
||||
@@ -1,339 +0,0 @@
|
||||
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.
|
||||
@@ -1,8 +0,0 @@
|
||||
# Next Terminal dashboard
|
||||
just do go dashboard
|
||||
|
||||
## 安装依赖
|
||||
|
||||
```shell
|
||||
npm install
|
||||
```
|
||||
@@ -1,8 +0,0 @@
|
||||
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
|
||||
|
||||
module.exports = function override(config, env) {
|
||||
config.plugins.push(new MonacoWebpackPlugin({
|
||||
languages: ['json']
|
||||
}));
|
||||
return config;
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"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.
|
Before Width: | Height: | Size: 134 KiB |
@@ -1,31 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,215 +0,0 @@
|
||||
@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;*/
|
||||
/*}*/
|
||||
@@ -1,127 +0,0 @@
|
||||
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;
|
||||
@@ -1,9 +0,0 @@
|
||||
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);
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
import Api from "./api";
|
||||
|
||||
class AccessGatewayApi extends Api{
|
||||
constructor() {
|
||||
super("access-gateways");
|
||||
}
|
||||
}
|
||||
|
||||
let accessGatewayApi = new AccessGatewayApi();
|
||||
export default accessGatewayApi;
|
||||
@@ -1,78 +0,0 @@
|
||||
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;
|
||||
@@ -1,50 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
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;
|
||||
@@ -1,66 +0,0 @@
|
||||
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;
|
||||
@@ -1,15 +0,0 @@
|
||||
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;
|
||||
@@ -1,10 +0,0 @@
|
||||
import Api from "./api";
|
||||
|
||||
class CommandFilterRuleApi extends Api{
|
||||
constructor() {
|
||||
super("command-filter-rules");
|
||||
}
|
||||
}
|
||||
|
||||
const commandFilterRuleApi = new CommandFilterRuleApi();
|
||||
export default commandFilterRuleApi;
|
||||
@@ -1,38 +0,0 @@
|
||||
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;
|
||||
@@ -1,16 +0,0 @@
|
||||
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;
|
||||
@@ -1,19 +0,0 @@
|
||||
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;
|
||||
@@ -1,36 +0,0 @@
|
||||
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;
|
||||
@@ -1,17 +0,0 @@
|
||||
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'];
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
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;
|
||||
@@ -1,40 +0,0 @@
|
||||
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;
|
||||
@@ -1,37 +0,0 @@
|
||||
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;
|
||||
@@ -1,16 +0,0 @@
|
||||
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;
|
||||
@@ -1,19 +0,0 @@
|
||||
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;
|
||||
@@ -1,10 +0,0 @@
|
||||
import Api from "./api";
|
||||
|
||||
class SecurityApi extends Api {
|
||||
constructor() {
|
||||
super("securities");
|
||||
}
|
||||
}
|
||||
|
||||
let securityApi = new SecurityApi();
|
||||
export default securityApi;
|
||||
@@ -1,57 +0,0 @@
|
||||
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;
|
||||
@@ -1,23 +0,0 @@
|
||||
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;
|
||||
@@ -1,10 +0,0 @@
|
||||
import Api from "./api";
|
||||
|
||||
class StorageApi extends Api{
|
||||
constructor() {
|
||||
super("storages");
|
||||
}
|
||||
}
|
||||
|
||||
let storageApi = new StorageApi();
|
||||
export default storageApi;
|
||||
@@ -1,19 +0,0 @@
|
||||
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;
|
||||
@@ -1,15 +0,0 @@
|
||||
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;
|
||||
@@ -1,19 +0,0 @@
|
||||
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;
|
||||
@@ -1,28 +0,0 @@
|
||||
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;
|
||||
@@ -1,19 +0,0 @@
|
||||
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;
|
||||
@@ -1,10 +0,0 @@
|
||||
import Api from "../api";
|
||||
|
||||
class WorkCommandApi extends Api{
|
||||
constructor() {
|
||||
super("worker/commands");
|
||||
}
|
||||
}
|
||||
|
||||
let workCommandApi = new WorkCommandApi();
|
||||
export default workCommandApi;
|
||||
@@ -1,8 +0,0 @@
|
||||
export const HasPermission = (permission) => {
|
||||
let permissionsStr = sessionStorage.getItem('permissions');
|
||||
let permissions = JSON.parse(permissionsStr);
|
||||
if (!permissions) {
|
||||
return false;
|
||||
}
|
||||
return permissions.includes(permission);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
export const PROTOCOL_COLORS = {
|
||||
'rdp': 'cyan',
|
||||
'ssh': 'blue',
|
||||
'telnet': 'geekblue',
|
||||
'vnc': 'purple',
|
||||
'kubernetes': 'volcano'
|
||||
}
|
||||
|
||||
export const MODE_COLORS = {
|
||||
'guacd': 'green',
|
||||
'native': 'orange',
|
||||
'terminal': 'purple',
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
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;
|
||||
@@ -1,146 +0,0 @@
|
||||
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
|
||||
@@ -1,11 +0,0 @@
|
||||
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}/>;
|
||||
};
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
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;
|
||||
@@ -1,118 +0,0 @@
|
||||
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;
|
||||
@@ -1,20 +0,0 @@
|
||||
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;
|
||||
@@ -1,20 +0,0 @@
|
||||
.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;
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
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;
|
||||
@@ -1,31 +0,0 @@
|
||||
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;
|
||||
@@ -1,31 +0,0 @@
|
||||
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;
|
||||
@@ -1,30 +0,0 @@
|
||||
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;
|
||||
@@ -1,131 +0,0 @@
|
||||
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;
|
||||
@@ -1,3 +0,0 @@
|
||||
.console-card .ant-card-body {
|
||||
padding: 0;
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
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;
|
||||
@@ -1,3 +0,0 @@
|
||||
#display > div {
|
||||
margin: 0 auto;
|
||||
}
|
||||
@@ -1,560 +0,0 @@
|
||||
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;
|
||||
@@ -1,48 +0,0 @@
|
||||
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;
|
||||
@@ -1,23 +0,0 @@
|
||||
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;
|
||||
@@ -1,5 +0,0 @@
|
||||
.description-content {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
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'])} {renderSize(rxOfSeconds)}/秒
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="发送" key={'发送' + index}>
|
||||
{renderSize(network[key]['tx'])} {renderSize(txOfSeconds)}/秒
|
||||
</Descriptions.Item>
|
||||
</React.Fragment>
|
||||
);
|
||||
})
|
||||
}
|
||||
</Descriptions>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Stats;
|
||||
@@ -1,357 +0,0 @@
|
||||
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;
|
||||
@@ -1,208 +0,0 @@
|
||||
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;
|
||||
@@ -1,136 +0,0 @@
|
||||
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;
|
||||
@@ -1,553 +0,0 @@
|
||||
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;
|
||||
@@ -1,52 +0,0 @@
|
||||
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;
|
||||
@@ -1,39 +0,0 @@
|
||||
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;
|
||||
@@ -1,3 +0,0 @@
|
||||
.asset-modal .ant-modal-body{
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
@@ -1,831 +0,0 @@
|
||||
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;
|
||||
@@ -1,145 +0,0 @@
|
||||
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;
|
||||
@@ -1,119 +0,0 @@
|
||||
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;
|
||||
@@ -1,146 +0,0 @@
|
||||
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;
|
||||
@@ -1,120 +0,0 @@
|
||||
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;
|
||||
@@ -1,43 +0,0 @@
|
||||
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;
|
||||
@@ -1,233 +0,0 @@
|
||||
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;
|
||||
@@ -1,93 +0,0 @@
|
||||
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;
|
||||
@@ -1,180 +0,0 @@
|
||||
import React, {useState} from 'react';
|
||||
|
||||
import {Button, Layout, Popconfirm, Tag} from "antd";
|
||||
import {ProTable} from "@ant-design/pro-components";
|
||||
import credentialApi from "../../api/credential";
|
||||
import CredentialModal from "./CredentialModal";
|
||||
import ColumnState, {useColumnState} from "../../hook/column-state";
|
||||
import Show from "../../dd/fi/show";
|
||||
|
||||
const {Content} = Layout;
|
||||
const actionRef = React.createRef();
|
||||
const api = credentialApi;
|
||||
|
||||
const Credential = () => {
|
||||
let [visible, setVisible] = useState(false);
|
||||
let [confirmLoading, setConfirmLoading] = useState(false);
|
||||
let [selectedRowKey, setSelectedRowKey] = useState(undefined);
|
||||
|
||||
const [columnsStateMap, setColumnsStateMap] = useColumnState(ColumnState.CREDENTIAL);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
dataIndex: 'index',
|
||||
valueType: 'indexBorder',
|
||||
width: 48,
|
||||
},
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
}, {
|
||||
title: '凭证类型',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
hideInSearch: true,
|
||||
render: (type, record) => {
|
||||
|
||||
if (type === 'private-key') {
|
||||
return (
|
||||
<Tag color="green">密钥</Tag>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Tag color="red">密码</Tag>
|
||||
);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
title: '授权账户',
|
||||
dataIndex: 'username',
|
||||
key: 'username',
|
||||
hideInSearch: true
|
||||
}, {
|
||||
title: '所有者',
|
||||
dataIndex: 'ownerName',
|
||||
key: 'ownerName',
|
||||
hideInSearch: true
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
key: 'created',
|
||||
dataIndex: 'created',
|
||||
hideInSearch: true,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
valueType: 'option',
|
||||
key: 'option',
|
||||
render: (text, record, _, action) => [
|
||||
<Show menu={'credential-edit'} key={'credential-edit'}>
|
||||
<a
|
||||
key="edit"
|
||||
onClick={() => {
|
||||
setVisible(true);
|
||||
setSelectedRowKey(record['id']);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</a>
|
||||
</Show>,
|
||||
<Show menu={'credential-del'} key={'credential-del'}>
|
||||
<Popconfirm
|
||||
key={'confirm-delete'}
|
||||
title="您确认要删除此行吗?"
|
||||
onConfirm={async () => {
|
||||
await api.deleteById(record.id);
|
||||
actionRef.current.reload();
|
||||
}}
|
||||
okText="确认"
|
||||
cancelText="取消"
|
||||
>
|
||||
<a key='delete' className='danger'>删除</a>
|
||||
</Popconfirm>
|
||||
</Show>,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (<Content className="page-container">
|
||||
<ProTable
|
||||
columns={columns}
|
||||
actionRef={actionRef}
|
||||
columnsState={{
|
||||
value: columnsStateMap,
|
||||
onChange: setColumnsStateMap
|
||||
}}
|
||||
request={async (params = {}, sort, filter) => {
|
||||
|
||||
let field = '';
|
||||
let order = '';
|
||||
if (Object.keys(sort).length > 0) {
|
||||
field = Object.keys(sort)[0];
|
||||
order = Object.values(sort)[0];
|
||||
}
|
||||
|
||||
let queryParams = {
|
||||
pageIndex: params.current,
|
||||
pageSize: params.pageSize,
|
||||
name: params.name,
|
||||
field: field,
|
||||
order: order
|
||||
}
|
||||
let result = await api.getPaging(queryParams);
|
||||
return {
|
||||
data: result['items'],
|
||||
success: true,
|
||||
total: result['total']
|
||||
};
|
||||
}}
|
||||
rowKey="id"
|
||||
search={{
|
||||
labelWidth: 'auto',
|
||||
}}
|
||||
pagination={{
|
||||
defaultPageSize: 10,
|
||||
}}
|
||||
dateFormatter="string"
|
||||
headerTitle="授权凭证列表"
|
||||
toolBarRender={() => [
|
||||
<Show menu={'credential-add'}>
|
||||
<Button key="button" type="primary" onClick={() => {
|
||||
setVisible(true)
|
||||
}}>
|
||||
新建
|
||||
</Button>
|
||||
</Show>,
|
||||
]}
|
||||
/>
|
||||
|
||||
<CredentialModal
|
||||
id={selectedRowKey}
|
||||
visible={visible}
|
||||
confirmLoading={confirmLoading}
|
||||
handleCancel={() => {
|
||||
setVisible(false);
|
||||
setSelectedRowKey(undefined);
|
||||
}}
|
||||
handleOk={async (values) => {
|
||||
setConfirmLoading(true);
|
||||
|
||||
try {
|
||||
let success;
|
||||
if (values['id']) {
|
||||
success = await api.updateById(values['id'], values);
|
||||
} else {
|
||||
success = await api.create(values);
|
||||
}
|
||||
if (success) {
|
||||
setVisible(false);
|
||||
}
|
||||
actionRef.current.reload();
|
||||
} finally {
|
||||
setConfirmLoading(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
</Content>);
|
||||
}
|
||||
|
||||
export default Credential;
|
||||
@@ -1,134 +0,0 @@
|
||||
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;
|
||||
@@ -1,249 +0,0 @@
|
||||
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;
|
||||
@@ -1,237 +0,0 @@
|
||||
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;
|
||||
@@ -1,33 +0,0 @@
|
||||
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;
|
||||
@@ -1,47 +0,0 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Descriptions, Tag} from "antd";
|
||||
import strategyApi from "../../api/strategy";
|
||||
|
||||
const api = strategyApi;
|
||||
|
||||
const renderStatus = (text) => {
|
||||
if (text === true) {
|
||||
return <Tag color={'green'}>开启</Tag>
|
||||
} else {
|
||||
return <Tag color={'red'}>关闭</Tag>
|
||||
}
|
||||
}
|
||||
|
||||
const StrategyInfo = ({active, id}) => {
|
||||
let [item, setItem] = useState({});
|
||||
|
||||
useEffect(() => {
|
||||
const getItem = async (id) => {
|
||||
let item = await api.getById(id);
|
||||
if (item) {
|
||||
setItem(item);
|
||||
}
|
||||
};
|
||||
if (active && id) {
|
||||
getItem(id);
|
||||
}
|
||||
}, [active]);
|
||||
|
||||
return (
|
||||
<div className={'page-detail-info'}>
|
||||
<Descriptions column={1}>
|
||||
<Descriptions.Item label="名称">{item['name']}</Descriptions.Item>
|
||||
<Descriptions.Item label="上传">{renderStatus(item['upload'])}</Descriptions.Item>
|
||||
<Descriptions.Item label="下载">{renderStatus(item['download'])}</Descriptions.Item>
|
||||
<Descriptions.Item label="编辑">{renderStatus(item['edit'])}</Descriptions.Item>
|
||||
<Descriptions.Item label="删除">{renderStatus(item['delete'])}</Descriptions.Item>
|
||||
<Descriptions.Item label="重命名">{renderStatus(item['rename'])}</Descriptions.Item>
|
||||
<Descriptions.Item label="复制">{renderStatus(item['copy'])}</Descriptions.Item>
|
||||
<Descriptions.Item label="粘贴">{renderStatus(item['paste'])}</Descriptions.Item>
|
||||
<Descriptions.Item label="创建时间">{item['created']}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StrategyInfo;
|
||||
@@ -1,106 +0,0 @@
|
||||
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;
|
||||
@@ -1,3 +0,0 @@
|
||||
.pie-card .ant-pro-card-body {
|
||||
padding: 16px !important;
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
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;
|
||||
@@ -1,3 +0,0 @@
|
||||
.ant-pro-card-body {
|
||||
padding: 16px !important;
|
||||
}
|
||||
@@ -1,353 +0,0 @@
|
||||
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;
|
||||
@@ -1,201 +0,0 @@
|
||||
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;
|
||||
@@ -1,24 +0,0 @@
|
||||
.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;
|
||||
}
|
||||
@@ -1,240 +0,0 @@
|
||||
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;
|
||||
@@ -1,92 +0,0 @@
|
||||
.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%;
|
||||
}
|
||||
@@ -1,883 +0,0 @@
|
||||
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} {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;
|
||||
@@ -1,12 +0,0 @@
|
||||
.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;
|
||||
}
|
||||
@@ -1,285 +0,0 @@
|
||||
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;
|
||||
@@ -1,110 +0,0 @@
|
||||
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;
|
||||
@@ -1,149 +0,0 @@
|
||||
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;
|
||||
@@ -1,239 +0,0 @@
|
||||
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;
|
||||
@@ -1,135 +0,0 @@
|
||||
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;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user