Compare commits

..

489 Commits

Author SHA1 Message Date
ibuler
e0b3f4eee9 perf: logo和文案
perf: 去掉title
2021-06-02 18:11:18 +08:00
Michael Bai
6f65bcf2d5 fix: 修复导入模版生成table时,没有显示number字段类型所在列的问题 2021-05-23 22:00:07 -05:00
Michael Bai
8a97928d1d fix: 修改用户创建/更新密码策略选项值 2021-05-23 21:58:07 -05:00
Jiangjie.Bai
0eb8e1fab3 Merge pull request #815 from jumpserver/dev
v2.10.0 rc4
2021-05-19 19:29:38 +08:00
ibuler
a80918139b perf: 修改命令存储创建 2021-05-19 05:59:40 -05:00
ibuler
67ffda2fd0 fix: 修复ldap导入不能隐藏dialog问题 2021-05-19 18:34:10 +08:00
ibuler
fa6831b743 perf: 优化钉钉企业微信提交 2021-05-19 18:33:11 +08:00
Bai
8bd77fa6c9 fix: 修复创建云管账号成功重定向页面问题 2021-05-19 18:29:50 +08:00
ibuler
6a45fccfcd fix: 修复系统用户用户名禁用的问题 2021-05-19 18:13:43 +08:00
ibuler
c39733cf66 fix: 修复多tab引起的设置和弹窗问题
fix: 优化

perf: 移除不用的代码

perf: 修复不能刷新的bug

perf: 去掉conole
2021-05-18 22:22:35 -05:00
Jiangjie.Bai
ea4a7f53e3 Merge pull request #808 from jumpserver/dev
v2.10.0 rc3
2021-05-18 19:18:19 +08:00
Bai
6639614caf fix: 修复云管中心创建账户后跳转到任务列表页面的问题 2021-05-18 18:31:59 +08:00
Bai
f17ec57b3a feat: 收集用户任务添加详情页面和执行历史页面 2021-05-18 17:46:53 +08:00
Bai
b04cf80201 fix: 修复云管账号详情页面点击更新失败的问题 2021-05-18 17:45:51 +08:00
ibuler
6dc71fe612 perf(assets): 修复系统用户详情中更新按钮地址不对的问题 2021-05-18 14:15:13 +08:00
ibuler
9cbec5e1ab perf: 优化系统用户用户名显示禁用
perf: 优化系统用户用户名隐藏与禁用
2021-05-18 14:14:22 +08:00
ibuler
703eadf292 perf: 优化工单,处理人去掉关闭的按钮,只能同意或拒绝 2021-05-18 14:12:13 +08:00
Bai
d2aa4e99da fix: 修复改密计划页面更新资产/节点报错的问题 2021-05-18 14:11:02 +08:00
Bai
4509970f7d fix: 修复组织详情关联用户报错的问题 2021-05-17 19:12:39 +08:00
ibuler
b03af2c995 perf(users): 修复全局组织用户组问题的 2021-05-17 18:35:14 +08:00
ibuler
328e068aca fix(xpack): 修复收集任务表头设置的bug 2021-05-17 18:34:04 +08:00
Bai
65c6922621 fix: 修复收集用户任务创建不填写interval报错的问题 2021-05-17 18:30:54 +08:00
xinwen
ebb8af42f2 fix: 修复绑定企业微信&钉钉的一些问题 2021-05-17 17:21:14 +08:00
ibuler
0b821ffc04 perf: 优化profile中的翻译 2021-05-17 16:57:53 +08:00
Bai
5b6e2970bf fix: 去掉RemoteApp的导入/导出功能 2021-05-17 16:36:05 +08:00
Bai
d01e991885 fix: 申请资产工单详情添加资产授权详情链接 2021-05-17 15:52:37 +08:00
Bai
552b26e163 fix: 申请资产详情添加 action 字段 2021-05-17 15:52:37 +08:00
Bai
35814d3d5c fix: 添加系统监控组件名称(lion) 2021-05-17 15:00:17 +08:00
xinwen
31e46a3ede fix: oauth2 i18n 2021-05-17 01:51:56 -05:00
ibuler
e7815f528c fix: 修复系统用户详情中,没有更多操作的bug 2021-05-17 01:51:25 -05:00
ibuler
4d747686cd fix: 修复资产详情页面,toLowerCase 报错 2021-05-17 01:50:47 -05:00
Jiangjie.Bai
12697ab279 Merge pull request #788 from jumpserver/dev
v2.10.0 rc1
2021-05-13 19:47:27 +08:00
老广
f4f4b7ccc1 增加 rdp vnc 监控 (#787)
Co-authored-by: Eric <xplzv@126.com>
2021-05-13 19:44:47 +08:00
ibuler
41bbd4a70b perf(assets): 优化资产创建更新时的label 2021-05-13 19:24:35 +08:00
Jiangjie.Bai
a66403dcf0 Merge pull request #786 from jumpserver/dev
v2.10 rc1
2021-05-13 19:21:04 +08:00
xinwen
31b75aa139 feat: 添加企业微信,钉钉扫码登录 2021-05-13 19:12:51 +08:00
xinwen
411aae6829 feat: ES 命令存储支持忽略证书验证 2021-05-07 03:48:15 -05:00
ibuler
0ad025f63b fix: 修复系统用户vnc用户名可以不必填 2021-05-06 22:31:40 -05:00
jym503558564
312de6c6eb perf(i18n): 修改用户页面Router 翻译 2021-05-06 22:30:47 -05:00
jym503558564
5b05bd12c6 fix(i18n): 修改更新资源密码时,更新密码字段没翻译的问题 2021-05-06 22:30:10 -05:00
Bai
a87ea94e78 fix: 删除资产授权创建页面多余的输入框 2021-04-30 14:57:25 +08:00
ibuler
9994020f97 perf: 优化自动推送那个按钮
perf: 修改名称
2021-04-29 04:31:31 -05:00
fit2bot
fc0709f1ce fix: 修复点击修改密码密码框丢失的问题 (#771)
Co-authored-by: ibuler <ibuler@qq.com>
2021-04-29 17:28:23 +08:00
ibuler
9235d68825 refactor: 改变组件位置
stash

refacter: 整理了forms组件位置

perf: 修改导入
2021-04-28 23:59:49 -05:00
ibuler
4a17efb015 perf: 优化登录前更新密码 2021-04-28 23:58:43 -05:00
fit2cloud-jiangweidong
1cc69e4203 feat: 管理员可以设置用户是否下次登录需修改密码 (#758)
* feat: 管理员可以设置用户是否下次登录需修改密码

* feat: 管理员可以设置用户下次是否需要更改密码,本次修改:字段命名规范化

* fix: 用户是否需要更改密码,变量名称规范化

* fix: 管理员可设置用户下次登录是否需要改密,字段名称更改
2021-04-28 19:24:30 +08:00
jym503558564
9de8d622e4 fix(i18n): 修改翻译 2021-04-28 05:20:40 -05:00
Bai
844dfb5ac1 feat: 修复用户页面获取命令复核功能详情失败的问题 2021-04-28 16:26:37 +08:00
fit2cloud-jiangweidong
9d3fd73367 feat: 用户更改密码不可使用前n次历史密码,管理员可设置历史密码重复次数 2021-04-28 02:46:13 -05:00
ibuler
c7731a1d2f perf: 优化公钥认证,根据开关来限制公钥认证 2021-04-27 04:50:43 -05:00
Bai
bf161d688a feat: 添加命令复核工单逻辑3 2021-04-27 16:37:54 +08:00
Bai
ab2d5b3fed feat: 添加命令复核工单逻辑2 2021-04-27 16:37:54 +08:00
Bai
6a40ab1a4c feat: 添加命令复核工单逻辑 2021-04-27 16:37:54 +08:00
jym503558564
4d11e638e8 fix:修改Xpack-router翻译 2021-04-26 05:37:13 -05:00
fit2bot
97c4a397e2 fix:修改翻译 (#762)
Co-authored-by: jym503558564 <503558564@qq.com>
2021-04-25 18:07:31 +08:00
Orange
9239d69c14 fix: 修改DB协议系统用户手动登录时用户名必填 2021-04-25 01:16:26 -05:00
ibuler
3ea68fe13f perf: 优化换行 2021-04-25 01:09:39 -05:00
Orange
04ce056076 fix: 社区版本隐藏全局组织名称设置 2021-04-25 01:09:39 -05:00
Orange
67c1c8db58 fix: 临时移除 Database 协议的用户名与密码相同 选项 2021-04-19 04:01:32 -05:00
Jiangjie.Bai
d8566d2f9e Merge pull request #753 from jumpserver/dev
v2.9.0 发版
2021-04-15 21:05:14 +08:00
fit2bot
d74da503c8 fix: 修复不能禁用的问题 (#751)
perf: 优化系统用户创建

Co-authored-by: ibuler <ibuler@qq.com>
2021-04-15 19:41:07 +08:00
Orange
e1d8e4aea6 fix: 修复v2.9测试中的bugs 2021-04-15 06:40:30 -05:00
Orange
d21559599f fix: 修复系统设置邮件bugs 2021-04-15 06:39:42 -05:00
Orange
f8cadb545f fix: 修复首页面板链接跳转有误的问题
Closes https://github.com/jumpserver/jumpserver/issues/5850
2021-04-15 06:39:07 -05:00
Orange
2bc4b53159 Merge pull request #747 from jumpserver/pr@dev@fix_create_asset_node_lost
fix(assets): 修复选中资产创建时,没有默认选中节点的问题
2021-04-15 16:19:51 +08:00
ibuler
7f28cc0aad fix(assets): 修复选中资产创建时,没有默认选中节点的问题 2021-04-15 15:39:26 +08:00
Orange
7e95e38d24 Merge pull request #744 from jumpserver/dev
v2.9.0 rc3
2021-04-14 18:55:07 +08:00
Orange
57bafc01e3 fix: 修改翻译 2021-04-14 18:54:34 +08:00
Orange
834033f2fd fix: 隐藏终端详情中的端口数据
Closes https://github.com/jumpserver/trello/issues/986
2021-04-14 05:30:25 -05:00
Orange
9a41ccbdd7 fix: 禁止全局组织中批量更新用户组 2021-04-14 05:30:04 -05:00
ibuler
3793370c9c fix: 修复导入id重复的bug 2021-04-14 05:29:38 -05:00
Orange
4486dc55a7 Merge pull request #740 from jumpserver/dev
v2.9.0 rc2
2021-04-13 19:30:56 +08:00
fit2bot
bdb63b865a fix: 修复v2.9 bugs (#739)
* fix: 修复资产列表协议组显示的问题

* fix: 全局组织批量更新用户禁止更新用户组

* fix: 修复网关无法克隆的Bug

* fix: 修复平台列表更新bug

* fix: 修复翻译问题

* fix: 修复更新管理用户账户秘钥的问题

Co-authored-by: Orange <orangemtony@gmail.com>
2021-04-13 19:30:24 +08:00
fit2bot
2d17b48b86 fix(import): 修复导入编辑,空格无法编辑的问题 (#735)
fix: 修复点击节点导入出问题的bug

perf(import): 优化使用分页导入

Co-authored-by: ibuler <ibuler@qq.com>
2021-04-13 14:19:07 +08:00
老广
67091d5a22 Merge pull request #733 from jumpserver/dev
Merge Dev v2.9
2021-04-08 06:24:13 -05:00
liuboF2c
798c4ca64e feat:支持配置全局组织的显示名称 (#731)
Co-authored-by: liubo <liubo@fit2cloud.com>
2021-04-08 13:57:26 +08:00
fit2bot
eddd27e95d perf: 优化 csv/xlsx 导入 (#725)
* perf: 优化导入csv

* perf: 优化导入

stash

perf: 优化导入

perf: 更新导入

perf: 优化导入

feat: 完成导入优化

perf: 修复bug

* perf: 继续优化导入,性能提高三倍

Co-authored-by: ibuler <ibuler@qq.com>
2021-04-08 10:09:58 +08:00
dependabot[bot]
12ffa363c1 chore(deps): bump lodash from 4.17.15 to 4.17.19 (#213)
* chore: 更新Submodule指向

* fix: 修复Build失败的问题

* fix(preload): 开启Preload

开启preload,提高首屏加载速度

Co-authored-by: Orange <orangemtony@gmail.com>
Co-authored-by: Jiangjie.Bai <32935519+BaiJiangJie@users.noreply.github.com>
Co-authored-by: 老广 <ibuler@qq.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-07 15:51:21 +08:00
dependabot[bot]
cd79246f0d build(deps): bump http-proxy from 1.18.0 to 1.18.1 (#368)
* build(deps): bump http-proxy from 1.18.0 to 1.18.1

Bumps [http-proxy](https://github.com/http-party/node-http-proxy) from 1.18.0 to 1.18.1.
- [Release notes](https://github.com/http-party/node-http-proxy/releases)
- [Changelog](https://github.com/http-party/node-http-proxy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/http-party/node-http-proxy/compare/1.18.0...1.18.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-07 15:50:29 +08:00
dependabot[bot]
94583e2156 build(deps): bump ini from 1.3.5 to 1.3.7 (#545)
* build(deps): bump ini from 1.3.5 to 1.3.7

Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.7.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.7)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-07 15:44:08 +08:00
dependabot[bot]
ca602a8052 build(deps): bump axios from 0.18.1 to 0.21.1 (#567)
* build(deps): bump axios from 0.18.1 to 0.21.1

Bumps [axios](https://github.com/axios/axios) from 0.18.1 to 0.21.1.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v0.21.1/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.18.1...v0.21.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-07 15:43:11 +08:00
dependabot[bot]
5bdc4e4e3a build(deps): bump elliptic from 6.5.2 to 6.5.4 (#649)
* build(deps): bump elliptic from 6.5.2 to 6.5.4

Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.2 to 6.5.4.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.2...v6.5.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-07 15:41:53 +08:00
dependabot[bot]
da1b73d3fd build(deps): bump y18n from 3.2.1 to 3.2.2 (#721)
* build(deps): bump y18n from 3.2.1 to 3.2.2

Bumps [y18n](https://github.com/yargs/y18n) from 3.2.1 to 3.2.2.
- [Release notes](https://github.com/yargs/y18n/releases)
- [Changelog](https://github.com/yargs/y18n/blob/master/CHANGELOG.md)
- [Commits](https://github.com/yargs/y18n/commits)
2021-04-07 15:35:50 +08:00
Orange
4bc4012520 feat: 支持首页PDF导出,优化打印样式 2021-04-07 15:32:28 +08:00
Orange
b8f1cb7a8e feat: 支持首页PDF导出,优化打印样式 2021-04-07 15:32:28 +08:00
liubo
ee6a3c6d68 feat: 支持添加nutanix云账号 2021-04-07 15:19:26 +08:00
Orange
be176ad408 perf: 用户来源不是本地时禁用更新密码 2021-04-07 11:17:50 +08:00
Orange
73c17fccbe perf: 优化翻译批量命令选择资产时提示资产不支持SSH协议;连接 2021-04-07 11:04:40 +08:00
Orange
30c1284a41 fix: 完善页面邮箱地址校验规则
Closes https://github.com/jumpserver/trello/issues/933
2021-04-07 11:03:09 +08:00
Orange
191900381a fix: 修复资产更多信息更新失败的问题 2021-04-07 11:02:08 +08:00
Orange
91e04a8d18 perf: 优化编译命令 2021-03-29 19:29:06 +08:00
Orange
1b223f0486 fix: 修复命令存储更新失败的问题 2021-03-29 19:27:58 +08:00
Orange
22eb78339e Merge pull request #719 from jumpserver/pr@dev@perf_status
perf: 优化系统监控页面
2021-03-29 19:23:00 +08:00
ibuler
5831cb326c perf: 优化系统监控页面 2021-03-29 16:59:33 +08:00
Orange
52a4c1824f fix: 修复创建K8S系统用户的权限问题 2021-03-23 17:55:06 +08:00
Orange
c155e5a59b perf: 优化全局组织下禁止用户更新用户组 2021-03-23 17:54:15 +08:00
Orange
ff90e56763 fix: 修复创建远程应用默认Path丢失的问题 2021-03-23 17:50:54 +08:00
Orange
14000317b9 fix: 恢复RDP系统用户user_group字段 2021-03-23 17:49:52 +08:00
Orange
a24fab51af Merge pull request #702 from jumpserver/dev
Merge v2.8.1
2021-03-19 19:06:18 +08:00
Orange
4184401432 fix: 修复LDAP属性映射的字段显示问题 2021-03-19 17:40:40 +08:00
Orange
dc59836a66 fix: 修复更多操作数量为空时仍然展示的Bug 2021-03-19 15:08:50 +08:00
Orange
ba6433a585 Merge pull request #699 from jumpserver/dev
v2.8真的发版
2021-03-18 20:47:40 +08:00
fit2bot
b88c90bb75 fix: 全局组织系统用户详情页中禁用添加资产 (#694)
* fix: 去掉组织创建保存并继续按钮

* fix: 全局组织系统用户详情页中禁用添加资产

* fix: 全局组织系统用户详情页中禁用添加资产

* fix: 修复bugs

* fix: 修复bugs

Co-authored-by: Orange <orangemtony@gmail.com>
2021-03-18 20:41:42 +08:00
ibuler
4eb5155aed perf: 优化has valid license 写法 2021-03-18 20:39:37 +08:00
ibuler
dd4aed9cf4 perf: 优化用户中角色显示问题 2021-03-18 20:39:10 +08:00
Bai
2274b65d83 feat: 添加云管中心账号 Azure 国际 2021-03-18 20:20:42 +08:00
ibuler
e0a927c7e1 perf: 去掉more actions 2021-03-18 20:11:24 +08:00
Jiangjie.Bai
d294111e9e Merge pull request #693 from jumpserver/dev
v2.8 发版 (rc6)
2021-03-18 17:44:46 +08:00
ibuler
d6fef086c0 fix: 修复一些license验证问题 2021-03-18 16:12:26 +08:00
Orange
cd6418ef4b fix: 全局组织禁用批量更新及修复翻译问题 2021-03-18 16:12:06 +08:00
Orange
00909b364d fix: 全局组织禁用批量更新和从节点移除 2021-03-18 16:12:06 +08:00
ibuler
cdd3df1562 perf: 优化 relacation card 在root组织下应该disabled的问题 2021-03-18 14:39:02 +08:00
Orange
081f43887b fix: 去掉组织创建保存并继续按钮 2021-03-18 14:09:09 +08:00
Orange
95ba08afa6 fix: 修复LDAP显示隐藏问题 2021-03-18 13:40:50 +08:00
ibuler
88aa17550b perf: 优化 nested field error 2021-03-18 12:39:32 +08:00
Orange
8c3337f581 fix: 补全登录控制更新URl 2021-03-18 10:51:57 +08:00
ibuler
6d0d9650b4 perf: 禁用 detail 更新
perf: terminal 可以更新
2021-03-18 10:51:23 +08:00
Orange
86285c12bd Merge pull request #684 from jumpserver/dev
v2.8-rc4 发版
2021-03-17 20:19:11 +08:00
fit2bot
89b96a4542 fix: 修复前端显示Bugs (#679)
* fix: 修复前端显示Bugs

* fix: 修复引用取值问题

* fix: 还原更新设置

Co-authored-by: Orange <orangemtony@gmail.com>
2021-03-17 20:18:39 +08:00
Orange
707bf497b0 perf: 不可用节点添加提示 2021-03-17 20:12:58 +08:00
Orange
7a0ef53d78 perf: 不可用节点添加提示 2021-03-17 20:12:58 +08:00
Orange
2a0626e4f0 fix: 增加RDP自动推送 2021-03-17 20:11:56 +08:00
Orange
a760d0eeaa fix: 优化应用表单克隆功能 2021-03-17 20:11:07 +08:00
ibuler
f7ccbde502 perf(orgs): 优化用户组织选择,避免在没有启用xpack情况下,切换到root组织中 2021-03-17 19:57:44 +08:00
ibuler
8b350ba819 perf: 优化requet错误日志 2021-03-17 16:08:01 +08:00
Orange
f87d605e13 fix: 修复组织ID变更为000000000002 2021-03-17 16:04:20 +08:00
ibuler
4a38cb7168 fix: 修复hasBulkDelete的问题 2021-03-17 10:48:55 +08:00
Jiangjie.Bai
1ebb8dda0a Merge pull request #675 from jumpserver/dev
Merge v2.8 rc4
2021-03-16 20:49:31 +08:00
Orange
2f9b33898f perf: 增加表格Url初始状态校验 2021-03-16 20:47:44 +08:00
fit2bot
14b854a872 fix: 移除多余的clone (#673)
* fix: 移除多余的clone

* fix: 移除多余的clone

* fix: 移除多余的更多操作

* fix: 移除多余的更多操作

Co-authored-by: Orange <orangemtony@gmail.com>
2021-03-16 19:18:07 +08:00
ibuler
235131fd81 fix(assets): 修复 root 组织节点可以创建的问题 2021-03-16 19:12:54 +08:00
Orange
9b1ba09404 fix: 统一修复批量移除url问题 2021-03-16 19:11:47 +08:00
ibuler
6fc8a43e34 perf: 优化创建storage,使用新的通用组件
perf: 优化工单创建使用新的组件
2021-03-16 19:11:42 +08:00
Orange
324db2fdae fix: 修复 cellValue 取值问题 2021-03-16 19:10:25 +08:00
ibuler
3367363445 perf(orgs): 优化在 root 组织下,可以 克隆和创建的问题
perf: 优化 update, root组织下不能编辑
2021-03-16 18:31:34 +08:00
ibuler
5f6846fa47 perf: 优化创建acl左侧路由高亮 2021-03-16 15:25:55 +08:00
老广
d15292ad0e Merge pull request #665 from jumpserver/dev
Merge V2.8 Rc3
2021-03-15 20:00:09 +08:00
fit2bot
65cd456ae9 fix: 修复密码匣子包含克隆的问题 (#659)
Co-authored-by: Orange <orangemtony@gmail.com>
2021-03-15 19:40:02 +08:00
fit2bot
1748ae760a fix: 修复ACL展示Bugs (#660)
* fix: 修复ACL展示Bugs

* fix: 修复资产ACL更新异常

Co-authored-by: Orange <orangemtony@gmail.com>
2021-03-15 19:39:29 +08:00
Orange
7dc5ec8fa7 fix: 删除管理用户详情页的冗余快捷功能 2021-03-15 19:34:07 +08:00
老广
543e0f7aa7 Merge pull request #663 from jumpserver/pr@dev@fix_system_user
fix: 修复系统用户详情选择协议的Bugs
2021-03-15 19:32:48 +08:00
ibuler
4b152bf9bf fix: 修复root组织可以创建的问题
fix: 修复全局组织可以clone的问题
2021-03-15 19:31:27 +08:00
老广
0afc160b56 Merge pull request #664 from jumpserver/pr@dev@fix_table_action_row_id
fix: 批量修改表格Action取值为Row.id
2021-03-15 19:31:05 +08:00
ibuler
79c89676a6 perf: 优化移除用户和删除用户的行为 2021-03-15 19:30:27 +08:00
Orange
78fa90c9f8 fix: 批量修改表格Action取值为Row.id 2021-03-15 19:21:40 +08:00
Orange
c1ed466b8b fix: 修复系统用户详情选择协议的Bugs 2021-03-15 19:01:08 +08:00
Eric_Lee
35b8181589 Merge pull request #657 from jumpserver/dev
v2.8 rc2
2021-03-12 18:26:43 +08:00
Orange
c172056998 fix: 修复默认优先级 2021-03-12 18:24:20 +08:00
Orange
752e3a7a28 fix: 修复默认优先级 2021-03-12 18:24:20 +08:00
Orange
4935c32bb9 fix: 修改flower路径 2021-03-12 18:10:55 +08:00
老广
7e184a4061 Merge pull request #654 from jumpserver/dev
v2.8 准备发版
2021-03-11 21:19:28 +08:00
fit2bot
30a7063999 perf: 优化各页面列控制 (#652)
perf: 优化页面列表col

Co-authored-by: ibuler <ibuler@qq.com>
2021-03-11 20:24:03 +08:00
ibuler
c94a451df9 fix: 修复 root 组织 下可以创建的问题 2021-03-11 20:21:08 +08:00
fit2bot
f6aab29ecc perf: 增加ACL控制页面 (#651)
* perf: 增加ACL控制页面

* fix: 修改页面

* fix: 修改页面

* fix: 修改页面

* fix: 修改页面

* fix: update

* fix: 完善路由条件

* fix: 完善默认值

* fix: 完善翻译

* fix: 完善表单默认值

* fix: 完善表单默认值

Co-authored-by: Orange <orangemtony@gmail.com>
2021-03-11 20:11:19 +08:00
fit2bot
3ad157848a refactor: 重构命令记录结构 (#622)
* refactor: 重构命令记录结构

* fix: 调整tree构建字段

* fix: 修复首次打开页面报错的问题

* fix: 修复命令记录Tree刷新的问题

* fix: 添加时间戳排序

* fix: 删除多余的Consolelog

Co-authored-by: Orange <orangemtony@gmail.com>
2021-03-11 20:09:37 +08:00
xinwen
e9c54d7eeb feat: 资产授权规则添加是否起作用过滤条件 2021-03-10 15:44:54 +08:00
ibuler
b8b19fed53 perf: 优化嵌套表单的问题 2021-03-10 01:04:39 +08:00
Orange
8dbc7a404f fix: 修复Tag搜索会出现重复项的问题 2021-03-09 18:22:59 +08:00
Orange
a9e95fd705 fix: 增加资产列表可显示字段 2021-03-09 18:21:13 +08:00
Orange
a127b872cc fix: 修复组织管理样式错误 2021-03-09 14:11:03 +08:00
Orange
01aa92adc0 feat: 添加SSH指纹字段显示 2021-03-09 14:07:10 +08:00
fit2bot
0d31318fd5 fix: 修复用户页面无权限显示的问题 (#638)
* fix: 修复用户页面无权限显示的问题

* fix: 修复用户页面无权限显示的问题

Co-authored-by: Orange <orangemtony@gmail.com>
2021-03-09 14:06:28 +08:00
Orange
a595d28a5b fix: 修复显示空更多操作列表的Bug 2021-03-09 12:37:03 +08:00
fit2bot
5d973944ea fix: 修复Dropdown Menu的回调异常 (#642)
* fix: 修复Dropdown Menu的回调异常

* perf: 优化创建

Co-authored-by: Orange <orangemtony@gmail.com>
Co-authored-by: ibuler <ibuler@qq.com>
2021-03-09 12:35:44 +08:00
ibuler
1d008330a1 feat(settings): 支持自定义忘记密码的 url 2021-03-08 16:15:38 +08:00
ibuler
337ff47806 perf: 继续优化action group 2021-03-05 18:53:27 +08:00
fit2bot
e453a9a740 perf: 创建系统用户时需选择协议 (#631)
* perf: 创建系统用户时需选择协议

* feat: 添加action groups

* perf: 优化创建的按钮

Co-authored-by: Orange <orangemtony@gmail.com>
Co-authored-by: ibuler <ibuler@qq.com>
2021-03-05 15:01:04 +08:00
Orange
e4ec8565f0 fix: 修复平台列表默认可删除的错误 (#634)
* fix: 修复平台列表默认可删除的错误
2021-03-05 14:09:59 +08:00
Orange
22904ba421 fix: 修复Default组织ID的问题 2021-03-05 13:59:56 +08:00
Orange
5371faf019 fix: 更新云账号字段 2021-03-04 11:31:28 +08:00
Orange
7c6a3340ad fix: 修复工单匹配Meta字段 2021-03-04 11:05:45 +08:00
Orange
166e66ff9e fix: 优化带Meta的表单数据获取与提交的问题 2021-03-04 11:05:45 +08:00
Orange
1c39d33d43 fix: 修复表格获取ID异常的问题 2021-03-03 19:07:56 +08:00
Orange
159c6d8208 fix: 修复表格获取ID异常的问题 2021-03-03 19:07:56 +08:00
ibuler
917d95cc7b fix: 修复编辑出问题的bug
perf: 优化嵌套的form

perf: 优化嵌套的form

perf: 优化其那套form

perf: 优化nestfield, 但报错存在问题

perf: 优化
2021-03-03 14:48:41 +08:00
fit2bot
6fde735cbd perf: 优化全局组织 (#621)
* feat: 默认组织改为实体组织

* perf: 优化在root组织下,应不能创建

* perf: 优化全局组织

* perf: 优化组织选择

Co-authored-by: ibuler <ibuler@qq.com>
2021-03-03 14:47:44 +08:00
Orange
4ee32dd51b fix: 修复应用授权克隆报错 2021-03-03 13:51:22 +08:00
ibuler
19dc6aa5a0 fix: 修复编辑出问题的bug 2021-03-01 17:12:17 +08:00
fit2bot
8c7f08a971 refactor: 重构云同步账号模块 (#624)
* refactor: cloud (v0.1)

* refactor: 重构云同步账号模块

* fix: 干掉多余的代码

Co-authored-by: Bai <bugatti_it@163.com>
Co-authored-by: Orange <orangemtony@gmail.com>
2021-03-01 15:01:31 +08:00
ibuler
8df3841040 perf(applications): 去掉remote app中的网域 2021-03-01 11:07:48 +08:00
fit2bot
a4a14fecdd refactor: 重构录像存储模块 (#620)
* refactor: 重构录像存储模块

* refactor: 重构录像存储模块

* refactor: 重构录像存储模块

Co-authored-by: Orange <orangemtony@gmail.com>
2021-02-26 14:54:25 +08:00
Orange
52616fead9 fix: 修复RelationCard展示问题 2021-02-26 14:52:11 +08:00
fit2bot
95d0afc5cb feat: 添加模块自定义表格列 (#618)
* feat: 添加资产管理模块自定义表格列

* feat: 添加应用管理模块自定义表格列

* feat: 添加授权模块自定义表格列

* feat: 添加会话模块自定义表格列

Co-authored-by: Orange <orangemtony@gmail.com>
2021-02-26 14:51:14 +08:00
xinwen
07c36e717e refactor: 调整校对节点资产数量 url 2021-02-23 17:29:10 +08:00
ibuler
de393cd2b6 perf(ops): 优化命令执行的输出 2021-02-20 11:09:47 +08:00
Orange
1203941e6b perf: 表格配置存储到localStorage中 2021-02-08 00:28:13 -06:00
Orange
cfa8fcf352 perf: 允许用户修改Source 2021-02-08 00:27:17 -06:00
ibuler
8bfd6b8654 perf: 优化table下拉的配色 2021-02-05 14:49:06 +08:00
ibuler
9840396a6f feat: 修复min的问题 2021-01-29 11:05:07 +08:00
ibuler
309d9379b9 feat: 修改过滤逻辑 2021-01-29 11:05:07 +08:00
Orange
a15ce0b77f feat: 添加自定义表格列功能 2021-01-29 11:05:07 +08:00
Orange
06e80fe75f feat: 添加自定义表格列功能 2021-01-29 11:05:07 +08:00
fit2bot
009be1be83 fix: 临时调整因为Chrome的兼容性问题导致的日期计算错误 (#605)
* fix: 临时调整因为Chrome的兼容性问题导致的日期计算错误

* fix: 修复工单字段展示问题

Co-authored-by: Orange <orangemtony@gmail.com>
2021-01-28 19:29:55 +08:00
Orange
eb20b32fcf fix: 修复工单列表字段展示错误 2021-01-26 04:08:56 -06:00
ibuler
9b19d862f6 perf: 优化ldap配置 2021-01-26 18:01:54 +08:00
ibuler
38b21357b7 perf(settings): 修改翻译 2021-01-26 18:01:54 +08:00
ibuler
c40bd0a9ab perf: 完成终端和安全setting 2021-01-26 18:01:54 +08:00
ibuler
48a7310739 perf(settings): 优化配置页面 2021-01-26 18:01:54 +08:00
Jiangjie.Bai
2c69b36291 Merge pull request #598 from jumpserver/dev
Dev
2021-01-21 15:35:46 +08:00
Orange
d267cd1f5e fix: 移除提交应用申请工单时多余的字段 2021-01-21 15:33:02 +08:00
Orange
807e3a407a fix: 移除重复的权限提醒 2021-01-21 15:33:02 +08:00
Orange
1ba790e680 fix: 调整工单主机名字段变成主机名组 2021-01-21 15:33:02 +08:00
Orange
44b701edbc fix: 修复命令过滤规则创建提示有详情的问题 2021-01-21 15:33:02 +08:00
fit2bot
8619ab8bca fix: 修改测试产生的bugs (#596)
* fix: 修改创建录像存储的条件为必填

* fix: 修改最后一次执行的log类型为Ansible

* fix: 修复批量测试资产可连接性的权限问题

Co-authored-by: Orange <orangemtony@gmail.com>
2021-01-21 12:43:36 +08:00
Jiangjie.Bai
79e92fa46b Merge pull request #595 from jumpserver/dev
Dev
2021-01-20 19:27:59 +08:00
fit2bot
f19c863440 feat: 添加管理用户列表 (#588)
* feat: 添加管理用户列表

* fix: 整理管理用户详情页面

* fix: 整理管理用户详情页面

Co-authored-by: Orange <orangemtony@gmail.com>
2021-01-20 19:07:00 +08:00
fit2bot
fff8b79a45 fix: 修正测试产生的Bugs (#593)
* fix: 调整登录复核工单字段

* fix: 移除创建网关的详情链接展示

* fix: 调整我发起的工单的API字段

* fix: 移除工单申请表单的非必选字段

* fix: 修正创建密码匣子时表单错误

* fix: 调整申请应用时推荐应用对应字段

* fix: 移除旧版本请求

* fix: 优化内部变量写法

* fix: 干掉旧刷新方法

* fix: 优化写法

Co-authored-by: Orange <orangemtony@gmail.com>
2021-01-20 17:32:52 +08:00
fit2bot
cf810b3d3e revert: 回滚表格自定义列功能 (#594)
* revert: 回滚表格自定义列功能

* fix: 优化写法

Co-authored-by: Orange <orangemtony@gmail.com>
2021-01-20 17:12:04 +08:00
fit2bot
f58e37a76a feat: 添加批量测试资产可连接性功能 (#590)
* feat: 添加批量测试资产可连接性功能

* feat: 添加批量测试资产可连接性功能

Co-authored-by: Orange <orangemtony@gmail.com>
2021-01-20 14:56:03 +08:00
ibuler
5889e20aae fix(perms): 修复应用详情中移除系统用户的错误 2021-01-20 14:39:00 +08:00
fit2bot
7caa2c8264 fix: 禁止用户更新用户名与登录名相同选项 (#591)
* fix: 禁止用户更新用户名与登录名相同选项

Co-authored-by: Orange <orangemtony@gmail.com>
2021-01-20 11:27:29 +08:00
Jiangjie.Bai
865388dedc Merge pull request #589 from jumpserver/dev
Dev
2021-01-19 19:52:52 +08:00
Orange
35c1077eed fix: 修复新版工单的Bugs 2021-01-19 19:50:35 +08:00
Orange
487e199995 fix: 修复资产列表无克隆创建的bug 2021-01-19 03:31:23 -06:00
fit2bot
f584e96675 fix: 暂时移除应用的克隆创建 (#587)
Co-authored-by: Orange <orangemtony@gmail.com>
2021-01-19 17:29:17 +08:00
fit2bot
1f4f1d3712 fix: 调整Session过期检查 10mins -> 30s (#586)
* fix: 调整Session过期检查 10mins -> 30s

Co-authored-by: Orange <orangemtony@gmail.com>
2021-01-19 17:23:02 +08:00
fit2bot
08facb1eda fix: 添加WS链接类型 (#585)
* fix: 添加WS链接类型


Co-authored-by: Orange <orangemtony@gmail.com>
2021-01-19 17:12:52 +08:00
Jiangjie.Bai
34cb9424d4 Merge pull request #582 from jumpserver/dev
Dev
2021-01-17 17:59:42 +08:00
fit2bot
36767cd265 feat: 添加组织详情统计 (#580)
* feat: 添加组织详情统计

* feat: 添加组织详情统计

Co-authored-by: Orange <orangemtony@gmail.com>
2021-01-13 20:48:25 +08:00
Orange
86b9fc8f5a fix: 修复工单详情问题 2021-01-13 19:05:50 +08:00
Orange
49054e5dc0 fix: 修复工单详情问题 2021-01-13 19:05:50 +08:00
Orange
bac7cef23c fix: 修复工单详情问题 2021-01-13 19:05:50 +08:00
Orange
4013ea6212 fix: 修复表单生成问题 2021-01-13 18:51:13 +08:00
Orange
37153ebe1d fix: 修复用户界面加载问题 2021-01-13 18:41:51 +08:00
Orange
da35d9be25 修改版权时间 2021-01-12 17:16:33 +08:00
Orange
c3c24b0ad1 feat: 创建资产授权时按协议过滤资产 2021-01-12 17:16:17 +08:00
Orange
98da517724 fix: 移除命令过滤规则的克隆功能 2021-01-12 17:16:04 +08:00
Orange
f002c7f917 feat: 优化重构工单 2021-01-12 15:07:42 +08:00
Orange
0d4e4324ce feat: 重构申请工单系统 2021-01-12 15:07:42 +08:00
Orange
fdeab46970 feat: 重构申请工单系统 2021-01-12 15:07:42 +08:00
Orange
5acbdd5679 feat: 修改录像存储和会话存储接口 2021-01-12 15:07:25 +08:00
Orange
3a64120241 fix: 移除多余的Console.log 2021-01-12 15:04:31 +08:00
Orange
81d1cbf3a1 perf: 修复表格生成Error并记录页码数据 2021-01-12 15:04:31 +08:00
Orange
cec17bbef8 Merge pull request #537 from jumpserver/pr@dev@feat_custom_col_list
feat: 允许用户自定义表格列显示功能
2021-01-12 02:10:29 +08:00
Orange
fbc3373e1b Merge branch 'dev' into pr@dev@feat_custom_col_list 2021-01-12 02:10:02 +08:00
老广
b4a935ab15 Merge pull request #563 from jumpserver/pr@dev@fix_test_storage_notify
fix: 统一测试存储时的命令提醒
2021-01-06 14:33:14 +08:00
老广
7fbff42067 Merge pull request #560 from jumpserver/pr@dev@fix_remove_unuse_func
fix: 移除未使用的函数
2021-01-06 14:08:36 +08:00
老广
5077fec5a8 Merge pull request #564 from jumpserver/pr@dev@fix_create_ticeket
fix: 修复创建工单提示信息
2021-01-06 11:20:27 +08:00
老广
c4619af96f Merge pull request #566 from jumpserver/pr@dev@fix_tree_rightclick
fix: 修复资产树超过屏幕,右键菜单显示不全问题
2021-01-06 11:03:30 +08:00
Orange
025d0abeae fix: 修复资产树超过屏幕,右键菜单显示不全问题 2021-01-05 16:45:22 +08:00
Orange
5735a591ba feat: 允许用户自定义表格列显示功能 2020-12-30 20:08:58 +08:00
Orange
3b664ee1dc feat: 允许用户自定义表格列显示功能 2020-12-30 19:59:23 +08:00
Orange
a3f6de330e fix: 修复创建工单提示信息 2020-12-25 11:15:14 +08:00
Orange
cbc67a5a4c fix: 统一测试存储时的命令提醒 2020-12-23 15:53:44 +08:00
Orange
dec0593907 fix: 移除未使用的函数 2020-12-21 18:23:23 +08:00
Orange
1ed432b1e2 feat: 添加资产编号字段 2020-12-21 14:32:24 +08:00
Jiangjie.Bai
b65664f9c4 Merge pull request #557 from jumpserver/dev
Merge Dev
2020-12-17 18:13:49 +08:00
Orange
f64def0bec fix: 修改箭头颜色 2020-12-17 18:13:09 +08:00
Orange
06f6202bc4 fix: Filter使用后端搜索 2020-12-17 18:13:09 +08:00
Jiangjie.Bai
6fa7800d6b Merge pull request #554 from jumpserver/dev
Chore: Merge Dev
2020-12-17 15:14:11 +08:00
ibuler
54aa252c20 fix(some): 修复没有clone的列表 2020-12-17 15:12:53 +08:00
ibuler
c083f6c4a4 fix(clone): 修复一下clone丢失的问题 2020-12-17 15:12:53 +08:00
Orange
73bb854ebb fix: 去除批量移除, 去除简单写法 2020-12-17 15:03:28 +08:00
Orange
d6f9df277e fix: 修复样式异常和Bug 2020-12-17 15:03:28 +08:00
Jiangjie.Bai
ba78e33f89 Merge pull request #551 from jumpserver/dev
chore: Merge master from dev
2020-12-16 18:42:40 +08:00
Orange
09ef15cff0 fix: 修复工单提示报错 2020-12-16 17:54:38 +08:00
Orange
62d520e625 fix: 修复工单提示报错 2020-12-16 17:54:38 +08:00
Orange
f07a857813 fix: 修复命令过滤器添加系统用户问题 2020-12-16 17:54:38 +08:00
Orange
2699d5e8eb fix: 禁用命令存储文档类型修改 2020-12-16 17:54:38 +08:00
Orange
fb398ca3e4 调整页面errorMessage 2020-12-16 17:54:38 +08:00
Orange
3230c37318 fix: 调整页面保存设置, 优化页面Bug 2020-12-16 17:54:38 +08:00
Jiangjie.Bai
bc258a7ff8 Merge pull request #549 from jumpserver/dev
chore: Merge master from dev
2020-12-15 20:33:14 +08:00
Orange
64d610e282 fix: 修改系统监控页面 2020-12-15 20:32:04 +08:00
Orange
4d95461b5c fix: 添加组件负载状态 2020-12-15 20:32:04 +08:00
Orange
f364c8fdf9 fix: 批量修复2.6版本测试产生的Bug 2020-12-15 20:32:04 +08:00
Orange
88dc2d9271 Merge pull request #547 from jumpserver/dev
fix: 批量修复2.6版本测试产生的Bug
2020-12-14 19:05:38 +08:00
Orange
3aced25da4 fix: 批量修复2.6版本测试产生的Bug 2020-12-14 19:00:16 +08:00
老广
fcf142b696 Merge pull request #544 from jumpserver/dev
Dev
2020-12-11 19:33:54 +08:00
fit2bot
2123037897 fix: 添加组件总量显示 (#543)
Co-authored-by: Orange <orangemtony@gmail.com>
2020-12-11 19:30:58 +08:00
Orange
f5bc2842ec fix: 修复日期显示问题及用户应用授权更新路由问题 2020-12-11 19:28:26 +08:00
Orange
2c95e5f10b fix: 移除中断心跳间隔设置并添加认证方式字段 2020-12-11 19:28:08 +08:00
Jiangjie.Bai
b3ff9c5bcb Merge pull request #539 from jumpserver/dev
chore(merge): dev 合并到 master
2020-12-10 23:47:30 +08:00
Orange
905e5e00b1 fix: 去掉多余JS引用 2020-12-10 23:43:43 +08:00
Orange
81db3d86fa feat: 添加系统监控页面 2020-12-10 23:43:43 +08:00
Orange
ea15515264 feat: 系统用户详情页面增加资产分页面,增加指定资产推送功能 2020-12-10 20:54:14 +08:00
fit2bot
4048a000c7 feat: 添加表格过滤字段选项 (#527)
* feat: 添加表格过滤字段选项

* fix: 如果已有Filter的情况下去掉过滤

Co-authored-by: Orange <orangemtony@gmail.com>
2020-12-10 17:52:12 +08:00
xinwen
a60693c41c perf(asset): 资产树,右击增加计算节点数量的菜单,可以让后台去计算 #527 2020-12-10 17:51:10 +08:00
Orange
06d6c54db8 feat: 允许用户自定义表格列显示功能 2020-12-10 14:26:17 +08:00
fit2bot
57d5c893d3 fix: 修复自动生成表格字段冲突问题 (#536)
Co-authored-by: Orange <orangemtony@gmail.com>
2020-12-09 16:32:52 +08:00
fit2bot
435ce24c75 fix: 修复删除source字段引起的无法更新密码的问题 (#534)
Co-authored-by: Orange <orangemtony@gmail.com>
2020-12-09 16:21:59 +08:00
Orange
4a757bb6bc fix: 不允许用户修改source字段 2020-12-08 20:56:11 +08:00
fit2bot
32fa4f0b11 feat: 资产允许编辑更多信息 (#530)
* feat: 资产允许编辑更多信息

* feat: 资产允许编辑更多资产信息

* feat: 资产允许编辑更多资产信息

* fix: 修改组件名称

Co-authored-by: Orange <orangemtony@gmail.com>
2020-12-08 20:25:54 +08:00
Orange
9f12e1aa18 perf: 禁用应用的导入导出 2020-12-08 11:12:00 +08:00
jym503558564
6165709747 perf(assetBatchOperation): 资产模块添加批量删除操作 2020-12-08 11:10:15 +08:00
fit2bot
0ad1eef196 feat: 添加xlsx格式支持 (#526)
* feat: 添加xlsx格式支持

* feat: 添加xlsx格式支持

* feat: 添加xlsx格式支持

* feat: 添加xlsx格式支持

Co-authored-by: Orange <orangemtony@gmail.com>
2020-12-07 15:22:56 +08:00
Orange
d2d07555b5 fix: 修复批量更新终端存储时附带的保存并继续添加 2020-12-07 14:06:48 +08:00
Orange
7f60224c6d fix: 修复会话时间显示问题 2020-12-07 14:04:26 +08:00
Orange
bbf502c85d fix: 调整select2组件默认长度 2020-12-07 14:03:30 +08:00
Orange
e6aaa52506 perf: 恢复Web终端入口 2020-11-26 12:38:28 +08:00
ibuler
70affacfde fix(list): 修复列表克隆的bug 2020-11-26 12:37:28 +08:00
Orange
40a8da5e58 fix: 修复批量更新组件的问题 2020-11-23 14:00:34 +08:00
Orange
24266bb929 fix: 修复批量批量更新组件的问题 2020-11-22 16:53:01 +08:00
Jiangjie.Bai
14286b961e Merge pull request #514 from jumpserver/dev
Merge dev
2020-11-18 11:49:17 +08:00
Orange
0498db9a8f fix: 修复应用授权过滤系统用户的问题及系统用户创建的问题 2020-11-18 11:47:19 +08:00
ibuler
266d107ffd fix(clone): 修复会话页面产生的clone的问题 2020-11-18 10:55:13 +08:00
ibuler
4f2a9c0c6c fix(clone): 修复会话页面产生的clone的问题 2020-11-18 10:35:40 +08:00
Jiangjie.Bai
affb0ec2bb Merge pull request #509 from jumpserver/dev
Merge Dev
2020-11-17 19:45:03 +08:00
Orange
3075d50357 fix: 修复任务列表字段显示问题 2020-11-17 19:30:36 +08:00
Orange
e49da02c4d fix: 修复RemoteApp详情点击更新失败的问题 2020-11-17 19:07:20 +08:00
Orange
7df5736354 fix: 修改默认版本号以及添加修改应用详情页添加系统用户bug 2020-11-17 18:33:14 +08:00
ibuler
98886149f9 perf(tickets): 优化工单备注 2020-11-17 18:13:40 +08:00
ibuler
abb98d55b9 fix(tickets): 修复工单备注的说明 2020-11-17 18:13:40 +08:00
ibuler
f9c979af88 fix(table): 修复克隆出现在不应该出现的表单上 2020-11-17 18:13:15 +08:00
ibuler
89018a2258 fix(assets|users): 修复批量更新上保存并提交 close #469 2020-11-17 18:13:15 +08:00
Orange
a0c29563ca fix: 显示License过期提示 2020-11-17 16:57:29 +08:00
Orange
21223fddea perf: 优化用户详情中授权的应用列表 2020-11-17 16:54:58 +08:00
Orange
254a2b58cc fix: 用户来源不是本地时禁用更新密码和更新SSH 2020-11-17 16:44:42 +08:00
fit2bot
777c371070 fix: 修复工单提交时必须输入系统用户的提醒 (#503)
* fix: 修复工单提交时必须输入系统用户的提醒

* fix: 去掉多余的console.log

Co-authored-by: Orange <orangemtony@gmail.com>
2020-11-17 16:33:40 +08:00
Orange
0cba2b3116 fix: 修复删除应用的提示框 2020-11-17 16:31:57 +08:00
Orange
c27dd0baef fix: 修复翻译缺失和更新系统用户的表单问题 2020-11-17 16:18:15 +08:00
Orange
990aebefdd Merge pull request #495 from jumpserver/dev
Dev
2020-11-12 16:14:36 +08:00
Orange
6f84312dbe fix: 修改改密计划密码默认长度 2020-11-12 14:26:23 +08:00
Orange
d4c12fb38f fix: 修复CSS和监控禁用逻辑问题 2020-11-12 12:16:49 +08:00
Orange
1bb94824df perf: 添加可中断API字段 2020-11-11 19:21:30 +08:00
Orange
5772430761 fix: 修复翻译和样式Bug 2020-11-11 19:20:53 +08:00
ibuler
790941f361 fix(common): 修复通用form的值的错误 2020-11-11 14:28:43 +08:00
fit2bot
a9ce01ac0e feat: 云管同步增加Azure模块 (#488)
* feat: 云管同步增加Azure模块


Co-authored-by: Orange <orangemtony@gmail.com>
2020-11-10 17:02:20 +08:00
fit2bot
1cdc406e70 fix: 修复生成的表格,不该拥有克隆的,有了克隆 (#490)
* fix: 修复生成的表格,不该拥有克隆的,有了克隆

* fix: 修复生成的表格,不该拥有克隆的,有了克隆

Co-authored-by: ibuler <ibuler@qq.com>
2020-11-10 15:32:11 +08:00
Orange
cb60660272 fix: 修复应用授权翻译 2020-11-10 10:28:30 +08:00
fit2bot
8625e21077 feat(createupdate): 修改和创建的msg增加detail连接 (#487)
* feat(form): 增加可以连续增加的功能

* feat(createupdate): 修改和创建的msg增加detail连接

Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: Orange <orangemtony@gmail.com>
2020-11-09 15:37:26 +08:00
fit2bot
2251a1653e feat(all): 增加clone创建 (#485)
* perf(lang): 优化语言切换

* perf: 优化命令获取

* feat(all): 增加clone创建

* fix: 修改排序

Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: Orange <orangemtony@gmail.com>
2020-11-09 15:35:43 +08:00
ibuler
ff1debcbce feat(form): 增加可以连续增加的功能 2020-11-09 12:41:03 +08:00
Orange
17e5564cd7 fix: 修复用户界面字段显示问题 2020-11-08 20:40:18 -06:00
fit2bot
615576b3fd perf(passwordExpireTip): 添加密码过期提醒 (#483)
* perf(passwordExpireTip): 初步实现密码过期提醒

* perf(passwordExpireTip): 添加密码过期提醒

Co-authored-by: jym503558564 <503558564@qq.com>
2020-11-09 10:39:27 +08:00
fit2bot
c0d3fbb47a fix: 修复权限认证问题 (#482)
* fix: 修复权限认证问题

* fix: 修复权限认证问题

Co-authored-by: Orange <orangemtony@gmail.com>
2020-11-05 18:59:08 +08:00
ibuler
09075b13b7 perf: 优化命令获取 2020-11-05 18:57:27 +08:00
ibuler
4e92c1a77c perf(lang): 优化语言切换 2020-11-05 18:57:27 +08:00
fit2bot
7cc49bc907 fix: 添加Tree加载重试组件 (#477)
* fix: 添加409错误信息

* fix(perms): 用户授权树重建冲突时弹出提示信息,并保留之前的树

* fix: 添加Tree加载重试组件

Co-authored-by: Orange <orangemtony@gmail.com>
2020-11-05 10:50:38 +08:00
jym503558564
2e2b5bf873 perf(opsTask): 支持批量删除任务信息 2020-11-05 09:53:21 +08:00
Orange
66b1d17dd2 fix: 用户界面隐藏左侧侧边栏web终端入口 2020-11-05 09:52:32 +08:00
Orange
ee26e47c4d fix: 修复CSS样式错误 2020-11-05 09:51:36 +08:00
Orange
aedf6d2158 fix: 修复组织url拼写错误 2020-11-03 18:15:06 +08:00
Orange
6e848e65b4 feat: RDP系统用户增加AD域名字段 2020-11-03 16:13:21 +08:00
Orange
5c0108906c feat: RDP系统用户增加AD域名字段 2020-11-03 16:13:21 +08:00
fit2bot
6c1f8ec8f7 feat: 整理重构应用模块及应用授权模块 (#465)
* feat: 整理重构应用模块及应用授权模块

Co-authored-by: Orange <orangemtony@gmail.com>
2020-11-03 14:28:36 +08:00
Orange
dda36d2b40 fix: 关闭查询保存参数 2020-10-30 17:48:13 +08:00
peijianbo
1abf30c347 feat:危险命令告警 2020-10-28 21:50:24 -05:00
fit2bot
697b5a3d13 perf(permTranslate): 修改翻译 (#462)
* perf(permTranslate): 修改授权一些翻译

* perf(permTranslate): 修改翻译

Co-authored-by: jym503558564 <503558564@qq.com>
2020-10-29 10:49:29 +08:00
jym503558564
74e4c3397e perf(tableWidth): 修改数据库应用列表的宽度 2020-10-28 21:48:34 -05:00
Orange
c227bf59a6 fix: 修复删除节点下资产授权报错的问题 2020-10-28 21:47:56 -05:00
jym503558564
0092f6d6d7 perf(sessionDetail): 修改session详情中的字段翻译 2020-10-28 21:47:31 -05:00
Orange
747477b27c perf: 隐藏左侧边栏Web终端入口 2020-10-28 21:47:00 -05:00
Orange
4943dab50c perf: 去除多余的console.log() 2020-10-28 21:46:28 -05:00
jym503558564
5cac3ee1f7 perf(changeAuthPlan): 改密计划详情中的资产选择,禁选已存在的资产 2020-10-21 12:51:38 +08:00
jym503558564
fa5a227aff perf(renameNode): 修复重命名节点,同名时有两条错误提示信息的问题 2020-10-21 12:51:22 +08:00
jym503558564
792e8595b8 perf(formatterPassword): 统一密码组件 2020-10-21 12:50:55 +08:00
jym503558564
9d62614ff4 perf(formatterPassword): 统一管理用户、系统用户密码组建 2020-10-21 12:50:55 +08:00
jym503558564
48c0f6e8c6 perf(licenseTip): 修复license过期提醒,开源版本出现split提示问题 2020-10-21 12:50:29 +08:00
jym503558564
31b17b384d perf(safari): 修复表格兼容safari,不错位 2020-10-21 12:49:42 +08:00
jym503558564
858d7a9d6f perf(genericCreateUpdate): 修改漏传attr 2020-10-21 12:49:21 +08:00
jym503558564
48b6c48581 perf(userDetail): 统一用户授权详情列表的宽度 2020-10-21 12:48:54 +08:00
八千流
38be9dd367 Merge pull request #443 from jumpserver/pr@dev@perf_userpage_k8s_detail
feat: 用户详情页面添加K8S应用
2020-10-16 09:07:30 +08:00
Orange
7e5570ad72 feat: 用户详情页面添加K8S应用 2020-10-15 18:54:23 +08:00
Orange
16476caa1e Merge pull request #441 from jumpserver/dev
chore: dev to master
2020-10-15 13:05:35 +08:00
八千流
6a5c28ac26 Merge pull request #442 from jumpserver/pr@dev@fix_command_execution
fix: 修复命令执行选择资产报错问题
2020-10-15 13:02:57 +08:00
Orange
5335faa789 fix: 修复命令执行选择资产报错问题 2020-10-15 13:00:27 +08:00
老广
fd1ee6ef7d Merge pull request #439 from jumpserver/pr@dev@fix_badge_hidden
fix: 如果数量为0 隐藏工单Badge
2020-10-14 22:57:40 -05:00
老广
52d8c34bbf Merge pull request #440 from jumpserver/pr@dev@asset_disabled_style
fix: 调整未激活资产展示样式
2020-10-14 22:57:10 -05:00
Orange
9eac41c0c3 fix: 如果数量为0 隐藏工单Badge 2020-10-15 11:45:10 +08:00
Orange
6881316203 fix: 调整未激活资产展示样式 2020-10-15 11:40:11 +08:00
Orange
d4ee8379e8 fix: 如果数量为0 隐藏工单Badge 2020-10-15 10:37:40 +08:00
老广
f64e877491 Merge pull request #435 from jumpserver/dev
chore: Merge Dev to Master
2020-10-14 07:51:05 -05:00
Orange
f46a63cfcf feat: 禁止未激活资产链接 2020-10-14 07:50:34 -05:00
Orange
65a71df10e fix: 在Default组织下隐藏隐藏邀请用户按钮 2020-10-14 19:05:28 +08:00
jym503558564
fd6b0532ba perf(OrgMember): 修复添加组织成员有更多按钮出现的问题 2020-10-14 18:49:11 +08:00
八千流
e02d05a327 Merge pull request #434 from jumpserver/pr@dev@hidden_userpage_tickets
fix: 隐藏用户界面工单
2020-10-14 17:59:24 +08:00
Orange
23740cdce0 fix: 隐藏用户界面工单 2020-10-14 17:57:59 +08:00
Orange
a50f224227 fix: 修复工单数量为0不显示工单按钮的问题 2020-10-14 17:45:47 +08:00
Orange
f8ec327f11 Merge pull request #432 from jumpserver/dev
chore: merge dev to master
2020-10-14 16:04:36 +08:00
ibuler
1d76e037a4 fix: 修复用户邀请 2020-10-14 15:28:58 +08:00
jym503558564
98f5f38694 perf(licenseTip): 管理员有license过期提醒,其它用户无license过期提醒 2020-10-14 15:10:32 +08:00
Orange
b78b95e67a Merge pull request #419 from jumpserver/dev
chore: merge dev to master
2020-10-13 18:53:29 +08:00
Orange
5f60130952 fix: 修复改密计划创建时定期任务创建冲突的问题 2020-10-13 05:49:40 -05:00
Orange
5a82931fc2 fix: 修复时间选择器显示宽度问题 2020-10-13 05:49:00 -05:00
jym503558564
ad49e3250b perf(licenseExpiredTip): 去掉系统设置里面license过期提醒 2020-10-13 17:57:49 +08:00
jym503558564
7ef95f4567 perf(licenseExporeTip): 初步实现license过期提醒 2020-10-13 17:57:49 +08:00
Orange
25a9d21fd7 fix: 修改Tree的刷新Url 2020-10-13 03:06:00 -05:00
Orange
b849df1dc1 fix: 修复表格的时间过滤问题 2020-10-13 03:00:31 -05:00
Orange
1519ccb8e2 fix: 修改添加用户API 2020-10-12 05:40:14 -05:00
fit2bot
47e88e7bb4 perf(org): 添加组织成员合并成一个card (#422)
* perf(org): 添加组织成员合并成一个card

* perf(org): 添加成员后,object需重新刷新才能重新获取新值

Co-authored-by: jym503558564 <503558564@qq.com>
2020-10-12 10:55:56 +08:00
Orange
c86aef999c fix: 调整badge位置 2020-10-11 21:45:41 -05:00
jym503558564
2f9d7ab826 perf(protocol): 批量更新协议组给默认值 2020-10-11 21:41:42 -05:00
fit2bot
b4311b8a59 perf(permSelectAssetCard): 优化授权详情页中的添加资产,将已添加的资产disable掉 (#405)
* perf(permSelectAssetCard): 优化授权详情页中的添加资产,将已添加的资产disable掉

* perf(permSelectAssetCard): 修改全选时,禁用也被选择的bug

* perf(assetSelect): 修改名称

Co-authored-by: jym503558564 <503558564@qq.com>
2020-10-09 11:05:42 +08:00
Jiangjie.Bai
04a97a9923 Merge pull request #418 from jumpserver/dev
Merge Dev
2020-10-09 10:56:15 +08:00
jym503558564
c7624f9092 perf(relationCard): 优化relationCard以及添加组织成员翻译 2020-10-08 21:08:02 -05:00
fit2bot
fc29fc6c6d feat: 工单入口移动至Header栏 (#417)
* feat: 工单入口移动至Header栏

* feat: 工单入口移动至Header栏

Co-authored-by: Orange <orangemtony@gmail.com>
2020-10-09 10:00:23 +08:00
ibuler
7afc501db5 perf: 修改长度 2020-09-30 03:45:31 -05:00
Orange
8ed5672e95 fix: 修复工单审批时系统用户多选参数配置问题 2020-09-29 19:02:13 +08:00
Orange
951c9f56c5 fix: 修复工单审批时系统用户多选参数配置问题 2020-09-29 19:02:13 +08:00
Orange
2c0e079aa2 fix: 修复工单审批时系统用户多选参数配置问题 2020-09-29 16:35:11 +08:00
jym503558564
a8e7ea9c80 perf(ticket): 优化授权工单,支持填写系统用户,审批时系统用户支持选多个 2020-09-29 15:13:10 +08:00
Orange
30143f833a feat: 添加邀请用户进入组织功能 (#411)
* feat: 添加邀请用户进入组织功能
2020-09-29 15:10:48 +08:00
ibuler
ad88daef9a chore: merge xpack dev pr 2020-09-29 14:26:05 +08:00
ibuler
9eb80eb6ca chore: add package 2020-09-29 14:26:05 +08:00
Orange
112de6e81c feat: 添加403请求拦截器
Closes https://github.com/jumpserver/trello/issues/238
2020-09-28 16:51:57 +08:00
Orange
d01f903a9e fix: 临时禁用树节点移动
Closes https://github.com/jumpserver/lina/issues/383
2020-09-28 16:50:39 +08:00
Orange
af6d0aff7c fix: 使用http-equiv功能禁止index.html缓存 2020-09-28 16:49:02 +08:00
Orange
edf8621e8f fix: 修复版本更新Index.html不刷新的问题 2020-09-28 16:49:02 +08:00
Orange
eeb15c624a fix: 修复组织名称更新列表未刷新的问题 2020-09-28 16:48:15 +08:00
Orange
79e2d49a3d fix: 修复页面title设置不生效问题
Closes https://github.com/jumpserver/trello/issues/302
2020-09-28 16:47:22 +08:00
Orange
628395e447 fix: 修复命令存储更新问题 2020-09-24 15:54:23 +08:00
八千流
6cb6e6444b Merge pull request #394 from jumpserver/pr@dev@fix_command_filter
feat: 修复命令过滤详情添加系统用户的问题
2020-09-22 15:09:19 +08:00
八千流
1b9aa23761 Merge pull request #397 from jumpserver/pe@dev@fix_change_orgs_url
fix: 修复更改组织时路径跳转的问题
2020-09-22 15:08:56 +08:00
jym503558564
537c385ecf perf(licenseExpired): 优化license过期提醒 2020-09-22 15:06:39 +08:00
jym503558564
c0f7d6e7ff perf(translate): 修改界面设置的一些字段翻译 2020-09-22 15:05:41 +08:00
jym503558564
78df96f888 feat(batchUpdateProtocols): 新增批量更新协议组 2020-09-22 15:05:08 +08:00
jym503558564
829957090d fix(gateway): 修复测试网关端口数据类型:String改为Int类型 2020-09-22 15:04:42 +08:00
jym503558564
026c8f37ea perf(navHeader): 在页面右上角添加web终端 2020-09-22 15:04:02 +08:00
Orange
97340c6aac feat: 修复更改组织时路径跳转的问题 2020-09-18 15:18:55 +08:00
Orange
bbe54eae48 fix: 修复Build失败的问题 2020-09-18 11:43:19 +08:00
Orange
9137207055 feat: 修复命令过滤详情添加系统用户的问题 2020-09-17 15:10:30 +08:00
Jiangjie.Bai
0ef30ef651 Merge pull request #392 from jumpserver/dev
Merge Dev
2020-09-17 12:11:23 +08:00
Orange
5ae8a6c9e4 fix: 禁用命令记录列表导出选择项 2020-09-17 12:10:46 +08:00
Orange
ace1dcd0b8 fix: 修复切换组织时权限展示问题
Closes https://github.com/jumpserver/trello/issues/332
2020-09-17 11:06:40 +08:00
Jiangjie.Bai
5df487d6bd Merge pull request #390 from jumpserver/dev
chore: 去掉submodule
2020-09-15 17:21:32 +08:00
ibuler
6e548749e1 Removed submodule 2020-09-15 16:26:11 +08:00
ibuler
99885d9f28 chore: 去掉submodule 2020-09-15 16:16:11 +08:00
Orange
83163e11e3 Merge pull request #387 from jumpserver/dev
Merge dev
2020-09-15 16:02:49 +08:00
Orange
8c191fee67 Merge pull request #386 from jumpserver/pr@dev@feat_hidden_tickets
perf: 优化license的路由判断
2020-09-15 15:17:32 +08:00
ibuler
ff9862fa06 perf: 优化license的路由判断 2020-09-15 15:14:07 +08:00
ibuler
db0cea7051 feat: 可以设置隐藏工单 2020-09-15 14:55:38 +08:00
Orange
305c713a57 Merge pull request #382 from jumpserver/dev
chore: 更新Xpack版本
2020-09-08 20:35:35 +08:00
Orange
8a5f93e268 chore: 更新Xpack版本 2020-09-08 20:34:19 +08:00
Jiangjie.Bai
c4d262150b Merge pull request #374 from jumpserver/dev
Merge dev to master
2020-09-08 20:28:47 +08:00
Jiangjie.Bai
94f161f7e6 Merge pull request #380 from jumpserver/pr@dev@merge_Master
修复Merge冲突
2020-09-08 20:26:52 +08:00
Orange
2c5bfb3f4c 修复Merge冲突 2020-09-08 20:24:35 +08:00
Orange
8e12837a77 fix: 修复请求计时器时间问题 2020-09-08 20:22:29 +08:00
ibuler
4385d84f01 feat: 请求时刷新session age 2020-09-08 20:15:42 +08:00
Orange
214bb28c4c chore: 更新Submodule指向 2020-09-08 20:14:56 +08:00
Orange
14c2285ac8 Merge pull request #375 from jumpserver/pr@master@update_xpack_brench
chore: 更新Submodule指向
2020-09-08 20:11:41 +08:00
Orange
18cbe578f0 chore: 更新Submodule指向 2020-09-08 20:09:09 +08:00
fit2bot
e19ded8365 bug(userDetail): 修复用户详情删除其某条授权规则失败的问题 (#371)
Co-authored-by: jym503558564 <503558564@qq.com>
2020-09-08 19:20:24 +08:00
fit2bot
0150008075 fix: 修复备注过长的显示问题 (#373)
Closes https://github.com/jumpserver/lina/issues/341

Co-authored-by: Orange <orangemtony@gmail.com>
2020-09-08 19:19:20 +08:00
Orange
b0bca65cab fix: 修复工单列表提交的问题
Closes https://github.com/jumpserver/trello/issues/312
2020-09-08 12:17:49 +08:00
jym503558564
cb95b9ba4f perf(assetCreateUpdate): 优化资产创建或更新页面的ip字段翻译 2020-09-07 20:05:25 +08:00
jym503558564
4a840288c5 pref(assetSelect): 优化资产选择表单字段翻译 2020-09-07 20:05:25 +08:00
ibuler
11b1d6638d perf(xpack): 优化license导入 2020-09-07 20:04:07 +08:00
Bai
2225807a36 feat(tickets): 工单添加comment字段 2020-09-07 20:01:22 +08:00
Bai
a8baca81d5 feat(i18n): 添加云同步实例任务的hostname_strategy翻译 2020-09-07 17:52:05 +08:00
jym503558564
30161c7178 perf(pref_ticket_badge): 优化badge的背景色 2020-09-07 17:41:33 +08:00
jym503558564
e222d147f6 pref(tickerBadge): 在ticket页面添加badge 2020-09-07 17:41:33 +08:00
jym503558564
3ddc41707c pref(storage): 修改录像存储的配置 2020-09-03 12:51:47 +08:00
jym503558564
a245055150 pref(storageHelpText): 添加录像存储endpoint提示 2020-09-03 12:51:47 +08:00
Orange
ba5fdf2027 fix: 修复LDAP部分问题 2020-09-03 12:50:50 +08:00
jym503558564
e1999e5ce8 pref(textarea): 优化textarea的minRows为3 2020-09-02 14:23:22 +08:00
jym503558564
be4e0b5e35 pref(formRequired): 统一form required 2020-09-02 14:22:50 +08:00
八千流
82c381b80d Merge pull request #359 from jumpserver/revert-356-pr@dev@pref_ticket_comment
Revert "pref(ticket): 优化工单评论显示,我的回复显示在右边,其它回复均显示在左边"
2020-09-02 10:16:53 +08:00
老广
7d71aa96b9 Revert "pref(ticket): 优化工单评论显示,我的回复显示在右边,其它回复均显示在左边 (#356)"
This reverts commit cea03df4eb.
2020-09-02 10:15:02 +08:00
Orange
93408e52c1 feat: 导入组件时按钮显示加载状态
Closes https://github.com/jumpserver/trello/issues/87
2020-09-01 11:45:59 +08:00
fit2bot
cea03df4eb pref(ticket): 优化工单评论显示,我的回复显示在右边,其它回复均显示在左边 (#356)
* pref(ticket): 优化工单评论显示,我的回复显示在右边,其它回复均显示在左边

* pref(ticket): 优化评论显示

* pref(ticket): 修改方法名

Co-authored-by: jym503558564 <503558564@qq.com>
2020-09-01 11:34:43 +08:00
fit2bot
72ee5f60b9 pref(permission): 资产授权详情页添加动作字段 (#353)
* pref(permission): 资产授权详情页添加动作字段

* pref(permission): 修改翻译

Co-authored-by: jym503558564 <503558564@qq.com>
2020-09-01 11:14:58 +08:00
jym503558564
f6a8e5634b pref(userProfile): 优化ssh公钥textarea字段为自适应高度 2020-09-01 11:00:00 +08:00
Orange
868e77c983 feat: 添加测试网关弹窗
Closes https://github.com/jumpserver/trello/issues/191
2020-09-01 10:57:57 +08:00
jym503558564
2a3fd42ca1 pref(storage): 优化存储表单页面 2020-08-31 13:45:11 +08:00
jym503558564
596a26bfb6 pref(session): 优化会话列表字段翻译 2020-08-31 13:40:19 +08:00
Orange
2fc8cea9ef fix: 修复K8S系统用户更新时令牌是必填项的问题
Closes https://github.com/jumpserver/trello/issues/276
2020-08-28 18:03:20 +08:00
Orange
6f8a5c2bfc fix: 授权Actions组件默认折叠
Closes https://github.com/jumpserver/trello/issues/287
2020-08-28 18:00:46 +08:00
Orange
260901351f 修复Core设置SECURITY_VIEW_AUTH_NEED_MFA失效的问题 2020-08-28 17:59:45 +08:00
Orange
66845e58db 修复Core设置SECURITY_VIEW_AUTH_NEED_MFA失效的问题 2020-08-28 17:59:45 +08:00
OrangeM21
0e9e549bea fix: 修改样例中的端口号 2020-08-20 16:51:46 +08:00
OrangeM21
785611414e fix: 修改样例中的端口号 2020-08-20 16:50:38 +08:00
Orange
58505d2b50 Update common.js 2020-08-19 23:42:17 -05:00
OrangeM21
ecae504a80 fix: 修复Safari时间显示问题
Closes https://github.com/jumpserver/trello/issues/237
2020-08-19 23:42:17 -05:00
OrangeM21
844bc5b44f fix: 更新翻译 2020-08-19 22:22:39 -05:00
OrangeM21
c1dcf82fbd fix: 更新翻译 2020-08-19 22:22:39 -05:00
OrangeM21
336406ddff fix: 调整工单翻译以及角色顺序 2020-08-19 21:51:41 -05:00
OrangeM21
81e8e650bf fix: 修改工单备注提交结构 2020-08-19 09:12:39 -05:00
OrangeM21
af6308e1b3 fix: 修改Xpack指向最新代码 2020-08-19 09:11:34 -05:00
OrangeM21
aae552f374 fix: 更新密码后刷新表单 2020-08-19 08:48:51 -05:00
OrangeM21
b4c1ee786a fix: 修改添加组织表单 2020-08-19 08:48:26 -05:00
OrangeM21
61da88114d update 2020-08-19 07:38:14 -05:00
OrangeM21
279859ce81 update 2020-08-19 07:38:14 -05:00
OrangeM21
820bb075a3 update 2020-08-19 07:38:14 -05:00
OrangeM21
d96bd76ca9 fix: 创建用户字段显示隐藏问题 2020-08-19 07:38:14 -05:00
OrangeM21
7c56c889f2 fix: 修改翻译
其他 => 其它

Closes https://github.com/jumpserver/trello/issues/181
2020-08-19 07:37:25 -05:00
OrangeM21
3547fb26ad fix: 优化创建系统用户时表单联动
Closes https://github.com/jumpserver/trello/issues/180
2020-08-19 07:36:57 -05:00
jym503558564
ac1363b377 pref(settings): 优化系统设置提示英文的问题 2020-08-19 07:36:11 -05:00
jym503558564
6b5c90ee86 pref(K8s): 统一Kubernetes翻译 2020-08-19 07:35:39 -05:00
OrangeM21
8164fa57ef update 2020-08-19 07:34:31 -05:00
OrangeM21
96c9f229e2 update 2020-08-19 07:34:31 -05:00
OrangeM21
58313f5fe0 update 2020-08-19 07:34:31 -05:00
OrangeM21
738a9c3da1 update 2020-08-19 07:34:31 -05:00
OrangeM21
0f10ed9ffc fix: 修复工单关闭请求问题 2020-08-19 07:34:31 -05:00
OrangeM21
294e05cb06 fix: 修复工单列表的状态展示
Closes https://github.com/jumpserver/trello/issues/198
2020-08-19 07:32:19 -05:00
OrangeM21
1598dcbfbc fix: 通用导出组件提供默认选项
Closes https://github.com/jumpserver/trello/issues/253
2020-08-19 07:31:00 -05:00
jym503558564
cf2d6a47c2 fix(systemUserCreate): 优化系统用户创建页面 2020-08-19 07:30:31 -05:00
jym503558564
c8fb334dae pref(i18n): 修改工单详情页的字段翻译 2020-08-19 07:27:55 -05:00
OrangeM21
93dba0bbee fix(csrfToken): 更新CsrfToken的获取方式,改为从Cookie中获取 2020-08-19 07:27:30 -05:00
fit2bot
2832d876fd fix(systemUser): 修复创建k8s系统用户的问题 (#315)
Co-authored-by: jym503558564 <503558564@qq.com>
2020-08-17 10:33:19 +08:00
339 changed files with 15537 additions and 5160 deletions

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "src/views/xpack"]
path = src/views/xpack
url = git@github.com:jumpserver/lina-xpack.git

View File

@@ -46,7 +46,7 @@ server {
## License & Copyright
Copyright (c) 2014-2020 飞致云 FIT2CLOUD, All rights reserved.
Copyright (c) 2014-2021 飞致云 FIT2CLOUD, All rights reserved.
Licensed under The GNU General Public License version 2 (GPLv2) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

View File

@@ -1,5 +1,8 @@
module.exports = {
presets: [
'@vue/app'
],
plugins: [
'@babel/plugin-proposal-optional-chaining',
]
}

View File

@@ -1,9 +1,19 @@
server {
listen 80;
gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
#gzip_http_version 1.0;
gzip_comp_level 8;
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
gzip_vary off;
gzip_static on;
gzip_disable "MSIE [1-6].";
location /ui/ {
try_files $uri / /ui/index.html;
alias /opt/lina/;
try_files $uri / /ui/index.html;
alias /opt/lina/;
}
location / {

View File

@@ -15,17 +15,23 @@
"test:ci": "npm run lint && npm run test:unit",
"svgo": "svgo -f src/icons/svg --config=src/icas/svgo.yml",
"vue-i18n-extract": "vue-i18n-extract",
"vue-i18n-report": "vue-i18n-extract report -v './src/**/*.?(js|vue)' -l './src/i18n/langs/**/*.json'"
"vue-i18n-report": "vue-i18n-extract report -v './src/**/*.?(js|vue)' -l './src/i18n/langs/**/*.json'",
"vue-i18n-report-json": "vue-i18n-extract report -v './src/**/*.?(js|vue)' -l './src/i18n/langs/**/*.json' -o /tmp/abc.json",
"vue-i18n-report-add-miss": "vue-i18n-extract report -v './src/**/*.?(js|vue)' -l './src/i18n/langs/**/*.json' -a"
},
"dependencies": {
"@babel/plugin-proposal-optional-chaining": "^7.13.12",
"@ztree/ztree_v3": "3.5.44",
"axios": "0.18.1",
"axios": "0.21.1",
"axios-retry": "^3.1.9",
"deepmerge": "^4.2.2",
"echarts": "^4.7.0",
"element-ui": "2.13.2",
"eslint-plugin-html": "^6.0.0",
"install": "^0.13.0",
"jquery": "^3.5.0",
"js-cookie": "2.2.0",
"krry-transfer": "^1.7.3",
"less": "^3.10.3",
"less-loader": "^5.0.0",
"lodash": "^4.17.15",
@@ -42,6 +48,7 @@
"lodash.values": "^4.3.0",
"moment-parseformat": "^3.0.0",
"normalize.css": "7.0.0",
"npm": "^7.8.0",
"nprogress": "0.2.0",
"path-to-regexp": "2.4.0",
"vue": "2.6.10",
@@ -72,6 +79,7 @@
"babel-eslint": "10.0.1",
"babel-jest": "23.6.0",
"chalk": "2.4.2",
"compression-webpack-plugin": "^6.1.1",
"connect": "3.6.6",
"element-theme-chalk": "^2.13.1",
"eslint": "^5.15.3",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -3,8 +3,11 @@
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta http-equiv="Expires" content="0">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Cache-control" content="no-cache">
<meta http-equiv="Cache" content="no-cache">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= webpackConfig.name %></title>
</head>
<body>

15
src/api/orgs.js Normal file
View File

@@ -0,0 +1,15 @@
import request from '@/utils/request'
export function getOrgDetail(oid) {
return request({
url: `/api/v1/orgs/orgs/current/?oid=${oid}`,
method: 'get'
})
}
export function getCurrentOrg() {
return request({
url: `/api/v1/orgs/orgs/current/`,
method: 'get'
})
}

8
src/api/ticket.js Normal file
View File

@@ -0,0 +1,8 @@
import request from '@/utils/request'
export function getTicketOpenCount(assign) {
return request({
url: `/api/v1/tickets/tickets/?assignees__id=${assign}&status=open&offset=0&limit=15&display=1&draw=1/`,
method: 'get'
})
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -1,40 +1,15 @@
<template>
<div :class="grouped ? 'el-button-group' : ''">
<el-button v-for="item in iActions" :key="item.name" :size="size" v-bind="item" @click="handleClick(item.name)">
<el-tooltip v-if="['actionExport', 'actionImport', 'actionRefresh'].indexOf(item.name) !== -1" effect="dark" :content="item.tip" placement="top">
<i v-if="item.fa" :class="'fa ' + item.fa" />{{ item.title }}
</el-tooltip>
<span v-else>
<i v-if="item.fa" :class="'fa ' + item.fa" />{{ item.title }}
</span>
</el-button>
<el-dropdown v-if="iMoreActions.length > 0" trigger="click" :placement="moreActionsPlacement" @command="handleClick">
<el-button :size="size" :type="moreActionsType" class="btn-more-actions">
{{ iMoreActionsTitle }}<i class="el-icon-arrow-down el-icon--right" />
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item v-for="item in iMoreActions" :key="item.name" :command="item.name" v-bind="item" @click="handleClick(item.name)">{{ item.title }} </el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
<DataActions :actions="iActions" v-bind="$attrs" />
</template>
<script>
import DataActions from '@/components/DataActions'
export default {
name: 'ActionsGroup',
components: {
DataActions
},
props: {
grouped: {
type: Boolean,
default: false
},
size: {
type: String,
default: 'small'
},
type: {
type: String,
default: ''
},
actions: {
type: Array,
default: () => []
@@ -61,74 +36,22 @@ export default {
},
computed: {
iActions() {
return this.cleanActions(this.actions)
},
iMoreActions() {
return this.cleanActions(this.moreActions)
},
totalActions() {
return [...this.actions, ...this.moreActions]
},
totalNamedActions() {
const actions = {}
for (const action of this.totalActions) {
if (!action || !action.hasOwnProperty('name')) {
continue
}
actions[action.name] = action
const actions = [...this.actions]
if (this.iMoreAction && this.iMoreAction.dropdown.length > 0) {
actions.push(this.iMoreAction)
}
return actions
},
iMoreAction() {
return {
name: 'moreActions',
title: this.iMoreActionsTitle,
dropdown: this.moreActions || []
}
},
iMoreActionsTitle() {
return this.moreActionsTitle || this.$t('common.MoreActions')
}
},
methods: {
handleClick(item) {
const action = this.totalNamedActions[item]
if (action && action.callback) {
action.callback(action)
} else {
this.$log.debug('No callback found')
}
this.$emit('actionClick', item)
},
checkItem(item, attr, defaults) {
if (!item) {
return true
}
let ok = item[attr]
if (ok && typeof ok === 'function') {
ok = ok(item)
} else if (ok == null) {
ok = defaults === undefined ? true : defaults
}
return ok
},
cleanActions(actions) {
const cleanedActions = []
const cloneActions = _.cloneDeep(actions)
for (const v of cloneActions) {
if (!v) {
continue
}
const action = Object.assign({}, v)
// 是否拥有这个action
const has = this.checkItem(action, 'has')
delete action['has']
if (!has) {
continue
}
// 是否是disabled
const can = this.checkItem(action, 'can')
delete action['can']
action.disabled = !can
cleanedActions.push(action)
// 删掉callback避免前台看到
delete action['callback']
}
return cleanedActions
}
}
}
</script>

View File

@@ -3,12 +3,12 @@
<table style="width: 100%">
<tr>
<td colspan="2">
<AssetSelect ref="assetSelect" />
<AssetSelect ref="assetSelect" :disabled="disabled" :can-select="canSelect" />
</td>
</tr>
<tr>
<td colspan="2">
<el-button :type="type" size="small" @click="addObjects">{{ $t('common.Add') }}</el-button>
<el-button :type="type" size="small" :disabled="disabled" @click="addObjects">{{ $t('common.Add') }}</el-button>
</td>
</tr>
</table>
@@ -38,6 +38,10 @@ export default {
type: String,
default: 'primary'
},
disabled: {
type: [Boolean, Function],
default: false
},
value: {
type: [Array, Number, String],
default: () => []
@@ -49,6 +53,12 @@ export default {
onAddSuccess: {
type: Function,
default: (objects, that) => {}
},
canSelect: {
type: Function,
default(row, index) {
return true
}
}
},
data() {

View File

@@ -23,8 +23,8 @@
<script>
import TreeTable from '@/components/TreeTable'
import { DetailFormatter } from '@/components/ListTable/formatters'
import Select2 from '@/components/Select2'
import { DetailFormatter } from '@/components/TableFormatters'
import Select2 from '@/components/FormFields/Select2'
import Dialog from '@/components/Dialog'
export default {
@@ -34,6 +34,16 @@ export default {
value: {
type: Array,
default: () => []
},
canSelect: {
type: Function,
default(row, index) {
return true
}
},
disabled: {
type: [Boolean, Function],
default: false
}
},
data() {
@@ -68,6 +78,7 @@ export default {
tableConfig: {
url: '/api/v1/assets/assets/',
hasTree: true,
canSelect: this.canSelect,
columns: [
{
prop: 'hostname',
@@ -81,7 +92,7 @@ export default {
},
{
prop: 'ip',
label: this.$t('assets.ip'),
label: this.$t('assets.ipDomain'),
sortable: 'custom'
}
],

View File

@@ -1,58 +1,59 @@
<template><div>
<template>
<div>
<ListTable ref="ListTable" :table-config="tableConfig" :header-actions="headerActions" />
<Dialog v-if="showMFADialog" width="50" :title="this.$t('common.MFAConfirm')" :visible.sync="showMFADialog" :show-confirm="false" :show-cancel="false" :destroy-on-close="true">
<div v-if="MFAConfirmed">
<el-form label-position="right" label-width="80px" :model="MFAInfo">
<div>
<ListTable ref="ListTable" :table-config="tableConfig" :header-actions="headerActions" />
<Dialog v-if="showMFADialog" width="50" :title="this.$t('common.MFAConfirm')" :visible.sync="showMFADialog" :show-confirm="false" :show-cancel="false" :destroy-on-close="true">
<div v-if="MFAConfirmed">
<el-form label-position="right" label-width="80px" :model="MFAInfo">
<el-form-item :label="this.$t('assets.Hostname')">
<el-input v-model="MFAInfo.hostname" disabled />
</el-form-item>
<el-form-item :label="this.$t('assets.Username')">
<el-input v-model="MFAInfo.username" disabled />
</el-form-item>
<el-form-item :label="this.$t('assets.Password')">
<el-input v-model="MFAInfo.password" type="password" show-password />
</el-form-item>
</el-form>
</div>
<el-row v-else :gutter="20">
<el-col :span="4">
<div style="line-height: 34px;text-align: center">MFA</div>
</el-col>
<el-col :span="14">
<el-input v-model="MFAInput" />
<span class="help-tips help-block">{{ $t('common.MFARequireForSecurity') }}</span>
</el-col>
<el-col :span="4">
<el-button size="mini" type="primary" style="line-height:20px " @click="MFAConfirm">{{ this.$t('common.Confirm') }}</el-button>
</el-col>
</el-row>
</Dialog>
<Dialog width="50" :title="this.$t('assets.UpdateAssetUserToken')" :visible.sync="showDialog" @confirm="handleConfirm()" @cancel="handleCancel()">
<el-form label-position="right" label-width="80px" :model="dialogInfo">
<el-form-item :label="this.$t('assets.Hostname')">
<el-input v-model="MFAInfo.hostname" disabled />
<el-input v-model="dialogInfo.hostname" disabled />
</el-form-item>
<el-form-item :label="this.$t('assets.Username')">
<el-input v-model="MFAInfo.username" disabled />
<el-input v-model="dialogInfo.username" disabled />
</el-form-item>
<el-form-item :label="this.$t('assets.Password')">
<el-input v-model="MFAInfo.password" type="password" show-password />
<el-input v-model="dialogInfo.password" type="password" />
</el-form-item>
<el-form-item :label="this.$t('assets.sshkey')">
<input type="file" @change="Onchange">
</el-form-item>
</el-form>
</div>
<el-row v-else :gutter="20">
<el-col :span="4">
<div style="line-height: 34px;text-align: center">MFA</div>
</el-col>
<el-col :span="14">
<el-input v-model="MFAInput" />
<span class="help-tips help-block">{{ $t('common.MFARequireForSecurity') }}</span>
</el-col>
<el-col :span="4">
<el-button size="mini" type="primary" style="line-height:20px " @click="MFAConfirm">{{ this.$t('common.Confirm') }}</el-button>
</el-col>
</el-row>
</Dialog>
<Dialog width="50" :title="this.$t('assets.UpdateAssetUserToken')" :visible.sync="showDialog" @confirm="handleConfirm()" @cancel="handleCancel()">
<el-form label-position="right" label-width="80px" :model="dialogInfo">
<el-form-item :label="this.$t('assets.Hostname')">
<el-input v-model="dialogInfo.hostname" disabled />
</el-form-item>
<el-form-item :label="this.$t('assets.Username')">
<el-input v-model="dialogInfo.username" disabled />
</el-form-item>
<el-form-item :label="this.$t('assets.Password')">
<el-input v-model="dialogInfo.password" type="password" />
</el-form-item>
<el-form-item :label="this.$t('assets.sshkey')">
<input type="file" @change="Onchange">
</el-form-item>
</el-form>
</Dialog>
</Dialog>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import ListTable from '@/components/ListTable/index'
import Dialog from '@/components/Dialog'
import { ActionsFormatter, DateFormatter } from '@/components/ListTable/formatters'
import { ActionsFormatter, DateFormatter } from '@/components/TableFormatters'
export default {
name: 'Detail',
@@ -88,6 +89,10 @@ export default {
hasExport: {
type: Boolean,
default: true
},
hasClone: {
type: Boolean,
default: true
}
},
data() {
@@ -107,7 +112,7 @@ export default {
username: '',
hostname: '',
password: '',
key: ''
private_key: ''
},
tableConfig: {
url: this.url,
@@ -146,6 +151,7 @@ export default {
formatterArgs: {
hasUpdate: false, // can set function(row, value)
hasDelete: false, // can set function(row, value)
hasClone: this.hasClone,
moreActionsTitle: this.$t('common.More'),
extraActions: [
{
@@ -153,8 +159,8 @@ export default {
title: this.$t('common.View'),
type: 'primary',
callback: function(val) {
this.MFAInfo.asset = val.cellValue
if (this.MFAVerifyAt + this.MFA_TTl * 1000 > (new Date()).valueOf()) {
this.MFAInfo.asset = val.row.id
if (!this.needMFAVerify) {
this.showMFADialog = true
this.MFAConfirmed = true
this.$axios.get(`/api/v1/assets/asset-user-auth-infos/${this.MFAInfo.asset}/`).then(res => {
@@ -173,7 +179,7 @@ export default {
title: this.$t('common.Delete'),
type: 'primary',
callback: (val) => {
this.$axios.delete(`/api/v1/assets/asset-users/${val.cellValue}/`).then(() => {
this.$axios.delete(`/api/v1/assets/asset-users/${val.row.id}/`).then(() => {
this.$message.success(this.$t('common.deleteSuccessMsg'))
this.$refs.ListTable.reloadTable()
})
@@ -184,7 +190,7 @@ export default {
title: this.$t('common.Test'),
callback: (val) => {
this.$axios.post(
`/api/v1/assets/asset-users/tasks/?id=${val.cellValue}`,
`/api/v1/assets/asset-users/tasks/?id=${val.row.id}`,
{ action: 'test' }
).then(res => {
window.open(`/#/ops/celery/task/${res.task}/log/`, '', 'width=900,height=600')
@@ -194,6 +200,7 @@ export default {
{
name: 'Update',
title: this.$t('common.Update'),
can: !this.$store.getters.currentOrgIsRoot,
callback: function(val) {
this.showDialog = true
this.dialogInfo.asset = val.row.asset
@@ -211,7 +218,7 @@ export default {
},
headerActions: {
hasLeftActions: this.hasLeftActions,
hasBulkDelete: false,
hasMoreActions: false,
hasImport: this.hasImport,
hasExport: this.hasExport,
hasSearch: true,
@@ -239,7 +246,8 @@ export default {
computed: {
...mapGetters([
'MFA_TTl',
'MFAVerifyAt'
'MFAVerifyAt',
'publicSettings'
]),
needMFAVerify() {
if (!this.publicSettings.SECURITY_VIEW_AUTH_NEED_MFA) {
@@ -309,7 +317,7 @@ export default {
username: '',
hostname: '',
password: '',
key: ''
private_key: ''
}
this.showDialog = false
this.$refs.ListTable.reloadTable()
@@ -319,7 +327,7 @@ export default {
// TODO 校验文件类型
const reader = new FileReader()
reader.onload = function() {
vm.dialogInfo.key = this.result
vm.dialogInfo.private_key = this.result
}
reader.readAsText(
e.target.files[0]
@@ -333,8 +341,8 @@ export default {
if (this.dialogInfo.password !== '') {
data.password = this.dialogInfo.password
}
if (this.dialogInfo.key !== '') {
data.key = this.dialogInfo.key
if (this.dialogInfo.private_key !== '') {
data.private_key = this.dialogInfo.private_key
}
this.$axios.post(
`/api/v1/assets/asset-users/`,
@@ -349,7 +357,7 @@ export default {
username: '',
hostname: '',
password: '',
key: ''
private_key: ''
}
this.showDialog = false
this.$refs.ListTable.reloadTable()

View File

@@ -0,0 +1,81 @@
<template>
<DataForm
:fields="iFields"
:form="value"
style="margin-left: -26%;margin-right: -6%"
v-bind="kwargs"
v-on="$listeners"
/>
</template>
<script>
import { DataForm } from '@/components'
export default {
name: 'NestedField',
components: {
DataForm
},
props: {
fields: {
type: Array,
default: () => []
},
value: {
type: Object,
default: () => ({})
},
errors: {
type: [Object, String],
default: ''
}
},
data() {
return {
kwargs: {
hasReset: false,
hasSaveContinue: false,
defaultButton: false
}
}
},
computed: {
iFields() {
const fields = this.fields
if (this.errors && typeof this.errors === 'object') {
// eslint-disable-next-line prefer-const
for (let [name, error] of Object.entries(this.errors)) {
const field = fields.find((v) => v.prop === name)
if (!field) {
continue
}
this.$log.debug(`${name}: ${error}`)
if (typeof error === 'object' && !Array.isArray(error)) {
error = this.objectToString(error)
}
field.attrs.error = error.toString()
}
}
this.$log.debug('Fields change: ', fields, this.errors)
return fields
}
},
methods: {
objectToString(obj) {
let data = ''
// eslint-disable-next-line prefer-const
for (let [key, value] of Object.entries(obj)) {
if (typeof value === 'object') {
value = this.objectToString(value)
}
data += ` ${key}: ${value} `
}
return data
}
}
}
</script>
<style scoped>
</style>

View File

@@ -1,15 +1,13 @@
<template>
<DataForm ref="dataForm" v-loading="loading" :fields="totalFields" v-bind="$attrs" v-on="$listeners">
<FormGroupHeader v-for="(group, i) in groups" :slot="'id:'+group.name" :key="'group-'+group.name" :title="group.title" :line="i != 0" />
<DataForm ref="dataForm" v-loading="loading" :fields="totalFields" :form="iForm" v-bind="$attrs" v-on="$listeners">
<FormGroupHeader v-for="(group, i) in groups" :slot="'id:'+group.name" :key="'group-'+group.name" :title="group.title" :line="i !== 0" />
</DataForm>
</template>
<script>
import DataForm from '../DataForm'
import FormGroupHeader from '@/components/FormGroupHeader'
// import { optionUrlMeta } from '@/api/common'
import rules from '@/components/DataForm/rules'
import Select2 from '@/components/Select2'
import { FormFieldGenerator } from '@/components/AutoDataForm/utils'
export default {
name: 'AutoDataForm',
components: {
@@ -31,6 +29,10 @@ export default {
return []
}
},
form: {
type: Object,
default: () => ({})
},
fieldsMeta: {
type: Object,
default: () => ({})
@@ -38,136 +40,51 @@ export default {
},
data() {
return {
meta: {},
remoteMeta: {},
totalFields: [],
loading: true,
groups: []
groups: [],
iForm: this.form
}
},
mounted() {
this.optionUrlMeta()
this.optionUrlMetaAndGenerateColumns()
},
methods: {
optionUrlMeta() {
optionUrlMetaAndGenerateColumns() {
this.$store.dispatch('common/getUrlMeta', { url: this.url }).then(data => {
this.meta = data.actions[this.method.toUpperCase()] || {}
this.remoteMeta = data.actions[this.method.toUpperCase()] || {}
this.generateColumns()
this.cleanFormValue()
}).catch(err => {
console.error(err)
this.$log.error(err)
}).finally(() => {
this.loading = false
})
},
generateFieldByType(type, field, fieldMeta) {
switch (type) {
case 'choice':
type = 'radio-group'
field.options = fieldMeta.choices.map(v => {
return { label: v.display_name, value: v.value }
})
break
case 'datetime':
type = 'date-picker'
field.el = {
type: 'datetime'
}
break
case 'field':
type = ''
field.component = Select2
if (fieldMeta.required) {
field.el.clearable = false
}
break
case 'string':
type = 'input'
if (!fieldMeta.max_length) {
field.el.type = 'textarea'
field.el.rows = 3
}
break
default:
type = 'input'
break
}
if (type === 'radio-group') {
const options = fieldMeta.choices.map(v => {
return { label: v.display_name, value: v.value }
})
if (options.length > 4) {
type = 'select'
field.el.filterable = true
}
}
field.type = type
return field
},
generateFieldByName(name, field) {
switch (name) {
case 'email':
field.el.type = 'email'
break
case 'password':
field.el.type = 'password'
break
case 'comment':
field.el.type = 'textarea'
break
}
return field
},
generateFieldByOther(field, fieldMeta) {
const filedRules = field.rules || []
if (fieldMeta.required) {
if (field.type === 'input') {
filedRules.push(rules.Required)
} else {
filedRules.push(rules.RequiredChange)
}
}
field.rules = filedRules
return field
},
generateField(name) {
let field = { id: name, prop: name, el: {}, attrs: {}}
const fieldMeta = this.meta[name] || {}
field.label = fieldMeta.label
field = this.generateFieldByType(fieldMeta.type, field, fieldMeta)
field = this.generateFieldByName(name, field)
field = this.generateFieldByOther(field, fieldMeta)
field = Object.assign(field, this.fieldsMeta[name] || {})
_.set(field, 'attrs.error', '')
return field
},
generateFieldGroup(data) {
const [groupTitle, fields] = data
this.groups.push({
id: groupTitle,
title: groupTitle,
name: fields[0]
})
return this.generateFields(fields)
},
generateFields(data) {
let fields = []
for (let field of data) {
if (field instanceof Array) {
const items = this.generateFieldGroup(field)
fields = [...fields, ...items]
} else if (typeof field === 'string') {
field = this.generateField(field)
fields.push(field)
} else if (field instanceof Object) {
this.errors[field.prop] = ''
_.set(field, 'attrs.error', '')
fields.push(field)
}
}
return fields
},
generateColumns() {
this.totalFields = this.generateFields(this.fields)
const generator = new FormFieldGenerator()
this.totalFields = generator.generateFields(this.fields, this.fieldsMeta, this.remoteMeta)
this.groups = generator.groups
this.$log.debug('Total fields: ', this.totalFields)
},
_cleanFormValue(form, remoteMeta) {
for (const [k, v] of Object.entries(remoteMeta)) {
if (v.default === undefined) {
continue
}
const valueSet = form[k]
if (valueSet !== undefined) {
continue
}
if (v.type === 'nested object' && typeof valueSet === 'object') {
this._cleanFormValue(valueSet, v.children)
}
form[k] = v.default
}
},
cleanFormValue() {
this._cleanFormValue(this.iForm, this.remoteMeta)
},
setFieldError(name, error) {
const field = this.totalFields.find((v) => v.prop === name)
@@ -177,7 +94,11 @@ export default {
if (field.attrs.error === error) {
error += '.'
}
field.attrs.error = error
if (field.type === 'nestedField') {
field.el.errors = error
} else {
field.attrs.error = error
}
}
}
}

View File

@@ -0,0 +1,155 @@
import Vue from 'vue'
import Select2 from '@/components/FormFields/Select2'
import NestedField from '@/components/AutoDataForm/components/NestedField'
import rules from '@/components/DataForm/rules'
import { assignIfNot } from '@/utils/common'
export class FormFieldGenerator {
constructor() {
this.groups = []
}
generateFieldByType(type, field, fieldMeta, fieldRemoteMeta) {
switch (type) {
case 'choice':
type = 'radio-group'
if (!fieldRemoteMeta.read_only) {
field.options = fieldRemoteMeta.choices.map(v => {
return { label: v.display_name, value: v.value }
})
}
break
case 'datetime':
type = 'date-picker'
field.el = {
type: 'datetime'
}
break
case 'field':
type = ''
field.component = Select2
if (fieldRemoteMeta.required) {
field.el.clearable = false
}
break
case 'string':
type = 'input'
if (!fieldRemoteMeta.max_length) {
field.el.type = 'textarea'
field.el.rows = 3
}
if (fieldRemoteMeta.write_only) {
field.el.type = 'password'
}
break
case 'boolean':
type = 'checkbox'
break
case 'nested object':
type = 'nestedField'
field.component = NestedField
field.label = ''
field.labelWidth = 0
field.el.fields = this.generateNestFields(field, fieldMeta, fieldRemoteMeta)
field.el.errors = {}
Vue.$log.debug('All fields in generate: ', field.el.allFields)
break
default:
type = 'input'
break
}
if (type === 'radio-group') {
if (!fieldRemoteMeta.read_only) {
const options = fieldRemoteMeta.choices.map(v => {
return { label: v.display_name, value: v.value }
})
if (options.length > 4) {
type = 'select'
field.el.filterable = true
}
}
}
field.type = type
return field
}
generateNestFields(field, fieldMeta, fieldRemoteMeta) {
const fields = []
const nestedFields = fieldMeta.fields || []
const nestedFieldsMeta = fieldMeta.fieldsMeta || {}
const nestedFieldsRemoteMeta = fieldRemoteMeta.children || {}
for (const name of nestedFields) {
const f = this.generateField(name, nestedFieldsMeta, nestedFieldsRemoteMeta)
fields.push(f)
}
Vue.$log.debug('NestFields: ', fields)
return fields
}
generateFieldByName(name, field) {
switch (name) {
case 'email':
field.el.type = 'email'
break
case 'password':
field.el.type = 'password'
break
case 'comment':
field.el.type = 'textarea'
break
}
return field
}
generateFieldByOther(field, fieldMeta, fieldRemoteMeta) {
const filedRules = field.rules || []
if (fieldRemoteMeta.required) {
if (field.type === 'input') {
filedRules.push(rules.Required)
} else {
filedRules.push(rules.RequiredChange)
}
}
field.rules = filedRules
return field
}
generateField(name, fieldsMeta, remoteFieldsMeta) {
let field = { id: name, prop: name, el: {}, attrs: {}, rules: [] }
const remoteFieldMeta = remoteFieldsMeta[name] || {}
const fieldMeta = fieldsMeta[name] || {}
field.label = remoteFieldMeta.label
field.helpText = remoteFieldMeta.help_text
field = this.generateFieldByType(remoteFieldMeta.type, field, fieldMeta, remoteFieldMeta)
field = this.generateFieldByName(name, field)
field = this.generateFieldByOther(field, fieldMeta, remoteFieldMeta)
const el = assignIfNot(fieldMeta.el || {}, field.el)
const rules = fieldMeta.rules || field.rules
field = Object.assign(field, fieldMeta)
field.el = el
field.rules = rules
_.set(field, 'attrs.error', '')
// Vue.$log.debug('Generate field: ', name, field)
return field
}
generateFieldGroup(field, fieldsMeta, remoteFieldsMeta) {
const [groupTitle, fields] = field
this.groups.push({
id: groupTitle,
title: groupTitle,
name: fields[0]
})
return this.generateFields(fields, fieldsMeta, remoteFieldsMeta)
}
generateFields(_fields, fieldsMeta, remoteFieldsMeta) {
let fields = []
for (let field of _fields) {
if (field instanceof Array) {
const items = this.generateFieldGroup(field, fieldsMeta, remoteFieldsMeta)
fields = [...fields, ...items]
} else if (typeof field === 'string') {
field = this.generateField(field, fieldsMeta, remoteFieldsMeta)
fields.push(field)
} else if (field instanceof Object) {
this.errors[field.prop] = ''
fields.push(field)
}
}
return fields
}
}

View File

@@ -1,5 +1,5 @@
<template>
<TagSearch :options="options" v-bind="$attrs" v-on="$listeners" />
<TagSearch :options="iOption" v-bind="$attrs" v-on="$listeners" />
</template>
<script>
@@ -23,9 +23,22 @@ export default {
default: () => []
}
},
data() {
return {
internalOptions: []
}
},
computed: {
iOption() {
return this.options.concat(this.internalOptions)
}
},
watch: {
options() {
// 空函数,方便子组件刷新
},
url() {
this.genericOptions()
}
},
mounted() {
@@ -36,6 +49,7 @@ export default {
methods: {
async genericOptions() {
const vm = this // 透传This
vm.internalOptions = [] // 重置
const data = await this.optionUrlMeta()
const meta = data.actions['GET'] || {}
for (const [name, field] of Object.entries(meta)) {
@@ -49,7 +63,6 @@ export default {
label: field.label,
type: field.type,
value: name
}
if (field.type === 'choice' && field.choices) {
option.children = field.choices.map(item => {
@@ -69,7 +82,7 @@ export default {
{ label: this.$t('common.No'), value: false }
]
}
vm.options.push(option)
vm.internalOptions.push(option)
}
},
optionUrlMeta() {
@@ -81,5 +94,4 @@ export default {
</script>
<style lang='less' scoped>
</style>

View File

@@ -0,0 +1,91 @@
<template>
<Dialog
v-if="showColumnSettingPopover"
:title="$t('common.CustomCol')"
:visible.sync="showColumnSettingPopover"
:destroy-on-close="true"
:show-cancel="false"
width="35%"
top="10%"
@confirm="handleColumnConfirm()"
>
<el-alert type="success">
{{ this.$t('common.TableColSettingInfo') }}
</el-alert>
<el-checkbox-group
v-model="iCurrentColumns"
>
<el-row>
<el-col
v-for="item in totalColumnsList"
:key="item.prop"
:span="8"
style="margin-top:5px;"
>
<el-checkbox
:label="item.prop"
:disabled="
item.prop==='id' ||
item.prop==='actions' ||
minColumns.indexOf(item.prop)!==-1
"
>
{{ item.label }}
</el-checkbox>
</el-col>
</el-row>
</el-checkbox-group>
</Dialog>
</template>
<script>
import Dialog from '@/components/Dialog/index'
export default {
name: 'ColumnSettingPopover',
components: {
Dialog
},
props: {
totalColumnsList: {
type: Array,
default: () => []
},
currentColumns: {
type: Array,
default: () => []
},
minColumns: {
type: Array,
default: () => []
},
url: {
type: String,
default: ''
}
},
data() {
return {
showColumnSettingPopover: false,
iCurrentColumns: ''
}
},
mounted() {
this.$eventBus.$on('showColumnSettingPopover', ({ url }) => {
if (url === this.url) {
this.showColumnSettingPopover = true
this.iCurrentColumns = this.currentColumns
}
})
},
methods: {
handleColumnConfirm() {
this.showColumnSettingPopover = false
this.$emit('columnsUpdate', { columns: this.iCurrentColumns, url: this.url })
}
}
}
</script>
<style lang='less' scoped>
</style>

View File

@@ -1,20 +1,36 @@
<template>
<DataTable v-if="!loading" ref="dataTable" v-loading="loading" :config="iConfig" v-bind="$attrs" v-on="$listeners" />
<div>
<DataTable v-if="!loading" ref="dataTable" v-loading="loading" :config="iConfig" v-bind="$attrs" v-on="$listeners" @filter-change="filterChange" />
<ColumnSettingPopover
:current-columns="popoverColumns.currentCols"
:total-columns-list="popoverColumns.totalColumnsList"
:min-columns="popoverColumns.minCols"
:url="config.url"
@columnsUpdate="handlePopoverColumnsChange"
/>
</div>
</template>
<script type="text/jsx">
import DataTable from '../DataTable'
import { DateFormatter, DetailFormatter, DisplayFormatter, BooleanFormatter, ActionsFormatter } from '@/components/ListTable/formatters'
import { DateFormatter, DetailFormatter, DisplayFormatter, BooleanFormatter, ActionsFormatter } from '@/components/TableFormatters'
import i18n from '@/i18n/i18n'
import ColumnSettingPopover from './components/ColumnSettingPopover'
import { newURL } from '@/utils/common'
export default {
name: 'AutoDataTable',
components: {
DataTable
DataTable,
ColumnSettingPopover
},
props: {
config: {
type: Object,
default: () => ({})
},
filterTable: {
type: Function,
default: () => ({})
}
},
data() {
@@ -23,8 +39,18 @@ export default {
method: 'get',
autoConfig: {},
iConfig: {},
meta: {}
meta: {},
cleanedColumnsShow: {},
totalColumns: [],
popoverColumns: {
totalColumnsList: [],
minCols: [],
currentCols: []
}
}
},
computed: {
},
watch: {
config: {
@@ -40,10 +66,18 @@ export default {
},
methods: {
async optionUrlMetaAndGenCols() {
if (this.config.url === '') { return }
const url = (this.config.url.indexOf('?') === -1) ? `${this.config.url}?draw=1&display=1` : `${this.config.url}&draw=1&display=1`
this.$store.dispatch('common/getUrlMeta', { url: url }).then(data => {
this.meta = data.actions[this.method.toUpperCase()] || {}
this.generateColumns()
const method = this.method.toUpperCase()
this.meta = data.actions && data.actions[method] ? data.actions[method] : {}
this.generateTotalColumns()
}).then(() => {
// 根据当前列重新生成最终渲染表格
this.filterShowColumns()
}).then(() => {
// 生成给子组件使用的TotalColList
this.generatePopoverColumns()
}).catch((error) => {
this.$log.error('Error occur: ', error)
}).finally(() => {
@@ -59,7 +93,7 @@ export default {
break
case 'actions':
col = {
prop: 'id',
prop: 'actions',
label: i18n.t('common.Actions'),
align: 'center',
width: '150px',
@@ -118,6 +152,37 @@ export default {
}
return col
},
addFilterIfNeed(col) {
if (col.prop) {
const column = this.meta[col.prop] || {}
if (!column.filter) {
return col
}
if (column.type === 'boolean') {
col.filters = [
{ text: this.$t('common.Yes'), value: true },
{ text: this.$t('common.No'), value: false }
]
col.sortable = false
col['column-key'] = col.prop
}
if (column.type === 'choice' && column.choices) {
col.filters = column.choices.map(item => {
if (typeof (item.value) === 'boolean') {
if (item.value) {
return { text: item.display_name, value: 'True' }
} else {
return { text: item.display_name, value: 'False' }
}
}
return { text: item.display_name, value: item.value }
})
col.sortable = false
col['column-key'] = col.prop
}
}
return col
},
generateColumn(name) {
const colMeta = this.meta[name] || {}
const customMeta = this.config.columnsMeta ? this.config.columnsMeta[name] : {}
@@ -127,9 +192,10 @@ export default {
col = this.generateColumnByType(colMeta.type, col)
col = Object.assign(col, customMeta)
col = this.addHelpTipsIfNeed(col)
col = this.addFilterIfNeed(col)
return col
},
generateColumns() {
generateTotalColumns() {
const config = _.cloneDeep(this.config)
const columns = []
for (let col of config.columns) {
@@ -140,8 +206,85 @@ export default {
columns.push(col)
}
}
// 第一次初始化时记录 totalColumns
this.totalColumns = columns
config.columns = columns
this.iConfig = config
},
// 生成给子组件使用的TotalColList
cleanColumnsShow() {
const totalColumnsNames = this.totalColumns.map(obj => obj.prop)
// 默认列
let defaultColumnsNames = _.get(this.iConfig, 'columnsShow.default', [])
if (defaultColumnsNames.length === 0) {
defaultColumnsNames = totalColumnsNames
}
// Clean it
defaultColumnsNames = totalColumnsNames.filter(n => defaultColumnsNames.indexOf(n) > -1)
// 最小列
const minColumnsNames = _.get(this.iConfig, 'columnsShow.min', ['action', 'id'])
.filter(n => defaultColumnsNames.indexOf(n) > -1)
// 应该显示的列
const _tableConfig = localStorage.getItem('tableConfig')
? JSON.parse(localStorage.getItem('tableConfig'))
: {}
const tableName = this.config.name || this.$route.name + '_' + newURL(this.iConfig.url).pathname
const configShowColumnsNames = _.get(_tableConfig[tableName], 'showColumns', null)
let showColumnsNames = configShowColumnsNames || defaultColumnsNames
if (showColumnsNames.length === 0) {
showColumnsNames = totalColumnsNames
}
// 校对显示的列,是不是包含最小列
minColumnsNames.forEach((v, i) => {
if (showColumnsNames.indexOf(v) === -1) {
showColumnsNames.push(v)
}
})
// Clean it
showColumnsNames = totalColumnsNames.filter(n => showColumnsNames.indexOf(n) > -1)
this.cleanedColumnsShow = {
default: defaultColumnsNames,
show: showColumnsNames,
min: minColumnsNames,
configShow: configShowColumnsNames
}
this.$log.debug('Cleaned columns show: ', this.cleanedColumnsShow)
},
filterShowColumns() {
this.cleanColumnsShow()
this.iConfig.columns = this.totalColumns.filter(obj => {
return this.cleanedColumnsShow.show.indexOf(obj.prop) > -1
})
},
generatePopoverColumns() {
this.popoverColumns.totalColumnsList = this.totalColumns.map(obj => {
return { prop: obj.prop, label: obj.label }
})
this.popoverColumns.currentCols = this.cleanedColumnsShow.show
this.popoverColumns.minCols = this.cleanedColumnsShow.min
this.$log.debug('Popover cols: ', this.popoverColumns)
},
handlePopoverColumnsChange({ columns, url }) {
this.$log.debug('Columns change: ', columns)
this.popoverColumns.currentCols = columns
const _tableConfig = localStorage.getItem('tableConfig')
? JSON.parse(localStorage.getItem('tableConfig'))
: {}
const tableName = this.config.name || this.$route.name + '_' + newURL(url).pathname
_tableConfig[tableName] = {
'showColumns': columns
}
localStorage.setItem('tableConfig', JSON.stringify(_tableConfig))
this.filterShowColumns()
},
filterChange(filters) {
const key = Object.keys(filters)[0]
const attr = {}
attr[key] = filters[key][0]
this.filterTable(attr)
}
}
}

View File

@@ -1,6 +1,6 @@
<template>
<DataZTree ref="dataztree" :setting="treeSetting">
<slot slot="rMenu">
<DataZTree ref="dataztree" :setting="treeSetting" class="data-z-tree" v-on="$listeners">
<slot v-if="treeSetting.hasRightMenu" slot="rMenu">
<li id="m_create" class="rmenu" tabindex="-1" @click="createTreeNode">
<i class="fa fa-plus-square-o" /> {{ this.$t('tree.CreateNode') }}
</li>
@@ -55,7 +55,8 @@ export default {
// beforeDrag
// onDrag
// beforeAsync: this.defaultCallback.bind(this, 'beforeAsync')
}
},
hasRightMenu: true
},
currentNode: '',
currentNodeId: ''
@@ -63,6 +64,7 @@ export default {
},
computed: {
treeSetting() {
this.$log.debug('Settings: ', this.setting)
return _.merge(this.defaultSetting, this.setting)
},
zTree() {
@@ -76,6 +78,10 @@ export default {
$('body').unbind('mousedown')
},
methods: {
refreshTree: function() {
const refreshIconRef = $('#tree-refresh')
refreshIconRef.click()
},
editTreeNode: function() {
this.hideRMenu()
const currentNode = this.zTree.getSelectedNodes()[0]
@@ -98,15 +104,19 @@ export default {
if (this.setting.url.indexOf('?') !== -1) {
combinator = '&'
}
let url = ''
const query = Object.assign({}, this.$route.query)
if (treeNode.meta.type === 'node') {
this.currentNode = treeNode
this.currentNodeId = treeNode.meta.node.id
this.$route.query['node'] = this.currentNodeId
this.$emit('urlChange', `${this.setting.url}${combinator}node_id=${treeNode.meta.node.id}&show_current_asset=${show_current_asset}`)
query['node'] = this.currentNodeId
url = `${this.setting.url}${combinator}node_id=${treeNode.meta.node.id}&show_current_asset=${show_current_asset}`
} else if (treeNode.meta.type === 'asset') {
this.$route.query['asset'] = treeNode.meta.asset.id
this.$emit('urlChange', `${this.setting.url}${combinator}asset_id=${treeNode.meta.asset.id}&show_current_asset=${show_current_asset}`)
query['asset'] = treeNode.meta.asset.id
url = `${this.setting.url}${combinator}asset_id=${treeNode.meta.asset.id}&show_current_asset=${show_current_asset}`
}
this.$router.push({ query })
this.$emit('urlChange', url)
},
removeTreeNode: function() {
this.hideRMenu()
@@ -119,8 +129,9 @@ export default {
).then(() => {
this.$message.success(this.$t('common.deleteSuccessMsg'))
this.zTree.removeNode(currentNode)
this.refreshTree()
}).catch(() => {
// this.$message.error(this.$t('common.deleteErrorMsg' + ' ' + error))
// this.$message.error(this.$t('common.deleteErrorMsg') + ' ' + error)
})
},
onRename: function(event, treeId, treeNode, isCancel) {
@@ -139,9 +150,7 @@ export default {
treeNode.name = treeNode.name + ' (' + assetsAmount + ')'
this.zTree.updateNode(treeNode)
this.$message.success(this.$t('common.updateSuccessMsg'))
}).catch(error => {
this.$message.error(this.$t('common.updateErrorMsg' + ' ' + error))
})
}).finally(() => { this.refreshTree() })
},
onBodyMouseDown: function(event) {
const rMenuID = this.$refs.dataztree.$refs.ztree.iRMenuID
@@ -159,6 +168,11 @@ export default {
y -= (offset.top + scrollTop) / 3 - 10
x += document.body.scrollLeft
y += document.body.scrollTop + document.documentElement.scrollTop
if (y + $(`#${rMenuID} ul`).height() >= window.innerHeight) {
y -= $(`#${rMenuID} ul`).height()
}
this.rMenu.css({ 'top': y + 'px', 'left': x + 'px', 'visibility': 'visible' })
$(`#${rMenuID} ul`).show()
$('body').bind('mousedown', this.onBodyMouseDown)
@@ -205,7 +219,7 @@ export default {
this.$message.success(this.$t('common.updateSuccessMsg'))
}).catch(error => {
this.$message.error(this.$t('common.updateErrorMsg' + ' ' + error))
})
}).finally(() => this.refreshTree())
},
createTreeNode: function() {
this.hideRMenu()
@@ -237,19 +251,21 @@ export default {
})
},
refresh: function() {
this.$axios.post(
'/api/v1/assets/nodes/00000000-0000-0000-0000-000000000000/tasks/',
{ action: 'refresh_cache' }
)
},
getSelectedNodes: function() {
return this.zTree.getSelectedNodes()
},
getNodes: function() {
return this.zTree.getNodes()
},
selectNode: function(node) {
return this.zTree.selectNode(node)
}
}
}
</script>
<style lang='less' scoped>
<style scoped>
.rmenu {
font-size: 12px;
padding: 0 16px;
@@ -272,4 +288,8 @@ export default {
.rmenu:hover{
background-color: #f5f7fa;
}
.data-z-tree >>> .fa {
width: 10px;
}
</style>

View File

@@ -0,0 +1,176 @@
<template>
<div :class="grouped ? 'el-button-group' : 'el-button-ungroup'">
<template v-for="action in iActions">
<el-dropdown
v-if="action.dropdown"
v-show="action.dropdown.length > 0"
:key="action.name"
class="action-item"
trigger="click"
placement="bottom-start"
@command="handleDropdownCallback"
>
<el-button :size="size" v-bind="cleanButtonAction(action)">
{{ action.title }}<i class="el-icon-arrow-down el-icon--right" />
</el-button>
<el-dropdown-menu slot="dropdown">
<template v-for="option in action.dropdown">
<div v-if="option.group" :key="'group:'+option.name" class="dropdown-menu-title">
{{ option.group }}
</div>
<el-dropdown-item
:key="option.name"
:command="[option, action]"
v-bind="option"
>
{{ option.title }}
</el-dropdown-item>
</template>
</el-dropdown-menu>
</el-dropdown>
<el-button
v-else
:key="action.name"
:size="size"
v-bind="cleanButtonAction(action)"
class="action-item"
@click="handleClick(action)"
>
<el-tooltip v-if="action.tip" effect="dark" :content="action.tip" placement="top">
<i v-if="action.fa" :class="'fa ' + action.fa" />{{ action.title }}
</el-tooltip>
<span v-else>
<i v-if="action.fa" :class="'fa ' + action.fa" />{{ action.title }}
</span>
</el-button>
</template>
</div>
</template>
<script>
export default {
name: 'DataActions',
props: {
grouped: {
type: Boolean,
default: false
},
size: {
type: String,
default: 'small'
},
type: {
type: String,
default: ''
},
actions: {
type: Array,
default: () => []
}
},
computed: {
iActions() {
return this.cleanActions(this.actions)
}
},
methods: {
handleDropdownCallback(command) {
const [option, dropdown] = command
const defaultCallback = () => this.$log.debug('No callback found: ', option, dropdown)
let callback = option.callback
if (!callback) {
callback = dropdown.callback
}
if (!callback) {
callback = defaultCallback
}
return callback(option)
},
handleClick(action) {
if (action && action.callback) {
action.callback(action)
} else {
this.$log.debug('No callback found')
}
this.$emit('actionClick', action)
},
checkItem(item, attr, defaults) {
if (!item) {
return true
}
let ok = item[attr]
if (ok && typeof ok === 'function') {
ok = ok(item)
} else if (ok == null) {
ok = defaults === undefined ? true : defaults
}
return ok
},
cleanButtonAction(action) {
action = _.cloneDeep(action)
delete action['dropdown']
delete action['callback']
delete action['name']
delete action['can']
return action
},
cleanActions(actions) {
const cleanedActions = []
const cloneActions = _.cloneDeep(actions)
for (const v of cloneActions) {
if (!v) {
continue
}
const action = Object.assign({}, v)
// 是否拥有这个action
const has = this.checkItem(action, 'has')
delete action['has']
if (!has) {
continue
}
// 是否有分割线
action.divided = this.checkItem(action, 'divided', false)
// 是否是disabled
const can = this.checkItem(action, 'can')
action.disabled = !can
if (action.dropdown) {
// const dropdown = this.cleanActions(action.dropdown)
action.dropdown = this.cleanActions(action.dropdown)
}
cleanedActions.push(action)
}
return cleanedActions
}
}
}
</script>
<style scoped>
.dropdown-menu-title {
text-align: left;
font-size: 12px;
color: #909399;
line-height: 30px;
padding-left: 10px;
padding-top: 10px;
border-top: solid 1px #e4e7ed;
}
.dropdown-menu-title:first-child {
padding-top: 0;
border-top: none;
}
.el-button-ungroup .action-item {
margin-left: 4px
}
.el-button-ungroup .action-item:first-child {
margin-left: 0;
}
</style>

View File

@@ -4,7 +4,7 @@
:content="fields"
:form="basicForm"
label-position="right"
label-width="17%"
label-width="20%"
v-bind="$attrs"
v-on="$listeners"
>
@@ -15,6 +15,7 @@
<el-form-item>
<el-button v-for="button in moreButtons" :key="button.title" size="small" v-bind="button" @click="handleClick(button)">{{ button.title }}</el-button>
<el-button v-if="defaultButton && hasReset" size="small" @click="resetForm('form')">{{ $t('common.Reset') }}</el-button>
<el-button v-if="defaultButton && hasSaveContinue" size="small" @click="submitForm('form', true)">{{ $t('common.SaveAndAddAnother') }}</el-button>
<el-button v-if="defaultButton" size="small" :loading="isSubmitting" type="primary" @click="submitForm('form')">{{ $t('common.Submit') }}</el-button>
</el-form-item>
</ElFormRender>
@@ -35,6 +36,10 @@ export default {
type: Boolean,
default: true
},
hasSaveContinue: {
type: Boolean,
default: true
},
fields: {
type: Array,
default: () => []
@@ -42,7 +47,7 @@ export default {
// 初始值
form: {
type: Object,
default: () => { return {} }
default: () => ({})
},
moreButtons: {
type: Array,
@@ -60,11 +65,11 @@ export default {
},
methods: {
// 获取表单数据
submitForm(formName) {
submitForm(formName, addContinue) {
const form = this.$refs[formName]
form.validate((valid) => {
if (valid) {
this.$emit('submit', form.getFormValue(), form)
this.$emit('submit', form.getFormValue(), form, addContinue)
} else {
this.$emit('invalid', valid)
return false
@@ -77,7 +82,7 @@ export default {
},
handleClick(button) {
const callback = button.callback || function(values, form) {
console.log('Click ', button.title, ': ', values)
// console.log('Click ', button.title, ': ', values)
}
const form = this.$refs['form']
const values = form.getFormValue()

View File

@@ -8,7 +8,14 @@ export const RequiredChange = {
required: true, message: i18n.t('common.fieldRequiredError'), trigger: 'change'
}
export const EmailCheck = {
type: 'email',
message: i18n.t('common.InputEmailAddress'),
trigger: ['blur', 'change']
}
export default {
Required,
RequiredChange
RequiredChange,
EmailCheck
}

View File

@@ -11,9 +11,10 @@
v-bind="tableAttrs"
:data="data"
:row-class-name="rowClassName"
v-on="$listeners"
@selection-change="selectStrategy.onSelectionChange"
@select="selectStrategy.onSelect"
@select-all="selectStrategy.onSelectAll($event, selectable)"
@select-all="selectStrategy.onSelectAll($event, canSelect)"
@sort-change="onSortChange"
>
<!--TODO 不用jsx写, 感觉template逻辑有点不清晰了-->
@@ -90,11 +91,14 @@
<!--非树-->
<template v-else>
<el-data-table-column v-if="hasSelection" type="selection" :align="selectionAlign" />
<el-data-table-column v-if="hasSelection" type="selection" :align="selectionAlign" :selectable="canSelect" />
<el-data-table-column
v-for="col in columns"
:key="col.prop"
:formatter="typeof col.formatter === 'function' ? col.formatter : null"
:filters="col.filters || null"
:filter-multiple="false"
:filter-method="typeof col.filterMethod === 'function' ? col.filterMethod : null"
v-bind="{align: columnsAlign, ...col}"
>
<template v-if="col.formatter && typeof col.formatter !== 'function'" v-slot:default="{row, column, index}">
@@ -163,7 +167,7 @@ import getLocatedSlotKeys from './utils/extract-keys'
import transformSearchImmediatelyItem from './utils/search-immediately-item'
import isFalsey from './utils/is-falsey'
import merge from 'deepmerge'
const defaultFirstPage = 0
const defaultFirstPage = 1
const noPaginationDataPath = 'payload'
export default {
@@ -422,7 +426,6 @@ export default {
onEdit: {
type: Function,
default(row) {
console.log('On delete row')
}
},
/**
@@ -713,6 +716,16 @@ export default {
hasDetail: {
type: Boolean,
default: true
},
canSelect: {
type: Function,
default(row, index) {
return true
}
},
totalData: {
type: Array,
default: null
}
},
data() {
@@ -800,6 +813,13 @@ export default {
},
_searchForm() {
return transformSearchImmediatelyItem(this.collapseForm, this)
},
lastPageNum() {
// page
const pageOffset = this.firstPage - defaultFirstPage
const pageCount = Math.ceil(this.total / this.size)
const lastPageNum = pageCount + pageOffset
return lastPageNum
}
},
watch: {
@@ -818,6 +838,13 @@ export default {
* @property {array} rows - 已选中的行数据的数组
*/
this.$emit('selection-change', val)
},
totalData(val) {
if (val) {
this.page = defaultFirstPage
this.total = val.length
this.getList()
}
}
},
mounted() {
@@ -834,6 +861,9 @@ export default {
}
}
}
if (this.totalData) {
this.getList()
}
},
methods: {
getQuery() {
@@ -867,12 +897,58 @@ export default {
}
return query
},
getPageData() {
return this.data
},
async gotoNextPage() {
if (!this.hasNextPage()) {
return false
}
this.page += 1
await this.getList({ loading: true })
},
hasNextPage() {
return this.page < this.lastPageNum
},
getList({ loading = true } = {}) {
const { url } = this
if (url) {
return this.getListFromRemote({ loading: loading })
}
if (this.totalData) {
return this.getListFromStaticData({ loading: true })
}
// this.$log.debug("last page is: ", this.lastPageNum)
},
getListFromStaticData({ loading = true } = {}) {
if (loading) {
this.loading = true
}
if (!this.hasPagination) {
this.data = this.totalData
this.loading = false
if (this.isTree) {
this.data = this.tree2Array(this.data, this.expandAll)
}
return this.data
}
// page
const pageOffset = this.firstPage - defaultFirstPage
const page = this.page === 0 ? 1 : this.page
const start = (page + pageOffset - 1) * this.size
const end = (page + pageOffset) * this.size
this.$log.debug(`page: ${page}, size: ${this.size}, start: ${start}, end: ${end}`)
this.data = this.totalData.slice(start, end)
this.loading = false
this.data = this.tree2Array(this.data, this.expandAll)
return this.data
},
/**
* 手动刷新列表数据,选项的默认值为: { loading: true }
* @public
* @param {object} options 方法选项
*/
getList({ loading = true } = {}) {
getListFromRemote({ loading = true } = {}) {
const { url } = this
if (!url) {
return
@@ -1000,7 +1076,7 @@ export default {
},
handleSizeChange(val) {
if (this.size === val) return
this.$emit('sizeChange', val)
this.page = defaultFirstPage
this.size = val
this.getList()

View File

@@ -1,9 +1,17 @@
<template>
<ElDatableTable ref="table" class="el-table" v-bind="tableConfig" @update="onUpdate" v-on="iListeners" />
<ElDatableTable
ref="table"
class="el-table"
v-bind="tableConfig"
@update="onUpdate"
v-on="iListeners"
@sizeChange="handleSizeChange"
/>
</template>
<script>
import { default as ElDatableTable } from './compenents/el-data-table'
import { mapGetters } from 'vuex'
export default {
name: 'DataTable',
@@ -58,7 +66,6 @@ export default {
pageCount: 5,
paginationLayout: 'total, sizes, prev, pager, next',
paginationSizes: [15, 30, 50, 100],
paginationSize: 15,
paginationBackground: true,
transformQuery: query => {
if (query.page && query.size) {
@@ -85,12 +92,20 @@ export default {
},
computed: {
tableConfig() {
const config = Object.assign(this.defaultConfig, this.config)
const tableDefaultConfig = this.defaultConfig
tableDefaultConfig.paginationSize = _.get(this.globalTableConfig, 'paginationSize', 15)
const config = Object.assign(tableDefaultConfig, this.config)
return config
},
iListeners() {
return Object.assign({}, this.$listeners, this.tableConfig.listeners)
}
},
dataTable() {
return this.$refs.table
},
...mapGetters({
'globalTableConfig': 'tableConfig'
})
},
watch: {
config: {
@@ -131,6 +146,14 @@ export default {
this.toggleRowSelection(row, true)
}
}
},
handleSizeChange(val) {
this.$store.commit('table/SET_TABLE_CONFIG',
{
key: 'paginationSize',
value: val
}
)
}
}
}

View File

@@ -1,6 +1,9 @@
<template>
<div>
<div class="treebox">
<ul v-show="loading" class="ztree">
{{ this.$t('common.tree.Loading') }}...
</ul>
<div v-show="!loading" class="treebox">
<ul :id="iZTreeID" class="ztree">
{{ this.$t('common.tree.Loading') }}...
</ul>
@@ -22,6 +25,7 @@
import $ from '@/utils/jquery-vendor.js'
import '@ztree/ztree_v3/js/jquery.ztree.all.min.js'
import '@/styles/ztree.css'
import axiosRetry from 'axios-retry'
const defaultObject = {
type: Object,
@@ -39,7 +43,9 @@ export default {
iZTreeID: `zTree_${this._uid}`,
iRMenuID: `rMenu_${this._uid}`,
zTree: '',
rMenu: ''
rMenu: '',
init: false,
loading: false
}
},
computed: {
@@ -56,7 +62,26 @@ export default {
},
methods: {
initTree: function() {
this.$axios.get(this.treeSetting.treeUrl).then(res => {
const vm = this
let treeUrl
if (this.init) {
this.loading = true
}
if (this.init && this.treeSetting.treeUrl.indexOf('/perms/') !== -1 && this.treeSetting.treeUrl.indexOf('rebuild_tree') === -1) {
treeUrl = (this.treeSetting.treeUrl.indexOf('?') === -1) ? `${this.treeSetting.treeUrl}?rebuild_tree=1` : `${this.treeSetting.treeUrl}&rebuild_tree=1`
} else {
treeUrl = this.treeSetting.treeUrl
}
this.$axios.get(treeUrl, {
'axios-retry': {
retries: 20,
retryCondition: e => {
return axiosRetry.isNetworkOrIdempotentRequestError(e) || e.response.status === 409
},
shouldResetTimeout: true,
retryDelay: () => { return 5000 }
}
}).then(res => {
if (!res) {
res = []
}
@@ -65,7 +90,13 @@ export default {
name: this.$t('common.tree.Empty')
})
}
this.treeSetting.treeUrl = treeUrl
if (this.init) {
vm.zTree.destroy()
}
this.zTree = $.fn.zTree.init($(`#${this.iZTreeID}`), this.treeSetting, res)
// 手动上报事件, Tree加载完成
this.$emit('TreeInitFinish', this.zTree)
if (this.treeSetting.showRefresh) {
this.rootNodeAddDom(
this.zTree,
@@ -79,6 +110,9 @@ export default {
if (this.treeSetting.otherMenu) {
$('.menu-actions').append(this.otherMenu)
}
}).finally(_ => {
vm.loading = false
vm.init = true
})
},
rootNodeAddDom: function(ztree, callback) {
@@ -95,7 +129,6 @@ export default {
}
const refreshIconRef = $('#tree-refresh')
refreshIconRef.bind('click', function() {
ztree.destroy()
const result = callback()
if (result && result.then) {
result.finally(() => {

View File

@@ -1,5 +1,5 @@
<template>
<ZTree ref="ztree" :setting="treeSetting">
<ZTree ref="ztree" :setting="treeSetting" v-on="$listeners">
<!--Slot透传-->
<div slot="rMenu" slot-scope="{data}">
<slot name="rMenu" :data="data" />
@@ -37,8 +37,8 @@ export default {
showRemoveBtn: false,
showRenameBtn: false,
drag: {
isCopy: true,
isMove: true
isCopy: false,
isMove: false
}
},
callback: {
@@ -72,7 +72,7 @@ export default {
},
methods: {
defaultCallback: function(action) {
console.log(action)
// console.log(action)
}
}
}

View File

@@ -10,7 +10,7 @@
<div slot="footer" class="dialog-footer">
<slot name="footer">
<el-button v-if="showCancel" size="small" @click="onCancel">{{ cancelTitle }}</el-button>
<el-button v-if="showConfirm" type="primary" size="small" @click="onConfirm">{{ confirmTitle }}</el-button>
<el-button v-if="showConfirm" type="primary" size="small" :loading="loadingStatus" @click="onConfirm">{{ confirmTitle }}</el-button>
</slot>
</div>
</el-dialog>
@@ -42,6 +42,10 @@ export default {
type: Boolean,
default: true
},
loadingStatus: {
type: Boolean,
default: false
},
confirmTitle: {
type: String,
default() {
@@ -51,7 +55,6 @@ export default {
},
data() {
return {
}
},
methods: {
@@ -70,4 +73,8 @@ export default {
/*padding-top: 10px;*/
}
.dialog-footer {
padding-right: 50px;
}
</style>

View File

@@ -93,7 +93,7 @@ export default {
<style lang='less' scoped>
.datepicker{
width: 240px;
width: 233px;
}
.el-input__inner{
border: 1px solid #dcdee2;

View File

@@ -0,0 +1,30 @@
<template>
<el-link @click="onClick">
{{ title }}
</el-link>
</template>
<script>
export default {
name: 'Link',
props: {
href: {
type: String,
default: ''
},
title: {
type: String,
default: ''
}
},
methods: {
onClick() {
window.open(this.href)
}
}
}
</script>
<style scoped>
</style>

View File

@@ -6,7 +6,6 @@
</template>
<script>
export default {
props: {
value: {

View File

@@ -7,7 +7,7 @@
</template>
<script>
import PasswordInput from '../PasswordInput'
import PasswordInput from './PasswordInput'
import { mapGetters } from 'vuex'
import store from '@/store'
import i18n from '@/i18n/i18n'

View File

@@ -0,0 +1,250 @@
<template>
<div>
<div class="hours-container">
<div v-for="(item, index) in hours" :key="index" class="hours-item">
<div class="hours-item-header">{{ compItem(item) }}</div>
<div class="hours-item-value">
<div
:class="compClass(2 * item)"
@click="handleClick(2 * item)"
@mouseover="handleHover(2 * item)"
/>
</div>
</div>
</div>
<div class="tips">{{ tips }}</div>
</div>
</template>
<script>
export default {
model: {
prop: 'sendTimeList'
},
props: {
sendTimeList: {
type: Object,
required: true,
default: () => []
},
readonly: {
type: Boolean,
default: false
}
},
data() {
return {
hours: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23], // 选项
selectStart: false, // 开始
startIndex: '', // 开始下标
timeRangeList: [], // 选择的时间段
timeRangeListIndex: [], // 选中的下标
tempRangeIndex: [], // 预选下标
tips: '向右选中,向左取消选择'
}
},
computed: {
},
watch: {
timeRangeList: function(value) {
this.$emit('change', value)
this.$parent.$emit('el.form.change')// 触发父组件的校验规则
},
sendTimeList: {
handler() {
this.transformedIndex()
},
deep: true
}
},
mounted() {
this.transformedIndex()
},
methods: {
// 时间区间转换成下标区间
transformedIndex() {
this.timeRangeListIndex = []
this.timeRangeList = this.sendTimeList
this.timeRangeList.forEach(element => {
const [startTime, endTime] = element.match(/\d+\:\d+/g)
if (startTime && endTime) {
const [startHour, startMin] = startTime.split(':')
const [endHour, endMin] = endTime.split(':')
if (startHour && startMin && endHour && endMin) {
let startNum, endNum
if (startMin === '00') {
startNum = 2 * parseInt(startHour)
} else {
startNum = 2 * parseInt(startHour) + 1
}
if (endMin === '00') {
endNum = 2 * parseInt(endHour) - 1
} else {
endNum = 2 * parseInt(endHour)
}
while (endNum >= startNum) {
this.timeRangeListIndex.push(startNum)
startNum++
}
} else {
this.$message.error('时间段格式不正确')
}
} else {
this.$message.error('没有拿到开始时间或结束时间或者时间段格式不对')
}
})
this.tips = this.timeRangeList && this.timeRangeList.length > 0 ? this.timeRangeList : '向右选中,向左取消选择'
},
// 下标区间转换成时间区间
transformedSection() {
this.timeRangeList = []
let startTime = ''; let endTime = ''; const len = this.hours.length
for (let index = this.hours[0] * 2; index < 2 * (len + 1); index++) {
if (this.timeRangeListIndex.indexOf(index) > -1) {
if (startTime) { // 如果有开始时间,直接确定结束时间
const endHour = Math.floor((index + 1) / 2)
const endMin = (index + 1) % 2 === 0 ? '00' : '30'
endTime = `${endHour < 10 ? '0' + endHour : endHour}:${endMin}`
} else { // 没有开始时间,确定当前点为开始时间
const startHour = Math.floor(index / 2)
const startMin = index % 2 === 0 ? '00' : '30'
startTime = `${startHour < 10 ? '0' + startHour : startHour}:${startMin}`
}
if (index === 2 * this.hours.length + 1) { // 如果是最后一格,直接结束
endTime = `${Math.floor((index + 1) / 2)}:00`
this.timeRangeList.push(`${startTime || '23:30'}-${endTime}`)
startTime = ''
endTime = ''
}
} else { // 若这个点不在选择区间,确定一个时间段
if (startTime && endTime) {
this.timeRangeList.push(`${startTime}-${endTime}`)
startTime = ''
endTime = ''
} else if (startTime && !endTime) { // 这里可能只选半个小时
const endHour = Math.floor(index / 2)
const endMin = index % 2 === 0 ? '00' : '30'
endTime = `${endHour < 10 ? '0' + endHour : endHour}:${endMin}`
this.timeRangeList.push(`${startTime}-${endTime}`)
startTime = ''
endTime = ''
}
}
}
this.tips = this.timeRangeList && this.timeRangeList.length > 0 ? this.timeRangeList : '向右选中,向左取消选择'
},
// 点击事件
handleClick(index) {
if (this.selectStart) {
if (index === this.startIndex) { // 双击取反
if (this.timeRangeListIndex.indexOf(index) > -1) {
this.timeRangeListIndex.splice(this.timeRangeListIndex.indexOf(index), 1)
} else {
this.timeRangeListIndex.push(this.startIndex)
}
} else if (index > this.startIndex) { // 选取数据--向右添加,向左取消
while (index >= this.startIndex) {
this.timeRangeListIndex.push(this.startIndex)
this.startIndex++
}
this.timeRangeListIndex = Array.from(new Set(this.timeRangeListIndex))
} else { // 删除数据
while (this.startIndex >= index) {
if (this.timeRangeListIndex.indexOf(index) > -1) {
this.timeRangeListIndex.splice(this.timeRangeListIndex.indexOf(index), 1)
}
index++
}
}
this.startIndex = ''
this.tempRangeIndex = []
this.transformedSection()
} else {
this.startIndex = index
}
this.selectStart = !this.selectStart
},
// 预选区间
handleHover(index) {
if (this.selectStart) {
this.tempRangeIndex = []
if (index > this.startIndex) { // 选取数据--向右添加,向左取消
while (index >= this.startIndex) {
this.tempRangeIndex.push(index)
index--
}
} else { // 删除数据
while (this.startIndex >= index) {
this.tempRangeIndex.push(index)
index++
}
}
}
},
// 是否选中计算className
compClass(index) {
if (index === this.startIndex) {
return 'hours-item-left preSelected'
}
if (index >= this.startIndex) {
if (this.tempRangeIndex.indexOf(index) > -1) {
return 'hours-item-left preSelected'
}
} else {
if (this.tempRangeIndex.indexOf(index) > -1) {
return 'hours-item-left unSelected'
}
}
return this.timeRangeListIndex.indexOf(index) > -1 ? 'hours-item-left selected' : 'hours-item-left'
},
compItem(item) { // 不足10前面补0
return item < 10 ? `0${item}` : item
}
}
}
</script>
<style lang='scss' scoped>
.hours-container {
display: flex;
cursor: pointer;
.hours-item {
width: 30px;
height: 60px;
border: 1px solid #c2d0f3;
border-right: none;
text-align: center;
&:last-child {
border-right: 1px solid #c2d0f3;
}
.hours-item-header {
width: 100%;
height: 30px;
line-height: 30px;
border-bottom: 1px solid #c2d0f3;
}
.hours-item-value {
width: 100%;
height: 30px;
box-sizing: border-box;
display: flex;
}
.selected {
background-color: #4e84fe;
border-bottom: 1px solid #c2d0f3;
}
.preSelected {
background-color: #8eaffc;
border-bottom: 1px solid #c2d0f3;
}
.unSelected {
background-color: #ffffff;
border-bottom: 1px solid #c2d0f3;
}
}
}
.tips {
width: 100%;
line-height: 30px;
}
</style>

View File

@@ -0,0 +1,33 @@
import DatetimeRangePicker from './DatetimeRangePicker'
import Link from './Link'
import PasswordInput from './PasswordInput'
import Select2 from './Select2'
import Swicher from './Swicher'
import UploadField from './UploadField'
import UploadKey from './UploadKey'
import UserPassword from './UserPassword'
import WeekCronSelect from './WeekCronSelect'
export default {
DatetimeRangePicker,
Link,
PasswordInput,
Select2,
Swicher,
UploadKey,
UploadField,
UserPassword,
WeekCronSelect
}
export {
DatetimeRangePicker,
Link,
PasswordInput,
Select2,
Swicher,
UploadKey,
UploadField,
UserPassword,
WeekCronSelect
}

View File

@@ -3,7 +3,7 @@
</template>
<script type="text/jsx">
import { DetailFormatter, SystemUserFormatter } from '@/components/ListTable/formatters'
import { DetailFormatter, SystemUserFormatter } from '@/components/TableFormatters'
import TreeTable from '../TreeTable'
export default {
@@ -65,22 +65,24 @@ export default {
sortable: true,
formatterArgs: {
route: 'AssetDetail'
}
},
showOverflowTooltip: true
},
{
prop: 'ip',
label: this.$t('assets.IP'),
width: '140px',
sortable: 'custom'
},
{
prop: 'systemUsers',
label: this.$t('assets.SystemUsers'),
align: 'center',
width: '200px',
formatter: SystemUserFormatter,
formatterArgs: {
getUrl: this.getShowUrl.bind(this)
}
},
showOverflowTooltip: true
}
]
},

View File

@@ -1,6 +1,11 @@
<template>
<Dialog v-if="showExportDialog" :title="$t('common.Export')" :visible.sync="showExportDialog" :destroy-on-close="true" @confirm="handleExportConfirm()" @cancel="handleExportCancel()">
<el-form label-position="left" style="padding-left: 50px">
<el-form-item :label="$t('common.fileType' )" :label-width="'100px'">
<el-radio-group v-model="exportTypeOption">
<el-radio v-for="option of exportTypeOptions" :key="option.value" style="padding: 10px 20px;" :label="option.value" :disabled="!option.can">{{ option.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item class="export-form" :label="this.$t('common.imExport.ExportRange')" :label-width="'100px'">
<el-radio-group v-model="exportOption">
<el-radio v-for="option of exportOptions" :key="option.value" class="export-item" :label="option.value" :disabled="!option.can">{{ option.label }}</el-radio>
@@ -52,6 +57,7 @@ export default {
return {
showExportDialog: false,
exportOption: 'all',
exportTypeOption: 'csv',
meta: {}
}
},
@@ -92,11 +98,27 @@ export default {
can: this.tableHasQuery && this.canExportFiltered
}
]
},
exportTypeOptions() {
return [
{
label: 'CSV',
value: 'csv',
can: true
},
{
label: 'Excel',
value: 'xlsx',
can: true
}
]
}
},
mounted() {
this.$eventBus.$on('showExportDialog', (row) => {
this.showExportDialog = true
this.$eventBus.$on('showExportDialog', ({ selectedRows, url }) => {
if (url === this.url) {
this.showExportDialog = true
}
})
},
methods: {
@@ -123,7 +145,7 @@ export default {
// delete query['limit']
// delete query['offset']
}
query['format'] = 'csv'
query['format'] = this.exportTypeOption
const queryStr =
(url.indexOf('?') > -1 ? '&' : '?') +
queryUtil.stringify(query, '=', '&')

View File

@@ -1,52 +1,73 @@
<template>
<Dialog :title="$t('common.Import')" :visible.sync="showImportDialog" :destroy-on-close="true" @confirm="handleImportConfirm" @cancel="handleImportCancel()">
<el-form label-position="left" style="padding-left: 50px">
<Dialog
:title="importTitle"
:visible.sync="showImportDialog"
:destroy-on-close="true"
:close-on-click-modal="false"
:loading-status="loadStatus"
width="80%"
class="importDialog"
:show-cancel="false"
:show-confirm="false"
@close="handleImportCancel"
>
<el-form v-if="!showTable" label-position="left" style="padding-left: 50px">
<el-form-item :label="$t('common.Import' )" :label-width="'100px'">
<el-radio v-model="importOption" class="export-item" label="1">{{ this.$t('common.Create') }}</el-radio>
<el-radio v-model="importOption" class="export-item" label="2">{{ this.$t('common.Update') }}</el-radio>
<el-radio v-model="importOption" class="export-item" label="create">{{ this.$t('common.Create') }}</el-radio>
<el-radio v-model="importOption" class="export-item" label="update">{{ this.$t('common.Update') }}</el-radio>
<div style="line-height: 1.5">
<span v-if="importOption==='1'" class="el-upload__tip">
{{ this.$t('common.imExport.downloadImportTemplateMsg') }}
<el-link type="success" :underline="false" :href="downloadImportTempUrl">{{ this.$t('common.Download') }}</el-link>
</span>
<span v-else class="el-upload__tip">
{{ this.$t('common.imExport.downloadUpdateTemplateMsg') }}
<el-link type="success" :underline="false" @click="downloadUpdateTempUrl">{{ this.$t('common.Download') }}</el-link>
<span class="el-upload__tip">
{{ downloadTemplateTitle }}
<el-link type="success" :underline="false" style="padding-left: 10px" @click="downloadTemplateFile('csv')"> CSV </el-link>
<el-link type="success" :underline="false" style="padding-left: 10px" @click="downloadTemplateFile('xlsx')"> XLSX </el-link>
</span>
</div>
</el-form-item>
<el-form-item :label="$t('common.Upload' )" :label-width="'100px'">
<el-form-item :label="$t('common.Upload' )" :label-width="'100px'" class="file-uploader">
<el-upload
ref="upload"
drag
action="string"
list-type="text/csv"
:http-request="handleImport"
:limit="1"
:auto-upload="false"
:on-change="onFileChange"
:before-upload="beforeUpload"
accept=".csv,.xlsx"
>
<el-button size="mini" type="default">{{ this.$t('common.SelectFile') }}</el-button>
<div slot="tip" :class="uploadHelpTextClass" style="line-height: 1.5">{{ this.$t('common.imExport.onlyCSVFilesTips') }}</div>
<i class="el-icon-upload" />
<div class="el-upload__text">{{ $t('common.imExport.dragUploadFileInfo') }}</div>
<div slot="tip" class="el-upload__tip">
<span :class="{'hasError': hasFileFormatOrSizeError }">{{ $t('common.imExport.uploadCsvLth10MHelpText') }}</span>
<div v-if="renderError" class="hasError">{{ renderError }}</div>
</div>
</el-upload>
</el-form-item>
</el-form>
<div v-if="errorMsg" class="error-msg error-results">
<ul v-if="typeof errorMsg === 'object'">
<li v-for="(item, index) in errorMsg" :key="item + '-' + index"> {{ item }}</li>
</ul>
<span v-else>{{ errorMsg }}</span>
<div v-else class="importTableZone">
<ImportTable
ref="importTable"
:json-data="jsonData"
:import-option="importOption"
:url="url"
@cancel="cancelUpload"
@finish="closeDialog"
/>
</div>
</Dialog>
</template>
<script>
import Dialog from '@/components/Dialog'
import ImportTable from '@/components/ListTable/TableAction/ImportTable'
import { getErrorResponseMsg } from '@/utils/common'
import { createSourceIdCache } from '@/api/common'
export default {
name: 'ImportDialog',
components: {
Dialog
Dialog,
ImportTable
},
props: {
selectedRows: {
@@ -61,82 +82,119 @@ export default {
data() {
return {
showImportDialog: false,
importOption: '1',
isCsv: true,
errorMsg: ''
importOption: 'create',
errorMsg: '',
loadStatus: false,
importTypeOption: 'csv',
importTypeIsCsv: true,
showTable: false,
renderError: '',
hasFileFormatOrSizeError: false,
jsonData: {}
}
},
computed: {
hasSelected() {
return this.selectedRows.length > 0
},
upLoadUrl() {
return this.url
},
downloadImportTempUrl() {
const url = (this.url.indexOf('?') === -1) ? `${this.url}?format=csv&template=import&limit=1` : `${this.url}&format=csv&template=import&limit=1`
return url
},
uploadHelpTextClass() {
const cls = ['el-upload__tip']
if (!this.isCsv) {
cls.push('error-msg')
}
return cls
},
downloadTemplateTitle() {
if (this.importOption === 'create') {
return this.$t('common.imExport.downloadImportTemplateMsg')
} else {
return this.$t('common.imExport.downloadUpdateTemplateMsg')
}
},
importTitle() {
if (this.importOption === 'create') {
return this.$t('common.Import') + this.$t('common.Create')
} else {
return this.$t('common.Import') + this.$t('common.Update')
}
}
},
watch: {
importOption(val) {
this.showTable = false
}
},
mounted() {
this.$eventBus.$on('showImportDialog', (row) => {
this.showImportDialog = true
this.$eventBus.$on('showImportDialog', ({ url }) => {
if (url === this.url) {
this.showImportDialog = true
}
})
},
methods: {
performUpdate(item) {
this.$axios.put(
this.upLoadUrl,
item.file,
{ headers: { 'Content-Type': 'text/csv' }, disableFlashErrorMsg: true }
).then((data) => {
const msg = this.$t('common.imExport.updateSuccessMsg', { count: data.length })
this.onSuccess(msg)
closeDialog() {
this.showImportDialog = false
},
cancelUpload() {
this.showTable = false
this.renderError = ''
this.jsonData = {}
},
onFileChange(file, fileList) {
fileList.splice(0, fileList.length)
if (file.status !== 'ready') {
return
}
// const isCsv = file.raw.type = 'text/csv'
if (!this.beforeUpload(file)) {
return
}
const isCsv = file.name.indexOf('csv') > -1
const url = new URL(this.url, 'http://localhost')
url.pathname += 'render-to-json/'
const renderToJsonUrl = url.toString().replace('http://localhost', '')
this.$axios.post(
renderToJsonUrl,
file.raw,
{ headers: { 'Content-Type': isCsv ? 'text/csv' : 'text/xlsx' }, disableFlashErrorMsg: true }
).then(data => {
this.jsonData = data
this.showTable = true
}).catch(error => {
this.catchError(error)
fileList.splice(0, fileList.length)
this.renderError = getErrorResponseMsg(error)
}).finally(() => {
this.loadStatus = false
})
},
performCreate(item) {
this.$axios.post(
this.upLoadUrl,
item.file,
{ headers: { 'Content-Type': 'text/csv' }, disableFlashErrorMsg: true }
).then((data) => {
const msg = this.$t('common.imExport.createSuccessMsg', { count: data.length })
this.onSuccess(msg)
}).catch(error => {
this.catchError(error)
})
beforeUpload(file) {
const isLt30M = file.size / 1024 / 1024 < 30
if (!isLt30M) {
this.hasFileFormatOrSizeError = true
}
return isLt30M
},
async downloadTemplateFile(tp) {
const downloadUrl = await this.getDownloadTemplateUrl(tp)
window.open(downloadUrl)
},
async getDownloadTemplateUrl(tp) {
const template = this.importOption === 'create' ? 'import' : 'update'
let query = `format=${tp}&template=${template}`
if (this.importOption === 'update' && this.selectedRows.length > 0) {
const resources = []
for (const item of this.selectedRows) {
resources.push(item.id)
}
const resp = await createSourceIdCache(resources)
query += `&spm=${resp.spm}`
} else {
query += '&limit=1'
}
return this.url.indexOf('?') === -1 ? `${this.url}?${query}` : `${this.url}&${query}`
},
catchError(error) {
this.$refs.upload.clearFiles()
if (error.response && error.response.status === 400) {
const errorData = error.response.data
const totalErrorMsg = []
errorData.forEach((value, index) => {
if (typeof value === 'string') {
totalErrorMsg.push(`line ${index}. ${value}`)
} else {
const errorMsg = [`line ${index}. `]
for (const [k, v] of Object.entries(value)) {
if (v) {
errorMsg.push(`${k}: ${v}`)
}
}
if (errorMsg.length > 1) {
totalErrorMsg.push(errorMsg.join(' '))
}
}
})
this.errorMsg = totalErrorMsg
}
console.log(error)
},
onSuccess(msg) {
this.errorMsg = ''
@@ -148,33 +206,14 @@ export default {
a.click()
window.URL.revokeObjectURL(url)
},
handleImport(item) {
if (this.importOption === '1') {
this.performCreate(item)
} else {
this.performUpdate(item)
}
},
async downloadUpdateTempUrl() {
var resources = []
const data = this.selectedRows
for (let index = 0; index < data.length; index++) {
resources.push(data[index].id)
}
const spm = await createSourceIdCache(resources)
const baseUrl = (process.env.VUE_APP_ENV === 'production') ? (`${this.url}`) : (`${process.env.VUE_APP_BASE_API}${this.url}`)
const url = `${baseUrl}?format=csv&template=update&spm=` + spm.spm
return this.downloadCsv(url)
},
async handleImportConfirm() {
this.$refs.upload.submit()
this.$refs['importTable'].performUpload()
},
handleImportCancel() {
this.showImportDialog = false
},
beforeUpload(file) {
this.isCsv = _.endsWith(file.name, 'csv')
return this.isCsv
this.showTable = false
this.renderError = ''
this.jsonData = {}
}
}
}
@@ -191,4 +230,49 @@ export default {
overflow: auto
}
.importDialog >>> .el-form-item.file-uploader {
padding-right: 150px;
}
.file-uploader >>> .el-upload {
width: 100%;
//padding-right: 150px;
}
.file-uploader >>> .el-upload-dragger {
width: 100%;
}
.importTableZone {
padding: 0 20px;
.importTable {
overflow: auto;
}
.tableFilter {
padding-bottom: 10px;
}
}
.importTable >>> .el-dialog__body {
padding-bottom: 20px;
}
.export-item {
margin-left: 80px;
}
.export-item:first-child {
margin-left: 0;
}
.hasError {
color: $--color-danger;
}
.el-upload__tip {
line-height: 1.5;
padding-top: 0;
}
</style>

View File

@@ -0,0 +1,426 @@
<template>
<div>
<el-row>
<el-col :span="8">
<div class="tableFilter">
<el-radio-group v-model="importStatusFilter" size="small">
<el-radio-button label="all">{{ $t('common.Total') }}</el-radio-button>
<el-radio-button label="ok">{{ $t('common.Success') }}</el-radio-button>
<el-radio-button label="error">{{ $t('common.Failed') }}</el-radio-button>
<el-radio-button label="pending">{{ $t('common.Pending') }}</el-radio-button>
</el-radio-group>
</div>
</el-col>
<el-col :span="8" style="text-align: center">
<span class="summary-item summary-total"> {{ $t('common.Total') }}: {{ totalCount }}</span>
<span class="summary-item summary-success"> {{ $t('common.Success') }}: {{ successCount }}</span>
<span class="summary-item summary-failed"> {{ $t('common.Failed') }}: {{ failedCount }}</span>
<span class="summary-item summary-pending"> {{ $t('common.Pending') }}: {{ pendingCount }}</span>
</el-col>
</el-row>
<div class="row">
<el-progress :percentage="processedPercent" />
</div>
<DataTable v-if="tableGenDone" id="importTable" ref="dataTable" :config="tableConfig" class="importTable" />
<div class="row" style="padding-top: 20px">
<div style="float: right">
<el-button size="small" @click="performCancel">{{ $t('common.Cancel') }}</el-button>
<el-button size="small" type="primary" @click="performImportAction">{{ importActionTitle }}</el-button>
</div>
</div>
</div>
</template>
<script>
import DataTable from '@/components/DataTable'
import { sleep } from '@/utils/common'
import { EditableInputFormatter, StatusFormatter } from '@/components/TableFormatters'
export default {
name: 'ImportTable',
components: {
DataTable
},
props: {
jsonData: {
type: Object,
default: () => ({})
},
url: {
type: String,
required: true
},
importOption: {
type: String,
required: true
}
},
data() {
return {
columns: [],
importStatusFilter: 'all',
iTotalData: [],
tableConfig: {
hasSelection: false,
// hasPagination: false,
columns: [],
totalData: [],
paginationSize: 10,
paginationSizes: [10],
tableAttrs: {
stripe: true, // 斑马纹表格
border: true, // 表格边框
fit: true, // 宽度自适应,
tooltipEffect: 'dark'
}
},
tableGenDone: false,
importTaskStatus: 'pending', // pending, started, stopped, done
importTaskResult: '', // success, hasError
hasImport: false,
hasContinueButton: false,
importActions: {
import: this.$t('common.Import'),
continue: this.$t('common.Continue'),
stop: this.$t('common.Stop'),
finished: this.$t('common.Finished')
}
}
},
computed: {
tableColumnNameMapper() {
const mapper = {}
for (const column of this.tableConfig.columns) {
mapper[column['prop']] = column['label']
}
return mapper
},
importAction() {
switch (this.importTaskStatus) {
case 'pending':
return 'import'
case 'started':
return 'stop'
}
if (this.totalCount === this.successCount) {
return 'finished'
} else {
return 'continue'
}
},
importActionTitle() {
return this.importActions[this.importAction]
},
successData() {
return this.iTotalData.filter((item) => {
return item['@status'] === 'ok'
})
},
failedData() {
return this.iTotalData.filter((item) => {
return typeof item['@status'] === 'object' && item['@status'].name === 'error'
})
},
pendingData() {
return this.iTotalData.filter((item) => {
return item['@status'] === 'pending'
})
},
totalCount() {
return this.iTotalData.length
},
successCount() {
return this.successData.length
},
failedCount() {
return this.failedData.length
},
pendingCount() {
return this.pendingData.length
},
processedCount() {
return this.totalCount - this.pendingCount
},
processedPercent() {
if (this.totalCount === 0) {
return 0
}
return Math.round(this.processedCount / this.totalCount * 100)
},
elDataTable() {
return this.$refs['dataTable'].dataTable
}
},
watch: {
importStatusFilter(val) {
if (val === 'all') {
this.tableConfig.totalData = this.iTotalData
} else if (val === 'error') {
this.tableConfig.totalData = this.failedData
} else {
this.tableConfig.totalData = this.iTotalData.filter((item) => {
return item['@status'] === val
})
}
}
},
mounted() {
this.generateTable()
},
methods: {
generateTableColumns(tableTitles, tableData) {
const vm = this
const columns = [{
prop: '@status',
label: vm.$t('common.Status'),
width: '80px',
align: 'center',
formatter: StatusFormatter,
formatterArgs: {
iconChoices: {
ok: 'fa-check text-primary',
error: 'fa-times text-danger',
pending: 'fa-clock-o'
},
getChoicesKey(val) {
if (val === 'ok' || val === 'pending') {
return val
}
return 'error'
},
getTip(val) {
if (val === 'ok') {
return vm.$t('common.Success')
} else if (val === 'pending') {
return vm.$t('common.Pending')
} else if (val && val.name === 'error') {
return val.error
}
return ''
},
hasTips: true
}
}]
for (const item of tableTitles) {
const dataItemLens = tableData.map(d => {
const prop = item[1]
const itemColData = d[prop]
if (!d) {
return 0
}
if (typeof itemColData !== 'number' && (!itemColData || !itemColData.length)) {
return 0
}
return itemColData.length
})
let colMaxWidth = Math.max(...dataItemLens) * 10
if (colMaxWidth === 0) {
continue
}
colMaxWidth = Math.min(180, colMaxWidth)
colMaxWidth = Math.max(colMaxWidth, 100)
columns.push({
prop: item[1],
label: item[0],
minWidth: colMaxWidth + 'px',
showOverflowTooltip: true,
formatter: EditableInputFormatter,
formatterArgs: {
onEnter: ({ row, col, oldValue, newValue }) => {
const prop = col.prop
row['@status'] = 'pending'
this.$log.debug(`Set value ${oldValue} => ${newValue}`)
this.$set(row, prop, newValue)
}
}
})
}
return columns
},
generateTableData(tableTitles, tableData) {
const totalData = []
tableData.forEach(item => {
this.$set(item, '@status', 'pending')
totalData.push(item)
})
return totalData
},
generateTable() {
const tableTitles = this.jsonData['title']
const tableData = this.jsonData['data']
const columns = this.generateTableColumns(tableTitles, tableData)
const totalData = this.generateTableData(tableTitles, tableData)
this.tableConfig.columns = columns
this.tableGenDone = true
setTimeout(() => {
this.iTotalData = totalData
this.tableConfig.totalData = totalData
}, 200)
},
beautifyErrorData(errorData) {
if (typeof errorData === 'string') {
return errorData
} else if (Array.isArray(errorData)) {
return errorData
} else if (typeof errorData !== 'object') {
return errorData
}
const data = []
// eslint-disable-next-line prefer-const
for (let [key, value] of Object.entries(errorData)) {
if (typeof value === 'object') {
value = this.beautifyErrorData(value)
}
let label = this.tableColumnNameMapper[key]
if (!label) {
label = key
}
data.push(`${label}: ${value}`)
}
return data
},
performCancel() {
this.performStop()
this.$emit('cancel')
},
performFinish() {
this.performStop()
this.$emit('finish')
},
taskIsStopped() {
return this.importTaskStatus === 'stopped'
},
performImportAction() {
switch (this.importAction) {
case 'continue':
return this.performContinue()
case 'import':
return this.performUpload()
case 'stop':
return this.performStop()
case 'finished':
return this.performFinish()
}
},
performContinue() {
if (this.importTaskStatus === 'done') {
for (const item of this.failedData) {
item['@status'] = 'pending'
}
this.tableConfig.totalData = this.pendingData
}
this.importTaskStatus = 'started'
setTimeout(() => {
this.performUpload()
}, 100)
},
performStop() {
this.importTaskStatus = 'stopped'
},
async performUploadCurrentPageData() {
const currentData = this.elDataTable.getPageData()
for (const item of currentData) {
if (item['@status'] !== 'pending') {
continue
}
if (this.taskIsStopped()) {
return
}
await this.performUploadObject(item)
await sleep(100)
}
},
async performUpload() {
this.importTaskStatus = 'started'
this.importStatusFilter = 'pending'
while (!this.taskIsStopped()) {
await this.performUploadCurrentPageData()
const hasNextPage = this.elDataTable.hasNextPage()
if (hasNextPage && !this.taskIsStopped()) {
await this.elDataTable.gotoNextPage()
await sleep(100)
} else {
break
}
}
if (this.pendingCount === 0) {
this.importTaskStatus = 'done'
}
if (this.failedCount > 0) {
this.$message.error(this.$t('common.imExport.hasImportErrorItemMsg') + '')
}
},
async performUpdateObject(item) {
const updateUrl = `${this.url}${item.id}/`
return this.$axios.put(
updateUrl,
item,
{ disableFlashErrorMsg: true }
)
},
async performUploadObject(item) {
let handler = this.performCreateObject
if (this.importOption === 'update') {
handler = this.performUpdateObject
}
try {
await handler.bind(this)(item)
item['@status'] = 'ok'
} catch (error) {
const errorData = error?.response?.data
const _error = this.beautifyErrorData(errorData)
item['@status'] = {
name: 'error',
error: _error
}
}
},
async performCreateObject(item) {
return this.$axios.post(
this.url,
item,
{ disableFlashErrorMsg: true }
)
},
keepElementInViewport() {
const tableRef = document.getElementById('importTable')
const pendingRef = tableRef?.getElementsByClassName('pendingStatus')[0]
if (!pendingRef) {
return
}
const parentTdRef = pendingRef.parentElement.parentElement.parentElement.parentElement
const rect = parentTdRef.getBoundingClientRect()
let windowInnerHeight = window.innerHeight || document.documentElement.clientHeight
windowInnerHeight = windowInnerHeight * 0.97 - 150
const inViewport = (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= windowInnerHeight
)
if (!inViewport) {
parentTdRef.scrollIntoView({ behavior: 'auto', block: 'start', inline: 'start' })
}
}
}
}
</script>
<style lang="scss" scoped>
@import "~@/styles/element-variables.scss";
.summary-item {
padding: 0 10px
}
.summary-success {
color: $--color-primary;
}
.summary-failed {
color: $--color-danger;
}
.importTable >>> .cell {
min-height: 20px;
height: 100%;
}
</style>

View File

@@ -1,24 +1,32 @@
<template>
<ActionsGroup v-if="hasLeftActions" :actions="actions" :more-actions="moreActions" :more-actions-title="moreActionsTitle" v-bind="$attrs" class="header-action" />
<DataActions
v-if="hasLeftActions"
:actions="iActions"
v-bind="$attrs"
class="header-action"
/>
</template>
<script>
import ActionsGroup from '@/components/ActionsGroup'
import i18n from '@/i18n/i18n'
import DataActions from '@/components/DataActions'
import { createSourceIdCache } from '@/api/common'
import { cleanActions } from './utils'
const defaultTrue = { type: Boolean, default: true }
const defaultFalse = { type: Boolean, default: false }
const defaultTrue = { type: [Boolean, Function], default: true }
const defaultFalse = { type: [Boolean, Function], default: false }
export default {
name: 'LeftSide',
components: {
ActionsGroup
DataActions
},
props: {
hasLeftActions: defaultTrue,
hasCreate: defaultTrue,
canCreate: defaultTrue,
hasBulkDelete: defaultTrue,
hasBulkUpdate: defaultFalse,
hasLeftActions: defaultTrue,
hasMoreActions: defaultTrue,
tableUrl: {
type: String,
default: ''
@@ -53,23 +61,41 @@ export default {
type: String,
default: null
},
moreActionsButton: {
moreCreates: {
type: Object,
default: () => ({})
default: null
},
createTitle: {
type: String,
default: () => i18n.t('common.Create')
}
},
data() {
const defaultActions = [
{
name: 'actionCreate',
title: this.createTitle,
type: 'primary',
has: this.hasCreate && !this.moreCreates,
can: this.canCreate,
callback: this.handleCreate
}
]
if (this.moreCreates) {
const defaultMoreCreate = {
name: 'actionMoreCreate',
title: this.createTitle,
type: 'primary',
has: true,
can: this.canCreate,
dropdown: [],
callback: this.handleCreate
}
const createCreateAction = Object.assign(defaultMoreCreate, this.moreCreates)
defaultActions.push(createCreateAction)
}
return {
defaultActions: [
{
name: 'actionCreate',
title: this.$t('common.Create'),
type: 'primary',
has: this.hasCreate,
can: true,
callback: this.handleCreate
}
],
defaultActions: defaultActions,
defaultMoreActions: [
{
title: this.$t('common.deleteSelected'),
@@ -92,6 +118,9 @@ export default {
}
},
computed: {
iActions() {
return [...this.actions, this.moreAction]
},
actions() {
const actions = [...this.defaultActions, ...this.extraActions]
return cleanActions(actions, true, {
@@ -99,12 +128,20 @@ export default {
reloadTable: this.reloadTable
})
},
moreActions() {
const actions = [...this.defaultMoreActions, ...this.extraMoreActions]
return cleanActions(actions, true, {
moreAction() {
if (!this.hasMoreActions) {
return
}
let dropdown = [...this.defaultMoreActions, ...this.extraMoreActions]
dropdown = cleanActions(dropdown, true, {
selectedRows: this.selectedRows,
reloadTable: this.reloadTable
})
return {
name: 'moreActions',
title: this.moreActionsTitle || this.$t('common.MoreActions'),
dropdown: dropdown
}
},
hasSelectedRows() {
return this.selectedRows.length > 0

View File

@@ -1,7 +1,7 @@
<template>
<div>
<ActionsGroup :is-fa="true" :actions="rightSideActions" class="right-side-actions right-side-item" />
<ImExportDialog :selected-rows="selectedRows" :url="tableUrl" />
<ActionsGroup :is-fa="true" :actions="rightSideActions" :url="tableUrl" class="right-side-actions right-side-item" />
<ImExportDialog :selected-rows="selectedRows" :url="tableUrl" v-bind="$attrs" />
</div>
</template>
@@ -27,14 +27,21 @@ export default {
handleExport: {
type: Function,
default: function({ selectedRows }) {
this.$eventBus.$emit('showExportDialog', { selectedRows })
this.$eventBus.$emit('showExportDialog', { selectedRows, url: this.tableUrl })
}
},
hasImport: defaultTrue,
handleImport: {
type: Function,
default: function({ selectedRows }) {
this.$eventBus.$emit('showImportDialog', { selectedRows })
this.$eventBus.$emit('showImportDialog', { selectedRows, url: this.tableUrl })
}
},
hasColumnSetting: defaultTrue,
handleColumnConfig: {
type: Function,
default: function({ selectedRows }) {
this.$eventBus.$emit('showColumnSettingPopover', { url: this.tableUrl })
}
},
hasRefresh: defaultTrue,
@@ -54,6 +61,7 @@ export default {
data() {
return {
defaultRightSideActions: [
{ name: 'actionColumnSetting', fa: 'fa-cog', tip: this.$t('common.CustomCol'), has: this.hasColumnSetting, callback: this.handleColumnConfig.bind(this) },
{ name: 'actionExport', fa: 'fa-download', tip: this.$t('common.Export'), has: this.hasExport, callback: this.handleExport.bind(this) },
{ name: 'actionImport', fa: 'fa-upload', tip: this.$t('common.Import'), has: this.hasImport, callback: this.handleImport.bind(this) },
{ name: 'actionRefresh', fa: 'fa-refresh', tip: this.$t('common.Refresh'), has: this.hasRefresh, callback: this.handleRefresh }
@@ -119,7 +127,7 @@ export default {
}
.right-side-actions >>> .el-button:hover {
background-color: rgb(0, 0, 0, 0.05);
background-color: rgba(0, 0, 0, 0.05);
}
.action-search >>> .el-input__suffix i {

View File

@@ -20,7 +20,7 @@
<script>
import AutoDataSearch from '@/components/AutoDataSearch'
import LeftSide from './LeftSide'
import DatetimeRangePicker from '@/components/DatetimeRangePicker'
import DatetimeRangePicker from '@/components/FormFields/DatetimeRangePicker'
import RightSide from './RightSide'
const defaultTrue = { type: Boolean, default: true }

View File

@@ -5,7 +5,9 @@ export function cleanActions(actions, canDefaults, { selectedRows, reloadTable }
cloneActions.forEach((action) => {
action.has = cleanBoolean(action, 'has', true, { selectedRows, reloadTable })
action.can = cleanBoolean(action, 'can', true, { selectedRows, reloadTable })
action.callback = cleanCallback(action, { selectedRows, reloadTable })
if (!action.dropdown) {
action.callback = cleanCallback(action, { selectedRows, reloadTable })
}
cleanedActions.push(action)
})
return cleanedActions

View File

@@ -1,78 +0,0 @@
<template>
<ActionsGroup :size="'mini'" :actions="cleanedActions" :more-actions="cleanMoreActions" v-bind="$attrs" />
</template>
<script>
import ActionsGroup from '@/components/ActionsGroup/index'
import BaseFormatter from './base'
export default {
name: 'CustomActionsFormatterVue',
components: {
ActionsGroup
},
extends: BaseFormatter,
computed: {
cleanedActions() {
if (this.col.actions.actions instanceof Array) {
const copy = _.cloneDeep(this.col.actions.actions)
let actions = [...copy]
actions = actions.map((v) => {
v.has = this.checkBool(v, 'has')
v.can = this.checkBool(v, 'can')
v.callback = this.cleanCallback(v)
return v
})
return actions
}
return []
},
cleanMoreActions() {
if (this.col.actions.extraActions instanceof Array) {
const copy = _.cloneDeep(this.col.actions.extraActions)
let actions = [...copy]
actions = actions.map((v) => {
v.has = this.checkBool(v, 'has')
v.can = this.checkBool(v, 'can')
v.callback = this.cleanCallback(v)
return v
})
return actions
}
return []
}
},
mounted() {
console.log(this.col)
},
methods: {
checkBool(item, attr, defaults) {
if (!item) {
return false
}
let ok = item[attr]
if (ok && typeof ok === 'function') {
ok = ok(this.row, this.cellValue)
} else if (ok == null) {
ok = defaults === undefined ? true : defaults
}
return ok
},
cleanCallback(item) {
const callback = item.callback
const attrs = {
reload: this.reload,
row: this.row,
col: this.col,
cellValue: this.cellValue,
tableData: this.tableData
}
return () => { return callback.bind(this)(attrs) }
}
}
}
</script>
<style scoped>
</style>

View File

@@ -2,7 +2,7 @@
<div>
<TableAction :table-url="iTableConfig.url" :search-table="search" :date-pick="handleDateChange" v-bind="headerActions" :selected-rows="selectedRows" :reload-table="reloadTable" />
<IBox class="table-content">
<AutoDataTable ref="dataTable" :config="iTableConfig" @selection-change="handleSelectionChange" v-on="$listeners" />
<AutoDataTable ref="dataTable" :filter-table="filter" :config="iTableConfig" @selection-change="handleSelectionChange" v-on="$listeners" />
</IBox>
</div>
</template>
@@ -13,6 +13,7 @@ import IBox from '../IBox'
import TableAction from './TableAction'
import Emitter from '@/mixins/emitter'
import deepmerge from 'deepmerge'
export default {
name: 'ListTable',
components: {
@@ -46,6 +47,7 @@ export default {
},
iTableConfig() {
const config = deepmerge(this.tableConfig, { extraQuery: this.extraQuery })
this.$log.debug('Header actions', this.headerActions)
this.$log.debug('ListTable: iTableConfig change', config)
return config
}
@@ -56,10 +58,14 @@ export default {
this.$log.debug('ListTable: found extraQuery change')
},
deep: true
},
tableColConfig: {
handler() {
this.$log.debug('ListTable: found colConfig change')
},
deep: true
}
},
mounted() {
},
methods: {
handleSelectionChange(val) {
this.selectedRows = val
@@ -68,8 +74,13 @@ export default {
this.dataTable.getList()
},
search(attrs) {
this.$emit('TagSearch', attrs)
return this.dataTable.search(attrs, true)
},
filter(attrs) {
this.$emit('TagFilter', attrs)
this.$refs.dataTable.$refs.dataTable.search(attrs, true)
},
handleDateChange(attrs) {
this.$set(this.extraQuery, 'date_from', attrs[0].toISOString())
this.$set(this.extraQuery, 'date_to', attrs[1].toISOString())
@@ -81,6 +92,7 @@ export default {
date_from: attrs[0].toISOString(),
date_to: attrs[1].toISOString()
}
this.$emit('TagDateChange', attrs)
return this.dataTable.searchDate(query)
},
toggleRowSelection(row, isSelected) {
@@ -92,28 +104,28 @@ export default {
<style lang="scss" scoped>
.table-content {
margin-top: 10px;
.table-content {
margin-top: 10px;
& >>> .el-card__body {
padding: 0;
}
& >>> .el-table__header thead > tr > th {
background-color: white;
}
/*& >>> .el-table--striped .el-table__body tr.el-table__row--striped td {*/
/*background: white;*/
/*}*/
/*& >>> .el-table th, .el-table tr {*/
/*background-color: red;*/
/*!*background-color: #FAFAFA;*!*/
/*}*/
& >>> .el-card__body {
padding: 0;
}
& >>> .el-table__header thead > tr > th {
background-color: white;
}
//修改颜色
// .el-button--text{
// color: #409EFF;
// }
/*& >>> .el-table--striped .el-table__body tr.el-table__row--striped td {*/
/*background: white;*/
/*}*/
/*& >>> .el-table th, .el-table tr {*/
/*background-color: red;*/
/*!*background-color: #FAFAFA;*!*/
/*}*/
}
//修改颜色
// .el-button--text{
// color: #409EFF;
// }
</style>

View File

@@ -10,7 +10,7 @@
</template>
<script>
import Switcher from '../Swicher'
import Switcher from '../FormFields/Swicher'
export default {
name: 'ActionItem',
components: {

View File

@@ -1,29 +1,36 @@
<template>
<IBox :fa="icon" :type="type" :title="title" v-bind="$attrs">
<table style="width: 100%">
<table style="width: 100%;table-layout:fixed;" class="CardTable">
<tr>
<td colspan="2">
<Select2 ref="select2" v-model="select2.value" v-bind="select2" />
<Select2 ref="select2" v-model="select2.value" :disabled="iDisabled" v-bind="select2" />
</td>
</tr>
<slot />
<tr>
<td colspan="2">
<el-button :type="type" size="small" :loading="submitLoading" @click="addObjects">{{ $t('common.Add') }}</el-button>
<el-button :type="type" size="small" :loading="submitLoading" :disabled="iDisabled" @click="addObjects">
{{ $t('common.Add') }}
</el-button>
</td>
</tr>
<template v-if="showHasObjects">
<tr v-for="obj of iHasObjects" :key="obj.value" style="width: 100%" class="item">
<td><b>{{ obj.label }}</b></td>
<tr v-for="obj of iHasObjects" :key="obj.value" class="item">
<td style="width: 100%;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;">
<el-tooltip style="margin: 4px;" effect="dark" :content="obj.label" placement="left">
<b>{{ obj.label }}</b>
</el-tooltip>
</td>
<td>
<el-button size="mini" type="danger" style="float: right" @click="removeObject(obj)">
<el-button size="mini" :disabled="iDisabled" type="danger" style="float: right" @click="removeObject(obj)">
<i class="fa fa-minus" />
</el-button>
</td>
</tr>
</template>
<tr v-if="params.hasMore" class="item">
<tr v-if="params.hasMore && showHasMore" class="item">
<td colspan="2">
<el-button :type="type" size="small" style="width: 100%" @click="loadMore">
<el-button :type="type" :disabled="iDisabled" size="small" style="width: 100%" @click="loadMore">
<i class="fa fa-arrow-down" />
{{ $t('common.More') }}
</el-button>
@@ -34,9 +41,10 @@
</template>
<script>
import Select2 from '../Select2'
import Select2 from '../FormFields/Select2'
import IBox from '../IBox'
import { createSourceIdCache } from '@/api/common'
import { mapGetters } from 'vuex'
export default {
name: 'RelationCard',
components: {
@@ -82,6 +90,14 @@ export default {
type: [Array, Number, String],
default: () => []
},
disabled: {
type: [Boolean, Function],
default: null
},
showHasMore: {
type: Boolean,
default: true
},
performDelete: {
type: Function,
default: (obj, that) => {}
@@ -129,11 +145,13 @@ export default {
ajax: this.objectsAjax,
options: this.objects,
value: this.value,
disabled: this.disabled,
disabledValues: []
}
}
},
computed: {
...mapGetters(['currentOrgIsRoot']),
iAjax() {
return this.$refs.select2.iAjax
},
@@ -142,6 +160,12 @@ export default {
},
hasObjectLeftLength() {
return this.totalHasObjectsLength - this.iHasObjects.length
},
iDisabled() {
if (this.disabled !== null) {
return this.disabled
}
return this.currentOrgIsRoot
}
},
watch: {

View File

@@ -3,7 +3,7 @@
</template>
<script>
import ActionsGroup from '@/components/ActionsGroup/index'
import ActionsGroup from '@/components/ActionsGroup'
import BaseFormatter from './base'
const defaultPerformDelete = function({ row, col }) {
@@ -24,6 +24,19 @@ const defaultUpdateCallback = function({ row, col }) {
this.$router.push(route)
}
const defaultCloneCallback = function({ row, col }) {
const id = row.id
let route = { query: { clone_from: id }}
const cloneRoute = this.colActions.cloneRoute
if (typeof cloneRoute === 'object') {
route = Object.assign(route, cloneRoute)
} else {
route.name = cloneRoute
}
this.$router.push(route)
}
const defaultDeleteCallback = function({ row, col, cellValue, reload }) {
let msg = this.$t('common.deleteWarningMsg')
const name = row.name || row.hostname
@@ -68,13 +81,21 @@ export default {
default: function() {
return {
hasUpdate: true, // can set function(row, value)
canUpdate: true, // can set function(row, value)
canUpdate: () => {
return !this.$store.getters.currentOrgIsRoot
}, // can set function(row, value)
hasDelete: true, // can set function(row, value)
canDelete: true,
hasClone: true,
canClone: () => {
return !this.$store.getters.currentOrgIsRoot
},
updateRoute: this.$route.name.replace('List', 'Update'),
cloneRoute: this.$route.name.replace('List', 'Create'),
performDelete: defaultPerformDelete,
onUpdate: defaultUpdateCallback,
onDelete: defaultDeleteCallback,
onClone: defaultCloneCallback,
extraActions: [] // format see defaultActions
}
}
@@ -89,7 +110,8 @@ export default {
type: 'primary',
has: colActions.hasUpdate,
can: colActions.canUpdate,
callback: colActions.onUpdate
callback: colActions.onUpdate,
order: 10
},
{
name: 'delete',
@@ -97,7 +119,17 @@ export default {
type: 'danger',
has: colActions.hasDelete,
can: colActions.canDelete,
callback: colActions.onDelete
callback: colActions.onDelete,
order: 20
},
{
name: 'clone',
title: this.$t('common.Clone'),
type: 'info',
has: colActions.hasClone,
can: colActions.canClone,
callback: colActions.onClone,
order: 30
}
]
return {
@@ -115,9 +147,11 @@ export default {
v.has = this.cleanBoolean(v, 'has')
v.can = this.cleanBoolean(v, 'can')
v.callback = this.cleanCallback(v)
v.order = v.order || 100
return v
})
actions = actions.filter((v) => v.has)
actions.sort((a, b) => a.order - b.order)
return actions
},
actions() {

View File

@@ -0,0 +1,15 @@
<template>
<span>{{ cellValue.toString() }}</span>
</template>
<script>
import BaseFormatter from './base'
export default {
name: 'ArrayFormatter',
extends: BaseFormatter
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,42 @@
<template>
<i :class="'fa ' + iconClass" />
</template>
<script>
import BaseFormatter from './base'
export default {
name: 'ChoicesFormatter',
extends: BaseFormatter,
props: {
formatterArgsDefault: {
type: Object,
default() {
return {
iconChoices: {
true: 'fa-check text-primary',
false: 'fa-times text-danger'
},
typeChange(val) {
return !!val
}
}
}
}
},
data() {
return {
formatterArgs: Object.assign(this.formatterArgsDefault, this.col.formatterArgs)
}
},
computed: {
iconClass() {
const key = this.formatterArgs.typeChange(this.cellValue)
return this.formatterArgs.iconChoices[key]
}
}
}
</script>
<style scoped>
</style>

View File

@@ -28,6 +28,9 @@ export default {
},
hasTips: false,
tipStatus(val, vm) {
if (!val) {
return vm.$t('assets.Unknown')
}
if (val.status === 0) {
return vm.$t('assets.Unreachable')
} else if (val.status === 1) {
@@ -55,6 +58,9 @@ export default {
return this.formatterArgs.tipStatus(this.cellValue, vm)
},
tipTime() {
if (!this.cellValue) {
return ''
}
return toSafeLocalDateStr(this.cellValue.datetime)
}
}

View File

@@ -1,5 +1,5 @@
<template>
<el-button ref="deleteButton" size="mini" type="danger" :disabled="canDelete" @click="onDelete(col, row, cellValue, reload)">
<el-button ref="deleteButton" size="mini" type="danger" :disabled="iDisabled" @click="onDelete(col, row, cellValue, reload)">
<i class="fa fa-minus" />
</el-button>
</template>
@@ -11,8 +11,9 @@ export default {
name: 'DeleteActionFormatter',
extends: BaseFormatter,
computed: {
canDelete() {
return this.iCanDelete()
iDisabled() {
//
return (this.disabled() || this.$store.getters.currentOrgIsRoot)
}
},
methods: {
@@ -22,7 +23,7 @@ export default {
this.$message.success(this.$t('common.deleteSuccessMsg'))
reload()
}).catch(error => {
this.$message.error(this.$t('common.deleteErrorMsg' + ' ' + error))
this.$message.error(this.$t('common.deleteErrorMsg') + ' ' + error)
})
},
onDelete(col, row, cellValue, reload) {
@@ -32,7 +33,7 @@ export default {
this.defaultOnDelete(col, row, cellValue, reload)
}
},
iCanDelete() {
disabled() {
if (this.col.objects === 'all') {
return false
}

View File

@@ -0,0 +1,84 @@
<template>
<div style="width: 100%;min-height: 20px" @click.stop="editCell">
<el-input
v-if="inEditMode"
v-model="value"
size="mini"
class="editInput"
@keyup.enter.native="onInputEnter"
@blur="onInputEnter"
/>
<template v-else>
<span>{{ cellValue }}</span>
</template>
</div>
</template>
<script>
import BaseFormatter from './base'
export default {
name: 'EditableInputFormatter',
components: {
},
extends: BaseFormatter,
props: {
formatterArgsDefault: {
type: Object,
default() {
return {
trigger: 'click',
onEnter: ({ row, col, oldValue, newValue }) => {
const prop = col.prop
this.$log.debug(`Set value ${oldValue} => ${newValue}`)
this.$set(row, prop, newValue)
}
}
}
}
},
data() {
const valueIsString = typeof this.cellValue === 'string'
const jsonValue = this.cellValue ? JSON.stringify(this.cellValue) : ''
return {
inEditMode: false,
value: valueIsString ? this.cellValue || '' : jsonValue,
valueIsString: valueIsString,
formatterArgs: Object.assign(this.formatterArgsDefault, this.col.formatterArgs)
}
},
methods: {
editCell() {
this.inEditMode = true
},
onInputEnter() {
let validValue = this.value
try {
validValue = JSON.parse(validValue)
} catch (e) {
// pass
}
this.formatterArgs.onEnter({
row: this.row, col: this.col,
oldValue: this.cellValue,
newValue: validValue
})
this.inEditMode = false
},
cancelEdit() {
this.inEditMode = false
}
}
}
</script>
<style scoped>
.editInput >>> .el-input__inner {
padding: 2px;
line-height: 12px;
}
.editInput {
padding: -6px;
}
</style>

View File

@@ -3,7 +3,7 @@
</template>
<script>
import ActionsGroup from '@/components/ActionsGroup/index'
import ActionsGroup from '@/components/ActionsGroup'
import BaseFormatter from './base'
export default {

View File

@@ -0,0 +1,68 @@
<template>
<div>
<el-tooltip v-if="formatterArgs.hasTips" placement="bottom" effect="dark">
<div slot="content">
<template v-if="tipsIsArray">
<div v-for="tip of tips" :key="tip">
<span>{{ tip }}</span>
<br>
</div>
</template>
<span v-else>
{{ tips }}
</span>
</div>
<i :class="'fa ' + iconClass" />
</el-tooltip>
<i v-else :class="'fa ' + iconClass" />
</div>
</template>
<script>
import BaseFormatter from './base'
export default {
name: 'StatusFormatter',
extends: BaseFormatter,
props: {
formatterArgsDefault: {
type: Object,
default() {
return {
iconChoices: {
true: 'fa-check text-primary',
false: 'fa-times text-danger'
},
getChoicesKey(val) {
return !!val
},
getTip(val, col) {
},
hasTips: false
}
}
}
},
data() {
return {
formatterArgs: Object.assign(this.formatterArgsDefault, this.col.formatterArgs)
}
},
computed: {
iconClass() {
const key = this.formatterArgs.getChoicesKey(this.cellValue)
return this.formatterArgs.iconChoices[key] + ' ' + key + 'Status'
},
tips() {
const vm = this
return this.formatterArgs.getTip(this.cellValue, vm)
},
tipsIsArray() {
return Array.isArray(this.tips)
}
}
}
</script>
<style scoped>
</style>

View File

@@ -1,39 +1,48 @@
import DetailFormatter from './DetailFormatter'
import ArrayFormatter from './ArrayFormatter'
import DisplayFormatter from './DisplayFormatter'
import BooleanFormatter from './ChoicesFormatter'
import BooleanFormatter from './BooleanFormatter'
import ChoicesFormatter from './ChoicesFormatter'
import ActionsFormatter from './ActionsFormatter'
import CustomActionsFormatter from './CustomActionsFormatter'
import DeleteActionFormatter from './DeleteActionFormatter'
import DateFormatter from './DateFormatter'
import SystemUserFormatter from './GrantedSystemUsersShowFormatter'
import ShowKeyFormatter from '@/components/ListTable/formatters/ShowKeyFormatter'
import ShowKeyFormatter from '@/components/TableFormatters/ShowKeyFormatter'
import DialogDetailFormatter from './DialogDetailFormatter'
import LoadingActionsFormatter from './LoadingActionsFormatter'
import EditableInputFormatter from './EditableInputFormatter'
import StatusFormatter from './StatusFormatter'
export default {
DetailFormatter,
DisplayFormatter,
BooleanFormatter,
ChoicesFormatter,
ActionsFormatter,
CustomActionsFormatter,
DeleteActionFormatter,
DateFormatter,
SystemUserFormatter,
ShowKeyFormatter,
DialogDetailFormatter,
LoadingActionsFormatter
LoadingActionsFormatter,
ArrayFormatter,
EditableInputFormatter,
StatusFormatter
}
export {
DetailFormatter,
DisplayFormatter,
BooleanFormatter,
ChoicesFormatter,
ActionsFormatter,
CustomActionsFormatter,
DeleteActionFormatter,
DateFormatter,
SystemUserFormatter,
ShowKeyFormatter,
DialogDetailFormatter,
LoadingActionsFormatter
LoadingActionsFormatter,
ArrayFormatter,
EditableInputFormatter,
StatusFormatter
}

View File

@@ -5,8 +5,10 @@
<component
:is="component"
ref="AutoDataZTree"
:key="componentTreeKey"
:setting="treeSetting"
class="auto-data-ztree"
v-on="$listeners"
@urlChange="handleUrlChange"
>
<div slot="rMenu" slot-scope="{data}">
@@ -22,7 +24,7 @@
</div>
<div class="transition-box" style="width: calc(100% - 17px);">
<slot name="table">
<ListTable ref="ListTable" :key="componentKey" :table-config="iTableConfig" :header-actions="headerActions" />
<ListTable ref="ListTable" :key="componentKey" :table-config="iTableConfig" :header-actions="headerActions" v-on="$listeners" />
</slot>
</div>
</div>
@@ -63,10 +65,16 @@ export default {
return {
iTableConfig: this.tableConfig,
iShowTree: this.showTree,
componentKey: 0
componentKey: 0,
componentTreeKey: 0
}
},
watch: {
treeConfig: {
handler(val) {
},
deep: true
}
},
methods: {
handleUrlChange(_url) {
@@ -77,11 +85,20 @@ export default {
forceRerender() {
this.componentKey += 1
},
forceRerenderTree() {
this.componentTreeKey += 1
},
hideRMenu() {
this.$refs.AutoDataZTree.hideRMenu()
},
getSelectedNodes: function() {
return this.$refs.AutoDataZTree.getSelectedNodes()
},
getNodes: function() {
return this.$refs.AutoDataZTree.getNodes()
},
selectNode: function(node) {
return this.$refs.AutoDataZTree.selectNode(node)
}
}
}

View File

@@ -12,14 +12,15 @@ export { default as FormGroupHeader } from './FormGroupHeader'
export { default as Hamburger } from './Hamburger'
export { default as ListTable } from './ListTable'
export { default as RelationCard } from './RelationCard'
export { default as Select2 } from './Select2'
export { default as Select2 } from './FormFields/Select2'
export { default as UploadKey } from './FormFields/UploadKey.vue'
export { default as AssetSelect } from './AssetSelect'
export { default as SvgIcon } from './SvgIcon'
export { default as TreeTable } from './TreeTable'
export { default as IBox } from './IBox'
export { default as QuickActions } from './QuickActions'
export { default as Switcher } from './Swicher'
export { default as Switcher } from './FormFields/Swicher'
export { default as SummaryCard } from './SummaryCard'
export { default as UploadField } from './UploadField'
export { default as UploadField } from './FormFields/UploadField'
export { default as AssetUserTable } from './AssetUserTable'
export { default as AssetRelationCard } from './AssetRelationCard'

View File

@@ -1,9 +1,51 @@
{
"": "",
"acl": {
"name": "名称",
"username": "用户名",
"ip_group": "IP 组",
"action": "动作",
"priority": "优先级",
"date_created": "创建时间",
"created_by": "创建者",
"asset": "资产信息",
"users":"用户信息",
"system_user": "系统用户",
"username_group":"用户名",
"hostname_group":"资产名",
"asset_ip_group": "资产IP",
"system_users_name_group": "系统用户名称",
"system_users_protocol_group": "系统用户协议",
"system_users_username_group": "系统用户名",
"apply_login_asset": "申请登录资产",
"apply_login_system_user": "申请登录系统用户",
"apply_login_user": "申请登录用户",
"RuleDetail": "规则详情"
},
"applications": {
"": "",
"RemoteApp": "远程应用",
"Database": "数据库",
"Cloud": "云",
"applicationsType": {
"chrome": "Chrome",
"mysql_workbench": "MySQL Workbench",
"vmware_client":"Vmware Client",
"custom":"Custom",
"mysql": "MySQL",
"oracle": "Oracle",
"postgresql": "PostgreSQL",
"mariadb": "MariaDB",
"k8s": "Kubernetes"
},
"applicationsCategory": {
"remote_app": "远程应用",
"db": "数据库应用",
"cloud": "云应用"
},
"appPath": "应用路径",
"appType": "应用类型",
"appName": "应用名称",
"asset": "资产",
"database": "数据库",
"host": "主机",
@@ -32,7 +74,8 @@
"Custom": "自定义",
"cluster": "集群",
"kubernetes":"Kubernetes",
"clusterHelpTextMessage": "例如https://172.16.8.8"
"clusterHelpTextMessage": "例如https://172.16.8.8:8443",
"DBInfo": "数据库信息"
},
"assets": {
"Action": "动作",
@@ -41,13 +84,19 @@
"AdminUserDetail": "管理用户详情",
"AdminUserListHelpMessage": "管理用户是资产(被控服务器)上的 root或拥有 NOPASSWD: ALL sudo 权限的用户, JumpServer 使用该用户来 `推送系统用户`、`获取资产硬件信息` 等。\n",
"Asset": "资产",
"HardwareInfo": "硬件信息",
"AssetDetail": "资产详情",
"AssetList": "资产列表",
"ReplaceNodeAssetsAdminUser":"替换节点资产的管理员",
"AssetListHelpMessage": "左侧是资产树,右击可以新建、删除、更改树节点,授权资产也是以节点方式组织的,右侧是属于该节点下的资产\n",
"TestGatewayTestConnection":"测试连接网关",
"TestGatewayHelpMessage": "如果使用了nat端口映射请设置为ssh真实监听的端口",
"SshPort": "SSH 端口",
"AssetNumber": "资产编号",
"AssetUserList": "资产用户列表",
"Assets": "资产",
"Auth": "认证",
"AccountList": "账号列表",
"AutoGenerateKey": "自动生成密钥",
"AutoPush": "自动推送",
"BasePlatform": "基础平台",
@@ -67,6 +116,8 @@
"DateUpdated": "更新日期",
"DeactiveSelected": "禁用所选",
"Disk": "硬盘",
"AdDomain": "AD域名",
"AdDomainHelpText": "提供给域用户登录的AD域名",
"Domain": "网域",
"DomainDetail": "网域详情",
"DomainHelpMessage": "网域功能是为了解决部分环境混合云无法直接连接而新增的功能原理是通过网关服务器进行跳转登录。JMS => 网域网关 => 目标资产",
@@ -91,6 +142,7 @@
"OnlyLatestVersion": "仅最新版本",
"Os": "操作系统",
"Other": "其它",
"Hardware": "硬件信息",
"Password": "密码",
"PasswordWithoutSpecialCharHelpText": "不能包含特殊字符",
"Pending": "等待",
@@ -110,6 +162,7 @@
"RefreshHardware": "更新硬件信息",
"RemoteAppListHelpMessage": "使用此功能前,请确保已将应用加载器上传到应用服务器并成功发布为一个 RemoteApp 应用 <b><a href='https://github.com/jumpserver/Jmservisor/releases'>下载应用加载器</a></b>",
"RemoteApps": "远程应用",
"Applications": "应用",
"RemoteType": "应用类型",
"RemoveFromCurrentNode": "从节点移除",
"ReplaceNodeAssetsAdminUserWithThis": "替换资产的管理员",
@@ -126,7 +179,7 @@
"TestAssetsConnective": "测试资产可连接性",
"TestConnection": "测试连接",
"Type": "类型",
"UnselectedAssets": "未选择资产",
"UnselectedAssets": "未选择资产或所选择的资产不支持SSH协议连接",
"UnselectedNodes": "未选择节点",
"UpdateAssetUserToken": "更新资产用户认证信息",
"Username": "用户名",
@@ -136,12 +189,17 @@
"Version": "版本",
"command_filter_list": "命令过滤器列表",
"date_joined": "创建日期",
"sshKeyFingerprint": "SSH 指纹",
"ip": "IP",
"sshkey": "sshkey",
"GroupsHelpMessage": "请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)",
"HomeHelpMessage": "默认家目录 /home/系统用户名: /home/username",
"Home": "家目录",
"LinuxUserAffiliateGroup": "用户附属组"
"LinuxUserAffiliateGroup": "用户附属组",
"ipDomain": "IP(域名)",
"HostProtocol": "主机协议",
"DatabaseProtocol": "数据库协议",
"OtherProtocol": "其它协议"
},
"audits": {
@@ -158,15 +216,27 @@
"Action": "动作",
"RequestTickets": "申请工单",
"Actions": "操作",
"CustomCol":"自定义列表字段",
"Activate": "激活",
"NeedSpecifiedFile": "需上传指定格式文件",
"TestPortErrorMsg":"端口错误,请重新输入",
"Active": "激活中",
"actionsTips":"剪切板权限控制目前仅支持 RDP/VNC 协议的连接",
"TableColSettingInfo": "请选择您想显示的列表详细信息。",
"Add": "添加",
"UpdateAssetDetail": "配置更多信息",
"AddSuccessMsg": "添加成功",
"Auth": "认证",
"bind": "绑定",
"unbind": "解绑",
"PushSelected":"推送所选",
"BadRequestErrorMsg": "请求错误,请检查填写内容",
"BadRoleErrorMsg": "请求错误,无该操作权限",
"BadConflictErrorMsg": "正在刷新中,请稍后再试",
"Basic": "基本",
"PleaseAgreeToTheTerms": "请同意条款",
"BasicInfo": "基本信息",
"ApplyInfo": "申请信息",
"Cancel": "取消",
"Close": "关闭",
"Command filter": "命令过滤器",
@@ -176,6 +246,7 @@
"CreatedBy": "创建者",
"CrontabHelpTips": "eg每周日 03:05 执行 <5 3 * * 0> <br> 提示: 使用5位 Linux crontab 表达式 <分 时 日 月 星期> <a href='https://tool.lu/crontab/' target='_blank'>在线工具</a> <br>注意: 如果同时设置了定期执行和周期执行,优先使用定期执行",
"DateEnd": "结束日期",
"Resource": "资源",
"DateLast24Hours": "最近一天",
"DateLast3Months": "最近三月",
"DateLastMonth": "最近一月",
@@ -189,10 +260,16 @@
"EnterForSearch": "按回车进行搜索",
"Export": "导出",
"Import": "导入",
"ContinueImport": "继续导入",
"Continue": "继续",
"Stop": "停止",
"Finished": "完成",
"Refresh": "刷新",
"Info": "提示",
"MFAConfirm": "MFA 认证",
"MFARequireForSecurity": "为了安全请输入MFA",
"PasswordConfirm": "密码认证",
"PasswordRequireForSecurity": "为了安全请输入密码",
"Members": "成员",
"More": "更多",
"Message": "消息",
@@ -213,11 +290,13 @@
"SelectFile": "选择文件",
"Show": "显示",
"Submit": "提交",
"SaveAndAddAnother": "保存并继续添加",
"Test": "测试",
"TestSuccessMsg": "测试成功",
"To": "至",
"Update": "更新",
"Upload": "上传",
"Clone": "克隆",
"Username": "用户名",
"Validity": "有效",
"Invalidity": "无效",
@@ -231,8 +310,10 @@
"NeedAssetsAndSystemUserErrMsg": "请先选择授权的系统用户和资产",
"bulkRemoveSuccessMsg": "批量移除成功",
"createBy": "创建者",
"cloneFrom": "克隆自",
"createErrorMsg": "创建失败",
"createSuccessMsg": "创建成功",
"saveSuccessContinueMsg": "创建成功,更新内容后可以继续添加",
"createdBy": "创建人",
"dateCreated": "创建日期",
"dateExpired": "失效日期",
@@ -247,17 +328,28 @@
"fieldRequiredError": "这个字段是必填项",
"getErrorMsg": "获取失败",
"MFAErrorMsg": "MFA错误请检查",
"Total": "总共",
"Success": "成功",
"Failed": "失败",
"Pending": "等待",
"Status": "状态",
"InputEmailAddress": "请输入正确的邮箱地址",
"Receivers": "接收人",
"imExport": {
"ExportAll": "导出所有",
"ExportOnlyFiltered": "仅导出搜索结果",
"ExportOnlySelectedItems": "仅导出选择项",
"ExportRange": "导出范围",
"createSuccessMsg": "导入创建成功,总共:{count}",
"downloadImportTemplateMsg": "下载导入模板",
"downloadImportTemplateMsg": "下载创建模板",
"downloadUpdateTemplateMsg": "下载更新模板",
"onlyCSVFilesTips": "仅支持csv文件导入",
"updateSuccessMsg": "导入更新成功,总共:{count}"
"updateSuccessMsg": "导入更新成功,总共:{count}",
"uploadCsvLth10MHelpText": "只能上传 csv/xlsx, 且不超过 10M",
"dragUploadFileInfo": "将文件拖到此处,或点击上传",
"hasImportErrorItemMsg": "存在导入失败项,点击左侧 x 查看失败原因,点击表格编辑后,可以继续导入失败项"
},
"fileType": "文件类型",
"isValid": "有效",
"nav": {
"APIKey": "API Key",
@@ -292,7 +384,8 @@
"NUMBER_REQUIRED": "须包含数字",
"SPECIAL_CHAR_REQUIRED": "须包含特殊字符",
"MIN_LENGTH_ERROR": "密码最小长度 {0} 位"
}
},
"lastCannotBeDeleteMsg": "最后一项,不能被删除"
},
"dashboard": {
"ActiveAsset": "近期被登录过",
@@ -372,7 +465,7 @@
},
"perms": {
"": "",
"Actions": "动作",
"Actions": "权限",
"Asset": "资产",
"Basic": "基本",
"Exclude": "不包含",
@@ -383,6 +476,11 @@
"SystemUser": "系统用户",
"User": "用户",
"UserGroups": "用户组",
"PermName":"授权名称",
"DatabaseAppPermission": "数据库授权",
"RemoteAppPermission": "远程应用授权",
"addApplicationToThisPermission": "添加应用",
"KubernetesAppPermission": "Kubernetes授权",
"addAssetToThisPermission": "添加资产",
"addDatabaseAppToThisPermission": "添加数据库应用",
"addNodeToThisPermission": "添加节点",
@@ -401,12 +499,15 @@
"downloadFile": "下载文件",
"hostName": "主机名",
"isValid": "有效",
"isEffective": "起作用的",
"nodeCount": "节点数量",
"refreshFail": "刷新失败",
"refreshPermissionCache": "刷新授权缓存",
"refreshSuccess": "刷新成功",
"remoteApp": "远程应用",
"remoteAppCount": "远程应用数量",
"appsCount": "应用数量",
"appsList":"应用列表",
"DatabaseAppCount": "数据库应用数量",
"KubernetesAppCount": "Kubernetes应用数量",
"systemUserCount": "系统用户数量",
@@ -421,6 +522,8 @@
},
"route": {
"": "",
"Ticket":"工单",
"CommandConfirm": "命令复核",
"AdminUserCreate": "创建管理用户",
"AdminUserDetail": "管理用户详情",
"AdminUserList": "管理用户",
@@ -451,7 +554,7 @@
"CreateCommandStorage": "创建命令存储",
"CreateReplayStorage": "创建录像存储",
"Dashboard": "仪表盘",
"DatabaseApp": "数据库应用",
"DatabaseApp": "数据库",
"DatabaseAppCreate": "创建数据库应用",
"DatabaseAppDetail": "数据库详情",
"DatabaseAppPermission": "数据库授权",
@@ -467,7 +570,16 @@
"KubernetesAppPermissionDetail": "Kubernetes授权详情",
"KubernetesAppPermissionUpdate": "更新Kubernetes授权规则",
"KubernetesAppUpdate": "更新Kubernetes",
"Acl": "访问控制",
"UserAclList": "用户登录",
"UserAclCreate": "创建用户登录规则",
"UserAclLists": "用户登录规则",
"UserAclUpdate": "更新用户登录规则",
"UserAclDetail": "用户登录规则详情",
"AssetAclList": "登录资产",
"AssetAclCreate": "创建资产登录规则",
"AssetAclUpdate": "更新资产登录规则",
"AssetAclDetail": "资产登录规则详情",
"DomainCreate": "创建网域",
"DomainDetail": "网域详情",
"DomainList": "网域列表",
@@ -497,6 +609,11 @@
"RemoteAppPermissionCreate": "创建远程应用授权规则",
"RemoteAppPermissionDetail": "远程应用授权详情",
"RemoteAppPermissionUpdate": "更新远程应用授权规则",
"ApplicationDetail": "应用详情",
"ApplicationPermission": "应用授权",
"ApplicationPermissionCreate": "创建应用授权规则",
"ApplicationPermissionDetail": "应用授权详情",
"ApplicationPermissionUpdate": "更新应用授权规则",
"RemoteAppUpdate": "更新远程应用",
"ReplayStorageUpdate": "更新录像存储",
"SessionDetail": "会话详情",
@@ -528,7 +645,8 @@
"UserUpdate": "更新用户",
"Users": "用户管理",
"WebFTP": "文件管理",
"WebTerminal": "Web终端"
"WebTerminal": "Web终端",
"Notifications": "通知"
},
"sessions": {
"StorageConfiguration": "存储配置",
@@ -537,6 +655,7 @@
"active": "激活中",
"alive": "在线",
"asset": "资产",
"target": "目标",
"bucket": "桶名称",
"command": "命令",
"commandStorage": "命令存储",
@@ -564,6 +683,11 @@
"name": "名称",
"protocol": "协议",
"region": "地域",
"EsDisabled": "节点不可用, 请联系管理员",
"sessionActiveCount": "在线会话数量",
"systemCpuLoad": "CPU负载",
"systemDiskUsedPercent": "硬盘使用率",
"systemMemoryUsedPercent": "内存使用率",
"remoteAddr": "远端地址",
"replay": "回放",
"replaySession": "回放会话",
@@ -588,9 +712,11 @@
"sessionMonitor": "监控",
"TerminateTaskSendSuccessMsg": "终断任务已下发,请稍后刷新查看",
"helpText": {
"esUrl": "提示:如果有多台主机,请使用逗号 ( , ) 进行分割。eg: http://www.jumpserver.a.com,http://www.jumpserver.b.com",
"esUrl": "提示:如果有多台主机,请使用逗号 ( , ) 进行分割。eg: http://www.jumpserver.a.com:3000,http://www.jumpserver.b.com:3000",
"esIndex": "es提供默认indexjumpserver",
"esDocType": "es默认文档类型command"
"esDocType": "es默认文档类型command",
"s3Endpoint": "S3 格式: http://s3.{REGION_NAME}.amazonaws.com<br>S3(China) 格式: http://s3.{REGION_NAME}.amazonaws.com.cn<br>如: http://s3.cn-north-1.amazonaws.com.cn",
"ossEndpoint": "OSS 格式: http://{REGION_NAME}.aliyuncs.com<br>如: http://oss-cn-hangzhou.aliyuncs.com"
}
},
"setting": {
@@ -612,6 +738,8 @@
"PasswordCheckRule": "密码校验规则",
"Security": "安全设置",
"SecuritySetting": "安全设置",
"SystemMessageSubscription": "系统消息订阅",
"insecureCommandEmailUpdate": "点我设置",
"SubscriptionID": "订阅授权ID",
"Terminal": "终端设置",
"all": "全部",
@@ -642,6 +770,11 @@
"emailTest": "测试连接",
"emailUserSSL": "使用SSL",
"emailUserTLS": "使用TLS",
"InsecureCommandAlert": "危险命令告警",
"SecurityInsecureCommandEmailReceiver": "告警接收邮件",
"MailSend": "邮件发送",
"LDAPServerInfo": "LDAP 服务器",
"LDAPUser": "LDAP 用户",
"helpText": {
"ApiKeyList": "使用api key签名请求头每个请求的头部是不一样的, 请查阅使用文档",
"authLdapSearchFilter": "可能的选项是(cn或uid或sAMAccountName=%(user)s)",
@@ -651,7 +784,7 @@
"emailCustomUserCreatedHonorific": "提示: 创建用户时,发送设置密码邮件的敬语 (例如: 您好)",
"emailCustomUserCreatedSignature": "提示: 邮件的署名 (例如: jumpserver)",
"emailCustomUserCreatedSubject": "提示: 创建用户时,发送设置密码邮件的主题 (例如: 创建用户成功)",
"emailEmailFrom": "提示发送邮件账号默认使用SMTP账号作为发送账号",
"emailEmailFrom": "",
"emailHostPassword": "提示一些邮件提供商需要输入的是Token",
"emailRecipient": "提示:仅用来作为测试邮件收件人",
"emailSubjectPrefix": "提示: 一些关键字可能会被邮件提供商拦截,如 跳板机、JumpServer",
@@ -662,7 +795,8 @@
"terminalHeartbeatInterval": "单位: 秒",
"terminalSessionKeepDuration": "单位:天。 会话、录像、命令记录超过该时长将会被删除(仅影响数据库存储, oss等不受影响)",
"terminalTelnetRegex": "登录telnet服务器成功后的提示正则表达式如: Last\\s*login|success|成功",
"userGuideUrl": "用户第一次登录修改profile后重定向到地址"
"userGuideUrl": "用户第一次登录修改profile后重定向到地址",
"SecurityInsecureCommandEmailReceiver": "多个邮箱时,以半角逗号','分隔"
},
"helpTip": {
"emailUserSSL": "如果SMTP端口是465通常需要启用SSL",
@@ -673,7 +807,8 @@
"securityPasswordNumber": "开启后,用户密码修改、重置必须包含数字字符",
"securityPasswordSpecialChar": "开启后,用户密码修改、重置必须包含特殊字符",
"securityPasswordUpperCase": "开启后,用户密码修改、重置必须包含大写字母",
"securityServiceAccountRegistration": "允许使用bootstrap token注册终端, 当终端注册成功后可以禁止"
"securityServiceAccountRegistration": "允许使用bootstrap token注册终端, 当终端注册成功后可以禁止",
"SecurityInsecureCommand": "开启后,当资产上有危险命令执行时,会发送邮件告警通知"
},
"validatorMessage": {
"EnsureThisValueIsGreaterThanOrEqualTo3": "请确保该值大于或者等于 3",
@@ -715,17 +850,25 @@
"userGuideUrl": "用户向导URL",
"username": "用户名",
"usernamePlaceholder": "请输入用户名",
"refreshLdapCache":"刷新Ldap缓存请稍后"
},
"settings": {
"refreshLdapCache":"刷新Ldap缓存请稍后",
"LicenseExpired": "许可证已经过期",
"LicenseWillBe": "许可证即将在 ",
"Expire": " 过期",
"WeCom": "企业微信",
"DingTalk": "钉钉",
"dingTalkTest": "测试",
"weComTest": "测试",
"setting": "设置"
},
"tickets": {
"PermissionName": "授权规则名称",
"Accept": "同意",
"AssignedMe": "待我审批",
"Assignee": "处理人",
"Assignees": "待处理人",
"Close": "关闭",
"OpenStatus":"开启",
"CloseStatus":"关闭",
"Comment": "备注",
"MyTickets": "我发起的",
"RequestPerm":"授权申请",
@@ -734,10 +877,14 @@
"reply": "回复",
"status": "状态",
"title": "标题",
"action": "动作",
"IPGroup": "IP 组",
"type": "类型",
"user": "用户",
"Status": "状态",
"Open": "待处理",
"OrgName":"组织名称",
"AssignedInfo":"审批信息",
"OpenTicket": "创建工单",
"HandleTicket": "处理工单",
"FinishedTicket": "完成工单",
@@ -746,11 +893,23 @@
"Asset": "资产",
"SystemUser": "系统用户",
"RequestAssetPerm": "申请资产授权",
"RequestApplicationPerm": "申请应用授权",
"Applicant": "申请人",
"Pending": "待处理",
"Approved": "已同意",
"Rejected": "已拒绝",
"Closed": "已完成"
"Closed": "已完成",
"helpText": {
"ips": "请输入逗号分割的IP地址组",
"fuzzySearch": "支持模糊搜索",
"application": "请输入逗号分割的应用名称组"
},
"ApplyRunUser": "申请运行的用户",
"ApplyRunAsset": "申请运行的资产",
"ApplyRunSystemUser": "申请运行的系统用户",
"ApplyRunCommand": "申请运行的命令",
"ApplyFromSession": "会话",
"ApplyFromCMDFilterRule": "命令过滤规则"
},
"tree": {
"AddAssetToNode": "添加资产到节点",
@@ -760,6 +919,7 @@
"RenameNode": "重命名节点",
"ShowAssetAllChildrenNode": "显示所有子节点资产",
"ShowAssetOnlyCurrentNode": "仅显示当前节点资产",
"CheckAssetsAmount": "校对资产数量",
"ShowNodeInfo": "显示节点详情",
"TestNodeAssetConnectivity": "测试资产节点可连接性",
"UpdateNodeAssetHardwareInfo": "更新节点资产硬件信息"
@@ -781,7 +941,12 @@
"OrgUser": "组织用户",
"OrgAdmin": "组织管理员",
"OrgAuditor": "组织审计员",
"InviteUser": "邀请用户",
"Invite": "邀请",
"InviteUserInOrg": "邀请用户加入此组织",
"Guide": "向导",
"setWeCom": "设置企业微信认证",
"setDingTalk": "设置钉钉认证",
"HelpText": {
"MFAOfUserFirstLoginPersonalInformationImprovementPage": "启用多因子认证,使账号更加安全。<br/> 启用之后您将会在下次登录时进入多因子认证绑定流程;您也可以在(个人信息->快速修改->更改多因子设置)中直接绑定!",
"MFAOfUserFirstLoginUserGuidePage": "为了保护您和公司的安全,请妥善保管您的账户、密码和密钥等重要敏感信息;(如:设置复杂密码,并启用多因子认证)",
@@ -794,6 +959,7 @@
"LoginConfirm": "登录复核",
"LoginPasswordSetting": "登录密码设置",
"MFA": "MFA",
"Existing":"已存在",
"MfaLevel": "多因子认证",
"Name": "姓名",
"NewPassword": "新密码",
@@ -828,7 +994,14 @@
"resetSSHKey": "重置SSH密钥",
"resetSSHKeySuccessMsg": "发送邮件任务已提交, 用户稍后会收到重置密钥邮件",
"resetSSHKeyWarningMsg": "你确定要发送重置用户的SSH Key的邮件吗?",
"resetWechat": "解绑企业微信",
"resetWechatLoginWarningMsg": "你确定要解绑用户的 企业微信 吗?",
"resetWechatLoginSuccessMsg": "重置成功, 用户可以重新绑定企业微信了",
"resetDingTalk": "解绑钉钉",
"resetDingTalkLoginWarningMsg": "你确定要解绑用户的 钉钉 吗?",
"resetDingTalkLoginSuccessMsg": "重置成功, 用户可以重新绑定钉钉了",
"send": "发送",
"unbind": "解绑",
"unblock": "解锁",
"unblockSuccessMsg": "解锁成功",
"unblockUser": "解锁用户"
@@ -837,14 +1010,28 @@
"tabs": {
"assetPermissionRules": "资产授权规则",
"databasePermissionRules": "数据库授权规则",
"k8sPermissionRules": "Kubernetes授权规则",
"grantedAssets": "授权的资产",
"grantedK8Ss": "授权的Kubernetes",
"grantedDatabases": "授权的数据库",
"grantedRemoteApps": "授权的远程应用",
"grantedApplications": "授权的应用",
"ApplicationPermissionRules": "应用授权规则",
"remoteAppPermissionRules": "远程应用授权规则"
},
"dateLastLogin": "最后登录日期",
"needUpdatePasswordNextLogin": "下次登录须修改密码",
"UpdatePassword": "更新密码",
"SetPublicKey": "设置SSH公钥"
"SetPublicKey": "设置SSH公钥",
"passwordExpired": "密码过期了",
"passwordWillExpiredPrefixMsg": "密码即将在 ",
"passwordWillExpiredSuffixMsg": "天 后过期,请尽快修改您的密码。"
},
"notifications": {
"MessageType": "消息类型",
"Receivers": "接收人",
"Subscription": "消息订阅",
"ChangeReceiver": "修改消息接收人"
},
"xpack": {
"Admin": "管理员",
@@ -893,6 +1080,14 @@
"Username": "用户名"
},
"Cloud": {
"Aliyun": "阿里云",
"Qcloud": "腾讯云",
"AWS_China": "AWS(中国)",
"AWS_Int": "AWS(国际)",
"HuaweiCloud": "华为云",
"Azure":"Azure(中国)",
"Azure_Int": "Azure(国际)",
"HostnameStrategy": "用于生成资产主机名。例如1. 实例名称 (instanceDemo)2. 实例名称和部分IP(后两位) (instanceDemo-250.1)",
"IsAlwaysUpdate": "资产信息保持最新",
"AccountCreate": "创建账户",
"AccountList": "账户列表",
@@ -944,13 +1139,22 @@
"GatherUserList": "收集用户",
"GatherUserTaskCreate": "创建任务",
"GatherUserTaskList": "任务列表",
"GatherUserTaskUpdate": "更新任务"
"GatherUserTaskUpdate": "更新任务",
"GatherUserTaskDetail": "任务详情",
"GatherUserTaskExecutionList": "任务执行列表"
},
"Import": "导入",
"ImportLicense": "导入许可证",
"ImportLicenseTip": "请导入许可证",
"InterfaceSettings": "界面设置",
"License": "许可证",
"SystemMonitor": "系统监控",
"ServiceRatio": "组件负载统计",
"LoadStatus":"组件状态",
"NormalLoad":"正常",
"HighLoad":"较高",
"CriticalLoad":"严重",
"Offline": "离线",
"LicenseDetail": "许可证详情",
"LicenseFile": "许可证文件",
"NoLicense": "暂无许可证",
@@ -964,7 +1168,16 @@
"DeleteOrgTitle": "请确保组织内的以下信息已删除",
"DeleteOrgMsg": "用户列表、用户组、资产列表、网域列表、管理用户、系统用户、标签管理、资产授权规则",
"OrgRole": "组织角色",
"CreateOrgMsg": "请去组织详情内添加用户"
"users_amount": "用户数量",
"groups_amount": "用户组数量",
"assets_amount": "资产数量",
"admin_users_amount": "管理用户数量",
"system_users_amount": "系统用户数量",
"applications_amount": "应用数量",
"asset_perms_amount": "资产授权数量",
"app_perms_amount": "应用授权数量",
"CreateOrgMsg": "请去组织详情内添加用户",
"AddOrgMembers": "添加组织成员"
},
"RestoreButton": "恢复默认",
"SubscriptionID": "订阅授权ID",
@@ -981,9 +1194,9 @@
"loginImageTip": "提示:将会显示在企业版用户登录页面(建议图片大小为: 492*472px",
"loginTitle": "登录页面标题",
"loginTitleTip": "提示将会显示在企业版用户登录页面eg: 欢迎使用JumpServer开源堡垒机)",
"logoIndex": "管理页面logo",
"logoIndex": "Logo (带文字)",
"logoIndexTip": "提示:将会显示在管理页面左上方(建议图片大小为: 185px*55px",
"logoLogout": "退出页面logo",
"logoLogout": "Logo (不带文字)",
"logoLogoutTip": "提示将会显示在企业版用户退出页面建议图片大小为82px*82px",
"restoreDialogMessage": "您确定要恢复默认初始化吗?",
"restoreDialogTitle": "你确认吗",

View File

@@ -1,9 +1,50 @@
{
"": "",
"acl": {
"name": "Name",
"username": "Username",
"ip_group": "IP group",
"action": "Action",
"priority": "Priority",
"date_created": "Date created",
"created_by": "Created by",
"asset": "Asset",
"system_user": "System user",
"username_group":"Username group",
"hostname_group":"Hostname group",
"asset_ip_group": "Asset ip group",
"system_users_name_group": "Systemusers name group",
"system_users_protocol_group": "Systemusers protocol group",
"system_users_username_group": "systemusers username group",
"apply_login_asset": "Apply login asset",
"apply_login_system_user": "Apply login system user",
"apply_login_user": "Apply login user",
"RuleDetail": "Rule detail"
},
"applications": {
"": "",
"RemoteApp": "Remote app",
"Database": "Database",
"Cloud": "Cloud",
"applicationsType": {
"chrome": "Chrome",
"mysql_workbench": "MySQL Workbench",
"vmware_client":"Vmware Client",
"custom":"Custom",
"mysql": "MySQL",
"oracle": "Oracle",
"postgresql": "PostgreSQL",
"mariadb": "MariaDB",
"k8s": "kubernetes"
},
"applicationsCategory": {
"remote_app": "Remote app",
"db": "Database app",
"cloud": "Cloud app"
},
"appPath": "App path",
"appType": "App type",
"appName": "App name",
"asset": "Asset",
"database": "Database",
"host": "Host",
@@ -32,20 +73,28 @@
"Custom": "Custom",
"cluster": "Cluster",
"kubernetes":"Kubernetes",
"clusterHelpTextMessage": "Tips: https://172.16.8.8"
"clusterHelpTextMessage": "Tips: https://172.16.8.8:8443",
"DBInfo": "Database Info"
},
"assets": {
"Action": "Action",
"ActiveSelected": "Active selected",
"AdminUser": "Admin user",
"ReplaceNodeAssetsAdminUser":"Replace node assets admin user with this",
"AdminUserDetail": "Admin user detail",
"AdminUserListHelpMessage": "Admin users are asset (charged server) on the root, or have NOPASSWD: ALL sudo permissions users, JumpServer users of the system using the user to `push system user`, `get assets hardware information`, etc.\n",
"Asset": "Asset",
"HardwareInfo": "Hardware info",
"Hardware": "Hardware",
"AccountList": "Account list",
"AssetDetail": "Asset detail",
"AssetList": "Asset list",
"AssetListHelpMessage": "The left side is the asset tree, right click to create, delete, and change the tree node, authorization asset is also organized as a node, and the right side is the asset under that node\n",
"AssetNumber": "Asset number",
"AssetUserList": "Asset user list",
"TestGatewayTestConnection":"Test gateway test connection",
"TestGatewayHelpMessage": "If use nat, set the ssh real port",
"SshPort": "SSH Port",
"Assets": "Assets",
"Auth": "Auth",
"AutoGenerateKey": "Auto generate ssh key",
@@ -67,6 +116,8 @@
"DateUpdated": "Date updated",
"DeactiveSelected": "Deactive selected",
"Disk": "Disk",
"AdDomain": "AD Domain",
"AdDomainHelpText": "AD domain provided to domain users for login",
"Domain": "Domain",
"DomainDetail": "Domain detail",
"DomainHelpMessage": "The domain function is added to address the fact that some environments (such as the hybrid cloud) cannot be connected directly by jumping on the gateway server.\nJMS => Domain gateway => Target assets",
@@ -110,6 +161,7 @@
"RefreshHardware": "Refresh hardware",
"RemoteAppListHelpMessage": "Before using this feature, make sure that the application loader has been uploaded to the application server and successfully published as a RemoteApp application <b><a href='https://github.com/jumpserver/Jmservisor/releases'> Download application loader</a></b>",
"RemoteApps": "Remote apps",
"Applications": "Applications",
"RemoteType": "Remote type",
"RemoveFromCurrentNode": "Remove from node",
"ReplaceNodeAssetsAdminUserWithThis": "Replace node assets admin user with this",
@@ -126,7 +178,7 @@
"TestAssetsConnective": "Test assets connective",
"TestConnection": "Test connection",
"Type": "Type",
"UnselectedAssets": "Unselected assets",
"UnselectedAssets": "No asset selected or the selected asset does not support SSH protocol connection",
"UnselectedNodes": "Unselected nodes",
"UpdateAssetUserToken": "Update asset user auth",
"Username": "Username",
@@ -136,12 +188,17 @@
"Version": "Version",
"command_filter_list": "Command filter list",
"date_joined": "Date joined",
"sshKeyFingerprint": "SSH fingerprint",
"ip": "IP",
"sshkey": "sshkey",
"GroupsHelpMessage": "Please fill in user groups, separated by commas if there are multiple user groups(Please fill in the existing user groups)",
"HomeHelpMessage": "Default home directory: /home/system username",
"Home": "Home",
"LinuxUserAffiliateGroup": "Linux user affiliate group"
"LinuxUserAffiliateGroup": "Linux user affiliate group",
"ipDomain": "IP(Domain)",
"HostProtocol": "Host Protocol",
"DatabaseProtocol": "Database Protocol",
"Other Protocol": "Database Protocol"
},
"audits": {
"Hosts": "Host",
@@ -156,17 +213,27 @@
"common": {
"Nothing": "Nothing",
"Action": "Action",
"CustomCol":"Custom table col",
"RequestTickets": "Request tickets",
"Actions": "Actions",
"NeedSpecifiedFile": "Required to upload the specified format file",
"TestPortErrorMsg":"Port Error, please check",
"Activate": "Activate",
"actionsTips":"Clipboard's copy and paste control only support RDP/VNC protocol.",
"Active": "Active",
"TableColSettingInfo": "Please select the list details you want to display",
"Add": "Add",
"PleaseAgreeToTheTerms": "Please agree to the terms",
"PushSelected":"Push selected",
"UpdateAssetDetail": "Update more detail",
"AddSuccessMsg": "Add success",
"Auth": "Authorization",
"BadRequestErrorMsg": "Bad request, please check again",
"BadRoleErrorMsg": "Bad request, no permission for this operation",
"BadConflictErrorMsg": "Refreshing, please try again later",
"Basic": "Basic",
"BasicInfo": "Basic info",
"ApplyInfo": "Apply info",
"Cancel": "Cancel",
"Close": "Close",
"Command filter": "Command filter",
@@ -176,6 +243,7 @@
"CreatedBy": "Created by",
"CrontabHelpTips": "eg: Every Sunday 03:05 run <5 3 * * 0> <br>Tips:Using 5 digits linux crontab expressions<min hour day month week> (<a href='https://tool.lu/crontab/' target='_blank'>Online tools</a>) <br>Note:If both Regularly perform and Cycle perform are set,give priority to Regularly perform",
"DateEnd": "End date",
"Resource": "Resource",
"DateLast24Hours": "Last 24 hours",
"DateLast3Months": "Last 3 months",
"DateLastMonth": "Last month",
@@ -189,10 +257,16 @@
"EnterForSearch": "Press enter to search",
"Export": "Export",
"Import": "Import",
"ContinueImport": "ContinueImport",
"Continue": "Continue",
"Stop": "Stop",
"Finished": "Finished",
"Refresh": "Refresh",
"Info": "Info",
"MFAConfirm": "MFA Confirm",
"MFARequireForSecurity": "MFA required for security",
"PasswordConfirm": "Password Confirm",
"PasswordRequireForSecurity": "Password required for security",
"Members": "Members",
"More": "More",
"Message": "Message",
@@ -204,20 +278,26 @@
"Other": "Other",
"Others": "Others",
"Push": "Push",
"Receivers": "Receivers",
"QuickUpdate": "Quick update",
"RemoveSuccessMsg": "Remove success",
"Reset": "Reset",
"Search": "Search",
"MFAErrorMsg": "MFA Errorplease check",
"InputEmailAddress": "Please enter your email address",
"Select": "Select",
"SelectFile": "Select file",
"Show": "Show",
"Submit": "Submit",
"Test": "Test",
"SaveAndAddAnother":"Save and add another",
"TestSuccessMsg": "Test Success",
"To": "To",
"Update": "Update",
"bind": "Bind",
"unbind": "Unbind",
"Upload": "Upload",
"Clone": "Clone",
"Username": "Username",
"Validity": "Validity",
"Invalidity": "Invalidity",
@@ -231,8 +311,10 @@
"bulkRemoveSuccessMsg": "Bulk remove success",
"NeedAssetsAndSystemUserErrMsg": "Need assets and systemuser",
"createBy": "Create by",
"cloneFrom": "Clone from",
"createErrorMsg": "Create error",
"createSuccessMsg": "Create success",
"saveSuccessContinueMsg": "Create success, you may add another",
"createdBy": "Created by",
"dateCreated": "Date created",
"dateFinished": "Date finished",
@@ -246,6 +328,12 @@
"disableSelected": "Disable selected",
"fieldRequiredError": "This field is required",
"getErrorMsg": "Get failed",
"fileType": "File type",
"Status": "Status",
"Total": "Total",
"Success": "Success",
"Failed": "Failed",
"Pending": "Pending",
"imExport": {
"ExportAll": "Export all",
"ExportOnlyFiltered": "Export only filtered",
@@ -255,7 +343,10 @@
"downloadImportTemplateMsg": "Download import template",
"downloadUpdateTemplateMsg": "Download update template",
"onlyCSVFilesTips": "Only csv supported",
"updateSuccessMsg": "Update success, total: {count}"
"updateSuccessMsg": "Update success, total: {count}",
"dragUploadFileInfo": "Drag file here or click to upload",
"uploadCsvLth10MHelpText": "csv/xlsx files with a size less than 10M",
"hasImportErrorItemMsg": "There is an error item, click the x icon to view the details, and continue to import after editing"
},
"isValid": "Is valid",
"nav": {
@@ -291,7 +382,8 @@
"NUMBER_REQUIRED": "Number required",
"SPECIAL_CHAR_REQUIRED": "Special char required",
"MIN_LENGTH_ERROR": "Password minimum length {}"
}
},
"lastCannotBeDeleteMsg": "The last one can't be delete"
},
"dashboard": {
"ActiveAsset": "Asset active",
@@ -371,7 +463,7 @@
},
"perms": {
"": "",
"Actions": "Actions",
"Actions": "Permission",
"Asset": "Asset",
"Basic": "Basic",
"Exclude": "Exclude",
@@ -382,15 +474,20 @@
"SystemUser": "System user",
"User": "User",
"UserGroups": "UserGroups",
"DatabaseAppPermission": "Databases permissions",
"RemoteAppPermission": "Remote apps permissions",
"KubernetesAppPermission": "Kubernetes permissions",
"addAssetToThisPermission": "Add asset to this permission",
"addDatabaseAppToThisPermission": "Add DatabaseApp to this permission",
"addK8sAppToThisPermission": "Add KubernetesApp to this permission",
"addApplicationToThisPermission": "Add Application to this permission",
"addNodeToThisPermission": "Add node to this permission",
"addRemoteAppToThisPermission": "Add RemoteApp to this permission",
"addSystemUserToThisPermission": "System user",
"addUserGroupToThisPermission": "Add user group to this permission",
"addUserToThisPermission": "Add user to this permission",
"all": "All",
"PermName":"Perm name",
"assetAndNode": "Assets and node",
"assetCount": "Asset count",
"connect": "Connect",
@@ -400,12 +497,15 @@
"downloadFile": "Download file",
"hostName": "Hostname",
"isValid": "Validity",
"isEffective": "Effective",
"nodeCount": "Node count",
"refreshFail": "Refresh fail",
"refreshPermissionCache": "Refresh permission cache",
"refreshSuccess": "Refresh success",
"remoteApp": "RemoteApp",
"remoteAppCount": "RemoteApp count",
"appsCount": "App count",
"appsList":"App list",
"DatabaseAppCount": "DatabaseApp count",
"KubernetesAppCount": "KubernetesApp count",
"systemUserCount": "System user count",
@@ -420,28 +520,30 @@
},
"route": {
"": "",
"Ticket": "Tickets",
"CommandConfirm": "Command confirm",
"AdminUserCreate": "Admin user create",
"AdminUserDetail": "Admin user detail",
"AdminUserList": "Admin users",
"AdminUserList": "Admin Users",
"AdminUserUpdate": "Admin user update",
"Applications": "Applications",
"AssetCreate": "Asset create",
"AssetDetail": "Asset detail",
"AssetList": "Assets",
"AssetPermission": "Asset permissions",
"AssetPermission": "Asset Permissions",
"AssetPermissionCreate": "Asset permissions create",
"AssetPermissionDetail": "Asset permissions detail",
"AssetPermissionUpdate": "Asset permissions update",
"AssetUpdate": "Asset update",
"Assets": "Assets",
"Audits": "Audits",
"BatchCommand": "Batch command",
"BatchCommandLog": "Batch command log",
"BatchCommand": "Batch Command",
"BatchCommandLog": "Batch Command Log",
"CeleryTaskLog": "Celery task log",
"CommandExecutions": "CommandExecutions ",
"CommandFilterCreate": "Command filter create",
"CommandFilterDetail": "Command filter detail",
"CommandFilterList": "Command filters",
"CommandFilterList": "Command Filters",
"CommandFilterRulesCreate": "Command filter rules create",
"CommandFilterRulesUpdate": "Command filter rules update",
"CommandFilterUpdate": "Command filter update",
@@ -450,7 +552,7 @@
"CreateCommandStorage": "Create command storage",
"CreateReplayStorage": "Create replay storage",
"Dashboard": "Dashboard",
"DatabaseApp": "Database apps",
"DatabaseApp": "Database Apps",
"DatabaseAppCreate": "Database app create",
"DatabaseAppDetail": "Database app detail",
"DatabaseAppPermission": "Databases permissions",
@@ -458,7 +560,7 @@
"DatabaseAppPermissionDetail": "Databases permissions detail",
"DatabaseAppPermissionUpdate": "Databases permissions update",
"DatabaseAppUpdate": "Database app update",
"KubernetesApp": "Kubernetes apps",
"KubernetesApp": "Kubernetes Apps",
"KubernetesAppCreate": "Kubernetes app create",
"KubernetesAppDetail": "Kubernetes app detail",
"KubernetesAppPermission": "Kubernetes permissions",
@@ -466,35 +568,50 @@
"KubernetesAppPermissionDetail": "Kubernetes permissions detail",
"KubernetesAppPermissionUpdate": "Kubernetes permissions update",
"KubernetesAppUpdate": "Kubernetes app update",
"Acl": "Access Control",
"UserAclList": "User acl list",
"UserAclCreate": "User acl create",
"UserAclUpdate": "User acl update",
"UserAclLists": "User acl lists",
"UserAclDetail": "User acl detail",
"AssetAclList": "Asset Acl",
"AssetAclCreate": "Asset acl create",
"AssetAclUpdate": "Asset acl update",
"AssetAclDetail": "Asset acl detail",
"DomainCreate": "Domain create",
"DomainDetail": "Domain detail",
"DomainList": "Domains",
"DomainUpdate": "Domain update",
"FileManager": "File manager",
"FtpLog": "FTP logs",
"FileManager": "File Manager",
"FtpLog": "FTP Logs",
"GatewayCreate": "Gateway create",
"GatewayUpdate": "Gateway update",
"JobCenter": "Jobcenter",
"LabelCreate": "Label create",
"LabelList": "Labels",
"LabelUpdate": "Label update",
"LoginLog": "Login logs",
"MyApps": "My apps",
"MyAssets": "My assets",
"OperateLog": "Operation logs",
"PasswordChangeLog": "Password update logs",
"LoginLog": "Login Logs",
"MyApps": "My Apps",
"MyAssets": "My Assets",
"OperateLog": "Operation Logs",
"PasswordChangeLog": "Password Update Logs",
"Perms": "Permissions",
"PersonalInformationImprovement": "PersonalInformationImprovement",
"PlatformCreate": "Platform create",
"PlatformDetail": "Platform detail",
"PlatformList": "Platforms",
"PlatformUpdate": "Platform update",
"RemoteApp": "Remote apps",
"RemoteApp": "Remote Apps",
"RemoteAppDetail": "Remote app detail",
"RemoteAppPermission": "Remote apps permissions",
"ApplicationPermission": "Application Permissions",
"RemoteAppPermissionCreate": "Remote apps permission create",
"RemoteAppPermissionDetail": "Remote apps permissions detail",
"RemoteAppPermissionUpdate": "Remote app permission update",
"ApplicationDetail": "Application detail",
"ApplicationPermissionCreate": "Application permission create",
"ApplicationPermissionDetail": "Application permission detail",
"ApplicationPermissionUpdate": "Application permission update",
"RemoteAppUpdate": "Remote app update",
"ReplayStorageUpdate": "Replay storage update",
"SessionDetail": "Sessions detail",
@@ -504,11 +621,11 @@
"Settings": "Settings",
"SystemUserCreate": "System user create",
"SystemUserDetail": "System user detail",
"SystemUserList": "System users",
"SystemUserList": "System Users",
"SystemUserUpdate": "System user update",
"TaskDetail": "Tasks detail",
"TaskList": "Tasks",
"TaskMonitor": "Task monitor",
"TaskMonitor": "Task Monitor",
"Terminal": "Terminal",
"TicketDetail": "Ticket detail",
"TicketCreate": "Ticket create",
@@ -518,7 +635,7 @@
"UserFirstLogin": "UserFirstLogin",
"UserGroupCreate": "User group create",
"UserGroupDetail": "User group detail",
"UserGroupList": "User groups",
"UserGroupList": "User Groups",
"UserGroupUpdate": "User group update",
"UserGuide": "UserGuide",
"UserList": "Users",
@@ -526,7 +643,8 @@
"UserUpdate": "User update",
"Users": "Users",
"WebFTP": "WebFTP",
"WebTerminal": "Web terminal"
"WebTerminal": "Web Terminal",
"Notifications": "Notifications"
},
"sessions": {
"StorageConfiguration": "Storage configuration",
@@ -535,6 +653,7 @@
"active": "active",
"alive": "alive",
"asset": "Asset",
"target": "Target",
"bucket": "Bucket",
"command": "Command",
"commandStorage": "Command storage",
@@ -550,6 +669,11 @@
"duration": "Duration",
"endPoint": "Endpoint",
"endpointSuffix": "Endpoint suffix",
"sessionActiveCount": "session active count",
"systemCpuLoad": "cpu load",
"systemDiskUsedPercent": "disk used percent",
"systemMemoryUsedPercent": "memory used percent",
"EsDisabled": "Node is unavailable, please contact administrator",
"go": "Go",
"goto": "Goto",
"hosts": "Hosts",
@@ -588,7 +712,9 @@
"helpText": {
"esUrl": "Tip: If you have multiple hosts, use comma (,) to split (eg: http://www.jumpserver.a.com,http://www.jumpserver.b.com)",
"esIndex":"Es provides the default index: jumpserver",
"esDocType": "Es provides the default document type: command"
"esDocType": "Es provides the default document type: command",
"s3Endpoint": "S3: http://s3.{REGION_NAME}.amazonaws.com<br>S3(China): http://s3.{REGION_NAME}.amazonaws.com.cn<br>Example: http://s3.cn-north-1.amazonaws.com.cn",
"ossEndpoint": "OSS: http://{REGION_NAME}.aliyuncs.com<br>Example: http://oss-cn-hangzhou.aliyuncs.com"
}
},
"setting": {
@@ -611,6 +737,8 @@
"Security": "Security setting",
"SecuritySetting": "Security setting",
"SubscriptionID": "Subscription ID",
"SystemMessageSubscription": "System messages",
"insecureCommandEmailUpdate": "Setting",
"Terminal": "Terminal setting",
"all": "All",
"authLdap": "Enable LDAP auth",
@@ -640,6 +768,10 @@
"emailTest": "Test connection",
"emailUserSSL": "Use SSL",
"emailUserTLS": "Use TLS",
"MailSend": "Mail send",
"LDAPServerInfo": "LDAP Server",
"LDAPUser": "LDAP User",
"InsecureCommandAlert": "Insecure command alert",
"helpText": {
"ApiKeyList": "The API key is used to sign the request header. The header of each request is different. Please refer to the usage documentation",
"authLdapSearchFilter": "Choice may be (cn|uid|sAMAccountName)=%(user)s)",
@@ -713,23 +845,35 @@
"userGuideUrl": "User Guide URL",
"username": "Username",
"usernamePlaceholder": "Please input username",
"refreshLdapCache":"Refreshing Ldap cache "
},
"settings": {
"refreshLdapCache":"Refreshing Ldap cache ",
"LicenseExpired": "License expired",
"LicenseWillBe": "License will expire at ",
"Expire": "",
"WeCom": "WeCom",
"DingTalk": "DingTalk",
"dingTalkTest": "Test",
"weComTest": "Test",
"setting": "Setting"
},
"tickets": {
"PermissionName": "Permission name",
"Accept": "Accept",
"AssignedMe": "Assigned me",
"Assignee": "Assignee",
"RequestPerm":"Request Perm",
"AssignedInfo":"Assigned Info",
"OpenTicket": "Open Ticket",
"HandleTicket": "Handle Ticket",
"FinishedTicket": "Finished Ticket",
"Assignees": "Assignees",
"Close": "Close",
"OpenStatus":"Open",
"CloseStatus":"Close",
"Comment": "Comment",
"MyTickets": "My tickets",
"action": "Action",
"IPGroup": "IP 组",
"Reject": "Reject",
"date": "Date",
"reply": "Reply",
@@ -739,15 +883,29 @@
"user": "User",
"Status": "Status",
"Open": "Open",
"OrgName":"Org name",
"IP": "IP",
"Hostname": "Hostname",
"Asset": "Asset",
"SystemUser": "System user",
"Applicant": "Applicant",
"RequestAssetPerm": "Request asset perm",
"RequestApplicationPerm": "Request application perm",
"Pending": "Open",
"Approved": "Approved",
"Rejected": "Rejected",
"Closed": "Closed"
"Closed": "Closed",
"helpText": {
"ips": "Enter the IP address group, separated by commas",
"fuzzySearch": "Support for fuzzy search",
"application": "Enter the application group, separated by commas"
},
"ApplyRunUser": "Apply run user",
"ApplyRunAsset": "Apply run asset",
"ApplyRunSystemUser": "Apply run system user",
"ApplyRunCommand": "Apply run command",
"ApplyFromSession": "Session",
"ApplyFromCMDFilterRule": "Command filter rule"
},
"tree": {
"AddAssetToNode": "Add asset to node",
@@ -757,12 +915,14 @@
"RenameNode": "Rename node",
"ShowAssetAllChildrenNode": "Show asset all children node",
"ShowAssetOnlyCurrentNode": "Show asset only current node",
"CheckAssetsAmount": "Check assets amount",
"ShowNodeInfo": "Show node information",
"TestNodeAssetConnectivity": "Test node asset connectivity",
"UpdateNodeAssetHardwareInfo": "Update node asset hardware information"
},
"users": {
"Account": "Account",
"Existing":"Existing",
"Authentication": "Account",
"Comment": "Comment",
"ConfirmPassword": "Confirm password",
@@ -770,15 +930,21 @@
"DateJoined": "Date joined",
"DateLastLogin": "Date last login",
"DatePasswordLastUpdated": "Date password last updated",
"setWeCom": "Set wecom login",
"setDingTalk": "Set dingtalk login",
"DatePasswordUpdated": "Date password updated",
"DescribeOfGuide": "Welcome to JumpServer. Click here for more information",
"Email": "Email",
"FingerPrint": "Fingerprint",
"FirstLogin": "First login",
"InviteUser": "Invite user",
"InviteUserInOrg": "Invite user in this org",
"Invite": "Invite",
"Guide": "Guide",
"OrgUser": "Org User",
"OrgAdmin": "Org Admin",
"OrgAuditor": "Org Auditor",
"HelpText": {
"MFAOfUserFirstLoginPersonalInformationImprovementPage": "Enable multi-factor authentication to make the account more secure <br/> After is enabled, you will enter the multi-factor authentication binding process on your next login <br/> You can also bind directly in (personal information -> fast modifier -> modifier multiple factor Settings)",
"MFAOfUserFirstLoginUserGuidePage": "To protect the security of you and the company <br/> please properly keep your account, password, key and other important and sensitive information <br/> (e.g., set a complex password and enable multi-factor authentication)",
@@ -825,7 +991,11 @@
"resetSSHKey": "Reset SSH key",
"resetSSHKeySuccessMsg": "An e-mail has been sent to the user`s mailbox",
"resetSSHKeyWarningMsg": "This will reset the user public key and send a reset mail",
"resetWechat": "Reset Wechat",
"resetWechatLoginWarningMsg": "This will reset the user Wechat setting, user can reset it",
"resetWechatLoginSuccessMsg": "Reset Wechat success",
"send": "Send",
"unbind": "Unbind",
"unblock": "Unblock",
"unblockSuccessMsg": "Account has unblocked",
"unblockUser": "Unblock login"
@@ -834,13 +1004,28 @@
"tabs": {
"assetPermissionRules": "Asset permission rules",
"databasePermissionRules": "Database Permission rules",
"k8sPermissionRules": "Kubernetes Permission rules",
"grantedAssets": "Granted assets",
"grantedK8Ss":"Granted K8Ss",
"grantedDatabases": "Granted databases",
"grantedRemoteApps": "Granted remote apps",
"grantedApplications": "Granted applications",
"ApplicationPermissionRules": "Application permission rules",
"remoteAppPermissionRules": "Remote app permission rules"
},
"UpdatePassword": "",
"UpdatePublicKey": ""
"needUpdatePasswordNextLogin": "Update password next login",
"UpdatePassword": "Update password",
"SetPublicKey": "Set public key",
"UpdatePublicKey": "",
"passwordExpired": "Password expired",
"passwordWillExpiredPrefixMsg": "The password will expire in ",
"passwordWillExpiredSuffixMsg": " days.Please change your password as soon as possible."
},
"notifications": {
"MessageType": "Message Type",
"Receivers": "Receivers",
"Subscription": "Subscription",
"ChangeReceiver": "Change Receivers"
},
"xpack": {
"Admin": "Admin",
@@ -853,7 +1038,7 @@
"Asset": "Asset",
"AssetAmount": "Asset",
"AssetAndNode": "Asset and Node",
"ChangeAuthPlan": "Change auth plan",
"ChangeAuthPlan": "Change Auth Plan",
"ChangeAuthPlanCreate": "Create change auth plan",
"ChangeAuthPlanUpdate": "Update change auth plan",
"CyclePerform": "Cycle perform",
@@ -889,13 +1074,21 @@
"Username": "Username"
},
"Cloud": {
"Aliyun": "Ali Cloud",
"Qcloud": "Tencent Cloud",
"AWS_China": "AWS(China)",
"AWS_Int": "AWS(International)",
"HuaweiCloud": "Huawei Cloud",
"Azure":"Azure(China)",
"Azure_Int": "Azure(International)",
"HostnameStrategy": "Used to produce the asset hostname. For example, 1. Instance name (instanceDemo)2. Instance name and Partial IP (instanceDemo-250.1)",
"IsAlwaysUpdate": "Asset info is kept up-to-date",
"AccountCreate": "Create account",
"AccountList": "Account list",
"AccountUpdate": "Update account",
"AccountDetail": "Account detail",
"Cloud": "Cloud center",
"CloudCenter": "Cloud center",
"CloudCenter": "Cloud Center",
"Provider": "Provider",
"Validity": "Validity",
"IsAlwaysUpdateHelpTips": "Whether the asset information, including Hostname, IP, Platform, and AdminUser, is updated synchronously each time a synchronization task is performed",
@@ -936,18 +1129,27 @@
"Execute": "Execute",
"Expired": "Expired",
"GatherUser": {
"GatherUser": "Gather user",
"GatherUser": "Gather User",
"GatherUserList": "Gather user",
"GatherUserTaskCreate": "Create gather user task",
"GatherUserTaskList": "Gather user task list",
"GatherUserTaskUpdate": "Update gather user task"
"GatherUserTaskUpdate": "Update gather user task",
"GatherUserTaskDetail": "Gather user detail",
"GatherUserTaskExecutionList": "Gather user task execution list"
},
"Import": "Import",
"ImportLicense": "Import license",
"ImportLicenseTip": "Please Import License",
"InterfaceSettings": "Interface setting",
"InterfaceSettings": "Interface Setting",
"License": "License",
"LicenseDetail": "License detail",
"SystemMonitor": "System Monitor",
"ServiceRatio": "Service ratio",
"LoadStatus":"Status",
"NormalLoad":"Normal",
"HighLoad":"High",
"Offline": "Offline",
"CriticalLoad":"Critical",
"LicenseFile": "License file",
"NoLicense": "No License",
"Node": "Node",
@@ -960,7 +1162,16 @@
"DeleteOrgTitle":"Please ensure that the following information in the organization has been deleted",
"DeleteOrgMsg":"User list、User group、Asset list、Domain list、Admin user、System user、Labels、Asset permission",
"OrgRole": "Org role",
"CreateOrgMsg": "Please go to Organization Details to add users"
"CreateOrgMsg": "Please go to Organization Details to add users",
"AddOrgMembers": "Add organization members",
"users_amount": "Users amount",
"groups_amount": "Groups amount",
"assets_amount": "Assets amount",
"admin_users_amount": "Admin users amount",
"system_users_amount": "System users amount",
"applications_amount": "Applications amount",
"asset_perms_amount": "Asset perms amount",
"app_perms_amount": "App perms amount"
},
"RestoreButton": "Restore Default",
"SubscriptionID": "Subscription ID",
@@ -977,9 +1188,9 @@
"loginImageTip": "Tips: This will be displayed on the enterprise user login page. (suggest image size: 492px*472px)",
"loginTitle": "Title of login page",
"loginTitleTip": "Tips: This will be displayed on the enterprise user login page. (eg: Welcome to the JumpServer open source fortress)",
"logoIndex": "Logo of management page",
"logoIndex": "Logo (It contains text)",
"logoIndexTip": "Tips: This will appear at the top left of the administration page. (suggest image size: 185px*55px)",
"logoLogout": "Logo of logout page",
"logoLogout": "Logo (It contains no text)",
"logoLogoutTip": "Tips: This will be displayed on the enterprise user logout page. (suggest image size: 82px*82px)",
"restoreDialogMessage": "This will restore default Settings of the interface !!!",
"restoreDialogTitle": "Are you sure?",

View File

@@ -1,10 +1,10 @@
<template>
<div class="footer" :style="style">
<div class="pull-right">
Version <strong>2.0.2</strong> <span v-if="!publicSettings.XPACK_LICENSE_IS_VALID"> GPLv2. </span>
Version <strong> dev </strong> <span v-if="!publicSettings.XPACK_LICENSE_IS_VALID"> GPLv2. </span>
</div>
<div v-if="!publicSettings.XPACK_LICENSE_IS_VALID" style="padding-left:20px;">
<strong>Copyright</strong> FIT2CLOUD 飞致云 © 2014-2020
<strong>Copyright</strong> FIT2CLOUD 飞致云 © 2014-2021
</div>
</div>
</template>

View File

@@ -5,6 +5,8 @@
:method="method"
:form="form"
:url="iUrl"
:has-save-continue="iHasSaveContinue"
:has-reset="iHasReset"
:is-submitting="isSubmitting"
v-bind="$attrs"
v-on="$listeners"
@@ -19,44 +21,64 @@ export default {
AutoDataForm
},
props: {
// 创建对象的地址
url: {
type: String,
default: ''
},
// 更新的对象
object: {
type: Object,
default: null
},
// form的默认值
initial: {
type: Object,
default: () => ({})
},
afterGetFormValue: {
type: Function,
default: (value) => value
},
// 提交前清理form的值
cleanFormValue: {
type: Function,
default: (value) => value
},
// 当提交的时候,怎么处理
onSubmit: {
type: Function,
default: null
},
// 如何提交数据
performSubmit: {
type: Function,
default(validValues) {
return this.$axios[this.method](this.iUrl, validValues)
}
},
// 创建成功的msg
createSuccessMsg: {
type: String,
default: function() {
return this.$t('common.createSuccessMsg')
}
},
// 保存成功继续添加的msg
saveSuccessContinueMsg: {
type: String,
default: function() {
return this.$t('common.saveSuccessContinueMsg')
}
},
// 更新成功的msg
updateSuccessMsg: {
type: String,
default: function() {
return this.$t('common.updateSuccessMsg')
}
},
// 创建成功的跳转路由
createSuccessNextRoute: {
type: Object,
default: function() {
@@ -64,6 +86,7 @@ export default {
return { name: routeName }
}
},
// 更新成功的跳转路由
updateSuccessNextRoute: {
type: Object,
default: function() {
@@ -71,12 +94,23 @@ export default {
return { name: routeName }
}
},
objectDetailRoute: {
type: Object,
default: function() {
const routeName = this.$route.name
.replace('Update', 'Detail')
.replace('Create', 'Detail')
return { name: routeName }
}
},
// 获取下一个路由
getNextRoute: {
type: Function,
default(res, method) {
return method === 'post' ? this.createSuccessNextRoute : this.updateSuccessNextRoute
}
},
// 获取提交的方法
getMethod: {
type: Function,
default: function() {
@@ -88,6 +122,7 @@ export default {
}
}
},
// 获取创建和更新的url function
getUrl: {
type: Function,
default: function() {
@@ -101,12 +136,47 @@ export default {
},
onPerformSuccess: {
type: Function,
default(res, method, vm) {
const msg = method === 'post' ? this.createSuccessMsg : this.updateSuccessMsg
default(res, method, vm, addContinue) {
let msg = method === 'post' ? this.createSuccessMsg : this.updateSuccessMsg
if (addContinue) {
msg = this.saveSuccessContinueMsg
}
let msgLinkName = this.$t('common.Resource')
if (res.name) {
msgLinkName = res.name
} else if (res.hostname) {
msgLinkName = res.hostname
}
const detailRoute = this.objectDetailRoute
detailRoute['params'] = { 'id': res.id }
const route = this.getNextRoute(res, method)
this.$emit('submitSuccess', res)
this.$message.success(msg)
setTimeout(() => this.$router.push(route), 100)
const h = this.$createElement
this.$log.debug('router is: ', detailRoute)
if (this.hasDetailInMsg) {
this.$message({
message: h('p', null, [
h('el-link', {
on: {
click: () => this.$router.push(detailRoute)
},
style: { 'vertical-align': 'top' }
}, msgLinkName),
h('span', { style: {
'padding-left': '5px',
'height': '18px',
'line-height': '18px',
'font-size': '13.5px',
'font-weight': ' 400' }}, msg)
]),
type: 'success'
})
} else {
this.$message.success(msg)
}
if (!addContinue) {
setTimeout(() => this.$router.push(route), 100)
}
}
},
onPerformError: {
@@ -125,13 +195,22 @@ export default {
}
}
}
},
hasSaveContinue: {
type: Boolean,
default: null
},
hasDetailInMsg: {
type: Boolean,
default: true
}
},
data() {
return {
form: {},
loading: true,
isSubmitting: false
isSubmitting: false,
clone: false
}
},
computed: {
@@ -140,44 +219,76 @@ export default {
},
iUrl() {
return this.getUrl()
},
iHasSaveContinue() {
if (this.hasSaveContinue != null) {
return this.hasSaveContinue
}
return this.method === 'post'
},
iHasReset() {
if (this.hasReset != null) {
return this.hasReset
}
return this.method === 'put'
}
},
async created() {
this.$log.debug('Object init is: ', this.object)
this.loading = true
try {
const values = await this.getFormValue()
this.form = Object.assign(this.form, values)
this.$log.debug('Final object is: ', values)
const formValue = Object.assign(this.form, values)
this.form = this.afterGetFormValue(formValue)
} finally {
this.loading = false
}
},
methods: {
handleSubmit(values) {
handleSubmit(values, formName, addContinue) {
let handler = this.onSubmit || this.defaultOnSubmit
handler = handler.bind(this)
values = this.cleanFormValue(values)
return handler(values)
return handler(values, formName, addContinue)
},
defaultOnSubmit(validValues) {
defaultOnSubmit(validValues, formName, addContinue) {
this.isSubmitting = true
this.performSubmit(validValues)
.then((res) => this.onPerformSuccess.bind(this)(res, this.method, this))
.then((res) => this.onPerformSuccess.bind(this)(res, this.method, this, addContinue))
.catch((error) => this.onPerformError(error, this.method, this))
.finally(() => { this.isSubmitting = false })
},
async getFormValue() {
if (this.method !== 'put') {
const cloneFrom = this.$route.query['clone_from']
if (this.method !== 'put' && !cloneFrom) {
return Object.assign(this.form, this.initial)
}
let object = this.object
if (object === null) {
object = await this.getObjectDetail()
if (!object) {
if (cloneFrom) {
this.$log.debug('Clone from: ', cloneFrom)
const url = `${this.url}${cloneFrom}/`
object = await this.getObjectDetail(url)
if (object['name']) {
object.name = this.$t('common.cloneFrom') + ' ' + object.name
} else if (object['hostname']) {
object.hostname = this.$t('common.cloneFrom') + ' ' + object.hostname
}
} else {
object = await this.getObjectDetail(this.iUrl)
}
}
if (object) {
object = _.cloneDeep(object)
this.$emit('update:object', object)
this.$emit('getObjectDone', object)
}
return object
},
async getObjectDetail() {
return this.$axios.get(this.iUrl)
async getObjectDetail(url) {
this.$log.debug('Get object detail: ', url)
return this.$axios.get(url)
}
}
}

View File

@@ -1,5 +1,5 @@
<template>
<Page>
<Page v-bind="$attrs">
<IBox>
<GenericCreateUpdateForm ref="createUpdateForm" v-bind="$attrs" v-on="$listeners" />
</IBox>

View File

@@ -19,6 +19,7 @@ import TabPage from '../TabPage'
import { flashErrorMsg } from '@/utils/request'
import { getApiPath } from '@/utils/common'
import ActionsGroup from '@/components/ActionsGroup'
import { mapGetters } from 'vuex'
export default {
name: 'GenericDetailPage',
@@ -80,22 +81,27 @@ export default {
}
},
data() {
const vm = this
const defaultActions = {
canDelete: true,
deleteCallback: function(item) { this.defaultDelete(item) },
deleteApiUrl: getApiPath(this),
deleteSuccessRoute: this.$route.name.replace('Detail', 'List'),
canUpdate: true,
canUpdate: () => {
return !vm.currentOrgIsRoot
},
updateCallback: function(item) { this.defaultUpdate(item) },
updateRoute: this.$route.name.replace('Detail', 'Update'),
detailApiUrl: getApiPath(this)
}
return {
defaultActions: defaultActions,
loading: true,
validActions: Object.assign(defaultActions, this.actions)
}
},
computed: {
...mapGetters(['currentOrgIsRoot']),
pageActions() {
return [
{
@@ -158,7 +164,7 @@ export default {
this.$message.success(this.$t('common.deleteSuccessMsg'))
this.$router.push({ name: this.validActions.deleteSuccessRoute })
} catch (error) {
this.$message.error(this.$t('common.deleteErrorMsg' + ' ' + error))
this.$message.error(this.$t('common.deleteErrorMsg') + ' ' + error)
} finally {
instance.confirmButtonLoading = false
}

View File

@@ -1,16 +1,16 @@
<template>
<Page v-bind="$attrs">
<ListTable v-bind="$attrs" />
<GenericListTable ref="ListTable" v-bind="$attrs" />
</Page>
</template>
<script>
import Page from '@/layout/components/Page'
import ListTable from '@/components/ListTable'
import GenericListTable from '@/layout/components/GenericListTable'
export default {
name: 'GenericListPage',
components: {
Page, ListTable
Page, GenericListTable
}
}
</script>

View File

@@ -0,0 +1,31 @@
<template>
<ListTable ref="ListTable" v-bind="iAttrs" v-on="$listeners" />
</template>
<script>
import ListTable from '@/components/ListTable/index'
import { mapGetters } from 'vuex'
export default {
name: 'GenericListTable',
components: {
ListTable
},
computed: {
...mapGetters(['currentOrgIsRoot']),
iAttrs() {
const attrs = _.cloneDeep(this.$attrs)
const canCreate = _.get(attrs, 'header-actions.canCreate', null)
this.$log.debug('Can create: ', canCreate)
if (canCreate === null && this.currentOrgIsRoot) {
_.set(attrs, 'header-actions.canCreate', false)
}
this.$log.debug('List table Attrs: ', attrs)
return attrs
}
}
}
</script>
<style scoped>
</style>

View File

@@ -1,7 +1,13 @@
<template>
<Page>
<el-alert v-if="helpMessage" type="success"> {{ helpMessage }} </el-alert>
<TreeTable ref="TreeTable" :table-config="tableConfig" :header-actions="headerActions" :tree-setting="treeSetting">
<TreeTable
ref="TreeTable"
:table-config="tableConfig"
:header-actions="iHeaderActions"
:tree-setting="treeSetting"
v-on="$listeners"
>
<template #table>
<slot name="table" />
</template>
@@ -15,6 +21,7 @@
<script>
import Page from '@/layout/components/Page'
import TreeTable from '@/components/TreeTable'
import { mapGetters } from 'vuex'
export default {
name: 'GenericTreeListPage',
components: {
@@ -27,12 +34,30 @@ export default {
default: null
}
},
computed: {
...mapGetters(['currentOrg']),
iHeaderActions() {
const attrs = _.cloneDeep(this.headerActions)
const canCreate = _.get(attrs, 'canCreate', null)
// this.$log.debug('Current org: ', this.currentOrg)
if (canCreate === null && this.currentOrg && this.currentOrg.is_root) {
_.set(attrs, 'canCreate', false)
}
return attrs
}
},
methods: {
hideRMenu() {
this.$refs.TreeTable.hideRMenu()
},
getSelectedNodes: function() {
return this.$refs.TreeTable.getSelectedNodes()
},
getNodes: function() {
return this.$refs.TreeTable.getNodes()
},
selectNode: function(node) {
return this.$refs.TreeTable.selectNode(node)
}
}
}

View File

@@ -1,6 +1,5 @@
<template>
<Dialog
v-if="dialogSetting.dialogVisible"
:title="this.$t('common.updateSelected')"
:visible.sync="dialogSetting.dialogVisible"
width="70%"
@@ -99,6 +98,7 @@ export default {
const url = this.url
const msg = this.updateSuccessMsg
this.$axios.patch(url, validValues).then((res) => {
vm.$emit('update')
this.$message.success(msg)
vm.dialogSetting.dialogVisible = false
}).catch(error => {

View File

@@ -2,13 +2,10 @@
<div class="sidebar-logo-container" :class="{'collapse':collapse}">
<transition name="sidebarLogoFade">
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
<img v-if="logo" :src="logo" class="sidebar-logo">
<h1 v-else class="sidebar-title">{{ title }}</h1>
<img :src="logoSrc" class="sidebar-logo">
</router-link>
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
<img :src="logoSrc" class="sidebar-logo-text">
<!-- <img v-else-if="logoText" :src="logoText" class="sidebar-logo-text">-->
<!-- <h1 class="sidebar-title">{{ title }}</h1>-->
<img :src="logoTextSrc" class="sidebar-logo-text">
</router-link>
</transition>
</div>
@@ -26,10 +23,6 @@ export default {
},
data() {
return {
title: 'JumpServer',
logoText: require('@/assets/img/logo-text.png'),
logo: require('@/assets/img/logo.png'),
xpackData: {}
}
},
computed: {
@@ -37,16 +30,14 @@ export default {
'publicSettings'
]),
// eslint-disable-next-line vue/return-in-computed-property
logoTextSrc() {
return this.publicSettings.LOGO_URLS.logo_index
},
logoSrc() {
if (this.publicSettings.LOGO_URLS.logo_index !== '/static/img/logo_text.png') {
return this.publicSettings.LOGO_URLS.logo_index
} else {
return this.logoText
}
return this.publicSettings.LOGO_URLS.logo_logout
}
},
created() {
}
}
</script>

View File

@@ -48,7 +48,10 @@ export default {
},
methods: {
needShow() {
return !this.isCollapse && this.userAdminOrgList.length > 1 && this.inAdminPage
const otherOrgs = this.userAdminOrgList.filter(org => {
return !org.is_root && !org.is_default
})
return !this.isCollapse && otherOrgs.length > 0 && this.inAdminPage
},
changeOrg(orgId) {
orgUtil.changeOrg(orgId)

View File

@@ -82,7 +82,7 @@ export default {
case 'userPage':
if (this.currentOrgUsePagePerm) {
this.$store.dispatch('users/setCurrentRole', rolec.USER)
console.log('Switch to: ', rolec.USER)
// console.log('Switch to: ', rolec.USER)
window.location.href = `/ui/`
}
break
@@ -97,7 +97,7 @@ export default {
},
logout() {
// Clean Status
const statusList = ['currentOrg', 'currentRole', 'jms_current_org', 'jms_current_role', 'sidebarStatus', 'django_language', 'X-JMS-ORG', 'activeTab']
const statusList = ['currentOrg', 'currentRole', 'jms_current_org', 'jms_current_role', 'sidebarStatus', 'X-JMS-ORG', 'activeTab']
for (const i in statusList) {
this.$cookie.delete(statusList[i])
}

View File

@@ -12,7 +12,7 @@
<script>
import Dialog from '@/components/Dialog'
import ListTable from '@/components/ListTable'
import { DateFormatter, ShowKeyFormatter } from '@/components/ListTable/formatters'
import { DateFormatter, ShowKeyFormatter } from '@/components/TableFormatters'
export default {
name: 'ApiKey',
components: {
@@ -61,7 +61,7 @@ export default {
this.$refs.ListTable.reloadTable()
this.$message.success(this.$t('common.deleteSuccessMsg'))
}).catch(error => {
this.$message.error(this.$t('common.deleteErrorMsg' + ' ' + error))
this.$message.error(this.$t('common.deleteErrorMsg') + ' ' + error)
})
}.bind(this),
extraActions: [

View File

@@ -31,25 +31,46 @@ export default {
}
},
computed: {
supportedLangMapper() {
return this.supportLanguages.reduce((map, obj) => {
map[obj.code] = obj
return map
})
},
currentLang() {
const cookieCode = this.$cookie.get(this.LANG_COOKIE_NAME)
let lang = this.supportLanguages.find((v) => v.cookieCode === cookieCode)
const langCode = this.getLangCode()
let lang = this.supportedLangMapper[langCode]
if (!lang) {
lang = this.supportLanguages[0]
this.changeLangTo(lang)
}
if (lang.code !== this.$i18n.locale) {
this.changeLangTo(lang)
}
return lang
}
},
mounted() {
if (this.currentLang.code !== this.$i18n.locale) {
this.changeLangTo(this.currentLang)
}
},
methods: {
changeLangTo(item) {
this.$i18n.locale = item.code
localStorage.setItem('lang', item.code)
this.$cookie.set(this.LANG_COOKIE_NAME, item.cookieCode)
window.location.reload()
},
getLangCode() {
let langCode = localStorage.lang
if (!langCode) {
langCode = this.$cookie.get(this.LANG_COOKIE_NAME)
}
if (!langCode) {
langCode = navigator.language || navigator.userLanguage
}
langCode = langCode.substr(0, 2)
langCode = langCode.replace('zh', 'cn')
if (langCode) {
return langCode
}
}
}
}

View File

@@ -0,0 +1,51 @@
<template>
<div>
<!-- <el-link class="el-link" target="_blank" @click="goToTickets">{{ $t('route.Ticket') }}</el-link>-->
<el-badge :value="assignedTicketCount" :hidden="assignedTicketCount===0" size="mini" type="primary">
<el-link class="el-link" target="_blank" @click="goToTickets">{{ $t('route.Ticket') }}</el-link>
</el-badge>
</div>
</template>
<script>
import { getTicketOpenCount } from '@/api/ticket'
import { mapGetters } from 'vuex'
export default {
name: 'WebTerminal',
data() {
return {
assignedTicketCount: 0
}
},
computed: {
...mapGetters([
'currentUser'
])
},
created() {
this.ticketsOpenedCount()
},
methods: {
ticketsOpenedCount() {
getTicketOpenCount(this.currentUser.id).then(data => {
this.assignedTicketCount = data.count
})
},
goToTickets() {
this.$router.push({ name: 'TicketList' })
}
}
}
</script>
<style scoped>
.el-link {
color: #606266 !important;
font-size: 13px;
font-weight: 400
}
.el-badge ::v-deep .el-badge__content.is-fixed{
top:10px;
}
</style>

View File

@@ -0,0 +1,24 @@
<template>
<el-link class="el-link" :href="webTerminalUrl" target="_blank">{{ $t('route.WebTerminal') }}</el-link>
</template>
<script>
import { BASE_URL } from '@/utils/common'
export default {
name: 'WebTerminal',
computed: {
webTerminalUrl() {
return `${BASE_URL}/luna/?_=${Date.now()}`
}
}
}
</script>
<style scoped>
.el-link {
color: #606266 !important;
font-size: 13px;
font-weight: 400
}
</style>

View File

@@ -10,6 +10,19 @@
<div class="header-item">
<Language />
</div>
<div
v-if="
publicSettings.TICKETS_ENABLED
&& publicSettings.XPACK_LICENSE_IS_VALID
&& !isOrgAuditor
"
class="header-item"
>
<Tickets />
</div>
<div class="header-item">
<WebTerminal />
</div>
<div class="header-item header-profile">
<AccountDropdown />
</div>
@@ -23,6 +36,9 @@ import Hamburger from '@/components/Hamburger'
import AccountDropdown from './AccountDropdown'
import Help from './Help'
import Language from './Language'
import WebTerminal from './WebTerminal'
import Tickets from './Tickets'
import rolc from '@/utils/role'
export default {
components: {
@@ -30,7 +46,9 @@ export default {
Hamburger,
AccountDropdown,
Language,
Help
Help,
Tickets,
WebTerminal
},
data() {
return {
@@ -38,8 +56,11 @@ export default {
},
computed: {
...mapGetters([
'sidebar'
])
'sidebar', 'publicSettings', 'currentOrgRoles'
]),
isOrgAuditor() {
return rolc.getRolesDisplay(this.currentOrgRoles).includes('OrgAuditor') || rolc.getRolesDisplay(this.currentOrgRoles).includes('Auditor')
}
},
methods: {
toggleSideBar() {

View File

@@ -0,0 +1,64 @@
<template>
<div v-if="!loading">
<el-alert v-if="isExpire" type="error">
{{ isExpire }}
</el-alert>
</div>
</template>
<script>
import { toSafeLocalDateStr } from '@/utils/common'
import { mapGetters } from 'vuex'
export default {
name: 'LicenseExpireTip',
data() {
return {
loading: true,
licenseData: {}
}
},
computed: {
...mapGetters([
'publicSettings',
'currentUser'
]),
isExpire() {
if (!this.publicSettings.XPACK_ENABLED || this.currentUser.role !== 'Admin') {
return false
}
const intervalDays = this.getIntervalDays(this.licenseData.date_expired)
if (intervalDays < 0) {
return this.$t('setting.LicenseExpired')
}
if (intervalDays < 7) {
return this.$t('setting.LicenseWillBe') + this.licenseData.date_expired + this.$t('setting.Expire')
}
return false
}
},
mounted() {
if (this.publicSettings.XPACK_ENABLED && this.currentUser.role === 'Admin') {
this.$axios.get('/api/v1/xpack/license/detail').then(res => {
this.licenseData = res
}).finally(() => {
this.loading = false
})
} else {
this.loading = false
}
},
methods: {
getIntervalDays(date) {
const dateExpired = new Date(toSafeLocalDateStr(date))
const dateNow = new Date()
const intervalTime = dateExpired.getTime() - dateNow.getTime()
return Math.floor(intervalTime / (24 * 3600 * 1000))
}
}
}
</script>
<style scoped>
</style>

View File

@@ -1,22 +1,34 @@
<template>
<div class="page-heading">
<el-row :gutter="0">
<el-col :span="16" class="page-heading-left">
<slot><h2>{{ title }}</h2></slot>
</el-col>
<el-col :span="8">
<div class="page-heading-right">
<slot name="rightSide" />
</div>
</el-col>
</el-row>
<div>
<slot name="globalNotification">
<LicenseExpireTip />
<PasswordExpireTip />
</slot>
<div class="page-heading">
<el-row :gutter="0">
<el-col :span="16" class="page-heading-left">
<slot><h2>{{ title }}</h2></slot>
</el-col>
<el-col :span="8">
<div class="page-heading-right">
<slot name="rightSide" />
</div>
</el-col>
</el-row>
</div>
</div>
<!-- <Breadcrumb />-->
</template>
<script>
import LicenseExpireTip from '@/layout/components/Page/LicenseExpireTip'
import PasswordExpireTip from '@/layout/components/Page/PasswordExpireTip'
export default {
name: 'PageHeading',
components: {
LicenseExpireTip,
PasswordExpireTip
},
props: {
title: {
type: String,

View File

@@ -0,0 +1,55 @@
<template>
<div>
<el-alert v-if="isExpire" type="error">
{{ isExpire }}
</el-alert>
</div>
</template>
<script>
import { toSafeLocalDateStr } from '@/utils/common'
import { mapGetters } from 'vuex'
export default {
name: 'PasswordExpireTip',
data() {
return {
loading: true,
securityData: {}
}
},
computed: {
...mapGetters([
'publicSettings',
'currentUser'
]),
isExpire() {
// 用户来源不是Local时不显示密码过期提示
if (this.currentUser.source !== 'local') {
return false
}
const intervalTime = this.getIntervalDays(this.currentUser.date_password_last_updated)
const securityPasswordExpirationTime = this.publicSettings.SECURITY_PASSWORD_EXPIRATION_TIME
if (intervalTime >= securityPasswordExpirationTime) {
return this.$t('users.passwordExpired')
}
if (securityPasswordExpirationTime - intervalTime <= 5) {
return this.$t('users.passwordWillExpiredPrefixMsg') + (securityPasswordExpirationTime - intervalTime) + this.$t('users.passwordWillExpiredSuffixMsg')
}
return false
}
},
methods: {
getIntervalDays(date) {
const dateExpired = new Date(toSafeLocalDateStr(date))
const dateNow = new Date()
const intervalTime = dateNow.getTime() - dateExpired.getTime()
return Math.floor(intervalTime / (24 * 3600 * 1000))
}
}
}
</script>
<style scoped>
</style>

View File

@@ -1,6 +1,6 @@
<template>
<div class="page">
<PageHeading>
<PageHeading class="disabled-when-print">
<slot name="title">{{ iTitle }}</slot>
<template #rightSide>
<slot name="headingRightSide" />
@@ -42,5 +42,15 @@ export default {
</script>
<style scoped>
@media print {
.disabled-when-print{
display: none;
}
.enabled-when-print{
display: inherit !important;
}
.print-margin{
margin-top: 10px;
}
}
</style>

View File

@@ -9,7 +9,14 @@
<div>
<el-tabs v-if="submenu.length > 0" slot="submenu" v-model="iActiveMenu" class="page-submenu" @tab-click="handleTabClick">
<el-tab-pane v-for="item in submenu" :key="item.name" :label="item.title" :label-content="item.labelContent" :name="item.name" />
<template v-for="item in submenu">
<el-tab-pane :key="item.name" :label-content="item.labelContent" :name="item.name" :disabled="item.disabled">
<span slot="label">
{{ item.title }}
<slot name="badge" :tab="item.name" />
</span>
</el-tab-pane>
</template>
</el-tabs>
<transition name="fade-transform" mode="out-in">
<slot />
@@ -73,22 +80,23 @@ export default {
},
getPropActiveTab() {
let activeTab = ''
let tabObj = null
const activeTabs = [
const preActiveTabs = [
this.$route.query[ACTIVE_TAB_KEY],
this.$cookie.get(ACTIVE_TAB_KEY),
this.activeMenu
]
for (activeTab of activeTabs) {
tabObj = this.tabIndices[activeTab]
if (tabObj !== undefined) {
return activeTab
for (const preTab of preActiveTabs) {
for (const tabName in this.tabIndices) {
if (preTab && tabName && preTab.toLowerCase() === tabName.toLowerCase()) {
return tabName
}
}
}
return this.submenu[0].name
activeTab = this.submenu[0].name
return activeTab
}
}
}

View File

@@ -1,14 +1,14 @@
<template>
<div :class="classObj" class="app-wrapper">
<div v-if="device==='mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
<NavBar class="sidebar-container" />
<NavBar class="sidebar-container disabled-when-print" />
<div :class="{hasTagsView:needTagsView}" class="main-container">
<div :class="{'fixed-header':fixedHeader}">
<div :class="{'fixed-header':fixedHeader}" class="disabled-when-print">
<NavHeader />
<tags-view v-if="needTagsView" />
</div>
<app-main />
<Footer />
<Footer class="disabled-when-print" />
</div>
</div>
</template>
@@ -98,4 +98,28 @@ export default {
.mobile .fixed-header {
width: 100%;
}
@media print {
.disabled-when-print{
display: none;
width: 100%;
}
.enabled-when-print{
display: inherit !important;
}
.print-margin{
margin-top: 10px;
}
.drawer-bg{
display: none;
}
.main-container{
margin-left: 0px !important;
}
//.fixed-header{
// width: 100% !important;
//}
//.hideSidebar .fixed-header{
// width: 100% !important;
//}
}
</style>

42
src/router/acl.js Normal file
View File

@@ -0,0 +1,42 @@
import i18n from '@/i18n/i18n'
import empty from '@/layout/empty'
export default [
{
path: 'asset-acl',
component: empty,
redirect: '',
meta: {
title: i18n.t('route.AssetAclList'),
licenseRequired: true
},
children: [
{
path: '',
name: 'AssetAclList',
component: () => import('@/views/acl/AssetAcl/AssetAclList'),
meta: { title: i18n.t('route.AssetAclList'), activeMenu: '/acl/asset-acl' }
},
{
path: 'create',
name: 'AssetAclCreate',
component: () => import('@/views/acl/AssetAcl/AssetAclCreateUpdate'),
meta: { title: i18n.t('route.AssetAclCreate'), activeMenu: '/acl/asset-acl' },
hidden: true
},
{
path: ':id',
name: 'AssetAclDetail',
component: () => import('@/views/acl/AssetAcl/AssetAclDetail'),
meta: { title: i18n.t('route.AssetAclDetail'), activeMenu: '/acl/asset-acl' },
hidden: true
},
{
path: ':id/update',
name: 'AssetAclUpdate',
component: () => import('@/views/acl/AssetAcl/AssetAclCreateUpdate'),
meta: { title: i18n.t('route.AssetAclUpdate'), activeMenu: '/acl/asset-acl' },
hidden: true
}
]
}
]

View File

@@ -34,6 +34,13 @@ export default [
component: () => import('@/views/assets/Asset/AssetCreateUpdate.vue'),
meta: { title: i18n.t('route.AssetUpdate'), activeMenu: '/assets/assets' },
hidden: true
},
{
path: 'detail/:id/update',
name: 'AssetMoreInformationEdit',
component: () => import('@/views/assets/Asset/AssetMoreInformationEdit.vue'),
meta: { title: i18n.t('common.UpdateAssetDetail'), activeMenu: '/assets/assets' },
hidden: true
}
]
},

View File

@@ -45,5 +45,12 @@ export default [
name: 'CeleryTaskLog',
hidden: true,
meta: { title: i18n.t('route.CeleryTaskLog'), roles: ['SuperAdmin', 'Admin', 'Auditor', 'User'] }
},
{
path: '/ops/task/task/:id/log/',
component: () => import('@/views/ops/CeleryTaskLog'),
name: 'TaskLog',
hidden: true,
meta: { title: i18n.t('route.CeleryTaskLog'), roles: ['SuperAdmin', 'Admin', 'Auditor', 'User'] }
}
]

View File

@@ -36,6 +36,7 @@ import OpsRoutes from './ops'
import TicketsRoutes from './tickets'
import AuditsRoutes from './audits'
import commonRoutes from './common'
import aclRoutes from './acl'
/**
* constantRoutes
@@ -117,6 +118,17 @@ export const allRoleRoutes = [
meta: { title: i18n.t('route.Perms'), icon: 'edit' },
children: PermsRoute
},
{
path: '/acl/',
component: Layout,
redirect: '/perms/access-control-list/',
name: 'Acl',
meta: {
licenseRequired: true,
title: i18n.t('route.Acl'),
icon: 'fort-awesome' },
children: aclRoutes
},
{
path: '/terminal/',
component: Layout,
@@ -140,8 +152,12 @@ export const allRoleRoutes = [
component: Layout,
redirect: '/tickets/tickets/',
children: TicketsRoutes,
hidden: true,
meta: {
licenseRequired: true
// hidden: ({ settings }) => {
// return !settings.TICKETS_ENABLED
// }
}
},
{

View File

@@ -34,7 +34,7 @@ export default [
path: 'command-executions/create',
name: 'BatchCommand',
component: () => import('@/views/ops/CommandExecution'),
meta: { title: i18n.t('route.BatchCommand'), commandExecutionRequired: true }
meta: { title: i18n.t('route.BatchCommand'), hidden: ({ settings }) => !settings.SECURITY_COMMAND_EXECUTION }
},
// {
// path: 'celery/task/:id',
@@ -44,7 +44,7 @@ export default [
// meta: { title: i18n.t('route.CeleryTaskLog') }
// },
{
path: `${BASE_URL}/core/flower?_=${Date.now()}`,
path: `${BASE_URL}/core/flower/?_=${Date.now()}`,
name: 'TaskMonitor',
// component: () => window.open(`/core/flower?_=${Date.now()}`),
meta: { title: i18n.t('route.TaskMonitor'), permissions: [rolec.PERM_SUPER] }

View File

@@ -31,107 +31,45 @@ const assetPermissionRoutes = [
}
]
const remoteAppPermissionRoutes = [
const ApplicationPermissionRoutes = [
{
path: 'remote-app-permissions',
path: 'app-permissions',
component: empty,
meta: { title: i18n.t('route.RemoteAppPermission'), licenseRequired: true },
meta: { title: i18n.t('route.ApplicationPermission') },
redirect: '',
children: [
{
path: '',
name: 'RemoteAppPermissionList',
component: () => import('@/views/perms/RemoteAppPermission/RemoteAppPermissionList'),
meta: { title: i18n.t('route.RemoteAppPermission'), activeMenu: '/perms/remote-app-permissions' }
name: 'ApplicationPermissionList',
component: () => import('@/views/perms/ApplicationPermission/ApplicationPermissionList'),
meta: { title: i18n.t('route.ApplicationPermission'), activeMenu: '/perms/app-permissions' }
},
{
path: 'create',
component: () => import('@/views/perms/RemoteAppPermission/RemoteAppPermissionCreateUpdate'),
name: 'RemoteAppPermissionCreate',
component: () => import('@/views/perms/ApplicationPermission/ApplicationPermissionCreateUpdate'),
name: 'ApplicationPermissionCreate',
hidden: true,
meta: { title: i18n.t('route.RemoteAppPermissionCreate'), activeMenu: '/perms/remote-app-permissions', action: 'create' }
},
{
path: 'update',
component: () => import('@/views/perms/RemoteAppPermission/RemoteAppPermissionCreateUpdate'),
name: 'RemoteAppPermissionUpdate',
hidden: true,
meta: { title: i18n.t('route.RemoteAppPermissionUpdate'), activeMenu: '/perms/remote-app-permissions', action: 'update' }
meta: { title: i18n.t('route.ApplicationPermissionCreate'), activeMenu: '/perms/app-permissions', action: 'create' }
},
{
path: ':id',
component: () => import('@/views/perms/RemoteAppPermission/RemoteAppPermissionDetail/index'),
name: 'RemoteAppPermissionDetail',
component: () => import('@/views/perms/ApplicationPermission/ApplicationPermissionDetail/index'),
name: 'ApplicationPermissionDetail',
hidden: true,
meta: { title: i18n.t('route.RemoteAppPermissionDetail'), activeMenu: '/perms/remote-app-permissions' }
meta: { title: i18n.t('route.ApplicationPermissionDetail'), activeMenu: '/perms/app-permissions' }
},
{
path: ':id/update',
component: () => import('@/views/perms/ApplicationPermission/ApplicationPermissionCreateUpdate'),
name: 'ApplicationPermissionUpdate',
hidden: true,
meta: { title: i18n.t('route.ApplicationPermissionUpdate'), activeMenu: '/perms/app-permissions', action: 'update' }
}]
}
]
const databasePermissionRoutes = [
{
path: 'database-app-permissions',
name: 'DatabaseAppPermissionList',
component: () => import('@/views/perms/DatabaseAppPermission/DatabaseAppPermissionList'),
meta: { title: i18n.t('route.DatabaseAppPermission') }
},
{
path: 'database-app-permissions/create',
component: () => import('@/views/perms/DatabaseAppPermission/DatabaseAppPermissionCreateUpdate'), // Parent router-view
name: 'DatabaseAppPermissionCreate',
hidden: true,
meta: { title: i18n.t('route.DatabaseAppPermissionCreate'), activeMenu: '/perms/database-app-permissions' }
},
{
path: 'database-app-permissions/update',
component: () => import('@/views/perms/DatabaseAppPermission/DatabaseAppPermissionCreateUpdate'), // Parent router-view
name: 'DatabaseAppPermissionUpdate',
hidden: true,
meta: { title: i18n.t('route.DatabaseAppPermissionUpdate'), activeMenu: '/perms/database-app-permissions', action: 'update' }
},
{
path: 'database-app-permissions/:id',
component: () => import('@/views/perms/DatabaseAppPermission/DatabaseAppPermissionDetail/index'),
name: 'DatabaseAppPermissionDetail',
hidden: true,
meta: { title: i18n.t('route.DatabaseAppPermissionDetail'), activeMenu: '/perms/database-app-permissions' }
}
]
const kubernetesPermissionRoutes = [
{
path: 'kubernetes-app-permissions',
name: 'KubernetesAppPermissionList',
component: () => import('@/views/perms/KubernetesAppPermission/KubernetesAppPermissionList'),
meta: { title: i18n.t('route.KubernetesAppPermission') }
},
{
path: 'kubernetes-app-permissions/create',
component: () => import('@/views/perms/KubernetesAppPermission/KubernetesAppPermissionCreateUpdate'), // Parent router-view
name: 'KubernetesAppPermissionCreate',
hidden: true,
meta: { title: i18n.t('route.KubernetesAppPermissionCreate'), activeMenu: '/perms/kubernetes-app-permissions' }
},
{
path: 'kubernetes-app-permissions/update',
component: () => import('@/views/perms/KubernetesAppPermission/KubernetesAppPermissionCreateUpdate'), // Parent router-view
name: 'KubernetesAppPermissionUpdate',
hidden: true,
meta: { title: i18n.t('route.KubernetesAppPermissionUpdate'), activeMenu: '/perms/kubernetes-app-permissions', action: 'update' }
},
{
path: 'kubernetes-app-permissions/:id',
component: () => import('@/views/perms/KubernetesAppPermission/KubernetesAppPermissionDetail/index'),
name: 'KubernetesAppPermissionDetail',
hidden: true,
meta: { title: i18n.t('route.KubernetesAppPermissionDetail'), activeMenu: '/perms/kubernetes-app-permissions' }
}
]
export default [
... assetPermissionRoutes,
... remoteAppPermissionRoutes,
... databasePermissionRoutes,
... kubernetesPermissionRoutes
... ApplicationPermissionRoutes
]

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