Compare commits

..

170 Commits

Author SHA1 Message Date
ibuler
48beb502bd fix(settings): 修复系统设置中邮件没有更改就无法更新的问题 2021-06-28 19:04:49 +08:00
ibuler
383a799c6a fix: 修复会话详情中播放bug 2021-06-18 18:07:45 +08:00
ibuler
c378b2bf0d fix(infra): 修复基础平台等可以删除更新问题 2021-06-18 11:05:17 +08:00
Jiangjie.Bai
95ccc5bc16 Merge pull request #869 from jumpserver/dev
Dev
2021-06-17 15:23:54 +08:00
ibuler
dc112bfee8 fix: 修复创建资产 2021-06-17 15:23:15 +08:00
ibuler
84930b27fa fix: 修复切换组织时的错误提示 2021-06-17 15:22:37 +08:00
Jiangjie.Bai
2bf1496480 Merge pull request #866 from jumpserver/dev
v2.11.0 rc5
2021-06-17 12:23:58 +08:00
Bai
27a78c3b23 fix: 修改应用账号(k8s)导出/查看均显示token字段 2021-06-17 12:13:41 +08:00
ibuler
95007a52a5 fix(assets): 修复资产创建时,没有把节点带过去的问题 2021-06-17 12:00:51 +08:00
Jiangjie.Bai
50850af382 Merge pull request #863 from jumpserver/dev
v2.11.0 rc5
2021-06-17 11:33:42 +08:00
Bai
92097e11e5 fix: 修改账号管理样式 2021-06-17 11:31:09 +08:00
Jiangjie.Bai
02e1299cbd Merge pull request #859 from jumpserver/dev
v2.11.0 rc5 (2)
2021-06-16 19:48:27 +08:00
ibuler
bccaaac1c7 perf: 优化系统用户详情信息 2021-06-16 19:10:40 +08:00
ibuler
3b91b60bc2 perf: 优化工单申请
去掉debug
2021-06-16 18:38:43 +08:00
ibuler
dca0753492 perf(setting): 优化安全设置中的,危险命令告警 2021-06-16 18:03:47 +08:00
Bai
d1920595d8 fix: 列表页添加org_name列表名 2021-06-16 14:29:49 +08:00
Bai
97638bc7a3 fix: 收集用户列表去掉asset搜索项 2021-06-16 14:29:49 +08:00
Bai
f8ab8035e5 fix: 修改云管账号详情页validity显示字段 2021-06-16 14:29:49 +08:00
Jiangjie.Bai
39c1aa1e7e Merge pull request #853 from jumpserver/dev
v2.11.0 rc5
2021-06-16 13:05:08 +08:00
ibuler
6f07c36393 fix(sessions): 修复监控地址错误的问题 2021-06-16 12:26:44 +08:00
Jiangjie.Bai
b700d0fc83 Merge pull request #850 from jumpserver/dev
v2.11.0 rc3
2021-06-15 14:50:23 +08:00
xinwen
2d7fd30da1 fix: 修复站内信 WebSocket 问题 2021-06-15 14:48:12 +08:00
Jiangjie.Bai
99b852d552 Merge pull request #848 from jumpserver/dev
v2.11.0 rc2
2021-06-15 10:51:38 +08:00
Jiangjie.Bai
37234f0e20 Merge pull request #840 from jumpserver/pr@dev@fix_accounts
fix: 修复账号管理-资产账号导出需要进行MFA认证样式
2021-06-15 10:43:14 +08:00
ibuler
da87848f44 fix: 去掉执行任务中的clone按钮 2021-06-11 18:12:03 +08:00
ibuler
2aa363f6ce perf: 优化websocket,实时获取数量
perf: 优化提示i18n

perf: remove yarn

..
2021-06-11 18:11:38 +08:00
Bai
97a98ace03 perf: 优化账号管理-列表2 2021-06-11 13:54:16 +08:00
Bai
f53be25e19 perf: 优化账号管理-列表 2021-06-11 11:48:07 +08:00
ibuler
3c36b50c35 fix: 修复es添加报错问题 2021-06-11 11:25:12 +08:00
Bai
eb75122a76 fix: 修复账号管理-应用账号导出需要进行MFA认证 2021-06-11 11:03:40 +08:00
fit2bot
9f3a1e082b fix: 修复我的资产页面button消失的bug (#844)
* perf: 修改命令存储创建

* fix: 修复我的资产页面button消失的bug

Co-authored-by: ibuler <ibuler@qq.com>
2021-06-11 10:44:52 +08:00
fit2bot
ca8307338f perf: 继续优化名称 (#842)
* perf: 修改命令存储创建

* perf: 继续优化名称

xxx

Co-authored-by: ibuler <ibuler@qq.com>
2021-06-10 19:18:32 +08:00
fit2bot
ee53941fb0 fix(settings): 修复ldap提交时,json报错 (#836)
* perf: 修改命令存储创建

* fix(settings): 修复ldap提交时,json报错

Co-authored-by: ibuler <ibuler@qq.com>
2021-06-10 19:05:08 +08:00
Bai
6d7fec5bf9 fix: 修复账号管理-资产账号导出需要进行MFA认证搜索项 2021-06-10 17:10:31 +08:00
Bai
d31492f43b fix: 修复账号管理-资产账号导出需要进行MFA认证样式 2021-06-10 16:56:56 +08:00
Bai
5056c8cbf9 fix: 修复账号管理-资产账号导出需要进行MFA认证 2021-06-10 16:52:28 +08:00
Jiangjie.Bai
00cf2c34b9 Merge pull request #835 from jumpserver/dev
v2.11.0 rc1
2021-06-10 14:01:52 +08:00
fit2bot
c15f22e05b perf: 优化一下,暂时不支持查看全部站内信 (#833)
* perf: 修改命令存储创建

* perf: 优化一下,暂时不支持查看全部站内信

Co-authored-by: ibuler <ibuler@qq.com>
2021-06-10 12:58:25 +08:00
fit2bot
283dfc97ec perf: 系统监控中添加 xrdp (#834)
* perf: 修改命令存储创建

* perf: 系统监控中添加 xrdp

perf: gua

Co-authored-by: ibuler <ibuler@qq.com>
2021-06-10 12:53:01 +08:00
ibuler
a13c009b37 perf: 优化站内信和xrdp监控
perf: 优化 can has

perf: 修改提示i18n
2021-06-09 19:59:31 +08:00
fit2bot
2494583098 perf: 优化session监控 (#830)
* perf: 修改命令存储创建

* perf: 优化session监控

Co-authored-by: ibuler <ibuler@qq.com>
2021-06-09 15:43:00 +08:00
fit2bot
2bb1fce491 feat: 添加rdp配置 (#828)
* perf: 修改命令存储创建

* feat: 添加rdp配置

* perf: 优化xrdp显示位置

Co-authored-by: ibuler <ibuler@qq.com>
2021-06-08 15:26:40 +08:00
Bai
49e2c547e7 fix: 修改账号管理列表相关问题 2021-06-08 11:38:26 +08:00
老广
0bac33c9fc Merge pull request #822 from jumpserver/pr@dev@feat_subscription
feat: 系统消息订阅页面
2021-06-08 11:23:14 +08:00
ibuler
1b3f5403dd perf: 国际化 2021-06-07 20:05:36 +08:00
xinwen
b4fd09e308 去掉websocket 2021-06-07 19:40:22 +08:00
xinwen
96b002eccb 添加标记已读与websockt 2021-06-07 17:09:10 +08:00
ibuler
15e253804e perf: 优化msg 2021-06-07 15:47:13 +08:00
fit2bot
8427cb4e22 feat(terminal): 危险命令变色 (#825)
* perf: 修改命令存储创建

* feat(terminal): 危险命令变色

* perf: 去掉那个标签

Co-authored-by: ibuler <ibuler@qq.com>
2021-06-07 10:47:57 +08:00
Jiangjie.Bai
c8c6cbc3b1 Merge pull request #826 from jumpserver/feat_account_manager
feat: 添加账号管理模块
2021-06-04 11:21:01 +08:00
xinwen
ef841149de feat: 添加站内信页面 2021-06-04 10:41:20 +08:00
Bai
b51783d98f feat: 账号管理(翻译) 2021-06-03 13:10:18 +08:00
dependabot[bot]
80ff4064e6 chore(deps): bump browserslist from 4.10.0 to 4.16.6 (#820)
* chore: 更新Submodule指向

* fix: 修复Build失败的问题

* fix(clone): 修复会话页面产生的clone的问题

* chore(deps): bump browserslist from 4.10.0 to 4.16.6

Bumps [browserslist](https://github.com/browserslist/browserslist) from 4.10.0 to 4.16.6.
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/browserslist/browserslist/compare/4.10.0...4.16.6)

Signed-off-by: dependabot[bot] <support@github.com>

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: Eric_Lee <xplzv@126.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-02 19:26:46 +08:00
Bai
091038de95 feat: 优化收集用户列表 2021-06-02 18:53:29 +08:00
ibuler
cc994b8ecd perf: logo和文案
perf: 去掉title
2021-06-02 18:11:34 +08:00
dependabot[bot]
73d4ec0fa5 chore(deps): bump dns-packet from 1.3.1 to 1.3.4 (#821)
* chore: 更新Submodule指向

* fix: 修复Build失败的问题

* fix(clone): 修复会话页面产生的clone的问题

* chore(deps): bump dns-packet from 1.3.1 to 1.3.4

Bumps [dns-packet](https://github.com/mafintosh/dns-packet) from 1.3.1 to 1.3.4.
- [Release notes](https://github.com/mafintosh/dns-packet/releases)
- [Changelog](https://github.com/mafintosh/dns-packet/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mafintosh/dns-packet/compare/v1.3.1...v1.3.4)

Signed-off-by: dependabot[bot] <support@github.com>

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: Eric_Lee <xplzv@126.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-02 17:52:15 +08:00
Bai
8ee39c036f feat: 资产账号列表添加 BackendDisplay 和 Name 字段 2021-06-02 17:01:34 +08:00
xinwen
e8335d9c73 feat: 系统消息订阅页面 2021-06-01 17:48:36 +08:00
Bai
f720eaf05a feat: 账号管理模块(基本完成) 2021-05-28 17:19:35 +08:00
Bai
7f118d7074 feat: 移动 xpack/ChangeAuthPlan > accounts/CahngeAuthPlan 2 2021-05-28 16:53:08 +08:00
Bai
a1ad901b40 feat: 移动 xpack/ChangeAuthPlan > accounts/CahngeAuthPlan 2021-05-28 16:50:40 +08:00
Bai
27b39ad910 feat: 移除 xpack/vault 2021-05-28 16:00:17 +08:00
Bai
f053a37606 feat: 移动 xpack/GatheredUser > accounts/GatheredUser 2021-05-28 15:40:34 +08:00
Bai
dcb94f4aac feat: 优化应用账号(操作按钮: 更新) 2021-05-28 11:35:18 +08:00
Bai
91cbe97a5b feat: 优化应用账号(应用列表选中行、应用用户列表背景色) 2021-05-28 11:23:49 +08:00
Bai
d3053fdf18 feat: 优化资产账号(资产列表选中行及资产用户列表背景色) 2021-05-27 16:45:55 +08:00
Bai
226f17c4fd feat: 优化资产账号(资产树的显示/隐藏) 2021-05-27 14:10:06 +08:00
Bai
34a30571c1 feat: 添加应用账号模块(基本完成,细节待优化) 2021-05-26 18:38:02 +08:00
Bai
0a19829c4a feat: 添加资产账号模块(基本完成,细节待优化) 2021-05-26 17:23:42 +08:00
Bai
09a252f282 feat: 添加账号管理路由 2021-05-25 12:51:28 +08:00
Michael Bai
c7c6f5ac82 fix: 修复导入模版生成table时,没有显示number字段类型所在列的问题 2021-05-23 22:00:30 -05:00
Michael Bai
f3b15727cb fix: 修改用户创建/更新密码策略选项值 2021-05-23 21:58:28 -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
204 changed files with 6617 additions and 2304 deletions

View File

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

BIN
dump.rdb Normal file

Binary file not shown.

View File

@@ -1,6 +1,16 @@
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/;

View File

@@ -15,18 +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",
@@ -41,8 +46,10 @@
"lodash.set": "^4.3.2",
"lodash.topairs": "^4.3.0",
"lodash.values": "^4.3.0",
"moment": "^2.29.1",
"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",
@@ -73,6 +80,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

@@ -8,7 +8,6 @@
<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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

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 {

View File

@@ -1,58 +1,75 @@
<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="iTableConfig" :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>
<Dialog :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 :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>
</el-radio-group>
</el-form-item>
</el-form>
</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 { createSourceIdCache } from '@/api/common'
import * as queryUtil from '@/components/DataTable/compenents/el-data-table/utils/query'
import { ActionsFormatter, DateFormatter } from '@/components/TableFormatters'
export default {
name: 'Detail',
@@ -65,6 +82,26 @@ export default {
type: String,
required: true
},
searchExclude: {
type: Array,
default: () => []
},
extraQuery: {
type: Object,
default: () => ({})
},
canExportAll: {
type: Boolean,
default: true
},
canExportSelected: {
type: Boolean,
default: true
},
canExportFiltered: {
type: Boolean,
default: true
},
hasLeftActions: {
type: Boolean,
default: false
@@ -92,6 +129,10 @@ export default {
hasClone: {
type: Boolean,
default: true
},
tableConfig: {
type: Object,
default: () => ({})
}
},
data() {
@@ -111,38 +152,38 @@ export default {
username: '',
hostname: '',
password: '',
key: ''
private_key: ''
},
tableConfig: {
selectedRows: '',
dialogStatus: '',
showExportDialog: false,
exportOption: 'all',
exportTypeOption: 'csv',
defaultTableConfig: {
url: this.url,
columns: [
{
prop: 'hostname',
columns: ['hostname', 'ip', 'username', 'version', 'date_created', 'actions'],
columnsMeta: {
'hostname': {
label: this.$t('assets.Hostname'),
showOverflowTooltip: true
},
{
prop: 'ip',
'ip': {
label: this.$t('assets.ip'),
width: '120px'
},
{
prop: 'username',
'username': {
label: this.$t('assets.Username'),
showOverflowTooltip: true
},
{
prop: 'version',
'version': {
label: this.$t('assets.Version'),
width: '70px'
},
{
prop: 'date_created',
'date_created': {
label: this.$t('assets.date_joined'),
formatter: DateFormatter
},
{
prop: 'id',
'actions': {
label: this.$t('common.Action'),
align: 'center',
width: 150,
@@ -158,6 +199,7 @@ export default {
title: this.$t('common.View'),
type: 'primary',
callback: function(val) {
this.dialogStatus = 'viewAutoInfo'
this.MFAInfo.asset = val.row.id
if (!this.needMFAVerify) {
this.showMFADialog = true
@@ -210,10 +252,8 @@ export default {
]
}
}
],
extraQuery: {
latest: 1
}
},
extraQuery: this.extraQuery || { latest: 1 }
},
headerActions: {
hasLeftActions: this.hasLeftActions,
@@ -222,6 +262,7 @@ export default {
hasExport: this.hasExport,
hasSearch: true,
searchConfig: {
exclude: this.searchExclude,
options: [
{
label: this.$t('assets.OnlyLatestVersion'),
@@ -255,25 +296,62 @@ export default {
const ttl = this.publicSettings.SECURITY_MFA_VERIFY_TTL
const now = new Date()
return !(this.MFAVerifyAt && (now - this.MFAVerifyAt) < ttl * 1000)
},
iTableConfig() {
const columnsMeta = Object.assign({}, this.defaultTableConfig.columnsMeta, this.tableConfig.columnsMeta || {})
const config = Object.assign(this.defaultTableConfig, this.tableConfig)
config.columnsMeta = columnsMeta
return config
},
exportOptions() {
return [
{
label: this.$t('common.imExport.ExportAll'),
value: 'all',
can: this.canExportAll
},
{
label: this.$t('common.imExport.ExportOnlySelectedItems'),
value: 'selected',
can: this.selectedRows.length > 0 && this.canExportSelected
},
{
label: this.$t('common.imExport.ExportOnlyFiltered'),
value: 'filtered',
can: this.tableHasQuery() && this.canExportFiltered
}
]
},
exportTypeOptions() {
return [
{
label: 'CSV',
value: 'csv',
can: true
},
{
label: 'Excel',
value: 'xlsx',
can: true
}
]
}
},
watch: {
url(iNew) {
this.$set(this.tableConfig, 'url', iNew)
this.$set(this.iTableConfig, 'url', iNew)
}
},
mounted() {
if (this.otherActions) {
const actionColumn = this.tableConfig.columns[this.tableConfig.columns.length - 1]
const actionColumn = this.iTableConfig.columns[this.iTableConfig.columns.length - 1]
for (const item of this.otherActions) {
actionColumn.formatterArgs.extraActions.push(item)
}
}
},
created() {
if (this.handleExport) {
this.headerActions.handleExport = this.handleExport
}
this.headerActions.handleExport = this.handleExport || this.defaultHandleExport
if (this.handleImport) {
this.headerActions.handleImport = this.handleImport
}
@@ -290,12 +368,17 @@ export default {
).then(
res => {
this.$store.dispatch('users/setMFAVerify')
this.$axios.get(`/api/v1/assets/asset-user-auth-infos/${this.MFAInfo.asset}/`).then(res => {
this.MFAConfirmed = true
this.MFAInfo.hostname = res.hostname
this.MFAInfo.password = res.password
this.MFAInfo.username = res.username
})
if (this.dialogStatus === 'export') {
this.showMFADialog = false
this.showExportDialog = true
} else {
this.$axios.get(`/api/v1/assets/asset-user-auth-infos/${this.MFAInfo.asset}/`).then(res => {
this.MFAConfirmed = true
this.MFAInfo.hostname = res.hostname
this.MFAInfo.password = res.password
this.MFAInfo.username = res.username
})
}
}
)
},
@@ -316,7 +399,7 @@ export default {
username: '',
hostname: '',
password: '',
key: ''
private_key: ''
}
this.showDialog = false
this.$refs.ListTable.reloadTable()
@@ -326,7 +409,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]
@@ -340,8 +423,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/`,
@@ -356,15 +439,91 @@ export default {
username: '',
hostname: '',
password: '',
key: ''
private_key: ''
}
this.showDialog = false
this.$refs.ListTable.reloadTable()
},
tableQuery() {
const listTableRef = this.$refs.ListTable
if (!listTableRef) {
return {}
}
const query = listTableRef.dataTable.getQuery()
delete query['limit']
delete query['offset']
delete query['date_from']
delete query['date_to']
return query
},
tableHasQuery() {
return Object.keys(this.tableQuery()).length > 0
},
defaultHandleExport({ selectedRows }) {
this.selectedRows = selectedRows
this.dialogStatus = 'export'
if (!this.needMFAVerify) {
this.showMFADialog = false
this.showExportDialog = true
} else {
this.showMFADialog = true
}
},
downloadCsv(url) {
const a = document.createElement('a')
a.href = url
a.click()
window.URL.revokeObjectURL(url)
},
async performExport(selectRows, exportOption, q) {
const url = `/api/v1/assets/asset-user-auth-infos/`
const query = Object.assign({}, q)
if (exportOption === 'selected') {
const resources = []
const data = selectRows
for (let index = 0; index < data.length; index++) {
resources.push(data[index].id)
}
const spm = await createSourceIdCache(resources)
query['spm'] = spm.spm
} else if (exportOption === 'filtered') {
// console.log(listTableRef)
// console.log(listTableRef.dataTable)
// delete query['limit']
// delete query['offset']
}
query['format'] = this.exportTypeOption
const queryStr =
(url.indexOf('?') > -1 ? '&' : '?') +
queryUtil.stringify(query, '=', '&')
return this.downloadCsv(url + queryStr)
},
async performExportConfirm() {
const listTableRef = this.$refs.ListTable
const query = listTableRef.dataTable.getQuery()
delete query['limit']
delete query['offset']
return this.performExport(this.selectedRows, this.exportOption, query)
},
async handleExportConfirm() {
await this.performExportConfirm()
this.showExportDialog = false
},
handleExportCancel() {
this.showExportDialog = false
}
}
}
</script>
<style lang='less' scoped>
<style lang='scss' scoped>
.export-item {
width: 100%;
display: block;
padding: 10px 20px;
}
.export-form >>> .el-form-item__label {
line-height: 2
}
</style>

View File

@@ -1,7 +1,8 @@
import Vue from 'vue'
import Select2 from '@/components/Select2'
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() {
@@ -109,19 +110,21 @@ export class FormFieldGenerator {
return field
}
generateField(name, fieldsMeta, remoteFieldsMeta) {
let field = { id: name, prop: name, el: {}, attrs: {}}
let field = { id: name, prop: name, el: {}, attrs: {}, rules: [] }
const remoteFieldMeta = remoteFieldsMeta[name] || {}
Vue.$log.debug('FieldsMeta: ', fieldsMeta, name)
const fieldMeta = fieldsMeta[name] || {}
Vue.$log.debug('FieldMeta is: ', fieldMeta)
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 = Object.assign(field.el || {}, fieldMeta.el || {})
field = Object.assign(field, fieldMeta || {}, { el: el })
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) {

View File

@@ -58,6 +58,10 @@ export default {
minColumns: {
type: Array,
default: () => []
},
url: {
type: String,
default: ''
}
},
data() {
@@ -67,15 +71,17 @@ export default {
}
},
mounted() {
this.$eventBus.$on('showColumnSettingPopover', () => {
this.showColumnSettingPopover = true
this.iCurrentColumns = this.currentColumns
this.$eventBus.$on('showColumnSettingPopover', ({ url }) => {
if (url === this.url) {
this.showColumnSettingPopover = true
this.iCurrentColumns = this.currentColumns
}
})
},
methods: {
handleColumnConfirm() {
this.showColumnSettingPopover = false
this.$emit('columnsUpdate', this.iCurrentColumns)
this.$emit('columnsUpdate', { columns: this.iCurrentColumns, url: this.url })
}
}
}

View File

@@ -5,6 +5,7 @@
:current-columns="popoverColumns.currentCols"
:total-columns-list="popoverColumns.totalColumnsList"
:min-columns="popoverColumns.minCols"
:url="config.url"
@columnsUpdate="handlePopoverColumnsChange"
/>
</div>
@@ -12,9 +13,10 @@
<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: {
@@ -228,7 +230,8 @@ export default {
const _tableConfig = localStorage.getItem('tableConfig')
? JSON.parse(localStorage.getItem('tableConfig'))
: {}
const configShowColumnsNames = _.get(_tableConfig[this.$route.name], 'showColumns', null)
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
@@ -248,7 +251,7 @@ export default {
min: minColumnsNames,
configShow: configShowColumnsNames
}
this.$log.debug('Cleaned colums show: ', this.cleanedColumnsShow)
this.$log.debug('Cleaned columns show: ', this.cleanedColumnsShow)
},
filterShowColumns() {
this.cleanColumnsShow()
@@ -264,13 +267,14 @@ export default {
this.popoverColumns.minCols = this.cleanedColumnsShow.min
this.$log.debug('Popover cols: ', this.popoverColumns)
},
handlePopoverColumnsChange(columns) {
// this.$log.debug('Columns change: ', columns)
handlePopoverColumnsChange({ columns, url }) {
this.$log.debug('Columns change: ', columns)
this.popoverColumns.currentCols = columns
const _tableConfig = localStorage.getItem('tableConfig')
? JSON.parse(localStorage.getItem('tableConfig'))
: {}
_tableConfig[this.$route.name] = {
const tableName = this.config.name || this.$route.name + '_' + newURL(url).pathname
_tableConfig[tableName] = {
'showColumns': columns
}
localStorage.setItem('tableConfig', JSON.stringify(_tableConfig))

View File

@@ -78,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]
@@ -100,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()
@@ -121,6 +129,7 @@ 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)
})
@@ -141,7 +150,7 @@ export default {
treeNode.name = treeNode.name + ' (' + assetsAmount + ')'
this.zTree.updateNode(treeNode)
this.$message.success(this.$t('common.updateSuccessMsg'))
})
}).finally(() => { this.refreshTree() })
},
onBodyMouseDown: function(event) {
const rMenuID = this.$refs.dataztree.$refs.ztree.iRMenuID
@@ -210,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()
@@ -242,7 +251,6 @@ export default {
})
},
refresh: function() {
},
getSelectedNodes: function() {
return this.zTree.getSelectedNodes()

View File

@@ -3,6 +3,7 @@
<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"
@@ -36,15 +37,13 @@
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 :disabled="!action.tip" :content="action.tip" placement="top">
<span>
<i v-if="action.fa" :class="'fa ' + action.fa" />{{ action.title }}
</span>
</el-tooltip>
<span v-else>
<i v-if="action.fa" :class="'fa ' + action.fa" />{{ action.title }}
</span>
</el-button>
</template>
</div>
</template>

View File

@@ -8,7 +8,27 @@ 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
}
export const JsonRequired = {
required: true,
trigger: 'change',
validator: (rule, value, callback) => {
try {
JSON.parse(value)
callback()
} catch (e) {
callback(new Error(i18n.t('common.InvalidJson')))
}
}
}

View File

@@ -167,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 {
@@ -426,7 +426,6 @@ export default {
onEdit: {
type: Function,
default(row) {
// console.log('On delete row')
}
},
/**
@@ -723,6 +722,10 @@ export default {
default(row, index) {
return true
}
},
totalData: {
type: Array,
default: null
}
},
data() {
@@ -810,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: {
@@ -828,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() {
@@ -844,6 +861,9 @@ export default {
}
}
}
if (this.totalData) {
this.getList()
}
},
methods: {
getQuery() {
@@ -877,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

View File

@@ -100,6 +100,9 @@ export default {
iListeners() {
return Object.assign({}, this.$listeners, this.tableConfig.listeners)
},
dataTable() {
return this.$refs.table
},
...mapGetters({
'globalTableConfig': 'tableConfig'
})

View File

@@ -55,7 +55,6 @@ export default {
},
data() {
return {
}
},
methods: {
@@ -74,4 +73,8 @@ export default {
/*padding-top: 10px;*/
}
.dialog-footer {
padding-right: 20px;
}
</style>

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 {

View File

@@ -115,8 +115,10 @@ export default {
}
},
mounted() {
this.$eventBus.$on('showExportDialog', (row) => {
this.showExportDialog = true
this.$eventBus.$on('showExportDialog', ({ selectedRows, url }) => {
if (url === this.url) {
this.showExportDialog = true
}
})
},
methods: {

View File

@@ -1,64 +1,73 @@
<template>
<Dialog
:title="$t('common.Import')"
:title="importTitle"
:visible.sync="showImportDialog"
:destroy-on-close="true"
:close-on-click-modal="false"
:loading-status="loadStatus"
@confirm="handleImportConfirm"
@cancel="handleImportCancel()"
width="80%"
class="importDialog"
:show-cancel="false"
:show-confirm="false"
@close="handleImportCancel"
>
<el-form label-position="left" style="padding-left: 50px">
<el-form-item :label="$t('common.fileType' )" :label-width="'100px'">
<el-radio-group v-model="importTypeOption">
<el-radio v-for="option of importTypeOptions" :key="option.value" class="export-item" :label="option.value" :disabled="!option.can">{{ option.label }}</el-radio>
</el-radio-group>
</el-form-item>
<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: {
@@ -73,103 +82,119 @@ export default {
data() {
return {
showImportDialog: false,
importOption: '1',
isCsv: true,
importOption: 'create',
errorMsg: '',
loadStatus: false,
importTypeOption: 'csv'
importTypeOption: 'csv',
importTypeIsCsv: true,
showTable: false,
renderError: '',
hasFileFormatOrSizeError: false,
jsonData: {}
}
},
computed: {
hasSelected() {
return this.selectedRows.length > 0
},
importTypeOptions() {
return [
{
label: 'CSV',
value: 'csv',
can: true
},
{
label: 'Excel',
value: 'xlsx',
can: true
}
]
},
upLoadUrl() {
return this.url
},
downloadImportTempUrl() {
const format = this.importTypeOption === 'csv' ? 'format=csv&template=import&limit=1' : 'format=xlsx&template=import&limit=1'
const url = (this.url.indexOf('?') === -1) ? `${this.url}?${format}` : `${this.url}&${format}`
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': this.importTypeOption === 'csv' ? 'text/csv' : 'text/xlsx' }, 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': this.importTypeOption === 'csv' ? 'text/csv' : 'text/xlsx' }, disableFlashErrorMsg: true }
).then((data) => {
const msg = this.$t('common.imExport.createSuccessMsg', { count: data.length })
this.onSuccess(msg)
}).catch(error => {
this.catchError(error)
}).finally(() => {
this.loadStatus = false
})
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 = ''
@@ -181,40 +206,14 @@ export default {
a.click()
window.URL.revokeObjectURL(url)
},
handleImport(item) {
this.loadStatus = true
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 format = this.importTypeOption === 'csv' ? '?format=csv&template=update&spm=' : '?format=xlsx&template=update&spm='
const url = `${baseUrl}${format}` + spm.spm
return this.downloadCsv(url)
},
async handleImportConfirm() {
this.$refs.upload.submit()
this.$refs['importTable'].performUpload()
},
handleImportCancel() {
this.showImportDialog = false
},
beforeUpload(file) {
this.isCsv = this.importTypeOption === 'csv' ? _.endsWith(file.name, 'csv') : _.endsWith(file.name, 'xlsx')
if (!this.isCsv) {
this.$message.error(
this.$t('common.NeedSpecifiedFile')
)
}
return this.isCsv
this.showTable = false
this.renderError = ''
this.jsonData = {}
}
}
}
@@ -231,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

@@ -32,7 +32,7 @@ export default {
default: ''
},
createRoute: {
type: [String, Object],
type: [String, Object, Function],
default: function() {
return this.$route.name.replace('List', 'Create')
}
@@ -149,10 +149,13 @@ export default {
},
methods: {
handleCreate() {
let route = {}
let route
if (typeof this.createRoute === 'string') {
route = { name: this.createRoute }
route.name = this.createRoute
} else {
} else if (typeof this.createRoute === 'function') {
route = this.createRoute()
} else if (typeof this.createRoute === 'object') {
route = this.createRoute
}
this.$router.push(route)

View File

@@ -1,6 +1,6 @@
<template>
<div>
<ActionsGroup :is-fa="true" :actions="rightSideActions" class="right-side-actions right-side-item" />
<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,21 +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() {
this.$eventBus.$emit('showColumnSettingPopover')
default: function({ selectedRows }) {
this.$eventBus.$emit('showColumnSettingPopover', { url: this.tableUrl })
}
},
hasRefresh: defaultTrue,

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

@@ -1,116 +0,0 @@
<template>
<ActionsGroup v-loading="loadStatus" :size="'mini'" :actions="actions" :more-actions="moreActions" />
</template>
<script>
import ActionsGroup from '@/components/ActionsGroup/index'
import BaseFormatter from './base'
export default {
name: 'LoadingActionsFormatter',
components: { ActionsGroup },
extends: BaseFormatter,
props: {
formatterArgsDefault: {
type: Object,
default: function() {
return {
hasUpdate: true, // can set function(row, value)
canUpdate: true, // can set function(row, value)
hasDelete: true, // can set function(row, value)
canDelete: true,
updateRoute: this.$route.name.replace('List', 'Update'),
extraActions: [] // format see defaultActions
}
}
}
},
data() {
const colActions = Object.assign(this.formatterArgsDefault, this.col.formatterArgs)
const defaultActions = [
{
name: 'update',
title: this.$t('common.Update'),
type: 'primary',
has: colActions.hasUpdate,
can: colActions.canUpdate,
callback: colActions.onUpdate
},
{
name: 'delete',
title: this.$t('common.Delete'),
type: 'danger',
has: colActions.hasDelete,
can: colActions.canDelete,
callback: colActions.onDelete
}
]
return {
colActions: colActions,
defaultActions: defaultActions,
extraActions: colActions.extraActions
}
},
computed: {
cleanedActions() {
let actions = [...this.defaultActions, ...this.extraActions]
actions = _.cloneDeep(actions)
actions = actions.map((v) => {
v.has = this.cleanBoolean(v, 'has')
v.can = this.cleanBoolean(v, 'can')
v.fa = this.cleanFa(v, 'fa')
v.callback = this.cleanCallback(v)
return v
})
actions = actions.filter((v) => v.has)
return actions
},
actions() {
if (this.cleanedActions.length <= 2) {
return this.cleanedActions
}
return this.cleanedActions.slice(0, 1)
},
moreActions() {
if (this.cleanedActions.length <= 2) {
return []
}
return this.cleanedActions.slice(1, this.cleanedActions.length)
},
loadStatus() {
return this.col.formatterArgs.loading
}
},
methods: {
cleanBoolean(item, attr) {
const ok = item[attr]
if (typeof ok !== 'function') {
return ok === undefined ? true : ok
}
return ok(this.row, this.cellValue)
},
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) }
},
cleanFa(item, attr) {
const ok = item[attr]
if (typeof ok !== 'function') {
return ok === undefined ? false : ok
}
return ok(this.row, this.cellValue)
}
}
}
</script>
<style scoped>
</style>

View File

@@ -45,17 +45,6 @@ export default {
dataTable() {
return this.$refs.dataTable.$refs.dataTable
},
// hasCreateAction() {
// const hasLeftAction = this.headerActions.hasLeftActions
// if (hasLeftAction === false) {
// return false
// }
// const hasCreate = this.headerActions.hasCreate
// if (hasCreate === false) {
// return false
// }
// return true
// },
iTableConfig() {
const config = deepmerge(this.tableConfig, { extraQuery: this.extraQuery })
this.$log.debug('Header actions', this.headerActions)

View File

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

View File

@@ -41,7 +41,7 @@
</template>
<script>
import Select2 from '../Select2'
import Select2 from '../FormFields/Select2'
import IBox from '../IBox'
import { createSourceIdCache } from '@/api/common'
import { mapGetters } from 'vuex'

View File

@@ -1,9 +1,9 @@
<template>
<ActionsGroup :size="'mini'" :actions="actions" :more-actions="moreActions" :more-actions-title="moreActionsTitle" />
<ActionsGroup v-loading="loadingStatus" :size="'mini'" :actions="actions" :more-actions="moreActions" :more-actions-title="moreActionsTitle" />
</template>
<script>
import ActionsGroup from '@/components/ActionsGroup/index'
import ActionsGroup from '@/components/ActionsGroup'
import BaseFormatter from './base'
const defaultPerformDelete = function({ row, col }) {
@@ -144,10 +144,12 @@ export default {
let actions = [...this.defaultActions, ...this.extraActions]
actions = _.cloneDeep(actions)
actions = actions.map((v) => {
v.has = this.cleanBoolean(v, 'has')
v.can = this.cleanBoolean(v, 'can')
v.callback = this.cleanCallback(v)
v.has = this.cleanBoolean(v, 'has', true)
v.can = this.cleanBoolean(v, 'can', true)
v.callback = this.cleanCallback(v, 'callback')
v.fa = this.cleanValue(v, 'fa')
v.order = v.order || 100
v.tip = this.cleanValue(v, 'tip')
return v
})
actions = actions.filter((v) => v.has)
@@ -165,18 +167,21 @@ export default {
return []
}
return this.cleanedActions.slice(1, this.cleanedActions.length)
},
loadingStatus() {
return this.col.formatterArgs.loading
}
},
methods: {
cleanBoolean(item, attr) {
cleanBoolean(item, attr, defaults) {
const ok = item[attr]
if (typeof ok !== 'function') {
return ok === undefined ? true : ok
return ok === undefined ? defaults : ok
}
return ok(this.row, this.cellValue)
return this.cleanValue(item, attr)
},
cleanCallback(item) {
const callback = item.callback
cleanCallback(item, attr) {
const callback = item[attr]
const attrs = {
reload: this.reload,
row: this.row,
@@ -185,6 +190,20 @@ export default {
tableData: this.tableData
}
return () => { return callback.bind(this)(attrs) }
},
cleanValue(item, attr) {
const value = item[attr]
if (!value || typeof value !== 'function') {
return value
}
const attrs = {
reload: this.reload,
row: this.row,
col: this.col,
cellValue: this.cellValue,
tableData: this.tableData
}
return value(attrs)
}
}
}

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

@@ -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

@@ -7,9 +7,10 @@ import ActionsFormatter from './ActionsFormatter'
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,
@@ -22,8 +23,9 @@ export default {
SystemUserFormatter,
ShowKeyFormatter,
DialogDetailFormatter,
LoadingActionsFormatter,
ArrayFormatter
ArrayFormatter,
EditableInputFormatter,
StatusFormatter
}
export {
@@ -37,6 +39,7 @@ export {
SystemUserFormatter,
ShowKeyFormatter,
DialogDetailFormatter,
LoadingActionsFormatter,
ArrayFormatter
ArrayFormatter,
EditableInputFormatter,
StatusFormatter
}

View File

@@ -72,7 +72,6 @@ export default {
watch: {
treeConfig: {
handler(val) {
},
deep: true
}

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,5 +1,10 @@
{
"": "",
"accounts": {
"PleaseClickLeftAssetToViewAssetAccount": "资产账号列表,点击左侧资产进行查看",
"PleaseClickLeftApplicationToViewApplicationAccount": "应用账号列表,点击左侧应用进行查看",
"PleaseClickLeftAssetToViewGatheredUser": "收集用户列表,点击左侧资产进行查看"
},
"acl": {
"name": "名称",
"username": "用户名",
@@ -179,7 +184,7 @@
"TestAssetsConnective": "测试资产可连接性",
"TestConnection": "测试连接",
"Type": "类型",
"UnselectedAssets": "未选择资产",
"UnselectedAssets": "未选择资产或所选择的资产不支持SSH协议连接",
"UnselectedNodes": "未选择节点",
"UpdateAssetUserToken": "更新资产用户认证信息",
"Username": "用户名",
@@ -199,20 +204,23 @@
"ipDomain": "IP(域名)",
"HostProtocol": "主机协议",
"DatabaseProtocol": "数据库协议",
"OtherProtocol": "其它协议"
"OtherProtocol": "其它协议",
"PasswordOrToken": "密码 / 令牌"
},
"audits": {
"Hosts": "主机",
"RunUser": "运行用户",
"User": "用户",
"View": "查看"
"Username": "用户名",
"View": "查看",
"SystemUserName": "系统用户名"
},
"auth": {
"LoginRequiredMsg": "账号已退出,请重新登录",
"ReLogin": "重新登录"
},
"common": {
"ConnectWebSocketError": "连接 WebSocket 失败",
"Action": "动作",
"RequestTickets": "申请工单",
"Actions": "操作",
@@ -227,6 +235,8 @@
"UpdateAssetDetail": "配置更多信息",
"AddSuccessMsg": "添加成功",
"Auth": "认证",
"bind": "绑定",
"unbind": "解绑",
"PushSelected":"推送所选",
"BadRequestErrorMsg": "请求错误,请检查填写内容",
"BadRoleErrorMsg": "请求错误,无该操作权限",
@@ -258,10 +268,16 @@
"EnterForSearch": "按回车进行搜索",
"Export": "导出",
"Import": "导入",
"ContinueImport": "继续导入",
"Continue": "继续",
"Stop": "停止",
"Finished": "完成",
"Refresh": "刷新",
"Info": "提示",
"MFAConfirm": "MFA 认证",
"MFARequireForSecurity": "为了安全请输入MFA",
"PasswordConfirm": "密码认证",
"PasswordRequireForSecurity": "为了安全请输入密码",
"Members": "成员",
"More": "更多",
"Message": "消息",
@@ -295,6 +311,7 @@
"View": "查看",
"Yes": "是",
"action": "动作",
"User": "用户",
"activateSelected": "激活所选",
"bulkDeleteErrorMsg": "批量删除失败: ",
"bulkDeleteSuccessMsg": "批量删除成功",
@@ -320,16 +337,26 @@
"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": "有效",
@@ -367,7 +394,8 @@
"SPECIAL_CHAR_REQUIRED": "须包含特殊字符",
"MIN_LENGTH_ERROR": "密码最小长度 {0} 位"
},
"lastCannotBeDeleteMsg": "最后一项,不能被删除"
"lastCannotBeDeleteMsg": "最后一项,不能被删除",
"InvalidJson": "不是合法 JSON"
},
"dashboard": {
"ActiveAsset": "近期被登录过",
@@ -504,7 +532,11 @@
},
"route": {
"": "",
"Accounts": "账号管理",
"AssetAccount": "资产账号",
"ApplicationAccount": "应用账号",
"Ticket":"工单",
"CommandConfirm": "命令复核",
"AdminUserCreate": "创建管理用户",
"AdminUserDetail": "管理用户详情",
"AdminUserList": "管理用户",
@@ -554,6 +586,7 @@
"Acl": "访问控制",
"UserAclList": "用户登录",
"UserAclCreate": "创建用户登录规则",
"UserAclLists": "用户登录规则",
"UserAclUpdate": "更新用户登录规则",
"UserAclDetail": "用户登录规则详情",
"AssetAclList": "登录资产",
@@ -625,7 +658,9 @@
"UserUpdate": "更新用户",
"Users": "用户管理",
"WebFTP": "文件管理",
"WebTerminal": "Web终端"
"WebTerminal": "Web终端",
"Notifications": "通知",
"SiteMessageList": "站内信"
},
"sessions": {
"StorageConfiguration": "存储配置",
@@ -688,6 +723,7 @@
"common": "普通"
},
"Monitor": "监控",
"XRDPNotSupport": "RDP 客户端会话, 暂不支持监控",
"sessionMonitor": "监控",
"TerminateTaskSendSuccessMsg": "终断任务已下发,请稍后刷新查看",
"helpText": {
@@ -699,6 +735,7 @@
}
},
"setting": {
"InsecureCommandNotifyToSubscription": "危险命令通知已升级到消息订阅中,支持更多通知方式",
"ApiKeyList": "API Key 列表",
"AssetCount": "资产数量",
"Basic": "基本设置",
@@ -717,6 +754,8 @@
"PasswordCheckRule": "密码校验规则",
"Security": "安全设置",
"SecuritySetting": "安全设置",
"SystemMessageSubscription": "系统消息订阅",
"insecureCommandEmailUpdate": "点我设置",
"SubscriptionID": "订阅授权ID",
"Terminal": "终端设置",
"all": "全部",
@@ -830,12 +869,15 @@
"refreshLdapCache":"刷新Ldap缓存请稍后",
"LicenseExpired": "许可证已经过期",
"LicenseWillBe": "许可证即将在 ",
"Expire": " 过期"
},
"settings": {
"Expire": " 过期",
"WeCom": "企业微信",
"DingTalk": "钉钉",
"dingTalkTest": "测试",
"weComTest": "测试",
"setting": "设置"
},
"tickets": {
"PermissionName": "授权规则名称",
"Accept": "同意",
"AssignedMe": "待我审批",
"Assignee": "处理人",
@@ -877,7 +919,13 @@
"ips": "请输入逗号分割的IP地址组",
"fuzzySearch": "支持模糊搜索",
"application": "请输入逗号分割的应用名称组"
}
},
"ApplyRunUser": "申请运行的用户",
"ApplyRunAsset": "申请运行的资产",
"ApplyRunSystemUser": "申请运行的系统用户",
"ApplyRunCommand": "申请运行的命令",
"ApplyFromSession": "会话",
"ApplyFromCMDFilterRule": "命令过滤规则"
},
"tree": {
"AddAssetToNode": "添加资产到节点",
@@ -893,6 +941,7 @@
"UpdateNodeAssetHardwareInfo": "更新节点资产硬件信息"
},
"users": {
"UserName": "姓名",
"Account": "账户",
"Authentication": "认证",
"Comment": "备注",
@@ -913,6 +962,8 @@
"Invite": "邀请",
"InviteUserInOrg": "邀请用户加入此组织",
"Guide": "向导",
"setWeCom": "设置企业微信认证",
"setDingTalk": "设置钉钉认证",
"HelpText": {
"MFAOfUserFirstLoginPersonalInformationImprovementPage": "启用多因子认证,使账号更加安全。<br/> 启用之后您将会在下次登录时进入多因子认证绑定流程;您也可以在(个人信息->快速修改->更改多因子设置)中直接绑定!",
"MFAOfUserFirstLoginUserGuidePage": "为了保护您和公司的安全,请妥善保管您的账户、密码和密钥等重要敏感信息;(如:设置复杂密码,并启用多因子认证)",
@@ -960,7 +1011,14 @@
"resetSSHKey": "重置SSH密钥",
"resetSSHKeySuccessMsg": "发送邮件任务已提交, 用户稍后会收到重置密钥邮件",
"resetSSHKeyWarningMsg": "你确定要发送重置用户的SSH Key的邮件吗?",
"resetWechat": "解绑企业微信",
"resetWechatLoginWarningMsg": "你确定要解绑用户的 企业微信 吗?",
"resetWechatLoginSuccessMsg": "重置成功, 用户可以重新绑定企业微信了",
"resetDingTalk": "解绑钉钉",
"resetDingTalkLoginWarningMsg": "你确定要解绑用户的 钉钉 吗?",
"resetDingTalkLoginSuccessMsg": "重置成功, 用户可以重新绑定钉钉了",
"send": "发送",
"unbind": "解绑",
"unblock": "解锁",
"unblockSuccessMsg": "解锁成功",
"unblockUser": "解锁用户"
@@ -979,12 +1037,27 @@
"remoteAppPermissionRules": "远程应用授权规则"
},
"dateLastLogin": "最后登录日期",
"needUpdatePasswordNextLogin": "下次登录须修改密码",
"UpdatePassword": "更新密码",
"SetPublicKey": "设置SSH公钥",
"passwordExpired": "密码过期了",
"passwordWillExpiredPrefixMsg": "密码即将在 ",
"passwordWillExpiredSuffixMsg": "天 后过期,请尽快修改您的密码。"
},
"notifications": {
"MessageType": "消息类型",
"Receivers": "接收人",
"Subscription": "消息订阅",
"ChangeReceiver": "修改消息接收人",
"Subject": "主题",
"Message": "消息",
"DeliveryTime": "发送时间",
"HasRead": "是否已读",
"Sender": "发送人",
"MarkAsRead": "标记已读",
"NoUnreadMsg": "暂无未读消息",
"SiteMessage": "站内信"
},
"xpack": {
"Admin": "管理员",
"Asset": "资产",
@@ -1091,7 +1164,9 @@
"GatherUserList": "收集用户",
"GatherUserTaskCreate": "创建任务",
"GatherUserTaskList": "任务列表",
"GatherUserTaskUpdate": "更新任务"
"GatherUserTaskUpdate": "更新任务",
"GatherUserTaskDetail": "任务详情",
"GatherUserTaskExecutionList": "任务执行列表"
},
"Import": "导入",
"ImportLicense": "导入许可证",

View File

@@ -1,5 +1,10 @@
{
"": "",
"accounts": {
"PleaseClickLeftAssetToViewAssetAccount": "Asset account list, please click on the assets on the left to view",
"PleaseClickLeftApplicationToViewApplicationAccount": "Application account list, please click on the application on the left to view",
"PleaseClickLeftAssetToViewGatheredUser": "Gathered user list please click on the assets on the left to view"
},
"acl": {
"name": "Name",
"username": "Username",
@@ -178,7 +183,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",
@@ -198,12 +203,15 @@
"ipDomain": "IP(Domain)",
"HostProtocol": "Host Protocol",
"DatabaseProtocol": "Database Protocol",
"Other Protocol": "Database Protocol"
"OtherProtocol": "Other Protocol",
"PasswordOrToken": "Password / Token"
},
"audits": {
"Hosts": "Host",
"RunUser": "Run user",
"User": "User",
"Username": "Username",
"SystemUserName": "System username",
"View": "View"
},
"auth": {
@@ -211,6 +219,7 @@
"ReLogin": "Re-Login"
},
"common": {
"ConnectWebSocketError": "Connect Websocket failed",
"Nothing": "Nothing",
"Action": "Action",
"CustomCol":"Custom table col",
@@ -257,10 +266,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",
@@ -272,19 +287,24 @@
"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",
@@ -318,6 +338,11 @@
"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",
@@ -327,7 +352,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": {
@@ -364,7 +392,8 @@
"SPECIAL_CHAR_REQUIRED": "Special char required",
"MIN_LENGTH_ERROR": "Password minimum length {}"
},
"lastCannotBeDeleteMsg": "The last one can't be delete"
"lastCannotBeDeleteMsg": "The last one can't be delete",
"InvalidJson": "Not a valid json format"
},
"dashboard": {
"ActiveAsset": "Asset active",
@@ -501,29 +530,33 @@
},
"route": {
"": "",
"Accounts": "Accounts",
"AssetAccount": "Asset account",
"ApplicationAccount": "Application account",
"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",
@@ -532,7 +565,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",
@@ -540,7 +573,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",
@@ -548,12 +581,13 @@
"KubernetesAppPermissionDetail": "Kubernetes permissions detail",
"KubernetesAppPermissionUpdate": "Kubernetes permissions update",
"KubernetesAppUpdate": "Kubernetes app update",
"Acl": "Access control",
"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 list",
"AssetAclList": "Asset Acl",
"AssetAclCreate": "Asset acl create",
"AssetAclUpdate": "Asset acl update",
"AssetAclDetail": "Asset acl detail",
@@ -561,32 +595,36 @@
"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",
"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",
@@ -596,11 +634,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",
@@ -610,7 +648,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",
@@ -618,7 +656,9 @@
"UserUpdate": "User update",
"Users": "Users",
"WebFTP": "WebFTP",
"WebTerminal": "Web terminal"
"WebTerminal": "Web Terminal",
"Notifications": "Notifications",
"SiteMessageList": "Site message"
},
"sessions": {
"StorageConfiguration": "Storage configuration",
@@ -681,6 +721,7 @@
"common": "common"
},
"Monitor": "Monitor",
"XRDPNotSupport": "RDP Client session not support now",
"sessionMonitor": "Session Monitor",
"TerminateTaskSendSuccessMsg": "Terminate task has been send, Please check later",
"helpText": {
@@ -692,6 +733,7 @@
}
},
"setting": {
"InsecureCommandNotifyToSubscription": "Insecure command notification setting, change to system message subscription, support more notify method",
"ApiKeyList": "Api key list",
"AssetCount": "Asset count",
"Basic": "Basic setting",
@@ -711,6 +753,8 @@
"Security": "Security setting",
"SecuritySetting": "Security setting",
"SubscriptionID": "Subscription ID",
"SystemMessageSubscription": "System messages",
"insecureCommandEmailUpdate": "Setting",
"Terminal": "Terminal setting",
"all": "All",
"authLdap": "Enable LDAP auth",
@@ -820,12 +864,16 @@
"refreshLdapCache":"Refreshing Ldap cache ",
"LicenseExpired": "License expired",
"LicenseWillBe": "License will expire at ",
"Expire": ""
},
"settings": {
"Expire": "",
"WeCom": "WeCom",
"DingTalk": "DingTalk",
"dingTalkTest": "Test",
"weComTest": "Test",
"setting": "Setting"
},
"tickets": {
"PermissionName": "Permission name",
"Accept": "Accept",
"AssignedMe": "Assigned me",
"Assignee": "Assignee",
@@ -867,7 +915,13 @@
"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",
@@ -883,6 +937,7 @@
"UpdateNodeAssetHardwareInfo": "Update node asset hardware information"
},
"users": {
"UserName": "Name",
"Account": "Account",
"Existing":"Existing",
"Authentication": "Account",
@@ -892,6 +947,8 @@
"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",
@@ -951,7 +1008,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"
@@ -969,12 +1030,28 @@
"ApplicationPermissionRules": "Application permission rules",
"remoteAppPermissionRules": "Remote app permission rules"
},
"UpdatePassword": "",
"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",
"Subject": "Subject",
"Message": "Message",
"DeliveryTime": "Delivery time",
"HasRead": "Has read",
"Sender": "Sender",
"MarkAsRead": "Mark as read",
"NoUnreadMsg": "No unread messages",
"SiteMessage": "Site messages"
},
"xpack": {
"Admin": "Admin",
"Asset": "Asset",
@@ -986,7 +1063,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",
@@ -1036,7 +1113,7 @@
"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",
@@ -1077,16 +1154,18 @@
"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",

View File

@@ -282,6 +282,7 @@ export default {
if (object) {
object = _.cloneDeep(object)
this.$emit('update:object', object)
this.$emit('getObjectDone', object)
}
return object
},

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

@@ -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: {

View File

@@ -10,7 +10,6 @@
</template>
<script>
export default {
name: 'Language',
data() {
@@ -47,11 +46,22 @@ export default {
}
},
mounted() {
if (this.currentLang.code !== this.$i18n.locale) {
this.changeLangTo(this.currentLang)
}
this.changeLang()
this.changeMomentLang()
},
methods: {
changeLang() {
if (this.currentLang.code !== this.$i18n.locale) {
this.changeLangTo(this.currentLang)
}
},
changeMomentLang() {
if (this.currentLang.code.indexOf('en') > -1) {
this.$moment.locale('en')
} else {
this.$moment.locale('zh-cn')
}
},
changeLangTo(item) {
this.$i18n.locale = item.code
localStorage.setItem('lang', item.code)

View File

@@ -0,0 +1,274 @@
<template>
<div>
<el-badge :value="unreadMsgCount" :hidden="unreadMsgCount === 0" :max="99" size="mini" type="primary">
<a style="color: #606266 !important; width: 30px" @click="toggleDrawer">
<i class="el-icon-message" style="font-size: 18px" />
</a>
</el-badge>
<el-drawer
:visible.sync="show"
:before-close="handleClose"
:modal="false"
:title="$t('notifications.SiteMessage')"
custom-class="site-msg"
size="25%"
@open="getMessages"
>
<div v-if="unreadMsgCount !== 0" class="msg-list">
<div
v-for="msg of messages"
:key="msg.id"
class="msg-item"
:class="msg.has_read ? 'msg-read' : 'msg-unread'"
@mouseover="hoverMsgId = msg.id"
@mouseleave="hoverMsgId = ''"
@click="showMsgDetail(msg)"
>
<div class="msg-item-head">
<span class="msg-item-head-type">
<i :class="msg.has_read ? 'fa-envelope-open-o' : 'fa-envelope'" class="fa msg-icon" />
{{ msg.subject }}
</span>
<span v-if="hoverMsgId !== msg.id || msg.has_read" class="msg-item-head-time">
{{ formatDate(msg.date_created) }}
</span>
<div v-else class="msg-item-read-btn" @click.stop="markAsRead(msg)">
<a>{{ $t('notifications.MarkAsRead') }}</a>
</div>
</div>
<div class="msg-item-txt">
<span v-html="msg.message" />
</div>
</div>
</div>
<div v-else class="no-msg">
{{ $t('notifications.NoUnreadMsg') }}
</div>
</el-drawer>
<Dialog
v-if="msgDetailVisible"
:visible.sync="msgDetailVisible"
:title="''"
:close-on-click-modal="false"
:confirm-title="$t('notifications.MarkAsRead')"
@confirm="markAsRead(currentMsg)"
@cancel="cancelRead"
>
<div class="msg-detail">
<div class="msg-detail-head">
<h3>{{ currentMsg.subject }}</h3>
<h5>
<span class="msg-detail-time">{{ formatDate(currentMsg.date_created) }}</span>
</h5>
</div>
<div class="msg-detail-txt">
<span v-html="currentMsg.message" />
</div>
</div>
</Dialog>
</div>
</template>
<script>
import { toSafeLocalDateStr } from '@/utils/common'
import Dialog from '@/components/Dialog'
export default {
name: 'SiteMessages',
components: { Dialog },
data() {
return {
show: false,
messages: [],
hoverMsgId: '',
msgDetailVisible: false,
currentMsg: null,
unreadMsgCount: 0
}
},
mounted() {
this.enablePullMsgCount()
},
methods: {
handleClose() {
this.show = false
},
toggleDrawer() {
this.show = !this.show
},
showMsgDetail(msg) {
this.currentMsg = msg
this.msgDetailVisible = true
},
getMessages() {
const url = '/api/v1/notifications/site-message/?offset=0&limit=15&has_read=false'
this.$axios.get(url).then(resp => {
this.messages = [...resp.results]
this.unreadMsgCount = resp.count
})
},
formatDate(s) {
if (!s) {
return ''
}
const d = new Date(s)
const now = new Date()
if (now.getTime() - d.getTime() > (3600 * 24 * 7) * 1000) {
return toSafeLocalDateStr(s)
} else {
return this.$moment(d).fromNow()
}
},
markAsRead(msg) {
const url = `/api/v1/notifications/site-message/mark-as-read/`
this.$axios.patch(url, { ids: [msg.id] }).then(res => {
this.msgDetailVisible = false
this.getMessages()
}).catch(err => {
this.$message(err.detail)
})
},
cancelRead() {
this.msgDetailVisible = false
},
enablePullMsgCount() {
const scheme = document.location.protocol === 'https:' ? 'wss' : 'ws'
const port = document.location.port ? ':' + document.location.port : ''
const url = '/ws/notifications/site-msg/'
const wsURL = scheme + '://' + document.location.hostname + port + url
const ws = new WebSocket(wsURL)
ws.onopen = (event) => {
this.$log.debug('Websocket connected: ', event)
}
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data)
this.$log.debug('Data: ', data)
const unreadCount = data['unread_count']
if (unreadCount !== undefined) {
this.unreadMsgCount = unreadCount
}
} catch (e) {
this.$log.debug('Recv site message error')
}
}
ws.onerror = (error) => {
this.$message.error(this.$t('common.ConnectWebSocketError'))
this.$log.debug('site message ws error: ', error)
}
}
}
}
</script>
<style lang="scss" scoped>
.el-badge ::v-deep .el-badge__content.is-fixed{
top:10px;
}
.msg-list {
padding: 0 25px 20px;
}
>>> .site-msg {
.el-drawer__header {
border-bottom: solid 1px rgb(231, 234, 239);
margin-bottom: 0;
padding-top: 10px;
font-size: 16px;
}
.el-drawer__body {
overflow-y: auto;
}
}
.msg-item {
border-bottom: solid 1px rgb(231, 234, 239);
padding: 15px 0 10px;
position: relative;
border-bottom: 1px solid #ddd;
cursor: pointer;
&:hover {
background-color: #f2f2f2;
padding: 15px 20px 10px;
margin: 0 -20px;
border-bottom: 1px solid #fff;
}
.msg-icon {
font-size: 13px;
line-height: 13px;
}
&.msg-unread {
.msg-item-txt {
font-weight: bolder;
}
}
}
.msg-item-head {
line-height: 20px;
color: #888;
font-size: 12px;
&:after {
clear: both;
content: ".";
display: block;
height: 0;
overflow: hidden;
}
.msg-item-head-type {
float: left;
width: 240px;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
white-space: nowrap;
}
.msg-item-head-time {
float: right;
}
.msg-item-read-btn {
float: right;
}
}
.msg-item-txt {
overflow: hidden;
color: #000;
padding: 4px 0 0;
line-height: 21px;
max-height: 21px;
display: -webkit-box;
font-size: 12px;
display: block;
}
.msg-detail {
padding-left: 20px;
.msg-detail-time {
font-weight: 400;
font-size: 12px;
line-height: 1.1;
}
.msg-detail-txt {
margin-bottom: 20px;
line-height: 25px;
}
}
.no-msg {
padding-top: 20px;
text-align: center;
}
>>> :focus{ outline:0; }
</style>

View File

@@ -3,30 +3,26 @@
<div class="navbar-header">
<hamburger :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
</div>
<div class="navbar-right">
<div class="header-item">
<ul class="navbar-right">
<li class="header-item header-icon">
<SiteMessages />
</li>
<li class="header-item" style="margin-left: 10px">
<Help />
</div>
<div class="header-item">
</li>
<li class="header-item">
<Language />
</div>
<div
v-if="
publicSettings.TICKETS_ENABLED
&& publicSettings.XPACK_LICENSE_IS_VALID
&& !isOrgAuditor
"
class="header-item"
>
</li>
<li v-if="showTickets" class="header-item">
<Tickets />
</div>
<div class="header-item">
</li>
<li class="header-item">
<WebTerminal />
</div>
<div class="header-item header-profile">
</li>
<li class="header-item header-profile">
<AccountDropdown />
</div>
</div>
</li>
</ul>
</div>
</template>
@@ -34,6 +30,7 @@
import { mapGetters } from 'vuex'
import Hamburger from '@/components/Hamburger'
import AccountDropdown from './AccountDropdown'
import SiteMessages from './SiteMessages'
import Help from './Help'
import Language from './Language'
import WebTerminal from './WebTerminal'
@@ -48,7 +45,8 @@ export default {
Language,
Help,
Tickets,
WebTerminal
WebTerminal,
SiteMessages
},
data() {
return {
@@ -60,13 +58,17 @@ export default {
]),
isOrgAuditor() {
return rolc.getRolesDisplay(this.currentOrgRoles).includes('OrgAuditor') || rolc.getRolesDisplay(this.currentOrgRoles).includes('Auditor')
},
showTickets() {
return this.publicSettings.TICKETS_ENABLED &&
this.publicSettings.XPACK_LICENSE_IS_VALID &&
!this.isOrgAuditor
}
},
methods: {
toggleSideBar() {
this.$store.dispatch('app/toggleSideBar')
}
}
}
</script>
@@ -91,12 +93,19 @@ export default {
.navbar-right {
float: right;
margin-right: 10px;
}
.header-item {
line-height: 50px;
display: inline-block;
padding-right: 20px;
.header-item {
line-height: 50px;
display: inline-block;
padding-right: 10px;
padding-left: 10px;
}
.header-icon {
&:hover {
background-color: #e6e6e6;
}
}
}
.breadcrumb-container {
@@ -108,5 +117,9 @@ export default {
.el-header {
background-color: #ffffff;
}
ul {
margin: 0;
}
</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

@@ -10,7 +10,7 @@
<div>
<el-tabs v-if="submenu.length > 0" slot="submenu" v-model="iActiveMenu" class="page-submenu" @tab-click="handleTabClick">
<template v-for="item in submenu">
<el-tab-pane :key="item.name" :label-content="item.labelContent" :name="item.name">
<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" />
@@ -80,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>

View File

@@ -40,8 +40,13 @@ Vue.config.productionTip = false
import VueCookie from 'vue-cookie'
Vue.use(VueCookie)
window.$cookie = VueCookie
import VueMoment from 'vue-moment'
Vue.use(VueMoment)
const moment = require('moment')
require('moment/locale/zh-cn')
Vue.use(require('vue-moment'), {
moment
})
// logger
import VueLogger from 'vuejs-logger'
import loggerOptions from './utils/logger'

120
src/router/accounts.js Normal file
View File

@@ -0,0 +1,120 @@
import i18n from '@/i18n/i18n'
import empty from '@/layout/empty'
export default [
{
path: 'asset-accounts',
component: empty,
meta: { title: i18n.t('route.AssetAccount') },
children: [
{
path: '',
name: 'AssetAccountList',
component: () => import('@/views/accounts/AssetAccount/AssetAccountList'),
meta: { title: i18n.t('route.AssetAccount') }
}
]
},
{
path: 'application-accounts',
component: empty,
meta: { title: i18n.t('route.AssetAccount') },
children: [
{
path: '',
name: 'ApplicationAccountList',
component: () => import('@/views/accounts/ApplicationAccount/ApplicationAccountList'),
meta: { title: i18n.t('route.ApplicationAccount') }
}
]
},
{
path: 'gathered-user',
component: empty,
redirect: '',
meta: { title: i18n.t('xpack.GatherUser.GatherUserList') },
children: [
{
path: '',
component: () => import('@/views/accounts/GatheredUser/index'),
name: 'GatherUserListIndex',
meta: { title: i18n.t('xpack.GatherUser.GatherUser'), activeMenu: '/accounts/gathered-user' }
},
{
path: '',
component: () => import('@/views/accounts/GatheredUser/GatheredUserList'),
name: 'GatherUserList',
hidden: true,
meta: { title: i18n.t('xpack.GatherUser.GatherUserList'), activeMenu: '/accounts/gathered-user' }
},
{
path: 'tasks',
component: () => import('@/views/accounts/GatheredUser/TaskList'),
name: 'GatherUserTaskList',
meta: { title: i18n.t('xpack.GatherUser.GatherUserTaskList'), activeMenu: '/accounts/gathered-user' },
hidden: true
},
{
path: 'tasks/:id',
component: () => import('@/views/accounts/GatheredUser/TaskDetail/index'),
name: 'GatherUserTaskDetail',
meta: { title: i18n.t('xpack.GatherUser.GatherUserTaskDetail'), activeMenu: '/accounts/gathered-user' },
hidden: true
},
{
path: 'tasks/create',
component: () => import('@/views/accounts/GatheredUser/TaskCreateUpdate'),
name: 'GatherUserTaskCreate',
meta: { title: i18n.t('xpack.GatherUser.GatherUserTaskCreate'), activeMenu: '/accounts/gathered-user' },
hidden: true
},
{
path: 'tasks/:id/update',
component: () => import('@/views/accounts/GatheredUser/TaskCreateUpdate'),
name: 'GatherUserTaskUpdate',
meta: { title: i18n.t('xpack.GatherUser.GatherUserTaskUpdate'), action: 'update', activeMenu: '/accounts/gathered-user' },
hidden: true
}
]
},
{
path: 'change-auth-plan',
component: empty,
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlan'), activeMenu: '/accounts/change-auth-plan/plan' },
children: [
{
path: 'plan',
component: () => import('@/views/accounts/ChangeAuthPlan/ChangeAuthPlanList.vue'),
name: 'ChangeAuthPlanList',
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlan'), activeMenu: '/accounts/change-auth-plan/plan' }
},
{
path: 'plan/create',
component: () => import('@/views/accounts/ChangeAuthPlan/ChangeAuthPlanCreateUpdate.vue'),
name: 'ChangeAuthPlanCreate',
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlanCreate'), activeMenu: '/accounts/change-auth-plan/plan', action: 'create' },
hidden: true
},
{
path: 'plan/:id/update',
component: () => import('@/views/accounts/ChangeAuthPlan/ChangeAuthPlanCreateUpdate.vue'),
name: 'ChangeAuthPlanUpdate',
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlanUpdate'), activeMenu: '/accounts/change-auth-plan/plan', action: 'update' },
hidden: true
},
{
path: 'plan/:id',
component: () => import('@/views/accounts/ChangeAuthPlan/ChangeAuthPlanDetail/index.vue'),
name: 'ChangeAuthPlanDetail',
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlan'), activeMenu: '/accounts/change-auth-plan/plan' },
hidden: true
},
{
path: 'plan-execution/:id',
component: () => import('@/views/accounts/ChangeAuthPlan/ChangeAuthPlanDetail/ChangeAuthPlanExecution/ChangeAuthPlanExecutionDetail/index.vue'),
name: 'ChangeAuthPlanExecutionDetail',
meta: { title: i18n.t('xpack.ChangeAuthPlan.ExecutionDetail'), activeMenu: '/accounts/change-auth-plan/plan' },
hidden: true
}
]
}
]

View File

@@ -37,6 +37,7 @@ import TicketsRoutes from './tickets'
import AuditsRoutes from './audits'
import commonRoutes from './common'
import aclRoutes from './acl'
import AccountRoutes from './accounts'
/**
* constantRoutes
@@ -110,6 +111,18 @@ export const allRoleRoutes = [
meta: { title: i18n.t('route.Applications'), icon: 'th' },
children: ApplicationsRoute
},
{
path: '/accounts',
component: Layout,
redirect: '/accounts/asset-accounts/',
name: 'Accounts',
meta: {
licenseRequired: true,
title: i18n.t('route.Accounts'),
icon: 'address-book'
},
children: AccountRoutes
},
{
path: '/perms/',
component: Layout,

View File

@@ -48,5 +48,12 @@ export default [
component: () => import('@/views/tickets/RequestApplicationPerm/Detail/index'),
meta: { title: i18n.t('route.TicketDetail'), activeMenu: '/tickets/tickets' },
hidden: true
},
{
path: 'tickets/command-confirm/:id',
name: 'CommandConfirmDetail',
component: () => import('@/views/tickets/CommandConfirm/Detail/index'),
meta: { title: i18n.t('route.CommandConfirm'), activeMenu: '/tickets/tickets' },
hidden: true
}
]

View File

@@ -65,7 +65,7 @@ export default [
path: '',
name: 'CommandExecutions',
component: () => import('@/views/ops/CommandExecution'),
meta: { title: i18n.t('route.CommandExecutions'), icon: 'terminal', permissions: [rolec.PERM_USE] }
meta: { title: i18n.t('route.BatchCommand'), icon: 'terminal', permissions: [rolec.PERM_USE] }
}
]
},
@@ -124,6 +124,13 @@ export default [
meta: { title: i18n.t('route.TicketDetail'), activeMenu: '/tickets/tickets', permissions: [rolec.PERM_USE] },
hidden: true
},
{
path: 'tickets/command-confirm/:id',
name: 'CommandConfirmDetail',
component: () => import('@/views/tickets/CommandConfirm/Detail/index'),
meta: { title: i18n.t('route.CommandConfirm'), activeMenu: '/tickets/tickets', permissions: [rolec.PERM_USE] },
hidden: true
},
{
path: 'tickets/:id',
name: 'TicketDetail',
@@ -155,7 +162,7 @@ export default [
children: [
{
path: `${BASE_URL}/koko/elfinder/sftp/`,
meta: { title: i18n.t('route.WebFTP'), icon: 'file', activeMenu: '/assets', permissions: [rolec.PERM_USE] }
meta: { title: i18n.t('route.FileManager'), icon: 'file', activeMenu: '/assets', permissions: [rolec.PERM_USE] }
}
]
}

View File

@@ -1,6 +1,5 @@
module.exports = {
title: 'JumpServer',
title: '.',
/**
* @type {boolean} true | false

View File

@@ -35,15 +35,18 @@ const actions = {
getPublicSettings({ commit, state }) {
return new Promise((resolve, reject) => {
getPublicSettings().then(response => {
const link = document.querySelector("link[rel*='icon']") || document.createElement('link')
link.type = 'image/x-icon'
link.rel = 'shortcut icon'
link.href = response.data.LOGO_URLS.favicon
document.getElementsByTagName('head')[0].appendChild(link)
const faviconURL = response.data.LOGO_URLS.favicon
let link = document.querySelector("link[rel*='icon']")
if (!link) {
link = document.createElement('link')
link.type = 'image/x-icon'
link.rel = 'shortcut icon'
document.getElementsByTagName('head')[0].appendChild(link)
}
link.href = faviconURL
// 动态修改Title
if (response.data.LOGIN_TITLE) { document.title = response.data.LOGIN_TITLE }
document.title = response.data.LOGIN_TITLE
commit('SET_PUBLIC_SETTINGS', response.data)
resolve(response)
}).catch(error => {

View File

@@ -8,7 +8,7 @@
<script>
import ListTable from '@/components/ListTable/index'
import Page from '@/layout/components/Page/index'
import { ActionsFormatter } from '@/components/ListTable/formatters'
import { ActionsFormatter } from '@/components/TableFormatters'
export default {
name: 'DatabaseApp',

View File

@@ -8,7 +8,7 @@
<script>
import ListTable from '@/components/ListTable/index'
import Page from '@/layout/components/Page/index'
import { ActionsFormatter } from '@/components/ListTable/formatters'
import { ActionsFormatter } from '@/components/TableFormatters'
export default {
name: 'KubernetesApp',

View File

@@ -7,7 +7,7 @@
<script>
import ListTable from '@/components/ListTable/index'
import Page from '@/layout/components/Page/index'
import { ActionsFormatter } from '@/components/ListTable/formatters'
import { ActionsFormatter } from '@/components/TableFormatters'
export default {
name: 'RemoteApp',

View File

@@ -4,7 +4,7 @@
<script>
import GenericTreeListPage from '@/layout/components/GenericTreeListPage/index'
import { LoadingActionsFormatter, SystemUserFormatter, DialogDetailFormatter } from '@/components/ListTable/formatters'
import { ActionsFormatter, SystemUserFormatter, DialogDetailFormatter } from '@/components/TableFormatters'
export default {
components: {
GenericTreeListPage
@@ -104,7 +104,7 @@ export default {
{
prop: 'id',
align: 'center',
formatter: LoadingActionsFormatter,
formatter: ActionsFormatter,
width: '100px',
label: this.$t('common.action'),
formatterArgs: {
@@ -117,7 +117,7 @@ export default {
name: 'connect',
fa: 'fa-terminal',
type: 'primary',
can: (row, cellValue) => {
can: function({ row, cellValue }) {
return row.is_active
},
callback: function({ row, col, cellValue, reload }) {
@@ -127,7 +127,7 @@ export default {
{
name: 'favor',
type: 'info',
fa: function(row, cellValue) {
fa: function({ row, cellValue }) {
if (this.checkFavorite(row.id)) {
return 'fa-star'
}
@@ -160,20 +160,25 @@ export default {
},
methods: {
refreshAllFavorites() {
this.tableConfig.columns[this.tableConfig.columns.length - 1].formatterArgs.loading = true
const actionsIndex = this.tableConfig.columns.length - 1
this.tableConfig.columns[actionsIndex].formatterArgs.loading = true
this.$axios.get('/api/v1/assets/favorite-assets/').then(resp => {
this.allFavorites = resp
this.tableConfig.columns[this.tableConfig.columns.length - 1].formatterArgs.loading = false
this.tableConfig.columns[actionsIndex].formatterArgs.loading = false
})
},
addOrDeleteFavorite(assetId) {
if (this.checkFavorite(assetId)) {
this.$axios.delete(`/api/v1/assets/favorite-assets/?asset=${assetId}`).then(res => this.removeFavorite(assetId))
this.$axios.delete(`/api/v1/assets/favorite-assets/?asset=${assetId}`).then(
res => this.removeFavorite(assetId)
)
} else {
const data = {
asset: assetId
}
this.$axios.post('/api/v1/assets/favorite-assets/', data).then(res => this.addFavorite(assetId))
this.$axios.post('/api/v1/assets/favorite-assets/', data).then(
res => this.addFavorite(assetId)
)
}
},
checkFavorite(assetId) {

View File

@@ -14,8 +14,9 @@
<script>
import GenericCreateUpdateForm from '@/layout/components/GenericCreateUpdateForm'
import UserPassword from '@/components/UserPassword'
import UserPassword from '@/components/FormFields/UserPassword'
import { IBox } from '@/components'
import rules from '@/components/DataForm/rules'
export default {
name: 'PasswordUpdate',
@@ -42,8 +43,8 @@ export default {
},
new_password: {
label: this.$t('users.NewPassword'),
component: UserPassword,
rules: []
rules: [rules.RequiredChange],
component: UserPassword
},
new_password_again: {
label: this.$t('users.ConfirmPassword'),

View File

@@ -1,23 +1,49 @@
<template>
<el-row :gutter="20">
<el-col :span="14">
<DetailCard :items="detailCardItems" />
</el-col>
<el-col :span="10">
<QuickActions type="primary" :actions="quickActions" />
</el-col>
</el-row>
<div>
<el-row :gutter="20">
<el-col :span="14">
<DetailCard :items="detailCardItems" />
</el-col>
<el-col :span="10">
<QuickActions type="primary" :actions="quickActions" />
</el-col>
</el-row>
<Dialog
width="50"
top="20vh"
:title="this.$t('common.PasswordConfirm')"
:visible.sync="showPasswordDialog"
:show-confirm="false"
:show-cancel="false"
:destroy-on-close="true"
>
<el-row :gutter="20">
<el-col :span="4">
<div style="line-height: 34px;text-align: center">{{ $t('assets.Password') }}</div>
</el-col>
<el-col :span="14">
<el-input v-model="passwordInput" type="password" />
<span class="help-tips help-block">{{ $t('common.PasswordRequireForSecurity') }}</span>
</el-col>
<el-col :span="4">
<el-button size="mini" type="primary" style="line-height:20px " @click="passConfirm">{{ this.$t('common.Confirm') }}</el-button>
</el-col>
</el-row>
</Dialog>
</div>
</template>
<script type="text/jsx">
import DetailCard from '@/components/DetailCard'
import QuickActions from '@/components/QuickActions'
import Dialog from '@/components/Dialog'
import { toSafeLocalDateStr } from '@/utils/common'
export default {
name: 'ProfileInfo',
components: {
DetailCard,
QuickActions
QuickActions,
Dialog
},
props: {
object: {
@@ -28,7 +54,40 @@ export default {
data() {
return {
url: `/api/v1/users/profile/`,
showPasswordDialog: false,
passwordInput: '',
currentEdit: '',
quickActions: [
{
title: this.$t('users.setWeCom'),
attrs: {
type: 'primary',
label: this.$store.state.users.profile.is_wecom_bound ? this.$t('common.unbind') : this.$t('common.bind'),
disabled: this.$store.state.users.profile.source !== 'local'
},
has: this.$store.getters.publicSettings.AUTH_WECOM,
callbacks: {
click: function() {
this.currentEdit = 'wecom'
this.showPasswordDialog = true
}.bind(this)
}
},
{
title: this.$t('users.setDingTalk'),
attrs: {
type: 'primary',
label: this.$store.state.users.profile.is_dingtalk_bound ? this.$t('common.unbind') : this.$t('common.bind'),
disabled: this.$store.state.users.profile.source !== 'local'
},
has: this.$store.getters.publicSettings.AUTH_DINGTALK,
callbacks: {
click: function() {
this.currentEdit = 'dingtalk'
this.showPasswordDialog = true
}.bind(this)
}
},
{
title: this.$t('users.SetMFA'),
attrs: {
@@ -79,7 +138,7 @@ export default {
attrs: {
type: 'primary',
label: this.$t('common.Update'),
disabled: this.$store.state.users.profile.source !== 'local'
disabled: !this.$store.state.users.profile.can_public_key_auth
},
callbacks: {
click: function() {
@@ -91,7 +150,8 @@ export default {
title: this.$t('users.ResetPublicKeyAndDownload'),
attrs: {
type: 'primary',
label: this.$t('common.Reset')
label: this.$t('common.Reset'),
disabled: !this.$store.state.users.profile.can_public_key_auth
},
callbacks: {
click: function() {
@@ -170,6 +230,24 @@ export default {
}
},
methods: {
passConfirm() {
this.$axios.post(
`/api/v1/authentication/password/verify/`, {
password: this.passwordInput
}
).then(res => {
if (!this.object[`is_${this.currentEdit}_bound`]) {
window.location.href = `/core/auth/${this.currentEdit}/qr/bind/?redirect_url=${this.$route.fullPath}`
} else {
this.$axios.post(`/api/v1/authentication/${this.currentEdit}/qr/unbind/`).then(res => {
this.$message.success(this.$t('common.updateSuccessMsg'))
this.$store.dispatch('users/getProfile')
})
}
})
this.passwordInput = ''
this.showPasswordDialog = false
}
}
}
</script>

View File

@@ -38,7 +38,7 @@ export default {
],
fieldsMeta: {
public_key_comment: {
label: this.$t('users.Name'),
label: this.$t('common.Name'),
disabled: true
},
public_key_hash_md5: {

View File

@@ -37,7 +37,7 @@ export default {
},
methods: {
getSubmenu() {
let submenu = [
return [
{
title: this.$t('common.BasicInfo'),
name: 'ProfileInfo'
@@ -45,21 +45,18 @@ export default {
{
title: this.$t('users.ProfileSetting'),
name: 'ProfileUpdate'
},
{
title: this.$t('users.LoginPasswordSetting'),
name: 'PasswordUpdate',
disabled: this.$store.state.users.profile.source !== 'local'
},
{
title: this.$t('users.SSHKeySetting'),
name: 'SSHUpdate',
disabled: !this.$store.state.users.profile.can_public_key_auth
}
]
if (this.$store.state.users.profile.source === 'local') {
submenu = submenu.concat([
{
title: this.$t('users.LoginPasswordSetting'),
name: 'PasswordUpdate'
},
{
title: this.$t('users.SSHKeySetting'),
name: 'SSHUpdate'
}
])
}
return submenu
},
handleUpdate(value) {
this.config.activeMenu = value

View File

@@ -1,3 +1,5 @@
const _ = require('lodash')
function getTimeUnits(u) {
const units = {
'd': '天',
@@ -86,11 +88,14 @@ export function toSafeLocalDateStr(d) {
}
export function getApiPath(that) {
const pagePath = that.$route.path
const isOrgPath = pagePath.split('/').indexOf('orgs') !== -1
if (isOrgPath) {
return `/api/v1/orgs/orgs/${pagePath.split('/').pop()}/`
let pagePath = that.$route.path
const pagePathArray = pagePath.split('/')
if (pagePathArray.indexOf('orgs') !== -1) {
pagePathArray[pagePathArray.indexOf('xpack')] = 'orgs'
} else if (pagePathArray.indexOf('gathered-user') !== -1 || pagePathArray.indexOf('change-auth-plan') !== -1) {
pagePathArray[pagePathArray.indexOf('accounts')] = 'xpack'
}
pagePath = pagePathArray.join('/')
return `/api/v1${pagePath}/`
}
@@ -187,8 +192,38 @@ export function getDayFuture(days, now) {
return new Date(now.getTime() + 3600 * 1000 * 24 * days)
}
export function getErrorResponseMsg(error) {
let msg = ''
const data = error.response && error.response.data || {}
if (data && (data.error || data.msg || data.detail)) {
msg = data.error || data.msg || data.detail
}
return msg
}
export function sleep(time) {
return new Promise((resolve) => setTimeout(resolve, time))
}
function customizer(objValue, srcValue) {
return _.isUndefined(objValue) ? srcValue : objValue
}
export function newURL(url) {
let obj
if (url.indexOf('//') > -1) {
obj = new URL(url)
} else {
obj = new URL(url, 'http://localhost')
}
return obj
}
export const assignIfNot = _.partialRight(_.assignInWith, customizer)
const scheme = document.location.protocol
const port = document.location.port ? ':' + document.location.port : ''
const BASE_URL = scheme + '//' + document.location.hostname + port
export { BASE_URL }

View File

@@ -1,6 +1,7 @@
import axios from 'axios'
import i18n from '@/i18n/i18n'
import { getTokenFromCookie } from '@/utils/auth'
import { getErrorResponseMsg } from '@/utils/common'
import { refreshSessionIdAge } from '@/api/users'
import { Message, MessageBox } from 'element-ui'
import store from '@/store'
@@ -84,11 +85,8 @@ function ifBadRequest({ response, error }) {
export function flashErrorMsg({ response, error }) {
if (!response.config.disableFlashErrorMsg) {
let msg = error.message
const data = response.data
if (data && (data.error || data.msg || data.detail)) {
msg = data.error || data.msg || data.detail
}
const responseErrorMsg = getErrorResponseMsg(error)
const msg = responseErrorMsg || error.message
Message({
message: msg,
type: 'error',

View File

@@ -80,6 +80,11 @@ export function hasUserPagePerm(userPerm) {
}
export function hasPerm(source, target) {
if (target === null) {
return true
} else if (source === null) {
return false
}
if (typeof source !== 'object') {
source = [source]
}

View File

@@ -1,4 +1,4 @@
import getPageTitle from '@/utils/get-page-title'
// import getPageTitle from '@/utils/get-page-title'
import store from '@/store'
import router from '@/router'
import { Message } from 'element-ui'
@@ -15,9 +15,9 @@ function reject(msg) {
return new Promise((resolve, reject) => reject(msg))
}
function setHeadTitle({ to, from, next }) {
document.title = getPageTitle(to.meta.title)
}
// function setHeadTitle({ to, from, next }) {
// document.title = getPageTitle(to.meta.title)
// }
async function checkLogin({ to, from, next }) {
if (whiteList.indexOf(to.path) !== -1) {
@@ -145,7 +145,7 @@ export async function startup({ to, from, next }) {
// set page title
await getPublicSetting({ to, from, next })
await setHeadTitle({ to, from, next })
// await setHeadTitle({ to, from, next })
await checkLogin({ to, from, next })
await changeCurrentOrgIfNeed({ to, from, next })
await changeCurrentRoleIfNeed({ to, from, next })

View File

@@ -0,0 +1,401 @@
<template>
<Page>
<el-row>
<el-col :span="11">
<GenericListTable ref="LeftTable" class="application-table" :header-actions="leftTable.headerActions" :table-config="leftTable.tableConfig" @row-click="leftTable.tableConfig.rowClick" />
</el-col>
<el-col :span="13">
<GenericListTable v-if="!isInit" ref="RightTable" class="application-user-table" :header-actions="rightTable.headerActions" :table-config="rightTable.tableConfig" />
<div v-else class="noDataR">
<div class="hintWrap">
<div>{{ $t('accounts.PleaseClickLeftApplicationToViewApplicationAccount') }}</div>
</div>
</div>
</el-col>
</el-row>
<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.Applications')">
<el-input v-model="MFAInfo.application" 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.PasswordOrToken')">
<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 :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 :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>
</el-radio-group>
</el-form-item>
</el-form>
</Dialog>
</Page>
</template>
<script>
import Page from '@/layout/components/Page'
import GenericListTable from '@/layout/components/GenericListTable'
import { ActionsFormatter, DetailFormatter } from '@/components/TableFormatters'
import Dialog from '@/components/Dialog'
import { mapGetters } from 'vuex'
import { createSourceIdCache } from '@/api/common'
import * as queryUtil from '@/components/DataTable/compenents/el-data-table/utils/query'
export default {
name: 'AssetAccountList',
components: {
GenericListTable, Page, Dialog
},
data() {
const vm = this
return {
isInit: true,
showMFADialog: false,
MFAConfirmed: false,
MFAInput: '',
MFAInfo: {
systemUser: '',
application: '',
username: '',
password: ''
},
selectedRows: '',
showExportDialog: false,
dialogStatus: '',
exportOption: 'all',
exportTypeOption: 'csv',
clickedRow: {},
leftTable: {
tableConfig: {
url: '/api/v1/applications/applications/',
columns: [
'name', 'category_display', 'type_display', 'comment', 'org_name'
],
columnsShow: {
min: ['name'],
default: ['name', 'category_display', 'type_display']
},
columnsMeta: {
name: {
formatter: DetailFormatter,
formatterArgs: {
getRoute({ row, col, cellValue }) {
return {
'db': 'DatabaseAppDetail', 'remote_app': 'RemoteAppDetail', 'cloud': 'KubernetesAppDetail'
}[row.category]
}
},
showOverflowTooltip: true,
sortable: false
}
},
tableAttrs: {
stripe: false, // 斑马纹表格
border: true, // 表格边框
fit: true, // 宽度自适应,
tooltipEffect: 'dark',
rowClassName({ row, rowIndex }) {
if (row === vm.clickedRow) {
return 'row-clicked'
}
return ''
}
},
rowClick: function(row, column, event) {
vm.rightTable.tableConfig.url = `/api/v1/applications/application-users/?application_id=${row.id}`
vm.rightTable.tableConfig.extraQuery.application_id = row.id
vm.clickedRow = row
vm.MFAInfo.application = row.name
vm.isInit = false
}
},
headerActions: {
hasLeftActions: false,
hasCreate: false,
hasExport: false,
hasImport: false,
hasBulkDelete: false,
hasBulkUpdate: false
}
},
rightTable: {
tableConfig: {
url: `/api/v1/applications/application-users/?application_id=`,
extraQuery: {
application_id: ''
},
columns: [
'name', 'username', 'username_same_with_user', 'protocol', 'login_mode', 'priority', 'comment', 'org_name', 'actions'
],
columnsShow: {
min: ['name', 'username', 'actions'],
default: ['name', 'username', 'date_created', 'actions']
},
columnsMeta: {
name: {
formatter: DetailFormatter,
formatterArgs: {
route: 'SystemUserDetail'
},
showOverflowTooltip: true,
sortable: false
},
protocol: {
sortable: false
},
login_mode: {
sortable: false
},
actions: {
label: this.$t('common.Action'),
align: 'center',
width: 150,
formatter: ActionsFormatter,
formatterArgs: {
hasUpdate: false, // can set function(row, value)
hasDelete: false, // can set function(row, value)
hasClone: false,
moreActionsTitle: this.$t('common.More'),
extraActions: [
{
name: 'View',
title: this.$t('common.View'),
type: 'primary',
callback: function(val) {
this.dialogStatus = 'viewAuthInfo'
this.MFAInfo.systemUser = val.row
if (!this.needMFAVerify) {
this.showMFADialog = true
this.MFAConfirmed = true
this.$axios.get(`/api/v1/assets/system-users/${this.MFAInfo.systemUser.id}/auth-info/`).then(res => {
this.MFAConfirmed = true
this.MFAInfo.username = res.username
if (res.protocol === 'k8s') {
this.MFAInfo.password = res.token
} else {
this.MFAInfo.password = res.password
}
})
} else {
this.showMFADialog = true
}
}.bind(this)
}
]
}
},
tableAttrs: {
stripe: false, // 斑马纹表格
border: true, // 表格边框
fit: true, // 宽度自适应,
tooltipEffect: 'dark',
rowClassName({ row, rowIndex }) {
return 'row-background-color'
}
}
}
},
headerActions: {
hasLeftActions: false,
hasImport: false,
handleExport({ selectedRows }) {
vm.selectedRows = selectedRows
vm.dialogStatus = 'export'
if (!vm.needMFAVerify) {
vm.showMFADialog = false
vm.showExportDialog = true
} else {
vm.showMFADialog = true
}
}
}
}
}
},
computed: {
...mapGetters([
'MFA_TTl',
'MFAVerifyAt',
'publicSettings'
]),
needMFAVerify() {
if (!this.publicSettings.SECURITY_VIEW_AUTH_NEED_MFA) {
return false
}
const ttl = this.publicSettings.SECURITY_MFA_VERIFY_TTL
const now = new Date()
return !(this.MFAVerifyAt && (now - this.MFAVerifyAt) < ttl * 1000)
},
exportOptions() {
return [
{
label: this.$t('common.imExport.ExportAll'),
value: 'all',
can: true
},
{
label: this.$t('common.imExport.ExportOnlySelectedItems'),
value: 'selected',
can: this.selectedRows.length > 0
}
]
},
exportTypeOptions() {
return [
{
label: 'CSV',
value: 'csv',
can: true
},
{
label: 'Excel',
value: 'xlsx',
can: true
}
]
}
},
methods: {
MFAConfirm() {
if (this.MFAInput.length !== 6) {
return this.$message.error(this.$t('common.MFAErrorMsg'))
}
this.$axios.post(
`/api/v1/authentication/otp/verify/`, {
code: this.MFAInput
}
).then(
res => {
this.$store.dispatch('users/setMFAVerify')
if (this.dialogStatus === 'export') {
this.showMFADialog = false
this.showExportDialog = true
} else {
this.$axios.get(`/api/v1/assets/system-users/${this.MFAInfo.systemUser.id}/auth-info/`).then(res => {
this.MFAConfirmed = true
this.MFAInfo.username = res.username
this.MFAInfo.password = res.password
})
}
}
)
},
downloadCsv(url) {
const a = document.createElement('a')
a.href = url
a.click()
window.URL.revokeObjectURL(url)
},
async performExport(selectRows, exportOption, q) {
const url = `/api/v1/applications/application-user-auth-infos/`
const query = Object.assign({}, q)
if (exportOption === 'selected') {
const resources = []
const data = selectRows
for (let index = 0; index < data.length; index++) {
resources.push(data[index].id)
}
const spm = await createSourceIdCache(resources)
query['spm'] = spm.spm
} else if (exportOption === 'filtered') {
// console.log(listTableRef)
// console.log(listTableRef.dataTable)
// delete query['limit']
// delete query['offset']
}
query['format'] = this.exportTypeOption
const queryStr =
(url.indexOf('?') > -1 ? '&' : '?') +
queryUtil.stringify(query, '=', '&')
return this.downloadCsv(url + queryStr)
},
async performExportConfirm() {
const listTableRef = this.$refs.RightTable.$refs.ListTable
const query = listTableRef.dataTable.getQuery()
delete query['limit']
delete query['offset']
return this.performExport(this.selectedRows, this.exportOption, query)
},
async handleExportConfirm() {
await this.performExportConfirm()
this.showExportDialog = false
},
handleExportCancel() {
this.showExportDialog = false
}
}
}
</script>
<style lang="scss" scoped>
.application-table ::v-deep .row-clicked, .application-user-table ::v-deep .row-background-color {
background-color: #f5f7fa;
}
.application-table {
&:hover {
cursor: pointer;
}
& ::v-deep .el-table__row{
height: 40px;
& > td{
padding: 0;
}
}
}
.export-item {
width: 100%;
display: block;
padding: 10px 20px;
}
.export-form >>> .el-form-item__label {
line-height: 2
}
.application-user-table{
padding-left:20px ;
}
.noDataR {
width: 100%;
height: 40vh;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
flex-direction: column;
.hintWrap {
color: #D4D6E6;
display: flex;
align-items: flex-start;
justify-content: center;
flex-direction: column;
}
}
</style>

View File

@@ -0,0 +1,222 @@
<template>
<Page>
<el-row>
<el-col v-show="iShowTree" :span="iShowTree?4:0">
<AutoDataZTree
ref="AUtoDataZTree"
:setting="treeSetting"
/>
</el-col>
<el-col :span="iShowTree?9:11">
<div class="mini">
<div style="display:block" class="mini-button" @click="iShowTree=!iShowTree">
<i v-show="iShowTree" class="fa fa-angle-left fa-x" />
<i v-show="!iShowTree" class="fa fa-angle-right fa-x" />
</div>
</div>
<GenericListTable
ref="LeftTable"
class="asset-table"
:header-actions="leftTable.headerActions"
:table-config="leftTable.tableConfig"
@row-click="leftTable.tableConfig.rowClick"
/>
</el-col>
<el-col :span="iShowTree?11:13">
<AssetUserTable
v-if="!isInit"
ref="RightTable"
class="asset-user-table"
:url="rightTable.url"
:search-exclude="rightTable.searchExclude"
:extra-query="rightTable.extraQuery"
:has-left-actions="rightTable.hasLeftActions"
:table-config="rightTable.tableConfig"
:has-clone="false"
:has-import="false"
/>
<div v-else class="noDataR">
<div class="hintWrap">
<div>
{{ $t('accounts.PleaseClickLeftAssetToViewAssetAccount') }}
</div>
</div>
</div>
</el-col>
</el-row>
</Page>
</template>
<script>
import Page from '@/layout/components/Page'
import GenericListTable from '@/layout/components/GenericListTable'
import AutoDataZTree from '@/components/AutoDataZTree/index'
import { AssetUserTable } from '@/components'
import { DetailFormatter } from '@/components/TableFormatters'
export default {
name: 'AssetAccountList',
components: {
AutoDataZTree, GenericListTable, Page, AssetUserTable
},
data() {
const vm = this
return {
isInit: true,
clickedRow: null,
iShowTree: true,
treeSetting: {
showMenu: false,
showRefresh: false,
showAssets: false,
url: '',
treeUrl: '/api/v1/assets/nodes/children/tree/',
callback: {
onSelected: function(event, treeNode) {
vm.leftTable.tableConfig.url = `/api/v1/assets/assets/?node_id=${treeNode.meta.node.id}`
vm.isInit = true
}
}
},
leftTable: {
tableConfig: {
url: '/api/v1/assets/assets/',
columns: [
'hostname', 'ip', 'protocols', 'platform', 'comment', 'org_name'
],
columnsShow: {
min: ['hostname', 'ip'],
default: ['hostname', 'ip', 'platform']
},
columnsMeta: {
hostname: {
formatter: DetailFormatter,
formatterArgs: {
route: 'AssetDetail',
routeQuery: {
activeTab: 'Detail'
}
},
showOverflowTooltip: true
},
ip: {
showOverflowTooltip: true
}
},
tableAttrs: {
stripe: false, // 斑马纹表格
border: true, // 表格边框
fit: true, // 宽度自适应,
tooltipEffect: 'dark',
rowClassName({ row, rowIndex }) {
if (row === vm.clickedRow) {
return 'row-clicked'
}
return ''
}
},
rowClick: function(row, column, event) {
vm.rightTable.url = `/api/v1/assets/asset-users/?asset_id=${row.id}`
vm.rightTable.extraQuery.asset_id = row.id
vm.clickedRow = row
vm.isInit = false
}
},
headerActions: {
hasLeftActions: false,
hasCreate: false,
hasExport: false,
hasImport: false,
hasBulkDelete: false,
hasColumnSetting: true,
hasRefresh: true,
hasBulkUpdate: false
}
},
rightTable: {
url: `/api/v1/assets/asset-users/?hostname=ShowFirstAssetRelated`,
extraQuery: {
latest: 1
},
tableConfig: {
columns: ['name', 'username', 'version', 'backend_display', 'date_created', 'org_name', 'actions'],
columnsShow: {
min: ['username', 'actions'],
default: ['name', 'username', 'version', 'backend_display', 'date_created', 'actions']
},
columnsMeta: {
name: {
formatter: null,
showOverflowTooltip: true,
sortable: false
}
},
tableAttrs: {
stripe: true, // 斑马纹表格
border: false, // 表格边框
fit: true, // 宽度自适应,
tooltipEffect: 'dark',
rowClassName({ row, rowIndex }) {
return 'row-background-color'
}
}
},
hasLeftActions: false,
searchExclude: ['hostname', 'id', 'ip']
}
}
}
}
</script>
<style lang="scss" scoped>
.asset-table ::v-deep .row-clicked, .asset-user-table ::v-deep .row-background-color {
background-color: #f5f7fa;
}
.asset-table {
&:hover {
cursor: pointer;
}
& >>> .table-content {
margin-left: 21px;
}
& ::v-deep .el-table__row{
height: 40px;
& > td{
padding: 0;
}
}
}
.mini-button{
width: 12px;
float: left;
margin-right: 10px;
text-align: center;
padding: 9px 0;
background-color: #1ab394;
border-color: #1ab394;
color: #FFFFFF;
border-radius: 5px;
line-height: 1.428;
cursor:pointer;
}
.noDataR{
width: 100%;
height: 40vh;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
flex-direction: column;
.hintWrap{
color: #D4D6E6;
display: flex;
align-items: flex-start;
justify-content: center;
flex-direction: column;
}
}
.asset-user-table{
padding-left: 20px;
}
</style>

View File

@@ -14,7 +14,7 @@
import GenericListTable from '@/layout/components/GenericListTable'
import RelationCard from '@/components/RelationCard/index'
import AssetRelationCard from '@/components/AssetRelationCard'
import { DeleteActionFormatter } from '@/components/ListTable/formatters'
import { DeleteActionFormatter } from '@/components/TableFormatters'
export default {
name: 'ChangeAuthPlanAsset',
@@ -87,7 +87,7 @@ export default {
onAddSuccess: (items, that) => {
this.$log.debug('AssetSelect value', that.assets)
this.$message.success(this.$t('common.updateSuccessMsg'))
this.$refs.listTable.reloadTable()
this.$refs.listTable.$refs.ListTable.reloadTable()
that.$refs.assetSelect.$refs.select2.clearSelected()
}
},
@@ -115,7 +115,7 @@ export default {
that.iHasObjects = [...that.iHasObjects, ...objects]
that.$refs.select2.clearSelected()
this.$message.success(this.$t('common.updateSuccessMsg'))
this.$refs.listTable.reloadTable()
this.$refs.listTable.$refs.ListTable.reloadTable()
},
performDelete: (item) => {
const nodes = this.object.nodes

View File

@@ -4,7 +4,7 @@
<script>
import { GenericListPage } from '@/layout/components'
import { DetailFormatter } from '@/components/ListTable/formatters'
import { DetailFormatter } from '@/components/TableFormatters'
export default {
name: 'ChangeAuthPlanList',
@@ -18,7 +18,7 @@ export default {
url: '/api/v1/xpack/change-auth-plan/plan/',
columns: [
'name', 'username', 'assets_amount', 'nodes_amount', 'password_strategy_display',
'periodic_display', 'run_times', 'comment', 'actions'
'periodic_display', 'run_times', 'comment', 'org_name', 'actions'
],
columnsMeta: {
username: {

View File

@@ -0,0 +1,249 @@
<template>
<div>
<el-row>
<el-col v-show="iShowTree" :span="iShowTree?4:0">
<AutoDataZTree
ref="AUtoDataZTree"
:setting="treeSetting"
/>
</el-col>
<el-col :span="iShowTree?9:11">
<div class="mini">
<div style="display:block" class="mini-button" @click="iShowTree=!iShowTree">
<i v-show="iShowTree" class="fa fa-angle-left fa-x" />
<i v-show="!iShowTree" class="fa fa-angle-right fa-x" />
</div>
</div>
<GenericListTable
ref="LeftTable"
class="asset-table"
:header-actions="leftTable.headerActions"
:table-config="leftTable.tableConfig"
@row-click="leftTable.tableConfig.rowClick"
/>
</el-col>
<el-col :span="iShowTree?11:13">
<GenericListTable
v-if="!isInit"
ref="RightTable"
class="asset-user-table"
:header-actions="rightTable.headerActions"
:table-config="rightTable.tableConfig"
/>
<div v-else class="noDataR">
<div class="hintWrap">
<div>{{ $t('accounts.PleaseClickLeftAssetToViewGatheredUser') }}</div>
</div>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
import GenericListTable from '@/layout/components/GenericListTable'
import AutoDataZTree from '@/components/AutoDataZTree/index'
import { ChoicesFormatter, DetailFormatter } from '@/components/TableFormatters'
export default {
name: 'AssetAccountList',
components: {
AutoDataZTree, GenericListTable
},
data() {
const vm = this
return {
isInit: true,
clickedRow: {},
iShowTree: true,
treeSetting: {
showMenu: false,
showRefresh: false,
showAssets: false,
url: '',
treeUrl: '/api/v1/assets/nodes/children/tree/',
callback: {
onSelected: function(event, treeNode) {
vm.leftTable.tableConfig.url = `/api/v1/assets/assets/?node_id=${treeNode.meta.node.id}`
vm.isInit = true
}
}
},
leftTable: {
tableConfig: {
url: '/api/v1/assets/assets/',
columns: [
'hostname', 'ip', 'public_ip', 'admin_user_display',
'protocols', 'platform', 'connectivity',
'created_by', 'date_created', 'comment', 'org_name'
],
columnsShow: {
min: ['hostname', 'ip', 'platform'],
default: ['hostname', 'ip', 'connectivity', 'platform']
},
columnsMeta: {
hostname: {
formatter: DetailFormatter,
formatterArgs: {
route: 'AssetDetail',
routeQuery: {
activeTab: 'Detail'
}
},
showOverflowTooltip: true
},
connectivity: {
label: this.$t('assets.Reachable'),
formatter: ChoicesFormatter,
formatterArgs: {
iconChoices: {
0: 'fa-times text-danger',
1: 'fa-check text-primary',
2: 'fa-circle text-warning'
},
typeChange: function(val) {
if (!val) {
return 2
}
return val.status
},
hasTips: true
},
width: '90px',
align: 'center'
}
},
tableAttrs: {
stripe: false, // 斑马纹表格
border: true, // 表格边框
fit: true, // 宽度自适应,
tooltipEffect: 'dark',
rowClassName({ row, rowIndex }) {
if (row === vm.clickedRow) {
return 'row-clicked'
}
return ''
}
},
rowClick: function(row, column, event) {
vm.rightTable.tableConfig.url = `/api/v1/assets/gathered-users/?asset_id=${row.id}`
vm.clickedRow = row
vm.isInit = false
}
},
headerActions: {
hasLeftActions: false,
hasCreate: false,
hasExport: false,
hasImport: false,
hasBulkDelete: false,
hasBulkUpdate: false
}
},
rightTable: {
tableConfig: {
url: `/api/v1/assets/gathered-users/?asset__hostname=ShowFirstAssetRelated`,
columns: [
'username', 'date_last_login', 'present', 'ip_last_login', 'date_updated', 'org_name'
],
columnsShow: {
min: ['username'],
default: [
'username', 'date_last_login', 'present', 'ip_last_login', 'date_updated'
]
},
columnsMeta: {
username: {
showOverflowTooltip: true
},
present: {
width: 80
},
ip_last_login: {
width: 120
}
},
tableAttrs: {
stripe: true, // 斑马纹表格
border: true, // 表格边框
fit: true, // 宽度自适应,
tooltipEffect: 'dark',
rowClassName({ row, rowIndex }) {
return 'row-background-color'
}
}
},
headerActions: {
hasLeftActions: false,
hasCreate: false,
hasExport: true,
hasImport: false,
hasBulkDelete: false,
hasBulkUpdate: false,
searchConfig: {
exclude: ['asset']
}
}
}
}
}
}
</script>
<style lang="scss" scoped>
.asset-table ::v-deep .row-clicked, .asset-user-table ::v-deep .row-background-color {
background-color: #f5f7fa;
}
.asset-table {
&:hover {
cursor: pointer;
}
& >>> .table-content {
margin-left: 21px;
}
& ::v-deep .el-table__row{
height: 40px;
& > td{
padding: 0;
}
}
}
.mini-button{
width: 12px;
float: left;
margin-right: 10px;
text-align: center;
padding: 9px 0;
background-color: #1ab394;
border-color: #1ab394;
color: #FFFFFF;
border-radius: 5px;
line-height: 1.428;
cursor:pointer;
}
.noDataR{
width: 100%;
height: 40vh;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
flex-direction: column;
.hintWrap{
color: #D4D6E6;
display: flex;
align-items: flex-start;
justify-content: center;
flex-direction: column;
}
}
.asset-user-table{
padding-left: 20px;
& ::v-deep .el-table__header-wrapper thead tr{
height: 40px;
& > th{
padding: 0;
}
}
}
</style>

View File

@@ -17,10 +17,6 @@ export default {
[this.$t('common.Other'), ['comment']]
],
url: '/api/v1/xpack/gathered-user/tasks/',
initial: {
is_periodic: true,
interval: 24
},
hasDetailInMsg: false,
fieldsMeta: {
crontab: {
@@ -48,12 +44,16 @@ export default {
}
}
},
'is_periodic': {
is_periodic: {
type: 'switch'
}
},
createSuccessNextRoute: { name: 'GatherUserListIndex' },
updateSuccessNextRoute: { name: 'GatherUserListIndex' }
updateSuccessNextRoute: { name: 'GatherUserListIndex' },
cleanFormValue(values) {
values.interval = values.interval || null
return values
}
}
}
}

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