Compare commits

..

484 Commits

Author SHA1 Message Date
pycook
8217053abf release: v2.3.5 2023-10-09 20:55:30 +08:00
pycook
8c17373e45 feat: get messenger url from common setting 2023-10-09 20:25:27 +08:00
wang-liang0615
a8e2595327 前端更新 (#192)
* fix:add package

* fix:notice_info为null的情况

* fix:2 bugs

* feat:1.common增加通知配置 2.cmdb预定义值webhook&其他模型

* fix:json 不支持预定义值

* fix:json 不支持预定义值

* fix:删除代码
2023-10-09 19:52:19 +08:00
simontigers
dfbba103cd fix: init company structure resource (#191)
* fix: init company structure resource

* fix: notice_info null
2023-10-09 19:25:49 +08:00
wang-liang0615
86b9d5a7f4 前端更新 (#189)
* fix:add package

* fix:notice_info为null的情况

* fix:2 bugs

* feat:1.common增加通知配置 2.cmdb预定义值webhook&其他模型

* fix:json 不支持预定义值

* fix:json 不支持预定义值
2023-10-09 17:43:34 +08:00
simontigers
612922a1b7 feat: notice_config access messenger (#190) 2023-10-09 17:32:20 +08:00
pycook
2758c5e468 fix: delete user role 2023-10-09 15:40:18 +08:00
pycook
d85c86a839 feat: The definition of attribute choice values supports webhook and other model attribute values. 2023-10-09 15:33:18 +08:00
wang-liang0615
8355137e43 Dev UI (#186)
* fix:add package

* fix:notice_info为null的情况

* fix:2 bugs
2023-09-28 09:45:11 +08:00
pycook
2e644233bc release 2.3.4 2023-09-27 11:37:18 +08:00
simontigers
d9b4082b46 feat: add api get_notice_by_ids (#184) 2023-09-27 09:54:30 +08:00
wang-liang0615
a07f984152 前端更新 (#183)
* fix:add package

* fix:notice_info为null的情况
2023-09-27 09:18:33 +08:00
pycook
4cab7ef6b0 feat: ci triggers 2023-09-26 21:18:34 +08:00
wang-liang0615
070c163de6 fix:add package (#182) 2023-09-26 21:12:10 +08:00
wang-liang0615
282a779fb1 Merge pull request #181 from veops/dev_ui
前端更新
2023-09-26 20:34:27 +08:00
wang-liang0615
cb6b51a84c Merge branch 'master' into dev_ui 2023-09-26 20:34:14 +08:00
wang-liang0615
34bd320e75 fix:topo图相同节点出现两次的bug 2023-09-26 20:13:12 +08:00
wang-liang0615
1eca5791f6 feat:wangeditor 注册自定义组件 2023-09-26 20:07:00 +08:00
wang-liang0615
13b1c9a30c delete:删除getwx 2023-09-26 20:04:38 +08:00
simontigers
b1a15a85d2 feat: common notice config (#180) 2023-09-26 19:44:20 +08:00
wang-liang0615
08e5a02caf feat: UI更新 触发器 (#179)
* feat:新增api&适配

* feat:触发器

* add packages & 注释代码

* feat: webhook tips
2023-09-26 18:25:04 +08:00
wang-liang0615
308827b8fc feat: webhook tips 2023-09-26 18:17:23 +08:00
wang-liang0615
dc4ccb22b9 add packages & 注释代码 2023-09-26 17:35:41 +08:00
wang-liang0615
c482e7ea43 feat:触发器 2023-09-26 17:01:31 +08:00
wang-liang0615
663c14f763 feat:新增api&适配 2023-09-26 16:26:25 +08:00
pycook
c6ee227bab fix: ci_cache 2023-09-25 15:46:07 +08:00
wang-liang0615
cb62cf2410 Merge pull request #178 from veops/dev_ui
前端更新:仪表盘优化
2023-09-25 14:52:09 +08:00
wang-liang0615
133f32a6b0 pref:仪表盘优化 2023-09-25 14:50:08 +08:00
wang-liang0615
45c48c86fe Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-09-25 14:43:34 +08:00
pycook
2321f17dae refactor: CI triggers 2023-09-22 17:39:54 +08:00
simontigers
ddb31a07a2 fix: icon svg support (#177) 2023-09-20 15:56:57 +08:00
pycook
b474914fbb fix date search 2023-09-18 18:15:02 +08:00
pycook
26099a3d69 fix dashboard compute 2023-09-18 13:04:50 +08:00
pycook
62829c885b release v2.3.3 2023-09-15 17:57:39 +08:00
pycook
260aed6462 dashboard ui update 2023-09-15 17:36:10 +08:00
simontigers
3841999cca feat: init resource for backend (#176) 2023-09-15 15:30:30 +08:00
pycook
14c03ce5d2 enhance dashboard 2023-09-15 15:26:20 +08:00
pycook
f463ecd6e6 cmdb-api/api/lib/resp_format.py 2023-09-12 20:01:30 +08:00
pycook
adc0cfd5c5 Detect circular dependencies when adding CIType relationships 2023-09-12 20:00:56 +08:00
wang-liang0615
086481657e 计算属性 触发计算 (#174) 2023-09-11 19:16:05 +08:00
pycook
d2f84ae3dc fix upload template and add /api/v0.1/attributes/<int:attr_id>/calc_computed_attribute 2023-09-11 19:15:31 +08:00
wang-liang0615
9f1b510cb3 Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-09-11 17:35:44 +08:00
wang-liang0615
61acb2483d 计算属性 触发计算 2023-09-11 17:34:51 +08:00
pycook
0196c8a82c release 2.3.2 2023-09-07 13:44:51 +08:00
pycook
bed2323fc1 Merge pull request #172 from veops/dev_ui
新建ci及批量导入时,新建关系
2023-09-07 11:04:49 +08:00
wang-liang0615
be9b308f56 新建ci及批量导入时,新建关系 2023-09-07 10:25:18 +08:00
pycook
8ba658ea1b Merge branch 'master' of github.com:veops/cmdb 2023-09-07 10:12:55 +08:00
pycook
0aa668cfa0 Add CI relationship when creating CI, the text value removes the escape 2023-09-07 10:12:42 +08:00
pycook
e20fd33a53 Merge pull request #171 from ronething/fix/makefile
optimize: makefile help
2023-09-05 20:34:16 +08:00
ashing
7462de63de fix: review
Signed-off-by: ashing <axingfly@gmail.com>
2023-09-05 20:33:07 +08:00
ashing
5f9ba069ad fix: review
Signed-off-by: ashing <axingfly@gmail.com>
2023-09-05 20:29:29 +08:00
ashing
5dc0d95ff8 fix: review
Signed-off-by: ashing <axingfly@gmail.com>
2023-09-05 20:21:20 +08:00
ashing
e5536b76e6 optimize: makefile help
Signed-off-by: ashing <axingfly@gmail.com>
2023-09-05 20:06:31 +08:00
pycook
8b044efd4e Merge pull request #170 from ronething/feat/xx
feat: support docker deploy mysql and redis
2023-09-05 19:28:47 +08:00
ivonGwy
747b5bf494 Merge pull request #169 from veops/doc
add document link
2023-09-05 15:41:52 +08:00
ivonGwy
21067022f6 add document link 2023-09-05 15:40:31 +08:00
ashing
4102c44fb2 feat: support docker deploy mysql and redis
Signed-off-by: ashing <axingfly@gmail.com>
2023-09-05 15:26:50 +08:00
wang-liang0615
600f95ce18 Merge pull request #168 from veops/dev_ui
UI更新
2023-09-05 15:23:43 +08:00
wang-liang0615
950fd38044 Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-09-05 15:22:18 +08:00
wang-liang0615
01085615b5 模型关联 展示反向关系 2023-09-05 15:22:08 +08:00
pycook
734f1940f9 Merge branch 'master' of github.com:veops/cmdb 2023-09-05 14:49:53 +08:00
pycook
c25c1e4e4b move Dockerfile to docs 2023-09-05 14:49:34 +08:00
wang-liang0615
826a8306d3 Merge pull request #167 from veops/dev_ui
sub menu color
2023-09-04 16:34:26 +08:00
wang-liang0615
740aae573e sub menu color 2023-09-04 16:33:35 +08:00
wang-liang0615
17828a7631 Merge pull request #166 from veops/dev_ui
ui更新
2023-09-04 13:15:27 +08:00
wang-liang0615
02cb497bdc Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-09-04 13:14:35 +08:00
wang-liang0615
05a7dc41ee sidebar 2023-09-04 13:14:11 +08:00
pycook
459c70ba2d import format 2023-09-02 12:09:41 +08:00
pycook
774f42ac34 format 2023-09-01 18:07:44 +08:00
pycook
420029a5e2 fix delete choice values 2023-08-31 16:02:24 +08:00
pycook
ab8acbfd20 fix delete choice values 2023-08-31 15:18:15 +08:00
wang-liang0615
4468b6a8de Merge pull request #165 from veops/dev_ui
proxy
2023-08-31 13:31:26 +08:00
wang-liang0615
6bf145d085 proxy 2023-08-31 13:28:15 +08:00
pycook
42b1e47e76 Merge pull request #162 from simontigers/cmdb_icon_manage
feat: add cmdb custom icon manage
2023-08-31 11:15:09 +08:00
pycook
673134003a Merge pull request #163 from veops/dev_ui
支持上传自定义图标
2023-08-31 11:14:42 +08:00
hu.sima
ef67885571 feat: add cmdb custom icon manage 2023-08-31 10:49:56 +08:00
wang-liang0615
075bf7217f 支持上传自定义图标 2023-08-31 10:05:11 +08:00
pycook
3b7b8f435c fix update attribute 2023-08-30 13:34:10 +08:00
pycook
2b7f6aeef3 Merge branch 'master' of github.com:veops/cmdb 2023-08-29 14:49:21 +08:00
pycook
544fac8aca The default value of USE_ACL is set to True 2023-08-29 14:49:09 +08:00
pycook
3d0a56ec8c Merge pull request #161 from simontigers/common_setting_format
fix: company info create
2023-08-29 11:01:25 +08:00
hu.sima
d2d8482052 fix: company info create 2023-08-29 10:56:48 +08:00
pycook
a0afae8d2e Merge branch 'master' of github.com:veops/cmdb 2023-08-25 11:01:24 +08:00
pycook
9f3da68636 update ad_ci when deleting ci 2023-08-25 10:59:38 +08:00
wang-liang0615
24b955c288 Merge pull request #160 from veops/dev_ui
前端更新
2023-08-25 10:12:31 +08:00
wang-liang0615
a07b2d37ec fix 新增类型回车键发送两次请求 2023-08-25 10:11:09 +08:00
wang-liang0615
c86fcb4e7b fix 新增类型回车键发送两次请求 2023-08-25 10:08:04 +08:00
pycook
ca7964f24b Merge pull request #158 from EvanSung/perf_20230824_optimize_ad_ci_relation
perf(ad_ci_relation): optimize ad_ci relation
2023-08-24 16:26:43 +08:00
EvanSung
c42ac634fb perf(ad_ci_relation): optimize ad_ci relation 2023-08-24 14:16:12 +08:00
pycook
a6fc3341ce docker-compose add flask db-setup 2023-08-24 11:32:09 +08:00
pycook
fc3f2e25f3 vxe-table-plugin-export-xlsx==2.0.0 2023-08-24 11:06:28 +08:00
pycook
511a5f70c6 add config CACHE_REDIS_PASSWORD and fix delete ci_type 2023-08-23 18:05:28 +08:00
pycook
f8ff4d5e45 fix update ci 2023-08-22 11:34:40 +08:00
pycook
3ab72cceaf Register api and commands with absolute paths 2023-08-21 20:08:23 +08:00
pycook
4ab7e3c70c fix merge conflict 2023-08-21 11:55:49 +08:00
pycook
a7fe75f7df fix g.user 2023-08-21 11:54:33 +08:00
pycook
3474a71a75 version: 2.3.1 2023-08-20 11:24:53 +08:00
pycook
6531baff64 lint 2023-08-20 11:23:55 +08:00
pycook
ed5936250f Merge pull request #157 from EvanSung/fix_20230817_guser_issue
fix(acl): g user issue
2023-08-17 22:12:45 +08:00
EvanSung
52c32e2ab1 fix(acl): g user issue 2023-08-17 18:40:45 +08:00
pycook
d3224625b6 fix MyJSONEncoder 2023-08-16 21:28:27 +08:00
pycook
f158c7e33a Merge pull request #155 from veops/dev_ui
前端更新
2023-08-16 13:01:13 +08:00
pycook
6dc12bb6ac Merge branch 'master' of github.com:veops/cmdb 2023-08-16 13:00:44 +08:00
pycook
b33ae16c00 Delete user without soft delete 2023-08-16 13:00:30 +08:00
wang-liang0615
2caffc2670 Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-08-16 10:09:47 +08:00
wang-liang0615
f28af51007 delete user 2023-08-16 10:09:25 +08:00
pycook
3a0369559f Merge pull request #154 from simontigers/common_setting_format
fix: init-import-user-from-acl
2023-08-15 20:47:36 +08:00
hu.sima
a74a2c5a94 fix: init-import-user-from-acl 2023-08-15 20:45:28 +08:00
pycook
9fbcb2838e Merge pull request #153 from simontigers/common_setting_format
fix: import_user_from_acl
2023-08-15 20:25:09 +08:00
hu.sima
60a445b972 fix: import_user_from_acl 2023-08-15 20:19:45 +08:00
pycook
bfdd7b6a0e Merge branch 'master' of github.com:veops/cmdb 2023-08-15 19:48:11 +08:00
pycook
ab093d2493 [update] delete roles, users, attributes 2023-08-15 19:47:59 +08:00
wang-liang0615
315a578a31 Merge pull request #152 from veops/dev_ui
前端更新
2023-08-15 19:47:03 +08:00
wang-liang0615
1e16dc5e5b 属性库 2023-08-15 19:34:17 +08:00
wang-liang0615
f67e196acf 属性库 2023-08-15 19:26:49 +08:00
wang-liang0615
439e25d5dd 属性库 2023-08-15 19:21:09 +08:00
wang-liang0615
ea59c0d71f 属性库 2023-08-15 19:10:26 +08:00
wang-liang0615
1137127aab 后台管理-模型关联 关系删除&&筛选 2023-08-15 15:02:46 +08:00
pycook
4ad1b5282e update gitattributes 2023-08-15 13:41:45 +08:00
wang-liang0615
cdd5e4d9aa Merge pull request #150 from EvanSung/optimize_20230810_acl_resource_fe
refactor(fe): reduce the width of resource mgt table
2023-08-10 19:32:49 +08:00
pycook
432de5e847 Merge pull request #148 from simontigers/common_setting_format
fix: default arg value
2023-08-10 19:31:18 +08:00
pycook
3a2339765a Merge pull request #149 from veops/dev_ui
ui更新:password
2023-08-10 19:28:24 +08:00
EvanSung
b5a2af7420 refactor(fe): reduce the width of resource mgt table 2023-08-10 19:23:41 +08:00
wang-liang0615
8b267613d6 Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-08-10 19:21:28 +08:00
wang-liang0615
b365eb27f6 增加密码明文传输 2023-08-10 19:21:10 +08:00
hu.sima
2125f020b5 fix: default arg value 2023-08-10 19:05:56 +08:00
pycook
ea762e35a0 Merge pull request #147 from simontigers/common_setting_format
fix: remove useless
2023-08-10 19:01:25 +08:00
hu.sima
f11aadf6d4 fix: remove useless 2023-08-10 18:55:32 +08:00
pycook
9cbf133b9f Merge pull request #146 from simontigers/common_setting_format
Common setting format
2023-08-10 18:23:24 +08:00
hu.sima
95e8f9de74 fix: remove unused column 2023-08-10 16:29:52 +08:00
hu.sima
26792147ae style: format common setting 2023-08-10 15:30:01 +08:00
pycook
4f9b581c2e Merge pull request #145 from EvanSung/optimize_20230810_auth_require
optimize(auth): auth request json
2023-08-10 11:24:23 +08:00
EvanSung
e2b1cb3003 optimize(auth): auth request json 2023-08-10 10:43:59 +08:00
pycook
f75a85b48a fix celery config 2023-08-08 16:33:24 +08:00
pycook
313fc80e54 Merge branch 'master' of github.com:veops/cmdb 2023-08-08 13:16:14 +08:00
pycook
e0666689e5 upgrade celery 2023-08-08 13:16:07 +08:00
pycook
7a9fd4f9d6 Merge pull request #144 from veops/dev_ui
UI更新:fix preferenceList=>attrList
2023-08-08 09:21:10 +08:00
wang-liang0615
2fd706be85 Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-08-08 09:11:24 +08:00
wang-liang0615
3df51bb670 fix preferenceList=>attrList 2023-08-08 09:11:03 +08:00
pycook
9bbbcbe6dc upgrade flask to 2.3.2 and replace g.user with current_user 2023-08-06 21:54:18 +08:00
pycook
16d6b40e8d Merge pull request #138 from lovvvve/fix_ldap
fix ldap login
2023-08-04 11:31:58 +08:00
pycook
ef2d3812a2 Merge pull request #142 from veops/dev_ui
ci 批量更新和删除的异步处理
2023-08-04 09:27:55 +08:00
wang-liang0615
bc653efd04 Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-08-03 16:54:47 +08:00
wang-liang0615
d891d7365d ci 批量更新和删除的异步处理 2023-08-03 16:54:27 +08:00
pycook
9953b2fc98 Merge pull request #139 from EvanSung/fix-post-acltrigger-session-invalid
fix(trigger): session invalid issue
2023-08-02 19:33:05 +08:00
songbing01249
8de54812dc fix(trigger): session invalid issue 2023-08-02 18:22:42 +08:00
lovvvve
eb7d52cf35 fix ldap login 2023-08-01 11:27:29 +00:00
pycook
6c4a5f2f6b Merge pull request #134 from veops/dependabot/pip/cmdb-api/pillow-9.3.0
Bump pillow from 9.2.0 to 9.3.0 in /cmdb-api
2023-08-01 15:57:02 +08:00
pycook
17c5d4538b Merge pull request #135 from simontigers/remove_pandas
fix: remove pandas
2023-08-01 15:55:15 +08:00
hu.sima
6c3e3f9eed fix: remove pandas 2023-08-01 15:32:44 +08:00
dependabot[bot]
b0494adc17 Bump pillow from 9.2.0 to 9.3.0 in /cmdb-api
Bumps [pillow](https://github.com/python-pillow/Pillow) from 9.2.0 to 9.3.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/9.2.0...9.3.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-08-01 05:51:16 +00:00
pycook
fc133f2ae9 Merge branch 'master' of github.com:veops/cmdb 2023-08-01 13:47:34 +08:00
pycook
ac6e3a0318 fix dependabot alerts 2023-08-01 13:47:11 +08:00
pycook
404ec976cc fix dependabot alerts 2023-08-01 13:46:47 +08:00
pycook
4211bbcbc9 Merge pull request #130 from veops/dependabot/pip/cmdb-api/certifi-2023.7.22
Bump certifi from 2023.5.7 to 2023.7.22 in /cmdb-api
2023-08-01 13:14:25 +08:00
pycook
0158636671 Merge pull request #132 from veops/dev_ui
删除角色相关
2023-07-31 19:54:19 +08:00
wang-liang0615
d986bc3bbc 删除角色相关 2023-07-31 19:52:06 +08:00
pycook
044b820548 Merge branch 'master' of github.com:veops/cmdb 2023-07-31 18:39:46 +08:00
pycook
536daa6d4f fix delete ci_type 2023-07-31 18:39:33 +08:00
pycook
b0620b043b Merge pull request #131 from veops/dev_ui
前端acl
2023-07-28 18:03:36 +08:00
wang-liang0615
a88c9cf7f7 common-setting 2023-07-27 15:47:13 +08:00
wang-liang0615
be50f505d1 acl 2023-07-27 15:30:27 +08:00
wang-liang0615
0bb4f633d6 fix acl change page size 2023-07-27 15:08:25 +08:00
dependabot[bot]
78b521f3af Bump certifi from 2023.5.7 to 2023.7.22 in /cmdb-api
Bumps [certifi](https://github.com/certifi/python-certifi) from 2023.5.7 to 2023.7.22.
- [Commits](https://github.com/certifi/python-certifi/compare/2023.05.07...2023.07.22)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-25 23:35:30 +00:00
pycook
77bc850d4a Merge pull request #129 from veops/dev_ui
前端更新
2023-07-25 18:19:47 +08:00
wang-liang0615
e52f201ba1 Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-07-25 13:11:03 +08:00
wang-liang0615
64aea424dc 授权高亮提示 2023-07-25 13:10:45 +08:00
pycook
0655b0e9eb add command cmdb-index-table-upgrade 2023-07-25 10:31:30 +08:00
wang-liang0615
cce0649299 style 新建属性行错乱 2023-07-25 10:18:22 +08:00
pycook
52574c64cc 废弃3个表: c_value_datetime c_value_floats c_value_integers, time类型属性值增加写入校验 2023-07-24 21:55:00 +08:00
pycook
fb904b01a6 禁止删除唯一标识的属性 2023-07-21 15:58:41 +08:00
pycook
63af79ec45 Merge branch 'master' of github.com:veops/cmdb 2023-07-20 18:37:14 +08:00
pycook
38af86317a fix docker-compose 2023-07-20 18:36:32 +08:00
pycook
03bac86588 Merge pull request #127 from veops/dev_ui
fix currentValueType
2023-07-20 15:39:11 +08:00
wang-liang0615
130b68cadd fix currentValueType 2023-07-20 15:30:12 +08:00
pycook
65000f8141 更新架构图 2023-07-20 11:01:25 +08:00
pycook
23692ad50b update readme 2023-07-20 11:01:25 +08:00
pycook
16cd34e8b8 update readme 2023-07-20 11:01:25 +08:00
pycook
985f67ee47 lint 2023-07-20 11:01:25 +08:00
wang-liang0615
8d95f8d57d 角色授权 2023-07-20 11:01:25 +08:00
pycook
cf6230008d 清理空间 2023-07-20 11:01:25 +08:00
songbing01249
ec97fa84d8 fix(ci_type_group_manager): fix resources issues 2023-07-20 11:01:25 +08:00
pycook
76f074704b update cmdb_api.md 2023-07-20 10:56:58 +08:00
wang-liang0615
e5addab3af 删除fullscreen相关代码 2023-07-19 17:46:27 +08:00
wang-liang0615
1c6be9e281 format 2023-07-19 15:36:46 +08:00
wang-liang0615
9552892c68 ops table getVxetableRef 2023-07-19 14:39:57 +08:00
wang-liang0615
b59e1af318 编译 acl 2023-07-19 13:52:24 +08:00
wang-liang0615
d164d883ab Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-07-18 15:16:50 +08:00
wang-liang0615
1fef160d9e 删除不必要文件 2023-07-18 15:16:32 +08:00
wang-liang0615
2e537d390a acl 样式升级 2023-07-18 15:14:35 +08:00
pycook
5b9fe15afa Merge pull request #119 from veops/dev_ui
模型属性 is_index
2023-07-17 18:15:28 +08:00
wang-liang0615
89fa5f2243 Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-07-17 17:21:26 +08:00
wang-liang0615
652a5c7fb8 模型属性 is_index 2023-07-17 17:19:44 +08:00
pycook
afb6adec89 update local.md 2023-07-17 13:32:34 +08:00
pycook
a9db4285ab Merge pull request #118 from veops/dev_ui
Dev UI
2023-07-14 18:06:02 +08:00
wang-liang0615
a04bdc29a5 删除usedfc 2023-07-14 15:20:58 +08:00
wang-liang0615
91e0e076a7 删除usedfc 2023-07-14 15:20:36 +08:00
wang-liang0615
339a7b857e acl 前端 2023-07-14 14:34:35 +08:00
pycook
e86e5ad1fd PyJWT==2.4.0 2023-07-13 17:07:33 +08:00
pycook
c50a69de77 Merge pull request #117 from lovvvve/patch-4
Update click_cmdb.py
2023-07-13 15:51:36 +08:00
lovvvve
4d16e9e6d9 Update click_cmdb.py
add-user remove --is_admin
2023-07-13 15:49:35 +08:00
pycook
fcea4dcb9f Merge pull request #116 from lovvvve/patch-3
Update click_cmdb.py
2023-07-13 15:23:24 +08:00
lovvvve
f98fd24c62 Update click_cmdb.py
add-user  remove --is_admin
2023-07-13 15:18:47 +08:00
pycook
f10eeb8439 update README 2023-07-13 09:34:17 +08:00
pycook
f070948122 Merge pull request #115 from veops/doc
change screenshot image
2023-07-12 17:22:58 +08:00
ivonGwy
4112bcf547 change screenshot image 2023-07-12 17:21:10 +08:00
pycook
2292756bf7 Merge pull request #114 from veops/doc
change image size
2023-07-12 17:11:20 +08:00
ivonGwy
93e2483974 change image size 2023-07-12 17:07:17 +08:00
pycook
fbb4fcc255 Merge pull request #113 from veops/doc
add qrcode for gzh
2023-07-12 16:49:12 +08:00
ivonGwy
fc77241006 add qrcode for gzh 2023-07-12 16:12:17 +08:00
pycook
0d04ad7d90 update requirements 2023-07-12 15:32:46 +08:00
pycook
e6290e49ea update docker-compose 2023-07-12 11:59:51 +08:00
pycook
97aa2e0ebe remove .gitattributes 2023-07-12 11:50:05 +08:00
pycook
939d9dc3cd md format 2023-07-12 10:14:47 +08:00
pycook
576d2e3bc4 Update README.md 2023-07-12 10:09:11 +08:00
pycook
9a40246d29 Update README.md 2023-07-12 10:01:15 +08:00
pycook
044f95c3be docker-compose 构建后的默认账号密码 2023-07-11 19:40:40 +08:00
pycook
a386de355e docker-compose is ok 2023-07-11 18:22:17 +08:00
pycook
b93afc1790 docker-compose is ok 2023-07-11 18:12:22 +08:00
pycook
77d89677ef 前后端全面升级 2023-07-10 20:13:39 +08:00
pycook
7ec6775f03 友链Spug 2023-07-10 20:07:31 +08:00
pycook
98cc853dbc 前后端全面升级 2023-07-10 20:07:20 +08:00
pycook
f57ff80099 Merge pull request #90 from lovvvve/patch-2
fix: 🐛 db search
2021-11-10 20:18:18 +08:00
lovvvve
51e4b5dd8f fix: 🐛 db search
Escape ":" character in SQLAlchemy
2021-11-10 18:56:42 +08:00
pycook
dbf44a020b Merge pull request #77 from x-7/x-7-patch-1
cmdb-api:add attr check in ci_manager update method
2021-04-22 09:14:33 +08:00
x-7
8e578797ef Update ci.py
cmdb-api:add attr check in ci_manager update method
2021-04-21 19:17:05 +08:00
pycook
158de4b946 Merge pull request #69 from lovvvve/patch-1
CiManager.add and AttributeValueManager.create_or_update_attr_value update
2021-01-28 17:09:01 +08:00
lovvvve
3cf234d49e Update value.py
value type 是 int 或 float 时 value 值等于 0 是会删除 的 BUG
2021-01-28 16:57:20 +08:00
lovvvve
a7debc1b3b Update ci.py
兼容 py2
2021-01-28 16:56:04 +08:00
lovvvve
9268da2ffa Update value.py 2021-01-28 16:46:19 +08:00
lovvvve
cfcb092478 Update value.py
feat(AttributeValueManager.create_or_update_attr_value()): AttributeValue update skip The same value
2021-01-28 16:32:53 +08:00
lovvvve
0d8b41b64a Update value.py
feat(AttributeValueManager.create_or_update_attr_value()): AttributeValue update skip The same value
2021-01-28 16:30:17 +08:00
lovvvve
d85715793f Update ci.py
feat(CiManager.add()): Check the attribute is in the ci_type attributes list
2021-01-28 16:27:56 +08:00
pycook
afbdbe4682 Merge pull request #65 from shaohaojiecoder/stable
Stable
2020-12-14 09:20:15 +08:00
shaohaojiecoder
e629abebb7 remove weeds 2020-12-13 16:58:06 +08:00
shaohaojiecoder
029c12365a delay render 2020-12-13 16:42:17 +08:00
pycook
4d000d9805 yarn.lock update 2020-11-22 11:45:33 +08:00
pycook
f1fc66bd2c Fix github security 2020-11-22 11:42:17 +08:00
pycook
d6af4af1d1 upgrade ui packages 2020-11-22 11:13:46 +08:00
pycook
7fe2bdca5f Create codeql-analysis.yml 2020-11-22 10:45:45 +08:00
pycook
1432131d2b Merge pull request #59 from pycook/dependabot/npm_and_yarn/cmdb-ui/dot-prop-4.2.1
Bump dot-prop from 4.2.0 to 4.2.1 in /cmdb-ui
2020-11-22 10:28:34 +08:00
pycook
bc94d039f5 Merge pull request #52 from pycook/dependabot/npm_and_yarn/cmdb-ui/elliptic-6.5.3
Bump elliptic from 6.4.1 to 6.5.3 in /cmdb-ui
2020-11-22 10:27:49 +08:00
pycook
5abafed9c8 Merge pull request #54 from pycook/dependabot/npm_and_yarn/cmdb-ui/quill-1.3.7
Bump quill from 1.3.6 to 1.3.7 in /cmdb-ui
2020-11-22 10:27:15 +08:00
dependabot[bot]
04e249feac Bump dot-prop from 4.2.0 to 4.2.1 in /cmdb-ui
Bumps [dot-prop](https://github.com/sindresorhus/dot-prop) from 4.2.0 to 4.2.1.
- [Release notes](https://github.com/sindresorhus/dot-prop/releases)
- [Commits](https://github.com/sindresorhus/dot-prop/compare/v4.2.0...v4.2.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-11-22 02:26:41 +00:00
pycook
ef3e6bc6b0 Merge pull request #55 from pycook/dependabot/npm_and_yarn/cmdb-ui/handlebars-4.7.6
Bump handlebars from 4.4.5 to 4.7.6 in /cmdb-ui
2020-11-22 10:26:38 +08:00
pycook
d9d5f8f818 Merge pull request #56 from pycook/dependabot/npm_and_yarn/cmdb-ui/http-proxy-1.18.1
Bump http-proxy from 1.17.0 to 1.18.1 in /cmdb-ui
2020-11-22 10:25:58 +08:00
dependabot[bot]
578da0807c Bump http-proxy from 1.17.0 to 1.18.1 in /cmdb-ui
Bumps [http-proxy](https://github.com/http-party/node-http-proxy) from 1.17.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.17.0...1.18.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-11 00:19:27 +00:00
dependabot[bot]
3eb35f5497 Bump handlebars from 4.4.5 to 4.7.6 in /cmdb-ui
Bumps [handlebars](https://github.com/wycats/handlebars.js) from 4.4.5 to 4.7.6.
- [Release notes](https://github.com/wycats/handlebars.js/releases)
- [Changelog](https://github.com/handlebars-lang/handlebars.js/blob/master/release-notes.md)
- [Commits](https://github.com/wycats/handlebars.js/compare/v4.4.5...v4.7.6)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-10 03:19:57 +00:00
dependabot[bot]
9669ad04cd Bump quill from 1.3.6 to 1.3.7 in /cmdb-ui
Bumps [quill](https://github.com/quilljs/quill) from 1.3.6 to 1.3.7.
- [Release notes](https://github.com/quilljs/quill/releases)
- [Changelog](https://github.com/quilljs/quill/blob/v1.3.7/CHANGELOG.md)
- [Commits](https://github.com/quilljs/quill/compare/v1.3.6...v1.3.7)

Signed-off-by: dependabot[bot] <support@github.com>
2020-09-04 00:38:10 +00:00
dependabot[bot]
70214807ca Bump elliptic from 6.4.1 to 6.5.3 in /cmdb-ui
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.4.1 to 6.5.3.
- [Release notes](https://github.com/indutny/elliptic/releases)
- [Commits](https://github.com/indutny/elliptic/compare/v6.4.1...v6.5.3)

Signed-off-by: dependabot[bot] <support@github.com>
2020-08-01 08:41:43 +00:00
pycook
7c1c309f7a Merge pull request #51 from pycook/dependabot/npm_and_yarn/cmdb-ui/lodash-4.17.19
Bump lodash from 4.17.14 to 4.17.19 in /cmdb-ui
2020-07-21 18:17:46 +08:00
dependabot[bot]
9b9799ff5e Bump lodash from 4.17.14 to 4.17.19 in /cmdb-ui
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.14 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.14...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-07-19 03:35:06 +00:00
pycook
b2578b61fa add command: add-user | del-user 2020-06-11 21:37:41 +08:00
pycook
619f47ae13 Merge pull request #49 from pycook/dependabot/npm_and_yarn/cmdb-ui/websocket-extensions-0.1.4
Bump websocket-extensions from 0.1.3 to 0.1.4 in /cmdb-ui
2020-06-08 18:37:31 +08:00
dependabot[bot]
37c5e31799 Bump websocket-extensions from 0.1.3 to 0.1.4 in /cmdb-ui
Bumps [websocket-extensions](https://github.com/faye/websocket-extensions-node) from 0.1.3 to 0.1.4.
- [Release notes](https://github.com/faye/websocket-extensions-node/releases)
- [Changelog](https://github.com/faye/websocket-extensions-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/faye/websocket-extensions-node/compare/0.1.3...0.1.4)

Signed-off-by: dependabot[bot] <support@github.com>
2020-06-07 15:54:30 +00:00
pycook
ab70b2a655 Merge pull request #48 from lovvvve/master
fix(sso login): sso login redirect
2020-06-01 12:05:05 +08:00
Lovvvve
c285606f4a fix(sso login): sso login redirect 2020-06-01 12:01:55 +08:00
Lovvvve
6d3611bd73 fix(sso login): sso login redirect 2020-06-01 11:48:21 +08:00
pycook
764f6a07e0 fix merge conflict 2020-05-28 20:39:47 +08:00
pycook
ae8d487af4 Readme is in Chinese by default
Committer: pycook <pycook@126.com>

Author:    pycook <pycook@126.com>
2020-05-28 20:35:11 +08:00
pycook
87c6554555 update readme 2020-05-28 20:28:49 +08:00
pycook
f5671c2a2a Fix: spelling mistakes 2020-05-28 20:28:49 +08:00
pycook
43ad3dfa7b release version 2.1 2020-05-28 20:28:49 +08:00
pycook
29fa17a0b8 update readme 2020-04-10 17:22:33 +08:00
pycook
5191d6ed73 Merge branch 'master' of https://github.com/pycook/cmdb 2020-04-07 18:03:03 +08:00
pycook
8348f8e7b1 Fix the judgment of app admin 2020-04-07 18:02:26 +08:00
pycook
75c48a0807 Fix: spelling mistakes 2020-04-01 21:40:51 +08:00
pycook
5b38385f7e release version 2.1 2020-04-01 21:20:47 +08:00
pycook
036e1d236b auth with ldap 2020-04-01 20:30:44 +08:00
pycook
c31be0f753 UI: batch update relation 2020-04-01 11:09:41 +08:00
pycook
764d2fac3f add .eslintrc.js 2020-03-26 17:35:26 +08:00
pycook
f4079e9c3e Merge pull request #42 from pycook/dependabot/npm_and_yarn/cmdb-ui/yarn-1.22.0
Bump yarn from 1.21.1 to 1.22.0 in /cmdb-ui
2020-03-26 17:29:53 +08:00
pycook
2a0ed72235 fix: delete attribute 2020-03-23 15:49:33 +08:00
dependabot[bot]
9e803ae4c7 Bump yarn from 1.21.1 to 1.22.0 in /cmdb-ui
Bumps [yarn](https://github.com/yarnpkg/yarn) from 1.21.1 to 1.22.0.
- [Release notes](https://github.com/yarnpkg/yarn/releases)
- [Changelog](https://github.com/yarnpkg/yarn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/yarnpkg/yarn/compare/v1.21.1...v1.22.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-03-22 03:14:30 +00:00
pycook
bebdb61adf update deps 2020-03-22 11:14:09 +08:00
pycook
f49cad771b Merge pull request #41 from pycook/dependabot/npm_and_yarn/cmdb-ui/acorn-5.7.4
Bump acorn from 5.7.3 to 5.7.4 in /cmdb-ui
2020-03-15 13:06:51 +08:00
dependabot[bot]
a5b4fbda40 Bump acorn from 5.7.3 to 5.7.4 in /cmdb-ui
Bumps [acorn](https://github.com/acornjs/acorn) from 5.7.3 to 5.7.4.
- [Release notes](https://github.com/acornjs/acorn/releases)
- [Commits](https://github.com/acornjs/acorn/compare/5.7.3...5.7.4)

Signed-off-by: dependabot[bot] <support@github.com>
2020-03-14 18:55:18 +00:00
pycook
2cce2d5cf2 Fix: permission management 2020-03-13 10:30:21 +08:00
pycook
e720b7af66 Merge branch 'develop' of https://github.com/pycook/cmdb into develop 2020-03-10 17:03:23 +08:00
pycook
09e4a5111b Merge pull request #40 from OhBonsai/develop
chore: use wait script to hang api before cache/db/es started
2020-03-10 17:01:58 +08:00
penzai
3539b12503 chore: use wait script to hang api before cache/db/es started 2020-03-10 16:49:40 +08:00
pycook
21d8673b5d Merge pull request #38 from AngrygrayWolf/dev
Change the requirements to support python3.8
2020-03-07 22:06:25 +08:00
what
7154426dc7 Change Pipfile 2020-03-07 21:51:37 +08:00
what
ca75c7dcd0 Change the requirements to support python3.8 2020-03-07 21:20:46 +08:00
pycook
194a2254a6 fix case sensitive of ES search 2020-02-28 14:32:51 +08:00
pycook
26abad14d0 Merge branch 'develop' of https://github.com/pycook/cmdb into develop 2020-02-23 23:13:59 +08:00
pycook
1521a71f9c alter table c_preference_relation_views column name varchar(64) 2020-02-23 23:13:22 +08:00
pycook
d425b455f1 Merge pull request #36 from OhBonsai/develop
test: add ci and ci relation crud test cases
2020-02-23 23:12:36 +08:00
pycook
230307474b version 2.1 and update readme 2020-02-23 20:21:13 +08:00
pycook
69d6b40e39 / redirect to /relation_views 2020-02-23 18:53:28 +08:00
pycook
5dc2f89e7f fix i18n 2020-02-23 18:41:23 +08:00
pycook
9eaca4d6a0 add library future 2020-02-21 23:29:20 +08:00
pycook
3680a462f5 Remove Chinese comments 2020-02-21 23:14:26 +08:00
pycook
3ac50e7cd8 lint 2020-02-21 22:46:12 +08:00
pycook
21b2cc1d5d The resource view is made into a two-level menu 2020-02-21 22:44:10 +08:00
penzai
cd5448cc7d test: add ci and ci relation crud test cases 2020-02-18 22:05:13 +08:00
pycook
10610bdb4b logo left justify 2020-02-16 19:18:51 +08:00
pycook
b5c2156387 Merge pull request #35 from shaohaojiecoder/i18n
I18n
2020-02-16 19:06:58 +08:00
pycook
b05ae0d1a7 Merge pull request #34 from OhBonsai/develop
add test cases
2020-02-16 19:06:24 +08:00
shaohaojiecoder
bbf6138d43 Merge branch 'develop' into i18n 2020-02-16 18:19:09 +08:00
shaohaojiecoder
1ba3e6a680 add a log pic 2020-02-16 18:18:15 +08:00
penzai
64045c1f93 test: add some test cases 2020-02-16 18:03:33 +08:00
penzai
5a3e55813c model: allow origin and ticket_id nullable in OperationRecord 2020-02-16 18:03:08 +08:00
penzai
bc72e58886 auth: add user in flask.g when auth by jwt 2020-02-16 18:02:24 +08:00
pycook
9e78955ba1 ACL i18n 2020-02-16 17:36:03 +08:00
pycook
136853d9a4 Merge pull request #33 from shaohaojiecoder/i18n
fix local storage for defalut lang
2020-02-16 14:51:59 +08:00
pycook
036e3ad00d modeling i18n 2020-02-16 14:50:17 +08:00
shaohaojiecoder
5ce6c93237 fix local storage for defalut lang 2020-02-16 13:47:30 +08:00
pycook
43dba7f7ed Merge pull request #32 from OhBonsai/develop
fix: recycle import by celery task
2020-02-16 10:38:27 +08:00
penzai
f4879d20d6 fix: recycle import by celery task 2020-02-16 09:39:33 +08:00
pycook
740e4c6034 i18n 2020-02-15 20:57:47 +08:00
pycook
0f2baa1d94 Merge pull request #31 from shaohaojiecoder/i18n
I18n
2020-02-11 09:50:50 +08:00
pycook
405b0af72c Merge branch 'develop' into i18n 2020-02-11 09:50:11 +08:00
shaohaojiecoder
a4e5178979 fix meta title 2020-02-09 19:50:23 +08:00
shaohaojiecoder
c14fe23283 add i18n basic structure 2020-02-09 17:54:57 +08:00
shaohaojiecoder
b3a058f908 add something 2020-02-09 17:22:17 +08:00
shaohaojiecoder
bd82a0e27c add some 2020-02-08 22:27:56 +08:00
pycook
f22a5c3543 Define display fields 2020-02-08 17:39:42 +08:00
pycook
ed81c3f091 Define display fields 2020-02-08 17:36:54 +08:00
shaohaojiecoder
07814b85f9 add basic 2020-02-07 22:05:52 +08:00
pycook
db52b28d6b fix jwt decode 2020-02-06 09:59:24 +08:00
pycook
fc85ba21c8 Merge pull request #29 from OhBonsai/master
fix ci_type_attr_group update bug and add ci_type test case
2020-02-04 12:47:15 +08:00
Bonsai
6c5ee3fcd9 Merge pull request #2 from pycook/master
merge from main repo
2020-02-04 10:56:03 +08:00
penzai
40f1ef88a9 test: add ci_type test cases 2020-02-04 10:54:16 +08:00
penzai
bce422ffc8 fix: update attribute group without name params will fail. #tests/test_cmdb_ci_type.py::test_update_attribute_group_ci_type 2020-02-04 10:53:07 +08:00
pycook
7c79066532 Merge branch 'master' of https://github.com/pycook/cmdb 2020-01-19 18:18:43 +08:00
pycook
1129ac93fb Merge pull request #28 from shaohaojiecoder/master
fix drag group and attrs
2020-01-19 18:18:23 +08:00
haojie.shao
5ab0e7e737 fix drag group and attrs 2020-01-19 18:14:53 +08:00
pycook
23319c7417 /ci_types/<int:type_id>/attributes/transfer and /ci_types/<int:type_id>/attribute_groups/transfer 2020-01-19 17:59:32 +08:00
pycook
c74f85cabb Merge pull request #26 from OhBonsai/master
test: add basic test code and attribute create api test case
2020-01-17 15:34:02 +08:00
penzai
fce2b689fb Merge remote-tracking branch 'origin/master' 2020-01-17 15:10:16 +08:00
penzai
105327bb0c test: add basic test code and attribute create api test case 2020-01-17 15:08:46 +08:00
Bonsai
745c43d0a4 Merge pull request #1 from pycook/master
merge master
2020-01-17 10:05:05 +08:00
pycook
3130d94568 [fix] cycle import 2020-01-15 11:52:33 +08:00
pycook
04a66eb239 flush cache when delete attribute 2020-01-15 09:06:31 +08:00
pycook
68390ec6f1 [fix] delete CIType's attribute 2020-01-14 20:52:36 +08:00
pycook
17392be138 api docs update 2020-01-06 21:58:06 +08:00
pycook
f2fdb29221 api docs update 2020-01-06 21:54:33 +08:00
pycook
4a18698423 update README 2019-12-31 22:53:21 +08:00
pycook
95ccee04f9 update README 2019-12-31 22:50:01 +08:00
pycook
b60628247b Update README.md
update
2019-12-31 22:37:36 +08:00
pycook
a6d7699ab4 Merge pull request #24 from fxiang21/master
add Readme of English
2019-12-31 22:32:48 +08:00
fxiang21
4b21bcc438 add Readme of English 2019-12-31 22:25:42 +08:00
pycook
33dce2f0f3 Update README.md
[fix] flask db-setup
2019-12-31 11:06:59 +08:00
pycook
d43b827fe5 [fix] fuzzy search 2019-12-25 13:36:43 +08:00
pycook
aec8bade41 [fix] security alerts 2019-12-25 10:19:03 +08:00
pycook
89ae89a449 [fix] validate attribute is required 2019-12-24 20:37:32 +08:00
pycook
945f90e386 disable eslint warning 2019-12-24 15:15:03 +08:00
pycook
2ba6a16613 support JSON type 2019-12-23 18:51:33 +08:00
pycook
6089039366 fix sidebar menu in mobile 2019-12-23 11:58:41 +08:00
pycook
e1e5307084 add yarn.lock 2019-12-23 11:27:47 +08:00
pycook
2ff7fce9dd flask init-acl 2019-12-20 12:57:39 +09:00
pycook
fc4d3e0c1a update makefile 2019-12-18 23:36:58 +09:00
pycook
f66a94712e Modify code organization 2019-12-18 23:33:22 +09:00
pycook
24664c7686 catch abort exception when getting relation views 2019-12-13 09:59:38 +08:00
pycook
1d668bab6e update 2019-12-12 21:45:19 +08:00
pycook
3d4b84909e fix delete relation view 2019-12-12 21:36:33 +08:00
pycook
8341e742eb [fix] update attribute which is list 2019-12-11 18:12:10 +08:00
pycook
a71ba83de0 release 2.0 2019-12-11 12:43:55 +08:00
pycook
9668131c18 V2.0 2019-12-11 12:14:23 +08:00
pycook
4a744dcad9 fix relation tree 2019-12-10 15:35:59 +08:00
pycook
2a420225e2 Merge pull request #22 from lovvvve/FixDelCi_type
fix(ci_type api): fix the judgment condition of deleting ci_type
2019-12-10 14:41:27 +08:00
Lovvvve
ff67785618 fix(ci_type api): fix the judgment condition of deleting ci_type 2019-12-10 14:31:27 +08:00
pycook
dfe1ba55d5 sidebar scroll 2019-12-09 17:16:38 +08:00
pycook
90b1b6b7af fix relation view 2019-12-09 12:03:58 +08:00
pycook
d5fbe42ed7 relation view bugfix 2019-12-08 00:20:55 +08:00
pycook
f424ad6864 acl done and bugfix 2019-12-06 22:33:31 +08:00
pycook
16b724bd40 ACL: permission management [doing] 2019-12-04 18:14:09 +08:00
pycook
f70ed54cad update readme 2019-12-04 09:26:01 +08:00
pycook
dd64564160 remove print 2019-12-03 22:13:14 +08:00
pycook
cc2cdbcc9f fix delete ci relation 2019-12-03 21:57:44 +08:00
pycook
81fe850627 fix get second cis api 2019-12-03 20:10:27 +08:00
pycook
487d9f76f6 关系视图定义支持两只方式 2019-12-03 19:54:01 +08:00
pycook
92dd4c5dfe relation view has been optimised 2019-12-03 19:10:54 +08:00
pycook
8ee7c6daf8 version 1.5: update docker file 2019-11-30 23:07:12 +08:00
pycook
882b158d18 cmdb.sql update 2019-11-29 22:21:41 +08:00
pycook
85222443c0 relation view [done] 2019-11-29 18:11:18 +08:00
pycook
1696ecf49d relation view [doing] 2019-11-28 21:17:06 +08:00
pycook
73b92ff533 relation view define [done] 2019-11-27 18:25:53 +08:00
pycook
e977bb15a5 GPLv2 2019-11-25 20:35:05 +08:00
pycook
7c46d6cdbf change to GPLv2 2019-11-25 20:33:56 +08:00
pycook
4d11c1f7db License change to GPLv3 2019-11-25 19:42:37 +08:00
pycook
0a563deb11 UI: relation type define [done] 2019-11-25 19:23:51 +08:00
pycook
ba80ec4403 /acl/resources add param resource_type_id 2019-11-24 22:33:57 +08:00
pycook
3b7cc4595b fix grant 2019-11-24 22:29:51 +08:00
pycook
9fe47657a6 Merge pull request #20 from kdyq007/master
[更新] 新增角色、资源、权限页面
2019-11-24 17:29:58 +08:00
kdyq007
5a4a6caa07 Merge branch 'master' of https://github.com/kdyq007/cmdb 2019-11-24 17:21:27 +08:00
kdyq007
9dadbe1599 Merge pull request #6 from pycook/master
fix acl api
2019-11-24 16:43:53 +08:00
pycook
40d016f513 fix acl api 2019-11-24 16:35:28 +08:00
kdyq007
655edaa7c8 [更新] 完成权限管理 2019-11-24 15:40:38 +08:00
kdyq007
7fa5cff919 [更新] 完成权限管理页面 2019-11-24 15:22:18 +08:00
kdyq007
d19834ed5d Merge pull request #5 from pycook/master
同步
2019-11-23 21:53:46 +08:00
pycook
b6be430aa3 fix 2019-11-23 21:50:45 +08:00
kdyq007
63792c242f [更新] 完成资源类型页面 2019-11-23 20:16:31 +08:00
kdyq007
10f7029722 [保存] 完成资源类型权限显示 2019-11-23 18:08:52 +08:00
pycook
ba176542dc fix acl resource 2019-11-23 17:42:33 +08:00
kdyq007
aae3b6e2ff Merge pull request #4 from pycook/master
fix acl resource_type
2019-11-23 17:36:42 +08:00
pycook
b370c7d46e fix acl resource_type 2019-11-23 17:24:43 +08:00
kdyq007
efa5a8ea5d Merge pull request #3 from pycook/master
同步
2019-11-23 14:52:41 +08:00
pycook
fd532626ac relative view api [done] 2019-11-22 18:18:22 +08:00
pycook
617337c614 Realize /api/v0.1/ci_relations/s [done] 2019-11-21 18:21:03 +08:00
kdyq007
9a3d24ac81 [更新] 保存一下 2019-11-20 19:02:36 +08:00
kdyq007
454dd4c56b Merge branch 'master' of https://github.com/kdyq007/cmdb 2019-11-19 21:52:33 +08:00
kdyq007
88ad72d4dc Merge pull request #2 from pycook/master
update
2019-11-19 21:52:02 +08:00
kdyq007
8d1517d550 [更新] 完成基础role和user管理 2019-11-19 21:49:51 +08:00
pycook
d3a8ef5966 fix get user by uid 2019-11-19 21:46:53 +08:00
pycook
e5baa5012d acl: resource type api 2019-11-19 21:41:46 +08:00
pycook
a1f63b00dd fix search 2019-11-19 18:32:35 +08:00
pycook
47ded84231 elastic search [done] 2019-11-19 18:16:31 +08:00
kdyq007
224a48a5f3 [更新] 去除app_id 2019-11-18 22:22:38 +08:00
pycook
0e7c52df71 es search update 2019-11-18 22:05:59 +08:00
pycook
ff701cc770 search by elasticsearch [doing] 2019-11-18 20:02:25 +08:00
kdyq007
6a7bb725cc Merge pull request #1 from pycook/master
怎么玩的?反向pull request
2019-11-18 18:31:14 +08:00
pycook
0a13186c13 fix acl api 2019-11-18 12:02:02 +08:00
kdyq007
a0ffeb9950 [更新] 完成角色管理页面 2019-11-17 21:08:04 +08:00
kdyq007
6c70ec6d53 [更新] 完成roles基本接口 2019-11-17 17:09:24 +08:00
qiqi
4b5f82699a [更新] 完成用户管理页面 2019-11-17 09:32:39 +08:00
pycook
f78c3b928b pep8 2019-11-15 18:03:06 +08:00
pycook
332659c1d5 update acl 2019-11-15 16:54:56 +08:00
pycook
3beb2706dc Merge pull request #18 from kdyq007/master
[更新] 修改图片路径、压缩图片
2019-11-14 21:59:38 +08:00
qiqi
a14111e1ce [更新] 优化格式 2019-11-14 21:51:58 +08:00
qiqi
c4320c14f9 [更新] 更换图片位置、压缩图片 2019-11-14 21:48:36 +08:00
qiqi
4c5442748f [更新] 优化说明文件格式 2019-11-14 21:00:24 +08:00
qiqi
a81750acba [更新] 新增Q群 README.md 2019-11-14 20:55:48 +08:00
pycook
0439e2462b update acl 2019-11-14 18:35:31 +08:00
pycook
3b62bd7ac9 update readme 2019-11-13 14:02:02 +08:00
pycook
f6add52721 python3.7 timezone fix 2019-11-13 13:56:44 +08:00
pycook
c85e535288 update acl 2019-11-13 13:25:42 +08:00
pycook
c0c6d116b5 docker images use aliyun 2019-11-13 11:56:17 +08:00
pycook
39153e92d1 update Makefile and support for install by make 2019-11-12 11:55:04 +08:00
pycook
42bcc2e510 fix py3 2019-11-12 11:15:25 +08:00
pycook
398fbb25dc merge Dockerfile 2019-11-12 10:40:37 +08:00
pycook
4b312d4f99 delete docs/Dockerfile 2019-11-11 23:12:50 +08:00
pycook
10414155a5 fix timezone 2019-11-11 23:11:12 +08:00
pycook
feda0c37e7 update README 2019-11-11 16:10:02 +08:00
pycook
173c120b64 flask init-cache 2019-11-11 15:46:57 +08:00
pycook
5f2a0d1a7b Remove package-lock.json and remove some compile warnings 2019-11-11 13:16:07 +08:00
pycook
50f894a01d add command init-cache 2019-11-11 11:27:43 +08:00
pycook
66e93e73af Merge branch 'master' of https://github.com/pycook/cmdb 2019-11-11 09:20:07 +08:00
pycook
58ad9d3f05 vue lint 2019-11-11 00:25:22 +08:00
pycook
08c96039e9 gunicorn==19.5.0 2019-11-10 19:10:23 +08:00
pycook
ca0dd97626 Docker to production 2019-11-10 19:06:38 +08:00
pycook
7810ee3974 Partially completed backend development of permissions management 2019-11-08 17:42:13 +08:00
pycook
2cfea7ef08 Update README.md
docker 一键安装说明补充
2019-11-08 15:26:22 +08:00
pycook
0cee6cea25 fix py2.7 unicode encoding error 2019-11-08 15:15:31 +08:00
pycook
5d13ba2f26 users drop is_admin 2019-11-08 14:58:21 +08:00
pycook
a583433530 fix unicode encode error 2019-11-08 14:37:53 +08:00
fxiang21
733ac3b2b4 移除多余的docker-start目录 2019-11-08 09:20:34 +08:00
fxiang21
ef6300255a 修复nginx转发问题 2019-11-08 09:20:27 +08:00
fxiang21
aad37dcf0b 添加容器化部署方式 2019-11-08 09:20:09 +08:00
pycook
cce10d39ea code format 2019-11-07 19:18:31 +08:00
pycook
c521dd447e Update README.md
pipenv run flask run -h 0.0.0.0
2019-11-05 17:52:45 +08:00
pycook
4d0cd4ba56 Update README.md
如果是非本机访问, 要修改ui/.env里VUE_APP_API_BASE_URL里的IP地址
2019-11-05 17:44:48 +08:00
pycook
7291274cb1 Update README.md
如果是非本机访问, 要修改ui/.env里VUE_APP_API_BASE_URL里的IP地址
2019-11-05 17:43:40 +08:00
pycook
44f2e383c3 update overview jpeg url 2019-11-01 11:37:16 +08:00
pycook
1f8219b418 fix add integer list 2019-11-01 11:27:24 +08:00
pycook
cb2f170ded mkdir logs, ignore *.log 2019-11-01 10:45:35 +08:00
pycook
1241a23ba8 Update README.md
add cmdb.sql
2019-10-28 21:48:46 +08:00
pycook
7d7744b7dc add docs/cmdb.sql 2019-10-28 21:46:41 +08:00
pycook
9c7d51127a fix date picker 2019-10-24 20:43:59 +08:00
pycook
b5a987f6b4 choice value tip fix 2019-10-24 20:43:58 +08:00
pycook
7bbc68bfd5 fix delete ci type 2019-10-24 20:43:58 +08:00
pycook
99d11e11ce fix ci types show 2019-10-24 20:43:58 +08:00
pycook
7b96ac4638 attribute alias must be unique 2019-10-24 20:43:58 +08:00
pycook
0a36330852 fix attributes paginate 2019-10-24 20:43:54 +08:00
pycook
9105f92c82 update README 2019-10-24 20:43:51 +08:00
pycook
57541ab486 Update README.md
create tables fix
2019-10-24 20:43:51 +08:00
pycook
a0fcbd220e attributes paginate and fix update value 2019-10-24 20:43:51 +08:00
pycook
d54b404eb6 add docs 2019-10-24 20:43:51 +08:00
pycook
620c5bb5eb ci search return unique key 2019-10-24 20:43:51 +08:00
pycook
0fde1d699d invalid username or password -> 403 2019-10-24 20:43:51 +08:00
shaohaojiecoder
61f77cf311 add batch module 2019-10-24 20:43:51 +08:00
lilixiang
13476128d5 add 添加属性库和模型模块 2019-10-24 20:43:51 +08:00
pycook
5cdb4ecd2a Revert "add 添加属性库和模型模块" 2019-10-24 20:43:51 +08:00
lilixiang
64c3b9da3b add 添加属性库和模型模块 2019-10-24 20:43:51 +08:00
pycook
55dad7a58c cache强制unicode编码 2019-08-30 09:46:24 +08:00
pycook
38dabc35e5 add .gitattributes 2019-08-28 21:08:28 +08:00
pycook
5b4f95a50e add ui 2019-08-28 20:51:51 +08:00
pycook
f3046d3c91 remove ui 2019-08-28 20:48:23 +08:00
pycook
5faae9af67 remove ui 2019-08-28 20:48:04 +08:00
pycook
c0b50642e0 update README 2019-08-28 20:45:59 +08:00
pycook
12ca296879 升级后端并开源UI 2019-08-28 20:34:10 +08:00
pycook
420c6cea2b delete。。。 2016-08-26 13:46:03 +08:00
pycook
ccc4bb48fa pep8 2016-06-27 10:50:32 +08:00
113 changed files with 21475 additions and 10283 deletions

View File

@@ -1,60 +0,0 @@
name: Bug Report
description: File a bug report
title: "[Bug]: "
labels: ["☢️ bug"]
assignees:
- Selina316
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: input
id: contact
attributes:
label: Contact Details
description: How can we get in touch with you if we need more info?
placeholder: ex. email@example.com
validations:
required: false
- type: dropdown
id: aspects
attributes:
label: This bug is related to UI or API?
multiple: true
options:
- UI
- API
- type: textarea
id: happened
attributes:
label: What happened?
description: Also tell us, what did you expect to happen?
placeholder: Tell us what you see!
value: "A bug happened!"
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: What version of our software are you running?
value: "newest"
validations:
required: true
- type: dropdown
id: browsers
attributes:
label: What browsers are you seeing the problem on?
multiple: true
options:
- Firefox
- Chrome
- Safari
- Microsoft Edge
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell

View File

@@ -1,44 +0,0 @@
name: Feature wanted
description: A new feature would be good
title: "[Feature]: "
labels: ["✏️ feature"]
assignees:
- pycook
body:
- type: markdown
attributes:
value: |
Thank you for your feature suggestion; we will evaluate it carefully!
- type: input
id: contact
attributes:
label: Contact Details
description: How can we get in touch with you if we need more info?
placeholder: ex. email@example.com
validations:
required: false
- type: dropdown
id: aspects
attributes:
label: feature is related to UI or API aspects?
multiple: true
options:
- UI
- API
- type: textarea
id: feature
attributes:
label: What is your advice?
description: Also tell us, what did you expect to happen?
placeholder: Tell us what you want!
value: "everyone wants this feature!"
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: What version of our software are you running?
value: "newest"
validations:
required: true

View File

@@ -1,36 +0,0 @@
name: Help wanted
description: I have a question
title: "[help wanted]: "
labels: ["help wanted"]
assignees:
- ivonGwy
body:
- type: markdown
attributes:
value: |
Please tell us what's you need!
- type: input
id: contact
attributes:
label: Contact Details
description: How can we get in touch with you if we need more info?
placeholder: ex. email@example.com
validations:
required: false
- type: textarea
id: question
attributes:
label: What is your question?
description: Also tell us, how can we help?
placeholder: Tell us what you need!
value: "i have a question!"
validations:
required: true
- type: input
id: version
attributes:
label: Version
description: What version of our software are you running?
value: "newest"
validations:
required: true

View File

@@ -1,60 +0,0 @@
name: Bug Report
description: File a bug report
title: "[Bug]: "
labels: ["bug"]
assignees:
- pycook
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: input
id: contact
attributes:
label: Contact Details
description: How can we get in touch with you if we need more info?
placeholder: ex. email@example.com
validations:
required: false
- type: dropdown
id: type
attributes:
label: bug is related to UI or API aspects?
multiple: true
options:
- UI
- API
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Also tell us, what did you expect to happen?
placeholder: Tell us what you see!
value: "A bug happened!"
validations:
required: true
- type: textarea
id: version
attributes:
label: Version
description: What version of our software are you running?
default: 2.3.5
validations:
required: true
- type: dropdown
id: browsers
attributes:
label: What browsers are you seeing the problem on?
multiple: true
options:
- Firefox
- Chrome
- Safari
- Microsoft Edge
- type: textarea
id: logs
attributes:
label: Relevant log output
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: shell

View File

@@ -1,6 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: veops official website
url: https://veops.cn/#hero
about: you can contact us here.

View File

@@ -1,44 +0,0 @@
name: Feature wanted
description: A new feature would be good
title: "[Feature]: "
labels: ["feature"]
assignees:
- pycook
body:
- type: markdown
attributes:
value: |
Thank you for your feature suggestion; we will evaluate it carefully!
- type: input
id: contact
attributes:
label: Contact Details
description: How can we get in touch with you if we need more info?
placeholder: ex. email@example.com
validations:
required: false
- type: dropdown
id: type
attributes:
label: feature is related to UI or API aspects?
multiple: true
options:
- UI
- API
- type: textarea
id: describe the feature
attributes:
label: What is your advice?
description: Also tell us, what did you expect to happen?
placeholder: Tell us what you want!
value: "everyone wants this feature!"
validations:
required: true
- type: textarea
id: version
attributes:
label: Version
description: What version of our software are you running?
default: 2.3.5
validations:
required: true

0
.github/config.yml vendored
View File

2
.gitignore vendored
View File

@@ -40,7 +40,6 @@ nosetests.xml
.pytest_cache
cmdb-api/test-output
cmdb-api/api/uploaded_files
cmdb-api/migrations/versions
# Translations
*.mo
@@ -70,7 +69,6 @@ settings.py
# UI
cmdb-ui/node_modules
cmdb-ui/dist
cmdb-ui/yarn.lock
# Log files
cmdb-ui/npm-debug.log*

View File

@@ -9,7 +9,7 @@ help: ## display this help
env: ## create a development environment using pipenv
sudo easy_install pip && \
pip install pipenv -i https://repo.huaweicloud.com/repository/pypi/simple && \
pip install pipenv -i https://pypi.douban.com/simple && \
npm install yarn && \
make deps
.PHONY: env
@@ -36,7 +36,7 @@ api: ## start api server
.PHONY: api
worker: ## start async tasks worker
cd cmdb-api && pipenv run celery -A celery_worker.celery worker -E -Q one_cmdb_async --autoscale=5,2 --logfile=one_cmdb_async.log -D && pipenv run celery -A celery_worker.celery worker -E -Q acl_async --autoscale=2,1 --logfile=one_acl_async.log -D
cd cmdb-api && pipenv run celery -A celery_worker.celery worker -E -Q one_cmdb_async --concurrency=1 -D && pipenv run celery -A celery_worker.celery worker -E -Q acl_async --concurrency=1 -D
.PHONY: worker
ui: ## start ui server

View File

@@ -1,20 +1,12 @@
![维易开源CMDB](docs/images/logo.png)
<p align="center">
<a href="https://veops.cn"><img src="docs/images/logo.png" alt="维易CMDB" width="300"/></a>
</p>
<h3 align="center">简单、轻量、通用的运维配置管理数据库</h3>
<p align="center">
<a href="https://github.com/veops/cmdb/blob/master/LICENSE"><img src="https://img.shields.io/badge/License-AGPLv3-brightgreen" alt="License: GPLv3"></a>
<a href="https:https://github.com/sendya/ant-design-pro-vue"><img src="https://img.shields.io/badge/UI-Ant%20Design%20Pro%20Vue-brightgreen" alt="UI"></a>
<a href="https://github.com/pallets/flask"><img src="https://img.shields.io/badge/API-Flask-brightgreen" alt="API"></a>
</p>
------------------------------
[![License](https://img.shields.io/badge/License-AGPLv3-brightgreen)](https://github.com/veops/cmdb/blob/master/LICENSE)
[![UI](https://img.shields.io/badge/UI-Ant%20Design%20Pro%20Vue-brightgreen)](https://github.com/sendya/ant-design-pro-vue)
[![API](https://img.shields.io/badge/API-Flask-brightgreen)](https://github.com/pallets/flask)
[English](docs/README_en.md) / [中文](README.md)
- 产品文档https://veops.cn/docs/
- 在线体验<a href="https://cmdb.veops.cn" target="_blank">CMDB</a>
- 在线体验: <a href="https://cmdb.veops.cn" target="_blank">CMDB</a>
- username: demo 或者 admin
- password: 123456
@@ -23,43 +15,45 @@
## 系统介绍
### 系统概览
### 整体架构
<img src=docs/images/dashboard.png />
<img src=docs/images/view.jpg />
[查看更多展示](docs/screenshot.md)
### 相关文章
### 相关文档
- <a href="https://mp.weixin.qq.com/s/v3eANth64UBW5xdyOkK3tg" target="_blank">概要设计</a>
- <a href="https://github.com/veops/cmdb/tree/master/docs/cmdb_api.md" target="_blank">API 文档</a>
- <a href="https://mp.weixin.qq.com/s/rQaf4AES7YJsyNQG_MKOLg" target="_blank">自动发现</a>
- 更多文章可以在公众号 **维易科技OneOps** 里查看
### 特点
- 灵活性
1. 配置灵活,不设定任何运维场景,有内置模板
2. 自动发现、入库 IT 资产
1. 规范并统一纳管复杂数据资产
2. 自动发现、入库 IT 资产
- 安全性
1. 细粒度权限控制
1. 细粒度访问控制
2. 完备操作日志
- 多应用
1. 丰富视图展示维度
2. API简单强大
2. 提供 Restful API
3. 支持定义属性触发器、计算属性
### 主要功能
- 模型属性支持索引、多值、默认排序、字体颜色,支持计算属性
- 支持自动发现、定时巡检、文件导入
- 支持资源、层级、关系视图展示
- 支持资源、树形、关系视图展示
- 支持模型间关系配置和展示
- 细粒度访问控制,完备的操作日志
- 支持跨模型搜索
### 系统概览
- 服务树
![服务树](docs/images/0.png "首页展示")
[查看更多展示](docs/screenshot.md)
### 更多功能
@@ -73,7 +67,7 @@
## 安装
### Docker 一键快速构建
- 进入主目录(先安装 docker 环境, 注意要clone整个项目
- 进入主目录(先安装 docker 环境)
```
docker-compose up -d
@@ -89,6 +83,6 @@ docker-compose up -d
---
_**欢迎关注公众号(维易科技OneOps),关注后可加入微信群,进行产品和技术交流。**_
_**欢迎关注我们的公众号点击联系我们加入微信、QQ群(336164978),获得更多产品、行业相关资讯**_
![公众号: 维易科技OneOps](docs/images/wechat.png)
![公众号: 维易科技OneOps](docs/images/qrcode_for_gzh.jpg)

View File

@@ -6,7 +6,7 @@ name = "pypi"
[packages]
# Flask
Flask = "==2.3.2"
Werkzeug = ">=2.3.6"
Werkzeug = "==2.3.6"
click = ">=5.0"
# Api
Flask-RESTful = "==0.3.10"
@@ -21,29 +21,28 @@ Flask-Migrate = "==2.5.2"
gunicorn = "==21.0.1"
supervisor = "==4.0.3"
# Auth
Flask-Login = ">=0.6.2"
Flask-Login = "==0.6.2"
Flask-Bcrypt = "==1.0.1"
Flask-Cors = ">=3.0.8"
ldap3 = "==2.9.1"
python-ldap = "==3.4.0"
pycryptodome = "==3.12.0"
cryptography = ">=41.0.2"
# Caching
Flask-Caching = ">=1.0.0"
# Environment variable parsing
environs = "==4.2.0"
marshmallow = "==2.20.2"
# async tasks
celery = ">=5.3.1"
celery = "==5.3.1"
celery_once = "==3.0.1"
more-itertools = "==5.0.0"
kombu = ">=5.3.1"
kombu = "==5.3.1"
# common setting
timeout-decorator = "==0.5.0"
WTForms = "==3.0.0"
email-validator = "==1.3.1"
treelib = "==1.6.1"
flasgger = "==0.9.5"
Pillow = ">=10.0.1"
Pillow = "==9.3.0"
# other
six = "==1.16.0"
bs4 = ">=0.0.1"
@@ -59,9 +58,6 @@ Jinja2 = "==3.1.2"
jinja2schema = "==0.1.4"
msgpack-python = "==0.5.6"
alembic = "==1.7.7"
hvac = "==2.0.0"
colorama = ">=0.4.6"
pycryptodomex = ">=3.19.0"
[dev-packages]
# Testing
@@ -78,3 +74,4 @@ flake8-isort = "==2.7.0"
isort = "==4.3.21"
pep8-naming = "==0.8.2"
pydocstyle = "==3.0.0"

View File

@@ -7,7 +7,6 @@ import os
import sys
from inspect import getmembers
from logging.handlers import RotatingFileHandler
from pathlib import Path
from flask import Flask
from flask import jsonify
@@ -18,14 +17,11 @@ from flask.json.provider import DefaultJSONProvider
import api.views.entry
from api.extensions import (bcrypt, cache, celery, cors, db, es, login_manager, migrate, rd)
from api.extensions import inner_secrets
from api.flask_cas import CAS
from api.lib.secrets.secrets import InnerKVManger
from api.models.acl import User
HERE = os.path.abspath(os.path.dirname(__file__))
PROJECT_ROOT = os.path.join(HERE, os.pardir)
BASE_DIR = Path(__file__).resolve().parent.parent
@login_manager.user_loader
@@ -80,6 +76,15 @@ class MyJSONEncoder(DefaultJSONProvider):
return o
def create_acl_app(config_object="settings"):
app = Flask(__name__.split(".")[0])
app.config.from_object(config_object)
register_extensions(app)
return app
def create_app(config_object="settings"):
"""Create application factory, as explained here: http://flask.pocoo.org/docs/patterns/appfactories/.
@@ -120,7 +125,7 @@ def register_extensions(app):
db.init_app(app)
cors.init_app(app)
login_manager.init_app(app)
migrate.init_app(app, db, directory=f"{BASE_DIR}/migrations")
migrate.init_app(app, db)
rd.init_app(app)
if app.config.get('USE_ES'):
es.init_app(app)
@@ -128,10 +133,6 @@ def register_extensions(app):
app.config.update(app.config.get("CELERY"))
celery.conf.update(app.config)
if app.config.get('SECRETS_ENGINE') == 'inner':
with app.app_context():
inner_secrets.init_app(app, InnerKVManger())
def register_blueprints(app):
for item in getmembers(api.views.entry):

View File

@@ -1,8 +1,6 @@
import click
from flask.cli import with_appcontext
from api.lib.perm.acl.user import UserCRUD
@click.command()
@with_appcontext
@@ -25,18 +23,50 @@ def init_acl():
role_rebuild.apply_async(args=(role.id, app.id), queue=ACL_QUEUE)
@click.command()
@with_appcontext
def add_user():
"""
create a user
is_admin: default is False
"""
username = click.prompt('Enter username', confirmation_prompt=False)
password = click.prompt('Enter password', hide_input=True, confirmation_prompt=True)
email = click.prompt('Enter email ', confirmation_prompt=False)
UserCRUD.add(username=username, password=password, email=email)
# @click.command()
# @with_appcontext
# def acl_clean():
# from api.models.acl import Resource
# from api.models.acl import Permission
# from api.models.acl import RolePermission
#
# perms = RolePermission.get_by(to_dict=False)
#
# for r in perms:
# perm = Permission.get_by_id(r.perm_id)
# if perm and perm.app_id != r.app_id:
# resource_id = r.resource_id
# resource = Resource.get_by_id(resource_id)
# perm_name = perm.name
# existed = Permission.get_by(resource_type_id=resource.resource_type_id, name=perm_name, first=True,
# to_dict=False)
# if existed is not None:
# other = RolePermission.get_by(rid=r.rid, perm_id=existed.id, resource_id=resource_id)
# if not other:
# r.update(perm_id=existed.id)
# else:
# r.soft_delete()
# else:
# r.soft_delete()
#
#
# @click.command()
# @with_appcontext
# def acl_has_resource_role():
# from api.models.acl import Role
# from api.models.acl import App
# from api.lib.perm.acl.cache import HasResourceRoleCache
# from api.lib.perm.acl.role import RoleCRUD
#
# roles = Role.get_by(to_dict=False)
# apps = App.get_by(to_dict=False)
# for role in roles:
# if role.app_id:
# res = RoleCRUD.recursive_resources(role.id, role.app_id)
# if res.get('resources') or res.get('groups'):
# HasResourceRoleCache.add(role.id, role.app_id)
# else:
# for app in apps:
# res = RoleCRUD.recursive_resources(role.id, app.id)
# if res.get('resources') or res.get('groups'):
# HasResourceRoleCache.add(role.id, app.id)

View File

@@ -7,7 +7,6 @@ import json
import time
import click
import requests
from flask import current_app
from flask.cli import with_appcontext
from flask_login import login_user
@@ -29,9 +28,7 @@ from api.lib.perm.acl.cache import AppCache
from api.lib.perm.acl.resource import ResourceCRUD
from api.lib.perm.acl.resource import ResourceTypeCRUD
from api.lib.perm.acl.role import RoleCRUD
from api.lib.secrets.inner import KeyManage
from api.lib.secrets.inner import global_key_threshold
from api.lib.secrets.secrets import InnerKVManger
from api.lib.perm.acl.user import UserCRUD
from api.models.acl import App
from api.models.acl import ResourceType
from api.models.cmdb import Attribute
@@ -56,7 +53,6 @@ def cmdb_init_cache():
if relations:
rd.create_or_update(relations, REDIS_PREFIX_CI_RELATION)
es = None
if current_app.config.get("USE_ES"):
from api.extensions import es
from api.models.cmdb import Attribute
@@ -127,10 +123,10 @@ def cmdb_init_acl():
# 3. add resource and grant
ci_types = CIType.get_by(to_dict=False)
resource_type_id = ResourceType.get_by(name=ResourceTypeEnum.CI, first=True, to_dict=False).id
type_id = ResourceType.get_by(name=ResourceTypeEnum.CI, first=True, to_dict=False).id
for ci_type in ci_types:
try:
ResourceCRUD.add(ci_type.name, resource_type_id, app_id)
ResourceCRUD.add(ci_type.name, type_id, app_id)
except AbortException:
pass
@@ -140,10 +136,10 @@ def cmdb_init_acl():
[PermEnum.READ])
relation_views = PreferenceRelationView.get_by(to_dict=False)
resource_type_id = ResourceType.get_by(name=ResourceTypeEnum.RELATION_VIEW, first=True, to_dict=False).id
type_id = ResourceType.get_by(name=ResourceTypeEnum.RELATION_VIEW, first=True, to_dict=False).id
for view in relation_views:
try:
ResourceCRUD.add(view.name, resource_type_id, app_id)
ResourceCRUD.add(view.name, type_id, app_id)
except AbortException:
pass
@@ -153,6 +149,57 @@ def cmdb_init_acl():
[PermEnum.READ])
@click.command()
@click.option(
'-u',
'--user',
help='username'
)
@click.option(
'-p',
'--password',
help='password'
)
@click.option(
'-m',
'--mail',
help='mail'
)
@with_appcontext
def add_user(user, password, mail):
"""
create a user
is_admin: default is False
Example: flask add-user -u <username> -p <password> -m <mail>
"""
assert user is not None
assert password is not None
assert mail is not None
UserCRUD.add(username=user, password=password, email=mail)
@click.command()
@click.option(
'-u',
'--user',
help='username'
)
@with_appcontext
def del_user(user):
"""
delete a user
Example: flask del-user -u <username>
"""
assert user is not None
from api.models.acl import User
u = User.get_by(username=user, first=True, to_dict=False)
u and UserCRUD.delete(u.uid)
@click.command()
@with_appcontext
def cmdb_counter():
@@ -264,197 +311,3 @@ def cmdb_index_table_upgrade():
CIIndexValueDateTime.create(ci_id=i.ci_id, attr_id=i.attr_id, value=i.value, commit=False)
i.delete(commit=False)
db.session.commit()
def valid_address(address):
if not address:
return False
if not address.startswith(("http://127.0.0.1", "https://127.0.0.1")):
response = {
"message": "Address should start with http://127.0.0.1 or https://127.0.0.1",
"status": "failed"
}
KeyManage.print_response(response)
return False
return True
@click.command()
@click.option(
'-a',
'--address',
help='inner cmdb api, http://127.0.0.1:8000',
)
@with_appcontext
def cmdb_inner_secrets_init(address):
"""
init inner secrets for password feature
"""
res, ok = KeyManage(backend=InnerKVManger).init()
if not ok:
if res.get("status") == "failed":
KeyManage.print_response(res)
return
token = res.get("details", {}).get("root_token", "")
if valid_address(address):
token = current_app.config.get("INNER_TRIGGER_TOKEN", "") if not token else token
if not token:
token = click.prompt(f'Enter root token', hide_input=True, confirmation_prompt=False)
assert token is not None
resp = requests.post("{}/api/v0.1/secrets/auto_seal".format(address.strip("/")),
headers={"Inner-Token": token})
if resp.status_code == 200:
KeyManage.print_response(resp.json())
else:
KeyManage.print_response({"message": resp.text or resp.status_code, "status": "failed"})
else:
KeyManage.print_response(res)
@click.command()
@click.option(
'-a',
'--address',
help='inner cmdb api, http://127.0.0.1:8000',
required=True,
)
@with_appcontext
def cmdb_inner_secrets_unseal(address):
"""
unseal the secrets feature
"""
if not valid_address(address):
return
address = "{}/api/v0.1/secrets/unseal".format(address.strip("/"))
for i in range(global_key_threshold):
token = click.prompt(f'Enter unseal token {i + 1}', hide_input=True, confirmation_prompt=False)
assert token is not None
resp = requests.post(address, headers={"Unseal-Token": token})
if resp.status_code == 200:
KeyManage.print_response(resp.json())
if resp.json().get("status") in ["success", "skip"]:
return
else:
KeyManage.print_response({"message": resp.status_code, "status": "failed"})
return
@click.command()
@click.option(
'-a',
'--address',
help='inner cmdb api, http://127.0.0.1:8000',
required=True,
)
@click.option(
'-k',
'--token',
help='root token',
prompt=True,
hide_input=True,
)
@with_appcontext
def cmdb_inner_secrets_seal(address, token):
"""
seal the secrets feature
"""
assert address is not None
assert token is not None
if not valid_address(address):
return
address = "{}/api/v0.1/secrets/seal".format(address.strip("/"))
resp = requests.post(address, headers={
"Inner-Token": token,
})
if resp.status_code == 200:
KeyManage.print_response(resp.json())
else:
KeyManage.print_response({"message": resp.status_code, "status": "failed"})
@click.command()
@with_appcontext
def cmdb_password_data_migrate():
"""
Migrate CI password data, version >= v2.3.6
"""
from api.models.cmdb import CIIndexValueText
from api.models.cmdb import CIValueText
from api.lib.secrets.inner import InnerCrypt
from api.lib.secrets.vault import VaultClient
attrs = Attribute.get_by(to_dict=False)
for attr in attrs:
if attr.is_password:
value_table = CIIndexValueText if attr.is_index else CIValueText
failed = False
for i in value_table.get_by(attr_id=attr.id, to_dict=False):
if current_app.config.get("SECRETS_ENGINE", 'inner') == 'inner':
_, status = InnerCrypt().decrypt(i.value)
if status:
continue
encrypt_value, status = InnerCrypt().encrypt(i.value)
if status:
CIValueText.create(ci_id=i.ci_id, attr_id=attr.id, value=encrypt_value)
else:
failed = True
continue
elif current_app.config.get("SECRETS_ENGINE") == 'vault':
if i.value == '******':
continue
vault = VaultClient(current_app.config.get('VAULT_URL'), current_app.config.get('VAULT_TOKEN'))
try:
vault.update("/{}/{}".format(i.ci_id, i.attr_id), dict(v=i.value))
except Exception as e:
print('save password to vault failed: {}'.format(e))
failed = True
continue
else:
continue
i.delete()
if not failed and attr.is_index:
attr.update(is_index=False)
@click.command()
@with_appcontext
def cmdb_agent_init():
"""
Initialize the agent's permissions and obtain the key and secret
"""
from api.models.acl import User
user = User.get_by(username="cmdb_agent", first=True, to_dict=False)
if user is None:
click.echo(
click.style('user cmdb_agent does not exist, please use flask add-user to create it first', fg='red'))
return
# grant
_app = AppCache.get('cmdb') or App.create(name='cmdb')
app_id = _app.id
ci_types = CIType.get_by(to_dict=False)
resource_type_id = ResourceType.get_by(name=ResourceTypeEnum.CI, first=True, to_dict=False).id
for ci_type in ci_types:
try:
ResourceCRUD.add(ci_type.name, resource_type_id, app_id)
except AbortException:
pass
ACLManager().grant_resource_to_role(ci_type.name,
"cmdb_agent",
ResourceTypeEnum.CI,
[PermEnum.READ, PermEnum.UPDATE, PermEnum.ADD, PermEnum.DELETE])
click.echo("Key : {}".format(click.style(user.key, bg='red')))
click.echo("Secret: {}".format(click.style(user.secret, bg='red')))

View File

@@ -10,6 +10,9 @@ from api.models.common_setting import Employee, Department
class InitEmployee(object):
"""
初始化员工
"""
def __init__(self):
self.log = current_app.logger
@@ -55,8 +58,7 @@ class InitEmployee(object):
self.log.error(ErrFormat.acl_import_user_failed.format(user['username'], str(e)))
self.log.error(e)
@staticmethod
def get_rid_by_uid(uid):
def get_rid_by_uid(self, uid):
from api.models.acl import Role
role = Role.get_by(first=True, uid=uid)
return role['id'] if role is not None else 0
@@ -69,8 +71,7 @@ class InitDepartment(object):
def init(self):
self.init_wide_company()
@staticmethod
def hard_delete(department_id, department_name):
def hard_delete(self, department_id, department_name):
existed_deleted_list = Department.query.filter(
Department.department_name == department_name,
Department.department_id == department_id,
@@ -79,12 +80,11 @@ class InitDepartment(object):
for existed in existed_deleted_list:
existed.delete()
@staticmethod
def get_department(department_name):
def get_department(self, department_name):
return Department.query.filter(
Department.department_name == department_name,
Department.deleted == 0,
).first()
).order_by(Department.created_at.asc()).first()
def run(self, department_id, department_name, department_parent_id):
self.hard_delete(department_id, department_name)
@@ -94,7 +94,7 @@ class InitDepartment(object):
if res.department_id == department_id:
return
else:
res.update(
new_d = res.update(
department_id=department_id,
department_parent_id=department_parent_id,
)
@@ -108,11 +108,11 @@ class InitDepartment(object):
new_d = self.get_department(department_name)
if new_d.department_id != department_id:
new_d.update(
new_d = new_d.update(
department_id=department_id,
department_parent_id=department_parent_id,
)
self.log.info(f"init {department_name} success.")
self.log.info(f"初始化 {department_name} 部门成功.")
def run_common(self, department_id, department_name, department_parent_id):
try:
@@ -123,14 +123,19 @@ class InitDepartment(object):
raise Exception(e)
def init_wide_company(self):
"""
创建 id 0, name 全公司 的部门
"""
department_id = 0
department_name = '全公司'
department_parent_id = -1
self.run_common(department_id, department_name, department_parent_id)
@staticmethod
def create_acl_role_with_department():
def create_acl_role_with_department(self):
"""
当前所有部门在ACL创建 role
"""
acl = ACLManager('acl')
role_name_map = {role['name']: role for role in acl.get_all_roles()}
@@ -141,7 +146,7 @@ class InitDepartment(object):
continue
role = role_name_map.get(department.department_name)
if not role:
if role is None:
payload = {
'app_id': 'acl',
'name': department.department_name,
@@ -160,65 +165,50 @@ class InitDepartment(object):
acl = self.check_app('backend')
resources_types = acl.get_all_resources_types()
perms = ['read', 'grant', 'delete', 'update']
acl_rid = self.get_admin_user_rid()
results = list(filter(lambda t: t['name'] == '操作权限', resources_types['groups']))
if len(results) == 0:
payload = dict(
app_id=acl.app_name,
name='操作权限',
description='',
perms=perms
perms=['read', 'grant', 'delete', 'update']
)
resource_type = acl.create_resources_type(payload)
else:
resource_type = results[0]
resource_type_id = resource_type['id']
existed_perms = resources_types.get('id2perms', {}).get(resource_type_id, [])
existed_perms = [p['name'] for p in existed_perms]
new_perms = []
for perm in perms:
if perm not in existed_perms:
new_perms.append(perm)
if len(new_perms) > 0:
resource_type['perms'] = existed_perms + new_perms
acl.update_resources_type(resource_type_id, resource_type)
resource_list = acl.get_resource_by_type(None, None, resource_type['id'])
for name in ['公司信息', '公司架构', '通知设置']:
target = list(filter(lambda r: r['name'] == name, resource_list))
if len(target) == 0:
payload = dict(
type_id=resource_type['id'],
app_id=acl.app_name,
name=name,
)
resource = acl.create_resource(payload)
else:
resource = target[0]
payload = dict(
type_id=resource_type['id'],
app_id=acl.app_name,
name=name,
)
try:
acl.create_resource(payload)
except Exception as e:
if '已经存在' in str(e):
pass
else:
raise Exception(e)
if acl_rid > 0:
acl.grant_resource(acl_rid, resource['id'], perms)
@staticmethod
def check_app(app_name):
def check_app(self, app_name):
acl = ACLManager(app_name)
payload = dict(
name=app_name,
description=app_name
)
app = acl.validate_app()
if not app:
acl.create_app(payload)
return acl
try:
app = acl.validate_app()
if app:
return acl
@staticmethod
def get_admin_user_rid():
admin = Employee.get_by(first=True, username='admin', to_dict=False)
return admin.acl_rid if admin else 0
acl.create_app(payload)
except Exception as e:
current_app.logger.error(e)
if '不存在' in str(e):
acl.create_app(payload)
return acl
raise Exception(e)
@click.command()
@@ -251,19 +241,17 @@ def common_check_new_columns():
from api.extensions import db
from sqlalchemy import inspect, text
def get_model_by_table_name(_table_name):
registry = getattr(db.Model, 'registry', None)
class_registry = getattr(registry, '_class_registry', None)
for _model in class_registry.values():
if hasattr(_model, '__tablename__') and _model.__tablename__ == _table_name:
return _model
def get_model_by_table_name(table_name):
for model in db.Model.registry._class_registry.values():
if hasattr(model, '__tablename__') and model.__tablename__ == table_name:
return model
return None
def add_new_column(target_table_name, new_column):
def add_new_column(table_name, new_column):
column_type = new_column.type.compile(engine.dialect)
default_value = new_column.default.arg if new_column.default else None
sql = "ALTER TABLE " + target_table_name + " ADD COLUMN " + new_column.name + " " + column_type
sql = f"ALTER TABLE {table_name} ADD COLUMN {new_column.name} {column_type} "
if new_column.comment:
sql += f" comment '{new_column.comment}'"
@@ -289,8 +277,7 @@ def common_check_new_columns():
model = get_model_by_table_name(table_name)
if model is None:
continue
model_columns = getattr(getattr(getattr(model, '__table__'), 'columns'), '_all_columns')
model_columns = model.__table__.columns._all_columns
for column in model_columns:
if column.name not in existed_column_name_list:
try:

View File

@@ -84,6 +84,66 @@ def clean():
os.remove(full_pathname)
@click.command()
@click.option("--url", default=None, help="Url to test (ex. /static/image.png)")
@click.option(
"--order", default="rule", help="Property on Rule to order by (default: rule)"
)
@with_appcontext
def urls(url, order):
"""Display all of the url matching routes for the project.
Borrowed from Flask-Script, converted to use Click.
"""
rows = []
column_headers = ("Rule", "Endpoint", "Arguments")
if url:
try:
rule, arguments = current_app.url_map.bind("localhost").match(
url, return_rule=True
)
rows.append((rule.rule, rule.endpoint, arguments))
column_length = 3
except (NotFound, MethodNotAllowed) as e:
rows.append(("<{}>".format(e), None, None))
column_length = 1
else:
rules = sorted(
current_app.url_map.iter_rules(), key=lambda rule: getattr(rule, order)
)
for rule in rules:
rows.append((rule.rule, rule.endpoint, None))
column_length = 2
str_template = ""
table_width = 0
if column_length >= 1:
max_rule_length = max(len(r[0]) for r in rows)
max_rule_length = max_rule_length if max_rule_length > 4 else 4
str_template += "{:" + str(max_rule_length) + "}"
table_width += max_rule_length
if column_length >= 2:
max_endpoint_length = max(len(str(r[1])) for r in rows)
max_endpoint_length = max_endpoint_length if max_endpoint_length > 8 else 8
str_template += " {:" + str(max_endpoint_length) + "}"
table_width += 2 + max_endpoint_length
if column_length >= 3:
max_arguments_length = max(len(str(r[2])) for r in rows)
max_arguments_length = max_arguments_length if max_arguments_length > 9 else 9
str_template += " {:" + str(max_arguments_length) + "}"
table_width += 2 + max_arguments_length
click.echo(str_template.format(*column_headers[:column_length]))
click.echo("-" * table_width)
for row in rows:
click.echo(str_template.format(*row[:column_length]))
@click.command()
@with_appcontext
def db_setup():

View File

@@ -9,7 +9,6 @@ from flask_login import LoginManager
from flask_migrate import Migrate
from flask_sqlalchemy import SQLAlchemy
from api.lib.secrets.inner import KeyManage
from api.lib.utils import ESHandler
from api.lib.utils import RedisHandler
@@ -22,4 +21,3 @@ celery = Celery()
cors = CORS(supports_credentials=True)
rd = RedisHandler()
es = ESHandler()
inner_secrets = KeyManage()

View File

@@ -60,34 +60,22 @@ class AttributeManager(object):
return []
@staticmethod
def _get_choice_values_from_other(choice_other):
def _get_choice_values_from_other_ci(choice_other):
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
if choice_other.get('type_ids'):
type_ids = choice_other.get('type_ids')
attr_id = choice_other.get('attr_id')
other_filter = choice_other.get('filter') or ''
type_ids = choice_other.get('type_ids')
attr_id = choice_other.get('attr_id')
other_filter = choice_other.get('filter') or ''
query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter)
s = search(query, fl=[str(attr_id)], facet=[str(attr_id)], count=1)
try:
_, _, _, _, _, facet = s.search()
return [[i[0], {}] for i in (list(facet.values()) or [[]])[0]]
except SearchError as e:
current_app.logger.error("get choice values from other ci failed: {}".format(e))
return []
elif choice_other.get('script'):
try:
x = compile(choice_other['script'], '', "exec")
local_ns = {}
exec(x, {}, local_ns)
res = local_ns['ChoiceValue']().values() or []
return [[i, {}] for i in res]
except Exception as e:
current_app.logger.error("get choice values from script: {}".format(e))
return []
query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter)
s = search(query, fl=[str(attr_id)], facet=[str(attr_id)], count=1)
try:
_, _, _, _, _, facet = s.search()
return [[i[0], {}] for i in (list(facet.values()) or [[]])[0]]
except SearchError as e:
current_app.logger.error("get choice values from other ci failed: {}".format(e))
return []
@classmethod
def get_choice_values(cls, attr_id, value_type, choice_web_hook, choice_other,
@@ -99,7 +87,7 @@ class AttributeManager(object):
return []
elif choice_other:
if choice_other_parse and isinstance(choice_other, dict):
return cls._get_choice_values_from_other(choice_other)
return cls._get_choice_values_from_other_ci(choice_other)
else:
return []
@@ -108,8 +96,7 @@ class AttributeManager(object):
return []
choice_values = choice_table.get_by(fl=["value", "option"], attr_id=attr_id)
return [[ValueTypeMap.serialize[value_type](choice_value['value']), choice_value['option']]
for choice_value in choice_values]
return [[choice_value['value'], choice_value['option']] for choice_value in choice_values]
@staticmethod
def add_choice_values(_id, value_type, choice_values):
@@ -231,15 +218,10 @@ class AttributeManager(object):
if name in BUILTIN_KEYWORDS:
return abort(400, ErrFormat.attribute_name_cannot_be_builtin)
while kwargs.get('choice_other'):
if isinstance(kwargs['choice_other'], dict):
if kwargs['choice_other'].get('script'):
break
if kwargs['choice_other'].get('type_ids') and kwargs['choice_other'].get('attr_id'):
break
return abort(400, ErrFormat.attribute_choice_other_invalid)
if kwargs.get('choice_other'):
if (not isinstance(kwargs['choice_other'], dict) or not kwargs['choice_other'].get('type_ids') or
not kwargs['choice_other'].get('attr_id')):
return abort(400, ErrFormat.attribute_choice_other_invalid)
alias = kwargs.pop("alias", "")
alias = name if not alias else alias
@@ -250,8 +232,6 @@ class AttributeManager(object):
kwargs.get('is_computed') and cls.can_create_computed_attribute()
kwargs.get('choice_other') and kwargs['choice_other'].get('script') and cls.can_create_computed_attribute()
attr = Attribute.create(flush=True,
name=name,
alias=alias,
@@ -337,6 +317,9 @@ class AttributeManager(object):
def update(self, _id, **kwargs):
attr = Attribute.get_by_id(_id) or abort(404, ErrFormat.attribute_not_found.format("id={}".format(_id)))
if not self._can_edit_attribute(attr):
return abort(403, ErrFormat.cannot_edit_attribute)
if kwargs.get("name"):
other = Attribute.get_by(name=kwargs['name'], first=True, to_dict=False)
if other and other.id != attr.id:
@@ -354,15 +337,10 @@ class AttributeManager(object):
self._change_index(attr, attr.is_index, kwargs['is_index'])
while kwargs.get('choice_other'):
if isinstance(kwargs['choice_other'], dict):
if kwargs['choice_other'].get('script'):
break
if kwargs['choice_other'].get('type_ids') and kwargs['choice_other'].get('attr_id'):
break
return abort(400, ErrFormat.attribute_choice_other_invalid)
if kwargs.get('choice_other'):
if (not isinstance(kwargs['choice_other'], dict) or not kwargs['choice_other'].get('type_ids') or
not kwargs['choice_other'].get('attr_id')):
return abort(400, ErrFormat.attribute_choice_other_invalid)
existed2 = attr.to_dict()
if not existed2['choice_web_hook'] and not existed2.get('choice_other') and existed2['is_choice']:
@@ -377,14 +355,6 @@ class AttributeManager(object):
kwargs.get('is_computed') and self.can_create_computed_attribute()
is_changed = False
for k in kwargs:
if kwargs[k] != getattr(attr, k, None):
is_changed = True
if is_changed and not self._can_edit_attribute(attr):
return abort(403, ErrFormat.cannot_edit_attribute)
attr.update(flush=True, filter_none=False, **kwargs)
if is_choice and choice_value:

View File

@@ -36,10 +36,9 @@ def parse_plugin_script(script):
attributes = []
try:
x = compile(script, '', "exec")
local_ns = {}
exec(x, {}, local_ns)
unique_key = local_ns['AutoDiscovery']().unique_key
attrs = local_ns['AutoDiscovery']().attributes() or []
exec(x)
unique_key = locals()['AutoDiscovery']().unique_key
attrs = locals()['AutoDiscovery']().attributes() or []
except Exception as e:
return abort(400, str(e))

View File

@@ -29,7 +29,6 @@ from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import REDIS_PREFIX_CI
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RetKey
from api.lib.cmdb.const import ValueTypeEnum
from api.lib.cmdb.history import AttributeHistoryManger
from api.lib.cmdb.history import CIRelationHistoryManager
from api.lib.cmdb.history import CITriggerHistoryManager
@@ -43,8 +42,6 @@ from api.lib.notify import notify_send
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import is_app_admin
from api.lib.perm.acl.acl import validate_permission
from api.lib.secrets.inner import InnerCrypt
from api.lib.secrets.vault import VaultClient
from api.lib.utils import Lock
from api.lib.utils import handle_arg_list
from api.lib.webhook import webhook_request
@@ -63,7 +60,6 @@ from api.tasks.cmdb import ci_relation_cache
from api.tasks.cmdb import ci_relation_delete
PRIVILEGED_USERS = {"worker", "cmdb_agent", "agent"}
PASSWORD_DEFAULT_SHOW = "******"
class CIManager(object):
@@ -327,8 +323,6 @@ class CIManager(object):
ci_attr2type_attr = {type_attr.attr_id: type_attr for type_attr, _ in attrs}
ci = None
record_id = None
password_dict = {}
need_lock = current_user.username not in current_app.config.get('PRIVILEGED_USERS', PRIVILEGED_USERS)
with Lock(ci_type_name, need_lock=need_lock):
existed = cls.ci_is_exist(unique_key, unique_value, ci_type.id)
@@ -357,23 +351,14 @@ class CIManager(object):
ci_dict.get(attr.name) is None and ci_dict.get(attr.alias) is None)):
ci_dict[attr.name] = attr.default.get('default')
if (type_attr.is_required and not attr.is_computed and
(attr.name not in ci_dict and attr.alias not in ci_dict)):
if type_attr.is_required and (attr.name not in ci_dict and attr.alias not in ci_dict):
return abort(400, ErrFormat.attribute_value_required.format(attr.name))
else:
for type_attr, attr in attrs:
if attr.default and attr.default.get('default') == AttributeDefaultValueEnum.UPDATED_AT:
ci_dict[attr.name] = now
computed_attrs = []
for _, attr in attrs:
if attr.is_computed:
computed_attrs.append(attr.to_dict())
elif attr.is_password:
if attr.name in ci_dict:
password_dict[attr.id] = ci_dict.pop(attr.name)
elif attr.alias in ci_dict:
password_dict[attr.id] = ci_dict.pop(attr.alias)
computed_attrs = [attr.to_dict() for _, attr in attrs if attr.is_computed] or None
value_manager = AttributeValueManager()
@@ -410,10 +395,6 @@ class CIManager(object):
cls.delete(ci.id)
raise e
if password_dict:
for attr_id in password_dict:
record_id = cls.save_password(ci.id, attr_id, password_dict[attr_id], record_id, ci_type.id)
if record_id: # has change
ci_cache.apply_async(args=(ci.id, operate_type, record_id), queue=CMDB_QUEUE)
@@ -433,16 +414,7 @@ class CIManager(object):
if attr.default and attr.default.get('default') == AttributeDefaultValueEnum.UPDATED_AT:
ci_dict[attr.name] = now
password_dict = dict()
computed_attrs = list()
for _, attr in attrs:
if attr.is_computed:
computed_attrs.append(attr.to_dict())
elif attr.is_password:
if attr.name in ci_dict:
password_dict[attr.id] = ci_dict.pop(attr.name)
elif attr.alias in ci_dict:
password_dict[attr.id] = ci_dict.pop(attr.alias)
computed_attrs = [attr.to_dict() for _, attr in attrs if attr.is_computed] or None
value_manager = AttributeValueManager()
@@ -451,7 +423,6 @@ class CIManager(object):
limit_attrs = self._valid_ci_for_no_read(ci) if not _is_admin else {}
record_id = None
need_lock = current_user.username not in current_app.config.get('PRIVILEGED_USERS', PRIVILEGED_USERS)
with Lock(ci.ci_type.name, need_lock=need_lock):
self._valid_unique_constraint(ci.type_id, ci_dict, ci_id)
@@ -469,10 +440,6 @@ class CIManager(object):
except BadRequest as e:
raise e
if password_dict:
for attr_id in password_dict:
record_id = self.save_password(ci.id, attr_id, password_dict[attr_id], record_id, ci.type_id)
if record_id: # has change
ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE)
@@ -498,17 +465,16 @@ class CIManager(object):
ci_dict = cls.get_cis_by_ids([ci_id])
ci_dict = ci_dict and ci_dict[0]
if ci_dict:
triggers = CITriggerManager.get(ci_dict['_type'])
for trigger in triggers:
option = trigger['option']
if not option.get('enable') or option.get('action') != OperateType.DELETE:
continue
triggers = CITriggerManager.get(ci_dict['_type'])
for trigger in triggers:
option = trigger['option']
if not option.get('enable') or option.get('action') != OperateType.DELETE:
continue
if option.get('filter') and not CITriggerManager.ci_filter(ci_dict.get('_id'), option['filter']):
continue
if option.get('filter') and not CITriggerManager.ci_filter(ci_dict.get('_id'), option['filter']):
continue
ci_delete_trigger.apply_async(args=(trigger, OperateType.DELETE, ci_dict), queue=CMDB_QUEUE)
ci_delete_trigger.apply_async(args=(trigger, OperateType.DELETE, ci_dict), queue=CMDB_QUEUE)
attrs = CITypeAttribute.get_by(type_id=ci.type_id, to_dict=False)
attr_names = set([AttributeCache.get(attr.attr_id).name for attr in attrs])
@@ -532,8 +498,7 @@ class CIManager(object):
db.session.commit()
if ci_dict:
AttributeHistoryManger.add(None, ci_id, [(None, OperateType.DELETE, ci_dict, None)], ci.type_id)
AttributeHistoryManger.add(None, ci_id, [(None, OperateType.DELETE, ci_dict, None)], ci.type_id)
ci_delete.apply_async(args=(ci_id,), queue=CMDB_QUEUE)
@@ -635,13 +600,10 @@ class CIManager(object):
_fields = list()
for field in fields:
attr = AttributeCache.get(field)
if attr is not None and not attr.is_password:
if attr is not None:
_fields.append(str(attr.id))
filter_fields_sql = "WHERE A.attr_id in ({0})".format(",".join(_fields))
ci2pos = {int(_id): _pos for _pos, _id in enumerate(ci_ids)}
res = [None] * len(ci_ids)
ci_ids = ",".join(map(str, ci_ids))
if value_tables is None:
value_tables = ValueTypeMap.table_name.values()
@@ -652,10 +614,11 @@ class CIManager(object):
# current_app.logger.debug(query_sql)
cis = db.session.execute(query_sql).fetchall()
ci_set = set()
res = list()
ci_dict = dict()
unique_id2obj = dict()
excludes = excludes and set(excludes)
for ci_id, type_id, attr_id, attr_name, attr_alias, value, value_type, is_list, is_password in cis:
for ci_id, type_id, attr_id, attr_name, attr_alias, value, value_type, is_list in cis:
if not fields and excludes and (attr_name in excludes or attr_alias in excludes):
continue
@@ -671,7 +634,7 @@ class CIManager(object):
ci_dict["unique"] = unique_id2obj[ci_type.unique_id] and unique_id2obj[ci_type.unique_id].name
ci_dict["unique_alias"] = unique_id2obj[ci_type.unique_id] and unique_id2obj[ci_type.unique_id].alias
ci_set.add(ci_id)
res[ci2pos[ci_id]] = ci_dict
res.append(ci_dict)
if ret_key == RetKey.NAME:
attr_key = attr_name
@@ -682,14 +645,11 @@ class CIManager(object):
else:
return abort(400, ErrFormat.argument_invalid.format("ret_key"))
if is_password and value:
ci_dict[attr_key] = PASSWORD_DEFAULT_SHOW
value = ValueTypeMap.serialize2[value_type](value)
if is_list:
ci_dict.setdefault(attr_key, []).append(value)
else:
value = ValueTypeMap.serialize2[value_type](value)
if is_list:
ci_dict.setdefault(attr_key, []).append(value)
else:
ci_dict[attr_key] = value
ci_dict[attr_key] = value
return res
@@ -721,84 +681,6 @@ class CIManager(object):
return cls._get_cis_from_db(ci_ids, ret_key, fields, value_tables, excludes=excludes)
@classmethod
def save_password(cls, ci_id, attr_id, value, record_id, type_id):
changed = None
encrypt_value = None
value_table = ValueTypeMap.table[ValueTypeEnum.PASSWORD]
if current_app.config.get('SECRETS_ENGINE') == 'inner':
if value:
encrypt_value, status = InnerCrypt().encrypt(value)
if not status:
current_app.logger.error('save password failed: {}'.format(encrypt_value))
return abort(400, ErrFormat.password_save_failed.format(encrypt_value))
else:
encrypt_value = PASSWORD_DEFAULT_SHOW
existed = value_table.get_by(ci_id=ci_id, attr_id=attr_id, first=True, to_dict=False)
if existed is None:
if value:
value_table.create(ci_id=ci_id, attr_id=attr_id, value=encrypt_value)
changed = [(ci_id, attr_id, OperateType.ADD, '', PASSWORD_DEFAULT_SHOW, type_id)]
elif existed.value != encrypt_value:
if value:
existed.update(ci_id=ci_id, attr_id=attr_id, value=encrypt_value)
changed = [(ci_id, attr_id, OperateType.UPDATE, PASSWORD_DEFAULT_SHOW, PASSWORD_DEFAULT_SHOW, type_id)]
else:
existed.delete()
changed = [(ci_id, attr_id, OperateType.DELETE, PASSWORD_DEFAULT_SHOW, '', type_id)]
if current_app.config.get('SECRETS_ENGINE') == 'vault':
vault = VaultClient(current_app.config.get('VAULT_URL'), current_app.config.get('VAULT_TOKEN'))
if value:
try:
vault.update("/{}/{}".format(ci_id, attr_id), dict(v=value))
except Exception as e:
current_app.logger.error('save password to vault failed: {}'.format(e))
return abort(400, ErrFormat.password_save_failed.format('write vault failed'))
else:
try:
vault.delete("/{}/{}".format(ci_id, attr_id))
except Exception as e:
current_app.logger.warning('delete password to vault failed: {}'.format(e))
if changed is not None:
return AttributeValueManager.write_change2(changed, record_id)
@classmethod
def load_password(cls, ci_id, attr_id):
ci = CI.get_by_id(ci_id) or abort(404, ErrFormat.ci_not_found.format(ci_id))
limit_attrs = cls._valid_ci_for_no_read(ci, ci.ci_type)
if limit_attrs:
attr = AttributeCache.get(attr_id)
if attr and attr.name not in limit_attrs:
return abort(403, ErrFormat.no_permission2)
if current_app.config.get('SECRETS_ENGINE', 'inner') == 'inner':
value_table = ValueTypeMap.table[ValueTypeEnum.PASSWORD]
v = value_table.get_by(ci_id=ci_id, attr_id=attr_id, first=True, to_dict=False)
v = v and v.value
if not v:
return
decrypt_value, status = InnerCrypt().decrypt(v)
if not status:
current_app.logger.error('load password failed: {}'.format(decrypt_value))
return abort(400, ErrFormat.password_load_failed.format(decrypt_value))
return decrypt_value
elif current_app.config.get('SECRETS_ENGINE') == 'vault':
vault = VaultClient(current_app.config.get('VAULT_URL'), current_app.config.get('VAULT_TOKEN'))
data, status = vault.read("/{}/{}".format(ci_id, attr_id))
if not status:
current_app.logger.error('read password from vault failed: {}'.format(data))
return abort(400, ErrFormat.password_load_failed.format(data))
return data.get('v')
class CIRelationManager(object):
"""

View File

@@ -12,8 +12,6 @@ class ValueTypeEnum(BaseEnum):
DATE = "4"
TIME = "5"
JSON = "6"
PASSWORD = TEXT
LINK = TEXT
class ConstraintEnum(BaseEnum):

View File

@@ -95,6 +95,3 @@ class ErrFormat(CommonErrFormat):
ci_filter_perm_cannot_or_query = "CI过滤授权 暂时不支持 或 查询"
ci_filter_perm_attr_no_permission = "您没有属性 {} 的操作权限!"
ci_filter_perm_ci_no_permission = "您没有该CI的操作权限!"
password_save_failed = "保存密码失败: {}"
password_load_failed = "获取密码失败: {}"

View File

@@ -7,7 +7,6 @@ QUERY_CIS_BY_VALUE_TABLE = """
attr.alias AS attr_alias,
attr.value_type,
attr.is_list,
attr.is_password,
c_cis.type_id,
{0}.ci_id,
{0}.attr_id,
@@ -27,8 +26,7 @@ QUERY_CIS_BY_IDS = """
A.attr_alias,
A.value,
A.value_type,
A.is_list,
A.is_password
A.is_list
FROM
({1}) AS A {0}
ORDER BY A.ci_id;
@@ -45,7 +43,7 @@ FACET_QUERY1 = """
FACET_QUERY = """
SELECT {0}.value,
count(distinct {0}.ci_id)
count({0}.ci_id)
FROM {0}
INNER JOIN ({1}) AS F ON F.ci_id={0}.ci_id
WHERE {0}.attr_id={2:d}

View File

@@ -9,7 +9,6 @@ import time
from flask import current_app
from flask_login import current_user
from jinja2 import Template
from sqlalchemy import text
from api.extensions import db
from api.lib.cmdb.cache import AttributeCache
@@ -29,7 +28,6 @@ from api.lib.cmdb.search.ci.db.query_sql import QUERY_CI_BY_NO_ATTR
from api.lib.cmdb.search.ci.db.query_sql import QUERY_CI_BY_TYPE
from api.lib.cmdb.search.ci.db.query_sql import QUERY_UNION_CI_ATTRIBUTE_IS_NULL
from api.lib.cmdb.utils import TableMap
from api.lib.cmdb.utils import ValueTypeMap
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import is_app_admin
from api.lib.utils import handle_arg_list
@@ -313,7 +311,7 @@ class Search(object):
start = time.time()
execute = db.session.execute
# current_app.logger.debug(v_query_sql)
res = execute(text(v_query_sql)).fetchall()
res = execute(v_query_sql).fetchall()
end_time = time.time()
current_app.logger.debug("query ci ids time is: {0}".format(end_time - start))
@@ -526,15 +524,15 @@ class Search(object):
if k:
table_name = TableMap(attr=attr).table_name
query_sql = FACET_QUERY.format(table_name, self.query_sql, attr.id)
result = db.session.execute(text(query_sql)).fetchall()
# current_app.logger.warning(query_sql)
result = db.session.execute(query_sql).fetchall()
facet[k] = result
facet_result = dict()
for k, v in facet.items():
if not k.startswith('_'):
attr = AttributeCache.get(k)
a = getattr(attr, self.ret_key)
facet_result[a] = [(ValueTypeMap.serialize[attr.value_type](f[0]), f[1], a) for f in v]
a = getattr(AttributeCache.get(k), self.ret_key)
facet_result[a] = [(f[0], f[1], a) for f in v]
return facet_result

View File

@@ -35,7 +35,7 @@ class Search(object):
self.sort = sort or ("ci_id" if current_app.config.get("USE_ES") else None)
self.root_id = root_id
self.level = level or 0
self.level = level
self.reverse = reverse
def _get_ids(self):
@@ -104,22 +104,16 @@ class Search(object):
ci_ids=merge_ids).search()
def statistics(self, type_ids):
self.level = int(self.level)
_tmp = []
ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id
for lv in range(0, self.level):
if not lv:
if type_ids and lv == self.level - 1:
_tmp = list(map(lambda x: [i for i in x if i[1] in type_ids],
(map(lambda x: list(json.loads(x).items()),
[i or '{}' for i in rd.get(ids, REDIS_PREFIX_CI_RELATION) or []]))))
else:
_tmp = list(map(lambda x: list(json.loads(x).items()),
[i or '{}' for i in rd.get(ids, REDIS_PREFIX_CI_RELATION) or []]))
for l in range(0, int(self.level)):
if not l:
_tmp = list(map(lambda x: list(json.loads(x).items()),
[i or '{}' for i in rd.get(ids, REDIS_PREFIX_CI_RELATION) or []]))
else:
for idx, item in enumerate(_tmp):
if item:
if type_ids and lv == self.level - 1:
if type_ids and l == self.level - 1:
__tmp = list(
map(lambda x: [(_id, type_id) for _id, type_id in json.loads(x).items()
if type_id in type_ids],

View File

@@ -12,7 +12,7 @@ import api.models.cmdb as model
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.const import ValueTypeEnum
TIME_RE = re.compile(r"^20|21|22|23|[0-1]\d:[0-5]\d:[0-5]\d$")
TIME_RE = re.compile(r"^(20|21|22|23|[0-1]\d):[0-5]\d:[0-5]\d$")
def string2int(x):
@@ -21,7 +21,7 @@ def string2int(x):
def str2datetime(x):
try:
return datetime.datetime.strptime(x, "%Y-%m-%d").date()
return datetime.datetime.strptime(x, "%Y-%m-%d")
except ValueError:
pass
@@ -44,8 +44,8 @@ class ValueTypeMap(object):
ValueTypeEnum.FLOAT: float,
ValueTypeEnum.TEXT: lambda x: x if isinstance(x, six.string_types) else str(x),
ValueTypeEnum.TIME: lambda x: x if isinstance(x, six.string_types) else str(x),
ValueTypeEnum.DATE: lambda x: x.strftime("%Y-%m-%d") if not isinstance(x, six.string_types) else x,
ValueTypeEnum.DATETIME: lambda x: x.strftime("%Y-%m-%d %H:%M:%S") if not isinstance(x, six.string_types) else x,
ValueTypeEnum.DATE: lambda x: x.strftime("%Y-%m-%d"),
ValueTypeEnum.DATETIME: lambda x: x.strftime("%Y-%m-%d %H:%M:%S"),
ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x,
}
@@ -64,8 +64,6 @@ class ValueTypeMap(object):
ValueTypeEnum.FLOAT: model.FloatChoice,
ValueTypeEnum.TEXT: model.TextChoice,
ValueTypeEnum.TIME: model.TextChoice,
ValueTypeEnum.DATE: model.TextChoice,
ValueTypeEnum.DATETIME: model.TextChoice,
}
table = {
@@ -99,7 +97,7 @@ class ValueTypeMap(object):
ValueTypeEnum.DATE: 'text',
ValueTypeEnum.TIME: 'text',
ValueTypeEnum.FLOAT: 'float',
ValueTypeEnum.JSON: 'object',
ValueTypeEnum.JSON: 'object'
}
@@ -112,9 +110,7 @@ class TableMap(object):
@property
def table(self):
attr = AttributeCache.get(self.attr_name) if not self.attr else self.attr
if attr.is_password or attr.is_link:
self.is_index = False
elif attr.value_type not in {ValueTypeEnum.TEXT, ValueTypeEnum.JSON}:
if attr.value_type != ValueTypeEnum.TEXT and attr.value_type != ValueTypeEnum.JSON:
self.is_index = True
elif self.is_index is None:
self.is_index = attr.is_index
@@ -126,9 +122,7 @@ class TableMap(object):
@property
def table_name(self):
attr = AttributeCache.get(self.attr_name) if not self.attr else self.attr
if attr.is_password or attr.is_link:
self.is_index = False
elif attr.value_type not in {ValueTypeEnum.TEXT, ValueTypeEnum.JSON}:
if attr.value_type != ValueTypeEnum.TEXT and attr.value_type != ValueTypeEnum.JSON:
self.is_index = True
elif self.is_index is None:
self.is_index = attr.is_index

View File

@@ -66,10 +66,9 @@ class AttributeValueManager(object):
use_master=use_master,
to_dict=False)
field_name = getattr(attr, ret_key)
if attr.is_list:
res[field_name] = [ValueTypeMap.serialize[attr.value_type](i.value) for i in rs]
elif attr.is_password and rs:
res[field_name] = '******' if rs[0].value else ''
else:
res[field_name] = ValueTypeMap.serialize[attr.value_type](rs[0].value) if rs else None
@@ -132,7 +131,8 @@ class AttributeValueManager(object):
return AttributeHistoryManger.add(record_id, ci_id, [(attr_id, operate_type, old, new)], type_id)
@staticmethod
def write_change2(changed, record_id=None):
def _write_change2(changed):
record_id = None
for ci_id, attr_id, operate_type, old, new, type_id in changed:
record_id = AttributeHistoryManger.add(record_id, ci_id, [(attr_id, operate_type, old, new)], type_id,
commit=False, flush=False)
@@ -284,9 +284,9 @@ class AttributeValueManager(object):
except Exception as e:
db.session.rollback()
current_app.logger.warning(str(e))
return abort(400, ErrFormat.attribute_value_unknown_error.format(e.args[0]))
return abort(400, ErrFormat.attribute_value_unknown_error.format(str(e)))
return self.write_change2(changed)
return self._write_change2(changed)
@staticmethod
def delete_attr_value(attr_id, ci_id):

View File

@@ -1,13 +1,12 @@
# -*- coding:utf-8 -*-
from flask import abort
from flask import current_app
from api.lib.common_setting.resp_format import ErrFormat
from api.lib.perm.acl.app import AppCRUD
from api.lib.perm.acl.cache import RoleCache, AppCache
from api.lib.perm.acl.permission import PermissionCRUD
from api.lib.perm.acl.resource import ResourceTypeCRUD, ResourceCRUD
from api.lib.perm.acl.role import RoleCRUD, RoleRelationCRUD
from api.lib.perm.acl.user import UserCRUD
from api.lib.perm.acl.resource import ResourceTypeCRUD, ResourceCRUD
class ACLManager(object):
@@ -80,22 +79,20 @@ class ACLManager(object):
return role.to_dict()
@staticmethod
def delete_role(_id):
def delete_role(_id, payload):
RoleCRUD.delete_role(_id)
return dict(rid=_id)
def get_user_info(self, username):
from api.lib.perm.acl.acl import ACLManager as ACL
user_info = ACL().get_user_info(username, self.app_name)
result = dict(
name=user_info.get('nickname') or username,
username=user_info.get('username') or username,
email=user_info.get('email'),
uid=user_info.get('uid'),
rid=user_info.get('rid'),
role=dict(permissions=user_info.get('parents')),
avatar=user_info.get('avatar')
)
result = dict(name=user_info.get('nickname') or username,
username=user_info.get('username') or username,
email=user_info.get('email'),
uid=user_info.get('uid'),
rid=user_info.get('rid'),
role=dict(permissions=user_info.get('parents')),
avatar=user_info.get('avatar'))
return result
@@ -112,32 +109,8 @@ class ACLManager(object):
id2perms=id2perms
)
def create_resources_type(self, payload):
payload['app_id'] = self.validate_app().id
rt = ResourceTypeCRUD.add(**payload)
return rt.to_dict()
def update_resources_type(self, _id, payload):
rt = ResourceTypeCRUD.update(_id, **payload)
return rt.to_dict()
def create_resource(self, payload):
payload['app_id'] = self.validate_app().id
resource = ResourceCRUD.add(**payload)
return resource.to_dict()
def get_resource_by_type(self, q, u, rt_id, page=1, page_size=999999):
numfound, res = ResourceCRUD.search(q, u, self.validate_app().id, rt_id, page, page_size)
return res
def grant_resource(self, rid, resource_id, perms):
PermissionCRUD.grant(rid, perms, resource_id=resource_id, group_id=None)
@staticmethod
def create_app(payload):
rt = AppCRUD.add(**payload)
return rt.to_dict()

View File

@@ -1,6 +1,6 @@
# -*- coding:utf-8 -*-
from flask import abort, current_app
from flask import abort
from treelib import Tree
from wtforms import Form
from wtforms import IntegerField
@@ -9,7 +9,6 @@ from wtforms import validators
from api.extensions import db
from api.lib.common_setting.resp_format import ErrFormat
from api.lib.common_setting.acl import ACLManager
from api.lib.perm.acl.role import RoleCRUD
from api.models.common_setting import Department, Employee
@@ -153,10 +152,6 @@ class DepartmentForm(Form):
class DepartmentCRUD(object):
@staticmethod
def get_department_by_id(d_id, to_dict=True):
return Department.get_by(first=True, department_id=d_id, to_dict=to_dict)
@staticmethod
def add(**kwargs):
DepartmentCRUD.check_department_name_unique(kwargs['department_name'])
@@ -191,11 +186,10 @@ class DepartmentCRUD(object):
filter(lambda d: d['department_id'] == department_parent_id, allow_p_d_id_list))
if len(target) == 0:
try:
dep = Department.get_by(
d = Department.get_by(
first=True, to_dict=False, department_id=department_parent_id)
name = dep.department_name if dep else ErrFormat.department_id_not_found.format(department_parent_id)
name = d.department_name if d else ErrFormat.department_id_not_found.format(department_parent_id)
except Exception as e:
current_app.logger.error(str(e))
name = ErrFormat.department_id_not_found.format(department_parent_id)
abort(400, ErrFormat.cannot_to_be_parent_department.format(name))
@@ -259,7 +253,7 @@ class DepartmentCRUD(object):
try:
RoleCRUD.delete_role(existed.acl_rid)
except Exception as e:
current_app.logger.error(str(e))
pass
return existed.soft_delete()
@@ -274,7 +268,7 @@ class DepartmentCRUD(object):
try:
tree.remove_subtree(department_id)
except Exception as e:
current_app.logger.error(str(e))
pass
[allow_d_id_list.append({'department_id': int(n.identifier), 'department_name': n.tag}) for n in
tree.all_nodes()]
@@ -396,125 +390,6 @@ class DepartmentCRUD(object):
[id_list.append(int(n.identifier))
for n in tmp_tree.all_nodes()]
except Exception as e:
current_app.logger.error(str(e))
pass
return id_list
class EditDepartmentInACL(object):
@staticmethod
def add_department_to_acl(department_id, op_uid):
db_department = DepartmentCRUD.get_department_by_id(department_id, to_dict=False)
if not db_department:
return
from api.models.acl import Role
role = Role.get_by(first=True, name=db_department.department_name, app_id=None)
acl = ACLManager('acl', str(op_uid))
if role is None:
payload = {
'app_id': 'acl',
'name': db_department.department_name,
}
role = acl.create_role(payload)
acl_rid = role.get('id') if role else 0
db_department.update(
acl_rid=acl_rid
)
info = f"add_department_to_acl, acl_rid: {acl_rid}"
current_app.logger.info(info)
return info
@staticmethod
def delete_department_from_acl(department_rids, op_uid):
acl = ACLManager('acl', str(op_uid))
result = []
for rid in department_rids:
try:
acl.delete_role(rid)
except Exception as e:
result.append(f"delete_department_in_acl, rid: {rid}, error: {e}")
continue
return result
@staticmethod
def edit_department_name_in_acl(d_rid: int, d_name: str, op_uid: int):
acl = ACLManager('acl', str(op_uid))
payload = {
'name': d_name
}
try:
acl.edit_role(d_rid, payload)
except Exception as e:
return f"edit_department_name_in_acl, rid: {d_rid}, error: {e}"
return f"edit_department_name_in_acl, rid: {d_rid}, success"
@staticmethod
def edit_employee_department_in_acl(e_list: list, new_d_id: int, op_uid: int):
result = []
new_department = DepartmentCRUD.get_department_by_id(new_d_id, False)
if not new_department:
result.append(f"{new_d_id} new_department is None")
return result
from api.models.acl import Role
new_role = Role.get_by(first=True, name=new_department.department_name, app_id=None)
new_d_rid_in_acl = new_role.get('id') if new_role else 0
if new_d_rid_in_acl == 0:
return
if new_d_rid_in_acl != new_department.acl_rid:
new_department.update(
acl_rid=new_d_rid_in_acl
)
new_department_acl_rid = new_department.acl_rid if new_d_rid_in_acl == new_department.acl_rid else \
new_d_rid_in_acl
acl = ACLManager('acl', str(op_uid))
for employee in e_list:
old_department = DepartmentCRUD.get_department_by_id(employee.get('department_id'), False)
if not old_department:
continue
employee_acl_rid = employee.get('e_acl_rid')
if employee_acl_rid == 0:
result.append(f"employee_acl_rid == 0")
continue
old_role = Role.get_by(first=True, name=old_department.department_name, app_id=None)
old_d_rid_in_acl = old_role.get('id') if old_role else 0
if old_d_rid_in_acl == 0:
return
if old_d_rid_in_acl != old_department.acl_rid:
old_department.update(
acl_rid=old_d_rid_in_acl
)
d_acl_rid = old_department.acl_rid if old_d_rid_in_acl == old_department.acl_rid else old_d_rid_in_acl
payload = {
'app_id': 'acl',
'parent_id': d_acl_rid,
}
try:
acl.remove_user_from_role(employee_acl_rid, payload)
except Exception as e:
result.append(
f"remove_user_from_role employee_acl_rid: {employee_acl_rid}, parent_id: {d_acl_rid}, err: {e}")
payload = {
'app_id': 'acl',
'child_ids': [employee_acl_rid],
}
try:
acl.add_user_to_role(new_department_acl_rid, payload)
except Exception as e:
result.append(
f"add_user_to_role employee_acl_rid: {employee_acl_rid}, parent_id: {d_acl_rid}, err: {e}")
return result

View File

@@ -121,19 +121,6 @@ class EmployeeCRUD(object):
employee = CreateEmployee().create_single(**data)
return employee.to_dict()
@staticmethod
def add_employee_from_acl_created(**kwargs):
try:
kwargs['acl_uid'] = kwargs.pop('uid')
kwargs['acl_rid'] = kwargs.pop('rid')
kwargs['department_id'] = 0
Employee.create(
**kwargs
)
except Exception as e:
abort(400, str(e))
@staticmethod
def add(**kwargs):
try:
@@ -178,7 +165,7 @@ class EmployeeCRUD(object):
def edit_employee_by_uid(_uid, **kwargs):
existed = EmployeeCRUD.get_employee_by_uid(_uid)
try:
edit_acl_user(_uid, **kwargs)
user = edit_acl_user(_uid, **kwargs)
for column in employee_pop_columns:
if kwargs.get(column):
@@ -190,9 +177,9 @@ class EmployeeCRUD(object):
@staticmethod
def change_password_by_uid(_uid, password):
EmployeeCRUD.get_employee_by_uid(_uid)
existed = EmployeeCRUD.get_employee_by_uid(_uid)
try:
edit_acl_user(_uid, password=password)
user = edit_acl_user(_uid, password=password)
except Exception as e:
return abort(400, str(e))
@@ -359,11 +346,9 @@ class EmployeeCRUD(object):
if value and column == "last_login":
try:
return datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
value = datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
except Exception as e:
err = f"{ErrFormat.datetime_format_error.format(column)}: {str(e)}"
abort(400, err)
return value
abort(400, ErrFormat.datetime_format_error.format(column))
@staticmethod
def get_attr_by_column(column):
@@ -384,7 +369,7 @@ class EmployeeCRUD(object):
relation = condition.get("relation", None)
value = condition.get("value", None)
value = EmployeeCRUD.check_condition(column, operator, value, relation)
EmployeeCRUD.check_condition(column, operator, value, relation)
a, o = EmployeeCRUD.get_expr_by_condition(
column, operator, value, relation)
and_list += a
@@ -567,125 +552,6 @@ class EmployeeCRUD(object):
results.append(tmp)
return results
@staticmethod
def import_employee(employee_list):
res = CreateEmployee().batch_create(employee_list)
return res
@staticmethod
def batch_edit_employee_department(employee_id_list, column_value):
err_list = []
employee_list = []
for _id in employee_id_list:
try:
existed = EmployeeCRUD.get_employee_by_id(_id)
employee = dict(
e_acl_rid=existed.acl_rid,
department_id=existed.department_id
)
employee_list.append(employee)
existed.update(department_id=column_value)
except Exception as e:
err_list.append({
'employee_id': _id,
'err': str(e),
})
from api.lib.common_setting.department import EditDepartmentInACL
EditDepartmentInACL.edit_employee_department_in_acl(
employee_list, column_value, current_user.uid
)
return err_list
@staticmethod
def batch_edit_password_or_block_column(column_name, employee_id_list, column_value, is_acl=False):
if column_name == 'block':
err_list = []
success_list = []
for _id in employee_id_list:
try:
employee = EmployeeCRUD.edit_employee_block_column(
_id, is_acl, **{column_name: column_value})
success_list.append(employee)
except Exception as e:
err_list.append({
'employee_id': _id,
'err': str(e),
})
return err_list
else:
return EmployeeCRUD.batch_edit_column(column_name, employee_id_list, column_value, is_acl)
@staticmethod
def batch_edit_column(column_name, employee_id_list, column_value, is_acl=False):
err_list = []
for _id in employee_id_list:
try:
EmployeeCRUD.edit_employee_single_column(
_id, is_acl, **{column_name: column_value})
except Exception as e:
err_list.append({
'employee_id': _id,
'err': str(e),
})
return err_list
@staticmethod
def edit_employee_single_column(_id, is_acl=False, **kwargs):
existed = EmployeeCRUD.get_employee_by_id(_id)
if 'direct_supervisor_id' in kwargs.keys():
if kwargs['direct_supervisor_id'] == existed.direct_supervisor_id:
raise Exception(ErrFormat.direct_supervisor_is_not_self)
if is_acl:
return edit_acl_user(existed.acl_uid, **kwargs)
try:
for column in employee_pop_columns:
if kwargs.get(column):
kwargs.pop(column)
return existed.update(**kwargs)
except Exception as e:
return abort(400, str(e))
@staticmethod
def edit_employee_block_column(_id, is_acl=False, **kwargs):
existed = EmployeeCRUD.get_employee_by_id(_id)
value = get_block_value(kwargs.get('block'))
if value is True:
check_department_director_id_or_direct_supervisor_id(_id)
value = 1
else:
value = 0
if is_acl:
kwargs['block'] = value
edit_acl_user(existed.acl_uid, **kwargs)
existed.update(block=value)
data = existed.to_dict()
return data
@staticmethod
def batch_employee(column_name, column_value, employee_id_list):
if column_value is None:
abort(400, ErrFormat.value_is_required)
if column_name in ['password', 'block']:
return EmployeeCRUD.batch_edit_password_or_block_column(column_name, employee_id_list, column_value, True)
elif column_name in ['department_id']:
return EmployeeCRUD.batch_edit_employee_department(employee_id_list, column_value)
elif column_name in [
'direct_supervisor_id', 'position_name'
]:
return EmployeeCRUD.batch_edit_column(column_name, employee_id_list, column_value, False)
else:
abort(400, ErrFormat.column_name_not_support)
def get_user_map(key='uid', acl=None):
"""
@@ -762,8 +628,7 @@ class CreateEmployee(object):
**kwargs
)
@staticmethod
def get_department_by_name(d_name):
def get_department_by_name(self, d_name):
return Department.get_by(first=True, department_name=d_name)
def get_end_department_id(self, department_name_list, department_name_map):

View File

@@ -162,4 +162,4 @@ class NoticeConfigUpdateForm(Form):
info = StringField(validators=[
validators.DataRequired(message="信息 不能为空"),
validators.Length(max=255),
])
])

View File

@@ -10,18 +10,14 @@ from api.lib.exception import CommitException
class FormatMixin(object):
def to_dict(self):
res = dict()
for k in getattr(self, "__mapper__").c.keys():
if k in {'password', '_password', 'secret', '_secret'}:
continue
res = dict([(k, getattr(self, k) if not isinstance(
getattr(self, k), (datetime.datetime, datetime.date, datetime.time)) else str(
getattr(self, k))) for k in getattr(self, "__mapper__").c.keys()])
# FIXME: getattr(cls, "__table__").columns k.name
if k.startswith('_'):
k = k[1:]
if not isinstance(getattr(self, k), (datetime.datetime, datetime.date, datetime.time)):
res[k] = getattr(self, k)
else:
res[k] = str(getattr(self, k))
res.pop('password', None)
res.pop('_password', None)
res.pop('secret', None)
return res

View File

@@ -4,14 +4,8 @@
from functools import wraps
from flask import abort
from flask import current_app
from flask import request
from sqlalchemy.exc import InvalidRequestError
from sqlalchemy.exc import OperationalError
from sqlalchemy.exc import PendingRollbackError
from sqlalchemy.exc import StatementError
from api.extensions import db
from api.lib.resp_format import CommonErrFormat
@@ -76,43 +70,3 @@ def args_validate(model_cls, exclude_args=None):
return wrapper
return decorate
def reconnect_db(func):
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except (StatementError, OperationalError, InvalidRequestError) as e:
error_msg = str(e)
if 'Lost connection' in error_msg or 'reconnect until invalid transaction' in error_msg or \
'can be emitted within this transaction' in error_msg:
current_app.logger.info('[reconnect_db] lost connect rollback then retry')
db.session.rollback()
return func(*args, **kwargs)
else:
raise e
except Exception as e:
raise e
return wrapper
def _flush_db():
try:
db.session.commit()
except (StatementError, OperationalError, InvalidRequestError, PendingRollbackError):
db.session.rollback()
def flush_db(func):
@wraps(func)
def wrapper(*args, **kwargs):
_flush_db()
return func(*args, **kwargs)
return wrapper
def run_flush_db():
_flush_db()

View File

@@ -4,7 +4,7 @@
import msgpack
from api.extensions import cache
from api.lib.decorator import flush_db
from api.extensions import db
from api.lib.utils import Lock
from api.models.acl import App
from api.models.acl import Permission
@@ -221,9 +221,9 @@ class RoleRelationCache(object):
return msgpack.loads(r_g, raw=False)
@classmethod
@flush_db
def rebuild(cls, rid, app_id):
cls.clean(rid, app_id)
db.session.remove()
cls.get_parent_ids(rid, app_id)
cls.get_child_ids(rid, app_id)
@@ -235,9 +235,9 @@ class RoleRelationCache(object):
cls.get_resources2(rid, app_id)
@classmethod
@flush_db
def rebuild2(cls, rid, app_id):
cache.delete(cls.PREFIX_RESOURCES2.format(rid, app_id))
db.session.remove()
cls.get_resources2(rid, app_id)
@classmethod

View File

@@ -260,8 +260,7 @@ class ResourceCRUD(object):
numfound = query.count()
res = [i.to_dict() for i in query.offset((page - 1) * page_size).limit(page_size)]
for i in res:
user = UserCache.get(i['uid']) if i['uid'] else ''
i['user'] = user and user.nickname
i['user'] = UserCache.get(i['uid']).nickname if i['uid'] else ''
return numfound, res
@@ -276,6 +275,7 @@ class ResourceCRUD(object):
from api.tasks.acl import apply_trigger
triggers = TriggerCRUD.match_triggers(app_id, r.name, r.resource_type_id, uid)
current_app.logger.info(triggers)
for trigger in triggers:
# auto trigger should be no uid
apply_trigger.apply_async(args=(trigger.id,),

View File

@@ -58,14 +58,10 @@ class UserCRUD(object):
kwargs['employee_id'] = '{0:04d}'.format(biggest_employee_id + 1)
user = User.create(**kwargs)
role = RoleCRUD.add_role(user.username, uid=user.uid)
RoleCRUD.add_role(user.username, uid=user.uid)
AuditCRUD.add_role_log(None, AuditOperateType.create,
AuditScope.user, user.uid, {}, user.to_dict(), {}, {}
)
from api.lib.common_setting.employee import EmployeeCRUD
payload = {column: getattr(user, column) for column in ['uid', 'username', 'nickname', 'email', 'block']}
payload['rid'] = role.id
EmployeeCRUD.add_employee_from_acl_created(**payload)
return user

View File

@@ -1 +0,0 @@
# -*- coding:utf-8 -*-

View File

@@ -1,430 +0,0 @@
import os
import secrets
import sys
from base64 import b64decode, b64encode
from Cryptodome.Protocol.SecretSharing import Shamir
from colorama import Back
from colorama import Fore
from colorama import Style
from colorama import init as colorama_init
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.primitives.ciphers import Cipher
from cryptography.hazmat.primitives.ciphers import algorithms
from cryptography.hazmat.primitives.ciphers import modes
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from flask import current_app
global_iv_length = 16
global_key_shares = 5 # Number of generated key shares
global_key_threshold = 3 # Minimum number of shares required to rebuild the key
backend_root_key_name = "root_key"
backend_encrypt_key_name = "encrypt_key"
backend_root_key_salt_name = "root_key_salt"
backend_encrypt_key_salt_name = "encrypt_key_salt"
backend_seal_key = "seal_status"
success = "success"
seal_status = True
def string_to_bytes(value):
if isinstance(value, bytes):
return value
if sys.version_info.major == 2:
byte_string = value
else:
byte_string = value.encode("utf-8")
return byte_string
class Backend:
def __init__(self, backend=None):
self.backend = backend
def get(self, key):
return self.backend.get(key)
def add(self, key, value):
return self.backend.add(key, value)
def update(self, key, value):
return self.backend.update(key, value)
class KeyManage:
def __init__(self, trigger=None, backend=None):
self.trigger = trigger
self.backend = backend
if backend:
self.backend = Backend(backend)
def init_app(self, app, backend=None):
if (sys.argv[0].endswith("gunicorn") or
(len(sys.argv) > 1 and sys.argv[1] in ("run", "cmdb-password-data-migrate"))):
self.trigger = app.config.get("INNER_TRIGGER_TOKEN")
if not self.trigger:
return
self.backend = backend
resp = self.auto_unseal()
self.print_response(resp)
def hash_root_key(self, value):
algorithm = hashes.SHA256()
salt = self.backend.get(backend_root_key_salt_name)
if not salt:
salt = secrets.token_hex(16)
msg, ok = self.backend.add(backend_root_key_salt_name, salt)
if not ok:
return msg, ok
kdf = PBKDF2HMAC(
algorithm=algorithm,
length=32,
salt=string_to_bytes(salt),
iterations=100000,
)
key = kdf.derive(string_to_bytes(value))
return b64encode(key).decode('utf-8'), True
def generate_encrypt_key(self, key):
algorithm = hashes.SHA256()
salt = self.backend.get(backend_encrypt_key_salt_name)
if not salt:
salt = secrets.token_hex(32)
kdf = PBKDF2HMAC(
algorithm=algorithm,
length=32,
salt=string_to_bytes(salt),
iterations=100000,
backend=default_backend()
)
key = kdf.derive(string_to_bytes(key))
msg, ok = self.backend.add(backend_encrypt_key_salt_name, salt)
if ok:
return b64encode(key).decode('utf-8'), ok
else:
return msg, ok
@classmethod
def generate_keys(cls, secret):
shares = Shamir.split(global_key_threshold, global_key_shares, secret, False)
new_shares = []
for share in shares:
t = [i for i in share[1]] + [ord(i) for i in "{:0>2}".format(share[0])]
new_shares.append(b64encode(bytes(t)))
return new_shares
def is_valid_root_key(self, root_key):
root_key_hash, ok = self.hash_root_key(root_key)
if not ok:
return root_key_hash, ok
backend_root_key_hash = self.backend.get(backend_root_key_name)
if not backend_root_key_hash:
return "should init firstly", False
elif backend_root_key_hash != root_key_hash:
return "invalid root key", False
else:
return "", True
def auth_root_secret(self, root_key):
msg, ok = self.is_valid_root_key(root_key)
if not ok:
return {
"message": msg,
"status": "failed"
}
encrypt_key_aes = self.backend.get(backend_encrypt_key_name)
if not encrypt_key_aes:
return {
"message": "encrypt key is empty",
"status": "failed"
}
secrets_encrypt_key, ok = InnerCrypt.aes_decrypt(string_to_bytes(root_key), encrypt_key_aes)
if ok:
msg, ok = self.backend.update(backend_seal_key, "open")
if ok:
current_app.config["secrets_encrypt_key"] = secrets_encrypt_key
current_app.config["secrets_root_key"] = root_key
current_app.config["secrets_shares"] = []
return {"message": success, "status": success}
return {"message": msg, "status": "failed"}
else:
return {
"message": secrets_encrypt_key,
"status": "failed"
}
def unseal(self, key):
if not self.is_seal():
return {
"message": "current status is unseal, skip",
"status": "skip"
}
try:
t = [i for i in b64decode(key)]
v = (int("".join([chr(i) for i in t[-2:]])), bytes(t[:-2]))
shares = current_app.config.get("secrets_shares", [])
if v not in shares:
shares.append(v)
current_app.config["secrets_shares"] = shares
if len(shares) >= global_key_threshold:
recovered_secret = Shamir.combine(shares[:global_key_threshold], False)
return self.auth_root_secret(b64encode(recovered_secret))
else:
return {
"message": "waiting for inputting other unseal key {0}/{1}".format(len(shares),
global_key_threshold),
"status": "waiting"
}
except Exception as e:
return {
"message": "invalid token: " + str(e),
"status": "failed"
}
def generate_unseal_keys(self):
info = self.backend.get(backend_root_key_name)
if info:
return "already exist", [], False
secret = AESGCM.generate_key(128)
shares = self.generate_keys(secret)
return b64encode(secret), shares, True
def init(self):
"""
init the master key, unseal key and store in backend
:return:
"""
root_key = self.backend.get(backend_root_key_name)
if root_key:
return {"message": "already init, skip", "status": "skip"}, False
else:
root_key, shares, status = self.generate_unseal_keys()
if not status:
return {"message": root_key, "status": "failed"}, False
# hash root key and store in backend
root_key_hash, ok = self.hash_root_key(root_key)
if not ok:
return {"message": root_key_hash, "status": "failed"}, False
msg, ok = self.backend.add(backend_root_key_name, root_key_hash)
if not ok:
return {"message": msg, "status": "failed"}, False
# generate encrypt key from root_key and store in backend
encrypt_key, ok = self.generate_encrypt_key(root_key)
if not ok:
return {"message": encrypt_key, "status": "failed"}
encrypt_key_aes, status = InnerCrypt.aes_encrypt(root_key, encrypt_key)
if not status:
return {"message": encrypt_key_aes, "status": "failed"}
msg, ok = self.backend.add(backend_encrypt_key_name, encrypt_key_aes)
if not ok:
return {"message": msg, "status": "failed"}, False
msg, ok = self.backend.add(backend_seal_key, "open")
if not ok:
return {"message": msg, "status": "failed"}, False
current_app.config["secrets_root_key"] = root_key
current_app.config["secrets_encrypt_key"] = encrypt_key
self.print_token(shares, root_token=root_key)
return {"message": "OK",
"details": {
"root_token": root_key,
"seal_tokens": shares,
}}, True
def auto_unseal(self):
if not self.trigger:
return {
"message": "trigger config is empty, skip",
"status": "skip"
}
if self.trigger.startswith("http"):
return {
"message": "todo in next step, skip",
"status": "skip"
}
# TODO
elif len(self.trigger.strip()) == 24:
res = self.auth_root_secret(self.trigger.encode())
if res.get("status") == success:
return {
"message": success,
"status": success
}
else:
return {
"message": res.get("message"),
"status": "failed"
}
else:
return {
"message": "trigger config is invalid, skip",
"status": "skip"
}
def seal(self, root_key):
root_key = root_key.encode()
msg, ok = self.is_valid_root_key(root_key)
if not ok:
return {
"message": msg,
"status": "failed"
}
else:
msg, ok = self.backend.update(backend_seal_key, "block")
if not ok:
return {
"message": msg,
"status": "failed",
}
current_app.config["secrets_root_key"] = ''
current_app.config["secrets_encrypt_key"] = ''
return {
"message": success,
"status": success
}
def is_seal(self):
"""
If there is no initialization or the root key is inconsistent, it is considered to be in a sealed state.
:return:
"""
secrets_root_key = current_app.config.get("secrets_root_key")
msg, ok = self.is_valid_root_key(secrets_root_key)
if not ok:
return {"message": msg, "status": "failed"}
status = self.backend.get(backend_seal_key)
return status == "block"
@classmethod
def print_token(cls, shares, root_token):
"""
data: {"message": "OK",
"details": {
"root_token": root_key,
"seal_tokens": shares,
}}
"""
colorama_init()
print(Style.BRIGHT, "Please be sure to store the Unseal Key in a secure location and avoid losing it."
" The Unseal Key is required to unseal the system every time when it restarts."
" Successful unsealing is necessary to enable the password feature." + Style.RESET_ALL)
for i, v in enumerate(shares):
print(
"unseal token " + str(i + 1) + ": " + Fore.RED + Back.BLACK + v.decode("utf-8") + Style.RESET_ALL)
print()
print(Fore.GREEN + "root token: " + root_token.decode("utf-8") + Style.RESET_ALL)
@classmethod
def print_response(cls, data):
status = data.get("status", "")
message = data.get("message", "")
status_colors = {
"skip": Style.BRIGHT,
"failed": Fore.RED,
"waiting": Fore.YELLOW,
}
print(status_colors.get(status, Fore.GREEN), message, Style.RESET_ALL)
class InnerCrypt:
def __init__(self):
secrets_encrypt_key = current_app.config.get("secrets_encrypt_key", "")
self.encrypt_key = b64decode(secrets_encrypt_key.encode("utf-8"))
def encrypt(self, plaintext):
"""
encrypt method contain aes currently
"""
return self.aes_encrypt(self.encrypt_key, plaintext)
def decrypt(self, ciphertext):
"""
decrypt method contain aes currently
"""
return self.aes_decrypt(self.encrypt_key, ciphertext)
@classmethod
def aes_encrypt(cls, key, plaintext):
if isinstance(plaintext, str):
plaintext = string_to_bytes(plaintext)
iv = os.urandom(global_iv_length)
try:
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
encryptor = cipher.encryptor()
v_padder = padding.PKCS7(algorithms.AES.block_size).padder()
padded_plaintext = v_padder.update(plaintext) + v_padder.finalize()
ciphertext = encryptor.update(padded_plaintext) + encryptor.finalize()
return b64encode(iv + ciphertext).decode("utf-8"), True
except Exception as e:
return str(e), False
@classmethod
def aes_decrypt(cls, key, ciphertext):
try:
s = b64decode(ciphertext.encode("utf-8"))
iv = s[:global_iv_length]
ciphertext = s[global_iv_length:]
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
decrypter = cipher.decryptor()
decrypted_padded_plaintext = decrypter.update(ciphertext) + decrypter.finalize()
unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
plaintext = unpadder.update(decrypted_padded_plaintext) + unpadder.finalize()
return plaintext.decode('utf-8'), True
except Exception as e:
return str(e), False
if __name__ == "__main__":
km = KeyManage()
# info, shares, status = km.generate_unseal_keys()
# print(info, shares, status)
# print("..................")
# for i in shares:
# print(b64encode(i[1]).decode())
res1, ok1 = km.init()
if not ok1:
print(res1)
# for j in res["details"]["seal_tokens"]:
# r = km.unseal(j)
# if r["status"] != "waiting":
# if r["status"] != "success":
# print("r........", r)
# else:
# print(r)
# break
t_plaintext = b"Hello, World!" # The plaintext to encrypt
c = InnerCrypt()
t_ciphertext, status1 = c.encrypt(t_plaintext)
print("Ciphertext:", t_ciphertext)
decrypted_plaintext, status2 = c.decrypt(t_ciphertext)
print("Decrypted plaintext:", decrypted_plaintext)

View File

@@ -1,35 +0,0 @@
from api.models.cmdb import InnerKV
class InnerKVManger(object):
def __init__(self):
pass
@classmethod
def add(cls, key, value):
data = {"key": key, "value": value}
res = InnerKV.create(**data)
if res.key == key:
return "success", True
return "add failed", False
@classmethod
def get(cls, key):
res = InnerKV.get_by(first=True, to_dict=False, key=key)
if not res:
return None
return res.value
@classmethod
def update(cls, key, value):
res = InnerKV.get_by(first=True, to_dict=False, key=key)
if not res:
return cls.add(key, value)
t = res.update(value=value)
if t.key == key:
return "success", True
return "update failed", True

View File

@@ -1,141 +0,0 @@
from base64 import b64decode
from base64 import b64encode
import hvac
class VaultClient:
def __init__(self, base_url, token, mount_path='cmdb'):
self.client = hvac.Client(url=base_url, token=token)
self.mount_path = mount_path
def create_app_role(self, role_name, policies):
resp = self.client.create_approle(role_name, policies=policies)
return resp == 200
def delete_app_role(self, role_name):
resp = self.client.delete_approle(role_name)
return resp == 204
def update_app_role_policies(self, role_name, policies):
resp = self.client.update_approle_role(role_name, policies=policies)
return resp == 204
def get_app_role(self, role_name):
resp = self.client.get_approle(role_name)
resp.json()
if resp.status_code == 200:
return resp.json
else:
return {}
def enable_secrets_engine(self):
resp = self.client.sys.enable_secrets_engine('kv', path=self.mount_path)
resp_01 = self.client.sys.enable_secrets_engine('transit')
if resp.status_code == 200 and resp_01.status_code == 200:
return resp.json
else:
return {}
def encrypt(self, plaintext):
response = self.client.secrets.transit.encrypt_data(name='transit-key', plaintext=plaintext)
ciphertext = response['data']['ciphertext']
return ciphertext
# decrypt data
def decrypt(self, ciphertext):
response = self.client.secrets.transit.decrypt_data(name='transit-key', ciphertext=ciphertext)
plaintext = response['data']['plaintext']
return plaintext
def write(self, path, data, encrypt=None):
if encrypt:
for k, v in data.items():
data[k] = self.encrypt(self.encode_base64(v))
response = self.client.secrets.kv.v2.create_or_update_secret(
path=path,
secret=data,
mount_point=self.mount_path
)
return response
# read data
def read(self, path, decrypt=True):
try:
response = self.client.secrets.kv.v2.read_secret_version(
path=path, raise_on_deleted_version=False, mount_point=self.mount_path
)
except Exception as e:
return str(e), False
data = response['data']['data']
if decrypt:
try:
for k, v in data.items():
data[k] = self.decode_base64(self.decrypt(v))
except:
return data, True
return data, True
# update data
def update(self, path, data, overwrite=True, encrypt=True):
if encrypt:
for k, v in data.items():
data[k] = self.encrypt(self.encode_base64(v))
if overwrite:
response = self.client.secrets.kv.v2.create_or_update_secret(
path=path,
secret=data,
mount_point=self.mount_path
)
else:
response = self.client.secrets.kv.v2.patch(path=path, secret=data, mount_point=self.mount_path)
return response
# delete data
def delete(self, path):
response = self.client.secrets.kv.v2.delete_metadata_and_all_versions(
path=path,
mount_point=self.mount_path
)
return response
# Base64 encode
@classmethod
def encode_base64(cls, data):
encoded_bytes = b64encode(data.encode())
encoded_string = encoded_bytes.decode()
return encoded_string
# Base64 decode
@classmethod
def decode_base64(cls, encoded_string):
decoded_bytes = b64decode(encoded_string)
decoded_string = decoded_bytes.decode()
return decoded_string
if __name__ == "__main__":
_base_url = "http://localhost:8200"
_token = "your token"
_path = "test001"
# Example
sdk = VaultClient(_base_url, _token)
# sdk.enable_secrets_engine()
_data = {"key1": "value1", "key2": "value2", "key3": "value3"}
_data = sdk.update(_path, _data, overwrite=True, encrypt=True)
print(_data)
_data = sdk.read(_path, decrypt=True)
print(_data)

View File

@@ -12,9 +12,6 @@ from Crypto.Cipher import AES
from elasticsearch import Elasticsearch
from flask import current_app
from api.lib.secrets.inner import InnerCrypt
from api.lib.secrets.inner import KeyManage
class BaseEnum(object):
_ALL_ = set() # type: Set[str]
@@ -289,33 +286,3 @@ class AESCrypto(object):
text_decrypted = cipher.decrypt(encode_bytes)
return cls.unpad(text_decrypted).decode('utf8')
class Crypto(AESCrypto):
@classmethod
def encrypt(cls, data):
from api.lib.secrets.secrets import InnerKVManger
if not KeyManage(backend=InnerKVManger()).is_seal():
res, status = InnerCrypt().encrypt(data)
if status:
return res
return AESCrypto().encrypt(data)
@classmethod
def decrypt(cls, data):
from api.lib.secrets.secrets import InnerKVManger
if not KeyManage(backend=InnerKVManger()).is_seal():
try:
res, status = InnerCrypt().decrypt(data)
if status:
return res
except:
pass
try:
return AESCrypto().decrypt(data)
except:
return data

View File

@@ -5,8 +5,7 @@ import copy
import hashlib
from datetime import datetime
from ldap3 import Server, Connection, ALL
from ldap3.core.exceptions import LDAPBindError, LDAPCertificateError
import ldap
from flask import current_app
from flask_sqlalchemy import BaseQuery
@@ -58,7 +57,9 @@ class UserQuery(BaseQuery):
return user, authenticated
def authenticate_with_ldap(self, username, password):
server = Server(current_app.config.get('LDAP_SERVER'), get_info=ALL)
ldap_conn = ldap.initialize(current_app.config.get('LDAP_SERVER'))
ldap_conn.protocol_version = 3
ldap_conn.set_option(ldap.OPT_REFERRALS, 0)
if '@' in username:
email = username
who = current_app.config.get('LDAP_USER_DN').format(username.split('@')[0])
@@ -69,14 +70,11 @@ class UserQuery(BaseQuery):
username = username.split('@')[0]
user = self.get_by_username(username)
try:
if not password:
raise LDAPCertificateError
conn = Connection(server, user=who, password=password)
conn.bind()
if conn.result['result'] != 0:
raise LDAPBindError
conn.unbind()
if not password:
raise ldap.INVALID_CREDENTIALS
ldap_conn.simple_bind_s(who, password)
if not user:
from api.lib.perm.acl.user import UserCRUD
@@ -86,7 +84,7 @@ class UserQuery(BaseQuery):
op_record.apply_async(args=(None, username, OperateType.LOGIN, ["ACL"]), queue=ACL_QUEUE)
return user, True
except LDAPBindError:
except ldap.INVALID_CREDENTIALS:
return user, False
def search(self, key):

View File

@@ -12,9 +12,7 @@ from api.lib.cmdb.const import CITypeOperateType
from api.lib.cmdb.const import ConstraintEnum
from api.lib.cmdb.const import OperateType
from api.lib.cmdb.const import ValueTypeEnum
from api.lib.database import Model
from api.lib.database import Model2
from api.lib.utils import Crypto
from api.lib.database import Model, Model2
# template
@@ -91,37 +89,13 @@ class Attribute(Model):
compute_expr = db.Column(db.Text)
compute_script = db.Column(db.Text)
_choice_web_hook = db.Column('choice_web_hook', db.JSON)
choice_web_hook = db.Column(db.JSON)
choice_other = db.Column(db.JSON)
uid = db.Column(db.Integer, index=True)
option = db.Column(db.JSON)
def _get_webhook(self):
if self._choice_web_hook:
if self._choice_web_hook.get('headers') and "Cookie" in self._choice_web_hook['headers']:
self._choice_web_hook['headers']['Cookie'] = Crypto.decrypt(self._choice_web_hook['headers']['Cookie'])
if self._choice_web_hook.get('authorization'):
for k, v in self._choice_web_hook['authorization'].items():
self._choice_web_hook['authorization'][k] = Crypto.decrypt(v)
return self._choice_web_hook
def _set_webhook(self, data):
if data:
if data.get('headers') and "Cookie" in data['headers']:
data['headers']['Cookie'] = Crypto.encrypt(data['headers']['Cookie'])
if data.get('authorization'):
for k, v in data['authorization'].items():
data['authorization'][k] = Crypto.encrypt(v)
self._choice_web_hook = data
choice_web_hook = db.synonym("_choice_web_hook", descriptor=property(_get_webhook, _set_webhook))
class CITypeAttribute(Model):
__tablename__ = "c_ci_type_attributes"
@@ -156,25 +130,7 @@ class CITypeTrigger(Model):
type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'), nullable=False)
attr_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id"))
_option = db.Column('notify', db.JSON)
def _get_option(self):
if self._option and self._option.get('webhooks'):
if self._option['webhooks'].get('authorization'):
for k, v in self._option['webhooks']['authorization'].items():
self._option['webhooks']['authorization'][k] = Crypto.decrypt(v)
return self._option
def _set_option(self, data):
if data and data.get('webhooks'):
if data['webhooks'].get('authorization'):
for k, v in data['webhooks']['authorization'].items():
data['webhooks']['authorization'][k] = Crypto.encrypt(v)
self._option = data
option = db.synonym("_option", descriptor=property(_get_option, _set_option))
option = db.Column('notify', db.JSON)
class CITriggerHistory(Model):
@@ -548,10 +504,3 @@ class CIFilterPerms(Model):
attr_filter = db.Column(db.Text)
rid = db.Column(db.Integer, index=True)
class InnerKV(Model):
__tablename__ = "c_kv"
key = db.Column(db.String(128), index=True)
value = db.Column(db.Text)

View File

@@ -46,4 +46,5 @@ def register_resources(resource_path, rest_api):
resource_cls.url_prefix = ("",)
if isinstance(resource_cls.url_prefix, six.string_types):
resource_cls.url_prefix = (resource_cls.url_prefix,)
rest_api.add_resource(resource_cls, *resource_cls.url_prefix)

View File

@@ -9,8 +9,7 @@ from werkzeug.exceptions import BadRequest
from werkzeug.exceptions import NotFound
from api.extensions import celery
from api.lib.decorator import flush_db
from api.lib.decorator import reconnect_db
from api.extensions import db
from api.lib.perm.acl.audit import AuditCRUD
from api.lib.perm.acl.audit import AuditOperateSource
from api.lib.perm.acl.audit import AuditOperateType
@@ -29,7 +28,6 @@ from api.models.acl import Trigger
name="acl.role_rebuild",
queue=ACL_QUEUE,
once={"graceful": True, "unlock_before_run": True})
@reconnect_db
def role_rebuild(rids, app_id):
rids = rids if isinstance(rids, list) else [rids]
for rid in rids:
@@ -39,7 +37,6 @@ def role_rebuild(rids, app_id):
@celery.task(name="acl.update_resource_to_build_role", queue=ACL_QUEUE)
@reconnect_db
def update_resource_to_build_role(resource_id, app_id, group_id=None):
rids = [i.id for i in Role.get_by(__func_isnot__key_uid=None, fl='id', to_dict=False)]
rids += [i.id for i in Role.get_by(app_id=app_id, fl='id', to_dict=False)]
@@ -55,9 +52,9 @@ def update_resource_to_build_role(resource_id, app_id, group_id=None):
@celery.task(name="acl.apply_trigger", queue=ACL_QUEUE)
@flush_db
@reconnect_db
def apply_trigger(_id, resource_id=None, operator_uid=None):
db.session.remove()
from api.lib.perm.acl.permission import PermissionCRUD
trigger = Trigger.get_by_id(_id)
@@ -121,9 +118,9 @@ def apply_trigger(_id, resource_id=None, operator_uid=None):
@celery.task(name="acl.cancel_trigger", queue=ACL_QUEUE)
@flush_db
@reconnect_db
def cancel_trigger(_id, resource_id=None, operator_uid=None):
db.session.remove()
from api.lib.perm.acl.permission import PermissionCRUD
trigger = Trigger.get_by_id(_id)
@@ -189,7 +186,6 @@ def cancel_trigger(_id, resource_id=None, operator_uid=None):
@celery.task(name="acl.op_record", queue=ACL_QUEUE)
@reconnect_db
def op_record(app, rolename, operate_type, obj):
if isinstance(app, int):
app = AppCache.get(app)

View File

@@ -16,8 +16,6 @@ from api.lib.cmdb.cache import CITypeAttributesCache
from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.cmdb.const import REDIS_PREFIX_CI
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
from api.lib.decorator import flush_db
from api.lib.decorator import reconnect_db
from api.lib.perm.acl.cache import UserCache
from api.lib.utils import Lock
from api.lib.utils import handle_arg_list
@@ -27,12 +25,11 @@ from api.models.cmdb import CITypeAttribute
@celery.task(name="cmdb.ci_cache", queue=CMDB_QUEUE)
@flush_db
@reconnect_db
def ci_cache(ci_id, operate_type, record_id):
from api.lib.cmdb.ci import CITriggerManager
time.sleep(0.01)
db.session.remove()
m = api.lib.cmdb.ci.CIManager()
ci_dict = m.get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
@@ -52,10 +49,9 @@ def ci_cache(ci_id, operate_type, record_id):
@celery.task(name="cmdb.batch_ci_cache", queue=CMDB_QUEUE)
@flush_db
@reconnect_db
def batch_ci_cache(ci_ids, ): # only for attribute change index
time.sleep(1)
db.session.remove()
for ci_id in ci_ids:
m = api.lib.cmdb.ci.CIManager()
@@ -70,7 +66,6 @@ def batch_ci_cache(ci_ids, ): # only for attribute change index
@celery.task(name="cmdb.ci_delete", queue=CMDB_QUEUE)
@reconnect_db
def ci_delete(ci_id):
current_app.logger.info(ci_id)
@@ -83,7 +78,6 @@ def ci_delete(ci_id):
@celery.task(name="cmdb.ci_delete_trigger", queue=CMDB_QUEUE)
@reconnect_db
def ci_delete_trigger(trigger, operate_type, ci_dict):
current_app.logger.info('delete ci {} trigger'.format(ci_dict['_id']))
from api.lib.cmdb.ci import CITriggerManager
@@ -95,9 +89,9 @@ def ci_delete_trigger(trigger, operate_type, ci_dict):
@celery.task(name="cmdb.ci_relation_cache", queue=CMDB_QUEUE)
@flush_db
@reconnect_db
def ci_relation_cache(parent_id, child_id):
db.session.remove()
with Lock("CIRelation_{}".format(parent_id)):
children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0]
children = json.loads(children) if children is not None else {}
@@ -112,8 +106,6 @@ def ci_relation_cache(parent_id, child_id):
@celery.task(name="cmdb.ci_relation_add", queue=CMDB_QUEUE)
@flush_db
@reconnect_db
def ci_relation_add(parent_dict, child_id, uid):
"""
:param parent_dict: key is '$parent_model.attr_name'
@@ -129,6 +121,8 @@ def ci_relation_add(parent_dict, child_id, uid):
current_app.test_request_context().push()
login_user(UserCache.get(uid))
db.session.remove()
for parent in parent_dict:
parent_ci_type_name, _attr_name = parent.strip()[1:].split('.', 1)
attr_name = CITypeAttributeManager.get_attr_name(parent_ci_type_name, _attr_name)
@@ -153,14 +147,10 @@ def ci_relation_add(parent_dict, child_id, uid):
except Exception as e:
current_app.logger.warning(e)
finally:
try:
db.session.commit()
except:
pass
db.session.remove()
@celery.task(name="cmdb.ci_relation_delete", queue=CMDB_QUEUE)
@reconnect_db
def ci_relation_delete(parent_id, child_id):
with Lock("CIRelation_{}".format(parent_id)):
children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0]
@@ -175,10 +165,9 @@ def ci_relation_delete(parent_id, child_id):
@celery.task(name="cmdb.ci_type_attribute_order_rebuild", queue=CMDB_QUEUE)
@flush_db
@reconnect_db
def ci_type_attribute_order_rebuild(type_id, uid):
current_app.logger.info('rebuild attribute order')
db.session.remove()
from api.lib.cmdb.ci_type import CITypeAttributeGroupManager
@@ -199,11 +188,11 @@ def ci_type_attribute_order_rebuild(type_id, uid):
@celery.task(name="cmdb.calc_computed_attribute", queue=CMDB_QUEUE)
@flush_db
@reconnect_db
def calc_computed_attribute(attr_id, uid):
from api.lib.cmdb.ci import CIManager
db.session.remove()
current_app.test_request_context().push()
login_user(UserCache.get(uid))

View File

@@ -84,10 +84,11 @@ class CIView(APIView):
ci_dict = self._wrap_ci_dict()
manager = CIManager()
current_app.logger.debug(ci_dict)
ci_id = manager.add(ci_type,
exist_policy=exist_policy or ExistPolicy.REJECT,
_no_attribute_policy=_no_attribute_policy,
_is_admin=request.values.pop('__is_admin', None) or False,
_is_admin=request.values.pop('__is_admin', False),
**ci_dict)
return self.jsonify(ci_id=ci_id)
@@ -95,6 +96,7 @@ class CIView(APIView):
@has_perm_for_ci("ci_id", ResourceTypeEnum.CI, PermEnum.UPDATE, CIManager.get_type)
def put(self, ci_id=None):
args = request.values
current_app.logger.info(args)
ci_type = args.get("ci_type")
_no_attribute_policy = args.get("no_attribute_policy", ExistPolicy.IGNORE)
@@ -102,14 +104,14 @@ class CIView(APIView):
manager = CIManager()
if ci_id is not None:
manager.update(ci_id,
_is_admin=request.values.pop('__is_admin', None) or False,
_is_admin=request.values.pop('__is_admin', False),
**ci_dict)
else:
request.values.pop('exist_policy', None)
ci_id = manager.add(ci_type,
exist_policy=ExistPolicy.REPLACE,
_no_attribute_policy=_no_attribute_policy,
_is_admin=request.values.pop('__is_admin', None) or False,
_is_admin=request.values.pop('__is_admin', False),
**ci_dict)
return self.jsonify(ci_id=ci_id)
@@ -226,11 +228,11 @@ class CIFlushView(APIView):
from api.tasks.cmdb import ci_cache
from api.lib.cmdb.const import CMDB_QUEUE
if ci_id is not None:
ci_cache.apply_async(args=(ci_id, None, None), queue=CMDB_QUEUE)
ci_cache.apply_async([ci_id], queue=CMDB_QUEUE)
else:
cis = CI.get_by(to_dict=False)
for ci in cis:
ci_cache.apply_async(args=(ci.id, None, None), queue=CMDB_QUEUE)
ci_cache.apply_async([ci.id], queue=CMDB_QUEUE)
return self.jsonify(code=200)
@@ -240,13 +242,3 @@ class CIAutoDiscoveryStatisticsView(APIView):
def get(self):
return self.jsonify(CIManager.get_ad_statistics())
class CIPasswordView(APIView):
url_prefix = "/ci/<int:ci_id>/attributes/<int:attr_id>/password"
def get(self, ci_id, attr_id):
return self.jsonify(ci_id=ci_id, attr_id=attr_id, value=CIManager.load_password(ci_id, attr_id))
def post(self, ci_id, attr_id):
return self.get(ci_id, attr_id)

View File

@@ -1,37 +0,0 @@
from flask import request
from api.lib.perm.auth import auth_abandoned
from api.lib.secrets.inner import KeyManage
from api.lib.secrets.secrets import InnerKVManger
from api.resource import APIView
class InnerSecretUnSealView(APIView):
url_prefix = "/secrets/unseal"
@auth_abandoned
def post(self):
unseal_key = request.headers.get("Unseal-Token")
res = KeyManage(backend=InnerKVManger()).unseal(unseal_key)
return self.jsonify(**res)
class InnerSecretSealView(APIView):
url_prefix = "/secrets/seal"
@auth_abandoned
def post(self):
unseal_key = request.headers.get("Inner-Token")
res = KeyManage(backend=InnerKVManger()).seal(unseal_key)
return self.jsonify(**res)
class InnerSecretAutoSealView(APIView):
url_prefix = "/secrets/auto_seal"
@auth_abandoned
def post(self):
root_key = request.headers.get("Inner-Token")
res = KeyManage(trigger=root_key,
backend=InnerKVManger()).auto_unseal()
return self.jsonify(**res)

View File

@@ -24,12 +24,12 @@ class DataView(APIView):
class DataViewWithId(APIView):
url_prefix = (f'{prefix}/<string:data_type>/<int:_id>',)
def put(self, _id):
def put(self, data_type, _id):
params = request.json
res = CommonDataCRUD.update_data(_id, **params)
return self.jsonify(res.to_dict())
def delete(self, _id):
def delete(self, data_type, _id):
CommonDataCRUD.delete(_id)
return self.jsonify({})

View File

@@ -1,7 +1,9 @@
# -*- coding:utf-8 -*-
from flask import abort
from flask import request
from api.lib.common_setting.company_info import CompanyInfoCRUD
from api.lib.common_setting.resp_format import ErrFormat
from api.resource import APIView
prefix = '/company'

View File

@@ -1,5 +1,7 @@
# -*- coding:utf-8 -*-
from flask import abort
import os
from flask import abort, current_app, send_from_directory
from flask import request
from werkzeug.datastructures import MultiDict

View File

@@ -47,7 +47,7 @@ class CheckEmailServer(APIView):
def post(self):
receive_address = request.args.get('receive_address')
info = request.values.get('info', {})
info = request.values.get('info')
try:

View File

@@ -1 +0,0 @@
Generic single-database configuration.

View File

@@ -1,45 +0,0 @@
# A generic, single database configuration.
[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

View File

@@ -1,110 +0,0 @@
from __future__ import with_statement
import logging
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env')
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
from flask import current_app
config.set_main_option(
'sqlalchemy.url', current_app.config.get(
'SQLALCHEMY_DATABASE_URI').replace('%', '%%'))
target_metadata = current_app.extensions['migrate'].db.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
# 添加要屏蔽的table列表
exclude_tables = ["c_cfp"]
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url, target_metadata=target_metadata, literal_binds=True,
include_name=include_name
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info('No changes in schema detected.')
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix='sqlalchemy.',
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
process_revision_directives=process_revision_directives,
include_name=include_name,
**current_app.extensions['migrate'].configure_args
)
with context.begin_transaction():
context.run_migrations()
def include_name(name, type_, parent_names):
if type_ == "table":
return name not in exclude_tables
elif parent_names.get("table_name") in exclude_tables:
return False
return True
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

View File

@@ -1,24 +0,0 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@@ -1,360 +0,0 @@
"""empty message
Revision ID: 6a4df2623057
Revises:
Create Date: 2023-10-13 15:17:00.066858
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
revision = '6a4df2623057'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('common_data',
sa.Column('deleted_at', sa.DateTime(), nullable=True),
sa.Column('deleted', sa.Boolean(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('data_type', sa.VARCHAR(length=255), nullable=True),
sa.Column('data', sa.JSON(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_common_data_deleted'), 'common_data', ['deleted'], unique=False)
op.create_table('common_notice_config',
sa.Column('deleted_at', sa.DateTime(), nullable=True),
sa.Column('deleted', sa.Boolean(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('platform', sa.VARCHAR(length=255), nullable=False),
sa.Column('info', sa.JSON(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_common_notice_config_deleted'), 'common_notice_config', ['deleted'], unique=False)
op.add_column('c_attributes', sa.Column('choice_other', sa.JSON(), nullable=True))
op.drop_index('idx_c_attributes_uid', table_name='c_attributes')
op.create_index(op.f('ix_c_attributes_uid'), 'c_attributes', ['uid'], unique=False)
op.drop_index('ix_c_custom_dashboard_deleted', table_name='c_c_d')
op.create_index(op.f('ix_c_c_d_deleted'), 'c_c_d', ['deleted'], unique=False)
op.drop_index('ix_c_ci_type_triggers_deleted', table_name='c_c_t_t')
op.create_index(op.f('ix_c_c_t_t_deleted'), 'c_c_t_t', ['deleted'], unique=False)
op.drop_index('ix_c_ci_type_unique_constraints_deleted', table_name='c_c_t_u_c')
op.create_index(op.f('ix_c_c_t_u_c_deleted'), 'c_c_t_u_c', ['deleted'], unique=False)
op.drop_index('c_ci_types_uid', table_name='c_ci_types')
op.create_index(op.f('ix_c_ci_types_uid'), 'c_ci_types', ['uid'], unique=False)
op.alter_column('c_prv', 'uid',
existing_type=mysql.INTEGER(),
nullable=False)
op.drop_index('ix_c_preference_relation_views_deleted', table_name='c_prv')
op.drop_index('ix_c_preference_relation_views_name', table_name='c_prv')
op.create_index(op.f('ix_c_prv_deleted'), 'c_prv', ['deleted'], unique=False)
op.create_index(op.f('ix_c_prv_name'), 'c_prv', ['name'], unique=False)
op.create_index(op.f('ix_c_prv_uid'), 'c_prv', ['uid'], unique=False)
op.drop_index('ix_c_preference_show_attributes_deleted', table_name='c_psa')
op.drop_index('ix_c_preference_show_attributes_uid', table_name='c_psa')
op.create_index(op.f('ix_c_psa_deleted'), 'c_psa', ['deleted'], unique=False)
op.create_index(op.f('ix_c_psa_uid'), 'c_psa', ['uid'], unique=False)
op.drop_index('ix_c_preference_tree_views_deleted', table_name='c_ptv')
op.drop_index('ix_c_preference_tree_views_uid', table_name='c_ptv')
op.create_index(op.f('ix_c_ptv_deleted'), 'c_ptv', ['deleted'], unique=False)
op.create_index(op.f('ix_c_ptv_uid'), 'c_ptv', ['uid'], unique=False)
op.alter_column('common_department', 'department_name',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
comment=None,
existing_comment='部门名称',
existing_nullable=True)
op.alter_column('common_department', 'department_director_id',
existing_type=mysql.INTEGER(),
comment=None,
existing_comment='部门负责人ID',
existing_nullable=True)
op.alter_column('common_department', 'department_parent_id',
existing_type=mysql.INTEGER(),
comment=None,
existing_comment='上级部门ID',
existing_nullable=True)
op.alter_column('common_department', 'sort_value',
existing_type=mysql.INTEGER(),
comment=None,
existing_comment='排序值',
existing_nullable=True)
op.alter_column('common_department', 'acl_rid',
existing_type=mysql.INTEGER(),
comment=None,
existing_comment='ACL中rid',
existing_nullable=True)
op.alter_column('common_employee', 'email',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
comment=None,
existing_comment='邮箱',
existing_nullable=True)
op.alter_column('common_employee', 'username',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
comment=None,
existing_comment='用户名',
existing_nullable=True)
op.alter_column('common_employee', 'nickname',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
comment=None,
existing_comment='姓名',
existing_nullable=True)
op.alter_column('common_employee', 'sex',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=64),
comment=None,
existing_comment='性别',
existing_nullable=True)
op.alter_column('common_employee', 'position_name',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
comment=None,
existing_comment='职位名称',
existing_nullable=True)
op.alter_column('common_employee', 'mobile',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
comment=None,
existing_comment='电话号码',
existing_nullable=True)
op.alter_column('common_employee', 'avatar',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
comment=None,
existing_comment='头像',
existing_nullable=True)
op.alter_column('common_employee', 'direct_supervisor_id',
existing_type=mysql.INTEGER(),
comment=None,
existing_comment='直接上级ID',
existing_nullable=True)
op.alter_column('common_employee', 'department_id',
existing_type=mysql.INTEGER(),
comment=None,
existing_comment='部门ID',
existing_nullable=True)
op.alter_column('common_employee', 'acl_uid',
existing_type=mysql.INTEGER(),
comment=None,
existing_comment='ACL中uid',
existing_nullable=True)
op.alter_column('common_employee', 'acl_rid',
existing_type=mysql.INTEGER(),
comment=None,
existing_comment='ACL中rid',
existing_nullable=True)
op.alter_column('common_employee', 'acl_virtual_rid',
existing_type=mysql.INTEGER(),
comment=None,
existing_comment='ACL中虚拟角色rid',
existing_nullable=True)
op.alter_column('common_employee', 'last_login',
existing_type=mysql.TIMESTAMP(),
comment=None,
existing_comment='上次登录时间',
existing_nullable=True)
op.alter_column('common_employee', 'block',
existing_type=mysql.INTEGER(),
comment=None,
existing_comment='锁定状态',
existing_nullable=True)
op.alter_column('common_employee_info', 'info',
existing_type=mysql.JSON(),
comment=None,
existing_comment='员工信息',
existing_nullable=True)
op.alter_column('common_employee_info', 'employee_id',
existing_type=mysql.INTEGER(),
comment=None,
existing_comment='员工ID',
existing_nullable=True)
op.alter_column('common_internal_message', 'title',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
comment=None,
existing_comment='标题',
existing_nullable=True)
op.alter_column('common_internal_message', 'content',
existing_type=mysql.TEXT(charset='utf8mb3', collation='utf8mb3_unicode_ci'),
comment=None,
existing_comment='内容',
existing_nullable=True)
op.alter_column('common_internal_message', 'path',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
comment=None,
existing_comment='跳转路径',
existing_nullable=True)
op.alter_column('common_internal_message', 'is_read',
existing_type=mysql.TINYINT(display_width=1),
comment=None,
existing_comment='是否已读',
existing_nullable=True)
op.alter_column('common_internal_message', 'app_name',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=128),
comment=None,
existing_comment='应用名称',
existing_nullable=False)
op.alter_column('common_internal_message', 'category',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=128),
comment=None,
existing_comment='分类',
existing_nullable=False)
op.alter_column('common_internal_message', 'message_data',
existing_type=mysql.JSON(),
comment=None,
existing_comment='数据',
existing_nullable=True)
op.drop_column('users', 'apps')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('apps', mysql.JSON(), nullable=True))
op.alter_column('common_internal_message', 'message_data',
existing_type=mysql.JSON(),
comment='数据',
existing_nullable=True)
op.alter_column('common_internal_message', 'category',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=128),
comment='分类',
existing_nullable=False)
op.alter_column('common_internal_message', 'app_name',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=128),
comment='应用名称',
existing_nullable=False)
op.alter_column('common_internal_message', 'is_read',
existing_type=mysql.TINYINT(display_width=1),
comment='是否已读',
existing_nullable=True)
op.alter_column('common_internal_message', 'path',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
comment='跳转路径',
existing_nullable=True)
op.alter_column('common_internal_message', 'content',
existing_type=mysql.TEXT(charset='utf8mb3', collation='utf8mb3_unicode_ci'),
comment='内容',
existing_nullable=True)
op.alter_column('common_internal_message', 'title',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
comment='标题',
existing_nullable=True)
op.alter_column('common_employee_info', 'employee_id',
existing_type=mysql.INTEGER(),
comment='员工ID',
existing_nullable=True)
op.alter_column('common_employee_info', 'info',
existing_type=mysql.JSON(),
comment='员工信息',
existing_nullable=True)
op.alter_column('common_employee', 'block',
existing_type=mysql.INTEGER(),
comment='锁定状态',
existing_nullable=True)
op.alter_column('common_employee', 'last_login',
existing_type=mysql.TIMESTAMP(),
comment='上次登录时间',
existing_nullable=True)
op.alter_column('common_employee', 'acl_virtual_rid',
existing_type=mysql.INTEGER(),
comment='ACL中虚拟角色rid',
existing_nullable=True)
op.alter_column('common_employee', 'acl_rid',
existing_type=mysql.INTEGER(),
comment='ACL中rid',
existing_nullable=True)
op.alter_column('common_employee', 'acl_uid',
existing_type=mysql.INTEGER(),
comment='ACL中uid',
existing_nullable=True)
op.alter_column('common_employee', 'department_id',
existing_type=mysql.INTEGER(),
comment='部门ID',
existing_nullable=True)
op.alter_column('common_employee', 'direct_supervisor_id',
existing_type=mysql.INTEGER(),
comment='直接上级ID',
existing_nullable=True)
op.alter_column('common_employee', 'avatar',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
comment='头像',
existing_nullable=True)
op.alter_column('common_employee', 'mobile',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
comment='电话号码',
existing_nullable=True)
op.alter_column('common_employee', 'position_name',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
comment='职位名称',
existing_nullable=True)
op.alter_column('common_employee', 'sex',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=64),
comment='性别',
existing_nullable=True)
op.alter_column('common_employee', 'nickname',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
comment='姓名',
existing_nullable=True)
op.alter_column('common_employee', 'username',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
comment='用户名',
existing_nullable=True)
op.alter_column('common_employee', 'email',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
comment='邮箱',
existing_nullable=True)
op.alter_column('common_department', 'acl_rid',
existing_type=mysql.INTEGER(),
comment='ACL中rid',
existing_nullable=True)
op.alter_column('common_department', 'sort_value',
existing_type=mysql.INTEGER(),
comment='排序值',
existing_nullable=True)
op.alter_column('common_department', 'department_parent_id',
existing_type=mysql.INTEGER(),
comment='上级部门ID',
existing_nullable=True)
op.alter_column('common_department', 'department_director_id',
existing_type=mysql.INTEGER(),
comment='部门负责人ID',
existing_nullable=True)
op.alter_column('common_department', 'department_name',
existing_type=mysql.VARCHAR(charset='utf8mb3', collation='utf8mb3_unicode_ci', length=255),
comment='部门名称',
existing_nullable=True)
op.drop_index(op.f('ix_c_ptv_uid'), table_name='c_ptv')
op.drop_index(op.f('ix_c_ptv_deleted'), table_name='c_ptv')
op.create_index('ix_c_preference_tree_views_uid', 'c_ptv', ['uid'], unique=False)
op.create_index('ix_c_preference_tree_views_deleted', 'c_ptv', ['deleted'], unique=False)
op.drop_index(op.f('ix_c_psa_uid'), table_name='c_psa')
op.drop_index(op.f('ix_c_psa_deleted'), table_name='c_psa')
op.create_index('ix_c_preference_show_attributes_uid', 'c_psa', ['uid'], unique=False)
op.create_index('ix_c_preference_show_attributes_deleted', 'c_psa', ['deleted'], unique=False)
op.drop_index(op.f('ix_c_prv_uid'), table_name='c_prv')
op.drop_index(op.f('ix_c_prv_name'), table_name='c_prv')
op.drop_index(op.f('ix_c_prv_deleted'), table_name='c_prv')
op.create_index('ix_c_preference_relation_views_name', 'c_prv', ['name'], unique=False)
op.create_index('ix_c_preference_relation_views_deleted', 'c_prv', ['deleted'], unique=False)
op.alter_column('c_prv', 'uid',
existing_type=mysql.INTEGER(),
nullable=True)
op.drop_index(op.f('ix_c_ci_types_uid'), table_name='c_ci_types')
op.create_index('c_ci_types_uid', 'c_ci_types', ['uid'], unique=False)
op.drop_index(op.f('ix_c_c_t_u_c_deleted'), table_name='c_c_t_u_c')
op.create_index('ix_c_ci_type_unique_constraints_deleted', 'c_c_t_u_c', ['deleted'], unique=False)
op.drop_index(op.f('ix_c_c_t_t_deleted'), table_name='c_c_t_t')
op.create_index('ix_c_ci_type_triggers_deleted', 'c_c_t_t', ['deleted'], unique=False)
op.drop_index(op.f('ix_c_c_d_deleted'), table_name='c_c_d')
op.create_index('ix_c_custom_dashboard_deleted', 'c_c_d', ['deleted'], unique=False)
op.drop_index(op.f('ix_c_attributes_uid'), table_name='c_attributes')
op.create_index('idx_c_attributes_uid', 'c_attributes', ['uid'], unique=False)
op.drop_column('c_attributes', 'choice_other')
op.drop_index(op.f('ix_common_notice_config_deleted'), table_name='common_notice_config')
op.drop_table('common_notice_config')
op.drop_index(op.f('ix_common_data_deleted'), table_name='common_data')
op.drop_table('common_data')
# ### end Alembic commands ###

View File

@@ -1,7 +1,7 @@
-i https://mirrors.aliyun.com/pypi/simple
alembic==1.7.7
bs4==0.0.1
celery>=5.3.1
celery==5.3.1
celery-once==3.0.1
click==8.1.3
elasticsearch==7.17.9
@@ -12,28 +12,27 @@ Flask==2.3.2
Flask-Bcrypt==1.0.1
Flask-Caching==2.0.2
Flask-Cors==4.0.0
Flask-Login>=0.6.2
Flask-Login==0.6.2
Flask-Migrate==2.5.2
Flask-RESTful==0.3.10
Flask-SQLAlchemy==2.5.0
future==0.18.3
gunicorn==21.0.1
hvac==2.0.0
itsdangerous==2.1.2
Jinja2==3.1.2
jinja2schema==0.1.4
jsonschema==4.18.0
kombu>=5.3.1
kombu==5.3.1
Mako==1.2.4
MarkupSafe==2.1.3
marshmallow==2.20.2
more-itertools==5.0.0
msgpack-python==0.5.6
Pillow>=10.0.1
cryptography>=41.0.2
Pillow==9.3.0
pycryptodome==3.12.0
PyJWT==2.4.0
PyMySQL==1.1.0
ldap3==2.9.1
python-ldap==3.4.0
PyYAML==6.0
redis==4.6.0
requests==2.31.0
@@ -45,9 +44,5 @@ supervisor==4.0.3
timeout-decorator==0.5.0
toposort==1.10
treelib==1.6.1
Werkzeug>=2.3.6
Werkzeug==2.3.6
WTForms==3.0.0
shamir~=17.12.0
hvac~=2.0.0
pycryptodomex>=3.19.0
colorama>=0.4.6

View File

@@ -97,9 +97,3 @@ BOOL_TRUE = ['true', 'TRUE', 'True', True, '1', 1, "Yes", "YES", "yes", 'Y', 'y'
# # messenger
USE_MESSENGER = True
# # secrets
SECRETS_ENGINE = 'inner' # 'inner' or 'vault'
VAULT_URL = ''
VAULT_TOKEN = ''
INNER_TRIGGER_TOKEN = ''

View File

@@ -20,7 +20,7 @@
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^1.0.0",
"ant-design-vue": "^1.6.5",
"axios": "1.6.0",
"axios": "0.18.0",
"babel-eslint": "^8.2.2",
"butterfly-dag": "^4.3.26",
"codemirror": "^5.65.13",
@@ -63,15 +63,14 @@
},
"devDependencies": {
"@ant-design/colors": "^3.2.2",
"@babel/core": "^7.23.2",
"@babel/polyfill": "^7.2.5",
"@babel/preset-env": "^7.23.2",
"@vue/cli-plugin-babel": "4.5.17",
"@vue/cli-plugin-eslint": "^4.0.5",
"@vue/cli-plugin-unit-jest": "^4.0.5",
"@vue/cli-service": "^4.0.5",
"@vue/eslint-config-standard": "^4.0.0",
"@vue/test-utils": "^1.0.0-beta.30",
"babel-core": "7.0.0-bridge.0",
"babel-jest": "^23.6.0",
"babel-plugin-import": "^1.11.0",
"babel-plugin-transform-remove-console": "^6.9.4",

View File

@@ -54,48 +54,6 @@
<div class="content unicode" style="display: block;">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont">&#xe894;</span>
<div class="name">icon-xianxing-password</div>
<div class="code-name">&amp;#xe894;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe895;</span>
<div class="name">icon-xianxing-link</div>
<div class="code-name">&amp;#xe895;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe892;</span>
<div class="name">itsm-oneclick download</div>
<div class="code-name">&amp;#xe892;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe893;</span>
<div class="name">itsm-package download</div>
<div class="code-name">&amp;#xe893;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe891;</span>
<div class="name">weixin</div>
<div class="code-name">&amp;#xe891;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe88f;</span>
<div class="name">itsm-again</div>
<div class="code-name">&amp;#xe88f;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe890;</span>
<div class="name">itsm-next</div>
<div class="code-name">&amp;#xe890;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe88e;</span>
<div class="name">wechatApp</div>
@@ -4044,9 +4002,9 @@
<pre><code class="language-css"
>@font-face {
font-family: 'iconfont';
src: url('iconfont.woff2?t=1698273699449') format('woff2'),
url('iconfont.woff?t=1698273699449') format('woff'),
url('iconfont.ttf?t=1698273699449') format('truetype');
src: url('iconfont.woff2?t=1696815443987') format('woff2'),
url('iconfont.woff?t=1696815443987') format('woff'),
url('iconfont.ttf?t=1696815443987') format('truetype');
}
</code></pre>
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@@ -4072,69 +4030,6 @@
<div class="content font-class">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont icon-xianxing-password"></span>
<div class="name">
icon-xianxing-password
</div>
<div class="code-name">.icon-xianxing-password
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-xianxing-link"></span>
<div class="name">
icon-xianxing-link
</div>
<div class="code-name">.icon-xianxing-link
</div>
</li>
<li class="dib">
<span class="icon iconfont a-itsm-oneclickdownload"></span>
<div class="name">
itsm-oneclick download
</div>
<div class="code-name">.a-itsm-oneclickdownload
</div>
</li>
<li class="dib">
<span class="icon iconfont a-itsm-packagedownload"></span>
<div class="name">
itsm-package download
</div>
<div class="code-name">.a-itsm-packagedownload
</div>
</li>
<li class="dib">
<span class="icon iconfont a-Frame4"></span>
<div class="name">
weixin
</div>
<div class="code-name">.a-Frame4
</div>
</li>
<li class="dib">
<span class="icon iconfont itsm-again"></span>
<div class="name">
itsm-again
</div>
<div class="code-name">.itsm-again
</div>
</li>
<li class="dib">
<span class="icon iconfont itsm-next"></span>
<div class="name">
itsm-next
</div>
<div class="code-name">.itsm-next
</div>
</li>
<li class="dib">
<span class="icon iconfont wechatApp"></span>
<div class="name">
@@ -10057,62 +9952,6 @@
<div class="content symbol">
<ul class="icon_lists dib-box">
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-xianxing-password"></use>
</svg>
<div class="name">icon-xianxing-password</div>
<div class="code-name">#icon-xianxing-password</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-xianxing-link"></use>
</svg>
<div class="name">icon-xianxing-link</div>
<div class="code-name">#icon-xianxing-link</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#a-itsm-oneclickdownload"></use>
</svg>
<div class="name">itsm-oneclick download</div>
<div class="code-name">#a-itsm-oneclickdownload</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#a-itsm-packagedownload"></use>
</svg>
<div class="name">itsm-package download</div>
<div class="code-name">#a-itsm-packagedownload</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#a-Frame4"></use>
</svg>
<div class="name">weixin</div>
<div class="code-name">#a-Frame4</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-again"></use>
</svg>
<div class="name">itsm-again</div>
<div class="code-name">#itsm-again</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-next"></use>
</svg>
<div class="name">itsm-next</div>
<div class="code-name">#itsm-next</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#wechatApp"></use>

View File

@@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 3857903 */
src: url('iconfont.woff2?t=1698273699449') format('woff2'),
url('iconfont.woff?t=1698273699449') format('woff'),
url('iconfont.ttf?t=1698273699449') format('truetype');
src: url('iconfont.woff2?t=1696815443987') format('woff2'),
url('iconfont.woff?t=1696815443987') format('woff'),
url('iconfont.ttf?t=1696815443987') format('truetype');
}
.iconfont {
@@ -13,34 +13,6 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-xianxing-password:before {
content: "\e894";
}
.icon-xianxing-link:before {
content: "\e895";
}
.a-itsm-oneclickdownload:before {
content: "\e892";
}
.a-itsm-packagedownload:before {
content: "\e893";
}
.a-Frame4:before {
content: "\e891";
}
.itsm-again:before {
content: "\e88f";
}
.itsm-next:before {
content: "\e890";
}
.wechatApp:before {
content: "\e88e";
}

File diff suppressed because one or more lines are too long

View File

@@ -5,55 +5,6 @@
"css_prefix_text": "",
"description": "",
"glyphs": [
{
"icon_id": "37830610",
"name": "icon-xianxing-password",
"font_class": "icon-xianxing-password",
"unicode": "e894",
"unicode_decimal": 59540
},
{
"icon_id": "37830609",
"name": "icon-xianxing-link",
"font_class": "icon-xianxing-link",
"unicode": "e895",
"unicode_decimal": 59541
},
{
"icon_id": "37822199",
"name": "itsm-oneclick download",
"font_class": "a-itsm-oneclickdownload",
"unicode": "e892",
"unicode_decimal": 59538
},
{
"icon_id": "37822198",
"name": "itsm-package download",
"font_class": "a-itsm-packagedownload",
"unicode": "e893",
"unicode_decimal": 59539
},
{
"icon_id": "37772067",
"name": "weixin",
"font_class": "a-Frame4",
"unicode": "e891",
"unicode_decimal": 59537
},
{
"icon_id": "37632784",
"name": "itsm-again",
"font_class": "itsm-again",
"unicode": "e88f",
"unicode_decimal": 59535
},
{
"icon_id": "37632783",
"name": "itsm-next",
"font_class": "itsm-next",
"unicode": "e890",
"unicode_decimal": 59536
},
{
"icon_id": "37590786",
"name": "wechatApp",

Binary file not shown.

View File

@@ -1,290 +1,282 @@
<template>
<div>
<a-popover
v-if="isDropdown"
v-model="visible"
trigger="click"
:placement="placement"
overlayClassName="table-filter"
@visibleChange="visibleChange"
>
<slot name="popover_item">
<a-button type="primary" ghost>条件过滤<a-icon type="filter"/></a-button>
</slot>
<template slot="content">
<Expression
v-model="ruleList"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)"
/>
<a-divider :style="{ margin: '10px 0' }" />
<div style="width:534px">
<a-space :style="{ display: 'flex', justifyContent: 'flex-end' }">
<a-button type="primary" size="small" @click="handleSubmit">确定</a-button>
<a-button size="small" @click="handleClear">清空</a-button>
</a-space>
</div>
</template>
</a-popover>
<Expression
v-else
v-model="ruleList"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)"
/>
</div>
</template>
<script>
import { v4 as uuidv4 } from 'uuid'
import Expression from './expression.vue'
import { advancedExpList, compareTypeList } from './constants'
export default {
name: 'FilterComp',
components: { Expression },
props: {
canSearchPreferenceAttrList: {
type: Array,
required: true,
default: () => [],
},
expression: {
type: String,
default: '',
},
regQ: {
type: String,
default: '(?<=q=).+(?=&)|(?<=q=).+$',
},
placement: {
type: String,
default: 'bottomRight',
},
isDropdown: {
type: Boolean,
default: true,
},
},
data() {
return {
advancedExpList,
compareTypeList,
visible: false,
ruleList: [],
filterExp: '',
}
},
methods: {
visibleChange(open, isInitOne = true) {
// isInitOne 初始化exp为空时ruleList是否默认给一条
// const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
const exp = this.expression.match(new RegExp(this.regQ, 'g'))
? this.expression.match(new RegExp(this.regQ, 'g'))[0]
: null
if (open && exp) {
const expArray = exp.split(',').map((item) => {
let has_not = ''
const key = item.split(':')[0]
const val = item
.split(':')
.slice(1)
.join(':')
let type, property, exp, value, min, max, compareType
if (key.includes('-')) {
type = 'or'
if (key.includes('~')) {
property = key.substring(2)
has_not = '~'
} else {
property = key.substring(1)
}
} else {
type = 'and'
if (key.includes('~')) {
property = key.substring(1)
has_not = '~'
} else {
property = key
}
}
const in_reg = /(?<=\().+(?=\))/g
const range_reg = /(?<=\[).+(?=\])/g
const compare_reg = /(?<=>=|<=|>(?!=)|<(?!=)).+/
if (val === '*') {
exp = has_not + 'value'
value = ''
} else if (in_reg.test(val)) {
exp = has_not + 'in'
value = val.match(in_reg)[0]
} else if (range_reg.test(val)) {
exp = has_not + 'range'
value = val.match(range_reg)[0]
min = value.split('_TO_')[0]
max = value.split('_TO_')[1]
} else if (compare_reg.test(val)) {
exp = has_not + 'compare'
value = val.match(compare_reg)[0]
const _compareType = val.substring(0, val.match(compare_reg)['index'])
const idx = compareTypeList.findIndex((item) => item.label === _compareType)
compareType = compareTypeList[idx].value
} else if (!val.includes('*')) {
exp = has_not + 'is'
value = val
} else {
const resList = [
['contain', /(?<=\*).*(?=\*)/g],
['end_with', /(?<=\*).+/g],
['start_with', /.+(?=\*)/g],
]
for (let i = 0; i < 3; i++) {
const reg = resList[i]
if (reg[1].test(val)) {
exp = has_not + reg[0]
value = val.match(reg[1])[0]
break
}
}
}
return {
id: uuidv4(),
type,
property,
exp,
value,
min,
max,
compareType,
}
})
this.ruleList = [...expArray]
} else if (open) {
const _canSearchPreferenceAttrList = this.canSearchPreferenceAttrList.filter((attr) => !attr.is_password)
this.ruleList = isInitOne
? [
{
id: uuidv4(),
type: 'and',
property:
_canSearchPreferenceAttrList && _canSearchPreferenceAttrList.length
? _canSearchPreferenceAttrList[0].name
: undefined,
exp: 'is',
value: null,
},
]
: []
}
},
handleClear() {
this.ruleList = [
{
id: uuidv4(),
type: 'and',
property: this.canSearchPreferenceAttrList[0].name,
exp: 'is',
value: null,
},
]
this.filterExp = ''
this.visible = false
this.$emit('setExpFromFilter', this.filterExp)
},
handleSubmit() {
if (this.ruleList && this.ruleList.length) {
this.ruleList[0].type = 'and' // 增删后以防万一第一个不是and
this.filterExp = ''
const expList = this.ruleList.map((rule) => {
let singleRuleExp = ''
let _exp = rule.exp
if (rule.type === 'or') {
singleRuleExp += '-'
}
if (rule.exp.includes('~')) {
singleRuleExp += '~'
_exp = rule.exp.split('~')[1]
}
singleRuleExp += `${rule.property}:`
if (_exp === 'is') {
singleRuleExp += `${rule.value ?? ''}`
}
if (_exp === 'contain') {
singleRuleExp += `*${rule.value ?? ''}*`
}
if (_exp === 'start_with') {
singleRuleExp += `${rule.value ?? ''}*`
}
if (_exp === 'end_with') {
singleRuleExp += `*${rule.value ?? ''}`
}
if (_exp === 'value') {
singleRuleExp += `*`
}
if (_exp === 'in') {
singleRuleExp += `(${rule.value ?? ''})`
}
if (_exp === 'range') {
singleRuleExp += `[${rule.min}_TO_${rule.max}]`
}
if (_exp === 'compare') {
const idx = compareTypeList.findIndex((item) => item.value === rule.compareType)
singleRuleExp += `${compareTypeList[idx].label}${rule.value ?? ''}`
}
return singleRuleExp
})
this.filterExp = expList.join(',')
this.$emit('setExpFromFilter', this.filterExp)
} else {
this.$emit('setExpFromFilter', '')
}
this.visible = false
},
},
}
</script>
<style lang="less" scoped>
.table-filter {
.table-filter-add {
margin-top: 10px;
& > a {
padding: 2px 8px;
&:hover {
background-color: #f0faff;
border-radius: 5px;
}
}
}
.table-filter-extra-icon {
padding: 0px 2px;
&:hover {
display: inline-block;
border-radius: 5px;
background-color: #f0faff;
}
}
}
</style>
<style lang="less">
.table-filter-extra-operation {
.ant-popover-inner-content {
padding: 3px 4px;
.operation {
cursor: pointer;
width: 90px;
height: 30px;
line-height: 30px;
padding: 3px 4px;
border-radius: 5px;
transition: all 0.3s;
&:hover {
background-color: #f0faff;
}
> .anticon {
margin-right: 10px;
}
}
}
}
</style>
<template>
<div>
<a-popover
v-if="isDropdown"
v-model="visible"
trigger="click"
:placement="placement"
overlayClassName="table-filter"
@visibleChange="visibleChange"
>
<slot name="popover_item">
<a-button type="primary" ghost>条件过滤<a-icon type="filter"/></a-button>
</slot>
<template slot="content">
<Expression v-model="ruleList" :canSearchPreferenceAttrList="canSearchPreferenceAttrList" />
<a-divider :style="{ margin: '10px 0' }" />
<div style="width:534px">
<a-space :style="{ display: 'flex', justifyContent: 'flex-end' }">
<a-button type="primary" size="small" @click="handleSubmit">确定</a-button>
<a-button size="small" @click="handleClear">清空</a-button>
</a-space>
</div>
</template>
</a-popover>
<Expression v-else v-model="ruleList" :canSearchPreferenceAttrList="canSearchPreferenceAttrList" />
</div>
</template>
<script>
import { v4 as uuidv4 } from 'uuid'
import Expression from './expression.vue'
import { advancedExpList, compareTypeList } from './constants'
export default {
name: 'FilterComp',
components: { Expression },
props: {
canSearchPreferenceAttrList: {
type: Array,
required: true,
default: () => [],
},
expression: {
type: String,
default: '',
},
regQ: {
type: String,
default: '(?<=q=).+(?=&)|(?<=q=).+$',
},
placement: {
type: String,
default: 'bottomRight',
},
isDropdown: {
type: Boolean,
default: true,
},
},
data() {
return {
advancedExpList,
compareTypeList,
visible: false,
ruleList: [],
filterExp: '',
}
},
methods: {
visibleChange(open, isInitOne = true) {
// isInitOne 初始化exp为空时ruleList是否默认给一条
// const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
const exp = this.expression.match(new RegExp(this.regQ, 'g'))
? this.expression.match(new RegExp(this.regQ, 'g'))[0]
: null
if (open && exp) {
const expArray = exp.split(',').map((item) => {
let has_not = ''
const key = item.split(':')[0]
const val = item
.split(':')
.slice(1)
.join(':')
let type, property, exp, value, min, max, compareType
if (key.includes('-')) {
type = 'or'
if (key.includes('~')) {
property = key.substring(2)
has_not = '~'
} else {
property = key.substring(1)
}
} else {
type = 'and'
if (key.includes('~')) {
property = key.substring(1)
has_not = '~'
} else {
property = key
}
}
const in_reg = /(?<=\().+(?=\))/g
const range_reg = /(?<=\[).+(?=\])/g
const compare_reg = /(?<=>=|<=|>(?!=)|<(?!=)).+/
if (val === '*') {
exp = has_not + 'value'
value = ''
} else if (in_reg.test(val)) {
exp = has_not + 'in'
value = val.match(in_reg)[0]
} else if (range_reg.test(val)) {
exp = has_not + 'range'
value = val.match(range_reg)[0]
min = value.split('_TO_')[0]
max = value.split('_TO_')[1]
} else if (compare_reg.test(val)) {
exp = has_not + 'compare'
value = val.match(compare_reg)[0]
const _compareType = val.substring(0, val.match(compare_reg)['index'])
const idx = compareTypeList.findIndex((item) => item.label === _compareType)
compareType = compareTypeList[idx].value
} else if (!val.includes('*')) {
exp = has_not + 'is'
value = val
} else {
const resList = [
['contain', /(?<=\*).*(?=\*)/g],
['end_with', /(?<=\*).+/g],
['start_with', /.+(?=\*)/g],
]
for (let i = 0; i < 3; i++) {
const reg = resList[i]
if (reg[1].test(val)) {
exp = has_not + reg[0]
value = val.match(reg[1])[0]
break
}
}
}
return {
id: uuidv4(),
type,
property,
exp,
value,
min,
max,
compareType,
}
})
this.ruleList = [...expArray]
} else if (open) {
this.ruleList = isInitOne
? [
{
id: uuidv4(),
type: 'and',
property:
this.canSearchPreferenceAttrList && this.canSearchPreferenceAttrList.length
? this.canSearchPreferenceAttrList[0].name
: undefined,
exp: 'is',
value: null,
},
]
: []
}
},
handleClear() {
this.ruleList = [
{
id: uuidv4(),
type: 'and',
property: this.canSearchPreferenceAttrList[0].name,
exp: 'is',
value: null,
},
]
this.filterExp = ''
this.visible = false
this.$emit('setExpFromFilter', this.filterExp)
},
handleSubmit() {
if (this.ruleList && this.ruleList.length) {
this.ruleList[0].type = 'and' // 增删后以防万一第一个不是and
this.filterExp = ''
const expList = this.ruleList.map((rule) => {
let singleRuleExp = ''
let _exp = rule.exp
if (rule.type === 'or') {
singleRuleExp += '-'
}
if (rule.exp.includes('~')) {
singleRuleExp += '~'
_exp = rule.exp.split('~')[1]
}
singleRuleExp += `${rule.property}:`
if (_exp === 'is') {
singleRuleExp += `${rule.value ?? ''}`
}
if (_exp === 'contain') {
singleRuleExp += `*${rule.value ?? ''}*`
}
if (_exp === 'start_with') {
singleRuleExp += `${rule.value ?? ''}*`
}
if (_exp === 'end_with') {
singleRuleExp += `*${rule.value ?? ''}`
}
if (_exp === 'value') {
singleRuleExp += `*`
}
if (_exp === 'in') {
singleRuleExp += `(${rule.value ?? ''})`
}
if (_exp === 'range') {
singleRuleExp += `[${rule.min}_TO_${rule.max}]`
}
if (_exp === 'compare') {
const idx = compareTypeList.findIndex((item) => item.value === rule.compareType)
singleRuleExp += `${compareTypeList[idx].label}${rule.value ?? ''}`
}
return singleRuleExp
})
this.filterExp = expList.join(',')
this.$emit('setExpFromFilter', this.filterExp)
} else {
this.$emit('setExpFromFilter', '')
}
this.visible = false
},
},
}
</script>
<style lang="less" scoped>
.table-filter {
.table-filter-add {
margin-top: 10px;
& > a {
padding: 2px 8px;
&:hover {
background-color: #f0faff;
border-radius: 5px;
}
}
}
.table-filter-extra-icon {
padding: 0px 2px;
&:hover {
display: inline-block;
border-radius: 5px;
background-color: #f0faff;
}
}
}
</style>
<style lang="less">
.table-filter-extra-operation {
.ant-popover-inner-content {
padding: 3px 4px;
.operation {
cursor: pointer;
width: 90px;
height: 30px;
line-height: 30px;
padding: 3px 4px;
border-radius: 5px;
transition: all 0.3s;
&:hover {
background-color: #f0faff;
}
> .anticon {
margin-right: 10px;
}
}
}
}
</style>

View File

@@ -1,45 +1,57 @@
<template>
<span>
<ops-icon :type="getPropertyIcon(attr)" />
</span>
</template>
<script>
export default {
name: 'ValueTypeIcon',
props: {
attr: {
type: Object,
default: () => {},
},
},
methods: {
getPropertyIcon(attr) {
switch (attr.value_type) {
case '0':
return 'icon-xianxing-shishu'
case '1':
return 'icon-xianxing-fudianshu'
case '2':
if (attr.is_password) {
return 'icon-xianxing-password'
}
if (attr.is_link) {
return 'icon-xianxing-link'
}
return 'icon-xianxing-wenben'
case '3':
return 'icon-xianxing-datetime'
case '4':
return 'icon-xianxing-date'
case '5':
return 'icon-xianxing-time'
case '6':
return 'icon-xianxing-json'
}
},
},
}
</script>
<style></style>
<template>
<span>
<ops-icon :type="getPropertyIcon(attr)" />
</span>
</template>
<script>
export default {
name: 'ValueTypeIcon',
props: {
attr: {
type: Object,
default: () => {},
},
},
methods: {
getPropertyStyle(attr) {
switch (attr.value_type) {
case '0':
return { color: '#cf1322', backgroundColor: '#fff1f0' }
case '1':
return { color: '#d4b106', backgroundColor: '#feffe6' }
case '2':
return { color: '#d46b08', backgroundColor: '#fff7e6' }
case '3':
return { color: '#531dab', backgroundColor: '#f9f0ff' }
case '4':
return { color: '#389e0d', backgroundColor: '#f6ffed' }
case '5':
return { color: '#08979c', backgroundColor: '#e6fffb' }
case '6':
return { color: '#c41d7f', backgroundColor: '#fff0f6' }
}
},
getPropertyIcon(attr) {
switch (attr.value_type) {
case '0':
return 'icon-xianxing-shishu'
case '1':
return 'icon-xianxing-fudianshu'
case '2':
return 'icon-xianxing-wenben'
case '3':
return 'icon-xianxing-datetime'
case '4':
return 'icon-xianxing-date'
case '5':
return 'icon-xianxing-time'
case '6':
return 'icon-xianxing-json'
}
},
},
}
</script>
<style></style>

View File

@@ -4,7 +4,7 @@ const appConfig = {
buildAclToModules: true, // 是否在各个应用下 内联权限管理
ssoLogoutURL: '/api/sso/logout',
showDocs: false,
useEncryption: false,
useEncryption: true,
}
export default appConfig

View File

@@ -99,7 +99,7 @@
align="center"
show-overflow>
<template #default="{ row }">
<span v-show="isGroup">
<span v-show="row.isGroup">
<a @click="handleDisplayMember(row)">成员</a>
<a-divider type="vertical" />
<a @click="handleGroupEdit(row)">编辑</a>

View File

@@ -35,8 +35,8 @@ export default {
secret: '',
},
rules: {
key: [{ required: false, message: 'key is required' }],
secret: [{ required: false, message: 'secret is required' }],
key: [{ required: true, message: 'key is required' }],
secret: [{ required: true, message: 'secret is required' }],
},
visible: false,
}

View File

@@ -1,198 +1,194 @@
<template>
<div class="acl-users">
<div class="acl-users-header">
<a-button v-if="isAclAdmin" @click="handleCreate" type="primary">{{ btnName }}</a-button>
<a-input-search
class="ops-input"
allowClear
:style="{ display: 'inline', marginLeft: '10px' }"
placeholder="搜索 | 用户名、中文名"
v-model="searchName"
></a-input-search>
</div>
<a-spin :spinning="loading">
<vxe-grid
stripe
class="ops-stripe-table"
:columns="tableColumns"
:data="tableData"
show-overflow
highlight-hover-row
:height="`${windowHeight - 165}px`"
size="small"
>
<template #block_default="{row}">
<a-icon type="lock" v-if="row.block" />
</template>
<template #action_default="{row}">
<a-space>
<a :disabled="isAclAdmin ? false : true" @click="handleEdit(row)">
<a-icon type="edit" />
</a>
<a-tooltip title="权限汇总">
<a @click="handlePermCollect(row)"><a-icon type="solution"/></a>
</a-tooltip>
<a-popconfirm :title="`确认删除【${row.nickname || row.username}】?`" @confirm="deleteUser(row.uid)">
<a :style="{ color: 'red' }"><ops-icon type="icon-xianxing-delete"/></a>
</a-popconfirm>
</a-space>
</template>
</vxe-grid>
</a-spin>
<userForm ref="userForm" :handleOk="handleOk"> </userForm>
<perm-collect-form ref="permCollectForm"></perm-collect-form>
</div>
</template>
<script>
import { mapState } from 'vuex'
import userForm from './module/userForm'
import PermCollectForm from './module/permCollectForm'
import { deleteUserById, searchUser, getOnDutyUser } from '@/modules/acl/api/user'
export default {
name: 'Users',
components: {
userForm,
PermCollectForm,
},
data() {
return {
loading: false,
tableColumns: [
{
title: '用户名',
field: 'username',
sortable: true,
minWidth: '100px',
fixed: 'left',
},
{
title: '中文名',
field: 'nickname',
minWidth: '100px',
},
{
title: '加入时间',
field: 'date_joined',
minWidth: '160px',
align: 'center',
sortable: true,
},
{
title: '锁定',
field: 'block',
width: '150px',
align: 'center',
slots: {
default: 'block_default',
},
},
{
title: '操作',
field: 'action',
width: '150px',
fixed: 'right',
align: 'center',
slots: {
default: 'action_default',
},
},
],
onDutuUids: [],
btnName: '新增用户',
allUsers: [],
tableData: [],
searchName: '',
}
},
beforeCreate() {
this.form = this.$form.createForm(this)
},
async beforeMount() {
this.loading = true
await this.getOnDutyUser()
this.search()
},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
}),
isAclAdmin: function() {
if (this.$store.state.user.roles.permissions.filter((item) => item === 'acl_admin').length > 0) {
return true
} else {
return false
}
},
},
watch: {
searchName: {
immediate: true,
handler(newVal, oldVal) {
if (newVal) {
this.tableData = this.allUsers.filter(
(item) =>
item.username.toLowerCase().includes(newVal.toLowerCase()) ||
item.nickname.toLowerCase().includes(newVal.toLowerCase())
)
} else {
this.tableData = this.allUsers
}
},
},
},
mounted() {},
inject: ['reload'],
methods: {
async getOnDutyUser() {
await getOnDutyUser().then((res) => {
this.onDutuUids = res.map((i) => i.uid)
})
},
search() {
searchUser({ page_size: 10000 }).then((res) => {
const ret = res.users.filter((u) => this.onDutuUids.includes(u.uid))
this.allUsers = ret
this.tableData = ret
this.loading = false
})
},
handlePermCollect(record) {
this.$refs['permCollectForm'].collect(record)
},
handleEdit(record) {
this.$refs.userForm.handleEdit(record)
},
async handleOk() {
this.searchName = ''
await this.getOnDutyUser()
this.search()
},
handleCreate() {
this.$refs.userForm.handleCreate()
},
deleteUser(uid) {
deleteUserById(uid).then((res) => {
this.$message.success(`删除成功`)
this.handleOk()
})
},
},
}
</script>
<style lang="less" scoped>
.acl-users {
border-radius: 15px;
background-color: #fff;
height: calc(100vh - 64px);
margin-bottom: -24px;
padding: 24px;
.acl-users-header {
display: inline-flex;
margin-bottom: 15px;
}
}
</style>
<template>
<div class="acl-users">
<div class="acl-users-header">
<a-button v-if="isAclAdmin" @click="handleCreate" type="primary">{{ btnName }}</a-button>
<a-input-search
class="ops-input"
allowClear
:style="{ display: 'inline', marginLeft: '10px' }"
placeholder="搜索 | 用户名、中文名"
v-model="searchName"
></a-input-search>
</div>
<a-spin :spinning="loading">
<vxe-grid
stripe
class="ops-stripe-table"
:columns="tableColumns"
:data="tableData"
show-overflow
highlight-hover-row
:height="`${windowHeight - 165}px`"
size="small"
>
<template #block_default="{row}">
<a-icon type="lock" v-if="row.block" />
</template>
<template #action_default="{row}">
<a-space>
<a :disabled="isAclAdmin ? false : true" @click="handleEdit(row)">
<a-icon type="edit" />
</a>
<a-tooltip title="权限汇总">
<a @click="handlePermCollect(row)"><a-icon type="solution"/></a>
</a-tooltip>
<a-popconfirm :title="`确认删除【${row.nickname || row.username}】?`" @confirm="deleteUser(row.uid)">
<a :style="{ color: 'red' }"><ops-icon type="icon-xianxing-delete"/></a>
</a-popconfirm>
</a-space>
</template>
</vxe-grid>
</a-spin>
<userForm ref="userForm" :handleOk="handleOk"> </userForm>
<perm-collect-form ref="permCollectForm"></perm-collect-form>
</div>
</template>
<script>
import { mapState } from 'vuex'
import userForm from './module/userForm'
import PermCollectForm from './module/permCollectForm'
import { deleteUserById, searchUser, getOnDutyUser } from '@/modules/acl/api/user'
export default {
name: 'Users',
components: {
userForm,
PermCollectForm,
},
data() {
return {
loading: false,
tableColumns: [
{
title: '用户名',
field: 'username',
sortable: true,
minWidth: '100px',
fixed: 'left',
},
{
title: '中文名',
field: 'nickname',
minWidth: '100px',
},
{
title: '加入时间',
field: 'date_joined',
minWidth: '160px',
align: 'center',
sortable: true,
},
{
title: '锁定',
field: 'block',
width: '150px',
align: 'center',
slots: {
default: 'block_default',
},
},
{
title: '操作',
field: 'action',
width: '150px',
fixed: 'right',
align: 'center',
slots: {
default: 'action_default',
},
},
],
onDutuUids: [],
btnName: '新增用户',
allUsers: [],
tableData: [],
searchName: '',
}
},
beforeCreate() {
this.form = this.$form.createForm(this)
},
async beforeMount() {
this.loading = true
await getOnDutyUser().then((res) => {
this.onDutuUids = res.map((i) => i.uid)
this.search()
})
},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
}),
isAclAdmin: function() {
if (this.$store.state.user.roles.permissions.filter((item) => item === 'acl_admin').length > 0) {
return true
} else {
return false
}
},
},
watch: {
searchName: {
immediate: true,
handler(newVal, oldVal) {
if (newVal) {
this.tableData = this.allUsers.filter(
(item) =>
item.username.toLowerCase().includes(newVal.toLowerCase()) ||
item.nickname.toLowerCase().includes(newVal.toLowerCase())
)
} else {
this.tableData = this.allUsers
}
},
},
},
mounted() {},
inject: ['reload'],
methods: {
search() {
searchUser({ page_size: 10000 }).then((res) => {
const ret = res.users.filter((u) => this.onDutuUids.includes(u.uid))
this.allUsers = ret
this.tableData = ret
this.loading = false
})
},
handlePermCollect(record) {
this.$refs['permCollectForm'].collect(record)
},
handleEdit(record) {
this.$refs.userForm.handleEdit(record)
},
handleOk() {
this.searchName = ''
this.search()
},
handleCreate() {
this.$refs.userForm.handleCreate()
},
deleteUser(uid) {
deleteUserById(uid).then((res) => {
this.$message.success(`删除成功`)
this.handleOk()
})
},
},
}
</script>
<style lang="less" scoped>
.acl-users {
border-radius: 15px;
background-color: #fff;
height: calc(100vh - 64px);
margin-bottom: -24px;
padding: 24px;
.acl-users-header {
display: inline-flex;
margin-bottom: 15px;
}
}
</style>

View File

@@ -1,177 +1,170 @@
import { axios } from '@/utils/request'
/**
* 获取 ci_type 的属性
* @param CITypeName
* @param parameter
* @returns {AxiosPromise}
*/
export function getCITypeAttributesByName(CITypeName, parameter) {
return axios({
url: `/v0.1/ci_types/${CITypeName}/attributes`,
method: 'get',
params: parameter
})
}
/**
* 获取 ci_type 的属性
* @param CITypeId
* @param parameter
* @returns {AxiosPromise}
*/
export function getCITypeAttributesById(CITypeId, parameter) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attributes`,
method: 'get',
params: parameter
})
}
/**
* 更新属性
* @param attrId
* @param data
* @returns {AxiosPromise}
*/
export function updateAttributeById(attrId, data) {
return axios({
url: `/v0.1/attributes/${attrId}`,
method: 'put',
data: data
})
}
/**
* 添加属性
* @param data
* @returns {AxiosPromise}
*/
export function createAttribute(data) {
return axios({
url: `/v0.1/attributes`,
method: 'post',
data: data
})
}
/**
* 搜索属性/ 获取所有的属性
* @param data
* @returns {AxiosPromise}
*/
export function searchAttributes(params) {
return axios({
url: `/v0.1/attributes/s`,
method: 'get',
params: params
})
}
export function getCITypeAttributesByTypeIds(params) {
return axios({
url: `/v0.1/ci_types/attributes`,
method: 'get',
params: params
})
}
export function getCITypeCommonAttributesByTypeIds(params) {
return axios({
url: `/v0.1/ci_types/common_attributes`,
method: 'get',
params: params
})
}
/**
* 删除属性
* @param attrId
* @returns {AxiosPromise}
*/
export function deleteAttributesById(attrId) {
return axios({
url: `/v0.1/attributes/${attrId}`,
method: 'delete'
})
}
/**
* 绑定ci_type 属性
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function createCITypeAttributes(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attributes`,
method: 'post',
data: data
})
}
/**
* 更新ci_type 属性
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function updateCITypeAttributesById(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attributes`,
method: 'put',
data: data
})
}
/**
* 删除ci_type 属性
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function deleteCITypeAttributesById(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attributes`,
method: 'delete',
data: data
})
}
export function transferCITypeAttrIndex(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attributes/transfer`,
method: 'POST',
data: data
})
}
export function transferCITypeGroupIndex(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attribute_groups/transfer`,
method: 'POST',
data: data
})
}
export function canDefineComputed() {
return axios({
url: `/v0.1/ci_types/can_define_computed`,
method: 'HEAD',
})
}
export function calcComputedAttribute(attr_id) {
return axios({
url: `/v0.1/attributes/${attr_id}/calc_computed_attribute`,
method: 'PUT',
})
}
export function getAttrPassword(ci_id, attr_id) {
return axios({
url: `/v0.1/ci/${ci_id}/attributes/${attr_id}/password`,
method: 'Get',
})
}
import { axios } from '@/utils/request'
/**
* 获取 ci_type 的属性
* @param CITypeName
* @param parameter
* @returns {AxiosPromise}
*/
export function getCITypeAttributesByName(CITypeName, parameter) {
return axios({
url: `/v0.1/ci_types/${CITypeName}/attributes`,
method: 'get',
params: parameter
})
}
/**
* 获取 ci_type 的属性
* @param CITypeId
* @param parameter
* @returns {AxiosPromise}
*/
export function getCITypeAttributesById(CITypeId, parameter) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attributes`,
method: 'get',
params: parameter
})
}
/**
* 更新属性
* @param attrId
* @param data
* @returns {AxiosPromise}
*/
export function updateAttributeById(attrId, data) {
return axios({
url: `/v0.1/attributes/${attrId}`,
method: 'put',
data: data
})
}
/**
* 添加属性
* @param data
* @returns {AxiosPromise}
*/
export function createAttribute(data) {
return axios({
url: `/v0.1/attributes`,
method: 'post',
data: data
})
}
/**
* 搜索属性/ 获取所有的属性
* @param data
* @returns {AxiosPromise}
*/
export function searchAttributes(params) {
return axios({
url: `/v0.1/attributes/s`,
method: 'get',
params: params
})
}
export function getCITypeAttributesByTypeIds(params) {
return axios({
url: `/v0.1/ci_types/attributes`,
method: 'get',
params: params
})
}
export function getCITypeCommonAttributesByTypeIds(params) {
return axios({
url: `/v0.1/ci_types/common_attributes`,
method: 'get',
params: params
})
}
/**
* 删除属性
* @param attrId
* @returns {AxiosPromise}
*/
export function deleteAttributesById(attrId) {
return axios({
url: `/v0.1/attributes/${attrId}`,
method: 'delete'
})
}
/**
* 绑定ci_type 属性
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function createCITypeAttributes(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attributes`,
method: 'post',
data: data
})
}
/**
* 更新ci_type 属性
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function updateCITypeAttributesById(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attributes`,
method: 'put',
data: data
})
}
/**
* 删除ci_type 属性
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function deleteCITypeAttributesById(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attributes`,
method: 'delete',
data: data
})
}
export function transferCITypeAttrIndex(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attributes/transfer`,
method: 'POST',
data: data
})
}
export function transferCITypeGroupIndex(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attribute_groups/transfer`,
method: 'POST',
data: data
})
}
export function canDefineComputed() {
return axios({
url: `/v0.1/ci_types/can_define_computed`,
method: 'HEAD',
})
}
export function calcComputedAttribute(attr_id) {
return axios({
url: `/v0.1/attributes/${attr_id}/calc_computed_attribute`,
method: 'PUT',
})
}

View File

@@ -1,51 +1,42 @@
<template>
<div>
<span v-if="!isShow && !isTableLoading">{{ showPassword }}</span>
<span v-else>{{ password }}</span>
<a :style="{ marginLeft: '10px' }" @click="getPassword"><a-icon :type="isShow ? 'eye-invisible' : 'eye'"/></a>
</div>
</template>
<script>
import { mapState } from 'vuex'
import { getAttrPassword } from '../../api/CITypeAttr'
export default {
name: 'PasswordField',
props: {
ci_id: {
type: Number,
default: 0,
},
attr_id: {
type: Number,
default: 0,
},
},
data() {
return {
isShow: false,
password: '',
}
},
computed: {
showPassword() {
return '******'
},
...mapState('cmdbStore', ['isTableLoading']),
},
methods: {
getPassword() {
if (this.isShow) {
this.isShow = false
} else {
getAttrPassword(this.ci_id, this.attr_id).then((res) => {
this.password = res.value
this.isShow = true
})
}
},
},
}
</script>
<style></style>
<template>
<div>
<span v-if="!isShow && !isTableLoading">{{ showPassword }}</span>
<span v-else>{{ password }}</span>
<a
:style="{ marginLeft: '10px' }"
@click="
() => {
isShow = !isShow
}
"
><a-icon
:type="isShow ? 'eye-invisible' : 'eye'"
/></a>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'PasswordField',
props: {
password: {
type: String,
default: '',
},
},
data() {
return {
isShow: false,
}
},
computed: {
showPassword() {
return '******'
},
...mapState('cmdbStore', ['isTableLoading']),
},
}
</script>
<style></style>

View File

@@ -16,7 +16,7 @@
</a-space> -->
</div>
<div style="margin-top:10px">
<codemirror style="z-index: 9999" :options="cmOptions" v-model="jsonData"></codemirror>
<vue-json-editor v-model="jsonData" :showBtns="false" :mode="'text'" />
<!-- <a-empty
v-else
:image-style="{
@@ -31,14 +31,11 @@
</template>
<script>
import { codemirror } from 'vue-codemirror'
import 'codemirror/lib/codemirror.css'
require('codemirror/mode/python/python.js')
import vueJsonEditor from 'vue-json-editor'
export default {
name: 'Body',
components: { codemirror },
components: { vueJsonEditor },
data() {
const segmentedContentTypes = [
{
@@ -63,14 +60,7 @@ export default {
return {
segmentedContentTypes,
// contentType: 'none',
jsonData: '',
cmOptions: {
lineNumbers: true,
mode: 'python',
height: '200px',
tabSize: 4,
lineWrapping: true,
},
jsonData: {},
}
},
}

View File

@@ -81,11 +81,7 @@ export default {
this.$refs.Parameters.parameters.forEach((item) => {
parameters[item.key] = item.value
})
let body = this.$refs.Body.jsonData
try {
JSON.parse(body)
body = JSON.parse(body)
} catch {}
const body = this.$refs.Body.jsonData
const headers = {}
this.$refs.Header.headers.forEach((item) => {
headers[item.key] = item.value
@@ -103,6 +99,7 @@ export default {
return { method, url, parameters, body, headers, authorization }
},
setParams(params) {
console.log(2222, params)
const { method, url, parameters, body, headers, authorization = {} } = params ?? {}
this.method = method
this.url = url
@@ -114,11 +111,7 @@ export default {
value: parameters[key],
}
}) || []
if (body && Object.prototype.toString.call(body) === '[object Object]') {
this.$refs.Body.jsonData = JSON.stringify(body)
} else {
this.$refs.Body.jsonData = body
}
this.$refs.Body.jsonData = body
this.$refs.Header.headers =
Object.keys(headers).map((key) => {
return {

View File

@@ -61,18 +61,18 @@ const genCmdbRoutes = async () => {
name: 'cmdb_disabled2',
meta: { title: '配置', disabled: true, },
},
{
path: '/cmdb/preference',
component: () => import('../views/preference/index'),
name: 'cmdb_preference',
meta: { title: '我的订阅', icon: 'ops-cmdb-preference', selectedIcon: 'ops-cmdb-preference-selected', keepAlive: false }
},
{
path: '/cmdb/batch',
component: () => import('../views/batch'),
name: 'cmdb_batch',
meta: { 'title': '批量导入', icon: 'ops-cmdb-batch', selectedIcon: 'ops-cmdb-batch-selected', keepAlive: false }
},
{
path: '/cmdb/preference',
component: () => import('../views/preference/index'),
name: 'cmdb_preference',
meta: { title: '我的订阅', icon: 'ops-cmdb-preference', selectedIcon: 'ops-cmdb-preference-selected', keepAlive: false }
},
{
path: '/cmdb/ci_types',
name: 'ci_type',

View File

@@ -1,28 +1,26 @@
export const valueTypeMap = {
'0': '整数',
'1': '浮点数',
'2': '文本',
'3': '日期时间',
'4': '日期',
'5': '时间',
'6': 'JSON',
'7': '密码',
'8': '链接'
}
export const defautValueColor = [
{ value: '#d9d9d9' },
{ value: '#ffccc7' },
{ value: '#ffd8bf' },
{ value: '#ffe7ba' },
{ value: '#fff1b8' },
{ value: '#f4ffb8' },
{ value: '#d9f7be' },
{ value: '#b5f5ec' },
{ value: '#bae7ff' },
{ value: '#d6e4ff' },
{ value: '#efdbff' },
{ value: '#ffd6e7' },
]
export const defaultBGColors = ['#ffccc7', '#ffd8bf', '#ffe7ba', '#fff1b8', '#d9f7be', '#b5f5ec', '#bae7ff', '#d6e4ff', '#efdbff', '#ffd6e7']
export const valueTypeMap = {
'0': '整数',
'1': '浮点数',
'2': '文本',
'3': 'datetime',
'4': 'date',
'5': 'time',
'6': 'json'
}
export const defautValueColor = [
{ value: '#d9d9d9' },
{ value: '#ffccc7' },
{ value: '#ffd8bf' },
{ value: '#ffe7ba' },
{ value: '#fff1b8' },
{ value: '#f4ffb8' },
{ value: '#d9f7be' },
{ value: '#b5f5ec' },
{ value: '#bae7ff' },
{ value: '#d6e4ff' },
{ value: '#efdbff' },
{ value: '#ffd6e7' },
]
export const defaultBGColors = ['#ffccc7', '#ffd8bf', '#ffe7ba', '#fff1b8', '#d9f7be', '#b5f5ec', '#bae7ff', '#d6e4ff', '#efdbff', '#ffd6e7']

View File

@@ -1,181 +1,176 @@
/* eslint-disable */
import _ from 'lodash'
import XLSX from 'xlsx'
import XLSXS from 'xlsx-js-style'
export function sum(arr) {
if (!arr.length) {
return 0
}
return arr.reduce(function (prev, curr, idx, arr) {
return prev + curr
})
}
const strLength = (fData) => {
if (!fData) {
return 0
}
if (fData.length && typeof fData === 'object') {
fData = fData.join(' ')
}
let intLength = 0
for (let i = 0; i < fData.length; i++) {
if ((fData.charCodeAt(i) < 0) || (fData.charCodeAt(i) > 255)) {
intLength = intLength + 2
}
else {
intLength = intLength + 1
}
}
return Math.floor(intLength * 7)
}
String.prototype.pxWidth = function (font) {
// re-use canvas object for better performance
const canvas = String.prototype.pxWidth.canvas || (String.prototype.pxWidth.canvas = document.createElement("canvas")),
context = canvas.getContext("2d");
font && (context.font = font);
const metrics = context.measureText(this);
return metrics.width;
}
export function getCITableColumns(data, attrList, width = 1600, height) {
// 计算出来 主table的列表 布局属性
const _attrList = _.orderBy(attrList, ['is_fixed'], ['desc'])
const columns = []
for (let attr of _attrList) {
const editRender = { name: 'input' }
switch (attr.value_type) {
case '0':
editRender['props'] = { 'type': 'float' }
break
case '1':
editRender['props'] = { 'type': 'float' }
break
case '2':
editRender['attrs'] = { 'type': 'text' }
break
case '3':
editRender['props'] = { 'type': 'datetime' }
break
case "4":
editRender['props'] = { 'type': 'date' }
break
case '5':
editRender['props'] = { 'type': 'time' }
break
case '6':
editRender['props'] = { 'type': 'text' }
break
default:
editRender['props'] = { 'type': 'text' }
break
}
if (attr.is_choice) {
editRender.name = '$select'
editRender.options = attr.choice_value ? attr.choice_value.map(item => { return { label: item, value: item } }) : []
delete editRender.props
}
columns.push({
attr_id:attr.id,
editRender,
title: attr.alias || attr.name,
field: attr.name,
value_type: attr.value_type,
sortable: !!attr.is_sortable,
filters: attr.is_choice ? attr.choice_value : null,
width: Math.min(Math.max(100, ...data.map(item => strLength(item[attr.name]))), 350),
is_link: attr.is_link,
is_password: attr.is_password,
is_list: attr.is_list,
is_choice: attr.is_choice,
is_fixed: attr.is_fixed,
})
}
const totalWidth = sum(columns.map(col => col.width))
if (totalWidth < width) {
columns.map(item => {
// if (item.width === 100) {
delete item.width
// }
})
}
return columns
}
export const getPropertyStyle = (attr) => {
switch (attr.value_type) {
case '0':
return { color: '#cf1322', backgroundColor: '#fff1f0' }
case '1':
return { color: '#d4b106', backgroundColor: '#feffe6' }
case '2':
return { color: '#d46b08', backgroundColor: '#fff7e6' }
case '3':
return { color: '#531dab', backgroundColor: '#f9f0ff' }
case '4':
return { color: '#389e0d', backgroundColor: '#f6ffed' }
case '5':
return { color: '#08979c', backgroundColor: '#e6fffb' }
case '6':
return { color: '#c41d7f', backgroundColor: '#fff0f6' }
case '7':
return { color: '#0390CC', backgroundColor: '#e6fffb' }
case '8':
return { color: '#144BD9', backgroundColor: '#fff0f6' }
}
}
export const getLastLayout = (data, x1 = 0, y1 = 0, w1 = 0) => {
const _tempData = _.orderBy(data, ['y', 'x'], ['asc', 'asc'])
if (!_tempData.length) {
return { xLast: 0, yLast: 0, wLast: 0 }
}
const { x, y, w } = _tempData[_tempData.length - 1]
if (y < y1) {
return { xLast: x1, yLast: y1, wLast: w1 }
} else if (y > y1) {
return { xLast: x, yLast: y, wLast: w }
} else {
const xLast = _.max([x, x1])
return { xLast, yLast: y, wLast: xLast === x ? w : w1 }
}
}
// 数字加逗号
export const toThousands = (num = 0) => {
return num.toString().replace(/\d+/, function (n) {
return n.replace(/(\d)(?=(?:\d{3})+$)/g, '$1,')
})
}
export const downloadExcel = (data, fileName = `${moment().format('YYYY-MM-DD HH:mm:ss')}.xls`) => {
// STEP 1: Create a new workbook
const wb = XLSXS.utils.book_new()
// STEP 2: Create data rows and styles
const rowArray = data
// STEP 3: Create worksheet with rows; Add worksheet to workbook
const ws = XLSXS.utils.aoa_to_sheet(rowArray)
XLSXS.utils.book_append_sheet(wb, ws, fileName)
let maxColumnNumber = 1 // 默认最大列数
rowArray.forEach(item => { if (item.length > maxColumnNumber) { maxColumnNumber = item.length } })
// 添加列宽
ws['!cols'] = (rowArray[0].map(item => {
return { width: 22 }
}))
// // 添加行高
// ws['!rows'] = [{ 'hpt': 80 }]
// STEP 4: Write Excel file to browser #导出
XLSXS.writeFile(wb, fileName + '.xlsx')
}
/* eslint-disable */
import _ from 'lodash'
import XLSX from 'xlsx'
import XLSXS from 'xlsx-js-style'
export function sum(arr) {
if (!arr.length) {
return 0
}
return arr.reduce(function (prev, curr, idx, arr) {
return prev + curr
})
}
const strLength = (fData) => {
if (!fData) {
return 0
}
if (fData.length && typeof fData === 'object') {
fData = fData.join(' ')
}
let intLength = 0
for (let i = 0; i < fData.length; i++) {
if ((fData.charCodeAt(i) < 0) || (fData.charCodeAt(i) > 255)) {
intLength = intLength + 2
}
else {
intLength = intLength + 1
}
}
return Math.floor(intLength * 7)
}
String.prototype.pxWidth = function (font) {
// re-use canvas object for better performance
const canvas = String.prototype.pxWidth.canvas || (String.prototype.pxWidth.canvas = document.createElement("canvas")),
context = canvas.getContext("2d");
font && (context.font = font);
const metrics = context.measureText(this);
return metrics.width;
}
export function getCITableColumns(data, attrList, width = 1600, height) {
// 计算出来 主table的列表 布局属性
const _attrList = _.orderBy(attrList, ['is_fixed'], ['desc'])
const columns = []
for (let attr of _attrList) {
const editRender = { name: 'input' }
switch (attr.value_type) {
case '0':
editRender['props'] = { 'type': 'float' }
break
case '1':
editRender['props'] = { 'type': 'float' }
break
case '2':
editRender['attrs'] = { 'type': 'text' }
break
case '3':
editRender['props'] = { 'type': 'datetime' }
break
case "4":
editRender['props'] = { 'type': 'date' }
break
case '5':
editRender['props'] = { 'type': 'time' }
break
case '6':
editRender['props'] = { 'type': 'text' }
break
default:
editRender['props'] = { 'type': 'text' }
break
}
if (attr.is_choice) {
editRender.name = '$select'
editRender.options = attr.choice_value ? attr.choice_value.map(item => { return { label: item, value: item } }) : []
delete editRender.props
}
columns.push({
editRender,
title: attr.alias || attr.name,
field: attr.name,
value_type: attr.value_type,
sortable: !!attr.is_sortable,
filters: attr.is_choice ? attr.choice_value : null,
width: Math.min(Math.max(100, ...data.map(item => strLength(item[attr.name]))), 350),
is_link: attr.is_link,
is_password: attr.is_password,
is_list: attr.is_list,
is_choice: attr.is_choice,
is_fixed: attr.is_fixed,
})
}
const totalWidth = sum(columns.map(col => col.width))
if (totalWidth < width) {
columns.map(item => {
// if (item.width === 100) {
delete item.width
// }
})
}
return columns
}
export const getPropertyStyle = (attr) => {
switch (attr.value_type) {
case '0':
return { color: '#cf1322', backgroundColor: '#fff1f0' }
case '1':
return { color: '#d4b106', backgroundColor: '#feffe6' }
case '2':
return { color: '#d46b08', backgroundColor: '#fff7e6' }
case '3':
return { color: '#531dab', backgroundColor: '#f9f0ff' }
case '4':
return { color: '#389e0d', backgroundColor: '#f6ffed' }
case '5':
return { color: '#08979c', backgroundColor: '#e6fffb' }
case '6':
return { color: '#c41d7f', backgroundColor: '#fff0f6' }
}
}
export const getLastLayout = (data, x1 = 0, y1 = 0, w1 = 0) => {
const _tempData = _.orderBy(data, ['y', 'x'], ['asc', 'asc'])
if (!_tempData.length) {
return { xLast: 0, yLast: 0, wLast: 0 }
}
const { x, y, w } = _tempData[_tempData.length - 1]
if (y < y1) {
return { xLast: x1, yLast: y1, wLast: w1 }
} else if (y > y1) {
return { xLast: x, yLast: y, wLast: w }
} else {
const xLast = _.max([x, x1])
return { xLast, yLast: y, wLast: xLast === x ? w : w1 }
}
}
// 数字加逗号
export const toThousands = (num = 0) => {
return num.toString().replace(/\d+/, function (n) {
return n.replace(/(\d)(?=(?:\d{3})+$)/g, '$1,')
})
}
export const downloadExcel = (data, fileName = `${moment().format('YYYY-MM-DD HH:mm:ss')}.xls`) => {
// STEP 1: Create a new workbook
const wb = XLSXS.utils.book_new()
// STEP 2: Create data rows and styles
const rowArray = data
// STEP 3: Create worksheet with rows; Add worksheet to workbook
const ws = XLSXS.utils.aoa_to_sheet(rowArray)
XLSXS.utils.book_append_sheet(wb, ws, fileName)
let maxColumnNumber = 1 // 默认最大列数
rowArray.forEach(item => { if (item.length > maxColumnNumber) { maxColumnNumber = item.length } })
// 添加列宽
ws['!cols'] = (rowArray[0].map(item => {
return { width: 22 }
}))
// // 添加行高
// ws['!rows'] = [{ 'hpt': 80 }]
// STEP 4: Write Excel file to browser #导出
XLSXS.writeFile(wb, fileName + '.xlsx')
}

View File

@@ -5,7 +5,7 @@
</div>
<a-row>
<a-col :span="12">
<upload-file-form :ciType="ciType" ref="uploadFileForm" @uploadDone="uploadDone"></upload-file-form>
<upload-file-form ref="uploadFileForm" @uploadDone="uploadDone"></upload-file-form>
</a-col>
<a-col :span="24" v-if="ciType && uploadData.length">
<CiUploadTable :ciTypeAttrs="ciTypeAttrs" ref="ciUploadTable" :uploadData="uploadData"></CiUploadTable>

View File

@@ -7,7 +7,6 @@
accept=".xls,.xlsx"
:showUploadList="false"
:fileList="fileList"
:disabled="!ciType"
>
<img :style="{ width: '80px', height: '80px' }" src="@/assets/file_upload.png" />
<p class="ant-upload-text">点击或拖拽文件至此上传</p>
@@ -25,12 +24,6 @@ import { processFile } from '@/modules/cmdb/api/batch'
export default {
name: 'UploadFileForm',
props: {
ciType: {
type: Number,
default: 0,
}
},
data() {
return {
ciItemNum: 0,

File diff suppressed because it is too large Load Diff

View File

@@ -30,7 +30,7 @@
:key="attr.name"
v-for="attr in group.attributes"
>
<CiDetailAttrContent :ci="ci" :attr="attr" @refresh="refresh" @updateCIByself="updateCIByself" />
<CiDetailAttrContent :ci="ci" :attr="attr" @refresh="refresh" />
</el-descriptions-item>
</el-descriptions>
</div>
@@ -97,7 +97,6 @@
</template>
<script>
import _ from 'lodash'
import { Descriptions, DescriptionsItem } from 'element-ui'
import { getCITypeGroupById, getCITypes } from '@/modules/cmdb/api/CIType'
import { getCIHistory } from '@/modules/cmdb/api/history'
@@ -293,19 +292,6 @@ export default {
}
}
},
updateCIByself(params, editAttrName) {
const _ci = { ..._.cloneDeep(this.ci), ...params }
this.ci = _ci
const _find = this.treeViewsLevels.find((level) => level.name === editAttrName)
// 修改的字段为树形视图订阅的字段 则全部reload
setTimeout(() => {
if (_find) {
this.reload()
} else {
this.handleSearch()
}
}, 500)
},
},
}
</script>

View File

@@ -1,377 +1,359 @@
<template>
<CustomDrawer
:title="title + CIType.alias"
width="800"
@close="handleClose"
:maskClosable="false"
:visible="visible"
wrapClassName="create-instance-form"
:bodyStyle="{ paddingTop: 0 }"
:headerStyle="{ borderBottom: 'none' }"
>
<div class="custom-drawer-bottom-action">
<a-button @click="handleClose">取消</a-button>
<a-button type="primary" @click="createInstance">提交</a-button>
</div>
<template v-if="action === 'create'">
<template v-for="group in attributesByGroup">
<CreateInstanceFormByGroup
:ref="`createInstanceFormByGroup_${group.id}`"
:key="group.id || group.name"
:group="group"
@handleFocusInput="handleFocusInput"
:attributeList="attributeList"
/>
</template>
<template v-if="parentsType && parentsType.length">
<a-divider style="font-size:14px;margin:14px 0;font-weight:700;">模型关系</a-divider>
<a-form>
<a-row :gutter="24" align="top" type="flex">
<a-col :span="12" v-for="item in parentsType" :key="item.id">
<a-form-item :label="item.alias || item.name" :colon="false">
<a-input-group compact style="width: 100%">
<a-select v-model="parentsForm[item.name].attr">
<a-select-option
:title="attr.alias || attr.name"
v-for="attr in item.attributes"
:key="attr.name"
:value="attr.name"
>
{{ attr.alias || attr.name }}
</a-select-option>
</a-select>
<a-input placeholder="多个值使用,分割" v-model="parentsForm[item.name].value" style="width: 50%" />
</a-input-group>
</a-form-item>
</a-col>
</a-row>
</a-form>
</template>
</template>
<template v-if="action === 'update'">
<a-form :form="form">
<p>可根据需要修改字段当值为<strong></strong>则该字段<strong>置空</strong></p>
<a-row :gutter="24" v-for="list in batchUpdateLists" :key="list.name">
<a-col :span="11">
<a-form-item>
<el-select showSearch size="small" filterable v-model="list.name" placeholder="请选择需要修改的字段">
<el-option
v-for="attr in attributeList"
:key="attr.name"
:value="attr.name"
:disabled="batchUpdateLists.findIndex((item) => item.name === attr.name) > -1"
:label="attr.alias || attr.name"
>
</el-option>
</el-select>
</a-form-item>
</a-col>
<a-col :span="11">
<a-form-item>
<a-select
:style="{ width: '100%' }"
v-decorator="[list.name, { rules: [{ required: false }] }]"
placeholder="请选择"
v-if="getFieldType(list.name).split('%%')[0] === 'select'"
:mode="getFieldType(list.name).split('%%')[1] === 'multiple' ? 'multiple' : 'default'"
showSearch
allowClear
>
<a-select-option
:value="choice[0]"
:key="'New_' + choice + choice_idx"
v-for="(choice, choice_idx) in getSelectFieldOptions(list.name)"
>
<span :style="choice[1] ? choice[1].style || {} : {}">
<ops-icon
:style="{ color: choice[1].icon.color }"
v-if="choice[1] && choice[1].icon && choice[1].icon.name"
:type="choice[1].icon.name"
/>
{{ choice[0] }}
</span>
</a-select-option>
</a-select>
<a-input-number
v-decorator="[list.name, { rules: [{ required: false }] }]"
style="width: 100%"
v-if="getFieldType(list.name) === 'input_number'"
/>
<a-date-picker
v-decorator="[list.name, { rules: [{ required: false }] }]"
style="width: 100%"
:format="getFieldType(list.name) == 'date' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
v-if="getFieldType(list.name) === 'date' || getFieldType(list.name) === 'datetime'"
:showTime="getFieldType(list.name) === 'date' ? false : { format: 'HH:mm:ss' }"
/>
<a-input
v-if="getFieldType(list.name) === 'input'"
@focus="(e) => handleFocusInput(e, list)"
v-decorator="[list.name, { rules: [{ required: false }] }]"
/>
</a-form-item>
</a-col>
<a-col :span="2">
<a-form-item>
<a :style="{ color: 'red', marginTop: '2px' }" @click="handleDelete(list.name)">
<a-icon type="delete" />
</a>
</a-form-item>
</a-col>
</a-row>
<a-button type="primary" ghost icon="plus" @click="handleAdd">新增修改字段</a-button>
</a-form>
</template>
<!-- </a-form> -->
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
</CustomDrawer>
</template>
<script>
import moment from 'moment'
import { Select, Option } from 'element-ui'
import { getCIType, getCITypeGroupById } from '@/modules/cmdb/api/CIType'
import { addCI } from '@/modules/cmdb/api/ci'
import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue'
import { valueTypeMap } from '../../../utils/const'
import CreateInstanceFormByGroup from './createInstanceFormByGroup.vue'
import { getCITypeParent } from '@/modules/cmdb/api/CITypeRelation'
export default {
name: 'CreateInstanceForm',
components: {
ElSelect: Select,
ElOption: Option,
JsonEditor,
CreateInstanceFormByGroup,
},
props: {
typeIdFromRelation: {
type: Number,
default: 0,
},
},
data() {
return {
valueTypeMap,
action: '',
form: this.$form.createForm(this),
visible: false,
attributeList: [],
CIType: {},
batchUpdateLists: [],
editAttr: null,
attributesByGroup: [],
parentsType: [],
parentsForm: {},
}
},
computed: {
title() {
return this.action === 'create' ? '创建 ' : '批量修改 '
},
typeId() {
if (this.typeIdFromRelation) {
return this.typeIdFromRelation
}
return this.$router.currentRoute.meta.typeId
},
},
provide() {
return {
getFieldType: this.getFieldType,
}
},
inject: ['attrList'],
methods: {
moment,
async getCIType() {
await getCIType(this.typeId).then((res) => {
this.CIType = res.ci_types[0]
})
},
async getAttributeList() {
const _attrList = this.attrList()
this.attributeList = _attrList.sort((x, y) => y.is_required - x.is_required)
await getCITypeGroupById(this.typeId).then((res1) => {
const _attributesByGroup = res1.map((g) => {
g.attributes = g.attributes.filter((attr) => !attr.is_computed)
return g
})
const attrHasGroupIds = []
res1.forEach((g) => {
const id = g.attributes.map((attr) => attr.id)
attrHasGroupIds.push(...id)
})
const otherGroupAttr = this.attributeList.filter(
(attr) => !attrHasGroupIds.includes(attr.id) && !attr.is_computed
)
if (otherGroupAttr.length) {
_attributesByGroup.push({ id: -1, name: '其他', attributes: otherGroupAttr })
}
this.attributesByGroup = _attributesByGroup
})
},
createInstance() {
const _this = this
if (_this.action === 'update') {
this.form.validateFields((err, values) => {
if (err) {
return
}
Object.keys(values).forEach((k) => {
const _tempFind = this.attributeList.find((item) => item.name === k)
if (
_tempFind.value_type === '3' &&
values[k] &&
Object.prototype.toString.call(values[k]) === '[object Object]'
) {
values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
}
if (
_tempFind.value_type === '4' &&
values[k] &&
Object.prototype.toString.call(values[k]) === '[object Object]'
) {
values[k] = values[k].format('YYYY-MM-DD')
}
if (_tempFind.value_type === '6') {
values[k] = values[k] ? JSON.parse(values[k]) : undefined
}
})
_this.$emit('submit', values)
})
} else {
let values = {}
for (let i = 0; i < this.attributesByGroup.length; i++) {
const data = this.$refs[`createInstanceFormByGroup_${this.attributesByGroup[i].id}`][0].getData()
if (data === 'error') {
return
}
values = { ...values, ...data }
}
Object.keys(values).forEach((k) => {
const _tempFind = this.attributeList.find((item) => item.name === k)
if (
_tempFind.value_type === '3' &&
values[k] &&
Object.prototype.toString.call(values[k]) === '[object Object]'
) {
values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
}
if (
_tempFind.value_type === '4' &&
values[k] &&
Object.prototype.toString.call(values[k]) === '[object Object]'
) {
values[k] = values[k].format('YYYY-MM-DD')
}
if (_tempFind.value_type === '6') {
values[k] = values[k] ? JSON.parse(values[k]) : undefined
}
})
values.ci_type = _this.typeId
console.log(this.parentsForm)
Object.keys(this.parentsForm).forEach((type) => {
if (this.parentsForm[type].value) {
values[`$${type}.${this.parentsForm[type].attr}`] = this.parentsForm[type].value
}
})
addCI(values).then((res) => {
_this.$message.success('新增成功!')
_this.visible = false
_this.$emit('reload', { ci_id: res.ci_id })
})
}
},
handleClose() {
this.visible = false
},
handleOpen(visible, action) {
this.visible = visible
this.action = action
this.$nextTick(() => {
this.form.resetFields()
Promise.all([this.getCIType(), this.getAttributeList()]).then(() => {
this.batchUpdateLists = [{ name: this.attributeList[0].name }]
})
if (action === 'create') {
getCITypeParent(this.typeId).then((res) => {
this.parentsType = res.parents
const _parentsForm = {}
res.parents.forEach((item) => {
const _find = item.attributes.find((attr) => attr.id === item.unique_id)
_parentsForm[item.name] = { attr: _find.name, value: '' }
})
this.parentsForm = _parentsForm
})
}
})
},
getFieldType(name) {
const _find = this.attributeList.find((item) => item.name === name)
if (_find) {
if (_find.is_choice) {
if (_find.is_list) {
return 'select%%multiple'
}
return 'select'
} else if (_find.value_type === '0' || _find.value_type === '1') {
return 'input_number'
} else if (_find.value_type === '4' || _find.value_type === '3') {
return valueTypeMap[_find.value_type]
} else {
return 'input'
}
}
return 'input'
},
getSelectFieldOptions(name) {
const _find = this.attributeList.find((item) => item.name === name)
if (_find) {
return _find.choice_value
}
return []
},
handleAdd() {
this.batchUpdateLists.push({ name: undefined })
},
handleDelete(name) {
const _idx = this.batchUpdateLists.findIndex((item) => item.name === name)
if (_idx > -1) {
this.batchUpdateLists.splice(_idx, 1)
}
},
handleFocusInput(e, attr) {
console.log(attr)
const _tempFind = this.attributeList.find((item) => item.name === attr.name)
if (_tempFind.value_type === '6') {
this.editAttr = attr
e.srcElement.blur()
const jsonData = this.form.getFieldValue(attr.name)
this.$refs.jsonEditor.open(null, null, jsonData ? JSON.parse(jsonData) : {})
} else {
this.editAttr = null
}
},
jsonEditorOk(jsonData) {
this.form.setFieldsValue({ [this.editAttr.name]: JSON.stringify(jsonData) })
},
},
}
</script>
<style lang="less">
.create-instance-form {
.ant-form-item {
margin-bottom: 5px;
}
.ant-drawer-body {
overflow-y: auto;
max-height: calc(100vh - 110px);
}
}
</style>
<template>
<CustomDrawer
:title="title + CIType.alias"
width="800"
@close="handleClose"
:maskClosable="false"
:visible="visible"
wrapClassName="create-instance-form"
:bodyStyle="{ paddingTop: 0 }"
:headerStyle="{ borderBottom: 'none' }"
>
<div class="custom-drawer-bottom-action">
<a-button @click="handleClose">取消</a-button>
<a-button type="primary" @click="createInstance">提交</a-button>
</div>
<template v-if="action === 'create'">
<template v-for="group in attributesByGroup">
<CreateInstanceFormByGroup
:ref="`createInstanceFormByGroup_${group.id}`"
:key="group.id || group.name"
:group="group"
@handleFocusInput="handleFocusInput"
:attributeList="attributeList"
/>
</template>
<template v-if="parentsType && parentsType.length">
<a-divider style="font-size:14px;margin:14px 0;font-weight:700;">模型关系</a-divider>
<a-form>
<a-row :gutter="24" align="top" type="flex">
<a-col :span="12" v-for="item in parentsType" :key="item.id">
<a-form-item :label="item.alias || item.name" :colon="false">
<a-input-group compact style="width: 100%">
<a-select v-model="parentsForm[item.name].attr">
<a-select-option
:title="attr.alias || attr.name"
v-for="attr in item.attributes"
:key="attr.name"
:value="attr.name"
>
{{ attr.alias || attr.name }}
</a-select-option>
</a-select>
<a-input placeholder="多个值使用,分割" v-model="parentsForm[item.name].value" style="width: 50%" />
</a-input-group>
</a-form-item>
</a-col>
</a-row>
</a-form>
</template>
</template>
<template v-if="action === 'update'">
<a-form :form="form">
<p>可根据需要修改字段当值为<strong></strong>则该字段<strong>置空</strong></p>
<a-row :gutter="24" v-for="list in batchUpdateLists" :key="list.name">
<a-col :span="11">
<a-form-item>
<el-select showSearch size="small" filterable v-model="list.name" placeholder="请选择需要修改的字段">
<el-option
v-for="attr in attributeList"
:key="attr.name"
:value="attr.name"
:disabled="batchUpdateLists.findIndex((item) => item.name === attr.name) > -1"
:label="attr.alias || attr.name"
>
</el-option>
</el-select>
</a-form-item>
</a-col>
<a-col :span="11">
<a-form-item>
<a-select
:style="{ width: '100%' }"
v-decorator="[list.name, { rules: [{ required: false }] }]"
placeholder="请选择"
v-if="getFieldType(list.name).split('%%')[0] === 'select'"
:mode="getFieldType(list.name).split('%%')[1] === 'multiple' ? 'multiple' : 'default'"
showSearch
allowClear
>
<a-select-option
:value="choice[0]"
:key="'New_' + choice + choice_idx"
v-for="(choice, choice_idx) in getSelectFieldOptions(list.name)"
>
<span :style="choice[1] ? choice[1].style || {} : {}">
<ops-icon
:style="{ color: choice[1].icon.color }"
v-if="choice[1] && choice[1].icon && choice[1].icon.name"
:type="choice[1].icon.name"
/>
{{ choice[0] }}
</span>
</a-select-option>
</a-select>
<a-input-number
v-decorator="[list.name, { rules: [{ required: false }] }]"
style="width: 100%"
v-if="getFieldType(list.name) === 'input_number'"
/>
<a-date-picker
v-decorator="[list.name, { rules: [{ required: false }] }]"
style="width: 100%"
:format="getFieldType(list.name) == 'date' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
v-if="getFieldType(list.name) === 'date' || getFieldType(list.name) === 'datetime'"
:showTime="getFieldType(list.name) === 'date' ? false : { format: 'HH:mm:ss' }"
/>
<a-input
v-if="getFieldType(list.name) === 'input'"
@focus="(e) => handleFocusInput(e, list)"
v-decorator="[list.name, { rules: [{ required: false }] }]"
/>
</a-form-item>
</a-col>
<a-col :span="2">
<a-form-item>
<a :style="{ color: 'red', marginTop: '2px' }" @click="handleDelete(list.name)">
<a-icon type="delete"/>
</a>
</a-form-item>
</a-col>
</a-row>
<a-button type="primary" ghost icon="plus" @click="handleAdd">新增修改字段</a-button>
</a-form>
</template>
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
</CustomDrawer>
</template>
<script>
import moment from 'moment'
import { Select, Option } from 'element-ui'
import { getCIType, getCITypeGroupById } from '@/modules/cmdb/api/CIType'
import { addCI } from '@/modules/cmdb/api/ci'
import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue'
import { valueTypeMap } from '../../../utils/const'
import CreateInstanceFormByGroup from './createInstanceFormByGroup.vue'
import { getCITypeParent } from '@/modules/cmdb/api/CITypeRelation'
export default {
name: 'CreateInstanceForm',
components: {
ElSelect: Select,
ElOption: Option,
JsonEditor,
CreateInstanceFormByGroup,
},
props: {
typeIdFromRelation: {
type: Number,
default: 0,
},
},
data() {
return {
valueTypeMap,
action: '',
form: this.$form.createForm(this),
visible: false,
attributeList: [],
CIType: {},
batchUpdateLists: [],
editAttr: null,
attributesByGroup: [],
parentsType: [],
parentsForm: {},
}
},
computed: {
title() {
return this.action === 'create' ? '创建 ' : '批量修改 '
},
typeId() {
if (this.typeIdFromRelation) {
return this.typeIdFromRelation
}
return this.$router.currentRoute.meta.typeId
},
},
provide() {
return {
getFieldType: this.getFieldType,
}
},
inject: ['attrList'],
methods: {
moment,
async getCIType() {
await getCIType(this.typeId).then((res) => {
this.CIType = res.ci_types[0]
})
},
async getAttributeList() {
const _attrList = this.attrList()
this.attributeList = _attrList.sort((x, y) => y.is_required - x.is_required)
await getCITypeGroupById(this.typeId).then((res1) => {
const _attributesByGroup = res1.map((g) => {
g.attributes = g.attributes.filter((attr) => !attr.is_computed)
return g
})
const attrHasGroupIds = []
res1.forEach((g) => {
const id = g.attributes.map((attr) => attr.id)
attrHasGroupIds.push(...id)
})
const otherGroupAttr = this.attributeList.filter(
(attr) => !attrHasGroupIds.includes(attr.id) && !attr.is_computed
)
if (otherGroupAttr.length) {
_attributesByGroup.push({ id: -1, name: '其他', attributes: otherGroupAttr })
}
this.attributesByGroup = _attributesByGroup
})
},
createInstance() {
const _this = this
if (_this.action === 'update') {
this.form.validateFields((err, values) => {
if (err) {
return
}
Object.keys(values).forEach((k) => {
const _tempFind = this.attributeList.find((item) => item.name === k)
if (_tempFind.value_type === '3' && values[k]) {
values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
}
if (_tempFind.value_type === '4' && values[k]) {
values[k] = values[k].format('YYYY-MM-DD')
}
if (_tempFind.value_type === '6') {
values[k] = values[k] ? JSON.parse(values[k]) : undefined
}
})
_this.$emit('submit', values)
})
} else {
let values = {}
for (let i = 0; i < this.attributesByGroup.length; i++) {
const data = this.$refs[`createInstanceFormByGroup_${this.attributesByGroup[i].id}`][0].getData()
if (data === 'error') {
return
}
values = { ...values, ...data }
}
Object.keys(values).forEach((k) => {
const _tempFind = this.attributeList.find((item) => item.name === k)
if (_tempFind.value_type === '3' && values[k]) {
values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
}
if (_tempFind.value_type === '4' && values[k]) {
values[k] = values[k].format('YYYY-MM-DD')
}
if (_tempFind.value_type === '6') {
values[k] = values[k] ? JSON.parse(values[k]) : undefined
}
})
values.ci_type = _this.typeId
Object.keys(this.parentsForm).forEach((type) => {
if (this.parentsForm[type].value) {
values[`$${type}.${this.parentsForm[type].attr}`] = this.parentsForm[type].value
}
})
addCI(values).then((res) => {
_this.$message.success('新增成功!')
_this.visible = false
_this.$emit('reload', { ci_id: res.ci_id })
})
}
},
handleClose() {
this.visible = false
},
handleOpen(visible, action) {
this.visible = visible
this.action = action
this.$nextTick(() => {
this.form.resetFields()
Promise.all([this.getCIType(), this.getAttributeList()]).then(() => {
this.batchUpdateLists = [{ name: this.attributeList[0].name }]
})
if (action === 'create') {
getCITypeParent(this.typeId).then((res) => {
this.parentsType = res.parents
const _parentsForm = {}
res.parents.forEach((item) => {
const _find = item.attributes.find((attr) => attr.id === item.unique_id)
_parentsForm[item.name] = { attr: _find.name, value: '' }
})
this.parentsForm = _parentsForm
})
}
})
},
getFieldType(name) {
const _find = this.attributeList.find((item) => item.name === name)
if (_find) {
if (_find.is_choice) {
if (_find.is_list) {
return 'select%%multiple'
}
return 'select'
} else if (_find.value_type === '0' || _find.value_type === '1') {
return 'input_number'
} else if (_find.value_type === '4' || _find.value_type === '3') {
return valueTypeMap[_find.value_type]
} else {
return 'input'
}
}
return 'input'
},
getSelectFieldOptions(name) {
const _find = this.attributeList.find((item) => item.name === name)
if (_find) {
return _find.choice_value
}
return []
},
handleAdd() {
this.batchUpdateLists.push({ name: undefined })
},
handleDelete(name) {
const _idx = this.batchUpdateLists.findIndex((item) => item.name === name)
if (_idx > -1) {
this.batchUpdateLists.splice(_idx, 1)
}
},
handleFocusInput(e, attr) {
console.log(attr)
const _tempFind = this.attributeList.find((item) => item.name === attr.name)
if (_tempFind.value_type === '6') {
this.editAttr = attr
e.srcElement.blur()
const jsonData = this.form.getFieldValue(attr.name)
this.$refs.jsonEditor.open(null, null, jsonData ? JSON.parse(jsonData) : {})
} else {
this.editAttr = null
}
},
jsonEditorOk(jsonData) {
this.form.setFieldsValue({ [this.editAttr.name]: JSON.stringify(jsonData) })
},
},
}
</script>
<style lang="less">
.create-instance-form {
.ant-form-item {
margin-bottom: 5px;
}
.ant-drawer-body {
overflow-y: auto;
max-height: calc(100vh - 110px);
}
}
</style>

View File

@@ -1,225 +1,215 @@
<template>
<CustomDrawer
:visible="visible"
:hasFooter="false"
@close="
() => {
visible = false
}
"
title="属性说明"
width="72%"
:bodyStyle="{ height: '100vh' }"
>
<vxe-toolbar>
<template #buttons>
<a-input
v-model="searchKey"
:style="{ display: 'inline-block', width: '244px' }"
class="ops-input ops-input-radius"
type="search"
placeholder="搜索 名称 | 别名"
@keyup="searchAttributes"
>
<a-icon type="search" slot="suffix" />
</a-input>
</template>
</vxe-toolbar>
<a-spin :spinning="loading">
<vxe-table
resizable
border
size="mini"
:height="windowHeight - 160"
:data="list"
:scroll-x="{ enabled: true, gt: 0 }"
show-overflow
show-header-overflow
align="center"
highlight-hover-row
class="ops-stripe-table"
>
<vxe-column
v-for="(column, index) in columns"
:field="column.field"
:title="column.title"
:min-width="column.width"
:align="column.align"
:key="column.field"
:fixed="index < 3 ? 'left' : ''"
:sortable="index < 3 ? true : false"
:title-help="column.help !== null ? { message: column.help } : null"
:filters="
index < 2
? null
: index === 2
? valueTypeFilters
: [
{ label: '', value: true },
{ label: '', value: false },
]
"
type="html"
>
<template #default="{ row }">
<span v-if="column.field !== 'name' && column.field !== 'alias' && column.field !== 'value_type'">
<a-icon :style="{ color: '#1fb51f' }" type="check" v-if="row[column.field]" />
</span>
<span v-else-if="column.field === 'value_type'" v-html="valueTypeMap[row.value_type]"> </span>
<span v-else v-html="row[column.field]"> </span>
</template>
</vxe-column>
</vxe-table>
</a-spin>
</CustomDrawer>
</template>
<script>
import XEUtils from 'xe-utils'
import { getCITypeAttributesByName } from '@/modules/cmdb/api/CITypeAttr'
import { valueTypeMap } from '@/modules/cmdb/utils/const'
export default {
name: 'MetadataDrawer',
data() {
const columns = [
{
field: 'name',
title: '',
width: 150,
align: 'left',
help: null,
},
{
field: 'alias',
title: '别名',
width: 150,
align: 'left',
help: null,
},
{
field: 'value_type',
title: '类型',
width: 100,
align: 'left',
help: null,
},
{
field: 'is_index',
title: '是否索引',
width: 110,
help: '加快检索, 可以全文搜索, 无需使用条件过滤\n\n json目前不支持建索引 \n\n文本字符长度超过190不能建索引',
},
{
field: 'default_show',
title: '默认显示',
width: 110,
help: '订阅CI默认显示在table里的属性',
},
{
field: 'is_unique',
title: '是否唯一',
width: 110,
help: null,
},
{
field: 'is_choice',
title: '是否选择',
width: 110,
help: '表现形式是下拉框, 值必须在预定义值里',
},
{
field: 'is_list',
title: '是否列表',
width: 110,
help: '多值, 比如内网IP',
},
{
field: 'is_sortable',
title: '可排序',
width: 100,
help: '仅针对前端',
},
{
field: 'is_computed',
title: '计算属性',
width: 110,
help: '模型的其他属性通过表达式的方式计算出来\n\n一个代码片段计算返回的值',
},
]
return {
columns,
visible: false,
list: [],
tableData: [],
loading: false,
valueTypeMap,
valueTypeFilters: [],
searchKey: '',
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
},
created: function() {
this.valueTypeFilters = Object.keys(this.valueTypeMap).map((key) => {
return { label: this.valueTypeMap[key], value: key }
})
},
methods: {
open(typeId) {
this.visible = true
this.typeId = typeId
this.getAttrs()
},
async getAttrs() {
this.loading = true
const { attributes = [] } = await getCITypeAttributesByName(this.typeId)
this.tableData = attributes.map((attr) => {
if (attr.is_password) {
attr.value_type = '7'
}
if (attr.is_link) {
attr.value_type = '8'
}
return attr
})
this.loading = false
this.searchAttributes()
},
searchAttributes() {
const filterName = XEUtils.toValueString(this.searchKey)
.trim()
.toLowerCase()
if (filterName) {
const filterRE = new RegExp(filterName, 'gi')
const searchProps = ['name', 'alias', 'value_type']
const rest = this.tableData.filter((item) =>
searchProps.some(
(key) =>
XEUtils.toValueString(item[key])
.toLowerCase()
.indexOf(filterName) > -1
)
)
this.list = rest.map((row) => {
const item = Object.assign({}, row)
searchProps.forEach((key) => {
item[key] = XEUtils.toValueString(item[key]).replace(
filterRE,
(match) => `<span style='background: yellow'>${match}</span>`
)
})
return item
})
} else {
this.list = this.tableData
}
},
},
}
</script>
<style></style>
<template>
<CustomDrawer
:visible="visible"
:hasFooter="false"
@close="
() => {
visible = false
}
"
title="属性说明"
width="72%"
:bodyStyle="{ height: '100vh' }"
>
<vxe-toolbar>
<template #buttons>
<a-input
v-model="searchKey"
:style="{ display: 'inline-block', width: '244px' }"
class="ops-input ops-input-radius"
type="search"
placeholder="搜索 名称 | 别名"
@keyup="searchAttributes"
>
<a-icon type="search" slot="suffix" />
</a-input>
</template>
</vxe-toolbar>
<a-spin :spinning="loading">
<vxe-table
resizable
border
size="mini"
:height="windowHeight - 160"
:data="list"
:scroll-x="{ enabled: true, gt: 0 }"
show-overflow
show-header-overflow
align="center"
highlight-hover-row
class="ops-stripe-table"
>
<vxe-column
v-for="(column, index) in columns"
:field="column.field"
:title="column.title"
:min-width="column.width"
:align="column.align"
:key="column.field"
:fixed="index < 3 ? 'left' : ''"
:sortable="index < 3 ? true : false"
:title-help="column.help !== null ? { message: column.help } : null"
:filters="
index < 2 ? null: index === 2
? valueTypeFilters: [{ label: '', value: true }, { label: '', value: false },]"
type="html"
>
<template #default="{ row }">
<span v-if="column.field !== 'name' && column.field !== 'alias' && column.field !== 'value_type'">
<a-icon :style="{ color: '#1fb51f' }" type="check" v-if="row[column.field]" />
</span>
<span v-else-if="column.field === 'value_type'" v-html="valueTypeMap[row.value_type]"> </span>
<span v-else v-html="row[column.field]"> </span>
</template>
</vxe-column>
</vxe-table>
</a-spin>
</CustomDrawer>
</template>
<script>
import XEUtils from 'xe-utils'
import { getCITypeAttributesByName } from '@/modules/cmdb/api/CITypeAttr'
import { valueTypeMap } from '@/modules/cmdb/utils/const'
export default {
name: 'MetadataDrawer',
data() {
const columns = [
{
field: 'name',
title: '名称',
width: 150,
align: 'left',
help: null,
},
{
field: 'alias',
title: '',
width: 150,
align: 'left',
help: null,
},
{
field: 'value_type',
title: '类型',
width: 100,
align: 'left',
help: null,
},
{
field: 'is_index',
title: '是否索引',
width: 110,
help: '加快检索, 可以全文搜索, 无需使用条件过滤\n\n json目前不支持建索引 \n\n文本字符长度超过190不能建索引',
},
{
field: 'default_show',
title: '默认显示',
width: 110,
help: '订阅CI默认显示在table里的属性',
},
{
field: 'is_unique',
title: '是否唯一',
width: 110,
help: null,
},
{
field: 'is_choice',
title: '是否选择',
width: 110,
help: '表现形式是下拉框, 值必须在预定义值里',
},
{
field: 'is_list',
title: '是否列表',
width: 110,
help: '多值, 比如内网IP',
},
{
field: 'is_sortable',
title: '可排序',
width: 100,
help: '仅针对前端',
},
{
field: 'is_password',
title: '是否密码',
width: 100,
help: null,
},
{
field: 'is_link',
title: '是否链接',
width: 110,
help: null,
},
{
field: 'is_computed',
title: '计算属性',
width: 110,
help: '模型的其他属性通过表达式的方式计算出来\n\n一个代码片段计算返回的值',
},
]
return {
columns,
visible: false,
list: [],
tableData: [],
loading: false,
valueTypeMap,
valueTypeFilters: [],
searchKey: '',
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
},
created: function () {
this.valueTypeFilters = Object.keys(this.valueTypeMap).map((key) => {
return { label: this.valueTypeMap[key], value: key }
})
},
methods: {
open(typeId) {
this.visible = true
this.typeId = typeId
this.getAttrs()
},
async getAttrs() {
this.loading = true
const { attributes = [] } = await getCITypeAttributesByName(this.typeId)
this.tableData = attributes
this.loading = false
this.searchAttributes()
},
searchAttributes() {
const filterName = XEUtils.toValueString(this.searchKey).trim().toLowerCase()
if (filterName) {
const filterRE = new RegExp(filterName, 'gi')
const searchProps = ['name', 'alias', 'value_type']
const rest = this.tableData.filter((item) =>
searchProps.some((key) => XEUtils.toValueString(item[key]).toLowerCase().indexOf(filterName) > -1)
)
this.list = rest.map((row) => {
const item = Object.assign({}, row)
searchProps.forEach((key) => {
item[key] = XEUtils.toValueString(item[key]).replace(
filterRE,
(match) => `<span style='background: yellow'>${match}</span>`
)
})
return item
})
} else {
this.list = this.tableData
}
},
},
}
</script>
<style></style>

View File

@@ -1,299 +1,234 @@
<template>
<span :id="`ci-detail-attr-${attr.name}`">
<span v-if="!isEdit || attr.value_type === '6'">
<PasswordField
:style="{ display: 'inline-block' }"
v-if="attr.is_password && ci[attr.name]"
:ci_id="ci._id"
:attr_id="attr.id"
></PasswordField>
<template v-else-if="attr.value_type === '6'">{{ JSON.stringify(ci[attr.name] || {}) }}</template>
<template v-else-if="attr.is_choice">
<template v-if="attr.is_list">
<span
v-for="value in ci[attr.name]"
:key="value"
:style="{
borderRadius: '4px',
padding: '1px 5px',
margin: '2px',
...getChoiceValueStyle(attr, value),
display: 'inline-flex',
alignItems: 'center',
}"
>
<img
v-if="getChoiceValueIcon(attr, value).id && getChoiceValueIcon(attr, value).url"
:src="`/api/common-setting/v1/file/${getChoiceValueIcon(attr, value).url}`"
:style="{ maxHeight: '13px', maxWidth: '13px', marginRight: '5px' }"
/>
<ops-icon
v-else
:style="{ color: getChoiceValueIcon(attr, value).color, marginRight: '5px' }"
:type="getChoiceValueIcon(attr, value).name"
/>
{{ value }}</span
>
</template>
<span
v-else
:style="{
borderRadius: '4px',
padding: '1px 5px',
margin: '2px 0',
...getChoiceValueStyle(attr, ci[attr.name]),
display: 'inline-flex',
alignItems: 'center',
}"
>
<img
v-if="getChoiceValueIcon(attr, ci[attr.name]).id && getChoiceValueIcon(attr, ci[attr.name]).url"
:src="`/api/common-setting/v1/file/${getChoiceValueIcon(attr, ci[attr.name]).url}`"
:style="{ maxHeight: '13px', maxWidth: '13px', marginRight: '5px' }"
/>
<ops-icon
v-else
:style="{ color: getChoiceValueIcon(attr, ci[attr.name]).color, marginRight: '5px' }"
:type="getChoiceValueIcon(attr, ci[attr.name]).name"
/>
{{ ci[attr.name] }}
</span>
</template>
<template v-else>{{ getName(ci[attr.name]) }}</template>
</span>
<template v-else>
<a-form :form="form">
<a-form-item label="" :colon="false">
<a-select
:style="{ width: '100%' }"
v-decorator="[
attr.name,
{
rules: [{ required: attr.is_required }],
},
]"
placeholder="请选择"
v-if="attr.is_choice"
:mode="attr.is_list ? 'multiple' : 'default'"
:multiple="attr.is_list"
showSearch
allowClear
size="small"
:getPopupContainer="(trigger) => trigger.parentElement"
>
<a-select-option
:value="choice[0]"
:key="'New_' + attr.name + choice_idx"
v-for="(choice, choice_idx) in attr.choice_value"
>
<span :style="{ ...(choice[1] ? choice[1].style : {}), display: 'inline-flex', alignItems: 'center' }">
<template v-if="choice[1] && choice[1].icon && choice[1].icon.name">
<img
v-if="choice[1].icon.id && choice[1].icon.url"
:src="`/api/common-setting/v1/file/${choice[1].icon.url}`"
:style="{ maxHeight: '13px', maxWidth: '13px', marginRight: '5px' }"
/>
<ops-icon
v-else
:style="{ color: choice[1].icon.color, marginRight: '5px' }"
:type="choice[1].icon.name"
/>
</template>
{{ choice[0] }}
</span>
</a-select-option>
</a-select>
<a-input-number
size="small"
v-decorator="[
attr.name,
{
rules: [{ required: attr.is_required }],
},
]"
style="width: 100%"
v-else-if="attr.value_type === '0' || attr.value_type === '1'"
/>
<a-date-picker
size="small"
v-decorator="[
attr.name,
{
rules: [{ required: attr.is_required }],
},
]"
style="width: 100%"
:format="attr.value_type === '4' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
:valueFormat="attr.value_type === '4' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
v-else-if="attr.value_type === '4' || attr.value_type === '3'"
:showTime="attr.value_type === '4' ? false : { format: 'HH:mm:ss' }"
/>
<!-- <a-input
size="small"
@focus="(e) => handleFocusInput(e, attr)"
v-decorator="[
attr.name,
{
validateTrigger: ['submit'],
rules: [{ required: attr.is_required }],
},
]"
style="width: 100%"
v-else-if="attr.value_type === '6'"
/> -->
<a-input
size="small"
v-decorator="[
attr.name,
{
validateTrigger: ['submit'],
rules: [{ required: attr.is_required }],
},
]"
style="width: 100%"
v-else
/>
</a-form-item>
</a-form>
</template>
<a v-if="!isEdit && !attr.is_computed" @click="handleEdit" :style="{ opacity: 0 }"><a-icon type="edit"/></a>
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
</span>
</template>
<script>
import _ from 'lodash'
import moment from 'moment'
import { updateCI } from '@/modules/cmdb/api/ci'
import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue'
import PasswordField from '../../../components/passwordField/index.vue'
import { getAttrPassword } from '../../../api/CITypeAttr'
export default {
name: 'CiDetailAttrContent',
components: { JsonEditor, PasswordField },
props: {
ci: {
type: Object,
default: () => {},
},
attr: {
type: Object,
default: () => {},
},
},
data() {
return {
isEdit: false,
form: this.$form.createForm(this, this.attr.name),
}
},
mounted() {
document.addEventListener('click', this.eventListener)
},
beforeDestroy() {
document.removeEventListener('click', this.eventListener)
},
methods: {
moment,
eventListener(e) {
const datePickerContainer = document.getElementsByClassName('ant-calendar-picker-container')
if (this.isEdit && !datePickerContainer.length) {
const dom = document.getElementById(`ci-detail-attr-${this.attr.name}`)
e.stopPropagation()
e.preventDefault()
if (dom) {
const isSelf = dom.contains(e.target)
if (!isSelf) {
this.handleCloseEdit()
}
}
}
},
handleEdit(e) {
e.stopPropagation()
e.preventDefault()
if (this.attr.value_type === '6') {
const jsonData = this.ci[this.attr.name]
this.$refs.jsonEditor.open(null, null, jsonData || {})
return
}
this.isEdit = true
this.$nextTick(async () => {
if (this.attr.is_list && !this.attr.is_choice) {
this.form.setFieldsValue({
[`${this.attr.name}`]: this.ci[this.attr.name].join(',') || null,
})
return
}
if (this.attr.is_password) {
await getAttrPassword(this.ci._id, this.attr.id).then((res) => {
this.form.setFieldsValue({
[`${this.attr.name}`]: res.value ?? null,
})
})
return
}
this.form.setFieldsValue({
[`${this.attr.name}`]: this.ci[this.attr.name] ?? null,
})
})
},
async handleCloseEdit() {
const newData = this.form.getFieldValue(this.attr.name)
if (!_.isEqual(this.ci[this.attr.name], newData)) {
await updateCI(this.ci._id, { [`${this.attr.name}`]: newData })
.then(() => {
this.$message.success('更新成功!')
this.$emit('updateCIByself', { [`${this.attr.name}`]: newData }, this.attr.name)
})
.catch(() => {
this.$emit('refresh', this.attr.name)
})
}
this.isEdit = false
},
// handleFocusInput(e, attr) {
// console.log('focus')
// if (this.attr.value_type === '6') {
// e.preventDefault()
// e.stopPropagation()
// // e.srcElement.blur()
// const jsonData = this.form.getFieldValue(attr.name)
// this.$refs.jsonEditor.open(null, null, jsonData ? JSON.parse(jsonData) : {})
// }
// },
jsonEditorOk(jsonData) {
if (!_.isEqual(this.ci[this.attr.name], jsonData)) {
updateCI(this.ci._id, { [`${this.attr.name}`]: jsonData })
.then(() => {
this.$message.success('更新成功!')
this.$emit('updateCIByself', { [`${this.attr.name}`]: jsonData }, this.attr.name)
})
.catch(() => {
this.$emit('refresh', this.attr.name)
})
}
},
getChoiceValueStyle(col, colValue) {
const _find = col.choice_value.find((item) => String(item[0]) === String(colValue))
if (_find) {
return _find[1]?.style || {}
}
return {}
},
getChoiceValueIcon(col, colValue) {
const _find = col.choice_value.find((item) => String(item[0]) === String(colValue))
if (_find) {
return _find[1]?.icon || {}
}
return {}
},
getName(name) {
return name ?? ''
},
},
}
</script>
<style></style>
<template>
<span :id="`ci-detail-attr-${attr.name}`">
<span v-if="!isEdit || attr.value_type === '6'">
<template v-if="attr.value_type === '6'">{{ JSON.stringify(ci[attr.name] || {}) }}</template>
<template v-else-if="attr.is_choice">
<template v-if="attr.is_list">
<span
v-for="value in ci[attr.name]"
:key="value"
:style="{
borderRadius: '4px',
padding: '1px 5px',
margin: '2px',
...getChoiceValueStyle(attr, value),
}"
>
<ops-icon
:style="{ color: getChoiceValueIcon(attr, value).color }"
:type="getChoiceValueIcon(attr, value).name"
/>
{{ value }}</span
>
</template>
<span
v-else
:style="{
borderRadius: '4px',
padding: '1px 5px',
margin: '2px 0',
...getChoiceValueStyle(attr, ci[attr.name]),
}"
>
<ops-icon
:style="{ color: getChoiceValueIcon(attr, ci[attr.name]).color }"
:type="getChoiceValueIcon(attr, ci[attr.name]).name"
/>
{{ ci[attr.name] }}
</span>
</template>
<template v-else>{{ getName(ci[attr.name]) }}</template>
</span>
<template v-else>
<a-form :form="form">
<a-form-item label="" :colon="false">
<a-select
:style="{ width: '100%' }"
v-decorator="[
attr.name,
{
rules: [{ required: attr.is_required }],
},
]"
placeholder="请选择"
v-if="attr.is_choice"
:mode="attr.is_list ? 'multiple' : 'default'"
:multiple="attr.is_list"
showSearch
allowClear
size="small"
:getPopupContainer="(trigger) => trigger.parentElement"
>
<a-select-option
:value="choice[0]"
:key="'New_' + attr.name + choice_idx"
v-for="(choice, choice_idx) in attr.choice_value"
>
<span :style="choice[1] ? choice[1].style || {} : {}">
<ops-icon
:style="{ color: choice[1].icon.color }"
v-if="choice[1] && choice[1].icon && choice[1].icon.name"
:type="choice[1].icon.name"
/>
{{ choice[0] }}
</span>
</a-select-option>
</a-select>
<a-input-number
size="small"
v-decorator="[
attr.name,
{
rules: [{ required: attr.is_required }],
},
]"
style="width: 100%"
v-else-if="attr.value_type === '0' || attr.value_type === '1'"
/>
<a-date-picker
size="small"
v-decorator="[
attr.name,
{
rules: [{ required: attr.is_required }],
},
]"
style="width: 100%"
:format="attr.value_type === '4' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
:valueFormat="attr.value_type === '4' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
v-else-if="attr.value_type === '4' || attr.value_type === '3'"
:showTime="attr.value_type === '4' ? false : { format: 'HH:mm:ss' }"
/>
<a-input
size="small"
v-decorator="[
attr.name,
{
validateTrigger: ['submit'],
rules: [{ required: attr.is_required }],
},
]"
style="width: 100%"
v-else
/>
</a-form-item>
</a-form>
</template>
<a v-if="!isEdit && !attr.is_computed" @click="handleEdit" :style="{ opacity: 0 }"><a-icon type="edit"/></a>
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
</span>
</template>
<script>
import _ from 'lodash'
import moment from 'moment'
import { updateCI } from '@/modules/cmdb/api/ci'
import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue'
export default {
name: 'CiDetailAttrContent',
components: { JsonEditor },
props: {
ci: {
type: Object,
default: () => {},
},
attr: {
type: Object,
default: () => {},
},
},
data() {
return {
isEdit: false,
form: this.$form.createForm(this, this.attr.name),
}
},
mounted() {
document.addEventListener('click', this.eventListener)
},
beforeDestroy() {
document.removeEventListener('click', this.eventListener)
},
methods: {
moment,
eventListener(e) {
const datePickerContainer = document.getElementsByClassName('ant-calendar-picker-container')
if (this.isEdit && !datePickerContainer.length) {
const dom = document.getElementById(`ci-detail-attr-${this.attr.name}`)
e.stopPropagation()
e.preventDefault()
if (dom) {
const isSelf = dom.contains(e.target)
if (!isSelf) {
this.handleCloseEdit()
}
}
}
},
handleEdit(e) {
e.stopPropagation()
e.preventDefault()
if (this.attr.value_type === '6') {
const jsonData = this.ci[this.attr.name]
this.$refs.jsonEditor.open(null, null, jsonData || {})
return
}
this.isEdit = true
this.$nextTick(() => {
if (this.attr.is_list && !this.attr.is_choice) {
this.form.setFieldsValue({
[`${this.attr.name}`]: this.ci[this.attr.name].join(',') || null,
})
return
}
this.form.setFieldsValue({
[`${this.attr.name}`]: this.ci[this.attr.name] ?? null,
})
})
},
async handleCloseEdit() {
const newData = this.form.getFieldValue(this.attr.name)
if (!_.isEqual(this.ci[this.attr.name], newData)) {
await updateCI(this.ci._id, { [`${this.attr.name}`]: newData })
.then(() => {
this.$message.success('更新成功!')
})
.finally(() => {
this.$emit('refresh', this.attr.name)
})
}
this.isEdit = false
},
jsonEditorOk(jsonData) {
if (!_.isEqual(this.ci[this.attr.name], jsonData)) {
updateCI(this.ci._id, { [`${this.attr.name}`]: jsonData })
.then(() => {
this.$message.success('更新成功!')
})
.finally(() => {
this.$emit('refresh', this.attr.name)
})
}
},
getChoiceValueStyle(col, colValue) {
const _find = col.choice_value.find((item) => String(item[0]) === String(colValue))
if (_find) {
return _find[1]?.style || {}
}
return {}
},
getChoiceValueIcon(col, colValue) {
const _find = col.choice_value.find((item) => String(item[0]) === String(colValue))
if (_find) {
return _find[1]?.icon || {}
}
return {}
},
getName(name) {
return name ?? ''
},
},
}
</script>
<style></style>

View File

@@ -1,274 +1,280 @@
<template>
<div class="attribute-card">
<div class="attribute-card-content">
<div class="attribute-card-value-type-icon handle" :style="{ ...getPropertyStyle(property) }">
<ValueTypeIcon :attr="property" />
</div>
<div :class="{ 'attribute-card-content-inner': true, 'attribute-card-name-required': property.is_required }">
<div :class="{ 'attribute-card-name': true, 'attribute-card-name-default-show': property.default_show }">
{{ property.alias || property.name }}
</div>
<div v-if="property.is_password" class="attribute-card_value-type">密码</div>
<div v-else-if="property.is_link" class="attribute-card_value-type">链接</div>
<div v-else class="attribute-card_value-type">{{ valueTypeMap[property.value_type] }}</div>
</div>
<div
class="attribute-card-trigger"
v-if="(property.value_type === '3' || property.value_type === '4') && !isStore"
>
<a @click="openTrigger"><ops-icon type="ops-trigger"/></a>
</div>
</div>
<div class="attribute-card-footer">
<a-popover
trigger="click"
:arrowPointAtCenter="true"
placement="bottom"
overlayClassName="attribute-card-footer-popover"
>
<div slot="content">
<h3 :style="{ textAlign: 'center', paddingTop: '0.5em' }">
<span>{{ property.alias }}({{ property.name }})</span>
</h3>
<a-descriptions layout="horizontal" bordered size="small" :column="2">
<a-descriptions-item v-for="item in propertyList" :key="item.property" :label="item.label">
<components
:is="`ops_${item.property}`"
v-if="property[item.property]"
:style="{ width: '1em', height: '1em' }"
/>
<ops-icon v-else :type="`ops-${item.property}-disabled`" />
</a-descriptions-item>
<a-descriptions-item label></a-descriptions-item>
</a-descriptions>
</div>
<a-space :style="{ cursor: 'pointer' }">
<components
v-for="item in propertyList.filter((p) => property[p.property])"
:key="item.property"
:is="`ops_${item.property}`"
/>
</a-space>
</a-popover>
<a-space class="attribute-card-operation">
<a v-if="!isStore"><a-icon type="edit" @click="handleEdit"/></a>
<a-tooltip title="所有CI触发计算">
<a v-if="!isStore && property.is_computed"><a-icon type="redo" @click="handleCalcComputed"/></a>
</a-tooltip>
<a style="color:red;"><a-icon type="delete" @click="handleDelete"/></a>
</a-space>
</div>
<TriggerForm ref="triggerForm" :CITypeId="CITypeId" />
</div>
</template>
<script>
import { deleteCITypeAttributesById, deleteAttributesById, calcComputedAttribute } from '@/modules/cmdb/api/CITypeAttr'
import ValueTypeIcon from '@/components/CMDBValueTypeMapIcon'
import {
ops_default_show,
ops_is_choice,
ops_is_index,
ops_is_link,
ops_is_password,
ops_is_sortable,
ops_is_unique,
} from '@/core/icons'
import { valueTypeMap } from '../../utils/const'
import { getPropertyStyle } from '../../utils/helper'
import TriggerForm from './triggerForm.vue'
export default {
name: 'AttributeCard',
components: {
ValueTypeIcon,
TriggerForm,
ops_default_show,
ops_is_choice,
ops_is_index,
ops_is_link,
ops_is_password,
ops_is_sortable,
ops_is_unique,
},
props: {
property: {
type: Object,
default: () => {},
},
CITypeId: {
type: Number,
default: null,
},
isStore: {
type: Boolean,
default: false,
},
},
data() {
const propertyList = [
{
label: '是否唯一',
property: 'is_unique',
},
{
label: '是否选择',
property: 'is_choice',
},
{
label: '默认显示',
property: 'default_show',
},
{
label: '可排序',
property: 'is_sortable',
},
{
label: '是否索引',
property: 'is_index',
},
]
return {
valueTypeMap,
propertyList,
}
},
methods: {
getPropertyStyle,
handleEdit() {
this.$emit('edit')
},
handleDelete() {
const that = this
this.$confirm({
title: '警告',
content: `确认删除 ${that.property.alias || that.property.name}`,
onOk() {
if (that.isStore) {
deleteAttributesById(that.property.id).then(() => {
that.$message.success('删除成功!')
that.$emit('ok')
})
} else {
deleteCITypeAttributesById(that.CITypeId, { attr_id: [that.property.id] }).then(() => {
that.$message.success('删除成功!')
that.$emit('ok')
})
}
},
onCancel() {},
})
},
openTrigger() {
this.$refs.triggerForm.open(this.property)
},
handleCalcComputed() {
const that = this
this.$confirm({
title: '警告',
content: `确认触发所有CI的计算`,
onOk() {
calcComputedAttribute(that.property.id).then(() => {
that.$message.success('触发成功!')
})
},
})
},
},
}
</script>
<style lang="less" scoped>
.attribute-card {
width: 182px;
height: 80px;
background: #f8faff;
border-radius: 5px;
position: relative;
margin-bottom: 16px;
transition: all 0.3s;
&:hover {
box-shadow: 0 4px 12px #4e5ea066;
.attribute-card-operation {
visibility: visible !important;
}
}
.attribute-card-content {
height: 50px;
display: inline-flex;
align-items: center;
padding: 8px;
width: 100%;
.attribute-card-value-type-icon {
width: 32px;
height: 32px;
font-size: 12px;
cursor: move;
background: #ffffff !important;
box-shadow: 0px 1px 2px rgba(47, 84, 235, 0.2);
border-radius: 2px;
text-align: center;
line-height: 32px;
}
.attribute-card-content-inner {
padding-left: 12px;
font-weight: 400;
font-size: 12px;
width: 120px;
position: relative;
.attribute-card-name {
width: 100%;
color: rgba(0, 0, 0, 0.8);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.attribute-card-name-default-show {
color: #2f54eb;
}
.attribute-card_value-type {
font-size: 10px;
color: rgba(0, 0, 0, 0.35);
}
}
.attribute-card-name-required::before {
content: '*';
width: 5px;
color: red;
position: absolute;
left: 3px;
}
.attribute-card-trigger {
position: absolute;
right: 8px;
top: 8px;
}
}
.attribute-card-footer {
width: 182px;
height: 30px;
padding: 0 8px;
position: absolute;
bottom: 0;
left: 0;
background: linear-gradient(180deg, #96abd6 0%, #ecf2ff 0.01%, #ffffff 143.33%);
border-radius: 0px 0px 5px 5px;
display: inline-flex;
align-items: center;
justify-content: space-between;
.attribute-card-operation {
visibility: hidden;
}
}
}
</style>
<style lang="less">
.attribute-card-footer-popover {
.ant-popover-inner-content {
padding: 0;
}
.ant-descriptions-bordered .ant-descriptions-item-label {
background-color: #f8faff;
}
}
</style>
<template>
<div class="attribute-card">
<div class="attribute-card-content">
<div class="attribute-card-value-type-icon handle" :style="{ ...getPropertyStyle(property) }">
<ValueTypeIcon :attr="property" />
</div>
<div :class="{ 'attribute-card-content-inner': true, 'attribute-card-name-required': property.is_required }">
<div :class="{ 'attribute-card-name': true, 'attribute-card-name-default-show': property.default_show }">
{{ property.alias || property.name }}
</div>
<div class="attribute-card_value-type">{{ valueTypeMap[property.value_type] }}</div>
</div>
<div
class="attribute-card-trigger"
v-if="(property.value_type === '3' || property.value_type === '4') && !isStore"
>
<a @click="openTrigger"><ops-icon type="ops-trigger"/></a>
</div>
</div>
<div class="attribute-card-footer">
<a-popover
trigger="click"
:arrowPointAtCenter="true"
placement="bottom"
overlayClassName="attribute-card-footer-popover"
>
<div slot="content">
<h3 :style="{ textAlign: 'center', paddingTop: '0.5em' }">
<span>{{ property.alias }}({{ property.name }})</span>
</h3>
<a-descriptions layout="horizontal" bordered size="small" :column="2">
<a-descriptions-item v-for="item in propertyList" :key="item.property" :label="item.label">
<components
:is="`ops_${item.property}`"
v-if="property[item.property]"
:style="{ width: '1em', height: '1em' }"
/>
<ops-icon v-else :type="`ops-${item.property}-disabled`" />
</a-descriptions-item>
<a-descriptions-item label></a-descriptions-item>
</a-descriptions>
</div>
<a-space :style="{ cursor: 'pointer' }">
<components
v-for="item in propertyList.filter((p) => property[p.property])"
:key="item.property"
:is="`ops_${item.property}`"
/>
</a-space>
</a-popover>
<a-space class="attribute-card-operation">
<a v-if="!isStore"><a-icon type="edit" @click="handleEdit"/></a>
<a-tooltip title="所有CI触发计算">
<a v-if="!isStore && property.is_computed"><a-icon type="redo" @click="handleCalcComputed"/></a>
</a-tooltip>
<a style="color:red;"><a-icon type="delete" @click="handleDelete"/></a>
</a-space>
</div>
<TriggerForm ref="triggerForm" :CITypeId="CITypeId" />
</div>
</template>
<script>
import { deleteCITypeAttributesById, deleteAttributesById, calcComputedAttribute } from '@/modules/cmdb/api/CITypeAttr'
import ValueTypeIcon from '@/components/CMDBValueTypeMapIcon'
import {
ops_default_show,
ops_is_choice,
ops_is_index,
ops_is_link,
ops_is_password,
ops_is_sortable,
ops_is_unique,
} from '@/core/icons'
import { valueTypeMap } from '../../utils/const'
import { getPropertyStyle } from '../../utils/helper'
import TriggerForm from './triggerForm.vue'
export default {
name: 'AttributeCard',
components: {
ValueTypeIcon,
TriggerForm,
ops_default_show,
ops_is_choice,
ops_is_index,
ops_is_link,
ops_is_password,
ops_is_sortable,
ops_is_unique,
},
props: {
property: {
type: Object,
default: () => {},
},
CITypeId: {
type: Number,
default: null,
},
isStore: {
type: Boolean,
default: false,
},
},
data() {
const propertyList = [
{
label: '是否唯一',
property: 'is_unique',
},
{
label: '是否选择',
property: 'is_choice',
},
{
label: '默认显示',
property: 'default_show',
},
{
label: '可排序',
property: 'is_sortable',
},
{
label: '是否索引',
property: 'is_index',
},
{
label: '是否密码',
property: 'is_password',
},
{
label: '是否链接',
property: 'is_link',
},
]
return {
valueTypeMap,
propertyList,
}
},
methods: {
getPropertyStyle,
handleEdit() {
this.$emit('edit')
},
handleDelete() {
const that = this
this.$confirm({
title: '警告',
content: `确认删除 ${that.property.alias || that.property.name}`,
onOk() {
if (that.isStore) {
deleteAttributesById(that.property.id).then(() => {
that.$message.success('删除成功!')
that.$emit('ok')
})
} else {
deleteCITypeAttributesById(that.CITypeId, { attr_id: [that.property.id] }).then(() => {
that.$message.success('删除成功!')
that.$emit('ok')
})
}
},
onCancel() {},
})
},
openTrigger() {
this.$refs.triggerForm.open(this.property)
},
handleCalcComputed() {
const that = this
this.$confirm({
title: '警告',
content: `确认触发所有CI的计算`,
onOk() {
calcComputedAttribute(that.property.id).then(() => {
that.$message.success('触发成功!')
})
},
})
},
},
}
</script>
<style lang="less" scoped>
.attribute-card {
width: 182px;
height: 80px;
background: #f8faff;
border-radius: 5px;
position: relative;
margin-bottom: 16px;
transition: all 0.3s;
&:hover {
box-shadow: 0 4px 12px #4e5ea066;
.attribute-card-operation {
visibility: visible !important;
}
}
.attribute-card-content {
height: 50px;
display: inline-flex;
align-items: center;
padding: 8px;
width: 100%;
.attribute-card-value-type-icon {
width: 32px;
height: 32px;
font-size: 12px;
cursor: move;
background: #ffffff !important;
box-shadow: 0px 1px 2px rgba(47, 84, 235, 0.2);
border-radius: 2px;
text-align: center;
line-height: 32px;
}
.attribute-card-content-inner {
padding-left: 12px;
font-weight: 400;
font-size: 12px;
width: 120px;
position: relative;
.attribute-card-name {
width: 100%;
color: rgba(0, 0, 0, 0.8);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.attribute-card-name-default-show {
color: #2f54eb;
}
.attribute-card_value-type {
font-size: 10px;
color: rgba(0, 0, 0, 0.35);
}
}
.attribute-card-name-required::before {
content: '*';
width: 5px;
color: red;
position: absolute;
left: 3px;
}
.attribute-card-trigger {
position: absolute;
right: 8px;
top: 8px;
}
}
.attribute-card-footer {
width: 182px;
height: 30px;
padding: 0 8px;
position: absolute;
bottom: 0;
left: 0;
background: linear-gradient(180deg, #96abd6 0%, #ecf2ff 0.01%, #ffffff 143.33%);
border-radius: 0px 0px 5px 5px;
display: inline-flex;
align-items: center;
justify-content: space-between;
.attribute-card-operation {
visibility: hidden;
}
}
}
</style>
<style lang="less">
.attribute-card-footer-popover {
.ant-popover-inner-content {
padding: 0;
}
.ant-descriptions-bordered .ant-descriptions-item-label {
background-color: #f8faff;
}
}
</style>

View File

@@ -84,12 +84,7 @@
</a-input-number>
<a-input
style="width: 100%"
v-else-if="
currentValueType === '2' ||
currentValueType === '5' ||
currentValueType === '7' ||
currentValueType === '8'
"
v-else-if="currentValueType === '2' || currentValueType === '5'"
v-decorator="['default_value', { rules: [{ required: false }] }]"
>
</a-input>
@@ -153,13 +148,13 @@
label="必须"
>
<a-switch
@change="(checked) => onChange(checked, 'is_required')"
@change="onChange"
name="is_required"
v-decorator="['is_required', { rules: [], valuePropName: 'checked' }]"
/>
</a-form-item>
</a-col>
<a-col :span="6" v-if="currentValueType !== '6' && currentValueType !== '7'">
<a-col :span="6" v-if="currentValueType !== '6'">
<a-form-item :label-col="{ span: 8 }" :wrapper-col="horizontalFormItemLayout.wrapperCol" label="唯一">
<a-switch
:disabled="isShowComputedArea"
@@ -233,7 +228,7 @@
/>
</a-form-item>
</a-col>
<a-col :span="6" v-if="currentValueType !== '6' && currentValueType !== '7'">
<a-col :span="6" v-if="currentValueType !== '6'">
<a-form-item
:label-col="currentValueType === '2' ? horizontalFormItemLayout.labelCol : { span: 8 }"
:wrapper-col="horizontalFormItemLayout.wrapperCol"
@@ -241,14 +236,14 @@
>
<a-switch
:disabled="isShowComputedArea"
@change="(checked) => onChange(checked, 'is_sortable')"
@change="onChange"
name="is_sortable"
v-decorator="['is_sortable', { rules: [], valuePropName: 'checked' }]"
/>
</a-form-item>
</a-col>
<a-col :span="6" v-if="currentValueType !== '6' && currentValueType !== '7'">
<a-col :span="6" v-if="currentValueType !== '6'">
<a-form-item
:label-col="currentValueType === '2' ? { span: 8 } : horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol"
@@ -280,6 +275,31 @@
/>
</a-form-item>
</a-col>
<a-col :span="6" v-if="currentValueType === '2'">
<a-form-item
:label-col="horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol"
label="密码"
>
<a-switch
:disabled="isShowComputedArea"
@change="(checked) => onChange(checked, 'is_password')"
name="is_password"
v-decorator="['is_password', { rules: [], valuePropName: 'checked' }]"
/>
</a-form-item>
</a-col>
<a-col :span="6" v-if="currentValueType === '2'">
<a-form-item :label-col="{ span: 8 }" :wrapper-col="horizontalFormItemLayout.wrapperCol" label="链接">
<a-switch
:disabled="isShowComputedArea"
@change="(checked) => onChange(checked, 'is_link')"
name="is_link"
v-decorator="['is_link', { rules: [], valuePropName: 'checked' }]"
/>
</a-form-item>
</a-col>
<a-divider style="font-size:14px;margin-top:6px;">高级设置</a-divider>
<a-row>
<a-col :span="24">
@@ -287,17 +307,12 @@
<FontArea ref="fontArea" />
</a-form-item>
</a-col>
<a-col :span="24" v-if="!['6', '7'].includes(currentValueType)">
<a-col :span="24" v-if="currentValueType !== '6'">
<a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }" label="预定义值">
<PreValueArea
v-if="drawerVisible"
:canDefineScript="canDefineScript"
ref="preValueArea"
:disabled="isShowComputedArea"
/>
<PreValueArea v-if="drawerVisible" ref="preValueArea" :disabled="isShowComputedArea" />
</a-form-item>
</a-col>
<a-col :span="24" v-if="!['6', '7'].includes(currentValueType)">
<a-col :span="24" v-if="currentValueType !== '6'">
<a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
<template slot="label">
<span
@@ -325,7 +340,7 @@
<a-switch
:disabled="!canDefineComputed"
@change="(checked) => onChange(checked, 'is_computed')"
name="is_computed"
name="is_password"
v-decorator="['is_computed', { rules: [], valuePropName: 'checked' }]"
/>
<ComputedArea
@@ -351,7 +366,6 @@
</template>
<script>
import _ from 'lodash'
import moment from 'moment'
import vueJsonEditor from 'vue-json-editor'
import {
@@ -420,9 +434,6 @@ export default {
wrapperCol: { span: 4 },
}
},
canDefineScript() {
return this.canDefineComputed
},
},
mounted() {},
methods: {
@@ -452,30 +463,28 @@ export default {
})
if (this.currentValueType === '2') {
this.form.setFieldsValue({
is_password: false,
is_link: false,
is_index: true,
})
}
}
}
if (checked && property === 'is_password') {
this.form.setFieldsValue({
is_link: false,
})
}
if (checked && property === 'is_link') {
this.form.setFieldsValue({
is_password: false,
})
}
if (property === 'is_list') {
this.form.setFieldsValue({
default_value: checked ? [] : '',
})
}
if (checked && property === 'is_sortable') {
this.$message.warning('选中排序,则必须也要选中!')
this.form.setFieldsValue({
is_required: true,
})
}
if (!checked && property === 'is_required' && this.form.getFieldValue('is_sortable')) {
this.$message.warning('选中排序,则必须也要选中!')
this.$nextTick(() => {
this.form.setFieldsValue({
is_required: true,
})
})
}
},
async handleEdit(record, attributes) {
@@ -485,91 +494,90 @@ export default {
} catch {
this.canDefineComputed = false
}
const _record = _.cloneDeep(record)
if (_record.is_password) {
_record.value_type = '7'
}
if (_record.is_link) {
_record.value_type = '8'
}
this.drawerTitle = '编辑属性'
this.drawerVisible = true
this.record = _record
this.currentValueType = _record.value_type
this.record = record
this.currentValueType = record.value_type
this.$nextTick(() => {
this.form.setFieldsValue({
id: _record.id,
alias: _record.alias,
name: _record.name,
value_type: _record.value_type,
is_required: _record.is_required,
default_show: _record.default_show,
id: record.id,
alias: record.alias,
name: record.name,
value_type: record.value_type,
is_required: record.is_required,
default_show: record.default_show,
})
if (!['6', '7'].includes(_record.value_type)) {
if (record.value_type !== '6') {
this.form.setFieldsValue({
is_list: _record.is_list,
is_unique: _record.is_unique,
is_index: _record.is_index,
is_sortable: _record.is_sortable,
is_computed: _record.is_computed,
is_list: record.is_list,
is_unique: record.is_unique,
is_index: record.is_index,
is_sortable: record.is_sortable,
is_computed: record.is_computed,
})
}
console.log(_record)
if (_record.default) {
if (record.value_type === '2') {
this.form.setFieldsValue({
is_password: record.is_password,
is_link: record.is_link,
})
}
console.log(record)
if (record.default) {
this.$nextTick(() => {
if (_record.value_type === '0') {
if (record.value_type === '0') {
this.form.setFieldsValue({
default_value: _record.default.default ? [_record.default.default] : [],
default_value: record.default.default ? [record.default.default] : [],
})
} else if (_record.value_type === '6') {
this.default_value_json = _record?.default?.default || null
} else if (_record.value_type === '3' || _record.value_type === '4') {
if (_record?.default?.default === '$created_at' || _record?.default?.default === '$updated_at') {
this.defaultForDatetime = _record.default.default
} else if (record.value_type === '6') {
this.default_value_json = record?.default?.default || null
} else if (record.value_type === '3' || record.value_type === '4') {
if (record?.default?.default === '$created_at' || record?.default?.default === '$updated_at') {
this.defaultForDatetime = record.default.default
this.form.setFieldsValue({
default_value: _record?.default?.default,
default_value: record?.default?.default,
})
} else {
this.defaultForDatetime = '$custom_time'
this.form.setFieldsValue({
default_value: _record.default && _record.default.default ? moment(_record.default.default) : null,
default_value: record.default && record.default.default ? moment(record.default.default) : null,
})
}
} else {
this.$nextTick(() => {
this.form.setFieldsValue({
default_value: _record.default && _record.default.default ? _record.default.default : null,
default_value: record.default && record.default.default ? record.default.default : null,
})
})
}
})
} else {
this.default_value_json = {}
if (_record.value_type === '0') {
if (record.value_type === '0') {
this.form.setFieldsValue({
default_value: [],
})
} else if (_record.value_type !== '6') {
} else if (record.value_type !== '6') {
this.form.setFieldsValue({
default_value: null,
})
}
}
this.isShowComputedArea = _record.is_computed
if (_record.is_computed) {
this.isShowComputedArea = record.is_computed
if (record.is_computed) {
this.$nextTick(() => {
this.$refs.computedArea.setData({
compute_expr: _record.compute_expr,
compute_script: _record.compute_script,
compute_expr: record.compute_expr,
compute_script: record.compute_script,
})
})
}
const _find = attributes.find((item) => item.id === _record.id)
if (!['6', '7'].includes(_record.value_type)) {
const _find = attributes.find((item) => item.id === record.id)
if (record.value_type !== '6') {
this.$refs.preValueArea.setData({
choice_value: (_find || {}).choice_value || [],
choice_web_hook: _record.choice_web_hook,
choice_other: _record.choice_other || undefined,
choice_web_hook: record.choice_web_hook,
choice_other: record.choice_other || undefined,
})
}
this.$refs.fontArea.setData({
@@ -625,20 +633,13 @@ export default {
values = { ...values, ...computedAreaData }
} else {
// 如果是非计算属性就看看有没有预定义值
if (!['6', '7'].includes(values.value_type)) {
if (values.value_type !== '6') {
const preValueAreaData = this.$refs.preValueArea.getData()
values = { ...values, ...preValueAreaData }
}
}
const fontOptions = this.$refs.fontArea.getData()
if (values.value_type === '7') {
values.value_type = '2'
values.is_password = true
}
if (values.value_type === '8') {
values.value_type = '2'
values.is_link = true
}
if (values.id) {
await this.updateAttribute(values.id, { ...values, option: { fontOptions } }, isCalcComputed)
} else {
@@ -659,6 +660,7 @@ export default {
handleOk() {
this.$emit('ok')
},
handleChangeValueType(value) {
this.currentValueType = value
this.$nextTick(() => {

View File

@@ -1,14 +1,9 @@
<template>
<a-form
:form="form"
class="create-new-attribute"
:label-col="formItemLayout.labelCol"
:wrapper-col="formItemLayout.wrapperCol"
>
<a-form :form="form" class="create-new-attribute">
<a-divider style="font-size:14px;margin-top:6px;">基础设置</a-divider>
<a-row>
<a-col :span="12">
<a-form-item label="属性名(英文)">
<a-form-item :label-col="formItemLayout.labelCol" :wrapper-col="formItemLayout.wrapperCol" label="属性名(英文)">
<a-input
name="name"
placeholder="英文"
@@ -29,33 +24,28 @@
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="别名">
<a-form-item :label-col="formItemLayout.labelCol" :wrapper-col="formItemLayout.wrapperCol" label="别名">
<a-input name="alias" v-decorator="['alias', { rules: [] }]" />
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col :span="12">
<a-form-item label="数据类型">
<a-form-item :label-col="formItemLayout.labelCol" :wrapper-col="formItemLayout.wrapperCol" label="数据类型">
<a-select
name="value_type"
style="width: 100%"
v-decorator="['value_type', { rules: [{ required: true }], initialValue: '2' }]"
@change="handleChangeValueType"
>
<a-select-option :value="key" :key="key" v-for="(value, key) in valueTypeMap">
{{ value }}
<span class="value-type-des" v-if="key === '3'">yyyy-mm-dd HH:MM:SS</span>
<span class="value-type-des" v-if="key === '4'">yyyy-mm-dd</span>
<span class="value-type-des" v-if="key === '5'">HH:MM:SS</span>
</a-select-option>
<a-select-option :value="key" :key="key" v-for="(value, key) in valueTypeMap">{{ value }}</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="currentValueType === '6' ? 24 : 12">
<a-form-item
:label-col="{ span: currentValueType === '6' ? 4 : 8 }"
:wrapper-col="{ span: currentValueType === '6' ? 18 : 15 }"
:wrapper-col="{ span: currentValueType === '6' ? 18 : 12 }"
label="默认值"
>
<template>
@@ -84,12 +74,7 @@
</a-select>
<a-input
style="width: 100%"
v-else-if="
currentValueType === '2' ||
currentValueType === '5' ||
currentValueType === '7' ||
currentValueType === '8'
"
v-else-if="currentValueType === '2' || currentValueType === '5'"
v-decorator="['default_value', { rules: [{ required: false }] }]"
>
</a-input>
@@ -155,13 +140,13 @@
label="必须"
>
<a-switch
@change="(checked) => onChange(checked, 'is_required')"
@change="onChange"
name="is_required"
v-decorator="['is_required', { rules: [], valuePropName: 'checked' }]"
/>
</a-form-item>
</a-col>
<a-col :span="6" v-if="currentValueType !== '6' && currentValueType !== '7'">
<a-col :span="6" v-if="currentValueType !== '6'">
<a-form-item :label-col="{ span: 8 }" :wrapper-col="horizontalFormItemLayout.wrapperCol" label="唯一">
<a-switch
:disabled="isShowComputedArea"
@@ -231,7 +216,7 @@
/>
</a-form-item>
</a-col>
<a-col :span="6" v-if="currentValueType !== '6' && currentValueType !== '7'">
<a-col :span="6" v-if="currentValueType !== '6'">
<a-form-item
:label-col="currentValueType === '2' ? horizontalFormItemLayout.labelCol : { span: 8 }"
:wrapper-col="horizontalFormItemLayout.wrapperCol"
@@ -239,14 +224,14 @@
>
<a-switch
:disabled="isShowComputedArea"
@change="(checked) => onChange(checked, 'is_sortable')"
@change="onChange"
name="is_sortable"
v-decorator="['is_sortable', { rules: [], valuePropName: 'checked' }]"
/>
</a-form-item>
</a-col>
<a-col :span="6" v-if="currentValueType !== '6' && currentValueType !== '7'">
<a-col :span="6" v-if="currentValueType !== '6'">
<a-form-item
:label-col="currentValueType === '2' ? { span: 8 } : horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol"
@@ -278,6 +263,30 @@
/>
</a-form-item>
</a-col>
<a-col :span="6" v-if="currentValueType === '2'">
<a-form-item
:label-col="horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol"
label="密码"
>
<a-switch
:disabled="isShowComputedArea"
@change="(checked) => onChange(checked, 'is_password')"
name="is_password"
v-decorator="['is_password', { rules: [], valuePropName: 'checked' }]"
/>
</a-form-item>
</a-col>
<a-col :span="6" v-if="currentValueType === '2'">
<a-form-item :label-col="{ span: 8 }" :wrapper-col="horizontalFormItemLayout.wrapperCol" label="链接">
<a-switch
:disabled="isShowComputedArea"
@change="(checked) => onChange(checked, 'is_link')"
name="is_link"
v-decorator="['is_link', { rules: [], valuePropName: 'checked' }]"
/>
</a-form-item>
</a-col>
<a-divider style="font-size:14px;margin-top:6px;">高级设置</a-divider>
<a-row>
<a-col :span="24">
@@ -285,12 +294,12 @@
<FontArea ref="fontArea" />
</a-form-item>
</a-col>
<a-col :span="24" v-if="!['6', '7'].includes(currentValueType)">
<a-col :span="24" v-if="currentValueType !== '6'">
<a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }" label="预定义值">
<PreValueArea ref="preValueArea" :canDefineScript="canDefineScript" :disabled="isShowComputedArea" />
<PreValueArea ref="preValueArea" :disabled="isShowComputedArea" />
</a-form-item>
</a-col>
<a-col :span="24" v-if="!['6', '7'].includes(currentValueType)">
<a-col :span="24">
<a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
<template slot="label">
<span
@@ -318,7 +327,7 @@
<a-switch
:disabled="!canDefineComputed"
@change="(checked) => onChange(checked, 'is_computed')"
name="is_computed"
name="is_password"
v-decorator="['is_computed', { rules: [], valuePropName: 'checked' }]"
/>
<ComputedArea ref="computedArea" v-if="isShowComputedArea" :canDefineComputed="canDefineComputed" />
@@ -360,7 +369,7 @@ export default {
valueTypeMap,
formItemLayout: {
labelCol: { span: 8 },
wrapperCol: { span: 15 },
wrapperCol: { span: 12 },
},
horizontalFormItemLayout: {
labelCol: { span: 16 },
@@ -377,11 +386,6 @@ export default {
defaultForDatetime: '',
}
},
computed: {
canDefineScript() {
return this.canDefineComputed
},
},
methods: {
handleSubmit(isCloseModal = true) {
this.form.validateFields(async (err, values) => {
@@ -424,7 +428,7 @@ export default {
values = { ...values, ...computedAreaData }
} else {
// 如果是非计算属性就看看有没有预定义值
if (!['6', '7'].includes(values.value_type)) {
if (values.value_type !== '6') {
const preValueAreaData = this.$refs.preValueArea.getData()
values = { ...values, ...preValueAreaData }
}
@@ -433,25 +437,17 @@ export default {
// is_index进行操作除了文本 索引隐藏掉 文本 索引默认是true
// 框里的5种类型 is_index=true
// json类型密码链接 is_index=false
if (['6', '7', '8'].includes(values.value_type)) {
// json类型 is_index=false
if (values.value_type === '6') {
values.is_index = false
} else if (values.value_type !== '2') {
values.is_index = true
}
if (values.value_type === '7') {
values.value_type = '2'
values.is_password = true
}
if (values.value_type === '8') {
values.value_type = '2'
values.is_link = true
}
const { attr_id } = await createAttribute({ ...values, option: { fontOptions } })
this.form.resetFields()
this.currentValueType = '2'
if (!['6'].includes(values.value_type) && !values.is_password) {
if (values.value_type !== '6') {
this.$refs.preValueArea.valueList = []
}
this.$emit('done', attr_id, data, isCloseModal)
@@ -489,27 +485,29 @@ export default {
is_index: false,
is_sortable: false,
})
if (this.currentValueType === '2') {
this.form.setFieldsValue({
is_password: false,
is_link: false,
})
}
}
}
if (checked && property === 'is_password') {
this.form.setFieldsValue({
is_link: false,
})
}
if (checked && property === 'is_link') {
this.form.setFieldsValue({
is_password: false,
})
}
if (property === 'is_list') {
this.form.setFieldsValue({
default_value: checked ? [] : '',
})
}
if (checked && property === 'is_sortable') {
this.$message.warning('选中排序,则必须也要选中!')
this.form.setFieldsValue({
is_required: true,
})
}
if (!checked && property === 'is_required' && this.form.getFieldValue('is_sortable')) {
this.$message.warning('选中排序,则必须也要选中!')
this.$nextTick(() => {
this.form.setFieldsValue({
is_required: true,
})
})
}
},
onJsonChange(value) {
this.default_value_json_right = true
@@ -550,8 +548,4 @@ export default {
background-color: #2f54eb;
}
}
.value-type-des {
font-size: 10px;
color: #a9a9a9;
}
</style>

View File

@@ -1,90 +1,91 @@
<template>
<a-tabs v-model="activeKey" size="small" :tabBarStyle="{ borderBottom: 'none' }">
<a-tab-pane key="expr" :disabled="!canDefineComputed">
<span style="font-size:12px;" slot="tab">表达式</span>
<a-textarea v-model="compute_expr" :placeholder="`{{a}}+{{b}}`" :rows="2" :disabled="!canDefineComputed" />
</a-tab-pane>
<a-tab-pane key="script" :disabled="!canDefineComputed">
<span style="font-size:12px;" slot="tab">代码</span>
<codemirror style="z-index: 9999" :options="cmOptions" v-model="compute_script"></codemirror>
</a-tab-pane>
<template slot="tabBarExtraContent" v-if="showCalcComputed">
<a-button type="primary" size="small" @click="handleCalcComputed">
应用
</a-button>
<a-tooltip title="所有CI触发计算">
<a-icon type="question-circle" style="margin-left:5px" />
</a-tooltip>
</template>
</a-tabs>
</template>
<script>
import { codemirror } from 'vue-codemirror'
import 'codemirror/lib/codemirror.css'
import 'codemirror/theme/monokai.css'
require('codemirror/mode/python/python.js')
export default {
name: 'ComputedArea',
components: { codemirror },
props: {
canDefineComputed: {
type: Boolean,
default: true,
},
showCalcComputed: {
type: Boolean,
default: false,
},
},
data() {
return {
activeKey: 'expr', // expr script
compute_expr: '',
compute_script: 'def computed(): \n return',
cmOptions: {
lineNumbers: true,
mode: 'python',
height: '200px',
theme: 'monokai',
tabSize: 4,
lineWrapping: true,
readOnly: !this.canDefineComputed,
},
}
},
methods: {
getData() {
const { activeKey, compute_expr, compute_script } = this
if (activeKey === 'expr') {
return { compute_expr, compute_script: null }
} else if (activeKey === 'script') {
return { compute_script, compute_expr: null }
}
},
setData(data) {
const { compute_expr, compute_script } = data
this.compute_expr = compute_expr
this.compute_script = compute_script || 'def computed(): \n return'
if (compute_script) {
this.activeKey = 'script'
} else {
this.activeKey = 'expr'
}
},
handleCalcComputed() {
const that = this
this.$confirm({
title: '警告',
content: `确认触发将保存当前配置及触发所有CI的计算`,
onOk() {
that.$emit('handleCalcComputed')
},
})
},
},
}
</script>
<style></style>
<template>
<a-tabs v-model="activeKey" size="small" :tabBarStyle="{ borderBottom: 'none' }">
<a-tab-pane key="expr" :disabled="!canDefineComputed">
<span style="font-size:12px;" slot="tab">表达式</span>
<a-textarea v-model="compute_expr" :placeholder="`{{a}}+{{b}}`" :rows="2" :disabled="!canDefineComputed" />
</a-tab-pane>
<a-tab-pane key="script" :disabled="!canDefineComputed">
<span style="font-size:12px;" slot="tab">代码</span>
<codemirror style="z-index: 9999" :options="cmOptions" v-model="compute_script"></codemirror>
</a-tab-pane>
<template slot="tabBarExtraContent" v-if="showCalcComputed">
<a-button type="primary" size="small" @click="handleCalcComputed">
应用
</a-button>
<a-tooltip title="所有CI触发计算">
<a-icon type="question-circle" style="margin-left:5px" />
</a-tooltip>
</template>
</a-tabs>
</template>
<script>
import { codemirror } from 'vue-codemirror'
import 'codemirror/lib/codemirror.css'
import 'codemirror/theme/monokai.css'
require('codemirror/mode/python/python.js')
export default {
name: 'ComputedArea',
components: { codemirror },
props: {
canDefineComputed: {
type: Boolean,
default: true,
},
showCalcComputed: {
type: Boolean,
default: false,
}
},
data() {
return {
activeKey: 'expr', // expr script
compute_expr: '',
compute_script: 'def computed(): \n return',
cmOptions: {
lineNumbers: true,
mode: 'python',
height: '200px',
theme: 'monokai',
smartIndent: true,
tabSize: 4,
lineWrapping: true,
readOnly: !this.canDefineComputed,
},
}
},
methods: {
getData() {
const { activeKey, compute_expr, compute_script } = this
if (activeKey === 'expr') {
return { compute_expr, compute_script: null }
} else if (activeKey === 'script') {
return { compute_script, compute_expr: null }
}
},
setData(data) {
const { compute_expr, compute_script } = data
this.compute_expr = compute_expr
this.compute_script = compute_script || 'def computed(): \n return'
if (compute_script) {
this.activeKey = 'script'
} else {
this.activeKey = 'expr'
}
},
handleCalcComputed() {
const that = this
this.$confirm({
title: '警告',
content: `确认触发将保存当前配置及触发所有CI的计算`,
onOk() {
that.$emit('handleCalcComputed')
},
})
},
},
}
</script>
<style></style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,5 @@
<template>
<a-tabs
id="preValueArea"
v-model="activeKey"
@change="changeActiveKey"
size="small"
:tabBarStyle="{ borderBottom: 'none' }"
>
<a-tabs id="preValueArea" v-model="activeKey" size="small" :tabBarStyle="{ borderBottom: 'none' }">
<a-tab-pane key="define" :disabled="disabled">
<span style="font-size:12px;" slot="tab">定义</span>
<PreValueTag type="add" :item="[]" @add="addNewValue" :disabled="disabled">
@@ -176,15 +170,6 @@
</a-col>
</a-row>
</a-tab-pane>
<a-tab-pane key="script" :disabled="disabled || !canDefineScript">
<span style="font-size:12px;" slot="tab">脚本</span>
<CustomCodeMirror
codeMirrorId="cmdb-pre-value"
ref="codemirror"
@changeCodeContent="changeCodeContent"
></CustomCodeMirror>
<!-- <codemirror style="z-index: 9999" :options="cmOptions" v-model="script"></codemirror> -->
</a-tab-pane>
</a-tabs>
</template>
@@ -199,22 +184,14 @@ import { getCITypeGroups } from '../../api/ciTypeGroup'
import { getCITypeCommonAttributesByTypeIds } from '../../api/CITypeAttr'
import FilterComp from '@/components/CMDBFilterComp'
import CustomCodeMirror from '@/components/CustomCodeMirror'
import 'codemirror/lib/codemirror.css'
import 'codemirror/theme/monokai.css'
require('codemirror/mode/python/python.js')
export default {
name: 'PreValueArea',
components: { draggable, PreValueTag, ColorPicker, Webhook, FilterComp, CustomCodeMirror },
components: { draggable, PreValueTag, ColorPicker, Webhook, FilterComp },
props: {
disabled: {
type: Boolean,
default: true,
},
canDefineScript: {
type: Boolean,
default: false,
},
},
data() {
return {
@@ -231,17 +208,6 @@ export default {
ciTypeGroup: [],
typeAttrs: [],
filterExp: '',
script:
'class ChoiceValue(object):\n @staticmethod\n def values():\n """\n 执行入口, 返回预定义值\n :return: 返回一个列表, 值的类型同属性的类型\n 例如:\n return ["在线", "下线"]\n """\n return []',
cmOptions: {
lineNumbers: true,
mode: 'python',
height: '200px',
theme: 'monokai',
tabSize: 4,
lineWrapping: true,
readOnly: this.disabled || !this.canDefineScript,
},
}
},
watch: {
@@ -315,14 +281,6 @@ export default {
const choice_web_hook = this.$refs.webhook.getParams()
choice_web_hook.ret_key = this.form.ret_key
return { choice_value: [], choice_web_hook, choice_other: null }
} else if (this.activeKey === 'script') {
return {
choice_value: [],
choice_web_hook: null,
choice_other: {
script: this.script,
},
}
} else {
let choice_other = {}
if (this.choice_other.type_ids && this.choice_other.type_ids.length) {
@@ -344,22 +302,14 @@ export default {
this.form.ret_key = choice_web_hook.ret_key ?? ''
})
} else if (choice_other) {
if (choice_other.script) {
this.activeKey = 'script'
this.script = choice_other.script
this.activeKey = 'choice_other'
const { type_ids, attr_id, filter } = choice_other
this.choice_other = { type_ids, attr_id }
this.filterExp = filter
if (type_ids && type_ids.length) {
this.$nextTick(() => {
this.$refs.codemirror.initCodeMirror(choice_other.script)
this.$refs.filterComp.visibleChange(true, false)
})
} else {
this.activeKey = 'choice_other'
const { type_ids, attr_id, filter } = choice_other
this.choice_other = { type_ids, attr_id }
this.filterExp = filter
if (type_ids && type_ids.length) {
this.$nextTick(() => {
this.$refs.filterComp.visibleChange(true, false)
})
}
}
} else {
this.valueList = choice_value
@@ -380,16 +330,6 @@ export default {
this.filterExp = ''
}
},
changeCodeContent(value) {
this.script = value && value.replace('\t', ' ')
},
changeActiveKey(value) {
if (value === 'script') {
this.$nextTick(() => {
this.$refs.codemirror.initCodeMirror(this.script)
})
}
},
},
}
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,7 @@ export const category_1_bar_options = (data, options) => {
})
return {
color: (options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD').split(','),
color: options.chartColor.split(','),
grid: {
top: 15,
left: 'left',
@@ -83,7 +83,7 @@ export const category_1_bar_options = (data, options) => {
export const category_1_line_options = (data, options) => {
const xData = Object.keys(data)
return {
color: (options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD').split(','),
color: options.chartColor.split(','),
grid: {
top: 15,
left: 'left',
@@ -117,7 +117,7 @@ export const category_1_line_options = (data, options) => {
x2: 0,
y2: 1,
colorStops: [{
offset: 0, color: (options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD').split(',')[0] // 0% 处的颜色
offset: 0, color: options.chartColor.split(',')[0] // 0% 处的颜色
}, {
offset: 1, color: '#ffffff' // 100% 处的颜色
}],
@@ -131,7 +131,7 @@ export const category_1_line_options = (data, options) => {
export const category_1_pie_options = (data, options) => {
return {
color: (options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD').split(','),
color: options.chartColor.split(','),
grid: {
top: 10,
left: 'left',
@@ -181,7 +181,7 @@ export const category_2_bar_options = (data, options, chartType) => {
})
const legend = [...new Set(_legend)]
return {
color: (options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD').split(','),
color: options.chartColor.split(','),
grid: {
top: 15,
left: 'left',
@@ -249,7 +249,7 @@ export const category_2_bar_options = (data, options, chartType) => {
x2: 0,
y2: 1,
colorStops: [{
offset: 0, color: (options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD').split(',')[index % 8] // 0% 处的颜色
offset: 0, color: options.chartColor.split(',')[index % 8] // 0% 处的颜色
}, {
offset: 1, color: '#ffffff' // 100% 处的颜色
}],
@@ -269,7 +269,7 @@ export const category_2_pie_options = (data, options) => {
})
})
return {
color: (options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD').split(','),
color: options.chartColor.split(','),
grid: {
top: 15,
left: 'left',

View File

@@ -1,344 +1,345 @@
<template>
<CustomDrawer width="800px" :title="title" :visible="visible" @close="handleClose">
<template v-if="adType === 'agent'">
<a-form-model
ref="autoDiscoveryForm"
:model="form"
:rules="rules"
:label-col="{ span: 2 }"
:wrapper-col="{ span: 20 }"
>
<a-divider :style="{ margin: '5px 0' }">基础设置</a-divider>
<a-form-model-item label="名称" prop="name">
<a-input v-model="form.name" />
</a-form-model-item>
<a-form-model-item label="图标" v-if="is_inner">
<CustomIconSelect v-model="customIcon" :style="{ marginTop: '6px' }" />
</a-form-model-item>
<a-form-model-item label="模式" prop="is_plugin">
<a-radio-group v-model="form.is_plugin" @change="changeIsPlugin" :disabled="!is_inner">
<a-radio :value="false">默认</a-radio>
<a-radio :value="true">plugin</a-radio>
</a-radio-group>
</a-form-model-item>
</a-form-model>
<a-divider :style="{ margin: '5px 0' }">采集设置</a-divider>
<CustomCodeMirror
codeMirrorId="cmdb-adt"
v-if="form.is_plugin"
ref="codemirror"
@changeCodeContent="changeCodeContent"
></CustomCodeMirror>
<div style="margin:10px 0;text-align:right;">
<a-button
v-show="form.is_plugin"
size="small"
type="primary"
ghost
@click="handleSubmit(true)"
>更新字段</a-button
>
</div>
<a-button
v-show="!form.is_plugin"
size="small"
type="primary"
ghost
icon="plus"
:style="{ marginBottom: '10px' }"
@click="insertEvent(-1)"
>新增</a-button
>
<vxe-table
size="mini"
stripe
class="ops-stripe-table"
show-overflow
keep-source
ref="xTable"
max-height="400"
:data="tableData"
:edit-config="{ trigger: 'manual', mode: 'row' }"
>
<vxe-column field="name" title="名称" :edit-render="{ autofocus: '.vxe-input--inner' }">
<template #edit="{ row }">
<vxe-input v-model="row.name" type="text"></vxe-input>
</template>
</vxe-column>
<vxe-column field="type" title="类型" :edit-render="{}">
<template #edit="{ row }">
<vxe-select v-model="row.type" transfer>
<vxe-option v-for="item in typeList" :key="item" :value="item" :label="item"></vxe-option>
</vxe-select>
</template>
</vxe-column>
<vxe-column field="desc" title="描述" :edit-render="{ autofocus: '.vxe-input--inner' }">
<template #edit="{ row }">
<vxe-input v-model="row.desc" type="text"></vxe-input>
</template>
</vxe-column>
<vxe-column title="操作" width="60" v-if="!form.is_plugin">
<template #default="{ row }">
<a-space v-if="$refs.xTable.isActiveByRow(row)">
<a @click="saveRowEvent(row)"><a-icon type="save"/></a>
<a @click="cancelRowEvent(row)"><a-icon type="close"/></a>
</a-space>
<a-space v-else>
<a @click="editRowEvent(row)"><a-icon type="edit"/></a>
<a :style="{ color: 'red' }" @click="deleteRowEvent(row)"><a-icon type="delete"/></a>
</a-space>
</template>
</vxe-column>
</vxe-table>
<div class="custom-drawer-bottom-action">
<a-button @click="handleClose">取消</a-button>
<a-button @click="handleSubmit(false)" type="primary">保存</a-button>
</div>
</template>
<template v-else>
<HttpSnmpAD ref="httpSnmpAd" :ruleType="adType" :ruleName="ruleData.name" />
</template>
</CustomDrawer>
</template>
<script>
import CustomIconSelect from '@/components/CustomIconSelect'
import { postDiscovery, putDiscovery } from '../../api/discovery'
import HttpSnmpAD from '../../components/httpSnmpAD'
import CustomCodeMirror from '@/components/CustomCodeMirror'
import 'codemirror/lib/codemirror.css'
import 'codemirror/theme/monokai.css'
export default {
name: 'EditDrawer',
components: { CustomIconSelect, CustomCodeMirror, HttpSnmpAD },
props: {
is_inner: {
type: Boolean,
default: true,
},
},
data() {
const default_plugin_script = `# -*- coding:utf-8 -*-
import json
class AutoDiscovery(object):
@property
def unique_key(self):
"""
:return: 返回唯一属性的名字
"""
return
@staticmethod
def attributes():
"""
定义属性字段
:return: 返回属性字段列表, 列表项是(名称, 类型, 描述), 名称必须是英文
类型: String Integer Float Date DateTime Time JSON
例如:
return [
("ci_type", "String", "模型名称"),
("private_ip", "String", "内网IP, 多值逗号分隔")
]
"""
return []
@staticmethod
def run():
"""
执行入口, 返回采集的属性值
:return: 返回一个列表, 列表项是字典, 字典key是属性名称, value是属性值
例如:
return [dict(ci_type="server", private_ip="192.168.1.1")]
"""
return []
if __name__ == "__main__":
result = AutoDiscovery().run()
if isinstance(result, list):
print("AutoDiscovery::Result::{}".format(json.dumps(result)))
else:
print("ERROR: 采集返回必须是列表")
`
const typeList = ['String', 'Integer', 'Float', 'Date', 'DateTime', 'Time', 'JSON']
return {
default_plugin_script,
typeList,
visible: false,
ruleData: {},
type: 'add',
adType: '',
form: { name: '', is_plugin: false },
rules: {},
customIcon: { name: '', color: '' },
tableData: [],
editDefaultTableData: [],
plugin_script: '',
cmOptions: {
lineNumbers: true,
mode: 'python',
height: '200px',
theme: 'monokai',
tabSize: 4,
lineWrapping: true,
},
}
},
computed: {
title() {
if (this.adType === 'http' || this.adType === 'snmp') {
return this.ruleData.name
}
if (this.type === 'edit') {
return `编辑${this.ruleData.name}`
}
return '新建'
},
},
inject: {
getDiscovery: {
from: 'getDiscovery',
default: () => {},
},
},
methods: {
open(data, type, adType) {
this.visible = true
this.type = type
this.ruleData = data
this.adType = adType
if (!this.is_inner) {
this.form = {
name: '',
is_plugin: true,
}
}
if (adType === 'http' || adType === 'snmp') {
return
}
this.$nextTick(() => {
if (this.type === 'edit') {
this.form = {
name: data.name,
is_plugin: data.is_plugin,
}
this.customIcon = data?.option?.icon ?? { name: 'caise-chajian', color: '' }
this.tableData = data?.attributes ?? []
this.editDefaultTableData = data?.attributes ?? []
this.plugin_script = data?.plugin_script ?? this.default_plugin_script
}
if (this.type === 'add') {
this.customIcon = { name: 'caise-chajian', color: '' }
// eslint-disable-next-line no-useless-escape
this.plugin_script = this.default_plugin_script
}
if (data?.is_plugin || !this.is_inner) {
this.$nextTick(() => {
this.$refs.codemirror.initCodeMirror(this.plugin_script)
})
}
})
},
handleClose() {
this.tableData = []
this.customIcon = { name: '', color: '' }
this.form = { name: '', is_plugin: false }
if (this.adType === 'agent') {
this.$refs.autoDiscoveryForm.clearValidate()
} else {
// this.$refs.httpSnmpAd.currentCate = ''
}
this.visible = false
},
async insertEvent(row) {
const $table = this.$refs.xTable
const record = {}
const { row: newRow } = await $table.insertAt(record, row)
await $table.setEditRow(newRow)
},
editRowEvent(row) {
const $table = this.$refs.xTable
$table.setActiveRow(row)
},
saveRowEvent() {
const $table = this.$refs.xTable
$table.clearActived().then(() => {
this.loading = true
setTimeout(() => {
this.loading = false
}, 300)
})
},
cancelRowEvent(row) {
const $table = this.$refs.xTable
$table.clearActived().then(() => {
// 还原行数据
$table.revertData(row)
})
},
deleteRowEvent(row) {
const $table = this.$refs.xTable
$table.remove(row)
},
async handleSubmit(isUpdateAttr = false) {
const $table = this.$refs.xTable
const { fullData: _tableData } = $table.getTableData()
console.log(_tableData)
const params = {
...this.form,
type: this.adType,
is_inner: this.is_inner,
option: { icon: this.customIcon },
attributes: this.form.is_plugin
? undefined
: _tableData.map(({ name, alias, desc, type }) => {
return { name, alias, desc, type }
}),
plugin_script: this.form.is_plugin ? this.plugin_script : undefined,
}
let res
if (this.type === 'add') {
res = await postDiscovery(params)
} else {
res = await putDiscovery(this.ruleData.id, params)
}
if (isUpdateAttr) {
this.tableData = res.attributes
this.type = 'edit'
this.ruleData = res
this.$message.success('更新成功!')
if (this.is_inner) {
this.getDiscovery()
}
return
}
this.handleClose()
console.log(this.is_inner)
if (this.is_inner) {
this.$message.success('保存成功!')
this.getDiscovery()
} else {
this.$emit('updateNotInner', res)
}
},
changeIsPlugin(e) {
if (e.target.value) {
this.$nextTick(() => {
this.$refs.codemirror.initCodeMirror(this.plugin_script)
})
}
},
changeCodeContent(value) {
this.plugin_script = value && value.replace('\t', ' ')
},
},
}
</script>
<style></style>
<template>
<CustomDrawer width="800px" :title="title" :visible="visible" @close="handleClose">
<template v-if="adType === 'agent'">
<a-form-model
ref="autoDiscoveryForm"
:model="form"
:rules="rules"
:label-col="{ span: 2 }"
:wrapper-col="{ span: 20 }"
>
<a-divider :style="{ margin: '5px 0' }">基础设置</a-divider>
<a-form-model-item label="名称" prop="name">
<a-input v-model="form.name" />
</a-form-model-item>
<a-form-model-item label="图标" v-if="is_inner">
<CustomIconSelect v-model="customIcon" :style="{ marginTop: '6px' }" />
</a-form-model-item>
<a-form-model-item label="模式" prop="is_plugin">
<a-radio-group v-model="form.is_plugin" @change="changeIsPlugin" :disabled="!is_inner">
<a-radio :value="false">默认</a-radio>
<a-radio :value="true">plugin</a-radio>
</a-radio-group>
</a-form-model-item>
</a-form-model>
<a-divider :style="{ margin: '5px 0' }">采集设置</a-divider>
<CustomCodeMirror
codeMirrorId="cmdb-adt"
v-if="form.is_plugin"
ref="codemirror"
@changeCodeContent="changeCodeContent"
></CustomCodeMirror>
<div style="margin:10px 0;text-align:right;">
<a-button
v-show="form.is_plugin"
size="small"
type="primary"
ghost
@click="handleSubmit(true)"
>更新字段</a-button
>
</div>
<a-button
v-show="!form.is_plugin"
size="small"
type="primary"
ghost
icon="plus"
:style="{ marginBottom: '10px' }"
@click="insertEvent(-1)"
>新增</a-button
>
<vxe-table
size="mini"
stripe
class="ops-stripe-table"
show-overflow
keep-source
ref="xTable"
max-height="400"
:data="tableData"
:edit-config="{ trigger: 'manual', mode: 'row' }"
>
<vxe-column field="name" title="名称" :edit-render="{ autofocus: '.vxe-input--inner' }">
<template #edit="{ row }">
<vxe-input v-model="row.name" type="text"></vxe-input>
</template>
</vxe-column>
<vxe-column field="type" title="类型" :edit-render="{}">
<template #edit="{ row }">
<vxe-select v-model="row.type" transfer>
<vxe-option v-for="item in typeList" :key="item" :value="item" :label="item"></vxe-option>
</vxe-select>
</template>
</vxe-column>
<vxe-column field="desc" title="描述" :edit-render="{ autofocus: '.vxe-input--inner' }">
<template #edit="{ row }">
<vxe-input v-model="row.desc" type="text"></vxe-input>
</template>
</vxe-column>
<vxe-column title="操作" width="60" v-if="!form.is_plugin">
<template #default="{ row }">
<a-space v-if="$refs.xTable.isActiveByRow(row)">
<a @click="saveRowEvent(row)"><a-icon type="save"/></a>
<a @click="cancelRowEvent(row)"><a-icon type="close"/></a>
</a-space>
<a-space v-else>
<a @click="editRowEvent(row)"><a-icon type="edit"/></a>
<a :style="{ color: 'red' }" @click="deleteRowEvent(row)"><a-icon type="delete"/></a>
</a-space>
</template>
</vxe-column>
</vxe-table>
<div class="custom-drawer-bottom-action">
<a-button @click="handleClose">取消</a-button>
<a-button @click="handleSubmit(false)" type="primary">保存</a-button>
</div>
</template>
<template v-else>
<HttpSnmpAD ref="httpSnmpAd" :ruleType="adType" :ruleName="ruleData.name" />
</template>
</CustomDrawer>
</template>
<script>
import CustomIconSelect from '@/components/CustomIconSelect'
import { postDiscovery, putDiscovery } from '../../api/discovery'
import HttpSnmpAD from '../../components/httpSnmpAD'
import CustomCodeMirror from '@/components/CustomCodeMirror'
import 'codemirror/lib/codemirror.css'
import 'codemirror/theme/monokai.css'
export default {
name: 'EditDrawer',
components: { CustomIconSelect, CustomCodeMirror, HttpSnmpAD },
props: {
is_inner: {
type: Boolean,
default: true,
},
},
data() {
const default_plugin_script = `# -*- coding:utf-8 -*-
import json
class AutoDiscovery(object):
@property
def unique_key(self):
"""
:return: 返回唯一属性的名字
"""
return
@staticmethod
def attributes():
"""
定义属性字段
:return: 返回属性字段列表, 列表项是(名称, 类型, 描述), 名称必须是英文
类型: String Integer Float Date DateTime Time JSON
例如:
return [
("ci_type", "String", "模型名称"),
("private_ip", "String", "内网IP, 多值逗号分隔")
]
"""
return []
@staticmethod
def run():
"""
执行入口, 返回采集的属性值
:return: 返回一个列表, 列表项是字典, 字典key是属性名称, value是属性值
例如:
return [dict(ci_type="server", private_ip="192.168.1.1")]
"""
return []
if __name__ == "__main__":
result = AutoDiscovery().run()
if isinstance(result, list):
print("AutoDiscovery::Result::{}".format(json.dumps(result)))
else:
print("ERROR: 采集返回必须是列表")
`
const typeList = ['String', 'Integer', 'Float', 'Date', 'DateTime', 'Time', 'JSON']
return {
default_plugin_script,
typeList,
visible: false,
ruleData: {},
type: 'add',
adType: '',
form: { name: '', is_plugin: false },
rules: {},
customIcon: { name: '', color: '' },
tableData: [],
editDefaultTableData: [],
plugin_script: '',
cmOptions: {
lineNumbers: true,
mode: 'python',
height: '200px',
theme: 'monokai',
smartIndent: true,
tabSize: 4,
lineWrapping: true,
},
}
},
computed: {
title() {
if (this.adType === 'http' || this.adType === 'snmp') {
return this.ruleData.name
}
if (this.type === 'edit') {
return `编辑${this.ruleData.name}`
}
return '新建'
},
},
inject: {
getDiscovery: {
from: 'getDiscovery',
default: () => {},
},
},
methods: {
open(data, type, adType) {
this.visible = true
this.type = type
this.ruleData = data
this.adType = adType
if (!this.is_inner) {
this.form = {
name: '',
is_plugin: true,
}
}
if (adType === 'http' || adType === 'snmp') {
return
}
this.$nextTick(() => {
if (this.type === 'edit') {
this.form = {
name: data.name,
is_plugin: data.is_plugin,
}
this.customIcon = data?.option?.icon ?? { name: 'caise-chajian', color: '' }
this.tableData = data?.attributes ?? []
this.editDefaultTableData = data?.attributes ?? []
this.plugin_script = data?.plugin_script ?? this.default_plugin_script
}
if (this.type === 'add') {
this.customIcon = { name: 'caise-chajian', color: '' }
// eslint-disable-next-line no-useless-escape
this.plugin_script = this.default_plugin_script
}
if (data?.is_plugin || !this.is_inner) {
this.$nextTick(() => {
this.$refs.codemirror.initCodeMirror(this.plugin_script)
})
}
})
},
handleClose() {
this.tableData = []
this.customIcon = { name: '', color: '' }
this.form = { name: '', is_plugin: false }
if (this.adType === 'agent') {
this.$refs.autoDiscoveryForm.clearValidate()
} else {
// this.$refs.httpSnmpAd.currentCate = ''
}
this.visible = false
},
async insertEvent(row) {
const $table = this.$refs.xTable
const record = {}
const { row: newRow } = await $table.insertAt(record, row)
await $table.setEditRow(newRow)
},
editRowEvent(row) {
const $table = this.$refs.xTable
$table.setActiveRow(row)
},
saveRowEvent() {
const $table = this.$refs.xTable
$table.clearActived().then(() => {
this.loading = true
setTimeout(() => {
this.loading = false
}, 300)
})
},
cancelRowEvent(row) {
const $table = this.$refs.xTable
$table.clearActived().then(() => {
// 还原行数据
$table.revertData(row)
})
},
deleteRowEvent(row) {
const $table = this.$refs.xTable
$table.remove(row)
},
async handleSubmit(isUpdateAttr = false) {
const $table = this.$refs.xTable
const { fullData: _tableData } = $table.getTableData()
console.log(_tableData)
const params = {
...this.form,
type: this.adType,
is_inner: this.is_inner,
option: { icon: this.customIcon },
attributes: this.form.is_plugin
? undefined
: _tableData.map(({ name, alias, desc, type }) => {
return { name, alias, desc, type }
}),
plugin_script: this.form.is_plugin ? this.plugin_script : undefined,
}
let res
if (this.type === 'add') {
res = await postDiscovery(params)
} else {
res = await putDiscovery(this.ruleData.id, params)
}
if (isUpdateAttr) {
this.tableData = res.attributes
this.type = 'edit'
this.ruleData = res
this.$message.success('更新成功!')
if (this.is_inner) {
this.getDiscovery()
}
return
}
this.handleClose()
console.log(this.is_inner)
if (this.is_inner) {
this.$message.success('保存成功!')
this.getDiscovery()
} else {
this.$emit('updateNotInner', res)
}
},
changeIsPlugin(e) {
if (e.target.value) {
this.$nextTick(() => {
this.$refs.codemirror.initCodeMirror(this.plugin_script)
})
}
},
changeCodeContent(value) {
this.plugin_script = value && value.replace('\t', ' ')
},
},
}
</script>
<style></style>

View File

@@ -25,7 +25,7 @@
:expandedKeys="expandedKeys"
>
<a-icon slot="switcherIcon" type="down" />
<template #title="{ key: treeKey, title,isLeaf }">
<template #title="{ key: treeKey, title, isLeaf }">
<ContextMenu
:title="title"
:treeKey="treeKey"
@@ -135,8 +135,7 @@
{{ col.title }}</span
>
</template>
<template v-if="col.is_choice || col.is_password" #edit="{ row }">
<vxe-input v-if="col.is_password" v-model="passwordValue[col.field]" />
<template v-if="col.is_choice" #edit="{ row }">
<a-select
:getPopupContainer="(trigger) => trigger.parentElement"
:style="{ width: '100%', height: '32px' }"
@@ -178,20 +177,10 @@
#default="{row}"
>
<span v-if="col.value_type === '6' && row[col.field]">{{ row[col.field] }}</span>
<a
v-else-if="col.is_link && row[col.field]"
:href="
row[col.field].startsWith('http') || row[col.field].startsWith('https')
? `${row[col.field]}`
: `http://${row[col.field]}`
"
target="_blank"
>{{ row[col.field] }}</a
>
<a v-else-if="col.is_link" :href="`${row[col.field]}`" target="_blank">{{ row[col.field] }}</a>
<PasswordField
v-else-if="col.is_password && row[col.field]"
:ci_id="row._id"
:attr_id="col.attr_id"
:password="row[col.field]"
></PasswordField>
<template v-else-if="col.is_choice">
<template v-if="col.is_list">
@@ -344,7 +333,7 @@ import {
} from '@/modules/cmdb/api/CIRelation'
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
import { searchCI2, updateCI, deleteCI, searchCI } from '@/modules/cmdb/api/ci'
import { searchCI2, updateCI, deleteCI } from '@/modules/cmdb/api/ci'
import { getCITypes } from '../../api/CIType'
import { roleHasPermissionToGrant } from '@/modules/acl/api/permission'
import { searchResourceType } from '@/modules/acl/api/resource'
@@ -358,7 +347,6 @@ import PasswordField from '../../components/passwordField/index.vue'
import PreferenceSearch from '../../components/preferenceSearch/preferenceSearch.vue'
import CMDBGrant from '../../components/cmdbGrant'
import { ops_move_icon as OpsMoveIcon } from '@/core/icons'
import { getAttrPassword } from '../../api/CITypeAttr'
export default {
name: 'RelationViews',
@@ -419,11 +407,6 @@ export default {
tableDragClassName: [],
resource_type: {},
initialPasswordValue: {},
passwordValue: {},
lastEditCiId: null,
isContinueCloseEdit: true,
}
},
@@ -566,11 +549,11 @@ export default {
q = q.slice(1)
}
if (this.treeKeys.length === 0) {
await this.judgeCITypes(q)
if (!refreshType) {
this.loadRoot()
}
await this.judgeCITypes(q)
const fuzzySearch = (this.$refs['search'] || {}).fuzzySearch || ''
if (fuzzySearch) {
q = `q=_type:${this.currentTypeId[0]},*${fuzzySearch}*,` + q
@@ -652,7 +635,6 @@ export default {
statisticsCIRelation({
root_ids: key.split('%')[0],
level: this.treeKeys.length - index,
type_ids: this.showTypes.map((type) => type.id).join(','),
}).then((res) => {
let result
const getTreeItem = (data, id) => {
@@ -707,7 +689,6 @@ export default {
}
const promises = _showTypeIds.map((typeId) => {
const _q = (`q=_type:${typeId},` + q).replace(/count=\d*/, 'count=1')
console.log(_q)
if (this.treeKeys.length === 0) {
return searchCI2(_q).then((res) => {
if (res.numfound !== 0) {
@@ -758,11 +739,7 @@ export default {
level = idx + 1
}
})
return statisticsCIRelation({
root_ids: ciIds.join(','),
level: level,
type_ids: this.showTypes.map((type) => type.id).join(','),
}).then((num) => {
return statisticsCIRelation({ root_ids: ciIds.join(','), level: level }).then((num) => {
facet.forEach((item, idx) => {
item[1] += num[ciIds[idx] + '']
})
@@ -775,35 +752,25 @@ export default {
async loadNoRoot(rootIdAndTypeId, level) {
const rootId = rootIdAndTypeId.split('%')[0]
const typeId = Number(rootIdAndTypeId.split('%')[1])
const topo_flatten = this.relationViews?.views[this.$route.meta.name]?.topo_flatten ?? []
const index = topo_flatten.findIndex((id) => id === typeId)
const _type = topo_flatten[index + 1]
if (_type) {
searchCIRelation(`q=_type:${_type}&root_id=${rootId}&level=1&count=10000`).then(async (res) => {
const facet = []
const ciIds = []
res.result.forEach((item) => {
facet.push([item[item.unique], 0, item._id, item._type, item.unique])
ciIds.push(item._id)
})
const promises = level.map((_level) => {
if (_level > 1) {
return statisticsCIRelation({
root_ids: ciIds.join(','),
level: _level - 1,
type_ids: this.showTypes.map((type) => type.id).join(','),
}).then((num) => {
facet.forEach((item, idx) => {
item[1] += num[ciIds[idx] + '']
})
})
}
})
await Promise.all(promises)
this.wrapTreeData(facet, 'loadNoRoot')
searchCIRelation(`root_id=${rootId}&level=1&count=10000`).then(async (res) => {
const facet = []
const ciIds = []
res.result.forEach((item) => {
facet.push([item[item.unique], 0, item._id, item._type, item.unique])
ciIds.push(item._id)
})
}
const promises = level.map((_level) => {
if (_level > 1) {
return statisticsCIRelation({ root_ids: ciIds.join(','), level: _level - 1 }).then((num) => {
facet.forEach((item, idx) => {
item[1] += num[ciIds[idx] + '']
})
})
}
})
await Promise.all(promises)
this.wrapTreeData(facet, 'loadNoRoot')
})
},
onNodeClick(keys) {
@@ -907,12 +874,6 @@ export default {
calcColumns() {
const width = document.getElementById('relation-views-right').clientWidth
this.columns = getCITableColumns(this.instanceList, this.preferenceAttrList, width)
this.columns.forEach((col) => {
if (col.is_password) {
this.initialPasswordValue[col.field] = ''
this.passwordValue[col.field] = ''
}
})
this.$nextTick(() => {
this.$refs.xTable.refreshColumn()
})
@@ -963,10 +924,10 @@ export default {
),
onOk() {
const _tempTree = that.treeKeys[that.treeKeys.length - 1].split('%')
const first_ci_id = Number(_tempTree[0])
const firstCIObj = JSON.parse(_tempTree[2])
batchDeleteCIRelation(
that.selectedRowKeys.map((item) => item._id),
[first_ci_id]
firstCIObj
).then((res) => {
that.$refs.xTable.clearCheckboxRow()
that.$refs.xTable.clearCheckboxReserve()
@@ -1099,48 +1060,15 @@ export default {
}
return _editRender
},
handleEditActived() {
const passwordCol = this.columns.filter((col) => col.is_password)
this.$nextTick(() => {
const editRecord = this.$refs.xTable.getEditRecord()
const { row, column } = editRecord
if (passwordCol.length && this.lastEditCiId !== row._id) {
this.$nextTick(async () => {
for (let i = 0; i < passwordCol.length; i++) {
await getAttrPassword(row._id, passwordCol[i].attr_id).then((res) => {
this.initialPasswordValue[passwordCol[i].field] = res.value
this.passwordValue[passwordCol[i].field] = res.value
})
}
this.isContinueCloseEdit = false
await this.$refs.xTable.clearEdit()
this.isContinueCloseEdit = true
this.$nextTick(() => {
this.$refs.xTable.setEditCell(row, column.field)
})
})
}
this.lastEditCiId = row._id
})
},
handleEditActived() {},
handleEditClose({ row, rowIndex, column }) {
if (!this.isContinueCloseEdit) {
return
}
const $table = this.$refs['xTable']
const data = {}
this.columns.forEach((item) => {
if (!(item.field in this.initialPasswordValue) && !_.isEqual(row[item.field], this.initialInstanceList[rowIndex][item.field])) {
data[item.field] = row[item.field] ?? null
if (!_.isEqual(row[item.field], this.initialInstanceList[rowIndex][item.field])) {
data[item.field] = row[item.field] || null
}
})
Object.keys(this.initialPasswordValue).forEach((key) => {
if (this.initialPasswordValue[key] !== this.passwordValue[key]) {
data[key] = this.passwordValue[key]
row[key] = this.passwordValue[key]
}
})
this.lastEditCiId = null
if (JSON.stringify(data) !== '{}') {
updateCI(row.ci_id || row._id, data)
.then(() => {
@@ -1158,12 +1086,6 @@ export default {
$table.revertData(row)
})
}
this.columns.forEach((col) => {
if (col.is_password) {
this.initialPasswordValue[col.field] = ''
this.passwordValue[col.field] = ''
}
})
},
deleteCI(record) {
const that = this

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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