mirror of
https://github.com/veops/cmdb.git
synced 2025-09-07 13:57:01 +08:00
Compare commits
45 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
dc1a2a7632 | ||
|
153fef4918 | ||
|
e218f8e065 | ||
|
b7137b3975 | ||
|
c6a9478dbb | ||
|
af40004fe9 | ||
|
d86eb1c5eb | ||
|
fef3bfceaf | ||
|
3a4f0b248f | ||
|
ffa3d7cd43 | ||
|
6aefac98cd | ||
|
22989f8d5a | ||
|
ebe9d1e29f | ||
|
f1dd5ca074 | ||
|
4dd95f0d7e | ||
|
7e3e248c2b | ||
|
a5ff1139f7 | ||
|
00135f4644 | ||
|
8297d4c9b4 | ||
|
5143539593 | ||
|
a6eb2f0d21 | ||
|
07a63bef6e | ||
|
d69efeea25 | ||
|
0ef67360ad | ||
|
e2f993bc11 | ||
|
05d2795e79 | ||
|
6ff77a140c | ||
|
6503d32e6e | ||
|
887a69c2bd | ||
|
6d052eaffc | ||
|
d0f0bf84dd | ||
|
802fda66e7 | ||
|
c95747c88a | ||
|
ed49b238d8 | ||
|
375f0879fb | ||
|
8bc1893ca9 | ||
|
53cd2342bf | ||
|
eff6d974d4 | ||
|
8478d2f858 | ||
|
80e99cc335 | ||
|
8f64fc4aa0 | ||
|
cfc345c993 | ||
|
928116d0b5 | ||
|
4c2e6ae69f | ||
|
a2c75fd34e |
60
.github/ISSUE_TEMPLATE/1bug.yaml
vendored
Normal file
60
.github/ISSUE_TEMPLATE/1bug.yaml
vendored
Normal 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
44
.github/ISSUE_TEMPLATE/2feature.yaml
vendored
Normal 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
|
36
.github/ISSUE_TEMPLATE/3consultation.yaml
vendored
Normal file
36
.github/ISSUE_TEMPLATE/3consultation.yaml
vendored
Normal 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
60
.github/ISSUE_TEMPLATE/bug.yaml
vendored
Normal 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
6
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal 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.
|
||||
|
0
.github/ISSUE_TEMPLATE/consultation.yaml
vendored
Normal file
0
.github/ISSUE_TEMPLATE/consultation.yaml
vendored
Normal file
44
.github/ISSUE_TEMPLATE/feature.yaml
vendored
Normal file
44
.github/ISSUE_TEMPLATE/feature.yaml
vendored
Normal 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
0
.github/config.yml
vendored
Normal file
1
.gitignore
vendored
1
.gitignore
vendored
@@ -40,6 +40,7 @@ nosetests.xml
|
||||
.pytest_cache
|
||||
cmdb-api/test-output
|
||||
cmdb-api/api/uploaded_files
|
||||
cmdb-api/migrations/versions
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
4
Makefile
4
Makefile
@@ -9,7 +9,7 @@ help: ## display this help
|
||||
|
||||
env: ## create a development environment using pipenv
|
||||
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 && \
|
||||
make deps
|
||||
.PHONY: env
|
||||
@@ -36,7 +36,7 @@ api: ## start api server
|
||||
.PHONY: api
|
||||
|
||||
worker: ## start async tasks worker
|
||||
cd cmdb-api && pipenv run celery -A celery_worker.celery worker -E -Q one_cmdb_async --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
|
||||
|
||||
ui: ## start ui server
|
||||
|
48
README.md
48
README.md
@@ -1,12 +1,20 @@
|
||||

|
||||
|
||||
[](https://github.com/veops/cmdb/blob/master/LICENSE)
|
||||
[](https://github.com/sendya/ant-design-pro-vue)
|
||||
[](https://github.com/pallets/flask)
|
||||
<p align="center">
|
||||
<a href="https://veops.cn"><img src="docs/images/logo.png" alt="维易CMDB" width="300"/></a>
|
||||
</p>
|
||||
<h3 align="center">简单、轻量、通用的运维配置管理数据库</h3>
|
||||
<p align="center">
|
||||
<a href="https://github.com/veops/cmdb/blob/master/LICENSE"><img src="https://img.shields.io/badge/License-AGPLv3-brightgreen" alt="License: GPLv3"></a>
|
||||
<a href="https:https://github.com/sendya/ant-design-pro-vue"><img src="https://img.shields.io/badge/UI-Ant%20Design%20Pro%20Vue-brightgreen" alt="UI"></a>
|
||||
<a href="https://github.com/pallets/flask"><img src="https://img.shields.io/badge/API-Flask-brightgreen" alt="API"></a>
|
||||
</p>
|
||||
|
||||
|
||||
------------------------------
|
||||
|
||||
[English](docs/README_en.md) / [中文](README.md)
|
||||
- 产品文档:https://veops.cn/docs/
|
||||
- 在线体验: <a href="https://cmdb.veops.cn" target="_blank">CMDB</a>
|
||||
- 在线体验:<a href="https://cmdb.veops.cn" target="_blank">CMDB</a>
|
||||
- username: demo 或者 admin
|
||||
- password: 123456
|
||||
|
||||
@@ -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://github.com/veops/cmdb/tree/master/docs/cmdb_api.md" target="_blank">API 文档</a>
|
||||
- <a href="https://mp.weixin.qq.com/s/rQaf4AES7YJsyNQG_MKOLg" target="_blank">自动发现</a>
|
||||
- 更多文章可以在公众号 **维易科技OneOps** 里查看
|
||||
|
||||
### 特点
|
||||
|
||||
- 灵活性
|
||||
1. 规范并统一纳管复杂数据资产
|
||||
2. 自动发现、入库 IT 资产
|
||||
1. 配置灵活,不设定任何运维场景,有内置模板
|
||||
2. 自动发现、入库 IT 资产
|
||||
- 安全性
|
||||
1. 细粒度访问控制
|
||||
1. 细粒度权限控制
|
||||
2. 完备操作日志
|
||||
- 多应用
|
||||
1. 丰富视图展示维度
|
||||
2. 提供 Restful API
|
||||
2. API简单强大
|
||||
3. 支持定义属性触发器、计算属性
|
||||
|
||||
### 主要功能
|
||||
|
||||
- 模型属性支持索引、多值、默认排序、字体颜色,支持计算属性
|
||||
- 支持自动发现、定时巡检、文件导入
|
||||
- 支持资源、树形、关系视图展示
|
||||
- 支持资源、层级、关系视图展示
|
||||
- 支持模型间关系配置和展示
|
||||
- 细粒度访问控制,完备的操作日志
|
||||
- 支持跨模型搜索
|
||||
|
||||
### 系统概览
|
||||
|
||||
- 服务树
|
||||
|
||||

|
||||
|
||||
[查看更多展示](docs/screenshot.md)
|
||||
|
||||
|
||||
### 更多功能
|
||||
@@ -67,7 +73,7 @@
|
||||
## 安装
|
||||
|
||||
### Docker 一键快速构建
|
||||
- 进入主目录(先安装 docker 环境)
|
||||
- 进入主目录(先安装 docker 环境, 注意要clone整个项目)
|
||||
|
||||
```
|
||||
docker-compose up -d
|
||||
@@ -83,6 +89,6 @@ docker-compose up -d
|
||||
|
||||
---
|
||||
|
||||
_**欢迎关注我们的公众号,点击联系我们,加入微信、QQ群(336164978),获得更多产品、行业相关资讯**_
|
||||
_**欢迎关注公众号(维易科技OneOps),关注后可加入微信群,进行产品和技术交流。**_
|
||||
|
||||

|
||||

|
||||
|
@@ -24,18 +24,19 @@ supervisor = "==4.0.3"
|
||||
Flask-Login = "==0.6.2"
|
||||
Flask-Bcrypt = "==1.0.1"
|
||||
Flask-Cors = ">=3.0.8"
|
||||
python-ldap = "==3.4.0"
|
||||
ldap3 = "==2.9.1"
|
||||
pycryptodome = "==3.12.0"
|
||||
cryptography = ">=41.0.2"
|
||||
# Caching
|
||||
Flask-Caching = ">=1.0.0"
|
||||
# Environment variable parsing
|
||||
environs = "==4.2.0"
|
||||
marshmallow = "==2.20.2"
|
||||
# async tasks
|
||||
celery = "==5.3.1"
|
||||
celery = ">=5.3.1"
|
||||
celery_once = "==3.0.1"
|
||||
more-itertools = "==5.0.0"
|
||||
kombu = "==5.3.1"
|
||||
kombu = ">=5.3.1"
|
||||
# common setting
|
||||
timeout-decorator = "==0.5.0"
|
||||
WTForms = "==3.0.0"
|
||||
@@ -58,6 +59,9 @@ Jinja2 = "==3.1.2"
|
||||
jinja2schema = "==0.1.4"
|
||||
msgpack-python = "==0.5.6"
|
||||
alembic = "==1.7.7"
|
||||
hvac = "==2.0.0"
|
||||
colorama = ">=0.4.6"
|
||||
pycryptodomex = ">=3.19.0"
|
||||
|
||||
[dev-packages]
|
||||
# Testing
|
||||
@@ -74,4 +78,3 @@ flake8-isort = "==2.7.0"
|
||||
isort = "==4.3.21"
|
||||
pep8-naming = "==0.8.2"
|
||||
pydocstyle = "==3.0.0"
|
||||
|
||||
|
@@ -7,6 +7,7 @@ import os
|
||||
import sys
|
||||
from inspect import getmembers
|
||||
from logging.handlers import RotatingFileHandler
|
||||
from pathlib import Path
|
||||
|
||||
from flask import Flask
|
||||
from flask import jsonify
|
||||
@@ -17,11 +18,14 @@ from flask.json.provider import DefaultJSONProvider
|
||||
|
||||
import api.views.entry
|
||||
from api.extensions import (bcrypt, cache, celery, cors, db, es, login_manager, migrate, rd)
|
||||
from api.extensions import inner_secrets
|
||||
from api.flask_cas import CAS
|
||||
from api.lib.secrets.secrets import InnerKVManger
|
||||
from api.models.acl import User
|
||||
|
||||
HERE = os.path.abspath(os.path.dirname(__file__))
|
||||
PROJECT_ROOT = os.path.join(HERE, os.pardir)
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
@login_manager.user_loader
|
||||
@@ -76,15 +80,6 @@ class MyJSONEncoder(DefaultJSONProvider):
|
||||
return o
|
||||
|
||||
|
||||
def create_acl_app(config_object="settings"):
|
||||
app = Flask(__name__.split(".")[0])
|
||||
app.config.from_object(config_object)
|
||||
|
||||
register_extensions(app)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def create_app(config_object="settings"):
|
||||
"""Create application factory, as explained here: http://flask.pocoo.org/docs/patterns/appfactories/.
|
||||
|
||||
@@ -125,7 +120,7 @@ def register_extensions(app):
|
||||
db.init_app(app)
|
||||
cors.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)
|
||||
if app.config.get('USE_ES'):
|
||||
es.init_app(app)
|
||||
@@ -133,6 +128,10 @@ def register_extensions(app):
|
||||
app.config.update(app.config.get("CELERY"))
|
||||
celery.conf.update(app.config)
|
||||
|
||||
if app.config.get('SECRETS_ENGINE') == 'inner':
|
||||
with app.app_context():
|
||||
inner_secrets.init_app(app, InnerKVManger())
|
||||
|
||||
|
||||
def register_blueprints(app):
|
||||
for item in getmembers(api.views.entry):
|
||||
|
@@ -7,6 +7,7 @@ import json
|
||||
import time
|
||||
|
||||
import click
|
||||
import requests
|
||||
from flask import current_app
|
||||
from flask.cli import with_appcontext
|
||||
from flask_login import login_user
|
||||
@@ -29,6 +30,9 @@ from api.lib.perm.acl.resource import ResourceCRUD
|
||||
from api.lib.perm.acl.resource import ResourceTypeCRUD
|
||||
from api.lib.perm.acl.role import RoleCRUD
|
||||
from api.lib.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 ResourceType
|
||||
from api.models.cmdb import Attribute
|
||||
@@ -53,6 +57,7 @@ def cmdb_init_cache():
|
||||
if relations:
|
||||
rd.create_or_update(relations, REDIS_PREFIX_CI_RELATION)
|
||||
|
||||
es = None
|
||||
if current_app.config.get("USE_ES"):
|
||||
from api.extensions import es
|
||||
from api.models.cmdb import Attribute
|
||||
@@ -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)
|
||||
i.delete(commit=False)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def valid_address(address):
|
||||
if not address:
|
||||
return False
|
||||
|
||||
if not address.startswith(("http://127.0.0.1", "https://127.0.0.1")):
|
||||
response = {
|
||||
"message": "Address should start with http://127.0.0.1 or https://127.0.0.1",
|
||||
"status": "failed"
|
||||
}
|
||||
KeyManage.print_response(response)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option(
|
||||
'-a',
|
||||
'--address',
|
||||
help='inner cmdb api, http://127.0.0.1:8000',
|
||||
)
|
||||
@with_appcontext
|
||||
def cmdb_inner_secrets_init(address):
|
||||
"""
|
||||
init inner secrets for password feature
|
||||
"""
|
||||
res, ok = KeyManage(backend=InnerKVManger).init()
|
||||
if not ok:
|
||||
if res.get("status") == "failed":
|
||||
KeyManage.print_response(res)
|
||||
return
|
||||
|
||||
token = res.get("details", {}).get("root_token", "")
|
||||
if valid_address(address):
|
||||
token = current_app.config.get("INNER_TRIGGER_TOKEN", "") if not token else token
|
||||
if not token:
|
||||
token = click.prompt(f'Enter root token', hide_input=True, confirmation_prompt=False)
|
||||
assert token is not None
|
||||
resp = requests.post("{}/api/v0.1/secrets/auto_seal".format(address.strip("/")),
|
||||
headers={"Inner-Token": token})
|
||||
if resp.status_code == 200:
|
||||
KeyManage.print_response(resp.json())
|
||||
else:
|
||||
KeyManage.print_response({"message": resp.text or resp.status_code, "status": "failed"})
|
||||
else:
|
||||
KeyManage.print_response(res)
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option(
|
||||
'-a',
|
||||
'--address',
|
||||
help='inner cmdb api, http://127.0.0.1:8000',
|
||||
required=True,
|
||||
)
|
||||
@with_appcontext
|
||||
def cmdb_inner_secrets_unseal(address):
|
||||
"""
|
||||
unseal the secrets feature
|
||||
"""
|
||||
if not valid_address(address):
|
||||
return
|
||||
address = "{}/api/v0.1/secrets/unseal".format(address.strip("/"))
|
||||
for i in range(global_key_threshold):
|
||||
token = click.prompt(f'Enter unseal token {i + 1}', hide_input=True, confirmation_prompt=False)
|
||||
assert token is not None
|
||||
resp = requests.post(address, headers={"Unseal-Token": token})
|
||||
if resp.status_code == 200:
|
||||
KeyManage.print_response(resp.json())
|
||||
if resp.json().get("status") in ["success", "skip"]:
|
||||
return
|
||||
else:
|
||||
KeyManage.print_response({"message": resp.status_code, "status": "failed"})
|
||||
return
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option(
|
||||
'-a',
|
||||
'--address',
|
||||
help='inner cmdb api, http://127.0.0.1:8000',
|
||||
required=True,
|
||||
)
|
||||
@click.option(
|
||||
'-k',
|
||||
'--token',
|
||||
help='root token',
|
||||
prompt=True,
|
||||
hide_input=True,
|
||||
)
|
||||
@with_appcontext
|
||||
def cmdb_inner_secrets_seal(address, token):
|
||||
"""
|
||||
seal the secrets feature
|
||||
"""
|
||||
assert address is not None
|
||||
assert token is not None
|
||||
if not valid_address(address):
|
||||
return
|
||||
address = "{}/api/v0.1/secrets/seal".format(address.strip("/"))
|
||||
resp = requests.post(address, headers={
|
||||
"Inner-Token": token,
|
||||
})
|
||||
if resp.status_code == 200:
|
||||
KeyManage.print_response(resp.json())
|
||||
else:
|
||||
KeyManage.print_response({"message": resp.status_code, "status": "failed"})
|
||||
|
||||
|
||||
@click.command()
|
||||
@with_appcontext
|
||||
def cmdb_password_data_migrate():
|
||||
"""
|
||||
Migrate CI password data, version >= v2.3.6
|
||||
"""
|
||||
from api.models.cmdb import CIIndexValueText
|
||||
from api.models.cmdb import CIValueText
|
||||
from api.lib.secrets.inner import InnerCrypt
|
||||
from api.lib.secrets.vault import VaultClient
|
||||
|
||||
attrs = Attribute.get_by(to_dict=False)
|
||||
for attr in attrs:
|
||||
if attr.is_password:
|
||||
|
||||
value_table = CIIndexValueText if attr.is_index else CIValueText
|
||||
|
||||
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()
|
||||
|
@@ -165,31 +165,48 @@ class InitDepartment(object):
|
||||
acl = self.check_app('backend')
|
||||
resources_types = acl.get_all_resources_types()
|
||||
|
||||
perms = ['read', 'grant', 'delete', 'update']
|
||||
|
||||
acl_rid = self.get_admin_user_rid()
|
||||
|
||||
results = list(filter(lambda t: t['name'] == '操作权限', resources_types['groups']))
|
||||
if len(results) == 0:
|
||||
payload = dict(
|
||||
app_id=acl.app_name,
|
||||
name='操作权限',
|
||||
description='',
|
||||
perms=['read', 'grant', 'delete', 'update']
|
||||
perms=perms
|
||||
)
|
||||
resource_type = acl.create_resources_type(payload)
|
||||
else:
|
||||
resource_type = results[0]
|
||||
resource_type_id = resource_type['id']
|
||||
existed_perms = resources_types.get('id2perms', {}).get(resource_type_id, [])
|
||||
existed_perms = [p['name'] for p in existed_perms]
|
||||
new_perms = []
|
||||
for perm in perms:
|
||||
if perm not in existed_perms:
|
||||
new_perms.append(perm)
|
||||
if len(new_perms) > 0:
|
||||
resource_type['perms'] = existed_perms + new_perms
|
||||
acl.update_resources_type(resource_type_id, resource_type)
|
||||
|
||||
resource_list = acl.get_resource_by_type(None, None, resource_type['id'])
|
||||
|
||||
for name in ['公司信息', '公司架构', '通知设置']:
|
||||
payload = dict(
|
||||
type_id=resource_type['id'],
|
||||
app_id=acl.app_name,
|
||||
name=name,
|
||||
)
|
||||
try:
|
||||
acl.create_resource(payload)
|
||||
except Exception as e:
|
||||
if '已经存在' in str(e):
|
||||
pass
|
||||
else:
|
||||
raise Exception(e)
|
||||
target = list(filter(lambda r: r['name'] == name, resource_list))
|
||||
if len(target) == 0:
|
||||
payload = dict(
|
||||
type_id=resource_type['id'],
|
||||
app_id=acl.app_name,
|
||||
name=name,
|
||||
)
|
||||
resource = acl.create_resource(payload)
|
||||
else:
|
||||
resource = target[0]
|
||||
|
||||
if acl_rid > 0:
|
||||
acl.grant_resource(acl_rid, resource['id'], perms)
|
||||
|
||||
def check_app(self, app_name):
|
||||
acl = ACLManager(app_name)
|
||||
@@ -199,10 +216,9 @@ class InitDepartment(object):
|
||||
)
|
||||
try:
|
||||
app = acl.validate_app()
|
||||
if app:
|
||||
return acl
|
||||
|
||||
acl.create_app(payload)
|
||||
if not app:
|
||||
acl.create_app(payload)
|
||||
return acl
|
||||
except Exception as e:
|
||||
current_app.logger.error(e)
|
||||
if '不存在' in str(e):
|
||||
@@ -210,6 +226,10 @@ class InitDepartment(object):
|
||||
return acl
|
||||
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()
|
||||
@with_appcontext
|
||||
|
@@ -9,6 +9,7 @@ from flask_login import LoginManager
|
||||
from flask_migrate import Migrate
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
from api.lib.secrets.inner import KeyManage
|
||||
from api.lib.utils import ESHandler
|
||||
from api.lib.utils import RedisHandler
|
||||
|
||||
@@ -21,3 +22,4 @@ celery = Celery()
|
||||
cors = CORS(supports_credentials=True)
|
||||
rd = RedisHandler()
|
||||
es = ESHandler()
|
||||
inner_secrets = KeyManage()
|
||||
|
@@ -60,22 +60,33 @@ class AttributeManager(object):
|
||||
return []
|
||||
|
||||
@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.ci import search
|
||||
|
||||
type_ids = choice_other.get('type_ids')
|
||||
attr_id = choice_other.get('attr_id')
|
||||
other_filter = choice_other.get('filter') or ''
|
||||
if choice_other.get('type_ids'):
|
||||
type_ids = choice_other.get('type_ids')
|
||||
attr_id = choice_other.get('attr_id')
|
||||
other_filter = choice_other.get('filter') or ''
|
||||
|
||||
query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter)
|
||||
s = search(query, fl=[str(attr_id)], facet=[str(attr_id)], count=1)
|
||||
try:
|
||||
_, _, _, _, _, facet = s.search()
|
||||
return [[i[0], {}] for i in (list(facet.values()) or [[]])[0]]
|
||||
except SearchError as e:
|
||||
current_app.logger.error("get choice values from other ci failed: {}".format(e))
|
||||
return []
|
||||
query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter)
|
||||
s = search(query, fl=[str(attr_id)], facet=[str(attr_id)], count=1)
|
||||
try:
|
||||
_, _, _, _, _, facet = s.search()
|
||||
return [[i[0], {}] for i in (list(facet.values()) or [[]])[0]]
|
||||
except SearchError as e:
|
||||
current_app.logger.error("get choice values from other ci failed: {}".format(e))
|
||||
return []
|
||||
|
||||
elif choice_other.get('script'):
|
||||
try:
|
||||
x = compile(choice_other['script'], '', "exec")
|
||||
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
|
||||
def get_choice_values(cls, attr_id, value_type, choice_web_hook, choice_other,
|
||||
@@ -87,7 +98,7 @@ class AttributeManager(object):
|
||||
return []
|
||||
elif choice_other:
|
||||
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:
|
||||
return []
|
||||
|
||||
@@ -96,7 +107,8 @@ class AttributeManager(object):
|
||||
return []
|
||||
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
|
||||
def add_choice_values(_id, value_type, choice_values):
|
||||
@@ -218,10 +230,15 @@ class AttributeManager(object):
|
||||
if name in BUILTIN_KEYWORDS:
|
||||
return abort(400, ErrFormat.attribute_name_cannot_be_builtin)
|
||||
|
||||
if kwargs.get('choice_other'):
|
||||
if (not isinstance(kwargs['choice_other'], dict) or not kwargs['choice_other'].get('type_ids') or
|
||||
not kwargs['choice_other'].get('attr_id')):
|
||||
return abort(400, ErrFormat.attribute_choice_other_invalid)
|
||||
while kwargs.get('choice_other'):
|
||||
if isinstance(kwargs['choice_other'], dict):
|
||||
if kwargs['choice_other'].get('script'):
|
||||
break
|
||||
|
||||
if kwargs['choice_other'].get('type_ids') and kwargs['choice_other'].get('attr_id'):
|
||||
break
|
||||
|
||||
return abort(400, ErrFormat.attribute_choice_other_invalid)
|
||||
|
||||
alias = kwargs.pop("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('choice_other') and kwargs['choice_other'].get('script') and cls.can_create_computed_attribute()
|
||||
|
||||
attr = Attribute.create(flush=True,
|
||||
name=name,
|
||||
alias=alias,
|
||||
@@ -337,10 +356,15 @@ class AttributeManager(object):
|
||||
|
||||
self._change_index(attr, attr.is_index, kwargs['is_index'])
|
||||
|
||||
if kwargs.get('choice_other'):
|
||||
if (not isinstance(kwargs['choice_other'], dict) or not kwargs['choice_other'].get('type_ids') or
|
||||
not kwargs['choice_other'].get('attr_id')):
|
||||
return abort(400, ErrFormat.attribute_choice_other_invalid)
|
||||
while kwargs.get('choice_other'):
|
||||
if isinstance(kwargs['choice_other'], dict):
|
||||
if kwargs['choice_other'].get('script'):
|
||||
break
|
||||
|
||||
if kwargs['choice_other'].get('type_ids') and kwargs['choice_other'].get('attr_id'):
|
||||
break
|
||||
|
||||
return abort(400, ErrFormat.attribute_choice_other_invalid)
|
||||
|
||||
existed2 = attr.to_dict()
|
||||
if not existed2['choice_web_hook'] and not existed2.get('choice_other') and existed2['is_choice']:
|
||||
|
@@ -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 ResourceTypeEnum
|
||||
from api.lib.cmdb.const import RetKey
|
||||
from api.lib.cmdb.const import ValueTypeEnum
|
||||
from api.lib.cmdb.history import AttributeHistoryManger
|
||||
from api.lib.cmdb.history import CIRelationHistoryManager
|
||||
from api.lib.cmdb.history import CITriggerHistoryManager
|
||||
@@ -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 is_app_admin
|
||||
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 Lock
|
||||
from api.lib.webhook import webhook_request
|
||||
from api.models.cmdb import AttributeHistory
|
||||
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
|
||||
|
||||
PRIVILEGED_USERS = {"worker", "cmdb_agent", "agent"}
|
||||
PASSWORD_DEFAULT_SHOW = "******"
|
||||
|
||||
|
||||
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 = None
|
||||
record_id = None
|
||||
password_dict = {}
|
||||
need_lock = current_user.username not in current_app.config.get('PRIVILEGED_USERS', PRIVILEGED_USERS)
|
||||
with Lock(ci_type_name, need_lock=need_lock):
|
||||
existed = cls.ci_is_exist(unique_key, unique_value, ci_type.id)
|
||||
@@ -351,14 +357,23 @@ class CIManager(object):
|
||||
ci_dict.get(attr.name) is None and ci_dict.get(attr.alias) is None)):
|
||||
ci_dict[attr.name] = attr.default.get('default')
|
||||
|
||||
if type_attr.is_required and (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))
|
||||
else:
|
||||
for type_attr, attr in attrs:
|
||||
if attr.default and attr.default.get('default') == AttributeDefaultValueEnum.UPDATED_AT:
|
||||
ci_dict[attr.name] = now
|
||||
|
||||
computed_attrs = [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()
|
||||
|
||||
@@ -395,6 +410,10 @@ class CIManager(object):
|
||||
cls.delete(ci.id)
|
||||
raise e
|
||||
|
||||
if password_dict:
|
||||
for attr_id in password_dict:
|
||||
record_id = cls.save_password(ci.id, attr_id, password_dict[attr_id], record_id, ci_type.id)
|
||||
|
||||
if record_id: # has change
|
||||
ci_cache.apply_async(args=(ci.id, operate_type, record_id), queue=CMDB_QUEUE)
|
||||
|
||||
@@ -414,7 +433,16 @@ class CIManager(object):
|
||||
if attr.default and attr.default.get('default') == AttributeDefaultValueEnum.UPDATED_AT:
|
||||
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()
|
||||
|
||||
@@ -423,6 +451,7 @@ class CIManager(object):
|
||||
|
||||
limit_attrs = self._valid_ci_for_no_read(ci) if not _is_admin else {}
|
||||
|
||||
record_id = None
|
||||
need_lock = current_user.username not in current_app.config.get('PRIVILEGED_USERS', PRIVILEGED_USERS)
|
||||
with Lock(ci.ci_type.name, need_lock=need_lock):
|
||||
self._valid_unique_constraint(ci.type_id, ci_dict, ci_id)
|
||||
@@ -440,6 +469,10 @@ class CIManager(object):
|
||||
except BadRequest as e:
|
||||
raise e
|
||||
|
||||
if password_dict:
|
||||
for attr_id in password_dict:
|
||||
record_id = self.save_password(ci.id, attr_id, password_dict[attr_id], record_id, ci.type_id)
|
||||
|
||||
if record_id: # has change
|
||||
ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE)
|
||||
|
||||
@@ -465,16 +498,17 @@ class CIManager(object):
|
||||
ci_dict = cls.get_cis_by_ids([ci_id])
|
||||
ci_dict = ci_dict and ci_dict[0]
|
||||
|
||||
triggers = CITriggerManager.get(ci_dict['_type'])
|
||||
for trigger in triggers:
|
||||
option = trigger['option']
|
||||
if not option.get('enable') or option.get('action') != OperateType.DELETE:
|
||||
continue
|
||||
if ci_dict:
|
||||
triggers = CITriggerManager.get(ci_dict['_type'])
|
||||
for trigger in triggers:
|
||||
option = trigger['option']
|
||||
if not option.get('enable') or option.get('action') != OperateType.DELETE:
|
||||
continue
|
||||
|
||||
if option.get('filter') and not CITriggerManager.ci_filter(ci_dict.get('_id'), option['filter']):
|
||||
continue
|
||||
if option.get('filter') and not CITriggerManager.ci_filter(ci_dict.get('_id'), option['filter']):
|
||||
continue
|
||||
|
||||
ci_delete_trigger.apply_async(args=(trigger, OperateType.DELETE, ci_dict), queue=CMDB_QUEUE)
|
||||
ci_delete_trigger.apply_async(args=(trigger, OperateType.DELETE, ci_dict), queue=CMDB_QUEUE)
|
||||
|
||||
attrs = CITypeAttribute.get_by(type_id=ci.type_id, to_dict=False)
|
||||
attr_names = set([AttributeCache.get(attr.attr_id).name for attr in attrs])
|
||||
@@ -498,7 +532,8 @@ class CIManager(object):
|
||||
|
||||
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)
|
||||
|
||||
@@ -600,7 +635,7 @@ class CIManager(object):
|
||||
_fields = list()
|
||||
for field in fields:
|
||||
attr = AttributeCache.get(field)
|
||||
if attr is not None:
|
||||
if attr is not None and not attr.is_password:
|
||||
_fields.append(str(attr.id))
|
||||
filter_fields_sql = "WHERE A.attr_id in ({0})".format(",".join(_fields))
|
||||
|
||||
@@ -618,7 +653,7 @@ class CIManager(object):
|
||||
ci_dict = dict()
|
||||
unique_id2obj = dict()
|
||||
excludes = excludes and set(excludes)
|
||||
for ci_id, type_id, attr_id, attr_name, attr_alias, value, value_type, is_list 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):
|
||||
continue
|
||||
|
||||
@@ -645,11 +680,14 @@ class CIManager(object):
|
||||
else:
|
||||
return abort(400, ErrFormat.argument_invalid.format("ret_key"))
|
||||
|
||||
value = ValueTypeMap.serialize2[value_type](value)
|
||||
if is_list:
|
||||
ci_dict.setdefault(attr_key, []).append(value)
|
||||
if is_password and value:
|
||||
ci_dict[attr_key] = PASSWORD_DEFAULT_SHOW
|
||||
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
|
||||
|
||||
@@ -681,6 +719,84 @@ class CIManager(object):
|
||||
|
||||
return cls._get_cis_from_db(ci_ids, ret_key, fields, value_tables, excludes=excludes)
|
||||
|
||||
@classmethod
|
||||
def save_password(cls, ci_id, attr_id, value, record_id, type_id):
|
||||
changed = None
|
||||
encrypt_value = None
|
||||
value_table = ValueTypeMap.table[ValueTypeEnum.PASSWORD]
|
||||
if current_app.config.get('SECRETS_ENGINE') == 'inner':
|
||||
if value:
|
||||
encrypt_value, status = InnerCrypt().encrypt(value)
|
||||
if not status:
|
||||
current_app.logger.error('save password failed: {}'.format(encrypt_value))
|
||||
return abort(400, ErrFormat.password_save_failed.format(encrypt_value))
|
||||
else:
|
||||
encrypt_value = PASSWORD_DEFAULT_SHOW
|
||||
|
||||
existed = value_table.get_by(ci_id=ci_id, attr_id=attr_id, first=True, to_dict=False)
|
||||
if existed is None:
|
||||
if value:
|
||||
value_table.create(ci_id=ci_id, attr_id=attr_id, value=encrypt_value)
|
||||
changed = [(ci_id, attr_id, OperateType.ADD, '', PASSWORD_DEFAULT_SHOW, type_id)]
|
||||
elif existed.value != encrypt_value:
|
||||
if value:
|
||||
existed.update(ci_id=ci_id, attr_id=attr_id, value=encrypt_value)
|
||||
changed = [(ci_id, attr_id, OperateType.UPDATE, PASSWORD_DEFAULT_SHOW, PASSWORD_DEFAULT_SHOW, type_id)]
|
||||
else:
|
||||
existed.delete()
|
||||
changed = [(ci_id, attr_id, OperateType.DELETE, PASSWORD_DEFAULT_SHOW, '', type_id)]
|
||||
|
||||
if current_app.config.get('SECRETS_ENGINE') == 'vault':
|
||||
vault = VaultClient(current_app.config.get('VAULT_URL'), current_app.config.get('VAULT_TOKEN'))
|
||||
if value:
|
||||
try:
|
||||
vault.update("/{}/{}".format(ci_id, attr_id), dict(v=value))
|
||||
except Exception as e:
|
||||
current_app.logger.error('save password to vault failed: {}'.format(e))
|
||||
return abort(400, ErrFormat.password_save_failed.format('write vault failed'))
|
||||
else:
|
||||
try:
|
||||
vault.delete("/{}/{}".format(ci_id, attr_id))
|
||||
except Exception as e:
|
||||
current_app.logger.warning('delete password to vault failed: {}'.format(e))
|
||||
|
||||
if changed is not None:
|
||||
return AttributeValueManager.write_change2(changed, record_id)
|
||||
|
||||
@classmethod
|
||||
def load_password(cls, ci_id, attr_id):
|
||||
ci = CI.get_by_id(ci_id) or abort(404, ErrFormat.ci_not_found.format(ci_id))
|
||||
|
||||
limit_attrs = cls._valid_ci_for_no_read(ci, ci.ci_type)
|
||||
if limit_attrs:
|
||||
attr = AttributeCache.get(attr_id)
|
||||
if attr and attr.name not in limit_attrs:
|
||||
return abort(403, ErrFormat.no_permission2)
|
||||
|
||||
if current_app.config.get('SECRETS_ENGINE', 'inner') == 'inner':
|
||||
value_table = ValueTypeMap.table[ValueTypeEnum.PASSWORD]
|
||||
v = value_table.get_by(ci_id=ci_id, attr_id=attr_id, first=True, to_dict=False)
|
||||
|
||||
v = v and v.value
|
||||
if not v:
|
||||
return
|
||||
|
||||
decrypt_value, status = InnerCrypt().decrypt(v)
|
||||
if not status:
|
||||
current_app.logger.error('load password failed: {}'.format(decrypt_value))
|
||||
return abort(400, ErrFormat.password_load_failed.format(decrypt_value))
|
||||
|
||||
return decrypt_value
|
||||
|
||||
elif current_app.config.get('SECRETS_ENGINE') == 'vault':
|
||||
vault = VaultClient(current_app.config.get('VAULT_URL'), current_app.config.get('VAULT_TOKEN'))
|
||||
data, status = vault.read("/{}/{}".format(ci_id, attr_id))
|
||||
if not status:
|
||||
current_app.logger.error('read password from vault failed: {}'.format(data))
|
||||
return abort(400, ErrFormat.password_load_failed.format(data))
|
||||
|
||||
return data.get('v')
|
||||
|
||||
|
||||
class CIRelationManager(object):
|
||||
"""
|
||||
|
@@ -12,6 +12,8 @@ class ValueTypeEnum(BaseEnum):
|
||||
DATE = "4"
|
||||
TIME = "5"
|
||||
JSON = "6"
|
||||
PASSWORD = TEXT
|
||||
LINK = TEXT
|
||||
|
||||
|
||||
class ConstraintEnum(BaseEnum):
|
||||
|
@@ -95,3 +95,6 @@ class ErrFormat(CommonErrFormat):
|
||||
ci_filter_perm_cannot_or_query = "CI过滤授权 暂时不支持 或 查询"
|
||||
ci_filter_perm_attr_no_permission = "您没有属性 {} 的操作权限!"
|
||||
ci_filter_perm_ci_no_permission = "您没有该CI的操作权限!"
|
||||
|
||||
password_save_failed = "保存密码失败: {}"
|
||||
password_load_failed = "获取密码失败: {}"
|
||||
|
@@ -7,6 +7,7 @@ QUERY_CIS_BY_VALUE_TABLE = """
|
||||
attr.alias AS attr_alias,
|
||||
attr.value_type,
|
||||
attr.is_list,
|
||||
attr.is_password,
|
||||
c_cis.type_id,
|
||||
{0}.ci_id,
|
||||
{0}.attr_id,
|
||||
@@ -26,7 +27,8 @@ QUERY_CIS_BY_IDS = """
|
||||
A.attr_alias,
|
||||
A.value,
|
||||
A.value_type,
|
||||
A.is_list
|
||||
A.is_list,
|
||||
A.is_password
|
||||
FROM
|
||||
({1}) AS A {0}
|
||||
ORDER BY A.ci_id;
|
||||
@@ -43,7 +45,7 @@ FACET_QUERY1 = """
|
||||
|
||||
FACET_QUERY = """
|
||||
SELECT {0}.value,
|
||||
count({0}.ci_id)
|
||||
count(distinct {0}.ci_id)
|
||||
FROM {0}
|
||||
INNER JOIN ({1}) AS F ON F.ci_id={0}.ci_id
|
||||
WHERE {0}.attr_id={2:d}
|
||||
|
@@ -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_UNION_CI_ATTRIBUTE_IS_NULL
|
||||
from api.lib.cmdb.utils import TableMap
|
||||
from api.lib.cmdb.utils import ValueTypeMap
|
||||
from api.lib.perm.acl.acl import ACLManager
|
||||
from api.lib.perm.acl.acl import is_app_admin
|
||||
from api.lib.utils import handle_arg_list
|
||||
@@ -524,15 +525,15 @@ class Search(object):
|
||||
if k:
|
||||
table_name = TableMap(attr=attr).table_name
|
||||
query_sql = FACET_QUERY.format(table_name, self.query_sql, attr.id)
|
||||
# current_app.logger.warning(query_sql)
|
||||
result = db.session.execute(query_sql).fetchall()
|
||||
facet[k] = result
|
||||
|
||||
facet_result = dict()
|
||||
for k, v in facet.items():
|
||||
if not k.startswith('_'):
|
||||
a = getattr(AttributeCache.get(k), self.ret_key)
|
||||
facet_result[a] = [(f[0], f[1], a) for f in v]
|
||||
attr = AttributeCache.get(k)
|
||||
a = getattr(attr, self.ret_key)
|
||||
facet_result[a] = [(ValueTypeMap.serialize[attr.value_type](f[0]), f[1], a) for f in v]
|
||||
|
||||
return facet_result
|
||||
|
||||
|
@@ -35,7 +35,7 @@ class Search(object):
|
||||
self.sort = sort or ("ci_id" if current_app.config.get("USE_ES") else None)
|
||||
|
||||
self.root_id = root_id
|
||||
self.level = level
|
||||
self.level = level or 0
|
||||
self.reverse = reverse
|
||||
|
||||
def _get_ids(self):
|
||||
@@ -104,16 +104,22 @@ class Search(object):
|
||||
ci_ids=merge_ids).search()
|
||||
|
||||
def statistics(self, type_ids):
|
||||
self.level = int(self.level)
|
||||
_tmp = []
|
||||
ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id
|
||||
for l in range(0, int(self.level)):
|
||||
if not l:
|
||||
_tmp = list(map(lambda x: list(json.loads(x).items()),
|
||||
[i or '{}' for i in rd.get(ids, REDIS_PREFIX_CI_RELATION) or []]))
|
||||
for lv in range(0, self.level):
|
||||
if not lv:
|
||||
if type_ids and lv == self.level - 1:
|
||||
_tmp = list(map(lambda x: [i for i in x if i[1] in type_ids],
|
||||
(map(lambda x: list(json.loads(x).items()),
|
||||
[i or '{}' for i in rd.get(ids, REDIS_PREFIX_CI_RELATION) or []]))))
|
||||
else:
|
||||
_tmp = list(map(lambda x: list(json.loads(x).items()),
|
||||
[i or '{}' for i in rd.get(ids, REDIS_PREFIX_CI_RELATION) or []]))
|
||||
else:
|
||||
for idx, item in enumerate(_tmp):
|
||||
if item:
|
||||
if type_ids and l == self.level - 1:
|
||||
if type_ids and lv == self.level - 1:
|
||||
__tmp = list(
|
||||
map(lambda x: [(_id, type_id) for _id, type_id in json.loads(x).items()
|
||||
if type_id in type_ids],
|
||||
|
@@ -12,7 +12,7 @@ import api.models.cmdb as model
|
||||
from api.lib.cmdb.cache import AttributeCache
|
||||
from api.lib.cmdb.const import ValueTypeEnum
|
||||
|
||||
TIME_RE = re.compile(r"^(20|21|22|23|[0-1]\d):[0-5]\d:[0-5]\d$")
|
||||
TIME_RE = re.compile(r"^20|21|22|23|[0-1]\d:[0-5]\d:[0-5]\d$")
|
||||
|
||||
|
||||
def string2int(x):
|
||||
@@ -21,7 +21,7 @@ def string2int(x):
|
||||
|
||||
def str2datetime(x):
|
||||
try:
|
||||
return datetime.datetime.strptime(x, "%Y-%m-%d")
|
||||
return datetime.datetime.strptime(x, "%Y-%m-%d").date()
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
@@ -44,8 +44,8 @@ class ValueTypeMap(object):
|
||||
ValueTypeEnum.FLOAT: float,
|
||||
ValueTypeEnum.TEXT: lambda x: x if isinstance(x, six.string_types) else str(x),
|
||||
ValueTypeEnum.TIME: lambda x: x if isinstance(x, six.string_types) else str(x),
|
||||
ValueTypeEnum.DATE: lambda x: x.strftime("%Y-%m-%d"),
|
||||
ValueTypeEnum.DATETIME: lambda x: x.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
ValueTypeEnum.DATE: lambda x: x.strftime("%Y-%m-%d") if not isinstance(x, six.string_types) else x,
|
||||
ValueTypeEnum.DATETIME: lambda x: x.strftime("%Y-%m-%d %H:%M:%S") if not isinstance(x, six.string_types) else x,
|
||||
ValueTypeEnum.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.TEXT: model.TextChoice,
|
||||
ValueTypeEnum.TIME: model.TextChoice,
|
||||
ValueTypeEnum.DATE: model.TextChoice,
|
||||
ValueTypeEnum.DATETIME: model.TextChoice,
|
||||
}
|
||||
|
||||
table = {
|
||||
@@ -97,7 +99,7 @@ class ValueTypeMap(object):
|
||||
ValueTypeEnum.DATE: 'text',
|
||||
ValueTypeEnum.TIME: 'text',
|
||||
ValueTypeEnum.FLOAT: 'float',
|
||||
ValueTypeEnum.JSON: 'object'
|
||||
ValueTypeEnum.JSON: 'object',
|
||||
}
|
||||
|
||||
|
||||
@@ -110,7 +112,9 @@ class TableMap(object):
|
||||
@property
|
||||
def table(self):
|
||||
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
|
||||
elif self.is_index is None:
|
||||
self.is_index = attr.is_index
|
||||
@@ -122,7 +126,9 @@ class TableMap(object):
|
||||
@property
|
||||
def table_name(self):
|
||||
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
|
||||
elif self.is_index is None:
|
||||
self.is_index = attr.is_index
|
||||
|
@@ -66,9 +66,10 @@ class AttributeValueManager(object):
|
||||
use_master=use_master,
|
||||
to_dict=False)
|
||||
field_name = getattr(attr, ret_key)
|
||||
|
||||
if attr.is_list:
|
||||
res[field_name] = [ValueTypeMap.serialize[attr.value_type](i.value) for i in rs]
|
||||
elif attr.is_password and rs:
|
||||
res[field_name] = '******' if rs[0].value else ''
|
||||
else:
|
||||
res[field_name] = ValueTypeMap.serialize[attr.value_type](rs[0].value) if rs else None
|
||||
|
||||
@@ -131,8 +132,7 @@ class AttributeValueManager(object):
|
||||
return AttributeHistoryManger.add(record_id, ci_id, [(attr_id, operate_type, old, new)], type_id)
|
||||
|
||||
@staticmethod
|
||||
def _write_change2(changed):
|
||||
record_id = None
|
||||
def write_change2(changed, record_id=None):
|
||||
for ci_id, attr_id, operate_type, old, new, type_id in changed:
|
||||
record_id = AttributeHistoryManger.add(record_id, ci_id, [(attr_id, operate_type, old, new)], type_id,
|
||||
commit=False, flush=False)
|
||||
@@ -284,9 +284,9 @@ class AttributeValueManager(object):
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
current_app.logger.warning(str(e))
|
||||
return abort(400, ErrFormat.attribute_value_unknown_error.format(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
|
||||
def delete_attr_value(attr_id, ci_id):
|
||||
|
@@ -1,12 +1,13 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
from flask import abort
|
||||
from flask import current_app
|
||||
|
||||
from api.lib.common_setting.resp_format import ErrFormat
|
||||
from api.lib.perm.acl.app import AppCRUD
|
||||
from api.lib.perm.acl.cache import RoleCache, AppCache
|
||||
from api.lib.perm.acl.permission import PermissionCRUD
|
||||
from api.lib.perm.acl.resource import ResourceTypeCRUD, ResourceCRUD
|
||||
from api.lib.perm.acl.role import RoleCRUD, RoleRelationCRUD
|
||||
from api.lib.perm.acl.user import UserCRUD
|
||||
from api.lib.perm.acl.resource import ResourceTypeCRUD, ResourceCRUD
|
||||
|
||||
|
||||
class ACLManager(object):
|
||||
@@ -109,8 +110,32 @@ class ACLManager(object):
|
||||
id2perms=id2perms
|
||||
)
|
||||
|
||||
def create_resources_type(self, payload):
|
||||
payload['app_id'] = self.validate_app().id
|
||||
rt = ResourceTypeCRUD.add(**payload)
|
||||
|
||||
return rt.to_dict()
|
||||
|
||||
def update_resources_type(self, _id, payload):
|
||||
rt = ResourceTypeCRUD.update(_id, **payload)
|
||||
|
||||
return rt.to_dict()
|
||||
|
||||
def create_resource(self, payload):
|
||||
payload['app_id'] = self.validate_app().id
|
||||
resource = ResourceCRUD.add(**payload)
|
||||
|
||||
return resource.to_dict()
|
||||
|
||||
def get_resource_by_type(self, q, u, rt_id, page=1, page_size=999999):
|
||||
numfound, res = ResourceCRUD.search(q, u, self.validate_app().id, rt_id, page, page_size)
|
||||
return res
|
||||
|
||||
def grant_resource(self, rid, resource_id, perms):
|
||||
PermissionCRUD.grant(rid, perms, resource_id=resource_id, group_id=None)
|
||||
|
||||
@staticmethod
|
||||
def create_app(payload):
|
||||
rt = AppCRUD.add(**payload)
|
||||
|
||||
return rt.to_dict()
|
||||
|
@@ -121,6 +121,19 @@ class EmployeeCRUD(object):
|
||||
employee = CreateEmployee().create_single(**data)
|
||||
return employee.to_dict()
|
||||
|
||||
@staticmethod
|
||||
def add_employee_from_acl_created(**kwargs):
|
||||
try:
|
||||
kwargs['acl_uid'] = kwargs.pop('uid')
|
||||
kwargs['acl_rid'] = kwargs.pop('rid')
|
||||
kwargs['department_id'] = 0
|
||||
|
||||
Employee.create(
|
||||
**kwargs
|
||||
)
|
||||
except Exception as e:
|
||||
abort(400, str(e))
|
||||
|
||||
@staticmethod
|
||||
def add(**kwargs):
|
||||
try:
|
||||
|
@@ -4,8 +4,14 @@
|
||||
from functools import wraps
|
||||
|
||||
from flask import abort
|
||||
from flask import current_app
|
||||
from flask import request
|
||||
from sqlalchemy.exc import InvalidRequestError
|
||||
from sqlalchemy.exc import OperationalError
|
||||
from sqlalchemy.exc import PendingRollbackError
|
||||
from sqlalchemy.exc import StatementError
|
||||
|
||||
from api.extensions import db
|
||||
from api.lib.resp_format import CommonErrFormat
|
||||
|
||||
|
||||
@@ -70,3 +76,43 @@ def args_validate(model_cls, exclude_args=None):
|
||||
return wrapper
|
||||
|
||||
return decorate
|
||||
|
||||
|
||||
def reconnect_db(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except (StatementError, OperationalError, InvalidRequestError) as e:
|
||||
error_msg = str(e)
|
||||
if 'Lost connection' in error_msg or 'reconnect until invalid transaction' in error_msg or \
|
||||
'can be emitted within this transaction' in error_msg:
|
||||
current_app.logger.info('[reconnect_db] lost connect rollback then retry')
|
||||
db.session.rollback()
|
||||
return func(*args, **kwargs)
|
||||
else:
|
||||
raise e
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def _flush_db():
|
||||
try:
|
||||
db.session.commit()
|
||||
except (StatementError, OperationalError, InvalidRequestError, PendingRollbackError):
|
||||
db.session.rollback()
|
||||
|
||||
|
||||
def flush_db(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
_flush_db()
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def run_flush_db():
|
||||
_flush_db()
|
||||
|
@@ -4,7 +4,7 @@
|
||||
import msgpack
|
||||
|
||||
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.models.acl import App
|
||||
from api.models.acl import Permission
|
||||
@@ -221,9 +221,9 @@ class RoleRelationCache(object):
|
||||
return msgpack.loads(r_g, raw=False)
|
||||
|
||||
@classmethod
|
||||
@flush_db
|
||||
def rebuild(cls, rid, app_id):
|
||||
cls.clean(rid, app_id)
|
||||
db.session.remove()
|
||||
|
||||
cls.get_parent_ids(rid, app_id)
|
||||
cls.get_child_ids(rid, app_id)
|
||||
@@ -235,9 +235,9 @@ class RoleRelationCache(object):
|
||||
cls.get_resources2(rid, app_id)
|
||||
|
||||
@classmethod
|
||||
@flush_db
|
||||
def rebuild2(cls, rid, app_id):
|
||||
cache.delete(cls.PREFIX_RESOURCES2.format(rid, app_id))
|
||||
db.session.remove()
|
||||
cls.get_resources2(rid, app_id)
|
||||
|
||||
@classmethod
|
||||
|
@@ -260,7 +260,8 @@ class ResourceCRUD(object):
|
||||
numfound = query.count()
|
||||
res = [i.to_dict() for i in query.offset((page - 1) * page_size).limit(page_size)]
|
||||
for i in res:
|
||||
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
|
||||
|
||||
|
@@ -58,10 +58,14 @@ class UserCRUD(object):
|
||||
kwargs['employee_id'] = '{0:04d}'.format(biggest_employee_id + 1)
|
||||
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,
|
||||
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
|
||||
|
||||
|
1
cmdb-api/api/lib/secrets/__init__.py
Normal file
1
cmdb-api/api/lib/secrets/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# -*- coding:utf-8 -*-
|
429
cmdb-api/api/lib/secrets/inner.py
Normal file
429
cmdb-api/api/lib/secrets/inner.py
Normal 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)
|
35
cmdb-api/api/lib/secrets/secrets.py
Normal file
35
cmdb-api/api/lib/secrets/secrets.py
Normal 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
|
141
cmdb-api/api/lib/secrets/vault.py
Normal file
141
cmdb-api/api/lib/secrets/vault.py
Normal 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)
|
@@ -5,7 +5,8 @@ import copy
|
||||
import hashlib
|
||||
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_sqlalchemy import BaseQuery
|
||||
|
||||
@@ -57,9 +58,7 @@ class UserQuery(BaseQuery):
|
||||
return user, authenticated
|
||||
|
||||
def authenticate_with_ldap(self, username, password):
|
||||
ldap_conn = ldap.initialize(current_app.config.get('LDAP_SERVER'))
|
||||
ldap_conn.protocol_version = 3
|
||||
ldap_conn.set_option(ldap.OPT_REFERRALS, 0)
|
||||
server = Server(current_app.config.get('LDAP_SERVER'), get_info=ALL)
|
||||
if '@' in username:
|
||||
email = username
|
||||
who = current_app.config.get('LDAP_USER_DN').format(username.split('@')[0])
|
||||
@@ -70,11 +69,14 @@ class UserQuery(BaseQuery):
|
||||
username = username.split('@')[0]
|
||||
user = self.get_by_username(username)
|
||||
try:
|
||||
|
||||
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:
|
||||
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)
|
||||
|
||||
return user, True
|
||||
except ldap.INVALID_CREDENTIALS:
|
||||
except LDAPBindError:
|
||||
return user, False
|
||||
|
||||
def search(self, key):
|
||||
|
@@ -504,3 +504,10 @@ class CIFilterPerms(Model):
|
||||
attr_filter = db.Column(db.Text)
|
||||
|
||||
rid = db.Column(db.Integer, index=True)
|
||||
|
||||
|
||||
class InnerKV(Model):
|
||||
__tablename__ = "c_kv"
|
||||
|
||||
key = db.Column(db.String(128), index=True)
|
||||
value = db.Column(db.Text)
|
||||
|
@@ -46,5 +46,4 @@ def register_resources(resource_path, rest_api):
|
||||
resource_cls.url_prefix = ("",)
|
||||
if isinstance(resource_cls.url_prefix, six.string_types):
|
||||
resource_cls.url_prefix = (resource_cls.url_prefix,)
|
||||
|
||||
rest_api.add_resource(resource_cls, *resource_cls.url_prefix)
|
||||
|
@@ -9,7 +9,8 @@ from werkzeug.exceptions import BadRequest
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
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 AuditOperateSource
|
||||
from api.lib.perm.acl.audit import AuditOperateType
|
||||
@@ -28,6 +29,7 @@ from api.models.acl import Trigger
|
||||
name="acl.role_rebuild",
|
||||
queue=ACL_QUEUE,
|
||||
once={"graceful": True, "unlock_before_run": True})
|
||||
@reconnect_db
|
||||
def role_rebuild(rids, app_id):
|
||||
rids = rids if isinstance(rids, list) else [rids]
|
||||
for rid in rids:
|
||||
@@ -37,6 +39,7 @@ def role_rebuild(rids, app_id):
|
||||
|
||||
|
||||
@celery.task(name="acl.update_resource_to_build_role", queue=ACL_QUEUE)
|
||||
@reconnect_db
|
||||
def update_resource_to_build_role(resource_id, app_id, group_id=None):
|
||||
rids = [i.id for i in Role.get_by(__func_isnot__key_uid=None, fl='id', to_dict=False)]
|
||||
rids += [i.id for i in Role.get_by(app_id=app_id, fl='id', to_dict=False)]
|
||||
@@ -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)
|
||||
@flush_db
|
||||
@reconnect_db
|
||||
def apply_trigger(_id, resource_id=None, operator_uid=None):
|
||||
db.session.remove()
|
||||
|
||||
from api.lib.perm.acl.permission import PermissionCRUD
|
||||
|
||||
trigger = Trigger.get_by_id(_id)
|
||||
@@ -118,9 +121,9 @@ def apply_trigger(_id, resource_id=None, operator_uid=None):
|
||||
|
||||
|
||||
@celery.task(name="acl.cancel_trigger", queue=ACL_QUEUE)
|
||||
@flush_db
|
||||
@reconnect_db
|
||||
def cancel_trigger(_id, resource_id=None, operator_uid=None):
|
||||
db.session.remove()
|
||||
|
||||
from api.lib.perm.acl.permission import PermissionCRUD
|
||||
|
||||
trigger = Trigger.get_by_id(_id)
|
||||
@@ -186,6 +189,7 @@ def cancel_trigger(_id, resource_id=None, operator_uid=None):
|
||||
|
||||
|
||||
@celery.task(name="acl.op_record", queue=ACL_QUEUE)
|
||||
@reconnect_db
|
||||
def op_record(app, rolename, operate_type, obj):
|
||||
if isinstance(app, int):
|
||||
app = AppCache.get(app)
|
||||
|
@@ -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 REDIS_PREFIX_CI
|
||||
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
|
||||
from api.lib.decorator import flush_db
|
||||
from api.lib.decorator import reconnect_db
|
||||
from api.lib.perm.acl.cache import UserCache
|
||||
from api.lib.utils import Lock
|
||||
from api.lib.utils import handle_arg_list
|
||||
@@ -25,11 +27,12 @@ from api.models.cmdb import CITypeAttribute
|
||||
|
||||
|
||||
@celery.task(name="cmdb.ci_cache", queue=CMDB_QUEUE)
|
||||
@flush_db
|
||||
@reconnect_db
|
||||
def ci_cache(ci_id, operate_type, record_id):
|
||||
from api.lib.cmdb.ci import CITriggerManager
|
||||
|
||||
time.sleep(0.01)
|
||||
db.session.remove()
|
||||
|
||||
m = api.lib.cmdb.ci.CIManager()
|
||||
ci_dict = m.get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
|
||||
@@ -49,9 +52,10 @@ def ci_cache(ci_id, operate_type, record_id):
|
||||
|
||||
|
||||
@celery.task(name="cmdb.batch_ci_cache", queue=CMDB_QUEUE)
|
||||
@flush_db
|
||||
@reconnect_db
|
||||
def batch_ci_cache(ci_ids, ): # only for attribute change index
|
||||
time.sleep(1)
|
||||
db.session.remove()
|
||||
|
||||
for ci_id in ci_ids:
|
||||
m = api.lib.cmdb.ci.CIManager()
|
||||
@@ -66,6 +70,7 @@ def batch_ci_cache(ci_ids, ): # only for attribute change index
|
||||
|
||||
|
||||
@celery.task(name="cmdb.ci_delete", queue=CMDB_QUEUE)
|
||||
@reconnect_db
|
||||
def ci_delete(ci_id):
|
||||
current_app.logger.info(ci_id)
|
||||
|
||||
@@ -78,6 +83,7 @@ def ci_delete(ci_id):
|
||||
|
||||
|
||||
@celery.task(name="cmdb.ci_delete_trigger", queue=CMDB_QUEUE)
|
||||
@reconnect_db
|
||||
def ci_delete_trigger(trigger, operate_type, ci_dict):
|
||||
current_app.logger.info('delete ci {} trigger'.format(ci_dict['_id']))
|
||||
from api.lib.cmdb.ci import CITriggerManager
|
||||
@@ -89,9 +95,9 @@ def ci_delete_trigger(trigger, operate_type, ci_dict):
|
||||
|
||||
|
||||
@celery.task(name="cmdb.ci_relation_cache", queue=CMDB_QUEUE)
|
||||
@flush_db
|
||||
@reconnect_db
|
||||
def ci_relation_cache(parent_id, child_id):
|
||||
db.session.remove()
|
||||
|
||||
with Lock("CIRelation_{}".format(parent_id)):
|
||||
children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0]
|
||||
children = json.loads(children) if children is not None else {}
|
||||
@@ -106,6 +112,8 @@ def ci_relation_cache(parent_id, child_id):
|
||||
|
||||
|
||||
@celery.task(name="cmdb.ci_relation_add", queue=CMDB_QUEUE)
|
||||
@flush_db
|
||||
@reconnect_db
|
||||
def ci_relation_add(parent_dict, child_id, uid):
|
||||
"""
|
||||
:param parent_dict: key is '$parent_model.attr_name'
|
||||
@@ -121,8 +129,6 @@ def ci_relation_add(parent_dict, child_id, uid):
|
||||
current_app.test_request_context().push()
|
||||
login_user(UserCache.get(uid))
|
||||
|
||||
db.session.remove()
|
||||
|
||||
for parent in parent_dict:
|
||||
parent_ci_type_name, _attr_name = parent.strip()[1:].split('.', 1)
|
||||
attr_name = CITypeAttributeManager.get_attr_name(parent_ci_type_name, _attr_name)
|
||||
@@ -147,10 +153,14 @@ def ci_relation_add(parent_dict, child_id, uid):
|
||||
except Exception as e:
|
||||
current_app.logger.warning(e)
|
||||
finally:
|
||||
db.session.remove()
|
||||
try:
|
||||
db.session.commit()
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
@celery.task(name="cmdb.ci_relation_delete", queue=CMDB_QUEUE)
|
||||
@reconnect_db
|
||||
def ci_relation_delete(parent_id, child_id):
|
||||
with Lock("CIRelation_{}".format(parent_id)):
|
||||
children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0]
|
||||
@@ -165,9 +175,10 @@ def ci_relation_delete(parent_id, child_id):
|
||||
|
||||
|
||||
@celery.task(name="cmdb.ci_type_attribute_order_rebuild", queue=CMDB_QUEUE)
|
||||
@flush_db
|
||||
@reconnect_db
|
||||
def ci_type_attribute_order_rebuild(type_id, uid):
|
||||
current_app.logger.info('rebuild attribute order')
|
||||
db.session.remove()
|
||||
|
||||
from api.lib.cmdb.ci_type import CITypeAttributeGroupManager
|
||||
|
||||
@@ -188,11 +199,11 @@ def ci_type_attribute_order_rebuild(type_id, uid):
|
||||
|
||||
|
||||
@celery.task(name="cmdb.calc_computed_attribute", queue=CMDB_QUEUE)
|
||||
@flush_db
|
||||
@reconnect_db
|
||||
def calc_computed_attribute(attr_id, uid):
|
||||
from api.lib.cmdb.ci import CIManager
|
||||
|
||||
db.session.remove()
|
||||
|
||||
current_app.test_request_context().push()
|
||||
login_user(UserCache.get(uid))
|
||||
|
||||
|
@@ -84,11 +84,10 @@ class CIView(APIView):
|
||||
ci_dict = self._wrap_ci_dict()
|
||||
|
||||
manager = CIManager()
|
||||
current_app.logger.debug(ci_dict)
|
||||
ci_id = manager.add(ci_type,
|
||||
exist_policy=exist_policy or ExistPolicy.REJECT,
|
||||
_no_attribute_policy=_no_attribute_policy,
|
||||
_is_admin=request.values.pop('__is_admin', False),
|
||||
_is_admin=request.values.pop('__is_admin', None) or False,
|
||||
**ci_dict)
|
||||
|
||||
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)
|
||||
def put(self, ci_id=None):
|
||||
args = request.values
|
||||
current_app.logger.info(args)
|
||||
ci_type = args.get("ci_type")
|
||||
_no_attribute_policy = args.get("no_attribute_policy", ExistPolicy.IGNORE)
|
||||
|
||||
@@ -104,14 +102,14 @@ class CIView(APIView):
|
||||
manager = CIManager()
|
||||
if ci_id is not None:
|
||||
manager.update(ci_id,
|
||||
_is_admin=request.values.pop('__is_admin', False),
|
||||
_is_admin=request.values.pop('__is_admin', None) or False,
|
||||
**ci_dict)
|
||||
else:
|
||||
request.values.pop('exist_policy', None)
|
||||
ci_id = manager.add(ci_type,
|
||||
exist_policy=ExistPolicy.REPLACE,
|
||||
_no_attribute_policy=_no_attribute_policy,
|
||||
_is_admin=request.values.pop('__is_admin', False),
|
||||
_is_admin=request.values.pop('__is_admin', None) or False,
|
||||
**ci_dict)
|
||||
|
||||
return self.jsonify(ci_id=ci_id)
|
||||
@@ -228,11 +226,11 @@ class CIFlushView(APIView):
|
||||
from api.tasks.cmdb import ci_cache
|
||||
from api.lib.cmdb.const import CMDB_QUEUE
|
||||
if ci_id is not None:
|
||||
ci_cache.apply_async([ci_id], queue=CMDB_QUEUE)
|
||||
ci_cache.apply_async(args=(ci_id, None, None), queue=CMDB_QUEUE)
|
||||
else:
|
||||
cis = CI.get_by(to_dict=False)
|
||||
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)
|
||||
|
||||
@@ -242,3 +240,13 @@ class CIAutoDiscoveryStatisticsView(APIView):
|
||||
|
||||
def get(self):
|
||||
return self.jsonify(CIManager.get_ad_statistics())
|
||||
|
||||
|
||||
class CIPasswordView(APIView):
|
||||
url_prefix = "/ci/<int:ci_id>/attributes/<int:attr_id>/password"
|
||||
|
||||
def get(self, ci_id, attr_id):
|
||||
return self.jsonify(ci_id=ci_id, attr_id=attr_id, value=CIManager.load_password(ci_id, attr_id))
|
||||
|
||||
def post(self, ci_id, attr_id):
|
||||
return self.get(ci_id, attr_id)
|
||||
|
37
cmdb-api/api/views/cmdb/inner_secrets.py
Normal file
37
cmdb-api/api/views/cmdb/inner_secrets.py
Normal 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)
|
1
cmdb-api/migrations/README
Normal file
1
cmdb-api/migrations/README
Normal file
@@ -0,0 +1 @@
|
||||
Generic single-database configuration.
|
45
cmdb-api/migrations/alembic.ini
Normal file
45
cmdb-api/migrations/alembic.ini
Normal 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
110
cmdb-api/migrations/env.py
Normal 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()
|
24
cmdb-api/migrations/script.py.mako
Normal file
24
cmdb-api/migrations/script.py.mako
Normal 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"}
|
360
cmdb-api/migrations/versions/6a4df2623057_.py
Normal file
360
cmdb-api/migrations/versions/6a4df2623057_.py
Normal 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 ###
|
@@ -1,7 +1,7 @@
|
||||
-i https://mirrors.aliyun.com/pypi/simple
|
||||
alembic==1.7.7
|
||||
bs4==0.0.1
|
||||
celery==5.3.1
|
||||
celery>=5.3.1
|
||||
celery-once==3.0.1
|
||||
click==8.1.3
|
||||
elasticsearch==7.17.9
|
||||
@@ -18,21 +18,22 @@ Flask-RESTful==0.3.10
|
||||
Flask-SQLAlchemy==2.5.0
|
||||
future==0.18.3
|
||||
gunicorn==21.0.1
|
||||
hvac==2.0.0
|
||||
itsdangerous==2.1.2
|
||||
Jinja2==3.1.2
|
||||
jinja2schema==0.1.4
|
||||
jsonschema==4.18.0
|
||||
kombu==5.3.1
|
||||
kombu>=5.3.1
|
||||
Mako==1.2.4
|
||||
MarkupSafe==2.1.3
|
||||
marshmallow==2.20.2
|
||||
more-itertools==5.0.0
|
||||
msgpack-python==0.5.6
|
||||
Pillow==9.3.0
|
||||
pycryptodome==3.12.0
|
||||
cryptography==41.0.2
|
||||
PyJWT==2.4.0
|
||||
PyMySQL==1.1.0
|
||||
python-ldap==3.4.0
|
||||
ldap3==2.9.1
|
||||
PyYAML==6.0
|
||||
redis==4.6.0
|
||||
requests==2.31.0
|
||||
@@ -46,3 +47,7 @@ toposort==1.10
|
||||
treelib==1.6.1
|
||||
Werkzeug==2.3.6
|
||||
WTForms==3.0.0
|
||||
shamir~=17.12.0
|
||||
hvac~=2.0.0
|
||||
pycryptodomex>=3.19.0
|
||||
colorama>=0.4.6
|
||||
|
@@ -97,3 +97,9 @@ BOOL_TRUE = ['true', 'TRUE', 'True', True, '1', 1, "Yes", "YES", "yes", 'Y', 'y'
|
||||
|
||||
# # messenger
|
||||
USE_MESSENGER = True
|
||||
|
||||
# # secrets
|
||||
SECRETS_ENGINE = 'inner' # 'inner' or 'vault'
|
||||
VAULT_URL = ''
|
||||
VAULT_TOKEN = ''
|
||||
INNER_TRIGGER_TOKEN = ''
|
||||
|
@@ -54,6 +54,48 @@
|
||||
<div class="content unicode" style="display: block;">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">icon-xianxing-password</div>
|
||||
<div class="code-name">&#xe894;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">icon-xianxing-link</div>
|
||||
<div class="code-name">&#xe895;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">itsm-oneclick download</div>
|
||||
<div class="code-name">&#xe892;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">itsm-package download</div>
|
||||
<div class="code-name">&#xe893;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">weixin</div>
|
||||
<div class="code-name">&#xe891;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">itsm-again</div>
|
||||
<div class="code-name">&#xe88f;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">itsm-next</div>
|
||||
<div class="code-name">&#xe890;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">wechatApp</div>
|
||||
@@ -4002,9 +4044,9 @@
|
||||
<pre><code class="language-css"
|
||||
>@font-face {
|
||||
font-family: 'iconfont';
|
||||
src: url('iconfont.woff2?t=1696815443987') format('woff2'),
|
||||
url('iconfont.woff?t=1696815443987') format('woff'),
|
||||
url('iconfont.ttf?t=1696815443987') format('truetype');
|
||||
src: url('iconfont.woff2?t=1698273699449') format('woff2'),
|
||||
url('iconfont.woff?t=1698273699449') format('woff'),
|
||||
url('iconfont.ttf?t=1698273699449') format('truetype');
|
||||
}
|
||||
</code></pre>
|
||||
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
|
||||
@@ -4030,6 +4072,69 @@
|
||||
<div class="content font-class">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-xianxing-password"></span>
|
||||
<div class="name">
|
||||
icon-xianxing-password
|
||||
</div>
|
||||
<div class="code-name">.icon-xianxing-password
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont icon-xianxing-link"></span>
|
||||
<div class="name">
|
||||
icon-xianxing-link
|
||||
</div>
|
||||
<div class="code-name">.icon-xianxing-link
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont a-itsm-oneclickdownload"></span>
|
||||
<div class="name">
|
||||
itsm-oneclick download
|
||||
</div>
|
||||
<div class="code-name">.a-itsm-oneclickdownload
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont a-itsm-packagedownload"></span>
|
||||
<div class="name">
|
||||
itsm-package download
|
||||
</div>
|
||||
<div class="code-name">.a-itsm-packagedownload
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont a-Frame4"></span>
|
||||
<div class="name">
|
||||
weixin
|
||||
</div>
|
||||
<div class="code-name">.a-Frame4
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont itsm-again"></span>
|
||||
<div class="name">
|
||||
itsm-again
|
||||
</div>
|
||||
<div class="code-name">.itsm-again
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont itsm-next"></span>
|
||||
<div class="name">
|
||||
itsm-next
|
||||
</div>
|
||||
<div class="code-name">.itsm-next
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont wechatApp"></span>
|
||||
<div class="name">
|
||||
@@ -9952,6 +10057,62 @@
|
||||
<div class="content symbol">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-xianxing-password"></use>
|
||||
</svg>
|
||||
<div class="name">icon-xianxing-password</div>
|
||||
<div class="code-name">#icon-xianxing-password</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-xianxing-link"></use>
|
||||
</svg>
|
||||
<div class="name">icon-xianxing-link</div>
|
||||
<div class="code-name">#icon-xianxing-link</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#a-itsm-oneclickdownload"></use>
|
||||
</svg>
|
||||
<div class="name">itsm-oneclick download</div>
|
||||
<div class="code-name">#a-itsm-oneclickdownload</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#a-itsm-packagedownload"></use>
|
||||
</svg>
|
||||
<div class="name">itsm-package download</div>
|
||||
<div class="code-name">#a-itsm-packagedownload</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#a-Frame4"></use>
|
||||
</svg>
|
||||
<div class="name">weixin</div>
|
||||
<div class="code-name">#a-Frame4</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#itsm-again"></use>
|
||||
</svg>
|
||||
<div class="name">itsm-again</div>
|
||||
<div class="code-name">#itsm-again</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#itsm-next"></use>
|
||||
</svg>
|
||||
<div class="name">itsm-next</div>
|
||||
<div class="code-name">#itsm-next</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#wechatApp"></use>
|
||||
|
@@ -1,8 +1,8 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 3857903 */
|
||||
src: url('iconfont.woff2?t=1696815443987') format('woff2'),
|
||||
url('iconfont.woff?t=1696815443987') format('woff'),
|
||||
url('iconfont.ttf?t=1696815443987') format('truetype');
|
||||
src: url('iconfont.woff2?t=1698273699449') format('woff2'),
|
||||
url('iconfont.woff?t=1698273699449') format('woff'),
|
||||
url('iconfont.ttf?t=1698273699449') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
@@ -13,6 +13,34 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-xianxing-password:before {
|
||||
content: "\e894";
|
||||
}
|
||||
|
||||
.icon-xianxing-link:before {
|
||||
content: "\e895";
|
||||
}
|
||||
|
||||
.a-itsm-oneclickdownload:before {
|
||||
content: "\e892";
|
||||
}
|
||||
|
||||
.a-itsm-packagedownload:before {
|
||||
content: "\e893";
|
||||
}
|
||||
|
||||
.a-Frame4:before {
|
||||
content: "\e891";
|
||||
}
|
||||
|
||||
.itsm-again:before {
|
||||
content: "\e88f";
|
||||
}
|
||||
|
||||
.itsm-next:before {
|
||||
content: "\e890";
|
||||
}
|
||||
|
||||
.wechatApp:before {
|
||||
content: "\e88e";
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
@@ -5,6 +5,55 @@
|
||||
"css_prefix_text": "",
|
||||
"description": "",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "37830610",
|
||||
"name": "icon-xianxing-password",
|
||||
"font_class": "icon-xianxing-password",
|
||||
"unicode": "e894",
|
||||
"unicode_decimal": 59540
|
||||
},
|
||||
{
|
||||
"icon_id": "37830609",
|
||||
"name": "icon-xianxing-link",
|
||||
"font_class": "icon-xianxing-link",
|
||||
"unicode": "e895",
|
||||
"unicode_decimal": 59541
|
||||
},
|
||||
{
|
||||
"icon_id": "37822199",
|
||||
"name": "itsm-oneclick download",
|
||||
"font_class": "a-itsm-oneclickdownload",
|
||||
"unicode": "e892",
|
||||
"unicode_decimal": 59538
|
||||
},
|
||||
{
|
||||
"icon_id": "37822198",
|
||||
"name": "itsm-package download",
|
||||
"font_class": "a-itsm-packagedownload",
|
||||
"unicode": "e893",
|
||||
"unicode_decimal": 59539
|
||||
},
|
||||
{
|
||||
"icon_id": "37772067",
|
||||
"name": "weixin",
|
||||
"font_class": "a-Frame4",
|
||||
"unicode": "e891",
|
||||
"unicode_decimal": 59537
|
||||
},
|
||||
{
|
||||
"icon_id": "37632784",
|
||||
"name": "itsm-again",
|
||||
"font_class": "itsm-again",
|
||||
"unicode": "e88f",
|
||||
"unicode_decimal": 59535
|
||||
},
|
||||
{
|
||||
"icon_id": "37632783",
|
||||
"name": "itsm-next",
|
||||
"font_class": "itsm-next",
|
||||
"unicode": "e890",
|
||||
"unicode_decimal": 59536
|
||||
},
|
||||
{
|
||||
"icon_id": "37590786",
|
||||
"name": "wechatApp",
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,282 +1,290 @@
|
||||
<template>
|
||||
<div>
|
||||
<a-popover
|
||||
v-if="isDropdown"
|
||||
v-model="visible"
|
||||
trigger="click"
|
||||
:placement="placement"
|
||||
overlayClassName="table-filter"
|
||||
@visibleChange="visibleChange"
|
||||
>
|
||||
<slot name="popover_item">
|
||||
<a-button type="primary" ghost>条件过滤<a-icon type="filter"/></a-button>
|
||||
</slot>
|
||||
<template slot="content">
|
||||
<Expression v-model="ruleList" :canSearchPreferenceAttrList="canSearchPreferenceAttrList" />
|
||||
<a-divider :style="{ margin: '10px 0' }" />
|
||||
<div style="width:534px">
|
||||
<a-space :style="{ display: 'flex', justifyContent: 'flex-end' }">
|
||||
<a-button type="primary" size="small" @click="handleSubmit">确定</a-button>
|
||||
<a-button size="small" @click="handleClear">清空</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
</a-popover>
|
||||
<Expression v-else v-model="ruleList" :canSearchPreferenceAttrList="canSearchPreferenceAttrList" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import Expression from './expression.vue'
|
||||
import { advancedExpList, compareTypeList } from './constants'
|
||||
|
||||
export default {
|
||||
name: 'FilterComp',
|
||||
components: { Expression },
|
||||
props: {
|
||||
canSearchPreferenceAttrList: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => [],
|
||||
},
|
||||
expression: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
regQ: {
|
||||
type: String,
|
||||
default: '(?<=q=).+(?=&)|(?<=q=).+$',
|
||||
},
|
||||
placement: {
|
||||
type: String,
|
||||
default: 'bottomRight',
|
||||
},
|
||||
isDropdown: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
advancedExpList,
|
||||
compareTypeList,
|
||||
visible: false,
|
||||
ruleList: [],
|
||||
filterExp: '',
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
visibleChange(open, isInitOne = true) {
|
||||
// isInitOne 初始化exp为空时,ruleList是否默认给一条
|
||||
// const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
|
||||
const exp = this.expression.match(new RegExp(this.regQ, 'g'))
|
||||
? this.expression.match(new RegExp(this.regQ, 'g'))[0]
|
||||
: null
|
||||
if (open && exp) {
|
||||
const expArray = exp.split(',').map((item) => {
|
||||
let has_not = ''
|
||||
const key = item.split(':')[0]
|
||||
const val = item
|
||||
.split(':')
|
||||
.slice(1)
|
||||
.join(':')
|
||||
let type, property, exp, value, min, max, compareType
|
||||
if (key.includes('-')) {
|
||||
type = 'or'
|
||||
if (key.includes('~')) {
|
||||
property = key.substring(2)
|
||||
has_not = '~'
|
||||
} else {
|
||||
property = key.substring(1)
|
||||
}
|
||||
} else {
|
||||
type = 'and'
|
||||
if (key.includes('~')) {
|
||||
property = key.substring(1)
|
||||
has_not = '~'
|
||||
} else {
|
||||
property = key
|
||||
}
|
||||
}
|
||||
|
||||
const in_reg = /(?<=\().+(?=\))/g
|
||||
const range_reg = /(?<=\[).+(?=\])/g
|
||||
const compare_reg = /(?<=>=|<=|>(?!=)|<(?!=)).+/
|
||||
if (val === '*') {
|
||||
exp = has_not + 'value'
|
||||
value = ''
|
||||
} else if (in_reg.test(val)) {
|
||||
exp = has_not + 'in'
|
||||
value = val.match(in_reg)[0]
|
||||
} else if (range_reg.test(val)) {
|
||||
exp = has_not + 'range'
|
||||
value = val.match(range_reg)[0]
|
||||
min = value.split('_TO_')[0]
|
||||
max = value.split('_TO_')[1]
|
||||
} else if (compare_reg.test(val)) {
|
||||
exp = has_not + 'compare'
|
||||
value = val.match(compare_reg)[0]
|
||||
const _compareType = val.substring(0, val.match(compare_reg)['index'])
|
||||
const idx = compareTypeList.findIndex((item) => item.label === _compareType)
|
||||
compareType = compareTypeList[idx].value
|
||||
} else if (!val.includes('*')) {
|
||||
exp = has_not + 'is'
|
||||
value = val
|
||||
} else {
|
||||
const resList = [
|
||||
['contain', /(?<=\*).*(?=\*)/g],
|
||||
['end_with', /(?<=\*).+/g],
|
||||
['start_with', /.+(?=\*)/g],
|
||||
]
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const reg = resList[i]
|
||||
if (reg[1].test(val)) {
|
||||
exp = has_not + reg[0]
|
||||
value = val.match(reg[1])[0]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
id: uuidv4(),
|
||||
type,
|
||||
property,
|
||||
exp,
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
compareType,
|
||||
}
|
||||
})
|
||||
this.ruleList = [...expArray]
|
||||
} else if (open) {
|
||||
this.ruleList = isInitOne
|
||||
? [
|
||||
{
|
||||
id: uuidv4(),
|
||||
type: 'and',
|
||||
property:
|
||||
this.canSearchPreferenceAttrList && this.canSearchPreferenceAttrList.length
|
||||
? this.canSearchPreferenceAttrList[0].name
|
||||
: undefined,
|
||||
exp: 'is',
|
||||
value: null,
|
||||
},
|
||||
]
|
||||
: []
|
||||
}
|
||||
},
|
||||
handleClear() {
|
||||
this.ruleList = [
|
||||
{
|
||||
id: uuidv4(),
|
||||
type: 'and',
|
||||
property: this.canSearchPreferenceAttrList[0].name,
|
||||
exp: 'is',
|
||||
value: null,
|
||||
},
|
||||
]
|
||||
this.filterExp = ''
|
||||
this.visible = false
|
||||
this.$emit('setExpFromFilter', this.filterExp)
|
||||
},
|
||||
handleSubmit() {
|
||||
if (this.ruleList && this.ruleList.length) {
|
||||
this.ruleList[0].type = 'and' // 增删后,以防万一第一个不是and
|
||||
this.filterExp = ''
|
||||
const expList = this.ruleList.map((rule) => {
|
||||
let singleRuleExp = ''
|
||||
let _exp = rule.exp
|
||||
if (rule.type === 'or') {
|
||||
singleRuleExp += '-'
|
||||
}
|
||||
if (rule.exp.includes('~')) {
|
||||
singleRuleExp += '~'
|
||||
_exp = rule.exp.split('~')[1]
|
||||
}
|
||||
singleRuleExp += `${rule.property}:`
|
||||
if (_exp === 'is') {
|
||||
singleRuleExp += `${rule.value ?? ''}`
|
||||
}
|
||||
if (_exp === 'contain') {
|
||||
singleRuleExp += `*${rule.value ?? ''}*`
|
||||
}
|
||||
if (_exp === 'start_with') {
|
||||
singleRuleExp += `${rule.value ?? ''}*`
|
||||
}
|
||||
if (_exp === 'end_with') {
|
||||
singleRuleExp += `*${rule.value ?? ''}`
|
||||
}
|
||||
if (_exp === 'value') {
|
||||
singleRuleExp += `*`
|
||||
}
|
||||
if (_exp === 'in') {
|
||||
singleRuleExp += `(${rule.value ?? ''})`
|
||||
}
|
||||
if (_exp === 'range') {
|
||||
singleRuleExp += `[${rule.min}_TO_${rule.max}]`
|
||||
}
|
||||
if (_exp === 'compare') {
|
||||
const idx = compareTypeList.findIndex((item) => item.value === rule.compareType)
|
||||
singleRuleExp += `${compareTypeList[idx].label}${rule.value ?? ''}`
|
||||
}
|
||||
return singleRuleExp
|
||||
})
|
||||
this.filterExp = expList.join(',')
|
||||
this.$emit('setExpFromFilter', this.filterExp)
|
||||
} else {
|
||||
this.$emit('setExpFromFilter', '')
|
||||
}
|
||||
this.visible = false
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.table-filter {
|
||||
.table-filter-add {
|
||||
margin-top: 10px;
|
||||
& > a {
|
||||
padding: 2px 8px;
|
||||
&:hover {
|
||||
background-color: #f0faff;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.table-filter-extra-icon {
|
||||
padding: 0px 2px;
|
||||
&:hover {
|
||||
display: inline-block;
|
||||
border-radius: 5px;
|
||||
background-color: #f0faff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.table-filter-extra-operation {
|
||||
.ant-popover-inner-content {
|
||||
padding: 3px 4px;
|
||||
.operation {
|
||||
cursor: pointer;
|
||||
width: 90px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
padding: 3px 4px;
|
||||
border-radius: 5px;
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
background-color: #f0faff;
|
||||
}
|
||||
> .anticon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div>
|
||||
<a-popover
|
||||
v-if="isDropdown"
|
||||
v-model="visible"
|
||||
trigger="click"
|
||||
:placement="placement"
|
||||
overlayClassName="table-filter"
|
||||
@visibleChange="visibleChange"
|
||||
>
|
||||
<slot name="popover_item">
|
||||
<a-button type="primary" ghost>条件过滤<a-icon type="filter"/></a-button>
|
||||
</slot>
|
||||
<template slot="content">
|
||||
<Expression
|
||||
v-model="ruleList"
|
||||
:canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)"
|
||||
/>
|
||||
<a-divider :style="{ margin: '10px 0' }" />
|
||||
<div style="width:534px">
|
||||
<a-space :style="{ display: 'flex', justifyContent: 'flex-end' }">
|
||||
<a-button type="primary" size="small" @click="handleSubmit">确定</a-button>
|
||||
<a-button size="small" @click="handleClear">清空</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
</a-popover>
|
||||
<Expression
|
||||
v-else
|
||||
v-model="ruleList"
|
||||
:canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import Expression from './expression.vue'
|
||||
import { advancedExpList, compareTypeList } from './constants'
|
||||
|
||||
export default {
|
||||
name: 'FilterComp',
|
||||
components: { Expression },
|
||||
props: {
|
||||
canSearchPreferenceAttrList: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => [],
|
||||
},
|
||||
expression: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
regQ: {
|
||||
type: String,
|
||||
default: '(?<=q=).+(?=&)|(?<=q=).+$',
|
||||
},
|
||||
placement: {
|
||||
type: String,
|
||||
default: 'bottomRight',
|
||||
},
|
||||
isDropdown: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
advancedExpList,
|
||||
compareTypeList,
|
||||
visible: false,
|
||||
ruleList: [],
|
||||
filterExp: '',
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
visibleChange(open, isInitOne = true) {
|
||||
// isInitOne 初始化exp为空时,ruleList是否默认给一条
|
||||
// const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
|
||||
const exp = this.expression.match(new RegExp(this.regQ, 'g'))
|
||||
? this.expression.match(new RegExp(this.regQ, 'g'))[0]
|
||||
: null
|
||||
if (open && exp) {
|
||||
const expArray = exp.split(',').map((item) => {
|
||||
let has_not = ''
|
||||
const key = item.split(':')[0]
|
||||
const val = item
|
||||
.split(':')
|
||||
.slice(1)
|
||||
.join(':')
|
||||
let type, property, exp, value, min, max, compareType
|
||||
if (key.includes('-')) {
|
||||
type = 'or'
|
||||
if (key.includes('~')) {
|
||||
property = key.substring(2)
|
||||
has_not = '~'
|
||||
} else {
|
||||
property = key.substring(1)
|
||||
}
|
||||
} else {
|
||||
type = 'and'
|
||||
if (key.includes('~')) {
|
||||
property = key.substring(1)
|
||||
has_not = '~'
|
||||
} else {
|
||||
property = key
|
||||
}
|
||||
}
|
||||
|
||||
const in_reg = /(?<=\().+(?=\))/g
|
||||
const range_reg = /(?<=\[).+(?=\])/g
|
||||
const compare_reg = /(?<=>=|<=|>(?!=)|<(?!=)).+/
|
||||
if (val === '*') {
|
||||
exp = has_not + 'value'
|
||||
value = ''
|
||||
} else if (in_reg.test(val)) {
|
||||
exp = has_not + 'in'
|
||||
value = val.match(in_reg)[0]
|
||||
} else if (range_reg.test(val)) {
|
||||
exp = has_not + 'range'
|
||||
value = val.match(range_reg)[0]
|
||||
min = value.split('_TO_')[0]
|
||||
max = value.split('_TO_')[1]
|
||||
} else if (compare_reg.test(val)) {
|
||||
exp = has_not + 'compare'
|
||||
value = val.match(compare_reg)[0]
|
||||
const _compareType = val.substring(0, val.match(compare_reg)['index'])
|
||||
const idx = compareTypeList.findIndex((item) => item.label === _compareType)
|
||||
compareType = compareTypeList[idx].value
|
||||
} else if (!val.includes('*')) {
|
||||
exp = has_not + 'is'
|
||||
value = val
|
||||
} else {
|
||||
const resList = [
|
||||
['contain', /(?<=\*).*(?=\*)/g],
|
||||
['end_with', /(?<=\*).+/g],
|
||||
['start_with', /.+(?=\*)/g],
|
||||
]
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const reg = resList[i]
|
||||
if (reg[1].test(val)) {
|
||||
exp = has_not + reg[0]
|
||||
value = val.match(reg[1])[0]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
id: uuidv4(),
|
||||
type,
|
||||
property,
|
||||
exp,
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
compareType,
|
||||
}
|
||||
})
|
||||
this.ruleList = [...expArray]
|
||||
} else if (open) {
|
||||
const _canSearchPreferenceAttrList = this.canSearchPreferenceAttrList.filter((attr) => !attr.is_password)
|
||||
this.ruleList = isInitOne
|
||||
? [
|
||||
{
|
||||
id: uuidv4(),
|
||||
type: 'and',
|
||||
property:
|
||||
_canSearchPreferenceAttrList && _canSearchPreferenceAttrList.length
|
||||
? _canSearchPreferenceAttrList[0].name
|
||||
: undefined,
|
||||
exp: 'is',
|
||||
value: null,
|
||||
},
|
||||
]
|
||||
: []
|
||||
}
|
||||
},
|
||||
handleClear() {
|
||||
this.ruleList = [
|
||||
{
|
||||
id: uuidv4(),
|
||||
type: 'and',
|
||||
property: this.canSearchPreferenceAttrList[0].name,
|
||||
exp: 'is',
|
||||
value: null,
|
||||
},
|
||||
]
|
||||
this.filterExp = ''
|
||||
this.visible = false
|
||||
this.$emit('setExpFromFilter', this.filterExp)
|
||||
},
|
||||
handleSubmit() {
|
||||
if (this.ruleList && this.ruleList.length) {
|
||||
this.ruleList[0].type = 'and' // 增删后,以防万一第一个不是and
|
||||
this.filterExp = ''
|
||||
const expList = this.ruleList.map((rule) => {
|
||||
let singleRuleExp = ''
|
||||
let _exp = rule.exp
|
||||
if (rule.type === 'or') {
|
||||
singleRuleExp += '-'
|
||||
}
|
||||
if (rule.exp.includes('~')) {
|
||||
singleRuleExp += '~'
|
||||
_exp = rule.exp.split('~')[1]
|
||||
}
|
||||
singleRuleExp += `${rule.property}:`
|
||||
if (_exp === 'is') {
|
||||
singleRuleExp += `${rule.value ?? ''}`
|
||||
}
|
||||
if (_exp === 'contain') {
|
||||
singleRuleExp += `*${rule.value ?? ''}*`
|
||||
}
|
||||
if (_exp === 'start_with') {
|
||||
singleRuleExp += `${rule.value ?? ''}*`
|
||||
}
|
||||
if (_exp === 'end_with') {
|
||||
singleRuleExp += `*${rule.value ?? ''}`
|
||||
}
|
||||
if (_exp === 'value') {
|
||||
singleRuleExp += `*`
|
||||
}
|
||||
if (_exp === 'in') {
|
||||
singleRuleExp += `(${rule.value ?? ''})`
|
||||
}
|
||||
if (_exp === 'range') {
|
||||
singleRuleExp += `[${rule.min}_TO_${rule.max}]`
|
||||
}
|
||||
if (_exp === 'compare') {
|
||||
const idx = compareTypeList.findIndex((item) => item.value === rule.compareType)
|
||||
singleRuleExp += `${compareTypeList[idx].label}${rule.value ?? ''}`
|
||||
}
|
||||
return singleRuleExp
|
||||
})
|
||||
this.filterExp = expList.join(',')
|
||||
this.$emit('setExpFromFilter', this.filterExp)
|
||||
} else {
|
||||
this.$emit('setExpFromFilter', '')
|
||||
}
|
||||
this.visible = false
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.table-filter {
|
||||
.table-filter-add {
|
||||
margin-top: 10px;
|
||||
& > a {
|
||||
padding: 2px 8px;
|
||||
&:hover {
|
||||
background-color: #f0faff;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.table-filter-extra-icon {
|
||||
padding: 0px 2px;
|
||||
&:hover {
|
||||
display: inline-block;
|
||||
border-radius: 5px;
|
||||
background-color: #f0faff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.table-filter-extra-operation {
|
||||
.ant-popover-inner-content {
|
||||
padding: 3px 4px;
|
||||
.operation {
|
||||
cursor: pointer;
|
||||
width: 90px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
padding: 3px 4px;
|
||||
border-radius: 5px;
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
background-color: #f0faff;
|
||||
}
|
||||
> .anticon {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,57 +1,45 @@
|
||||
<template>
|
||||
<span>
|
||||
<ops-icon :type="getPropertyIcon(attr)" />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ValueTypeIcon',
|
||||
props: {
|
||||
attr: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getPropertyStyle(attr) {
|
||||
switch (attr.value_type) {
|
||||
case '0':
|
||||
return { color: '#cf1322', backgroundColor: '#fff1f0' }
|
||||
case '1':
|
||||
return { color: '#d4b106', backgroundColor: '#feffe6' }
|
||||
case '2':
|
||||
return { color: '#d46b08', backgroundColor: '#fff7e6' }
|
||||
case '3':
|
||||
return { color: '#531dab', backgroundColor: '#f9f0ff' }
|
||||
case '4':
|
||||
return { color: '#389e0d', backgroundColor: '#f6ffed' }
|
||||
case '5':
|
||||
return { color: '#08979c', backgroundColor: '#e6fffb' }
|
||||
case '6':
|
||||
return { color: '#c41d7f', backgroundColor: '#fff0f6' }
|
||||
}
|
||||
},
|
||||
getPropertyIcon(attr) {
|
||||
switch (attr.value_type) {
|
||||
case '0':
|
||||
return 'icon-xianxing-shishu'
|
||||
case '1':
|
||||
return 'icon-xianxing-fudianshu'
|
||||
case '2':
|
||||
return 'icon-xianxing-wenben'
|
||||
case '3':
|
||||
return 'icon-xianxing-datetime'
|
||||
case '4':
|
||||
return 'icon-xianxing-date'
|
||||
case '5':
|
||||
return 'icon-xianxing-time'
|
||||
case '6':
|
||||
return 'icon-xianxing-json'
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
<template>
|
||||
<span>
|
||||
<ops-icon :type="getPropertyIcon(attr)" />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ValueTypeIcon',
|
||||
props: {
|
||||
attr: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getPropertyIcon(attr) {
|
||||
switch (attr.value_type) {
|
||||
case '0':
|
||||
return 'icon-xianxing-shishu'
|
||||
case '1':
|
||||
return 'icon-xianxing-fudianshu'
|
||||
case '2':
|
||||
if (attr.is_password) {
|
||||
return 'icon-xianxing-password'
|
||||
}
|
||||
if (attr.is_link) {
|
||||
return 'icon-xianxing-link'
|
||||
}
|
||||
return 'icon-xianxing-wenben'
|
||||
case '3':
|
||||
return 'icon-xianxing-datetime'
|
||||
case '4':
|
||||
return 'icon-xianxing-date'
|
||||
case '5':
|
||||
return 'icon-xianxing-time'
|
||||
case '6':
|
||||
return 'icon-xianxing-json'
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
@@ -4,7 +4,7 @@ const appConfig = {
|
||||
buildAclToModules: true, // 是否在各个应用下 内联权限管理
|
||||
ssoLogoutURL: '/api/sso/logout',
|
||||
showDocs: false,
|
||||
useEncryption: true,
|
||||
useEncryption: false,
|
||||
}
|
||||
|
||||
export default appConfig
|
||||
|
@@ -99,7 +99,7 @@
|
||||
align="center"
|
||||
show-overflow>
|
||||
<template #default="{ row }">
|
||||
<span v-show="row.isGroup">
|
||||
<span v-show="isGroup">
|
||||
<a @click="handleDisplayMember(row)">成员</a>
|
||||
<a-divider type="vertical" />
|
||||
<a @click="handleGroupEdit(row)">编辑</a>
|
||||
|
@@ -35,8 +35,8 @@ export default {
|
||||
secret: '',
|
||||
},
|
||||
rules: {
|
||||
key: [{ required: true, message: 'key is required' }],
|
||||
secret: [{ required: true, message: 'secret is required' }],
|
||||
key: [{ required: false, message: 'key is required' }],
|
||||
secret: [{ required: false, message: 'secret is required' }],
|
||||
},
|
||||
visible: false,
|
||||
}
|
||||
|
@@ -1,194 +1,198 @@
|
||||
<template>
|
||||
<div class="acl-users">
|
||||
<div class="acl-users-header">
|
||||
<a-button v-if="isAclAdmin" @click="handleCreate" type="primary">{{ btnName }}</a-button>
|
||||
<a-input-search
|
||||
class="ops-input"
|
||||
allowClear
|
||||
:style="{ display: 'inline', marginLeft: '10px' }"
|
||||
placeholder="搜索 | 用户名、中文名"
|
||||
v-model="searchName"
|
||||
></a-input-search>
|
||||
</div>
|
||||
<a-spin :spinning="loading">
|
||||
<vxe-grid
|
||||
stripe
|
||||
class="ops-stripe-table"
|
||||
:columns="tableColumns"
|
||||
:data="tableData"
|
||||
show-overflow
|
||||
highlight-hover-row
|
||||
:height="`${windowHeight - 165}px`"
|
||||
size="small"
|
||||
>
|
||||
<template #block_default="{row}">
|
||||
<a-icon type="lock" v-if="row.block" />
|
||||
</template>
|
||||
<template #action_default="{row}">
|
||||
<a-space>
|
||||
<a :disabled="isAclAdmin ? false : true" @click="handleEdit(row)">
|
||||
<a-icon type="edit" />
|
||||
</a>
|
||||
<a-tooltip title="权限汇总">
|
||||
<a @click="handlePermCollect(row)"><a-icon type="solution"/></a>
|
||||
</a-tooltip>
|
||||
<a-popconfirm :title="`确认删除【${row.nickname || row.username}】?`" @confirm="deleteUser(row.uid)">
|
||||
<a :style="{ color: 'red' }"><ops-icon type="icon-xianxing-delete"/></a>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</vxe-grid>
|
||||
</a-spin>
|
||||
<userForm ref="userForm" :handleOk="handleOk"> </userForm>
|
||||
<perm-collect-form ref="permCollectForm"></perm-collect-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import userForm from './module/userForm'
|
||||
import PermCollectForm from './module/permCollectForm'
|
||||
import { deleteUserById, searchUser, getOnDutyUser } from '@/modules/acl/api/user'
|
||||
|
||||
export default {
|
||||
name: 'Users',
|
||||
components: {
|
||||
userForm,
|
||||
PermCollectForm,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
tableColumns: [
|
||||
{
|
||||
title: '用户名',
|
||||
field: 'username',
|
||||
sortable: true,
|
||||
minWidth: '100px',
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
title: '中文名',
|
||||
field: 'nickname',
|
||||
minWidth: '100px',
|
||||
},
|
||||
{
|
||||
title: '加入时间',
|
||||
field: 'date_joined',
|
||||
minWidth: '160px',
|
||||
align: 'center',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
title: '锁定',
|
||||
field: 'block',
|
||||
width: '150px',
|
||||
align: 'center',
|
||||
slots: {
|
||||
default: 'block_default',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
field: 'action',
|
||||
width: '150px',
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
slots: {
|
||||
default: 'action_default',
|
||||
},
|
||||
},
|
||||
],
|
||||
onDutuUids: [],
|
||||
btnName: '新增用户',
|
||||
allUsers: [],
|
||||
tableData: [],
|
||||
searchName: '',
|
||||
}
|
||||
},
|
||||
beforeCreate() {
|
||||
this.form = this.$form.createForm(this)
|
||||
},
|
||||
async beforeMount() {
|
||||
this.loading = true
|
||||
await getOnDutyUser().then((res) => {
|
||||
this.onDutuUids = res.map((i) => i.uid)
|
||||
this.search()
|
||||
})
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
windowHeight: (state) => state.windowHeight,
|
||||
}),
|
||||
isAclAdmin: function() {
|
||||
if (this.$store.state.user.roles.permissions.filter((item) => item === 'acl_admin').length > 0) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
searchName: {
|
||||
immediate: true,
|
||||
handler(newVal, oldVal) {
|
||||
if (newVal) {
|
||||
this.tableData = this.allUsers.filter(
|
||||
(item) =>
|
||||
item.username.toLowerCase().includes(newVal.toLowerCase()) ||
|
||||
item.nickname.toLowerCase().includes(newVal.toLowerCase())
|
||||
)
|
||||
} else {
|
||||
this.tableData = this.allUsers
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
mounted() {},
|
||||
inject: ['reload'],
|
||||
|
||||
methods: {
|
||||
search() {
|
||||
searchUser({ page_size: 10000 }).then((res) => {
|
||||
const ret = res.users.filter((u) => this.onDutuUids.includes(u.uid))
|
||||
this.allUsers = ret
|
||||
this.tableData = ret
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
handlePermCollect(record) {
|
||||
this.$refs['permCollectForm'].collect(record)
|
||||
},
|
||||
handleEdit(record) {
|
||||
this.$refs.userForm.handleEdit(record)
|
||||
},
|
||||
handleOk() {
|
||||
this.searchName = ''
|
||||
this.search()
|
||||
},
|
||||
handleCreate() {
|
||||
this.$refs.userForm.handleCreate()
|
||||
},
|
||||
deleteUser(uid) {
|
||||
deleteUserById(uid).then((res) => {
|
||||
this.$message.success(`删除成功!`)
|
||||
this.handleOk()
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.acl-users {
|
||||
border-radius: 15px;
|
||||
background-color: #fff;
|
||||
height: calc(100vh - 64px);
|
||||
margin-bottom: -24px;
|
||||
padding: 24px;
|
||||
.acl-users-header {
|
||||
display: inline-flex;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class="acl-users">
|
||||
<div class="acl-users-header">
|
||||
<a-button v-if="isAclAdmin" @click="handleCreate" type="primary">{{ btnName }}</a-button>
|
||||
<a-input-search
|
||||
class="ops-input"
|
||||
allowClear
|
||||
:style="{ display: 'inline', marginLeft: '10px' }"
|
||||
placeholder="搜索 | 用户名、中文名"
|
||||
v-model="searchName"
|
||||
></a-input-search>
|
||||
</div>
|
||||
<a-spin :spinning="loading">
|
||||
<vxe-grid
|
||||
stripe
|
||||
class="ops-stripe-table"
|
||||
:columns="tableColumns"
|
||||
:data="tableData"
|
||||
show-overflow
|
||||
highlight-hover-row
|
||||
:height="`${windowHeight - 165}px`"
|
||||
size="small"
|
||||
>
|
||||
<template #block_default="{row}">
|
||||
<a-icon type="lock" v-if="row.block" />
|
||||
</template>
|
||||
<template #action_default="{row}">
|
||||
<a-space>
|
||||
<a :disabled="isAclAdmin ? false : true" @click="handleEdit(row)">
|
||||
<a-icon type="edit" />
|
||||
</a>
|
||||
<a-tooltip title="权限汇总">
|
||||
<a @click="handlePermCollect(row)"><a-icon type="solution"/></a>
|
||||
</a-tooltip>
|
||||
<a-popconfirm :title="`确认删除【${row.nickname || row.username}】?`" @confirm="deleteUser(row.uid)">
|
||||
<a :style="{ color: 'red' }"><ops-icon type="icon-xianxing-delete"/></a>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</vxe-grid>
|
||||
</a-spin>
|
||||
<userForm ref="userForm" :handleOk="handleOk"> </userForm>
|
||||
<perm-collect-form ref="permCollectForm"></perm-collect-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import userForm from './module/userForm'
|
||||
import PermCollectForm from './module/permCollectForm'
|
||||
import { deleteUserById, searchUser, getOnDutyUser } from '@/modules/acl/api/user'
|
||||
|
||||
export default {
|
||||
name: 'Users',
|
||||
components: {
|
||||
userForm,
|
||||
PermCollectForm,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
tableColumns: [
|
||||
{
|
||||
title: '用户名',
|
||||
field: 'username',
|
||||
sortable: true,
|
||||
minWidth: '100px',
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
title: '中文名',
|
||||
field: 'nickname',
|
||||
minWidth: '100px',
|
||||
},
|
||||
{
|
||||
title: '加入时间',
|
||||
field: 'date_joined',
|
||||
minWidth: '160px',
|
||||
align: 'center',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
title: '锁定',
|
||||
field: 'block',
|
||||
width: '150px',
|
||||
align: 'center',
|
||||
slots: {
|
||||
default: 'block_default',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
field: 'action',
|
||||
width: '150px',
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
slots: {
|
||||
default: 'action_default',
|
||||
},
|
||||
},
|
||||
],
|
||||
onDutuUids: [],
|
||||
btnName: '新增用户',
|
||||
allUsers: [],
|
||||
tableData: [],
|
||||
searchName: '',
|
||||
}
|
||||
},
|
||||
beforeCreate() {
|
||||
this.form = this.$form.createForm(this)
|
||||
},
|
||||
async beforeMount() {
|
||||
this.loading = true
|
||||
await this.getOnDutyUser()
|
||||
this.search()
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
windowHeight: (state) => state.windowHeight,
|
||||
}),
|
||||
isAclAdmin: function() {
|
||||
if (this.$store.state.user.roles.permissions.filter((item) => item === 'acl_admin').length > 0) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
searchName: {
|
||||
immediate: true,
|
||||
handler(newVal, oldVal) {
|
||||
if (newVal) {
|
||||
this.tableData = this.allUsers.filter(
|
||||
(item) =>
|
||||
item.username.toLowerCase().includes(newVal.toLowerCase()) ||
|
||||
item.nickname.toLowerCase().includes(newVal.toLowerCase())
|
||||
)
|
||||
} else {
|
||||
this.tableData = this.allUsers
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
mounted() {},
|
||||
inject: ['reload'],
|
||||
|
||||
methods: {
|
||||
async getOnDutyUser() {
|
||||
await getOnDutyUser().then((res) => {
|
||||
this.onDutuUids = res.map((i) => i.uid)
|
||||
})
|
||||
},
|
||||
search() {
|
||||
searchUser({ page_size: 10000 }).then((res) => {
|
||||
const ret = res.users.filter((u) => this.onDutuUids.includes(u.uid))
|
||||
this.allUsers = ret
|
||||
this.tableData = ret
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
handlePermCollect(record) {
|
||||
this.$refs['permCollectForm'].collect(record)
|
||||
},
|
||||
handleEdit(record) {
|
||||
this.$refs.userForm.handleEdit(record)
|
||||
},
|
||||
async handleOk() {
|
||||
this.searchName = ''
|
||||
await this.getOnDutyUser()
|
||||
this.search()
|
||||
},
|
||||
handleCreate() {
|
||||
this.$refs.userForm.handleCreate()
|
||||
},
|
||||
deleteUser(uid) {
|
||||
deleteUserById(uid).then((res) => {
|
||||
this.$message.success(`删除成功!`)
|
||||
this.handleOk()
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.acl-users {
|
||||
border-radius: 15px;
|
||||
background-color: #fff;
|
||||
height: calc(100vh - 64px);
|
||||
margin-bottom: -24px;
|
||||
padding: 24px;
|
||||
.acl-users-header {
|
||||
display: inline-flex;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,170 +1,177 @@
|
||||
import { axios } from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 获取 ci_type 的属性
|
||||
* @param CITypeName
|
||||
* @param parameter
|
||||
* @returns {AxiosPromise}
|
||||
*/
|
||||
export function getCITypeAttributesByName(CITypeName, parameter) {
|
||||
return axios({
|
||||
|
||||
url: `/v0.1/ci_types/${CITypeName}/attributes`,
|
||||
method: 'get',
|
||||
params: parameter
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 ci_type 的属性
|
||||
* @param CITypeId
|
||||
* @param parameter
|
||||
* @returns {AxiosPromise}
|
||||
*/
|
||||
export function getCITypeAttributesById(CITypeId, parameter) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${CITypeId}/attributes`,
|
||||
method: 'get',
|
||||
params: parameter
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新属性
|
||||
* @param attrId
|
||||
* @param data
|
||||
* @returns {AxiosPromise}
|
||||
*/
|
||||
export function updateAttributeById(attrId, data) {
|
||||
return axios({
|
||||
url: `/v0.1/attributes/${attrId}`,
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加属性
|
||||
* @param data
|
||||
* @returns {AxiosPromise}
|
||||
*/
|
||||
export function createAttribute(data) {
|
||||
return axios({
|
||||
url: `/v0.1/attributes`,
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索属性/ 获取所有的属性
|
||||
* @param data
|
||||
* @returns {AxiosPromise}
|
||||
*/
|
||||
export function searchAttributes(params) {
|
||||
return axios({
|
||||
url: `/v0.1/attributes/s`,
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function getCITypeAttributesByTypeIds(params) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/attributes`,
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function getCITypeCommonAttributesByTypeIds(params) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/common_attributes`,
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除属性
|
||||
* @param attrId
|
||||
* @returns {AxiosPromise}
|
||||
*/
|
||||
export function deleteAttributesById(attrId) {
|
||||
return axios({
|
||||
url: `/v0.1/attributes/${attrId}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定ci_type 属性
|
||||
* @param CITypeId
|
||||
* @param data
|
||||
* @returns {AxiosPromise}
|
||||
*/
|
||||
export function createCITypeAttributes(CITypeId, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${CITypeId}/attributes`,
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新ci_type 属性
|
||||
* @param CITypeId
|
||||
* @param data
|
||||
* @returns {AxiosPromise}
|
||||
*/
|
||||
export function updateCITypeAttributesById(CITypeId, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${CITypeId}/attributes`,
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除ci_type 属性
|
||||
* @param CITypeId
|
||||
* @param data
|
||||
* @returns {AxiosPromise}
|
||||
*/
|
||||
export function deleteCITypeAttributesById(CITypeId, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${CITypeId}/attributes`,
|
||||
method: 'delete',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function transferCITypeAttrIndex(CITypeId, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${CITypeId}/attributes/transfer`,
|
||||
method: 'POST',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function transferCITypeGroupIndex(CITypeId, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${CITypeId}/attribute_groups/transfer`,
|
||||
method: 'POST',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function canDefineComputed() {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/can_define_computed`,
|
||||
method: 'HEAD',
|
||||
})
|
||||
}
|
||||
|
||||
export function calcComputedAttribute(attr_id) {
|
||||
return axios({
|
||||
url: `/v0.1/attributes/${attr_id}/calc_computed_attribute`,
|
||||
method: 'PUT',
|
||||
})
|
||||
}
|
||||
import { axios } from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 获取 ci_type 的属性
|
||||
* @param CITypeName
|
||||
* @param parameter
|
||||
* @returns {AxiosPromise}
|
||||
*/
|
||||
export function getCITypeAttributesByName(CITypeName, parameter) {
|
||||
return axios({
|
||||
|
||||
url: `/v0.1/ci_types/${CITypeName}/attributes`,
|
||||
method: 'get',
|
||||
params: parameter
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 ci_type 的属性
|
||||
* @param CITypeId
|
||||
* @param parameter
|
||||
* @returns {AxiosPromise}
|
||||
*/
|
||||
export function getCITypeAttributesById(CITypeId, parameter) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${CITypeId}/attributes`,
|
||||
method: 'get',
|
||||
params: parameter
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新属性
|
||||
* @param attrId
|
||||
* @param data
|
||||
* @returns {AxiosPromise}
|
||||
*/
|
||||
export function updateAttributeById(attrId, data) {
|
||||
return axios({
|
||||
url: `/v0.1/attributes/${attrId}`,
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加属性
|
||||
* @param data
|
||||
* @returns {AxiosPromise}
|
||||
*/
|
||||
export function createAttribute(data) {
|
||||
return axios({
|
||||
url: `/v0.1/attributes`,
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索属性/ 获取所有的属性
|
||||
* @param data
|
||||
* @returns {AxiosPromise}
|
||||
*/
|
||||
export function searchAttributes(params) {
|
||||
return axios({
|
||||
url: `/v0.1/attributes/s`,
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function getCITypeAttributesByTypeIds(params) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/attributes`,
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
export function getCITypeCommonAttributesByTypeIds(params) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/common_attributes`,
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除属性
|
||||
* @param attrId
|
||||
* @returns {AxiosPromise}
|
||||
*/
|
||||
export function deleteAttributesById(attrId) {
|
||||
return axios({
|
||||
url: `/v0.1/attributes/${attrId}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定ci_type 属性
|
||||
* @param CITypeId
|
||||
* @param data
|
||||
* @returns {AxiosPromise}
|
||||
*/
|
||||
export function createCITypeAttributes(CITypeId, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${CITypeId}/attributes`,
|
||||
method: 'post',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新ci_type 属性
|
||||
* @param CITypeId
|
||||
* @param data
|
||||
* @returns {AxiosPromise}
|
||||
*/
|
||||
export function updateCITypeAttributesById(CITypeId, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${CITypeId}/attributes`,
|
||||
method: 'put',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除ci_type 属性
|
||||
* @param CITypeId
|
||||
* @param data
|
||||
* @returns {AxiosPromise}
|
||||
*/
|
||||
export function deleteCITypeAttributesById(CITypeId, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${CITypeId}/attributes`,
|
||||
method: 'delete',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function transferCITypeAttrIndex(CITypeId, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${CITypeId}/attributes/transfer`,
|
||||
method: 'POST',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function transferCITypeGroupIndex(CITypeId, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${CITypeId}/attribute_groups/transfer`,
|
||||
method: 'POST',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function canDefineComputed() {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/can_define_computed`,
|
||||
method: 'HEAD',
|
||||
})
|
||||
}
|
||||
|
||||
export function calcComputedAttribute(attr_id) {
|
||||
return axios({
|
||||
url: `/v0.1/attributes/${attr_id}/calc_computed_attribute`,
|
||||
method: 'PUT',
|
||||
})
|
||||
}
|
||||
|
||||
export function getAttrPassword(ci_id, attr_id) {
|
||||
return axios({
|
||||
url: `/v0.1/ci/${ci_id}/attributes/${attr_id}/password`,
|
||||
method: 'Get',
|
||||
})
|
||||
}
|
||||
|
@@ -1,42 +1,51 @@
|
||||
<template>
|
||||
<div>
|
||||
<span v-if="!isShow && !isTableLoading">{{ showPassword }}</span>
|
||||
<span v-else>{{ password }}</span>
|
||||
<a
|
||||
:style="{ marginLeft: '10px' }"
|
||||
@click="
|
||||
() => {
|
||||
isShow = !isShow
|
||||
}
|
||||
"
|
||||
><a-icon
|
||||
:type="isShow ? 'eye-invisible' : 'eye'"
|
||||
/></a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
export default {
|
||||
name: 'PasswordField',
|
||||
props: {
|
||||
password: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isShow: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showPassword() {
|
||||
return '******'
|
||||
},
|
||||
...mapState('cmdbStore', ['isTableLoading']),
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
<template>
|
||||
<div>
|
||||
<span v-if="!isShow && !isTableLoading">{{ showPassword }}</span>
|
||||
<span v-else>{{ password }}</span>
|
||||
<a :style="{ marginLeft: '10px' }" @click="getPassword"><a-icon :type="isShow ? 'eye-invisible' : 'eye'"/></a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import { getAttrPassword } from '../../api/CITypeAttr'
|
||||
export default {
|
||||
name: 'PasswordField',
|
||||
props: {
|
||||
ci_id: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
attr_id: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isShow: false,
|
||||
password: '',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showPassword() {
|
||||
return '******'
|
||||
},
|
||||
...mapState('cmdbStore', ['isTableLoading']),
|
||||
},
|
||||
methods: {
|
||||
getPassword() {
|
||||
if (this.isShow) {
|
||||
this.isShow = false
|
||||
} else {
|
||||
getAttrPassword(this.ci_id, this.attr_id).then((res) => {
|
||||
this.password = res.value
|
||||
this.isShow = true
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
@@ -16,7 +16,7 @@
|
||||
</a-space> -->
|
||||
</div>
|
||||
<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
|
||||
v-else
|
||||
:image-style="{
|
||||
@@ -31,11 +31,14 @@
|
||||
</template>
|
||||
|
||||
<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 {
|
||||
name: 'Body',
|
||||
components: { vueJsonEditor },
|
||||
components: { codemirror },
|
||||
data() {
|
||||
const segmentedContentTypes = [
|
||||
{
|
||||
@@ -60,7 +63,14 @@ export default {
|
||||
return {
|
||||
segmentedContentTypes,
|
||||
// contentType: 'none',
|
||||
jsonData: {},
|
||||
jsonData: '',
|
||||
cmOptions: {
|
||||
lineNumbers: true,
|
||||
mode: 'python',
|
||||
height: '200px',
|
||||
tabSize: 4,
|
||||
lineWrapping: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@@ -81,7 +81,11 @@ export default {
|
||||
this.$refs.Parameters.parameters.forEach((item) => {
|
||||
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 = {}
|
||||
this.$refs.Header.headers.forEach((item) => {
|
||||
headers[item.key] = item.value
|
||||
@@ -99,7 +103,6 @@ export default {
|
||||
return { method, url, parameters, body, headers, authorization }
|
||||
},
|
||||
setParams(params) {
|
||||
console.log(2222, params)
|
||||
const { method, url, parameters, body, headers, authorization = {} } = params ?? {}
|
||||
this.method = method
|
||||
this.url = url
|
||||
@@ -111,7 +114,11 @@ export default {
|
||||
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 =
|
||||
Object.keys(headers).map((key) => {
|
||||
return {
|
||||
|
@@ -61,18 +61,18 @@ const genCmdbRoutes = async () => {
|
||||
name: 'cmdb_disabled2',
|
||||
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',
|
||||
component: () => import('../views/preference/index'),
|
||||
name: 'cmdb_preference',
|
||||
meta: { title: '我的订阅', icon: 'ops-cmdb-preference', selectedIcon: 'ops-cmdb-preference-selected', keepAlive: false }
|
||||
},
|
||||
{
|
||||
path: '/cmdb/batch',
|
||||
component: () => import('../views/batch'),
|
||||
name: 'cmdb_batch',
|
||||
meta: { 'title': '批量导入', icon: 'ops-cmdb-batch', selectedIcon: 'ops-cmdb-batch-selected', keepAlive: false }
|
||||
},
|
||||
{
|
||||
path: '/cmdb/ci_types',
|
||||
name: 'ci_type',
|
||||
|
@@ -1,26 +1,28 @@
|
||||
export const valueTypeMap = {
|
||||
'0': '整数',
|
||||
'1': '浮点数',
|
||||
'2': '文本',
|
||||
'3': 'datetime',
|
||||
'4': 'date',
|
||||
'5': 'time',
|
||||
'6': 'json'
|
||||
}
|
||||
|
||||
export const defautValueColor = [
|
||||
{ value: '#d9d9d9' },
|
||||
{ value: '#ffccc7' },
|
||||
{ value: '#ffd8bf' },
|
||||
{ value: '#ffe7ba' },
|
||||
{ value: '#fff1b8' },
|
||||
{ value: '#f4ffb8' },
|
||||
{ value: '#d9f7be' },
|
||||
{ value: '#b5f5ec' },
|
||||
{ value: '#bae7ff' },
|
||||
{ value: '#d6e4ff' },
|
||||
{ value: '#efdbff' },
|
||||
{ value: '#ffd6e7' },
|
||||
]
|
||||
|
||||
export const defaultBGColors = ['#ffccc7', '#ffd8bf', '#ffe7ba', '#fff1b8', '#d9f7be', '#b5f5ec', '#bae7ff', '#d6e4ff', '#efdbff', '#ffd6e7']
|
||||
export const valueTypeMap = {
|
||||
'0': '整数',
|
||||
'1': '浮点数',
|
||||
'2': '文本',
|
||||
'3': '日期时间',
|
||||
'4': '日期',
|
||||
'5': '时间',
|
||||
'6': 'JSON',
|
||||
'7': '密码',
|
||||
'8': '链接'
|
||||
}
|
||||
|
||||
export const defautValueColor = [
|
||||
{ value: '#d9d9d9' },
|
||||
{ value: '#ffccc7' },
|
||||
{ value: '#ffd8bf' },
|
||||
{ value: '#ffe7ba' },
|
||||
{ value: '#fff1b8' },
|
||||
{ value: '#f4ffb8' },
|
||||
{ value: '#d9f7be' },
|
||||
{ value: '#b5f5ec' },
|
||||
{ value: '#bae7ff' },
|
||||
{ value: '#d6e4ff' },
|
||||
{ value: '#efdbff' },
|
||||
{ value: '#ffd6e7' },
|
||||
]
|
||||
|
||||
export const defaultBGColors = ['#ffccc7', '#ffd8bf', '#ffe7ba', '#fff1b8', '#d9f7be', '#b5f5ec', '#bae7ff', '#d6e4ff', '#efdbff', '#ffd6e7']
|
||||
|
@@ -1,176 +1,181 @@
|
||||
/* eslint-disable */
|
||||
import _ from 'lodash'
|
||||
import XLSX from 'xlsx'
|
||||
import XLSXS from 'xlsx-js-style'
|
||||
export function sum(arr) {
|
||||
if (!arr.length) {
|
||||
return 0
|
||||
}
|
||||
return arr.reduce(function (prev, curr, idx, arr) {
|
||||
return prev + curr
|
||||
})
|
||||
}
|
||||
|
||||
const strLength = (fData) => {
|
||||
|
||||
if (!fData) {
|
||||
return 0
|
||||
}
|
||||
if (fData.length && typeof fData === 'object') {
|
||||
fData = fData.join(' ')
|
||||
}
|
||||
let intLength = 0
|
||||
for (let i = 0; i < fData.length; i++) {
|
||||
if ((fData.charCodeAt(i) < 0) || (fData.charCodeAt(i) > 255)) {
|
||||
intLength = intLength + 2
|
||||
}
|
||||
else {
|
||||
intLength = intLength + 1
|
||||
}
|
||||
|
||||
}
|
||||
return Math.floor(intLength * 7)
|
||||
}
|
||||
|
||||
String.prototype.pxWidth = function (font) {
|
||||
// re-use canvas object for better performance
|
||||
const canvas = String.prototype.pxWidth.canvas || (String.prototype.pxWidth.canvas = document.createElement("canvas")),
|
||||
context = canvas.getContext("2d");
|
||||
|
||||
font && (context.font = font);
|
||||
const metrics = context.measureText(this);
|
||||
|
||||
return metrics.width;
|
||||
}
|
||||
|
||||
export function getCITableColumns(data, attrList, width = 1600, height) {
|
||||
// 计算出来 主table的列表 布局属性
|
||||
|
||||
const _attrList = _.orderBy(attrList, ['is_fixed'], ['desc'])
|
||||
const columns = []
|
||||
for (let attr of _attrList) {
|
||||
|
||||
const editRender = { name: 'input' }
|
||||
switch (attr.value_type) {
|
||||
case '0':
|
||||
editRender['props'] = { 'type': 'float' }
|
||||
break
|
||||
case '1':
|
||||
editRender['props'] = { 'type': 'float' }
|
||||
break
|
||||
case '2':
|
||||
editRender['attrs'] = { 'type': 'text' }
|
||||
break
|
||||
case '3':
|
||||
editRender['props'] = { 'type': 'datetime' }
|
||||
break
|
||||
case "4":
|
||||
editRender['props'] = { 'type': 'date' }
|
||||
break
|
||||
case '5':
|
||||
editRender['props'] = { 'type': 'time' }
|
||||
break
|
||||
case '6':
|
||||
editRender['props'] = { 'type': 'text' }
|
||||
break
|
||||
default:
|
||||
editRender['props'] = { 'type': 'text' }
|
||||
break
|
||||
}
|
||||
|
||||
if (attr.is_choice) {
|
||||
editRender.name = '$select'
|
||||
editRender.options = attr.choice_value ? attr.choice_value.map(item => { return { label: item, value: item } }) : []
|
||||
delete editRender.props
|
||||
|
||||
}
|
||||
columns.push({
|
||||
editRender,
|
||||
title: attr.alias || attr.name,
|
||||
field: attr.name,
|
||||
value_type: attr.value_type,
|
||||
sortable: !!attr.is_sortable,
|
||||
filters: attr.is_choice ? attr.choice_value : null,
|
||||
width: Math.min(Math.max(100, ...data.map(item => strLength(item[attr.name]))), 350),
|
||||
is_link: attr.is_link,
|
||||
is_password: attr.is_password,
|
||||
is_list: attr.is_list,
|
||||
is_choice: attr.is_choice,
|
||||
is_fixed: attr.is_fixed,
|
||||
})
|
||||
}
|
||||
|
||||
const totalWidth = sum(columns.map(col => col.width))
|
||||
if (totalWidth < width) {
|
||||
columns.map(item => {
|
||||
// if (item.width === 100) {
|
||||
delete item.width
|
||||
// }
|
||||
})
|
||||
}
|
||||
return columns
|
||||
}
|
||||
|
||||
export const getPropertyStyle = (attr) => {
|
||||
switch (attr.value_type) {
|
||||
case '0':
|
||||
return { color: '#cf1322', backgroundColor: '#fff1f0' }
|
||||
case '1':
|
||||
return { color: '#d4b106', backgroundColor: '#feffe6' }
|
||||
case '2':
|
||||
return { color: '#d46b08', backgroundColor: '#fff7e6' }
|
||||
case '3':
|
||||
return { color: '#531dab', backgroundColor: '#f9f0ff' }
|
||||
case '4':
|
||||
return { color: '#389e0d', backgroundColor: '#f6ffed' }
|
||||
case '5':
|
||||
return { color: '#08979c', backgroundColor: '#e6fffb' }
|
||||
case '6':
|
||||
return { color: '#c41d7f', backgroundColor: '#fff0f6' }
|
||||
}
|
||||
}
|
||||
|
||||
export const getLastLayout = (data, x1 = 0, y1 = 0, w1 = 0) => {
|
||||
const _tempData = _.orderBy(data, ['y', 'x'], ['asc', 'asc'])
|
||||
if (!_tempData.length) {
|
||||
return { xLast: 0, yLast: 0, wLast: 0 }
|
||||
}
|
||||
const { x, y, w } = _tempData[_tempData.length - 1]
|
||||
if (y < y1) {
|
||||
return { xLast: x1, yLast: y1, wLast: w1 }
|
||||
} else if (y > y1) {
|
||||
return { xLast: x, yLast: y, wLast: w }
|
||||
} else {
|
||||
const xLast = _.max([x, x1])
|
||||
return { xLast, yLast: y, wLast: xLast === x ? w : w1 }
|
||||
}
|
||||
}
|
||||
|
||||
// 数字加逗号
|
||||
export const toThousands = (num = 0) => {
|
||||
return num.toString().replace(/\d+/, function (n) {
|
||||
return n.replace(/(\d)(?=(?:\d{3})+$)/g, '$1,')
|
||||
})
|
||||
}
|
||||
|
||||
export const downloadExcel = (data, fileName = `${moment().format('YYYY-MM-DD HH:mm:ss')}.xls`) => {
|
||||
// STEP 1: Create a new workbook
|
||||
const wb = XLSXS.utils.book_new()
|
||||
// STEP 2: Create data rows and styles
|
||||
const rowArray = data
|
||||
// STEP 3: Create worksheet with rows; Add worksheet to workbook
|
||||
const ws = XLSXS.utils.aoa_to_sheet(rowArray)
|
||||
XLSXS.utils.book_append_sheet(wb, ws, fileName)
|
||||
|
||||
let maxColumnNumber = 1 // 默认最大列数
|
||||
rowArray.forEach(item => { if (item.length > maxColumnNumber) { maxColumnNumber = item.length } })
|
||||
|
||||
// 添加列宽
|
||||
ws['!cols'] = (rowArray[0].map(item => {
|
||||
return { width: 22 }
|
||||
}))
|
||||
// // 添加行高
|
||||
// ws['!rows'] = [{ 'hpt': 80 }]
|
||||
// STEP 4: Write Excel file to browser #导出
|
||||
XLSXS.writeFile(wb, fileName + '.xlsx')
|
||||
}
|
||||
/* eslint-disable */
|
||||
import _ from 'lodash'
|
||||
import XLSX from 'xlsx'
|
||||
import XLSXS from 'xlsx-js-style'
|
||||
export function sum(arr) {
|
||||
if (!arr.length) {
|
||||
return 0
|
||||
}
|
||||
return arr.reduce(function (prev, curr, idx, arr) {
|
||||
return prev + curr
|
||||
})
|
||||
}
|
||||
|
||||
const strLength = (fData) => {
|
||||
|
||||
if (!fData) {
|
||||
return 0
|
||||
}
|
||||
if (fData.length && typeof fData === 'object') {
|
||||
fData = fData.join(' ')
|
||||
}
|
||||
let intLength = 0
|
||||
for (let i = 0; i < fData.length; i++) {
|
||||
if ((fData.charCodeAt(i) < 0) || (fData.charCodeAt(i) > 255)) {
|
||||
intLength = intLength + 2
|
||||
}
|
||||
else {
|
||||
intLength = intLength + 1
|
||||
}
|
||||
|
||||
}
|
||||
return Math.floor(intLength * 7)
|
||||
}
|
||||
|
||||
String.prototype.pxWidth = function (font) {
|
||||
// re-use canvas object for better performance
|
||||
const canvas = String.prototype.pxWidth.canvas || (String.prototype.pxWidth.canvas = document.createElement("canvas")),
|
||||
context = canvas.getContext("2d");
|
||||
|
||||
font && (context.font = font);
|
||||
const metrics = context.measureText(this);
|
||||
|
||||
return metrics.width;
|
||||
}
|
||||
|
||||
export function getCITableColumns(data, attrList, width = 1600, height) {
|
||||
// 计算出来 主table的列表 布局属性
|
||||
|
||||
const _attrList = _.orderBy(attrList, ['is_fixed'], ['desc'])
|
||||
const columns = []
|
||||
for (let attr of _attrList) {
|
||||
|
||||
const editRender = { name: 'input' }
|
||||
switch (attr.value_type) {
|
||||
case '0':
|
||||
editRender['props'] = { 'type': 'float' }
|
||||
break
|
||||
case '1':
|
||||
editRender['props'] = { 'type': 'float' }
|
||||
break
|
||||
case '2':
|
||||
editRender['attrs'] = { 'type': 'text' }
|
||||
break
|
||||
case '3':
|
||||
editRender['props'] = { 'type': 'datetime' }
|
||||
break
|
||||
case "4":
|
||||
editRender['props'] = { 'type': 'date' }
|
||||
break
|
||||
case '5':
|
||||
editRender['props'] = { 'type': 'time' }
|
||||
break
|
||||
case '6':
|
||||
editRender['props'] = { 'type': 'text' }
|
||||
break
|
||||
default:
|
||||
editRender['props'] = { 'type': 'text' }
|
||||
break
|
||||
}
|
||||
|
||||
if (attr.is_choice) {
|
||||
editRender.name = '$select'
|
||||
editRender.options = attr.choice_value ? attr.choice_value.map(item => { return { label: item, value: item } }) : []
|
||||
delete editRender.props
|
||||
|
||||
}
|
||||
columns.push({
|
||||
attr_id:attr.id,
|
||||
editRender,
|
||||
title: attr.alias || attr.name,
|
||||
field: attr.name,
|
||||
value_type: attr.value_type,
|
||||
sortable: !!attr.is_sortable,
|
||||
filters: attr.is_choice ? attr.choice_value : null,
|
||||
width: Math.min(Math.max(100, ...data.map(item => strLength(item[attr.name]))), 350),
|
||||
is_link: attr.is_link,
|
||||
is_password: attr.is_password,
|
||||
is_list: attr.is_list,
|
||||
is_choice: attr.is_choice,
|
||||
is_fixed: attr.is_fixed,
|
||||
})
|
||||
}
|
||||
|
||||
const totalWidth = sum(columns.map(col => col.width))
|
||||
if (totalWidth < width) {
|
||||
columns.map(item => {
|
||||
// if (item.width === 100) {
|
||||
delete item.width
|
||||
// }
|
||||
})
|
||||
}
|
||||
return columns
|
||||
}
|
||||
|
||||
export const getPropertyStyle = (attr) => {
|
||||
switch (attr.value_type) {
|
||||
case '0':
|
||||
return { color: '#cf1322', backgroundColor: '#fff1f0' }
|
||||
case '1':
|
||||
return { color: '#d4b106', backgroundColor: '#feffe6' }
|
||||
case '2':
|
||||
return { color: '#d46b08', backgroundColor: '#fff7e6' }
|
||||
case '3':
|
||||
return { color: '#531dab', backgroundColor: '#f9f0ff' }
|
||||
case '4':
|
||||
return { color: '#389e0d', backgroundColor: '#f6ffed' }
|
||||
case '5':
|
||||
return { color: '#08979c', backgroundColor: '#e6fffb' }
|
||||
case '6':
|
||||
return { color: '#c41d7f', backgroundColor: '#fff0f6' }
|
||||
case '7':
|
||||
return { color: '#0390CC', backgroundColor: '#e6fffb' }
|
||||
case '8':
|
||||
return { color: '#144BD9', backgroundColor: '#fff0f6' }
|
||||
}
|
||||
}
|
||||
|
||||
export const getLastLayout = (data, x1 = 0, y1 = 0, w1 = 0) => {
|
||||
const _tempData = _.orderBy(data, ['y', 'x'], ['asc', 'asc'])
|
||||
if (!_tempData.length) {
|
||||
return { xLast: 0, yLast: 0, wLast: 0 }
|
||||
}
|
||||
const { x, y, w } = _tempData[_tempData.length - 1]
|
||||
if (y < y1) {
|
||||
return { xLast: x1, yLast: y1, wLast: w1 }
|
||||
} else if (y > y1) {
|
||||
return { xLast: x, yLast: y, wLast: w }
|
||||
} else {
|
||||
const xLast = _.max([x, x1])
|
||||
return { xLast, yLast: y, wLast: xLast === x ? w : w1 }
|
||||
}
|
||||
}
|
||||
|
||||
// 数字加逗号
|
||||
export const toThousands = (num = 0) => {
|
||||
return num.toString().replace(/\d+/, function (n) {
|
||||
return n.replace(/(\d)(?=(?:\d{3})+$)/g, '$1,')
|
||||
})
|
||||
}
|
||||
|
||||
export const downloadExcel = (data, fileName = `${moment().format('YYYY-MM-DD HH:mm:ss')}.xls`) => {
|
||||
// STEP 1: Create a new workbook
|
||||
const wb = XLSXS.utils.book_new()
|
||||
// STEP 2: Create data rows and styles
|
||||
const rowArray = data
|
||||
// STEP 3: Create worksheet with rows; Add worksheet to workbook
|
||||
const ws = XLSXS.utils.aoa_to_sheet(rowArray)
|
||||
XLSXS.utils.book_append_sheet(wb, ws, fileName)
|
||||
|
||||
let maxColumnNumber = 1 // 默认最大列数
|
||||
rowArray.forEach(item => { if (item.length > maxColumnNumber) { maxColumnNumber = item.length } })
|
||||
|
||||
// 添加列宽
|
||||
ws['!cols'] = (rowArray[0].map(item => {
|
||||
return { width: 22 }
|
||||
}))
|
||||
// // 添加行高
|
||||
// ws['!rows'] = [{ 'hpt': 80 }]
|
||||
// STEP 4: Write Excel file to browser #导出
|
||||
XLSXS.writeFile(wb, fileName + '.xlsx')
|
||||
}
|
@@ -5,7 +5,7 @@
|
||||
</div>
|
||||
<a-row>
|
||||
<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 :span="24" v-if="ciType && uploadData.length">
|
||||
<CiUploadTable :ciTypeAttrs="ciTypeAttrs" ref="ciUploadTable" :uploadData="uploadData"></CiUploadTable>
|
||||
|
@@ -7,6 +7,7 @@
|
||||
accept=".xls,.xlsx"
|
||||
:showUploadList="false"
|
||||
:fileList="fileList"
|
||||
:disabled="!ciType"
|
||||
>
|
||||
<img :style="{ width: '80px', height: '80px' }" src="@/assets/file_upload.png" />
|
||||
<p class="ant-upload-text">点击或拖拽文件至此上传!</p>
|
||||
@@ -24,6 +25,12 @@ import { processFile } from '@/modules/cmdb/api/batch'
|
||||
|
||||
export default {
|
||||
name: 'UploadFileForm',
|
||||
props: {
|
||||
ciType: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
ciItemNum: 0,
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -30,7 +30,7 @@
|
||||
:key="attr.name"
|
||||
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>
|
||||
</div>
|
||||
@@ -97,6 +97,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { Descriptions, DescriptionsItem } from 'element-ui'
|
||||
import { getCITypeGroupById, getCITypes } from '@/modules/cmdb/api/CIType'
|
||||
import { getCIHistory } from '@/modules/cmdb/api/history'
|
||||
@@ -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>
|
||||
|
@@ -1,359 +1,377 @@
|
||||
<template>
|
||||
<CustomDrawer
|
||||
:title="title + CIType.alias"
|
||||
width="800"
|
||||
@close="handleClose"
|
||||
:maskClosable="false"
|
||||
:visible="visible"
|
||||
wrapClassName="create-instance-form"
|
||||
:bodyStyle="{ paddingTop: 0 }"
|
||||
:headerStyle="{ borderBottom: 'none' }"
|
||||
>
|
||||
<div class="custom-drawer-bottom-action">
|
||||
<a-button @click="handleClose">取消</a-button>
|
||||
<a-button type="primary" @click="createInstance">提交</a-button>
|
||||
</div>
|
||||
<template v-if="action === 'create'">
|
||||
<template v-for="group in attributesByGroup">
|
||||
<CreateInstanceFormByGroup
|
||||
:ref="`createInstanceFormByGroup_${group.id}`"
|
||||
:key="group.id || group.name"
|
||||
:group="group"
|
||||
@handleFocusInput="handleFocusInput"
|
||||
:attributeList="attributeList"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="parentsType && parentsType.length">
|
||||
<a-divider style="font-size:14px;margin:14px 0;font-weight:700;">模型关系</a-divider>
|
||||
<a-form>
|
||||
<a-row :gutter="24" align="top" type="flex">
|
||||
<a-col :span="12" v-for="item in parentsType" :key="item.id">
|
||||
<a-form-item :label="item.alias || item.name" :colon="false">
|
||||
<a-input-group compact style="width: 100%">
|
||||
<a-select v-model="parentsForm[item.name].attr">
|
||||
<a-select-option
|
||||
:title="attr.alias || attr.name"
|
||||
v-for="attr in item.attributes"
|
||||
:key="attr.name"
|
||||
:value="attr.name"
|
||||
>
|
||||
{{ attr.alias || attr.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<a-input placeholder="多个值使用,分割" v-model="parentsForm[item.name].value" style="width: 50%" />
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</template>
|
||||
</template>
|
||||
<template v-if="action === 'update'">
|
||||
<a-form :form="form">
|
||||
<p>可根据需要修改字段,当值为<strong>空</strong>时,则该字段<strong>置空</strong></p>
|
||||
<a-row :gutter="24" v-for="list in batchUpdateLists" :key="list.name">
|
||||
<a-col :span="11">
|
||||
<a-form-item>
|
||||
<el-select showSearch size="small" filterable v-model="list.name" placeholder="请选择需要修改的字段">
|
||||
<el-option
|
||||
v-for="attr in attributeList"
|
||||
:key="attr.name"
|
||||
:value="attr.name"
|
||||
:disabled="batchUpdateLists.findIndex((item) => item.name === attr.name) > -1"
|
||||
:label="attr.alias || attr.name"
|
||||
>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="11">
|
||||
<a-form-item>
|
||||
<a-select
|
||||
:style="{ width: '100%' }"
|
||||
v-decorator="[list.name, { rules: [{ required: false }] }]"
|
||||
placeholder="请选择"
|
||||
v-if="getFieldType(list.name).split('%%')[0] === 'select'"
|
||||
:mode="getFieldType(list.name).split('%%')[1] === 'multiple' ? 'multiple' : 'default'"
|
||||
showSearch
|
||||
allowClear
|
||||
>
|
||||
<a-select-option
|
||||
:value="choice[0]"
|
||||
:key="'New_' + choice + choice_idx"
|
||||
v-for="(choice, choice_idx) in getSelectFieldOptions(list.name)"
|
||||
>
|
||||
<span :style="choice[1] ? choice[1].style || {} : {}">
|
||||
<ops-icon
|
||||
:style="{ color: choice[1].icon.color }"
|
||||
v-if="choice[1] && choice[1].icon && choice[1].icon.name"
|
||||
:type="choice[1].icon.name"
|
||||
/>
|
||||
{{ choice[0] }}
|
||||
</span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<a-input-number
|
||||
v-decorator="[list.name, { rules: [{ required: false }] }]"
|
||||
style="width: 100%"
|
||||
v-if="getFieldType(list.name) === 'input_number'"
|
||||
/>
|
||||
<a-date-picker
|
||||
v-decorator="[list.name, { rules: [{ required: false }] }]"
|
||||
style="width: 100%"
|
||||
:format="getFieldType(list.name) == 'date' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
|
||||
v-if="getFieldType(list.name) === 'date' || getFieldType(list.name) === 'datetime'"
|
||||
:showTime="getFieldType(list.name) === 'date' ? false : { format: 'HH:mm:ss' }"
|
||||
/>
|
||||
<a-input
|
||||
v-if="getFieldType(list.name) === 'input'"
|
||||
@focus="(e) => handleFocusInput(e, list)"
|
||||
v-decorator="[list.name, { rules: [{ required: false }] }]"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="2">
|
||||
<a-form-item>
|
||||
<a :style="{ color: 'red', marginTop: '2px' }" @click="handleDelete(list.name)">
|
||||
<a-icon type="delete"/>
|
||||
</a>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-button type="primary" ghost icon="plus" @click="handleAdd">新增修改字段</a-button>
|
||||
</a-form>
|
||||
</template>
|
||||
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
|
||||
</CustomDrawer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment'
|
||||
import { Select, Option } from 'element-ui'
|
||||
import { getCIType, getCITypeGroupById } from '@/modules/cmdb/api/CIType'
|
||||
import { addCI } from '@/modules/cmdb/api/ci'
|
||||
import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue'
|
||||
import { valueTypeMap } from '../../../utils/const'
|
||||
import CreateInstanceFormByGroup from './createInstanceFormByGroup.vue'
|
||||
import { getCITypeParent } from '@/modules/cmdb/api/CITypeRelation'
|
||||
|
||||
export default {
|
||||
name: 'CreateInstanceForm',
|
||||
components: {
|
||||
ElSelect: Select,
|
||||
ElOption: Option,
|
||||
JsonEditor,
|
||||
CreateInstanceFormByGroup,
|
||||
},
|
||||
props: {
|
||||
typeIdFromRelation: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
valueTypeMap,
|
||||
action: '',
|
||||
form: this.$form.createForm(this),
|
||||
visible: false,
|
||||
attributeList: [],
|
||||
|
||||
CIType: {},
|
||||
|
||||
batchUpdateLists: [],
|
||||
editAttr: null,
|
||||
attributesByGroup: [],
|
||||
parentsType: [],
|
||||
parentsForm: {},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
return this.action === 'create' ? '创建 ' : '批量修改 '
|
||||
},
|
||||
typeId() {
|
||||
if (this.typeIdFromRelation) {
|
||||
return this.typeIdFromRelation
|
||||
}
|
||||
return this.$router.currentRoute.meta.typeId
|
||||
},
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
getFieldType: this.getFieldType,
|
||||
}
|
||||
},
|
||||
inject: ['attrList'],
|
||||
methods: {
|
||||
moment,
|
||||
async getCIType() {
|
||||
await getCIType(this.typeId).then((res) => {
|
||||
this.CIType = res.ci_types[0]
|
||||
})
|
||||
},
|
||||
async getAttributeList() {
|
||||
const _attrList = this.attrList()
|
||||
this.attributeList = _attrList.sort((x, y) => y.is_required - x.is_required)
|
||||
await getCITypeGroupById(this.typeId).then((res1) => {
|
||||
const _attributesByGroup = res1.map((g) => {
|
||||
g.attributes = g.attributes.filter((attr) => !attr.is_computed)
|
||||
return g
|
||||
})
|
||||
const attrHasGroupIds = []
|
||||
res1.forEach((g) => {
|
||||
const id = g.attributes.map((attr) => attr.id)
|
||||
attrHasGroupIds.push(...id)
|
||||
})
|
||||
const otherGroupAttr = this.attributeList.filter(
|
||||
(attr) => !attrHasGroupIds.includes(attr.id) && !attr.is_computed
|
||||
)
|
||||
if (otherGroupAttr.length) {
|
||||
_attributesByGroup.push({ id: -1, name: '其他', attributes: otherGroupAttr })
|
||||
}
|
||||
this.attributesByGroup = _attributesByGroup
|
||||
})
|
||||
},
|
||||
createInstance() {
|
||||
const _this = this
|
||||
if (_this.action === 'update') {
|
||||
this.form.validateFields((err, values) => {
|
||||
if (err) {
|
||||
return
|
||||
}
|
||||
Object.keys(values).forEach((k) => {
|
||||
const _tempFind = this.attributeList.find((item) => item.name === k)
|
||||
if (_tempFind.value_type === '3' && values[k]) {
|
||||
values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
if (_tempFind.value_type === '4' && values[k]) {
|
||||
values[k] = values[k].format('YYYY-MM-DD')
|
||||
}
|
||||
if (_tempFind.value_type === '6') {
|
||||
values[k] = values[k] ? JSON.parse(values[k]) : undefined
|
||||
}
|
||||
})
|
||||
|
||||
_this.$emit('submit', values)
|
||||
})
|
||||
} else {
|
||||
let values = {}
|
||||
for (let i = 0; i < this.attributesByGroup.length; i++) {
|
||||
const data = this.$refs[`createInstanceFormByGroup_${this.attributesByGroup[i].id}`][0].getData()
|
||||
if (data === 'error') {
|
||||
return
|
||||
}
|
||||
values = { ...values, ...data }
|
||||
}
|
||||
|
||||
Object.keys(values).forEach((k) => {
|
||||
const _tempFind = this.attributeList.find((item) => item.name === k)
|
||||
if (_tempFind.value_type === '3' && values[k]) {
|
||||
values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
if (_tempFind.value_type === '4' && values[k]) {
|
||||
values[k] = values[k].format('YYYY-MM-DD')
|
||||
}
|
||||
if (_tempFind.value_type === '6') {
|
||||
values[k] = values[k] ? JSON.parse(values[k]) : undefined
|
||||
}
|
||||
})
|
||||
values.ci_type = _this.typeId
|
||||
Object.keys(this.parentsForm).forEach((type) => {
|
||||
if (this.parentsForm[type].value) {
|
||||
values[`$${type}.${this.parentsForm[type].attr}`] = this.parentsForm[type].value
|
||||
}
|
||||
})
|
||||
addCI(values).then((res) => {
|
||||
_this.$message.success('新增成功!')
|
||||
_this.visible = false
|
||||
_this.$emit('reload', { ci_id: res.ci_id })
|
||||
})
|
||||
}
|
||||
},
|
||||
handleClose() {
|
||||
this.visible = false
|
||||
},
|
||||
handleOpen(visible, action) {
|
||||
this.visible = visible
|
||||
this.action = action
|
||||
this.$nextTick(() => {
|
||||
this.form.resetFields()
|
||||
Promise.all([this.getCIType(), this.getAttributeList()]).then(() => {
|
||||
this.batchUpdateLists = [{ name: this.attributeList[0].name }]
|
||||
})
|
||||
if (action === 'create') {
|
||||
getCITypeParent(this.typeId).then((res) => {
|
||||
this.parentsType = res.parents
|
||||
const _parentsForm = {}
|
||||
res.parents.forEach((item) => {
|
||||
const _find = item.attributes.find((attr) => attr.id === item.unique_id)
|
||||
_parentsForm[item.name] = { attr: _find.name, value: '' }
|
||||
})
|
||||
this.parentsForm = _parentsForm
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
getFieldType(name) {
|
||||
const _find = this.attributeList.find((item) => item.name === name)
|
||||
if (_find) {
|
||||
if (_find.is_choice) {
|
||||
if (_find.is_list) {
|
||||
return 'select%%multiple'
|
||||
}
|
||||
return 'select'
|
||||
} else if (_find.value_type === '0' || _find.value_type === '1') {
|
||||
return 'input_number'
|
||||
} else if (_find.value_type === '4' || _find.value_type === '3') {
|
||||
return valueTypeMap[_find.value_type]
|
||||
} else {
|
||||
return 'input'
|
||||
}
|
||||
}
|
||||
return 'input'
|
||||
},
|
||||
getSelectFieldOptions(name) {
|
||||
const _find = this.attributeList.find((item) => item.name === name)
|
||||
if (_find) {
|
||||
return _find.choice_value
|
||||
}
|
||||
return []
|
||||
},
|
||||
handleAdd() {
|
||||
this.batchUpdateLists.push({ name: undefined })
|
||||
},
|
||||
handleDelete(name) {
|
||||
const _idx = this.batchUpdateLists.findIndex((item) => item.name === name)
|
||||
if (_idx > -1) {
|
||||
this.batchUpdateLists.splice(_idx, 1)
|
||||
}
|
||||
},
|
||||
handleFocusInput(e, attr) {
|
||||
console.log(attr)
|
||||
const _tempFind = this.attributeList.find((item) => item.name === attr.name)
|
||||
if (_tempFind.value_type === '6') {
|
||||
this.editAttr = attr
|
||||
e.srcElement.blur()
|
||||
const jsonData = this.form.getFieldValue(attr.name)
|
||||
this.$refs.jsonEditor.open(null, null, jsonData ? JSON.parse(jsonData) : {})
|
||||
} else {
|
||||
this.editAttr = null
|
||||
}
|
||||
},
|
||||
jsonEditorOk(jsonData) {
|
||||
this.form.setFieldsValue({ [this.editAttr.name]: JSON.stringify(jsonData) })
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="less">
|
||||
.create-instance-form {
|
||||
.ant-form-item {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.ant-drawer-body {
|
||||
overflow-y: auto;
|
||||
max-height: calc(100vh - 110px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<CustomDrawer
|
||||
:title="title + CIType.alias"
|
||||
width="800"
|
||||
@close="handleClose"
|
||||
:maskClosable="false"
|
||||
:visible="visible"
|
||||
wrapClassName="create-instance-form"
|
||||
:bodyStyle="{ paddingTop: 0 }"
|
||||
:headerStyle="{ borderBottom: 'none' }"
|
||||
>
|
||||
<div class="custom-drawer-bottom-action">
|
||||
<a-button @click="handleClose">取消</a-button>
|
||||
<a-button type="primary" @click="createInstance">提交</a-button>
|
||||
</div>
|
||||
<template v-if="action === 'create'">
|
||||
<template v-for="group in attributesByGroup">
|
||||
<CreateInstanceFormByGroup
|
||||
:ref="`createInstanceFormByGroup_${group.id}`"
|
||||
:key="group.id || group.name"
|
||||
:group="group"
|
||||
@handleFocusInput="handleFocusInput"
|
||||
:attributeList="attributeList"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="parentsType && parentsType.length">
|
||||
<a-divider style="font-size:14px;margin:14px 0;font-weight:700;">模型关系</a-divider>
|
||||
<a-form>
|
||||
<a-row :gutter="24" align="top" type="flex">
|
||||
<a-col :span="12" v-for="item in parentsType" :key="item.id">
|
||||
<a-form-item :label="item.alias || item.name" :colon="false">
|
||||
<a-input-group compact style="width: 100%">
|
||||
<a-select v-model="parentsForm[item.name].attr">
|
||||
<a-select-option
|
||||
:title="attr.alias || attr.name"
|
||||
v-for="attr in item.attributes"
|
||||
:key="attr.name"
|
||||
:value="attr.name"
|
||||
>
|
||||
{{ attr.alias || attr.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<a-input placeholder="多个值使用,分割" v-model="parentsForm[item.name].value" style="width: 50%" />
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</template>
|
||||
</template>
|
||||
<template v-if="action === 'update'">
|
||||
<a-form :form="form">
|
||||
<p>可根据需要修改字段,当值为<strong>空</strong>时,则该字段<strong>置空</strong></p>
|
||||
<a-row :gutter="24" v-for="list in batchUpdateLists" :key="list.name">
|
||||
<a-col :span="11">
|
||||
<a-form-item>
|
||||
<el-select showSearch size="small" filterable v-model="list.name" placeholder="请选择需要修改的字段">
|
||||
<el-option
|
||||
v-for="attr in attributeList"
|
||||
:key="attr.name"
|
||||
:value="attr.name"
|
||||
:disabled="batchUpdateLists.findIndex((item) => item.name === attr.name) > -1"
|
||||
:label="attr.alias || attr.name"
|
||||
>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="11">
|
||||
<a-form-item>
|
||||
<a-select
|
||||
:style="{ width: '100%' }"
|
||||
v-decorator="[list.name, { rules: [{ required: false }] }]"
|
||||
placeholder="请选择"
|
||||
v-if="getFieldType(list.name).split('%%')[0] === 'select'"
|
||||
:mode="getFieldType(list.name).split('%%')[1] === 'multiple' ? 'multiple' : 'default'"
|
||||
showSearch
|
||||
allowClear
|
||||
>
|
||||
<a-select-option
|
||||
:value="choice[0]"
|
||||
:key="'New_' + choice + choice_idx"
|
||||
v-for="(choice, choice_idx) in getSelectFieldOptions(list.name)"
|
||||
>
|
||||
<span :style="choice[1] ? choice[1].style || {} : {}">
|
||||
<ops-icon
|
||||
:style="{ color: choice[1].icon.color }"
|
||||
v-if="choice[1] && choice[1].icon && choice[1].icon.name"
|
||||
:type="choice[1].icon.name"
|
||||
/>
|
||||
{{ choice[0] }}
|
||||
</span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<a-input-number
|
||||
v-decorator="[list.name, { rules: [{ required: false }] }]"
|
||||
style="width: 100%"
|
||||
v-if="getFieldType(list.name) === 'input_number'"
|
||||
/>
|
||||
<a-date-picker
|
||||
v-decorator="[list.name, { rules: [{ required: false }] }]"
|
||||
style="width: 100%"
|
||||
:format="getFieldType(list.name) == 'date' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
|
||||
v-if="getFieldType(list.name) === 'date' || getFieldType(list.name) === 'datetime'"
|
||||
:showTime="getFieldType(list.name) === 'date' ? false : { format: 'HH:mm:ss' }"
|
||||
/>
|
||||
<a-input
|
||||
v-if="getFieldType(list.name) === 'input'"
|
||||
@focus="(e) => handleFocusInput(e, list)"
|
||||
v-decorator="[list.name, { rules: [{ required: false }] }]"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="2">
|
||||
<a-form-item>
|
||||
<a :style="{ color: 'red', marginTop: '2px' }" @click="handleDelete(list.name)">
|
||||
<a-icon type="delete" />
|
||||
</a>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-button type="primary" ghost icon="plus" @click="handleAdd">新增修改字段</a-button>
|
||||
</a-form>
|
||||
</template>
|
||||
<!-- </a-form> -->
|
||||
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
|
||||
</CustomDrawer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment'
|
||||
import { Select, Option } from 'element-ui'
|
||||
import { getCIType, getCITypeGroupById } from '@/modules/cmdb/api/CIType'
|
||||
import { addCI } from '@/modules/cmdb/api/ci'
|
||||
import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue'
|
||||
import { valueTypeMap } from '../../../utils/const'
|
||||
import CreateInstanceFormByGroup from './createInstanceFormByGroup.vue'
|
||||
import { getCITypeParent } from '@/modules/cmdb/api/CITypeRelation'
|
||||
|
||||
export default {
|
||||
name: 'CreateInstanceForm',
|
||||
components: {
|
||||
ElSelect: Select,
|
||||
ElOption: Option,
|
||||
JsonEditor,
|
||||
CreateInstanceFormByGroup,
|
||||
},
|
||||
props: {
|
||||
typeIdFromRelation: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
valueTypeMap,
|
||||
action: '',
|
||||
form: this.$form.createForm(this),
|
||||
visible: false,
|
||||
attributeList: [],
|
||||
|
||||
CIType: {},
|
||||
|
||||
batchUpdateLists: [],
|
||||
editAttr: null,
|
||||
attributesByGroup: [],
|
||||
parentsType: [],
|
||||
parentsForm: {},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
return this.action === 'create' ? '创建 ' : '批量修改 '
|
||||
},
|
||||
typeId() {
|
||||
if (this.typeIdFromRelation) {
|
||||
return this.typeIdFromRelation
|
||||
}
|
||||
return this.$router.currentRoute.meta.typeId
|
||||
},
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
getFieldType: this.getFieldType,
|
||||
}
|
||||
},
|
||||
inject: ['attrList'],
|
||||
methods: {
|
||||
moment,
|
||||
async getCIType() {
|
||||
await getCIType(this.typeId).then((res) => {
|
||||
this.CIType = res.ci_types[0]
|
||||
})
|
||||
},
|
||||
async getAttributeList() {
|
||||
const _attrList = this.attrList()
|
||||
this.attributeList = _attrList.sort((x, y) => y.is_required - x.is_required)
|
||||
await getCITypeGroupById(this.typeId).then((res1) => {
|
||||
const _attributesByGroup = res1.map((g) => {
|
||||
g.attributes = g.attributes.filter((attr) => !attr.is_computed)
|
||||
return g
|
||||
})
|
||||
const attrHasGroupIds = []
|
||||
res1.forEach((g) => {
|
||||
const id = g.attributes.map((attr) => attr.id)
|
||||
attrHasGroupIds.push(...id)
|
||||
})
|
||||
const otherGroupAttr = this.attributeList.filter(
|
||||
(attr) => !attrHasGroupIds.includes(attr.id) && !attr.is_computed
|
||||
)
|
||||
if (otherGroupAttr.length) {
|
||||
_attributesByGroup.push({ id: -1, name: '其他', attributes: otherGroupAttr })
|
||||
}
|
||||
this.attributesByGroup = _attributesByGroup
|
||||
})
|
||||
},
|
||||
createInstance() {
|
||||
const _this = this
|
||||
if (_this.action === 'update') {
|
||||
this.form.validateFields((err, values) => {
|
||||
if (err) {
|
||||
return
|
||||
}
|
||||
Object.keys(values).forEach((k) => {
|
||||
const _tempFind = this.attributeList.find((item) => item.name === k)
|
||||
if (
|
||||
_tempFind.value_type === '3' &&
|
||||
values[k] &&
|
||||
Object.prototype.toString.call(values[k]) === '[object Object]'
|
||||
) {
|
||||
values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
if (
|
||||
_tempFind.value_type === '4' &&
|
||||
values[k] &&
|
||||
Object.prototype.toString.call(values[k]) === '[object Object]'
|
||||
) {
|
||||
values[k] = values[k].format('YYYY-MM-DD')
|
||||
}
|
||||
if (_tempFind.value_type === '6') {
|
||||
values[k] = values[k] ? JSON.parse(values[k]) : undefined
|
||||
}
|
||||
})
|
||||
|
||||
_this.$emit('submit', values)
|
||||
})
|
||||
} else {
|
||||
let values = {}
|
||||
for (let i = 0; i < this.attributesByGroup.length; i++) {
|
||||
const data = this.$refs[`createInstanceFormByGroup_${this.attributesByGroup[i].id}`][0].getData()
|
||||
if (data === 'error') {
|
||||
return
|
||||
}
|
||||
values = { ...values, ...data }
|
||||
}
|
||||
|
||||
Object.keys(values).forEach((k) => {
|
||||
const _tempFind = this.attributeList.find((item) => item.name === k)
|
||||
if (
|
||||
_tempFind.value_type === '3' &&
|
||||
values[k] &&
|
||||
Object.prototype.toString.call(values[k]) === '[object Object]'
|
||||
) {
|
||||
values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
if (
|
||||
_tempFind.value_type === '4' &&
|
||||
values[k] &&
|
||||
Object.prototype.toString.call(values[k]) === '[object Object]'
|
||||
) {
|
||||
values[k] = values[k].format('YYYY-MM-DD')
|
||||
}
|
||||
if (_tempFind.value_type === '6') {
|
||||
values[k] = values[k] ? JSON.parse(values[k]) : undefined
|
||||
}
|
||||
})
|
||||
values.ci_type = _this.typeId
|
||||
console.log(this.parentsForm)
|
||||
Object.keys(this.parentsForm).forEach((type) => {
|
||||
if (this.parentsForm[type].value) {
|
||||
values[`$${type}.${this.parentsForm[type].attr}`] = this.parentsForm[type].value
|
||||
}
|
||||
})
|
||||
addCI(values).then((res) => {
|
||||
_this.$message.success('新增成功!')
|
||||
_this.visible = false
|
||||
_this.$emit('reload', { ci_id: res.ci_id })
|
||||
})
|
||||
}
|
||||
},
|
||||
handleClose() {
|
||||
this.visible = false
|
||||
},
|
||||
handleOpen(visible, action) {
|
||||
this.visible = visible
|
||||
this.action = action
|
||||
this.$nextTick(() => {
|
||||
this.form.resetFields()
|
||||
Promise.all([this.getCIType(), this.getAttributeList()]).then(() => {
|
||||
this.batchUpdateLists = [{ name: this.attributeList[0].name }]
|
||||
})
|
||||
if (action === 'create') {
|
||||
getCITypeParent(this.typeId).then((res) => {
|
||||
this.parentsType = res.parents
|
||||
const _parentsForm = {}
|
||||
res.parents.forEach((item) => {
|
||||
const _find = item.attributes.find((attr) => attr.id === item.unique_id)
|
||||
_parentsForm[item.name] = { attr: _find.name, value: '' }
|
||||
})
|
||||
this.parentsForm = _parentsForm
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
getFieldType(name) {
|
||||
const _find = this.attributeList.find((item) => item.name === name)
|
||||
if (_find) {
|
||||
if (_find.is_choice) {
|
||||
if (_find.is_list) {
|
||||
return 'select%%multiple'
|
||||
}
|
||||
return 'select'
|
||||
} else if (_find.value_type === '0' || _find.value_type === '1') {
|
||||
return 'input_number'
|
||||
} else if (_find.value_type === '4' || _find.value_type === '3') {
|
||||
return valueTypeMap[_find.value_type]
|
||||
} else {
|
||||
return 'input'
|
||||
}
|
||||
}
|
||||
return 'input'
|
||||
},
|
||||
getSelectFieldOptions(name) {
|
||||
const _find = this.attributeList.find((item) => item.name === name)
|
||||
if (_find) {
|
||||
return _find.choice_value
|
||||
}
|
||||
return []
|
||||
},
|
||||
handleAdd() {
|
||||
this.batchUpdateLists.push({ name: undefined })
|
||||
},
|
||||
handleDelete(name) {
|
||||
const _idx = this.batchUpdateLists.findIndex((item) => item.name === name)
|
||||
if (_idx > -1) {
|
||||
this.batchUpdateLists.splice(_idx, 1)
|
||||
}
|
||||
},
|
||||
handleFocusInput(e, attr) {
|
||||
console.log(attr)
|
||||
const _tempFind = this.attributeList.find((item) => item.name === attr.name)
|
||||
if (_tempFind.value_type === '6') {
|
||||
this.editAttr = attr
|
||||
e.srcElement.blur()
|
||||
const jsonData = this.form.getFieldValue(attr.name)
|
||||
this.$refs.jsonEditor.open(null, null, jsonData ? JSON.parse(jsonData) : {})
|
||||
} else {
|
||||
this.editAttr = null
|
||||
}
|
||||
},
|
||||
jsonEditorOk(jsonData) {
|
||||
this.form.setFieldsValue({ [this.editAttr.name]: JSON.stringify(jsonData) })
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="less">
|
||||
.create-instance-form {
|
||||
.ant-form-item {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.ant-drawer-body {
|
||||
overflow-y: auto;
|
||||
max-height: calc(100vh - 110px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,215 +1,225 @@
|
||||
<template>
|
||||
<CustomDrawer
|
||||
:visible="visible"
|
||||
:hasFooter="false"
|
||||
@close="
|
||||
() => {
|
||||
visible = false
|
||||
}
|
||||
"
|
||||
title="属性说明"
|
||||
width="72%"
|
||||
:bodyStyle="{ height: '100vh' }"
|
||||
>
|
||||
<vxe-toolbar>
|
||||
<template #buttons>
|
||||
<a-input
|
||||
v-model="searchKey"
|
||||
:style="{ display: 'inline-block', width: '244px' }"
|
||||
class="ops-input ops-input-radius"
|
||||
type="search"
|
||||
placeholder="搜索 名称 | 别名"
|
||||
@keyup="searchAttributes"
|
||||
>
|
||||
<a-icon type="search" slot="suffix" />
|
||||
</a-input>
|
||||
</template>
|
||||
</vxe-toolbar>
|
||||
|
||||
<a-spin :spinning="loading">
|
||||
<vxe-table
|
||||
resizable
|
||||
border
|
||||
size="mini"
|
||||
:height="windowHeight - 160"
|
||||
:data="list"
|
||||
:scroll-x="{ enabled: true, gt: 0 }"
|
||||
show-overflow
|
||||
show-header-overflow
|
||||
align="center"
|
||||
highlight-hover-row
|
||||
class="ops-stripe-table"
|
||||
>
|
||||
<vxe-column
|
||||
v-for="(column, index) in columns"
|
||||
:field="column.field"
|
||||
:title="column.title"
|
||||
:min-width="column.width"
|
||||
:align="column.align"
|
||||
:key="column.field"
|
||||
:fixed="index < 3 ? 'left' : ''"
|
||||
:sortable="index < 3 ? true : false"
|
||||
:title-help="column.help !== null ? { message: column.help } : null"
|
||||
:filters="
|
||||
index < 2 ? null: index === 2
|
||||
? valueTypeFilters: [{ label: '是', value: true }, { label: '否', value: false },]"
|
||||
type="html"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span v-if="column.field !== 'name' && column.field !== 'alias' && column.field !== 'value_type'">
|
||||
<a-icon :style="{ color: '#1fb51f' }" type="check" v-if="row[column.field]" />
|
||||
</span>
|
||||
<span v-else-if="column.field === 'value_type'" v-html="valueTypeMap[row.value_type]"> </span>
|
||||
<span v-else v-html="row[column.field]"> </span>
|
||||
</template>
|
||||
</vxe-column>
|
||||
</vxe-table>
|
||||
</a-spin>
|
||||
</CustomDrawer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import XEUtils from 'xe-utils'
|
||||
import { getCITypeAttributesByName } from '@/modules/cmdb/api/CITypeAttr'
|
||||
import { valueTypeMap } from '@/modules/cmdb/utils/const'
|
||||
export default {
|
||||
name: 'MetadataDrawer',
|
||||
data() {
|
||||
const columns = [
|
||||
{
|
||||
field: 'name',
|
||||
title: '名称',
|
||||
width: 150,
|
||||
align: 'left',
|
||||
help: null,
|
||||
},
|
||||
{
|
||||
field: 'alias',
|
||||
title: '别名',
|
||||
width: 150,
|
||||
align: 'left',
|
||||
help: null,
|
||||
},
|
||||
{
|
||||
field: 'value_type',
|
||||
title: '类型',
|
||||
width: 100,
|
||||
align: 'left',
|
||||
help: null,
|
||||
},
|
||||
{
|
||||
field: 'is_index',
|
||||
title: '是否索引',
|
||||
width: 110,
|
||||
help: '加快检索, 可以全文搜索, 无需使用条件过滤\n\n json目前不支持建索引 \n\n文本字符长度超过190不能建索引',
|
||||
},
|
||||
{
|
||||
field: 'default_show',
|
||||
title: '默认显示',
|
||||
width: 110,
|
||||
help: '订阅CI,默认显示在table里的属性',
|
||||
},
|
||||
{
|
||||
field: 'is_unique',
|
||||
title: '是否唯一',
|
||||
width: 110,
|
||||
help: null,
|
||||
},
|
||||
{
|
||||
field: 'is_choice',
|
||||
title: '是否选择',
|
||||
width: 110,
|
||||
help: '表现形式是下拉框, 值必须在预定义值里',
|
||||
},
|
||||
{
|
||||
field: 'is_list',
|
||||
title: '是否列表',
|
||||
width: 110,
|
||||
help: '多值, 比如内网IP',
|
||||
},
|
||||
{
|
||||
field: 'is_sortable',
|
||||
title: '可排序',
|
||||
width: 100,
|
||||
help: '仅针对前端',
|
||||
},
|
||||
{
|
||||
field: 'is_password',
|
||||
title: '是否密码',
|
||||
width: 100,
|
||||
help: null,
|
||||
},
|
||||
{
|
||||
field: 'is_link',
|
||||
title: '是否链接',
|
||||
width: 110,
|
||||
help: null,
|
||||
},
|
||||
{
|
||||
field: 'is_computed',
|
||||
title: '计算属性',
|
||||
width: 110,
|
||||
help: '模型的其他属性通过表达式的方式计算出来\n\n一个代码片段计算返回的值',
|
||||
},
|
||||
]
|
||||
return {
|
||||
columns,
|
||||
visible: false,
|
||||
list: [],
|
||||
tableData: [],
|
||||
loading: false,
|
||||
valueTypeMap,
|
||||
valueTypeFilters: [],
|
||||
searchKey: '',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
windowHeight() {
|
||||
return this.$store.state.windowHeight
|
||||
},
|
||||
},
|
||||
created: function () {
|
||||
this.valueTypeFilters = Object.keys(this.valueTypeMap).map((key) => {
|
||||
return { label: this.valueTypeMap[key], value: key }
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
open(typeId) {
|
||||
this.visible = true
|
||||
this.typeId = typeId
|
||||
this.getAttrs()
|
||||
},
|
||||
async getAttrs() {
|
||||
this.loading = true
|
||||
const { attributes = [] } = await getCITypeAttributesByName(this.typeId)
|
||||
this.tableData = attributes
|
||||
this.loading = false
|
||||
this.searchAttributes()
|
||||
},
|
||||
searchAttributes() {
|
||||
const filterName = XEUtils.toValueString(this.searchKey).trim().toLowerCase()
|
||||
if (filterName) {
|
||||
const filterRE = new RegExp(filterName, 'gi')
|
||||
const searchProps = ['name', 'alias', 'value_type']
|
||||
const rest = this.tableData.filter((item) =>
|
||||
searchProps.some((key) => XEUtils.toValueString(item[key]).toLowerCase().indexOf(filterName) > -1)
|
||||
)
|
||||
this.list = rest.map((row) => {
|
||||
const item = Object.assign({}, row)
|
||||
searchProps.forEach((key) => {
|
||||
item[key] = XEUtils.toValueString(item[key]).replace(
|
||||
filterRE,
|
||||
(match) => `<span style='background: yellow'>${match}</span>`
|
||||
)
|
||||
})
|
||||
return item
|
||||
})
|
||||
} else {
|
||||
this.list = this.tableData
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
<template>
|
||||
<CustomDrawer
|
||||
:visible="visible"
|
||||
:hasFooter="false"
|
||||
@close="
|
||||
() => {
|
||||
visible = false
|
||||
}
|
||||
"
|
||||
title="属性说明"
|
||||
width="72%"
|
||||
:bodyStyle="{ height: '100vh' }"
|
||||
>
|
||||
<vxe-toolbar>
|
||||
<template #buttons>
|
||||
<a-input
|
||||
v-model="searchKey"
|
||||
:style="{ display: 'inline-block', width: '244px' }"
|
||||
class="ops-input ops-input-radius"
|
||||
type="search"
|
||||
placeholder="搜索 名称 | 别名"
|
||||
@keyup="searchAttributes"
|
||||
>
|
||||
<a-icon type="search" slot="suffix" />
|
||||
</a-input>
|
||||
</template>
|
||||
</vxe-toolbar>
|
||||
|
||||
<a-spin :spinning="loading">
|
||||
<vxe-table
|
||||
resizable
|
||||
border
|
||||
size="mini"
|
||||
:height="windowHeight - 160"
|
||||
:data="list"
|
||||
:scroll-x="{ enabled: true, gt: 0 }"
|
||||
show-overflow
|
||||
show-header-overflow
|
||||
align="center"
|
||||
highlight-hover-row
|
||||
class="ops-stripe-table"
|
||||
>
|
||||
<vxe-column
|
||||
v-for="(column, index) in columns"
|
||||
:field="column.field"
|
||||
:title="column.title"
|
||||
:min-width="column.width"
|
||||
:align="column.align"
|
||||
:key="column.field"
|
||||
:fixed="index < 3 ? 'left' : ''"
|
||||
:sortable="index < 3 ? true : false"
|
||||
:title-help="column.help !== null ? { message: column.help } : null"
|
||||
:filters="
|
||||
index < 2
|
||||
? null
|
||||
: index === 2
|
||||
? valueTypeFilters
|
||||
: [
|
||||
{ label: '是', value: true },
|
||||
{ label: '否', value: false },
|
||||
]
|
||||
"
|
||||
type="html"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span v-if="column.field !== 'name' && column.field !== 'alias' && column.field !== 'value_type'">
|
||||
<a-icon :style="{ color: '#1fb51f' }" type="check" v-if="row[column.field]" />
|
||||
</span>
|
||||
<span v-else-if="column.field === 'value_type'" v-html="valueTypeMap[row.value_type]"> </span>
|
||||
<span v-else v-html="row[column.field]"> </span>
|
||||
</template>
|
||||
</vxe-column>
|
||||
</vxe-table>
|
||||
</a-spin>
|
||||
</CustomDrawer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import XEUtils from 'xe-utils'
|
||||
import { getCITypeAttributesByName } from '@/modules/cmdb/api/CITypeAttr'
|
||||
import { valueTypeMap } from '@/modules/cmdb/utils/const'
|
||||
export default {
|
||||
name: 'MetadataDrawer',
|
||||
data() {
|
||||
const columns = [
|
||||
{
|
||||
field: 'name',
|
||||
title: '名称',
|
||||
width: 150,
|
||||
align: 'left',
|
||||
help: null,
|
||||
},
|
||||
{
|
||||
field: 'alias',
|
||||
title: '别名',
|
||||
width: 150,
|
||||
align: 'left',
|
||||
help: null,
|
||||
},
|
||||
{
|
||||
field: 'value_type',
|
||||
title: '类型',
|
||||
width: 100,
|
||||
align: 'left',
|
||||
help: null,
|
||||
},
|
||||
{
|
||||
field: 'is_index',
|
||||
title: '是否索引',
|
||||
width: 110,
|
||||
help: '加快检索, 可以全文搜索, 无需使用条件过滤\n\n json目前不支持建索引 \n\n文本字符长度超过190不能建索引',
|
||||
},
|
||||
{
|
||||
field: 'default_show',
|
||||
title: '默认显示',
|
||||
width: 110,
|
||||
help: '订阅CI,默认显示在table里的属性',
|
||||
},
|
||||
{
|
||||
field: 'is_unique',
|
||||
title: '是否唯一',
|
||||
width: 110,
|
||||
help: null,
|
||||
},
|
||||
{
|
||||
field: 'is_choice',
|
||||
title: '是否选择',
|
||||
width: 110,
|
||||
help: '表现形式是下拉框, 值必须在预定义值里',
|
||||
},
|
||||
{
|
||||
field: 'is_list',
|
||||
title: '是否列表',
|
||||
width: 110,
|
||||
help: '多值, 比如内网IP',
|
||||
},
|
||||
{
|
||||
field: 'is_sortable',
|
||||
title: '可排序',
|
||||
width: 100,
|
||||
help: '仅针对前端',
|
||||
},
|
||||
{
|
||||
field: 'is_computed',
|
||||
title: '计算属性',
|
||||
width: 110,
|
||||
help: '模型的其他属性通过表达式的方式计算出来\n\n一个代码片段计算返回的值',
|
||||
},
|
||||
]
|
||||
return {
|
||||
columns,
|
||||
visible: false,
|
||||
list: [],
|
||||
tableData: [],
|
||||
loading: false,
|
||||
valueTypeMap,
|
||||
valueTypeFilters: [],
|
||||
searchKey: '',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
windowHeight() {
|
||||
return this.$store.state.windowHeight
|
||||
},
|
||||
},
|
||||
created: function() {
|
||||
this.valueTypeFilters = Object.keys(this.valueTypeMap).map((key) => {
|
||||
return { label: this.valueTypeMap[key], value: key }
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
open(typeId) {
|
||||
this.visible = true
|
||||
this.typeId = typeId
|
||||
this.getAttrs()
|
||||
},
|
||||
async getAttrs() {
|
||||
this.loading = true
|
||||
const { attributes = [] } = await getCITypeAttributesByName(this.typeId)
|
||||
this.tableData = attributes.map((attr) => {
|
||||
if (attr.is_password) {
|
||||
attr.value_type = '7'
|
||||
}
|
||||
if (attr.is_link) {
|
||||
attr.value_type = '8'
|
||||
}
|
||||
return attr
|
||||
})
|
||||
this.loading = false
|
||||
this.searchAttributes()
|
||||
},
|
||||
searchAttributes() {
|
||||
const filterName = XEUtils.toValueString(this.searchKey)
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
if (filterName) {
|
||||
const filterRE = new RegExp(filterName, 'gi')
|
||||
const searchProps = ['name', 'alias', 'value_type']
|
||||
const rest = this.tableData.filter((item) =>
|
||||
searchProps.some(
|
||||
(key) =>
|
||||
XEUtils.toValueString(item[key])
|
||||
.toLowerCase()
|
||||
.indexOf(filterName) > -1
|
||||
)
|
||||
)
|
||||
this.list = rest.map((row) => {
|
||||
const item = Object.assign({}, row)
|
||||
searchProps.forEach((key) => {
|
||||
item[key] = XEUtils.toValueString(item[key]).replace(
|
||||
filterRE,
|
||||
(match) => `<span style='background: yellow'>${match}</span>`
|
||||
)
|
||||
})
|
||||
return item
|
||||
})
|
||||
} else {
|
||||
this.list = this.tableData
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
@@ -1,234 +1,299 @@
|
||||
<template>
|
||||
<span :id="`ci-detail-attr-${attr.name}`">
|
||||
<span v-if="!isEdit || attr.value_type === '6'">
|
||||
<template v-if="attr.value_type === '6'">{{ JSON.stringify(ci[attr.name] || {}) }}</template>
|
||||
<template v-else-if="attr.is_choice">
|
||||
<template v-if="attr.is_list">
|
||||
<span
|
||||
v-for="value in ci[attr.name]"
|
||||
:key="value"
|
||||
:style="{
|
||||
borderRadius: '4px',
|
||||
padding: '1px 5px',
|
||||
margin: '2px',
|
||||
...getChoiceValueStyle(attr, value),
|
||||
}"
|
||||
>
|
||||
<ops-icon
|
||||
:style="{ color: getChoiceValueIcon(attr, value).color }"
|
||||
:type="getChoiceValueIcon(attr, value).name"
|
||||
/>
|
||||
{{ value }}</span
|
||||
>
|
||||
</template>
|
||||
<span
|
||||
v-else
|
||||
:style="{
|
||||
borderRadius: '4px',
|
||||
padding: '1px 5px',
|
||||
margin: '2px 0',
|
||||
...getChoiceValueStyle(attr, ci[attr.name]),
|
||||
}"
|
||||
>
|
||||
<ops-icon
|
||||
:style="{ color: getChoiceValueIcon(attr, ci[attr.name]).color }"
|
||||
:type="getChoiceValueIcon(attr, ci[attr.name]).name"
|
||||
/>
|
||||
{{ ci[attr.name] }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>{{ getName(ci[attr.name]) }}</template>
|
||||
</span>
|
||||
<template v-else>
|
||||
<a-form :form="form">
|
||||
<a-form-item label="" :colon="false">
|
||||
<a-select
|
||||
:style="{ width: '100%' }"
|
||||
v-decorator="[
|
||||
attr.name,
|
||||
{
|
||||
rules: [{ required: attr.is_required }],
|
||||
},
|
||||
]"
|
||||
placeholder="请选择"
|
||||
v-if="attr.is_choice"
|
||||
:mode="attr.is_list ? 'multiple' : 'default'"
|
||||
:multiple="attr.is_list"
|
||||
showSearch
|
||||
allowClear
|
||||
size="small"
|
||||
:getPopupContainer="(trigger) => trigger.parentElement"
|
||||
>
|
||||
<a-select-option
|
||||
:value="choice[0]"
|
||||
:key="'New_' + attr.name + choice_idx"
|
||||
v-for="(choice, choice_idx) in attr.choice_value"
|
||||
>
|
||||
<span :style="choice[1] ? choice[1].style || {} : {}">
|
||||
<ops-icon
|
||||
:style="{ color: choice[1].icon.color }"
|
||||
v-if="choice[1] && choice[1].icon && choice[1].icon.name"
|
||||
:type="choice[1].icon.name"
|
||||
/>
|
||||
{{ choice[0] }}
|
||||
</span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-decorator="[
|
||||
attr.name,
|
||||
{
|
||||
rules: [{ required: attr.is_required }],
|
||||
},
|
||||
]"
|
||||
style="width: 100%"
|
||||
v-else-if="attr.value_type === '0' || attr.value_type === '1'"
|
||||
/>
|
||||
<a-date-picker
|
||||
size="small"
|
||||
v-decorator="[
|
||||
attr.name,
|
||||
{
|
||||
rules: [{ required: attr.is_required }],
|
||||
},
|
||||
]"
|
||||
style="width: 100%"
|
||||
:format="attr.value_type === '4' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
|
||||
:valueFormat="attr.value_type === '4' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
|
||||
v-else-if="attr.value_type === '4' || attr.value_type === '3'"
|
||||
:showTime="attr.value_type === '4' ? false : { format: 'HH:mm:ss' }"
|
||||
/>
|
||||
<a-input
|
||||
size="small"
|
||||
v-decorator="[
|
||||
attr.name,
|
||||
{
|
||||
validateTrigger: ['submit'],
|
||||
rules: [{ required: attr.is_required }],
|
||||
},
|
||||
]"
|
||||
style="width: 100%"
|
||||
v-else
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
<a v-if="!isEdit && !attr.is_computed" @click="handleEdit" :style="{ opacity: 0 }"><a-icon type="edit"/></a>
|
||||
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import moment from 'moment'
|
||||
import { updateCI } from '@/modules/cmdb/api/ci'
|
||||
import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue'
|
||||
export default {
|
||||
name: 'CiDetailAttrContent',
|
||||
components: { JsonEditor },
|
||||
props: {
|
||||
ci: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
attr: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isEdit: false,
|
||||
form: this.$form.createForm(this, this.attr.name),
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('click', this.eventListener)
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('click', this.eventListener)
|
||||
},
|
||||
methods: {
|
||||
moment,
|
||||
eventListener(e) {
|
||||
const datePickerContainer = document.getElementsByClassName('ant-calendar-picker-container')
|
||||
if (this.isEdit && !datePickerContainer.length) {
|
||||
const dom = document.getElementById(`ci-detail-attr-${this.attr.name}`)
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
if (dom) {
|
||||
const isSelf = dom.contains(e.target)
|
||||
if (!isSelf) {
|
||||
this.handleCloseEdit()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
handleEdit(e) {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
if (this.attr.value_type === '6') {
|
||||
const jsonData = this.ci[this.attr.name]
|
||||
this.$refs.jsonEditor.open(null, null, jsonData || {})
|
||||
return
|
||||
}
|
||||
this.isEdit = true
|
||||
this.$nextTick(() => {
|
||||
if (this.attr.is_list && !this.attr.is_choice) {
|
||||
this.form.setFieldsValue({
|
||||
[`${this.attr.name}`]: this.ci[this.attr.name].join(',') || null,
|
||||
})
|
||||
return
|
||||
}
|
||||
this.form.setFieldsValue({
|
||||
[`${this.attr.name}`]: this.ci[this.attr.name] ?? null,
|
||||
})
|
||||
})
|
||||
},
|
||||
async handleCloseEdit() {
|
||||
const newData = this.form.getFieldValue(this.attr.name)
|
||||
if (!_.isEqual(this.ci[this.attr.name], newData)) {
|
||||
await updateCI(this.ci._id, { [`${this.attr.name}`]: newData })
|
||||
.then(() => {
|
||||
this.$message.success('更新成功!')
|
||||
})
|
||||
.finally(() => {
|
||||
this.$emit('refresh', this.attr.name)
|
||||
})
|
||||
}
|
||||
this.isEdit = false
|
||||
},
|
||||
jsonEditorOk(jsonData) {
|
||||
if (!_.isEqual(this.ci[this.attr.name], jsonData)) {
|
||||
updateCI(this.ci._id, { [`${this.attr.name}`]: jsonData })
|
||||
.then(() => {
|
||||
this.$message.success('更新成功!')
|
||||
})
|
||||
.finally(() => {
|
||||
this.$emit('refresh', this.attr.name)
|
||||
})
|
||||
}
|
||||
},
|
||||
getChoiceValueStyle(col, colValue) {
|
||||
const _find = col.choice_value.find((item) => String(item[0]) === String(colValue))
|
||||
if (_find) {
|
||||
return _find[1]?.style || {}
|
||||
}
|
||||
return {}
|
||||
},
|
||||
getChoiceValueIcon(col, colValue) {
|
||||
const _find = col.choice_value.find((item) => String(item[0]) === String(colValue))
|
||||
if (_find) {
|
||||
return _find[1]?.icon || {}
|
||||
}
|
||||
return {}
|
||||
},
|
||||
getName(name) {
|
||||
return name ?? ''
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
<template>
|
||||
<span :id="`ci-detail-attr-${attr.name}`">
|
||||
<span v-if="!isEdit || attr.value_type === '6'">
|
||||
<PasswordField
|
||||
:style="{ display: 'inline-block' }"
|
||||
v-if="attr.is_password && ci[attr.name]"
|
||||
:ci_id="ci._id"
|
||||
:attr_id="attr.id"
|
||||
></PasswordField>
|
||||
<template v-else-if="attr.value_type === '6'">{{ JSON.stringify(ci[attr.name] || {}) }}</template>
|
||||
<template v-else-if="attr.is_choice">
|
||||
<template v-if="attr.is_list">
|
||||
<span
|
||||
v-for="value in ci[attr.name]"
|
||||
:key="value"
|
||||
:style="{
|
||||
borderRadius: '4px',
|
||||
padding: '1px 5px',
|
||||
margin: '2px',
|
||||
...getChoiceValueStyle(attr, value),
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
}"
|
||||
>
|
||||
<img
|
||||
v-if="getChoiceValueIcon(attr, value).id && getChoiceValueIcon(attr, value).url"
|
||||
:src="`/api/common-setting/v1/file/${getChoiceValueIcon(attr, value).url}`"
|
||||
:style="{ maxHeight: '13px', maxWidth: '13px', marginRight: '5px' }"
|
||||
/>
|
||||
<ops-icon
|
||||
v-else
|
||||
:style="{ color: getChoiceValueIcon(attr, value).color, marginRight: '5px' }"
|
||||
:type="getChoiceValueIcon(attr, value).name"
|
||||
/>
|
||||
{{ value }}</span
|
||||
>
|
||||
</template>
|
||||
<span
|
||||
v-else
|
||||
:style="{
|
||||
borderRadius: '4px',
|
||||
padding: '1px 5px',
|
||||
margin: '2px 0',
|
||||
...getChoiceValueStyle(attr, ci[attr.name]),
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
}"
|
||||
>
|
||||
<img
|
||||
v-if="getChoiceValueIcon(attr, ci[attr.name]).id && getChoiceValueIcon(attr, ci[attr.name]).url"
|
||||
:src="`/api/common-setting/v1/file/${getChoiceValueIcon(attr, ci[attr.name]).url}`"
|
||||
:style="{ maxHeight: '13px', maxWidth: '13px', marginRight: '5px' }"
|
||||
/>
|
||||
<ops-icon
|
||||
v-else
|
||||
:style="{ color: getChoiceValueIcon(attr, ci[attr.name]).color, marginRight: '5px' }"
|
||||
:type="getChoiceValueIcon(attr, ci[attr.name]).name"
|
||||
/>
|
||||
{{ ci[attr.name] }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>{{ getName(ci[attr.name]) }}</template>
|
||||
</span>
|
||||
<template v-else>
|
||||
<a-form :form="form">
|
||||
<a-form-item label="" :colon="false">
|
||||
<a-select
|
||||
:style="{ width: '100%' }"
|
||||
v-decorator="[
|
||||
attr.name,
|
||||
{
|
||||
rules: [{ required: attr.is_required }],
|
||||
},
|
||||
]"
|
||||
placeholder="请选择"
|
||||
v-if="attr.is_choice"
|
||||
:mode="attr.is_list ? 'multiple' : 'default'"
|
||||
:multiple="attr.is_list"
|
||||
showSearch
|
||||
allowClear
|
||||
size="small"
|
||||
:getPopupContainer="(trigger) => trigger.parentElement"
|
||||
>
|
||||
<a-select-option
|
||||
:value="choice[0]"
|
||||
:key="'New_' + attr.name + choice_idx"
|
||||
v-for="(choice, choice_idx) in attr.choice_value"
|
||||
>
|
||||
<span :style="{ ...(choice[1] ? choice[1].style : {}), display: 'inline-flex', alignItems: 'center' }">
|
||||
<template v-if="choice[1] && choice[1].icon && choice[1].icon.name">
|
||||
<img
|
||||
v-if="choice[1].icon.id && choice[1].icon.url"
|
||||
:src="`/api/common-setting/v1/file/${choice[1].icon.url}`"
|
||||
:style="{ maxHeight: '13px', maxWidth: '13px', marginRight: '5px' }"
|
||||
/>
|
||||
<ops-icon
|
||||
v-else
|
||||
:style="{ color: choice[1].icon.color, marginRight: '5px' }"
|
||||
:type="choice[1].icon.name"
|
||||
/>
|
||||
</template>
|
||||
{{ choice[0] }}
|
||||
</span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<a-input-number
|
||||
size="small"
|
||||
v-decorator="[
|
||||
attr.name,
|
||||
{
|
||||
rules: [{ required: attr.is_required }],
|
||||
},
|
||||
]"
|
||||
style="width: 100%"
|
||||
v-else-if="attr.value_type === '0' || attr.value_type === '1'"
|
||||
/>
|
||||
<a-date-picker
|
||||
size="small"
|
||||
v-decorator="[
|
||||
attr.name,
|
||||
{
|
||||
rules: [{ required: attr.is_required }],
|
||||
},
|
||||
]"
|
||||
style="width: 100%"
|
||||
:format="attr.value_type === '4' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
|
||||
:valueFormat="attr.value_type === '4' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
|
||||
v-else-if="attr.value_type === '4' || attr.value_type === '3'"
|
||||
:showTime="attr.value_type === '4' ? false : { format: 'HH:mm:ss' }"
|
||||
/>
|
||||
<!-- <a-input
|
||||
size="small"
|
||||
@focus="(e) => handleFocusInput(e, attr)"
|
||||
v-decorator="[
|
||||
attr.name,
|
||||
{
|
||||
validateTrigger: ['submit'],
|
||||
rules: [{ required: attr.is_required }],
|
||||
},
|
||||
]"
|
||||
style="width: 100%"
|
||||
v-else-if="attr.value_type === '6'"
|
||||
/> -->
|
||||
<a-input
|
||||
size="small"
|
||||
v-decorator="[
|
||||
attr.name,
|
||||
{
|
||||
validateTrigger: ['submit'],
|
||||
rules: [{ required: attr.is_required }],
|
||||
},
|
||||
]"
|
||||
style="width: 100%"
|
||||
v-else
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
<a v-if="!isEdit && !attr.is_computed" @click="handleEdit" :style="{ opacity: 0 }"><a-icon type="edit"/></a>
|
||||
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import moment from 'moment'
|
||||
import { updateCI } from '@/modules/cmdb/api/ci'
|
||||
import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue'
|
||||
import PasswordField from '../../../components/passwordField/index.vue'
|
||||
import { getAttrPassword } from '../../../api/CITypeAttr'
|
||||
|
||||
export default {
|
||||
name: 'CiDetailAttrContent',
|
||||
components: { JsonEditor, PasswordField },
|
||||
props: {
|
||||
ci: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
attr: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isEdit: false,
|
||||
form: this.$form.createForm(this, this.attr.name),
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('click', this.eventListener)
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('click', this.eventListener)
|
||||
},
|
||||
methods: {
|
||||
moment,
|
||||
eventListener(e) {
|
||||
const datePickerContainer = document.getElementsByClassName('ant-calendar-picker-container')
|
||||
if (this.isEdit && !datePickerContainer.length) {
|
||||
const dom = document.getElementById(`ci-detail-attr-${this.attr.name}`)
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
if (dom) {
|
||||
const isSelf = dom.contains(e.target)
|
||||
if (!isSelf) {
|
||||
this.handleCloseEdit()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
handleEdit(e) {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
if (this.attr.value_type === '6') {
|
||||
const jsonData = this.ci[this.attr.name]
|
||||
this.$refs.jsonEditor.open(null, null, jsonData || {})
|
||||
return
|
||||
}
|
||||
this.isEdit = true
|
||||
this.$nextTick(async () => {
|
||||
if (this.attr.is_list && !this.attr.is_choice) {
|
||||
this.form.setFieldsValue({
|
||||
[`${this.attr.name}`]: this.ci[this.attr.name].join(',') || null,
|
||||
})
|
||||
return
|
||||
}
|
||||
if (this.attr.is_password) {
|
||||
await getAttrPassword(this.ci._id, this.attr.id).then((res) => {
|
||||
this.form.setFieldsValue({
|
||||
[`${this.attr.name}`]: res.value ?? null,
|
||||
})
|
||||
})
|
||||
return
|
||||
}
|
||||
this.form.setFieldsValue({
|
||||
[`${this.attr.name}`]: this.ci[this.attr.name] ?? null,
|
||||
})
|
||||
})
|
||||
},
|
||||
async handleCloseEdit() {
|
||||
const newData = this.form.getFieldValue(this.attr.name)
|
||||
if (!_.isEqual(this.ci[this.attr.name], newData)) {
|
||||
await updateCI(this.ci._id, { [`${this.attr.name}`]: newData })
|
||||
.then(() => {
|
||||
this.$message.success('更新成功!')
|
||||
this.$emit('updateCIByself', { [`${this.attr.name}`]: newData }, this.attr.name)
|
||||
})
|
||||
.catch(() => {
|
||||
this.$emit('refresh', this.attr.name)
|
||||
})
|
||||
}
|
||||
this.isEdit = false
|
||||
},
|
||||
// handleFocusInput(e, attr) {
|
||||
// console.log('focus')
|
||||
// if (this.attr.value_type === '6') {
|
||||
// e.preventDefault()
|
||||
// e.stopPropagation()
|
||||
// // e.srcElement.blur()
|
||||
// const jsonData = this.form.getFieldValue(attr.name)
|
||||
// this.$refs.jsonEditor.open(null, null, jsonData ? JSON.parse(jsonData) : {})
|
||||
// }
|
||||
// },
|
||||
jsonEditorOk(jsonData) {
|
||||
if (!_.isEqual(this.ci[this.attr.name], jsonData)) {
|
||||
updateCI(this.ci._id, { [`${this.attr.name}`]: jsonData })
|
||||
.then(() => {
|
||||
this.$message.success('更新成功!')
|
||||
this.$emit('updateCIByself', { [`${this.attr.name}`]: jsonData }, this.attr.name)
|
||||
})
|
||||
.catch(() => {
|
||||
this.$emit('refresh', this.attr.name)
|
||||
})
|
||||
}
|
||||
},
|
||||
getChoiceValueStyle(col, colValue) {
|
||||
const _find = col.choice_value.find((item) => String(item[0]) === String(colValue))
|
||||
if (_find) {
|
||||
return _find[1]?.style || {}
|
||||
}
|
||||
return {}
|
||||
},
|
||||
getChoiceValueIcon(col, colValue) {
|
||||
const _find = col.choice_value.find((item) => String(item[0]) === String(colValue))
|
||||
if (_find) {
|
||||
return _find[1]?.icon || {}
|
||||
}
|
||||
return {}
|
||||
},
|
||||
getName(name) {
|
||||
return name ?? ''
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
@@ -1,280 +1,274 @@
|
||||
<template>
|
||||
<div class="attribute-card">
|
||||
<div class="attribute-card-content">
|
||||
<div class="attribute-card-value-type-icon handle" :style="{ ...getPropertyStyle(property) }">
|
||||
<ValueTypeIcon :attr="property" />
|
||||
</div>
|
||||
<div :class="{ 'attribute-card-content-inner': true, 'attribute-card-name-required': property.is_required }">
|
||||
<div :class="{ 'attribute-card-name': true, 'attribute-card-name-default-show': property.default_show }">
|
||||
{{ property.alias || property.name }}
|
||||
</div>
|
||||
<div class="attribute-card_value-type">{{ valueTypeMap[property.value_type] }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="attribute-card-trigger"
|
||||
v-if="(property.value_type === '3' || property.value_type === '4') && !isStore"
|
||||
>
|
||||
<a @click="openTrigger"><ops-icon type="ops-trigger"/></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="attribute-card-footer">
|
||||
<a-popover
|
||||
trigger="click"
|
||||
:arrowPointAtCenter="true"
|
||||
placement="bottom"
|
||||
overlayClassName="attribute-card-footer-popover"
|
||||
>
|
||||
<div slot="content">
|
||||
<h3 :style="{ textAlign: 'center', paddingTop: '0.5em' }">
|
||||
<span>{{ property.alias }}({{ property.name }})</span>
|
||||
</h3>
|
||||
<a-descriptions layout="horizontal" bordered size="small" :column="2">
|
||||
<a-descriptions-item v-for="item in propertyList" :key="item.property" :label="item.label">
|
||||
<components
|
||||
:is="`ops_${item.property}`"
|
||||
v-if="property[item.property]"
|
||||
:style="{ width: '1em', height: '1em' }"
|
||||
/>
|
||||
<ops-icon v-else :type="`ops-${item.property}-disabled`" />
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label></a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</div>
|
||||
<a-space :style="{ cursor: 'pointer' }">
|
||||
<components
|
||||
v-for="item in propertyList.filter((p) => property[p.property])"
|
||||
:key="item.property"
|
||||
:is="`ops_${item.property}`"
|
||||
/>
|
||||
</a-space>
|
||||
</a-popover>
|
||||
|
||||
<a-space class="attribute-card-operation">
|
||||
<a v-if="!isStore"><a-icon type="edit" @click="handleEdit"/></a>
|
||||
<a-tooltip title="所有CI触发计算">
|
||||
<a v-if="!isStore && property.is_computed"><a-icon type="redo" @click="handleCalcComputed"/></a>
|
||||
</a-tooltip>
|
||||
<a style="color:red;"><a-icon type="delete" @click="handleDelete"/></a>
|
||||
</a-space>
|
||||
</div>
|
||||
<TriggerForm ref="triggerForm" :CITypeId="CITypeId" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { deleteCITypeAttributesById, deleteAttributesById, calcComputedAttribute } from '@/modules/cmdb/api/CITypeAttr'
|
||||
import ValueTypeIcon from '@/components/CMDBValueTypeMapIcon'
|
||||
import {
|
||||
ops_default_show,
|
||||
ops_is_choice,
|
||||
ops_is_index,
|
||||
ops_is_link,
|
||||
ops_is_password,
|
||||
ops_is_sortable,
|
||||
ops_is_unique,
|
||||
} from '@/core/icons'
|
||||
import { valueTypeMap } from '../../utils/const'
|
||||
import { getPropertyStyle } from '../../utils/helper'
|
||||
import TriggerForm from './triggerForm.vue'
|
||||
export default {
|
||||
name: 'AttributeCard',
|
||||
components: {
|
||||
ValueTypeIcon,
|
||||
TriggerForm,
|
||||
ops_default_show,
|
||||
ops_is_choice,
|
||||
ops_is_index,
|
||||
ops_is_link,
|
||||
ops_is_password,
|
||||
ops_is_sortable,
|
||||
ops_is_unique,
|
||||
},
|
||||
props: {
|
||||
property: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
CITypeId: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
isStore: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const propertyList = [
|
||||
{
|
||||
label: '是否唯一',
|
||||
property: 'is_unique',
|
||||
},
|
||||
{
|
||||
label: '是否选择',
|
||||
property: 'is_choice',
|
||||
},
|
||||
{
|
||||
label: '默认显示',
|
||||
property: 'default_show',
|
||||
},
|
||||
{
|
||||
label: '可排序',
|
||||
property: 'is_sortable',
|
||||
},
|
||||
{
|
||||
label: '是否索引',
|
||||
property: 'is_index',
|
||||
},
|
||||
{
|
||||
label: '是否密码',
|
||||
property: 'is_password',
|
||||
},
|
||||
{
|
||||
label: '是否链接',
|
||||
property: 'is_link',
|
||||
},
|
||||
]
|
||||
return {
|
||||
valueTypeMap,
|
||||
propertyList,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getPropertyStyle,
|
||||
handleEdit() {
|
||||
this.$emit('edit')
|
||||
},
|
||||
handleDelete() {
|
||||
const that = this
|
||||
this.$confirm({
|
||||
title: '警告',
|
||||
content: `确认删除 【${that.property.alias || that.property.name}】?`,
|
||||
onOk() {
|
||||
if (that.isStore) {
|
||||
deleteAttributesById(that.property.id).then(() => {
|
||||
that.$message.success('删除成功!')
|
||||
that.$emit('ok')
|
||||
})
|
||||
} else {
|
||||
deleteCITypeAttributesById(that.CITypeId, { attr_id: [that.property.id] }).then(() => {
|
||||
that.$message.success('删除成功!')
|
||||
that.$emit('ok')
|
||||
})
|
||||
}
|
||||
},
|
||||
onCancel() {},
|
||||
})
|
||||
},
|
||||
openTrigger() {
|
||||
this.$refs.triggerForm.open(this.property)
|
||||
},
|
||||
handleCalcComputed() {
|
||||
const that = this
|
||||
this.$confirm({
|
||||
title: '警告',
|
||||
content: `确认触发所有CI的计算?`,
|
||||
onOk() {
|
||||
calcComputedAttribute(that.property.id).then(() => {
|
||||
that.$message.success('触发成功!')
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.attribute-card {
|
||||
width: 182px;
|
||||
height: 80px;
|
||||
background: #f8faff;
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
margin-bottom: 16px;
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
box-shadow: 0 4px 12px #4e5ea066;
|
||||
.attribute-card-operation {
|
||||
visibility: visible !important;
|
||||
}
|
||||
}
|
||||
.attribute-card-content {
|
||||
height: 50px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
width: 100%;
|
||||
.attribute-card-value-type-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 12px;
|
||||
cursor: move;
|
||||
background: #ffffff !important;
|
||||
box-shadow: 0px 1px 2px rgba(47, 84, 235, 0.2);
|
||||
border-radius: 2px;
|
||||
text-align: center;
|
||||
line-height: 32px;
|
||||
}
|
||||
.attribute-card-content-inner {
|
||||
padding-left: 12px;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
width: 120px;
|
||||
position: relative;
|
||||
.attribute-card-name {
|
||||
width: 100%;
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.attribute-card-name-default-show {
|
||||
color: #2f54eb;
|
||||
}
|
||||
.attribute-card_value-type {
|
||||
font-size: 10px;
|
||||
color: rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
}
|
||||
.attribute-card-name-required::before {
|
||||
content: '*';
|
||||
width: 5px;
|
||||
color: red;
|
||||
position: absolute;
|
||||
left: 3px;
|
||||
}
|
||||
.attribute-card-trigger {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 8px;
|
||||
}
|
||||
}
|
||||
.attribute-card-footer {
|
||||
width: 182px;
|
||||
height: 30px;
|
||||
padding: 0 8px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: linear-gradient(180deg, #96abd6 0%, #ecf2ff 0.01%, #ffffff 143.33%);
|
||||
border-radius: 0px 0px 5px 5px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
.attribute-card-operation {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="less">
|
||||
.attribute-card-footer-popover {
|
||||
.ant-popover-inner-content {
|
||||
padding: 0;
|
||||
}
|
||||
.ant-descriptions-bordered .ant-descriptions-item-label {
|
||||
background-color: #f8faff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<div class="attribute-card">
|
||||
<div class="attribute-card-content">
|
||||
<div class="attribute-card-value-type-icon handle" :style="{ ...getPropertyStyle(property) }">
|
||||
<ValueTypeIcon :attr="property" />
|
||||
</div>
|
||||
<div :class="{ 'attribute-card-content-inner': true, 'attribute-card-name-required': property.is_required }">
|
||||
<div :class="{ 'attribute-card-name': true, 'attribute-card-name-default-show': property.default_show }">
|
||||
{{ property.alias || property.name }}
|
||||
</div>
|
||||
<div v-if="property.is_password" class="attribute-card_value-type">密码</div>
|
||||
<div v-else-if="property.is_link" class="attribute-card_value-type">链接</div>
|
||||
<div v-else class="attribute-card_value-type">{{ valueTypeMap[property.value_type] }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="attribute-card-trigger"
|
||||
v-if="(property.value_type === '3' || property.value_type === '4') && !isStore"
|
||||
>
|
||||
<a @click="openTrigger"><ops-icon type="ops-trigger"/></a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="attribute-card-footer">
|
||||
<a-popover
|
||||
trigger="click"
|
||||
:arrowPointAtCenter="true"
|
||||
placement="bottom"
|
||||
overlayClassName="attribute-card-footer-popover"
|
||||
>
|
||||
<div slot="content">
|
||||
<h3 :style="{ textAlign: 'center', paddingTop: '0.5em' }">
|
||||
<span>{{ property.alias }}({{ property.name }})</span>
|
||||
</h3>
|
||||
<a-descriptions layout="horizontal" bordered size="small" :column="2">
|
||||
<a-descriptions-item v-for="item in propertyList" :key="item.property" :label="item.label">
|
||||
<components
|
||||
:is="`ops_${item.property}`"
|
||||
v-if="property[item.property]"
|
||||
:style="{ width: '1em', height: '1em' }"
|
||||
/>
|
||||
<ops-icon v-else :type="`ops-${item.property}-disabled`" />
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label></a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</div>
|
||||
<a-space :style="{ cursor: 'pointer' }">
|
||||
<components
|
||||
v-for="item in propertyList.filter((p) => property[p.property])"
|
||||
:key="item.property"
|
||||
:is="`ops_${item.property}`"
|
||||
/>
|
||||
</a-space>
|
||||
</a-popover>
|
||||
|
||||
<a-space class="attribute-card-operation">
|
||||
<a v-if="!isStore"><a-icon type="edit" @click="handleEdit"/></a>
|
||||
<a-tooltip title="所有CI触发计算">
|
||||
<a v-if="!isStore && property.is_computed"><a-icon type="redo" @click="handleCalcComputed"/></a>
|
||||
</a-tooltip>
|
||||
<a style="color:red;"><a-icon type="delete" @click="handleDelete"/></a>
|
||||
</a-space>
|
||||
</div>
|
||||
<TriggerForm ref="triggerForm" :CITypeId="CITypeId" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { deleteCITypeAttributesById, deleteAttributesById, calcComputedAttribute } from '@/modules/cmdb/api/CITypeAttr'
|
||||
import ValueTypeIcon from '@/components/CMDBValueTypeMapIcon'
|
||||
import {
|
||||
ops_default_show,
|
||||
ops_is_choice,
|
||||
ops_is_index,
|
||||
ops_is_link,
|
||||
ops_is_password,
|
||||
ops_is_sortable,
|
||||
ops_is_unique,
|
||||
} from '@/core/icons'
|
||||
import { valueTypeMap } from '../../utils/const'
|
||||
import { getPropertyStyle } from '../../utils/helper'
|
||||
import TriggerForm from './triggerForm.vue'
|
||||
export default {
|
||||
name: 'AttributeCard',
|
||||
components: {
|
||||
ValueTypeIcon,
|
||||
TriggerForm,
|
||||
ops_default_show,
|
||||
ops_is_choice,
|
||||
ops_is_index,
|
||||
ops_is_link,
|
||||
ops_is_password,
|
||||
ops_is_sortable,
|
||||
ops_is_unique,
|
||||
},
|
||||
props: {
|
||||
property: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
CITypeId: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
isStore: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const propertyList = [
|
||||
{
|
||||
label: '是否唯一',
|
||||
property: 'is_unique',
|
||||
},
|
||||
{
|
||||
label: '是否选择',
|
||||
property: 'is_choice',
|
||||
},
|
||||
{
|
||||
label: '默认显示',
|
||||
property: 'default_show',
|
||||
},
|
||||
{
|
||||
label: '可排序',
|
||||
property: 'is_sortable',
|
||||
},
|
||||
{
|
||||
label: '是否索引',
|
||||
property: 'is_index',
|
||||
},
|
||||
]
|
||||
return {
|
||||
valueTypeMap,
|
||||
propertyList,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getPropertyStyle,
|
||||
handleEdit() {
|
||||
this.$emit('edit')
|
||||
},
|
||||
handleDelete() {
|
||||
const that = this
|
||||
this.$confirm({
|
||||
title: '警告',
|
||||
content: `确认删除 【${that.property.alias || that.property.name}】?`,
|
||||
onOk() {
|
||||
if (that.isStore) {
|
||||
deleteAttributesById(that.property.id).then(() => {
|
||||
that.$message.success('删除成功!')
|
||||
that.$emit('ok')
|
||||
})
|
||||
} else {
|
||||
deleteCITypeAttributesById(that.CITypeId, { attr_id: [that.property.id] }).then(() => {
|
||||
that.$message.success('删除成功!')
|
||||
that.$emit('ok')
|
||||
})
|
||||
}
|
||||
},
|
||||
onCancel() {},
|
||||
})
|
||||
},
|
||||
openTrigger() {
|
||||
this.$refs.triggerForm.open(this.property)
|
||||
},
|
||||
handleCalcComputed() {
|
||||
const that = this
|
||||
this.$confirm({
|
||||
title: '警告',
|
||||
content: `确认触发所有CI的计算?`,
|
||||
onOk() {
|
||||
calcComputedAttribute(that.property.id).then(() => {
|
||||
that.$message.success('触发成功!')
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.attribute-card {
|
||||
width: 182px;
|
||||
height: 80px;
|
||||
background: #f8faff;
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
margin-bottom: 16px;
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
box-shadow: 0 4px 12px #4e5ea066;
|
||||
.attribute-card-operation {
|
||||
visibility: visible !important;
|
||||
}
|
||||
}
|
||||
.attribute-card-content {
|
||||
height: 50px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
width: 100%;
|
||||
.attribute-card-value-type-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 12px;
|
||||
cursor: move;
|
||||
background: #ffffff !important;
|
||||
box-shadow: 0px 1px 2px rgba(47, 84, 235, 0.2);
|
||||
border-radius: 2px;
|
||||
text-align: center;
|
||||
line-height: 32px;
|
||||
}
|
||||
.attribute-card-content-inner {
|
||||
padding-left: 12px;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
width: 120px;
|
||||
position: relative;
|
||||
.attribute-card-name {
|
||||
width: 100%;
|
||||
color: rgba(0, 0, 0, 0.8);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.attribute-card-name-default-show {
|
||||
color: #2f54eb;
|
||||
}
|
||||
.attribute-card_value-type {
|
||||
font-size: 10px;
|
||||
color: rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
}
|
||||
.attribute-card-name-required::before {
|
||||
content: '*';
|
||||
width: 5px;
|
||||
color: red;
|
||||
position: absolute;
|
||||
left: 3px;
|
||||
}
|
||||
.attribute-card-trigger {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 8px;
|
||||
}
|
||||
}
|
||||
.attribute-card-footer {
|
||||
width: 182px;
|
||||
height: 30px;
|
||||
padding: 0 8px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: linear-gradient(180deg, #96abd6 0%, #ecf2ff 0.01%, #ffffff 143.33%);
|
||||
border-radius: 0px 0px 5px 5px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
.attribute-card-operation {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="less">
|
||||
.attribute-card-footer-popover {
|
||||
.ant-popover-inner-content {
|
||||
padding: 0;
|
||||
}
|
||||
.ant-descriptions-bordered .ant-descriptions-item-label {
|
||||
background-color: #f8faff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -84,7 +84,12 @@
|
||||
</a-input-number>
|
||||
<a-input
|
||||
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 }] }]"
|
||||
>
|
||||
</a-input>
|
||||
@@ -148,13 +153,13 @@
|
||||
label="必须"
|
||||
>
|
||||
<a-switch
|
||||
@change="onChange"
|
||||
@change="(checked) => onChange(checked, 'is_required')"
|
||||
name="is_required"
|
||||
v-decorator="['is_required', { rules: [], valuePropName: 'checked' }]"
|
||||
/>
|
||||
</a-form-item>
|
||||
</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-switch
|
||||
:disabled="isShowComputedArea"
|
||||
@@ -228,7 +233,7 @@
|
||||
/>
|
||||
</a-form-item>
|
||||
</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="currentValueType === '2' ? horizontalFormItemLayout.labelCol : { span: 8 }"
|
||||
:wrapper-col="horizontalFormItemLayout.wrapperCol"
|
||||
@@ -236,14 +241,14 @@
|
||||
>
|
||||
<a-switch
|
||||
:disabled="isShowComputedArea"
|
||||
@change="onChange"
|
||||
@change="(checked) => onChange(checked, 'is_sortable')"
|
||||
name="is_sortable"
|
||||
v-decorator="['is_sortable', { rules: [], valuePropName: 'checked' }]"
|
||||
/>
|
||||
</a-form-item>
|
||||
</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="currentValueType === '2' ? { span: 8 } : horizontalFormItemLayout.labelCol"
|
||||
:wrapper-col="horizontalFormItemLayout.wrapperCol"
|
||||
@@ -275,31 +280,6 @@
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="6" v-if="currentValueType === '2'">
|
||||
<a-form-item
|
||||
:label-col="horizontalFormItemLayout.labelCol"
|
||||
:wrapper-col="horizontalFormItemLayout.wrapperCol"
|
||||
label="密码"
|
||||
>
|
||||
<a-switch
|
||||
:disabled="isShowComputedArea"
|
||||
@change="(checked) => onChange(checked, 'is_password')"
|
||||
name="is_password"
|
||||
v-decorator="['is_password', { rules: [], valuePropName: 'checked' }]"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="6" v-if="currentValueType === '2'">
|
||||
<a-form-item :label-col="{ span: 8 }" :wrapper-col="horizontalFormItemLayout.wrapperCol" label="链接">
|
||||
<a-switch
|
||||
:disabled="isShowComputedArea"
|
||||
@change="(checked) => onChange(checked, 'is_link')"
|
||||
name="is_link"
|
||||
v-decorator="['is_link', { rules: [], valuePropName: 'checked' }]"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
|
||||
<a-divider style="font-size:14px;margin-top:6px;">高级设置</a-divider>
|
||||
<a-row>
|
||||
<a-col :span="24">
|
||||
@@ -307,12 +287,17 @@
|
||||
<FontArea ref="fontArea" />
|
||||
</a-form-item>
|
||||
</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="预定义值">
|
||||
<PreValueArea v-if="drawerVisible" ref="preValueArea" :disabled="isShowComputedArea" />
|
||||
<PreValueArea
|
||||
v-if="drawerVisible"
|
||||
:canDefineScript="canDefineScript"
|
||||
ref="preValueArea"
|
||||
:disabled="isShowComputedArea"
|
||||
/>
|
||||
</a-form-item>
|
||||
</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 }">
|
||||
<template slot="label">
|
||||
<span
|
||||
@@ -340,7 +325,7 @@
|
||||
<a-switch
|
||||
:disabled="!canDefineComputed"
|
||||
@change="(checked) => onChange(checked, 'is_computed')"
|
||||
name="is_password"
|
||||
name="is_computed"
|
||||
v-decorator="['is_computed', { rules: [], valuePropName: 'checked' }]"
|
||||
/>
|
||||
<ComputedArea
|
||||
@@ -366,6 +351,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import moment from 'moment'
|
||||
import vueJsonEditor from 'vue-json-editor'
|
||||
import {
|
||||
@@ -434,6 +420,9 @@ export default {
|
||||
wrapperCol: { span: 4 },
|
||||
}
|
||||
},
|
||||
canDefineScript() {
|
||||
return this.canDefineComputed
|
||||
},
|
||||
},
|
||||
mounted() {},
|
||||
methods: {
|
||||
@@ -463,28 +452,30 @@ export default {
|
||||
})
|
||||
if (this.currentValueType === '2') {
|
||||
this.form.setFieldsValue({
|
||||
is_password: false,
|
||||
is_link: false,
|
||||
is_index: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if (checked && property === 'is_password') {
|
||||
this.form.setFieldsValue({
|
||||
is_link: false,
|
||||
})
|
||||
}
|
||||
if (checked && property === 'is_link') {
|
||||
this.form.setFieldsValue({
|
||||
is_password: false,
|
||||
})
|
||||
}
|
||||
if (property === 'is_list') {
|
||||
this.form.setFieldsValue({
|
||||
default_value: checked ? [] : '',
|
||||
})
|
||||
}
|
||||
if (checked && property === 'is_sortable') {
|
||||
this.$message.warning('选中排序,则必须也要选中!')
|
||||
this.form.setFieldsValue({
|
||||
is_required: true,
|
||||
})
|
||||
}
|
||||
if (!checked && property === 'is_required' && this.form.getFieldValue('is_sortable')) {
|
||||
this.$message.warning('选中排序,则必须也要选中!')
|
||||
this.$nextTick(() => {
|
||||
this.form.setFieldsValue({
|
||||
is_required: true,
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
async handleEdit(record, attributes) {
|
||||
@@ -494,90 +485,91 @@ export default {
|
||||
} catch {
|
||||
this.canDefineComputed = false
|
||||
}
|
||||
const _record = _.cloneDeep(record)
|
||||
if (_record.is_password) {
|
||||
_record.value_type = '7'
|
||||
}
|
||||
if (_record.is_link) {
|
||||
_record.value_type = '8'
|
||||
}
|
||||
this.drawerTitle = '编辑属性'
|
||||
this.drawerVisible = true
|
||||
this.record = record
|
||||
this.currentValueType = record.value_type
|
||||
this.record = _record
|
||||
this.currentValueType = _record.value_type
|
||||
this.$nextTick(() => {
|
||||
this.form.setFieldsValue({
|
||||
id: record.id,
|
||||
alias: record.alias,
|
||||
name: record.name,
|
||||
value_type: record.value_type,
|
||||
is_required: record.is_required,
|
||||
default_show: record.default_show,
|
||||
id: _record.id,
|
||||
alias: _record.alias,
|
||||
name: _record.name,
|
||||
value_type: _record.value_type,
|
||||
is_required: _record.is_required,
|
||||
default_show: _record.default_show,
|
||||
})
|
||||
if (record.value_type !== '6') {
|
||||
if (!['6', '7'].includes(_record.value_type)) {
|
||||
this.form.setFieldsValue({
|
||||
is_list: record.is_list,
|
||||
is_unique: record.is_unique,
|
||||
is_index: record.is_index,
|
||||
is_sortable: record.is_sortable,
|
||||
is_computed: record.is_computed,
|
||||
is_list: _record.is_list,
|
||||
is_unique: _record.is_unique,
|
||||
is_index: _record.is_index,
|
||||
is_sortable: _record.is_sortable,
|
||||
is_computed: _record.is_computed,
|
||||
})
|
||||
}
|
||||
if (record.value_type === '2') {
|
||||
this.form.setFieldsValue({
|
||||
is_password: record.is_password,
|
||||
is_link: record.is_link,
|
||||
})
|
||||
}
|
||||
console.log(record)
|
||||
if (record.default) {
|
||||
console.log(_record)
|
||||
if (_record.default) {
|
||||
this.$nextTick(() => {
|
||||
if (record.value_type === '0') {
|
||||
if (_record.value_type === '0') {
|
||||
this.form.setFieldsValue({
|
||||
default_value: record.default.default ? [record.default.default] : [],
|
||||
default_value: _record.default.default ? [_record.default.default] : [],
|
||||
})
|
||||
} else if (record.value_type === '6') {
|
||||
this.default_value_json = record?.default?.default || null
|
||||
} else if (record.value_type === '3' || record.value_type === '4') {
|
||||
if (record?.default?.default === '$created_at' || record?.default?.default === '$updated_at') {
|
||||
this.defaultForDatetime = record.default.default
|
||||
} else if (_record.value_type === '6') {
|
||||
this.default_value_json = _record?.default?.default || null
|
||||
} else if (_record.value_type === '3' || _record.value_type === '4') {
|
||||
if (_record?.default?.default === '$created_at' || _record?.default?.default === '$updated_at') {
|
||||
this.defaultForDatetime = _record.default.default
|
||||
this.form.setFieldsValue({
|
||||
default_value: record?.default?.default,
|
||||
default_value: _record?.default?.default,
|
||||
})
|
||||
} else {
|
||||
this.defaultForDatetime = '$custom_time'
|
||||
this.form.setFieldsValue({
|
||||
default_value: record.default && record.default.default ? moment(record.default.default) : null,
|
||||
default_value: _record.default && _record.default.default ? moment(_record.default.default) : null,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
this.$nextTick(() => {
|
||||
this.form.setFieldsValue({
|
||||
default_value: record.default && record.default.default ? record.default.default : null,
|
||||
default_value: _record.default && _record.default.default ? _record.default.default : null,
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.default_value_json = {}
|
||||
if (record.value_type === '0') {
|
||||
if (_record.value_type === '0') {
|
||||
this.form.setFieldsValue({
|
||||
default_value: [],
|
||||
})
|
||||
} else if (record.value_type !== '6') {
|
||||
} else if (_record.value_type !== '6') {
|
||||
this.form.setFieldsValue({
|
||||
default_value: null,
|
||||
})
|
||||
}
|
||||
}
|
||||
this.isShowComputedArea = record.is_computed
|
||||
if (record.is_computed) {
|
||||
this.isShowComputedArea = _record.is_computed
|
||||
if (_record.is_computed) {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.computedArea.setData({
|
||||
compute_expr: record.compute_expr,
|
||||
compute_script: record.compute_script,
|
||||
compute_expr: _record.compute_expr,
|
||||
compute_script: _record.compute_script,
|
||||
})
|
||||
})
|
||||
}
|
||||
const _find = attributes.find((item) => item.id === record.id)
|
||||
if (record.value_type !== '6') {
|
||||
const _find = attributes.find((item) => item.id === _record.id)
|
||||
if (!['6', '7'].includes(_record.value_type)) {
|
||||
this.$refs.preValueArea.setData({
|
||||
choice_value: (_find || {}).choice_value || [],
|
||||
choice_web_hook: record.choice_web_hook,
|
||||
choice_other: record.choice_other || undefined,
|
||||
choice_web_hook: _record.choice_web_hook,
|
||||
choice_other: _record.choice_other || undefined,
|
||||
})
|
||||
}
|
||||
this.$refs.fontArea.setData({
|
||||
@@ -633,13 +625,20 @@ export default {
|
||||
values = { ...values, ...computedAreaData }
|
||||
} else {
|
||||
// 如果是非计算属性,就看看有没有预定义值
|
||||
if (values.value_type !== '6') {
|
||||
if (!['6', '7'].includes(values.value_type)) {
|
||||
const preValueAreaData = this.$refs.preValueArea.getData()
|
||||
values = { ...values, ...preValueAreaData }
|
||||
}
|
||||
}
|
||||
|
||||
const fontOptions = this.$refs.fontArea.getData()
|
||||
if (values.value_type === '7') {
|
||||
values.value_type = '2'
|
||||
values.is_password = true
|
||||
}
|
||||
if (values.value_type === '8') {
|
||||
values.value_type = '2'
|
||||
values.is_link = true
|
||||
}
|
||||
if (values.id) {
|
||||
await this.updateAttribute(values.id, { ...values, option: { fontOptions } }, isCalcComputed)
|
||||
} else {
|
||||
@@ -660,7 +659,6 @@ export default {
|
||||
handleOk() {
|
||||
this.$emit('ok')
|
||||
},
|
||||
|
||||
handleChangeValueType(value) {
|
||||
this.currentValueType = value
|
||||
this.$nextTick(() => {
|
||||
|
@@ -1,9 +1,14 @@
|
||||
<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-row>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label-col="formItemLayout.labelCol" :wrapper-col="formItemLayout.wrapperCol" label="属性名(英文)">
|
||||
<a-form-item label="属性名(英文)">
|
||||
<a-input
|
||||
name="name"
|
||||
placeholder="英文"
|
||||
@@ -24,28 +29,33 @@
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<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-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label-col="formItemLayout.labelCol" :wrapper-col="formItemLayout.wrapperCol" label="数据类型">
|
||||
<a-form-item label="数据类型">
|
||||
<a-select
|
||||
name="value_type"
|
||||
style="width: 100%"
|
||||
v-decorator="['value_type', { rules: [{ required: true }], initialValue: '2' }]"
|
||||
@change="handleChangeValueType"
|
||||
>
|
||||
<a-select-option :value="key" :key="key" v-for="(value, key) in valueTypeMap">{{ value }}</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-form-item>
|
||||
</a-col>
|
||||
<a-col :span="currentValueType === '6' ? 24 : 12">
|
||||
<a-form-item
|
||||
:label-col="{ span: currentValueType === '6' ? 4 : 8 }"
|
||||
:wrapper-col="{ span: currentValueType === '6' ? 18 : 12 }"
|
||||
:wrapper-col="{ span: currentValueType === '6' ? 18 : 15 }"
|
||||
label="默认值"
|
||||
>
|
||||
<template>
|
||||
@@ -74,7 +84,12 @@
|
||||
</a-select>
|
||||
<a-input
|
||||
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 }] }]"
|
||||
>
|
||||
</a-input>
|
||||
@@ -140,13 +155,13 @@
|
||||
label="必须"
|
||||
>
|
||||
<a-switch
|
||||
@change="onChange"
|
||||
@change="(checked) => onChange(checked, 'is_required')"
|
||||
name="is_required"
|
||||
v-decorator="['is_required', { rules: [], valuePropName: 'checked' }]"
|
||||
/>
|
||||
</a-form-item>
|
||||
</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-switch
|
||||
:disabled="isShowComputedArea"
|
||||
@@ -216,7 +231,7 @@
|
||||
/>
|
||||
</a-form-item>
|
||||
</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="currentValueType === '2' ? horizontalFormItemLayout.labelCol : { span: 8 }"
|
||||
:wrapper-col="horizontalFormItemLayout.wrapperCol"
|
||||
@@ -224,14 +239,14 @@
|
||||
>
|
||||
<a-switch
|
||||
:disabled="isShowComputedArea"
|
||||
@change="onChange"
|
||||
@change="(checked) => onChange(checked, 'is_sortable')"
|
||||
name="is_sortable"
|
||||
v-decorator="['is_sortable', { rules: [], valuePropName: 'checked' }]"
|
||||
/>
|
||||
</a-form-item>
|
||||
</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="currentValueType === '2' ? { span: 8 } : horizontalFormItemLayout.labelCol"
|
||||
:wrapper-col="horizontalFormItemLayout.wrapperCol"
|
||||
@@ -263,30 +278,6 @@
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="6" v-if="currentValueType === '2'">
|
||||
<a-form-item
|
||||
:label-col="horizontalFormItemLayout.labelCol"
|
||||
:wrapper-col="horizontalFormItemLayout.wrapperCol"
|
||||
label="密码"
|
||||
>
|
||||
<a-switch
|
||||
:disabled="isShowComputedArea"
|
||||
@change="(checked) => onChange(checked, 'is_password')"
|
||||
name="is_password"
|
||||
v-decorator="['is_password', { rules: [], valuePropName: 'checked' }]"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="6" v-if="currentValueType === '2'">
|
||||
<a-form-item :label-col="{ span: 8 }" :wrapper-col="horizontalFormItemLayout.wrapperCol" label="链接">
|
||||
<a-switch
|
||||
:disabled="isShowComputedArea"
|
||||
@change="(checked) => onChange(checked, 'is_link')"
|
||||
name="is_link"
|
||||
v-decorator="['is_link', { rules: [], valuePropName: 'checked' }]"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-divider style="font-size:14px;margin-top:6px;">高级设置</a-divider>
|
||||
<a-row>
|
||||
<a-col :span="24">
|
||||
@@ -294,12 +285,12 @@
|
||||
<FontArea ref="fontArea" />
|
||||
</a-form-item>
|
||||
</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="预定义值">
|
||||
<PreValueArea ref="preValueArea" :disabled="isShowComputedArea" />
|
||||
<PreValueArea ref="preValueArea" :canDefineScript="canDefineScript" :disabled="isShowComputedArea" />
|
||||
</a-form-item>
|
||||
</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 }">
|
||||
<template slot="label">
|
||||
<span
|
||||
@@ -327,7 +318,7 @@
|
||||
<a-switch
|
||||
:disabled="!canDefineComputed"
|
||||
@change="(checked) => onChange(checked, 'is_computed')"
|
||||
name="is_password"
|
||||
name="is_computed"
|
||||
v-decorator="['is_computed', { rules: [], valuePropName: 'checked' }]"
|
||||
/>
|
||||
<ComputedArea ref="computedArea" v-if="isShowComputedArea" :canDefineComputed="canDefineComputed" />
|
||||
@@ -369,7 +360,7 @@ export default {
|
||||
valueTypeMap,
|
||||
formItemLayout: {
|
||||
labelCol: { span: 8 },
|
||||
wrapperCol: { span: 12 },
|
||||
wrapperCol: { span: 15 },
|
||||
},
|
||||
horizontalFormItemLayout: {
|
||||
labelCol: { span: 16 },
|
||||
@@ -386,6 +377,11 @@ export default {
|
||||
defaultForDatetime: '',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
canDefineScript() {
|
||||
return this.canDefineComputed
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleSubmit(isCloseModal = true) {
|
||||
this.form.validateFields(async (err, values) => {
|
||||
@@ -428,7 +424,7 @@ export default {
|
||||
values = { ...values, ...computedAreaData }
|
||||
} else {
|
||||
// 如果是非计算属性,就看看有没有预定义值
|
||||
if (values.value_type !== '6') {
|
||||
if (!['6', '7'].includes(values.value_type)) {
|
||||
const preValueAreaData = this.$refs.preValueArea.getData()
|
||||
values = { ...values, ...preValueAreaData }
|
||||
}
|
||||
@@ -437,17 +433,25 @@ export default {
|
||||
|
||||
// is_index进行操作,除了文本 索引隐藏掉 文本 索引默认是true
|
||||
// 框里的5种类型 is_index=true
|
||||
// json类型 is_index=false
|
||||
if (values.value_type === '6') {
|
||||
// json类型、密码、链接 is_index=false
|
||||
if (['6', '7', '8'].includes(values.value_type)) {
|
||||
values.is_index = false
|
||||
} else if (values.value_type !== '2') {
|
||||
values.is_index = true
|
||||
}
|
||||
if (values.value_type === '7') {
|
||||
values.value_type = '2'
|
||||
values.is_password = true
|
||||
}
|
||||
if (values.value_type === '8') {
|
||||
values.value_type = '2'
|
||||
values.is_link = true
|
||||
}
|
||||
const { attr_id } = await createAttribute({ ...values, option: { fontOptions } })
|
||||
|
||||
this.form.resetFields()
|
||||
this.currentValueType = '2'
|
||||
if (values.value_type !== '6') {
|
||||
if (!['6'].includes(values.value_type) && !values.is_password) {
|
||||
this.$refs.preValueArea.valueList = []
|
||||
}
|
||||
this.$emit('done', attr_id, data, isCloseModal)
|
||||
@@ -485,29 +489,27 @@ export default {
|
||||
is_index: false,
|
||||
is_sortable: false,
|
||||
})
|
||||
if (this.currentValueType === '2') {
|
||||
this.form.setFieldsValue({
|
||||
is_password: false,
|
||||
is_link: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if (checked && property === 'is_password') {
|
||||
this.form.setFieldsValue({
|
||||
is_link: false,
|
||||
})
|
||||
}
|
||||
if (checked && property === 'is_link') {
|
||||
this.form.setFieldsValue({
|
||||
is_password: false,
|
||||
})
|
||||
}
|
||||
if (property === 'is_list') {
|
||||
this.form.setFieldsValue({
|
||||
default_value: checked ? [] : '',
|
||||
})
|
||||
}
|
||||
if (checked && property === 'is_sortable') {
|
||||
this.$message.warning('选中排序,则必须也要选中!')
|
||||
this.form.setFieldsValue({
|
||||
is_required: true,
|
||||
})
|
||||
}
|
||||
if (!checked && property === 'is_required' && this.form.getFieldValue('is_sortable')) {
|
||||
this.$message.warning('选中排序,则必须也要选中!')
|
||||
this.$nextTick(() => {
|
||||
this.form.setFieldsValue({
|
||||
is_required: true,
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
onJsonChange(value) {
|
||||
this.default_value_json_right = true
|
||||
@@ -548,4 +550,8 @@ export default {
|
||||
background-color: #2f54eb;
|
||||
}
|
||||
}
|
||||
.value-type-des {
|
||||
font-size: 10px;
|
||||
color: #a9a9a9;
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,91 +1,90 @@
|
||||
<template>
|
||||
<a-tabs v-model="activeKey" size="small" :tabBarStyle="{ borderBottom: 'none' }">
|
||||
<a-tab-pane key="expr" :disabled="!canDefineComputed">
|
||||
<span style="font-size:12px;" slot="tab">表达式</span>
|
||||
<a-textarea v-model="compute_expr" :placeholder="`{{a}}+{{b}}`" :rows="2" :disabled="!canDefineComputed" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="script" :disabled="!canDefineComputed">
|
||||
<span style="font-size:12px;" slot="tab">代码</span>
|
||||
<codemirror style="z-index: 9999" :options="cmOptions" v-model="compute_script"></codemirror>
|
||||
</a-tab-pane>
|
||||
<template slot="tabBarExtraContent" v-if="showCalcComputed">
|
||||
<a-button type="primary" size="small" @click="handleCalcComputed">
|
||||
应用
|
||||
</a-button>
|
||||
<a-tooltip title="所有CI触发计算">
|
||||
<a-icon type="question-circle" style="margin-left:5px" />
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</a-tabs>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { codemirror } from 'vue-codemirror'
|
||||
import 'codemirror/lib/codemirror.css'
|
||||
import 'codemirror/theme/monokai.css'
|
||||
|
||||
require('codemirror/mode/python/python.js')
|
||||
export default {
|
||||
name: 'ComputedArea',
|
||||
components: { codemirror },
|
||||
props: {
|
||||
canDefineComputed: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showCalcComputed: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeKey: 'expr', // expr script
|
||||
compute_expr: '',
|
||||
compute_script: 'def computed(): \n return',
|
||||
cmOptions: {
|
||||
lineNumbers: true,
|
||||
mode: 'python',
|
||||
height: '200px',
|
||||
theme: 'monokai',
|
||||
smartIndent: true,
|
||||
tabSize: 4,
|
||||
lineWrapping: true,
|
||||
readOnly: !this.canDefineComputed,
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getData() {
|
||||
const { activeKey, compute_expr, compute_script } = this
|
||||
if (activeKey === 'expr') {
|
||||
return { compute_expr, compute_script: null }
|
||||
} else if (activeKey === 'script') {
|
||||
return { compute_script, compute_expr: null }
|
||||
}
|
||||
},
|
||||
setData(data) {
|
||||
const { compute_expr, compute_script } = data
|
||||
this.compute_expr = compute_expr
|
||||
this.compute_script = compute_script || 'def computed(): \n return'
|
||||
if (compute_script) {
|
||||
this.activeKey = 'script'
|
||||
} else {
|
||||
this.activeKey = 'expr'
|
||||
}
|
||||
},
|
||||
handleCalcComputed() {
|
||||
const that = this
|
||||
this.$confirm({
|
||||
title: '警告',
|
||||
content: `确认触发将保存当前配置及触发所有CI的计算?`,
|
||||
onOk() {
|
||||
that.$emit('handleCalcComputed')
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
<template>
|
||||
<a-tabs v-model="activeKey" size="small" :tabBarStyle="{ borderBottom: 'none' }">
|
||||
<a-tab-pane key="expr" :disabled="!canDefineComputed">
|
||||
<span style="font-size:12px;" slot="tab">表达式</span>
|
||||
<a-textarea v-model="compute_expr" :placeholder="`{{a}}+{{b}}`" :rows="2" :disabled="!canDefineComputed" />
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="script" :disabled="!canDefineComputed">
|
||||
<span style="font-size:12px;" slot="tab">代码</span>
|
||||
<codemirror style="z-index: 9999" :options="cmOptions" v-model="compute_script"></codemirror>
|
||||
</a-tab-pane>
|
||||
<template slot="tabBarExtraContent" v-if="showCalcComputed">
|
||||
<a-button type="primary" size="small" @click="handleCalcComputed">
|
||||
应用
|
||||
</a-button>
|
||||
<a-tooltip title="所有CI触发计算">
|
||||
<a-icon type="question-circle" style="margin-left:5px" />
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</a-tabs>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { codemirror } from 'vue-codemirror'
|
||||
import 'codemirror/lib/codemirror.css'
|
||||
import 'codemirror/theme/monokai.css'
|
||||
|
||||
require('codemirror/mode/python/python.js')
|
||||
export default {
|
||||
name: 'ComputedArea',
|
||||
components: { codemirror },
|
||||
props: {
|
||||
canDefineComputed: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showCalcComputed: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeKey: 'expr', // expr script
|
||||
compute_expr: '',
|
||||
compute_script: 'def computed(): \n return',
|
||||
cmOptions: {
|
||||
lineNumbers: true,
|
||||
mode: 'python',
|
||||
height: '200px',
|
||||
theme: 'monokai',
|
||||
tabSize: 4,
|
||||
lineWrapping: true,
|
||||
readOnly: !this.canDefineComputed,
|
||||
},
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getData() {
|
||||
const { activeKey, compute_expr, compute_script } = this
|
||||
if (activeKey === 'expr') {
|
||||
return { compute_expr, compute_script: null }
|
||||
} else if (activeKey === 'script') {
|
||||
return { compute_script, compute_expr: null }
|
||||
}
|
||||
},
|
||||
setData(data) {
|
||||
const { compute_expr, compute_script } = data
|
||||
this.compute_expr = compute_expr
|
||||
this.compute_script = compute_script || 'def computed(): \n return'
|
||||
if (compute_script) {
|
||||
this.activeKey = 'script'
|
||||
} else {
|
||||
this.activeKey = 'expr'
|
||||
}
|
||||
},
|
||||
handleCalcComputed() {
|
||||
const that = this
|
||||
this.$confirm({
|
||||
title: '警告',
|
||||
content: `确认触发将保存当前配置及触发所有CI的计算?`,
|
||||
onOk() {
|
||||
that.$emit('handleCalcComputed')
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,11 @@
|
||||
<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">
|
||||
<span style="font-size:12px;" slot="tab">定义</span>
|
||||
<PreValueTag type="add" :item="[]" @add="addNewValue" :disabled="disabled">
|
||||
@@ -170,6 +176,15 @@
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="script" :disabled="disabled || !canDefineScript">
|
||||
<span style="font-size:12px;" slot="tab">脚本</span>
|
||||
<CustomCodeMirror
|
||||
codeMirrorId="cmdb-pre-value"
|
||||
ref="codemirror"
|
||||
@changeCodeContent="changeCodeContent"
|
||||
></CustomCodeMirror>
|
||||
<!-- <codemirror style="z-index: 9999" :options="cmOptions" v-model="script"></codemirror> -->
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</template>
|
||||
|
||||
@@ -184,14 +199,22 @@ import { getCITypeGroups } from '../../api/ciTypeGroup'
|
||||
import { getCITypeCommonAttributesByTypeIds } from '../../api/CITypeAttr'
|
||||
import FilterComp from '@/components/CMDBFilterComp'
|
||||
|
||||
import CustomCodeMirror from '@/components/CustomCodeMirror'
|
||||
import 'codemirror/lib/codemirror.css'
|
||||
import 'codemirror/theme/monokai.css'
|
||||
require('codemirror/mode/python/python.js')
|
||||
export default {
|
||||
name: 'PreValueArea',
|
||||
components: { draggable, PreValueTag, ColorPicker, Webhook, FilterComp },
|
||||
components: { draggable, PreValueTag, ColorPicker, Webhook, FilterComp, CustomCodeMirror },
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
canDefineScript: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -208,6 +231,17 @@ export default {
|
||||
ciTypeGroup: [],
|
||||
typeAttrs: [],
|
||||
filterExp: '',
|
||||
script:
|
||||
'class ChoiceValue(object):\n @staticmethod\n def values():\n """\n 执行入口, 返回预定义值\n :return: 返回一个列表, 值的类型同属性的类型\n 例如:\n return ["在线", "下线"]\n """\n return []',
|
||||
cmOptions: {
|
||||
lineNumbers: true,
|
||||
mode: 'python',
|
||||
height: '200px',
|
||||
theme: 'monokai',
|
||||
tabSize: 4,
|
||||
lineWrapping: true,
|
||||
readOnly: this.disabled || !this.canDefineScript,
|
||||
},
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -281,6 +315,14 @@ export default {
|
||||
const choice_web_hook = this.$refs.webhook.getParams()
|
||||
choice_web_hook.ret_key = this.form.ret_key
|
||||
return { choice_value: [], choice_web_hook, choice_other: null }
|
||||
} else if (this.activeKey === 'script') {
|
||||
return {
|
||||
choice_value: [],
|
||||
choice_web_hook: null,
|
||||
choice_other: {
|
||||
script: this.script,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
let choice_other = {}
|
||||
if (this.choice_other.type_ids && this.choice_other.type_ids.length) {
|
||||
@@ -302,14 +344,22 @@ export default {
|
||||
this.form.ret_key = choice_web_hook.ret_key ?? ''
|
||||
})
|
||||
} else if (choice_other) {
|
||||
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) {
|
||||
if (choice_other.script) {
|
||||
this.activeKey = 'script'
|
||||
this.script = choice_other.script
|
||||
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 {
|
||||
this.valueList = choice_value
|
||||
@@ -330,6 +380,16 @@ export default {
|
||||
this.filterExp = ''
|
||||
}
|
||||
},
|
||||
changeCodeContent(value) {
|
||||
this.script = value && value.replace('\t', ' ')
|
||||
},
|
||||
changeActiveKey(value) {
|
||||
if (value === 'script') {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.codemirror.initCodeMirror(this.script)
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -24,7 +24,7 @@ export const category_1_bar_options = (data, options) => {
|
||||
})
|
||||
return {
|
||||
|
||||
color: options.chartColor.split(','),
|
||||
color: (options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD').split(','),
|
||||
grid: {
|
||||
top: 15,
|
||||
left: 'left',
|
||||
@@ -83,7 +83,7 @@ export const category_1_bar_options = (data, options) => {
|
||||
export const category_1_line_options = (data, options) => {
|
||||
const xData = Object.keys(data)
|
||||
return {
|
||||
color: options.chartColor.split(','),
|
||||
color: (options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD').split(','),
|
||||
grid: {
|
||||
top: 15,
|
||||
left: 'left',
|
||||
@@ -117,7 +117,7 @@ export const category_1_line_options = (data, options) => {
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [{
|
||||
offset: 0, color: options.chartColor.split(',')[0] // 0% 处的颜色
|
||||
offset: 0, color: (options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD').split(',')[0] // 0% 处的颜色
|
||||
}, {
|
||||
offset: 1, color: '#ffffff' // 100% 处的颜色
|
||||
}],
|
||||
@@ -131,7 +131,7 @@ export const category_1_line_options = (data, options) => {
|
||||
|
||||
export const category_1_pie_options = (data, options) => {
|
||||
return {
|
||||
color: options.chartColor.split(','),
|
||||
color: (options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD').split(','),
|
||||
grid: {
|
||||
top: 10,
|
||||
left: 'left',
|
||||
@@ -181,7 +181,7 @@ export const category_2_bar_options = (data, options, chartType) => {
|
||||
})
|
||||
const legend = [...new Set(_legend)]
|
||||
return {
|
||||
color: options.chartColor.split(','),
|
||||
color: (options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD').split(','),
|
||||
grid: {
|
||||
top: 15,
|
||||
left: 'left',
|
||||
@@ -249,7 +249,7 @@ export const category_2_bar_options = (data, options, chartType) => {
|
||||
x2: 0,
|
||||
y2: 1,
|
||||
colorStops: [{
|
||||
offset: 0, color: options.chartColor.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% 处的颜色
|
||||
}],
|
||||
@@ -269,7 +269,7 @@ export const category_2_pie_options = (data, options) => {
|
||||
})
|
||||
})
|
||||
return {
|
||||
color: options.chartColor.split(','),
|
||||
color: (options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD').split(','),
|
||||
grid: {
|
||||
top: 15,
|
||||
left: 'left',
|
||||
|
@@ -1,345 +1,344 @@
|
||||
<template>
|
||||
<CustomDrawer width="800px" :title="title" :visible="visible" @close="handleClose">
|
||||
<template v-if="adType === 'agent'">
|
||||
<a-form-model
|
||||
ref="autoDiscoveryForm"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:label-col="{ span: 2 }"
|
||||
:wrapper-col="{ span: 20 }"
|
||||
>
|
||||
<a-divider :style="{ margin: '5px 0' }">基础设置</a-divider>
|
||||
<a-form-model-item label="名称" prop="name">
|
||||
<a-input v-model="form.name" />
|
||||
</a-form-model-item>
|
||||
<a-form-model-item label="图标" v-if="is_inner">
|
||||
<CustomIconSelect v-model="customIcon" :style="{ marginTop: '6px' }" />
|
||||
</a-form-model-item>
|
||||
<a-form-model-item label="模式" prop="is_plugin">
|
||||
<a-radio-group v-model="form.is_plugin" @change="changeIsPlugin" :disabled="!is_inner">
|
||||
<a-radio :value="false">默认</a-radio>
|
||||
<a-radio :value="true">plugin</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-model-item>
|
||||
</a-form-model>
|
||||
<a-divider :style="{ margin: '5px 0' }">采集设置</a-divider>
|
||||
<CustomCodeMirror
|
||||
codeMirrorId="cmdb-adt"
|
||||
v-if="form.is_plugin"
|
||||
ref="codemirror"
|
||||
@changeCodeContent="changeCodeContent"
|
||||
></CustomCodeMirror>
|
||||
<div style="margin:10px 0;text-align:right;">
|
||||
<a-button
|
||||
v-show="form.is_plugin"
|
||||
size="small"
|
||||
type="primary"
|
||||
ghost
|
||||
@click="handleSubmit(true)"
|
||||
>更新字段</a-button
|
||||
>
|
||||
</div>
|
||||
<a-button
|
||||
v-show="!form.is_plugin"
|
||||
size="small"
|
||||
type="primary"
|
||||
ghost
|
||||
icon="plus"
|
||||
:style="{ marginBottom: '10px' }"
|
||||
@click="insertEvent(-1)"
|
||||
>新增</a-button
|
||||
>
|
||||
<vxe-table
|
||||
size="mini"
|
||||
stripe
|
||||
class="ops-stripe-table"
|
||||
show-overflow
|
||||
keep-source
|
||||
ref="xTable"
|
||||
max-height="400"
|
||||
:data="tableData"
|
||||
:edit-config="{ trigger: 'manual', mode: 'row' }"
|
||||
>
|
||||
<vxe-column field="name" title="名称" :edit-render="{ autofocus: '.vxe-input--inner' }">
|
||||
<template #edit="{ row }">
|
||||
<vxe-input v-model="row.name" type="text"></vxe-input>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="type" title="类型" :edit-render="{}">
|
||||
<template #edit="{ row }">
|
||||
<vxe-select v-model="row.type" transfer>
|
||||
<vxe-option v-for="item in typeList" :key="item" :value="item" :label="item"></vxe-option>
|
||||
</vxe-select>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="desc" title="描述" :edit-render="{ autofocus: '.vxe-input--inner' }">
|
||||
<template #edit="{ row }">
|
||||
<vxe-input v-model="row.desc" type="text"></vxe-input>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column title="操作" width="60" v-if="!form.is_plugin">
|
||||
<template #default="{ row }">
|
||||
<a-space v-if="$refs.xTable.isActiveByRow(row)">
|
||||
<a @click="saveRowEvent(row)"><a-icon type="save"/></a>
|
||||
<a @click="cancelRowEvent(row)"><a-icon type="close"/></a>
|
||||
</a-space>
|
||||
<a-space v-else>
|
||||
<a @click="editRowEvent(row)"><a-icon type="edit"/></a>
|
||||
<a :style="{ color: 'red' }" @click="deleteRowEvent(row)"><a-icon type="delete"/></a>
|
||||
</a-space>
|
||||
</template>
|
||||
</vxe-column>
|
||||
</vxe-table>
|
||||
|
||||
<div class="custom-drawer-bottom-action">
|
||||
<a-button @click="handleClose">取消</a-button>
|
||||
<a-button @click="handleSubmit(false)" type="primary">保存</a-button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<HttpSnmpAD ref="httpSnmpAd" :ruleType="adType" :ruleName="ruleData.name" />
|
||||
</template>
|
||||
</CustomDrawer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CustomIconSelect from '@/components/CustomIconSelect'
|
||||
import { postDiscovery, putDiscovery } from '../../api/discovery'
|
||||
import HttpSnmpAD from '../../components/httpSnmpAD'
|
||||
import CustomCodeMirror from '@/components/CustomCodeMirror'
|
||||
import 'codemirror/lib/codemirror.css'
|
||||
import 'codemirror/theme/monokai.css'
|
||||
export default {
|
||||
name: 'EditDrawer',
|
||||
components: { CustomIconSelect, CustomCodeMirror, HttpSnmpAD },
|
||||
props: {
|
||||
is_inner: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const default_plugin_script = `# -*- coding:utf-8 -*-
|
||||
|
||||
import json
|
||||
|
||||
|
||||
class AutoDiscovery(object):
|
||||
|
||||
@property
|
||||
def unique_key(self):
|
||||
"""
|
||||
|
||||
:return: 返回唯一属性的名字
|
||||
"""
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
def attributes():
|
||||
"""
|
||||
定义属性字段
|
||||
:return: 返回属性字段列表, 列表项是(名称, 类型, 描述), 名称必须是英文
|
||||
类型: String Integer Float Date DateTime Time JSON
|
||||
例如:
|
||||
return [
|
||||
("ci_type", "String", "模型名称"),
|
||||
("private_ip", "String", "内网IP, 多值逗号分隔")
|
||||
]
|
||||
"""
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def run():
|
||||
"""
|
||||
执行入口, 返回采集的属性值
|
||||
:return: 返回一个列表, 列表项是字典, 字典key是属性名称, value是属性值
|
||||
例如:
|
||||
return [dict(ci_type="server", private_ip="192.168.1.1")]
|
||||
"""
|
||||
return []
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
result = AutoDiscovery().run()
|
||||
if isinstance(result, list):
|
||||
print("AutoDiscovery::Result::{}".format(json.dumps(result)))
|
||||
else:
|
||||
print("ERROR: 采集返回必须是列表")
|
||||
`
|
||||
const typeList = ['String', 'Integer', 'Float', 'Date', 'DateTime', 'Time', 'JSON']
|
||||
return {
|
||||
default_plugin_script,
|
||||
typeList,
|
||||
visible: false,
|
||||
ruleData: {},
|
||||
type: 'add',
|
||||
adType: '',
|
||||
form: { name: '', is_plugin: false },
|
||||
rules: {},
|
||||
customIcon: { name: '', color: '' },
|
||||
tableData: [],
|
||||
editDefaultTableData: [],
|
||||
plugin_script: '',
|
||||
cmOptions: {
|
||||
lineNumbers: true,
|
||||
mode: 'python',
|
||||
height: '200px',
|
||||
theme: 'monokai',
|
||||
smartIndent: true,
|
||||
tabSize: 4,
|
||||
lineWrapping: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
if (this.adType === 'http' || this.adType === 'snmp') {
|
||||
return this.ruleData.name
|
||||
}
|
||||
if (this.type === 'edit') {
|
||||
return `编辑:${this.ruleData.name}`
|
||||
}
|
||||
return '新建'
|
||||
},
|
||||
},
|
||||
inject: {
|
||||
getDiscovery: {
|
||||
from: 'getDiscovery',
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
open(data, type, adType) {
|
||||
this.visible = true
|
||||
this.type = type
|
||||
this.ruleData = data
|
||||
this.adType = adType
|
||||
if (!this.is_inner) {
|
||||
this.form = {
|
||||
name: '',
|
||||
is_plugin: true,
|
||||
}
|
||||
}
|
||||
if (adType === 'http' || adType === 'snmp') {
|
||||
return
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
if (this.type === 'edit') {
|
||||
this.form = {
|
||||
name: data.name,
|
||||
is_plugin: data.is_plugin,
|
||||
}
|
||||
this.customIcon = data?.option?.icon ?? { name: 'caise-chajian', color: '' }
|
||||
this.tableData = data?.attributes ?? []
|
||||
this.editDefaultTableData = data?.attributes ?? []
|
||||
this.plugin_script = data?.plugin_script ?? this.default_plugin_script
|
||||
}
|
||||
if (this.type === 'add') {
|
||||
this.customIcon = { name: 'caise-chajian', color: '' }
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
this.plugin_script = this.default_plugin_script
|
||||
}
|
||||
if (data?.is_plugin || !this.is_inner) {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.codemirror.initCodeMirror(this.plugin_script)
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
handleClose() {
|
||||
this.tableData = []
|
||||
this.customIcon = { name: '', color: '' }
|
||||
this.form = { name: '', is_plugin: false }
|
||||
if (this.adType === 'agent') {
|
||||
this.$refs.autoDiscoveryForm.clearValidate()
|
||||
} else {
|
||||
// this.$refs.httpSnmpAd.currentCate = ''
|
||||
}
|
||||
this.visible = false
|
||||
},
|
||||
async insertEvent(row) {
|
||||
const $table = this.$refs.xTable
|
||||
const record = {}
|
||||
const { row: newRow } = await $table.insertAt(record, row)
|
||||
await $table.setEditRow(newRow)
|
||||
},
|
||||
editRowEvent(row) {
|
||||
const $table = this.$refs.xTable
|
||||
$table.setActiveRow(row)
|
||||
},
|
||||
saveRowEvent() {
|
||||
const $table = this.$refs.xTable
|
||||
$table.clearActived().then(() => {
|
||||
this.loading = true
|
||||
setTimeout(() => {
|
||||
this.loading = false
|
||||
}, 300)
|
||||
})
|
||||
},
|
||||
cancelRowEvent(row) {
|
||||
const $table = this.$refs.xTable
|
||||
$table.clearActived().then(() => {
|
||||
// 还原行数据
|
||||
$table.revertData(row)
|
||||
})
|
||||
},
|
||||
deleteRowEvent(row) {
|
||||
const $table = this.$refs.xTable
|
||||
$table.remove(row)
|
||||
},
|
||||
async handleSubmit(isUpdateAttr = false) {
|
||||
const $table = this.$refs.xTable
|
||||
const { fullData: _tableData } = $table.getTableData()
|
||||
console.log(_tableData)
|
||||
const params = {
|
||||
...this.form,
|
||||
type: this.adType,
|
||||
is_inner: this.is_inner,
|
||||
option: { icon: this.customIcon },
|
||||
attributes: this.form.is_plugin
|
||||
? undefined
|
||||
: _tableData.map(({ name, alias, desc, type }) => {
|
||||
return { name, alias, desc, type }
|
||||
}),
|
||||
plugin_script: this.form.is_plugin ? this.plugin_script : undefined,
|
||||
}
|
||||
let res
|
||||
if (this.type === 'add') {
|
||||
res = await postDiscovery(params)
|
||||
} else {
|
||||
res = await putDiscovery(this.ruleData.id, params)
|
||||
}
|
||||
if (isUpdateAttr) {
|
||||
this.tableData = res.attributes
|
||||
this.type = 'edit'
|
||||
this.ruleData = res
|
||||
this.$message.success('更新成功!')
|
||||
if (this.is_inner) {
|
||||
this.getDiscovery()
|
||||
}
|
||||
return
|
||||
}
|
||||
this.handleClose()
|
||||
console.log(this.is_inner)
|
||||
if (this.is_inner) {
|
||||
this.$message.success('保存成功!')
|
||||
this.getDiscovery()
|
||||
} else {
|
||||
this.$emit('updateNotInner', res)
|
||||
}
|
||||
},
|
||||
changeIsPlugin(e) {
|
||||
if (e.target.value) {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.codemirror.initCodeMirror(this.plugin_script)
|
||||
})
|
||||
}
|
||||
},
|
||||
changeCodeContent(value) {
|
||||
this.plugin_script = value && value.replace('\t', ' ')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
<template>
|
||||
<CustomDrawer width="800px" :title="title" :visible="visible" @close="handleClose">
|
||||
<template v-if="adType === 'agent'">
|
||||
<a-form-model
|
||||
ref="autoDiscoveryForm"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:label-col="{ span: 2 }"
|
||||
:wrapper-col="{ span: 20 }"
|
||||
>
|
||||
<a-divider :style="{ margin: '5px 0' }">基础设置</a-divider>
|
||||
<a-form-model-item label="名称" prop="name">
|
||||
<a-input v-model="form.name" />
|
||||
</a-form-model-item>
|
||||
<a-form-model-item label="图标" v-if="is_inner">
|
||||
<CustomIconSelect v-model="customIcon" :style="{ marginTop: '6px' }" />
|
||||
</a-form-model-item>
|
||||
<a-form-model-item label="模式" prop="is_plugin">
|
||||
<a-radio-group v-model="form.is_plugin" @change="changeIsPlugin" :disabled="!is_inner">
|
||||
<a-radio :value="false">默认</a-radio>
|
||||
<a-radio :value="true">plugin</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-model-item>
|
||||
</a-form-model>
|
||||
<a-divider :style="{ margin: '5px 0' }">采集设置</a-divider>
|
||||
<CustomCodeMirror
|
||||
codeMirrorId="cmdb-adt"
|
||||
v-if="form.is_plugin"
|
||||
ref="codemirror"
|
||||
@changeCodeContent="changeCodeContent"
|
||||
></CustomCodeMirror>
|
||||
<div style="margin:10px 0;text-align:right;">
|
||||
<a-button
|
||||
v-show="form.is_plugin"
|
||||
size="small"
|
||||
type="primary"
|
||||
ghost
|
||||
@click="handleSubmit(true)"
|
||||
>更新字段</a-button
|
||||
>
|
||||
</div>
|
||||
<a-button
|
||||
v-show="!form.is_plugin"
|
||||
size="small"
|
||||
type="primary"
|
||||
ghost
|
||||
icon="plus"
|
||||
:style="{ marginBottom: '10px' }"
|
||||
@click="insertEvent(-1)"
|
||||
>新增</a-button
|
||||
>
|
||||
<vxe-table
|
||||
size="mini"
|
||||
stripe
|
||||
class="ops-stripe-table"
|
||||
show-overflow
|
||||
keep-source
|
||||
ref="xTable"
|
||||
max-height="400"
|
||||
:data="tableData"
|
||||
:edit-config="{ trigger: 'manual', mode: 'row' }"
|
||||
>
|
||||
<vxe-column field="name" title="名称" :edit-render="{ autofocus: '.vxe-input--inner' }">
|
||||
<template #edit="{ row }">
|
||||
<vxe-input v-model="row.name" type="text"></vxe-input>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="type" title="类型" :edit-render="{}">
|
||||
<template #edit="{ row }">
|
||||
<vxe-select v-model="row.type" transfer>
|
||||
<vxe-option v-for="item in typeList" :key="item" :value="item" :label="item"></vxe-option>
|
||||
</vxe-select>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="desc" title="描述" :edit-render="{ autofocus: '.vxe-input--inner' }">
|
||||
<template #edit="{ row }">
|
||||
<vxe-input v-model="row.desc" type="text"></vxe-input>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column title="操作" width="60" v-if="!form.is_plugin">
|
||||
<template #default="{ row }">
|
||||
<a-space v-if="$refs.xTable.isActiveByRow(row)">
|
||||
<a @click="saveRowEvent(row)"><a-icon type="save"/></a>
|
||||
<a @click="cancelRowEvent(row)"><a-icon type="close"/></a>
|
||||
</a-space>
|
||||
<a-space v-else>
|
||||
<a @click="editRowEvent(row)"><a-icon type="edit"/></a>
|
||||
<a :style="{ color: 'red' }" @click="deleteRowEvent(row)"><a-icon type="delete"/></a>
|
||||
</a-space>
|
||||
</template>
|
||||
</vxe-column>
|
||||
</vxe-table>
|
||||
|
||||
<div class="custom-drawer-bottom-action">
|
||||
<a-button @click="handleClose">取消</a-button>
|
||||
<a-button @click="handleSubmit(false)" type="primary">保存</a-button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<HttpSnmpAD ref="httpSnmpAd" :ruleType="adType" :ruleName="ruleData.name" />
|
||||
</template>
|
||||
</CustomDrawer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CustomIconSelect from '@/components/CustomIconSelect'
|
||||
import { postDiscovery, putDiscovery } from '../../api/discovery'
|
||||
import HttpSnmpAD from '../../components/httpSnmpAD'
|
||||
import CustomCodeMirror from '@/components/CustomCodeMirror'
|
||||
import 'codemirror/lib/codemirror.css'
|
||||
import 'codemirror/theme/monokai.css'
|
||||
export default {
|
||||
name: 'EditDrawer',
|
||||
components: { CustomIconSelect, CustomCodeMirror, HttpSnmpAD },
|
||||
props: {
|
||||
is_inner: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const default_plugin_script = `# -*- coding:utf-8 -*-
|
||||
|
||||
import json
|
||||
|
||||
|
||||
class AutoDiscovery(object):
|
||||
|
||||
@property
|
||||
def unique_key(self):
|
||||
"""
|
||||
|
||||
:return: 返回唯一属性的名字
|
||||
"""
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
def attributes():
|
||||
"""
|
||||
定义属性字段
|
||||
:return: 返回属性字段列表, 列表项是(名称, 类型, 描述), 名称必须是英文
|
||||
类型: String Integer Float Date DateTime Time JSON
|
||||
例如:
|
||||
return [
|
||||
("ci_type", "String", "模型名称"),
|
||||
("private_ip", "String", "内网IP, 多值逗号分隔")
|
||||
]
|
||||
"""
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def run():
|
||||
"""
|
||||
执行入口, 返回采集的属性值
|
||||
:return: 返回一个列表, 列表项是字典, 字典key是属性名称, value是属性值
|
||||
例如:
|
||||
return [dict(ci_type="server", private_ip="192.168.1.1")]
|
||||
"""
|
||||
return []
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
result = AutoDiscovery().run()
|
||||
if isinstance(result, list):
|
||||
print("AutoDiscovery::Result::{}".format(json.dumps(result)))
|
||||
else:
|
||||
print("ERROR: 采集返回必须是列表")
|
||||
`
|
||||
const typeList = ['String', 'Integer', 'Float', 'Date', 'DateTime', 'Time', 'JSON']
|
||||
return {
|
||||
default_plugin_script,
|
||||
typeList,
|
||||
visible: false,
|
||||
ruleData: {},
|
||||
type: 'add',
|
||||
adType: '',
|
||||
form: { name: '', is_plugin: false },
|
||||
rules: {},
|
||||
customIcon: { name: '', color: '' },
|
||||
tableData: [],
|
||||
editDefaultTableData: [],
|
||||
plugin_script: '',
|
||||
cmOptions: {
|
||||
lineNumbers: true,
|
||||
mode: 'python',
|
||||
height: '200px',
|
||||
theme: 'monokai',
|
||||
tabSize: 4,
|
||||
lineWrapping: true,
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
if (this.adType === 'http' || this.adType === 'snmp') {
|
||||
return this.ruleData.name
|
||||
}
|
||||
if (this.type === 'edit') {
|
||||
return `编辑:${this.ruleData.name}`
|
||||
}
|
||||
return '新建'
|
||||
},
|
||||
},
|
||||
inject: {
|
||||
getDiscovery: {
|
||||
from: 'getDiscovery',
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
open(data, type, adType) {
|
||||
this.visible = true
|
||||
this.type = type
|
||||
this.ruleData = data
|
||||
this.adType = adType
|
||||
if (!this.is_inner) {
|
||||
this.form = {
|
||||
name: '',
|
||||
is_plugin: true,
|
||||
}
|
||||
}
|
||||
if (adType === 'http' || adType === 'snmp') {
|
||||
return
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
if (this.type === 'edit') {
|
||||
this.form = {
|
||||
name: data.name,
|
||||
is_plugin: data.is_plugin,
|
||||
}
|
||||
this.customIcon = data?.option?.icon ?? { name: 'caise-chajian', color: '' }
|
||||
this.tableData = data?.attributes ?? []
|
||||
this.editDefaultTableData = data?.attributes ?? []
|
||||
this.plugin_script = data?.plugin_script ?? this.default_plugin_script
|
||||
}
|
||||
if (this.type === 'add') {
|
||||
this.customIcon = { name: 'caise-chajian', color: '' }
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
this.plugin_script = this.default_plugin_script
|
||||
}
|
||||
if (data?.is_plugin || !this.is_inner) {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.codemirror.initCodeMirror(this.plugin_script)
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
handleClose() {
|
||||
this.tableData = []
|
||||
this.customIcon = { name: '', color: '' }
|
||||
this.form = { name: '', is_plugin: false }
|
||||
if (this.adType === 'agent') {
|
||||
this.$refs.autoDiscoveryForm.clearValidate()
|
||||
} else {
|
||||
// this.$refs.httpSnmpAd.currentCate = ''
|
||||
}
|
||||
this.visible = false
|
||||
},
|
||||
async insertEvent(row) {
|
||||
const $table = this.$refs.xTable
|
||||
const record = {}
|
||||
const { row: newRow } = await $table.insertAt(record, row)
|
||||
await $table.setEditRow(newRow)
|
||||
},
|
||||
editRowEvent(row) {
|
||||
const $table = this.$refs.xTable
|
||||
$table.setActiveRow(row)
|
||||
},
|
||||
saveRowEvent() {
|
||||
const $table = this.$refs.xTable
|
||||
$table.clearActived().then(() => {
|
||||
this.loading = true
|
||||
setTimeout(() => {
|
||||
this.loading = false
|
||||
}, 300)
|
||||
})
|
||||
},
|
||||
cancelRowEvent(row) {
|
||||
const $table = this.$refs.xTable
|
||||
$table.clearActived().then(() => {
|
||||
// 还原行数据
|
||||
$table.revertData(row)
|
||||
})
|
||||
},
|
||||
deleteRowEvent(row) {
|
||||
const $table = this.$refs.xTable
|
||||
$table.remove(row)
|
||||
},
|
||||
async handleSubmit(isUpdateAttr = false) {
|
||||
const $table = this.$refs.xTable
|
||||
const { fullData: _tableData } = $table.getTableData()
|
||||
console.log(_tableData)
|
||||
const params = {
|
||||
...this.form,
|
||||
type: this.adType,
|
||||
is_inner: this.is_inner,
|
||||
option: { icon: this.customIcon },
|
||||
attributes: this.form.is_plugin
|
||||
? undefined
|
||||
: _tableData.map(({ name, alias, desc, type }) => {
|
||||
return { name, alias, desc, type }
|
||||
}),
|
||||
plugin_script: this.form.is_plugin ? this.plugin_script : undefined,
|
||||
}
|
||||
let res
|
||||
if (this.type === 'add') {
|
||||
res = await postDiscovery(params)
|
||||
} else {
|
||||
res = await putDiscovery(this.ruleData.id, params)
|
||||
}
|
||||
if (isUpdateAttr) {
|
||||
this.tableData = res.attributes
|
||||
this.type = 'edit'
|
||||
this.ruleData = res
|
||||
this.$message.success('更新成功!')
|
||||
if (this.is_inner) {
|
||||
this.getDiscovery()
|
||||
}
|
||||
return
|
||||
}
|
||||
this.handleClose()
|
||||
console.log(this.is_inner)
|
||||
if (this.is_inner) {
|
||||
this.$message.success('保存成功!')
|
||||
this.getDiscovery()
|
||||
} else {
|
||||
this.$emit('updateNotInner', res)
|
||||
}
|
||||
},
|
||||
changeIsPlugin(e) {
|
||||
if (e.target.value) {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.codemirror.initCodeMirror(this.plugin_script)
|
||||
})
|
||||
}
|
||||
},
|
||||
changeCodeContent(value) {
|
||||
this.plugin_script = value && value.replace('\t', ' ')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
@@ -25,7 +25,7 @@
|
||||
:expandedKeys="expandedKeys"
|
||||
>
|
||||
<a-icon slot="switcherIcon" type="down" />
|
||||
<template #title="{ key: treeKey, title, isLeaf }">
|
||||
<template #title="{ key: treeKey, title,isLeaf }">
|
||||
<ContextMenu
|
||||
:title="title"
|
||||
:treeKey="treeKey"
|
||||
@@ -135,7 +135,8 @@
|
||||
{{ col.title }}</span
|
||||
>
|
||||
</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
|
||||
:getPopupContainer="(trigger) => trigger.parentElement"
|
||||
:style="{ width: '100%', height: '32px' }"
|
||||
@@ -177,10 +178,20 @@
|
||||
#default="{row}"
|
||||
>
|
||||
<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
|
||||
v-else-if="col.is_password && row[col.field]"
|
||||
:password="row[col.field]"
|
||||
:ci_id="row._id"
|
||||
:attr_id="col.attr_id"
|
||||
></PasswordField>
|
||||
<template v-else-if="col.is_choice">
|
||||
<template v-if="col.is_list">
|
||||
@@ -333,7 +344,7 @@ import {
|
||||
} from '@/modules/cmdb/api/CIRelation'
|
||||
|
||||
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 { roleHasPermissionToGrant } from '@/modules/acl/api/permission'
|
||||
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 CMDBGrant from '../../components/cmdbGrant'
|
||||
import { ops_move_icon as OpsMoveIcon } from '@/core/icons'
|
||||
import { getAttrPassword } from '../../api/CITypeAttr'
|
||||
|
||||
export default {
|
||||
name: 'RelationViews',
|
||||
@@ -407,6 +419,11 @@ export default {
|
||||
tableDragClassName: [],
|
||||
|
||||
resource_type: {},
|
||||
|
||||
initialPasswordValue: {},
|
||||
passwordValue: {},
|
||||
lastEditCiId: null,
|
||||
isContinueCloseEdit: true,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -549,11 +566,11 @@ export default {
|
||||
q = q.slice(1)
|
||||
}
|
||||
if (this.treeKeys.length === 0) {
|
||||
await this.judgeCITypes(q)
|
||||
if (!refreshType) {
|
||||
this.loadRoot()
|
||||
}
|
||||
|
||||
await this.judgeCITypes(q)
|
||||
const fuzzySearch = (this.$refs['search'] || {}).fuzzySearch || ''
|
||||
if (fuzzySearch) {
|
||||
q = `q=_type:${this.currentTypeId[0]},*${fuzzySearch}*,` + q
|
||||
@@ -635,6 +652,7 @@ export default {
|
||||
statisticsCIRelation({
|
||||
root_ids: key.split('%')[0],
|
||||
level: this.treeKeys.length - index,
|
||||
type_ids: this.showTypes.map((type) => type.id).join(','),
|
||||
}).then((res) => {
|
||||
let result
|
||||
const getTreeItem = (data, id) => {
|
||||
@@ -689,6 +707,7 @@ export default {
|
||||
}
|
||||
const promises = _showTypeIds.map((typeId) => {
|
||||
const _q = (`q=_type:${typeId},` + q).replace(/count=\d*/, 'count=1')
|
||||
console.log(_q)
|
||||
if (this.treeKeys.length === 0) {
|
||||
return searchCI2(_q).then((res) => {
|
||||
if (res.numfound !== 0) {
|
||||
@@ -739,7 +758,11 @@ export default {
|
||||
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) => {
|
||||
item[1] += num[ciIds[idx] + '']
|
||||
})
|
||||
@@ -752,25 +775,35 @@ export default {
|
||||
|
||||
async loadNoRoot(rootIdAndTypeId, level) {
|
||||
const rootId = rootIdAndTypeId.split('%')[0]
|
||||
searchCIRelation(`root_id=${rootId}&level=1&count=10000`).then(async (res) => {
|
||||
const facet = []
|
||||
const ciIds = []
|
||||
res.result.forEach((item) => {
|
||||
facet.push([item[item.unique], 0, item._id, item._type, item.unique])
|
||||
ciIds.push(item._id)
|
||||
})
|
||||
const promises = level.map((_level) => {
|
||||
if (_level > 1) {
|
||||
return statisticsCIRelation({ root_ids: ciIds.join(','), level: _level - 1 }).then((num) => {
|
||||
facet.forEach((item, idx) => {
|
||||
item[1] += num[ciIds[idx] + '']
|
||||
const typeId = Number(rootIdAndTypeId.split('%')[1])
|
||||
const topo_flatten = this.relationViews?.views[this.$route.meta.name]?.topo_flatten ?? []
|
||||
const index = topo_flatten.findIndex((id) => id === typeId)
|
||||
const _type = topo_flatten[index + 1]
|
||||
if (_type) {
|
||||
searchCIRelation(`q=_type:${_type}&root_id=${rootId}&level=1&count=10000`).then(async (res) => {
|
||||
const facet = []
|
||||
const ciIds = []
|
||||
res.result.forEach((item) => {
|
||||
facet.push([item[item.unique], 0, item._id, item._type, item.unique])
|
||||
ciIds.push(item._id)
|
||||
})
|
||||
const promises = level.map((_level) => {
|
||||
if (_level > 1) {
|
||||
return statisticsCIRelation({
|
||||
root_ids: ciIds.join(','),
|
||||
level: _level - 1,
|
||||
type_ids: this.showTypes.map((type) => type.id).join(','),
|
||||
}).then((num) => {
|
||||
facet.forEach((item, idx) => {
|
||||
item[1] += num[ciIds[idx] + '']
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
await Promise.all(promises)
|
||||
this.wrapTreeData(facet, 'loadNoRoot')
|
||||
})
|
||||
await Promise.all(promises)
|
||||
this.wrapTreeData(facet, 'loadNoRoot')
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
onNodeClick(keys) {
|
||||
@@ -874,6 +907,12 @@ export default {
|
||||
calcColumns() {
|
||||
const width = document.getElementById('relation-views-right').clientWidth
|
||||
this.columns = getCITableColumns(this.instanceList, this.preferenceAttrList, width)
|
||||
this.columns.forEach((col) => {
|
||||
if (col.is_password) {
|
||||
this.initialPasswordValue[col.field] = ''
|
||||
this.passwordValue[col.field] = ''
|
||||
}
|
||||
})
|
||||
this.$nextTick(() => {
|
||||
this.$refs.xTable.refreshColumn()
|
||||
})
|
||||
@@ -924,10 +963,10 @@ export default {
|
||||
),
|
||||
onOk() {
|
||||
const _tempTree = that.treeKeys[that.treeKeys.length - 1].split('%')
|
||||
const firstCIObj = JSON.parse(_tempTree[2])
|
||||
const first_ci_id = Number(_tempTree[0])
|
||||
batchDeleteCIRelation(
|
||||
that.selectedRowKeys.map((item) => item._id),
|
||||
firstCIObj
|
||||
[first_ci_id]
|
||||
).then((res) => {
|
||||
that.$refs.xTable.clearCheckboxRow()
|
||||
that.$refs.xTable.clearCheckboxReserve()
|
||||
@@ -1060,15 +1099,48 @@ export default {
|
||||
}
|
||||
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 }) {
|
||||
if (!this.isContinueCloseEdit) {
|
||||
return
|
||||
}
|
||||
const $table = this.$refs['xTable']
|
||||
const data = {}
|
||||
this.columns.forEach((item) => {
|
||||
if (!_.isEqual(row[item.field], this.initialInstanceList[rowIndex][item.field])) {
|
||||
data[item.field] = row[item.field] || null
|
||||
if (!(item.field in this.initialPasswordValue) && !_.isEqual(row[item.field], this.initialInstanceList[rowIndex][item.field])) {
|
||||
data[item.field] = row[item.field] ?? null
|
||||
}
|
||||
})
|
||||
Object.keys(this.initialPasswordValue).forEach((key) => {
|
||||
if (this.initialPasswordValue[key] !== this.passwordValue[key]) {
|
||||
data[key] = this.passwordValue[key]
|
||||
row[key] = this.passwordValue[key]
|
||||
}
|
||||
})
|
||||
this.lastEditCiId = null
|
||||
if (JSON.stringify(data) !== '{}') {
|
||||
updateCI(row.ci_id || row._id, data)
|
||||
.then(() => {
|
||||
@@ -1086,6 +1158,12 @@ export default {
|
||||
$table.revertData(row)
|
||||
})
|
||||
}
|
||||
this.columns.forEach((col) => {
|
||||
if (col.is_password) {
|
||||
this.initialPasswordValue[col.field] = ''
|
||||
this.passwordValue[col.field] = ''
|
||||
}
|
||||
})
|
||||
},
|
||||
deleteCI(record) {
|
||||
const that = this
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -17,9 +17,11 @@ const logo = {
|
||||
getCompanyInfo({ commit }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
getCompanyInfo().then(res => {
|
||||
commit('SET_FILENAME', res.info.logoName)
|
||||
commit('SET_SMALL_FILENAME', res.info.smallLogoName)
|
||||
resolve(res.info)
|
||||
if (res.info) {
|
||||
commit('SET_FILENAME', res.info.logoName)
|
||||
commit('SET_SMALL_FILENAME', res.info.smallLogoName)
|
||||
resolve(res.info)
|
||||
}
|
||||
}).catch(err => {
|
||||
console.log('获取失败', err)
|
||||
reject(err)
|
||||
|
@@ -30,7 +30,7 @@ services:
|
||||
- redis
|
||||
|
||||
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:
|
||||
# context: .
|
||||
# target: cmdb-api
|
||||
@@ -47,13 +47,15 @@ services:
|
||||
flask db-setup
|
||||
flask common-check-new-columns
|
||||
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-acl
|
||||
nohup flask cmdb-trigger > trigger.log 2>&1 &
|
||||
nohup flask cmdb-counter > counter.log 2>&1 &
|
||||
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:
|
||||
- cmdb-db
|
||||
- cmdb-cache
|
||||
@@ -63,7 +65,7 @@ services:
|
||||
- cmdb-api
|
||||
|
||||
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:
|
||||
# context: .
|
||||
# target: cmdb-ui
|
||||
|
@@ -1,8 +1,16 @@
|
||||

|
||||
|
||||
[](https://github.com/veops/cmdb/blob/master/LICENSE)
|
||||
[](https://github.com/sendya/ant-design-pro-vue)
|
||||
[](https://github.com/pallets/flask)
|
||||
<p align="center">
|
||||
<a href="https://veops.cn"><img src="images/logo.png" alt="维易CMDB" width="300"/></a>
|
||||
</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)
|
||||
|
||||
@@ -16,9 +24,11 @@
|
||||
|
||||
## Overview
|
||||
|
||||
### Technical Architecture
|
||||
### System Overview
|
||||
|
||||
<img src=images/view.jpg />
|
||||
<img src=images/dashboard.png />
|
||||
|
||||
[View more screenshots](screenshot.md)
|
||||
|
||||
### Document
|
||||
|
||||
@@ -48,12 +58,7 @@
|
||||
- Fine-grained access control and comprehensive operation logs.
|
||||
- Support cross-model search.
|
||||
|
||||
### System Overview
|
||||
|
||||
- Service Tree
|
||||

|
||||
|
||||
[View more screenshots](screenshot.md)
|
||||
|
||||
### More Features
|
||||
|
||||
|
@@ -33,6 +33,7 @@
|
||||
* **`IN`**查询: 例如: `hostname:(cmdb*;cmdb-web*)` 小括号, 分号分隔
|
||||
* **`范围`**查询: 例如: `hostname:[cmdb* _TO_ cmdb-web*]` `_TO_`分隔
|
||||
* **`比较`**查询: 例如: `cpu_count:>5` 支持`>, >=, <, <=`
|
||||
* 多个条件可以用`小括号`进行组合
|
||||
|
||||
* 结果字段说明
|
||||
|
||||
|
@@ -25,6 +25,8 @@
|
||||
- `IN`查询. eg. `hostname:(cmdb*;cmdb-web*)` 小括号, 分号分隔
|
||||
- `RANGE`查询. eg. `hostname:[cmdb* _TO_ cmdb-web*]` `_TO_`分隔
|
||||
- `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`
|
||||
- 返回数据(默认 json)
|
||||
@@ -91,6 +93,8 @@
|
||||
- `IN`查询. eg. `hostname:(cmdb*;cmdb-web*)` 小括号, 分号分隔
|
||||
- `RANGE`查询. eg. `hostname:[cmdb* _TO_ cmdb-web*]` `_TO_`分隔
|
||||
- `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`
|
||||
- 返回数据(默认 json)
|
||||
|
15
docs/flask-migrate.md
Normal file
15
docs/flask-migrate.md
Normal 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
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
BIN
docs/images/wechat.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 99 KiB |
@@ -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-ui**目录执行`yarn run serve`
|
||||
- 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 acl_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 --autoscale=2,1 --logfile=one_acl_async.log -D`
|
||||
|
||||
- 浏览器打开: [http://127.0.0.1:8000](http://127.0.0.1:8000)
|
||||
- 如果是非本机访问, 要修改**cmdb-ui/.env**里**VUE_APP_API_BASE_URL**里的 IP 地址为后端服务的 ip 地址
|
||||
|
@@ -26,8 +26,8 @@
|
||||
- backend: in **cmdb-api** directory: `pipenv run flask run -h 0.0.0.0`
|
||||
- frontend: in **cmdb-ui** directory: `yarn run serve`
|
||||
- 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 acl_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 --autoscale=2,1 --logfile=one_acl_async.log -D`
|
||||
|
||||
- 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
|
||||
|
Reference in New Issue
Block a user