Compare commits

..

142 Commits
v2.7.1 ... v2.9

Author SHA1 Message Date
ibuler
1cbafdf6b7 fix: 修复系统用户vnc用户名可以不必填 2021-05-13 19:25:00 +08:00
Orange
65eda50419 fix: 修改DB协议系统用户手动登录时用户名必填 2021-04-25 01:15:07 -05:00
Orange
9736d94762 fix: 临时移除 Database 协议的用户名与密码相同 选项 2021-04-19 04:00:28 -05:00
Jiangjie.Bai
d8566d2f9e Merge pull request #753 from jumpserver/dev
v2.9.0 发版
2021-04-15 21:05:14 +08:00
fit2bot
d74da503c8 fix: 修复不能禁用的问题 (#751)
perf: 优化系统用户创建

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

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

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

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

* fix: 修复翻译问题

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

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

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

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

* perf: 优化导入

stash

perf: 优化导入

perf: 更新导入

perf: 优化导入

feat: 完成导入优化

perf: 修复bug

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

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

* fix: 修复Build失败的问题

* fix(preload): 开启Preload

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

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

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

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

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

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

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

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

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

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

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

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

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

* fix: 修复bugs

* fix: 修复bugs

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

* fix: 修复引用取值问题

* fix: 还原更新设置

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

* fix: 移除多余的clone

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

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

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

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

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

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

* fix: 修改页面

* fix: 修改页面

* fix: 修改页面

* fix: 修改页面

* fix: update

* fix: 完善路由条件

* fix: 完善默认值

* fix: 完善翻译

* fix: 完善表单默认值

* fix: 完善表单默认值

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

* fix: 调整tree构建字段

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

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

* fix: 添加时间戳排序

* fix: 删除多余的Consolelog

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

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

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

* perf: 优化创建

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

* feat: 添加action groups

* perf: 优化创建的按钮

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

perf: 优化嵌套的form

perf: 优化其那套form

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

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

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

* perf: 优化全局组织

* perf: 优化组织选择

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

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

* fix: 干掉多余的代码

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

* refactor: 重构录像存储模块

* refactor: 重构录像存储模块

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

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

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

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

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

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

Co-authored-by: Orange <orangemtony@gmail.com>
2021-01-28 19:29:55 +08:00
Orange
eb20b32fcf fix: 修复工单列表字段展示错误 2021-01-26 04:08:56 -06:00
ibuler
9b19d862f6 perf: 优化ldap配置 2021-01-26 18:01:54 +08:00
ibuler
38b21357b7 perf(settings): 修改翻译 2021-01-26 18:01:54 +08:00
ibuler
c40bd0a9ab perf: 完成终端和安全setting 2021-01-26 18:01:54 +08:00
ibuler
48a7310739 perf(settings): 优化配置页面 2021-01-26 18:01:54 +08:00
185 changed files with 6775 additions and 3209 deletions

View File

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

View File

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

View File

@@ -18,13 +18,15 @@
"vue-i18n-report": "vue-i18n-extract report -v './src/**/*.?(js|vue)' -l './src/i18n/langs/**/*.json'"
},
"dependencies": {
"@babel/plugin-proposal-optional-chaining": "^7.13.12",
"@ztree/ztree_v3": "3.5.44",
"axios": "0.18.1",
"axios": "0.21.1",
"axios-retry": "^3.1.9",
"deepmerge": "^4.2.2",
"echarts": "^4.7.0",
"element-ui": "2.13.2",
"eslint-plugin-html": "^6.0.0",
"install": "^0.13.0",
"jquery": "^3.5.0",
"js-cookie": "2.2.0",
"less": "^3.10.3",
@@ -43,6 +45,7 @@
"lodash.values": "^4.3.0",
"moment-parseformat": "^3.0.0",
"normalize.css": "7.0.0",
"npm": "^7.8.0",
"nprogress": "0.2.0",
"path-to-regexp": "2.4.0",
"vue": "2.6.10",
@@ -73,6 +76,7 @@
"babel-eslint": "10.0.1",
"babel-jest": "23.6.0",
"chalk": "2.4.2",
"compression-webpack-plugin": "^6.1.1",
"connect": "3.6.6",
"element-theme-chalk": "^2.13.1",
"eslint": "^5.15.3",

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

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

View File

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

View File

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

View File

@@ -40,6 +40,10 @@ export default {
default(row, index) {
return true
}
},
disabled: {
type: [Boolean, Function],
default: false
}
},
data() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,15 +1,25 @@
<template>
<DataTable v-if="!loading" ref="dataTable" v-loading="loading" :config="iConfig" v-bind="$attrs" v-on="$listeners" @filter-change="filterChange" />
<div>
<DataTable v-if="!loading" ref="dataTable" v-loading="loading" :config="iConfig" v-bind="$attrs" v-on="$listeners" @filter-change="filterChange" />
<ColumnSettingPopover
:current-columns="popoverColumns.currentCols"
:total-columns-list="popoverColumns.totalColumnsList"
:min-columns="popoverColumns.minCols"
@columnsUpdate="handlePopoverColumnsChange"
/>
</div>
</template>
<script type="text/jsx">
import DataTable from '../DataTable'
import { DateFormatter, DetailFormatter, DisplayFormatter, BooleanFormatter, ActionsFormatter } from '@/components/ListTable/formatters'
import i18n from '@/i18n/i18n'
import ColumnSettingPopover from './components/ColumnSettingPopover'
export default {
name: 'AutoDataTable',
components: {
DataTable
DataTable,
ColumnSettingPopover
},
props: {
config: {
@@ -27,8 +37,18 @@ export default {
method: 'get',
autoConfig: {},
iConfig: {},
meta: {}
meta: {},
cleanedColumnsShow: {},
totalColumns: [],
popoverColumns: {
totalColumnsList: [],
minCols: [],
currentCols: []
}
}
},
computed: {
},
watch: {
config: {
@@ -44,10 +64,18 @@ export default {
},
methods: {
async optionUrlMetaAndGenCols() {
if (this.config.url === '') { return }
const url = (this.config.url.indexOf('?') === -1) ? `${this.config.url}?draw=1&display=1` : `${this.config.url}&draw=1&display=1`
this.$store.dispatch('common/getUrlMeta', { url: url }).then(data => {
this.meta = data.actions[this.method.toUpperCase()] || {}
this.generateColumns()
const method = this.method.toUpperCase()
this.meta = data.actions && data.actions[method] ? data.actions[method] : {}
this.generateTotalColumns()
}).then(() => {
// 根据当前列重新生成最终渲染表格
this.filterShowColumns()
}).then(() => {
// 生成给子组件使用的TotalColList
this.generatePopoverColumns()
}).catch((error) => {
this.$log.error('Error occur: ', error)
}).finally(() => {
@@ -63,7 +91,7 @@ export default {
break
case 'actions':
col = {
prop: 'id',
prop: 'actions',
label: i18n.t('common.Actions'),
align: 'center',
width: '150px',
@@ -165,7 +193,7 @@ export default {
col = this.addFilterIfNeed(col)
return col
},
generateColumns() {
generateTotalColumns() {
const config = _.cloneDeep(this.config)
const columns = []
for (let col of config.columns) {
@@ -176,9 +204,78 @@ export default {
columns.push(col)
}
}
// 第一次初始化时记录 totalColumns
this.totalColumns = columns
config.columns = columns
this.iConfig = config
},
// 生成给子组件使用的TotalColList
cleanColumnsShow() {
const totalColumnsNames = this.totalColumns.map(obj => obj.prop)
// 默认列
let defaultColumnsNames = _.get(this.iConfig, 'columnsShow.default', [])
if (defaultColumnsNames.length === 0) {
defaultColumnsNames = totalColumnsNames
}
// Clean it
defaultColumnsNames = totalColumnsNames.filter(n => defaultColumnsNames.indexOf(n) > -1)
// 最小列
const minColumnsNames = _.get(this.iConfig, 'columnsShow.min', ['action', 'id'])
.filter(n => defaultColumnsNames.indexOf(n) > -1)
// 应该显示的列
const _tableConfig = localStorage.getItem('tableConfig')
? JSON.parse(localStorage.getItem('tableConfig'))
: {}
const configShowColumnsNames = _.get(_tableConfig[this.$route.name], 'showColumns', null)
let showColumnsNames = configShowColumnsNames || defaultColumnsNames
if (showColumnsNames.length === 0) {
showColumnsNames = totalColumnsNames
}
// 校对显示的列,是不是包含最小列
minColumnsNames.forEach((v, i) => {
if (showColumnsNames.indexOf(v) === -1) {
showColumnsNames.push(v)
}
})
// Clean it
showColumnsNames = totalColumnsNames.filter(n => showColumnsNames.indexOf(n) > -1)
this.cleanedColumnsShow = {
default: defaultColumnsNames,
show: showColumnsNames,
min: minColumnsNames,
configShow: configShowColumnsNames
}
this.$log.debug('Cleaned colums show: ', this.cleanedColumnsShow)
},
filterShowColumns() {
this.cleanColumnsShow()
this.iConfig.columns = this.totalColumns.filter(obj => {
return this.cleanedColumnsShow.show.indexOf(obj.prop) > -1
})
},
generatePopoverColumns() {
this.popoverColumns.totalColumnsList = this.totalColumns.map(obj => {
return { prop: obj.prop, label: obj.label }
})
this.popoverColumns.currentCols = this.cleanedColumnsShow.show
this.popoverColumns.minCols = this.cleanedColumnsShow.min
this.$log.debug('Popover cols: ', this.popoverColumns)
},
handlePopoverColumnsChange(columns) {
// this.$log.debug('Columns change: ', columns)
this.popoverColumns.currentCols = columns
const _tableConfig = localStorage.getItem('tableConfig')
? JSON.parse(localStorage.getItem('tableConfig'))
: {}
_tableConfig[this.$route.name] = {
'showColumns': columns
}
localStorage.setItem('tableConfig', JSON.stringify(_tableConfig))
this.filterShowColumns()
},
filterChange(filters) {
const key = Object.keys(filters)[0]
const attr = {}

View File

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

View File

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

View File

@@ -4,7 +4,7 @@
:content="fields"
:form="basicForm"
label-position="right"
label-width="17%"
label-width="20%"
v-bind="$attrs"
v-on="$listeners"
>
@@ -47,7 +47,7 @@ export default {
// 初始值
form: {
type: Object,
default: () => { return {} }
default: () => ({})
},
moreButtons: {
type: Array,

View File

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

View File

@@ -167,7 +167,7 @@ import getLocatedSlotKeys from './utils/extract-keys'
import transformSearchImmediatelyItem from './utils/search-immediately-item'
import isFalsey from './utils/is-falsey'
import merge from 'deepmerge'
const defaultFirstPage = 0
const defaultFirstPage = 1
const noPaginationDataPath = 'payload'
export default {
@@ -723,6 +723,10 @@ export default {
default(row, index) {
return true
}
},
totalData: {
type: Array,
default: null
}
},
data() {
@@ -810,6 +814,13 @@ export default {
},
_searchForm() {
return transformSearchImmediatelyItem(this.collapseForm, this)
},
lastPageNum() {
// page
const pageOffset = this.firstPage - defaultFirstPage
const pageCount = Math.ceil(this.total / this.size)
const lastPageNum = pageCount + pageOffset
return lastPageNum
}
},
watch: {
@@ -828,6 +839,13 @@ export default {
* @property {array} rows - 已选中的行数据的数组
*/
this.$emit('selection-change', val)
},
totalData(val) {
if (val) {
this.page = defaultFirstPage
this.total = val.length
this.getList()
}
}
},
mounted() {
@@ -877,12 +895,54 @@ export default {
}
return query
},
getPageData() {
return this.data
},
async gotoNextPage() {
if (!this.hasNextPage()) {
return false
}
this.page += 1
await this.getList({ loading: true })
},
hasNextPage() {
return this.page < this.lastPageNum
},
getList({ loading = true } = {}) {
const { url } = this
if (url) {
return this.getListFromRemote({ loading: loading })
}
if (this.totalData) {
return this.getListFromStaticData({ loading: true })
}
// this.$log.debug("last page is: ", this.lastPageNum)
},
getListFromStaticData({ loading = true } = {}) {
if (loading) {
this.loading = true
}
if (!this.hasPagination) {
this.data = this.totalData
this.loading = false
return this.data
}
// page
const pageOffset = this.firstPage - defaultFirstPage
const page = this.page === 0 ? 1 : this.page
const start = (page + pageOffset - 1) * this.size
const end = (page + pageOffset) * this.size
this.$log.debug(`page: ${page}, size: ${this.size}, start: ${start}, end: ${end}`)
this.data = this.totalData.slice(start, end)
this.loading = false
return this.data
},
/**
* 手动刷新列表数据,选项的默认值为: { loading: true }
* @public
* @param {object} options 方法选项
*/
getList({ loading = true } = {}) {
getListFromRemote({ loading = true } = {}) {
const { url } = this
if (!url) {
return

View File

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

View File

@@ -95,6 +95,8 @@ export default {
vm.zTree.destroy()
}
this.zTree = $.fn.zTree.init($(`#${this.iZTreeID}`), this.treeSetting, res)
// 手动上报事件, Tree加载完成
this.$emit('TreeInitFinish', this.zTree)
if (this.treeSetting.showRefresh) {
this.rootNodeAddDom(
this.zTree,

View File

@@ -1,5 +1,5 @@
<template>
<ZTree ref="ztree" :setting="treeSetting">
<ZTree ref="ztree" :setting="treeSetting" v-on="$listeners">
<!--Slot透传-->
<div slot="rMenu" slot-scope="{data}">
<slot name="rMenu" :data="data" />

View File

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

View File

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

View File

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

View File

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

View File

@@ -37,6 +37,13 @@ export default {
this.$eventBus.$emit('showImportDialog', { selectedRows })
}
},
hasColumnSetting: defaultTrue,
handleColumnConfig: {
type: Function,
default: function() {
this.$eventBus.$emit('showColumnSettingPopover')
}
},
hasRefresh: defaultTrue,
selectedRows: {
type: Array,
@@ -54,6 +61,7 @@ export default {
data() {
return {
defaultRightSideActions: [
{ name: 'actionColumnSetting', fa: 'fa-cog', tip: this.$t('common.CustomCol'), has: this.hasColumnSetting, callback: this.handleColumnConfig.bind(this) },
{ name: 'actionExport', fa: 'fa-download', tip: this.$t('common.Export'), has: this.hasExport, callback: this.handleExport.bind(this) },
{ name: 'actionImport', fa: 'fa-upload', tip: this.$t('common.Import'), has: this.hasImport, callback: this.handleImport.bind(this) },
{ name: 'actionRefresh', fa: 'fa-refresh', tip: this.$t('common.Refresh'), has: this.hasRefresh, callback: this.handleRefresh }

View File

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

View File

@@ -81,11 +81,15 @@ export default {
default: function() {
return {
hasUpdate: true, // can set function(row, value)
canUpdate: true, // can set function(row, value)
canUpdate: () => {
return !this.$store.getters.currentOrgIsRoot
}, // can set function(row, value)
hasDelete: true, // can set function(row, value)
canDelete: true,
hasClone: false,
canClone: true,
hasClone: true,
canClone: () => {
return !this.$store.getters.currentOrgIsRoot
},
updateRoute: this.$route.name.replace('List', 'Update'),
cloneRoute: this.$route.name.replace('List', 'Create'),
performDelete: defaultPerformDelete,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -42,21 +42,9 @@ export default {
}
},
computed: {
dataTable() {
return this.$refs.dataTable.$refs.dataTable
},
hasCreateAction() {
const hasLeftAction = this.headerActions.hasLeftActions
if (hasLeftAction === false) {
return false
}
const hasCreate = this.headerActions.hasCreate
if (hasCreate === false) {
return false
}
return true
},
iTableConfig() {
const config = deepmerge(this.tableConfig, { extraQuery: this.extraQuery })
this.$log.debug('Header actions', this.headerActions)
@@ -86,9 +74,11 @@ export default {
this.dataTable.getList()
},
search(attrs) {
this.$emit('TagSearch', attrs)
return this.dataTable.search(attrs, true)
},
filter(attrs) {
this.$emit('TagFilter', attrs)
this.$refs.dataTable.$refs.dataTable.search(attrs, true)
},
handleDateChange(attrs) {
@@ -102,6 +92,7 @@ export default {
date_from: attrs[0].toISOString(),
date_to: attrs[1].toISOString()
}
this.$emit('TagDateChange', attrs)
return this.dataTable.searchDate(query)
},
toggleRowSelection(row, isSelected) {

View File

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

View File

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

View File

@@ -1,7 +1,32 @@
{
"": "",
"acl": {
"name": "名称",
"username": "用户名",
"ip_group": "IP 组",
"action": "动作",
"priority": "优先级",
"date_created": "创建时间",
"created_by": "创建者",
"asset": "资产信息",
"users":"用户信息",
"system_user": "系统用户",
"username_group":"用户名",
"hostname_group":"资产名",
"asset_ip_group": "资产IP",
"system_users_name_group": "系统用户名称",
"system_users_protocol_group": "系统用户协议",
"system_users_username_group": "系统用户名",
"apply_login_asset": "申请登录资产",
"apply_login_system_user": "申请登录系统用户",
"apply_login_user": "申请登录用户",
"RuleDetail": "规则详情"
},
"applications": {
"": "",
"RemoteApp": "远程应用",
"Database": "数据库",
"Cloud": "云",
"applicationsType": {
"chrome": "Chrome",
"mysql_workbench": "MySQL Workbench",
@@ -154,7 +179,7 @@
"TestAssetsConnective": "测试资产可连接性",
"TestConnection": "测试连接",
"Type": "类型",
"UnselectedAssets": "未选择资产",
"UnselectedAssets": "未选择资产或所选择的资产不支持SSH协议连接",
"UnselectedNodes": "未选择节点",
"UpdateAssetUserToken": "更新资产用户认证信息",
"Username": "用户名",
@@ -164,13 +189,17 @@
"Version": "版本",
"command_filter_list": "命令过滤器列表",
"date_joined": "创建日期",
"sshKeyFingerprint": "SSH 指纹",
"ip": "IP",
"sshkey": "sshkey",
"GroupsHelpMessage": "请输入用户组,多个用户组使用逗号分隔(需填写已存在的用户组)",
"HomeHelpMessage": "默认家目录 /home/系统用户名: /home/username",
"Home": "家目录",
"LinuxUserAffiliateGroup": "用户附属组",
"ipDomain": "IP(域名)"
"ipDomain": "IP(域名)",
"HostProtocol": "主机协议",
"DatabaseProtocol": "数据库协议",
"OtherProtocol": "其它协议"
},
"audits": {
@@ -229,6 +258,10 @@
"EnterForSearch": "按回车进行搜索",
"Export": "导出",
"Import": "导入",
"ContinueImport": "继续导入",
"Continue": "继续",
"Stop": "停止",
"Finished": "完成",
"Refresh": "刷新",
"Info": "提示",
"MFAConfirm": "MFA 认证",
@@ -291,16 +324,25 @@
"fieldRequiredError": "这个字段是必填项",
"getErrorMsg": "获取失败",
"MFAErrorMsg": "MFA错误请检查",
"Total": "总共",
"Success": "成功",
"Failed": "失败",
"Pending": "等待",
"Status": "状态",
"InputEmailAddress": "请输入正确的邮箱地址",
"imExport": {
"ExportAll": "导出所有",
"ExportOnlyFiltered": "仅导出搜索结果",
"ExportOnlySelectedItems": "仅导出选择项",
"ExportRange": "导出范围",
"createSuccessMsg": "导入创建成功,总共:{count}",
"downloadImportTemplateMsg": "下载导入模板",
"downloadImportTemplateMsg": "下载创建模板",
"downloadUpdateTemplateMsg": "下载更新模板",
"onlyCSVFilesTips": "仅支持csv文件导入",
"updateSuccessMsg": "导入更新成功,总共:{count}"
"updateSuccessMsg": "导入更新成功,总共:{count}",
"uploadCsvLth10MHelpText": "只能上传 csv/xlsx, 且不超过 10M",
"dragUploadFileInfo": "将文件拖到此处,或点击上传",
"hasImportErrorItemMsg": "存在导入失败项,点击左侧 x 查看失败原因,点击表格编辑后,可以继续导入失败项"
},
"fileType": "文件类型",
"isValid": "有效",
@@ -452,6 +494,7 @@
"downloadFile": "下载文件",
"hostName": "主机名",
"isValid": "有效",
"isEffective": "起作用的",
"nodeCount": "节点数量",
"refreshFail": "刷新失败",
"refreshPermissionCache": "刷新授权缓存",
@@ -521,7 +564,16 @@
"KubernetesAppPermissionDetail": "Kubernetes授权详情",
"KubernetesAppPermissionUpdate": "更新Kubernetes授权规则",
"KubernetesAppUpdate": "更新Kubernetes",
"Acl": "访问控制",
"UserAclList": "用户登录",
"UserAclCreate": "创建用户登录规则",
"UserAclLists": "用户登录规则",
"UserAclUpdate": "更新用户登录规则",
"UserAclDetail": "用户登录规则详情",
"AssetAclList": "登录资产",
"AssetAclCreate": "创建资产登录规则",
"AssetAclUpdate": "更新资产登录规则",
"AssetAclDetail": "资产登录规则详情",
"DomainCreate": "创建网域",
"DomainDetail": "网域详情",
"DomainList": "网域列表",
@@ -624,6 +676,7 @@
"name": "名称",
"protocol": "协议",
"region": "地域",
"EsDisabled": "节点不可用, 请联系管理员",
"sessionActiveCount": "在线会话数量",
"systemCpuLoad": "CPU负载",
"systemDiskUsedPercent": "硬盘使用率",
@@ -652,7 +705,7 @@
"sessionMonitor": "监控",
"TerminateTaskSendSuccessMsg": "终断任务已下发,请稍后刷新查看",
"helpText": {
"esUrl": "提示:如果有多台主机,请使用逗号 ( , ) 进行分割。eg: http://www.jumpserver.a.com,http://www.jumpserver.b.com",
"esUrl": "提示:如果有多台主机,请使用逗号 ( , ) 进行分割。eg: http://www.jumpserver.a.com:3000,http://www.jumpserver.b.com:3000",
"esIndex": "es提供默认indexjumpserver",
"esDocType": "es默认文档类型command",
"s3Endpoint": "S3 格式: http://s3.{REGION_NAME}.amazonaws.com<br>S3(China) 格式: http://s3.{REGION_NAME}.amazonaws.com.cn<br>如: http://s3.cn-north-1.amazonaws.com.cn",
@@ -708,9 +761,11 @@
"emailTest": "测试连接",
"emailUserSSL": "使用SSL",
"emailUserTLS": "使用TLS",
"SecurityInsecureCommand": "危险命令告警",
"Insecure_Command_Alert": "危险命令告警",
"InsecureCommandAlert": "危险命令告警",
"SecurityInsecureCommandEmailReceiver": "告警接收邮件",
"MailSend": "邮件发送",
"LDAPServerInfo": "LDAP 服务器",
"LDAPUser": "LDAP 用户",
"helpText": {
"ApiKeyList": "使用api key签名请求头每个请求的头部是不一样的, 请查阅使用文档",
"authLdapSearchFilter": "可能的选项是(cn或uid或sAMAccountName=%(user)s)",
@@ -720,7 +775,7 @@
"emailCustomUserCreatedHonorific": "提示: 创建用户时,发送设置密码邮件的敬语 (例如: 您好)",
"emailCustomUserCreatedSignature": "提示: 邮件的署名 (例如: jumpserver)",
"emailCustomUserCreatedSubject": "提示: 创建用户时,发送设置密码邮件的主题 (例如: 创建用户成功)",
"emailEmailFrom": "提示发送邮件账号默认使用SMTP账号作为发送账号",
"emailEmailFrom": "",
"emailHostPassword": "提示一些邮件提供商需要输入的是Token",
"emailRecipient": "提示:仅用来作为测试邮件收件人",
"emailSubjectPrefix": "提示: 一些关键字可能会被邮件提供商拦截,如 跳板机、JumpServer",
@@ -997,6 +1052,7 @@
"AWS_Int": "AWS(国际)",
"HuaweiCloud": "华为云",
"Azure":"Azure(中国)",
"Azure_Int": "Azure(国际)",
"HostnameStrategy": "用于生成资产主机名。例如1. 实例名称 (instanceDemo)2. 实例名称和部分IP(后两位) (instanceDemo-250.1)",
"IsAlwaysUpdate": "资产信息保持最新",
"AccountCreate": "创建账户",

View File

@@ -1,7 +1,31 @@
{
"": "",
"acl": {
"name": "Name",
"username": "Username",
"ip_group": "IP group",
"action": "Action",
"priority": "Priority",
"date_created": "Date created",
"created_by": "Created by",
"asset": "Asset",
"system_user": "System user",
"username_group":"Username group",
"hostname_group":"Hostname group",
"asset_ip_group": "Asset ip group",
"system_users_name_group": "Systemusers name group",
"system_users_protocol_group": "Systemusers protocol group",
"system_users_username_group": "systemusers username group",
"apply_login_asset": "Apply login asset",
"apply_login_system_user": "Apply login system user",
"apply_login_user": "Apply login user",
"RuleDetail": "Rule detail"
},
"applications": {
"": "",
"RemoteApp": "Remote app",
"Database": "Database",
"Cloud": "Cloud",
"applicationsType": {
"chrome": "Chrome",
"mysql_workbench": "MySQL Workbench",
@@ -154,7 +178,7 @@
"TestAssetsConnective": "Test assets connective",
"TestConnection": "Test connection",
"Type": "Type",
"UnselectedAssets": "Unselected assets",
"UnselectedAssets": "No asset selected or the selected asset does not support SSH protocol connection",
"UnselectedNodes": "Unselected nodes",
"UpdateAssetUserToken": "Update asset user auth",
"Username": "Username",
@@ -164,13 +188,17 @@
"Version": "Version",
"command_filter_list": "Command filter list",
"date_joined": "Date joined",
"sshKeyFingerprint": "SSH fingerprint",
"ip": "IP",
"sshkey": "sshkey",
"GroupsHelpMessage": "Please fill in user groups, separated by commas if there are multiple user groups(Please fill in the existing user groups)",
"HomeHelpMessage": "Default home directory: /home/system username",
"Home": "Home",
"LinuxUserAffiliateGroup": "Linux user affiliate group",
"ipDomain": "IP(Domain)"
"ipDomain": "IP(Domain)",
"HostProtocol": "Host Protocol",
"DatabaseProtocol": "Database Protocol",
"Other Protocol": "Database Protocol"
},
"audits": {
"Hosts": "Host",
@@ -229,6 +257,10 @@
"EnterForSearch": "Press enter to search",
"Export": "Export",
"Import": "Import",
"ContinueImport": "ContinueImport",
"Continue": "Continue",
"Stop": "Stop",
"Finished": "Finished",
"Refresh": "Refresh",
"Info": "Info",
"MFAConfirm": "MFA Confirm",
@@ -249,11 +281,13 @@
"Reset": "Reset",
"Search": "Search",
"MFAErrorMsg": "MFA Errorplease check",
"InputEmailAddress": "Please enter your email address",
"Select": "Select",
"SelectFile": "Select file",
"Show": "Show",
"Submit": "Submit",
"Test": "Test",
"SaveAndAddAnother":"Save and add another",
"TestSuccessMsg": "Test Success",
"To": "To",
"Update": "Update",
@@ -290,6 +324,11 @@
"fieldRequiredError": "This field is required",
"getErrorMsg": "Get failed",
"fileType": "File type",
"Status": "Status",
"Total": "Total",
"Success": "Success",
"Failed": "Failed",
"Pending": "Pending",
"imExport": {
"ExportAll": "Export all",
"ExportOnlyFiltered": "Export only filtered",
@@ -299,7 +338,10 @@
"downloadImportTemplateMsg": "Download import template",
"downloadUpdateTemplateMsg": "Download update template",
"onlyCSVFilesTips": "Only csv supported",
"updateSuccessMsg": "Update success, total: {count}"
"updateSuccessMsg": "Update success, total: {count}",
"dragUploadFileInfo": "Drag file here or click to upload",
"uploadCsvLth10MHelpText": "csv/xlsx files with a size less than 10M",
"hasImportErrorItemMsg": "There is an error item, click the x icon to view the details, and continue to import after editing"
},
"isValid": "Is valid",
"nav": {
@@ -450,6 +492,7 @@
"downloadFile": "Download file",
"hostName": "Hostname",
"isValid": "Validity",
"isEffective": "Effective",
"nodeCount": "Node count",
"refreshFail": "Refresh fail",
"refreshPermissionCache": "Refresh permission cache",
@@ -519,6 +562,16 @@
"KubernetesAppPermissionDetail": "Kubernetes permissions detail",
"KubernetesAppPermissionUpdate": "Kubernetes permissions update",
"KubernetesAppUpdate": "Kubernetes app update",
"Acl": "Access control",
"UserAclList": "User acl list",
"UserAclCreate": "User acl create",
"UserAclUpdate": "User acl update",
"UserAclLists": "User acl lists",
"UserAclDetail": "User acl detail",
"AssetAclList": "Asset acl list",
"AssetAclCreate": "Asset acl create",
"AssetAclUpdate": "Asset acl update",
"AssetAclDetail": "Asset acl detail",
"DomainCreate": "Domain create",
"DomainDetail": "Domain detail",
"DomainList": "Domains",
@@ -549,6 +602,10 @@
"RemoteAppPermissionCreate": "Remote apps permission create",
"RemoteAppPermissionDetail": "Remote apps permissions detail",
"RemoteAppPermissionUpdate": "Remote app permission update",
"ApplicationDetail": "Application detail",
"ApplicationPermissionCreate": "Application permission create",
"ApplicationPermissionDetail": "Application permission detail",
"ApplicationPermissionUpdate": "Application permission update",
"RemoteAppUpdate": "Remote app update",
"ReplayStorageUpdate": "Replay storage update",
"SessionDetail": "Sessions detail",
@@ -609,6 +666,7 @@
"systemCpuLoad": "cpu load",
"systemDiskUsedPercent": "disk used percent",
"systemMemoryUsedPercent": "memory used percent",
"EsDisabled": "Node is unavailable, please contact administrator",
"go": "Go",
"goto": "Goto",
"hosts": "Hosts",
@@ -701,6 +759,10 @@
"emailTest": "Test connection",
"emailUserSSL": "Use SSL",
"emailUserTLS": "Use TLS",
"MailSend": "Mail send",
"LDAPServerInfo": "LDAP Server",
"LDAPUser": "LDAP User",
"InsecureCommandAlert": "Insecure command alert",
"helpText": {
"ApiKeyList": "The API key is used to sign the request header. The header of each request is different. Please refer to the usage documentation",
"authLdapSearchFilter": "Choice may be (cn|uid|sAMAccountName)=%(user)s)",
@@ -985,6 +1047,7 @@
"AWS_Int": "AWS(International)",
"HuaweiCloud": "Huawei Cloud",
"Azure":"Azure(China)",
"Azure_Int": "Azure(International)",
"HostnameStrategy": "Used to produce the asset hostname. For example, 1. Instance name (instanceDemo)2. Instance name and Partial IP (instanceDemo-250.1)",
"IsAlwaysUpdate": "Asset info is kept up-to-date",
"AccountCreate": "Create account",

View File

@@ -15,7 +15,6 @@
</template>
<script>
import AutoDataForm from '@/components/AutoDataForm'
import deepmerge from 'deepmerge'
export default {
name: 'GenericCreateUpdateForm',
components: {
@@ -37,6 +36,10 @@ export default {
type: Object,
default: () => ({})
},
afterGetFormValue: {
type: Function,
default: (value) => value
},
// 提交前清理form的值
cleanFormValue: {
type: Function,
@@ -61,13 +64,14 @@ export default {
return this.$t('common.createSuccessMsg')
}
},
// 更新成功的msg
// 保存成功,继续添加的msg
saveSuccessContinueMsg: {
type: String,
default: function() {
return this.$t('common.saveSuccessContinueMsg')
}
},
// 更新成功的msg
updateSuccessMsg: {
type: String,
default: function() {
@@ -93,7 +97,9 @@ export default {
objectDetailRoute: {
type: Object,
default: function() {
const routeName = this.$route.name.replace('Update', 'Detail').replace('Create', 'Detail')
const routeName = this.$route.name
.replace('Update', 'Detail')
.replace('Create', 'Detail')
return { name: routeName }
}
},
@@ -233,7 +239,8 @@ export default {
try {
const values = await this.getFormValue()
this.$log.debug('Final object is: ', values)
this.form = Object.assign(this.form, values)
const formValue = Object.assign(this.form, values)
this.form = this.afterGetFormValue(formValue)
} finally {
this.loading = false
}
@@ -273,10 +280,7 @@ export default {
}
}
if (object) {
if (object['attrs']) {
object = deepmerge(object, object['attrs'])
}
this.$log.debug('Object is: ', object)
object = _.cloneDeep(object)
this.$emit('update:object', object)
}
return object

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -61,7 +61,7 @@ export default {
this.$refs.ListTable.reloadTable()
this.$message.success(this.$t('common.deleteSuccessMsg'))
}).catch(error => {
this.$message.error(this.$t('common.deleteErrorMsg' + ' ' + error))
this.$message.error(this.$t('common.deleteErrorMsg') + ' ' + error)
})
}.bind(this),
extraActions: [

View File

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

View File

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

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

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

View File

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

View File

@@ -44,7 +44,7 @@ export default [
// meta: { title: i18n.t('route.CeleryTaskLog') }
// },
{
path: `${BASE_URL}/core/flower?_=${Date.now()}`,
path: `${BASE_URL}/core/flower/?_=${Date.now()}`,
name: 'TaskMonitor',
// component: () => window.open(`/core/flower?_=${Date.now()}`),
meta: { title: i18n.t('route.TaskMonitor'), permissions: [rolec.PERM_SUPER] }

View File

@@ -28,6 +28,13 @@ export default [
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',

View File

@@ -103,6 +103,13 @@ export default [
meta: { title: i18n.t('route.TicketDetail'), activeMenu: '/tickets', permissions: [rolec.PERM_USE] },
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', permissions: [rolec.PERM_USE] },
hidden: true
},
{
path: 'tickets/request-application-perm/create',
name: 'RequestApplicationPermTicketCreateUpdate',

View File

@@ -1,4 +1,5 @@
import i18n from '@/i18n/i18n'
import empty from '@/layout/empty'
export default [
{
path: 'users',
@@ -53,5 +54,42 @@ export default [
name: 'UserGroupDetail',
hidden: true,
meta: { title: i18n.t('route.UserGroupDetail'), activeMenu: '/users/groups' }
},
{
path: 'user-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' },
hidden: true
},
{
path: ':id',
name: 'UserAclDetail',
component: () => import('@/views/acl/UserAcl/UserAclDetail'),
meta: { title: i18n.t('route.UserAclDetail'), activeMenu: '/users/users' },
hidden: true
},
{
path: ':id/update',
name: 'UserAclUpdate',
component: () => import('@/views/acl/UserAcl/UserAclCreateUpdate'),
meta: { title: i18n.t('route.UserAclUpdate') },
hidden: true
}
]
}
]

View File

@@ -3,8 +3,11 @@ const getters = {
device: state => state.app.device,
token: state => state.users.token,
currentOrg: state => state.users.currentOrg,
currentOrgIsDefault: state => state.users.currentOrg.is_default,
currentOrgIsRoot: state => {
return state.users.currentOrg && state.users.currentOrg.is_root
},
currentRole: state => state.users.currentRole,
userAdminOrgList: state => state.users.orgs,
currentUser: state => state.users.profile,
permission_routes: state => state.permission.routes,
visitedViews: state => state.tagsView.visitedViews,
@@ -14,6 +17,17 @@ const getters = {
currentOrgPerms: state => state.users.perms,
MFAVerifyAt: state => state.users.MFAVerifyAt,
MFA_TTl: state => state.settings.publicSettings.SECURITY_MFA_VERIFY_TTL,
tableConfig: state => state.table.tableConfig
tableConfig: state => state.table.tableConfig,
currentUserIsSuperAdmin: state => {
return state.users.sysRole === 'Admin'
},
hasValidLicense: state => state.settings.hasValidLicense,
userAdminOrgList: (state, getters) => {
let orgs = state.users.orgs
if (!getters.hasValidLicense) {
orgs = orgs.filter(org => !org.is_root)
}
return orgs
}
}
export default getters

View File

@@ -19,7 +19,7 @@ function hasPermission(roles, route) {
}
function hasLicense(route, rootState) {
const licenseIsValid = rootState.settings.publicSettings.XPACK_LICENSE_IS_VALID
const licenseIsValid = rootState.settings.hasValidLicense
const licenseRequired = route.meta ? route.meta.licenseRequired : false
if (!licenseIsValid && licenseRequired) {
return false

View File

@@ -8,7 +8,8 @@ const state = {
fixedHeader: fixedHeader,
sidebarLogo: sidebarLogo,
tagsView: tagsView,
publicSettings: null
publicSettings: null,
hasValidLicense: false
}
const mutations = {
@@ -19,6 +20,10 @@ const mutations = {
},
SET_PUBLIC_SETTINGS: (state, settings) => {
state.publicSettings = settings
if (settings['XPACK_ENABLED']) {
state.hasValidLicense = settings['XPACK_LICENSE_IS_VALID']
}
}
}

View File

@@ -1,8 +1,8 @@
import VueCookie from 'vue-cookie'
import Vue from 'vue'
function getTableConfigfromCookie() {
return VueCookie.get('tableConfig') ? JSON.parse(VueCookie.get('tableConfig')) : {}
return localStorage.getItem('tableConfig') ? JSON.parse(localStorage.getItem('tableConfig')) : {}
}
const state = {
@@ -11,8 +11,9 @@ const state = {
const mutations = {
SET_TABLE_CONFIG: (state, tableConfig) => {
Vue.set(state.tableConfig, tableConfig.key, tableConfig.value)
VueCookie.set('tableConfig', JSON.stringify(state.tableConfig), 14)
const _tableConfig = localStorage.getItem('tableConfig') ? JSON.parse(localStorage.getItem('tableConfig')) : {}
Vue.set(_tableConfig, tableConfig.key, tableConfig.value)
localStorage.setItem('tableConfig', JSON.stringify(_tableConfig))
}
}

View File

@@ -16,9 +16,11 @@ const getDefaultState = () => {
currentRole: getCurrentRoleFromCookie(),
profile: {},
roles: {},
sysRole: '',
orgs: [],
perms: 0b00000000,
MFAVerifyAt: null
MFAVerifyAt: null,
isSuperAdmin: false
}
}
@@ -52,6 +54,9 @@ const mutations = {
SET_ROLES(state, roles) {
state.roles = roles
},
SET_SYS_ROLE(state, role) {
state.sysRole = role
},
SET_PERMS(state, perms) {
state.perms = perms
},
@@ -111,6 +116,7 @@ const actions = {
return dispatch('getProfile').then((profile) => {
const { current_org_roles: currentOrgRoles, role } = profile
const roles = rolec.parseUserRoles(currentOrgRoles, role)
commit('SET_SYS_ROLE', role)
commit('SET_ROLES', roles)
commit('SET_PERMS', rolec.sumPerms(roles))
resolve(roles)

View File

@@ -454,3 +454,7 @@ a {
.el-table {
font-size: 13px;
}
.el-table-filter__list-item:hover {
color: $--color-text-primary;
}

View File

@@ -59,8 +59,8 @@ export default {
name: 'connect',
fa: 'fa-terminal',
type: 'primary',
callback: function({ row, col, cellValue, reload }) {
window.open(`/luna/?type=database_app&login_to=${cellValue}`, '_blank')
callback: function({ row }) {
window.open(`/luna/?type=database_app&login_to=${row.id}`, '_blank')
}
}
]

View File

@@ -59,8 +59,8 @@ export default {
name: 'connect',
fa: 'fa-terminal',
type: 'primary',
callback: function({ row, col, cellValue, reload }) {
window.open(`/luna/?type=k8s_app&login_to=${cellValue}`, '_blank')
callback: function({ row }) {
window.open(`/luna/?type=k8s_app&login_to=${row.id}`, '_blank')
}
}
]

View File

@@ -58,8 +58,8 @@ export default {
name: 'connect',
fa: 'fa-terminal',
type: 'primary',
callback: function({ row, col, cellValue, reload }) {
window.open(`/luna/?type=remote_app&login_to=${cellValue}`, '_blank')
callback: function({ row }) {
window.open(`/luna/?type=remote_app&login_to=${row.id}`, '_blank')
}
}
]

View File

@@ -121,20 +121,20 @@ export default {
return row.is_active
},
callback: function({ row, col, cellValue, reload }) {
window.open(`/luna/?login_to=${cellValue}`, '_blank')
window.open(`/luna/?login_to=${row.id}`, '_blank')
}
},
{
name: 'favor',
type: 'info',
fa: function(row, cellValue) {
if (this.checkFavorite(cellValue)) {
if (this.checkFavorite(row.id)) {
return 'fa-star'
}
return 'fa-star-o'
}.bind(this),
callback: function({ row, col, cellValue, reload }) {
this.addOrDeleteFavorite(cellValue)
this.addOrDeleteFavorite(row.id)
}.bind(this)
}
]

View File

@@ -1,3 +1,5 @@
const _ = require('lodash')
function getTimeUnits(u) {
const units = {
'd': '天',
@@ -187,8 +189,28 @@ export function getDayFuture(days, now) {
return new Date(now.getTime() + 3600 * 1000 * 24 * days)
}
export function getErrorResponseMsg(error) {
let msg = ''
const data = error.response && error.response.data || {}
if (data && (data.error || data.msg || data.detail)) {
msg = data.error || data.msg || data.detail
}
return msg
}
export function sleep(time) {
return new Promise((resolve) => setTimeout(resolve, time))
}
function customizer(objValue, srcValue) {
return _.isUndefined(objValue) ? srcValue : objValue
}
export const assignIfNot = _.partialRight(_.assignInWith, customizer)
const scheme = document.location.protocol
const port = document.location.port ? ':' + document.location.port : ''
const BASE_URL = scheme + '//' + document.location.hostname + port
export { BASE_URL }

View File

@@ -1,13 +1,13 @@
import { hasUUID, BASE_URL } from '@/utils/common'
import { getOrgDetail } from '@/api/orgs'
import store from '@/store'
export const DEFAULT_ORG_ID = '00000000-0000-0000-0000-000000000002'
// const ROOT_ORG_ID = '00000000-0000-0000-0000-000000000000'
function getPropOrg() {
const userAdminOrgList = store.getters.userAdminOrgList
let defaultOrg = userAdminOrgList.find((item) => item.id === '')
if (defaultOrg) {
return defaultOrg
}
defaultOrg = userAdminOrgList.find((item) => item.id === 'DEFAULT')
const defaultOrg = userAdminOrgList.find((item) => item.is_default)
if (defaultOrg) {
return defaultOrg
}
@@ -19,14 +19,14 @@ function change2PropOrg() {
setTimeout(() => changeOrg(org.id), 100)
}
function getOrgIdMapper() {
const mapper = {}
const userAdminOrgList = store.getters.userAdminOrgList
userAdminOrgList.forEach((v) => {
mapper[v.id] = v
})
return mapper
}
// function getOrgIdMapper() {
// const mapper = {}
// const userAdminOrgList = store.getters.userAdminOrgList
// userAdminOrgList.forEach((v) => {
// mapper[v.id] = v
// })
// return mapper
// }
function hasCurrentOrgPermission() {
const currentOrg = store.getters.currentOrg
@@ -37,7 +37,7 @@ function hasCurrentOrgPermission() {
}
async function changeOrg(orgId) {
const org = getOrgIdMapper()[orgId]
const org = await getOrgDetail(orgId)
if (!org) {
console.debug('Error: org not found')
} else {
@@ -59,5 +59,6 @@ async function changeOrg(orgId) {
export default {
hasCurrentOrgPermission,
changeOrg,
DEFAULT_ORG_ID,
change2PropOrg
}

View File

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

View File

@@ -6,6 +6,7 @@ import 'nprogress/nprogress.css' // progress bar style
import { getTokenFromCookie } from '@/utils/auth'
import rolec from '@/utils/role'
import orgUtil from '@/utils/org'
import { getCurrentOrg } from '@/api/orgs'
const whiteList = ['/login', process.env.VUE_APP_LOGIN_PATH] // no redirect whitelist
let initial = false
@@ -34,9 +35,12 @@ async function checkLogin({ to, from, next }) {
try {
return await store.dispatch('users/getProfile')
} catch (e) {
setTimeout(() => {
window.location = process.env.VUE_APP_LOGIN_PATH
}, 100)
const status = e.response.status
if (status === 401 || status === 403) {
setTimeout(() => {
window.location = process.env.VUE_APP_LOGIN_PATH
}, 100)
}
return reject('No profile get: ' + e)
}
}
@@ -49,22 +53,29 @@ async function getPublicSetting({ to, from, next }) {
}
}
async function refreshCurrentOrg() {
getCurrentOrg().then(org => {
store.dispatch('users/setCurrentOrg', org)
})
}
async function changeCurrentOrgIfNeed({ to, from, next }) {
await store.dispatch('users/getInOrgs')
const adminOrgs = store.getters.userAdminOrgList
if (!adminOrgs || adminOrgs.length === 0) {
return
}
await refreshCurrentOrg()
const currentOrg = store.getters.currentOrg
if (!currentOrg || typeof currentOrg !== 'object') {
// console.log('Not has current org')
orgUtil.change2PropOrg()
return reject('change prop org')
return reject('Change prop org')
}
if (!orgUtil.hasCurrentOrgPermission()) {
console.debug('Not has current org permission')
orgUtil.change2PropOrg()
return reject('change prop org')
return reject('Change prop org')
}
}
@@ -133,11 +144,11 @@ export async function startup({ to, from, next }) {
initial = true
// set page title
await getPublicSetting({ to, from, next })
await setHeadTitle({ to, from, next })
await checkLogin({ to, from, next })
await changeCurrentOrgIfNeed({ to, from, next })
await changeCurrentRoleIfNeed({ to, from, next })
await getPublicSetting({ to, from, next })
await generatePageRoutes({ to, from, next })
await checkUserFirstLogin({ to, from, next })
return true

View File

@@ -0,0 +1,122 @@
<template>
<GenericCreateUpdatePage :fields="fields" :initial="initial" :fields-meta="fieldsMeta" :url="url" :perform-submit="performSubmit" :after-get-form-value="afterGetFormValue" />
</template>
<script>
import GenericCreateUpdatePage from '@/layout/components/GenericCreateUpdatePage'
export default {
name: 'AclCreateUpdate',
components: {
GenericCreateUpdatePage
},
data() {
return {
initial: {
action: 'login_confirm',
system_users: {
name_group: '*',
protocol_group: '*',
username_group: '*'
},
users: {
username_group: '*'
},
assets: {
hostname_group: '*',
ip_group: '*'
}
},
fields: [
[this.$t('common.Basic'), ['name', 'priority']],
[this.$t('acl.users'), ['users']],
[this.$t('acl.asset'), ['assets']],
[this.$t('acl.system_user'), ['system_users']],
[this.$t('acl.action'), ['action', 'reviewers']],
[this.$t('common.Other'), ['is_active', 'comment']]
],
fieldsMeta: {
assets: {
fields: ['hostname_group', 'ip_group']
},
users: {
fields: ['username_group'],
fieldsMeta: {
}
},
system_users: {
fields: ['name_group', 'username_group', 'protocol_group']
},
reviewers: {
el: {
value: [],
ajax: {
url: '/api/v1/users/users/?fields_size=mini',
transformOption: (item) => {
return { label: item.name + '(' + item.username + ')', value: item.id }
}
}
}
}
},
url: '/api/v1/acls/login-asset-acls/'
}
},
methods: {
getUrl() {
const params = this.$route.params
let url = `/api/v1/acls/login-asset-acls/`
if (params.id) {
url = `${url}${params.id}/`
} else {
url = `${url}`
}
return url
},
getMethod() {
const params = this.$route.params
if (params.id) {
return 'put'
} else {
return 'post'
}
},
afterGetFormValue(validValues) {
validValues.assets.ip_group = validValues.assets.ip_group.toString()
validValues.assets.hostname_group = validValues.assets.hostname_group.toString()
validValues.system_users.name_group = validValues.system_users.name_group.toString()
validValues.system_users.protocol_group = validValues.system_users.protocol_group.toString()
validValues.system_users.username_group = validValues.system_users.username_group.toString()
validValues.users.username_group = validValues.users.username_group.toString()
return validValues
},
performSubmit(validValues) {
if (!Array.isArray(validValues.assets.ip_group)) {
validValues.assets.ip_group = validValues.assets.ip_group ? validValues.assets.ip_group.split(',') : []
}
if (!Array.isArray(validValues.assets.hostname_group)) {
validValues.assets.hostname_group = validValues.assets.hostname_group ? validValues.assets.hostname_group.split(',') : []
}
if (!Array.isArray(validValues.system_users.protocol_group)) {
validValues.system_users.protocol_group = validValues.system_users.protocol_group ? validValues.system_users.protocol_group.split(',') : []
}
if (!Array.isArray(validValues.system_users.name_group)) {
validValues.system_users.name_group = validValues.system_users.name_group ? validValues.system_users.name_group.split(',') : []
}
if (!Array.isArray(validValues.system_users.username_group)) {
validValues.system_users.username_group = validValues.system_users.username_group ? validValues.system_users.username_group.split(',') : []
}
if (!Array.isArray(validValues.users.username_group)) {
validValues.users.username_group = validValues.users.username_group ? validValues.users.username_group.split(',') : []
}
const method = this.getMethod()
return this.$axios[method](`${this.getUrl()}`, validValues)
}
}
}
</script>
<style>
</style>

View File

@@ -0,0 +1,113 @@
<template>
<el-row :gutter="20">
<el-col :span="14">
<DetailCard :items="detailCardItems" />
</el-col>
<el-col :span="10">
<!-- <RelationCard ref="RelationCard" type="info" v-bind="nodeRelationConfig" />-->
</el-col>
</el-row>
</template>
<script>
import DetailCard from '@/components/DetailCard'
// import RelationCard from '@/components/RelationCard'
import { toSafeLocalDateStr } from '@/utils/common'
export default {
name: 'Detail',
components: {
DetailCard
// RelationCard
},
props: {
object: {
type: Object,
default: () => {}
}
},
data() {
return {
nodeRelationConfig: {
icon: 'fa-info',
title: this.$t('assets.ReplaceNodeAssetsAdminUserWithThis'),
objectsAjax: {
url: '/api/v1/assets/nodes/',
transformOption: (item) => {
return { label: item.full_value, value: item.id }
}
},
performAdd: (items) => {
const data = []
const relationUrl = `/api/v1/assets/admin-users/${this.object.id}/nodes/`
items.map(v => {
data.push(v.value)
})
return this.$axios.patch(relationUrl, { nodes: data }).then(res => {
this.$message.success(this.$t('common.updateSuccessMsg'))
}).catch(err => {
this.$message.error(this.$t('common.updateErrorMsg' + ' ' + err))
})
},
onAddSuccess: () => {
this.$refs.RelationCard.$refs.select2.clearSelected()
}
}
}
},
computed: {
detailCardItems() {
return [
{
key: this.$t('acl.name'),
value: this.object.name
},
{
key: this.$t('acl.username_group'),
value: this.object.users.username_group.toString()
},
{
key: this.$t('acl.hostname_group'),
value: this.object.assets.hostname_group.toString()
},
{
key: this.$t('acl.asset_ip_group'),
value: this.object.assets.ip_group.toString()
},
{
key: this.$t('acl.system_users_name_group'),
value: this.object.system_users.name_group.toString()
},
{
key: this.$t('acl.system_users_protocol_group'),
value: this.object.system_users.protocol_group.toString()
},
{
key: this.$t('acl.system_users_username_group'),
value: this.object.system_users.username_group.toString()
},
{
key: this.$t('acl.action'),
value: this.object.action_display
},
{
key: this.$t('acl.priority'),
value: this.object.priority
},
{
key: this.$t('acl.date_created'),
value: toSafeLocalDateStr(this.object.date_created)
},
{
key: this.$t('acl.created_by'),
value: this.object.created_by
}
]
}
}
}
</script>
<style lang='less' scoped>
</style>

View File

@@ -0,0 +1,41 @@
<template>
<GenericDetailPage :object.sync="TaskDetail" :active-menu.sync="config.activeMenu" v-bind="config" v-on="$listeners">
<keep-alive>
<component :is="config.activeMenu" :object="TaskDetail" />
</keep-alive>
</GenericDetailPage>
</template>
<script>
import { GenericDetailPage } from '@/layout/components'
import detail from './detail.vue'
export default {
components: {
GenericDetailPage,
detail
},
data() {
return {
TaskDetail: {},
config: {
activeMenu: 'detail',
submenu: [
{
title: this.$t('acl.RuleDetail'),
name: 'detail'
}
],
hasRightSide: true,
actions: {
detailApiUrl: `/api/v1/acls/login-asset-acls/${this.$route.params.id}/`,
deleteApiUrl: `/api/v1/acls/login-asset-acls/${this.$route.params.id}/`
}
}
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,90 @@
<template>
<GenericListPage :table-config="tableConfig" :header-actions="headerActions" />
</template>
<script>
import { GenericListPage } from '@/layout/components'
export default {
components: {
GenericListPage
},
data() {
return {
tableConfig: {
url: '/api/v1/acls/login-asset-acls/',
columns: ['name', 'user_username_group', 'hostname_group', 'ip_group', 'name_group', 'protocol_group', 'systemuser_username_group', 'reviewers', 'priority', 'is_active', 'comment', 'actions'],
columnsShow: {
min: ['name', 'actions'],
default: ['name', 'user_username_group', 'hostname_group', 'ip_group', 'reviewers', 'priority', 'is_active', 'comment', 'actions']
},
columnsMeta: {
user_username_group: {
prop: 'users.username_group',
showOverflowTooltip: true,
formatter: function(row) {
return <span> {row.users.username_group.toString()} </span>
},
label: this.$t('acl.username_group')
},
reviewers: {
prop: 'reviewers_amount'
},
hostname_group: {
prop: 'assets.hostname_group',
label: this.$t('acl.hostname_group'),
showOverflowTooltip: true,
formatter: function(row) {
return <span> {row.assets.hostname_group.toString()} </span>
}
},
ip_group: {
prop: 'assets.ip_group',
label: this.$t('acl.asset_ip_group'),
showOverflowTooltip: true,
formatter: function(row) {
return <span> {row.assets.ip_group.toString()} </span>
}
},
name_group: {
prop: 'system_users.name_group',
label: this.$t('acl.system_users_name_group'),
showOverflowTooltip: true,
formatter: function(row) {
return <span> {row.system_users.name_group.toString()} </span>
}
},
protocol_group: {
prop: 'system_users.protocol_group',
label: this.$t('acl.system_users_protocol_group'),
showOverflowTooltip: true,
formatter: function(row) {
return <span> {row.system_users.protocol_group.toString()} </span>
}
},
systemuser_username_group: {
prop: 'system_users.username_group',
label: this.$t('acl.system_users_username_group'),
showOverflowTooltip: true,
formatter: function(row) {
return <span> {row.system_users.username_group.toString()} </span>
}
}
}
},
updateRoute: 'AssetAclUpdate',
headerActions: {
createRoute: 'AssetAclCreate',
hasRefresh: true,
hasExport: false,
hasImport: false,
hasMoreActions: false
}
}
}
}
</script>
<style>
</style>

View File

@@ -0,0 +1,91 @@
<template>
<GenericCreateUpdatePage
v-bind="$data"
:perform-submit="performSubmit"
:after-get-form-value="afterGetFormValue"
:get-url="getUrl"
/>
</template>
<script>
import GenericCreateUpdatePage from '@/layout/components/GenericCreateUpdatePage'
export default {
name: 'AclCreateUpdate',
components: {
GenericCreateUpdatePage
},
data() {
return {
initial: {
action: 'reject',
ip_group: '*',
user: this.$route.query.user
},
fields: [
[this.$t('common.Basic'), ['name', 'user', 'ip_group', 'action', 'priority']],
[this.$t('common.Other'), ['is_active', 'comment']]
],
fieldsMeta: {
is_active: {
type: 'checkbox'
},
user: {
el: {
disabled: true,
multiple: false,
ajax: {
url: '/api/v1/users/users/?fields_size=mini',
transformOption: (item) => {
return { label: item.name + '(' + item.username + ')', value: item.id }
}
}
}
}
},
url: `/api/v1/acls/login-acls/`,
updateSuccessNextRoute: { name: 'UserDetail', params: {
id: this.$route.query.user
}},
createSuccessNextRoute: { name: 'UserDetail', params: {
id: this.$route.query.user
}}
}
},
methods: {
getMethod() {
const params = this.$route.params
if (params.id) {
return 'put'
} else {
return 'post'
}
},
getUrl() {
const params = this.$route.params
let url = this.url
if (params.id) {
url = `${url}${params.id}/?user=${this.$route.query.user}`
} else {
url = `${url}?user=${this.$route.query.user}`
}
return url
},
afterGetFormValue(validValues) {
validValues.ip_group = validValues.ip_group.toString()
return validValues
},
performSubmit(validValues) {
if (!Array.isArray(validValues.ip_group)) {
validValues.ip_group = validValues.ip_group ? validValues.ip_group.split(',') : []
}
const method = this.getMethod()
return this.$axios[method](`${this.getUrl()}`, validValues)
}
}
}
</script>
<style>
</style>

View File

@@ -0,0 +1,97 @@
<template>
<el-row :gutter="20">
<el-col :span="14">
<DetailCard :items="detailCardItems" />
</el-col>
<el-col :span="10">
<!-- <RelationCard ref="RelationCard" type="info" v-bind="nodeRelationConfig" />-->
</el-col>
</el-row>
</template>
<script>
import DetailCard from '@/components/DetailCard'
// import RelationCard from '@/components/RelationCard'
import { toSafeLocalDateStr } from '@/utils/common'
export default {
name: 'Detail',
components: {
DetailCard
// RelationCard
},
props: {
object: {
type: Object,
default: () => {}
}
},
data() {
return {
nodeRelationConfig: {
icon: 'fa-info',
title: this.$t('assets.ReplaceNodeAssetsAdminUserWithThis'),
objectsAjax: {
url: '/api/v1/assets/nodes/',
transformOption: (item) => {
return { label: item.full_value, value: item.id }
}
},
performAdd: (items) => {
const data = []
const relationUrl = `/api/v1/assets/admin-users/${this.object.id}/nodes/`
items.map(v => {
data.push(v.value)
})
return this.$axios.patch(relationUrl, { nodes: data }).then(res => {
this.$message.success(this.$t('common.updateSuccessMsg'))
}).catch(err => {
this.$message.error(this.$t('common.updateErrorMsg' + ' ' + err))
})
},
onAddSuccess: () => {
this.$refs.RelationCard.$refs.select2.clearSelected()
}
}
}
},
computed: {
detailCardItems() {
return [
{
key: this.$t('acl.name'),
value: this.object.name
},
{
key: this.$t('acl.username'),
value: this.object.user_display
},
{
key: this.$t('acl.ip_group'),
value: this.object.ip_group.toString()
},
{
key: this.$t('acl.action'),
value: this.object.action_display
},
{
key: this.$t('acl.priority'),
value: this.object.priority
},
{
key: this.$t('acl.date_created'),
value: toSafeLocalDateStr(this.object.date_created)
},
{
key: this.$t('acl.created_by'),
value: this.object.created_by
}
]
}
}
}
</script>
<style lang='less' scoped>
</style>

View File

@@ -0,0 +1,41 @@
<template>
<GenericDetailPage :object.sync="TaskDetail" :active-menu.sync="config.activeMenu" v-bind="config" v-on="$listeners">
<keep-alive>
<component :is="config.activeMenu" :object="TaskDetail" />
</keep-alive>
</GenericDetailPage>
</template>
<script>
import { GenericDetailPage } from '@/layout/components'
import Detail from './Detail.vue'
export default {
components: {
GenericDetailPage,
Detail
},
data() {
return {
TaskDetail: {},
config: {
activeMenu: 'Detail',
submenu: [
{
title: this.$t('acl.RuleDetail'),
name: 'Detail'
}
],
hasRightSide: false,
actions: {
detailApiUrl: `/api/v1/acls/login-acls/${this.$route.params.id}/?user=${this.$route.query.user}`,
deleteApiUrl: `/api/v1/acls/login-acls/${this.$route.params.id}/?user=${this.$route.query.user}`
}
}
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,65 @@
<template>
<GenericListTable :table-config="tableConfig" :header-actions="headerActions" />
</template>
<script>
import GenericListTable from '@/layout/components/GenericListTable'
import { ArrayFormatter } from '@/components/ListTable/formatters'
export default {
components: {
GenericListTable
},
data() {
return {
tableConfig: {
url: `/api/v1/acls/login-acls/?user=${this.$route.params.id}`,
columns: ['name', 'ip_group', 'priority', 'action', 'is_active', 'comment', 'actions'],
columnsShow: {
min: ['name', 'actions'],
default: ['name', 'ip_group', 'priority', 'action', 'is_active', 'comment', 'actions']
},
columnsMeta: {
name: {
formatterArgs: {
route: 'UserAclDetail',
routeQuery: {
user: this.$route.params.id
}
}
},
ip_group: {
formatter: ArrayFormatter,
showOverflowTooltip: true
},
action: {
prop: 'action_display'
},
actions: {
formatterArgs: {
hasClone: false,
updateRoute: { name: 'UserAclUpdate', query: { user: this.$route.params.id }},
performDelete: ({ row, col }) => {
const id = row.id
const url = `/api/v1/acls/login-acls/${id}/?user=${this.$route.params.id}`
return this.$axios.delete(url)
}
}
}
}
},
headerActions: {
createRoute: { name: 'UserAclCreate', query: { user: this.$route.params.id }},
hasRefresh: true,
hasExport: false,
hasImport: false,
hasMoreActions: false
}
}
}
}
</script>
<style>
</style>

View File

@@ -10,7 +10,6 @@ export default {
},
data() {
return {
fields: [
[this.$t('common.Basic'), ['name', 'type', 'domain']],
[this.$t('applications.DBInfo'), ['attrs']],
@@ -25,9 +24,6 @@ export default {
}],
disabled: true
},
host: {
type: 'input'
},
domain: {
el: {
multiple: false,
@@ -36,6 +32,14 @@ export default {
url: '/api/v1/assets/domains/'
}
}
},
attrs: {
fields: ['host', 'port', 'database'],
fieldsMeta: {
host: {
type: 'input'
}
}
}
},
url: '/api/v1/applications/applications/',
@@ -53,11 +57,6 @@ export default {
const baseUrl = `/api/v1/applications/applications/`
const url = (params.id) ? `${baseUrl}${params.id}/` : baseUrl
const method = this.getMethod()
validValues.attrs = {
host: validValues.host,
port: validValues.port,
database: validValues.database
}
validValues.category = 'db'
return this.$axios[method](`${url}?type=${validValues.type}`, validValues)
}

View File

@@ -4,19 +4,24 @@
<script>
import { GenericListPage } from '@/layout/components'
import { mapGetters } from 'vuex'
export default {
components: {
GenericListPage
},
data() {
const vm = this
return {
tableConfig: {
url: '/api/v1/applications/applications/?category=db',
columns: [
'name', 'type_display', 'attrs.host', 'attrs.port', 'attrs.database', 'comment', 'actions'
'name', 'type_display', 'attrs.host', 'attrs.port', 'attrs.database',
'created_by', 'date_created', 'date_updated', 'comment', 'org_name', 'actions'
],
columnsShow: {
min: ['name', 'actions'],
default: ['name', 'type_display', 'attrs.host', 'attrs.port', 'attrs.database', 'comment', 'actions']
},
columnsMeta: {
type_display: {
label: this.$t('applications.type'),
@@ -35,17 +40,19 @@ export default {
showOverflowTooltip: true
},
actions: {
prop: '',
prop: 'actions',
formatterArgs: {
hasClone: false,
onClone: ({ row }) => {
vm.$router.push({ name: 'DatabaseAppCreate', query: { type: row.type, clone_from: row.id }})
},
performDelete: function({ row, col, cellValue, reload }) {
this.$axios.delete(
`/api/v1/applications/applications/${row.id}/`
).then(res => {
this.$refs.GenericListTable.$refs.ListTable.reloadTable()
this.$refs.GenericListTable.$refs.ListTable.$refs.ListTable.reloadTable()
// this.$message.success(this.$t('common.deleteSuccessMsg'))
}).catch(error => {
this.$message.error(this.$t('common.deleteErrorMsg' + ' ' + error))
this.$message.error(this.$t('common.deleteErrorMsg') + ' ' + error)
})
}.bind(this)
}
@@ -56,65 +63,38 @@ export default {
hasCreate: false,
hasExport: false,
hasImport: false,
hasBulkDelete: false,
hasMoreActions: false,
createRoute: 'DatabaseAppCreate',
moreActionsTitle: this.$t('common.Create'),
moreActionsType: 'primary',
extraMoreActions: [
{
name: 'MySQL',
title: 'MySQL',
type: 'primary',
has: true,
callback: this.createMysql.bind(this)
moreCreates: {
callback: (item) => {
vm.$router.push({ name: 'DatabaseAppCreate', query: { type: item.name.toLowerCase() }})
},
{
name: 'PostgreSQL',
title: 'PostgreSQL',
type: 'primary',
has: this.isValidateLicense,
callback: this.createPostgreSQL.bind(this)
},
{
name: 'MariaDB',
title: 'MariaDB',
type: 'primary',
has: this.isValidateLicense,
callback: this.createMariaDB.bind(this)
},
{
name: 'Oracle',
title: 'Oracle',
type: 'primary',
has: this.isValidateLicense,
callback: this.createOracle.bind(this)
}
]
dropdown: [
{
name: 'MySQL',
title: 'MySQL',
has: true
},
{
name: 'PostgreSQL',
title: 'PostgreSQL',
has: this.$store.getters.hasValidLicense
},
{
name: 'MariaDB',
title: 'MariaDB',
type: 'primary',
has: this.$store.getters.hasValidLicense
},
{
name: 'Oracle',
title: 'Oracle',
has: this.$store.getters.hasValidLicense
}
]
}
}
}
},
computed: {
...mapGetters(['publicSettings', 'currentOrg'])
},
methods: {
createMysql() {
this.$router.push({ name: 'DatabaseAppCreate', query: { type: 'mysql' }})
},
createPostgreSQL() {
this.$router.push({ name: 'DatabaseAppCreate', query: { type: 'postgresql' }})
},
createMariaDB() {
this.$router.push({ name: 'DatabaseAppCreate', query: { type: 'mariadb' }})
},
createOracle() {
this.$router.push({ name: 'DatabaseAppCreate', query: { type: 'oracle' }})
},
isValidateLicense() {
if (this.publicSettings.XPACK_ENABLED) {
return this.publicSettings.XPACK_LICENSE_IS_VALID
}
return false
}
}
}
</script>

View File

@@ -23,8 +23,13 @@ export default {
type: {
disabled: true
},
cluster: {
helpText: this.$t('applications.clusterHelpTextMessage')
attrs: {
fields: ['cluster'],
fieldsMeta: {
cluster: {
helpText: this.$t('applications.clusterHelpTextMessage')
}
}
},
domain: {
el: {
@@ -50,9 +55,6 @@ export default {
const baseUrl = `/api/v1/applications/applications/`
const url = (params.id) ? `${baseUrl}${params.id}/` : baseUrl
const method = this.getMethod()
validValues.attrs = {
cluster: validValues.cluster
}
validValues.category = 'cloud'
return this.$axios[method](`${url}?type=${validValues.type}`, validValues)
}

View File

@@ -10,12 +10,18 @@ export default {
GenericListPage
},
data() {
const vm = this
return {
tableConfig: {
url: '/api/v1/applications/applications/?category=cloud',
columns: [
'name', 'type', 'attrs.cluster', 'comment', 'actions'
'name', 'type', 'attrs.cluster',
'created_by', 'date_created', 'date_updated', 'comment', 'org_name', 'actions'
],
columnsShow: {
min: ['name', 'actions'],
default: ['name', 'type', 'attrs.cluster', 'comment', 'actions']
},
columnsMeta: {
'attrs.cluster': {
label: this.$t('applications.cluster')
@@ -27,17 +33,19 @@ export default {
width: '140px'
},
actions: {
prop: '',
prop: 'actions',
formatterArgs: {
hasClone: false,
onClone: ({ row }) => {
vm.$router.push({ name: 'KubernetesAppCreate', query: { type: row.type, clone_from: row.id }})
},
performDelete: function({ row, col, cellValue, reload }) {
this.$axios.delete(
`/api/v1/applications/applications/${row.id}/`
).then(res => {
this.$refs.GenericListTable.$refs.ListTable.reloadTable()
this.$refs.GenericListTable.$refs.ListTable.$refs.ListTable.reloadTable()
// this.$message.success(this.$t('common.deleteSuccessMsg'))
}).catch(error => {
this.$message.error(this.$t('common.deleteErrorMsg' + ' ' + error))
this.$message.error(this.$t('common.deleteErrorMsg') + ' ' + error)
})
}.bind(this)
}
@@ -45,7 +53,7 @@ export default {
}
},
headerActions: {
hasBulkDelete: false,
hasMoreActions: false,
hasExport: false,
hasImport: false,
createRoute: 'KubernetesAppCreate'

View File

@@ -19,13 +19,37 @@ export default {
return {
initial: {
type: appTypeMeta.name,
path: pathInitial
attrs: {
path: pathInitial
}
},
fields: [
[this.$t('common.Basic'), ['name', 'type', 'domain']],
[this.$t('common.Basic'), ['name', 'type']],
[appTypeMeta.title, ['attrs']],
[this.$t('common.Other'), ['comment']]
],
fieldsMeta: {
type: {
readonly: true
},
attrs: {
fields: fieldsMap,
fieldsMeta: {
asset: {
rules: [{ required: true }],
el: {
multiple: false,
ajax: {
url: '/api/v1/assets/assets/?platform__base=Windows',
transformOption: (item) => {
return { label: item.hostname, value: item.id }
}
}
}
}
}
}
},
url: '/api/v1/applications/applications/',
getUrl() {
const params = this.$route.params
@@ -36,96 +60,13 @@ export default {
return `${url}?type=${this.$route.query.type}`
},
performSubmit(validValues) {
this.$log.debug('Validated data: ', validValues)
const params = this.$route.params
const baseUrl = `/api/v1/applications/applications/`
const url = (params.id) ? `${baseUrl}${params.id}/` : baseUrl
const method = this.getMethod()
switch (validValues.type) {
case 'chrome': {
validValues.attrs = {
chrome_target: validValues.chrome_target,
chrome_username: validValues.chrome_username,
chrome_password: validValues.chrome_password,
asset: validValues.asset,
path: validValues.path
}
break
}
case 'mysql_workbench': {
validValues.attrs = {
mysql_workbench_ip: validValues.mysql_workbench_ip,
mysql_workbench_port: validValues.mysql_workbench_port,
mysql_workbench_name: validValues.mysql_workbench_name,
mysql_workbench_username: validValues.mysql_workbench_username,
mysql_workbench_password: validValues.mysql_workbench_password,
asset: validValues.asset,
path: validValues.path
}
break
}
case 'vmware_client': {
validValues.attrs = {
vmware_password: validValues.vmware_password,
vmware_username: validValues.vmware_username,
vmware_target: validValues.vmware_target,
asset: validValues.asset,
path: validValues.path
}
break
}
case 'custom': {
validValues.attrs = {
custom_cmdline: validValues.custom_cmdline,
custom_target: validValues.custom_target,
custom_username: validValues.custom_username,
custom_password: validValues.custom_password,
asset: validValues.asset,
path: validValues.path
}
break
}
}
validValues.category = 'remote_app'
console.log(validValues)
return this.$axios[method](`${url}?type=${validValues.type}`, validValues)
},
fieldsMeta: {
asset: {
rules: [{ required: true }],
el: {
multiple: false,
ajax: {
url: '/api/v1/assets/assets/?platform__base=Windows',
transformOption: (item) => {
return { label: item.hostname, value: item.id }
}
}
}
},
type: {
type: 'select',
options: [
{
label: appTypeMeta.title,
value: appTypeMeta.name
}
],
disabled: true
},
asset_info: {
type: 'input',
hidden: () => true
},
domain: {
el: {
multiple: false,
clearable: true,
ajax: {
url: '/api/v1/assets/domains/'
}
}
},
...fieldsMap
}
}
},

View File

@@ -17,8 +17,13 @@ export default {
tableConfig: {
url: '/api/v1/applications/applications/?category=remote_app',
columns: [
'name', 'type', 'attrs.asset', 'comment', 'actions'
'name', 'type', 'attrs.asset',
'created_by', 'date_created', 'date_updated', 'comment', 'org_name', 'actions'
],
columnsShow: {
min: ['name', 'actions'],
default: ['name', 'type', 'attrs.asset', 'comment', 'actions']
},
columnsMeta: {
type: {
displayKey: 'get_type_display',
@@ -33,8 +38,11 @@ export default {
}
},
actions: {
prop: 'actions',
formatterArgs: {
hasClone: false,
onClone: ({ row }) => {
vm.$router.push({ name: 'RemoteAppCreate', query: { type: row.type, clone_from: row.id }})
},
onUpdate: ({ row }) => {
vm.$router.push({ name: 'RemoteAppUpdate', params: { id: row.id }, query: { type: row.type }})
},
@@ -42,10 +50,10 @@ export default {
this.$axios.delete(
`/api/v1/applications/applications/${row.id}/`
).then(res => {
this.$refs.GenericListTable.$refs.ListTable.reloadTable()
this.$refs.GenericListTable.$refs.ListTable.$refs.ListTable.reloadTable()
// this.$message.success(this.$t('common.deleteSuccessMsg'))
}).catch(error => {
this.$message.error(this.$t('common.deleteErrorMsg' + ' ' + error))
this.$message.error(this.$t('common.deleteErrorMsg') + ' ' + error)
})
}.bind(this)
}
@@ -56,26 +64,24 @@ export default {
hasCreate: false,
hasMoreActions: false,
hasBulkDelete: false,
hasExport: false,
hasImport: false,
// createRoute: 'RemoteAppCreate',
moreActionsTitle: this.$t('common.Create'),
moreActionsType: 'primary',
extraMoreActions: this.genExtraMoreActions()
moreCreates: {
dropdown: this.getCreateAppType(),
callback: (app) => {
console.log('App: ', app)
vm.$router.push({ name: 'RemoteAppCreate', query: { type: app.name }})
}
}
}
}
},
methods: {
onCallback(type) {
this.$router.push({ name: 'RemoteAppCreate', query: { type: type }})
},
genExtraMoreActions() {
getCreateAppType() {
const extraMoreActions = []
for (const value of ALL_TYPES) {
const item = { ...REMOTE_APP_TYPE_META_MAP[value] }
item.type = 'primary'
item.can = true
item.callback = this.onCallback.bind(this, value)
item.has = true
extraMoreActions.push(item)
}
return extraMoreActions

View File

@@ -8,73 +8,27 @@ export const CUSTOM = 'custom'
export const ALL_TYPES = [CHROME, MYSQL_WORKBENCH, VMWARE_CLIENT, CUSTOM]
export const REMOTE_APP_TYPE_FIELDS_MAP = {
[CHROME]: [
{
id: 'chrome_target', el: {}, attrs: {}, type: 'input', prop: 'chrome_target',
label: i18n.t('applications.chrome_target')
},
{
id: 'chrome_username', el: {}, attrs: {}, type: 'input', prop: 'chrome_username',
label: i18n.t('applications.chrome_username')
},
{
id: 'chrome_password', el: { 'show-password': true }, attrs: {}, type: 'password', prop: 'chrome_password',
label: i18n.t('applications.chrome_password')
}
],
[CHROME]: ['asset', 'path', 'chrome_target', 'chrome_username', 'chrome_password'],
[MYSQL_WORKBENCH]: [
{
id: 'mysql_workbench_ip', el: {}, attrs: {}, type: 'input', prop: 'mysql_workbench_ip',
label: i18n.t('applications.mysql_workbench_ip')
},
{
id: 'mysql_workbench_port', el: {}, attrs: {}, type: 'input', prop: 'mysql_workbench_port',
label: i18n.t('applications.mysql_workbench_port')
},
{
id: 'mysql_workbench_name', el: {}, attrs: {}, type: 'input', prop: 'mysql_workbench_name',
label: i18n.t('applications.mysql_workbench_name')
},
{
id: 'mysql_workbench_username', el: {}, attrs: {}, type: 'input', prop: 'mysql_workbench_username',
label: i18n.t('applications.mysql_workbench_username')
},
{
id: 'mysql_workbench_password', el: { 'show-password': true }, attrs: {}, type: 'password', prop: 'mysql_workbench_password',
label: i18n.t('applications.mysql_workbench_password')
}
'asset', 'path',
'mysql_workbench_ip',
'mysql_workbench_port',
'mysql_workbench_name',
'mysql_workbench_username',
'mysql_workbench_password'
],
[VMWARE_CLIENT]: [
{
id: 'vmware_target', el: {}, attrs: {}, type: 'input', prop: 'vmware_target',
label: i18n.t('applications.vmware_target')
},
{
id: 'vmware_username', el: {}, attrs: {}, type: 'input', prop: 'vmware_username',
label: i18n.t('applications.vmware_username')
},
{
id: 'vmware_password', el: { 'show-password': true }, attrs: {}, type: 'password', prop: 'vmware_password',
label: i18n.t('applications.vmware_password')
}
'asset', 'path',
'vmware_target',
'vmware_username',
'vmware_password'
],
[CUSTOM]: [
{
id: 'custom_cmdline', el: {}, attrs: {}, type: 'input', prop: 'custom_cmdline',
label: i18n.t('applications.custom_cmdline')
},
{
id: 'custom_target', el: {}, attrs: {}, type: 'input', prop: 'custom_target',
label: i18n.t('applications.custom_target')
},
{
id: 'custom_username', el: {}, attrs: {}, type: 'input', prop: 'custom_username',
label: i18n.t('applications.custom_username')
},
{
id: 'custom_password', el: { 'show-password': true }, attrs: {}, type: 'password', prop: 'custom_password',
label: i18n.t('applications.custom_password')
}
'asset', 'path',
'custom_cmdline',
'custom_target',
'custom_username',
'custom_password'
]
}

View File

@@ -2,7 +2,7 @@
<div>
<el-row :gutter="20">
<el-col :span="16">
<AssetUserTable :url="assetUserUrl" :has-import="false" />
<AssetUserTable :url="assetUserUrl" :has-import="false" :has-clone="false" />
</el-col>
</el-row>
</div>

View File

@@ -16,7 +16,7 @@
import QuickActions from '@/components/QuickActions/index'
import ListTable from '@/components/ListTable'
import RelationCard from '@/components/RelationCard'
import { BooleanFormatter } from '@/components/ListTable/formatters'
import { ChoicesFormatter } from '@/components/ListTable/formatters'
export default {
name: 'AssetList',
@@ -44,7 +44,7 @@ export default {
},
connectivity: {
label: this.$t('assets.Reachable'),
formatter: BooleanFormatter,
formatter: ChoicesFormatter,
formatterArgs: {
iconChoices: {
0: 'fa-times text-danger',

View File

@@ -3,22 +3,17 @@
<el-col :span="14">
<DetailCard :items="detailCardItems" />
</el-col>
<el-col :span="10">
<RelationCard ref="RelationCard" type="info" v-bind="nodeRelationConfig" />
</el-col>
</el-row>
</template>
<script>
import DetailCard from '@/components/DetailCard'
import RelationCard from '@/components/RelationCard'
import { toSafeLocalDateStr } from '@/utils/common'
export default {
name: 'Detail',
components: {
DetailCard,
RelationCard
DetailCard
},
props: {
object: {
@@ -28,31 +23,7 @@ export default {
},
data() {
return {
nodeRelationConfig: {
icon: 'fa-info',
title: this.$t('assets.ReplaceNodeAssetsAdminUserWithThis'),
objectsAjax: {
url: '/api/v1/assets/nodes/',
transformOption: (item) => {
return { label: item.full_value, value: item.id }
}
},
performAdd: (items) => {
const data = []
const relationUrl = `/api/v1/assets/admin-users/${this.object.id}/nodes/`
items.map(v => {
data.push(v.value)
})
return this.$axios.patch(relationUrl, { nodes: data }).then(res => {
this.$message.success(this.$t('common.updateSuccessMsg'))
}).catch(err => {
this.$message.error(this.$t('common.updateErrorMsg' + ' ' + err))
})
},
onAddSuccess: () => {
this.$refs.RelationCard.$refs.select2.clearSelected()
}
}
}
},
computed: {
@@ -66,6 +37,10 @@ export default {
key: this.$t('assets.Username'),
value: this.object.username
},
{
key: this.$t('assets.sshKeyFingerprint'),
value: this.object.ssh_key_fingerprint
},
{
key: this.$t('assets.date_joined'),
value: toSafeLocalDateStr(this.object.date_created)

View File

@@ -13,7 +13,14 @@ export default {
return {
tableConfig: {
url: '/api/v1/assets/admin-users/',
columns: ['name', 'username', 'assets_amount', 'comment', 'actions'],
columns: [
'name', 'username', 'assets_amount',
'created_by', 'date_created', 'date_updated', 'comment', 'org_name', 'actions'
],
columnsShow: {
min: ['name', 'actions'],
default: ['name', 'username', 'assets_amount', 'comment', 'actions']
},
columnsMeta: {
username: {
showOverflowTooltip: true

View File

@@ -2,7 +2,7 @@
<div>
<el-row :gutter="24">
<el-col :span="16">
<AssetUserTable ref="ListTable" :url="assetUserUrl" :has-import="false" />
<AssetUserTable ref="ListTable" :url="assetUserUrl" :has-import="false" :has-clone="false" />
</el-col>
<el-col :span="8">
<QuickActions type="primary" :actions="quickActions" />

View File

@@ -67,13 +67,14 @@
<script>
import GenericTreeListPage from '@/layout/components/GenericTreeListPage/index'
import { DetailFormatter, ActionsFormatter, BooleanFormatter } from '@/components/ListTable/formatters'
import { DetailFormatter, ActionsFormatter, ChoicesFormatter } from '@/components/ListTable/formatters'
import $ from '@/utils/jquery-vendor'
import Dialog from '@/components/Dialog'
import TreeTable from '@/components/TreeTable'
import { GenericUpdateFormDialog } from '@/layout/components'
import rules from '@/components/DataForm/rules'
import Protocols from '@/views/assets/Asset/components/Protocols/index'
import { mapGetters } from 'vuex'
export default {
components: {
@@ -89,6 +90,7 @@ export default {
showMenu: true,
showRefresh: true,
showAssets: false,
hasRightMenu: this.currentOrgIsRoot,
url: '/api/v1/assets/assets/',
nodeUrl: '/api/v1/assets/nodes/',
// ?assets=0不显示资产. =1显示资产
@@ -98,8 +100,20 @@ export default {
url: '/api/v1/assets/assets/',
hasTree: true,
columns: [
'hostname', 'ip', 'hardware_info', 'connectivity', 'actions'
'hostname', 'ip', 'public_ip', 'admin_user_display',
'protocols',
'platform', 'hardware_info', 'model',
'cpu_model', 'cpu_cores', 'cpu_count', 'cpu_vcpus',
'disk_info', 'disk_total', 'memory',
'os', 'os_arch', 'os_version',
'number', 'vendor', 'sn',
'connectivity',
'created_by', 'date_created', 'comment', 'org_name', 'actions'
],
columnsShow: {
min: ['hostname', 'ip', 'actions'],
default: ['hostname', 'ip', 'hardware_info', 'connectivity', 'actions']
},
columnsMeta: {
hostname: {
formatter: DetailFormatter,
@@ -108,6 +122,11 @@ export default {
},
showOverflowTooltip: true
},
protocols: {
formatter: function(row) {
return <span> {row.protocols.toString()} </span>
}
},
ip: {
sortable: 'custom',
width: '140px'
@@ -115,9 +134,18 @@ export default {
hardware_info: {
showOverflowTooltip: true
},
cpu_model: {
showOverflowTooltip: true
},
sn: {
showOverflowTooltip: true
},
comment: {
showOverflowTooltip: true
},
connectivity: {
label: this.$t('assets.Reachable'),
formatter: BooleanFormatter,
formatter: ChoicesFormatter,
formatterArgs: {
iconChoices: {
0: 'fa-times text-danger',
@@ -125,6 +153,9 @@ export default {
2: 'fa-circle text-warning'
},
typeChange: function(val) {
if (!val) {
return 2
}
return val.status
},
hasTips: true
@@ -135,7 +166,6 @@ export default {
actions: {
formatter: ActionsFormatter,
formatterArgs: {
hasClone: true,
performDelete: ({ row, col }) => {
const id = row.id
const url = `/api/v1/assets/assets/${id}/`
@@ -146,8 +176,8 @@ export default {
name: 'View',
title: this.$t(`common.UpdateAssetDetail`),
type: 'primary',
callback: function({ cellValue, tableData }) {
return this.$router.push({ name: 'AssetMoreInformationEdit', params: { id: cellValue }})
callback: function({ cellValue, tableData, row }) {
return this.$router.push({ name: 'AssetMoreInformationEdit', params: { id: row.id }})
}
}
]
@@ -156,6 +186,7 @@ export default {
}
},
headerActions: {
// canCreate: false,
createRoute: {
name: 'AssetCreate',
query: this.$route.query
@@ -201,7 +232,7 @@ export default {
{
name: 'updateSelected',
title: this.$t('common.updateSelected'),
can: ({ selectedRows }) => selectedRows.length > 0,
can: ({ selectedRows }) => selectedRows.length > 0 && !this.$store.getters.currentOrgIsRoot,
callback: ({ selectedRows, reloadTable }) => {
vm.updateSelectedDialogSetting.dialogSetting.dialogVisible = true
vm.updateSelectedDialogSetting.selectedRows = selectedRows
@@ -214,7 +245,7 @@ export default {
if (!this.$route.query.node) {
return false
}
return selectedRows.length > 0
return selectedRows.length > 0 && !this.$store.getters.currentOrgIsRoot
},
callback: function({ selectedRows, reloadTable }) {
const assetsId = []
@@ -363,8 +394,12 @@ export default {
}
}
},
computed: {
...mapGetters(['currentOrgIsRoot'])
},
mounted() {
this.decorateRMenu()
this.treeSetting.hasRightMenu = !this.currentOrgIsRoot
},
methods: {
decorateRMenu() {
@@ -465,7 +500,7 @@ export default {
},
rCheckAssetsAmount: function() {
this.$axios.post(
`/api/v1/assets/nodes/launch_check_assets_amount_task/`
`/api/v1/assets/nodes/check_assets_amount_task/`
).then(res => {
window.open(`/#/ops/celery/task/${res.task}/log/`, '', 'width=900,height=600')
}).catch(error => {

View File

@@ -1,5 +1,5 @@
<template>
<GenericCreateUpdatePage v-bind="$data" />
<GenericCreateUpdatePage v-bind="$data" :perform-submit="performSubmit" />
</template>
<script>
@@ -94,6 +94,19 @@ export default {
updateSuccessNextRoute: { name: 'AssetList' },
createSuccessNextRoute: { name: 'AssetList' }
}
},
methods: {
getUrl() {
const params = this.$route.params
let url = this.url
if (params.id) {
url = `${url}${params.id}/`
}
return url
},
performSubmit(validValues) {
return this.$axios['patch'](this.getUrl(), validValues)
}
}
}
</script>

View File

@@ -1,14 +1,14 @@
<template>
<ListTable :table-config="tableConfig" :header-actions="headerActions" />
<GenericListTable :table-config="tableConfig" :header-actions="headerActions" />
</template>
<script>
import ListTable from '@/components/ListTable'
import GenericListTable from '@/layout/components/GenericListTable'
export default {
name: 'Rules',
components: {
ListTable
GenericListTable
},
props: {
object: {
@@ -50,6 +50,7 @@ export default {
headerActions: {
hasSearch: true,
hasBulkDelete: false,
hasMoreActions: false,
createRoute: {
name: 'CommandFilterRulesCreate',
query: {

View File

@@ -25,7 +25,6 @@ export default {
initial: {
filter: filterId,
type: 'regex',
priority: 50,
action: 0
},
fields: [
@@ -66,7 +65,7 @@ export default {
},
priority: {
// helpText: '优先级可选范围为1-1001最低优先级100最高优先级'
helpText: this.$t('assets.CommandFilterRulePriorityHelpText')
// helpText: this.$t('assets.CommandFilterRulePriorityHelpText')
}
},
getNextRoute(res, method) {

View File

@@ -14,7 +14,14 @@ export default {
return {
tableConfig: {
url: '/api/v1/assets/cmd-filters/',
columns: ['name', 'rules', 'system_users', 'comment', 'actions'],
columns: [
'name', 'rules', 'system_users', 'is_active',
'created_by', 'date_created', 'comment', 'org_name', 'actions'
],
columnsShow: {
min: ['name', 'actions'],
default: ['name', 'rules', 'system_users', 'comment', 'actions']
},
columnsMeta: {
rules: {
label: this.$t('assets.Rules'),
@@ -28,6 +35,9 @@ export default {
}
}
},
date_created: {
label: this.$t('users.DateJoined')
},
system_users: {
label: this.$t('assets.SystemUsers'),
formatter: DetailFormatter,
@@ -41,7 +51,7 @@ export default {
}
},
headerActions: {
hasRightActions: false,
hasRightActions: true,
hasExport: false,
hasImport: false,
hasRefresh: true,

View File

@@ -1,6 +1,6 @@
<template>
<div>
<ListTable :table-config="tableConfig" :header-actions="headerActions" />
<GenericListTable :table-config="tableConfig" :header-actions="headerActions" />
<Dialog
v-if="dialogVisible"
:title="this.$t('assets.TestGatewayTestConnection')"
@@ -29,12 +29,12 @@
</template>
<script>
import ListTable from '@/components/ListTable/index'
import GenericListTable from '@/layout/components/GenericListTable/index'
import DisplayFormatter from '@/components/ListTable/formatters/DisplayFormatter'
import Dialog from '@/components/Dialog'
export default {
components: {
ListTable,
GenericListTable,
Dialog
},
props: {
@@ -81,11 +81,21 @@ export default {
return this.$message.error(this.$t('common.BadRequestErrorMsg'))
} else {
this.portInput = val.row.port
this.cellValue = val.cellValue
this.cellValue = val.row.id
}
}.bind(this)
}
]
],
onClone: function({ row, col }) {
const cloneRoute = {
name: 'GatewayCreate',
query: {
domain: this.object.id,
clone_from: row.id
}
}
this.$router.push(cloneRoute)
}.bind(this)
}
}

View File

@@ -15,8 +15,13 @@ export default {
tableConfig: {
url: '/api/v1/assets/domains/',
columns: [
'name', 'asset_count', 'application_count', 'gateway_count', 'comment', 'actions'
'name', 'asset_count', 'application_count', 'gateway_count', 'date_created',
'comment', 'org_name', 'actions'
],
columnsShow: {
min: ['name', 'actions'],
default: ['name', 'asset_count', 'application_count', 'gateway_count', 'comment', 'actions']
},
columnsMeta: {
asset_count: {
label: this.$t('assets.Assets')

View File

@@ -13,7 +13,10 @@ export default {
return {
tableConfig: {
url: '/api/v1/assets/labels/',
columns: ['name', 'value', 'asset_count', 'actions'],
columns: [
'name', 'value', 'asset_count',
'date_created', 'comment', 'org_name', 'actions'
],
columnsMeta: {
name: {
formatter: null
@@ -24,11 +27,6 @@ export default {
}
},
headerActions: {
hasRightActions: false,
hasExport: false,
hasImport: false,
hasRefresh: true,
hasSearch: true,
hasMoreActions: false,
createRoute: 'LabelCreate'
}

View File

@@ -1,5 +1,5 @@
<template>
<GenericCreateUpdatePage :fields="fields" :initial="initial" :fields-meta="fieldsMeta" :url="url" :perform-submit="performSubmit.bind(this)" />
<GenericCreateUpdatePage :fields="fields" :initial="initial" :fields-meta="fieldsMeta" :url="url" />
</template>
<script>
@@ -17,65 +17,48 @@ export default {
charset: 'utf8'
},
fields: [
[this.$t('common.Basic'), ['name', 'base', 'charset', 'security', 'console', 'comment']]
[this.$t('common.Basic'), ['name', 'base', 'charset', 'meta', 'comment']]
],
fieldsMeta: {
security: {
type: 'select',
label: 'RDP security',
options: [{
label: 'RDP',
value: 'rdp'
meta: {
fields: ['security', 'console'],
fieldsMeta: {
security: {
prop: 'meta.security',
type: 'select',
label: 'RDP security',
options: [{
label: 'RDP',
value: 'rdp'
},
{
label: 'NLA',
value: 'nla'
},
{
label: 'TLS',
value: 'tls'
},
{
label: 'Any',
value: 'any'
}]
},
console: {
type: 'select',
label: 'RDP console',
options: [{
label: this.$t('common.Yes'),
value: 'true'
}, {
label: this.$t('common.No'),
value: 'false'
}]
}
},
{
label: 'NLA',
value: 'nla'
},
{
label: 'TLS',
value: 'tls'
},
{
label: 'Any',
value: 'any'
}],
hidden: form => form.base !== 'Windows'
},
console: {
type: 'select',
label: 'RDP console',
options: [{
label: '是',
value: 'true'
}, {
label: '否',
value: 'false'
}],
hidden: form => form.base !== 'Windows'
}
},
performSubmit: function(formdata) {
var postData = {}
if (formdata.base === 'Windows') {
postData.meta = {}
postData.meta.security = formdata.security
postData.meta.console = (formdata.console === 'true')
}
postData.name = formdata.name
postData.base = formdata.base
postData.charset = formdata.charset
postData.comment = formdata.comment || ''
const params = this.$route.params
if (params.id) {
return this.$axios.put(
`${this.url}${params.id}/`, postData
)
} else {
return this.$axios.post(
this.url, postData
)
}
},
url: '/api/v1/assets/platforms/'
}

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