Compare commits

...

45 Commits
2.3.5 ... 2.3.6

Author SHA1 Message Date
pycook
dc1a2a7632 fix(api): secrets 2023-10-30 17:23:42 +08:00
pycook
153fef4918 feat: add inner password storage and optimize flask command about inner cmdb (#248)
Co-authored-by: fxiang21 <fxiang21@126.com>
2023-10-30 16:48:53 +08:00
wang-liang0615
e218f8e065 Dev UI 231030 (#247)
* config(ui):useEncryption default false

* fix(cmdb-ui):ident 4

* fix(cmdb-ui):relation views
2023-10-30 12:38:05 +08:00
pycook
b7137b3975 Dev api password (#244)
* fix: delete CI password data

* fix(api): update CI password to flush cache
2023-10-29 11:42:07 +08:00
pycook
c6a9478dbb fix: delete CI password data (#243) 2023-10-29 10:53:29 +08:00
pycook
af40004fe9 fix(cmdb-ui): CI update password 2023-10-28 23:07:01 +08:00
pycook
d86eb1c5eb release: v2.3.6 2023-10-28 17:51:13 +08:00
pycook
fef3bfceaf feat(cmdb-api): ci password 2023-10-28 16:55:02 +08:00
pycook
3a4f0b248f feat(cmdb-api): CI password data store (#242)
* add secrets,for test

* feat: vault SDK (#238)

* feat: vault SDK

* docs: i18n

* perf(vault): format code

* feat(secrets): support vault

* feat: add inner password storage

* feat: secrets

* feat: add inner password storage

* feat: add secrets feature

* perf(secrets): review

---------

Co-authored-by: fxiang21 <fxiang21@126.com>
Co-authored-by: Mimo <osatmnzn@gmail.com>
2023-10-28 16:19:00 +08:00
wang-liang0615
ffa3d7cd43 feat:预定义值支持脚本&&密码存储&&一些bugfix (#239) 2023-10-27 11:10:43 +08:00
ivonGwy
6aefac98cd Doc (#235)
* change assignees
2023-10-25 16:00:04 +08:00
ivonGwy
22989f8d5a Doc (#234)
* final template
2023-10-25 15:52:12 +08:00
ivonGwy
ebe9d1e29f Doc (#232)
* fix bugs
2023-10-25 14:54:23 +08:00
ivonGwy
f1dd5ca074 Update issue templates 2023-10-25 14:46:44 +08:00
ivonGwy
4dd95f0d7e Doc (#231)
* add issue template
2023-10-25 14:36:36 +08:00
ivonGwy
7e3e248c2b Update issue templates 2023-10-25 14:06:24 +08:00
ivonGwy
a5ff1139f7 Update issue templates 2023-10-25 14:04:53 +08:00
pycook
00135f4644 fix(api): add ci (#230) 2023-10-25 13:51:29 +08:00
ivonGwy
8297d4c9b4 Doc (#229)
* change reandme
2023-10-25 13:19:30 +08:00
kdyq007
5143539593 关闭前端密码加密;加强 ldap 用户验证 (#216)
* [更新] python-ldap 更新到 ldap3

* [更新] 关闭前端密码加密;加强 ldap 用户验证

* Update app.js

---------

Co-authored-by: sherlock <sherlock@gmail.com>
Co-authored-by: pycook <pycook@126.com>
2023-10-24 19:47:46 +08:00
pycook
a6eb2f0d21 feat: Predefined values support executing scripts (#227) 2023-10-24 19:32:43 +08:00
simontigers
07a63bef6e fix: add_employee_from_acl (#225) 2023-10-24 14:20:40 +08:00
wang-liang0615
d69efeea25 fix:关系视图删除关系接口传参修改 (#224)
* fix:acl新增用户展示异常问题

* fix:关系视图删除关系接口传参修改
2023-10-24 06:04:21 +08:00
wang-liang0615
0ef67360ad fix:acl新增用户展示异常问题 (#223) 2023-10-24 05:59:08 +08:00
pycook
e2f993bc11 feat: add cryptography to requirements 2023-10-23 14:37:01 +08:00
pycook
05d2795e79 fix: acl cache 2023-10-23 13:57:06 +08:00
Evan Sung
6ff77a140c fix(common): fix 'ACLManager' object has no attribute 'create_app' (#217) 2023-10-21 11:38:19 +08:00
Evan Sung
6503d32e6e fix(ci_cache): ci cache async args (#215) 2023-10-20 12:05:19 +08:00
kdyq007
887a69c2bd feat: python-ldap 更新到 ldap3 (#214)
Co-authored-by: sherlock <sherlock@gmail.com>
2023-10-20 09:36:38 +08:00
pycook
6d052eaffc Dev api 20231019 (#210)
* fix(acl): get resources

* fix(celery worker): db server has gone away
2023-10-19 11:51:34 +08:00
wang-liang0615
d0f0bf84dd fix:ci relation add type filter (#208) 2023-10-18 14:06:28 +08:00
pycook
802fda66e7 fix: ci relation statistics 2023-10-18 13:35:01 +08:00
pycook
c95747c88a docs: api doc 2023-10-17 12:06:37 +08:00
wang-liang0615
ed49b238d8 feat:webhook body 支持非json (#203) 2023-10-17 10:44:38 +08:00
Evan Sung
375f0879fb Feature db migrate 20231013 (#202)
* feat(db): support flask migrate

* minor

---------

Co-authored-by: s01249 <songbing@smyfinancial.com>
2023-10-13 16:24:49 +08:00
Evan Sung
8bc1893ca9 feat(db): support flask migrate (#201)
Co-authored-by: s01249 <songbing@smyfinancial.com>
2023-10-13 15:55:26 +08:00
simontigers
53cd2342bf fix: common perms (#200) 2023-10-12 16:02:35 +08:00
wang-liang0615
eff6d974d4 pref:批量上传&资源管理小优化 (#199) 2023-10-12 15:06:39 +08:00
ivonGwy
8478d2f858 Merge pull request #197 from veops/doc
Doc
2023-10-11 14:34:05 +08:00
ivonGwy
80e99cc335 change wechat pic size 2023-10-11 14:33:19 +08:00
ivonGwy
8f64fc4aa0 change wechat pic 2023-10-11 14:28:49 +08:00
pycook
cfc345c993 Dev api (#196)
* docs: update

* docs: README & Makefile
2023-10-11 13:40:15 +08:00
wang-liang0615
928116d0b5 Dev UI 231009 (#195)
* pref:用户密钥非必填

* fix:chartColor undefined
2023-10-11 09:12:04 +08:00
pycook
4c2e6ae69f docs: update (#194) 2023-10-10 16:53:30 +08:00
wang-liang0615
a2c75fd34e pref:用户密钥非必填 (#193) 2023-10-10 09:25:24 +08:00
99 changed files with 9796 additions and 7184 deletions

60
.github/ISSUE_TEMPLATE/1bug.yaml vendored Normal file
View File

@@ -0,0 +1,60 @@
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

44
.github/ISSUE_TEMPLATE/2feature.yaml vendored Normal file
View File

@@ -0,0 +1,44 @@
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

@@ -0,0 +1,36 @@
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

60
.github/ISSUE_TEMPLATE/bug.yaml vendored Normal file
View File

@@ -0,0 +1,60 @@
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

6
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

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

View File

44
.github/ISSUE_TEMPLATE/feature.yaml vendored Normal file
View File

@@ -0,0 +1,44 @@
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 Normal file
View File

1
.gitignore vendored
View File

@@ -40,6 +40,7 @@ nosetests.xml
.pytest_cache .pytest_cache
cmdb-api/test-output cmdb-api/test-output
cmdb-api/api/uploaded_files cmdb-api/api/uploaded_files
cmdb-api/migrations/versions
# Translations # Translations
*.mo *.mo

View File

@@ -9,7 +9,7 @@ help: ## display this help
env: ## create a development environment using pipenv env: ## create a development environment using pipenv
sudo easy_install pip && \ sudo easy_install pip && \
pip install pipenv -i https://pypi.douban.com/simple && \ pip install pipenv -i https://repo.huaweicloud.com/repository/pypi/simple && \
npm install yarn && \ npm install yarn && \
make deps make deps
.PHONY: env .PHONY: env
@@ -36,7 +36,7 @@ api: ## start api server
.PHONY: api .PHONY: api
worker: ## start async tasks worker worker: ## start async tasks worker
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 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
.PHONY: worker .PHONY: worker
ui: ## start ui server ui: ## start ui server

View File

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

View File

@@ -24,18 +24,19 @@ supervisor = "==4.0.3"
Flask-Login = "==0.6.2" Flask-Login = "==0.6.2"
Flask-Bcrypt = "==1.0.1" Flask-Bcrypt = "==1.0.1"
Flask-Cors = ">=3.0.8" Flask-Cors = ">=3.0.8"
python-ldap = "==3.4.0" ldap3 = "==2.9.1"
pycryptodome = "==3.12.0" pycryptodome = "==3.12.0"
cryptography = ">=41.0.2"
# Caching # Caching
Flask-Caching = ">=1.0.0" Flask-Caching = ">=1.0.0"
# Environment variable parsing # Environment variable parsing
environs = "==4.2.0" environs = "==4.2.0"
marshmallow = "==2.20.2" marshmallow = "==2.20.2"
# async tasks # async tasks
celery = "==5.3.1" celery = ">=5.3.1"
celery_once = "==3.0.1" celery_once = "==3.0.1"
more-itertools = "==5.0.0" more-itertools = "==5.0.0"
kombu = "==5.3.1" kombu = ">=5.3.1"
# common setting # common setting
timeout-decorator = "==0.5.0" timeout-decorator = "==0.5.0"
WTForms = "==3.0.0" WTForms = "==3.0.0"
@@ -58,6 +59,9 @@ Jinja2 = "==3.1.2"
jinja2schema = "==0.1.4" jinja2schema = "==0.1.4"
msgpack-python = "==0.5.6" msgpack-python = "==0.5.6"
alembic = "==1.7.7" alembic = "==1.7.7"
hvac = "==2.0.0"
colorama = ">=0.4.6"
pycryptodomex = ">=3.19.0"
[dev-packages] [dev-packages]
# Testing # Testing
@@ -74,4 +78,3 @@ flake8-isort = "==2.7.0"
isort = "==4.3.21" isort = "==4.3.21"
pep8-naming = "==0.8.2" pep8-naming = "==0.8.2"
pydocstyle = "==3.0.0" pydocstyle = "==3.0.0"

View File

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

View File

@@ -7,6 +7,7 @@ import json
import time import time
import click import click
import requests
from flask import current_app from flask import current_app
from flask.cli import with_appcontext from flask.cli import with_appcontext
from flask_login import login_user from flask_login import login_user
@@ -29,6 +30,9 @@ from api.lib.perm.acl.resource import ResourceCRUD
from api.lib.perm.acl.resource import ResourceTypeCRUD from api.lib.perm.acl.resource import ResourceTypeCRUD
from api.lib.perm.acl.role import RoleCRUD from api.lib.perm.acl.role import RoleCRUD
from api.lib.perm.acl.user import UserCRUD from api.lib.perm.acl.user import UserCRUD
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.models.acl import App from api.models.acl import App
from api.models.acl import ResourceType from api.models.acl import ResourceType
from api.models.cmdb import Attribute from api.models.cmdb import Attribute
@@ -53,6 +57,7 @@ def cmdb_init_cache():
if relations: if relations:
rd.create_or_update(relations, REDIS_PREFIX_CI_RELATION) rd.create_or_update(relations, REDIS_PREFIX_CI_RELATION)
es = None
if current_app.config.get("USE_ES"): if current_app.config.get("USE_ES"):
from api.extensions import es from api.extensions import es
from api.models.cmdb import Attribute from api.models.cmdb import Attribute
@@ -311,3 +316,156 @@ def cmdb_index_table_upgrade():
CIIndexValueDateTime.create(ci_id=i.ci_id, attr_id=i.attr_id, value=i.value, commit=False) CIIndexValueDateTime.create(ci_id=i.ci_id, attr_id=i.attr_id, value=i.value, commit=False)
i.delete(commit=False) i.delete(commit=False)
db.session.commit() 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
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:
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))
continue
else:
continue
i.delete()

View File

@@ -165,31 +165,48 @@ class InitDepartment(object):
acl = self.check_app('backend') acl = self.check_app('backend')
resources_types = acl.get_all_resources_types() 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'])) results = list(filter(lambda t: t['name'] == '操作权限', resources_types['groups']))
if len(results) == 0: if len(results) == 0:
payload = dict( payload = dict(
app_id=acl.app_name, app_id=acl.app_name,
name='操作权限', name='操作权限',
description='', description='',
perms=['read', 'grant', 'delete', 'update'] perms=perms
) )
resource_type = acl.create_resources_type(payload) resource_type = acl.create_resources_type(payload)
else: else:
resource_type = results[0] 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 ['公司信息', '公司架构', '通知设置']: for name in ['公司信息', '公司架构', '通知设置']:
payload = dict( target = list(filter(lambda r: r['name'] == name, resource_list))
type_id=resource_type['id'], if len(target) == 0:
app_id=acl.app_name, payload = dict(
name=name, type_id=resource_type['id'],
) app_id=acl.app_name,
try: name=name,
acl.create_resource(payload) )
except Exception as e: resource = acl.create_resource(payload)
if '已经存在' in str(e): else:
pass resource = target[0]
else:
raise Exception(e) if acl_rid > 0:
acl.grant_resource(acl_rid, resource['id'], perms)
def check_app(self, app_name): def check_app(self, app_name):
acl = ACLManager(app_name) acl = ACLManager(app_name)
@@ -199,10 +216,9 @@ class InitDepartment(object):
) )
try: try:
app = acl.validate_app() app = acl.validate_app()
if app: if not app:
return acl acl.create_app(payload)
return acl
acl.create_app(payload)
except Exception as e: except Exception as e:
current_app.logger.error(e) current_app.logger.error(e)
if '不存在' in str(e): if '不存在' in str(e):
@@ -210,6 +226,10 @@ class InitDepartment(object):
return acl return acl
raise Exception(e) raise Exception(e)
def get_admin_user_rid(self):
admin = Employee.get_by(first=True, username='admin', to_dict=False)
return admin.acl_rid if admin else 0
@click.command() @click.command()
@with_appcontext @with_appcontext

View File

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

View File

@@ -60,22 +60,33 @@ class AttributeManager(object):
return [] return []
@staticmethod @staticmethod
def _get_choice_values_from_other_ci(choice_other): def _get_choice_values_from_other(choice_other):
from api.lib.cmdb.search import SearchError from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search from api.lib.cmdb.search.ci import search
type_ids = choice_other.get('type_ids') if choice_other.get('type_ids'):
attr_id = choice_other.get('attr_id') type_ids = choice_other.get('type_ids')
other_filter = choice_other.get('filter') or '' attr_id = choice_other.get('attr_id')
other_filter = choice_other.get('filter') or ''
query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter) query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter)
s = search(query, fl=[str(attr_id)], facet=[str(attr_id)], count=1) s = search(query, fl=[str(attr_id)], facet=[str(attr_id)], count=1)
try: try:
_, _, _, _, _, facet = s.search() _, _, _, _, _, facet = s.search()
return [[i[0], {}] for i in (list(facet.values()) or [[]])[0]] return [[i[0], {}] for i in (list(facet.values()) or [[]])[0]]
except SearchError as e: except SearchError as e:
current_app.logger.error("get choice values from other ci failed: {}".format(e)) current_app.logger.error("get choice values from other ci failed: {}".format(e))
return [] return []
elif choice_other.get('script'):
try:
x = compile(choice_other['script'], '', "exec")
exec(x)
res = locals()['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 []
@classmethod @classmethod
def get_choice_values(cls, attr_id, value_type, choice_web_hook, choice_other, def get_choice_values(cls, attr_id, value_type, choice_web_hook, choice_other,
@@ -87,7 +98,7 @@ class AttributeManager(object):
return [] return []
elif choice_other: elif choice_other:
if choice_other_parse and isinstance(choice_other, dict): if choice_other_parse and isinstance(choice_other, dict):
return cls._get_choice_values_from_other_ci(choice_other) return cls._get_choice_values_from_other(choice_other)
else: else:
return [] return []
@@ -96,7 +107,8 @@ class AttributeManager(object):
return [] return []
choice_values = choice_table.get_by(fl=["value", "option"], attr_id=attr_id) choice_values = choice_table.get_by(fl=["value", "option"], attr_id=attr_id)
return [[choice_value['value'], choice_value['option']] for choice_value in choice_values] return [[ValueTypeMap.serialize[value_type](choice_value['value']), choice_value['option']]
for choice_value in choice_values]
@staticmethod @staticmethod
def add_choice_values(_id, value_type, choice_values): def add_choice_values(_id, value_type, choice_values):
@@ -218,10 +230,15 @@ class AttributeManager(object):
if name in BUILTIN_KEYWORDS: if name in BUILTIN_KEYWORDS:
return abort(400, ErrFormat.attribute_name_cannot_be_builtin) return abort(400, ErrFormat.attribute_name_cannot_be_builtin)
if kwargs.get('choice_other'): while kwargs.get('choice_other'):
if (not isinstance(kwargs['choice_other'], dict) or not kwargs['choice_other'].get('type_ids') or if isinstance(kwargs['choice_other'], dict):
not kwargs['choice_other'].get('attr_id')): if kwargs['choice_other'].get('script'):
return abort(400, ErrFormat.attribute_choice_other_invalid) break
if kwargs['choice_other'].get('type_ids') and kwargs['choice_other'].get('attr_id'):
break
return abort(400, ErrFormat.attribute_choice_other_invalid)
alias = kwargs.pop("alias", "") alias = kwargs.pop("alias", "")
alias = name if not alias else alias alias = name if not alias else alias
@@ -232,6 +249,8 @@ class AttributeManager(object):
kwargs.get('is_computed') and cls.can_create_computed_attribute() 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, attr = Attribute.create(flush=True,
name=name, name=name,
alias=alias, alias=alias,
@@ -337,10 +356,15 @@ class AttributeManager(object):
self._change_index(attr, attr.is_index, kwargs['is_index']) self._change_index(attr, attr.is_index, kwargs['is_index'])
if kwargs.get('choice_other'): while kwargs.get('choice_other'):
if (not isinstance(kwargs['choice_other'], dict) or not kwargs['choice_other'].get('type_ids') or if isinstance(kwargs['choice_other'], dict):
not kwargs['choice_other'].get('attr_id')): if kwargs['choice_other'].get('script'):
return abort(400, ErrFormat.attribute_choice_other_invalid) break
if kwargs['choice_other'].get('type_ids') and kwargs['choice_other'].get('attr_id'):
break
return abort(400, ErrFormat.attribute_choice_other_invalid)
existed2 = attr.to_dict() existed2 = attr.to_dict()
if not existed2['choice_web_hook'] and not existed2.get('choice_other') and existed2['is_choice']: if not existed2['choice_web_hook'] and not existed2.get('choice_other') and existed2['is_choice']:

View File

@@ -29,6 +29,7 @@ from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import REDIS_PREFIX_CI from api.lib.cmdb.const import REDIS_PREFIX_CI
from api.lib.cmdb.const import ResourceTypeEnum from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RetKey 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 AttributeHistoryManger
from api.lib.cmdb.history import CIRelationHistoryManager from api.lib.cmdb.history import CIRelationHistoryManager
from api.lib.cmdb.history import CITriggerHistoryManager from api.lib.cmdb.history import CITriggerHistoryManager
@@ -42,8 +43,10 @@ from api.lib.notify import notify_send
from api.lib.perm.acl.acl import ACLManager 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 is_app_admin
from api.lib.perm.acl.acl import validate_permission from api.lib.perm.acl.acl import validate_permission
from api.lib.utils import Lock from api.lib.secrets.inner import InnerCrypt
from api.lib.secrets.vault import VaultClient
from api.lib.utils import handle_arg_list from api.lib.utils import handle_arg_list
from api.lib.utils import Lock
from api.lib.webhook import webhook_request from api.lib.webhook import webhook_request
from api.models.cmdb import AttributeHistory from api.models.cmdb import AttributeHistory
from api.models.cmdb import AutoDiscoveryCI from api.models.cmdb import AutoDiscoveryCI
@@ -60,6 +63,7 @@ from api.tasks.cmdb import ci_relation_cache
from api.tasks.cmdb import ci_relation_delete from api.tasks.cmdb import ci_relation_delete
PRIVILEGED_USERS = {"worker", "cmdb_agent", "agent"} PRIVILEGED_USERS = {"worker", "cmdb_agent", "agent"}
PASSWORD_DEFAULT_SHOW = "******"
class CIManager(object): class CIManager(object):
@@ -323,6 +327,8 @@ class CIManager(object):
ci_attr2type_attr = {type_attr.attr_id: type_attr for type_attr, _ in attrs} ci_attr2type_attr = {type_attr.attr_id: type_attr for type_attr, _ in attrs}
ci = None ci = None
record_id = None
password_dict = {}
need_lock = current_user.username not in current_app.config.get('PRIVILEGED_USERS', PRIVILEGED_USERS) need_lock = current_user.username not in current_app.config.get('PRIVILEGED_USERS', PRIVILEGED_USERS)
with Lock(ci_type_name, need_lock=need_lock): with Lock(ci_type_name, need_lock=need_lock):
existed = cls.ci_is_exist(unique_key, unique_value, ci_type.id) existed = cls.ci_is_exist(unique_key, unique_value, ci_type.id)
@@ -351,14 +357,23 @@ class CIManager(object):
ci_dict.get(attr.name) is None and ci_dict.get(attr.alias) is None)): ci_dict.get(attr.name) is None and ci_dict.get(attr.alias) is None)):
ci_dict[attr.name] = attr.default.get('default') ci_dict[attr.name] = attr.default.get('default')
if type_attr.is_required and (attr.name not in ci_dict and attr.alias not in ci_dict): if (type_attr.is_required and not attr.is_computed and
(attr.name not in ci_dict and attr.alias not in ci_dict)):
return abort(400, ErrFormat.attribute_value_required.format(attr.name)) return abort(400, ErrFormat.attribute_value_required.format(attr.name))
else: else:
for type_attr, attr in attrs: for type_attr, attr in attrs:
if attr.default and attr.default.get('default') == AttributeDefaultValueEnum.UPDATED_AT: if attr.default and attr.default.get('default') == AttributeDefaultValueEnum.UPDATED_AT:
ci_dict[attr.name] = now ci_dict[attr.name] = now
computed_attrs = [attr.to_dict() for _, attr in attrs if attr.is_computed] or None 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)
value_manager = AttributeValueManager() value_manager = AttributeValueManager()
@@ -395,6 +410,10 @@ class CIManager(object):
cls.delete(ci.id) cls.delete(ci.id)
raise e 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 if record_id: # has change
ci_cache.apply_async(args=(ci.id, operate_type, record_id), queue=CMDB_QUEUE) ci_cache.apply_async(args=(ci.id, operate_type, record_id), queue=CMDB_QUEUE)
@@ -414,7 +433,16 @@ class CIManager(object):
if attr.default and attr.default.get('default') == AttributeDefaultValueEnum.UPDATED_AT: if attr.default and attr.default.get('default') == AttributeDefaultValueEnum.UPDATED_AT:
ci_dict[attr.name] = now ci_dict[attr.name] = now
computed_attrs = [attr.to_dict() for _, attr in attrs if attr.is_computed] or None 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)
value_manager = AttributeValueManager() value_manager = AttributeValueManager()
@@ -423,6 +451,7 @@ class CIManager(object):
limit_attrs = self._valid_ci_for_no_read(ci) if not _is_admin else {} 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) 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): with Lock(ci.ci_type.name, need_lock=need_lock):
self._valid_unique_constraint(ci.type_id, ci_dict, ci_id) self._valid_unique_constraint(ci.type_id, ci_dict, ci_id)
@@ -440,6 +469,10 @@ class CIManager(object):
except BadRequest as e: except BadRequest as e:
raise 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 if record_id: # has change
ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE) ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE)
@@ -465,16 +498,17 @@ class CIManager(object):
ci_dict = cls.get_cis_by_ids([ci_id]) ci_dict = cls.get_cis_by_ids([ci_id])
ci_dict = ci_dict and ci_dict[0] ci_dict = ci_dict and ci_dict[0]
triggers = CITriggerManager.get(ci_dict['_type']) if ci_dict:
for trigger in triggers: triggers = CITriggerManager.get(ci_dict['_type'])
option = trigger['option'] for trigger in triggers:
if not option.get('enable') or option.get('action') != OperateType.DELETE: option = trigger['option']
continue 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']): if option.get('filter') and not CITriggerManager.ci_filter(ci_dict.get('_id'), option['filter']):
continue 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) 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]) attr_names = set([AttributeCache.get(attr.attr_id).name for attr in attrs])
@@ -498,7 +532,8 @@ class CIManager(object):
db.session.commit() db.session.commit()
AttributeHistoryManger.add(None, ci_id, [(None, OperateType.DELETE, ci_dict, None)], ci.type_id) if ci_dict:
AttributeHistoryManger.add(None, ci_id, [(None, OperateType.DELETE, ci_dict, None)], ci.type_id)
ci_delete.apply_async(args=(ci_id,), queue=CMDB_QUEUE) ci_delete.apply_async(args=(ci_id,), queue=CMDB_QUEUE)
@@ -600,7 +635,7 @@ class CIManager(object):
_fields = list() _fields = list()
for field in fields: for field in fields:
attr = AttributeCache.get(field) attr = AttributeCache.get(field)
if attr is not None: if attr is not None and not attr.is_password:
_fields.append(str(attr.id)) _fields.append(str(attr.id))
filter_fields_sql = "WHERE A.attr_id in ({0})".format(",".join(_fields)) filter_fields_sql = "WHERE A.attr_id in ({0})".format(",".join(_fields))
@@ -618,7 +653,7 @@ class CIManager(object):
ci_dict = dict() ci_dict = dict()
unique_id2obj = dict() unique_id2obj = dict()
excludes = excludes and set(excludes) excludes = excludes and set(excludes)
for ci_id, type_id, attr_id, attr_name, attr_alias, value, value_type, is_list in cis: for ci_id, type_id, attr_id, attr_name, attr_alias, value, value_type, is_list, is_password in cis:
if not fields and excludes and (attr_name in excludes or attr_alias in excludes): if not fields and excludes and (attr_name in excludes or attr_alias in excludes):
continue continue
@@ -645,11 +680,14 @@ class CIManager(object):
else: else:
return abort(400, ErrFormat.argument_invalid.format("ret_key")) return abort(400, ErrFormat.argument_invalid.format("ret_key"))
value = ValueTypeMap.serialize2[value_type](value) if is_password and value:
if is_list: ci_dict[attr_key] = PASSWORD_DEFAULT_SHOW
ci_dict.setdefault(attr_key, []).append(value)
else: else:
ci_dict[attr_key] = value value = ValueTypeMap.serialize2[value_type](value)
if is_list:
ci_dict.setdefault(attr_key, []).append(value)
else:
ci_dict[attr_key] = value
return res return res
@@ -681,6 +719,84 @@ class CIManager(object):
return cls._get_cis_from_db(ci_ids, ret_key, fields, value_tables, excludes=excludes) 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): class CIRelationManager(object):
""" """

View File

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

View File

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

View File

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

View File

@@ -28,6 +28,7 @@ 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_CI_BY_TYPE
from api.lib.cmdb.search.ci.db.query_sql import QUERY_UNION_CI_ATTRIBUTE_IS_NULL 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 TableMap
from api.lib.cmdb.utils import ValueTypeMap
from api.lib.perm.acl.acl import ACLManager 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 is_app_admin
from api.lib.utils import handle_arg_list from api.lib.utils import handle_arg_list
@@ -524,15 +525,15 @@ class Search(object):
if k: if k:
table_name = TableMap(attr=attr).table_name table_name = TableMap(attr=attr).table_name
query_sql = FACET_QUERY.format(table_name, self.query_sql, attr.id) query_sql = FACET_QUERY.format(table_name, self.query_sql, attr.id)
# current_app.logger.warning(query_sql)
result = db.session.execute(query_sql).fetchall() result = db.session.execute(query_sql).fetchall()
facet[k] = result facet[k] = result
facet_result = dict() facet_result = dict()
for k, v in facet.items(): for k, v in facet.items():
if not k.startswith('_'): if not k.startswith('_'):
a = getattr(AttributeCache.get(k), self.ret_key) attr = AttributeCache.get(k)
facet_result[a] = [(f[0], f[1], a) for f in v] a = getattr(attr, self.ret_key)
facet_result[a] = [(ValueTypeMap.serialize[attr.value_type](f[0]), f[1], a) for f in v]
return facet_result 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.sort = sort or ("ci_id" if current_app.config.get("USE_ES") else None)
self.root_id = root_id self.root_id = root_id
self.level = level self.level = level or 0
self.reverse = reverse self.reverse = reverse
def _get_ids(self): def _get_ids(self):
@@ -104,16 +104,22 @@ class Search(object):
ci_ids=merge_ids).search() ci_ids=merge_ids).search()
def statistics(self, type_ids): def statistics(self, type_ids):
self.level = int(self.level)
_tmp = [] _tmp = []
ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id
for l in range(0, int(self.level)): for lv in range(0, self.level):
if not l: if not lv:
_tmp = list(map(lambda x: list(json.loads(x).items()), if type_ids and lv == self.level - 1:
[i or '{}' for i in rd.get(ids, REDIS_PREFIX_CI_RELATION) or []])) _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 []]))
else: else:
for idx, item in enumerate(_tmp): for idx, item in enumerate(_tmp):
if item: if item:
if type_ids and l == self.level - 1: if type_ids and lv == self.level - 1:
__tmp = list( __tmp = list(
map(lambda x: [(_id, type_id) for _id, type_id in json.loads(x).items() map(lambda x: [(_id, type_id) for _id, type_id in json.loads(x).items()
if type_id in type_ids], 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.cache import AttributeCache
from api.lib.cmdb.const import ValueTypeEnum 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): def string2int(x):
@@ -21,7 +21,7 @@ def string2int(x):
def str2datetime(x): def str2datetime(x):
try: try:
return datetime.datetime.strptime(x, "%Y-%m-%d") return datetime.datetime.strptime(x, "%Y-%m-%d").date()
except ValueError: except ValueError:
pass pass
@@ -44,8 +44,8 @@ class ValueTypeMap(object):
ValueTypeEnum.FLOAT: float, ValueTypeEnum.FLOAT: float,
ValueTypeEnum.TEXT: lambda x: x if isinstance(x, six.string_types) else str(x), 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.TIME: lambda x: x if isinstance(x, six.string_types) else str(x),
ValueTypeEnum.DATE: lambda x: x.strftime("%Y-%m-%d"), 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"), ValueTypeEnum.DATETIME: lambda x: x.strftime("%Y-%m-%d %H:%M:%S") if not isinstance(x, six.string_types) else x,
ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x, ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x,
} }
@@ -64,6 +64,8 @@ class ValueTypeMap(object):
ValueTypeEnum.FLOAT: model.FloatChoice, ValueTypeEnum.FLOAT: model.FloatChoice,
ValueTypeEnum.TEXT: model.TextChoice, ValueTypeEnum.TEXT: model.TextChoice,
ValueTypeEnum.TIME: model.TextChoice, ValueTypeEnum.TIME: model.TextChoice,
ValueTypeEnum.DATE: model.TextChoice,
ValueTypeEnum.DATETIME: model.TextChoice,
} }
table = { table = {
@@ -97,7 +99,7 @@ class ValueTypeMap(object):
ValueTypeEnum.DATE: 'text', ValueTypeEnum.DATE: 'text',
ValueTypeEnum.TIME: 'text', ValueTypeEnum.TIME: 'text',
ValueTypeEnum.FLOAT: 'float', ValueTypeEnum.FLOAT: 'float',
ValueTypeEnum.JSON: 'object' ValueTypeEnum.JSON: 'object',
} }
@@ -110,7 +112,9 @@ class TableMap(object):
@property @property
def table(self): def table(self):
attr = AttributeCache.get(self.attr_name) if not self.attr else self.attr attr = AttributeCache.get(self.attr_name) if not self.attr else self.attr
if attr.value_type != ValueTypeEnum.TEXT and attr.value_type != ValueTypeEnum.JSON: if attr.is_password or attr.is_link:
self.is_index = False
elif attr.value_type not in {ValueTypeEnum.TEXT, ValueTypeEnum.JSON}:
self.is_index = True self.is_index = True
elif self.is_index is None: elif self.is_index is None:
self.is_index = attr.is_index self.is_index = attr.is_index
@@ -122,7 +126,9 @@ class TableMap(object):
@property @property
def table_name(self): def table_name(self):
attr = AttributeCache.get(self.attr_name) if not self.attr else self.attr attr = AttributeCache.get(self.attr_name) if not self.attr else self.attr
if attr.value_type != ValueTypeEnum.TEXT and attr.value_type != ValueTypeEnum.JSON: if attr.is_password or attr.is_link:
self.is_index = False
elif attr.value_type not in {ValueTypeEnum.TEXT, ValueTypeEnum.JSON}:
self.is_index = True self.is_index = True
elif self.is_index is None: elif self.is_index is None:
self.is_index = attr.is_index self.is_index = attr.is_index

View File

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

View File

@@ -1,12 +1,13 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
from flask import abort
from flask import current_app from flask import current_app
from api.lib.common_setting.resp_format import ErrFormat 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.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.role import RoleCRUD, RoleRelationCRUD
from api.lib.perm.acl.user import UserCRUD from api.lib.perm.acl.user import UserCRUD
from api.lib.perm.acl.resource import ResourceTypeCRUD, ResourceCRUD
class ACLManager(object): class ACLManager(object):
@@ -109,8 +110,32 @@ class ACLManager(object):
id2perms=id2perms 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): def create_resource(self, payload):
payload['app_id'] = self.validate_app().id payload['app_id'] = self.validate_app().id
resource = ResourceCRUD.add(**payload) resource = ResourceCRUD.add(**payload)
return resource.to_dict() 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

@@ -121,6 +121,19 @@ class EmployeeCRUD(object):
employee = CreateEmployee().create_single(**data) employee = CreateEmployee().create_single(**data)
return employee.to_dict() 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 @staticmethod
def add(**kwargs): def add(**kwargs):
try: try:

View File

@@ -4,8 +4,14 @@
from functools import wraps from functools import wraps
from flask import abort from flask import abort
from flask import current_app
from flask import request 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 from api.lib.resp_format import CommonErrFormat
@@ -70,3 +76,43 @@ def args_validate(model_cls, exclude_args=None):
return wrapper return wrapper
return decorate 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 import msgpack
from api.extensions import cache from api.extensions import cache
from api.extensions import db from api.lib.decorator import flush_db
from api.lib.utils import Lock from api.lib.utils import Lock
from api.models.acl import App from api.models.acl import App
from api.models.acl import Permission from api.models.acl import Permission
@@ -221,9 +221,9 @@ class RoleRelationCache(object):
return msgpack.loads(r_g, raw=False) return msgpack.loads(r_g, raw=False)
@classmethod @classmethod
@flush_db
def rebuild(cls, rid, app_id): def rebuild(cls, rid, app_id):
cls.clean(rid, app_id) cls.clean(rid, app_id)
db.session.remove()
cls.get_parent_ids(rid, app_id) cls.get_parent_ids(rid, app_id)
cls.get_child_ids(rid, app_id) cls.get_child_ids(rid, app_id)
@@ -235,9 +235,9 @@ class RoleRelationCache(object):
cls.get_resources2(rid, app_id) cls.get_resources2(rid, app_id)
@classmethod @classmethod
@flush_db
def rebuild2(cls, rid, app_id): def rebuild2(cls, rid, app_id):
cache.delete(cls.PREFIX_RESOURCES2.format(rid, app_id)) cache.delete(cls.PREFIX_RESOURCES2.format(rid, app_id))
db.session.remove()
cls.get_resources2(rid, app_id) cls.get_resources2(rid, app_id)
@classmethod @classmethod

View File

@@ -260,7 +260,8 @@ class ResourceCRUD(object):
numfound = query.count() numfound = query.count()
res = [i.to_dict() for i in query.offset((page - 1) * page_size).limit(page_size)] res = [i.to_dict() for i in query.offset((page - 1) * page_size).limit(page_size)]
for i in res: for i in res:
i['user'] = UserCache.get(i['uid']).nickname if i['uid'] else '' user = UserCache.get(i['uid']) if i['uid'] else ''
i['user'] = user and user.nickname
return numfound, res return numfound, res

View File

@@ -58,10 +58,14 @@ class UserCRUD(object):
kwargs['employee_id'] = '{0:04d}'.format(biggest_employee_id + 1) kwargs['employee_id'] = '{0:04d}'.format(biggest_employee_id + 1)
user = User.create(**kwargs) user = User.create(**kwargs)
RoleCRUD.add_role(user.username, uid=user.uid) role = RoleCRUD.add_role(user.username, uid=user.uid)
AuditCRUD.add_role_log(None, AuditOperateType.create, AuditCRUD.add_role_log(None, AuditOperateType.create,
AuditScope.user, user.uid, {}, user.to_dict(), {}, {} 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 return user

View File

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

View File

@@ -0,0 +1,429 @@
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] == "run"):
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

@@ -0,0 +1,35 @@
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

@@ -0,0 +1,141 @@
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

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

View File

@@ -504,3 +504,10 @@ class CIFilterPerms(Model):
attr_filter = db.Column(db.Text) attr_filter = db.Column(db.Text)
rid = db.Column(db.Integer, index=True) 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,5 +46,4 @@ def register_resources(resource_path, rest_api):
resource_cls.url_prefix = ("",) resource_cls.url_prefix = ("",)
if isinstance(resource_cls.url_prefix, six.string_types): if isinstance(resource_cls.url_prefix, six.string_types):
resource_cls.url_prefix = (resource_cls.url_prefix,) resource_cls.url_prefix = (resource_cls.url_prefix,)
rest_api.add_resource(resource_cls, *resource_cls.url_prefix) rest_api.add_resource(resource_cls, *resource_cls.url_prefix)

View File

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

View File

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

View File

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

@@ -0,0 +1,37 @@
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

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

View File

@@ -0,0 +1,45 @@
# 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

110
cmdb-api/migrations/env.py Normal file
View File

@@ -0,0 +1,110 @@
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

@@ -0,0 +1,24 @@
"""${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

@@ -0,0 +1,360 @@
"""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 -i https://mirrors.aliyun.com/pypi/simple
alembic==1.7.7 alembic==1.7.7
bs4==0.0.1 bs4==0.0.1
celery==5.3.1 celery>=5.3.1
celery-once==3.0.1 celery-once==3.0.1
click==8.1.3 click==8.1.3
elasticsearch==7.17.9 elasticsearch==7.17.9
@@ -18,21 +18,22 @@ Flask-RESTful==0.3.10
Flask-SQLAlchemy==2.5.0 Flask-SQLAlchemy==2.5.0
future==0.18.3 future==0.18.3
gunicorn==21.0.1 gunicorn==21.0.1
hvac==2.0.0
itsdangerous==2.1.2 itsdangerous==2.1.2
Jinja2==3.1.2 Jinja2==3.1.2
jinja2schema==0.1.4 jinja2schema==0.1.4
jsonschema==4.18.0 jsonschema==4.18.0
kombu==5.3.1 kombu>=5.3.1
Mako==1.2.4 Mako==1.2.4
MarkupSafe==2.1.3 MarkupSafe==2.1.3
marshmallow==2.20.2 marshmallow==2.20.2
more-itertools==5.0.0 more-itertools==5.0.0
msgpack-python==0.5.6 msgpack-python==0.5.6
Pillow==9.3.0 Pillow==9.3.0
pycryptodome==3.12.0 cryptography==41.0.2
PyJWT==2.4.0 PyJWT==2.4.0
PyMySQL==1.1.0 PyMySQL==1.1.0
python-ldap==3.4.0 ldap3==2.9.1
PyYAML==6.0 PyYAML==6.0
redis==4.6.0 redis==4.6.0
requests==2.31.0 requests==2.31.0
@@ -46,3 +47,7 @@ toposort==1.10
treelib==1.6.1 treelib==1.6.1
Werkzeug==2.3.6 Werkzeug==2.3.6
WTForms==3.0.0 WTForms==3.0.0
shamir~=17.12.0
hvac~=2.0.0
pycryptodomex>=3.19.0
colorama>=0.4.6

View File

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

View File

@@ -54,6 +54,48 @@
<div class="content unicode" style="display: block;"> <div class="content unicode" style="display: block;">
<ul class="icon_lists dib-box"> <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"> <li class="dib">
<span class="icon iconfont">&#xe88e;</span> <span class="icon iconfont">&#xe88e;</span>
<div class="name">wechatApp</div> <div class="name">wechatApp</div>
@@ -4002,9 +4044,9 @@
<pre><code class="language-css" <pre><code class="language-css"
>@font-face { >@font-face {
font-family: 'iconfont'; font-family: 'iconfont';
src: url('iconfont.woff2?t=1696815443987') format('woff2'), src: url('iconfont.woff2?t=1698273699449') format('woff2'),
url('iconfont.woff?t=1696815443987') format('woff'), url('iconfont.woff?t=1698273699449') format('woff'),
url('iconfont.ttf?t=1696815443987') format('truetype'); url('iconfont.ttf?t=1698273699449') format('truetype');
} }
</code></pre> </code></pre>
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3> <h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@@ -4030,6 +4072,69 @@
<div class="content font-class"> <div class="content font-class">
<ul class="icon_lists dib-box"> <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"> <li class="dib">
<span class="icon iconfont wechatApp"></span> <span class="icon iconfont wechatApp"></span>
<div class="name"> <div class="name">
@@ -9952,6 +10057,62 @@
<div class="content symbol"> <div class="content symbol">
<ul class="icon_lists dib-box"> <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"> <li class="dib">
<svg class="icon svg-icon" aria-hidden="true"> <svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#wechatApp"></use> <use xlink:href="#wechatApp"></use>

View File

@@ -1,8 +1,8 @@
@font-face { @font-face {
font-family: "iconfont"; /* Project id 3857903 */ font-family: "iconfont"; /* Project id 3857903 */
src: url('iconfont.woff2?t=1696815443987') format('woff2'), src: url('iconfont.woff2?t=1698273699449') format('woff2'),
url('iconfont.woff?t=1696815443987') format('woff'), url('iconfont.woff?t=1698273699449') format('woff'),
url('iconfont.ttf?t=1696815443987') format('truetype'); url('iconfont.ttf?t=1698273699449') format('truetype');
} }
.iconfont { .iconfont {
@@ -13,6 +13,34 @@
-moz-osx-font-smoothing: grayscale; -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 { .wechatApp:before {
content: "\e88e"; content: "\e88e";
} }

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,55 @@
"css_prefix_text": "", "css_prefix_text": "",
"description": "", "description": "",
"glyphs": [ "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", "icon_id": "37590786",
"name": "wechatApp", "name": "wechatApp",

Binary file not shown.

View File

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

View File

@@ -1,57 +1,45 @@
<template> <template>
<span> <span>
<ops-icon :type="getPropertyIcon(attr)" /> <ops-icon :type="getPropertyIcon(attr)" />
</span> </span>
</template> </template>
<script> <script>
export default { export default {
name: 'ValueTypeIcon', name: 'ValueTypeIcon',
props: { props: {
attr: { attr: {
type: Object, type: Object,
default: () => {}, default: () => {},
}, },
}, },
methods: { methods: {
getPropertyStyle(attr) { getPropertyIcon(attr) {
switch (attr.value_type) { switch (attr.value_type) {
case '0': case '0':
return { color: '#cf1322', backgroundColor: '#fff1f0' } return 'icon-xianxing-shishu'
case '1': case '1':
return { color: '#d4b106', backgroundColor: '#feffe6' } return 'icon-xianxing-fudianshu'
case '2': case '2':
return { color: '#d46b08', backgroundColor: '#fff7e6' } if (attr.is_password) {
case '3': return 'icon-xianxing-password'
return { color: '#531dab', backgroundColor: '#f9f0ff' } }
case '4': if (attr.is_link) {
return { color: '#389e0d', backgroundColor: '#f6ffed' } return 'icon-xianxing-link'
case '5': }
return { color: '#08979c', backgroundColor: '#e6fffb' } return 'icon-xianxing-wenben'
case '6': case '3':
return { color: '#c41d7f', backgroundColor: '#fff0f6' } return 'icon-xianxing-datetime'
} case '4':
}, return 'icon-xianxing-date'
getPropertyIcon(attr) { case '5':
switch (attr.value_type) { return 'icon-xianxing-time'
case '0': case '6':
return 'icon-xianxing-shishu' return 'icon-xianxing-json'
case '1': }
return 'icon-xianxing-fudianshu' },
case '2': },
return 'icon-xianxing-wenben' }
case '3': </script>
return 'icon-xianxing-datetime'
case '4': <style></style>
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, // 是否在各个应用下 内联权限管理 buildAclToModules: true, // 是否在各个应用下 内联权限管理
ssoLogoutURL: '/api/sso/logout', ssoLogoutURL: '/api/sso/logout',
showDocs: false, showDocs: false,
useEncryption: true, useEncryption: false,
} }
export default appConfig export default appConfig

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -61,18 +61,18 @@ const genCmdbRoutes = async () => {
name: 'cmdb_disabled2', name: 'cmdb_disabled2',
meta: { title: '配置', disabled: true, }, meta: { title: '配置', disabled: true, },
}, },
{
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', path: '/cmdb/preference',
component: () => import('../views/preference/index'), component: () => import('../views/preference/index'),
name: 'cmdb_preference', name: 'cmdb_preference',
meta: { title: '我的订阅', icon: 'ops-cmdb-preference', selectedIcon: 'ops-cmdb-preference-selected', keepAlive: false } 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/ci_types', path: '/cmdb/ci_types',
name: 'ci_type', name: 'ci_type',

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -30,7 +30,7 @@
:key="attr.name" :key="attr.name"
v-for="attr in group.attributes" v-for="attr in group.attributes"
> >
<CiDetailAttrContent :ci="ci" :attr="attr" @refresh="refresh" /> <CiDetailAttrContent :ci="ci" :attr="attr" @refresh="refresh" @updateCIByself="updateCIByself" />
</el-descriptions-item> </el-descriptions-item>
</el-descriptions> </el-descriptions>
</div> </div>
@@ -97,6 +97,7 @@
</template> </template>
<script> <script>
import _ from 'lodash'
import { Descriptions, DescriptionsItem } from 'element-ui' import { Descriptions, DescriptionsItem } from 'element-ui'
import { getCITypeGroupById, getCITypes } from '@/modules/cmdb/api/CIType' import { getCITypeGroupById, getCITypes } from '@/modules/cmdb/api/CIType'
import { getCIHistory } from '@/modules/cmdb/api/history' import { getCIHistory } from '@/modules/cmdb/api/history'
@@ -292,6 +293,19 @@ 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> </script>

View File

@@ -1,359 +1,377 @@
<template> <template>
<CustomDrawer <CustomDrawer
:title="title + CIType.alias" :title="title + CIType.alias"
width="800" width="800"
@close="handleClose" @close="handleClose"
:maskClosable="false" :maskClosable="false"
:visible="visible" :visible="visible"
wrapClassName="create-instance-form" wrapClassName="create-instance-form"
:bodyStyle="{ paddingTop: 0 }" :bodyStyle="{ paddingTop: 0 }"
:headerStyle="{ borderBottom: 'none' }" :headerStyle="{ borderBottom: 'none' }"
> >
<div class="custom-drawer-bottom-action"> <div class="custom-drawer-bottom-action">
<a-button @click="handleClose">取消</a-button> <a-button @click="handleClose">取消</a-button>
<a-button type="primary" @click="createInstance">提交</a-button> <a-button type="primary" @click="createInstance">提交</a-button>
</div> </div>
<template v-if="action === 'create'"> <template v-if="action === 'create'">
<template v-for="group in attributesByGroup"> <template v-for="group in attributesByGroup">
<CreateInstanceFormByGroup <CreateInstanceFormByGroup
:ref="`createInstanceFormByGroup_${group.id}`" :ref="`createInstanceFormByGroup_${group.id}`"
:key="group.id || group.name" :key="group.id || group.name"
:group="group" :group="group"
@handleFocusInput="handleFocusInput" @handleFocusInput="handleFocusInput"
:attributeList="attributeList" :attributeList="attributeList"
/> />
</template> </template>
<template v-if="parentsType && parentsType.length"> <template v-if="parentsType && parentsType.length">
<a-divider style="font-size:14px;margin:14px 0;font-weight:700;">模型关系</a-divider> <a-divider style="font-size:14px;margin:14px 0;font-weight:700;">模型关系</a-divider>
<a-form> <a-form>
<a-row :gutter="24" align="top" type="flex"> <a-row :gutter="24" align="top" type="flex">
<a-col :span="12" v-for="item in parentsType" :key="item.id"> <a-col :span="12" v-for="item in parentsType" :key="item.id">
<a-form-item :label="item.alias || item.name" :colon="false"> <a-form-item :label="item.alias || item.name" :colon="false">
<a-input-group compact style="width: 100%"> <a-input-group compact style="width: 100%">
<a-select v-model="parentsForm[item.name].attr"> <a-select v-model="parentsForm[item.name].attr">
<a-select-option <a-select-option
:title="attr.alias || attr.name" :title="attr.alias || attr.name"
v-for="attr in item.attributes" v-for="attr in item.attributes"
:key="attr.name" :key="attr.name"
:value="attr.name" :value="attr.name"
> >
{{ attr.alias || attr.name }} {{ attr.alias || attr.name }}
</a-select-option> </a-select-option>
</a-select> </a-select>
<a-input placeholder="多个值使用,分割" v-model="parentsForm[item.name].value" style="width: 50%" /> <a-input placeholder="多个值使用,分割" v-model="parentsForm[item.name].value" style="width: 50%" />
</a-input-group> </a-input-group>
</a-form-item> </a-form-item>
</a-col> </a-col>
</a-row> </a-row>
</a-form> </a-form>
</template> </template>
</template> </template>
<template v-if="action === 'update'"> <template v-if="action === 'update'">
<a-form :form="form"> <a-form :form="form">
<p>可根据需要修改字段当值为<strong></strong>则该字段<strong>置空</strong></p> <p>可根据需要修改字段当值为<strong></strong>则该字段<strong>置空</strong></p>
<a-row :gutter="24" v-for="list in batchUpdateLists" :key="list.name"> <a-row :gutter="24" v-for="list in batchUpdateLists" :key="list.name">
<a-col :span="11"> <a-col :span="11">
<a-form-item> <a-form-item>
<el-select showSearch size="small" filterable v-model="list.name" placeholder="请选择需要修改的字段"> <el-select showSearch size="small" filterable v-model="list.name" placeholder="请选择需要修改的字段">
<el-option <el-option
v-for="attr in attributeList" v-for="attr in attributeList"
:key="attr.name" :key="attr.name"
:value="attr.name" :value="attr.name"
:disabled="batchUpdateLists.findIndex((item) => item.name === attr.name) > -1" :disabled="batchUpdateLists.findIndex((item) => item.name === attr.name) > -1"
:label="attr.alias || attr.name" :label="attr.alias || attr.name"
> >
</el-option> </el-option>
</el-select> </el-select>
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="11"> <a-col :span="11">
<a-form-item> <a-form-item>
<a-select <a-select
:style="{ width: '100%' }" :style="{ width: '100%' }"
v-decorator="[list.name, { rules: [{ required: false }] }]" v-decorator="[list.name, { rules: [{ required: false }] }]"
placeholder="请选择" placeholder="请选择"
v-if="getFieldType(list.name).split('%%')[0] === 'select'" v-if="getFieldType(list.name).split('%%')[0] === 'select'"
:mode="getFieldType(list.name).split('%%')[1] === 'multiple' ? 'multiple' : 'default'" :mode="getFieldType(list.name).split('%%')[1] === 'multiple' ? 'multiple' : 'default'"
showSearch showSearch
allowClear allowClear
> >
<a-select-option <a-select-option
:value="choice[0]" :value="choice[0]"
:key="'New_' + choice + choice_idx" :key="'New_' + choice + choice_idx"
v-for="(choice, choice_idx) in getSelectFieldOptions(list.name)" v-for="(choice, choice_idx) in getSelectFieldOptions(list.name)"
> >
<span :style="choice[1] ? choice[1].style || {} : {}"> <span :style="choice[1] ? choice[1].style || {} : {}">
<ops-icon <ops-icon
:style="{ color: choice[1].icon.color }" :style="{ color: choice[1].icon.color }"
v-if="choice[1] && choice[1].icon && choice[1].icon.name" v-if="choice[1] && choice[1].icon && choice[1].icon.name"
:type="choice[1].icon.name" :type="choice[1].icon.name"
/> />
{{ choice[0] }} {{ choice[0] }}
</span> </span>
</a-select-option> </a-select-option>
</a-select> </a-select>
<a-input-number <a-input-number
v-decorator="[list.name, { rules: [{ required: false }] }]" v-decorator="[list.name, { rules: [{ required: false }] }]"
style="width: 100%" style="width: 100%"
v-if="getFieldType(list.name) === 'input_number'" v-if="getFieldType(list.name) === 'input_number'"
/> />
<a-date-picker <a-date-picker
v-decorator="[list.name, { rules: [{ required: false }] }]" v-decorator="[list.name, { rules: [{ required: false }] }]"
style="width: 100%" style="width: 100%"
:format="getFieldType(list.name) == 'date' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'" :format="getFieldType(list.name) == 'date' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
v-if="getFieldType(list.name) === 'date' || getFieldType(list.name) === 'datetime'" v-if="getFieldType(list.name) === 'date' || getFieldType(list.name) === 'datetime'"
:showTime="getFieldType(list.name) === 'date' ? false : { format: 'HH:mm:ss' }" :showTime="getFieldType(list.name) === 'date' ? false : { format: 'HH:mm:ss' }"
/> />
<a-input <a-input
v-if="getFieldType(list.name) === 'input'" v-if="getFieldType(list.name) === 'input'"
@focus="(e) => handleFocusInput(e, list)" @focus="(e) => handleFocusInput(e, list)"
v-decorator="[list.name, { rules: [{ required: false }] }]" v-decorator="[list.name, { rules: [{ required: false }] }]"
/> />
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="2"> <a-col :span="2">
<a-form-item> <a-form-item>
<a :style="{ color: 'red', marginTop: '2px' }" @click="handleDelete(list.name)"> <a :style="{ color: 'red', marginTop: '2px' }" @click="handleDelete(list.name)">
<a-icon type="delete"/> <a-icon type="delete" />
</a> </a>
</a-form-item> </a-form-item>
</a-col> </a-col>
</a-row> </a-row>
<a-button type="primary" ghost icon="plus" @click="handleAdd">新增修改字段</a-button> <a-button type="primary" ghost icon="plus" @click="handleAdd">新增修改字段</a-button>
</a-form> </a-form>
</template> </template>
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" /> <!-- </a-form> -->
</CustomDrawer> <JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
</template> </CustomDrawer>
</template>
<script>
import moment from 'moment' <script>
import { Select, Option } from 'element-ui' import moment from 'moment'
import { getCIType, getCITypeGroupById } from '@/modules/cmdb/api/CIType' import { Select, Option } from 'element-ui'
import { addCI } from '@/modules/cmdb/api/ci' import { getCIType, getCITypeGroupById } from '@/modules/cmdb/api/CIType'
import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue' import { addCI } from '@/modules/cmdb/api/ci'
import { valueTypeMap } from '../../../utils/const' import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue'
import CreateInstanceFormByGroup from './createInstanceFormByGroup.vue' import { valueTypeMap } from '../../../utils/const'
import { getCITypeParent } from '@/modules/cmdb/api/CITypeRelation' import CreateInstanceFormByGroup from './createInstanceFormByGroup.vue'
import { getCITypeParent } from '@/modules/cmdb/api/CITypeRelation'
export default {
name: 'CreateInstanceForm', export default {
components: { name: 'CreateInstanceForm',
ElSelect: Select, components: {
ElOption: Option, ElSelect: Select,
JsonEditor, ElOption: Option,
CreateInstanceFormByGroup, JsonEditor,
}, CreateInstanceFormByGroup,
props: { },
typeIdFromRelation: { props: {
type: Number, typeIdFromRelation: {
default: 0, type: Number,
}, default: 0,
}, },
data() { },
return { data() {
valueTypeMap, return {
action: '', valueTypeMap,
form: this.$form.createForm(this), action: '',
visible: false, form: this.$form.createForm(this),
attributeList: [], visible: false,
attributeList: [],
CIType: {},
CIType: {},
batchUpdateLists: [],
editAttr: null, batchUpdateLists: [],
attributesByGroup: [], editAttr: null,
parentsType: [], attributesByGroup: [],
parentsForm: {}, parentsType: [],
} parentsForm: {},
}, }
computed: { },
title() { computed: {
return this.action === 'create' ? '创建 ' : '批量修改 ' title() {
}, return this.action === 'create' ? '创建 ' : '批量修改 '
typeId() { },
if (this.typeIdFromRelation) { typeId() {
return this.typeIdFromRelation if (this.typeIdFromRelation) {
} return this.typeIdFromRelation
return this.$router.currentRoute.meta.typeId }
}, return this.$router.currentRoute.meta.typeId
}, },
provide() { },
return { provide() {
getFieldType: this.getFieldType, return {
} getFieldType: this.getFieldType,
}, }
inject: ['attrList'], },
methods: { inject: ['attrList'],
moment, methods: {
async getCIType() { moment,
await getCIType(this.typeId).then((res) => { async getCIType() {
this.CIType = res.ci_types[0] await getCIType(this.typeId).then((res) => {
}) this.CIType = res.ci_types[0]
}, })
async getAttributeList() { },
const _attrList = this.attrList() async getAttributeList() {
this.attributeList = _attrList.sort((x, y) => y.is_required - x.is_required) const _attrList = this.attrList()
await getCITypeGroupById(this.typeId).then((res1) => { this.attributeList = _attrList.sort((x, y) => y.is_required - x.is_required)
const _attributesByGroup = res1.map((g) => { await getCITypeGroupById(this.typeId).then((res1) => {
g.attributes = g.attributes.filter((attr) => !attr.is_computed) const _attributesByGroup = res1.map((g) => {
return g g.attributes = g.attributes.filter((attr) => !attr.is_computed)
}) return g
const attrHasGroupIds = [] })
res1.forEach((g) => { const attrHasGroupIds = []
const id = g.attributes.map((attr) => attr.id) res1.forEach((g) => {
attrHasGroupIds.push(...id) const id = g.attributes.map((attr) => attr.id)
}) attrHasGroupIds.push(...id)
const otherGroupAttr = this.attributeList.filter( })
(attr) => !attrHasGroupIds.includes(attr.id) && !attr.is_computed const otherGroupAttr = this.attributeList.filter(
) (attr) => !attrHasGroupIds.includes(attr.id) && !attr.is_computed
if (otherGroupAttr.length) { )
_attributesByGroup.push({ id: -1, name: '其他', attributes: otherGroupAttr }) if (otherGroupAttr.length) {
} _attributesByGroup.push({ id: -1, name: '其他', attributes: otherGroupAttr })
this.attributesByGroup = _attributesByGroup }
}) this.attributesByGroup = _attributesByGroup
}, })
createInstance() { },
const _this = this createInstance() {
if (_this.action === 'update') { const _this = this
this.form.validateFields((err, values) => { if (_this.action === 'update') {
if (err) { this.form.validateFields((err, values) => {
return if (err) {
} return
Object.keys(values).forEach((k) => { }
const _tempFind = this.attributeList.find((item) => item.name === k) Object.keys(values).forEach((k) => {
if (_tempFind.value_type === '3' && values[k]) { const _tempFind = this.attributeList.find((item) => item.name === k)
values[k] = values[k].format('YYYY-MM-DD HH:mm:ss') if (
} _tempFind.value_type === '3' &&
if (_tempFind.value_type === '4' && values[k]) { values[k] &&
values[k] = values[k].format('YYYY-MM-DD') Object.prototype.toString.call(values[k]) === '[object Object]'
} ) {
if (_tempFind.value_type === '6') { values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
values[k] = values[k] ? JSON.parse(values[k]) : undefined }
} if (
}) _tempFind.value_type === '4' &&
values[k] &&
_this.$emit('submit', values) Object.prototype.toString.call(values[k]) === '[object Object]'
}) ) {
} else { values[k] = values[k].format('YYYY-MM-DD')
let values = {} }
for (let i = 0; i < this.attributesByGroup.length; i++) { if (_tempFind.value_type === '6') {
const data = this.$refs[`createInstanceFormByGroup_${this.attributesByGroup[i].id}`][0].getData() values[k] = values[k] ? JSON.parse(values[k]) : undefined
if (data === 'error') { }
return })
}
values = { ...values, ...data } _this.$emit('submit', values)
} })
} else {
Object.keys(values).forEach((k) => { let values = {}
const _tempFind = this.attributeList.find((item) => item.name === k) for (let i = 0; i < this.attributesByGroup.length; i++) {
if (_tempFind.value_type === '3' && values[k]) { const data = this.$refs[`createInstanceFormByGroup_${this.attributesByGroup[i].id}`][0].getData()
values[k] = values[k].format('YYYY-MM-DD HH:mm:ss') if (data === 'error') {
} return
if (_tempFind.value_type === '4' && values[k]) { }
values[k] = values[k].format('YYYY-MM-DD') values = { ...values, ...data }
} }
if (_tempFind.value_type === '6') {
values[k] = values[k] ? JSON.parse(values[k]) : undefined Object.keys(values).forEach((k) => {
} const _tempFind = this.attributeList.find((item) => item.name === k)
}) if (
values.ci_type = _this.typeId _tempFind.value_type === '3' &&
Object.keys(this.parentsForm).forEach((type) => { values[k] &&
if (this.parentsForm[type].value) { Object.prototype.toString.call(values[k]) === '[object Object]'
values[`$${type}.${this.parentsForm[type].attr}`] = this.parentsForm[type].value ) {
} values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
}) }
addCI(values).then((res) => { if (
_this.$message.success('新增成功!') _tempFind.value_type === '4' &&
_this.visible = false values[k] &&
_this.$emit('reload', { ci_id: res.ci_id }) Object.prototype.toString.call(values[k]) === '[object Object]'
}) ) {
} values[k] = values[k].format('YYYY-MM-DD')
}, }
handleClose() { if (_tempFind.value_type === '6') {
this.visible = false values[k] = values[k] ? JSON.parse(values[k]) : undefined
}, }
handleOpen(visible, action) { })
this.visible = visible values.ci_type = _this.typeId
this.action = action console.log(this.parentsForm)
this.$nextTick(() => { Object.keys(this.parentsForm).forEach((type) => {
this.form.resetFields() if (this.parentsForm[type].value) {
Promise.all([this.getCIType(), this.getAttributeList()]).then(() => { values[`$${type}.${this.parentsForm[type].attr}`] = this.parentsForm[type].value
this.batchUpdateLists = [{ name: this.attributeList[0].name }] }
}) })
if (action === 'create') { addCI(values).then((res) => {
getCITypeParent(this.typeId).then((res) => { _this.$message.success('新增成功!')
this.parentsType = res.parents _this.visible = false
const _parentsForm = {} _this.$emit('reload', { ci_id: res.ci_id })
res.parents.forEach((item) => { })
const _find = item.attributes.find((attr) => attr.id === item.unique_id) }
_parentsForm[item.name] = { attr: _find.name, value: '' } },
}) handleClose() {
this.parentsForm = _parentsForm this.visible = false
}) },
} handleOpen(visible, action) {
}) this.visible = visible
}, this.action = action
getFieldType(name) { this.$nextTick(() => {
const _find = this.attributeList.find((item) => item.name === name) this.form.resetFields()
if (_find) { Promise.all([this.getCIType(), this.getAttributeList()]).then(() => {
if (_find.is_choice) { this.batchUpdateLists = [{ name: this.attributeList[0].name }]
if (_find.is_list) { })
return 'select%%multiple' if (action === 'create') {
} getCITypeParent(this.typeId).then((res) => {
return 'select' this.parentsType = res.parents
} else if (_find.value_type === '0' || _find.value_type === '1') { const _parentsForm = {}
return 'input_number' res.parents.forEach((item) => {
} else if (_find.value_type === '4' || _find.value_type === '3') { const _find = item.attributes.find((attr) => attr.id === item.unique_id)
return valueTypeMap[_find.value_type] _parentsForm[item.name] = { attr: _find.name, value: '' }
} else { })
return 'input' this.parentsForm = _parentsForm
} })
} }
return 'input' })
}, },
getSelectFieldOptions(name) { getFieldType(name) {
const _find = this.attributeList.find((item) => item.name === name) const _find = this.attributeList.find((item) => item.name === name)
if (_find) { if (_find) {
return _find.choice_value if (_find.is_choice) {
} if (_find.is_list) {
return [] return 'select%%multiple'
}, }
handleAdd() { return 'select'
this.batchUpdateLists.push({ name: undefined }) } else if (_find.value_type === '0' || _find.value_type === '1') {
}, return 'input_number'
handleDelete(name) { } else if (_find.value_type === '4' || _find.value_type === '3') {
const _idx = this.batchUpdateLists.findIndex((item) => item.name === name) return valueTypeMap[_find.value_type]
if (_idx > -1) { } else {
this.batchUpdateLists.splice(_idx, 1) return 'input'
} }
}, }
handleFocusInput(e, attr) { return 'input'
console.log(attr) },
const _tempFind = this.attributeList.find((item) => item.name === attr.name) getSelectFieldOptions(name) {
if (_tempFind.value_type === '6') { const _find = this.attributeList.find((item) => item.name === name)
this.editAttr = attr if (_find) {
e.srcElement.blur() return _find.choice_value
const jsonData = this.form.getFieldValue(attr.name) }
this.$refs.jsonEditor.open(null, null, jsonData ? JSON.parse(jsonData) : {}) return []
} else { },
this.editAttr = null handleAdd() {
} this.batchUpdateLists.push({ name: undefined })
}, },
jsonEditorOk(jsonData) { handleDelete(name) {
this.form.setFieldsValue({ [this.editAttr.name]: JSON.stringify(jsonData) }) const _idx = this.batchUpdateLists.findIndex((item) => item.name === name)
}, if (_idx > -1) {
}, this.batchUpdateLists.splice(_idx, 1)
} }
</script> },
<style lang="less"> handleFocusInput(e, attr) {
.create-instance-form { console.log(attr)
.ant-form-item { const _tempFind = this.attributeList.find((item) => item.name === attr.name)
margin-bottom: 5px; if (_tempFind.value_type === '6') {
} this.editAttr = attr
.ant-drawer-body { e.srcElement.blur()
overflow-y: auto; const jsonData = this.form.getFieldValue(attr.name)
max-height: calc(100vh - 110px); this.$refs.jsonEditor.open(null, null, jsonData ? JSON.parse(jsonData) : {})
} } else {
} this.editAttr = null
</style> }
},
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,215 +1,225 @@
<template> <template>
<CustomDrawer <CustomDrawer
:visible="visible" :visible="visible"
:hasFooter="false" :hasFooter="false"
@close=" @close="
() => { () => {
visible = false visible = false
} }
" "
title="属性说明" title="属性说明"
width="72%" width="72%"
:bodyStyle="{ height: '100vh' }" :bodyStyle="{ height: '100vh' }"
> >
<vxe-toolbar> <vxe-toolbar>
<template #buttons> <template #buttons>
<a-input <a-input
v-model="searchKey" v-model="searchKey"
:style="{ display: 'inline-block', width: '244px' }" :style="{ display: 'inline-block', width: '244px' }"
class="ops-input ops-input-radius" class="ops-input ops-input-radius"
type="search" type="search"
placeholder="搜索 名称 | 别名" placeholder="搜索 名称 | 别名"
@keyup="searchAttributes" @keyup="searchAttributes"
> >
<a-icon type="search" slot="suffix" /> <a-icon type="search" slot="suffix" />
</a-input> </a-input>
</template> </template>
</vxe-toolbar> </vxe-toolbar>
<a-spin :spinning="loading"> <a-spin :spinning="loading">
<vxe-table <vxe-table
resizable resizable
border border
size="mini" size="mini"
:height="windowHeight - 160" :height="windowHeight - 160"
:data="list" :data="list"
:scroll-x="{ enabled: true, gt: 0 }" :scroll-x="{ enabled: true, gt: 0 }"
show-overflow show-overflow
show-header-overflow show-header-overflow
align="center" align="center"
highlight-hover-row highlight-hover-row
class="ops-stripe-table" class="ops-stripe-table"
> >
<vxe-column <vxe-column
v-for="(column, index) in columns" v-for="(column, index) in columns"
:field="column.field" :field="column.field"
:title="column.title" :title="column.title"
:min-width="column.width" :min-width="column.width"
:align="column.align" :align="column.align"
:key="column.field" :key="column.field"
:fixed="index < 3 ? 'left' : ''" :fixed="index < 3 ? 'left' : ''"
:sortable="index < 3 ? true : false" :sortable="index < 3 ? true : false"
:title-help="column.help !== null ? { message: column.help } : null" :title-help="column.help !== null ? { message: column.help } : null"
:filters=" :filters="
index < 2 ? null: index === 2 index < 2
? valueTypeFilters: [{ label: '', value: true }, { label: '', value: false },]" ? null
type="html" : index === 2
> ? valueTypeFilters
<template #default="{ row }"> : [
<span v-if="column.field !== 'name' && column.field !== 'alias' && column.field !== 'value_type'"> { label: '', value: true },
<a-icon :style="{ color: '#1fb51f' }" type="check" v-if="row[column.field]" /> { label: '', value: false },
</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> type="html"
</template> >
</vxe-column> <template #default="{ row }">
</vxe-table> <span v-if="column.field !== 'name' && column.field !== 'alias' && column.field !== 'value_type'">
</a-spin> <a-icon :style="{ color: '#1fb51f' }" type="check" v-if="row[column.field]" />
</CustomDrawer> </span>
</template> <span v-else-if="column.field === 'value_type'" v-html="valueTypeMap[row.value_type]"> </span>
<span v-else v-html="row[column.field]"> </span>
<script> </template>
import XEUtils from 'xe-utils' </vxe-column>
import { getCITypeAttributesByName } from '@/modules/cmdb/api/CITypeAttr' </vxe-table>
import { valueTypeMap } from '@/modules/cmdb/utils/const' </a-spin>
export default { </CustomDrawer>
name: 'MetadataDrawer', </template>
data() {
const columns = [ <script>
{ import XEUtils from 'xe-utils'
field: 'name', import { getCITypeAttributesByName } from '@/modules/cmdb/api/CITypeAttr'
title: '名称', import { valueTypeMap } from '@/modules/cmdb/utils/const'
width: 150, export default {
align: 'left', name: 'MetadataDrawer',
help: null, data() {
}, const columns = [
{ {
field: 'alias', field: 'name',
title: '', title: '',
width: 150, width: 150,
align: 'left', align: 'left',
help: null, help: null,
}, },
{ {
field: 'value_type', field: 'alias',
title: '类型', title: '别名',
width: 100, width: 150,
align: 'left', align: 'left',
help: null, help: null,
}, },
{ {
field: 'is_index', field: 'value_type',
title: '是否索引', title: '类型',
width: 110, width: 100,
help: '加快检索, 可以全文搜索, 无需使用条件过滤\n\n json目前不支持建索引 \n\n文本字符长度超过190不能建索引', align: 'left',
}, help: null,
{ },
field: 'default_show', {
title: '默认显示', field: 'is_index',
width: 110, title: '是否索引',
help: '订阅CI默认显示在table里的属性', width: 110,
}, help: '加快检索, 可以全文搜索, 无需使用条件过滤\n\n json目前不支持建索引 \n\n文本字符长度超过190不能建索引',
{ },
field: 'is_unique', {
title: '是否唯一', field: 'default_show',
width: 110, title: '默认显示',
help: null, width: 110,
}, help: '订阅CI默认显示在table里的属性',
{ },
field: 'is_choice', {
title: '是否选择', field: 'is_unique',
width: 110, title: '是否唯一',
help: '表现形式是下拉框, 值必须在预定义值里', width: 110,
}, help: null,
{ },
field: 'is_list', {
title: '是否列表', field: 'is_choice',
width: 110, title: '是否选择',
help: '多值, 比如内网IP', width: 110,
}, help: '表现形式是下拉框, 值必须在预定义值里',
{ },
field: 'is_sortable', {
title: '可排序', field: 'is_list',
width: 100, title: '是否列表',
help: '仅针对前端', width: 110,
}, help: '多值, 比如内网IP',
{ },
field: 'is_password', {
title: '是否密码', field: 'is_sortable',
width: 100, title: '可排序',
help: null, width: 100,
}, help: '仅针对前端',
{ },
field: 'is_link', {
title: '是否链接', field: 'is_computed',
width: 110, title: '计算属性',
help: null, width: 110,
}, help: '模型的其他属性通过表达式的方式计算出来\n\n一个代码片段计算返回的值',
{ },
field: 'is_computed', ]
title: '计算属性', return {
width: 110, columns,
help: '模型的其他属性通过表达式的方式计算出来\n\n一个代码片段计算返回的值', visible: false,
}, list: [],
] tableData: [],
return { loading: false,
columns, valueTypeMap,
visible: false, valueTypeFilters: [],
list: [], searchKey: '',
tableData: [], }
loading: false, },
valueTypeMap, computed: {
valueTypeFilters: [], windowHeight() {
searchKey: '', return this.$store.state.windowHeight
} },
}, },
computed: { created: function() {
windowHeight() { this.valueTypeFilters = Object.keys(this.valueTypeMap).map((key) => {
return this.$store.state.windowHeight return { label: this.valueTypeMap[key], value: key }
}, })
}, },
created: function () { methods: {
this.valueTypeFilters = Object.keys(this.valueTypeMap).map((key) => { open(typeId) {
return { label: this.valueTypeMap[key], value: key } this.visible = true
}) this.typeId = typeId
}, this.getAttrs()
methods: { },
open(typeId) { async getAttrs() {
this.visible = true this.loading = true
this.typeId = typeId const { attributes = [] } = await getCITypeAttributesByName(this.typeId)
this.getAttrs() this.tableData = attributes.map((attr) => {
}, if (attr.is_password) {
async getAttrs() { attr.value_type = '7'
this.loading = true }
const { attributes = [] } = await getCITypeAttributesByName(this.typeId) if (attr.is_link) {
this.tableData = attributes attr.value_type = '8'
this.loading = false }
this.searchAttributes() return attr
}, })
searchAttributes() { this.loading = false
const filterName = XEUtils.toValueString(this.searchKey).trim().toLowerCase() this.searchAttributes()
if (filterName) { },
const filterRE = new RegExp(filterName, 'gi') searchAttributes() {
const searchProps = ['name', 'alias', 'value_type'] const filterName = XEUtils.toValueString(this.searchKey)
const rest = this.tableData.filter((item) => .trim()
searchProps.some((key) => XEUtils.toValueString(item[key]).toLowerCase().indexOf(filterName) > -1) .toLowerCase()
) if (filterName) {
this.list = rest.map((row) => { const filterRE = new RegExp(filterName, 'gi')
const item = Object.assign({}, row) const searchProps = ['name', 'alias', 'value_type']
searchProps.forEach((key) => { const rest = this.tableData.filter((item) =>
item[key] = XEUtils.toValueString(item[key]).replace( searchProps.some(
filterRE, (key) =>
(match) => `<span style='background: yellow'>${match}</span>` XEUtils.toValueString(item[key])
) .toLowerCase()
}) .indexOf(filterName) > -1
return item )
}) )
} else { this.list = rest.map((row) => {
this.list = this.tableData const item = Object.assign({}, row)
} searchProps.forEach((key) => {
}, item[key] = XEUtils.toValueString(item[key]).replace(
}, filterRE,
} (match) => `<span style='background: yellow'>${match}</span>`
</script> )
})
<style></style> return item
})
} else {
this.list = this.tableData
}
},
},
}
</script>
<style></style>

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,11 @@
<template> <template>
<a-tabs id="preValueArea" v-model="activeKey" size="small" :tabBarStyle="{ borderBottom: 'none' }"> <a-tabs
id="preValueArea"
v-model="activeKey"
@change="changeActiveKey"
size="small"
:tabBarStyle="{ borderBottom: 'none' }"
>
<a-tab-pane key="define" :disabled="disabled"> <a-tab-pane key="define" :disabled="disabled">
<span style="font-size:12px;" slot="tab">定义</span> <span style="font-size:12px;" slot="tab">定义</span>
<PreValueTag type="add" :item="[]" @add="addNewValue" :disabled="disabled"> <PreValueTag type="add" :item="[]" @add="addNewValue" :disabled="disabled">
@@ -170,6 +176,15 @@
</a-col> </a-col>
</a-row> </a-row>
</a-tab-pane> </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> </a-tabs>
</template> </template>
@@ -184,14 +199,22 @@ import { getCITypeGroups } from '../../api/ciTypeGroup'
import { getCITypeCommonAttributesByTypeIds } from '../../api/CITypeAttr' import { getCITypeCommonAttributesByTypeIds } from '../../api/CITypeAttr'
import FilterComp from '@/components/CMDBFilterComp' 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 { export default {
name: 'PreValueArea', name: 'PreValueArea',
components: { draggable, PreValueTag, ColorPicker, Webhook, FilterComp }, components: { draggable, PreValueTag, ColorPicker, Webhook, FilterComp, CustomCodeMirror },
props: { props: {
disabled: { disabled: {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
canDefineScript: {
type: Boolean,
default: false,
},
}, },
data() { data() {
return { return {
@@ -208,6 +231,17 @@ export default {
ciTypeGroup: [], ciTypeGroup: [],
typeAttrs: [], typeAttrs: [],
filterExp: '', 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: { watch: {
@@ -281,6 +315,14 @@ export default {
const choice_web_hook = this.$refs.webhook.getParams() const choice_web_hook = this.$refs.webhook.getParams()
choice_web_hook.ret_key = this.form.ret_key choice_web_hook.ret_key = this.form.ret_key
return { choice_value: [], choice_web_hook, choice_other: null } 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 { } else {
let choice_other = {} let choice_other = {}
if (this.choice_other.type_ids && this.choice_other.type_ids.length) { if (this.choice_other.type_ids && this.choice_other.type_ids.length) {
@@ -302,14 +344,22 @@ export default {
this.form.ret_key = choice_web_hook.ret_key ?? '' this.form.ret_key = choice_web_hook.ret_key ?? ''
}) })
} else if (choice_other) { } else if (choice_other) {
this.activeKey = 'choice_other' if (choice_other.script) {
const { type_ids, attr_id, filter } = choice_other this.activeKey = 'script'
this.choice_other = { type_ids, attr_id } this.script = choice_other.script
this.filterExp = filter
if (type_ids && type_ids.length) {
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.filterComp.visibleChange(true, false) this.$refs.codemirror.initCodeMirror(choice_other.script)
}) })
} 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 { } else {
this.valueList = choice_value this.valueList = choice_value
@@ -330,6 +380,16 @@ export default {
this.filterExp = '' this.filterExp = ''
} }
}, },
changeCodeContent(value) {
this.script = value && value.replace('\t', ' ')
},
changeActiveKey(value) {
if (value === 'script') {
this.$nextTick(() => {
this.$refs.codemirror.initCodeMirror(this.script)
})
}
},
}, },
} }
</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 { return {
color: options.chartColor.split(','), color: (options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD').split(','),
grid: { grid: {
top: 15, top: 15,
left: 'left', left: 'left',
@@ -83,7 +83,7 @@ export const category_1_bar_options = (data, options) => {
export const category_1_line_options = (data, options) => { export const category_1_line_options = (data, options) => {
const xData = Object.keys(data) const xData = Object.keys(data)
return { return {
color: options.chartColor.split(','), color: (options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD').split(','),
grid: { grid: {
top: 15, top: 15,
left: 'left', left: 'left',
@@ -117,7 +117,7 @@ export const category_1_line_options = (data, options) => {
x2: 0, x2: 0,
y2: 1, y2: 1,
colorStops: [{ colorStops: [{
offset: 0, color: options.chartColor.split(',')[0] // 0% 处的颜色 offset: 0, color: (options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD').split(',')[0] // 0% 处的颜色
}, { }, {
offset: 1, color: '#ffffff' // 100% 处的颜色 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) => { export const category_1_pie_options = (data, options) => {
return { return {
color: options.chartColor.split(','), color: (options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD').split(','),
grid: { grid: {
top: 10, top: 10,
left: 'left', left: 'left',
@@ -181,7 +181,7 @@ export const category_2_bar_options = (data, options, chartType) => {
}) })
const legend = [...new Set(_legend)] const legend = [...new Set(_legend)]
return { return {
color: options.chartColor.split(','), color: (options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD').split(','),
grid: { grid: {
top: 15, top: 15,
left: 'left', left: 'left',
@@ -249,7 +249,7 @@ export const category_2_bar_options = (data, options, chartType) => {
x2: 0, x2: 0,
y2: 1, y2: 1,
colorStops: [{ colorStops: [{
offset: 0, color: options.chartColor.split(',')[index % 8] // 0% 处的颜色 offset: 0, color: (options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD').split(',')[index % 8] // 0% 处的颜色
}, { }, {
offset: 1, color: '#ffffff' // 100% 处的颜色 offset: 1, color: '#ffffff' // 100% 处的颜色
}], }],
@@ -269,7 +269,7 @@ export const category_2_pie_options = (data, options) => {
}) })
}) })
return { return {
color: options.chartColor.split(','), color: (options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD').split(','),
grid: { grid: {
top: 15, top: 15,
left: 'left', left: 'left',

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -17,9 +17,11 @@ const logo = {
getCompanyInfo({ commit }) { getCompanyInfo({ commit }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
getCompanyInfo().then(res => { getCompanyInfo().then(res => {
commit('SET_FILENAME', res.info.logoName) if (res.info) {
commit('SET_SMALL_FILENAME', res.info.smallLogoName) commit('SET_FILENAME', res.info.logoName)
resolve(res.info) commit('SET_SMALL_FILENAME', res.info.smallLogoName)
resolve(res.info)
}
}).catch(err => { }).catch(err => {
console.log('获取失败', err) console.log('获取失败', err)
reject(err) reject(err)

View File

@@ -30,7 +30,7 @@ services:
- redis - redis
cmdb-api: cmdb-api:
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.3.5 image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.3.6
# build: # build:
# context: . # context: .
# target: cmdb-api # target: cmdb-api
@@ -47,13 +47,15 @@ services:
flask db-setup flask db-setup
flask common-check-new-columns flask common-check-new-columns
gunicorn --workers=3 autoapp:app -b 0.0.0.0:5000 -D gunicorn --workers=3 autoapp:app -b 0.0.0.0:5000 -D
celery -A celery_worker.celery worker -E -Q one_cmdb_async --autoscale=5,2 --logfile=one_cmdb_async.log -D
celery -A celery_worker.celery worker -E -Q acl_async --logfile=one_acl_async.log --concurrency=2 -D
nohup flask cmdb-trigger > trigger.log 2>&1 &
flask cmdb-init-cache flask cmdb-init-cache
flask cmdb-init-acl flask cmdb-init-acl
nohup flask cmdb-trigger > trigger.log 2>&1 & flask cmdb-counter > counter.log 2>&1
nohup flask cmdb-counter > counter.log 2>&1 &
celery -A celery_worker.celery worker -E -Q one_cmdb_async --autoscale=5,2 --logfile=one_cmdb_async.log -D
celery -A celery_worker.celery worker -E -Q acl_async --logfile=one_acl_async.log --concurrency=2
depends_on: depends_on:
- cmdb-db - cmdb-db
- cmdb-cache - cmdb-cache
@@ -63,7 +65,7 @@ services:
- cmdb-api - cmdb-api
cmdb-ui: cmdb-ui:
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.3.5 image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.3.6
# build: # build:
# context: . # context: .
# target: cmdb-ui # target: cmdb-ui

View File

@@ -1,8 +1,16 @@
![维易CMDB](images/logo.png)
[![License](https://img.shields.io/badge/License-AGPLv3-brightgreen)](https://github.com/veops/cmdb/blob/master/LICENSE) <p align="center">
[![UI](https://img.shields.io/badge/UI-Ant%20Design%20Pro%20Vue-brightgreen)](https://github.com/sendya/ant-design-pro-vue) <a href="https://veops.cn"><img src="images/logo.png" alt="维易CMDB" width="300"/></a>
[![API](https://img.shields.io/badge/API-Flask-brightgreen)](https://github.com/pallets/flask) </p>
<h3 align="center">Simple, lightweight, and versatile operational CMDB</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>
------------------------------
[English](README_en.md) / [中文](../README.md) [English](README_en.md) / [中文](../README.md)
@@ -16,9 +24,11 @@
## Overview ## Overview
### Technical Architecture ### System Overview
<img src=images/view.jpg /> <img src=images/dashboard.png />
[View more screenshots](screenshot.md)
### Document ### Document
@@ -48,12 +58,7 @@
- Fine-grained access control and comprehensive operation logs. - Fine-grained access control and comprehensive operation logs.
- Support cross-model search. - Support cross-model search.
### System Overview
- Service Tree
![1](images/0.png "首页展示")
[View more screenshots](screenshot.md)
### More Features ### More Features

View File

@@ -33,6 +33,7 @@
* **`IN`**查询: 例如: `hostname:(cmdb*;cmdb-web*)` 小括号, 分号分隔 * **`IN`**查询: 例如: `hostname:(cmdb*;cmdb-web*)` 小括号, 分号分隔
* **`范围`**查询: 例如: `hostname:[cmdb* _TO_ cmdb-web*]` `_TO_`分隔 * **`范围`**查询: 例如: `hostname:[cmdb* _TO_ cmdb-web*]` `_TO_`分隔
* **`比较`**查询: 例如: `cpu_count:>5` 支持`>, >=, <, <=` * **`比较`**查询: 例如: `cpu_count:>5` 支持`>, >=, <, <=`
* 多个条件可以用`小括号`进行组合
* 结果字段说明 * 结果字段说明

View File

@@ -25,6 +25,8 @@
- `IN`查询. eg. `hostname:(cmdb*;cmdb-web*)` 小括号, 分号分隔 - `IN`查询. eg. `hostname:(cmdb*;cmdb-web*)` 小括号, 分号分隔
- `RANGE`查询. eg. `hostname:[cmdb* _TO_ cmdb-web*]` `_TO_`分隔 - `RANGE`查询. eg. `hostname:[cmdb* _TO_ cmdb-web*]` `_TO_`分隔
- `COMPARISON`查询. eg. `cpu_count:>5` 支持`>, >=, <, <=` - `COMPARISON`查询. eg. `cpu_count:>5` 支持`>, >=, <, <=`
- 多个条件可以用`小括号`进行组合
- 返回结果 - 返回结果
- 搜索表达式 `/api/v0.1/ci/s?q=_type:kvm,status:在线,idc:南汇,private_ip:10.1.1.1*&page=1&fl=hostname,private_ip&facet=private_ip&count=1` - 搜索表达式 `/api/v0.1/ci/s?q=_type:kvm,status:在线,idc:南汇,private_ip:10.1.1.1*&page=1&fl=hostname,private_ip&facet=private_ip&count=1`
- 返回数据(默认 json - 返回数据(默认 json
@@ -91,6 +93,8 @@
- `IN`查询. eg. `hostname:(cmdb*;cmdb-web*)` 小括号, 分号分隔 - `IN`查询. eg. `hostname:(cmdb*;cmdb-web*)` 小括号, 分号分隔
- `RANGE`查询. eg. `hostname:[cmdb* _TO_ cmdb-web*]` `_TO_`分隔 - `RANGE`查询. eg. `hostname:[cmdb* _TO_ cmdb-web*]` `_TO_`分隔
- `COMPARISON`查询. eg. `cpu_count:>5` 支持`>, >=, <, <=` - `COMPARISON`查询. eg. `cpu_count:>5` 支持`>, >=, <, <=`
- 多个条件可以用`小括号`进行组合
- 返回结果 - 返回结果
- 搜索表达式 `/api/v0.1/ci_relations/s?root_id=53&level=3&q=_type:kvm,status:在线,idc:南汇,private_ip:10.1.1.1*&page=1&fl=hostname,private_ip&facet=private_ip&count=1` - 搜索表达式 `/api/v0.1/ci_relations/s?root_id=53&level=3&q=_type:kvm,status:在线,idc:南汇,private_ip:10.1.1.1*&page=1&fl=hostname,private_ip&facet=private_ip&count=1`
- 返回数据(默认 json - 返回数据(默认 json

15
docs/flask-migrate.md Normal file
View File

@@ -0,0 +1,15 @@
## 使用Flask-Migrate做数据库版本管理
- 首次可以删除cmdb-api/migrations/versions下的所有文件
-
### 进入cmdb-api完成下面步骤操作可能会删除数据库中不被代码管理的表如需保留请看文末中的tips
- 如果是首次使用需要先删除cmdb-api/migrations/versions下的所有文件非首次跳过
- 执行`flask db migrate` 生成对应版本数据库表的升级文件到versions文件夹下需要你的数据库是已经upgrade的
- 执行`flask db upgrade` 数据库表同步更新到mysql
### tips
- cmdb-api/migrations/env.py文件内的exclude_tables列表可以填写不想被flask-migrate管理的数据库表

BIN
docs/images/dashboard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

BIN
docs/images/wechat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View File

@@ -28,8 +28,8 @@ cp cmdb-api/settings.example.py cmdb-api/settings.py
- 后端: 进入**cmdb-api**目录执行 `pipenv run flask run -h 0.0.0.0` - 后端: 进入**cmdb-api**目录执行 `pipenv run flask run -h 0.0.0.0`
- 前端: 进入**cmdb-ui**目录执行`yarn run serve` - 前端: 进入**cmdb-ui**目录执行`yarn run serve`
- worker: - worker:
- 进入**cmdb-api**目录执行 `pipenv run celery -A celery_worker.celery worker -E -Q one_cmdb_async --concurrency=1 -D` - 进入**cmdb-api**目录执行 `pipenv run celery -A celery_worker.celery worker -E -Q one_cmdb_async --autoscale=5,2 --logfile=one_cmdb_async.log -D`
- 进入**cmdb-api**目录执行 `pipenv run celery -A celery_worker.celery worker -E -Q acl_async --concurrency=1 -D` - 进入**cmdb-api**目录执行 `pipenv run celery -A celery_worker.celery worker -E -Q acl_async --autoscale=2,1 --logfile=one_acl_async.log -D`
- 浏览器打开: [http://127.0.0.1:8000](http://127.0.0.1:8000) - 浏览器打开: [http://127.0.0.1:8000](http://127.0.0.1:8000)
- 如果是非本机访问, 要修改**cmdb-ui/.env**里**VUE_APP_API_BASE_URL**里的 IP 地址为后端服务的 ip 地址 - 如果是非本机访问, 要修改**cmdb-ui/.env**里**VUE_APP_API_BASE_URL**里的 IP 地址为后端服务的 ip 地址

View File

@@ -26,8 +26,8 @@
- backend: in **cmdb-api** directory: `pipenv run flask run -h 0.0.0.0` - backend: in **cmdb-api** directory: `pipenv run flask run -h 0.0.0.0`
- frontend: in **cmdb-ui** directory: `yarn run serve` - frontend: in **cmdb-ui** directory: `yarn run serve`
- worker: - worker:
- in **cmdb-api** directory: `pipenv run celery -A celery_worker.celery worker -E -Q one_cmdb_async --concurrency=1 -D` - in **cmdb-api** directory: `pipenv run celery -A celery_worker.celery worker -E -Q one_cmdb_async --autoscale=5,2 --logfile=one_cmdb_async.log -D`
- in **cmdb-api** directory: `pipenv run celery -A celery_worker.celery worker -E -Q acl_async --concurrency=1 -D` - in **cmdb-api** directory: `pipenv run celery -A celery_worker.celery worker -E -Q acl_async --autoscale=2,1 --logfile=one_acl_async.log -D`
- homepage: [http://127.0.0.1:8000](http://127.0.0.1:8000) - homepage: [http://127.0.0.1:8000](http://127.0.0.1:8000)
- if not run localhost: please change ip address(**VUE_APP_API_BASE_URL**) in config file **cmdb-ui/.env** into your backend ip address - if not run localhost: please change ip address(**VUE_APP_API_BASE_URL**) in config file **cmdb-ui/.env** into your backend ip address