Compare commits

..

368 Commits

Author SHA1 Message Date
Jiangjie.Bai
0b18b57701 Merge pull request #1244 from jumpserver/dev
v2.17.0
2021-12-16 13:45:59 +08:00
“怀磊”
2804a488dc fix: 工单设置默认搜索条件 2021-12-16 13:17:36 +08:00
Jiangjie.Bai
0637fa9e8b Merge pull request #1242 from jumpserver/dev
v2.17.0 rc4
2021-12-15 22:04:42 +08:00
ibuler
66ecf89d35 perf: 优化 saml2 配置 2021-12-15 21:01:01 +08:00
xinwen
f11dc2f7a3 fix: ansible 任务日志读取错误 2021-12-15 18:01:25 +08:00
“怀磊”
e54c9cbd3b fix: 系统用户更新增加判断,如果设置了AD域名不能设置自动推送 2021-12-15 16:36:33 +08:00
“怀磊”
b9cdac0850 fix: 修复系统用户关联节点不显示问题 2021-12-15 16:36:07 +08:00
“怀磊”
33368cb901 perf: 优化搜索条件选项 2021-12-15 16:35:46 +08:00
“怀磊”
44e899e271 fix: 系统用户去掉重复的搜索条件 2021-12-15 16:22:20 +08:00
“怀磊”
f416cfa435 feat: 封装json editor组件 2021-12-15 15:36:53 +08:00
feng626
e2aa932ccb Merge pull request #1234 from jumpserver/pr@dev@translate
perf: 翻译修改
2021-12-15 14:41:02 +08:00
feng626
47e0435ad9 Merge pull request #1236 from jumpserver/pr@dev@ticket_filer_perf
perf: ticket filter perf
2021-12-15 14:40:49 +08:00
feng626
d36802e7bb perf: ticket filter perf 2021-12-15 14:24:08 +08:00
feng626
eda66cab41 perf: 翻译修改 2021-12-14 22:45:13 +08:00
Jiangjie.Bai
3a44b8fea7 Merge pull request #1233 from jumpserver/dev
v2.17.0 rc3
2021-12-14 21:59:16 +08:00
feng626
c461b707c8 fix: 云管特权用户bug 2021-12-14 20:20:25 +08:00
Jiangjie.Bai
01e6367196 Merge pull request #1230 from jumpserver/dev
v2.17.0 rc2
2021-12-13 19:48:49 +08:00
“怀磊”
aa2fe5e87e fix: 修改站内信文案和显示遮挡问题 2021-12-13 19:33:48 +08:00
“怀磊”
31d29e9e6a fex: 修复多个tab页相互切换时搜索条件还会保留问题,
cearchconfig 添加参数getUrlQuery:是否获取url中参数
2021-12-13 19:31:11 +08:00
“怀磊”
cb2ceea132 feat: 资产授权去掉节点的筛选条件 2021-12-13 19:29:00 +08:00
Michael Bai
582b2582bf fix: 查看节点详情不包含节点key信息 2021-12-13 19:28:30 +08:00
Michael Bai
89c6936d56 fix: 修复系统用户详情页面绑定命令过滤器的显示问题 2021-12-13 19:12:08 +08:00
jiangweidong
66ea5ab714 feat: saml2单点登录支持在页面上设置高级配置 2021-12-13 10:55:55 +08:00
Michael Bai
32dc622f1a fix: 修改命令过滤规则列表页字段排序问题 2021-12-13 10:39:18 +08:00
feng626
744acee097 Merge pull request #1223 from jumpserver/pr@dev@ticket_perf
perf: 默认展示当前组织
2021-12-13 10:26:30 +08:00
feng626
c088726412 perf: 默认展示当前组织 2021-12-11 12:53:48 +08:00
ibuler
bef02b3e29 fix: 修复创建 remote app 中类型 label 问题
fix: 修复
2021-12-09 21:03:19 +08:00
Jiangjie.Bai
821ce00f5c Merge pull request #1220 from jumpserver/dev
v2.17.0 rc1
2021-12-09 21:01:00 +08:00
ibuler
6d4d826629 perf: 修改翻译问题
fix: 修改词
2021-12-09 20:46:32 +08:00
Michael Bai
6e3ca56e98 fix: 修复系统用户命令过滤器创建页面 2021-12-09 20:09:23 +08:00
Michael Bai
90adcaf843 feat: 命令过滤器支持多维度绑定 2021-12-09 19:10:30 +08:00
jiangweidong
3eff78817b pr@dev@feat_saml2_auth (#1216)
* feat: 支持saml2协议单点登录认证

* fix: 合并遗漏的翻译文件

* fix: 合并遗漏的翻译文件
2021-12-09 16:01:46 +08:00
feng626
0b71032631 fix: 修复加密文件跳转命题 2021-12-08 17:31:27 +08:00
“怀磊”
ee2c15ccd5 feat: 查询参数保存在url中 2021-12-08 17:23:57 +08:00
halo
669bd50bfa feat: 站内信一键已读已经加载信息 2021-12-08 15:56:22 +08:00
feng626
5eff271cb2 feat: 密码计划邮件提醒 2021-12-07 17:01:00 +08:00
feng626
91162f6013 fix: 工单bug 2021-12-06 19:10:42 +08:00
“怀磊”
022309ef15 feat: 修改LICENSE 2021-12-06 18:03:36 +08:00
feng626
4466d0ddd4 feat: 工单创建资产添加节点 2021-11-30 16:54:19 +08:00
xinwen
04584bbd7b fix: 将应用从某个授权中移除时,会从所有授权中移除 2021-11-29 17:10:55 +08:00
ibuler
5ac8a6bdba feat: 资产显示标签 2021-11-29 14:57:11 +08:00
“怀磊”
6675a01b9d fix: 修复返回时间时区不同的问题 2021-11-29 14:39:52 +08:00
ibuler
84851b89a6 fix: 修复法国用户因为时区时间不同引起的错误 2021-11-25 17:51:23 +08:00
“怀磊”
408590a9e3 fix: 修复终端管理数字显示不正确问题 2021-11-25 14:48:40 +08:00
jiangweidong
09f91941d5 feat: 云管同步支持OpenStack私有云同步 2021-11-24 16:18:04 +08:00
fit2cloud--chenqiao
7132c869f7 feat: 支持数据库sqlserver的托管 (#1194) 2021-11-24 11:23:29 +08:00
Michael Bai
96c10f6980 fix: 修改翻译 2021-11-19 18:48:05 +08:00
feng626
9184026ca0 perf: ip黑白名单 2021-11-19 18:48:05 +08:00
Jiangjie.Bai
8b320d92c1 Merge pull request #1199 from jumpserver/dev
v2.16
2021-11-18 18:52:58 +08:00
“怀磊”
e46c1cfad9 fix: 修复系统设置-飞书认证:启动时按钮状态的显示 2021-11-18 18:50:04 +08:00
Michael Bai
f17df86e98 fix: 修复创建资产成功后提示信息中不包含详情链接 2021-11-18 18:08:19 +08:00
Jiangjie.Bai
fb89f710d7 Merge pull request #1196 from jumpserver/dev
v2.16.0 rc4
2021-11-18 17:00:51 +08:00
feng626
d64aea15c2 fix: 资产创建工单添加校验 2021-11-18 16:26:45 +08:00
“怀磊”
8c106ec718 fix: 替换了密码即将过期时天数的计算方法 2021-11-18 14:22:49 +08:00
Jiangjie.Bai
e5033407f4 Merge pull request #1191 from jumpserver/dev
v2.16.0 rc3
2021-11-18 13:03:22 +08:00
ibuler
7665104347 pref: 修改获取不到语言默认为英文 2021-11-18 12:54:24 +08:00
Jiangjie.Bai
cfb7f2be43 Merge pull request #1189 from jumpserver/dev
v2.16.0 rc2
2021-11-17 19:43:41 +08:00
Michael Bai
6fb9234583 fix: 修复资产列表资产树右击菜单添加/移动资产到节点确认/取消后自动关闭弹窗 2021-11-17 19:35:48 +08:00
Michael Bai
520abd783a fix: 修复更新资产更多信息成功后,点击详情跳转到编辑页面的问题 2021-11-17 19:35:32 +08:00
“怀磊”
133dd02de7 feat: 终端管理数字显示小数保留1位 2021-11-17 19:15:04 +08:00
feng626
c6988578cb perf: help信息迁移到public中 2021-11-17 15:45:29 +08:00
“怀磊”
b1eef7d2f3 perf: 搜索框组件:如果没有搜索选项不能点击下拉菜单 2021-11-17 15:24:03 +08:00
ibuler
526578a520 fix: 修复公告中的重置无反应 2021-11-17 15:22:17 +08:00
“怀磊”
adf5b51a63 fix: 修复应用授权详情,用户组不能删除问题 2021-11-17 15:05:13 +08:00
feng626
d9fab3f258 Merge pull request #1181 from jumpserver/pr@dev@ticket_flow_bug
fix: 修复工单流修改不生效
2021-11-17 11:40:08 +08:00
feng626
7887458239 fix: 修复工单流修改不生效 2021-11-17 11:36:18 +08:00
“怀磊”
4ed40f763c fix: 修复用户登录规则详情更新完之后,点击message跳转详情没有数据问题 2021-11-15 17:14:14 +08:00
ibuler
64b3be5d45 fix: 修复重置mfa的bug 2021-11-15 16:23:41 +08:00
“怀磊”
9a1263f9af fix: 修复select2组件选项过长文字溢出问题 2021-11-15 12:23:10 +08:00
feng626
bdb38362ff fix: 修复登录资产复合路由跳转问题 2021-11-15 11:26:29 +08:00
“怀磊”
cf8b0970e3 fix: 修复工单-登录复核:点击返回路由不能返回列表页问题 2021-11-12 19:06:27 +08:00
“怀磊”
e9bab6a623 fix: 修复创建工单时必填字段校验不生效问题 2021-11-12 19:05:47 +08:00
feng626
df088cc1b6 fix: 修复关闭ssh后 仍然可以修改密钥 2021-11-12 18:00:39 +08:00
“怀磊”
63cde369fa fix: 密码框使用 updatetoken组件 2021-11-12 15:04:03 +08:00
“怀磊”
902364fdc3 fix: 修复命令记录,命令存储tree搜索时刷新问题 2021-11-12 15:02:18 +08:00
feng626
cceca0e1ab feat: 添加全局ip黑名单 2021-11-12 14:58:26 +08:00
Michael Bai
39b84e9a8f fix: 移动【会话设置保留时间】至定期清理设置页面 2021-11-12 14:57:32 +08:00
feng626
1e2b6caf93 fix: 完善工单样式 2021-11-12 14:15:24 +08:00
Jiangjie.Bai
4f7ea2b0ab Merge pull request #1167 from jumpserver/dev
v2.16.0 rc1
2021-11-11 14:03:33 +08:00
ibuler
e0b98ba758 perf: 修改i18n
pref: 修改翻译
2021-11-11 13:01:51 +08:00
ibuler
a7ba36b734 perf: 优化资产更多信息 2021-11-11 13:00:15 +08:00
ibuler
27332dffb8 perf: 修改翻译 2021-11-10 17:11:42 +08:00
jiangweidong
2d174bc46d feat: 支持管理员配置导航栏上帮助中的url 2021-11-10 17:09:28 +08:00
“怀磊”
f000810ccf fix: 优化UpdateToken组件:创建时直接显示输入框和防止浏览器自动填充密码 2021-11-10 17:05:14 +08:00
xinwen
68a08a25dc feat: 异地登录提醒可配置是否启用 2021-11-08 15:25:36 +08:00
Michael Bai
b5a268bf83 fix: 修改切换用户 -> 用户切换 2021-11-05 16:42:11 +08:00
Michael Bai
c446d1dcf6 feat: 添加Su用户列表 2021-11-05 16:04:07 +08:00
Michael Bai
92620510da feat: 添加切换至用户列表 2021-11-05 16:04:07 +08:00
Michael Bai
d62a4ca36e feat: 添加系统用户su_enabled 2021-11-05 16:04:07 +08:00
Michael Bai
d2cd0a2242 feat: 添加OBS存储 2021-11-05 13:38:25 +08:00
“怀磊”
a39f2294bc fix: 修复创建工单,应用类型下拉选择联动清空 2021-11-04 18:13:00 +08:00
halo
4a8f147178 fix: 修复用户应用授权规则更新和删除操作报错 2021-11-04 12:39:26 +08:00
“怀磊”
69b378c2a7 feat: rdp 系统用户还原自动生成密码 2021-11-04 11:43:57 +08:00
“怀磊”
55743eb2c8 feat: 修改系统用户查看ssh密钥的input框展示样式 2021-11-03 17:37:11 +08:00
“怀磊”
9ba5214937 perf: 系统用户相关表单使用UpdateToken组件 2021-11-03 17:01:24 +08:00
fit2bot
e1c6d0a686 feat: 封装密码输入框组件 (#1149)
* feat: 封装密码输入框组件

* feat: 批量替换密码输入框组件

* perf: 修改样式

* feat: 批量替换密码输入框组件

Co-authored-by: “怀磊” <2280131253@qq.com>
Co-authored-by: ibuler <ibuler@qq.com>
2021-11-02 18:51:58 +08:00
Michael Bai
b68616501c fix: 修复UploadKey失败问题 2021-11-01 14:58:20 +08:00
Michael Bai
3355036f49 fix: 修改基本设置项
fix: 修改基本设置项
2021-10-29 19:24:57 +08:00
xinwen
7ee3d307df fix: 非本地用户不能绑定三方认证 2021-10-29 13:20:17 +08:00
Michael Bai
803090dff9 fix: 修改基本设置工单启用控制 2021-10-29 13:19:35 +08:00
Jiangjie.Bai
886d34211c Merge pull request #1138 from jumpserver/dev
v2.15.0
2021-10-28 18:13:43 +08:00
fit2bot
22d8cb7e44 fix: 创建工单-应用类型,去掉默认值 (#1137)
* fix: 修复创建应用授权工单报错问题

* fix: 创建工单-应用类型,去掉默认值

Co-authored-by: “怀磊” <2280131253@qq.com>
2021-10-28 18:12:31 +08:00
Jiangjie.Bai
1a1c50086f Merge pull request #1136 from jumpserver/dev
v2.15.0
2021-10-28 17:50:12 +08:00
feng626
87c8d11bf4 fix: acl migrate 2021-10-28 17:38:30 +08:00
“怀磊”
dcbb73e59d fix: 修复创建应用授权工单报错问题 2021-10-28 17:25:00 +08:00
“怀磊”
79068f44bf fix: crontab组件增加清除功能 2021-10-28 16:59:15 +08:00
Jiangjie.Bai
0575ff606c Merge pull request #1132 from jumpserver/dev
v2.15.0
2021-10-28 15:32:37 +08:00
ibuler
68411de25b perf: 修复组织table
perf: 修改翻译
2021-10-28 15:21:37 +08:00
“怀磊”
d174fdae6f fix: 修复table组件的tooltip提示的最大宽度 2021-10-28 14:34:14 +08:00
ibuler
fa1dad3731 perf: 修改requied提示 2021-10-28 14:19:07 +08:00
Michael Bai
44edc1e712 fix: 修复创建云服务账号json格式文件判断 2021-10-28 13:36:22 +08:00
feng626
1cd8de8cbc fix: 登陆复合信息显示不全 2021-10-28 13:25:10 +08:00
Jiangjie.Bai
040cb0aa5a Merge pull request #1124 from jumpserver/dev
v2.15.0-rc3
2021-10-27 19:34:31 +08:00
“怀磊”
ffe152809d feat: 系统设置邮件服务器设置设置传参 2021-10-27 19:34:12 +08:00
“怀磊”
dc0bb5d557 fix: 定时执行控制定期执行显示隐藏 2021-10-27 19:07:08 +08:00
“怀磊”
233aaaa82b feat: 分时段登录组件增加全选功能 2021-10-27 14:46:41 +08:00
ibuler
b13e5995c9 perf: 修改label 2021-10-27 14:30:36 +08:00
ibuler
5f8e2e4f75 perf: 修改ticket组件名称,太长了 2021-10-27 14:30:36 +08:00
Michael Bai
b9baf360c0 fix: 修改工单列表action->state 2021-10-27 11:16:27 +08:00
“怀磊”
5e40da0037 feat: 调整远程应用创建是类型展示风格 2021-10-27 10:31:26 +08:00
Michael Bai
a948105ebd fix: 普通系统用户列表搜索项排除: type 字段 2021-10-26 18:10:17 +08:00
“怀磊”
46c9598788 feat: 用户登录规则详情:修改时段展示、增加右侧快速更新板块 2021-10-26 15:18:30 +08:00
ibuler
8f35d4edc0 fix: 修复启用公告后,不变绿的问题 2021-10-26 15:12:48 +08:00
ibuler
2e70c3fa4c perf: 去掉debug 2021-10-26 15:12:19 +08:00
ibuler
5e377c2572 perf: 修改工单 2021-10-26 15:12:19 +08:00
Michael Bai
d50b1201e7 fix: 修复用户详情页面最后登录日期显示1970的问题 2021-10-26 13:47:16 +08:00
“怀磊”
fc72fe9ff2 fix: 修改日志审批-批量命令,主机弹窗展示字段和遮罩层问题 2021-10-26 13:07:04 +08:00
ibuler
23a7d9dfbe perf: 统一dashboard页面,去掉email签名设置 2021-10-26 11:12:36 +08:00
Jiangjie.Bai
08452d1e00 Merge pull request #1112 from jumpserver/dev
v2.15.0-rc2
2021-10-25 15:08:35 +08:00
Michael Bai
45f29e3e6d fix: 系统用户列表添加 应用数量 字段 2021-10-25 15:06:45 +08:00
feng626
3f03f0f18c Merge pull request #1109 from jumpserver/pr@dev@ticket_bug
fix: 工单资产信息不显示
2021-10-25 11:32:28 +08:00
feng626
2d3387b739 fix: 工单资产信息不显示 2021-10-25 11:25:24 +08:00
“怀磊”
6d50c6df35 fix: 修复公告组件数字不换行问题 2021-10-21 18:11:16 +08:00
ibuler
0a3f7f6616 fix: 修复 select2 error bug 2021-10-21 17:34:32 +08:00
feng626
4c74e75673 Merge pull request #1104 from jumpserver/pr@dev@ticket_bug
fix: 工单详情资产信息未显示
2021-10-21 15:11:11 +08:00
feng626
98c44a51e7 Merge pull request #1106 from jumpserver/pr@dev@acl_bug
perf: acl 列表界面添加字段
2021-10-21 15:10:55 +08:00
feng626
6cb7e47747 perf: acl 列表界面添加字段 2021-10-21 14:57:53 +08:00
feng626
5b96475610 fix: 工单详情资产信息未显示 2021-10-21 14:37:30 +08:00
Jiangjie.Bai
c0a412a12d Merge pull request #1102 from jumpserver/dev
v2.15.0 rc1
2021-10-20 20:25:15 +08:00
“怀磊”
4b75d90090 fix: 修复crontab月组件名称错误问题 2021-10-20 19:53:10 +08:00
“怀磊”
7dd11f1259 fix: 修复crontab选择月 日的时间重置问题 2021-10-20 19:53:10 +08:00
ibuler
761eb0c3b1 perf: 修改ldap密码不是必填 2021-10-20 19:16:15 +08:00
“怀磊”
f7fb35e625 fix: 修复资产授权列表-来自工单,否显示空 2021-10-20 19:14:27 +08:00
fit2bot
224b13b4e7 perf: 整理 acl 位置 (#734)
* stash

* s

* 界面调整

* feat: 添加分时段选择组件

* feat: 添加分时段选择组件

* feat: 分时段登录组件创建时传参

* 分时登陆

* feat: 分时段组件展示默认数据

* feat: 分时段选中时间在详情里展示

* alc 部分优化

* fix: 修复用户规则登录error报错

* feat: 调整规则详情-时段展示

Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: feng626 <1304903146@qq.com>
Co-authored-by: “怀磊” <2280131253@qq.com>
2021-10-20 18:56:59 +08:00
“怀磊”
ae9b3f2707 style: 修复跳转链接文本溢出长度问题 2021-10-20 14:47:12 +08:00
Michael Bai
0675f22181 fix: 用户登录日志添加reason_display字段 2021-10-19 15:29:39 +08:00
fit2bot
92dd661a3b 登录验证可配置 (#1092)
* 登录验证可配置

* perf: 修改安全内容设置

* perf: 还原yarn

Co-authored-by: feng626 <1304903146@qq.com>
Co-authored-by: ibuler <ibuler@qq.com>
2021-10-18 14:50:57 +08:00
“怀磊”
1926cdde15 style: table跳转链接文本溢出显示省略号随页面宽度变化 2021-10-15 15:28:00 +08:00
fit2bot
3e60c9501e feat: 资产详情添加授权用户列表页 (#1093)
* feat: 资产详情添加授权用户列表页

* feat: 取消console log

* perf: 优化一下前端

* feat: 修改授权用户组分隔符

Co-authored-by: Michael Bai <baijiangjie@gmail.com>
Co-authored-by: ibuler <ibuler@qq.com>
2021-10-15 15:02:31 +08:00
“怀磊”
c86fb5cfde fix: 全局导出公告组件 2021-10-12 16:11:20 +08:00
“怀磊”
d8e1e14bd6 feat: crontab 组件封装 2021-10-12 11:13:30 +08:00
“怀磊”
bb9e9a87d0 feat: 列表增加定时执行字段展示 2021-10-12 10:52:18 +08:00
“怀磊”
96506acc9b feat: 授权列表来自工单,是显示对勾 2021-10-12 10:29:01 +08:00
ibuler
9a5171a1bf perf: 优化公告 2021-10-12 10:27:12 +08:00
Jiangjie.Bai
d1f61797a5 Merge pull request #1085 from jumpserver/pr@dev@feat_annoucement
perf: 完成公告功能
2021-09-29 14:59:42 +08:00
ibuler
30b936330c perf: 优化翻译 2021-09-28 18:59:34 +08:00
ibuler
6f5db82ce3 perf: 优化公告 2021-09-28 18:09:34 +08:00
ibuler
9fcc9bc9da perf: 完成公告功能 2021-09-28 17:02:27 +08:00
ibuler
972b7276cf feat: 添加公告功能 2021-09-28 10:20:39 +08:00
ibuler
8db10e806a Merge branch 'dev' of github.com:jumpserver/lina into dev 2021-09-27 15:40:02 +08:00
fit2bot
1cb6e49785 perf: 优化站内信 (#1082)
* perf: 修改不再存储 .env.devlopement

* perf: 优化站内信

Co-authored-by: ibuler <ibuler@qq.com>
2021-09-27 15:40:00 +08:00
ibuler
8950fadfa1 Merge branch 'dev' of github.com:jumpserver/lina into dev 2021-09-27 14:54:22 +08:00
fit2bot
e32d4e7ee0 perf: 优化弹窗 (#1081)
* perf: 修改不再存储 .env.devlopement

* perf: 优化弹窗

Co-authored-by: ibuler <ibuler@qq.com>
2021-09-27 14:54:14 +08:00
fit2bot
a382c82732 perf: 优化代码,搜索 (#1080)
* perf: 修改不再存储 .env.devlopement

* perf: 优化代码,搜索

Co-authored-by: ibuler <ibuler@qq.com>
2021-09-27 14:29:37 +08:00
ibuler
fa458cd456 Merge branch 'dev' of github.com:jumpserver/lina into dev 2021-09-27 14:23:10 +08:00
fit2bot
731d01450f fix: 修复setting中forget url 多处显示的问题 (#1079)
* perf: 修改不再存储 .env.devlopement

* fix: 修复setting中forget url 多处显示的问题

Co-authored-by: ibuler <ibuler@qq.com>
2021-09-27 14:22:51 +08:00
ibuler
bf97d82626 Merge branch 'dev' of github.com:jumpserver/lina into dev 2021-09-24 15:11:04 +08:00
Michael Bai
49b8e0b33a fix: 修复邮件测试参数传递 2021-09-24 14:15:09 +08:00
ibuler
5a913e4d3a branch -D pr@dev@perf_env_file
Merge branch 'dev' of github.com:jumpserver/lina into dev
2021-09-24 13:29:22 +08:00
ibuler
2983f89f54 perf: 修改不再存储 .env.devlopement 2021-09-24 13:29:00 +08:00
ibuler
87a0384949 perf: 修改不再存储 .env.devlopement 2021-09-24 13:20:39 +08:00
ibuler
d36fe8aa94 perf: 修复 asset select 2021-09-24 13:14:04 +08:00
“怀磊”
5764ddc271 fix: 修复webpack配置报错 2021-09-23 13:56:01 +08:00
“怀磊”
edbdef3f58 perf: 增加webpack别名配置 2021-09-22 18:18:52 +08:00
feng626
5d308ac546 perf: 工单优化 2021-09-22 17:20:18 +08:00
ibuler
986cf6b4f7 perf: 修复dialog 不能滚动问题 2021-09-22 17:02:17 +08:00
ibuler
6cbac3c875 fix: 修复select2初始可能报错的bug
perf: 去掉debug
2021-09-18 15:05:44 +08:00
feng626
09a04efe1a Merge pull request #1062 from jumpserver/pr@dev@ticket_perf
perf: 工单 优化
2021-09-17 15:50:53 +08:00
feng626
d5b631c012 perf: 工单 优化 2021-09-17 15:40:02 +08:00
Jiangjie.Bai
7e8de4c73b Merge pull request #1061 from jumpserver/dev
merge: dev to master
2021-09-16 20:16:02 +08:00
老广
47a0641fed Merge pull request #1060 from jumpserver/pr@dev@perF_setting
perf: 修改setting
2021-09-16 20:02:35 +08:00
ibuler
ad118c5eda perf: 修改setting 2021-09-16 20:01:38 +08:00
老广
9bd3b37371 Merge pull request #1059 from jumpserver/dev
Merge dev to master
2021-09-16 19:33:01 +08:00
ibuler
274f50da95 fix: 修改select2 2021-09-16 19:09:55 +08:00
xinwen
8f87c42c8c fix: Otp 绑定问题 2021-09-16 19:09:34 +08:00
ibuler
35c793785c perf: 优化 detail formatter 2021-09-16 16:54:01 +08:00
feng626
abe176831b fix: 修复数据库改密bug 2021-09-16 16:53:41 +08:00
xinwen
c97512c99b fix: 重置用户密码提示消息 2021-09-16 16:46:43 +08:00
Jiangjie.Bai
6a8553ddfd Merge pull request #1053 from jumpserver/dev
v2.14.0 rc3
2021-09-15 21:06:24 +08:00
feng626
23f1f371e6 Merge pull request #1052 from jumpserver/pr@dev@mail_setting_bug
fix: 去掉站内信
2021-09-15 20:45:11 +08:00
feng626
0878e93f24 fix: 去掉站内信 2021-09-15 20:36:15 +08:00
fit2bot
cc76a66a34 perf: 优化用户资产页面宽度 (#1049)
Co-authored-by: ibuler <ibuler@qq.com>
2021-09-15 20:32:39 +08:00
fit2bot
d17556cdc7 perf: 优化认证中的url检查 (#1050)
* perf: 优化认证中的url检查

* perf: 优化配置

Co-authored-by: ibuler <ibuler@qq.com>
2021-09-15 20:32:17 +08:00
Michael Bai
f5f4edaf63 fix: 修复vmware账号密码必填标志显示问题 2021-09-15 20:21:53 +08:00
Michael Bai
62329d57a4 fix: 修复系统用户页面资产页中没有显示已关联节点的问题 2021-09-15 19:40:13 +08:00
ibuler
bc90d1e9c6 perf: 优化安全设置,保存后刷新页面 2021-09-15 19:39:35 +08:00
feng626
60d4da29cb Merge pull request #1047 from jumpserver/pr@dev@ticket_perf
perf: 工单流详情优化
2021-09-15 19:34:52 +08:00
feng626
b9bdf35bb9 perf: 工单流详情优化 2021-09-15 19:32:28 +08:00
fit2bot
b9b6bccb7f perf: 优化路由 (#1045)
* perf: 优化路由

* perf: 优化提示

* oerf: ...

Co-authored-by: ibuler <ibuler@qq.com>
2021-09-15 18:56:09 +08:00
xinwen
f2bbe4841a feat: 用户详情页面显示 手机号,钉钉,企业微信,飞书 2021-09-15 18:26:18 +08:00
Michael Bai
d920eeda7a fix: 修改创建/更新系统用户标题文案 2021-09-15 18:05:04 +08:00
ibuler
7361acae6a perf: 优化弹窗显示 2021-09-15 18:04:39 +08:00
xinwen
a56799ca22 fix: 用户不能重置自己的 mfa 2021-09-15 17:28:53 +08:00
Michael Bai
88ddd86154 fix: 修改录像水印 2021-09-15 17:28:07 +08:00
feng626
d9532d5a93 Merge pull request #1040 from jumpserver/pr@dev@ticket_perf
perf: 工单详情样式优化
2021-09-15 14:07:18 +08:00
feng626
02b9e83e3d perf: 工单详情样式优化 2021-09-15 14:03:35 +08:00
feng626
a29afbbeea Merge pull request #1039 from jumpserver/pr@dev@ticket_bug
perf: 工单优化
2021-09-15 11:19:02 +08:00
feng626
0f4f4a2ca7 perf: 工单优化 2021-09-15 11:18:26 +08:00
feng626
a23cd151ae Merge pull request #1037 from jumpserver/pr@dev@ticket_flow_perm
fix: 工单流程权限
2021-09-14 22:26:23 +08:00
feng626
5f3855d8c1 fix: 工单流程权限 2021-09-14 22:12:30 +08:00
ibuler
6ad44e2830 perf: 修复路由问题 2021-09-14 18:54:32 +08:00
ibuler
51446c0c9a perf: 添加会话翻译 2021-09-14 17:43:34 +08:00
feng626
e3a493a6c5 fix: 不同组织资产可读 2021-09-14 17:42:06 +08:00
Michael Bai
9dfca98828 fix: 删除 PERIOD_TASK_ENABLED 配置 2021-09-14 17:41:34 +08:00
Michael Bai
599a1bb705 fix: 修复CAS配置RENAME_ATTR未加载的问题 2021-09-14 16:42:59 +08:00
ibuler
52e4a6d6ed perf: remove debug 2021-09-14 16:41:57 +08:00
ibuler
4b5ac9aaa9 perf: 优化资产列表,拆分大组件 2021-09-14 16:41:57 +08:00
feng626
3b2c4975b6 Merge pull request #1030 from jumpserver/pr@dev@ticket_perf
perf: 工单详情优化
2021-09-14 14:09:55 +08:00
feng626
c3cbafce6d perf: 工单详情优化 2021-09-14 14:00:53 +08:00
ibuler
b75f0be9e5 fix: 修复资产页面table不能拖拽 2021-09-14 11:02:09 +08:00
Jiangjie.Bai
8df0f05ee5 Merge pull request #1028 from jumpserver/dev
v2.14.0 rc2
2021-09-13 21:28:12 +08:00
Michael Bai
fa7a78523f fix: i18n 2021-09-13 20:44:30 +08:00
ibuler
11e803be19 perf: 优化短信通用 2021-09-13 20:40:18 +08:00
Michael Bai
060781443f fix: 授权规则支持通过 from_ticket 工单过滤 2021-09-13 20:25:11 +08:00
fit2bot
555c8fd2d6 perf: 优化短信设置 (#1024)
* perf: 优化短信设置

* perf: 优化分宜

Co-authored-by: ibuler <ibuler@qq.com>
2021-09-13 19:44:14 +08:00
feng626
2c84e4ffa6 fix: 修复工单bug 2021-09-13 14:18:58 +08:00
Michael Bai
16dafaaf42 perf: 取消API Token的配置 2021-09-13 14:03:16 +08:00
xinwen
819438a2e8 fix: 用户消息订阅暂不支持短信 2021-09-13 13:25:20 +08:00
fit2bot
61e8467849 perf: 优化更新auth (#1018)
* perf: 优化更新auth

* perf: 优化提交

Co-authored-by: ibuler <ibuler@qq.com>
2021-09-10 16:46:51 +08:00
feng626
491f3d3dde feat: 添加登陆复合 2021-09-10 16:23:02 +08:00
ibuler
4588965a8d perf: 优化站内信点击的问题 2021-09-10 13:59:19 +08:00
feng626
48d07b4d6d perf: 工单流优化 2021-09-10 13:51:44 +08:00
feng626
45c74ba470 perf: sms样式修改 2021-09-10 13:51:08 +08:00
feng626
2e2c8fe086 feat: 短信迁移 2021-09-10 13:51:08 +08:00
Michael Bai
459843669b perf: 修改授权规则Actions上传下载复制粘贴选择时,连接不可取消 2021-09-10 11:28:36 +08:00
fit2bot
f839e1fc0d perf: 去掉debug (#1013)
* perf: 去掉debug

* perf: 修改 debug

* perf: 修改msg

Co-authored-by: ibuler <ibuler@qq.com>
2021-09-10 10:21:03 +08:00
Jiangjie.Bai
399b381aa9 Merge pull request #1014 from jumpserver/dev
v2.14.0 rc1
2021-09-09 21:19:35 +08:00
Eric_Lee
7670c277aa Merge pull request #1012 from jumpserver/revert-1004-dependabot/npm_and_yarn/axios-0.21.2
Revert "chore(deps): bump axios from 0.21.1 to 0.21.2"
2021-09-09 21:03:52 +08:00
老广
cbc78de7c4 Revert "chore(deps): bump axios from 0.21.1 to 0.21.2"
This reverts commit e8f013a5be.
2021-09-09 21:02:36 +08:00
ibuler
e64cd82dba perf: 修改setting 2021-09-09 20:45:21 +08:00
ibuler
f154a252d3 perf: 修改配置 2021-09-09 20:01:06 +08:00
Michael Bai
984483c470 perf: 用户个人信息页面添加开启消息接受方式 2021-09-09 19:46:31 +08:00
dependabot[bot]
e8f013a5be chore(deps): bump axios from 0.21.1 to 0.21.2
Bumps [axios](https://github.com/axios/axios) from 0.21.1 to 0.21.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/master/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.21.1...v0.21.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-09 19:41:02 +08:00
ibuler
e1850d758f perf: 提供xrdp开启禁用 2021-09-09 19:39:28 +08:00
feng626
60e5c994f4 feat: 短信服务配置 2021-09-09 18:53:32 +08:00
xinwen
1adf138175 fix: 用户个人页面,修改个人部分信息使用 patch 2021-09-09 16:16:05 +08:00
fit2bot
4e5ed82ec7 perf: 优化导入导出顺序 (#999)
Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: Jiangjie.Bai <32935519+BaiJiangJie@users.noreply.github.com>
2021-09-09 16:15:21 +08:00
fit2cloud-jiangweidong
52e2d58567 feat: 改密计划支持数据库改密 (#991)
* feat: 改密计划支持数据库改密

* perf: 修改应用账号

* merge: dev

* perf: 暂存修改数据库

* fix: bug

* fix: bug

* perf: 优化系统用户详情,增加应用列表

* perf: 修改clone

* fix: 修复更新系统用户bug

Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: feng626 <1304903146@qq.com>
2021-09-09 16:04:40 +08:00
fit2bot
411fca98b0 feat: 页面配置serializer版 (#1000)
* feat: 页面配置serializer版

* perf: 优化配置

* perf: 优化匹配值

* perf: 优化设置

* perf: 优化配置

* perf: 修改翻译

* perf: 修改secure

Co-authored-by: feng626 <1304903146@qq.com>
Co-authored-by: ibuler <ibuler@qq.com>
2021-09-09 14:03:47 +08:00
Bai
e0c3243986 feat: 云管中心添加GCP 2021-09-07 18:26:48 +08:00
Jiangjie.Bai
e4240d7a8f feat: 会话详情添加活动页面 (#1003)
* fix: 添加配置 SECURITY_SESSION_SHARE

* fix: 添加会话活动页面

* fix: 添加会话活动页面

* fix: 添加会话活动页面

* fix: 添加会话活动页面
2021-09-07 18:16:38 +08:00
feng626
db4b09603b perf: disabled 2021-09-06 18:16:51 +08:00
Eric
a88499d10c fix: 修复普通用户页面 动作 问题 2021-08-27 15:17:31 +08:00
feng626
0b2cfb88a2 Merge pull request #993 from jumpserver/pr@dev@ticket_flow_bug
perf: 优化
2021-08-27 10:14:17 +08:00
feng626
13443a7137 perf: 优化 2021-08-26 18:31:52 +08:00
fit2bot
5aa3226132 feat: 审批模版管理 (#964)
Co-authored-by: feng626 <1304903146@qq.com>
Co-authored-by: Jiangjie.Bai <32935519+BaiJiangJie@users.noreply.github.com>
2021-08-25 19:01:45 +08:00
fit2bot
ea9316f0a0 feat: 工单管理 (#963)
* feat: 工单多级审批

* fix: action bug

Co-authored-by: feng626 <1304903146@qq.com>
2021-08-25 19:00:21 +08:00
ibuler
d05d3f3fa3 perf: 优化创建简单 2021-08-20 14:22:09 +08:00
Bai
5da000c8a1 fix: 修复AssetSelect弹窗的protocols显示 2021-08-20 14:14:36 +08:00
Jiangjie.Bai
f85d3567a7 Merge pull request #986 from jumpserver/dev
v2.13.0
2021-08-19 18:53:13 +08:00
ibuler
78174b23fd fix: 修复app详情更新按钮 2021-08-19 16:05:10 +08:00
xinwen
07151b2bfe fix: 命令记录默认倒序 2021-08-19 15:47:28 +08:00
ibuler
1cc395c22d perf: 修复 url 2021-08-19 15:36:00 +08:00
ibuler
a18507f9c6 perf: 修复命令导出问题
perf: 优化
2021-08-19 15:00:59 +08:00
ibuler
790e2315e2 perf: 修复app 授权详情中更新 2021-08-19 12:00:03 +08:00
Jiangjie.Bai
4f2f4a7a9d Merge pull request #978 from jumpserver/dev
v2.13.0 rc4
2021-08-18 19:46:06 +08:00
Bai
32817390c2 fix: 修复同步任务创建时协议字段为空的错误提示 2021-08-18 19:33:16 +08:00
Bai
8af7c46f02 feat: 系统设置添加水印开启选项 2021-08-18 19:15:55 +08:00
ibuler
ed4162cbd5 perf: 优化select2初始值
perf: 优化select2

perf: 修改初始值
2021-08-18 15:16:42 +08:00
Eric
973b1fbae2 fix: 修复消息接受人用户搜索问题 2021-08-18 11:50:28 +08:00
ibuler
8906fa3292 fix: 修复资产树点击的bug 2021-08-18 11:13:16 +08:00
Bai
5c94c1c870 fix: 修复管理员更新用户登录规则没有权限的问题 2021-08-18 10:32:13 +08:00
Jiangjie.Bai
16757b7df0 Merge pull request #972 from jumpserver/dev
v2.13.0 rc3
2021-08-17 20:34:39 +08:00
Bai
679103db03 fix: 更新用户,密码规则根据当前用户的原角色(非当前表单角色)进行处理 2021-08-17 20:29:12 +08:00
ibuler
a7aa763e37 fix: 修复导入时页码跳动问题 2021-08-17 20:28:21 +08:00
ibuler
f30ba0a98d perf: 修改应用权限创建的bug
perf: 修改列表

fix: 去掉get url
2021-08-17 14:00:46 +08:00
ibuler
c426841a7c perf: 优化应用授权 2021-08-17 14:00:46 +08:00
老广
288bfe4727 Merge pull request #967 from jumpserver/pr@dev@fix_ldap_setting
fix(settings): 修复ldap settings的bug
2021-08-16 15:09:48 +08:00
ibuler
c4c39188aa fix(settings): 修复ldap settings的bug 2021-08-16 15:07:07 +08:00
老广
52d8bfeac3 Merge pull request #966 from jumpserver/dev
v2.13.0-rc1
2021-08-16 11:24:36 +08:00
ibuler
20c201f351 fix(applications): 修复更新数据库页面不对的问题 2021-08-16 11:01:25 +08:00
老广
94b5aa700d Merge pull request #962 from jumpserver/dev
v2.13.0 rc1
2021-08-13 11:05:47 +08:00
fit2bot
e030fc313e feat: 添加飞书 (#958)
* feat: 添加飞书

* s

* 翻译

* 翻译

Co-authored-by: xinwen <coderWen@126.com>
2021-08-13 11:03:53 +08:00
Jiangjie.Bai
917fa0ea44 Merge pull request #961 from jumpserver/dev
v2.13.0 rc1
2021-08-12 19:51:30 +08:00
Bai
68df311a9c feat: 云管中心同步任务添加 unix_admin_user 和 windows_admin_user 2021-08-11 14:43:25 +08:00
Bai
e819e4aaff feat: 添加Monitor组件 celery和core 2021-08-10 10:17:21 +08:00
feng626
364b6b0bcf perf: 改密计划前端优化 2021-08-06 17:25:11 +08:00
feng626
a0534e0c2e feat: xpack 批量修改密钥 2021-08-03 15:47:29 +08:00
ibuler
0152ad269b perf: 修改import 2021-07-30 19:28:19 +08:00
ibuler
b651f82eb6 perf: 优化树 2021-07-30 17:30:22 +08:00
ibuler
0e665912ff perf: 优化应用授权列表 2021-07-30 17:30:22 +08:00
fit2bot
8929957fd9 perf: 管理员密码长度 (#946)
* feat: 支持配置系统管理员强制MFA和独立密码长度限制

* feat: 支持配置系统管理员强制MFA和独立密码长度限制

* feat: 支持配置系统管理员强制MFA和独立密码长度限制

* feat: 支持配置系统管理员强制MFA和独立密码长度限制

* fix: 修改管理员最小密码长度变量名称

* fix: 修改管理员最小密码长度变量名称

* perf: 基本完成更改

Co-authored-by: fit2cloud-jiangweidong <weidong.jiang@fit2cloud.com>
Co-authored-by: ibuler <ibuler@qq.com>
2021-07-30 15:33:49 +08:00
Bai
959bb56a3e fix: 修复数据库应用更新页面异常问题 2021-07-29 14:57:48 +08:00
fit2bot
cb5fe0f2a0 perf: 使用 mini 接口 (#945)
* perf: 使用 mini 接口

* perf: 资产列表默认展示平台和协议

* perf: 添加排序

Co-authored-by: 吴小白 <296015668@qq.com>
2021-07-29 14:57:20 +08:00
吴小白
05c06e4fc2 feat: 资产授权添加批量删除 2021-07-29 11:33:04 +08:00
ibuler
d3ae9e3f0f perf: 账号列表去掉克隆 2021-07-28 18:20:42 +08:00
fit2bot
1af14a5743 perf: 修改系统用户展现 (#940)
* perf: 优化系统用户select显示

* perf: 修改系统用户展现

Co-authored-by: ibuler <ibuler@qq.com>
2021-07-28 15:18:29 +08:00
feng626
7c0621b357 feat: 应用管理添加批量删除 2021-07-27 16:18:32 +08:00
feng626
4d4085798d fix: 去除前端规则判断 自定义承接后端required 2021-07-27 15:52:34 +08:00
Jiangjie.Bai
0da0c2af4f Merge pull request #931 from jumpserver/pr@dev@perf_account_list
perf: 优化账号列表
2021-07-27 15:52:06 +08:00
ibuler
ba78bdeed7 perf: 还原收集用户 2021-07-27 14:37:00 +08:00
ibuler
0f76f6ad96 perf: 基本完成应用正好改造 2021-07-27 14:18:37 +08:00
Bai
2a14b0cfcd feat: 改密计划执行列表添加trigger字段和搜索项 2021-07-27 10:45:55 +08:00
Jiangjie.Bai
8537a7b6e4 Merge pull request #935 from jumpserver/pr@dev@fix_generic_route_userloginacl
fix: 修改通用创建更新成功后的Route; 优化用户ACL创建ip_group错误时的信息显示
2021-07-26 17:45:25 +08:00
Bai
bf74adc037 fix: 修改通用创建更新成功后的Route; 优化用户ACL创建ip_group错误时的信息显示 2021-07-26 17:20:36 +08:00
Bai
f7d75ca92c fix: 修改通用创建更新成功后的Route; 优化用户ACL创建ip_group错误时的信息显示 2021-07-26 17:11:43 +08:00
fit2bot
0d3029e6b1 perf: 优化表单 (#933)
* perf: 优化表单

* fix: 修复国际化

* fix: 去掉排序

* perf: 系统平台排序

* fix: 修复错误

* fix: 去掉多余的逗号

Co-authored-by: 吴小白 <296015668@qq.com>
2021-07-26 17:09:28 +08:00
feng626
7fda66cda4 工单处理人显示 与右侧一致 2021-07-26 17:04:13 +08:00
ibuler
de4af07858 perf: 修改 icon 2021-07-26 11:09:06 +08:00
Bai
6bab31d74c fix: 修改账号列表更新表单错误提示 2021-07-23 19:02:52 +08:00
ibuler
1c6da15301 perf: 优化账号列表 2021-07-22 18:56:04 +08:00
dependabot[bot]
cbd18814f6 chore(deps): bump ws from 5.2.2 to 5.2.3 (#838)
* chore: 更新Submodule指向

* fix: 修复Build失败的问题

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

* chore(deps): bump ws from 5.2.2 to 5.2.3

Bumps [ws](https://github.com/websockets/ws) from 5.2.2 to 5.2.3.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/5.2.2...5.2.3)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: indirect
...

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-07-22 14:15:37 +08:00
ibuler
3be8794a58 perf: 优化记录当前组织和角色 2021-07-22 14:08:00 +08:00
ibuler
2183cf9bb0 perf: 修复微信钉钉测试两次报错 2021-07-22 14:06:49 +08:00
ibuler
dfd710daf7 perf: 优化打开task页面 2021-07-22 14:02:26 +08:00
ibuler
e38817b24a fix(settings): 修复配置消息订阅时,用户为当前组织的问题 2021-07-21 19:11:51 +08:00
fit2bot
34b18d5220 云管中心同步任务支持设置同步IP网段和协议组 (#925)
* 云管中心同步任务支持设置同步IP网段和协议组

* fix: 修改重复定义的getMethod()和getUrl()等方法

* fix: 修改重复定义的getMethod()和getUrl()等方法2

Co-authored-by: Bai <bugatti_it@163.com>
2021-07-21 19:06:14 +08:00
ibuler
d07b311b76 perf: 去掉首页无意义的标签显示 2021-07-21 10:24:21 +08:00
ibuler
0e67594ba9 perf: 新页面创建资产,创建成功重定向到详情页 2021-07-20 19:33:38 +08:00
wojiushixiaobai
5f3b307785 perf: 优化构建速度 2021-07-20 19:31:18 +08:00
ibuler
96ee47b4ac perf: 优化工单详情 2021-07-20 19:30:34 +08:00
ibuler
574ec53d23 fix(assets): 修复创建网关没有密码的问题 2021-07-20 19:29:52 +08:00
Bai
1720c183ad feat: 资产详情页面添加关联系统用户列表 2021-07-20 17:01:13 +08:00
Jiangjie.Bai
9bbd2ac293 Merge pull request #916 from jumpserver/dev
v2.12.0 rc5
2021-07-15 19:19:49 +08:00
Bai
fc7bb98fbb fix: 修改翻译 2021-07-15 19:18:18 +08:00
Jiangjie.Bai
32d37a4860 Merge pull request #914 from jumpserver/dev
v2.12.0 rc5
2021-07-15 18:24:21 +08:00
xinwen
699b12178a fix: 组织管理页面 管理用户改成特权用户 2021-07-15 18:15:01 +08:00
Jiangjie.Bai
fb87ebd0e7 Merge pull request #912 from jumpserver/dev
v2.12.0 rc5
2021-07-15 17:05:11 +08:00
fit2bot
b8ba53cf5e perf: 添加 系统用户 到 账号 (#911)
Co-authored-by: ibuler <ibuler@qq.com>
2021-07-15 10:30:22 +08:00
Jiangjie.Bai
c3f27dd0a8 Merge pull request #910 from jumpserver/dev
v2.12.0 rc4
2021-07-14 19:03:39 +08:00
Bai
c94544df39 fix: 隐藏ROOT组织资产列表导入按钮 2021-07-14 18:06:43 +08:00
ibuler
35c7b81359 perf: 优化clone 2021-07-14 16:51:09 +08:00
ibuler
e7ae4bfbd3 perf: 优化特权账号创建,都特殊的api 2021-07-14 16:31:59 +08:00
Bai
d1c3c63efc fix: 修改收集用户任务详情也文案 2021-07-14 15:39:59 +08:00
Bai
bfc5ae9bf7 fix: 修改普通用户/特权用户文案 2021-07-14 12:25:33 +08:00
Jiangjie.Bai
fa0c4d66bb Merge pull request #904 from jumpserver/dev
v2.12.0 rc3
2021-07-13 20:47:54 +08:00
ibuler
31d011b1fc perf: 修复账号导出了所有的bug 2021-07-13 19:12:48 +08:00
老广
f4cd0efd29 Merge pull request #901 from jumpserver/dev
v2.12.0 rc3
2021-07-13 19:10:00 +08:00
Bai
f946cf5d11 fix: 完美修复录像存储无法clone的问题 2021-07-13 18:51:07 +08:00
ibuler
8d42071d4a perf: root组织,去掉导入 2021-07-13 18:38:54 +08:00
Bai
b374979b32 fix: 修复网关更新密码代填提交问题 2021-07-13 18:25:02 +08:00
ibuler
430f2e4856 perf: 普通系统用户添加批量处理 2021-07-13 18:24:41 +08:00
ibuler
59a8971af0 perf: 修复消息订阅,取消按钮无法使用的问题 2021-07-13 18:07:10 +08:00
fit2bot
4f70f3bea7 pref: 添加指纹 (#896)
* fix(perms): 修复无法clone的问题

* pref: 添加指纹

Co-authored-by: ibuler <ibuler@qq.com>
2021-07-13 17:26:18 +08:00
ibuler
30ffa13b01 fix(perms): 修复无法clone的问题 2021-07-13 17:22:52 +08:00
fit2bot
4fb8e4073c fix: 修复审计员首页用户数空白的问题 (#894)
Co-authored-by: ibuler <ibuler@qq.com>
2021-07-13 16:24:58 +08:00
Bai
fef38ea007 fix: 修复审计员可以点击dashboard页面的资产/用户总数的问题 2021-07-13 13:44:30 +08:00
ibuler
41c5ac5f12 perf: 优化左侧组织列表,有xpack时直接显示
perf: 优化显示组织
2021-07-13 13:09:03 +08:00
ibuler
4056c601b1 perf: admin sftp root help text 2021-07-13 13:06:03 +08:00
ibuler
34a72467d2 fix: 修复导入时url中有参数导致的拼接错误
perf: 优化一下获取url
2021-07-13 10:56:58 +08:00
fit2bot
e4657f3bf6 fix: 修复系统用户详情中测试连接性 (#889)
Co-authored-by: ibuler <ibuler@qq.com>
2021-07-12 20:32:37 +08:00
Jiangjie.Bai
c6cde723ec Merge pull request #888 from jumpserver/dev
v2.12.0 rc2
2021-07-12 18:29:50 +08:00
ibuler
8b31666f67 fix: 修复管理用户克隆 2021-07-12 18:22:18 +08:00
Bai
0a01ad3552 fix: 修复资产可连接性显示图标 2021-07-12 18:21:47 +08:00
ibuler
8d783dd650 fix: 修复动态系统用户无法提交
fix: 修复动态系统用户
2021-07-12 12:59:12 +08:00
ibuler
425e47d613 perf: 修复资产详情中账号列表 2021-07-08 18:55:43 +08:00
Jiangjie.Bai
2bf83e241e Merge pull request #880 from jumpserver/dev
v2.12 rc1
2021-07-08 15:24:38 +08:00
fit2bot
ec3bdb0eac perf: 显示账号 (#881)
Co-authored-by: ibuler <ibuler@qq.com>
2021-07-08 15:18:36 +08:00
健健
49ee321c83 for k8s system user, hide () (#879)
* chore: 更新Submodule指向

* fix: 修复Build失败的问题

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

* for k8s system user, not show ()

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>
2021-07-08 14:32:58 +08:00
fit2bot
44f93f5aea perf: 合并系统用户和管理用户 (#878)
* perf: 暂存优化 vault table

* perf: 修改导入出

* refactor: 整合系统用户和管理用户

* perf: 暂存一下

* perf: 暂存

* xxx

* ..

* perf: ...

* .

* xxx

* perf: 优化翻译

* perf: 优化提示

* ...

* 完成list

* xx

* perf: 修改更新

* perf: 完成更新和查看

* ...

* stash it

* perf: 完成下载导入的 mfa

* xxx

* perf: 基本完成

* 修改一点

* perf: 优化资产列表

Co-authored-by: ibuler <ibuler@qq.com>
2021-07-08 14:28:23 +08:00
ibuler
baf6ba7496 fix(settings): 修复系统设置中邮件没有更改就无法更新的问题 2021-06-28 19:03:34 +08:00
Bai
fa9b4abba1 feat: 支持设置默认存储(2) 2021-06-28 10:33:20 +08:00
Bai
35e0d57a05 feat: 支持设置默认存储 2021-06-28 10:33:20 +08:00
jiangweidong
90eebea2e6 fix: 修改华为及青云私有云变量名称 2021-06-21 15:56:41 +08:00
jiangweidong
dab32e33ad feat: 华为私有云、青云私有云云管同步 2021-06-21 15:56:41 +08:00
ibuler
bf877f415a fix: 修复会话详情中播放bug 2021-06-18 18:08:13 +08:00
ibuler
1ffb1d9cab fix(infra): 修复基础平台等可以删除更新问题 2021-06-18 11:05:47 +08:00
ibuler
4d4dc35735 perf: 优化 table 去掉clone
perf: 去掉

perf: xxx
2021-06-18 10:26:59 +08:00
322 changed files with 13318 additions and 5389 deletions

View File

@@ -1,3 +1,4 @@
lina
dist
node_modules
.git

View File

@@ -1,8 +1,6 @@
# 全局环境变量 请勿随意改动
ENV = 'development'
# base api
VUE_APP_BASE_API = ''
VUE_APP_PUBLIC_PATH = '/ui/'
@@ -22,5 +20,6 @@ VUE_APP_LOGOUT_PATH = '/core/auth/logout/'
# Dev server for core proxy
VUE_APP_CORE_HOST = 'http://localhost:8080'
VUE_APP_CORE_HOST = 'http://localhost:8080/'
VUE_APP_CORE_WS = 'http://localhost:8080/'
VUE_APP_ENV = 'development'

1
.gitignore vendored
View File

@@ -15,3 +15,4 @@ tests/**/coverage/
*.ntvs*
*.njsproj
*.sln
.env.development

View File

@@ -12,8 +12,8 @@ RUN npm config set sass_binary_site=${SASS_BINARY_SITE}
RUN npm config set registry ${NPM_REGISTRY}
RUN yarn config set registry ${NPM_REGISTRY}
COPY package.json yarn.lock /data/
COPY utils /data/utils/
RUN ls && cd utils && bash -xieu build.sh dep
RUN yarn install
RUN npm rebuild node-sass
ADD . /data
RUN cd utils && bash -xieu build.sh build

837
LICENSE
View File

@@ -1,281 +1,622 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Version 3, 29 June 2007
Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
TERMS AND CONDITIONS
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
0. Definitions.
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
"This License" refers to version 3 of the GNU General Public License.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
A "covered work" means either the unmodified Program or a work based
on the Program.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
1. Source Code.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
The Corresponding Source for a work in source code form is that
same work.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
2. Basic Permissions.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
13. Use with the GNU Affero General Public License.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
14. Revised Versions of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
NO WARRANTY
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
15. Disclaimer of Warranty.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
@@ -287,15 +628,15 @@ free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
{description}
Copyright (C) {year} {fullname}
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
@@ -303,37 +644,31 @@ the "copyright" line and a pointer to where the full notice is found.
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
{signature of Ty Coon}, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

View File

@@ -50,7 +50,7 @@ Copyright (c) 2014-2021 飞致云 FIT2CLOUD, All rights reserved.
Licensed under The GNU General Public License version 2 (GPLv2) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
https://www.gnu.org/licenses/gpl-2.0.html
https://github.com/jumpserver/lina/blob/dev/LICENSE
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

BIN
dump.rdb

Binary file not shown.

View File

@@ -24,6 +24,7 @@
"@ztree/ztree_v3": "3.5.44",
"axios": "0.21.1",
"axios-retry": "^3.1.9",
"cron-parser": "^4.0.0",
"deepmerge": "^4.2.2",
"echarts": "^4.7.0",
"element-ui": "2.13.2",
@@ -57,6 +58,7 @@
"vue-cookie": "^1.1.4",
"vue-echarts": "^5.0.0-beta.0",
"vue-i18n": "^8.15.5",
"vue-json-editor": "^1.4.3",
"vue-moment": "^4.1.0",
"vue-password-strength-meter": "^1.7.2",
"vue-router": "3.0.6",

View File

@@ -9,6 +9,23 @@
<meta http-equiv="Cache" content="no-cache">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title><%= webpackConfig.name %></title>
<style>
::-webkit-scrollbar {
width:14px;
}
::-webkit-scrollbar-track {
border-radius:10px;
}
::-webkit-scrollbar-thumb {
border-radius: 8px;
box-shadow: 8px 10px 20px #C6C6C6 inset;
border: 3px solid rgba(0, 0, 0, 0);
}
::-webkit-scrollbar-thumb:hover {
box-shadow: 8px 10px 20px #878787 inset;
}
</style>
</head>
<body>
<noscript>

View File

@@ -57,6 +57,26 @@ export function TestReplayStorage(id) {
})
}
function SetToDefaultStorage(url) {
return request({
url: url,
method: 'patch',
data: { 'is_default': true }
})
}
export function SetToDefaultCommandStorage(id) {
return SetToDefaultStorage(
`/api/v1/terminal/command-storages/${id}/`,
)
}
export function SetToDefaultReplayStorage(id) {
return SetToDefaultStorage(
`/api/v1/terminal/replay-storages/${id}/`,
)
}
export function getReplayStorage(id) {
return request({
url: `/api/v1/terminal/replay-storages/${id}/`,

View File

@@ -51,7 +51,7 @@ export function refreshLdapUserCache() {
})
}
export function StartLdapUserCache() {
export function startLdapUserCache() {
return request({
disableFlashErrorMsg: true,
url: '/api/v1/settings/ldap/users/?cache_police=1',

View File

@@ -0,0 +1,85 @@
<template>
<div>
<MFAVerifyDialog
@MFAVerifyDone="getAuthInfo"
@MFAVerifyCancel="exit"
/>
<Dialog
:title="dialogTitle"
:show-confirm="false"
:show-cancel="false"
:destroy-on-close="true"
:width="'50'"
:visible.sync="showAuthInfo"
v-bind="$attrs"
v-on="$listeners"
>
<div>
<el-form label-position="right" label-width="80px" :model="authInfo">
<el-form-item :label="this.$t('assets.Hostname')">
<el-input v-model="account.hostname" readonly />
</el-form-item>
<el-form-item :label="this.$t('assets.Username')">
<el-input v-model="account['username']" readonly />
</el-form-item>
<el-form-item :label="this.$t('assets.Password')">
<el-input v-model="authInfo.password" type="password" show-password />
</el-form-item>
<el-form-item :label="this.$t('assets.SSHKey')">
<el-input v-model="authInfo['private_key']" class="item-textarea" type="textarea" show-password />
</el-form-item>
</el-form>
</div>
</Dialog>
</div>
</template>
<script>
import Dialog from '@/components/Dialog'
import MFAVerifyDialog from '@/components/MFAVerifyDialog'
export default {
name: 'ShowSecretInfo',
components: {
Dialog,
MFAVerifyDialog
},
props: {
account: {
type: Object,
default: () => ({})
},
visible: {
type: Boolean,
default: false
}
},
data() {
return {
dialogTitle: this.$t('common.ViewSecret'),
authInfo: {},
showAuthInfo: false
}
},
mounted() {
this.getAuthInfo()
},
methods: {
getAuthInfo() {
const url = `/api/v1/assets/account-secrets/${this.account.id}/`
this.$axios.get(url, { disableFlashErrorMsg: true }).then(resp => {
this.authInfo = resp
this.showAuthInfo = true
})
},
exit() {
this.$emit('update:visible', false)
}
}
}
</script>
<style scoped>
.item-textarea >>> .el-textarea__inner {
height: 110px;
}
</style>

View File

@@ -0,0 +1,92 @@
<template>
<Dialog
width="50"
:title="this.$t('assets.UpdateAssetUserToken')"
:destroy-on-close="true"
v-bind="$attrs"
@confirm="handleConfirm()"
@cancel="handleCancel()"
v-on="$listeners"
>
<el-form label-position="right" label-width="80px">
<el-form-item :label="this.$t('assets.Hostname')">
<el-input v-model="account.hostname" readonly />
</el-form-item>
<el-form-item :label="this.$t('assets.Username')">
<el-input v-model="account['username']" readonly />
</el-form-item>
<el-form-item :label="this.$t('assets.Password')">
<el-input v-model="authInfo.password" type="password" />
</el-form-item>
<el-form-item :label="this.$t('assets.SSHKey')">
<input type="file" @change="onPrivateKeyLoaded">
</el-form-item>
</el-form>
</Dialog>
</template>
<script>
import Dialog from '@/components/Dialog'
export default {
name: 'UpdateSecretInfo',
components: {
Dialog
},
props: {
account: {
type: Object,
default: () => ({})
}
},
data() {
return {
authInfo: {
password: '',
private_key: ''
}
}
},
methods: {
handleConfirm() {
const data = {}
if (this.authInfo.password !== '') {
data.password = this.authInfo.password
}
if (this.authInfo.private_key !== '') {
data.private_key = this.authInfo.private_key
}
this.$axios.patch(
`/api/v1/assets/accounts/${this.account.id}/`,
data
).then(res => {
this.authInfo = { password: '', private_key: '' }
this.$message.success(this.$tc('common.updateSuccessMsg'))
this.$emit('updateAuthDone', res)
this.$emit('update:visible', false)
}).catch(err => {
const errMsg = Object.values(err.response.data).join(', ')
this.$message.error(this.$tc('common.updateErrorMsg') + ' ' + errMsg)
this.$emit('update:visible', true)
})
},
handleCancel() {
this.$emit('update:visible', false)
},
onPrivateKeyLoaded(e) {
const vm = this
// TODO 校验文件类型
const reader = new FileReader()
reader.onload = function() {
vm.authInfo.private_key = this.result
}
reader.readAsText(
e.target.files[0]
)
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,36 @@
import { toSafeLocalDateStr } from '@/utils/common'
import ChoicesFormatter from '@/components/TableFormatters/ChoicesFormatter'
import i18n from '@/i18n/i18n'
export const connectivityMeta = {
label: i18n.t('assets.Reachable'),
formatter: ChoicesFormatter,
formatterArgs: {
iconChoices: {
ok: 'fa-check',
failed: 'fa-times',
unknown: 'fa-circle-o'
},
classChoices: {
ok: 'text-primary',
failed: 'text-danger',
unknown: 'text-warning'
},
hasTips: true,
getTips: ({ row, cellValue }) => {
const mapper = {
'ok': i18n.t('assets.Reachable'),
'failed': i18n.t('assets.Unreachable'),
'unknown': i18n.t('assets.Unknown')
}
let tips = mapper[cellValue]
if (row['date_verified']) {
const datetime = toSafeLocalDateStr(row['date_verified'])
tips += '<br> ' + datetime
}
return tips
}
},
width: '90px',
align: 'center'
}

View File

@@ -0,0 +1,184 @@
<template>
<div>
<ListTable ref="ListTable" :table-config="tableConfig" :header-actions="headerActions" />
<ShowSecretInfo v-if="showViewSecretDialog" :visible.sync="showViewSecretDialog" :account="account" />
<UpdateSecretInfo :visible.sync="showUpdateSecretDialog" :account="account" @updateAuthDone="onUpdateAuthDone" />
</div>
</template>
<script>
import ListTable from '@/components/ListTable/index'
import { ActionsFormatter, DetailFormatter, DisplayFormatter } from '@/components/TableFormatters'
import ShowSecretInfo from './ShowSecretInfo'
import UpdateSecretInfo from './UpdateSecretInfo'
import { connectivityMeta } from './const'
import { openTaskPage } from '@/utils/jms'
export default {
name: 'AccountListTable',
components: {
ListTable,
UpdateSecretInfo,
ShowSecretInfo
},
props: {
url: {
type: String,
required: true
},
exportUrl: {
type: String,
default() {
return this.url.replace('/assets/accounts/', '/assets/account-secrets/')
}
},
hasLeftActions: {
type: Boolean,
default: false
},
otherActions: {
type: Array,
default: null
},
hasClone: {
type: Boolean,
default: false
}
},
data() {
return {
showViewSecretDialog: false,
showUpdateSecretDialog: false,
account: {},
tableConfig: {
url: this.url,
columns: [
'hostname', 'ip', 'username', 'version', 'connectivity',
'systemuser', 'date_created', 'date_updated', 'actions'
],
columnsShow: {
min: ['username', 'actions'],
default: ['hostname', 'ip', 'username', 'version', 'actions']
},
columnsMeta: {
hostname: {
prop: 'hostname',
label: this.$t('assets.Hostname'),
showOverflowTooltip: true,
formatter: DetailFormatter,
formatterArgs: {
getRoute({ row }) {
return {
name: 'AssetDetail',
params: { id: row.asset }
}
}
}
},
ip: {
width: '120px'
},
username: {
showOverflowTooltip: true
},
systemuser: {
formatter: DisplayFormatter
},
version: {
width: '70px'
},
connectivity: connectivityMeta,
actions: {
formatter: ActionsFormatter,
formatterArgs: {
hasUpdate: false, // can set function(row, value)
hasDelete: false, // can set function(row, value)
hasClone: this.hasClone,
moreActionsTitle: this.$t('common.More'),
extraActions: [
{
name: 'View',
title: this.$t('common.View'),
type: 'primary',
callback: function({ row }) {
this.account = row
this.showViewSecretDialog = true
}.bind(this)
},
{
name: 'Delete',
title: this.$t('common.Delete'),
type: 'primary',
callback: ({ row }) => {
this.$axios.delete(`/api/v1/assets/accounts/${row.id}/`).then(() => {
this.$message.success(this.$tc('common.deleteSuccessMsg'))
this.$refs.ListTable.reloadTable()
})
}
},
{
name: 'Test',
title: this.$t('common.Test'),
callback: ({ row }) => {
this.$axios.post(
`/api/v1/assets/accounts/${row.id}/verify/`,
{ action: 'test' }
).then(res => {
openTaskPage(res['task'])
})
}
},
{
name: 'Update',
title: this.$t('common.Update'),
can: !this.$store.getters.currentOrgIsRoot,
callback: function({ row }) {
this.account = row
this.showUpdateSecretDialog = true
}.bind(this)
}
]
}
}
}
},
headerActions: {
hasLeftActions: this.hasLeftActions,
hasMoreActions: false,
hasImport: false,
hasExport: true,
exportOptions: {
url: this.exportUrl,
mfaVerifyRequired: true
},
searchConfig: {
exclude: ['systemuser', 'asset']
},
hasSearch: true
}
}
},
watch: {
url(iNew) {
this.$set(this.tableConfig, 'url', iNew)
}
},
mounted() {
if (this.otherActions) {
const actionColumn = this.tableConfig.columns[this.tableConfig.columns.length - 1]
for (const item of this.otherActions) {
actionColumn.formatterArgs.extraActions.push(item)
}
}
},
methods: {
onUpdateAuthDone(account) {
Object.assign(this.account, account)
}
}
}
</script>
<style lang='less' scoped>
</style>

View File

@@ -0,0 +1,70 @@
<template>
<el-alert
v-if="enabled && !isViewed()"
class="announcement"
type="success"
:center="false"
:title="title"
@close="onClose"
>
<span class="announcement-main"> {{ announcement.content }}</span>
<span v-if="announcement.link">
<el-link :href="announcement.link" target="_blank" class="link-more">
{{ $t('common.ViewMore') }}
</el-link>
<i class="fa fa-share-square-o" />
</span>
</el-alert>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'Announcement',
data() {
return {
viewedKey: 'AnnouncementViewed'
}
},
computed: {
...mapGetters([
'publicSettings'
]),
announcement() {
const ann = this.publicSettings.ANNOUNCEMENT
return { id: ann['ID'], subject: ann['SUBJECT'], content: ann['CONTENT'], link: ann['LINK'] }
},
enabled() {
return this.publicSettings.ANNOUNCEMENT_ENABLED && (this.announcement.content || this.announcement.subject)
},
title() {
return this.$t('common.Announcement') + ': ' + this.announcement.subject
}
},
methods: {
onClose() {
localStorage.setItem(this.viewedKey, this.announcement.id)
},
isViewed() {
const viewedId = localStorage.getItem(this.viewedKey)
return viewedId === this.announcement.id
}
}
}
</script>
<style scoped>
.announcement >>> .el-alert__content {
width: 100%;
}
.announcement-main {
word-wrap:break-word;
}
.link-more {
font-size: 10px;
margin-left: 10px;
border-bottom: solid 1px;
}
</style>

View File

@@ -0,0 +1,80 @@
<template>
<div>
<MFAVerifyDialog
@MFAVerifyDone="getAuthInfo"
@MFAVerifyCancel="exit"
/>
<Dialog
:title="dialogTitle"
:show-confirm="false"
:show-cancel="false"
:destroy-on-close="true"
:width="'50'"
:visible.sync="showAuthInfo"
v-bind="$attrs"
v-on="$listeners"
>
<div>
<el-form label-position="right" label-width="80px" :model="authInfo">
<el-form-item :label="this.$t('applications.appName')">
<el-input v-model="account['app_display']" readonly />
</el-form-item>
<el-form-item :label="this.$t('assets.Username')">
<el-input v-model="account['username']" readonly />
</el-form-item>
<el-form-item :label="this.$t('assets.Password')">
<el-input v-model="authInfo.password" type="password" show-password />
</el-form-item>
</el-form>
</div>
</Dialog>
</div>
</template>
<script>
import Dialog from '@/components/Dialog'
import MFAVerifyDialog from '@/components/MFAVerifyDialog'
export default {
name: 'ShowSecretInfo',
components: {
Dialog,
MFAVerifyDialog
},
props: {
account: {
type: Object,
default: () => ({})
},
visible: {
type: Boolean,
default: false
}
},
data() {
return {
dialogTitle: this.$t('common.ViewSecret'),
authInfo: {},
showAuthInfo: false
}
},
mounted() {
this.getAuthInfo()
},
methods: {
getAuthInfo() {
const url = `/api/v1/applications/account-secrets/${this.account.id}/`
this.$axios.get(url, { disableFlashErrorMsg: true }).then(resp => {
this.authInfo = resp
this.showAuthInfo = true
})
},
exit() {
this.$emit('update:visible', false)
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,31 @@
import { ChoicesFormatter } from '@/components/TableFormatters'
import { toSafeLocalDateStr } from '@/utils/common'
import i18n from '@/i18n/i18n'
export const connectivityMeta = {
label: i18n.t('assets.Reachable'),
formatter: ChoicesFormatter,
formatterArgs: {
iconChoices: {
ok: 'fa-check text-primary',
failed: 'fa-times text-danger',
unknown: 'fa-circle text-warning'
},
hasTips: true,
getTips: ({ row, cellValue }) => {
const mapper = {
'ok': i18n.t('assets.Reachable'),
'failed': i18n.t('assets.Unreachable'),
'unknown': i18n.t('assets.Unknown')
}
let tips = mapper[cellValue]
if (row['date_verified']) {
const datetime = toSafeLocalDateStr(row['date_verified'])
tips += '<br> ' + datetime
}
return tips
}
},
width: '90px',
align: 'center'
}

View File

@@ -0,0 +1,167 @@
<template>
<div>
<ListTable ref="ListTable" :table-config="tableConfig" :header-actions="headerActions" />
<ShowSecretInfo v-if="showViewSecretDialog" :visible.sync="showViewSecretDialog" :account="account" />
</div>
</template>
<script>
import ListTable from '@/components/ListTable/index'
import { ActionsFormatter, DetailFormatter } from '@/components/TableFormatters'
import ShowSecretInfo from './ShowSecretInfo'
export default {
name: 'Detail',
components: {
ListTable,
ShowSecretInfo
},
props: {
url: {
type: String,
required: true
},
exportUrl: {
type: String,
default() {
return this.url.replace('/applications/accounts/', '/applications/account-secrets/')
}
},
hasLeftActions: {
type: Boolean,
default: false
},
otherActions: {
type: Array,
default: null
},
hasClone: {
type: Boolean,
default: false
}
},
data() {
return {
showViewSecretDialog: false,
showUpdateSecretDialog: false,
account: {},
tableConfig: {
url: this.url,
columns: [
'app_display', 'username', 'category_display',
'type_display', 'systemuser', 'actions'
],
columnsMeta: {
app_display: {
showOverflowTooltip: true,
formatter: DetailFormatter,
formatterArgs: {
getRoute({ row }) {
switch (row['category']) {
case 'remote_app':
return {
name: 'RemoteAppDetail',
params: { id: row.app }
}
case 'db':
return {
name: 'DatabaseAppDetail',
params: { id: row.app }
}
default:
return {
name: 'KubernetesAppDetail',
params: { id: row.app }
}
}
}
}
},
username: {
showOverflowTooltip: true
},
systemuser: {
showOverflowTooltip: true,
formatter: DetailFormatter,
formatterArgs: {
getTitle({ row }) {
return row.systemuser_display
},
getRoute({ row }) {
return {
name: 'SystemUserDetail',
params: { id: row.systemuser }
}
}
}
},
actions: {
formatter: ActionsFormatter,
formatterArgs: {
hasUpdate: false, // can set function(row, value)
hasDelete: false, // can set function(row, value)
hasClone: this.hasClone,
moreActionsTitle: this.$t('common.More'),
extraActions: [
{
name: 'View',
title: this.$t('common.View'),
type: 'primary',
callback: function({ row }) {
this.account = row
this.showViewSecretDialog = true
}.bind(this)
},
{
name: 'Update',
title: this.$t('common.Update'),
can: !this.$store.getters.currentOrgIsRoot,
callback: function({ row }) {
this.$message.success(this.$tc('applications.updateAccountMsg'))
}.bind(this)
}
]
}
}
}
},
headerActions: {
hasLeftActions: this.hasLeftActions,
hasMoreActions: false,
hasImport: false,
hasExport: true,
exportOptions: {
url: this.exportUrl,
mfaVerifyRequired: true
},
searchConfig: {
exclude: ['systemuser', 'asset']
},
hasSearch: true
}
}
},
watch: {
url(iNew) {
this.$set(this.tableConfig, 'url', iNew)
}
},
mounted() {
if (this.otherActions) {
const actionColumn = this.tableConfig.columns[this.tableConfig.columns.length - 1]
for (const item of this.otherActions) {
actionColumn.formatterArgs.extraActions.push(item)
}
}
},
methods: {
onUpdateAuthDone(account) {
Object.assign(this.account, account)
}
}
}
</script>
<style lang='less' scoped>
</style>

View File

@@ -1,6 +1,13 @@
<template>
<div class="asset-select-dialog">
<Select2 ref="select2" v-bind="select2Config" @input="onInputChange" @focus.stop="handleFocus" v-on="$listeners" />
<Select2
ref="select2"
v-model="select2Config.value"
v-bind="select2Config"
@input="onInputChange"
@focus.stop="handleFocus"
v-on="$listeners"
/>
<Dialog
v-if="dialogVisible"
:title="this.$t('assets.Assets')"
@@ -76,7 +83,7 @@ export default {
select2Config: select2Config,
dialogSelect2Config: select2Config,
tableConfig: {
url: '/api/v1/assets/assets/',
url: '/api/v1/assets/assets/?fields_size=mini',
hasTree: true,
canSelect: this.canSelect,
columns: [
@@ -94,6 +101,18 @@ export default {
prop: 'ip',
label: this.$t('assets.ipDomain'),
sortable: 'custom'
},
{
prop: 'platform',
label: this.$t('assets.Platform'),
sortable: true
},
{
prop: 'protocols',
formatter: function(row) {
return <span> {row.protocols.toString()} </span>
},
label: this.$t('assets.Protocols')
}
],
listeners: {

View File

@@ -1,529 +0,0 @@
<template>
<div>
<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="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 :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>
</template>
<script>
import { mapGetters } from 'vuex'
import ListTable from '@/components/ListTable/index'
import Dialog from '@/components/Dialog'
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',
components: {
ListTable,
Dialog
},
props: {
url: {
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
},
otherActions: {
type: Array,
default: null
},
handleExport: {
type: Function,
default: null
},
handleImport: {
type: Function,
default: null
},
hasImport: {
type: Boolean,
default: true
},
hasExport: {
type: Boolean,
default: true
},
hasClone: {
type: Boolean,
default: true
},
tableConfig: {
type: Object,
default: () => ({})
}
},
data() {
return {
MFAConfirmed: false,
MFAInput: '',
MFAInfo: {
asset: '',
username: '',
hostname: '',
password: ''
},
showDialog: false,
showMFADialog: false,
dialogInfo: {
asset: '',
username: '',
hostname: '',
password: '',
private_key: ''
},
selectedRows: '',
dialogStatus: '',
showExportDialog: false,
exportOption: 'all',
exportTypeOption: 'csv',
defaultTableConfig: {
url: this.url,
columns: ['hostname', 'ip', 'username', 'version', 'date_created', 'actions'],
columnsMeta: {
'hostname': {
label: this.$t('assets.Hostname'),
showOverflowTooltip: true
},
'ip': {
label: this.$t('assets.ip'),
width: '120px'
},
'username': {
label: this.$t('assets.Username'),
showOverflowTooltip: true
},
'version': {
label: this.$t('assets.Version'),
width: '70px'
},
'date_created': {
label: this.$t('assets.date_joined'),
formatter: DateFormatter
},
'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: this.hasClone,
moreActionsTitle: this.$t('common.More'),
extraActions: [
{
name: 'View',
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
this.MFAConfirmed = true
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
})
} else {
this.showMFADialog = true
}
}.bind(this)
},
{
name: 'Delete',
title: this.$t('common.Delete'),
type: 'primary',
callback: (val) => {
this.$axios.delete(`/api/v1/assets/asset-users/${val.row.id}/`).then(() => {
this.$message.success(this.$t('common.deleteSuccessMsg'))
this.$refs.ListTable.reloadTable()
})
}
},
{
name: 'Test',
title: this.$t('common.Test'),
callback: (val) => {
this.$axios.post(
`/api/v1/assets/asset-users/tasks/?id=${val.row.id}`,
{ action: 'test' }
).then(res => {
window.open(`/#/ops/celery/task/${res.task}/log/`, '', 'width=900,height=600')
})
}
},
{
name: 'Update',
title: this.$t('common.Update'),
can: !this.$store.getters.currentOrgIsRoot,
callback: function(val) {
this.showDialog = true
this.dialogInfo.asset = val.row.asset
this.dialogInfo.hostname = val.row.hostname
this.dialogInfo.username = val.row.username
}.bind(this)
}
]
}
}
},
extraQuery: this.extraQuery || { latest: 1 }
},
headerActions: {
hasLeftActions: this.hasLeftActions,
hasMoreActions: false,
hasImport: this.hasImport,
hasExport: this.hasExport,
hasSearch: true,
searchConfig: {
exclude: this.searchExclude,
options: [
{
label: this.$t('assets.OnlyLatestVersion'),
value: 'latest',
children: [
{
label: this.$t('common.Yes'),
value: 1
},
{
label: this.$t('common.No'),
value: 0
}
]
}
]
}
}
}
},
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)
},
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.iTableConfig, 'url', iNew)
}
},
mounted() {
if (this.otherActions) {
const actionColumn = this.iTableConfig.columns[this.iTableConfig.columns.length - 1]
for (const item of this.otherActions) {
actionColumn.formatterArgs.extraActions.push(item)
}
}
},
created() {
this.headerActions.handleExport = this.handleExport || this.defaultHandleExport
if (this.handleImport) {
this.headerActions.handleImport = this.handleImport
}
},
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/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
})
}
}
)
},
handleMFAConfirm() {
this.MFAInfo = {
asset: '',
username: '',
hostname: '',
password: ''
}
this.MFAInput = ''
this.showMFADialog = false
this.MFAConfirmed = false
},
handleCancel() {
this.dialogInfo = {
asset: '',
username: '',
hostname: '',
password: '',
private_key: ''
}
this.showDialog = false
this.$refs.ListTable.reloadTable()
},
Onchange(e) {
const vm = this
// TODO 校验文件类型
const reader = new FileReader()
reader.onload = function() {
vm.dialogInfo.private_key = this.result
}
reader.readAsText(
e.target.files[0]
)
},
handleConfirm() {
const data = {
asset: this.dialogInfo.asset,
username: this.dialogInfo.username
}
if (this.dialogInfo.password !== '') {
data.password = this.dialogInfo.password
}
if (this.dialogInfo.private_key !== '') {
data.private_key = this.dialogInfo.private_key
}
this.$axios.post(
`/api/v1/assets/asset-users/`,
data
).then(res => {
this.$message.success(this.$t('common.updateSuccessMsg'))
}).catch(err => {
this.$message.error(this.$t('common.updateErrorMsg' + ' ' + err))
})
this.dialogInfo = {
asset: '',
username: '',
hostname: '',
password: '',
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='scss' scoped>
.export-item {
width: 100%;
display: block;
padding: 10px 20px;
}
.export-form >>> .el-form-item__label {
line-height: 2
}
</style>

View File

@@ -9,7 +9,7 @@
</template>
<script>
import { DataForm } from '@/components'
import DataForm from '@/components/DataForm'
export default {
name: 'NestedField',
@@ -35,7 +35,7 @@ export default {
kwargs: {
hasReset: false,
hasSaveContinue: false,
defaultButton: false
hasButtons: false
}
}
},

View File

@@ -1,6 +1,13 @@
<template>
<DataForm ref="dataForm" v-loading="loading" :fields="totalFields" :form="iForm" v-bind="$attrs" v-on="$listeners">
<FormGroupHeader v-for="(group, i) in groups" :slot="'id:'+group.name" :key="'group-'+group.name" :title="group.title" :line="i !== 0" />
<FormGroupHeader
v-for="(group, i) in groups"
:slot="'id:'+group.name"
:key="'group-'+group.name"
:group="group"
:index="i"
:line="i !== 0"
/>
</DataForm>
</template>
@@ -44,7 +51,8 @@ export default {
totalFields: [],
loading: true,
groups: [],
iForm: this.form
iForm: this.form,
errors: {}
}
},
mounted() {

View File

@@ -33,11 +33,11 @@ export class FormFieldGenerator {
break
case 'string':
type = 'input'
if (!fieldRemoteMeta.max_length) {
if (!fieldRemoteMeta['max_length']) {
field.el.type = 'textarea'
field.el.rows = 3
}
if (fieldRemoteMeta.write_only) {
if (fieldRemoteMeta['write_only']) {
field.el.type = 'password'
}
break
@@ -132,7 +132,8 @@ export class FormFieldGenerator {
this.groups.push({
id: groupTitle,
title: groupTitle,
name: fields[0]
name: fields[0],
fields: fields
})
return this.generateFields(fields, fieldsMeta, remoteFieldsMeta)
}
@@ -146,7 +147,9 @@ export class FormFieldGenerator {
field = this.generateField(field, fieldsMeta, remoteFieldsMeta)
fields.push(field)
} else if (field instanceof Object) {
this.errors[field.prop] = ''
if (this.errors) {
this.errors[field.prop] = ''
}
fields.push(field)
}
}

View File

@@ -1,6 +1,14 @@
<template>
<div>
<DataTable v-if="!loading" ref="dataTable" v-loading="loading" :config="iConfig" v-bind="$attrs" v-on="$listeners" @filter-change="filterChange" />
<DataTable
v-if="!loading"
ref="dataTable"
v-loading="loading"
:config="iConfig"
v-bind="$attrs"
v-on="$listeners"
@filter-change="filterChange"
/>
<ColumnSettingPopover
:current-columns="popoverColumns.currentCols"
:total-columns-list="popoverColumns.totalColumnsList"
@@ -13,7 +21,13 @@
<script type="text/jsx">
import DataTable from '../DataTable'
import { DateFormatter, DetailFormatter, DisplayFormatter, BooleanFormatter, ActionsFormatter } from '@/components/TableFormatters'
import {
DateFormatter,
DetailFormatter,
DisplayFormatter,
ActionsFormatter,
ChoicesFormatter
} from '@/components/TableFormatters'
import i18n from '@/i18n/i18n'
import ColumnSettingPopover from './components/ColumnSettingPopover'
import { newURL } from '@/utils/common'
@@ -103,7 +117,7 @@ export default {
break
case 'is_valid':
col.label = i18n.t('common.Validity')
col.formatter = BooleanFormatter
col.formatter = ChoicesFormatter
col.align = 'center'
col.width = '80px'
break
@@ -123,7 +137,7 @@ export default {
col.formatter = DisplayFormatter
break
case 'boolean':
col.formatter = BooleanFormatter
col.formatter = ChoicesFormatter
col.align = 'center'
col.width = '80px'
break
@@ -170,12 +184,12 @@ export default {
col.filters = column.choices.map(item => {
if (typeof (item.value) === 'boolean') {
if (item.value) {
return { text: item.display_name, value: 'True' }
return { text: item['display_name'], value: 'True' }
} else {
return { text: item.display_name, value: 'False' }
return { text: item['display_name'], value: 'False' }
}
}
return { text: item.display_name, value: item.value }
return { text: item['display_name'], value: item.value }
})
col.sortable = false
col['column-key'] = col.prop
@@ -223,7 +237,7 @@ export default {
defaultColumnsNames = totalColumnsNames.filter(n => defaultColumnsNames.indexOf(n) > -1)
// 最小列
const minColumnsNames = _.get(this.iConfig, 'columnsShow.min', ['action', 'id'])
const minColumnsNames = _.get(this.iConfig, 'columnsShow.min', ['actions', 'id'])
.filter(n => defaultColumnsNames.indexOf(n) > -1)
// 应该显示的列

View File

@@ -40,7 +40,7 @@ export default {
autoParam: ['id=key', 'name=n', 'level=lv'],
type: 'get',
headers: {
'X-JMS-ORG': JSON.parse(this.$cookie.get('jms_current_org')) ? JSON.parse(this.$cookie.get('jms_current_org')).id : ''
'X-JMS-ORG': this.$store.getters.currentOrg ? this.$store.getters.currentOrg.id : ''
}
},
callback: {
@@ -89,7 +89,7 @@ export default {
return
}
if (currentNode) {
currentNode.name = currentNode.meta.node.value
currentNode.name = currentNode.meta.data.value
}
this.zTree.editName(currentNode)
},
@@ -108,12 +108,12 @@ export default {
const query = Object.assign({}, this.$route.query)
if (treeNode.meta.type === 'node') {
this.currentNode = treeNode
this.currentNodeId = treeNode.meta.node.id
this.currentNodeId = treeNode.meta.data.id
query['node'] = this.currentNodeId
url = `${this.setting.url}${combinator}node_id=${treeNode.meta.node.id}&show_current_asset=${show_current_asset}`
url = `${this.setting.url}${combinator}node_id=${treeNode.meta.data.id}&show_current_asset=${show_current_asset}`
} else if (treeNode.meta.type === 'asset') {
query['asset'] = treeNode.meta.asset.id
url = `${this.setting.url}${combinator}asset_id=${treeNode.meta.asset.id}&show_current_asset=${show_current_asset}`
query['asset'] = treeNode.meta.data.id
url = `${this.setting.url}${combinator}asset_id=${treeNode.meta.data.id}&show_current_asset=${show_current_asset}`
}
this.$router.push({ query })
this.$emit('urlChange', url)
@@ -125,7 +125,7 @@ export default {
return
}
this.$axios.delete(
`${this.treeSetting.nodeUrl}${currentNode.meta.node.id}/`
`${this.treeSetting.nodeUrl}${currentNode.meta.data.id}/`
).then(() => {
this.$message.success(this.$t('common.deleteSuccessMsg'))
this.zTree.removeNode(currentNode)
@@ -143,7 +143,7 @@ export default {
url,
{ 'value': treeNode.name }
).then(res => {
let assetsAmount = treeNode.meta.node.assetsAmount
let assetsAmount = treeNode.meta.data.assetsAmount
if (!assetsAmount) {
assetsAmount = 0
}
@@ -208,9 +208,9 @@ export default {
onDrop: function(event, treeId, treeNodes, targetNode, moveType) {
const treeNodesIds = []
$.each(treeNodes, function(index, value) {
treeNodesIds.push(value.meta.node.id)
treeNodesIds.push(value.meta.data.id)
})
const theUrl = `${this.treeSetting.nodeUrl}${targetNode.meta.node.id}/children/add/`
const theUrl = `${this.treeSetting.nodeUrl}${targetNode.meta.data.id}/children/add/`
this.$axios.put(
theUrl, {
nodes: treeNodesIds
@@ -229,21 +229,21 @@ export default {
}
this.zTree.expandNode(parentNode, true, false, true, false)
// http://localhost/api/v1/assets/nodes/85aa4ee2-0bd9-41db-9079-aa3646448d0c/children/
const url = `${this.treeSetting.nodeUrl}${parentNode.meta.node.id}/children/`
const url = `${this.treeSetting.nodeUrl}${parentNode.meta.data.id}/children/`
this.$axios.post(url, {}).then(data => {
const newNode = {
id: data['key'],
name: data['value'],
pId: parentNode.id,
meta: {
'node': data
data: data
}
}
newNode.checked = this.zTree.getSelectedNodes()[0].checked
this.zTree.addNodes(parentNode, 0, newNode)
// vm.$refs.dataztree.refresh()
const node = this.zTree.getNodeByParam('id', newNode.id, parentNode)
this.currentNodeId = node.meta.node.id || newNode.id
this.currentNodeId = node.meta.data.id || newNode.id
this.zTree.editName(node)
this.$message.success(this.$t('common.createSuccessMsg'))
}).catch(error => {

View File

@@ -0,0 +1,452 @@
/* eslint-disable */
<template>
<div>
<el-tabs type="border-card">
<el-tab-pane v-if="shouldHide('min')" :label="this.$t('common.CronTab.min')">
<CrontabMin
ref="cronmin"
:check="checkNumber"
:cron="contabValueObj"
@update="updateContabValue"
/>
</el-tab-pane>
<el-tab-pane v-if="shouldHide('hour')" :label="this.$t('common.CronTab.hour')">
<CrontabHour
ref="cronhour"
:check="checkNumber"
:cron="contabValueObj"
@update="updateContabValue"
/>
</el-tab-pane>
<el-tab-pane v-if="shouldHide('day')" :label="this.$t('common.CronTab.day')">
<CrontabDay
ref="cronday"
:check="checkNumber"
:cron="contabValueObj"
@update="updateContabValue"
/>
</el-tab-pane>
<el-tab-pane v-if="shouldHide('month')" :label="this.$t('common.CronTab.month')">
<CrontabMonth
ref="cronmonth"
:check="checkNumber"
:cron="contabValueObj"
@update="updateContabValue"
/>
</el-tab-pane>
<el-tab-pane v-if="shouldHide('week')" :label="this.$t('common.CronTab.week')">
<CrontabWeek
ref="cronweek"
:check="checkNumber"
:cron="contabValueObj"
@update="updateContabValue"
/>
</el-tab-pane>
</el-tabs>
<div class="popup-main">
<div class="popup-result">
<p class="title">{{ this.$t('common.CronTab.timeExpression') }}</p>
<table>
<thead>
<th v-for="item of tabTitles" :key="item" width="40">{{ item }}</th>
</thead>
<tbody>
<td>
<el-input
v-model.trim="contabValueObj.min"
min="0"
max="5"
size="small"
onkeyup="value=value.replace(/[^\0-9\-\*\,]/g,'')"
/>
</td>
<td>
<el-input
v-model.trim="contabValueObj.hour"
size="small"
onkeyup="value=value.replace(/[^\0-9\-\*\,]/g,'')"
/>
</td>
<td>
<el-input
v-model.trim="contabValueObj.day"
size="small"
onkeyup="value=value.replace(/[^\0-9\\-\*\,]/g,'')"
/>
</td>
<td>
<el-input
v-model.trim="contabValueObj.month"
size="small"
onkeyup="value=value.replace(/[^\0-9\-\*\,]/g,'')"
/>
</td>
<td>
<el-input
v-model.trim="contabValueObj.week"
size="small"
onkeyup="value=value.replace(/[^\0-9\-\*\,]/g,'')"
/>
</td>
</tbody>
</table>
<div style="margin: 0 auto; text-align: center">
<div style="font-size: 13px;">{{ this.$t('common.CronTab.cronExpression') }}</div>
<div style="font-size: 13px;">{{ contabValueString }}</div>
</div>
</div>
<CrontabResult :ex="contabValueString" />
<div class="pop_btn">
<el-button
size="small"
@click="clearCron"
>
{{ this.$t('common.Reset') }}
</el-button>
<el-button
size="small"
type="primary"
@click="submitFill"
>
{{ this.$t('common.Confirm') }}
</el-button>
</div>
</div>
</div>
</template>
<script>
import CrontabMin from './components/Crontab-Min.vue'
import CrontabHour from './components/Crontab-Hour.vue'
import CrontabDay from './components/Crontab-Day.vue'
import CrontabMonth from './components/Crontab-Month.vue'
import CrontabWeek from './components/Crontab-Week.vue'
import CrontabResult from './components/Crontab-Result.vue'
export default {
name: 'Vcrontab',
components: {
CrontabMin,
CrontabHour,
CrontabDay,
CrontabMonth,
CrontabWeek,
CrontabResult
},
props: {
expression: {
type: String,
default() {
return ''
}
},
hideComponent: {
type: Array,
default() {
return []
}
}
},
data() {
return {
tabTitles: [this.$t('common.CronTab.min'), this.$t('common.CronTab.hour'), this.$t('common.CronTab.day'), this.$t('common.CronTab.month'), this.$t('common.CronTab.week')],
tabActive: 0,
myindex: 0,
contabValueObj: {
second: '0',
min: '0',
hour: '*',
day: '*',
month: '*',
week: '*'
// year: "",
},
newContabValueString: ''
}
},
computed: {
contabValueString: {
get() {
const obj = this.contabValueObj
const str =
obj.min +
' ' +
obj.hour +
' ' +
obj.day +
' ' +
obj.month +
' ' +
obj.week
return str
},
set() {
}
}
},
watch: {
expression: 'resolveExp',
hideComponent(value) {
// 隐藏部分组件
}
},
mounted: function() {
this.resolveExp()
},
methods: {
shouldHide(key) {
if (this.hideComponent && this.hideComponent.includes(key)) return false
return true
},
resolveExp() {
// 反解析 表达式
if (this.expression) {
const arr = this.expression.split(' ')
if (arr.length >= 5) {
// 5 位以上是合法表达式
const obj = {
min: arr[0],
hour: arr[1],
day: arr[2],
month: arr[3],
week: arr[4]
}
this.contabValueObj = {
...this.contabValueObj,
...obj
}
for (const i in obj) {
if (obj[i]) this.changeRadio(i, obj[i])
}
}
} else {
// 没有传入的表达式 则还原
this.clearCron()
}
},
// tab切换值
tabCheck(index) {
this.tabActive = index
},
// 由子组件触发,更改表达式组成的字段值
updateContabValue(name, value, from) {
this.contabValueObj[name] = value
if (from && from !== name) {
console.log(`来自组件 ${from} 改变了 ${name} ${value}`)
this.changeRadio(name, value)
}
},
// 赋值到组件
changeRadio(name, value) {
const arr = ['second', 'min', 'hour', 'month']
const refName = 'cron' + name
let insVlaue
if (!this.$refs[refName]) return
if (arr.includes(name)) {
if (value === '*') {
insVlaue = 1
} else if (value.indexOf('-') > -1) {
const indexArr = value.split('-')
isNaN(indexArr[0])
? (this.$refs[refName].cycle01 = 0)
: (this.$refs[refName].cycle01 = indexArr[0])
this.$refs[refName].cycle02 = indexArr[1]
insVlaue = 2
} else if (value.indexOf('/') > -1) {
const indexArr = value.split('/')
isNaN(indexArr[0])
? (this.$refs[refName].average01 = 0)
: (this.$refs[refName].average01 = indexArr[0])
this.$refs[refName].average02 = indexArr[1]
insVlaue = 3
} else {
insVlaue = 4
this.$refs[refName].checkboxList = value.split(',')
}
} else if (name === 'day') {
if (value === '*') {
insVlaue = 1
} else if (value === '?') {
insVlaue = 2
} else if (value.indexOf('-') > -1) {
const indexArr = value.split('-')
isNaN(indexArr[0])
? (this.$refs[refName].cycle01 = 0)
: (this.$refs[refName].cycle01 = indexArr[0])
this.$refs[refName].cycle02 = indexArr[1]
insVlaue = 3
} else if (value.indexOf('/') > -1) {
const indexArr = value.split('/')
isNaN(indexArr[0])
? (this.$refs[refName].average01 = 0)
: (this.$refs[refName].average01 = indexArr[0])
this.$refs[refName].average02 = indexArr[1]
insVlaue = 4
} else if (value.indexOf('W') > -1) {
const indexArr = value.split('W')
isNaN(indexArr[0])
? (this.$refs[refName].workday = 0)
: (this.$refs[refName].workday = indexArr[0])
insVlaue = 5
} else if (value === 'L') {
insVlaue = 6
} else {
this.$refs[refName].checkboxList = value.split(',')
insVlaue = 7
}
} else if (name === 'week') {
if (value === '*') {
insVlaue = 1
} else if (value === '?') {
insVlaue = 2
} else if (value.indexOf('-') > -1) {
const indexArr = value.split('-')
isNaN(indexArr[0])
? (this.$refs[refName].cycle01 = 0)
: (this.$refs[refName].cycle01 = indexArr[0])
this.$refs[refName].cycle02 = indexArr[1]
insVlaue = 3
} else if (value.indexOf('#') > -1) {
const indexArr = value.split('#')
isNaN(indexArr[0])
? (this.$refs[refName].average01 = 1)
: (this.$refs[refName].average01 = indexArr[0])
this.$refs[refName].average02 = indexArr[1]
insVlaue = 4
} else if (value.indexOf('L') > -1) {
const indexArr = value.split('L')
isNaN(indexArr[0])
? (this.$refs[refName].weekday = 1)
: (this.$refs[refName].weekday = indexArr[0])
insVlaue = 5
} else {
this.$refs[refName].checkboxList = value.split(',')
insVlaue = 6
}
} else if (name === 'year') {
if (value === '') {
insVlaue = 1
} else if (value === '*') {
insVlaue = 2
} else if (value.indexOf('-') > -1) {
insVlaue = 3
} else if (value.indexOf('/') > -1) {
insVlaue = 4
} else {
this.$refs[refName].checkboxList = value.split(',')
insVlaue = 5
}
}
this.$refs[refName].radioValue = insVlaue
},
// 表单选项的子组件校验数字格式(通过-props传递
checkNumber(value, minLimit, maxLimit) {
// 检查必须为整数
value = Math.floor(value)
if (value < minLimit) {
value = minLimit
} else if (value > maxLimit) {
value = maxLimit
}
return value
},
// 隐藏弹窗
hidePopup() {
this.$emit('hide')
},
// 填充表达式
submitFill() {
this.$emit('fill', this.contabValueString)
this.hidePopup()
},
clearCron() {
// 还原选择项
this.contabValueObj = {
second: '0',
min: '0',
hour: '0',
day: '*',
month: '*',
week: '*'
// year: "",
}
for (const j in this.contabValueObj) {
this.changeRadio(j, this.contabValueObj[j])
}
}
}
}
</script>
<style scoped>
.pop_btn {
text-align: center;
margin-top: 20px;
}
.popup-main {
position: relative;
margin: 10px auto 0;
background: #fff;
border-radius: 5px;
font-size: 12px;
overflow: hidden;
}
.popup-title {
overflow: hidden;
line-height: 34px;
padding-top: 6px;
background: #f2f2f2;
}
.popup-result {
position: relative;
box-sizing: border-box;
line-height: 24px;
margin: 17px auto;
padding: 10px 10px 10px;
border: 1px solid #dcdfe6;
box-shadow: 0 2px 4px 0 rgb(0 0 0 / 12%), 0 0 6px 0 rgb(0 0 0 / 4%);
}
.popup-result .title {
position: absolute;
top: -17px;
left: 50%;
width: 140px;
font-size: 14px;
margin-left: -70px;
text-align: center;
line-height: 30px;
background: #fff;
}
.popup-result table {
text-align: center;
width: 100%;
margin: 0 auto;
}
.popup-result table span {
display: block;
width: 100%;
font-family: arial;
line-height: 30px;
height: 30px;
white-space: nowrap;
overflow: hidden;
border: 1px solid #e8e8e8;
}
.popup-result-scroll {
font-size: 12px;
line-height: 24px;
height: 10em;
overflow-y: auto;
}
.el-form-item--mini.el-form-item,
.el-form-item--small.el-form-item {
margin-bottom: 10px;
}
</style>

View File

@@ -0,0 +1,189 @@
/* eslint-disable */
<template>
<el-form size="small">
<el-form-item>
<el-radio v-model="radioValue" :label="1">
{{ this.$t('common.CronTab.day') }}{{ this.$t('common.CronTab.wildcardsAllowed') }}[, - * /]
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="3">
{{ this.$t('common.CronTab.from') }}
<el-input-number v-model="cycle01" :min="0" :max="31" /> -
<el-input-number v-model="cycle02" :min="0" :max="31" /> {{ this.$t('common.CronTab.day') }}
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="4">
{{ this.$t('common.CronTab.every') }}
<el-input-number v-model="average02" :min="1" :max="31" /> {{ this.$t('common.CronTab.day') }}{{ this.$t('common.CronTab.executeOnce') }}
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="7">
{{ this.$t('common.CronTab.appoint') }}
<el-select v-model="checkboxList" clearable :placeholder="this.$t('common.CronTab.manyChoose')" multiple style="width:100%">
<el-option v-for="item in 31" :key="item" :value="item">{{ item }}</el-option>
</el-select>
</el-radio>
</el-form-item>
</el-form>
</template>
<script>
export default {
name: 'CrontabDay',
props: {
cron: {
type: Object,
default: () => {
return {}
}
},
check: {
type: Function,
default: () => {}
}
},
data() {
return {
radioValue: 1,
workday: 1,
cycle01: 1,
cycle02: 2,
average01: 1,
average02: 1,
checkboxList: [],
checkNum: this.$options.propsData.check
}
},
computed: {
cycleTotal: {
get() {
return this.cycle01 + '-' + this.cycle02
},
set() {
this.cycle01 = this.checkNum(this.cycle01, 1, 31)
this.cycle02 = this.checkNum(this.cycle02, 1, 31)
}
},
averageTotal: {
get() {
return '*' + '/' + this.average02
},
set() {
this.average01 = this.checkNum(this.average01, 1, 31)
this.average02 = this.checkNum(this.average02, 1, 31)
}
},
checkboxString: {
get() {
const str = this.checkboxList.join()
return str === '' ? '*' : str
},
set() {
}
}
},
watch: {
'radioValue': 'radioChange',
'cycleTotal': 'cycleChange',
'averageTotal': 'averageChange',
'workdayCheck': 'workdayChange',
'checkboxString': 'checkboxChange'
},
created() {
this.$nextTick(() => {
const arrs = []
for (let index = 0; index < this.checkboxList.length; index++) {
const cur = this.checkboxList[index]
arrs.push(parseFloat(cur))
}
this.checkboxList = arrs
})
},
methods: {
// 单选按钮值变化时
radioChange() {
('day rachange')
if (this.radioValue === 1) {
this.$emit('update', 'day', '*', 'day')
} else {
if (this.cron.hour === '*') {
this.$emit('update', 'hour', '0', 'day')
}
if (this.cron.min === '*') {
this.$emit('update', 'min', '0', 'day')
}
if (this.cron.second === '*') {
this.$emit('update', 'second', '0', 'day')
}
}
switch (this.radioValue) {
case 2:
this.$emit('update', 'day', '?')
break
case 3:
this.$emit('update', 'day', this.cycle01 + '-' + this.cycle02)
break
case 4:
this.$emit('update', 'day', '*' + '/' + this.average02)
break
case 5:
this.$emit('update', 'day', this.workday + 'W')
break
case 6:
this.$emit('update', 'day', 'L')
break
case 7:
this.$emit('update', 'day', this.checkboxString)
break
}
('day rachange end')
},
// 周期两个值变化时
cycleChange() {
if (this.radioValue === 3) {
this.$emit('update', 'day', this.cycleTotal)
}
},
// 平均两个值变化时
averageChange() {
if (this.radioValue === 4) {
this.$emit('update', 'day', this.averageTotal)
}
},
// 最近工作日值变化时
workdayChange() {
if (this.radioValue === 5) {
this.$emit('update', 'day', this.workday + 'W')
}
},
// checkbox值变化时
checkboxChange() {
if (this.radioValue === 7) {
this.$emit('update', 'day', this.checkboxString)
}
},
// 父组件传递的week发生变化触发
weekChange() {
// 判断week值与day不能同时为“?”
if (this.cron.week === '?' && this.radioValue === 2) {
this.radioValue = '1'
} else if (this.cron.week !== '?' && this.radioValue !== 2) {
this.radioValue = '2'
}
}
}
}
</script>
<style scoped>
.el-form-item--small.el-form-item {
margin-bottom: 10px;
}
</style>

View File

@@ -0,0 +1,158 @@
/* eslint-disable */
<template>
<el-form size="small">
<el-form-item>
<el-radio v-model="radioValue" :label="1">
{{ this.$t('common.CronTab.hour') }}{{ this.$t('common.CronTab.wildcardsAllowed') }}[, - * /]
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="2">
{{ this.$t('common.CronTab.from') }}
<el-input-number v-model="cycle01" :min="0" :max="60" /> -
<el-input-number v-model="cycle02" :min="0" :max="60" /> {{ this.$t('common.CronTab.hour') }}
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="3">
{{ this.$t('common.CronTab.every') }}
<el-input-number v-model="average02" :min="1" :max="60" /> {{ this.$t('common.CronTab.hour') }}{{ this.$t('common.CronTab.executeOnce') }}
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="4">
{{ this.$t('common.CronTab.appoint') }}
<el-select v-model="checkboxList" clearable :placeholder="this.$t('common.CronTab.manyChoose')" multiple style="width:100%">
<el-option v-for="item in 24" :key="item" :value="item-1">{{ item-1 }}</el-option>
</el-select>
</el-radio>
</el-form-item>
</el-form>
</template>
<script>
export default {
name: 'CrontabHour',
props: {
cron: {
type: Object,
default: () => {
return {}
}
},
check: {
type: Function,
default: () => {}
}
},
data() {
return {
radioValue: 1,
cycle01: 0,
cycle02: 1,
average01: 0,
average02: 1,
checkboxList: [],
checkNum: this.$options.propsData.check
}
},
computed: {
cycleTotal: {
get() {
return this.cycle01 + '-' + this.cycle02
},
set() {
this.cycle01 = this.checkNum(this.cycle01, 0, 23)
this.cycle02 = this.checkNum(this.cycle02, 0, 23)
}
},
averageTotal: {
get() {
return '*' + '/' + this.average02
},
set() {
this.average01 = this.checkNum(this.average01, 0, 23)
this.average02 = this.checkNum(this.average02, 1, 23)
}
},
checkboxString: {
get() {
const str = this.checkboxList.join()
return str === '' ? '*' : str
},
set() {
}
}
},
watch: {
'radioValue': 'radioChange',
'cycleTotal': 'cycleChange',
'averageTotal': 'averageChange',
'checkboxString': 'checkboxChange'
},
created() {
this.$nextTick(() => {
const arrs = []
for (let index = 0; index < this.checkboxList.length; index++) {
const cur = this.checkboxList[index]
arrs.push(parseFloat(cur))
}
this.checkboxList = arrs
})
},
methods: {
// 单选按钮值变化时
radioChange() {
if (this.radioValue === 1) {
this.$emit('update', 'hour', '*', 'hour')
// this.$emit('update', 'day', '*', 'hour')
} else {
if (this.cron.min === '*') {
this.$emit('update', 'min', '0', 'hour')
}
if (this.cron.second === '*') {
this.$emit('update', 'second', '0', 'hour')
}
}
switch (this.radioValue) {
case 2:
this.$emit('update', 'hour', this.cycle01 + '-' + this.cycle02)
break
case 3:
this.$emit('update', 'hour', '*' + '/' + this.average02)
break
case 4:
this.$emit('update', 'hour', this.checkboxString)
break
}
},
// 周期两个值变化时
cycleChange() {
if (this.radioValue === 2) {
this.$emit('update', 'hour', this.cycleTotal)
}
},
// 平均两个值变化时
averageChange() {
if (this.radioValue === 3) {
this.$emit('update', 'hour', this.averageTotal)
}
},
// checkbox值变化时
checkboxChange() {
if (this.radioValue === 4) {
this.$emit('update', 'hour', this.checkboxString)
}
}
}
}
</script>
<style scoped>
.el-form-item--small.el-form-item {
margin-bottom: 10px
}
</style>

View File

@@ -0,0 +1,156 @@
/* eslint-disable */
<template>
<el-form size="small">
<el-form-item>
<el-radio v-model="radioValue" :label="1" size="mini">
{{ this.$t('common.CronTab.min') }}{{ this.$t('common.CronTab.wildcardsAllowed') }}[, - * /]
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="2">
{{ this.$t('common.CronTab.from') }}
<el-input-number v-model="cycle01" :min="0" :max="60" /> -
<el-input-number v-model="cycle02" :min="0" :max="60" /> {{ this.$t('common.CronTab.min') }}
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="3">
{{ this.$t('common.CronTab.from') }}
<el-input-number v-model="average02" :min="1" :max="60" /> {{ this.$t('common.CronTab.min') }}{{ this.$t('common.CronTab.executeOnce') }}
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="4">
{{ this.$t('common.CronTab.appoint') }}
<el-select v-model="checkboxList" clearable :placeholder="this.$t('common.CronTab.manyChoose')" multiple style="width:100%" size="small">
<el-option v-for="item in 60" :key="item" :value="item-1">{{ item-1 }}</el-option>
</el-select>
</el-radio>
</el-form-item>
</el-form>
</template>
<script>
export default {
name: 'CrontabMin',
props: {
cron: {
type: Object,
default: () => {
return {}
}
},
check: {
type: Function,
default: () => {}
}
},
data() {
return {
radioValue: 1,
cycle01: 1,
cycle02: 2,
average01: 0,
average02: 1,
checkboxList: [],
checkNum: this.$options.propsData.check
}
},
computed: {
cycleTotal: {
get() {
return this.cycle01 + '-' + (this.cycle02 ? this.cycle02 : '')
},
set() {
this.cycle01 = this.checkNum(this.cycle01, 0, 59)
this.cycle02 = this.checkNum(this.cycle02, 0, 59)
}
},
averageTotal: {
get() {
return '*' + '/' + (this.average02 ? this.average02 : 0)
},
set() {
this.average01 = this.checkNum(this.average01, 0, 59)
this.average02 = this.checkNum(this.average02, 1, 59)
}
},
checkboxString: {
get() {
const str = this.checkboxList.join()
return str === '' ? '*' : str
},
set() {
}
}
},
watch: {
'radioValue': 'radioChange',
'cycleTotal': 'cycleChange',
'averageTotal': 'averageChange',
'checkboxString': 'checkboxChange'
},
created() {
this.$nextTick(() => {
const arrs = []
for (let index = 0; index < this.checkboxList.length; index++) {
const cur = this.checkboxList[index]
arrs.push(parseFloat(cur))
}
this.checkboxList = arrs
})
},
methods: {
// 单选按钮值变化时
radioChange() {
if (this.radioValue !== 1 && this.cron.second === '*') {
this.$emit('update', 'second', '0', 'min')
}
switch (this.radioValue) {
case 1:
this.$emit('update', 'min', '*', 'min')
this.$emit('update', 'hour', '*', 'min')
break
case 2:
this.$emit('update', 'min', this.cycle01 + '-' + this.cycle02, 'min')
break
case 3:
this.$emit('update', 'min', '*' + '/' + this.average02, 'min')
break
case 4:
this.$emit('update', 'min', this.checkboxString, 'min')
break
}
},
// 周期两个值变化时
cycleChange() {
if (this.radioValue === 2) {
this.$emit('update', 'min', this.cycleTotal, 'min')
}
},
// 平均两个值变化时
averageChange() {
if (this.radioValue === 3) {
this.$emit('update', 'min', this.averageTotal, 'min')
}
},
// checkbox值变化时
checkboxChange() {
if (this.radioValue === 4) {
this.$emit('update', 'min', this.checkboxString, 'min')
}
}
}
}
</script>
<style scoped>
.el-form-item--small.el-form-item {
margin-bottom: 10px;
}
</style>

View File

@@ -0,0 +1,163 @@
/* eslint-disable */
<template>
<el-form size="small">
<el-form-item>
<el-radio v-model="radioValue" :label="1">
{{ this.$t('common.CronTab.month') }}{{ this.$t('common.CronTab.wildcardsAllowed') }}[, - * /]
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="2">
{{ this.$t('common.CronTab.from') }}
<el-input-number v-model="cycle01" :min="1" :max="12" /> -
<el-input-number v-model="cycle02" :min="1" :max="12" /> {{ this.$t('common.CronTab.month') }}
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="3">
{{ this.$t('common.CronTab.every') }}
<el-input-number v-model="average02" :min="1" :max="12" /> {{ this.$t('common.CronTab.month') }}{{ this.$t('common.CronTab.executeOnce') }}
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="4">
{{ this.$t('common.CronTab.appoint') }}
<el-select v-model="checkboxList" clearable :placeholder="this.$t('common.CronTab.manyChoose')" multiple style="width:100%">
<el-option v-for="item in 12" :key="item" :value="item">{{ item }}</el-option>
</el-select>
</el-radio>
</el-form-item>
</el-form>
</template>
<script>
export default {
name: 'CrontabMonth',
props: {
cron: {
type: Object,
default: () => {
return {}
}
},
check: {
type: Function,
default: () => {}
}
},
data() {
return {
radioValue: 1,
cycle01: 1,
cycle02: 2,
average01: 1,
average02: 1,
checkboxList: [],
checkNum: this.check
}
},
computed: {
cycleTotal: {
get() {
return this.cycle01 + '-' + this.cycle02
},
set() {
this.cycle01 = this.checkNum(this.cycle01, 1, 12)
this.cycle02 = this.checkNum(this.cycle02, 1, 12)
}
},
averageTotal: {
get() {
return '*' + '/' + this.average02
},
set() {
this.average01 = this.checkNum(this.average01, 1, 12)
this.average02 = this.checkNum(this.average02, 1, 12)
}
},
checkboxString: {
get() {
const str = this.checkboxList.join()
return str === '' ? '*' : str
},
set() {
}
}
},
watch: {
'radioValue': 'radioChange',
'cycleTotal': 'cycleChange',
'averageTotal': 'averageChange',
'checkboxString': 'checkboxChange'
},
created() {
this.$nextTick(() => {
const arrs = []
for (let index = 0; index < this.checkboxList.length; index++) {
const cur = this.checkboxList[index]
arrs.push(parseFloat(cur))
}
this.checkboxList = arrs
})
},
methods: {
// 单选按钮值变化时
radioChange() {
if (this.radioValue === 1) {
this.$emit('update', 'month', '*')
} else {
if (this.cron.day === '*') {
this.$emit('update', 'day', '*', 'month')
}
if (this.cron.hour === '*') {
this.$emit('update', 'hour', '0', 'month')
}
if (this.cron.min === '*') {
this.$emit('update', 'min', '0', 'month')
}
if (this.cron.second === '*') {
this.$emit('update', 'second', '0', 'month')
}
}
switch (this.radioValue) {
case 2:
this.$emit('update', 'month', this.cycle01 + '-' + this.cycle02)
break
case 3:
this.$emit('update', 'month', '*' + '/' + this.average02)
break
case 4:
this.$emit('update', 'month', this.checkboxString)
break
}
},
// 周期两个值变化时
cycleChange() {
if (this.radioValue === 2) {
this.$emit('update', 'month', this.cycleTotal)
}
},
// 平均两个值变化时
averageChange() {
if (this.radioValue === 3) {
this.$emit('update', 'month', this.averageTotal)
}
},
// checkbox值变化时
checkboxChange() {
if (this.radioValue === 4) {
this.$emit('update', 'month', this.checkboxString)
}
}
}
}
</script>
<style scoped>
.el-form-item--small.el-form-item {
margin-bottom: 10px;
}
</style>

View File

@@ -0,0 +1,62 @@
/* eslint-disable */
<template>
<div class="popup-result">
<p class="title">{{ this.$t('common.CronTab.runningTimes') }}</p>
<ul class="popup-result-scroll">
<template v-if="isShow">
<li v-for="item in resultList" :key="item">{{ item }}</li>
</template>
<li v-else>{{ this.$t('common.CronTab.calculationResults') }}</li>
</ul>
</div>
</template>
<script>
import parser from 'cron-parser'
import moment from 'moment'
export default {
name: 'CrontabResult',
props: {
ex: {
type: String,
default() {
return ''
}
}
},
data() {
return {
dayRule: '',
dayRuleSup: '',
dateArr: [],
resultList: [],
isShow: false
}
},
watch: {
'ex': 'expressionChange222'
},
mounted: function() {
// 初始化 获取一次结果
this.expressionChange222()
},
methods: {
expressionChange222() {
this.isShow = true
const rule = 0 + ' ' + this.$options.propsData.ex
try {
this.resultList = []
var interval = parser.parseExpression(rule)
for (let index = 0; index < 5; index++) {
const cur = interval.next().toString()
this.resultList.push(moment(cur).format('YYYY-MM-DD HH:mm:ss'))
}
} catch (error) {
this.isShow = false
console.log(error, 'error')
}
}
}
}
</script>

View File

@@ -0,0 +1,178 @@
/* eslint-disable */
<template>
<el-form size="small">
<el-form-item>
<el-radio v-model="radioValue" :label="1">
{{ this.$t('common.CronTab.week') }}{{ this.$t('common.CronTab.wildcardsAllowed') }}[, - * /]
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="3">
{{ this.$t('common.CronTab.cycleFromWeek') }}
<el-input-number v-model="cycle01" :min="1" :max="7" /> -
<el-input-number v-model="cycle02" :min="1" :max="7" />
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="6">
{{ this.$t('common.CronTab.appoint') }}
<el-select v-model="checkboxList" clearable :placeholder="this.$t('common.CronTab.manyChoose')" multiple style="width:100%">
<el-option v-for="(item,index) of weekList" :key="index" :value="index+1">{{ item }}</el-option>
</el-select>
</el-radio>
</el-form-item>
</el-form>
</template>
<script>
export default {
name: 'CrontabWeek',
props: {
cron: {
type: Object,
default: () => {
return {}
}
},
check: {
type: Function,
default: () => {}
}
},
data() {
return {
radioValue: 2,
weekday: 1,
cycle01: 1,
cycle02: 2,
average01: 1,
average02: 1,
checkboxList: [],
weekList: [this.$t('common.CronTab.Monday'), this.$t('common.CronTab.Tuesday'), this.$t('common.CronTab.Wednesday'), this.$t('common.CronTab.Thursday'), this.$t('common.CronTab.Friday'), this.$t('common.CronTab.Saturday'), this.$t('common.CronTab.Sunday')],
checkNum: this.$options.propsData.check
}
},
computed: {
cycleTotal: {
get() {
return this.cycle01 + '-' + this.cycle02
},
set() {
this.cycle01 = this.checkNum(this.cycle01, 1, 7)
this.cycle02 = this.checkNum(this.cycle02, 1, 7)
}
},
averageTotal: {
get() {
return this.average01 + '#' + this.average02
},
set() {
this.average01 = this.checkNum(this.average01, 1, 4)
this.average02 = this.checkNum(this.average02, 1, 7)
}
},
checkboxString: {
get() {
const str = this.checkboxList.join()
return str === '' ? '*' : str
},
set() {
}
}
},
watch: {
'radioValue': 'radioChange',
'cycleTotal': 'cycleChange',
'averageTotal': 'averageChange',
'weekdayCheck': 'weekdayChange',
'checkboxString': 'checkboxChange'
},
created() {
this.$nextTick(() => {
const arrs = []
for (let index = 0; index < this.checkboxList.length; index++) {
const cur = this.checkboxList[index]
arrs.push(parseFloat(cur))
}
this.checkboxList = arrs
})
},
methods: {
// 单选按钮值变化时
radioChange() {
if (this.radioValue === 1) {
this.$emit('update', 'week', '*')
this.$emit('update', 'year', '*')
} else {
if (this.cron.month === '*') {
this.$emit('update', 'month', '*', 'week')
}
if (this.cron.day === '*') {
this.$emit('update', 'day', '*', 'week')
}
if (this.cron.hour === '*') {
this.$emit('update', 'hour', '*', 'week')
}
if (this.cron.min === '*') {
this.$emit('update', 'min', '*', 'week')
}
if (this.cron.second === '*') {
this.$emit('update', 'second', '*', 'week')
}
}
switch (this.radioValue) {
case 2:
this.$emit('update', 'week', '?')
break
case 3:
this.$emit('update', 'week', this.cycle01 + '-' + this.cycle02)
break
case 4:
this.$emit('update', 'week', this.average01 + '#' + this.average02)
break
case 5:
this.$emit('update', 'week', this.weekday + 'L')
break
case 6:
this.$emit('update', 'week', this.checkboxString)
break
}
},
// 根据互斥事件更改radio的值
// 周期两个值变化时
cycleChange() {
if (this.radioValue === 3) {
this.$emit('update', 'week', this.cycleTotal)
}
},
// 平均两个值变化时
averageChange() {
if (this.radioValue === 4) {
this.$emit('update', 'week', this.averageTotal)
}
},
// 最近工作日值变化时
weekdayChange() {
if (this.radioValue === 5) {
this.$emit('update', 'week', this.weekday + 'L')
}
},
// checkbox值变化时
checkboxChange() {
if (this.radioValue === 6) {
this.$emit('update', 'week', this.checkboxString)
}
}
}
}
</script>
<style scoped>
.el-form-item--small.el-form-item {
margin-bottom: 10px;
}
</style>

View File

@@ -0,0 +1,55 @@
<template>
<div>
<div class="box">
<el-input v-model="input" clearable @focus="showDialog" @clear="onClear" />
</div>
<el-dialog :title="this.$t('common.CronTab.newCron')" :visible.sync="showCron" top="8vh" width="580px" append-to-body>
<Crontab
:expression="expression"
@hide="showCron = false"
@fill="crontabFill"
/>
</el-dialog>
</div>
</template>
<script>
import Crontab from './Crontab.vue'
export default {
components: { Crontab },
props: {
value: {
type: String,
default: ''
}
},
data() {
return {
input: _.cloneDeep(this.value),
expression: _.cloneDeep(this.value),
showCron: false
}
},
methods: {
crontabFill(value) {
// 确定后回传的值
this.input = value
this.$emit('change', value)
},
showDialog() {
this.expression = this.input
this.showCron = true
},
onClear() {
this.input = ''
this.$emit('change', '')
}
}
}
</script>
<style scoped>
.el-dialog__body {
padding: 12px 16px;
}
</style>

View File

@@ -97,6 +97,7 @@ export default {
}
},
props: {
// eslint-disable-next-line vue/require-default-prop
data: Object,
prop: {
type: String,
@@ -104,10 +105,13 @@ export default {
return this.data.id
}
},
// eslint-disable-next-line vue/require-prop-types,vue/require-default-prop
itemValue: {},
// eslint-disable-next-line vue/require-default-prop
value: Object,
disabled: Boolean,
readonly: Boolean,
// eslint-disable-next-line vue/require-default-prop
options: Array
},
data() {

View File

@@ -1,7 +1,7 @@
<template>
<el-form ref="elForm" v-bind="$attrs" :model="value" class="el-form-renderer">
<template v-for="item in innerContent">
<slot :name="`id:${item.id}`" />
<slot v-if="!isHidden(item)" :name="`id:${item.id}`" />
<component
:is="item.type === GROUP ? 'render-form-group' : 'render-form-item'"
:key="item.id"
@@ -13,7 +13,7 @@
:options="options[item.id]"
@updateValue="updateValue"
/>
<slot :name="`$id:${item.id}`" />
<slot v-if="!isHidden(item)" :name="`$id:${item.id}`" />
</template>
<slot />
</el-form>
@@ -202,6 +202,18 @@ export default {
setOptions(id, options) {
_set(this.options, id, options)
this.options = { ...this.options } // 设置之前不存在的 options 时需要重新设置响应式更新
},
isHidden(item) {
if (!item.el || !item.el['hiddenGroup']) {
return false
}
if (item.hidden === true) {
return true
}
if (typeof item.hidden === 'function') {
return item.hidden(this.value)
}
return false
}
}
}

View File

@@ -12,7 +12,7 @@
<slot v-for="item in fields" :slot="`id:${item.id}`" :name="`id:${item.id}`" />
<slot v-for="item in fields" :slot="`$id:${item.id}`" :name="`$id:${item.id}`" />
<el-form-item>
<el-form-item v-if="hasButtons" class="form-buttons">
<el-button v-for="button in moreButtons" :key="button.title" size="small" v-bind="button" @click="handleClick(button)">{{ button.title }}</el-button>
<el-button v-if="defaultButton && hasReset" size="small" @click="resetForm('form')">{{ $t('common.Reset') }}</el-button>
<el-button v-if="defaultButton && hasSaveContinue" size="small" @click="submitForm('form', true)">{{ $t('common.SaveAndAddAnother') }}</el-button>
@@ -32,6 +32,10 @@ export default {
type: Boolean,
default: true
},
hasButtons: {
type: Boolean,
default: true
},
hasReset: {
type: Boolean,
default: true

View File

@@ -840,7 +840,7 @@ export default {
this.$emit('selection-change', val)
},
totalData(val) {
if (val) {
if (val && val.length !== this.total) {
this.page = defaultFirstPage
this.total = val.length
this.getList()

View File

@@ -94,7 +94,12 @@ export default {
tableConfig() {
const tableDefaultConfig = this.defaultConfig
tableDefaultConfig.paginationSize = _.get(this.globalTableConfig, 'paginationSize', 15)
let tableAttrs = tableDefaultConfig.tableAttrs
if (this.config.tableAttrs) {
tableAttrs = Object.assign(tableAttrs, this.config.tableAttrs)
}
const config = Object.assign(tableDefaultConfig, this.config)
config.tableAttrs = tableAttrs
return config
},
iListeners() {

View File

@@ -28,13 +28,30 @@ export default {
return this.formatter(this.item, this.value)
}
if (typeof this.value === 'boolean') {
return <span>{this.toChoicesDisplay(this.value)}</span>
return (
<span class='item-value'>{this.toChoicesDisplay(this.value)}</span>
)
}
return <span>{this.value}</span>
if (this.value instanceof Array) {
const newArr = this.value || []
return (
<span class='item-value'>
{
newArr.map((item, index) => <div key={index}>{item.key}{item.value} </div>)
}
</span>
)
}
return (
<span class='item-value'>{this.value}</span>
)
}
}
</script>
<style scoped>
.item-value {
word-break: break-word;
}
</style>

View File

@@ -0,0 +1,73 @@
<template>
<div class="json-editor">
<JsonEditor
v-model="resultInfo"
:show-btns="false"
:mode="'code'"
@json-change="onJsonChange"
@json-save="onJsonSave"
@has-error="onError"
/>
</div>
</template>
<script>
import JsonEditor from 'vue-json-editor'
export default {
components: { JsonEditor },
props: {
value: {
type: String,
default: () => ''
}
},
data() {
return {
hasJsonFlag: true, // json是否验证通过
resultInfo: {}
}
},
created() {
this.resultInfo = JSON.parse(this.value)
},
methods: {
// 数据改变
onJsonChange(value) {
this.onJsonSave(value)
},
// 保存
onJsonSave(value) {
this.resultInfo = value
this.hasJsonFlag = true
setTimeout(() => {
this.$emit('change', JSON.stringify(this.resultInfo))
}, 500)
},
onError: _.debounce(function(value) {
this.$message.error(this.$t('common.FormatError'))
}, 1100)
}
}
</script>
<style lang="scss" scoped>
.json-editor {
&>>> .jsoneditor {
border: 1px solid #e5e6e7;
}
&>>> .jsoneditor-compact {
display: none;
}
&>>> .jsoneditor-modes {
display: none;
}
&>>> .jsoneditor-poweredBy {
display: none;
}
&>>> .jsoneditor-menu {
background: #1ab394;
border-bottom: 1px solid #1ab394;
}
}
</style>

View File

@@ -2,13 +2,14 @@
<el-select
ref="select"
v-model="iValue"
v-loading="!initialized"
v-loadmore="loadMore"
:options="iOptions"
:remote="remote"
:remote-method="filterOptions"
:multiple="multiple"
filterable
:clearable="clearable"
filterable
popper-append-to-body
class="select2"
v-bind="$attrs"
@@ -97,7 +98,6 @@ export default {
return {
loading: false,
initialized: false,
iValue: this.value ? this.value : [],
defaultParams: _.cloneDeep(defaultParams),
params: _.cloneDeep(defaultParams),
iOptions: this.options || [],
@@ -112,6 +112,18 @@ export default {
optionsValues() {
return this.iOptions.map((v) => v.value)
},
iValue: {
set(val) {
const noValue = !this.value || this.value.length === 0
if (noValue && !this.initialized) {
return
}
this.$emit('input', val)
},
get() {
return this.value
}
},
iAjax() {
const defaultPageSize = 10
const defaultMakeParams = (params) => {
@@ -161,11 +173,14 @@ export default {
this.iValue = iNew
}
},
mounted() {
async mounted() {
// this.$log.debug('Select2 url is: ', this.iAjax.url)
if (!this.initialized) {
this.initialSelect()
this.initialized = true
await this.initialSelect()
setTimeout(() => {
this.iValue = this.value
this.initialized = true
})
}
this.$nextTick(() => {
// 因为elform存在问题这个来清楚验证
@@ -243,9 +258,13 @@ export default {
// this.$log.debug('Select ajax config', this.iAjax)
if (this.iAjax.url) {
if (this.value && this.value.length !== 0) {
this.$log.debug('Start init select2 value')
const data = await createSourceIdCache(this.value)
this.params.spm = data.spm
this.$log.debug('Start init select2 value, ', this.value)
let value = this.value
if (!Array.isArray(value)) {
value = [value]
}
const data = await createSourceIdCache(value)
this.params.spm = data['spm']
await this.getInitialOptions()
}
await this.getOptions()
@@ -301,4 +320,8 @@ export default {
.select2 {
width: 100%;
}
.select2 >>> .el-tag.el-tag--info {
height: auto;
white-space: normal;
}
</style>

View File

@@ -0,0 +1,60 @@
<template>
<div>
<el-button v-show="!isShow" type="text" icon="el-icon-edit" @click="isShow=true">
{{ text }}
</el-button>
<el-input
v-show="isShow"
v-model.trim="curValue"
show-password
:type="type"
autocomplete="new-password"
:placeholder="placeholder"
@change="onChange"
/>
</div>
</template>
<script>
export default {
props: {
value: {
type: String,
default: () => ''
},
type: {
type: String,
default: () => 'input'
},
text: {
type: String,
default() {
return this.$t('common.Update')
}
},
placeholder: {
type: String,
default: () => ''
}
},
data() {
return {
isShow: false,
curValue: this.value
}
},
created() {
if (this.$route.path.indexOf('/create') !== -1) {
this.isShow = true
}
},
methods: {
onChange(e) {
this.$emit('input', this.curValue)
}
}
}
</script>
<style scoped>
</style>

View File

@@ -8,13 +8,17 @@
<script>
export default {
props: {
value: {
type: String,
default: () => ''
},
// value: {
// type: String,
// default: () => ''
// },
tip: {
type: String,
default: () => ''
},
toFormat: {
type: String,
default: () => 'string'
}
},
methods: {
@@ -25,7 +29,11 @@ export default {
const vm = this
const reader = new FileReader()
reader.onload = function() {
vm.$emit('input', this.result)
let result = this.result
if (vm.toFormat === 'object') {
result = JSON.parse(result)
}
vm.$emit('input', result)
}
reader.readAsText(
e.target.files[0]

View File

@@ -22,8 +22,12 @@ export default {
}
},
rules(item) {
let userIsOrgAdmin = item.el.userIsOrgAdmin
// undefined 个人信息更新或用户更改密码页面,使用当前用户;否则使用更新用户表单中传递的值
userIsOrgAdmin = userIsOrgAdmin === undefined ? store.getters.currentUserIsAdmin : userIsOrgAdmin
const passwordRule = store.getters.publicSettings.PASSWORD_RULE
const validatePassword = (rule, value, callback) => {
const validatePassword = function(rule, value, callback) {
if (!value) {
return callback()
}
@@ -46,7 +50,10 @@ export default {
return callback(new Error(msg))
}
}
const secureLength = passwordRule ? passwordRule.SECURITY_PASSWORD_MIN_LENGTH : 7
let secureLength = passwordRule ? passwordRule.SECURITY_PASSWORD_MIN_LENGTH : 7
if (userIsOrgAdmin) {
secureLength = passwordRule ? passwordRule.SECURITY_ADMIN_USER_PASSWORD_MIN_LENGTH : 7
}
if (value.length < secureLength) {
return callback(new Error(i18n.t('common.password.MIN_LENGTH_ERROR', [secureLength])))
}
@@ -59,17 +66,12 @@ export default {
data() {
return {
attrs: {
secureLength: 7
}
}
},
computed: {
...mapGetters(['publicSettings'])
},
created() {
const passwordRule = this.publicSettings.PASSWORD_RULE || {}
this.attrs.secureLength = passwordRule ? passwordRule.SECURITY_PASSWORD_MIN_LENGTH : 7
},
methods: {
handleInput(value) {
this.$emit('input', value)

View File

@@ -1,250 +1,409 @@
<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 class="c-weektime">
<div class="c-schedue" />
<div :class="{'c-schedue': true, 'c-schedue-notransi': mode}" :style="styleValue" />
<table class="c-weektime-table" :class="{'c-min-table': colspan < 2}">
<thead class="c-weektime-head">
<tr>
<th rowspan="8" class="week-td">{{ this.$t('common.WeekCronSelect.WeekOrTime') }}</th>
<th :colspan="12 * colspan">00:00 - 12:00</th>
<th :colspan="12 * colspan">12:00 - 24:00</th>
</tr>
<tr>
<td v-for="t in theadArr" :key="t" :colspan="colspan">{{ t }}</td>
</tr>
</thead>
<tbody class="c-weektime-body">
<tr v-for="t in weektimeData" :key="t.row">
<td>{{ t.value }}</td>
<td
v-for="n in t.child"
:key="`${n.row}-${n.col}`"
:data-week="n.row"
:data-time="n.col"
:class="selectClasses(n)"
class="weektime-atom-item"
@mouseenter="cellEnter(n)"
@mousedown="cellDown(n)"
@mouseup="cellUp(n)"
/>
</div>
</div>
</div>
<div class="tips">{{ tips }}</div>
</tr>
<tr>
<td colspan="49" class="c-weektime-preview">
<div class="g-clearfix c-weektime-con">
<span class="g-pull-left">{{ this.$t('common.WeekCronSelect.CanDragSelect') }}</span>
<a class="g-pull-right" @click.prevent="clearWeektime">{{ this.$t('common.WeekCronSelect.ClearSelection') }}</a>
<a class="g-pull-right g-pull-margin" @click.prevent="selectAll">{{ this.$t('common.WeekCronSelect.SelectAll') }}</a>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
const createArr = len => {
return Array.from(Array(len)).map((ret, id) => id)
}
function splicing(list) {
let same
let i = -1
const len = list.length
const arr = []
if (!len) return
while (++i < len) {
const item = list[i]
if (item.check) {
if (item.check !== Boolean(same)) {
arr.push(...['、', item.begin, '~', item.end])
} else if (arr.length) {
arr.pop()
arr.push(item.end)
}
}
same = Boolean(item.check)
}
arr.shift()
return arr.join('')
}
export default {
model: {
prop: 'sendTimeList'
},
name: 'WeekCronSelect',
props: {
sendTimeList: {
type: Object,
required: true,
value: {
type: Array,
default: () => []
},
readonly: {
type: Boolean,
default: false
colspan: {
type: Number,
default() {
return 2
}
}
},
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: '向右选中,向左取消选择'
width: 0,
height: 0,
left: 0,
top: 0,
mode: 0,
row: 0,
col: 0,
theadArr: [],
weekArr: [
this.$t('common.WeekCronSelect.Monday'),
this.$t('common.WeekCronSelect.Tuesday'),
this.$t('common.WeekCronSelect.Wednesday'),
this.$t('common.WeekCronSelect.Thursday'),
this.$t('common.WeekCronSelect.Friday'),
this.$t('common.WeekCronSelect.Saturday'),
this.$t('common.WeekCronSelect.Sunday')
],
weektimeData: [],
timeRange: [] // 格式化之后数据
}
},
computed: {
},
watch: {
timeRangeList: function(value) {
this.$emit('change', value)
this.$parent.$emit('el.form.change')// 触发父组件的校验规则
styleValue() {
return {
width: `${this.width}px`,
height: `${this.height}px`,
left: `${this.left}px`,
top: `${this.top}px`
}
},
sendTimeList: {
handler() {
this.transformedIndex()
},
deep: true
selectClasses() {
return n => n.check ? 'ui-selected' : ''
}
},
mounted() {
this.transformedIndex()
created() {
this.init()
if (this.value.length > 0) this.nextValue()
},
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
// 初始化数据结构
init() {
this.theadArr = createArr(24)
const isData = this.weekArr.map((ret, index) => {
const children = (ret, row, max) => {
return createArr(max).map((t, col) => {
const curValue = this.formatWeektime(col)
return {
week: ret,
value: curValue,
begin: curValue.split('~')[0],
end: curValue.split('~')[1],
row: row,
col: col
}
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('没有拿到开始时间或结束时间或者时间段格式不对')
})
}
return {
value: ret,
row: index,
child: children(ret, index, 48)
}
})
this.tips = this.timeRangeList && this.timeRangeList.length > 0 ? this.timeRangeList : '向右选中,向左取消选择'
this.weektimeData = isData
},
// 下标区间转换成时间区间
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++
// 反解析传递过来的默认值
nextValue() {
const deepValue = _.cloneDeep(this.value)
for (let i = 0, len = deepValue.length; i < len; i++) {
const cur = deepValue[i]
const curValue = cur?.value
if (curValue.length > 0) {
const childValue = curValue.split('、')
for (let j = 0; j < childValue.length; j++) {
const curJ = childValue[j]
this.renderWeekRange(curJ, cur.id)
}
}
}
},
// 是否选中计算className
compClass(index) {
if (index === this.startIndex) {
return 'hours-item-left preSelected'
// 渲染时间区间
renderWeekRange(val, id) {
const idNum = id === 0 ? 6 : id - 1
const [start, end] = val.split('~')
const startVal = this.countIndex(start)
const endVal = this.countIndex(end)
for (let i = startVal; i < (endVal === 0 ? 48 : endVal); i++) {
const curWeek = this.weektimeData[idNum]
curWeek.child[i].check = true
}
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
}
// 计算索引
countIndex(val) {
const one = val.substr(0, 2)
const a1 = one.startsWith('0') ? one.substr(1, 2) : one
var reg = RegExp(/30/)
const a2 = val.match(reg) ? 1 : 0
const curIndex = (a1 * 2) + a2
return curIndex
},
formatDate(date, fmt) {
const o = {
'M+': date.getMonth() + 1,
'd+': date.getDate(),
'h+': date.getHours(),
'm+': date.getMinutes(),
's+': date.getSeconds(),
'q+': Math.floor((date.getMonth() + 3) / 3),
'S': date.getMilliseconds()
}
if (/(y+)/.test(fmt)) {
fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length))
}
for (var k in o) {
if (new RegExp('(' + k + ')').test(fmt)) {
fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : (('00' + o[k]).substr(('' + o[k]).length)))
}
}
return fmt
},
formatWeektime(col) {
const timeStamp = 1542384000000 // '2018-11-17 00:00:00'
const beginStamp = timeStamp + col * 1800000 // col * 30 * 60 * 1000
const endStamp = beginStamp + 1800000
const begin = this.formatDate(new Date(beginStamp), 'hh:mm')
const end = this.formatDate(new Date(endStamp), 'hh:mm')
return `${begin}~${end}`
},
// 清空时间段
clearWeektime() {
this.weektimeData.forEach(item => {
item.child.forEach(t => {
this.$set(t, 'check', false)
})
})
this.timeRange = []
this.$emit('change', this.timeRange)
},
// 全选
selectAll() {
this.weektimeData.forEach(item => {
item.child.forEach(t => {
this.$set(t, 'check', true)
})
})
this.setTimeRange()
},
setTimeRange() {
this.timeRange = this.weektimeData.map(item => {
return {
id: item.row === 6 ? 0 : item.row + 1,
value: splicing(item.child)
}
})
this.$emit('change', this.timeRange)
},
cellEnter(item) {
const ele = document.querySelector(`td[data-week='${item.row}'][data-time='${item.col}']`)
if (ele && !this.mode) {
this.left = ele.offsetLeft
this.top = ele.offsetTop
} else {
if (item.col <= this.col && item.row <= this.row) {
this.width = (this.col - item.col + 1) * ele.offsetWidth
this.height = (this.row - item.row + 1) * ele.offsetHeight
this.left = ele.offsetLeft
this.top = ele.offsetTop
} else if (item.col >= this.col && item.row >= this.row) {
this.width = (item.col - this.col + 1) * ele.offsetWidth
this.height = (item.row - this.row + 1) * ele.offsetHeight
if (item.col > this.col && item.row === this.row) this.top = ele.offsetTop
if (item.col === this.col && item.row > this.row) this.left = ele.offsetLeft
} else if (item.col > this.col && item.row < this.row) {
this.width = (item.col - this.col + 1) * ele.offsetWidth
this.height = (this.row - item.row + 1) * ele.offsetHeight
this.top = ele.offsetTop
} else if (item.col < this.col && item.row > this.row) {
this.width = (this.col - item.col + 1) * ele.offsetWidth
this.height = (item.row - this.row + 1) * ele.offsetHeight
this.left = ele.offsetLeft
}
}
},
cellDown(item) {
const ele = document.querySelector(`td[data-week='${item.row}'][data-time='${item.col}']`)
this.check = Boolean(item.check)
this.mode = 1
if (ele) {
this.width = ele.offsetWidth
this.height = ele.offsetHeight
}
this.row = item.row
this.col = item.col
},
cellUp(item) {
if (item.col <= this.col && item.row <= this.row) {
this.selectWeek([item.row, this.row], [item.col, this.col], !this.check)
} else if (item.col >= this.col && item.row >= this.row) {
this.selectWeek([this.row, item.row], [this.col, item.col], !this.check)
} else if (item.col > this.col && item.row < this.row) {
this.selectWeek([item.row, this.row], [this.col, item.col], !this.check)
} else if (item.col < this.col && item.row > this.row) {
this.selectWeek([this.row, item.row], [item.col, this.col], !this.check)
}
this.width = 0
this.height = 0
this.mode = 0
this.setTimeRange()
},
selectWeek(row, col, check) {
const [minRow, maxRow] = row
const [minCol, maxCol] = col
this.weektimeData.forEach(item => {
item.child.forEach(t => {
if (t.row >= minRow && t.row <= maxRow && t.col >= minCol && t.col <= maxCol) {
this.$set(t, 'check', check)
}
})
})
}
}
}
</script>
<style lang='scss' scoped>
.hours-container {
display: flex;
cursor: pointer;
.hours-item {
width: 30px;
height: 60px;
border: 1px solid #c2d0f3;
border-right: none;
<style lang="scss" scoped>
.c-weektime {
min-width: 640px;
position: relative;
display: inline-block;
}
.c-schedue {
background: #598fe6;
position: absolute;
width: 0;
height: 0;
opacity: .6;
pointer-events: none;
}
.c-schedue-notransi {
transition: width .12s ease, height .12s ease, top .12s ease, left .12s ease;
}
.c-weektime-table {
border-collapse: collapse;
th {
vertical-align: inherit;
font-weight: bold;
}
tr {
height: 30px;
}
tr, td, th {
user-select: none;
border: 1px solid #dee4f5;
text-align: center;
&:last-child {
border-right: 1px solid #c2d0f3;
min-width: 12px;
line-height: 1.6em;
transition: background .16s ease;
}
.c-weektime-head {
font-size: 12px;
.week-td {
width: 72px;
}
.hours-item-header {
width: 100%;
height: 30px;
line-height: 30px;
border-bottom: 1px solid #c2d0f3;
}
.c-weektime-body {
font-size: 12px;
td {
&.weektime-atom-item {
user-select: unset;
background-color: #f5f5f5;
}
&.ui-selected {
background-color: #598fe6;
}
}
.hours-item-value {
width: 100%;
height: 30px;
box-sizing: border-box;
display: flex;
}
.c-weektime-preview {
line-height: 2.4em;
padding: 0 10px;
font-size: 13px;
.c-weektime-con {
line-height: 42px;
user-select: none;
}
.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;
.c-weektime-time {
text-align: left;
line-height: 2.4em;
p {
max-width: 625px;
line-height: 1.4em;
word-break: break-all;
margin-bottom: 8px;
}
}
}
}
.tips {
width: 100%;
line-height: 30px;
.c-min-table {
tr, td, th {
min-width: 24px;
}
}
.g-clearfix {
&:after, &:before {
clear: both;
content: " ";
display: table;
}
}
.g-pull-left {
float: left;
}
.g-pull-right {
float: right;
color: #409eff!important;
}
.g-pull-margin {
margin-right: 12px;
}
.g-tip-text {
color: #999;
}
</style>

View File

@@ -7,6 +7,8 @@ import UploadField from './UploadField'
import UploadKey from './UploadKey'
import UserPassword from './UserPassword'
import WeekCronSelect from './WeekCronSelect'
import UpdateToken from './UpdateToken'
import JsonEditor from './JsonEditor'
export default {
DatetimeRangePicker,
@@ -17,7 +19,9 @@ export default {
UploadKey,
UploadField,
UserPassword,
WeekCronSelect
WeekCronSelect,
UpdateToken,
JsonEditor
}
export {
@@ -29,5 +33,7 @@ export {
UploadKey,
UploadField,
UserPassword,
WeekCronSelect
WeekCronSelect,
UpdateToken,
JsonEditor
}

View File

@@ -1,20 +1,24 @@
<template>
<div class="form-group-header">
<div v-if="line" class="hr-line-dashed" />
<h3>{{ title }}</h3>
<h3>{{ group.title }}</h3>
</div>
</template>
<script>
export default {
props: {
title: {
type: String,
default: 'Title'
},
line: {
type: Boolean,
default: true
},
index: {
type: Number,
default: 1
},
group: {
type: Object,
default: () => ({})
}
}
}

View File

@@ -27,7 +27,7 @@ export default {
vm.tableConfig.initialUrl = vm.tableConfig.url
}
const initialUrl = vm.tableConfig.initialUrl
const nodeId = node.meta.node.id
const nodeId = node.meta.data.id
const url = initialUrl.replace('/assets/', `/nodes/${nodeId}/assets/`)
vm.tableConfig.url = url
}

View File

@@ -1,6 +1,6 @@
<template>
<IBox v-bind="$attrs">
<div class="ibox-heading">
<div v-if="contentHeading" class="ibox-heading">
<slot name="content-heading">
<h3 v-if="contentHeading.title"><i v-if="contentHeading.fa" :class="'fa ' + contentHeading.fa" /> {{ contentHeading.title }}</h3>
<small v-if="contentHeading.content"><i class="fa fa-tim" /> {{ contentHeading.content }}</small>
@@ -18,7 +18,7 @@ export default {
props: {
contentHeading: {
type: Object,
default: () => ({})
default: null
}
}
}

View File

@@ -1,29 +1,45 @@
<template>
<Dialog v-if="showExportDialog" :title="$t('common.Export')" :visible.sync="showExportDialog" :destroy-on-close="true" @confirm="handleExportConfirm()" @cancel="handleExportCancel()">
<el-form label-position="left" style="padding-left: 50px">
<el-form-item :label="$t('common.fileType' )" :label-width="'100px'">
<el-radio-group v-model="exportTypeOption">
<el-radio v-for="option of exportTypeOptions" :key="option.value" style="padding: 10px 20px;" :label="option.value" :disabled="!option.can">{{ option.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item class="export-form" :label="this.$t('common.imExport.ExportRange')" :label-width="'100px'">
<el-radio-group v-model="exportOption">
<el-radio v-for="option of exportOptions" :key="option.value" class="export-item" :label="option.value" :disabled="!option.can">{{ option.label }}</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
</Dialog>
<div>
<MFAVerifyDialog
v-if="mfaDialogShow"
@MFAVerifyDone="showExportDialog"
@MFAVerifyCancel="handleExportCancel"
/>
<Dialog
v-if="exportDialogShow"
:title="$t('common.Export')"
:visible.sync="exportDialogShow"
:destroy-on-close="true"
@confirm="handleExportConfirm()"
@cancel="handleExportCancel()"
>
<el-form label-position="left" style="padding-left: 50px">
<el-form-item :label="$t('common.fileType' )" :label-width="'100px'">
<el-radio-group v-model="exportTypeOption">
<el-radio v-for="option of exportTypeOptions" :key="option.value" style="padding: 10px 20px;" :label="option.value" :disabled="!option.can">{{ option.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item class="export-form" :label="this.$t('common.imExport.ExportRange')" :label-width="'100px'">
<el-radio-group v-model="exportOption">
<el-radio v-for="option of exportOptions" :key="option.value" class="export-item" :label="option.value" :disabled="!option.can">{{ option.label }}</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
</Dialog>
</div>
</template>
<script>
import Dialog from '@/components/Dialog'
import MFAVerifyDialog from '@/components/MFAVerifyDialog'
import { createSourceIdCache } from '@/api/common'
import * as queryUtil from '@/components/DataTable/compenents/el-data-table/utils/query'
export default {
name: 'ExportDialog',
components: {
Dialog
Dialog,
MFAVerifyDialog
},
props: {
selectedRows: {
@@ -32,12 +48,20 @@ export default {
},
url: {
type: String,
default: () => ''
default: ''
},
beforeExport: {
type: Function,
default: () => {}
},
mfaVerifyRequired: {
type: Boolean,
default: false
},
performExport: {
type: Function,
default(selectedRows, exportOptions, query) {
return this.defaultPerformExport(selectedRows, exportOptions, query)
default(selectedRows, exportOptions, query, exportType) {
return this.defaultPerformExport(selectedRows, exportOptions, query, exportType)
}
},
canExportAll: {
@@ -55,10 +79,12 @@ export default {
},
data() {
return {
showExportDialog: false,
exportDialogShow: false,
exportOption: 'all',
exportTypeOption: 'csv',
meta: {}
meta: {},
mfaVerified: false,
mfaDialogShow: false
}
},
computed: {
@@ -115,20 +141,33 @@ export default {
}
},
mounted() {
this.$eventBus.$on('showExportDialog', ({ selectedRows, url }) => {
if (url === this.url) {
this.showExportDialog = true
this.$eventBus.$on('showExportDialog', ({ selectedRows, url, name }) => {
// Todo: 没有时间了,只能先这么处理了
if (url === this.url || url.indexOf(this.url) > -1 || url.indexOf('account') > -1) {
this.showExportDialog()
}
})
},
methods: {
showExportDialog() {
if (!this.mfaVerifyRequired) {
this.exportDialogShow = true
return
}
// 这是需要校验 MFA 的
if (!this.mfaDialogShow) {
this.mfaDialogShow = true
} else {
this.exportDialogShow = true
}
},
downloadCsv(url) {
const a = document.createElement('a')
a.href = url
a.click()
window.URL.revokeObjectURL(url)
},
async defaultPerformExport(selectRows, exportOption, q) {
async defaultPerformExport(selectRows, exportOption, q, exportTypeOption) {
const url = (process.env.VUE_APP_ENV === 'production') ? (`${this.url}`) : (`${process.env.VUE_APP_BASE_API}${this.url}`)
const query = Object.assign({}, q)
if (exportOption === 'selected') {
@@ -139,13 +178,8 @@ export default {
}
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
query['format'] = exportTypeOption
const queryStr =
(url.indexOf('?') > -1 ? '&' : '?') +
queryUtil.stringify(query, '=', '&')
@@ -153,17 +187,20 @@ export default {
},
async handleExport() {
const listTableRef = this.$parent.$parent.$parent.$parent
const query = listTableRef.dataTable.getQuery()
const query = listTableRef['dataTable'].getQuery()
delete query['limit']
delete query['offset']
return this.performExport(this.selectedRows, this.exportOption, query)
await this.beforeExport()
return this.performExport(this.selectedRows, this.exportOption, query, this.exportTypeOption)
},
async handleExportConfirm() {
await this.handleExport()
this.showExportDialog = false
this.exportDialogShow = false
this.mfaDialogShow = false
},
handleExportCancel() {
this.showExportDialog = false
this.exportDialogShow = false
this.mfaDialogShow = false
}
}
}

View File

@@ -1,7 +1,7 @@
<template>
<div>
<ExportDialog :selected-rows="selectedRows" :url="url" v-bind="$attrs" v-on="$listeners" />
<ImportDialog :selected-rows="selectedRows" :url="url" v-bind="$attrs" v-on="$listeners" />
<ExportDialog :selected-rows="selectedRows" v-bind="exportOptions" v-on="$listeners" />
<ImportDialog :selected-rows="selectedRows" v-bind="importOptions" v-on="$listeners" />
</div>
</template>
@@ -10,7 +10,7 @@ import ExportDialog from './ExportDialog'
import ImportDialog from './ImportDialog'
export default {
name: 'DialogAction',
name: 'ImExportDialog',
components: {
ExportDialog,
ImportDialog
@@ -20,9 +20,13 @@ export default {
type: Array,
default: () => []
},
url: {
type: String,
default: () => ''
exportOptions: {
type: Object,
default: () => ({})
},
importOptions: {
type: Object,
default: () => ({})
}
},
data() {

View File

@@ -33,7 +33,7 @@
<script>
import DataTable from '@/components/DataTable'
import { sleep } from '@/utils/common'
import { sleep, getUpdateObjURL } from '@/utils/common'
import { EditableInputFormatter, StatusFormatter } from '@/components/TableFormatters'
export default {
name: 'ImportTable',
@@ -350,7 +350,7 @@ export default {
}
},
async performUpdateObject(item) {
const updateUrl = `${this.url}${item.id}/`
const updateUrl = getUpdateObjURL(this.url, item.id)
return this.$axios.put(
updateUrl,
item,
@@ -421,6 +421,7 @@ export default {
.importTable >>> .cell {
min-height: 20px;
height: 100%;
overflow: auto;
}
</style>

View File

@@ -24,6 +24,16 @@ export default {
hasLeftActions: defaultTrue,
hasCreate: defaultTrue,
canCreate: defaultTrue,
createRoute: {
type: [String, Object, Function],
default: function() {
return this.$route.name.replace('List', 'Create')
}
},
createInNewPage: {
type: Boolean,
default: false
},
hasBulkDelete: defaultTrue,
hasBulkUpdate: defaultFalse,
hasMoreActions: defaultTrue,
@@ -31,12 +41,6 @@ export default {
type: String,
default: ''
},
createRoute: {
type: [String, Object, Function],
default: function() {
return this.$route.name.replace('List', 'Create')
}
},
reloadTable: {
type: Function,
default: () => {}
@@ -158,8 +162,13 @@ export default {
} else if (typeof this.createRoute === 'object') {
route = this.createRoute
}
this.$router.push(route)
this.$log.debug('handle create')
if (this.createInNewPage) {
const { href } = this.$router.resolve(route)
window.open(href, '_blank')
} else {
this.$router.push(route)
}
},
defaultBulkDeleteCallback({ selectedRows, reloadTable }) {
const msg = this.$t('common.deleteWarningMsg') + ' ' + selectedRows.length + ' ' + this.$t('common.rows') + ' ?'

View File

@@ -1,7 +1,12 @@
<template>
<div>
<ActionsGroup :is-fa="true" :actions="rightSideActions" :url="tableUrl" class="right-side-actions right-side-item" />
<ImExportDialog :selected-rows="selectedRows" :url="tableUrl" v-bind="$attrs" />
<ActionsGroup :is-fa="true" :actions="rightSideActions" class="right-side-actions right-side-item" />
<ImExportDialog
:selected-rows="selectedRows"
:export-options="iExportOptions"
:import-options="iImportOptions"
v-bind="$attrs"
/>
</div>
</template>
@@ -9,6 +14,7 @@
import ActionsGroup from '@/components/ActionsGroup'
import ImExportDialog from './ImExportDialog'
import { cleanActions } from './utils'
import { assignIfNot } from '@/utils/common'
const defaultTrue = { type: Boolean, default: true }
@@ -24,27 +30,41 @@ export default {
default: ''
},
hasExport: defaultTrue,
handleExport: {
exportOptions: {
type: Object,
default: () => ({})
},
handleExportClick: {
type: Function,
default: function({ selectedRows }) {
this.$eventBus.$emit('showExportDialog', { selectedRows, url: this.tableUrl })
this.$eventBus.$emit('showExportDialog', { selectedRows, url: this.tableUrl, name: this.name })
}
},
hasImport: defaultTrue,
handleImport: {
importOptions: {
type: Object,
default: () => ({})
},
handleImportClick: {
type: Function,
default: function({ selectedRows }) {
this.$eventBus.$emit('showImportDialog', { selectedRows, url: this.tableUrl })
this.$eventBus.$emit('showImportDialog', { selectedRows, url: this.tableUrl, name: this.name })
}
},
hasColumnSetting: defaultTrue,
handleColumnConfig: {
handleTableSettingClick: {
type: Function,
default: function({ selectedRows }) {
this.$eventBus.$emit('showColumnSettingPopover', { url: this.tableUrl })
this.$eventBus.$emit('showColumnSettingPopover', { url: this.tableUrl, row: selectedRows, name: this.name })
}
},
hasRefresh: defaultTrue,
handleRefreshClick: {
type: Function,
default: function() {
this.reloadTable()
}
},
selectedRows: {
type: Array,
default: () => []
@@ -61,13 +81,12 @@ export default {
data() {
return {
defaultRightSideActions: [
{ name: 'actionColumnSetting', fa: 'fa-cog', tip: this.$t('common.CustomCol'), has: this.hasColumnSetting, callback: this.handleColumnConfig.bind(this) },
{ name: 'actionExport', fa: 'fa-download', tip: this.$t('common.Export'), has: this.hasExport, callback: this.handleExport.bind(this) },
{ name: 'actionImport', fa: 'fa-upload', tip: this.$t('common.Import'), has: this.hasImport, callback: this.handleImport.bind(this) },
{ name: 'actionRefresh', fa: 'fa-refresh', tip: this.$t('common.Refresh'), has: this.hasRefresh, callback: this.handleRefresh }
{ name: 'actionColumnSetting', fa: 'fa-cog', tip: this.$t('common.CustomCol'), has: this.hasColumnSetting, callback: this.handleTableSettingClick.bind(this) },
{ name: 'actionImport', fa: 'fa-upload', tip: this.$t('common.Import'), has: this.hasImport, callback: this.handleImportClick.bind(this) },
{ name: 'actionExport', fa: 'fa-download', tip: this.$t('common.Export'), has: this.hasExport, callback: this.handleExportClick.bind(this) },
{ name: 'actionRefresh', fa: 'fa-refresh', tip: this.$t('common.Refresh'), has: this.hasRefresh, callback: this.handleRefreshClick.bind(this) }
],
dialogExportVisible: false,
exportValue: 2
dialogExportVisible: false
}
},
computed: {
@@ -81,20 +100,18 @@ export default {
},
hasSelectedRows() {
return this.selectedRows.length > 0
},
iImportOptions() {
return assignIfNot(this.importOptions, { url: this.tableUrl })
},
iExportOptions() {
const options = assignIfNot(this.exportOptions, { url: this.tableUrl })
return options
}
},
methods: {
handleTagSearch(val) {
this.searchTable(val)
},
// handleExport({ selectedRows }) {
// this.$eventBus.$emit('showExportDialog', { selectedRows })
// },
// handleImport({ selectedRows }) {
// this.$eventBus.$emit('showImportDialog', { selectedRows })
// },
handleRefresh() {
this.reloadTable()
}
}
}

View File

@@ -1,8 +1,21 @@
<template>
<div>
<TableAction :table-url="iTableConfig.url" :search-table="search" :date-pick="handleDateChange" v-bind="headerActions" :selected-rows="selectedRows" :reload-table="reloadTable" />
<TableAction
:table-url="tableUrl"
:search-table="search"
:date-pick="handleDateChange"
:selected-rows="selectedRows"
:reload-table="reloadTable"
v-bind="headerActions"
/>
<IBox class="table-content">
<AutoDataTable ref="dataTable" :filter-table="filter" :config="iTableConfig" @selection-change="handleSelectionChange" v-on="$listeners" />
<AutoDataTable
ref="dataTable"
:filter-table="filter"
:config="iTableConfig"
@selection-change="handleSelectionChange"
v-on="$listeners"
/>
</IBox>
</div>
</template>
@@ -50,6 +63,9 @@ export default {
this.$log.debug('Header actions', this.headerActions)
this.$log.debug('ListTable: iTableConfig change', config)
return config
},
tableUrl() {
return this.iTableConfig.url
}
},
watch: {
@@ -75,22 +91,32 @@ export default {
},
search(attrs) {
this.$emit('TagSearch', attrs)
return this.dataTable.search(attrs, true)
return this.dataTable?.search(attrs, true)
},
filter(attrs) {
this.$emit('TagFilter', attrs)
this.$refs.dataTable.$refs.dataTable.search(attrs, true)
},
handleDateChange(attrs) {
this.$set(this.extraQuery, 'date_from', attrs[0].toISOString())
this.$set(this.extraQuery, 'date_to', attrs[1].toISOString())
// this.extraQuery = {
// date_from: attrs[0].toISOString(),
// date_to: attrs[1].toISOString()
// }
let dateFrom = ''
let dateTo = ''
try {
dateFrom = attrs[0].toISOString()
dateTo = attrs[1].toISOString()
} catch (e) {
this.$log.error('Handle date change error: ', attrs)
dateFrom = new Date()
dateFrom.setDate(dateFrom.getDate() - 5)
dateFrom = dateFrom.toISOString()
dateTo = new Date()
dateTo.setDate(dateTo.getDate() + 1)
dateTo = dateTo.toISOString()
}
this.$set(this.extraQuery, 'date_from', dateFrom)
this.$set(this.extraQuery, 'date_to', dateTo)
const query = {
date_from: attrs[0].toISOString(),
date_to: attrs[1].toISOString()
date_from: dateFrom,
date_to: dateTo
}
this.$emit('TagDateChange', attrs)
return this.dataTable.searchDate(query)
@@ -113,15 +139,6 @@ export default {
& >>> .el-table__header thead > tr > th {
background-color: white;
}
/*& >>> .el-table--striped .el-table__body tr.el-table__row--striped td {*/
/*background: white;*/
/*}*/
/*& >>> .el-table th, .el-table tr {*/
/*background-color: red;*/
/*!*background-color: #FAFAFA;*!*/
/*}*/
}
//修改颜色

View File

@@ -0,0 +1,77 @@
<template>
<Dialog
:title="$t('common.MFAVerify')"
:width="'50'"
:show-confirm="false"
:show-cancel="false"
:visible.sync="visible"
:destroy-on-close="true"
v-bind="$attrs"
v-on="$listeners"
>
<el-row :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="MFAToken" />
<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="verifyMFA">
{{ this.$t('common.Confirm') }}
</el-button>
</el-col>
</el-row>
</Dialog>
</template>
<script>
import Dialog from '@/components/Dialog'
export default {
name: 'MFAVerifyDialog',
components: {
Dialog
},
data() {
return {
MFAToken: '',
visible: false
}
},
watch: {
visible(val) {
if (!val) {
this.$emit('MFAVerifyCancel', true)
}
}
},
mounted() {
this.$axios.get('/api/v1/authentication/otp/verify/', { disableFlashErrorMsg: true }).then(() => {
this.$emit('MFAVerifyDone', true)
}).catch(err => {
this.$log.debug('Verify otp code error: ', err)
this.visible = true
})
},
methods: {
verifyMFA() {
if (this.MFAToken.length !== 6) {
return this.$message.error(this.$tc('common.MFAErrorMsg'))
}
this.$axios.post(
`/api/v1/authentication/otp/verify/`, {
code: this.MFAToken
}
).then(res => {
this.$emit('MFAVerifyDone', true)
})
}
}
}
</script>
<style scoped>
</style>

View File

@@ -1,5 +1,5 @@
<template>
<IBox fa="fa-edit" :title="title" v-bind="$attrs">
<IBox :fa="fa" :title="title" v-bind="$attrs">
<div v-for="action of actions" :key="action.title" class="quick-actions">
<table>
<ActionItem v-if="action.has === undefined || action.has" :action="action" />
@@ -20,6 +20,10 @@ export default {
ActionItem
},
props: {
fa: {
type: String,
default: () => 'fa-edit'
},
title: {
type: String,
default() {

View File

@@ -2,15 +2,11 @@
<el-card shadow="never">
<div slot="header" class="summary-header">
<span class="header-title">{{ title }}</span>
<span class="pull-right right-side">
<slot name="header-right">
<el-tag :type="rightSideLabel.type || 'success'" effect="dark" size="mini">{{ rightSideLabel.title }}</el-tag>
</slot>
</span>
</div>
<slot>
<h1 class="no-margins">
<router-link :to="body.route">
<span v-if="body.disabled" class="disabled-link">{{ body.count }}</span>
<router-link v-else :to="body.route">
<span>{{ body.count }}</span>
</router-link>
</h1>
@@ -74,4 +70,8 @@ export default {
.no-margins {
margin: 0 !important;
}
.disabled-link {
color: #428bca;
}
</style>

View File

@@ -8,9 +8,12 @@ import BaseFormatter from './base'
const defaultPerformDelete = function({ row, col }) {
const id = row.id
const url = `${this.url}${id}/`
return this.$axios.delete(url)
const url = new URL(this.url, location.origin)
url.pathname += `${id}/`
const deleteUrl = url.href
return this.$axios.delete(deleteUrl)
}
const defaultUpdateCallback = function({ row, col }) {
const id = row.id
let route = { params: { id: id }}

View File

@@ -5,7 +5,7 @@
<script>
import BaseFormatter from './base'
export default {
name: 'ChoicesFormatter',
name: 'BooleanFormatter',
extends: BaseFormatter,
props: {
formatterArgsDefault: {
@@ -16,6 +16,7 @@ export default {
true: 'fa-check text-primary',
false: 'fa-times text-danger'
},
showFalse: true,
typeChange(val) {
return !!val
}
@@ -31,6 +32,9 @@ export default {
computed: {
iconClass() {
const key = this.formatterArgs.typeChange(this.cellValue)
if (!key && !this.formatterArgs.showFalse) {
return ''
}
return this.formatterArgs.iconChoices[key]
}
}

View File

@@ -1,16 +1,15 @@
<template>
<div>
<el-tooltip v-if="formatterArgs.hasTips" placement="bottom" effect="dark">
<div slot="content">{{ tipStatus }}<br>{{ tipTime }}</div>
<i :class="'fa ' + iconClass" />
</el-tooltip>
<i v-else :class="'fa ' + iconClass" />
</div>
<el-tooltip v-if="shown" :disabled="!formatterArgs.hasTips" placement="bottom" effect="dark">
<div slot="content" v-html="tips" />
<span :class="classes">
<i v-if="formatterArgs.useIcon" :class="'fa ' + icon" />
<span v-if="formatterArgs.useText">{{ text }}</span>
</span>
</el-tooltip>
</template>
<script>
import BaseFormatter from './base'
import { toSafeLocalDateStr } from '@/utils/common'
export default {
name: 'ChoicesFormatter',
extends: BaseFormatter,
@@ -20,24 +19,26 @@ export default {
default() {
return {
iconChoices: {
true: 'fa-check text-primary',
false: 'fa-times text-danger'
true: 'fa-check',
false: 'fa-times'
},
typeChange(val) {
return !!val
classChoices: {
true: 'text-primary',
false: 'text-danger'
},
textChoices: {
true: this.$t('common.Yes'),
false: this.$t('common.No')
},
getKey({ row, cellValue }) {
return cellValue
},
hasTips: false,
tipStatus(val, vm) {
if (!val) {
return vm.$t('assets.Unknown')
}
if (val.status === 0) {
return vm.$t('assets.Unreachable')
} else if (val.status === 1) {
return vm.$t('assets.Reachable')
} else if (val.status === 2) {
return vm.$t('assets.Unknown')
}
useIcon: true,
useText: false,
showFalse: true,
getTips: ({ row, cellValue }) => {
return cellValue
}
}
}
@@ -49,19 +50,28 @@ export default {
}
},
computed: {
iconClass() {
const key = this.formatterArgs.typeChange(this.cellValue)
return this.formatterArgs.iconChoices[key]
key() {
return this.formatterArgs.getKey(
{ row: this.row, cellValue: this.cellValue }
)
},
tipStatus() {
const vm = this
return this.formatterArgs.tipStatus(this.cellValue, vm)
icon() {
return this.formatterArgs.iconChoices[this.key]
},
tipTime() {
if (!this.cellValue) {
return ''
classes() {
return this.formatterArgs.classChoices[this.key]
},
text() {
return this.formatterArgs.textChoices[this.key]
},
tips() {
return this.formatterArgs.getTips({ cellValue: this.cellValue, row: this.row })
},
shown() {
if (!this.formatterArgs.showFalse && !this.key) {
return false
}
return toSafeLocalDateStr(this.cellValue.datetime)
return true
}
}
}

View File

@@ -20,7 +20,7 @@ export default {
defaultOnDelete(col, row, cellValue, reload) {
const url = col.deleteUrl + cellValue
this.$axios.delete(url).then(res => {
this.$message.success(this.$t('common.deleteSuccessMsg'))
this.$message.success(this.$tc('common.deleteSuccessMsg'))
reload()
}).catch(error => {
this.$message.error(this.$t('common.deleteErrorMsg') + ' ' + error)

View File

@@ -25,8 +25,16 @@ export default {
data() {
const formatterArgs = Object.assign(this.formatterArgsDefault, this.col.formatterArgs)
return {
formatterArgs: formatterArgs,
iTitle: formatterArgs.getTitle({ col: this.col, row: this.row, cellValue: this.cellValue })
formatterArgs: formatterArgs
}
},
computed: {
iTitle() {
return this.formatterArgs.getTitle({
col: this.col,
row: this.row,
cellValue: this.cellValue
})
}
},
methods: {
@@ -63,6 +71,11 @@ export default {
<style scoped>
.detail {
display: inline-block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 400;
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<span>{{ display }}</span>
<span :class="cls"> {{ value }}</span>
</template>
<script>
@@ -19,19 +19,27 @@ export default {
},
data() {
return {
display: this.getValue()
formatterArgs: Object.assign(this.formatterArgsDefault, this.col.formatterArgs)
}
},
methods: {
getValue() {
const formatterArgs = Object.assign(this.formatterArgsDefault, this.col.formatterArgs)
const displayKey = formatterArgs.displayKey
computed: {
value() {
const displayKey = this.formatterArgs.displayKey
let value = this.row[displayKey]
if (value === undefined) {
value = this.row[this.col.prop]
}
return value
},
cls() {
const classChoices = this.formatterArgs?.classChoices
if (!classChoices) {
return ''
}
return classChoices[this.cellValue]
}
},
methods: {
}
}
</script>

View File

@@ -0,0 +1,50 @@
<template>
<div>
<el-tag
v-for="tag of cellValue"
:key="tag"
:type="getTagType(tag)"
class="tag-formatter"
disable-transitions
>
<i class="fa fa-tag" /> {{ tag }}
</el-tag>
</div>
</template>
<script>
import BaseFormatter from './base'
export default {
name: 'TagsFormatter',
extends: BaseFormatter,
props: {
formatterArgsDefault: {
type: Object,
default() {
return {
getTagType(tag) {
return 'primary'
}
}
}
}
},
data() {
return {
formatterArgs: Object.assign(this.formatterArgsDefault, this.col.formatterArgs)
}
},
methods: {
getTagType(tag) {
return this.formatterArgs.getTagType(tag)
}
}
}
</script>
<style scoped>
.tag-formatter {
margin: 2px 0;
}
</style>

View File

@@ -1,7 +1,6 @@
import DetailFormatter from './DetailFormatter'
import ArrayFormatter from './ArrayFormatter'
import DisplayFormatter from './DisplayFormatter'
import BooleanFormatter from './BooleanFormatter'
import ChoicesFormatter from './ChoicesFormatter'
import ActionsFormatter from './ActionsFormatter'
import DeleteActionFormatter from './DeleteActionFormatter'
@@ -11,11 +10,11 @@ import ShowKeyFormatter from '@/components/TableFormatters/ShowKeyFormatter'
import DialogDetailFormatter from './DialogDetailFormatter'
import EditableInputFormatter from './EditableInputFormatter'
import StatusFormatter from './StatusFormatter'
import TagsFormatter from './TagsFormatter'
export default {
DetailFormatter,
DisplayFormatter,
BooleanFormatter,
ChoicesFormatter,
ActionsFormatter,
DeleteActionFormatter,
@@ -25,13 +24,13 @@ export default {
DialogDetailFormatter,
ArrayFormatter,
EditableInputFormatter,
StatusFormatter
StatusFormatter,
TagsFormatter
}
export {
DetailFormatter,
DisplayFormatter,
BooleanFormatter,
ChoicesFormatter,
ActionsFormatter,
DeleteActionFormatter,
@@ -41,5 +40,6 @@ export {
DialogDetailFormatter,
ArrayFormatter,
EditableInputFormatter,
StatusFormatter
StatusFormatter,
TagsFormatter
}

View File

@@ -1,7 +1,13 @@
<template>
<div class="filter-field">
<el-cascader ref="Cascade" :options="options" :props="config" @change="handleMenuItemChange" />
<el-cascader
v-show="options.length > 0"
ref="Cascade"
:options="options"
:props="config"
@change="handleMenuItemChange"
/>
<el-tag
v-for="(v, k) in filterTags"
:key="k"
@@ -24,6 +30,7 @@
v-model="filterValue"
:placeholder="placeholder"
class="search-input"
:class="options.length < 1 ? 'search-input2': ''"
@blur="focus = false"
@focus="focus = true"
@change="handleConfirm"
@@ -44,6 +51,10 @@ export default {
type: Array,
default: () => []
},
getUrlQuery: {
type: Boolean,
default: () => true
},
default: {
type: Object,
default: null
@@ -93,6 +104,25 @@ export default {
// },
// deep: true
// }
options: {
handler(val) {
if (val && val.length > 0) {
const routeFilter = this.checkInTableColumns()
const asFilterTags = _.cloneDeep(this.filterTags)
this.filterTags = {
...asFilterTags,
...routeFilter
}
if (Object.keys(routeFilter).length > 0) {
setTimeout(() => {
return this.$emit('tagSearch', this.filterMaps)
}, 490)
}
}
},
immediate: true,
deep: true
}
},
mounted() {
setTimeout(() => {
@@ -104,6 +134,39 @@ export default {
// this.$nextTick(() => this.$emit('tagSearch', this.filterMaps))
},
methods: {
// 判断url中的查询条件
checkInTableColumns() {
const routeQuery = this.getUrlQuery ? this.$route?.query : {}
const routeQueryKeys = Object.keys(routeQuery)
const keys = {}
if (routeQueryKeys.length < 1) return keys
if (routeQueryKeys.length > 0) {
for (let i = 0; i < routeQueryKeys.length; i++) {
const key = routeQueryKeys[i]
const valueDecode = decodeURI(routeQuery[key])
const isSearch = key !== 'search'
for (let k = 0, len = this.options.length; k < len; k++) {
const cur = this.options[k]
if (key === cur.value || !isSearch) {
const curChildren = cur.children || []
keys[key] = {
key,
label: isSearch ? cur.label : '',
value: valueDecode
}
if (isSearch && curChildren.length > 0) {
curChildren.forEach(item => {
if (valueDecode === item.value) {
keys[key].valueLabel = item.label
}
})
}
}
}
}
}
return keys
},
getValueLabel(key, value) {
for (const field of this.options) {
if (field.value !== key) {
@@ -136,6 +199,7 @@ export default {
this.$nextTick(() => this.$refs.Cascade.handleClear())
},
handleTagClose(evt) {
this.checkUrlFilds(evt)
this.$delete(this.filterTags, evt)
this.$emit('tagSearch', this.filterMaps)
return true
@@ -147,6 +211,13 @@ export default {
const tag = { key: this.filterKey, label: this.keyLabel, value: this.filterValue, valueLabel: this.valueLabel }
this.$set(this.filterTags, this.filterKey, tag)
this.$emit('tagSearch', this.filterMaps)
if (this.getUrlQuery) {
let newQuery = _.cloneDeep(this.$route.query)
newQuery = { ...newQuery, [this.filterKey]: encodeURI(this.filterValue) }
this.$router.replace({ query: newQuery })
}
this.filterKey = ''
this.filterValue = ''
this.valueLabel = ''
@@ -173,6 +244,11 @@ export default {
this.filterKey = v.key
this.filterValue = v.value
this.$refs.SearchInput.focus()
},
// 删除查询条件时改变url
checkUrlFilds(evt) {
const newQuery = _.omit(this.$route.query, evt)
this.$router.replace({ query: newQuery })
}
}
}
@@ -182,10 +258,14 @@ export default {
.filter-field {
display: flex;
align-items: center;
min-width: 198px;
border: 1px solid #dcdee2;
border-radius: 3px;
background-color:#fff;
}
.search-input2 >>> .el-input__inner {
text-indent: 5px;
}
.search-input >>> .el-input__inner {
/*max-width:inherit !important;*/
max-width: 200px;

View File

@@ -77,9 +77,9 @@ export default {
}
},
methods: {
handleUrlChange(_url) {
this.$set(this.iTableConfig, 'url', _url)
this.$emit('urlChange', _url)
handleUrlChange(url) {
this.$set(this.iTableConfig, 'url', url)
this.$emit('urlChange', url)
this.forceRerender()
},
forceRerender() {

View File

@@ -22,5 +22,9 @@ export { default as QuickActions } from './QuickActions'
export { default as Switcher } from './FormFields/Swicher'
export { default as SummaryCard } from './SummaryCard'
export { default as UploadField } from './FormFields/UploadField'
export { default as AssetUserTable } from './AssetUserTable'
export { default as AccountListTable } from './AccountListTable/index'
export { default as AppAccountListTable } from './AppAccountListTable'
export { default as AssetRelationCard } from './AssetRelationCard'
export { default as MFAVerifyDialog } from './MFAVerifyDialog'
export { default as Announcement } from './Announcement'
export { default as CronTab } from './CronTab'

View File

@@ -9,12 +9,15 @@
"name": "名称",
"username": "用户名",
"ip_group": "IP 组",
"ip_group_help_text": "格式为逗号分隔的字符串, * 表示匹配所有。例如: 192.168.10.1, 192.168.1.0/24, 10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64",
"action": "动作",
"Rules": "规则",
"Action": "动作",
"priority": "优先级",
"date_created": "创建时间",
"created_by": "创建者",
"asset": "资产信息",
"users":"用户信息",
"users":"用户",
"system_user": "系统用户",
"username_group":"用户名",
"hostname_group":"资产名",
@@ -25,13 +28,17 @@
"apply_login_asset": "申请登录资产",
"apply_login_system_user": "申请登录系统用户",
"apply_login_user": "申请登录用户",
"login_confirm_user": "登录复核 受理人",
"RuleDetail": "规则详情"
},
"applications": {
"": "",
"updateAccountMsg": "请更新系统用户的账号信息",
"associateApplication": "关联应用",
"RemoteApp": "远程应用",
"Database": "数据库",
"Cloud": "云",
"App": "应用",
"applicationsType": {
"chrome": "Chrome",
"mysql_workbench": "MySQL Workbench",
@@ -41,6 +48,7 @@
"oracle": "Oracle",
"postgresql": "PostgreSQL",
"mariadb": "MariaDB",
"sqlserver": "SQLServer",
"k8s": "Kubernetes"
},
"applicationsCategory": {
@@ -83,11 +91,15 @@
"DBInfo": "数据库信息"
},
"assets": {
"AppList": "应用列表",
"AssociateSystemUsers": "关联系统用户",
"AssociateAssets": "关联资产",
"AssociateNodes": "关联节点",
"Action": "动作",
"ActiveSelected": "激活所选",
"AdminUser": "管理用户",
"AdminUser": "特权用户",
"AdminUserDetail": "管理用户详情",
"AdminUserListHelpMessage": "管理用户是资产(被控服务器)上的 root,或拥有 NOPASSWD: ALL sudo 权限的用户 JumpServer 使用该用户来 `推送系统用户`、`获取资产硬件信息` 等。\n",
"AdminUserListHelpMessage": "<b>特权用户</b> 是资产已存在的, 并且拥有 高级权限 的系统用户, 如 root拥有 `NOPASSWD: ALL` sudo 权限的用户 JumpServer 使用该用户来 `推送系统用户`、`获取资产硬件信息` 等。",
"Asset": "资产",
"HardwareInfo": "硬件信息",
"AssetDetail": "资产详情",
@@ -98,11 +110,13 @@
"TestGatewayHelpMessage": "如果使用了nat端口映射请设置为ssh真实监听的端口",
"SshPort": "SSH 端口",
"AssetNumber": "资产编号",
"AssetUserList": "资产用户列表",
"AssetUserList": "账号列表",
"Assets": "资产",
"Auth": "认证",
"AccountList": "账号列表",
"AutoGenerateKey": "自动生成密钥",
"PermUserList": "授权用户",
"ViewPerm": "查看授权",
"AutoGenerateKey": "自动生成",
"AutoPush": "自动推送",
"BasePlatform": "基础平台",
"Basic": "基本",
@@ -115,6 +129,7 @@
"CommandFilterRules": "命令过滤器规则",
"Comment": "备注",
"Cpu": "CPU",
"CommonUser": "普通用户",
"CreatedBy": "创建者",
"Database": "数据库",
"DateJoined": "创建日期",
@@ -123,6 +138,7 @@
"Disk": "硬盘",
"AdDomain": "AD域名",
"AdDomainHelpText": "提供给域用户登录的AD域名",
"SetAdDomainNoDisabled": "如果设置了AD域名不能修改",
"Domain": "网域",
"DomainDetail": "网域详情",
"DomainHelpMessage": "网域功能是为了解决部分环境混合云无法直接连接而新增的功能原理是通过网关服务器进行跳转登录。JMS => 网域网关 => 目标资产",
@@ -137,12 +153,15 @@
"LoginModeHelpMessage": "如果选择手动登录模式,用户名和密码可以不填写",
"LoginModel": "登录模式",
"Memory": "内存",
"Meta": "数据",
"Meta": "数据",
"Model": "型号",
"Name": "名称",
"NoInputCommand": "未输入命令",
"NoSystemUserWasSelected": "未选择系统用户",
"Node": "节点",
"UserSwitch": "用户切换",
"SwitchToUser": "Su 用户",
"SwitchToUserListTips": "通过以下用户连接资产时,会使用当前系统用户登录再进行切换。",
"NodeInformation": "节点信息",
"OnlyLatestVersion": "仅最新版本",
"Os": "操作系统",
@@ -156,9 +175,11 @@
"PriorityHelpMessage": "1-100, 1最低优先级100最高优先级。授权多个用户时高优先级的系统用户将会作为默认登录用户",
"Protocol": "协议",
"Protocols": "协议组",
"LoginOption": "登录选项",
"PublicIp": "公网IP",
"Push": "推送",
"PushSystemUserNow": "推送系统用户",
"PushAllSystemUsersToAsset": "推送所有系统用户到资产",
"QuickUpdate": "快速更新",
"Reachable": "可连接",
"Unreachable": "不可连接",
@@ -178,15 +199,17 @@
"PasswordHelpMessage": "密码或密钥密码",
"SystemUser": "系统用户",
"SystemUserDetail": "系统用户详情",
"SystemUserListHelpMessage": "系统用户是 JumpServer 跳转登录资产时使用的用户,可以理解为登录资产用户,如 websadba`ssh web@some-host`,而不是使用某个用户的用户名跳转登录服务器(`ssh xiaoming@some-host` 简单来说是用户使用自己的用户名登录 JumpServerJumpServer 使用系统用户登录资产。 系统用户创建时,如果选择了自动推送,JumpServer 使用 Ansible 自动推送系统用户到资产中,如果资产(交换机)不支持 Ansible请手动填写账号密码。\n",
"SystemUserListHelpMessage": "<b>系统用户</b> 是JumpServer 登录资产时使用的账号,如 root `ssh root@host`,而不是使用该用户名登录资产ssh admin@host)`;<br><b>特权用户</b> 是资产已存在的, 并且拥有 高级权限 的系统用户, JumpServer 使用该用户来 `推送系统用户`、`获取资产硬件信息` 等;</br><b>普通用户</b> 可以在资产上预先存在,也可以由 特权用户 来自动创建。",
"DynamicUsername": "动态用户名",
"SystemUsers": "系统用户",
"Test": "测试",
"TestAssetsConnective": "测试资产可连接性",
"TestAllSystemUsersConnective": "测试所有系统用户可连接性",
"TestConnection": "测试连接",
"Type": "类型",
"UnselectedAssets": "未选择资产或所选择的资产不支持SSH协议连接",
"UnselectedNodes": "未选择节点",
"UpdateAssetUserToken": "更新资产用户认证信息",
"UpdateAssetUserToken": "更新账号认证信息",
"Username": "用户名",
"UsernameHelpMessage": "用户名是动态的,登录资产时使用当前用户的用户名登录",
"Value": "值",
@@ -197,6 +220,7 @@
"sshKeyFingerprint": "SSH 指纹",
"ip": "IP",
"sshkey": "sshkey",
"SSHKey": "SSH 密钥",
"GroupsHelpMessage": "请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)",
"HomeHelpMessage": "默认家目录 /home/系统用户名: /home/username",
"Home": "家目录",
@@ -220,6 +244,23 @@
"ReLogin": "重新登录"
},
"common": {
"PrivateCloud": "私有云",
"PublicCloud": "公有云",
"Correlation": "关联",
"UserLoginLimit": "用户登录限制",
"IPLoginLimit": "IP 登录限制",
"Setting": "设置",
"Certificate": "证书",
"ViewMore": "查看更多",
"Announcement": "公告",
"Logging": "日志记录",
"Database": "数据库记录",
"Params": "参数",
"BasicInfo": "基本信息",
"DateUpdated": "更新日期",
"ApprovaLevel": "审批信息",
"MFAVerify": "验证 MFA",
"ViewSecret": "查看密码",
"ConnectWebSocketError": "连接 WebSocket 失败",
"Action": "动作",
"RequestTickets": "申请工单",
@@ -238,12 +279,18 @@
"bind": "绑定",
"unbind": "解绑",
"PushSelected":"推送所选",
"PushSelectedSystemUsersToAsset": "推送所选系统用户到资产",
"TestSelected": "测试所选",
"TestSelectedSystemUsersConnective": "测试所选系统用户可连接性",
"BadRequestErrorMsg": "请求错误,请检查填写内容",
"BadRoleErrorMsg": "请求错误,无该操作权限",
"BadConflictErrorMsg": "正在刷新中,请稍后再试",
"Basic": "基本",
"PleaseAgreeToTheTerms": "请同意条款",
"BasicInfo": "基本信息",
"OpenId": "OpenId设置",
"Radius": "Radius设置",
"Cas": "Cas设置",
"other": "其它设置",
"ApplyInfo": "申请信息",
"Cancel": "取消",
"Close": "关闭",
@@ -303,6 +350,7 @@
"TestSuccessMsg": "测试成功",
"To": "至",
"Update": "更新",
"Revise": "修改",
"Upload": "上传",
"Clone": "克隆",
"Username": "用户名",
@@ -316,7 +364,8 @@
"bulkDeleteErrorMsg": "批量删除失败: ",
"bulkDeleteSuccessMsg": "批量删除成功",
"bulkRemoveErrorMsg": "批量移除失败: ",
"NeedAssetsAndSystemUserErrMsg": "请先选择授权的系统用户和资产",
"SelectAtLeastOneAssetOrNodeErrMsg": "资产或者节点至少选择一项",
"RequiredSystemUserErrMsg": "请先选择系统用户",
"bulkRemoveSuccessMsg": "批量移除成功",
"createBy": "创建者",
"cloneFrom": "克隆自",
@@ -395,7 +444,57 @@
"MIN_LENGTH_ERROR": "密码最小长度 {0} 位"
},
"lastCannotBeDeleteMsg": "最后一项,不能被删除",
"InvalidJson": "不是合法 JSON"
"InvalidJson": "不是合法 JSON",
"time_period": "时段",
"FormatError": "格式错误",
"WeekCronSelect": {
"Monday": "星期一",
"Tuesday": "星期二",
"Wednesday": "星期三",
"Thursday": "星期四",
"Friday": "星期五",
"Saturday": "星期六",
"Sunday": "星期天",
"ClearSelection": "清空选择",
"SelectAll": "全选",
"CanDragSelect": "可拖动鼠标选择时间段",
"WeekOrTime": "星期/时间"
},
"CronTab": {
"newCron": "生成 cron",
"timeExpression": "时间表达式",
"cronExpression": "crontab完整表达式",
"min": "分钟",
"hour": "小时",
"day": "日",
"month": "月",
"week": "周",
"wildcardsAllowed": "允许的通配符",
"from": "从",
"every": "每",
"appoint": "指定",
"manyChoose": "可多选",
"startEvery": "开始,每",
"executeOnce": "执行一次",
"lastDay": "本月最后一天",
"everyMonth": "每月",
"lastWorking": "最近的那个工作日",
"lastWeek": "本月最后一个星期",
"num": "号",
"cycleFromWeek": "周期从星期",
"the": "第",
"weekOf": "周的星期",
"Monday": "周一",
"Tuesday": "周二",
"Wednesday": "周三",
"Thursday": "周四",
"Friday": "周五",
"Saturday": "周六",
"Sunday": "周日",
"runningTimes": "最近5次运行时间",
"calculationResults": "cron 表达式错误",
"failedConditions": "没有达到条件的结果!"
}
},
"dashboard": {
"ActiveAsset": "近期被登录过",
@@ -487,6 +586,7 @@
"User": "用户",
"UserGroups": "用户组",
"PermName":"授权名称",
"Permissions": "授权列表",
"DatabaseAppPermission": "数据库授权",
"RemoteAppPermission": "远程应用授权",
"addApplicationToThisPermission": "添加应用",
@@ -509,6 +609,7 @@
"downloadFile": "下载文件",
"hostName": "主机名",
"isValid": "有效",
"fromTicket": "来自工单",
"isEffective": "起作用的",
"nodeCount": "节点数量",
"refreshFail": "刷新失败",
@@ -523,7 +624,7 @@
"systemUserCount": "系统用户数量",
"upDownload": "上传下载",
"uploadFile": "上传文件",
"clipboardCopyPaste":"复制粘贴",
"clipboardCopyPaste":"剪贴板复制粘贴",
"clipboardCopy":"剪切板复制",
"clipboardPaste":"剪切板粘贴",
"userCount": "用户数量",
@@ -532,10 +633,14 @@
},
"route": {
"": "",
"TicketFlow": "工单流",
"TicketFlowCreate": "创建审批流",
"TicketFlowUpdate": "更新审批流",
"Accounts": "账号管理",
"AssetAccount": "资产账号",
"ApplicationAccount": "应用账号",
"Ticket":"工单",
"SessionDetail": "会话详情",
"CommandConfirm": "命令复核",
"AdminUserCreate": "创建管理用户",
"AdminUserDetail": "管理用户详情",
@@ -545,6 +650,7 @@
"AssetCreate": "创建资产",
"AssetDetail": "资产详情",
"AssetList": "资产列表",
"Session": "会话",
"AssetPermission": "资产授权",
"AssetPermissionCreate": "创建资产授权规则",
"AssetPermissionDetail": "资产授权详情",
@@ -585,11 +691,11 @@
"KubernetesAppUpdate": "更新Kubernetes",
"Acl": "访问控制",
"UserAclList": "用户登录",
"UserAclCreate": "创建用户登录规则",
"UserLoginACLCreate": "创建用户登录规则",
"UserAclLists": "用户登录规则",
"UserAclUpdate": "更新用户登录规则",
"UserLoginACLUpdate": "更新用户登录规则",
"UserAclDetail": "用户登录规则详情",
"AssetAclList": "登录资产",
"AssetAclList": "资产登录",
"AssetAclCreate": "创建资产登录规则",
"AssetAclUpdate": "更新资产登录规则",
"AssetAclDetail": "资产登录规则详情",
@@ -629,7 +735,7 @@
"ApplicationPermissionUpdate": "更新应用授权规则",
"RemoteAppUpdate": "更新远程应用",
"ReplayStorageUpdate": "更新录像存储",
"SessionDetail": "会话详情",
"Detail": "详情",
"SessionOffline": "历史会话",
"SessionOnline": "在线会话",
"Sessions": "会话管理",
@@ -638,6 +744,7 @@
"SystemUserDetail": "系统用户详情",
"SystemUserList": "系统用户",
"SystemUserUpdate": "更新系统用户",
"AssetUserList": "资产用户",
"TaskDetail": "任务详情",
"TaskList": "任务列表",
"TaskMonitor": "任务监控",
@@ -645,6 +752,10 @@
"TicketDetail": "工单详情",
"TicketCreate": "创建工单",
"Tickets": "工单管理",
"Templates": "模版管理",
"TemplateDetail": "模版详情",
"TemplateCreate": "创建模版",
"TemplateUpdate": "更新模版",
"UserCreate": "创建用户",
"UserDetail": "用户详情",
"UserFirstLogin": "首次登录",
@@ -660,9 +771,14 @@
"WebFTP": "文件管理",
"WebTerminal": "Web终端",
"Notifications": "通知",
"SiteMessageList": "站内信"
"SiteMessageList": "站内信",
"UserLoginACL": "用户登录"
},
"sessions": {
"SetToDefaultStorage": "设置为默认存储",
"SetToDefault": "设为默认",
"SetSuccess": "设置成功",
"SetFailed": "设置失败",
"StorageConfiguration": "存储配置",
"accountKey": "账户密钥",
"accountName": "账户名称",
@@ -672,6 +788,7 @@
"target": "目标",
"bucket": "桶名称",
"command": "命令",
"Activity": "活动",
"commandStorage": "命令存储",
"comment": "备注",
"containerName": "容器名称",
@@ -735,6 +852,31 @@
}
},
"setting": {
"Feature": "功能",
"AlibabaCloud": "阿里云",
"TencentCloud": "腾讯云",
"Radius": "Radius",
"VerifySignTmpl": "验证码短信模板",
"Enable": "启用",
"AuthLimit": "登录限制",
"SMTP": "邮件服务器",
"Perm": "授权",
"Setting": "设置",
"Config": "配置",
"AuthMethod": "认证方式",
"SSO": "单点认证",
"AppAuth": "App认证",
"OtherAuth": "其它认证",
"Ops": "任务",
"OTP": "MFA (OTP)",
"JMSSSO": "SSO Token 登录",
"MessageSub": "消息订阅",
"Cleaning": "定期清理",
"Other": "其它设置",
"CASSetting": "CAS 配置",
"Auth": "认证设置",
"SyncSetting": "同步设置",
"Advanced": "高级设置",
"InsecureCommandNotifyToSubscription": "危险命令通知已升级到消息订阅中,支持更多通知方式",
"ApiKeyList": "API Key 列表",
"AssetCount": "资产数量",
@@ -743,15 +885,16 @@
"Create": "创建",
"Edition": "版本",
"Email": "邮件设置",
"EmailContent": "邮件内容设置",
"EmailContent": "邮件内容定制",
"Expired": "过期时间",
"Hostname": "主机名",
"ImportLicense": "导入许可证",
"ImportLicenseTip": "请导入许可证",
"Ldap": "LDAP设置",
"Ldap": "LDAP",
"License": "许可证",
"Senior": "高级",
"LicenseFile": "许可证文件",
"PasswordCheckRule": "密码校验规则",
"PasswordCheckRule": "密码强弱规则",
"Security": "安全设置",
"SecuritySetting": "安全设置",
"SystemMessageSubscription": "系统消息订阅",
@@ -765,13 +908,27 @@
"authLdapSearchFilter": "用户过滤器",
"authLdapSearchOu": "用户OU",
"authLdapServerUri": "LDAP地址",
"authLdapUserAttrMap": "LDAP属性映射",
"authLdapUserAttrMap": "用户属性映射",
"authUserAttrMap": "用户属性映射",
"authUserAttrMapHelpText": "映射关系 {idpsp}",
"SAML2": "SAML2",
"enableSAML2Auth": "开启 SAML2 认证",
"SAML2Auth": "SAML2 认证",
"authSAML2Xml": "IDP metadata XML",
"authSAML2MetadataUrl": "IDP metadata URL",
"authSAML2AdvancedSettings": "高级配置",
"IdpMetadataHelpText": "IDP metadata URL 和 IDP metadata XML参数二选一即可IDP metadata URL的优先级高",
"IdpMetadataUrlHelpText": "从远端地址中加载 IDP Metadata",
"authSAMLKeyHelpText": "SP 证书和密钥 是用来和 IDP 加密通信的",
"authSAMLCertHelpText": "上传证书密钥后保存, 然后查看 SP Metadata",
"authCASAttrMap": "用户属性映射",
"SignaturesAndTemplates": "Signatures and Templates",
"unselectedUser": "没有选择用户",
"auto": "自动",
"basicSetting": "基本设置",
"communityEdition": "社区版",
"consult": "咨询",
"createUserSetting": "创建用户设置",
"CreateUserSetting": "创建用户内容",
"emailCustomUserCreatedBody": "创建用户邮件的内容",
"emailCustomUserCreatedHonorific": "创建用户邮件的敬语",
"emailCustomUserCreatedSignature": "署名",
@@ -834,7 +991,7 @@
"import": "导入",
"importLdapUserTip": "请先提交LDAP配置再进行导入",
"importLdapUserTitle": "LDAP 用户列表",
"ldapBulkImport": "一键导入",
"ldapBulkImport": "用户导入",
"ldapConnectTest": "测试连接",
"ldapLoginTest": "测试登录",
"password": "密码",
@@ -874,12 +1031,29 @@
"DingTalk": "钉钉",
"dingTalkTest": "测试",
"weComTest": "测试",
"setting": "设置"
"FeiShu": "飞书",
"SMS": "短信设置",
"feiShuTest": "测试",
"setting": "设置",
"SMSProvider": "短信服务商"
},
"tickets": {
"OneAssigneeType": "一级受理人类型",
"OneAssignee": "一级受理人",
"TwoAssigneeType": "二级受理人类型",
"TwoAssignee": "二级受理人",
"SuperAdmin": "超级管理员",
"OrgAdmin": "组织管理员",
"SuperOrgAdmin": "超级管理员+组织管理员",
"CustomUser": "自定义用户",
"ApprovalLevel": "审批级别",
"FlowDetail": "流程详情",
"PermissionName": "授权规则名称",
"Accept": "同意",
"AssignedMe": "待我审批",
"FlowSetUp": "流程设置",
"ApprovalProcess": "审批流程",
"LevelApproval": "级审批",
"Assignee": "处理人",
"Assignees": "待处理人",
"Close": "关闭",
@@ -894,12 +1068,11 @@
"status": "状态",
"title": "标题",
"action": "动作",
"IPGroup": "IP 组",
"type": "类型",
"user": "用户",
"Status": "状态",
"Open": "待处理",
"OrgName":"组织名称",
"OrgName":"授权组织名称",
"AssignedInfo":"审批信息",
"OpenTicket": "创建工单",
"HandleTicket": "处理工单",
@@ -915,6 +1088,7 @@
"Approved": "已同意",
"Rejected": "已拒绝",
"Closed": "已完成",
"StateClosed": "已关闭",
"helpText": {
"ips": "请输入逗号分割的IP地址组",
"fuzzySearch": "支持模糊搜索",
@@ -941,6 +1115,9 @@
"UpdateNodeAssetHardwareInfo": "更新节点资产硬件信息"
},
"users": {
"FileEncryptionPassword": "文件加密密码",
"MessageSubscription": "消息订阅",
"AuthSettings": "认证配置",
"UserName": "姓名",
"Account": "账户",
"Authentication": "认证",
@@ -953,6 +1130,10 @@
"DatePasswordUpdated": "密码更新日期",
"DescribeOfGuide": "欢迎使用JumpServer堡垒机系统获取更多信息请点击",
"Email": "邮件",
"Phone": "手机号",
"WeCom": "企业微信",
"DingTalk": "钉钉",
"FeiShu": "飞书",
"FingerPrint": "指纹",
"FirstLogin": "首次登录",
"OrgUser": "组织用户",
@@ -964,6 +1145,7 @@
"Guide": "向导",
"setWeCom": "设置企业微信认证",
"setDingTalk": "设置钉钉认证",
"setFeiShu": "设置飞书认证",
"HelpText": {
"MFAOfUserFirstLoginPersonalInformationImprovementPage": "启用多因子认证,使账号更加安全。<br/> 启用之后您将会在下次登录时进入多因子认证绑定流程;您也可以在(个人信息->快速修改->更改多因子设置)中直接绑定!",
"MFAOfUserFirstLoginUserGuidePage": "为了保护您和公司的安全,请妥善保管您的账户、密码和密钥等重要敏感信息;(如:设置复杂密码,并启用多因子认证)",
@@ -1006,7 +1188,7 @@
"resetMFAWarningMsg": "你确定要重置用户的 MFA 吗?",
"resetMFAdSuccessMsg": "重置MFA成功, 用户可以重新设置MFA了",
"resetPassword": "重置密码",
"resetPasswordSuccessMsg": "发送邮件任务已提交, 用户稍后会收到重置密码邮件",
"resetPasswordSuccessMsg": "已向用户发送重置密码消息",
"resetPasswordWarningMsg": "你确定要发送重置用户密码的邮件吗",
"resetSSHKey": "重置SSH密钥",
"resetSSHKeySuccessMsg": "发送邮件任务已提交, 用户稍后会收到重置密钥邮件",
@@ -1055,23 +1237,49 @@
"HasRead": "是否已读",
"Sender": "发送人",
"MarkAsRead": "标记已读",
"OneClickRead": "当前已读",
"OneClickReadMsg": "你确定要将当前信息标记为已读吗?",
"NoUnreadMsg": "暂无未读消息",
"SiteMessage": "站内信"
"SiteMessage": "站内信",
"SMS": "短信"
},
"xpack": {
"NavHelp": "导航栏链接",
"helpDocument": "文档链接",
"helpSupport": "支持链接",
"officialWebsite": "官网链接",
"helpDocumentTip": "可以更改网站导航栏 帮助 -> 文档 的网址",
"helpSupportTip": "可以更改网站导航栏 帮助 -> 支持 的网址",
"officialWebsiteTip": "可以更改网站导航栏 帮助 -> 官网 的网址",
"Admin": "管理员",
"Asset": "资产",
"Database": "数据库",
"AssetCount": "资产数量",
"Auditor": "审计员",
"ChangeAuthPlan": {
"ContainAttachment": "含附件",
"AddAsset": "添加资产",
"MailRecipient": "邮件收件人",
"AddNode": "添加节点",
"AddSystemUser": "添加系统用户",
"Asset": "资产",
"Database": "数据库",
"DatabaseId": "数据库Id",
"AppAmount": "应用数量",
"SystemUserAmount": "系统用户数量",
"SystemUser": "系统用户",
"SystemUserId": "系统用户Id",
"AssetAmount": "资产数量",
"AssetAndNode": "资产和节点",
"ChangeAuthPlan": "改密计划",
"ChangeAuthPlanCreate": "创建改密计划",
"ChangeAuthPlanUpdate": "更新改密计划",
"AssetChangeAuthPlan": "资产改密计划",
"AppChangeAuthPlan": "应用改密计划",
"AssetChangeAuthPlanCreate": "创建资产改密计划",
"AppChangeAuthPlanCreate": "创建应用改密计划",
"AssetChangeAuthPlanUpdate": "更新资产改密计划",
"AppChangeAuthPlanUpdate": "更新应用改密计划",
"SymbolSet": "特殊符号集合",
"SymbolSetHelpText": "请输入此类型数据库支持的特殊符号集合,若生成的随机密码中有此类数据库不支持的特殊字符,改密计划将会失败",
"CyclePerform": "周期执行",
"DateJoined": "创建日期",
"DateStart": "开始日期",
@@ -1095,6 +1303,7 @@
"NodeAmount": "节点数量",
"PasswordLength": "密码长度",
"PasswordStrategy": "密码策略",
"SecretKeyStrategy": "密钥策略",
"RegularlyPerform": "定期执行",
"Result": "结果",
"Retry": "重试",
@@ -1102,11 +1311,18 @@
"TaskList": "任务列表",
"TimeDelta": "运行时间",
"Timer": "定时执行",
"TimerPeriod": "定时执行周期",
"Username": "用户名"
},
"Cloud": {
"ServerAccountKey": "服务账号密钥",
"IPNetworkSegment": "IP网段",
"Aliyun": "阿里云",
"Qcloud": "腾讯云",
"QingyunPrivatecloud": "青云私有云",
"HuaweiPrivatecloud": "华为私有云",
"OpenStack": "OpenStack",
"GCP": "谷歌云",
"AWS_China": "AWS(中国)",
"AWS_Int": "AWS(国际)",
"HuaweiCloud": "华为云",
@@ -1134,7 +1350,8 @@
"Name": "名称",
"Account":"账户",
"Node": "节点",
"AdminUser":"管理用户",
"WindowsAdminUser":"Windows 特权用户",
"LinuxAdminUser":"Linux 特权用户",
"Periodic":"执行周期",
"PeriodicPerform":"定时执行",
"RegularlyPerform": "定期执行",
@@ -1155,6 +1372,9 @@
"Log": "日志",
"DeleteReleasedAssets": "删除已释放资产"
},
"Template": {
"Template": "模版管理"
},
"Corporation": "公司",
"Edition": "版本",
"Execute": "执行",
@@ -1196,7 +1416,7 @@
"users_amount": "用户数量",
"groups_amount": "用户组数量",
"assets_amount": "资产数量",
"admin_users_amount": "管理用户数量",
"admin_users_amount": "特权用户数量",
"system_users_amount": "系统用户数量",
"applications_amount": "应用数量",
"asset_perms_amount": "资产授权数量",

View File

@@ -1,9 +1,9 @@
{
"": "",
"accounts": {
"PleaseClickLeftAssetToViewAssetAccount": "Asset account list, please click on the assets on the left to view",
"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"
"PleaseClickLeftAssetToViewGatheredUser": "Gathered user list, please click on the assets on the left to view"
},
"acl": {
"name": "Name",
@@ -18,9 +18,9 @@
"username_group":"Username group",
"hostname_group":"Hostname group",
"asset_ip_group": "Asset ip group",
"system_users_name_group": "Systemusers name group",
"system_users_protocol_group": "Systemusers protocol group",
"system_users_username_group": "systemusers username group",
"system_users_name_group": "Systemuser name",
"system_users_protocol_group": "Systemuser protocol",
"system_users_username_group": "systemuser username",
"apply_login_asset": "Apply login asset",
"apply_login_system_user": "Apply login system user",
"apply_login_user": "Apply login user",
@@ -28,9 +28,12 @@
},
"applications": {
"": "",
"updateAccountMsg": "Please update system user account info",
"associateApplication": "Associate application",
"RemoteApp": "Remote app",
"Database": "Database",
"Cloud": "Cloud",
"App": "Application",
"applicationsType": {
"chrome": "Chrome",
"mysql_workbench": "MySQL Workbench",
@@ -40,6 +43,7 @@
"oracle": "Oracle",
"postgresql": "PostgreSQL",
"mariadb": "MariaDB",
"sqlserver": "SQLServer",
"k8s": "kubernetes"
},
"applicationsCategory": {
@@ -82,16 +86,24 @@
"DBInfo": "Database Info"
},
"assets": {
"AppList": "Application list",
"AssociateSystemUsers": "Associate system users",
"AssociateAssets": "Associate assets",
"AssociateNodes": "Associate nodes",
"Action": "Action",
"ActiveSelected": "Active selected",
"AdminUser": "Admin user",
"ReplaceNodeAssetsAdminUser":"Replace node assets admin user with this",
"AdminUserDetail": "Admin user detail",
"DynamicUsername": "Dynamic username",
"AdminUserListHelpMessage": "Admin users are asset (charged server) on the root, or have NOPASSWD: ALL sudo permissions users, JumpServer users of the system using the user to `push system user`, `get assets hardware information`, etc.\n",
"Asset": "Asset",
"HardwareInfo": "Hardware info",
"Hardware": "Hardware",
"AccountList": "Account list",
"PermUserList": "Authorized user",
"ViewPerm": "View permission",
"LoginOption": "Login option",
"AssetDetail": "Asset detail",
"AssetList": "Asset list",
"AssetListHelpMessage": "The left side is the asset tree, right click to create, delete, and change the tree node, authorization asset is also organized as a node, and the right side is the asset under that node\n",
@@ -115,6 +127,7 @@
"CommandFilterRules": "Command filter rules",
"Comment": "Comment",
"Cpu": "Cpu",
"CommonUser": "Common user",
"CreatedBy": "Created by",
"Database": "Database",
"DateJoined": "Date joined",
@@ -123,6 +136,7 @@
"Disk": "Disk",
"AdDomain": "AD Domain",
"AdDomainHelpText": "AD domain provided to domain users for login",
"SetAdDomainNoDisabled": "If AD domain is set, it cannot be modified",
"Domain": "Domain",
"DomainDetail": "Domain detail",
"DomainHelpMessage": "The domain function is added to address the fact that some environments (such as the hybrid cloud) cannot be connected directly by jumping on the gateway server.\nJMS => Domain gateway => Target assets",
@@ -143,6 +157,9 @@
"NoInputCommand": "No input command",
"NoSystemUserWasSelected": "No system user was selected",
"Node": "Node",
"UserSwitch": "User switch",
"SwitchToUser": "Switch to user",
"SwitchToUserListTips": "When the following users are used to connect to assets, the current system user is used to log in and then switch.",
"NodeInformation": "Node information",
"OnlyLatestVersion": "Only latest version",
"Os": "Os",
@@ -158,6 +175,7 @@
"PublicIp": "Public ip",
"Push": "Push",
"PushSystemUserNow": "Push system user now",
"PushAllSystemUsersToAsset": "Push all system users to asset",
"QuickUpdate": "Quick update",
"Reachable": "Reachable",
"Unreachable": "Unreachable",
@@ -173,14 +191,15 @@
"Rules": "Rules",
"SFTPHelpMessage": "SFTP root dir, tmp, home or custom",
"SerialNumber": "Serial number",
"SudoHelpMessage": "Use comma split multi command, ex: /bin/whoami,/bin/ifconfig",
"SudoHelpMessage": "Use comma split multi command, ex: /bin/whoami, /bin/ifconfig",
"PasswordHelpMessage": "Password or private key password",
"SystemUser": "System user",
"SystemUserDetail": "System user detail",
"SystemUserListHelpMessage": "System user is JumpServer jump login assets used by the users, can be understood as the user login assets, such as web, sa, the dba (` ssh web@some-host `), rather than using a user the username login server jump (` ssh xiaoming@some-host `); In simple terms, users log into JumpServer using their own username, and JumpServer uses system users to log into assets. When system users are created, if you choose auto push JumpServer to use Ansible push system users into the asset, if the asset (Switch) does not support ansible, please manually fill in the account password.\n",
"SystemUserListHelpMessage": "<b>System user</b> is the account JumpServer used to log into the asset, such as using root `ssh root@host`, rather than the current user usernamessh admin@host)`;<br><b>Admin user</b> is the account that already exists on an asset, and have privileged permissions, JumpServer using this create common system user, and gather hardware Etc;</br><b>Common user</b> can pre-exist assets or created automatically by the admin user.",
"SystemUsers": "System users",
"Test": "Test",
"TestAssetsConnective": "Test assets connective",
"TestAllSystemUsersConnective": "Test all system users connective",
"TestConnection": "Test connection",
"Type": "Type",
"UnselectedAssets": "No asset selected or the selected asset does not support SSH protocol connection",
@@ -219,6 +238,20 @@
"ReLogin": "Re-Login"
},
"common": {
"PrivateCloud": "Private cloud",
"PublicCloud": "Public cloud",
"Correlation": "Correlation",
"UserLoginLimit": "User login limit",
"IPLoginLimit": "IP login limit",
"Setting": "Setting",
"Certificate": "Certificate",
"ViewMore": "View more",
"Announcement": "Announcement",
"Logging": "Logging",
"Database": "Database",
"Params": "Params",
"MFAVerify": "Verify MFA",
"ViewSecret": "View secret",
"ConnectWebSocketError": "Connect Websocket failed",
"Nothing": "Nothing",
"Action": "Action",
@@ -234,6 +267,9 @@
"Add": "Add",
"PleaseAgreeToTheTerms": "Please agree to the terms",
"PushSelected":"Push selected",
"PushSelectedSystemUsersToAsset": "Push selected system users to asset",
"TestSelected": "Test selected",
"TestSelectedSystemUsersConnective": "Test selected system users connective",
"UpdateAssetDetail": "Update more detail",
"AddSuccessMsg": "Add success",
"Auth": "Authorization",
@@ -250,7 +286,7 @@
"Confirm": "Confirm",
"Create": "Create",
"CreatedBy": "Created by",
"CrontabHelpTips": "eg: Every Sunday 03:05 run <5 3 * * 0> <br>Tips:Using 5 digits linux crontab expressions<min hour day month week> (<a href='https://tool.lu/crontab/' target='_blank'>Online tools</a>) <br>Note:If both Regularly perform and Cycle perform are set,give priority to Regularly perform",
"CrontabHelpTips": "eg: Every Sunday 03:05 run <5 3 * * 0> <br>Tips:Using 5 digits linux crontab expressions<min hour day month week> (<a href='https://tool.lu/crontab/' target='_blank'>Online tools</a>) <br>Note:If both Regularly perform and Cycle perform are set, give priority to Regularly perform",
"DateEnd": "End date",
"Resource": "Resource",
"DateLast24Hours": "Last 24 hours",
@@ -292,7 +328,7 @@
"RemoveSuccessMsg": "Remove success",
"Reset": "Reset",
"Search": "Search",
"MFAErrorMsg": "MFA Errorplease check",
"MFAErrorMsg": "MFA Error, please check",
"InputEmailAddress": "Please enter your email address",
"Select": "Select",
"SelectFile": "Select file",
@@ -303,6 +339,7 @@
"TestSuccessMsg": "Test Success",
"To": "To",
"Update": "Update",
"Revise": "Revise",
"bind": "Bind",
"unbind": "Unbind",
"Upload": "Upload",
@@ -318,7 +355,8 @@
"bulkDeleteSuccessMsg": "Bulk delete success",
"bulkRemoveErrorMsg": "Bulk remove failed: ",
"bulkRemoveSuccessMsg": "Bulk remove success",
"NeedAssetsAndSystemUserErrMsg": "Need assets and systemuser",
"SelectAtLeastOneAssetOrNodeErrMsg": "Select at least one asset or node",
"RequiredSystemUserErrMsg": "Required systemuser",
"createBy": "Create by",
"cloneFrom": "Clone from",
"createErrorMsg": "Create error",
@@ -393,7 +431,58 @@
"MIN_LENGTH_ERROR": "Password minimum length {}"
},
"lastCannotBeDeleteMsg": "The last one can't be delete",
"InvalidJson": "Not a valid json format"
"InvalidJson": "Not a valid json format",
"time_period": "Time period",
"CronTab": {
"newCron": "new cron",
"timeExpression": "time expression",
"cronExpression": "cron expression",
"min": "minute",
"hour": "hour",
"day": "day",
"month": "month",
"week": "week",
"wildcardsAllowed": "wildcards allowed",
"from": "from",
"every": "every",
"appoint": "appoint",
"manyChoose": "many choose",
"startEvery": " start, every",
"executeOnce": " execute once",
"lastDay": "last day of the month",
"everyMonth": "every month",
"lastWorking": "the last working day",
"lastWeek": "last week of the month",
"num": "number ",
"cycleFromWeek": "cycle from week",
"the": "the",
"weekOf": "week of week",
"Monday": "Monday",
"Tuesday": "Tuesday",
"Wednesday": "Wednesday",
"Thursday": "Thursday",
"Friday": "Friday",
"Saturday": "Saturday",
"Sunday": "Sunday",
"runningTimes": "last 5 running times",
"calculationResults": "calculationResults...",
"failedConditions": "cron expression error"
},
"Cycle": "Cycle",
"FormatError": "Format error",
"WeekCronSelect": {
"Monday": "Monday",
"Tuesday": "Tuesday",
"Wednesday": "Wednesday",
"Thursday": "Thursday",
"Friday": "Friday",
"Saturday": "Saturday",
"Sunday": "Sunday",
"ClearSelection": "Clear selection",
"SelectAll": "Select all",
"CanDragSelect": "Can drag the mouse to select a time period",
"WeekOrTime": "Week/Time"
}
},
"dashboard": {
"ActiveAsset": "Asset active",
@@ -474,6 +563,7 @@
"perms": {
"": "",
"Actions": "Permission",
"Permission": "Permissions",
"Asset": "Asset",
"Basic": "Basic",
"Exclude": "Exclude",
@@ -484,6 +574,7 @@
"SystemUser": "System user",
"User": "User",
"UserGroups": "UserGroups",
"Permissions": "Permissions",
"DatabaseAppPermission": "Databases permissions",
"RemoteAppPermission": "Remote apps permissions",
"KubernetesAppPermission": "Kubernetes permissions",
@@ -507,6 +598,7 @@
"downloadFile": "Download file",
"hostName": "Hostname",
"isValid": "Validity",
"fromTicket": "From ticket",
"isEffective": "Effective",
"nodeCount": "Node count",
"refreshFail": "Refresh fail",
@@ -530,6 +622,7 @@
},
"route": {
"": "",
"SessionDetail": "SessionDetail",
"Accounts": "Accounts",
"AssetAccount": "Asset account",
"ApplicationAccount": "Application account",
@@ -627,7 +720,8 @@
"ApplicationPermissionUpdate": "Application permission update",
"RemoteAppUpdate": "Remote app update",
"ReplayStorageUpdate": "Replay storage update",
"SessionDetail": "Sessions detail",
"Detail": "Detail",
"Activity": "Activity",
"SessionOffline": "Sessions offline",
"SessionOnline": "Sessions online",
"Sessions": "Sessions",
@@ -658,9 +752,14 @@
"WebFTP": "WebFTP",
"WebTerminal": "Web Terminal",
"Notifications": "Notifications",
"SiteMessageList": "Site message"
"SiteMessageList": "Site message",
"UserLoginACL": "User Login ACL"
},
"sessions": {
"SetToDefaultStorage": "Set to default storage",
"SetToDefault": "Set to default",
"SetSuccess": "Set success",
"SetFailed": "Set failed",
"StorageConfiguration": "Storage configuration",
"accountKey": "Account key",
"accountName": "Account name",
@@ -670,6 +769,7 @@
"target": "Target",
"bucket": "Bucket",
"command": "Command",
"Activity": "Activity",
"commandStorage": "Command storage",
"comment": "Comment",
"containerName": "Container name",
@@ -725,7 +825,7 @@
"sessionMonitor": "Session Monitor",
"TerminateTaskSendSuccessMsg": "Terminate task has been send, Please check later",
"helpText": {
"esUrl": "Tip: If you have multiple hosts, use comma (,) to split (eg: http://www.jumpserver.a.com,http://www.jumpserver.b.com)",
"esUrl": "Tip: If you have multiple hosts, use comma (, ) to split (eg: http://www.jumpserver.a.com, http://www.jumpserver.b.com)",
"esIndex":"Es provides the default index: jumpserver",
"esDocType": "Es provides the default document type: command",
"s3Endpoint": "S3: http://s3.{REGION_NAME}.amazonaws.com<br>S3(China): http://s3.{REGION_NAME}.amazonaws.com.cn<br>Example: http://s3.cn-north-1.amazonaws.com.cn",
@@ -733,29 +833,52 @@
}
},
"setting": {
"Feature": "Feature",
"SMSProvider": "SMS provider",
"SMS": "SMS",
"AlibabaCloud": "Alibaba cloud",
"TencentCloud": "Tencent cloud",
"VerifySignTmpl": "Verification code template",
"Radius": "Radius",
"Enable": "Enable",
"Perm": "Permission",
"SMTP": "SMTP server",
"Setting": "Setting",
"AuthMethod": "Auth methods",
"AuthLimit": "Auth limit",
"Ops": "Task",
"OTP": "OTP(MFA)",
"MessageSub": "Message",
"Cleaning": "Period clean",
"Perms": "Perms",
"CASSetting": "CAS setting",
"Other": "More...",
"Auth": "Auth",
"SyncSetting": "Sync setting",
"Advanced": "Advanced",
"InsecureCommandNotifyToSubscription": "Insecure command notification setting, change to system message subscription, support more notify method",
"ApiKeyList": "Api key list",
"AssetCount": "Asset count",
"Basic": "Basic setting",
"Basic": "Basic",
"Corporation": "Corporation",
"Create": "Create",
"Edition": "Edition",
"Email": "Email setting",
"Email": "Email",
"EmailContent": "Email content setting",
"Expired": "Expired",
"Hostname": "Hostname",
"ImportLicense": "Import license",
"ImportLicenseTip": "Please Import License",
"Ldap": "LDAP setting",
"Ldap": "LDAP",
"License": "License",
"LicenseFile": "License file",
"PasswordCheckRule": "Password check rule",
"Security": "Security setting",
"SecuritySetting": "Security setting",
"Security": "Security",
"SecuritySetting": "Security",
"SubscriptionID": "Subscription ID",
"SystemMessageSubscription": "System messages",
"insecureCommandEmailUpdate": "Setting",
"Terminal": "Terminal setting",
"Terminal": "Terminal",
"all": "All",
"authLdap": "Enable LDAP auth",
"authLdapBindDn": "Bind DN",
@@ -764,12 +887,24 @@
"authLdapSearchOu": "User OU",
"authLdapServerUri": "LDAP server",
"authLdapUserAttrMap": "User attr map",
"authUserAttrMap": "User attr map",
"authUserAttrMapHelpText": "Mapping relationship { idp_key: sp_key}",
"SAML2": "SAML2",
"enableSAML2Auth": "Enable SAML2 Auth",
"SAML2Auth": "SAML2 Auth",
"authSAML2Xml": "IDP metadata XML",
"authSAML2MetadataUrl": "IDP metadata URL",
"IdpMetadataHelpText": "Choose one of IDP metadata URL and IDP metadata XML parameters. IDP metadata URL has high priority",
"IdpMetadataUrlHelpText": "Load IDP Metadata from remote url",
"authSAMLKeyHelpText": "SP cert and private key, using communicate with IDP",
"authSAMLCertHelpText": "After upload cert and private key, View SP Metadata",
"authSAML2AdvancedSettings": "Advanced Settings",
"unselectedUser": "Unselected user",
"auto": "Auto",
"basicSetting": "Basic setting",
"communityEdition": "Community edition",
"consult": "Consult",
"createUserSetting": "Create User setting",
"CreateUserSetting": "Create User setting",
"emailCustomUserCreatedBody": "Create user email content",
"emailCustomUserCreatedHonorific": "Create user honorific",
"emailCustomUserCreatedSignature": "Signature",
@@ -792,7 +927,7 @@
"ApiKeyList": "The API key is used to sign the request header. The header of each request is different. Please refer to the usage documentation",
"authLdapSearchFilter": "Choice may be (cn|uid|sAMAccountName)=%(user)s)",
"authLdapSearchOu": "Use | split User OUs",
"authLdapUserAttrMap": "User attr map present how to map LDAP user attr to jumpserver, username,name,email is jumpserver attr",
"authLdapUserAttrMap": "User attr map present how to map LDAP user attr to jumpserver, username, name, email is jumpserver attr",
"emailCustomUserCreatedBody": "Tips:When creating a user, send the content of the email",
"emailCustomUserCreatedHonorific": "Tips: When creating a user, send the honorific of the email (eg:Hello)",
"emailCustomUserCreatedSignature": "Tips: Email signature (eg:jumpserver)",
@@ -869,6 +1004,8 @@
"DingTalk": "DingTalk",
"dingTalkTest": "Test",
"weComTest": "Test",
"FeiShu": "FeiShu",
"feiShuTest": "Test",
"setting": "Setting"
},
@@ -876,6 +1013,9 @@
"PermissionName": "Permission name",
"Accept": "Accept",
"AssignedMe": "Assigned me",
"FlowSetUp": "Flow set up",
"ApprovalProcess": "Approval process",
"LevelApproval": "Level approval",
"Assignee": "Assignee",
"RequestPerm":"Request Perm",
"AssignedInfo":"Assigned Info",
@@ -889,7 +1029,7 @@
"Comment": "Comment",
"MyTickets": "My tickets",
"action": "Action",
"IPGroup": "IP ",
"IPGroup": "IP group",
"Reject": "Reject",
"date": "Date",
"reply": "Reply",
@@ -937,6 +1077,9 @@
"UpdateNodeAssetHardwareInfo": "Update node asset hardware information"
},
"users": {
"FileEncryptionPassword": "File encryption password",
"MessageSubscription": "Message Subscription",
"AuthSettings": "Auth settings",
"UserName": "Name",
"Account": "Account",
"Existing":"Existing",
@@ -949,9 +1092,14 @@
"DatePasswordLastUpdated": "Date password last updated",
"setWeCom": "Set wecom login",
"setDingTalk": "Set dingtalk login",
"setFeiShu": "Set feishu login",
"DatePasswordUpdated": "Date password updated",
"DescribeOfGuide": "Welcome to JumpServer. Click here for more information",
"Email": "Email",
"Phone": "Phone",
"WeCom": "WeCom",
"DingTalk": "DingTalk",
"FeiShu": "FeiShu",
"FingerPrint": "Fingerprint",
"FirstLogin": "First login",
"InviteUser": "Invite user",
@@ -1003,7 +1151,7 @@
"resetMFAWarningMsg": "This will reset the user MFA setting, user can reset it",
"resetMFAdSuccessMsg": "Reset MFA success",
"resetPassword": "Reset password",
"resetPasswordSuccessMsg": "An e-mail has been sent to the user`s mailbox",
"resetPasswordSuccessMsg": "A password reset message has been sent to the user",
"resetPasswordWarningMsg": "This will reset the user password and send a reset mail",
"resetSSHKey": "Reset SSH key",
"resetSSHKeySuccessMsg": "An e-mail has been sent to the user`s mailbox",
@@ -1049,23 +1197,46 @@
"HasRead": "Has read",
"Sender": "Sender",
"MarkAsRead": "Mark as read",
"OneClickRead": "Currently read",
"OneClickReadMsg": "Are you sure you want to mark the current information as read?",
"NoUnreadMsg": "No unread messages",
"SiteMessage": "Site messages"
"SiteMessage": "Site messages",
"SMS": "SMS"
},
"xpack": {
"NavHelp": "Navigation Link",
"helpDocument": "Docs link",
"helpSupport": "Support link",
"helpDocumentTip": "You can change the URL of the site navigation bar help -> Docs",
"helpSupportTip": "You can change the URL of the site navigation bar help -> Support",
"officialWebsiteTip": "You can change the URL of the site navigation bar help -> Website",
"Admin": "Admin",
"Asset": "Asset",
"Database": "Database",
"AssetCount": "Asset count",
"Auditor": "Auditor",
"ChangeAuthPlan": {
"MailRecipient": "Mail recipient",
"AddAsset": "Add asset",
"ContainAttachment": "Contain attachment",
"AddNode": "Add node",
"AddSystemUser": "Add systemuser",
"Asset": "Asset",
"Database": "Database",
"DatabaseId": "Database Id",
"SystemUser": "SystemUser",
"SystemUserId": "SystemUser Id",
"AssetAmount": "Asset",
"AssetAndNode": "Asset and Node",
"ChangeAuthPlan": "Change Auth Plan",
"ChangeAuthPlanCreate": "Create change auth plan",
"ChangeAuthPlanUpdate": "Update change auth plan",
"AssetChangeAuthPlan": "Asset Change Auth Plan",
"AppChangeAuthPlan": "App Change Auth Plan",
"AssetChangeAuthPlanCreate": "Create Asset change auth plan",
"AppChangeAuthPlanCreate": "Create App change auth plan",
"AssetChangeAuthPlanUpdate": "Update Asset change auth plan",
"AppChangeAuthPlanUpdate": "Update App change auth plan",
"SymbolSet": "Special symbol set",
"SymbolSetHelpText": "Please enter the special symbol set supported by this type of database. If there are special characters in the generated random password that are not supported by this type of database, the password change plan will fail",
"CyclePerform": "Cycle perform",
"DateJoined": "Date joined",
"DateStart": "Date start",
@@ -1089,6 +1260,7 @@
"NodeAmount": "Node",
"PasswordLength": "Password length",
"PasswordStrategy": "Password strategy",
"SecretKeyStrategy": "Secret key strategy",
"RegularlyPerform": "Regularly perform",
"Result": "Result",
"Retry": "Retry",
@@ -1096,11 +1268,18 @@
"TaskList": "Task list",
"TimeDelta": "Time delta",
"Timer": "Timer",
"TimerPeriod": "Timer period",
"Username": "Username"
},
"Cloud": {
"ServerAccountKey": "Server Account Key",
"IPNetworkSegment": "Ip Network Segment",
"Aliyun": "Ali Cloud",
"Qcloud": "Tencent Cloud",
"QingyunPrivatecloud": "Qingyun Private Cloud",
"HuaweiPrivatecloud": "Huawei Private Cloud",
"OpenStack": "OpenStack",
"GCP": "Google Cloud Platform",
"AWS_China": "AWS(China)",
"AWS_Int": "AWS(International)",
"HuaweiCloud": "Huawei Cloud",
@@ -1128,7 +1307,8 @@
"Name": "Name",
"Account":"Account",
"Node": "Node",
"AdminUser":"Admin user",
"WindowsAdminUser":"Windows Admin user",
"LinuxAdminUser":"Linux Admin user",
"PeriodicPerform":"Periodic perform",
"Periodic":"Periodic",
"RegularlyPerform": "Regularly perform",
@@ -1181,7 +1361,7 @@
"Organization": {
"OrganizationCreate": "Create organization",
"OrganizationDetail": "Org detail",
"OrganizationList": "Organlizations",
"OrganizationList": "Organizations",
"OrganizationUpdate": "Update org",
"OrganizationMembership": "Organization membership",
"DeleteOrgTitle":"Please ensure that the following information in the organization has been deleted",

View File

@@ -15,6 +15,7 @@
</template>
<script>
import AutoDataForm from '@/components/AutoDataForm'
import { getUpdateObjURL } from '@/utils/common'
export default {
name: 'GenericCreateUpdateForm',
components: {
@@ -50,6 +51,10 @@ export default {
type: Function,
default: null
},
hasReset: {
type: Boolean,
default: null
},
// 如何提交数据
performSubmit: {
type: Function,
@@ -111,7 +116,7 @@ export default {
}
},
// 获取提交的方法
getMethod: {
submitMethod: {
type: Function,
default: function() {
const params = this.$route.params
@@ -129,36 +134,31 @@ export default {
const params = this.$route.params
let url = this.url
if (params.id) {
url = `${url}${params.id}/`
url = getUpdateObjURL(url, params.id)
}
return url
}
},
onPerformSuccess: {
emitPerformSuccessMsg: {
type: Function,
default(res, method, vm, addContinue) {
default(method, res, addContinue) {
let msg = method === 'post' ? this.createSuccessMsg : this.updateSuccessMsg
if (addContinue) {
msg = this.saveSuccessContinueMsg
}
let msgLinkName = this.$t('common.Resource')
let msgLinkName = ''
if (res.name) {
msgLinkName = res.name
} else if (res.hostname) {
msgLinkName = res.hostname
}
const detailRoute = this.objectDetailRoute
detailRoute['params'] = { 'id': res.id }
const route = this.getNextRoute(res, method)
this.$emit('submitSuccess', res)
const h = this.$createElement
this.$log.debug('router is: ', detailRoute)
if (this.hasDetailInMsg) {
this.$message({
message: h('p', null, [
h('el-link', {
on: {
click: () => this.$router.push(detailRoute)
click: () => this.$router.push(this.objectDetailRoute)
},
style: { 'vertical-align': 'top' }
}, msgLinkName),
@@ -174,6 +174,18 @@ export default {
} else {
this.$message.success(msg)
}
}
},
onPerformSuccess: {
type: Function,
default(res, method, vm, addContinue) {
const route = this.getNextRoute(res, method)
if (!(route.params && route.params.id)) {
route['params'] = { 'id': res.id }
}
this.$emit('submitSuccess', res)
this.emitPerformSuccessMsg(method, res, addContinue)
if (!addContinue) {
setTimeout(() => this.$router.push(route), 100)
}
@@ -215,9 +227,10 @@ export default {
},
computed: {
method() {
return this.getMethod(this)
return this.submitMethod(this)
},
iUrl() {
// 更新或创建的url
return this.getUrl()
},
iHasSaveContinue() {
@@ -230,7 +243,7 @@ export default {
if (this.hasReset != null) {
return this.hasReset
}
return this.method === 'put'
return this.isUpdateMethod()
}
},
async created() {
@@ -246,6 +259,9 @@ export default {
}
},
methods: {
isUpdateMethod() {
return ['put', 'patch'].indexOf(this.method.toLowerCase()) > -1
},
handleSubmit(values, formName, addContinue) {
let handler = this.onSubmit || this.defaultOnSubmit
handler = handler.bind(this)
@@ -261,11 +277,11 @@ export default {
},
async getFormValue() {
const cloneFrom = this.$route.query['clone_from']
if (this.method !== 'put' && !cloneFrom) {
if (!this.isUpdateMethod() && !cloneFrom) {
return Object.assign(this.form, this.initial)
}
let object = this.object
if (!object) {
if (!object || Object.keys(object).length === 0) {
if (cloneFrom) {
this.$log.debug('Clone from: ', cloneFrom)
const url = `${this.url}${cloneFrom}/`

View File

@@ -175,13 +175,18 @@ export default {
},
defaultUpdate() {
const id = this.$route.params.id
const routeName = this.validActions.updateRoute
this.$router.push({ name: routeName, params: { id: id }})
let route = this.validActions.updateRoute
if (typeof route === 'string') {
route = { name: route, params: {}}
}
route.params.id = id
this.$router.push(route)
},
getObject() {
const url = this.validActions.detailApiUrl
return this.$axios.get(url, { disableFlashErrorMsg: true }).then(data => {
this.$emit('update:object', data)
this.$emit('getObjectDone', data)
}).catch(error => {
if (error.response && error.response.status === 404) {
const msg = this.$t('common.ObjectNotFoundOrDeletedMsg')

View File

@@ -1,5 +1,5 @@
<template>
<ListTable ref="ListTable" v-bind="iAttrs" v-on="$listeners" />
<ListTable ref="ListTable" v-bind="$attrs" v-on="$listeners" />
</template>
<script>
@@ -11,16 +11,15 @@ export default {
ListTable
},
computed: {
...mapGetters(['currentOrgIsRoot']),
iAttrs() {
const attrs = _.cloneDeep(this.$attrs)
const canCreate = _.get(attrs, 'header-actions.canCreate', null)
this.$log.debug('Can create: ', canCreate)
if (canCreate === null && this.currentOrgIsRoot) {
_.set(attrs, 'header-actions.canCreate', false)
}
this.$log.debug('List table Attrs: ', attrs)
return attrs
...mapGetters(['currentOrgIsRoot'])
},
created() {
const headerActions = this.$attrs['header-actions'] || {}
if (headerActions.canCreate === undefined && this.currentOrgIsRoot) {
_.set(this.$attrs, 'header-actions.canCreate', false)
}
if (headerActions.hasImport === undefined && this.currentOrgIsRoot) {
_.set(this.$attrs, 'header-actions.hasImport', false)
}
}
}

View File

@@ -0,0 +1,60 @@
<template>
<Dialog
:title="title"
:visible.sync="iVisible"
top="1vh"
:width="width"
:show-cancel="false"
:show-confirm="false"
>
<GenericListTable :table-config="tableConfig" :header-actions="headerActions" />
</Dialog>
</template>
<script>
import Dialog from '@/components/Dialog'
import { GenericListTable } from '@/layout/components'
export default {
name: 'Index',
components: {
Dialog, GenericListTable
},
props: {
title: {
type: String,
required: true
},
visible: {
type: Boolean,
default: false
},
tableConfig: {
type: Object,
default: () => ({})
},
headerActions: {
type: Object,
default: () => ({})
},
width: {
type: String,
default: '60%'
}
},
computed: {
iVisible: {
set(val) {
this.$emit('update:visible', val)
},
get() {
return this.visible
}
}
}
}
</script>
<style scoped>
</style>

View File

@@ -1,11 +1,8 @@
<template>
<Page>
<el-alert v-if="helpMessage" type="success"> {{ helpMessage }} </el-alert>
<Page v-bind="$attrs">
<TreeTable
ref="TreeTable"
:table-config="tableConfig"
:header-actions="iHeaderActions"
:tree-setting="treeSetting"
v-bind="$attrs"
v-on="$listeners"
>
<template #table>
@@ -27,23 +24,16 @@ export default {
components: {
Page, TreeTable
},
props: {
...TreeTable.props,
helpMessage: {
type: String,
default: null
}
},
computed: {
...mapGetters(['currentOrg']),
iHeaderActions() {
const attrs = _.cloneDeep(this.headerActions)
const canCreate = _.get(attrs, 'canCreate', null)
// this.$log.debug('Current org: ', this.currentOrg)
if (canCreate === null && this.currentOrg && this.currentOrg.is_root) {
_.set(attrs, 'canCreate', false)
}
return attrs
...mapGetters(['currentOrgIsRoot'])
},
created() {
const headerActions = this.$attrs['header-actions'] || {}
if (headerActions.canCreate === undefined && this.currentOrgIsRoot) {
_.set(this.$attrs, 'header-actions.canCreate', false)
}
if (headerActions.hasImport === undefined && this.currentOrgIsRoot) {
_.set(this.$attrs, 'header-actions.hasImport', false)
}
},
methods: {

View File

@@ -1,7 +1,7 @@
<template>
<Dialog
:title="this.$t('common.updateSelected')"
:visible.sync="dialogSetting.dialogVisible"
:visible.sync="iVisible"
width="70%"
top="1vh"
:show-cancel="false"
@@ -45,13 +45,13 @@ export default {
type: Array,
default: () => ([])
},
dialogSetting: {
type: Object,
default: () => ({})
},
formSetting: {
type: Object,
default: () => ({})
},
visible: {
type: Boolean,
default: false
}
},
data: function() {
@@ -62,6 +62,16 @@ export default {
iFormSetting: {}
}
},
computed: {
iVisible: {
set(val) {
this.$emit('update:visible', val)
},
get() {
return this.visible
}
}
},
mounted() {
const defaultFormSetting = this.getDefaultFormSetting()
this.iFormSetting = Object.assign({}, this.formSetting, defaultFormSetting)
@@ -80,7 +90,7 @@ export default {
getDefaultFormSetting() {
const vm = this
return {
getMethod: () => 'post',
submitMethod: () => 'post',
cleanFormValue: function(value) {
const filterValue = {}
Object.keys(value).filter((key) => vm.checkedFields.includes(key)).forEach((key) => {
@@ -100,7 +110,7 @@ export default {
this.$axios.patch(url, validValues).then((res) => {
vm.$emit('update')
this.$message.success(msg)
vm.dialogSetting.dialogVisible = false
this.iVisible = false
}).catch(error => {
this.$emit('submitError', error)
const response = error.response

View File

@@ -15,7 +15,7 @@
<el-option
v-for="item in userAdminOrgList"
:key="item.id"
selected="item.id == currentOrg.id"
:selected="item.id === currentOrg.id"
:label="item.name"
:value="item.id"
/>
@@ -48,10 +48,8 @@ export default {
},
methods: {
needShow() {
const otherOrgs = this.userAdminOrgList.filter(org => {
return !org.is_root && !org.is_default
})
return !this.isCollapse && otherOrgs.length > 0 && this.inAdminPage
const hasValidLicense = this.$store.getters.hasValidLicense
return !this.isCollapse && this.inAdminPage && hasValidLicense
},
changeOrg(orgId) {
orgUtil.changeOrg(orgId)

View File

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

View File

@@ -6,7 +6,7 @@
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="docs">{{ $t('common.nav.Docs') }}</el-dropdown-item>
<el-dropdown-item command="support">{{ $t('common.nav.Support') }}</el-dropdown-item>
<el-dropdown-item command="EnterpriseEdition">{{ $t('common.nav.EnterpriseEdition') }}</el-dropdown-item>
<el-dropdown-item v-if="!hasLicence" command="enterprise">{{ $t('common.nav.EnterpriseEdition') }}</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
@@ -14,17 +14,37 @@
<script>
export default {
name: 'Help',
data() {
return {
URLSite: {
HELP_DOCUMENT_URL: '',
HELP_SUPPORT_URL: ''
}
}
},
computed: {
hasLicence() {
return this.$store.getters.hasValidLicense
}
},
created() {
this.initHelpURL()
},
methods: {
initHelpURL() {
this.URLSite.HELP_DOCUMENT_URL = this.$store.getters.publicSettings.HELP_DOCUMENT_URL
this.URLSite.HELP_SUPPORT_URL = this.$store.getters.publicSettings.HELP_SUPPORT_URL
},
handleCommand(command) {
switch (command) {
case 'support':
window.open('http://www.jumpserver.org/support/', '_blank')
window.open(this.URLSite.HELP_SUPPORT_URL, '_blank')
break
case 'EnterpriseEdition':
case 'enterprise':
window.open('https://jumpserver.org/enterprise.html', '_blank')
break
default:
window.open('http://docs.jumpserver.org', '_blank')
window.open(this.URLSite.HELP_DOCUMENT_URL, '_blank')
break
}
}

View File

@@ -16,15 +16,15 @@ export default {
return {
LANG_COOKIE_NAME: 'django_language', // 后端Django需要的COOKIE KEY
supportLanguages: [
{
title: '中文(简体)',
code: 'cn',
cookieCode: 'zh-hans' // cookie code是为了让后端知道当前语言
},
{
title: 'English',
code: 'en',
cookieCode: 'en'
},
{
title: '中文(简体)',
code: 'cn',
cookieCode: 'zh-hans' // cookie code是为了让后端知道当前语言
}
]
}
@@ -78,9 +78,7 @@ export default {
}
langCode = langCode.substr(0, 2)
langCode = langCode.replace('zh', 'cn')
if (langCode) {
return langCode
}
return langCode
}
}
}

View File

@@ -14,25 +14,31 @@
size="25%"
@open="getMessages"
>
<div slot="title">
<span>{{ $t('notifications.SiteMessage') }}</span>
<div v-if="unreadMsgCount !== 0" class="msg-list-all-read-btn" @click.stop="oneClickRead(messages)">
<a> {{ $t('notifications.OneClickRead') }}</a>
</div>
</div>
<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'"
: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" />
<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">
<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)">
<div v-else class="msg-item-read-btn" @click.stop="markAsRead([msg])">
<a>{{ $t('notifications.MarkAsRead') }}</a>
</div>
</div>
@@ -52,7 +58,7 @@
:title="''"
:close-on-click-modal="false"
:confirm-title="$t('notifications.MarkAsRead')"
@confirm="markAsRead(currentMsg)"
@confirm="markAsRead([currentMsg])"
@cancel="cancelRead"
>
<div class="msg-detail">
@@ -120,9 +126,26 @@ export default {
return this.$moment(d).fromNow()
}
},
markAsRead(msg) {
oneClickRead(msgs) {
this.$confirm(this.$t('notifications.OneClickReadMsg'), this.$t('common.Info'), {
type: 'warning',
confirmButtonClass: 'el-button--danger',
beforeClose: async(action, instance, done) => {
if (action !== 'confirm') return done()
this.markAsRead(msgs)
done()
}
}).catch(() => {
/* 取消*/
})
},
markAsRead(msgs) {
const url = `/api/v1/notifications/site-message/mark-as-read/`
this.$axios.patch(url, { ids: [msg.id] }).then(res => {
const msgIds = []
for (const item of msgs) {
msgIds.push(item.id)
}
this.$axios.patch(url, { ids: msgIds }).then(res => {
this.msgDetailVisible = false
this.getMessages()
}).catch(err => {
@@ -178,6 +201,10 @@ export default {
margin-bottom: 0;
padding-top: 10px;
font-size: 16px;
.msg-list-all-read-btn {
font-size: 12px;
float: right;
}
}
.el-drawer__body {
@@ -225,7 +252,7 @@ export default {
.msg-item-head-type {
float: left;
width: 240px;
width: 220px;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
@@ -243,8 +270,8 @@ export default {
overflow: hidden;
color: #000;
padding: 4px 0 0;
line-height: 21px;
max-height: 21px;
line-height: 25px;
max-height: 25px;
display: -webkit-box;
font-size: 12px;
display: block;

View File

@@ -61,8 +61,7 @@ export default {
},
showTickets() {
return this.publicSettings.TICKETS_ENABLED &&
this.publicSettings.XPACK_LICENSE_IS_VALID &&
!this.isOrgAuditor
this.publicSettings['XPACK_LICENSE_IS_VALID']
}
},
methods: {

View File

@@ -7,7 +7,6 @@
</template>
<script>
import { toSafeLocalDateStr } from '@/utils/common'
import { mapGetters } from 'vuex'
export default {
@@ -41,10 +40,10 @@ export default {
},
methods: {
getIntervalDays(date) {
const dateExpired = new Date(toSafeLocalDateStr(date))
const dateNow = new Date()
const intervalTime = dateNow.getTime() - dateExpired.getTime()
return Math.floor(intervalTime / (24 * 3600 * 1000))
const dateExpired = this.$moment(date, 'YYYY-MM-DD').format('YYYY-MM-DD')
const dateNow = this.$moment(new Date()).format('YYYY-MM-DD')
const intervalTime = this.$moment(dateNow).diff(this.$moment(dateExpired), 'days')
return intervalTime
}
}
}

View File

@@ -8,10 +8,23 @@
</template>
<div>
<el-tabs v-if="submenu.length > 0" slot="submenu" v-model="iActiveMenu" class="page-submenu" @tab-click="handleTabClick">
<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" :disabled="item.disabled">
<el-tab-pane
v-if="checkShow(item)"
:key="item.name"
:label-content="item.labelContent"
:name="item.name"
:disabled="item.disabled"
>
<span slot="label">
<i v-if="item.icon" class="fa " :class="item.icon" />
{{ item.title }}
<slot name="badge" :tab="item.name" />
</span>
@@ -67,6 +80,13 @@ export default {
this.iActiveMenu = this.getPropActiveTab()
},
methods: {
checkShow(item) {
let hidden = item.hidden
if (typeof hidden === 'function') {
hidden = hidden()
}
return !hidden
},
handleTabClick(tab) {
this.$emit('tab-click', tab)
this.$emit('update:activeMenu', tab.name)
@@ -103,15 +123,16 @@ export default {
</script>
<style scoped>
.page-submenu >>> .el-tabs__header {
background-color: white;
margin-left: -25px;
padding-left: 25px;
margin-right: -25px;
padding-right: 25px;
margin-top: -30px;
}
.page-submenu >>> .el-tabs__nav-wrap {
position: static;
}
.page-submenu >>> .el-tabs__header {
background-color: white;
margin-left: -25px;
padding-left: 25px;
margin-right: -25px;
padding-right: 25px;
margin-top: -30px;
}
.page-submenu >>> .el-tabs__nav-wrap {
position: static;
}
</style>

View File

@@ -7,6 +7,8 @@ export { default as GenericDetailPage } from './GenericDetailPage'
export { default as TabPage } from './TabPage'
export { default as Footer } from './Footer'
export { default as GenericListPage } from './GenericListPage'
export { default as GenericListTable } from './GenericListTable'
export { default as GenericCreateUpdatePage } from './GenericCreateUpdatePage'
export { default as GenericCreateUpdateForm } from './GenericCreateUpdateForm'
export { default as GenericUpdateFormDialog } from './GenericUpdateFormDialog'
export { default as GenericListTableDialog } from './GenericListTableDialog'

View File

@@ -3,8 +3,8 @@ import Vue from 'vue'
import 'normalize.css/normalize.css' // A modern alternative to CSS resets
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import locale from 'element-ui/lib/locale/lang/en' // lang i18n
import 'elementCss'
import locale from 'elementLocale'
import '@/styles/index.scss' // global css

View File

@@ -5,6 +5,7 @@ export default [
path: 'asset-accounts',
component: empty,
meta: { title: i18n.t('route.AssetAccount') },
redirect: '',
children: [
{
path: '',
@@ -17,6 +18,7 @@ export default [
{
path: 'application-accounts',
component: empty,
redirect: '',
meta: { title: i18n.t('route.AssetAccount') },
children: [
{
@@ -79,40 +81,83 @@ export default [
{
path: 'change-auth-plan',
component: empty,
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlan'), activeMenu: '/accounts/change-auth-plan/plan' },
redirect: '',
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlan') },
children: [
{
path: '',
component: () => import('@/views/accounts/ChangeAuthPlan/index.vue'),
name: 'ChangeAuthPlanIndex',
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlan'), activeMenu: '/accounts/change-auth-plan' }
},
{
path: 'plan',
component: () => import('@/views/accounts/ChangeAuthPlan/ChangeAuthPlanList.vue'),
name: 'ChangeAuthPlanList',
meta: { title: i18n.t('xpack.ChangeAuthPlan.ChangeAuthPlan'), activeMenu: '/accounts/change-auth-plan/plan' }
component: () => import('@/views/accounts/ChangeAuthPlan/AssetChangeAuthPlan/ChangeAuthPlanList.vue'),
name: 'AssetChangeAuthPlanList',
meta: { title: i18n.t('xpack.ChangeAuthPlan.AssetChangeAuthPlan'), activeMenu: '/accounts/change-auth-plan' },
hidden: true
},
{
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' },
component: () => import('@/views/accounts/ChangeAuthPlan/AssetChangeAuthPlan/ChangeAuthPlanCreateUpdate.vue'),
name: 'AssetChangeAuthPlanCreate',
meta: { title: i18n.t('xpack.ChangeAuthPlan.AssetChangeAuthPlanCreate'), activeMenu: '/accounts/change-auth-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' },
component: () => import('@/views/accounts/ChangeAuthPlan/AssetChangeAuthPlan/ChangeAuthPlanCreateUpdate.vue'),
name: 'AssetChangeAuthPlanUpdate',
meta: { title: i18n.t('xpack.ChangeAuthPlan.AssetChangeAuthPlanUpdate'), activeMenu: '/accounts/change-auth-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' },
component: () => import('@/views/accounts/ChangeAuthPlan/AssetChangeAuthPlan/ChangeAuthPlanDetail/index.vue'),
name: 'AssetChangeAuthPlanDetail',
meta: { title: i18n.t('xpack.ChangeAuthPlan.AssetChangeAuthPlan'), activeMenu: '/accounts/change-auth-plan' },
hidden: true
},
{
path: 'plan-execution/:id',
component: () => import('@/views/accounts/ChangeAuthPlan/ChangeAuthPlanDetail/ChangeAuthPlanExecution/ChangeAuthPlanExecutionDetail/index.vue'),
component: () => import('@/views/accounts/ChangeAuthPlan/AssetChangeAuthPlan/ChangeAuthPlanDetail/ChangeAuthPlanExecution/ChangeAuthPlanExecutionDetail/index.vue'),
name: 'ChangeAuthPlanExecutionDetail',
meta: { title: i18n.t('xpack.ChangeAuthPlan.ExecutionDetail'), activeMenu: '/accounts/change-auth-plan/plan' },
meta: { title: i18n.t('xpack.ChangeAuthPlan.ExecutionDetail'), activeMenu: '/accounts/change-auth-plan' },
hidden: true
},
{
path: 'app-plan',
component: () => import('@/views/accounts/ChangeAuthPlan/AppChangeAuthPlan/AppChangeAuthPlanList.vue'),
name: 'AppChangeAuthPlanList',
meta: { title: i18n.t('xpack.ChangeAuthPlan.AppChangeAuthPlan'), activeMenu: '/accounts/change-auth-plan' },
hidden: true
},
{
path: 'app-plan/create',
component: () => import('@/views/accounts/ChangeAuthPlan/AppChangeAuthPlan/AppChangeAuthPlanCreateUpdate.vue'),
name: 'AppChangeAuthPlanCreate',
meta: { title: i18n.t('xpack.ChangeAuthPlan.AppChangeAuthPlanCreate'), activeMenu: '/accounts/change-auth-plan', action: 'create' },
hidden: true
},
{
path: 'app-plan/:id',
component: () => import('@/views/accounts/ChangeAuthPlan/AppChangeAuthPlan/ChangeAuthPlanDetail/index.vue'),
name: 'AppChangeAuthPlanDetail',
meta: { title: i18n.t('xpack.ChangeAuthPlan.AppChangeAuthPlan'), activeMenu: '/accounts/change-auth-plan' },
hidden: true
},
{
path: 'app-plan/:id/update',
component: () => import('@/views/accounts/ChangeAuthPlan/AppChangeAuthPlan/AppChangeAuthPlanCreateUpdate.vue'),
name: 'AppChangeAuthPlanUpdate',
meta: { title: i18n.t('xpack.ChangeAuthPlan.AppChangeAuthPlanUpdate'), activeMenu: '/accounts/change-auth-plan', action: 'update' },
hidden: true
},
{
path: 'app-plan-execution/:id',
component: () => import('@/views/accounts/ChangeAuthPlan/AppChangeAuthPlan/ChangeAuthPlanDetail/AppChangeAuthPlanExecution/ChangeAuthPlanExecutionDetail/index.vue'),
name: 'AppChangeAuthPlanExecutionDetail',
meta: { title: i18n.t('xpack.ChangeAuthPlan.ExecutionDetail'), activeMenu: '/accounts/change-auth-plan' },
hidden: true
}
]

View File

@@ -38,57 +38,72 @@ export default [
]
},
{
path: 'database-apps',
name: 'DatabaseAppList',
component: () => import('@/views/applications/DatabaseApp/DatabaseAppList'),
meta: { title: i18n.t('route.DatabaseApp') }
path: 'databases',
component: empty,
redirect: '',
meta: { title: i18n.t('route.DatabaseApp') },
children: [
{
path: '',
name: 'DatabaseAppList',
component: () => import('@/views/applications/DatabaseApp/DatabaseAppList'),
meta: { title: i18n.t('route.DatabaseApp') }
},
{
path: 'create',
name: 'DatabaseAppCreate',
component: () => import('@/views/applications/DatabaseApp/DatabaseAppCreateUpdate'),
meta: { title: i18n.t('route.DatabaseAppCreate'), activeMenu: '/applications/databases', action: 'create' },
hidden: true
},
{
path: ':id/update',
name: 'DatabaseAppUpdate',
component: () => import('@/views/applications/DatabaseApp/DatabaseAppCreateUpdate'),
meta: { title: i18n.t('route.DatabaseAppUpdate'), activeMenu: '/applications/databases', action: 'update' },
hidden: true
},
{
path: ':id',
name: 'DatabaseAppDetail',
component: () => import('@/views/applications/DatabaseApp/DatabaseAppDetail/index'),
meta: { title: i18n.t('route.DatabaseAppDetail'), activeMenu: '/applications/databases' },
hidden: true
}
]
},
{
path: 'database-apps/create',
name: 'DatabaseAppCreate',
component: () => import('@/views/applications/DatabaseApp/DatabaseAppCreateUpdate'),
meta: { title: i18n.t('route.DatabaseAppCreate'), activeMenu: '/applications/database-apps', action: 'create' },
hidden: true
},
{
path: 'database-apps/:id/update',
name: 'DatabaseAppUpdate',
component: () => import('@/views/applications/DatabaseApp/DatabaseAppCreateUpdate'),
meta: { title: i18n.t('route.DatabaseAppUpdate'), activeMenu: '/applications/database-apps', action: 'update' },
hidden: true
},
{
path: 'database-apps/:id',
name: 'DatabaseAppDetail',
component: () => import('@/views/applications/DatabaseApp/DatabaseAppDetail/index'),
meta: { title: i18n.t('route.DatabaseAppDetail'), activeMenu: '/applications/database-apps' },
hidden: true
},
{
path: 'kubernetes-apps',
name: 'KubernetesAppList',
component: () => import('@/views/applications/KubernetesApp/KubernetesAppList'),
meta: { title: i18n.t('route.KubernetesApp') }
},
{
path: 'kubernetes-apps/create',
name: 'KubernetesAppCreate',
component: () => import('@/views/applications/KubernetesApp/KubernetesAppCreateUpdate'),
meta: { title: i18n.t('route.KubernetesAppCreate'), activeMenu: '/applications/kubernetes-apps', action: 'create' },
hidden: true
},
{
path: 'kubernetes-apps/:id/update',
name: 'KubernetesAppUpdate',
component: () => import('@/views/applications/KubernetesApp/KubernetesAppCreateUpdate'),
meta: { title: i18n.t('route.KubernetesAppUpdate'), activeMenu: '/applications/kubernetes-apps', action: 'update' },
hidden: true
},
{
path: 'kubernetes-apps/:id',
name: 'KubernetesAppDetail',
component: () => import('@/views/applications/KubernetesApp/KubernetesAppDetail/index'),
meta: { title: i18n.t('route.KubernetesAppDetail'), activeMenu: '/applications/kubernetes-apps' },
hidden: true
path: 'kubernetes',
component: empty,
meta: { title: i18n.t('route.KubernetesApp') },
children: [
{
path: '',
name: 'KubernetesAppList',
component: () => import('@/views/applications/KubernetesApp/KubernetesAppList'),
meta: { title: i18n.t('route.KubernetesApp') }
},
{
path: 'create',
name: 'KubernetesAppCreate',
component: () => import('@/views/applications/KubernetesApp/KubernetesAppCreateUpdate'),
meta: { title: i18n.t('route.KubernetesAppCreate'), activeMenu: '/applications/kubernetes', action: 'create' },
hidden: true
},
{
path: ':id/update',
name: 'KubernetesAppUpdate',
component: () => import('@/views/applications/KubernetesApp/KubernetesAppCreateUpdate'),
meta: { title: i18n.t('route.KubernetesAppUpdate'), activeMenu: '/applications/kubernetes', action: 'update' },
hidden: true
},
{
path: ':id',
name: 'KubernetesAppDetail',
component: () => import('@/views/applications/KubernetesApp/KubernetesAppDetail/index'),
meta: { title: i18n.t('route.KubernetesAppDetail'), activeMenu: '/applications/kubernetes' },
hidden: true
}
]
}
]

View File

@@ -101,41 +101,6 @@ export default [
}
]
},
{
path: 'admin-users',
component: empty,
redirect: '',
meta: { title: i18n.t('route.AdminUserList') },
children: [
{
path: '',
name: 'AdminUserList',
component: () => import('@/views/assets/AdminUser/AdminUserList'),
meta: { title: i18n.t('route.AdminUserList'), activeMenu: '/assets/admin-users' }
},
{
path: 'create',
component: () => import('@/views/assets/AdminUser/AdminUserCreateUpdate.vue'), // Parent router-view
name: 'AdminUserCreate',
meta: { title: i18n.t('route.AdminUserCreate'), activeMenu: '/assets/admin-users' },
hidden: true
},
{
path: ':id/update',
component: () => import('@/views/assets/AdminUser/AdminUserCreateUpdate.vue'), // Parent router-view
name: 'AdminUserUpdate',
meta: { title: i18n.t('route.AdminUserUpdate'), activeMenu: '/assets/admin-users' },
hidden: true
},
{
path: ':id',
component: () => import('@/views/assets/AdminUser/AdminUserDetail/index.vue'), // Parent router-view
name: 'AdminUserDetail',
meta: { title: i18n.t('route.AdminUserDetail'), activeMenu: '/assets/admin-users' },
hidden: true
}
]
},
{
path: 'system-users',
component: empty,
@@ -145,20 +110,20 @@ export default [
{
path: '',
name: 'SystemUserList',
component: () => import('@/views/assets/SystemUser/SystemUserList.vue'),
component: () => import('@/views/assets/SystemUser/SystemUserList/index.vue'),
meta: { title: i18n.t('route.SystemUserList'), activeMenu: '/assets/system-users' }
},
{
path: 'create',
name: 'SystemUserCreate',
component: () => import('@/views/assets/SystemUser/SystemUserCreateUpdate.vue'),
component: () => import('@/views/assets/SystemUser/SystemUserCreateUpdate/index.vue'),
meta: { title: i18n.t('route.SystemUserCreate'), activeMenu: '/assets/system-users' },
hidden: true
},
{
path: ':id/update',
name: 'SystemUserUpdate',
component: () => import('@/views/assets/SystemUser/SystemUserCreateUpdate.vue'),
component: () => import('@/views/assets/SystemUser/SystemUserCreateUpdate/index.vue'),
meta: { title: i18n.t('route.SystemUserUpdate'), activeMenu: '/assets/system-users' },
hidden: true
},

View File

@@ -167,7 +167,8 @@ export const allRoleRoutes = [
children: TicketsRoutes,
hidden: true,
meta: {
licenseRequired: true
licenseRequired: true,
permissions: [rolec.PERM_AUDIT]
// hidden: ({ settings }) => {
// return !settings.TICKETS_ENABLED
// }

View File

@@ -5,24 +5,32 @@ import { BASE_URL } from '@/utils/common'
export default [
{
path: 'session',
name: 'SessionList',
component: () => import('@/views/sessions/SessionList/index'),
meta: { title: i18n.t('route.Sessions'), permissions: [rolec.PERM_AUDIT] }
path: 'sessions',
component: empty,
redirect: '',
meta: { title: i18n.t('route.Sessions'), permissions: [rolec.PERM_AUDIT] },
children: [
{
path: '',
name: 'SessionList',
component: () => import('@/views/sessions/SessionList/index'),
meta: { title: i18n.t('route.Sessions'), permissions: [rolec.PERM_AUDIT] }
},
{
path: ':id',
name: 'SessionDetail',
component: () => import('@/views/sessions/SessionDetail/index'),
meta: { title: i18n.t('route.SessionDetail'), activeMenu: '/terminal/sessions' },
hidden: true
}
]
},
{
path: 'command',
path: 'commands',
name: 'CommandList',
component: () => import('@/views/sessions/CommandList'),
meta: { title: i18n.t('route.Commands'), permissions: [rolec.PERM_AUDIT] }
},
{
path: 'sessions/:id',
name: 'SessionDetail',
component: () => import('@/views/sessions/SessionDetail/index'),
meta: { title: i18n.t('route.SessionDetail'), activeMenu: '/terminal/session', permissions: [rolec.PERM_AUDIT] },
hidden: true
},
{
path: `${BASE_URL}/luna/?_=${Date.now()}`,
name: 'WebTerminal',
@@ -66,37 +74,41 @@ export default [
},
{
path: 'storages',
name: 'Storage',
component: () => import('@/views/sessions/Storage/index'),
component: empty,
meta: { activeMenu: '/terminal/terminal', permissions: [rolec.PERM_SUPER] },
hidden: true
},
{
path: 'replay-storage/create',
name: 'CreateReplayStorage',
component: () => import('@/views/sessions/ReplayStorageCreateUpdate'),
meta: { title: i18n.t('route.CreateReplayStorage'), activeMenu: '/terminal/terminal', permissions: [rolec.PERM_SUPER] },
hidden: true
},
{
path: 'command-storage/create',
name: 'CreateCommandStorage',
component: () => import('@/views/sessions/CommandStorageCreateUpdate'),
meta: { title: i18n.t('route.CreateCommandStorage'), activeMenu: '/terminal/terminal', permissions: [rolec.PERM_SUPER] },
hidden: true
},
{
path: 'replay-storage/:id/update',
name: 'ReplayStorageUpdate',
component: () => import('@/views/sessions/ReplayStorageCreateUpdate'),
meta: { title: i18n.t('route.ReplayStorageUpdate'), activeMenu: '/terminal/terminal', permissions: [rolec.PERM_SUPER] },
hidden: true
},
{
path: 'command-storage/:id/update',
name: 'CommandStorageUpdate',
component: () => import('@/views/sessions/CommandStorageCreateUpdate'),
meta: { title: i18n.t('route.CommandStorageUpdate'), activeMenu: '/terminal/terminal', permissions: [rolec.PERM_SUPER] },
hidden: true
redirect: '',
hidden: true,
children: [
{
path: '',
name: 'Storage',
component: () => import('@/views/sessions/Storage/index'),
meta: { activeMenu: '/terminal/terminal' }
},
{
path: 'replay-storage/create',
name: 'CreateReplayStorage',
component: () => import('@/views/sessions/ReplayStorageCreateUpdate'),
meta: { title: i18n.t('route.CreateReplayStorage'), activeMenu: '/terminal/terminal' }
},
{
path: 'replay-storage/:id/update',
name: 'ReplayStorageUpdate',
component: () => import('@/views/sessions/ReplayStorageCreateUpdate'),
meta: { title: i18n.t('route.ReplayStorageUpdate'), activeMenu: '/terminal/terminal' }
},
{
path: 'command-storage/create',
name: 'CreateCommandStorage',
component: () => import('@/views/sessions/CommandStorageCreateUpdate'),
meta: { title: i18n.t('route.CreateCommandStorage'), activeMenu: '/terminal/terminal' }
},
{
path: 'command-storage/:id/update',
name: 'CommandStorageUpdate',
component: () => import('@/views/sessions/CommandStorageCreateUpdate'),
meta: { title: i18n.t('route.CommandStorageUpdate'), activeMenu: '/terminal/terminal' }
}
]
}
]

View File

@@ -1,59 +1,101 @@
import i18n from '@/i18n/i18n'
import empty from '@/layout/empty'
import rolec from '@/utils/role'
export default [
{
path: 'tickets',
name: 'TicketList',
component: () => import('@/views/tickets/TicketList'),
meta: { title: i18n.t('route.Tickets'), icon: 'check-square-o', activeMenu: '/tickets/tickets' },
hidden: true
component: empty,
meta: { permissions: [rolec.PERM_AUDIT] },
hidden: true,
redirect: '',
children: [
{
path: '',
name: 'TicketList',
component: () => import('@/views/tickets/TicketList'),
meta: { title: i18n.t('route.Tickets'), icon: 'check-square-o', activeMenu: '/tickets/tickets', permissions: [rolec.PERM_AUDIT] },
hidden: true
},
{
path: 'request-asset-perm/create',
name: 'RequestAssetPermTicketCreateUpdate',
component: () => import('@/views/tickets/RequestAssetPerm/CreateUpdate'),
meta: { title: i18n.t('route.TicketCreate'), activeMenu: '/tickets/tickets', permissions: [rolec.PERM_AUDIT] },
hidden: true
},
{
path: 'request-asset-perm/:id',
name: 'AssetsTicketDetail',
component: () => import('@/views/tickets/RequestAssetPerm/Detail/index'),
meta: { title: i18n.t('route.TicketDetail'), activeMenu: '/tickets/tickets', permissions: [rolec.PERM_AUDIT] },
hidden: true
},
{
path: 'login-asset-confirm/:id',
name: 'LoginAssetTicketDetail',
component: () => import('@/views/tickets/LoginAssetConfirm/Detail/index'),
meta: { title: i18n.t('route.TicketDetail'), activeMenu: '/tickets/tickets', permissions: [rolec.PERM_AUDIT] },
hidden: true
},
{
path: 'request-application-perm/create',
name: 'RequestApplicationPermTicketCreateUpdate',
component: () => import('@/views/tickets/RequestApplicationPerm/CreateUpdate'),
meta: { title: i18n.t('route.TicketCreate'), activeMenu: '/tickets/tickets', permissions: [rolec.PERM_AUDIT] },
hidden: true
},
{
path: 'request-application-perm/:id',
name: 'AppsTicketDetail',
component: () => import('@/views/tickets/RequestApplicationPerm/Detail/index'),
meta: { title: i18n.t('route.TicketDetail'), activeMenu: '/tickets/tickets', permissions: [rolec.PERM_AUDIT] },
hidden: true
},
{
path: 'command-confirm/:id',
name: 'CommandConfirmDetail',
component: () => import('@/views/tickets/CommandConfirm/Detail/index'),
meta: { title: i18n.t('route.CommandConfirm'), activeMenu: '/tickets/tickets', permissions: [rolec.PERM_AUDIT] },
hidden: true
},
{
path: ':id',
name: 'TicketDetail',
component: () => import('@/views/tickets/TicketDetail/index'),
meta: { title: i18n.t('route.TicketDetail'), activeMenu: '/tickets/tickets', permissions: [rolec.PERM_AUDIT] },
hidden: true
}
]
},
{
path: 'tickets/:id',
name: 'TicketDetail',
component: () => import('@/views/tickets/TicketDetail/index'),
meta: { title: i18n.t('route.TicketDetail'), activeMenu: '/tickets/tickets' },
hidden: true
path: 'flows',
name: 'TicketFlowList',
component: empty,
redirect: '',
meta: { title: i18n.t('route.TicketFlow'), icon: 'check-square-o', activeMenu: '/tickets/tickets' },
hidden: true,
children: [
{
path: 'create',
name: 'TicketFlowCreate',
component: () => import('@/views/tickets/TicketFlow/FlowCreateUpdate'),
meta: { title: i18n.t('route.TicketFlowCreate') },
hidden: true
},
{
path: ':id/update',
name: 'TicketFlowUpdate',
component: () => import('@/views/tickets/TicketFlow/FlowCreateUpdate'),
meta: { title: i18n.t('route.TicketFlowUpdate') },
hidden: true
}
]
},
{
path: 'tickets/request-asset-perm/create',
name: 'RequestAssetPermTicketCreateUpdate',
component: () => import('@/views/tickets/RequestAssetPerm/RequestAssetPermTicketCreateUpdate'),
meta: { title: i18n.t('route.TicketCreate'), activeMenu: '/tickets/tickets' },
hidden: true
},
{
path: 'tickets/request-asset-perm/:id',
name: 'AssetsTicketDetail',
component: () => import('@/views/tickets/RequestAssetPerm/Detail/index'),
meta: { title: i18n.t('route.TicketDetail'), activeMenu: '/tickets/tickets' },
hidden: true
},
{
path: 'tickets/login-asset-confirm/:id',
name: 'loginAssetTicketDetail',
component: () => import('@/views/tickets/LoginAssetConfirm/Detail/index'),
meta: { title: i18n.t('route.TicketDetail'), activeMenu: '/tickets/tickets' },
hidden: true
},
{
path: 'tickets/request-application-perm/create',
name: 'RequestApplicationPermTicketCreateUpdate',
component: () => import('@/views/tickets/RequestApplicationPerm/RequestApplicationPermTicketCreateUpdate'),
meta: { title: i18n.t('route.TicketCreate'), activeMenu: '/tickets/tickets' },
hidden: true
},
{
path: 'tickets/request-application-perm/:id',
name: 'AppsTicketDetail',
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' },
path: 'tickets/flow/:id',
name: 'FlowDetail',
component: () => import('@/views/tickets/TicketFlow/Detail/index'),
meta: { title: i18n.t('route.TicketFlow'), activeMenu: '/tickets/tickets' },
hidden: true
}
]

View File

@@ -92,7 +92,7 @@ export default [
{
path: 'tickets/request-asset-perm/create',
name: 'RequestAssetPermTicketCreateUpdate',
component: () => import('@/views/tickets/RequestAssetPerm/RequestAssetPermTicketCreateUpdate'),
component: () => import('@/views/tickets/RequestAssetPerm/CreateUpdate'),
meta: { title: i18n.t('route.TicketDetail'), activeMenu: '/tickets', permissions: [rolec.PERM_USE] },
hidden: true
},
@@ -105,7 +105,7 @@ export default [
},
{
path: 'tickets/login-asset-confirm/:id',
name: 'loginAssetTicketDetail',
name: 'LoginAssetTicketDetail',
component: () => import('@/views/tickets/LoginAssetConfirm/Detail/index'),
meta: { title: i18n.t('route.TicketDetail'), activeMenu: '/tickets', permissions: [rolec.PERM_USE] },
hidden: true
@@ -113,7 +113,7 @@ export default [
{
path: 'tickets/request-application-perm/create',
name: 'RequestApplicationPermTicketCreateUpdate',
component: () => import('@/views/tickets/RequestApplicationPerm/RequestApplicationPermTicketCreateUpdate'),
component: () => import('@/views/tickets/RequestApplicationPerm/CreateUpdate'),
meta: { title: i18n.t('route.TicketCreate'), activeMenu: '/tickets/tickets', permissions: [rolec.PERM_USE] },
hidden: true
},

View File

@@ -56,38 +56,29 @@ export default [
meta: { title: i18n.t('route.UserGroupDetail'), activeMenu: '/users/groups' }
},
{
path: 'user-acl',
path: 'user-login-acl',
component: empty,
redirect: '',
meta: { title: i18n.t('route.UserAclList') },
hidden: true,
children: [
{
path: '',
name: 'UserAclList',
component: () => import('@/views/acl/UserAcl/UserAclList'),
meta: { title: i18n.t('route.UserAclList'), activeMenu: '/users/users' },
hidden: true
},
{
path: 'create',
name: 'UserAclCreate',
component: () => import('@/views/acl/UserAcl/UserAclCreateUpdate'),
meta: { title: i18n.t('route.UserAclCreate'), activeMenu: '/users/users' },
name: 'UserLoginACLCreate',
component: () => import('@/views/acl/UserLoginACL/UserLoginACLCreateUpdate'),
meta: { title: i18n.t('route.UserLoginACLCreate'), activeMenu: '/users/users' },
hidden: true
},
{
path: ':id',
name: 'UserAclDetail',
component: () => import('@/views/acl/UserAcl/UserAclDetail'),
meta: { title: i18n.t('route.UserAclDetail'), activeMenu: '/users/users' },
name: 'UserLoginACLDetail',
component: () => import('@/views/acl/UserLoginACL/UserDetail'),
meta: { title: i18n.t('route.UserLoginACL'), activeMenu: '/users/users' },
hidden: true
},
{
path: ':id/update',
name: 'UserAclUpdate',
component: () => import('@/views/acl/UserAcl/UserAclCreateUpdate'),
meta: { title: i18n.t('route.UserAclUpdate') },
name: 'UserLoginACLUpdate',
component: () => import('@/views/acl/UserLoginACL/UserLoginACLCreateUpdate'),
meta: { title: i18n.t('route.UserLoginACLUpdate'), activeMenu: '/users/users' },
hidden: true
}
]

View File

@@ -3,9 +3,9 @@ const getters = {
device: state => state.app.device,
token: state => state.users.token,
currentOrg: state => state.users.currentOrg,
currentOrgIsDefault: state => state.users.currentOrg.is_default,
currentOrgIsDefault: state => state.users.currentOrg['is_default'],
currentOrgIsRoot: state => {
return state.users.currentOrg && state.users.currentOrg.is_root
return state.users.currentOrg && state.users.currentOrg['is_root']
},
currentRole: state => state.users.currentRole,
currentUser: state => state.users.profile,
@@ -16,16 +16,15 @@ const getters = {
currentOrgRoles: state => state.users.roles,
currentOrgPerms: state => state.users.perms,
MFAVerifyAt: state => state.users.MFAVerifyAt,
MFA_TTl: state => state.settings.publicSettings.SECURITY_MFA_VERIFY_TTL,
MFA_TTl: state => state.settings.publicSettings['SECURITY_MFA_VERIFY_TTL'],
tableConfig: state => state.table.tableConfig,
currentUserIsSuperAdmin: state => {
return state.users.sysRole === 'Admin'
},
currentUserIsSuperAdmin: state => state.users.isSuperAdmin,
currentUserIsAdmin: state => state.users.isAdmin,
hasValidLicense: state => state.settings.hasValidLicense,
userAdminOrgList: (state, getters) => {
let orgs = state.users.orgs
if (!getters.hasValidLicense) {
orgs = orgs.filter(org => !org.is_root)
orgs = orgs.filter(org => !org['is_root'])
}
return orgs
}

View File

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

View File

@@ -1,24 +1,28 @@
import Vue from 'vue'
function getTableConfigfromCookie() {
return localStorage.getItem('tableConfig') ? JSON.parse(localStorage.getItem('tableConfig')) : {}
function getTableConfigFromLocal() {
const configs = localStorage.getItem('tableConfig')
try {
return JSON.parse(configs)
} catch (e) {
return {}
}
}
const state = {
tableConfig: getTableConfigfromCookie()
tableConfig: getTableConfigFromLocal()
}
const mutations = {
SET_TABLE_CONFIG: (state, tableConfig) => {
const _tableConfig = localStorage.getItem('tableConfig') ? JSON.parse(localStorage.getItem('tableConfig')) : {}
Vue.set(_tableConfig, tableConfig.key, tableConfig.value)
SET_TABLE_CONFIG: (state, item) => {
const _tableConfig = getTableConfigFromLocal()
Vue.set(_tableConfig, item.key, item.value)
localStorage.setItem('tableConfig', JSON.stringify(_tableConfig))
}
}
const actions = {
}
export default {

View File

@@ -1,10 +1,10 @@
import { logout, getProfile } from '@/api/users'
import {
getCurrentOrgLocal,
getCurrentRoleLocal,
getTokenFromCookie,
getCurrentOrgFromCookie,
saveCurrentOrgToCookie,
getCurrentRoleFromCookie,
saveCurrentRoleToCookie
saveCurrentOrgLocal,
saveCurrentRoleLocal
} from '@/utils/auth'
import { resetRouter } from '@/router'
import rolec from '@/utils/role'
@@ -12,15 +12,19 @@ import rolec from '@/utils/role'
const getDefaultState = () => {
return {
token: getTokenFromCookie(),
currentOrg: getCurrentOrgFromCookie(),
currentRole: getCurrentRoleFromCookie(),
currentOrg: '',
currentRole: '',
profile: {},
username: '',
roles: {},
sysRole: '',
orgs: [],
perms: 0b00000000,
MFAVerifyAt: null,
isSuperAdmin: false
isSuperAdmin: false,
isAdmin: false,
hasAdminPerm: false,
hasAuditPerm: false
}
}
@@ -35,6 +39,12 @@ const mutations = {
},
SET_PROFILE: (state, profile) => {
state.profile = profile
const username = profile.username
state.username = username
state.currentOrg = getCurrentOrgLocal(username)
state.currentRole = getCurrentRoleLocal(username)
state.isAdmin = profile['is_org_admin']
state.isSuperAdmin = profile['is_superuser']
},
SET_ORGS: (state, orgs) => {
state.orgs = orgs
@@ -53,20 +63,23 @@ const mutations = {
},
SET_ROLES(state, roles) {
state.roles = roles
// rolec.PERM_ADMIN &
},
SET_SYS_ROLE(state, role) {
state.sysRole = role
},
SET_PERMS(state, perms) {
state.perms = perms
state.hasAdmin = (perms & rolec.PERM_ADMIN) === rolec.PERM_ADMIN
state.hasAudit = (perms & rolec.PERM_AUDIT) === rolec.PERM_AUDIT
},
SET_CURRENT_ORG(state, org) {
saveCurrentOrgToCookie(org)
state.currentOrg = org
saveCurrentOrgLocal(state.username, org)
},
SET_CURRENT_ROLE(state, role) {
saveCurrentRoleToCookie(role)
state.currentRole = role
saveCurrentRoleLocal(state.username, role)
},
SET_MFA_VERIFY(state) {
state.MFAVerifyAt = (new Date()).valueOf()

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