Compare commits

...

121 Commits

Author SHA1 Message Date
pycook
fe6373422e chore: release v2.4.13 2024-10-18 09:52:26 +08:00
pycook
b3ea776886 Merge pull request #628 from veops/dev_api_relation_path_search
feat(api): relation path search
2024-10-17 19:47:34 +08:00
pycook
c4d2ce313d feat(api): relation path search 2024-10-17 19:46:39 +08:00
Leo Song
20103a0fe6 Merge pull request #627 from veops/dev_ui_241017
feat(ui): add relation search
2024-10-17 17:56:13 +08:00
songlh
394e2aeac6 feat(ui): add relation search 2024-10-17 17:55:36 +08:00
pycook
8f7d78c26c Merge pull request #623 from veops/dev_api_relation_path_search
Dev api relation path search
2024-09-30 17:33:45 +08:00
pycook
7eecf3cec3 feat(api): add api /ci_type_relations/path 2024-09-26 20:32:21 +08:00
pycook
f6e9c443f7 Merge pull request #622 from novohool/master
Update cache support for environment variables in settings.example.py
2024-09-26 18:09:54 +08:00
pycook
857cbd82fd feat(api): add relation path search 2024-09-26 17:59:08 +08:00
novohool
9a14296e02 Update settings.example.py 2024-09-26 17:00:51 +08:00
pycook
f638b52759 fix(api): change records of attribute values for date and datetime 2024-09-25 19:37:08 +08:00
pycook
78da728105 fix(api): search for multiple CIType 2024-09-24 17:46:27 +08:00
pycook
eb69029a51 fix(api): ci relations search 2024-09-23 19:46:43 +08:00
Leo Song
07a097eba2 Merge pull request #619 from veops/dev_ui_240920
feat: update computed attr tip
2024-09-20 15:36:55 +08:00
songlh
e843e3eac9 feat: update computed attr tip 2024-09-20 15:36:19 +08:00
Leo Song
7308cfa6c2 Merge pull request #617 from veops/dev_ui_240914
dev_ui_240914
2024-09-14 17:28:42 +08:00
songlh
64ea4fb21f fix(ui): operation history search expand error 2024-09-14 17:27:57 +08:00
songlh
e15cefaa38 fix(ui): employeeTreeSelect display error 2024-09-14 17:26:33 +08:00
pycook
f32339b969 Merge pull request #616 from thexqn/optimize_history
feat: Add show_attr value column to operation history table
2024-09-14 11:55:01 +08:00
thexqn
131d213a73 优化CITypeCache的调用方式 2024-09-14 11:30:45 +08:00
thexqn
ff98777689 feat(cmdb): 添加操作历史表的唯一值列 (Add unique value column to operation history table) 2024-09-14 01:13:07 +08:00
thexqn
383d4c88ed feat: Add unique value column to operation history table 2024-09-13 23:44:40 +08:00
Leo Song
bb7157e292 Merge pull request #615 from veops/dev_ui_240913
feat(ui): add employeeTreeSelect otherOptions prop
2024-09-13 18:36:48 +08:00
songlh
b1a82f1a67 feat(ui): add employeeTreeSelect otherOptions prop 2024-09-13 18:36:24 +08:00
pycook
de86ea3852 fix(api): remote ip for login log 2024-09-10 11:41:35 +08:00
pycook
bf05ea240e feat(api): acl supports channel 2024-09-09 15:28:20 +08:00
Leo Song
8ec0d619d7 Merge pull request #613 from veops/dev_ui_240909
feat(ui): add SplitPane calcBasedParent prop
2024-09-09 10:45:27 +08:00
songlh
61f8c463bc feat(ui): add SplitPane calcBasedParent prop 2024-09-09 10:44:58 +08:00
Leo Song
9b4dc3e43b Merge pull request #611 from veops/dev_ui_240903
feat: update icon select
2024-09-03 16:41:18 +08:00
songlh
9e69be8256 feat: update icon select 2024-09-03 16:40:46 +08:00
pycook
251b9e7fd5 chore: release v2.4.12 2024-09-03 14:18:53 +08:00
Leo Song
f3cc12f1f9 Merge pull request #610 from veops/dev_ui_240903
fix(ui): build error
2024-09-03 13:15:43 +08:00
songlh
56f03e1624 fix(ui): build error 2024-09-03 13:14:56 +08:00
Leo Song
42ad2b6dde Merge pull request #609 from veops/dev_ui_240903
feat: update resource search
2024-09-03 11:30:08 +08:00
songlh
5aba1ff257 feat: update resource search 2024-09-03 11:29:32 +08:00
pycook
417e8fe349 perf(api): resource search supports recent searches and my favorites 2024-09-02 16:56:06 +08:00
Leo Song
02235d8cc0 Merge pull request #607 from veops/dev_ui_240828
fix(ui): ci choice attr error
2024-08-28 18:52:43 +08:00
songlh
00c7a644a2 fix(ui): ci choice attr error 2024-08-28 18:52:20 +08:00
pycook
f3e8757450 fix(api): CIType templates import 2024-08-28 17:52:15 +08:00
Leo Song
f0749341ba Merge pull request #606 from veops/dev_ui_240828
feat(ui): update ui
2024-08-28 16:55:45 +08:00
songlh
89da671e46 feat(ui): update ui 2024-08-28 16:55:07 +08:00
Leo Song
0e60aae076 Merge pull request #605 from veops/dev_ui_240827
Dev UI 240827
2024-08-27 10:33:30 +08:00
songlh
4dfa97d404 fix(ui): resource search export error 2024-08-27 10:32:53 +08:00
songlh
9b778f9bc7 fix(ui): update create attr icon 2024-08-27 10:32:25 +08:00
pycook
eafb5f053a fix(api): custom dashboard for enum type 2024-08-26 22:31:58 +08:00
Leo Song
834054e216 Merge pull request #604 from veops/dev_ui_240826
feat: export remove reference attr
2024-08-26 22:22:43 +08:00
LH_R
a97cabbedc feat: export remove reference attr 2024-08-26 22:21:25 +08:00
Leo Song
ae77852d5f Merge pull request #603 from veops/dev_ui_240826
fix(ui): define value filter error
2024-08-26 21:40:03 +08:00
LH_R
611ee40dca fix(ui): define value filter error 2024-08-26 21:38:02 +08:00
pycook
c0d55b2126 Merge branch 'master' of github.com:veops/cmdb 2024-08-26 19:50:44 +08:00
pycook
2cc4499ef9 fix(api): custom dashboard 2024-08-26 19:50:22 +08:00
Leo Song
1268404bca Merge pull request #602 from veops/dev_ui_240826
fix(ui): menu icon display
2024-08-26 19:49:46 +08:00
songlh
570a9203c4 fix(ui): menu icon display 2024-08-26 19:47:23 +08:00
pycook
adae7b5519 chore: release v2.4.11 2024-08-26 18:44:23 +08:00
Leo Song
8a91ec7b11 Merge pull request #601 from veops/dev_ui_240826
fix(ui): some bugs
2024-08-26 18:35:44 +08:00
songlh
92fca65383 fix(ui): some bugs 2024-08-26 18:34:42 +08:00
Leo Song
4b8e6c2841 Merge pull request #600 from veops/dev_ui_240826
fix(ui): update builtIn params
2024-08-26 16:03:37 +08:00
songlh
ab240cb003 fix(ui): update builtIn params 2024-08-26 16:02:05 +08:00
Leo Song
61e62e4740 Merge pull request #599 from veops/dev_ui_240826
feat(ui) update CMDBFilterComp label
2024-08-26 15:16:07 +08:00
songlh
1fd72d6c78 feat(ui) update CMDBFilterComp label 2024-08-26 15:14:52 +08:00
Leo Song
51e16f6b23 Merge pull request #598 from veops/dev_ui_240826
Dev UI 240826
2024-08-26 15:09:01 +08:00
songlh
037378e384 fix(ui): create ad plugin params 2024-08-26 15:08:19 +08:00
songlh
631871a8cf feat(ui): update ci type choice config 2024-08-26 15:05:11 +08:00
pycook
6e02f6a21f fix(api): in query 2024-08-26 13:29:03 +08:00
pycook
a2224ba2ac Merge pull request #597 from veops/dev_api_0826
feat(api): enum supports
2024-08-26 12:15:05 +08:00
pycook
11a289aac9 feat(api): enum supports 2024-08-26 12:14:14 +08:00
Leo Song
55ab04dd28 Merge pull request #596 from thexqn/fix_order_bug
修复在继承模型的情况下,非继承属性与继承属性的排序以及其他分组的排序提示问题Fix order bug
2024-08-26 11:20:15 +08:00
thexqn
256a4f4844 清理多余的router-view 2024-08-23 16:55:21 +08:00
thexqn
018a349336 feat: 修复在继承模型的情况下,非继承属性与继承属性的排序以及其他分组的排序的问题 2024-08-23 16:50:37 +08:00
thexqn
8f62227adb feat: 修复在继承模型的情况下,非继承属性与继承属性的排序以及其他分组的排序的问题 2024-08-23 16:29:29 +08:00
thexqn
de51cb3e21 Merge branch 'veops:master' into master 2024-08-23 14:56:12 +08:00
Leo Song
ecb069cf14 Merge pull request #594 from veops/dev_ui_240820
feat(ui): add bool and reference type
2024-08-20 15:31:45 +08:00
songlh
937cb84393 feat(ui): add bool and reference type 2024-08-20 15:31:11 +08:00
pycook
40a4db06b5 Merge pull request #593 from veops/dev_api_0820
feat(api): supports bool and reference
2024-08-20 13:51:44 +08:00
pycook
cc98f903ea feat(api): supports bool and reference 2024-08-20 13:49:51 +08:00
kinyXu
fb7471ce04 feat: add attribute sorted tips for non-inherited attributes 2024-08-20 11:48:44 +08:00
Leo Song
e2872f041e Merge pull request #591 from veops/dev_ui_240813
refactor(ui): ci table
2024-08-13 17:15:15 +08:00
songlh
250fde127c refactor(ui): ci table 2024-08-13 17:14:05 +08:00
pycook
73dbb14944 Merge pull request #590 from lgphone/patch-1
bugfix: cmdb-api  auto_discovery add unique_value param
2024-08-07 16:05:56 +08:00
YangEver
73c9a6fa72 bugfix: cmdb-api auto_discovery add unique_value param
自动发现接口需要根据unique_value参数进行数据唯一性校验,此参数为必填项
2024-08-07 15:50:42 +08:00
Leo Song
09d957db79 Merge pull request #589 from veops/dev_ui_240807
Dev UI 240807
2024-08-07 14:42:01 +08:00
songlh
b73d796891 fix(ui): dashboard chart config 2024-08-07 14:41:22 +08:00
songlh
e7cbd0caa9 feat(ui): update common settings btn 2024-08-07 14:40:54 +08:00
pycook
3e4c385d91 fix(api): Dashboard using display attributes 2024-08-06 19:59:16 +08:00
pycook
3aac012ee9 chore: release v2.4.10 2024-07-31 16:42:26 +08:00
Leo Song
78d762cacc Merge pull request #588 from veops/dev_ui_240731
feat(ui): update ci type
2024-07-31 16:01:17 +08:00
songlh
c668ba7d3f feat(ui): update ci type 2024-07-31 16:00:40 +08:00
pycook
542a876ead fix(api): delete item for multi-value attributes 2024-07-30 20:05:21 +08:00
pycook
68b7497bba Merge pull request #587 from thexqn/master
修复在用了计算属性的情况下,批量上传功能可能出现的错误.
2024-07-30 09:18:30 +08:00
thexqn
dfbf3d462d 修复在用了计算属性的情况下,批量上传功能可能出现的错误. 2024-07-30 01:17:53 +08:00
pycook
692708fcba feat(api): Multi-valued attribute values ​​support adding and deleting 2024-07-29 19:55:07 +08:00
pycook
330b64edb3 chore: release v2.4.9 2024-07-26 17:03:00 +08:00
Leo Song
63a3074cb7 Merge pull request #585 from veops/fix_ui_240726
fix: discovery card eye btn
2024-07-26 16:50:43 +08:00
songlh
31b8cf49dc fix: discovery card eye btn 2024-07-26 16:49:28 +08:00
Leo Song
b01c335456 Merge pull request #584 from veops/dev_ui_240726
feat(ui): update auto discovery
2024-07-26 10:41:19 +08:00
songlh
002fef09e2 feat(ui): update auto discovery 2024-07-26 10:40:37 +08:00
pycook
175778a162 perf(api): auto discovery (#582) 2024-07-25 17:45:26 +08:00
Leo Song
5050a1bef5 Merge pull request #581 from veops/dev_ui_240722
feat: add accounts config
2024-07-22 17:39:18 +08:00
songlh
46a6cf67d6 feat: add accounts config 2024-07-22 17:38:48 +08:00
Leo Song
4e857c2775 Merge pull request #580 from veops/dev_ui_240716
feat: add history export
2024-07-16 13:46:40 +08:00
songlh
835df1bdeb feat: add history export 2024-07-16 13:45:31 +08:00
ivonGwy
579339d13c change pic 2024-07-15 16:23:34 +08:00
ivonGwy
629967ce82 change pic 2024-07-15 16:22:20 +08:00
pycook
3a00bfd236 chore: update docker compose 2024-07-11 14:29:34 +08:00
pycook
2e97ebd895 chore: release v2.4.8 2024-07-10 19:43:01 +08:00
Leo Song
eb6a813cbc Merge pull request #578 from veops/dev_ui_24071002
feat(ui): update
2024-07-10 19:19:08 +08:00
songlh
ff78face48 feat(ui): update 2024-07-10 19:18:22 +08:00
pycook
d55433c438 fix(api): computed attributes for multi values (#577) 2024-07-10 19:18:03 +08:00
Leo Song
daf0254616 Merge pull request #575 from veops/dev_ui_240710
fix: topoview search error
2024-07-10 10:12:11 +08:00
songlh
6b32009955 fix: topoview search error 2024-07-10 10:11:40 +08:00
Leo Song
d53288c1fb Merge pull request #574 from veops/dev_ui_240709
feat: update auto discovery
2024-07-09 09:45:25 +08:00
songlh
586d820a08 feat: update auto discovery 2024-07-09 09:44:28 +08:00
pycook
6776be4599 fix(api): auto discovery update
fix(api): auto discovery update
2024-07-08 18:03:21 +08:00
pycook
ff2b8ea198 perf(api): relationships built by attribute values (#572) 2024-07-08 11:42:18 +08:00
Leo Song
ed46a1e1c1 Merge pull request #571 from veops/dev_ui_240703
feat: add http attr mapping
2024-07-03 18:49:23 +08:00
songlh
0dc614fb46 feat: add http attr mapping 2024-07-03 18:47:55 +08:00
pycook
bc66d33ce0 fix(api): auto discovery configuration save password
fix(api): auto discovery configuration save password
2024-07-02 21:32:30 +08:00
pycook
d5db68d7d0 feat(api): auto discovery supports mapping (#569) 2024-07-02 20:19:50 +08:00
Leo Song
b22b8b286b Merge pull request #568 from veops/dev_ui_240628
dev_ui_240628
2024-06-28 17:43:30 +08:00
songlh
dd4f3b0e9c feat: update model export 2024-06-28 17:42:20 +08:00
songlh
688f4e0ea4 fix(ui): load ci type error 2024-06-28 17:42:10 +08:00
182 changed files with 23914 additions and 5086 deletions

View File

@@ -5,9 +5,9 @@ on:
branches: branches:
- master - master
tags: ["v*"] tags: ["v*"]
pull_request: # pull_request:
branches: # branches:
- master # - master
env: env:
# Use docker.io for Docker Hub if empty # Use docker.io for Docker Hub if empty
@@ -49,31 +49,31 @@ jobs:
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
push: true push: true
tags: ${{ env.REGISTRY_SERVER_ADDRESS }}/cmdb-api:${{ env.TAG }} tags: ${{ env.REGISTRY_SERVER_ADDRESS }}/cmdb-api:${{ env.TAG }}
release-ui-images: # release-ui-images:
runs-on: ubuntu-latest # runs-on: ubuntu-latest
needs: [setup-environment] # needs: [setup-environment]
permissions: # permissions:
contents: read # contents: read
packages: write # packages: write
timeout-minutes: 90 # timeout-minutes: 90
steps: # steps:
- name: Checkout Repo # - name: Checkout Repo
uses: actions/checkout@v4 # uses: actions/checkout@v4
- name: Login to GitHub Package Registry # - name: Login to GitHub Package Registry
uses: docker/login-action@v2 # uses: docker/login-action@v2
with: # with:
registry: ghcr.io # registry: ghcr.io
username: ${{ github.repository_owner }} # username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} # password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU # - name: Set up QEMU
uses: docker/setup-qemu-action@v3 # uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx # - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 # uses: docker/setup-buildx-action@v3
- name: Build and push CMDB-UI Docker image # - name: Build and push CMDB-UI Docker image
uses: docker/build-push-action@v6 # uses: docker/build-push-action@v6
with: # with:
file: docker/Dockerfile-UI # file: docker/Dockerfile-UI
context: . # context: .
platforms: linux/amd64,linux/arm64 # platforms: linux/amd64,linux/arm64
push: true # push: true
tags: ${{ env.REGISTRY_SERVER_ADDRESS }}/cmdb-ui:${{ env.TAG }} # tags: ${{ env.REGISTRY_SERVER_ADDRESS }}/cmdb-ui:${{ env.TAG }}

1
.gitignore vendored
View File

@@ -78,3 +78,4 @@ cmdb-ui/npm-debug.log*
cmdb-ui/yarn-debug.log* cmdb-ui/yarn-debug.log*
cmdb-ui/yarn-error.log* cmdb-ui/yarn-error.log*
cmdb-ui/package-lock.json cmdb-ui/package-lock.json
start.sh

View File

@@ -73,7 +73,8 @@
## 安装 ## 安装
### Docker 一键快速构建 ### Docker 一键快速构建
> 方法一
[//]: # (> 方法一)
- 第一步: 先安装 Docker 环境, 以及Docker Compose (v2) - 第一步: 先安装 Docker 环境, 以及Docker Compose (v2)
- 第二步: 拷贝项目 - 第二步: 拷贝项目
```shell ```shell
@@ -83,13 +84,20 @@ git clone https://github.com/veops/cmdb.git
``` ```
docker compose up -d docker compose up -d
``` ```
> 方法二, 该方法适用于linux系统
- 第一步: 先安装 Docker 环境, 以及Docker Compose (v2) [//]: # (> 方法二, 该方法适用于linux系统)
- 第二步: 直接使用项目根目录下的install.sh 文件进行 `安装`、`启动`、`暂停`、`查状态`、`删除`、`卸载`
```shell [//]: # (- 第一步: 先安装 Docker 环境, 以及Docker Compose (v2))
curl -so install.sh https://raw.githubusercontent.com/veops/cmdb/deploy_on_kylin_docker/install.sh
sh install.sh install [//]: # (- 第二步: 直接使用项目根目录下的install.sh 文件进行 `安装`、`启动`、`暂停`、`查状态`、`删除`、`卸载`)
```
[//]: # (```shell)
[//]: # (curl -so install.sh https://raw.githubusercontent.com/veops/cmdb/deploy_on_kylin_docker/install.sh)
[//]: # (sh install.sh install)
[//]: # (```)
### [本地开发环境搭建](docs/local.md) ### [本地开发环境搭建](docs/local.md)
@@ -105,4 +113,7 @@ sh install.sh install
_**欢迎关注公众号(维易科技OneOps),关注后可加入微信群,进行产品和技术交流。**_ _**欢迎关注公众号(维易科技OneOps),关注后可加入微信群,进行产品和技术交流。**_
![公众号: 维易科技OneOps](docs/images/wechat.png)
<p align="center">
<img src="docs/images/wechat.png" alt="公众号: 维易科技OneOps" />
</p>

View File

@@ -67,6 +67,8 @@ colorama = ">=0.4.6"
pycryptodomex = ">=3.19.0" pycryptodomex = ">=3.19.0"
lz4 = ">=4.3.2" lz4 = ">=4.3.2"
python-magic = "==0.4.27" python-magic = "==0.4.27"
jsonpath = "==0.82.2"
networkx = ">=3.1"
[dev-packages] [dev-packages]
# Testing # Testing

View File

@@ -108,7 +108,8 @@ class AttributeManager(object):
return [] return []
choice_values = choice_table.get_by(fl=["value", "option"], attr_id=attr_id) choice_values = choice_table.get_by(fl=["value", "option"], attr_id=attr_id)
return [[ValueTypeMap.serialize[value_type](choice_value['value']), choice_value['option']] return [[ValueTypeMap.serialize[value_type](choice_value['value']), choice_value['option'] or
{"label": ValueTypeMap.serialize[value_type](choice_value['value'])}]
for choice_value in choice_values] for choice_value in choice_values]
@staticmethod @staticmethod
@@ -135,6 +136,15 @@ class AttributeManager(object):
choice_table and choice_table.get_by(attr_id=_id, only_query=True).delete() choice_table and choice_table.get_by(attr_id=_id, only_query=True).delete()
db.session.flush() db.session.flush()
@classmethod
def get_enum_map(cls, _attr_id, _attr=None):
attr = AttributeCache.get(_attr_id) if _attr_id else _attr
if attr and attr.is_choice:
choice_values = cls.get_choice_values(attr.id, attr.value_type, None, None)
return {i[0]: i[1]['label'] for i in choice_values if i[1] and i[1].get('label')}
return {}
@classmethod @classmethod
def search_attributes(cls, name=None, alias=None, page=1, page_size=None): def search_attributes(cls, name=None, alias=None, page=1, page_size=None):
""" """
@@ -167,24 +177,30 @@ class AttributeManager(object):
def get_attribute_by_name(self, name): def get_attribute_by_name(self, name):
attr = Attribute.get_by(name=name, first=True) attr = Attribute.get_by(name=name, first=True)
if attr.get("is_choice"): if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"], attr["choice_value"] = self.get_choice_values(attr["id"],
attr["choice_web_hook"], attr.get("choice_other")) attr["value_type"],
attr["choice_web_hook"],
attr.get("choice_other"))
return attr return attr
def get_attribute_by_alias(self, alias): def get_attribute_by_alias(self, alias):
attr = Attribute.get_by(alias=alias, first=True) attr = Attribute.get_by(alias=alias, first=True)
if attr.get("is_choice"): if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"], attr["choice_value"] = self.get_choice_values(attr["id"],
attr["choice_web_hook"], attr.get("choice_other")) attr["value_type"],
attr["choice_web_hook"],
attr.get("choice_other"))
return attr return attr
def get_attribute_by_id(self, _id): def get_attribute_by_id(self, _id):
attr = Attribute.get_by_id(_id).to_dict() attr = Attribute.get_by_id(_id).to_dict()
if attr.get("is_choice"): if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"], attr["choice_value"] = self.get_choice_values(attr["id"],
attr["choice_web_hook"], attr.get("choice_other")) attr["value_type"],
attr["choice_web_hook"],
attr.get("choice_other"))
return attr return attr
@@ -229,7 +245,7 @@ class AttributeManager(object):
is_choice = True if choice_value or kwargs.get('choice_web_hook') or kwargs.get('choice_other') else False is_choice = True if choice_value or kwargs.get('choice_web_hook') or kwargs.get('choice_other') else False
name = kwargs.pop("name") name = kwargs.pop("name")
if name in BUILTIN_KEYWORDS: if name in BUILTIN_KEYWORDS or kwargs.get('alias') in BUILTIN_KEYWORDS:
return abort(400, ErrFormat.attribute_name_cannot_be_builtin) return abort(400, ErrFormat.attribute_name_cannot_be_builtin)
while kwargs.get('choice_other'): while kwargs.get('choice_other'):

View File

@@ -2,6 +2,7 @@
import copy import copy
import datetime import datetime
import json import json
import jsonpath
import os import os
from flask import abort from flask import abort
from flask import current_app from flask import current_app
@@ -9,14 +10,14 @@ from flask_login import current_user
from sqlalchemy import func from sqlalchemy import func
from api.extensions import db from api.extensions import db
from api.lib.cmdb.auto_discovery.const import ClOUD_MAP from api.lib.cmdb.auto_discovery.const import CLOUD_MAP
from api.lib.cmdb.auto_discovery.const import DEFAULT_INNER from api.lib.cmdb.auto_discovery.const import DEFAULT_INNER
from api.lib.cmdb.auto_discovery.const import PRIVILEGED_USERS from api.lib.cmdb.auto_discovery.const import PRIVILEGED_USERS
from api.lib.cmdb.cache import AttributeCache from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import AutoDiscoveryMappingCache
from api.lib.cmdb.cache import CITypeAttributeCache from api.lib.cmdb.cache import CITypeAttributeCache
from api.lib.cmdb.cache import CITypeCache from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIManager from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.ci_type import CITypeGroupManager from api.lib.cmdb.ci_type import CITypeGroupManager
from api.lib.cmdb.const import AutoDiscoveryType from api.lib.cmdb.const import AutoDiscoveryType
from api.lib.cmdb.const import CMDB_QUEUE from api.lib.cmdb.const import CMDB_QUEUE
@@ -32,6 +33,7 @@ from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import is_app_admin from api.lib.perm.acl.acl import is_app_admin
from api.lib.perm.acl.acl import validate_permission from api.lib.perm.acl.acl import validate_permission
from api.lib.utils import AESCrypto from api.lib.utils import AESCrypto
from api.models.cmdb import AutoDiscoveryAccount
from api.models.cmdb import AutoDiscoveryCI from api.models.cmdb import AutoDiscoveryCI
from api.models.cmdb import AutoDiscoveryCIType from api.models.cmdb import AutoDiscoveryCIType
from api.models.cmdb import AutoDiscoveryCITypeRelation from api.models.cmdb import AutoDiscoveryCITypeRelation
@@ -39,6 +41,7 @@ from api.models.cmdb import AutoDiscoveryCounter
from api.models.cmdb import AutoDiscoveryExecHistory from api.models.cmdb import AutoDiscoveryExecHistory
from api.models.cmdb import AutoDiscoveryRule from api.models.cmdb import AutoDiscoveryRule
from api.models.cmdb import AutoDiscoveryRuleSyncHistory from api.models.cmdb import AutoDiscoveryRuleSyncHistory
from api.tasks.cmdb import build_relations_for_ad_accept
from api.tasks.cmdb import write_ad_rule_sync_history from api.tasks.cmdb import write_ad_rule_sync_history
PWD = os.path.abspath(os.path.dirname(__file__)) PWD = os.path.abspath(os.path.dirname(__file__))
@@ -223,14 +226,14 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
adr = AutoDiscoveryRuleCRUD.get_by_id(adt.adr_id) adr = AutoDiscoveryRuleCRUD.get_by_id(adt.adr_id)
if not adr: if not adr:
continue continue
if adr.type == "http": if adr.type == AutoDiscoveryType.HTTP:
for i in DEFAULT_INNER: for i in DEFAULT_INNER:
if adr.name == i['name']: if adr.name == i['name']:
attrs = AutoDiscoveryHTTPManager.get_attributes( attrs = AutoDiscoveryHTTPManager.get_attributes(
i['en'], (adt.extra_option or {}).get('category')) or [] i['en'], (adt.extra_option or {}).get('category')) or []
result.extend([i.get('name') for i in attrs]) result.extend([i.get('name') for i in attrs])
break break
elif adr.type == "snmp": elif adr.type == AutoDiscoveryType.SNMP:
attributes = AutoDiscoverySNMPManager.get_attributes() attributes = AutoDiscoverySNMPManager.get_attributes()
result.extend([i.get('name') for i in (attributes or [])]) result.extend([i.get('name') for i in (attributes or [])])
else: else:
@@ -240,21 +243,29 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
@classmethod @classmethod
def get(cls, ci_id, oneagent_id, oneagent_name, last_update_at=None): def get(cls, ci_id, oneagent_id, oneagent_name, last_update_at=None):
"""
OneAgent sync rules
:param ci_id:
:param oneagent_id:
:param oneagent_name:
:param last_update_at:
:return:
"""
result = [] result = []
rules = cls.cls.get_by(to_dict=True) rules = cls.cls.get_by(to_dict=True)
for rule in rules: for rule in rules:
if isinstance(rule.get("extra_option"), dict) and rule['extra_option'].get('secret'): if not rule['enabled']:
if not (current_user.username in PRIVILEGED_USERS or current_user.uid == rule['uid']): continue
rule['extra_option'].pop('secret', None)
else:
rule['extra_option']['secret'] = AESCrypto.decrypt(rule['extra_option']['secret'])
if isinstance(rule.get("extra_option"), dict) and rule['extra_option'].get('password'): if isinstance(rule.get("extra_option"), dict):
if not (current_user.username in PRIVILEGED_USERS or current_user.uid == rule['uid']): decrypt_account(rule['extra_option'], rule['uid'])
if rule['extra_option'].get('_reference'):
rule['extra_option'].pop('password', None) rule['extra_option'].pop('password', None)
else: rule['extra_option'].pop('secret', None)
rule['extra_option']['password'] = AESCrypto.decrypt(rule['extra_option']['password']) rule['extra_option'].update(
AutoDiscoveryAccountCRUD().get_config_by_id(rule['extra_option']['_reference']))
if oneagent_id and rule['agent_id'] == oneagent_id: if oneagent_id and rule['agent_id'] == oneagent_id:
result.append(rule) result.append(rule)
@@ -271,6 +282,12 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
result.append(rule) result.append(rule)
break break
elif not rule['agent_id'] and not rule['query_expr'] and rule['adr_id']: elif not rule['agent_id'] and not rule['query_expr'] and rule['adr_id']:
try:
if not int(oneagent_id, 16): # excludes master
continue
except Exception:
pass
adr = AutoDiscoveryRuleCRUD.get_by_id(rule['adr_id']) adr = AutoDiscoveryRuleCRUD.get_by_id(rule['adr_id'])
if not adr: if not adr:
continue continue
@@ -279,7 +296,6 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
result.append(rule) result.append(rule)
ad_rules_updated_at = (SystemConfigManager.get('ad_rules_updated_at') or {}).get('option', {}).get('v') or "" ad_rules_updated_at = (SystemConfigManager.get('ad_rules_updated_at') or {}).get('option', {}).get('v') or ""
new_last_update_at = "" new_last_update_at = ""
for i in result: for i in result:
@@ -353,27 +369,31 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
if kwargs.get('adr_id'): if kwargs.get('adr_id'):
adr = AutoDiscoveryRule.get_by_id(kwargs['adr_id']) or abort( adr = AutoDiscoveryRule.get_by_id(kwargs['adr_id']) or abort(
404, ErrFormat.adr_not_found.format("id={}".format(kwargs['adr_id']))) 404, ErrFormat.adr_not_found.format("id={}".format(kwargs['adr_id'])))
if adr.type == "http": if adr.type == AutoDiscoveryType.HTTP:
kwargs.setdefault('extra_option', dict) kwargs.setdefault('extra_option', dict())
en_name = None en_name = None
for i in DEFAULT_INNER: for i in DEFAULT_INNER:
if i['name'] == adr.name: if i['name'] == adr.name:
en_name = i['en'] en_name = i['en']
break break
if en_name and kwargs['extra_option'].get('category'): if en_name and kwargs['extra_option'].get('category'):
for item in ClOUD_MAP[en_name]: for item in CLOUD_MAP[en_name]:
if item["collect_key_map"].get(kwargs['extra_option']['category']): if item["collect_key_map"].get(kwargs['extra_option']['category']):
kwargs["extra_option"]["collect_key"] = item["collect_key_map"][ kwargs["extra_option"]["collect_key"] = item["collect_key_map"][
kwargs['extra_option']['category']] kwargs['extra_option']['category']]
kwargs["extra_option"]["provider"] = en_name
break break
if adr.type == AutoDiscoveryType.COMPONENTS and kwargs.get('extra_option'):
for i in DEFAULT_INNER:
if i['name'] == adr.name:
kwargs['extra_option']['collect_key'] = i['option'].get('collect_key')
break
if kwargs.get('is_plugin') and kwargs.get('plugin_script'): if kwargs.get('is_plugin') and kwargs.get('plugin_script'):
kwargs = check_plugin_script(**kwargs) kwargs = check_plugin_script(**kwargs)
if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('secret'): encrypt_account(kwargs.get('extra_option'))
kwargs['extra_option']['secret'] = AESCrypto.encrypt(kwargs['extra_option']['secret'])
if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('password'):
kwargs['extra_option']['password'] = AESCrypto.encrypt(kwargs['extra_option']['password'])
ci_type = CITypeCache.get(kwargs['type_id']) ci_type = CITypeCache.get(kwargs['type_id'])
unique = AttributeCache.get(ci_type.unique_id) unique = AttributeCache.get(ci_type.unique_id)
@@ -391,20 +411,27 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
adr = AutoDiscoveryRule.get_by_id(existed.adr_id) or abort( adr = AutoDiscoveryRule.get_by_id(existed.adr_id) or abort(
404, ErrFormat.adr_not_found.format("id={}".format(existed.adr_id))) 404, ErrFormat.adr_not_found.format("id={}".format(existed.adr_id)))
if adr.type == "http": if adr.type == AutoDiscoveryType.HTTP:
kwargs.setdefault('extra_option', dict) kwargs.setdefault('extra_option', dict())
en_name = None en_name = None
for i in DEFAULT_INNER: for i in DEFAULT_INNER:
if i['name'] == adr.name: if i['name'] == adr.name:
en_name = i['en'] en_name = i['en']
break break
if en_name and kwargs['extra_option'].get('category'): if en_name and kwargs['extra_option'].get('category'):
for item in ClOUD_MAP[en_name]: for item in CLOUD_MAP[en_name]:
if item["collect_key_map"].get(kwargs['extra_option']['category']): if item["collect_key_map"].get(kwargs['extra_option']['category']):
kwargs["extra_option"]["collect_key"] = item["collect_key_map"][ kwargs["extra_option"]["collect_key"] = item["collect_key_map"][
kwargs['extra_option']['category']] kwargs['extra_option']['category']]
kwargs["extra_option"]["provider"] = en_name
break break
if adr.type == AutoDiscoveryType.COMPONENTS and kwargs.get('extra_option'):
for i in DEFAULT_INNER:
if i['name'] == adr.name:
kwargs['extra_option']['collect_key'] = i['option'].get('collect_key')
break
if 'attributes' in kwargs: if 'attributes' in kwargs:
self.__valid_exec_target(kwargs.get('agent_id'), kwargs.get('query_expr')) self.__valid_exec_target(kwargs.get('agent_id'), kwargs.get('query_expr'))
@@ -428,13 +455,12 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
if kwargs.get('is_plugin') and kwargs.get('plugin_script'): if kwargs.get('is_plugin') and kwargs.get('plugin_script'):
kwargs = check_plugin_script(**kwargs) kwargs = check_plugin_script(**kwargs)
if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('secret'): encrypt_account(kwargs.get('extra_option'))
kwargs['extra_option']['secret'] = AESCrypto.encrypt(kwargs['extra_option']['secret'])
if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('password'):
kwargs['extra_option']['password'] = AESCrypto.encrypt(kwargs['extra_option']['password'])
inst = self._can_update(_id=_id, **kwargs) inst = self._can_update(_id=_id, **kwargs)
if inst.agent_id != kwargs.get('agent_id') or inst.query_expr != kwargs.get('query_expr'): if len(kwargs) == 1 and 'enabled' in kwargs: # enable or disable
pass
elif inst.agent_id != kwargs.get('agent_id') or inst.query_expr != kwargs.get('query_expr'):
for item in AutoDiscoveryRuleSyncHistory.get_by(adt_id=inst.id, to_dict=False): for item in AutoDiscoveryRuleSyncHistory.get_by(adt_id=inst.id, to_dict=False):
item.delete(commit=False) item.delete(commit=False)
db.session.commit() db.session.commit()
@@ -479,6 +505,7 @@ class AutoDiscoveryCITypeRelationCRUD(DBMixin):
def get_all(cls, type_ids=None): def get_all(cls, type_ids=None):
res = cls.cls.get_by(to_dict=False) res = cls.cls.get_by(to_dict=False)
return [i for i in res if type_ids is None or i.ad_type_id in type_ids] return [i for i in res if type_ids is None or i.ad_type_id in type_ids]
@classmethod @classmethod
def get_by_type_id(cls, type_id, to_dict=False): def get_by_type_id(cls, type_id, to_dict=False):
return cls.cls.get_by(ad_type_id=type_id, to_dict=to_dict) return cls.cls.get_by(ad_type_id=type_id, to_dict=to_dict)
@@ -543,7 +570,6 @@ class AutoDiscoveryCICRUD(DBMixin):
adts = AutoDiscoveryCITypeCRUD.get_by_type_id(type_id) adts = AutoDiscoveryCITypeCRUD.get_by_type_id(type_id)
for adt in adts: for adt in adts:
attr_names |= set((adt.attributes or {}).values()) attr_names |= set((adt.attributes or {}).values())
return [attr for attr in attributes if attr['name'] in attr_names] return [attr for attr in attributes if attr['name'] in attr_names]
@classmethod @classmethod
@@ -673,38 +699,24 @@ class AutoDiscoveryCICRUD(DBMixin):
adt = AutoDiscoveryCITypeCRUD.get_by_id(adc.adt_id) or abort(404, ErrFormat.adt_not_found) adt = AutoDiscoveryCITypeCRUD.get_by_id(adc.adt_id) or abort(404, ErrFormat.adt_not_found)
ci_id = None ci_id = None
if adt.attributes:
ci_dict = {adt.attributes[k]: v for k, v in adc.instance.items() if k in adt.attributes} ad_key2attr = adt.attributes or {}
if ad_key2attr:
ci_dict = {ad_key2attr[k]: None if not v and isinstance(v, (list, dict)) else v
for k, v in adc.instance.items() if k in ad_key2attr}
extra_option = adt.extra_option or {}
mapping, path_mapping = AutoDiscoveryHTTPManager.get_predefined_value_mapping(
extra_option.get('provider'), extra_option.get('category'))
if mapping:
ci_dict = {k: (mapping.get(k) or {}).get(str(v), v) for k, v in ci_dict.items()}
if path_mapping:
ci_dict = {k: jsonpath.jsonpath(v, path_mapping[k]) if k in path_mapping else v
for k, v in ci_dict.items()}
ci_id = CIManager.add(adc.type_id, is_auto_discovery=True, _is_admin=True, **ci_dict) ci_id = CIManager.add(adc.type_id, is_auto_discovery=True, _is_admin=True, **ci_dict)
AutoDiscoveryExecHistoryCRUD().add(type_id=adt.type_id, AutoDiscoveryExecHistoryCRUD().add(type_id=adt.type_id,
stdout="accept resource: {}".format(adc.unique_value)) stdout="accept resource: {}".format(adc.unique_value))
relation_ads = AutoDiscoveryCITypeRelation.get_by(ad_type_id=adt.type_id, to_dict=False) build_relations_for_ad_accept.apply_async(args=(adc.to_dict(), ci_id, ad_key2attr), queue=CMDB_QUEUE)
for r_adt in relation_ads:
ad_key = r_adt.ad_key
if not adc.instance.get(ad_key):
continue
ad_key_values = [adc.instance.get(ad_key)] if not isinstance(
adc.instance.get(ad_key), list) else adc.instance.get(ad_key)
for ad_key_value in ad_key_values:
query = "_type:{},{}:{}".format(r_adt.peer_type_id, r_adt.peer_attr_id, ad_key_value)
s = ci_search(query, use_ci_filter=False, count=1000000)
try:
response, _, _, _, _, _ = s.search()
except SearchError as e:
current_app.logger.warning(e)
return abort(400, str(e))
for relation_ci in response:
relation_ci_id = relation_ci['_id']
try:
CIRelationManager.add(ci_id, relation_ci_id, valid=False)
except:
try:
CIRelationManager.add(relation_ci_id, ci_id, valid=False)
except:
pass
adc.update(is_accept=True, adc.update(is_accept=True,
accept_by=nickname or current_user.nickname, accept_by=nickname or current_user.nickname,
@@ -715,7 +727,7 @@ class AutoDiscoveryCICRUD(DBMixin):
class AutoDiscoveryHTTPManager(object): class AutoDiscoveryHTTPManager(object):
@staticmethod @staticmethod
def get_categories(name): def get_categories(name):
categories = (ClOUD_MAP.get(name) or {}) or [] categories = (CLOUD_MAP.get(name) or {}) or []
for item in copy.deepcopy(categories): for item in copy.deepcopy(categories):
item.pop('map', None) item.pop('map', None)
item.pop('collect_key_map', None) item.pop('collect_key_map', None)
@@ -738,16 +750,52 @@ class AutoDiscoveryHTTPManager(object):
@staticmethod @staticmethod
def get_attributes(provider, resource): def get_attributes(provider, resource):
for item in (ClOUD_MAP.get(provider) or {}): for item in (CLOUD_MAP.get(provider) or {}):
for _resource in (item.get('map') or {}): for _resource in (item.get('map') or {}):
if _resource == resource: if _resource == resource:
tpt = item['map'][_resource] tpt = item['map'][_resource]
if isinstance(tpt, dict):
tpt = tpt.get('template')
if tpt and os.path.exists(os.path.join(PWD, tpt)): if tpt and os.path.exists(os.path.join(PWD, tpt)):
with open(os.path.join(PWD, tpt)) as f: with open(os.path.join(PWD, tpt)) as f:
return json.loads(f.read()) return json.loads(f.read())
return [] return []
@staticmethod
def get_mapping(provider, resource):
for item in (CLOUD_MAP.get(provider) or {}):
for _resource in (item.get('map') or {}):
if _resource == resource:
mapping = item['map'][_resource]
if not isinstance(mapping, dict):
return {}
name = mapping.get('mapping')
mapping = AutoDiscoveryMappingCache.get(name)
if isinstance(mapping, dict):
return {mapping[key][provider]['key'].split('.')[0]: key for key in mapping if
(mapping[key].get(provider) or {}).get('key')}
return {}
@staticmethod
def get_predefined_value_mapping(provider, resource):
for item in (CLOUD_MAP.get(provider) or {}):
for _resource in (item.get('map') or {}):
if _resource == resource:
mapping = item['map'][_resource]
if not isinstance(mapping, dict):
return {}, {}
name = mapping.get('mapping')
mapping = AutoDiscoveryMappingCache.get(name)
if isinstance(mapping, dict):
return ({key: mapping[key][provider].get('map') for key in mapping if
mapping[key].get(provider, {}).get('map')},
{key: mapping[key][provider]['key'].split('.', 1)[1] for key in mapping if
((mapping[key].get(provider) or {}).get('key') or '').split('.')[1:]})
return {}, {}
class AutoDiscoverySNMPManager(object): class AutoDiscoverySNMPManager(object):
@@ -828,3 +876,121 @@ class AutoDiscoveryCounterCRUD(DBMixin):
def _can_delete(self, **kwargs): def _can_delete(self, **kwargs):
pass pass
def encrypt_account(config):
if isinstance(config, dict):
if config.get('secret'):
config['secret'] = AESCrypto.encrypt(config['secret'])
if config.get('password'):
config['password'] = AESCrypto.encrypt(config['password'])
def decrypt_account(config, uid):
if isinstance(config, dict):
if config.get('password'):
if not (current_user.username in PRIVILEGED_USERS or current_user.uid == uid):
config.pop('password', None)
else:
try:
config['password'] = AESCrypto.decrypt(config['password'])
except Exception as e:
current_app.logger.error('decrypt account failed: {}'.format(e))
if config.get('secret'):
if not (current_user.username in PRIVILEGED_USERS or current_user.uid == uid):
config.pop('secret', None)
else:
try:
config['secret'] = AESCrypto.decrypt(config['secret'])
except Exception as e:
current_app.logger.error('decrypt account failed: {}'.format(e))
class AutoDiscoveryAccountCRUD(DBMixin):
cls = AutoDiscoveryAccount
def get(self, adr_id):
res = self.cls.get_by(adr_id=adr_id, to_dict=True)
for i in res:
decrypt_account(i.get('config'), i['uid'])
return res
def get_config_by_id(self, _id):
res = self.cls.get_by_id(_id)
if not res:
return {}
config = res.to_dict().get('config') or {}
decrypt_account(config, res.uid)
return config
def _can_add(self, **kwargs):
encrypt_account(kwargs.get('config'))
kwargs['uid'] = current_user.uid
return kwargs
def upsert(self, adr_id, accounts):
existed_all = self.cls.get_by(adr_id=adr_id, to_dict=False)
account_names = {i['name'] for i in accounts}
name_changed = dict()
for account in accounts:
existed = None
if account.get('id'):
existed = self.cls.get_by_id(account.get('id'))
if existed is None:
continue
account.pop('id')
name_changed[existed.name] = account.get('name')
else:
account = self._can_add(**account)
if existed is not None:
if current_user.uid == existed.uid:
config = copy.deepcopy(existed.config) or {}
config.update(account.get('config') or {})
account['config'] = config
existed.update(**account)
else:
self.cls.create(adr_id=adr_id, **account)
for item in existed_all:
if name_changed.get(item.name, item.name) not in account_names:
if current_user.uid == item.uid:
item.soft_delete()
def _can_update(self, **kwargs):
existed = self.cls.get_by_id(kwargs['_id']) or abort(404, ErrFormat.not_found)
if isinstance(kwargs.get('config'), dict) and kwargs['config'].get('secret'):
if current_user.uid != existed.uid:
return abort(403, ErrFormat.adt_secret_no_permission)
if isinstance(kwargs.get('config'), dict) and kwargs['config'].get('password'):
if current_user.uid != existed.uid:
return abort(403, ErrFormat.adt_secret_no_permission)
return existed
def update(self, _id, **kwargs):
if kwargs.get('is_plugin') and kwargs.get('plugin_script'):
kwargs = check_plugin_script(**kwargs)
encrypt_account(kwargs.get('config'))
inst = self._can_update(_id=_id, **kwargs)
obj = inst.update(_id=_id, filter_none=False, **kwargs)
return obj
def _can_delete(self, **kwargs):
pass

View File

@@ -12,17 +12,28 @@ DEFAULT_INNER = [
dict(name="华为云", en="huaweicloud", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False, dict(name="华为云", en="huaweicloud", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-huaweiyun'}, "en": "huaweicloud"}), option={'icon': {'name': 'caise-huaweiyun'}, "en": "huaweicloud"}),
dict(name="AWS", en="aws", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False, dict(name="AWS", en="aws", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-aws'}}), option={'icon': {'name': 'caise-aws'}, "en": "aws"}),
dict(name="VCenter", en="vcenter", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False, dict(name="VCenter", en="vcenter", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'cmdb-vcenter'}, "category": "private_cloud", "en": "vcenter"}), option={'icon': {'name': 'cmdb-vcenter'}, "category": "private_cloud", "en": "vcenter"}),
dict(name="KVM", en="kvm", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False, dict(name="KVM", en="kvm", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'ops-KVM'}, "category": "private_cloud", "en": "kvm"}), option={'icon': {'name': 'ops-KVM'}, "category": "private_cloud", "en": "kvm"}),
dict(name="Nginx", en="nginx", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False, dict(name="Nginx", en="nginx", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-nginx'}, "en": "nginx"}), option={'icon': {'name': 'caise-nginx'}, "en": "nginx", "collect_key": "nginx"}),
dict(name="Apache", en="apache", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-apache'}, "en": "apache", "collect_key": "apache"}),
dict(name="Tomcat", en="tomcat", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-tomcat'}, "en": "tomcat", "collect_key": "tomcat"}),
dict(name="MySQL", en="mysql", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-mySQL'}, "en": "mysql", "collect_key": "mysql"}),
dict(name="MSSQL", en="mssql", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-SQLServer'}, "en": "mssql", "collect_key": "sqlserver"}),
dict(name="Oracle", en="oracle", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-oracle'}, "en": "oracle", "collect_key": "oracle"}),
dict(name="Redis", en="redis", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False, dict(name="Redis", en="redis", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-redis'}, "en": "redis"}), option={'icon': {'name': 'caise-redis'}, "en": "redis", "collect_key": "redis"}),
dict(name="交换机", type=AutoDiscoveryType.SNMP, is_inner=True, is_plugin=False, dict(name="交换机", type=AutoDiscoveryType.SNMP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-jiaohuanji'}}), option={'icon': {'name': 'caise-jiaohuanji'}}),
@@ -34,14 +45,14 @@ DEFAULT_INNER = [
option={'icon': {'name': 'caise-dayinji'}}), option={'icon': {'name': 'caise-dayinji'}}),
] ]
ClOUD_MAP = { CLOUD_MAP = {
"aliyun": [ "aliyun": [
{ {
"category": "计算", "category": "计算",
"items": ["云服务器 ECS", "云服务器 Disk"], "items": ["云服务器 ECS", "云服务器 Disk"],
"map": { "map": {
"云服务器 ECS": "templates/aliyun_ecs.json", "云服务器 ECS": {"template": "templates/aliyun_ecs.json", "mapping": "ecs"},
"云服务器 Disk": "templates/aliyun_ecs_disk2.json", "云服务器 Disk": {"template": "templates/aliyun_ecs_disk.json", "mapping": "evs"},
}, },
"collect_key_map": { "collect_key_map": {
"云服务器 ECS": "ali.ecs", "云服务器 ECS": "ali.ecs",
@@ -57,10 +68,10 @@ ClOUD_MAP = {
"交换机Switch", "交换机Switch",
], ],
"map": { "map": {
"内容分发CDN": "templates/aliyun_cdn.json", "内容分发CDN": {"template": "templates/aliyun_cdn.json", "mapping": "CDN"},
"负载均衡SLB": "templates/aliyun_slb.json", "负载均衡SLB": {"template": "templates/aliyun_slb.json", "mapping": "loadbalancer"},
"专有网络VPC": "templates/aliyun_vpc.json", "专有网络VPC": {"template": "templates/aliyun_vpc.json", "mapping": "vpc"},
"交换机Switch": "templates/aliyun_switch.json", "交换机Switch": {"template": "templates/aliyun_switch.json", "mapping": "vswitch"},
}, },
"collect_key_map": { "collect_key_map": {
"内容分发CDN": "ali.cdn", "内容分发CDN": "ali.cdn",
@@ -73,8 +84,8 @@ ClOUD_MAP = {
"category": "存储", "category": "存储",
"items": ["块存储EBS", "对象存储OSS"], "items": ["块存储EBS", "对象存储OSS"],
"map": { "map": {
"块存储EBS": "templates/aliyun_ebs.json", "块存储EBS": {"template": "templates/aliyun_ebs.json", "mapping": "evs"},
"对象存储OSS": "templates/aliyun_oss.json", "对象存储OSS": {"template": "templates/aliyun_oss.json", "mapping": "objectStorage"},
}, },
"collect_key_map": { "collect_key_map": {
"块存储EBS": "ali.ebs", "块存储EBS": "ali.ebs",
@@ -85,9 +96,9 @@ ClOUD_MAP = {
"category": "数据库", "category": "数据库",
"items": ["云数据库RDS MySQL", "云数据库RDS PostgreSQL", "云数据库 Redis"], "items": ["云数据库RDS MySQL", "云数据库RDS PostgreSQL", "云数据库 Redis"],
"map": { "map": {
"云数据库RDS MySQL": "templates/aliyun_rds_mysql.json", "云数据库RDS MySQL": {"template": "templates/aliyun_rds_mysql.json", "mapping": "mysql"},
"云数据库RDS PostgreSQL": "templates/aliyun_rds_postgre.json", "云数据库RDS PostgreSQL": {"template": "templates/aliyun_rds_postgre.json", "mapping": "postgresql"},
"云数据库 Redis": "templates/aliyun_redis.json", "云数据库 Redis": {"template": "templates/aliyun_redis.json", "mapping": "redis"},
}, },
"collect_key_map": { "collect_key_map": {
"云数据库RDS MySQL": "ali.rds_mysql", "云数据库RDS MySQL": "ali.rds_mysql",
@@ -101,7 +112,7 @@ ClOUD_MAP = {
"category": "计算", "category": "计算",
"items": ["云服务器 CVM"], "items": ["云服务器 CVM"],
"map": { "map": {
"云服务器 CVM": "templates/tencent_cvm.json", "云服务器 CVM": {"template": "templates/tencent_cvm.json", "mapping": "ecs"},
}, },
"collect_key_map": { "collect_key_map": {
"云服务器 CVM": "tencent.cvm", "云服务器 CVM": "tencent.cvm",
@@ -111,7 +122,7 @@ ClOUD_MAP = {
"category": "CDN与边缘", "category": "CDN与边缘",
"items": ["内容分发CDN"], "items": ["内容分发CDN"],
"map": { "map": {
"内容分发CDN": "templates/tencent_cdn.json", "内容分发CDN": {"template": "templates/tencent_cdn.json", "mapping": "CDN"},
}, },
"collect_key_map": { "collect_key_map": {
"内容分发CDN": "tencent.cdn", "内容分发CDN": "tencent.cdn",
@@ -121,9 +132,9 @@ ClOUD_MAP = {
"category": "网络", "category": "网络",
"items": ["负载均衡CLB", "私有网络VPC", "子网"], "items": ["负载均衡CLB", "私有网络VPC", "子网"],
"map": { "map": {
"负载均衡CLB": "templates/tencent_clb.json", "负载均衡CLB": {"template": "templates/tencent_clb.json", "mapping": "loadbalancer"},
"私有网络VPC": "templates/tencent_vpc.json", "私有网络VPC": {"template": "templates/tencent_vpc.json", "mapping": "vpc"},
"子网": "templates/tencent_subnet.json", "子网": {"template": "templates/tencent_subnet.json", "mapping": "vswitch"},
}, },
"collect_key_map": { "collect_key_map": {
"负载均衡CLB": "tencent.clb", "负载均衡CLB": "tencent.clb",
@@ -135,21 +146,21 @@ ClOUD_MAP = {
"category": "存储", "category": "存储",
"items": ["云硬盘CBS", "对象存储COS"], "items": ["云硬盘CBS", "对象存储COS"],
"map": { "map": {
"云硬盘CBS": "templates/tencent_cbs.json", "云硬盘CBS": {"template": "templates/tencent_cbs.json", "mapping": "evs"},
"对象存储OSS": "templates/tencent_cos.json", "对象存储COS": {"template": "templates/tencent_cos.json", "mapping": "objectStorage"},
}, },
"collect_key_map": { "collect_key_map": {
"云硬盘CBS": "tencent.cbs", "云硬盘CBS": "tencent.cbs",
"对象存储OSS": "tencent.cos", "对象存储COS": "tencent.cos",
}, },
}, },
{ {
"category": "数据库", "category": "数据库",
"items": ["云数据库 MySQL", "云数据库 PostgreSQL", "云数据库 Redis"], "items": ["云数据库 MySQL", "云数据库 PostgreSQL", "云数据库 Redis"],
"map": { "map": {
"云数据库 MySQL": "templates/tencent_rdb.json", "云数据库 MySQL": {"template": "templates/tencent_rdb.json", "mapping": "mysql"},
"云数据库 PostgreSQL": "templates/tencent_postgres.json", "云数据库 PostgreSQL": {"template": "templates/tencent_postgres.json", "mapping": "postgresql"},
"云数据库 Redis": "templates/tencent_redis.json", "云数据库 Redis": {"template": "templates/tencent_redis.json", "mapping": "redis"},
}, },
"collect_key_map": { "collect_key_map": {
"云数据库 MySQL": "tencent.rdb", "云数据库 MySQL": "tencent.rdb",
@@ -163,7 +174,7 @@ ClOUD_MAP = {
"category": "计算", "category": "计算",
"items": ["云服务器 ECS"], "items": ["云服务器 ECS"],
"map": { "map": {
"云服务器 ECS": "templates/huaweicloud_ecs.json", "云服务器 ECS": {"template": "templates/huaweicloud_ecs.json", "mapping": "ecs"},
}, },
"collect_key_map": { "collect_key_map": {
"云服务器 ECS": "huawei.ecs", "云服务器 ECS": "huawei.ecs",
@@ -173,7 +184,7 @@ ClOUD_MAP = {
"category": "CDN与智能边缘", "category": "CDN与智能边缘",
"items": ["内容分发网络CDN"], "items": ["内容分发网络CDN"],
"map": { "map": {
"内容分发网络CDN": "templates/huawei_cdn.json", "内容分发网络CDN": {"template": "templates/huawei_cdn.json", "mapping": "CDN"},
}, },
"collect_key_map": { "collect_key_map": {
"内容分发网络CDN": "huawei.cdn", "内容分发网络CDN": "huawei.cdn",
@@ -183,9 +194,9 @@ ClOUD_MAP = {
"category": "网络", "category": "网络",
"items": ["弹性负载均衡ELB", "虚拟私有云VPC", "子网"], "items": ["弹性负载均衡ELB", "虚拟私有云VPC", "子网"],
"map": { "map": {
"弹性负载均衡ELB": "templates/huawei_elb.json", "弹性负载均衡ELB": {"template": "templates/huawei_elb.json", "mapping": "loadbalancer"},
"虚拟私有云VPC": "templates/huawei_vpc.json", "虚拟私有云VPC": {"template": "templates/huawei_vpc.json", "mapping": "vpc"},
"子网": "templates/huawei_subnet.json", "子网": {"template": "templates/huawei_subnet.json", "mapping": "vswitch"},
}, },
"collect_key_map": { "collect_key_map": {
"弹性负载均衡ELB": "huawei.elb", "弹性负载均衡ELB": "huawei.elb",
@@ -197,8 +208,8 @@ ClOUD_MAP = {
"category": "存储", "category": "存储",
"items": ["云硬盘EVS", "对象存储OBS"], "items": ["云硬盘EVS", "对象存储OBS"],
"map": { "map": {
"云硬盘EVS": "templates/huawei_evs.json", "云硬盘EVS": {"template": "templates/huawei_evs.json", "mapping": "evs"},
"对象存储OBS": "templates/huawei_obs.json", "对象存储OBS": {"template": "templates/huawei_obs.json", "mapping": "objectStorage"},
}, },
"collect_key_map": { "collect_key_map": {
"云硬盘EVS": "huawei.evs", "云硬盘EVS": "huawei.evs",
@@ -209,8 +220,8 @@ ClOUD_MAP = {
"category": "数据库", "category": "数据库",
"items": ["云数据库RDS MySQL", "云数据库RDS PostgreSQL"], "items": ["云数据库RDS MySQL", "云数据库RDS PostgreSQL"],
"map": { "map": {
"云数据库RDS MySQL": "templates/huawei_rds_mysql.json", "云数据库RDS MySQL": {"template": "templates/huawei_rds_mysql.json", "mapping": "mysql"},
"云数据库RDSPostgreSQL": "templates/huaweirds_postgre.json", "云数据库RDS PostgreSQL": {"template": "templates/huawei_rds_postgre.json", "mapping": "postgresql"},
}, },
"collect_key_map": { "collect_key_map": {
"云数据库RDS MySQL": "huawei.rds_mysql", "云数据库RDS MySQL": "huawei.rds_mysql",
@@ -221,7 +232,7 @@ ClOUD_MAP = {
"category": "应用中间件", "category": "应用中间件",
"items": ["分布式缓存Redis"], "items": ["分布式缓存Redis"],
"map": { "map": {
"分布式缓存Redis": "templates/huawei_dcs.json", "分布式缓存Redis": {"template": "templates/huawei_dcs.json", "mapping": "redis"},
}, },
"collect_key_map": { "collect_key_map": {
"分布式缓存Redis": "huawei.dcs", "分布式缓存Redis": "huawei.dcs",
@@ -233,7 +244,7 @@ ClOUD_MAP = {
"category": "计算", "category": "计算",
"items": ["云服务器 EC2"], "items": ["云服务器 EC2"],
"map": { "map": {
"云服务器 EC2": "templates/aws_ec2.json", "云服务器 EC2": {"template": "templates/aws_ec2.json", "mapping": "ecs"},
}, },
"collect_key_map": { "collect_key_map": {
"云服务器 EC2": "aws.ec2", "云服务器 EC2": "aws.ec2",
@@ -283,7 +294,7 @@ ClOUD_MAP = {
"items": ["数据存储", "数据存储集群"], "items": ["数据存储", "数据存储集群"],
"map": { "map": {
"数据存储": "templates/vsphere_datastore.json", "数据存储": "templates/vsphere_datastore.json",
"数据存储集群": "templates/vsphere.storage_pod.json", "数据存储集群": "templates/vsphere_storage_pod.json",
}, },
"collect_key_map": { "collect_key_map": {
"数据存储": "vsphere.datastore", "数据存储": "vsphere.datastore",
@@ -294,7 +305,7 @@ ClOUD_MAP = {
"category": "其他", "category": "其他",
"items": ["资源池", "数据中心", "文件夹"], "items": ["资源池", "数据中心", "文件夹"],
"map": { "map": {
"资源池": "templates/vsphere_datastore.json", "资源池": "templates/vsphere_pool.json",
"数据中心": "templates/vsphere_datacenter.json", "数据中心": "templates/vsphere_datacenter.json",
"文件夹": "templates/vsphere_folder.json", "文件夹": "templates/vsphere_folder.json",
}, },

View File

@@ -2,10 +2,13 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import defaultdict
import datetime import datetime
import os
import yaml
from flask import current_app from flask import current_app
import json
from api.extensions import cache from api.extensions import cache
from api.extensions import db from api.extensions import db
from api.lib.cmdb.custom_dashboard import CustomDashboardManager from api.lib.cmdb.custom_dashboard import CustomDashboardManager
@@ -252,7 +255,7 @@ class CMDBCounterCache(object):
@classmethod @classmethod
def set(cls, result): def set(cls, result):
cache.set(cls.KEY, result, timeout=0) cache.set(cls.KEY, json.loads(json.dumps(result)), timeout=0)
@classmethod @classmethod
def reset(cls): def reset(cls):
@@ -274,7 +277,7 @@ class CMDBCounterCache(object):
cls.set(result) cls.set(result)
return result return json.loads(json.dumps(result))
@classmethod @classmethod
def update(cls, custom, flush=True): def update(cls, custom, flush=True):
@@ -296,25 +299,36 @@ class CMDBCounterCache(object):
result[custom['id']] = res result[custom['id']] = res
cls.set(result) cls.set(result)
return res return json.loads(json.dumps(res))
@staticmethod @classmethod
def relation_counter(type_id, level, other_filer, type_ids): def relation_counter(cls, type_id, level, other_filer, type_ids):
from api.lib.cmdb.search.ci_relation.search import Search as RelSearch from api.lib.cmdb.search.ci_relation.search import Search as RelSearch
from api.lib.cmdb.search import SearchError from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search from api.lib.cmdb.search.ci import search
from api.lib.cmdb.attribute import AttributeManager
query = "_type:{}".format(type_id) query = "_type:{}".format(type_id)
if other_filer:
query = "{},{}".format(query, other_filer)
s = search(query, count=1000000) s = search(query, count=1000000)
try: try:
type_names, _, _, _, _, _ = s.search() type_names, _, _, _, _, _ = s.search()
except SearchError as e: except SearchError as e:
current_app.logger.error(e) current_app.logger.error(e)
return return
root_type = CITypeCache.get(type_id)
show_attr_id = root_type and root_type.show_id
show_attr = AttributeCache.get(show_attr_id)
type_id_names = [(str(i.get('_id')), i.get(i.get('unique'))) for i in type_names] type_id_names = []
for i in type_names:
attr_value = i.get(show_attr and show_attr.name) or i.get(i.get('unique'))
enum_map = AttributeManager.get_enum_map(show_attr_id or i.get('unique'))
s = RelSearch([i[0] for i in type_id_names], level, other_filer or '') type_id_names.append((str(i.get('_id')), enum_map.get(attr_value, attr_value)))
s = RelSearch([i[0] for i in type_id_names], level)
try: try:
stats = s.statistics(type_ids, need_filter=False) stats = s.statistics(type_ids, need_filter=False)
except SearchError as e: except SearchError as e:
@@ -344,11 +358,12 @@ class CMDBCounterCache(object):
return result return result
@staticmethod @classmethod
def attribute_counter(custom): def attribute_counter(cls, custom):
from api.lib.cmdb.search import SearchError from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search from api.lib.cmdb.search.ci import search
from api.lib.cmdb.utils import ValueTypeMap from api.lib.cmdb.utils import ValueTypeMap
from api.lib.cmdb.attribute import AttributeManager
custom.setdefault('options', {}) custom.setdefault('options', {})
type_id = custom.get('type_id') type_id = custom.get('type_id')
@@ -364,16 +379,24 @@ class CMDBCounterCache(object):
other_filter = "{}".format(other_filter) if other_filter else '' other_filter = "{}".format(other_filter) if other_filter else ''
if custom['options'].get('ret') == 'cis': if custom['options'].get('ret') == 'cis':
enum_map = {}
for _attr_id in attr_ids:
_attr = AttributeCache.get(_attr_id)
if _attr:
enum_map[_attr.alias] = AttributeManager.get_enum_map(_attr_id)
query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter) query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter)
s = search(query, fl=attr_ids, ret_key='alias', count=100) s = search(query, fl=attr_ids, ret_key='alias', count=100)
try: try:
cis, _, _, _, _, _ = s.search() cis, _, _, _, _, _ = s.search()
cis = [{k: (enum_map.get(k) or {}).get(v, v) for k, v in ci.items()} for ci in cis]
except SearchError as e: except SearchError as e:
current_app.logger.error(e) current_app.logger.error(e)
return return
return cis return cis
origin_result = dict()
result = dict() result = dict()
# level = 1 # level = 1
query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter) query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter)
@@ -383,13 +406,18 @@ class CMDBCounterCache(object):
except SearchError as e: except SearchError as e:
current_app.logger.error(e) current_app.logger.error(e)
return return
enum_map1 = AttributeManager.get_enum_map(attr_ids[0])
for i in (list(facet.values()) or [[]])[0]: for i in (list(facet.values()) or [[]])[0]:
result[ValueTypeMap.serialize2[attr2value_type[0]](str(i[0]))] = i[1] k = ValueTypeMap.serialize2[attr2value_type[0]](str(i[0]))
result[enum_map1.get(k, k)] = i[1]
origin_result[k] = i[1]
if len(attr_ids) == 1: if len(attr_ids) == 1:
return result return result
# level = 2 # level = 2
for v in result: enum_map2 = AttributeManager.get_enum_map(attr_ids[1])
for v in origin_result:
query = "_type:({}),{},{}:{}".format(";".join(map(str, type_ids)), other_filter, attr_ids[0], v) query = "_type:({}),{},{}:{}".format(";".join(map(str, type_ids)), other_filter, attr_ids[0], v)
s = search(query, fl=attr_ids, facet=[attr_ids[1]], count=1) s = search(query, fl=attr_ids, facet=[attr_ids[1]], count=1)
try: try:
@@ -397,18 +425,22 @@ class CMDBCounterCache(object):
except SearchError as e: except SearchError as e:
current_app.logger.error(e) current_app.logger.error(e)
return return
result[v] = dict() result[enum_map1.get(v, v)] = dict()
origin_result[v] = dict()
for i in (list(facet.values()) or [[]])[0]: for i in (list(facet.values()) or [[]])[0]:
result[v][ValueTypeMap.serialize2[attr2value_type[1]](str(i[0]))] = i[1] k = ValueTypeMap.serialize2[attr2value_type[1]](str(i[0]))
result[enum_map1.get(v, v)][enum_map2.get(k, k)] = i[1]
origin_result[v][k] = i[1]
if len(attr_ids) == 2: if len(attr_ids) == 2:
return result return result
# level = 3 # level = 3
for v1 in result: enum_map3 = AttributeManager.get_enum_map(attr_ids[2])
if not isinstance(result[v1], dict): for v1 in origin_result:
if not isinstance(result[enum_map1.get(v1, v1)], dict):
continue continue
for v2 in result[v1]: for v2 in origin_result[v1]:
query = "_type:({}),{},{}:{},{}:{}".format(";".join(map(str, type_ids)), other_filter, query = "_type:({}),{},{}:{},{}:{}".format(";".join(map(str, type_ids)), other_filter,
attr_ids[0], v1, attr_ids[1], v2) attr_ids[0], v1, attr_ids[1], v2)
s = search(query, fl=attr_ids, facet=[attr_ids[2]], count=1) s = search(query, fl=attr_ids, facet=[attr_ids[2]], count=1)
@@ -417,9 +449,10 @@ class CMDBCounterCache(object):
except SearchError as e: except SearchError as e:
current_app.logger.error(e) current_app.logger.error(e)
return return
result[v1][v2] = dict() result[enum_map1.get(v1, v1)][enum_map2.get(v2, v2)] = dict()
for i in (list(facet.values()) or [[]])[0]: for i in (list(facet.values()) or [[]])[0]:
result[v1][v2][ValueTypeMap.serialize2[attr2value_type[2]](str(i[0]))] = i[1] k = ValueTypeMap.serialize2[attr2value_type[2]](str(i[0]))
result[enum_map1.get(v1, v1)][enum_map2.get(v2, v2)][enum_map3.get(k, k)] = i[1]
return result return result
@@ -523,19 +556,18 @@ class CMDBCounterCache(object):
@classmethod @classmethod
def flush_sub_counter(cls): def flush_sub_counter(cls):
result = dict(type_id2users=dict()) result = dict(type_id2users=defaultdict(list))
types = db.session.query(PreferenceShowAttributes.type_id, types = db.session.query(PreferenceShowAttributes.type_id,
PreferenceShowAttributes.uid, PreferenceShowAttributes.created_at).filter( PreferenceShowAttributes.uid, PreferenceShowAttributes.created_at).filter(
PreferenceShowAttributes.deleted.is_(False)).group_by( PreferenceShowAttributes.deleted.is_(False)).group_by(
PreferenceShowAttributes.uid, PreferenceShowAttributes.type_id) PreferenceShowAttributes.uid, PreferenceShowAttributes.type_id)
for i in types: for i in types:
result['type_id2users'].setdefault(i.type_id, []).append(i.uid) result['type_id2users'][i.type_id].append(i.uid)
types = PreferenceTreeView.get_by(to_dict=False) types = PreferenceTreeView.get_by(to_dict=False)
for i in types: for i in types:
result['type_id2users'].setdefault(i.type_id, [])
if i.uid not in result['type_id2users'][i.type_id]: if i.uid not in result['type_id2users'][i.type_id]:
result['type_id2users'][i.type_id].append(i.uid) result['type_id2users'][i.type_id].append(i.uid)
@@ -546,3 +578,21 @@ class CMDBCounterCache(object):
@classmethod @classmethod
def get_sub_counter(cls): def get_sub_counter(cls):
return cache.get(cls.KEY3) or cls.flush_sub_counter() return cache.get(cls.KEY3) or cls.flush_sub_counter()
class AutoDiscoveryMappingCache(object):
PREFIX = 'CMDB::AutoDiscovery::Mapping::{}'
@classmethod
def get(cls, name):
res = cache.get(cls.PREFIX.format(name)) or {}
if not res:
path = os.path.join(os.path.abspath(os.path.dirname(__file__)),
"auto_discovery/mapping/{}.yaml".format(name))
if os.path.exists(path):
with open(path, 'r') as f:
mapping = yaml.safe_load(f)
res = mapping.get('mapping') or {}
res and cache.set(cls.PREFIX.format(name), res, timeout=0)
return res

View File

@@ -4,12 +4,12 @@
import copy import copy
import datetime import datetime
import json import json
import threading
import redis_lock import redis_lock
import threading
from flask import abort from flask import abort
from flask import current_app from flask import current_app
from flask_login import current_user from flask_login import current_user
from sqlalchemy.orm import aliased
from werkzeug.exceptions import BadRequest from werkzeug.exceptions import BadRequest
from api.extensions import db from api.extensions import db
@@ -28,6 +28,7 @@ from api.lib.cmdb.const import ExistPolicy
from api.lib.cmdb.const import OperateType from api.lib.cmdb.const import OperateType
from api.lib.cmdb.const import PermEnum from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import REDIS_PREFIX_CI from api.lib.cmdb.const import REDIS_PREFIX_CI
from api.lib.cmdb.const import RelationSourceEnum
from api.lib.cmdb.const import ResourceTypeEnum from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RetKey from api.lib.cmdb.const import RetKey
from api.lib.cmdb.const import ValueTypeEnum from api.lib.cmdb.const import ValueTypeEnum
@@ -160,7 +161,7 @@ class CIManager(object):
@classmethod @classmethod
def get_ci_by_id_from_db(cls, ci_id, ret_key=RetKey.NAME, fields=None, need_children=True, use_master=False, def get_ci_by_id_from_db(cls, ci_id, ret_key=RetKey.NAME, fields=None, need_children=True, use_master=False,
valid=False): valid=False, enum_use_label=False):
""" """
:param ci_id: :param ci_id:
@@ -169,6 +170,7 @@ class CIManager(object):
:param need_children: :param need_children:
:param use_master: whether to use master db :param use_master: whether to use master db
:param valid: :param valid:
:param enum_use_label:
:return: :return:
""" """
@@ -186,13 +188,19 @@ class CIManager(object):
res["ci_type"] = ci_type.name res["ci_type"] = ci_type.name
fields = CITypeAttributeManager.get_attr_names_by_type_id(ci.type_id) if not fields else fields enum_map = dict()
if not enum_use_label:
fields = CITypeAttributeManager.get_attr_names_by_type_id(ci.type_id) if not fields else fields
else:
fields, enum_map = CITypeAttributeManager.get_attr_names_label_enum(
ci.type_id) if not fields else (fields, {})
unique_key = AttributeCache.get(ci_type.unique_id) unique_key = AttributeCache.get(ci_type.unique_id)
_res = AttributeValueManager().get_attr_values(fields, _res = AttributeValueManager().get_attr_values(fields,
ci_id, ci_id,
ret_key=ret_key, ret_key=ret_key,
unique_key=unique_key, unique_key=unique_key,
use_master=use_master) use_master=use_master,
enum_map=enum_map)
res.update(_res) res.update(_res)
res['_type'] = ci_type.id res['_type'] = ci_type.id
@@ -217,7 +225,7 @@ class CIManager(object):
@classmethod @classmethod
def get_ad_statistics(cls): def get_ad_statistics(cls):
return CMDBCounterCache.get_adc_counter() return CMDBCounterCache.get_adc_counter() or {}
@staticmethod @staticmethod
def ci_is_exist(unique_key, unique_value, type_id): def ci_is_exist(unique_key, unique_value, type_id):
@@ -265,7 +273,7 @@ class CIManager(object):
value_table = TableMap(attr_name=id2name[attr_id]).table value_table = TableMap(attr_name=id2name[attr_id]).table
values = value_table.get_by(attr_id=attr_id, values = value_table.get_by(attr_id=attr_id,
value=ci_dict.get(id2name[attr_id]) or None, value=ci_dict.get(id2name[attr_id]),
only_query=True).join( only_query=True).join(
CI, CI.id == value_table.ci_id).filter(CI.type_id == type_id) CI, CI.id == value_table.ci_id).filter(CI.type_id == type_id)
_ci_ids = set([i.ci_id for i in values]) _ci_ids = set([i.ci_id for i in values])
@@ -291,6 +299,53 @@ class CIManager(object):
return 1 return 1
@staticmethod
def _reference_to_ci_id(attr, payload):
def __unique_value2id(_type, _v):
value_table = TableMap(attr_name=_type.unique_id).table
ci = value_table.get_by(attr_id=attr.id, value=_v)
if ci is not None:
return ci.ci_id
return abort(400, ErrFormat.ci_reference_invalid.format(attr.alias, _v))
def __valid_reference_id_existed(_id, _type_id):
ci = CI.get_by_id(_id) or abort(404, ErrFormat.ci_reference_not_found.format(attr.alias, _id))
if ci.type_id != _type_id:
return abort(400, ErrFormat.ci_reference_invalid.format(attr.alias, _id))
if attr.name in payload:
k, reference_value = attr.name, payload[attr.name]
elif attr.alias in payload:
k, reference_value = attr.alias, payload[attr.alias]
else:
return
if not reference_value:
return
reference_type = None
if isinstance(reference_value, list):
for idx, v in enumerate(reference_value):
if isinstance(v, dict) and v.get('unique'):
if reference_type is None:
reference_type = CITypeCache.get(attr.reference_type_id)
if reference_type is not None:
reference_value[idx] = __unique_value2id(reference_type, v)
else:
__valid_reference_id_existed(v, attr.reference_type_id)
elif isinstance(reference_value, dict) and reference_value.get('unique'):
if reference_type is None:
reference_type = CITypeCache.get(attr.reference_type_id)
if reference_type is not None:
reference_value = __unique_value2id(reference_type, reference_value)
elif str(reference_value).isdigit():
reference_value = int(reference_value)
__valid_reference_id_existed(reference_value, attr.reference_type_id)
payload[k] = reference_value
@classmethod @classmethod
def add(cls, ci_type_name, def add(cls, ci_type_name,
exist_policy=ExistPolicy.REPLACE, exist_policy=ExistPolicy.REPLACE,
@@ -318,8 +373,8 @@ class CIManager(object):
400, ErrFormat.unique_value_not_found.format("unique_id={}".format(ci_type.unique_id))) 400, ErrFormat.unique_value_not_found.format("unique_id={}".format(ci_type.unique_id)))
unique_value = None unique_value = None
if not (unique_key.default and unique_key.default.get('default') == AttributeDefaultValueEnum.AUTO_INC_ID and # primary key is not auto inc id
not ci_dict.get(unique_key.name)): # primary key is not auto inc id if not (unique_key.default and unique_key.default.get('default') == AttributeDefaultValueEnum.AUTO_INC_ID):
unique_value = ci_dict.get(unique_key.name) or ci_dict.get(unique_key.alias) or ci_dict.get(unique_key.id) unique_value = ci_dict.get(unique_key.name) or ci_dict.get(unique_key.alias) or ci_dict.get(unique_key.id)
unique_value = unique_value or abort(400, ErrFormat.unique_key_required.format(unique_key.name)) unique_value = unique_value or abort(400, ErrFormat.unique_key_required.format(unique_key.name))
@@ -327,6 +382,7 @@ class CIManager(object):
ci_type_attrs_name = {attr.name: attr for _, attr in attrs} ci_type_attrs_name = {attr.name: attr for _, attr in attrs}
ci_type_attrs_alias = {attr.alias: attr for _, attr in attrs} ci_type_attrs_alias = {attr.alias: attr for _, attr in attrs}
ci_attr2type_attr = {type_attr.attr_id: type_attr for type_attr, _ in attrs} ci_attr2type_attr = {type_attr.attr_id: type_attr for type_attr, _ in attrs}
ci_type_attrs_name_alias = {**ci_type_attrs_name, **ci_type_attrs_alias}
ci = None ci = None
record_id = None record_id = None
@@ -381,6 +437,7 @@ class CIManager(object):
for _, attr in attrs: for _, attr in attrs:
if attr.is_computed: if attr.is_computed:
computed_attrs.append(attr.to_dict()) computed_attrs.append(attr.to_dict())
ci_dict[attr.name] = None
elif attr.is_password: elif attr.is_password:
if attr.name in ci_dict: if attr.name in ci_dict:
password_dict[attr.id] = (ci_dict.pop(attr.name), attr.is_dynamic) password_dict[attr.id] = (ci_dict.pop(attr.name), attr.is_dynamic)
@@ -388,10 +445,9 @@ class CIManager(object):
password_dict[attr.id] = (ci_dict.pop(attr.alias), attr.is_dynamic) password_dict[attr.id] = (ci_dict.pop(attr.alias), attr.is_dynamic)
if attr.re_check and password_dict.get(attr.id): if attr.re_check and password_dict.get(attr.id):
value_manager.check_re(attr.re_check, password_dict[attr.id][0]) value_manager.check_re(attr.re_check, attr.alias, password_dict[attr.id][0])
elif attr.is_reference:
if computed_attrs: cls._reference_to_ci_id(attr, ci_dict)
value_manager.handle_ci_compute_attributes(ci_dict, computed_attrs, ci)
cls._valid_unique_constraint(ci_type.id, ci_dict, ci and ci.id) cls._valid_unique_constraint(ci_type.id, ci_dict, ci and ci.id)
@@ -413,11 +469,14 @@ class CIManager(object):
else: else:
ci_dict.pop(k) ci_dict.pop(k)
ci_dict = {k: v for k, v in ci_dict.items() if k in ci_type_attrs_name or k in ci_type_attrs_alias} ci_dict = {ci_type_attrs_name_alias[k].name: v for k, v in ci_dict.items() if k in ci_type_attrs_name_alias}
key2attr = value_manager.valid_attr_value(ci_dict, ci_type.id, ci and ci.id, key2attr = value_manager.valid_attr_value(ci_dict, ci_type.id, ci and ci.id,
ci_type_attrs_name, ci_type_attrs_alias, ci_attr2type_attr) ci_type_attrs_name, ci_type_attrs_alias, ci_attr2type_attr)
if computed_attrs:
value_manager.handle_ci_compute_attributes(ci_dict, computed_attrs, ci)
operate_type = OperateType.UPDATE if ci is not None else OperateType.ADD operate_type = OperateType.UPDATE if ci is not None else OperateType.ADD
try: try:
ci = ci or CI.create(type_id=ci_type.id, is_auto_discovery=is_auto_discovery) ci = ci or CI.create(type_id=ci_type.id, is_auto_discovery=is_auto_discovery)
@@ -440,9 +499,10 @@ class CIManager(object):
return ci.id return ci.id
def update(self, ci_id, _is_admin=False, ticket_id=None, __sync=False, **ci_dict): def update(self, ci_id, _is_admin=False, ticket_id=None, _sync=False, **ci_dict):
now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
ci = self.confirm_ci_existed(ci_id) ci = self.confirm_ci_existed(ci_id)
ci_type = ci.ci_type
attrs = CITypeAttributeManager.get_all_attributes(ci.type_id) attrs = CITypeAttributeManager.get_all_attributes(ci.type_id)
ci_type_attrs_name = {attr.name: attr for _, attr in attrs} ci_type_attrs_name = {attr.name: attr for _, attr in attrs}
@@ -463,6 +523,7 @@ class CIManager(object):
for _, attr in attrs: for _, attr in attrs:
if attr.is_computed: if attr.is_computed:
computed_attrs.append(attr.to_dict()) computed_attrs.append(attr.to_dict())
ci_dict[attr.name] = None
elif attr.is_password: elif attr.is_password:
if attr.name in ci_dict: if attr.name in ci_dict:
password_dict[attr.id] = (ci_dict.pop(attr.name), attr.is_dynamic) password_dict[attr.id] = (ci_dict.pop(attr.name), attr.is_dynamic)
@@ -470,15 +531,14 @@ class CIManager(object):
password_dict[attr.id] = (ci_dict.pop(attr.alias), attr.is_dynamic) password_dict[attr.id] = (ci_dict.pop(attr.alias), attr.is_dynamic)
if attr.re_check and password_dict.get(attr.id): if attr.re_check and password_dict.get(attr.id):
value_manager.check_re(attr.re_check, password_dict[attr.id][0]) value_manager.check_re(attr.re_check, attr.alias, password_dict[attr.id][0])
elif attr.is_reference:
if computed_attrs: self._reference_to_ci_id(attr, ci_dict)
value_manager.handle_ci_compute_attributes(ci_dict, computed_attrs, ci)
limit_attrs = self._valid_ci_for_no_read(ci) if not _is_admin else {} limit_attrs = self._valid_ci_for_no_read(ci) if not _is_admin else {}
record_id = None record_id = None
with redis_lock.Lock(rd.r, ci.ci_type.name): with redis_lock.Lock(rd.r, ci_type.name):
db.session.commit() db.session.commit()
self._valid_unique_constraint(ci.type_id, ci_dict, ci_id) self._valid_unique_constraint(ci.type_id, ci_dict, ci_id)
@@ -486,6 +546,10 @@ class CIManager(object):
ci_dict = {k: v for k, v in ci_dict.items() if k in ci_type_attrs_name} ci_dict = {k: v for k, v in ci_dict.items() if k in ci_type_attrs_name}
key2attr = value_manager.valid_attr_value(ci_dict, ci.type_id, ci.id, ci_type_attrs_name, key2attr = value_manager.valid_attr_value(ci_dict, ci.type_id, ci.id, ci_type_attrs_name,
ci_attr2type_attr=ci_attr2type_attr) ci_attr2type_attr=ci_attr2type_attr)
if computed_attrs:
value_manager.handle_ci_compute_attributes(ci_dict, computed_attrs, ci)
if limit_attrs: if limit_attrs:
for k in copy.deepcopy(ci_dict): for k in copy.deepcopy(ci_dict):
if k not in limit_attrs: if k not in limit_attrs:
@@ -505,14 +569,14 @@ class CIManager(object):
record_id = self.save_password(ci.id, attr_id, password_dict[attr_id], record_id, ci.type_id) record_id = self.save_password(ci.id, attr_id, password_dict[attr_id], record_id, ci.type_id)
if record_id or has_dynamic: # has changed if record_id or has_dynamic: # has changed
if not __sync: if not _sync:
ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE) ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE)
else: else:
ci_cache(ci_id, OperateType.UPDATE, record_id) ci_cache(ci_id, OperateType.UPDATE, record_id)
ref_ci_dict = {k: v for k, v in ci_dict.items() if k.startswith("$") and "." in k} ref_ci_dict = {k: v for k, v in ci_dict.items() if k.startswith("$") and "." in k}
if ref_ci_dict: if ref_ci_dict:
if not __sync: if not _sync:
ci_relation_add.apply_async(args=(ref_ci_dict, ci.id), queue=CMDB_QUEUE) ci_relation_add.apply_async(args=(ref_ci_dict, ci.id), queue=CMDB_QUEUE)
else: else:
ci_relation_add(ref_ci_dict, ci.id) ci_relation_add(ref_ci_dict, ci.id)
@@ -573,7 +637,7 @@ class CIManager(object):
if ci_dict: if ci_dict:
AttributeHistoryManger.add(None, ci_id, [(None, OperateType.DELETE, ci_dict, None)], ci.type_id) AttributeHistoryManger.add(None, ci_id, [(None, OperateType.DELETE, ci_dict, None)], ci.type_id)
ci_delete.apply_async(args=(ci_id,), queue=CMDB_QUEUE) ci_delete.apply_async(args=(ci_id, ci.type_id), queue=CMDB_QUEUE)
delete_id_filter.apply_async(args=(ci_id,), queue=CMDB_QUEUE) delete_id_filter.apply_async(args=(ci_id,), queue=CMDB_QUEUE)
return ci_id return ci_id
@@ -645,13 +709,18 @@ class CIManager(object):
elif fields: elif fields:
_res = [] _res = []
for d in res: for d in res:
if isinstance(fields, dict) and d.get("_type") not in fields:
_res.append(d)
continue
_d = dict() _d = dict()
_d["_id"], _d["_type"] = d.get("_id"), d.get("_type") _d["_id"], _d["_type"] = d.get("_id"), d.get("_type")
_d["ci_type"] = d.get("ci_type") _d["ci_type"] = d.get("ci_type")
if unique_required: if unique_required:
_d[d.get('unique')] = d.get(d.get('unique')) _d[d.get('unique')] = d.get(d.get('unique'))
for field in fields + ['ci_type_alias', 'unique', 'unique_alias']: _fields = list(fields.get(_d['_type']) or [] if isinstance(fields, dict) else fields)
for field in _fields + ['ci_type_alias', 'unique', 'unique_alias']:
_d[field] = d.get(field) _d[field] = d.get(field)
_res.append(_d) _res.append(_d)
return _res return _res
@@ -668,9 +737,8 @@ class CIManager(object):
from api.lib.cmdb.search.ci.db.query_sql import QUERY_CIS_BY_IDS from api.lib.cmdb.search.ci.db.query_sql import QUERY_CIS_BY_IDS
from api.lib.cmdb.search.ci.db.query_sql import QUERY_CIS_BY_VALUE_TABLE from api.lib.cmdb.search.ci.db.query_sql import QUERY_CIS_BY_VALUE_TABLE
if not fields: filter_fields_sql = ""
filter_fields_sql = "" if fields and isinstance(fields, list):
else:
_fields = list() _fields = list()
for field in fields: for field in fields:
attr = AttributeCache.get(field) attr = AttributeCache.get(field)
@@ -712,6 +780,10 @@ class CIManager(object):
ci_set.add(ci_id) ci_set.add(ci_id)
res[ci2pos[ci_id]] = ci_dict res[ci2pos[ci_id]] = ci_dict
if isinstance(fields, dict) and fields.get(type_id):
if attr_name not in fields[type_id]:
continue
if ret_key == RetKey.NAME: if ret_key == RetKey.NAME:
attr_key = attr_name attr_key = attr_name
elif ret_key == RetKey.ALIAS: elif ret_key == RetKey.ALIAS:
@@ -749,7 +821,7 @@ class CIManager(object):
if not ci_ids: if not ci_ids:
return [] return []
fields = [] if fields is None or not isinstance(fields, list) else fields fields = [] if not fields else fields
ci_id_tuple = tuple(map(int, ci_ids)) ci_id_tuple = tuple(map(int, ci_ids))
res = cls._get_cis_from_cache(ci_id_tuple, ret_key, fields, unique_required, excludes=excludes) res = cls._get_cis_from_cache(ci_id_tuple, ret_key, fields, unique_required, excludes=excludes)
@@ -768,7 +840,7 @@ class CIManager(object):
value_table = ValueTypeMap.table[ValueTypeEnum.PASSWORD] value_table = ValueTypeMap.table[ValueTypeEnum.PASSWORD]
if current_app.config.get('SECRETS_ENGINE') == 'inner': if current_app.config.get('SECRETS_ENGINE') == 'inner':
if value: if value:
encrypt_value, status = InnerCrypt().encrypt(value) encrypt_value, status = InnerCrypt().encrypt(str(value))
if not status: if not status:
current_app.logger.error('save password failed: {}'.format(encrypt_value)) current_app.logger.error('save password failed: {}'.format(encrypt_value))
return abort(400, ErrFormat.password_save_failed.format(encrypt_value)) return abort(400, ErrFormat.password_save_failed.format(encrypt_value))
@@ -796,7 +868,7 @@ class CIManager(object):
vault = VaultClient(current_app.config.get('VAULT_URL'), current_app.config.get('VAULT_TOKEN')) vault = VaultClient(current_app.config.get('VAULT_URL'), current_app.config.get('VAULT_TOKEN'))
if value: if value:
try: try:
vault.update("/{}/{}".format(ci_id, attr_id), dict(v=value)) vault.update("/{}/{}".format(ci_id, attr_id), dict(v=str(value)))
except Exception as e: except Exception as e:
current_app.logger.error('save password to vault failed: {}'.format(e)) current_app.logger.error('save password to vault failed: {}'.format(e))
return abort(400, ErrFormat.password_save_failed.format('write vault failed')) return abort(400, ErrFormat.password_save_failed.format('write vault failed'))
@@ -1133,7 +1205,14 @@ class CIRelationManager(object):
return abort(400, ErrFormat.relation_constraint.format("1-N")) return abort(400, ErrFormat.relation_constraint.format("1-N"))
@classmethod @classmethod
def add(cls, first_ci_id, second_ci_id, more=None, relation_type_id=None, ancestor_ids=None, valid=True): def add(cls, first_ci_id, second_ci_id,
more=None,
relation_type_id=None,
ancestor_ids=None,
valid=True,
apply_async=True,
source=None,
uid=None):
first_ci = CIManager.confirm_ci_existed(first_ci_id) first_ci = CIManager.confirm_ci_existed(first_ci_id)
second_ci = CIManager.confirm_ci_existed(second_ci_id) second_ci = CIManager.confirm_ci_existed(second_ci_id)
@@ -1145,9 +1224,10 @@ class CIRelationManager(object):
first=True) first=True)
if existed is not None: if existed is not None:
if existed.relation_type_id != relation_type_id and relation_type_id is not None: if existed.relation_type_id != relation_type_id and relation_type_id is not None:
existed.update(relation_type_id=relation_type_id) source = existed.source or source
existed.update(relation_type_id=relation_type_id, source=source)
CIRelationHistoryManager().add(existed, OperateType.UPDATE) CIRelationHistoryManager().add(existed, OperateType.UPDATE, uid=uid)
else: else:
if relation_type_id is None: if relation_type_id is None:
type_relation = CITypeRelation.get_by(parent_id=first_ci.type_id, type_relation = CITypeRelation.get_by(parent_id=first_ci.type_id,
@@ -1177,11 +1257,13 @@ class CIRelationManager(object):
existed = CIRelation.create(first_ci_id=first_ci_id, existed = CIRelation.create(first_ci_id=first_ci_id,
second_ci_id=second_ci_id, second_ci_id=second_ci_id,
relation_type_id=relation_type_id, relation_type_id=relation_type_id,
ancestor_ids=ancestor_ids) ancestor_ids=ancestor_ids,
source=source)
CIRelationHistoryManager().add(existed, OperateType.ADD) CIRelationHistoryManager().add(existed, OperateType.ADD, uid=uid)
if apply_async:
ci_relation_cache.apply_async(args=(first_ci_id, second_ci_id, ancestor_ids), queue=CMDB_QUEUE) ci_relation_cache.apply_async(args=(first_ci_id, second_ci_id, ancestor_ids), queue=CMDB_QUEUE)
else:
ci_relation_cache(first_ci_id, second_ci_id, ancestor_ids)
if more is not None: if more is not None:
existed.upadte(more=more) existed.upadte(more=more)
@@ -1189,7 +1271,7 @@ class CIRelationManager(object):
return existed.id return existed.id
@staticmethod @staticmethod
def delete(cr_id): def delete(cr_id, apply_async=True):
cr = CIRelation.get_by_id(cr_id) or abort(404, ErrFormat.relation_not_found.format("id={}".format(cr_id))) cr = CIRelation.get_by_id(cr_id) or abort(404, ErrFormat.relation_not_found.format("id={}".format(cr_id)))
if current_app.config.get('USE_ACL') and current_user.username != 'worker': if current_app.config.get('USE_ACL') and current_user.username != 'worker':
@@ -1205,8 +1287,12 @@ class CIRelationManager(object):
his_manager = CIRelationHistoryManager() his_manager = CIRelationHistoryManager()
his_manager.add(cr, operate_type=OperateType.DELETE) his_manager.add(cr, operate_type=OperateType.DELETE)
ci_relation_delete.apply_async(args=(cr.first_ci_id, cr.second_ci_id, cr.ancestor_ids), queue=CMDB_QUEUE) if apply_async:
delete_id_filter.apply_async(args=(cr.second_ci_id,), queue=CMDB_QUEUE) ci_relation_delete.apply_async(args=(cr.first_ci_id, cr.second_ci_id, cr.ancestor_ids), queue=CMDB_QUEUE)
delete_id_filter.apply_async(args=(cr.second_ci_id,), queue=CMDB_QUEUE)
else:
ci_relation_delete(cr.first_ci_id, cr.second_ci_id, cr.ancestor_ids)
delete_id_filter(cr.second_ci_id)
return cr_id return cr_id
@@ -1221,23 +1307,23 @@ class CIRelationManager(object):
if cr is not None: if cr is not None:
cls.delete(cr.id) cls.delete(cr.id)
ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id, ancestor_ids), queue=CMDB_QUEUE) # ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id, ancestor_ids), queue=CMDB_QUEUE)
delete_id_filter.apply_async(args=(second_ci_id,), queue=CMDB_QUEUE) # delete_id_filter.apply_async(args=(second_ci_id,), queue=CMDB_QUEUE)
return cr return cr
@classmethod @classmethod
def delete_3(cls, first_ci_id, second_ci_id): def delete_3(cls, first_ci_id, second_ci_id, apply_async=True):
cr = CIRelation.get_by(first_ci_id=first_ci_id, cr = CIRelation.get_by(first_ci_id=first_ci_id,
second_ci_id=second_ci_id, second_ci_id=second_ci_id,
to_dict=False, to_dict=False,
first=True) first=True)
if cr is not None: if cr is not None:
ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id, cr.ancestor_ids), queue=CMDB_QUEUE) # ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id, cr.ancestor_ids), queue=CMDB_QUEUE)
delete_id_filter.apply_async(args=(second_ci_id,), queue=CMDB_QUEUE) # delete_id_filter.apply_async(args=(second_ci_id,), queue=CMDB_QUEUE)
cls.delete(cr.id) cls.delete(cr.id, apply_async=apply_async)
return cr return cr
@@ -1276,6 +1362,27 @@ class CIRelationManager(object):
for ci_id in ci_ids: for ci_id in ci_ids:
cls.delete_2(parent_id, ci_id, ancestor_ids=ancestor_ids) cls.delete_2(parent_id, ci_id, ancestor_ids=ancestor_ids)
@classmethod
def delete_relations_by_source(cls, source,
first_ci_id=None, second_ci_type_id=None,
second_ci_id=None, first_ci_type_id=None,
added=None):
existed = []
if first_ci_id is not None and second_ci_type_id is not None:
existed = [(i.first_ci_id, i.second_ci_id) for i in CIRelation.get_by(
source=source, first_ci_id=first_ci_id, only_query=True).join(
CI, CIRelation.second_ci_id == CI.id).filter(CI.type_id == second_ci_type_id)]
if second_ci_id is not None and first_ci_type_id is not None:
existed = [(i.first_ci_id, i.second_ci_id) for i in CIRelation.get_by(
source=source, second_ci_id=second_ci_id, only_query=True).join(
CI, CIRelation.first_ci_id == CI.id).filter(CI.type_id == first_ci_type_id)]
deleted = set(existed) - set(added or [])
for first, second in deleted:
cls.delete_3(first, second, apply_async=False)
@classmethod @classmethod
def build_by_attribute(cls, ci_dict): def build_by_attribute(cls, ci_dict):
type_id = ci_dict['_type'] type_id = ci_dict['_type']
@@ -1296,8 +1403,15 @@ class CIRelationManager(object):
relations = _relations relations = _relations
else: else:
relations &= _relations relations &= _relations
cls.delete_relations_by_source(RelationSourceEnum.ATTRIBUTE_VALUES,
first_ci_id=ci_dict['_id'],
second_ci_type_id=item.child_id,
added=relations)
for parent_ci_id, child_ci_id in (relations or []): for parent_ci_id, child_ci_id in (relations or []):
CIRelationManager.add(parent_ci_id, child_ci_id, valid=False) cls.add(parent_ci_id, child_ci_id,
valid=False,
source=RelationSourceEnum.ATTRIBUTE_VALUES)
parent_items = CITypeRelation.get_by(child_id=type_id, only_query=True).filter( parent_items = CITypeRelation.get_by(child_id=type_id, only_query=True).filter(
CITypeRelation.child_attr_ids.isnot(None)) CITypeRelation.child_attr_ids.isnot(None))
@@ -1316,11 +1430,18 @@ class CIRelationManager(object):
relations = _relations relations = _relations
else: else:
relations &= _relations relations &= _relations
cls.delete_relations_by_source(RelationSourceEnum.ATTRIBUTE_VALUES,
second_ci_id=ci_dict['_id'],
first_ci_type_id=item.parent_id,
added=relations)
for parent_ci_id, child_ci_id in (relations or []): for parent_ci_id, child_ci_id in (relations or []):
CIRelationManager.add(parent_ci_id, child_ci_id, valid=False) cls.add(parent_ci_id, child_ci_id,
valid=False,
source=RelationSourceEnum.ATTRIBUTE_VALUES)
@classmethod @classmethod
def rebuild_all_by_attribute(cls, ci_type_relation): def rebuild_all_by_attribute(cls, ci_type_relation, uid):
relations = None relations = None
for parent_attr_id, child_attr_id in zip(ci_type_relation['parent_attr_ids'] or [], for parent_attr_id, child_attr_id in zip(ci_type_relation['parent_attr_ids'] or [],
ci_type_relation['child_attr_ids'] or []): ci_type_relation['child_attr_ids'] or []):
@@ -1352,11 +1473,29 @@ class CIRelationManager(object):
else: else:
relations &= _relations relations &= _relations
t1 = aliased(CI)
t2 = aliased(CI)
query = db.session.query(CIRelation).join(t1, t1.id == CIRelation.first_ci_id).join(
t2, t2.id == CIRelation.second_ci_id).filter(t1.type_id == ci_type_relation['parent_id']).filter(
t2.type_id == ci_type_relation['child_id'])
for i in query:
db.session.delete(i)
ci_relation_delete(i.first_ci_id, i.second_ci_id, i.ancestor_ids)
try:
db.session.commit()
except Exception as e:
current_app.logger.error(e)
db.session.rollback()
for parent_ci_id, child_ci_id in (relations or []): for parent_ci_id, child_ci_id in (relations or []):
try: try:
cls.add(parent_ci_id, child_ci_id, valid=False) cls.add(parent_ci_id, child_ci_id,
except: valid=False,
pass apply_async=False,
source=RelationSourceEnum.ATTRIBUTE_VALUES,
uid=uid)
except Exception as e:
current_app.logger.error(e)
class CITriggerManager(object): class CITriggerManager(object):
@@ -1385,7 +1524,8 @@ class CITriggerManager(object):
cls._update_old_attr_value(record_id, ci_dict) cls._update_old_attr_value(record_id, ci_dict)
if ci_id is not None: if ci_id is not None:
ci_dict = CIManager().get_ci_by_id_from_db(ci_id, need_children=False, use_master=False) ci_dict = CIManager().get_ci_by_id_from_db(
ci_id, need_children=False, use_master=False, enum_use_label=True)
try: try:
response = webhook_request(webhook, ci_dict).text response = webhook_request(webhook, ci_dict).text
@@ -1412,7 +1552,8 @@ class CITriggerManager(object):
with app.app_context(): with app.app_context():
if ci_id is not None: if ci_id is not None:
ci_dict = CIManager().get_ci_by_id_from_db(ci_id, need_children=False, use_master=False) ci_dict = CIManager().get_ci_by_id_from_db(
ci_id, need_children=False, use_master=False, enum_use_label=True)
if operate_type == OperateType.UPDATE: if operate_type == OperateType.UPDATE:
cls._update_old_attr_value(record_id, ci_dict) cls._update_old_attr_value(record_id, ci_dict)

View File

@@ -1,6 +1,9 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
from collections import defaultdict
import copy import copy
import networkx as nx
import toposort import toposort
from flask import abort from flask import abort
from flask import current_app from flask import current_app
@@ -145,7 +148,7 @@ class CITypeManager(object):
kwargs["alias"] = kwargs["name"] if not kwargs.get("alias") else kwargs["alias"] kwargs["alias"] = kwargs["name"] if not kwargs.get("alias") else kwargs["alias"]
cls._validate_unique(name=kwargs['name']) cls._validate_unique(name=kwargs['name'])
cls._validate_unique(alias=kwargs['alias']) # cls._validate_unique(alias=kwargs['alias'])
kwargs["unique_id"] = unique_key.id kwargs["unique_id"] = unique_key.id
kwargs['uid'] = current_user.uid kwargs['uid'] = current_user.uid
@@ -184,7 +187,7 @@ class CITypeManager(object):
ci_type = cls.check_is_existed(type_id) ci_type = cls.check_is_existed(type_id)
cls._validate_unique(type_id=type_id, name=kwargs.get('name')) cls._validate_unique(type_id=type_id, name=kwargs.get('name'))
cls._validate_unique(type_id=type_id, alias=kwargs.get('alias') or kwargs.get('name')) # cls._validate_unique(type_id=type_id, alias=kwargs.get('alias') or kwargs.get('name'))
unique_key = kwargs.pop("unique_key", None) unique_key = kwargs.pop("unique_key", None)
unique_key = AttributeCache.get(unique_key) unique_key = AttributeCache.get(unique_key)
@@ -234,6 +237,10 @@ class CITypeManager(object):
if CITypeInheritance.get_by(parent_id=type_id, first=True): if CITypeInheritance.get_by(parent_id=type_id, first=True):
return abort(400, ErrFormat.ci_type_inheritance_cannot_delete) return abort(400, ErrFormat.ci_type_inheritance_cannot_delete)
reference = Attribute.get_by(reference_type_id=type_id, first=True, to_dict=False)
if reference is not None:
return abort(400, ErrFormat.ci_type_referenced_cannot_delete.format(reference.alias))
relation_views = PreferenceRelationView.get_by(to_dict=False) relation_views = PreferenceRelationView.get_by(to_dict=False)
for rv in relation_views: for rv in relation_views:
for item in (rv.cr_ids or []): for item in (rv.cr_ids or []):
@@ -343,9 +350,9 @@ class CITypeInheritanceManager(object):
@classmethod @classmethod
def add(cls, parent_ids, child_id): def add(cls, parent_ids, child_id):
rels = {} rels = defaultdict(set)
for i in cls.cls.get_by(to_dict=False): for i in cls.cls.get_by(to_dict=False):
rels.setdefault(i.child_id, set()).add(i.parent_id) rels[i.child_id].add(i.parent_id)
try: try:
toposort_flatten(rels) toposort_flatten(rels)
@@ -359,7 +366,7 @@ class CITypeInheritanceManager(object):
existed = cls.cls.get_by(parent_id=parent_id, child_id=child_id, first=True, to_dict=False) existed = cls.cls.get_by(parent_id=parent_id, child_id=child_id, first=True, to_dict=False)
if existed is None: if existed is None:
rels.setdefault(child_id, set()).add(parent_id) rels[child_id].add(parent_id)
try: try:
toposort_flatten(rels) toposort_flatten(rels)
except toposort.CircularDependencyError as e: except toposort.CircularDependencyError as e:
@@ -500,14 +507,13 @@ class CITypeAttributeManager(object):
def __init__(self): def __init__(self):
pass pass
@staticmethod @classmethod
def get_attr_name(ci_type_name, key): def get_attr_name(cls, ci_type_name, key):
ci_type = CITypeCache.get(ci_type_name) ci_type = CITypeCache.get(ci_type_name)
if ci_type is None: if ci_type is None:
return return
for i in CITypeAttributesCache.get(ci_type.id): for _, attr in cls.get_all_attributes(ci_type.id):
attr = AttributeCache.get(i.attr_id)
if attr and (attr.name == key or attr.alias == key): if attr and (attr.name == key or attr.alias == key):
return attr.name return attr.name
@@ -519,12 +525,31 @@ class CITypeAttributeManager(object):
for _type_id in parent_ids + [type_id]: for _type_id in parent_ids + [type_id]:
result.extend(CITypeAttributesCache.get2(_type_id)) result.extend(CITypeAttributesCache.get2(_type_id))
return result attr_ids = set()
result2 = []
for i in result:
if i[1].id not in attr_ids:
result2.append(i)
attr_ids.add(i[1].id)
return result2
@classmethod @classmethod
def get_attr_names_by_type_id(cls, type_id): def get_attr_names_by_type_id(cls, type_id):
return [attr.name for _, attr in cls.get_all_attributes(type_id)] return [attr.name for _, attr in cls.get_all_attributes(type_id)]
@classmethod
def get_attr_names_label_enum(cls, type_id):
attr_names, enum_map = list(), defaultdict(dict)
for _, attr in cls.get_all_attributes(type_id):
attr_names.append(attr.name)
if attr.is_choice and not attr.choice_other and not attr.choice_web_hook:
_map = AttributeManager.get_enum_map(attr.id)
if _map:
enum_map[attr.name].update(_map)
return attr_names, enum_map
@staticmethod @staticmethod
def get_attributes_by_type_id(type_id, choice_web_hook_parse=True, choice_other_parse=True): def get_attributes_by_type_id(type_id, choice_web_hook_parse=True, choice_other_parse=True):
has_config_perm = ACLManager('cmdb').has_permission( has_config_perm = ACLManager('cmdb').has_permission(
@@ -565,10 +590,10 @@ class CITypeAttributeManager(object):
CITypeManager.get_name_by_id(type_id), ResourceTypeEnum.CI, PermEnum.CONFIG) CITypeManager.get_name_by_id(type_id), ResourceTypeEnum.CI, PermEnum.CONFIG)
result = {type_id: [i for _, i in cls.get_all_attributes(type_id)] for type_id in type_ids} result = {type_id: [i for _, i in cls.get_all_attributes(type_id)] for type_id in type_ids}
attr2types = {} attr2types = defaultdict(list)
for type_id in result: for type_id in result:
for i in result[type_id]: for i in result[type_id]:
attr2types.setdefault(i.id, []).append(type_id) attr2types[i.id].append(type_id)
attrs = [] attrs = []
for attr_id in attr2types: for attr_id in attr2types:
@@ -821,6 +846,29 @@ class CITypeRelationManager(object):
return ids return ids
@staticmethod
def find_path(source_type_id, target_type_ids):
source_type_id = int(source_type_id)
target_type_ids = map(int, target_type_ids)
graph = nx.DiGraph()
def get_children(_id):
children = CITypeRelation.get_by(parent_id=_id, to_dict=False)
for i in children:
if i.child_id != _id:
graph.add_edge(i.parent_id, i.child_id)
get_children(i.child_id)
get_children(source_type_id)
paths = list(nx.all_simple_paths(graph, source_type_id, target_type_ids))
del graph
return paths
@staticmethod @staticmethod
def _wrap_relation_type_dict(type_id, relation_inst): def _wrap_relation_type_dict(type_id, relation_inst):
ci_type_dict = CITypeCache.get(type_id).to_dict() ci_type_dict = CITypeCache.get(type_id).to_dict()
@@ -846,12 +894,12 @@ class CITypeRelationManager(object):
@classmethod @classmethod
def recursive_level2children(cls, parent_id): def recursive_level2children(cls, parent_id):
result = dict() result = defaultdict(list)
def get_children(_id, level): def get_children(_id, level):
children = CITypeRelation.get_by(parent_id=_id, to_dict=False) children = CITypeRelation.get_by(parent_id=_id, to_dict=False)
if children: if children:
result.setdefault(level + 1, []).extend([i.child.to_dict() for i in children]) result[level + 1].extend([i.child.to_dict() for i in children])
for i in children: for i in children:
if i.child_id != _id: if i.child_id != _id:
@@ -950,10 +998,10 @@ class CITypeRelationManager(object):
p = CITypeManager.check_is_existed(parent) p = CITypeManager.check_is_existed(parent)
c = CITypeManager.check_is_existed(child) c = CITypeManager.check_is_existed(child)
rels = {} rels = defaultdict(set)
for i in CITypeRelation.get_by(to_dict=False): for i in CITypeRelation.get_by(to_dict=False):
rels.setdefault(i.child_id, set()).add(i.parent_id) rels[i.child_id].add(i.parent_id)
rels.setdefault(c.id, set()).add(p.id) rels[c.id].add(p.id)
try: try:
toposort_flatten(rels) toposort_flatten(rels)
@@ -993,7 +1041,7 @@ class CITypeRelationManager(object):
if ((parent_attr_ids and parent_attr_ids != old_parent_attr_ids) or if ((parent_attr_ids and parent_attr_ids != old_parent_attr_ids) or
(child_attr_ids and child_attr_ids != old_child_attr_ids)): (child_attr_ids and child_attr_ids != old_child_attr_ids)):
from api.tasks.cmdb import rebuild_relation_for_attribute_changed from api.tasks.cmdb import rebuild_relation_for_attribute_changed
rebuild_relation_for_attribute_changed.apply_async(args=(existed.to_dict(),)) rebuild_relation_for_attribute_changed.apply_async(args=(existed.to_dict(), current_user.uid))
CITypeHistoryManager.add(CITypeOperateType.ADD_RELATION, p.id, CITypeHistoryManager.add(CITypeOperateType.ADD_RELATION, p.id,
change=dict(parent=p.to_dict(), child=c.to_dict(), relation_type_id=relation_type_id)) change=dict(parent=p.to_dict(), child=c.to_dict(), relation_type_id=relation_type_id))
@@ -1244,17 +1292,16 @@ class CITypeAttributeGroupManager(object):
if isinstance(_from, int): if isinstance(_from, int):
from_group = CITypeAttributeGroup.get_by_id(_from) from_group = CITypeAttributeGroup.get_by_id(_from)
else: else:
from_group = CITypeAttributeGroup.get_by(name=_from, first=True, to_dict=False) from_group = CITypeAttributeGroup.get_by(name=_from, type_id=type_id, first=True, to_dict=False)
from_group or abort(404, ErrFormat.ci_type_attribute_group_not_found.format("id={}".format(_from))) from_group or abort(404, ErrFormat.ci_type_attribute_group_not_found.format("id={}".format(_from)))
if isinstance(_to, int): if isinstance(_to, int):
to_group = CITypeAttributeGroup.get_by_id(_to) to_group = CITypeAttributeGroup.get_by_id(_to)
else: else:
to_group = CITypeAttributeGroup.get_by(name=_to, first=True, to_dict=False) to_group = CITypeAttributeGroup.get_by(name=_to, type_id=type_id, first=True, to_dict=False)
to_group or abort(404, ErrFormat.ci_type_attribute_group_not_found.format("id={}".format(_to))) to_group or abort(404, ErrFormat.ci_type_attribute_group_not_found.format("id={}".format(_to)))
from_order, to_order = from_group.order, to_group.order from_order, to_order = from_group.order, to_group.order
from_group.update(order=to_order) from_group.update(order=to_order)
to_group.update(order=from_order) to_group.update(order=from_order)
@@ -1324,6 +1371,7 @@ class CITypeTemplateManager(object):
def _import_attributes(self, type2attributes): def _import_attributes(self, type2attributes):
attributes = [attr for type_id in type2attributes for attr in type2attributes[type_id]] attributes = [attr for type_id in type2attributes for attr in type2attributes[type_id]]
attrs = [] attrs = []
references = []
for i in copy.deepcopy(attributes): for i in copy.deepcopy(attributes):
if i.pop('inherited', None): if i.pop('inherited', None):
continue continue
@@ -1338,6 +1386,10 @@ class CITypeTemplateManager(object):
if not choice_value: if not choice_value:
i['is_choice'] = False i['is_choice'] = False
if i.get('reference_type_id'):
references.append(copy.deepcopy(i))
i.pop('reference_type_id')
attrs.append((i, choice_value)) attrs.append((i, choice_value))
attr_id_map = self.__import(Attribute, [i[0] for i in copy.deepcopy(attrs)]) attr_id_map = self.__import(Attribute, [i[0] for i in copy.deepcopy(attrs)])
@@ -1346,7 +1398,7 @@ class CITypeTemplateManager(object):
if choice_value and not i.get('choice_web_hook') and not i.get('choice_other'): if choice_value and not i.get('choice_web_hook') and not i.get('choice_other'):
AttributeManager.add_choice_values(attr_id_map.get(i['id'], i['id']), i['value_type'], choice_value) AttributeManager.add_choice_values(attr_id_map.get(i['id'], i['id']), i['value_type'], choice_value)
return attr_id_map return attr_id_map, references
def _import_ci_types(self, ci_types, attr_id_map): def _import_ci_types(self, ci_types, attr_id_map):
for i in ci_types: for i in ci_types:
@@ -1360,6 +1412,11 @@ class CITypeTemplateManager(object):
return self.__import(CIType, ci_types) return self.__import(CIType, ci_types)
def _import_reference_attributes(self, attrs, type_id_map):
for attr in attrs:
attr['reference_type_id'] = type_id_map.get(attr['reference_type_id'], attr['reference_type_id'])
self.__import(Attribute, attrs)
def _import_ci_type_groups(self, ci_type_groups, type_id_map): def _import_ci_type_groups(self, ci_type_groups, type_id_map):
_ci_type_groups = copy.deepcopy(ci_type_groups) _ci_type_groups = copy.deepcopy(ci_type_groups)
for i in _ci_type_groups: for i in _ci_type_groups:
@@ -1373,6 +1430,10 @@ class CITypeTemplateManager(object):
payload = dict(group_id=group_id_map.get(group['id'], group['id']), payload = dict(group_id=group_id_map.get(group['id'], group['id']),
type_id=type_id_map.get(ci_type['id'], ci_type['id']), type_id=type_id_map.get(ci_type['id'], ci_type['id']),
order=order) order=order)
for i in CITypeGroupItem.get_by(type_id=payload['type_id'], to_dict=False):
if i.group_id != payload['group_id']:
i.soft_delete(flush=True)
existed = CITypeGroupItem.get_by(group_id=payload['group_id'], type_id=payload['type_id'], existed = CITypeGroupItem.get_by(group_id=payload['group_id'], type_id=payload['type_id'],
first=True, to_dict=False) first=True, to_dict=False)
if existed is None: if existed is None:
@@ -1541,6 +1602,9 @@ class CITypeTemplateManager(object):
if ((i.extra_option or {}).get('alias') or None) == ( if ((i.extra_option or {}).get('alias') or None) == (
(rule.get('extra_option') or {}).get('alias') or None): (rule.get('extra_option') or {}).get('alias') or None):
existed = True existed = True
rule.pop('extra_option', None)
rule.pop('enabled', None)
rule.pop('cron', None)
AutoDiscoveryCITypeCRUD().update(i.id, **rule) AutoDiscoveryCITypeCRUD().update(i.id, **rule)
break break
@@ -1582,13 +1646,15 @@ class CITypeTemplateManager(object):
import time import time
s = time.time() s = time.time()
attr_id_map = self._import_attributes(tpt.get('type2attributes') or {}) attr_id_map, references = self._import_attributes(tpt.get('type2attributes') or {})
current_app.logger.info('import attributes cost: {}'.format(time.time() - s)) current_app.logger.info('import attributes cost: {}'.format(time.time() - s))
s = time.time() s = time.time()
ci_type_id_map = self._import_ci_types(tpt.get('ci_types') or [], attr_id_map) ci_type_id_map = self._import_ci_types(tpt.get('ci_types') or [], attr_id_map)
current_app.logger.info('import ci_types cost: {}'.format(time.time() - s)) current_app.logger.info('import ci_types cost: {}'.format(time.time() - s))
self._import_reference_attributes(references, ci_type_id_map)
s = time.time() s = time.time()
self._import_ci_type_groups(tpt.get('ci_type_groups') or [], ci_type_id_map) self._import_ci_type_groups(tpt.get('ci_type_groups') or [], ci_type_id_map)
current_app.logger.info('import ci_type_groups cost: {}'.format(time.time() - s)) current_app.logger.info('import ci_type_groups cost: {}'.format(time.time() - s))
@@ -1673,6 +1739,16 @@ class CITypeTemplateManager(object):
type_ids.extend(extend_type_ids) type_ids.extend(extend_type_ids)
ci_types.extend(CITypeManager.get_ci_types(type_ids=extend_type_ids)) ci_types.extend(CITypeManager.get_ci_types(type_ids=extend_type_ids))
# handle reference type
references = Attribute.get_by(only_query=True).join(
CITypeAttribute, CITypeAttribute.attr_id == Attribute.id).filter(
CITypeAttribute.type_id.in_(type_ids)).filter(CITypeAttribute.deleted.is_(False)).filter(
Attribute.reference_type_id.isnot(None))
reference_type_ids = list(set([i.reference_type_id for i in references if i.reference_type_id]))
if reference_type_ids:
type_ids.extend(reference_type_ids)
ci_types.extend(CITypeManager.get_ci_types(type_ids=reference_type_ids))
tpt = dict( tpt = dict(
ci_types=ci_types, ci_types=ci_types,
relation_types=[i.to_dict() for i in RelationTypeManager.get_all()], relation_types=[i.to_dict() for i in RelationTypeManager.get_all()],
@@ -1685,6 +1761,7 @@ class CITypeTemplateManager(object):
icons=dict() icons=dict()
) )
tpt['ci_type_groups'] = CITypeGroupManager.get(ci_types=tpt['ci_types'], type_ids=type_ids) tpt['ci_type_groups'] = CITypeGroupManager.get(ci_types=tpt['ci_types'], type_ids=type_ids)
tpt['ci_type_groups'] = [i for i in tpt['ci_type_groups'] if i.get('ci_types')]
def get_icon_value(icon): def get_icon_value(icon):
try: try:
@@ -1698,6 +1775,9 @@ class CITypeTemplateManager(object):
for r in ad_rules: for r in ad_rules:
r = r.to_dict() r = r.to_dict()
if r.get('extra_option') and '_reference' in r['extra_option']:
r['extra_option'].pop('_reference')
r['type_name'] = type_id2name.get(r.pop('type_id')) r['type_name'] = type_id2name.get(r.pop('type_id'))
if r.get('adr_id'): if r.get('adr_id'):
adr = AutoDiscoveryRuleCRUD.get_by_id(r.pop('adr_id')) adr = AutoDiscoveryRuleCRUD.get_by_id(r.pop('adr_id'))

View File

@@ -14,6 +14,8 @@ class ValueTypeEnum(BaseEnum):
JSON = "6" JSON = "6"
PASSWORD = TEXT PASSWORD = TEXT
LINK = TEXT LINK = TEXT
BOOL = "7"
REFERENCE = INT
class ConstraintEnum(BaseEnum): class ConstraintEnum(BaseEnum):
@@ -41,23 +43,23 @@ class OperateType(BaseEnum):
class CITypeOperateType(BaseEnum): class CITypeOperateType(BaseEnum):
ADD = "0" # 新增模型 ADD = "0" # add CIType
UPDATE = "1" # 修改模型 UPDATE = "1" # update CIType
DELETE = "2" # 删除模型 DELETE = "2" # delete CIType
ADD_ATTRIBUTE = "3" # 新增属性 ADD_ATTRIBUTE = "3"
UPDATE_ATTRIBUTE = "4" # 修改属性 UPDATE_ATTRIBUTE = "4"
DELETE_ATTRIBUTE = "5" # 删除属性 DELETE_ATTRIBUTE = "5"
ADD_TRIGGER = "6" # 新增触发器 ADD_TRIGGER = "6"
UPDATE_TRIGGER = "7" # 修改触发器 UPDATE_TRIGGER = "7"
DELETE_TRIGGER = "8" # 删除触发器 DELETE_TRIGGER = "8"
ADD_UNIQUE_CONSTRAINT = "9" # 新增联合唯一 ADD_UNIQUE_CONSTRAINT = "9"
UPDATE_UNIQUE_CONSTRAINT = "10" # 修改联合唯一 UPDATE_UNIQUE_CONSTRAINT = "10"
DELETE_UNIQUE_CONSTRAINT = "11" # 删除联合唯一 DELETE_UNIQUE_CONSTRAINT = "11"
ADD_RELATION = "12" # 新增关系 ADD_RELATION = "12"
DELETE_RELATION = "13" # 删除关系 DELETE_RELATION = "13"
ADD_RECONCILIATION = "14" # 新增数据合规 ADD_RECONCILIATION = "14"
UPDATE_RECONCILIATION = "15" # 修改数据合规 UPDATE_RECONCILIATION = "15"
DELETE_RECONCILIATION = "16" # 删除数据合规 DELETE_RECONCILIATION = "16"
class RetKey(BaseEnum): class RetKey(BaseEnum):
@@ -93,7 +95,7 @@ class RoleEnum(BaseEnum):
class AutoDiscoveryType(BaseEnum): class AutoDiscoveryType(BaseEnum):
AGENT = "agent" AGENT = "agent"
SNMP = "snmp" SNMP = "snmp"
HTTP = "http" # cloud HTTP = "http" # cloud
COMPONENTS = "components" COMPONENTS = "components"
@@ -108,13 +110,17 @@ class ExecuteStatusEnum(BaseEnum):
FAILED = '1' FAILED = '1'
RUNNING = '2' RUNNING = '2'
class RelationSourceEnum(BaseEnum):
ATTRIBUTE_VALUES = "0"
AUTO_DISCOVERY = "1"
CMDB_QUEUE = "one_cmdb_async" CMDB_QUEUE = "one_cmdb_async"
REDIS_PREFIX_CI = "ONE_CMDB" REDIS_PREFIX_CI = "ONE_CMDB"
REDIS_PREFIX_CI_RELATION = "CMDB_CI_RELATION" REDIS_PREFIX_CI_RELATION = "CMDB_CI_RELATION"
REDIS_PREFIX_CI_RELATION2 = "CMDB_CI_RELATION2" REDIS_PREFIX_CI_RELATION2 = "CMDB_CI_RELATION2"
BUILTIN_KEYWORDS = {'id', '_id', 'ci_id', 'type', '_type', 'ci_type'} BUILTIN_KEYWORDS = {'id', '_id', 'ci_id', 'type', '_type', 'ci_type', 'ticket_id'}
L_TYPE = None L_TYPE = None
L_CI = None L_CI = None

View File

@@ -10,6 +10,7 @@ from api.extensions import db
from api.lib.cmdb.cache import AttributeCache from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import RelationTypeCache from api.lib.cmdb.cache import RelationTypeCache
from api.lib.cmdb.const import OperateType from api.lib.cmdb.const import OperateType
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.perms import CIFilterPermsCRUD from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.cmdb.resp_format import ErrFormat from api.lib.cmdb.resp_format import ErrFormat
from api.lib.perm.acl.cache import UserCache from api.lib.perm.acl.cache import UserCache
@@ -22,6 +23,7 @@ from api.models.cmdb import CITypeHistory
from api.models.cmdb import CITypeTrigger from api.models.cmdb import CITypeTrigger
from api.models.cmdb import CITypeUniqueConstraint from api.models.cmdb import CITypeUniqueConstraint
from api.models.cmdb import OperationRecord from api.models.cmdb import OperationRecord
from api.lib.cmdb.utils import TableMap
class AttributeHistoryManger(object): class AttributeHistoryManger(object):
@@ -59,8 +61,23 @@ class AttributeHistoryManger(object):
total = len(records) total = len(records)
res = {} res = {}
show_attr_set = {}
show_attr_cache = {}
for record in records: for record in records:
record_id = record.OperationRecord.id record_id = record.OperationRecord.id
type_id = record.OperationRecord.type_id
ci_id = record.AttributeHistory.ci_id
show_attr_set[ci_id] = None
show_attr = show_attr_cache.setdefault(
type_id,
AttributeCache.get(
CITypeCache.get(type_id).show_id or CITypeCache.get(type_id).unique_id) if CITypeCache.get(type_id) else None
)
if show_attr:
attr_table = TableMap(attr=show_attr).table
attr_record = attr_table.get_by(attr_id=show_attr.id, ci_id=ci_id, first=True, to_dict=False)
show_attr_set[ci_id] = attr_record.value if attr_record else None
attr_hist = record.AttributeHistory.to_dict() attr_hist = record.AttributeHistory.to_dict()
attr_hist['attr'] = AttributeCache.get(attr_hist['attr_id']) attr_hist['attr'] = AttributeCache.get(attr_hist['attr_id'])
if attr_hist['attr']: if attr_hist['attr']:
@@ -76,6 +93,7 @@ class AttributeHistoryManger(object):
if record_id not in res: if record_id not in res:
record_dict = record.OperationRecord.to_dict() record_dict = record.OperationRecord.to_dict()
record_dict['show_attr_value'] = show_attr_set.get(ci_id)
record_dict["user"] = UserCache.get(record_dict.get("uid")) record_dict["user"] = UserCache.get(record_dict.get("uid"))
if record_dict["user"]: if record_dict["user"]:
record_dict['user'] = record_dict['user'].nickname record_dict['user'] = record_dict['user'].nickname
@@ -232,8 +250,8 @@ class AttributeHistoryManger(object):
class CIRelationHistoryManager(object): class CIRelationHistoryManager(object):
@staticmethod @staticmethod
def add(rel_obj, operate_type=OperateType.ADD): def add(rel_obj, operate_type=OperateType.ADD, uid=None):
record = OperationRecord.create(uid=current_user.uid) record = OperationRecord.create(uid=uid or current_user.uid)
CIRelationHistory.create(relation_id=rel_obj.id, CIRelationHistory.create(relation_id=rel_obj.id,
record_id=record.id, record_id=record.id,

View File

@@ -1,8 +1,9 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
import copy from collections import defaultdict
import copy
import six import six
import toposort import toposort
from flask import abort from flask import abort
@@ -263,12 +264,12 @@ class PreferenceManager(object):
else: else:
views = _views views = _views
view2cr_ids = dict() view2cr_ids = defaultdict(list)
name2view = dict() name2view = dict()
result = dict() result = dict()
name2id = list() name2id = list()
for view in views: for view in views:
view2cr_ids.setdefault(view['name'], []).extend(view['cr_ids']) view2cr_ids[view['name']].extend(view['cr_ids'])
name2id.append([view['name'], view['id']]) name2id.append([view['name'], view['id']])
name2view[view['name']] = view name2view[view['name']] = view
@@ -383,14 +384,22 @@ class PreferenceManager(object):
def add_search_option(**kwargs): def add_search_option(**kwargs):
kwargs['uid'] = current_user.uid kwargs['uid'] = current_user.uid
existed = PreferenceSearchOption.get_by(uid=current_user.uid, if kwargs['name'] in ('__recent__', '__favor__'):
name=kwargs.get('name'), if kwargs['name'] == '__recent__':
prv_id=kwargs.get('prv_id'), for i in PreferenceSearchOption.get_by(
ptv_id=kwargs.get('ptv_id'), only_query=True, name=kwargs['name'], uid=current_user.uid).order_by(
type_id=kwargs.get('type_id'), PreferenceSearchOption.id.desc()).offset(20):
) i.delete()
if existed:
return abort(400, ErrFormat.preference_search_option_exists) else:
existed = PreferenceSearchOption.get_by(uid=current_user.uid,
name=kwargs.get('name'),
prv_id=kwargs.get('prv_id'),
ptv_id=kwargs.get('ptv_id'),
type_id=kwargs.get('type_id'),
)
if existed:
return abort(400, ErrFormat.preference_search_option_exists)
return PreferenceSearchOption.create(**kwargs) return PreferenceSearchOption.create(**kwargs)

View File

@@ -35,7 +35,7 @@ class ErrFormat(CommonErrFormat):
"Only creators and administrators are allowed to delete attributes!") # 目前只允许 属性创建人、管理员 删除属性! "Only creators and administrators are allowed to delete attributes!") # 目前只允许 属性创建人、管理员 删除属性!
# 属性字段名不能是内置字段: id, _id, ci_id, type, _type, ci_type # 属性字段名不能是内置字段: id, _id, ci_id, type, _type, ci_type
attribute_name_cannot_be_builtin = _l( attribute_name_cannot_be_builtin = _l(
"Attribute field names cannot be built-in fields: id, _id, ci_id, type, _type, ci_type") "Attribute field names cannot be built-in fields: id, _id, ci_id, type, _type, ci_type, ticket_id")
attribute_choice_other_invalid = _l( attribute_choice_other_invalid = _l(
"Predefined value: Other model request parameters are illegal!") # 预定义值: 其他模型请求参数不合法! "Predefined value: Other model request parameters are illegal!") # 预定义值: 其他模型请求参数不合法!
@@ -44,6 +44,8 @@ class ErrFormat(CommonErrFormat):
unique_value_not_found = _l("The model's primary key {} does not exist!") # 模型的主键 {} 不存在! unique_value_not_found = _l("The model's primary key {} does not exist!") # 模型的主键 {} 不存在!
unique_key_required = _l("Primary key {} is missing") # 主键字段 {} 缺失 unique_key_required = _l("Primary key {} is missing") # 主键字段 {} 缺失
ci_is_already_existed = _l("CI already exists!") # CI 已经存在! ci_is_already_existed = _l("CI already exists!") # CI 已经存在!
ci_reference_not_found = _l("{}: CI reference {} does not exist!") # {}: CI引用 {} 不存在!
ci_reference_invalid = _l("{}: CI reference {} is illegal!") # {}, CI引用 {} 不合法!
relation_constraint = _l("Relationship constraint: {}, verification failed") # 关系约束: {}, 校验失败 relation_constraint = _l("Relationship constraint: {}, verification failed") # 关系约束: {}, 校验失败
# 多对多关系 限制: 模型 {} <-> {} 已经存在多对多关系! # 多对多关系 限制: 模型 {} <-> {} 已经存在多对多关系!
m2m_relation_constraint = _l( m2m_relation_constraint = _l(
@@ -63,6 +65,8 @@ class ErrFormat(CommonErrFormat):
ci_exists_and_cannot_delete_inheritance = _l( ci_exists_and_cannot_delete_inheritance = _l(
"The inheritance cannot be deleted because the CI already exists") # 因为CI已经存在不能删除继承关系 "The inheritance cannot be deleted because the CI already exists") # 因为CI已经存在不能删除继承关系
ci_type_inheritance_cannot_delete = _l("The model is inherited and cannot be deleted") # 该模型被继承, 不能删除 ci_type_inheritance_cannot_delete = _l("The model is inherited and cannot be deleted") # 该模型被继承, 不能删除
ci_type_referenced_cannot_delete = _l(
"The model is referenced by attribute {} and cannot be deleted") # 该模型被属性 {} 引用, 不能删除
# 因为关系视图 {} 引用了该模型,不能删除模型 # 因为关系视图 {} 引用了该模型,不能删除模型
ci_relation_view_exists_and_cannot_delete_type = _l( ci_relation_view_exists_and_cannot_delete_type = _l(
@@ -150,3 +154,5 @@ class ErrFormat(CommonErrFormat):
topology_group_exists = _l("Topology group {} already exists") # 拓扑视图分组 {} 已经存在 topology_group_exists = _l("Topology group {} already exists") # 拓扑视图分组 {} 已经存在
# 因为该分组下定义了拓扑视图,不能删除 # 因为该分组下定义了拓扑视图,不能删除
topo_view_exists_cannot_delete_group = _l("The group cannot be deleted because the topology view already exists") topo_view_exists_cannot_delete_group = _l("The group cannot be deleted because the topology view already exists")
relation_path_search_src_target_required = _l("Both the source model and the target model must be selected")

View File

@@ -56,7 +56,7 @@ QUERY_CI_BY_ATTR_NAME = """
SELECT {0}.ci_id SELECT {0}.ci_id
FROM {0} FROM {0}
WHERE {0}.attr_id={1:d} WHERE {0}.attr_id={1:d}
AND {0}.value {2} AND ({0}.value {2})
""" """
QUERY_CI_BY_ID = """ QUERY_CI_BY_ID = """

View File

@@ -4,8 +4,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import copy import copy
import six
import time import time
from flask import current_app from flask import current_app
from flask_login import current_user from flask_login import current_user
from jinja2 import Template from jinja2 import Template
@@ -66,6 +66,7 @@ class Search(object):
self.use_id_filter = use_id_filter self.use_id_filter = use_id_filter
self.use_ci_filter = use_ci_filter self.use_ci_filter = use_ci_filter
self.only_ids = only_ids self.only_ids = only_ids
self.multi_type_has_ci_filter = False
self.valid_type_names = [] self.valid_type_names = []
self.type2filter_perms = dict() self.type2filter_perms = dict()
@@ -104,35 +105,56 @@ class Search(object):
else: else:
raise SearchError(ErrFormat.attribute_not_found.format(key)) raise SearchError(ErrFormat.attribute_not_found.format(key))
def _type_query_handler(self, v, queries): def _type_query_handler(self, v, queries, is_sub=False):
new_v = v[1:-1].split(";") if v.startswith("(") and v.endswith(")") else [v] new_v = v[1:-1].split(";") if v.startswith("(") and v.endswith(")") else [v]
type_num = len(new_v)
type_id_list = []
for _v in new_v: for _v in new_v:
ci_type = CITypeCache.get(_v) ci_type = CITypeCache.get(_v)
if len(new_v) == 1 and not self.sort and ci_type and ci_type.default_order_attr: if type_num == 1 and not self.sort and ci_type and ci_type.default_order_attr:
self.sort = ci_type.default_order_attr self.sort = ci_type.default_order_attr
if ci_type is not None: if ci_type is not None:
if self.valid_type_names == "ALL" or ci_type.name in self.valid_type_names: if self.valid_type_names == "ALL" or ci_type.name in self.valid_type_names:
self.type_id_list.append(str(ci_type.id)) if not is_sub:
if ci_type.id in self.type2filter_perms: self.type_id_list.append(str(ci_type.id))
type_id_list.append(str(ci_type.id))
if ci_type.id in self.type2filter_perms and not is_sub:
ci_filter = self.type2filter_perms[ci_type.id].get('ci_filter') ci_filter = self.type2filter_perms[ci_type.id].get('ci_filter')
if ci_filter and self.use_ci_filter and not self.use_id_filter: if ci_filter and self.use_ci_filter and not self.use_id_filter:
sub = [] sub = []
ci_filter = Template(ci_filter).render(user=current_user) ci_filter = Template(ci_filter).render(user=current_user)
for i in ci_filter.split(','): for i in ci_filter.split(','):
if i.startswith("~") and not sub: if type_num == 1:
queries.append(i) if i.startswith("~") and not sub:
queries.append(i)
else:
sub.append(i)
else: else:
sub.append(i) sub.append(i)
if sub: if sub:
queries.append(dict(operator="&", queries=sub)) if type_num == 1:
queries.append(dict(operator="&", queries=sub))
else:
if str(ci_type.id) in self.type_id_list:
self.type_id_list.remove(str(ci_type.id))
type_id_list.remove(str(ci_type.id))
sub.extend([i for i in queries[1:] if isinstance(i, six.string_types)])
sub.insert(0, "_type:{}".format(ci_type.id))
queries.append(dict(operator="|", queries=sub))
self.multi_type_has_ci_filter = True
if self.type2filter_perms[ci_type.id].get('attr_filter'): if self.type2filter_perms[ci_type.id].get('attr_filter'):
if not self.fl: if type_num == 1:
self.fl = set(self.type2filter_perms[ci_type.id]['attr_filter']) if not self.fl:
self.fl = set(self.type2filter_perms[ci_type.id]['attr_filter'])
else:
self.fl = set(self.fl) & set(self.type2filter_perms[ci_type.id]['attr_filter'])
else: else:
self.fl = set(self.fl) & set(self.type2filter_perms[ci_type.id]['attr_filter']) self.fl = self.fl or {}
if not self.fl or isinstance(self.fl, dict):
self.fl[ci_type.id] = set(self.type2filter_perms[ci_type.id]['attr_filter'])
if self.type2filter_perms[ci_type.id].get('id_filter') and self.use_id_filter: if self.type2filter_perms[ci_type.id].get('id_filter') and self.use_id_filter:
@@ -146,13 +168,17 @@ class Search(object):
else: else:
raise SearchError(ErrFormat.ci_type_not_found2.format(_v)) raise SearchError(ErrFormat.ci_type_not_found2.format(_v))
if self.type_id_list: if type_num != len(self.type_id_list) and queries and queries[0].startswith('_type') and not is_sub:
type_ids = ",".join(self.type_id_list) queries[0] = "_type:({})".format(";".join(self.type_id_list))
if type_id_list:
type_ids = ",".join(type_id_list)
_query_sql = QUERY_CI_BY_TYPE.format(type_ids) _query_sql = QUERY_CI_BY_TYPE.format(type_ids)
if self.only_type_query: if self.only_type_query or self.multi_type_has_ci_filter:
return _query_sql return _query_sql
else: elif type_num > 1: # there must be instance-level access control
return "" return "select c_cis.id as ci_id from c_cis where c_cis.id=0"
return "" return ""
@staticmethod @staticmethod
@@ -229,7 +255,7 @@ class Search(object):
return ret_sql.format(query_sql, "ORDER BY B.ci_id {1} LIMIT {0:d}, {2};".format( return ret_sql.format(query_sql, "ORDER BY B.ci_id {1} LIMIT {0:d}, {2};".format(
(self.page - 1) * self.count, sort_type, self.count)) (self.page - 1) * self.count, sort_type, self.count))
elif self.type_id_list: elif self.type_id_list and not self.multi_type_has_ci_filter:
self.query_sql = "SELECT B.ci_id FROM ({0}) AS B {1}".format( self.query_sql = "SELECT B.ci_id FROM ({0}) AS B {1}".format(
query_sql, query_sql,
"INNER JOIN c_cis on c_cis.id=B.ci_id WHERE c_cis.type_id IN ({0}) ".format( "INNER JOIN c_cis on c_cis.id=B.ci_id WHERE c_cis.type_id IN ({0}) ".format(
@@ -254,7 +280,7 @@ class Search(object):
def __sort_by_type(self, sort_type, query_sql): def __sort_by_type(self, sort_type, query_sql):
ret_sql = "SELECT SQL_CALC_FOUND_ROWS DISTINCT B.ci_id FROM ({0}) AS B {1}" ret_sql = "SELECT SQL_CALC_FOUND_ROWS DISTINCT B.ci_id FROM ({0}) AS B {1}"
if self.type_id_list: if self.type_id_list and not self.multi_type_has_ci_filter:
self.query_sql = "SELECT B.ci_id FROM ({0}) AS B {1}".format( self.query_sql = "SELECT B.ci_id FROM ({0}) AS B {1}".format(
query_sql, query_sql,
"INNER JOIN c_cis on c_cis.id=B.ci_id WHERE c_cis.type_id IN ({0}) ".format( "INNER JOIN c_cis on c_cis.id=B.ci_id WHERE c_cis.type_id IN ({0}) ".format(
@@ -287,7 +313,7 @@ class Search(object):
WHERE {1}.attr_id = {3}""".format("ALIAS", table_name, query_sql, attr_id) WHERE {1}.attr_id = {3}""".format("ALIAS", table_name, query_sql, attr_id)
new_table = _v_query_sql new_table = _v_query_sql
if self.only_type_query or not self.type_id_list: if self.only_type_query or not self.type_id_list or self.multi_type_has_ci_filter:
return ("SELECT SQL_CALC_FOUND_ROWS DISTINCT C.ci_id FROM ({0}) AS C ORDER BY C.value {2} " return ("SELECT SQL_CALC_FOUND_ROWS DISTINCT C.ci_id FROM ({0}) AS C ORDER BY C.value {2} "
"LIMIT {1:d}, {3};".format(new_table, (self.page - 1) * self.count, sort_type, self.count)) "LIMIT {1:d}, {3};".format(new_table, (self.page - 1) * self.count, sort_type, self.count))
@@ -325,7 +351,9 @@ class Search(object):
INNER JOIN ({2}) as {3} USING(ci_id)""".format(query_sql, alias, _query_sql, alias + "A") INNER JOIN ({2}) as {3} USING(ci_id)""".format(query_sql, alias, _query_sql, alias + "A")
elif operator == "|" or operator == "|~": elif operator == "|" or operator == "|~":
query_sql = "SELECT * FROM ({0}) as {1} UNION ALL ({2})".format(query_sql, alias, _query_sql) query_sql = "SELECT * FROM ({0}) as {1} UNION ALL SELECT * FROM ({2}) as {3}".format(query_sql, alias,
_query_sql,
alias + "A")
elif operator == "~": elif operator == "~":
query_sql = """SELECT * FROM ({0}) as {1} LEFT JOIN ({2}) as {3} USING(ci_id) query_sql = """SELECT * FROM ({0}) as {1} LEFT JOIN ({2}) as {3} USING(ci_id)
@@ -430,14 +458,14 @@ class Search(object):
return result return result
def __query_by_attr(self, q, queries, alias): def __query_by_attr(self, q, queries, alias, is_sub=False):
k = q.split(":")[0].strip() k = q.split(":")[0].strip()
v = "\:".join(q.split(":")[1:]).strip() v = "\:".join(q.split(":")[1:]).strip()
v = v.replace("'", "\\'") v = v.replace("'", "\\'")
v = v.replace('"', '\\"') v = v.replace('"', '\\"')
field, field_type, operator, attr = self._attr_name_proc(k) field, field_type, operator, attr = self._attr_name_proc(k)
if field == "_type": if field == "_type":
_query_sql = self._type_query_handler(v, queries) _query_sql = self._type_query_handler(v, queries, is_sub)
elif field == "_id": elif field == "_id":
_query_sql = self._id_query_handler(v) _query_sql = self._id_query_handler(v)
@@ -451,6 +479,9 @@ class Search(object):
if field_type == ValueTypeEnum.DATE and len(v) == 10: if field_type == ValueTypeEnum.DATE and len(v) == 10:
v = "{} 00:00:00".format(v) v = "{} 00:00:00".format(v)
if field_type == ValueTypeEnum.BOOL and "*" not in str(v):
v = str(int(v in current_app.config.get('BOOL_TRUE')))
# in query # in query
if v.startswith("(") and v.endswith(")"): if v.startswith("(") and v.endswith(")"):
_query_sql = self._in_query_handler(attr, v, is_not) _query_sql = self._in_query_handler(attr, v, is_not)
@@ -481,19 +512,20 @@ class Search(object):
return alias, _query_sql, operator return alias, _query_sql, operator
def __query_build_by_field(self, queries, is_first=True, only_type_query_special=True, alias='A', operator='&'): def __query_build_by_field(self, queries, is_first=True, only_type_query_special=True, alias='A', operator='&',
is_sub=False):
query_sql = "" query_sql = ""
for q in queries: for q in queries:
_query_sql = "" _query_sql = ""
if isinstance(q, dict): if isinstance(q, dict):
alias, _query_sql, operator = self.__query_build_by_field(q['queries'], True, True, alias) alias, _query_sql, operator = self.__query_build_by_field(q['queries'], True, True, alias, is_sub=True)
current_app.logger.info(_query_sql) # current_app.logger.info(_query_sql)
current_app.logger.info((operator, is_first, alias)) # current_app.logger.info((operator, is_first, alias))
operator = q['operator'] operator = q['operator']
elif ":" in q and not q.startswith("*"): elif ":" in q and not q.startswith("*"):
alias, _query_sql, operator = self.__query_by_attr(q, queries, alias) alias, _query_sql, operator = self.__query_by_attr(q, queries, alias, is_sub)
elif q == "*": elif q == "*":
continue continue
elif q: elif q:
@@ -544,7 +576,6 @@ class Search(object):
queries = handle_arg_list(self.orig_query) queries = handle_arg_list(self.orig_query)
queries = self._extra_handle_query_expr(queries) queries = self._extra_handle_query_expr(queries)
queries = self.__confirm_type_first(queries) queries = self.__confirm_type_first(queries)
current_app.logger.debug(queries)
_, query_sql, _ = self.__query_build_by_field(queries) _, query_sql, _ = self.__query_build_by_field(queries)
@@ -582,13 +613,16 @@ class Search(object):
return facet_result return facet_result
def _fl_build(self): def _fl_build(self):
_fl = list() if isinstance(self.fl, list):
for f in self.fl: _fl = list()
k, _, _, _ = self._attr_name_proc(f) for f in self.fl:
if k: k, _, _, _ = self._attr_name_proc(f)
_fl.append(k) if k:
_fl.append(k)
return _fl return _fl
else:
return self.fl
def search(self): def search(self):
numfound, ci_ids = self._query_build_raw() numfound, ci_ids = self._query_build_raw()
@@ -607,6 +641,8 @@ class Search(object):
if ci_ids: if ci_ids:
response = CIManager.get_cis_by_ids(ci_ids, ret_key=self.ret_key, fields=_fl, excludes=self.excludes) response = CIManager.get_cis_by_ids(ci_ids, ret_key=self.ret_key, fields=_fl, excludes=self.excludes)
for res in response: for res in response:
if not res:
continue
ci_type = res.get("ci_type") ci_type = res.get("ci_type")
if ci_type not in counter.keys(): if ci_type not in counter.keys():
counter[ci_type] = 0 counter[ci_type] = 0

View File

@@ -1,8 +1,11 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
import json
import sys
from collections import Counter from collections import Counter
from collections import defaultdict
import copy
import json
import networkx as nx
import sys
from flask import abort from flask import abort
from flask import current_app from flask import current_app
from flask_login import current_user from flask_login import current_user
@@ -13,6 +16,7 @@ from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIRelationManager from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.ci_type import CITypeRelationManager from api.lib.cmdb.ci_type import CITypeRelationManager
from api.lib.cmdb.const import ConstraintEnum from api.lib.cmdb.const import ConstraintEnum
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2 from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2
from api.lib.cmdb.const import ResourceTypeEnum from api.lib.cmdb.const import ResourceTypeEnum
@@ -25,10 +29,12 @@ from api.lib.cmdb.utils import ValueTypeMap
from api.lib.perm.acl.acl import ACLManager from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import is_app_admin from api.lib.perm.acl.acl import is_app_admin
from api.models.cmdb import CI from api.models.cmdb import CI
from api.models.cmdb import CITypeRelation
from api.models.cmdb import RelationType
class Search(object): class Search(object):
def __init__(self, root_id, def __init__(self, root_id=None,
level=None, level=None,
query=None, query=None,
fl=None, fl=None,
@@ -419,3 +425,169 @@ class Search(object):
level_ids = _level_ids level_ids = _level_ids
return result return result
@staticmethod
def _get_src_ids(src):
q = src.get('q') or ''
if not q.startswith('_type:'):
q = "_type:{},{}".format(src['type_id'], q)
return SearchFromDB(q, use_ci_filter=True, only_ids=True, count=100000).search()
@staticmethod
def _filter_target_ids(target_ids, type_ids, q):
if not q.startswith('_type:'):
q = "_type:({}),{}".format(";".join(map(str, type_ids)), q)
ci_ids = SearchFromDB(q, ci_ids=target_ids, use_ci_filter=True, only_ids=True, count=100000).search()
cis = CI.get_by(fl=['id', 'type_id'], only_query=True).filter(CI.id.in_(ci_ids))
return [(str(i.id), i.type_id) for i in cis]
@staticmethod
def _path2level(src_type_id, target_type_ids, path):
if not src_type_id or not target_type_ids:
return abort(400, ErrFormat.relation_path_search_src_target_required)
graph = nx.DiGraph()
graph.add_edges_from([(n, _path[idx + 1]) for _path in path for idx, n in enumerate(_path[:-1])])
relation_types = defaultdict(dict)
level2type = defaultdict(set)
type2show_key = dict()
for _path in path:
for idx, node in enumerate(_path[1:]):
level2type[idx + 1].add(node)
src = CITypeCache.get(_path[idx])
target = CITypeCache.get(node)
relation_type = RelationType.get_by(only_query=True).join(
CITypeRelation, CITypeRelation.relation_type_id == RelationType.id).filter(
CITypeRelation.parent_id == src.id).filter(CITypeRelation.child_id == target.id).first()
relation_types[src.alias].update({target.alias: relation_type.name})
if src.id not in type2show_key:
type2show_key[src.id] = AttributeCache.get(src.show_id or src.unique_id).name
if target.id not in type2show_key:
type2show_key[target.id] = AttributeCache.get(target.show_id or target.unique_id).name
nodes = graph.nodes()
return level2type, list(nodes), relation_types, type2show_key
def _build_graph(self, source_ids, source_type_id, level2type, target_type_ids, acl):
type2filter_perms = dict()
if not self.is_app_admin:
res2 = acl.get_resources(ResourceTypeEnum.CI_FILTER)
if res2:
type2filter_perms = CIFilterPermsCRUD().get_by_ids(list(map(int, [i['name'] for i in res2])))
target_type_ids = set(target_type_ids)
graph = nx.DiGraph()
target_ids = []
key = [(str(i), source_type_id) for i in source_ids]
graph.add_nodes_from(key)
for level in level2type:
filter_type_ids = level2type[level]
id_filter_limit = dict()
for _type_id in filter_type_ids:
if type2filter_perms.get(_type_id):
_id_filter_limit, _ = self._get_ci_filter(type2filter_perms[_type_id])
id_filter_limit.update(_id_filter_limit)
has_target = filter_type_ids & target_type_ids
res = [json.loads(x).items() for x in [i or '{}' for i in rd.get([i[0] for i in key],
REDIS_PREFIX_CI_RELATION) or []]]
_key = []
for idx, _id in enumerate(key):
valid_targets = [i for i in res[idx] if i[1] in filter_type_ids and
(not id_filter_limit or int(i[0]) in id_filter_limit)]
_key.extend(valid_targets)
graph.add_edges_from(zip([_id] * len(valid_targets), valid_targets))
if has_target:
target_ids.extend([j[0] for i in res for j in i if j[1] in target_type_ids])
key = copy.deepcopy(_key)
return graph, target_ids
@staticmethod
def _find_paths(graph, source_ids, source_type_id, target_ids, valid_path, max_depth=6):
paths = []
for source_id in source_ids:
_paths = nx.all_simple_paths(graph,
source=(source_id, source_type_id),
target=target_ids,
cutoff=max_depth)
for __path in _paths:
if tuple([i[1] for i in __path]) in valid_path:
paths.append([i[0] for i in __path])
return paths
@staticmethod
def _wrap_path_result(paths, types, valid_path, target_types, type2show_key):
ci_ids = [j for i in paths for j in i]
response, _, _, _, _, _ = SearchFromDB("_type:({})".format(";".join(map(str, types))),
use_ci_filter=False,
ci_ids=list(map(int, ci_ids)),
count=1000000).search()
id2ci = {str(i.get('_id')): i if i['_type'] in target_types else {
type2show_key[i['_type']]: i[type2show_key[i['_type']]],
"ci_type_alias": i["ci_type_alias"],
"_type": i["_type"],
} for i in response}
result = defaultdict(list)
counter = defaultdict(int)
for path in paths:
key = "-".join([id2ci.get(i, {}).get('ci_type_alias') or '' for i in path])
if tuple([id2ci.get(i, {}).get('_type') for i in path]) in valid_path:
counter[key] += 1
result[key].append(path)
return result, counter, id2ci
def search_by_path(self, source, target, path):
"""
:param source: {type_id: id, q: expr}
:param target: {type_ids: [id], q: expr}
:param path: [source_type_id, ..., target_type_id], use type id
:return:
"""
acl = ACLManager('cmdb')
if not self.is_app_admin:
res = {i['name'] for i in acl.get_resources(ResourceTypeEnum.CI_TYPE)}
for type_id in (source.get('type_id') and [source['type_id']] or []) + (target.get('type_ids') or []):
_type = CITypeCache.get(type_id)
if _type and _type.name not in res:
return abort(403, ErrFormat.no_permission.format(_type.alias, PermEnum.READ))
target['type_ids'] = [i[-1] for i in path]
level2type, types, relation_types, type2show_key = self._path2level(
source.get('type_id'), target.get('type_ids'), path)
if not level2type:
return [], {}, 0, self.page, 0, {}, {}
source_ids = self._get_src_ids(source)
graph, target_ids = self._build_graph(source_ids, source['type_id'], level2type, target['type_ids'], acl)
target_ids = self._filter_target_ids(target_ids, target['type_ids'], target.get('q') or '')
paths = self._find_paths(graph,
source_ids,
source['type_id'],
set(target_ids),
{tuple(i): 1 for i in path})
numfound = len(paths)
paths = paths[(self.page - 1) * self.count:self.page * self.count]
response, counter, id2ci = self._wrap_path_result(paths,
types,
{tuple(i): 1 for i in path},
set(target.get('type_ids') or []),
type2show_key)
return response, counter, len(paths), self.page, numfound, id2ci, relation_types, type2show_key

View File

@@ -7,6 +7,7 @@ import json
import re import re
import six import six
from flask import current_app
import api.models.cmdb as model import api.models.cmdb as model
from api.lib.cmdb.cache import AttributeCache from api.lib.cmdb.cache import AttributeCache
@@ -28,13 +29,31 @@ def string2int(x):
return v return v
def str2datetime(x): def str2date(x):
try: try:
return datetime.datetime.strptime(x, "%Y-%m-%d").date() return datetime.datetime.strptime(x, "%Y-%m-%d").date()
except ValueError: except ValueError:
pass pass
return datetime.datetime.strptime(x, "%Y-%m-%d %H:%M:%S") try:
return datetime.datetime.strptime(x, "%Y-%m-%d %H:%M:%S").date()
except ValueError:
pass
def str2datetime(x):
x = x.replace('T', ' ')
x = x.replace('Z', '')
try:
return datetime.datetime.strptime(x, "%Y-%m-%d %H:%M:%S")
except ValueError:
pass
return datetime.datetime.strptime(x, "%Y-%m-%d %H:%M")
class ValueTypeMap(object): class ValueTypeMap(object):
@@ -44,8 +63,9 @@ class ValueTypeMap(object):
ValueTypeEnum.TEXT: lambda x: x, ValueTypeEnum.TEXT: lambda x: x,
ValueTypeEnum.TIME: lambda x: TIME_RE.findall(x)[0], ValueTypeEnum.TIME: lambda x: TIME_RE.findall(x)[0],
ValueTypeEnum.DATETIME: str2datetime, ValueTypeEnum.DATETIME: str2datetime,
ValueTypeEnum.DATE: str2datetime, ValueTypeEnum.DATE: str2date,
ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x, ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x,
ValueTypeEnum.BOOL: lambda x: x in current_app.config.get('BOOL_TRUE'),
} }
serialize = { serialize = {
@@ -56,6 +76,7 @@ class ValueTypeMap(object):
ValueTypeEnum.DATE: lambda x: x.strftime("%Y-%m-%d") if not isinstance(x, six.string_types) else x, ValueTypeEnum.DATE: lambda x: x.strftime("%Y-%m-%d") if not isinstance(x, six.string_types) else x,
ValueTypeEnum.DATETIME: lambda x: x.strftime("%Y-%m-%d %H:%M:%S") if not isinstance(x, six.string_types) else x, ValueTypeEnum.DATETIME: lambda x: x.strftime("%Y-%m-%d %H:%M:%S") if not isinstance(x, six.string_types) else x,
ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x, ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x,
ValueTypeEnum.BOOL: lambda x: x in current_app.config.get('BOOL_TRUE'),
} }
serialize2 = { serialize2 = {
@@ -66,6 +87,7 @@ class ValueTypeMap(object):
ValueTypeEnum.DATE: lambda x: (x.decode() if not isinstance(x, six.string_types) else x).split()[0], ValueTypeEnum.DATE: lambda x: (x.decode() if not isinstance(x, six.string_types) else x).split()[0],
ValueTypeEnum.DATETIME: lambda x: x.decode() if not isinstance(x, six.string_types) else x, ValueTypeEnum.DATETIME: lambda x: x.decode() if not isinstance(x, six.string_types) else x,
ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x, ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x,
ValueTypeEnum.BOOL: lambda x: x in current_app.config.get('BOOL_TRUE'),
} }
choice = { choice = {
@@ -87,6 +109,7 @@ class ValueTypeMap(object):
'index_{0}'.format(ValueTypeEnum.TIME): model.CIIndexValueText, 'index_{0}'.format(ValueTypeEnum.TIME): model.CIIndexValueText,
'index_{0}'.format(ValueTypeEnum.FLOAT): model.CIIndexValueFloat, 'index_{0}'.format(ValueTypeEnum.FLOAT): model.CIIndexValueFloat,
'index_{0}'.format(ValueTypeEnum.JSON): model.CIValueJson, 'index_{0}'.format(ValueTypeEnum.JSON): model.CIValueJson,
'index_{0}'.format(ValueTypeEnum.BOOL): model.CIIndexValueInteger,
} }
table_name = { table_name = {
@@ -99,6 +122,7 @@ class ValueTypeMap(object):
'index_{0}'.format(ValueTypeEnum.TIME): 'c_value_index_texts', 'index_{0}'.format(ValueTypeEnum.TIME): 'c_value_index_texts',
'index_{0}'.format(ValueTypeEnum.FLOAT): 'c_value_index_floats', 'index_{0}'.format(ValueTypeEnum.FLOAT): 'c_value_index_floats',
'index_{0}'.format(ValueTypeEnum.JSON): 'c_value_json', 'index_{0}'.format(ValueTypeEnum.JSON): 'c_value_json',
'index_{0}'.format(ValueTypeEnum.BOOL): 'c_value_index_integers',
} }
es_type = { es_type = {

View File

@@ -3,13 +3,13 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import copy
import imp import imp
import copy
import jinja2
import os import os
import re import re
import tempfile import tempfile
import jinja2
from flask import abort from flask import abort
from flask import current_app from flask import current_app
from jinja2schema import infer from jinja2schema import infer
@@ -47,7 +47,7 @@ class AttributeValueManager(object):
""" """
return AttributeCache.get(key) return AttributeCache.get(key)
def get_attr_values(self, fields, ci_id, ret_key="name", unique_key=None, use_master=False): def get_attr_values(self, fields, ci_id, ret_key="name", unique_key=None, use_master=False, enum_map=None):
""" """
:param fields: :param fields:
@@ -55,6 +55,7 @@ class AttributeValueManager(object):
:param ret_key: It can be name or alias :param ret_key: It can be name or alias
:param unique_key: primary attribute :param unique_key: primary attribute
:param use_master: Only for master-slave read-write separation :param use_master: Only for master-slave read-write separation
:param enum_map:
:return: :return:
""" """
res = dict() res = dict()
@@ -76,6 +77,12 @@ class AttributeValueManager(object):
else: else:
res[field_name] = ValueTypeMap.serialize[attr.value_type](rs[0].value) if rs else None res[field_name] = ValueTypeMap.serialize[attr.value_type](rs[0].value) if rs else None
if enum_map and field_name in enum_map:
if attr.is_list:
res[field_name] = [enum_map[field_name].get(i, i) for i in res[field_name]]
else:
res[field_name] = enum_map[field_name].get(res[field_name], res[field_name])
if unique_key is not None and attr.id == unique_key.id and rs: if unique_key is not None and attr.id == unique_key.id and rs:
res['unique'] = unique_key.name res['unique'] = unique_key.name
res['unique_alias'] = unique_key.alias res['unique_alias'] = unique_key.alias
@@ -90,11 +97,13 @@ class AttributeValueManager(object):
deserialize = ValueTypeMap.deserialize[value_type] deserialize = ValueTypeMap.deserialize[value_type]
try: try:
v = deserialize(value) v = deserialize(value)
if value_type in (ValueTypeEnum.DATE, ValueTypeEnum.DATETIME):
return str(v)
return v return v
except ValueDeserializeError as e: except ValueDeserializeError as e:
return abort(400, ErrFormat.attribute_value_invalid2.format(alias, e)) return abort(400, ErrFormat.attribute_value_invalid2.format(alias, e))
except ValueError: except ValueError:
return abort(400, ErrFormat.attribute_value_invalid.format(value)) return abort(400, ErrFormat.attribute_value_invalid2.format(alias, value))
@staticmethod @staticmethod
def _check_is_choice(attr, value_type, value): def _check_is_choice(attr, value_type, value):
@@ -123,25 +132,31 @@ class AttributeValueManager(object):
return abort(400, ErrFormat.attribute_value_required.format(attr.alias)) return abort(400, ErrFormat.attribute_value_required.format(attr.alias))
@staticmethod @staticmethod
def check_re(expr, value): def check_re(expr, alias, value):
if not re.compile(expr).match(str(value)): if not re.compile(expr).match(str(value)):
return abort(400, ErrFormat.attribute_value_invalid.format(value)) return abort(400, ErrFormat.attribute_value_invalid2.format(alias, value))
def _validate(self, attr, value, value_table, ci=None, type_id=None, ci_id=None, type_attr=None): def _validate(self, attr, value, value_table, ci=None, type_id=None, ci_id=None, type_attr=None):
ci = ci or {} if not attr.is_reference:
v = self._deserialize_value(attr.alias, attr.value_type, value) ci = ci or {}
v = self._deserialize_value(attr.alias, attr.value_type, value)
attr.is_choice and value and self._check_is_choice(attr, attr.value_type, v)
else:
v = value or None
attr.is_choice and value and self._check_is_choice(attr, attr.value_type, v)
attr.is_unique and self._check_is_unique( attr.is_unique and self._check_is_unique(
value_table, attr, ci and ci.id or ci_id, ci and ci.type_id or type_id, v) value_table, attr, ci and ci.id or ci_id, ci and ci.type_id or type_id, v)
self._check_is_required(ci and ci.type_id or type_id, attr, v, type_attr=type_attr) self._check_is_required(ci and ci.type_id or type_id, attr, v, type_attr=type_attr)
if attr.is_reference:
return v
if v == "" and attr.value_type not in (ValueTypeEnum.TEXT,): if v == "" and attr.value_type not in (ValueTypeEnum.TEXT,):
v = None v = None
if attr.re_check and value: if attr.re_check and value:
self.check_re(attr.re_check, value) self.check_re(attr.re_check, attr.alias, value)
return v return v
@@ -235,10 +250,19 @@ class AttributeValueManager(object):
try: try:
if attr.is_list: if attr.is_list:
if isinstance(value, dict):
if value.get('op') == "delete":
value['v'] = [ValueTypeMap.serialize[attr.value_type](
self._deserialize_value(attr.alias, attr.value_type, i))
for i in handle_arg_list(value['v'])]
continue
_value = value.get('v') or []
else:
_value = value
value_list = [self._validate(attr, i, value_table, ci=None, type_id=type_id, ci_id=ci_id, value_list = [self._validate(attr, i, value_table, ci=None, type_id=type_id, ci_id=ci_id,
type_attr=ci_attr2type_attr.get(attr.id)) type_attr=ci_attr2type_attr.get(attr.id))
for i in handle_arg_list(value)] for i in handle_arg_list(_value)]
ci_dict[key] = value_list ci_dict[key] = value_list if not isinstance(value, dict) else dict(op=value.get('op'), v=value_list)
if not value_list: if not value_list:
self._check_is_required(type_id, attr, '') self._check_is_required(type_id, attr, '')
@@ -278,28 +302,47 @@ class AttributeValueManager(object):
existed_values = [(ValueTypeMap.serialize[attr.value_type](i.value) if existed_values = [(ValueTypeMap.serialize[attr.value_type](i.value) if
i.value or i.value == 0 else i.value) for i in existed_attrs] i.value or i.value == 0 else i.value) for i in existed_attrs]
# Comparison array starts from which position changes if isinstance(value, dict):
min_len = min(len(value), len(existed_values)) if value.get('op') == "add":
index = 0 for v in (value.get('v') or []):
while index < min_len: if v not in existed_values:
if value[index] != existed_values[index]: value_table.create(ci_id=ci.id, attr_id=attr.id, value=v, flush=False, commit=False)
break if not attr.is_dynamic:
index += 1 changed.append((ci.id, attr.id, OperateType.ADD, None, v, ci.type_id))
else:
has_dynamic = True
# Delete first and then add to ensure id sorting elif value.get('op') == "delete":
for idx in range(index, len(existed_attrs)): for v in (value.get('v') or []):
existed_attr = existed_attrs[idx] if v in existed_values:
existed_attr.delete(flush=False, commit=False) existed_attrs[existed_values.index(v)].delete(flush=False, commit=False)
if not attr.is_dynamic: if not attr.is_dynamic:
changed.append((ci.id, attr.id, OperateType.DELETE, existed_values[idx], None, ci.type_id)) changed.append((ci.id, attr.id, OperateType.DELETE, v, None, ci.type_id))
else: else:
has_dynamic = True has_dynamic = True
for idx in range(index, len(value)): else:
value_table.create(ci_id=ci.id, attr_id=attr.id, value=value[idx], flush=False, commit=False) # Comparison array starts from which position changes
if not attr.is_dynamic: min_len = min(len(value), len(existed_values))
changed.append((ci.id, attr.id, OperateType.ADD, None, value[idx], ci.type_id)) index = 0
else: while index < min_len:
has_dynamic = True if value[index] != existed_values[index]:
break
index += 1
# Delete first and then add to ensure id sorting
for idx in range(index, len(existed_attrs)):
existed_attr = existed_attrs[idx]
existed_attr.delete(flush=False, commit=False)
if not attr.is_dynamic:
changed.append((ci.id, attr.id, OperateType.DELETE, existed_values[idx], None, ci.type_id))
else:
has_dynamic = True
for idx in range(index, len(value)):
value_table.create(ci_id=ci.id, attr_id=attr.id, value=value[idx], flush=False, commit=False)
if not attr.is_dynamic:
changed.append((ci.id, attr.id, OperateType.ADD, None, value[idx], ci.type_id))
else:
has_dynamic = True
else: else:
existed_attr = value_table.get_by(attr_id=attr.id, ci_id=ci.id, first=True, to_dict=False) existed_attr = value_table.get_by(attr_id=attr.id, ci_id=ci.id, first=True, to_dict=False)
existed_value = existed_attr and existed_attr.value existed_value = existed_attr and existed_attr.value

View File

@@ -1,21 +1,19 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
from flask import abort
from sqlalchemy import func from sqlalchemy import func
from api.extensions import db from api.extensions import db
from api.lib.utils import get_page from api.lib.utils import get_page
from api.lib.utils import get_page_size from api.lib.utils import get_page_size
__author__ = 'pycook'
class DBMixin(object): class DBMixin(object):
cls = None cls = None
@classmethod @classmethod
def search(cls, page, page_size, fl=None, only_query=False, reverse=False, count_query=False, **kwargs): def search(cls, page, page_size, fl=None, only_query=False, reverse=False, count_query=False,
last_size=None, **kwargs):
page = get_page(page) page = get_page(page)
page_size = get_page_size(page_size) page_size = get_page_size(page_size)
if fl is None: if fl is None:
@@ -47,14 +45,15 @@ class DBMixin(object):
return _query, query return _query, query
numfound = query.count() numfound = query.count()
return numfound, [i.to_dict() if fl is None else getattr(i, '_asdict')() if not last_size:
for i in query.offset((page - 1) * page_size).limit(page_size)] return numfound, [i.to_dict() if fl is None else getattr(i, '_asdict')()
for i in query.offset((page - 1) * page_size).limit(page_size)]
def _must_be_required(self, _id): else:
existed = self.cls.get_by_id(_id) offset = numfound - last_size
existed or abort(404, "Factor [{}] does not exist".format(_id)) if offset < 0:
offset = 0
return existed return numfound, [i.to_dict() if fl is None else getattr(i, '_asdict')()
for i in query.offset(offset).limit(last_size)]
def _can_add(self, **kwargs): def _can_add(self, **kwargs):
raise NotImplementedError raise NotImplementedError

View File

@@ -58,7 +58,7 @@ def _request_messenger(subject, body, tos, sender, payload):
def notify_send(subject, body, methods, tos, payload=None): def notify_send(subject, body, methods, tos, payload=None):
payload = payload or {} payload = payload or {}
payload = {k: v or '' for k, v in payload.items()} payload = {k: '' if v is None else v for k, v in payload.items()}
subject = Template(subject).render(payload) subject = Template(subject).render(payload)
body = Template(body).render(payload) body = Template(body).render(payload)

View File

@@ -376,7 +376,7 @@ class AuditCRUD(object):
origin=origin, current=current, extra=extra, source=source.value) origin=origin, current=current, extra=extra, source=source.value)
@classmethod @classmethod
def add_login_log(cls, username, is_ok, description, _id=None, logout_at=None): def add_login_log(cls, username, is_ok, description, _id=None, logout_at=None, ip=None, browser=None):
if _id is not None: if _id is not None:
existed = AuditLoginLog.get_by_id(_id) existed = AuditLoginLog.get_by_id(_id)
if existed is not None: if existed is not None:
@@ -387,8 +387,9 @@ class AuditCRUD(object):
is_ok=is_ok, is_ok=is_ok,
description=description, description=description,
logout_at=logout_at, logout_at=logout_at,
ip=request.headers.get('X-Real-IP') or request.remote_addr, ip=(ip or request.headers.get('X-Forwarded-For') or
browser=request.headers.get('User-Agent'), request.headers.get('X-Real-IP') or request.remote_addr or '').split(',')[0],
browser=browser or request.headers.get('User-Agent'),
channel=request.values.get('channel', 'web'), channel=request.values.get('channel', 'web'),
) )

View File

@@ -2,7 +2,6 @@
import datetime import datetime
from sqlalchemy.dialects.mysql import DOUBLE from sqlalchemy.dialects.mysql import DOUBLE
from api.extensions import db from api.extensions import db
@@ -11,6 +10,7 @@ from api.lib.cmdb.const import CIStatusEnum
from api.lib.cmdb.const import CITypeOperateType from api.lib.cmdb.const import CITypeOperateType
from api.lib.cmdb.const import ConstraintEnum from api.lib.cmdb.const import ConstraintEnum
from api.lib.cmdb.const import OperateType from api.lib.cmdb.const import OperateType
from api.lib.cmdb.const import RelationSourceEnum
from api.lib.cmdb.const import ValueTypeEnum from api.lib.cmdb.const import ValueTypeEnum
from api.lib.database import Model from api.lib.database import Model
from api.lib.database import Model2 from api.lib.database import Model2
@@ -105,6 +105,10 @@ class Attribute(Model):
is_password = db.Column(db.Boolean, default=False) is_password = db.Column(db.Boolean, default=False)
is_sortable = db.Column(db.Boolean, default=False) is_sortable = db.Column(db.Boolean, default=False)
is_dynamic = db.Column(db.Boolean, default=False) is_dynamic = db.Column(db.Boolean, default=False)
is_bool = db.Column(db.Boolean, default=False)
is_reference = db.Column(db.Boolean, default=False)
reference_type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'))
default = db.Column(db.JSON) # {"default": None} default = db.Column(db.JSON) # {"default": None}
@@ -260,6 +264,7 @@ class CIRelation(Model):
second_ci_id = db.Column(db.Integer, db.ForeignKey("c_cis.id"), nullable=False) second_ci_id = db.Column(db.Integer, db.ForeignKey("c_cis.id"), nullable=False)
relation_type_id = db.Column(db.Integer, db.ForeignKey("c_relation_types.id"), nullable=False) relation_type_id = db.Column(db.Integer, db.ForeignKey("c_relation_types.id"), nullable=False)
more = db.Column(db.Integer, db.ForeignKey("c_cis.id")) more = db.Column(db.Integer, db.ForeignKey("c_cis.id"))
source = db.Column(db.Enum(*RelationSourceEnum.all()), name="source")
ancestor_ids = db.Column(db.String(128), index=True) ancestor_ids = db.Column(db.String(128), index=True)
@@ -578,6 +583,7 @@ class AutoDiscoveryCIType(Model):
extra_option = db.Column(db.JSON) extra_option = db.Column(db.JSON)
uid = db.Column(db.Integer, index=True) uid = db.Column(db.Integer, index=True)
enabled = db.Column(db.Boolean, default=True)
class AutoDiscoveryCITypeRelation(Model): class AutoDiscoveryCITypeRelation(Model):
@@ -634,6 +640,15 @@ class AutoDiscoveryCounter(Model2):
last_week_count = db.Column(db.Integer, default=0) last_week_count = db.Column(db.Integer, default=0)
class AutoDiscoveryAccount(Model):
__tablename__ = "c_ad_accounts"
uid = db.Column(db.Integer, index=True)
name = db.Column(db.String(64))
adr_id = db.Column(db.Integer, db.ForeignKey('c_ad_rules.id'))
config = db.Column(db.JSON)
class CIFilterPerms(Model): class CIFilterPerms(Model):
__tablename__ = "c_ci_filter_perms" __tablename__ = "c_ci_filter_perms"

View File

@@ -1,9 +1,8 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
import json
import datetime import datetime
import json
import redis_lock import redis_lock
from flask import current_app from flask import current_app
from flask_login import login_user from flask_login import login_user
@@ -13,21 +12,26 @@ from api.extensions import celery
from api.extensions import db from api.extensions import db
from api.extensions import es from api.extensions import es
from api.extensions import rd from api.extensions import rd
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeAttributesCache from api.lib.cmdb.cache import CITypeAttributesCache
from api.lib.cmdb.const import CMDB_QUEUE from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.cmdb.const import REDIS_PREFIX_CI from api.lib.cmdb.const import REDIS_PREFIX_CI
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2 from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2
from api.lib.cmdb.const import RelationSourceEnum
from api.lib.cmdb.perms import CIFilterPermsCRUD from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.cmdb.utils import TableMap
from api.lib.decorator import flush_db from api.lib.decorator import flush_db
from api.lib.decorator import reconnect_db from api.lib.decorator import reconnect_db
from api.lib.perm.acl.cache import UserCache from api.lib.perm.acl.cache import UserCache
from api.lib.utils import handle_arg_list from api.lib.utils import handle_arg_list
from api.models.cmdb import Attribute
from api.models.cmdb import AutoDiscoveryCI
from api.models.cmdb import AutoDiscoveryCIType
from api.models.cmdb import AutoDiscoveryCITypeRelation
from api.models.cmdb import CI from api.models.cmdb import CI
from api.models.cmdb import CIRelation from api.models.cmdb import CIRelation
from api.models.cmdb import CITypeAttribute from api.models.cmdb import CITypeAttribute
from api.models.cmdb import AutoDiscoveryCI
from api.models.cmdb import AutoDiscoveryCIType
@celery.task(name="cmdb.ci_cache", queue=CMDB_QUEUE) @celery.task(name="cmdb.ci_cache", queue=CMDB_QUEUE)
@@ -36,6 +40,7 @@ from api.models.cmdb import AutoDiscoveryCIType
def ci_cache(ci_id, operate_type, record_id): def ci_cache(ci_id, operate_type, record_id):
from api.lib.cmdb.ci import CITriggerManager from api.lib.cmdb.ci import CITriggerManager
from api.lib.cmdb.ci import CIRelationManager from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.ci_type import CITypeAttributeManager
m = api.lib.cmdb.ci.CIManager() m = api.lib.cmdb.ci.CIManager()
ci_dict = m.get_ci_by_id_from_db(ci_id, need_children=False, use_master=False) ci_dict = m.get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
@@ -51,17 +56,27 @@ def ci_cache(ci_id, operate_type, record_id):
current_app.test_request_context().push() current_app.test_request_context().push()
login_user(UserCache.get('worker')) login_user(UserCache.get('worker'))
CITriggerManager.fire(operate_type, ci_dict, record_id) _, enum_map = CITypeAttributeManager.get_attr_names_label_enum(ci_dict.get('_type'))
payload = dict()
for k, v in ci_dict.items():
if k in enum_map:
if isinstance(v, list):
payload[k] = [enum_map[k].get(i, i) for i in v]
else:
payload[k] = enum_map[k].get(v, v)
else:
payload[k] = v
CITriggerManager.fire(operate_type, payload, record_id)
ci_dict and CIRelationManager.build_by_attribute(ci_dict) ci_dict and CIRelationManager.build_by_attribute(ci_dict)
@celery.task(name="cmdb.rebuild_relation_for_attribute_changed", queue=CMDB_QUEUE) @celery.task(name="cmdb.rebuild_relation_for_attribute_changed", queue=CMDB_QUEUE)
@reconnect_db @reconnect_db
def rebuild_relation_for_attribute_changed(ci_type_relation): def rebuild_relation_for_attribute_changed(ci_type_relation, uid):
from api.lib.cmdb.ci import CIRelationManager from api.lib.cmdb.ci import CIRelationManager
CIRelationManager.rebuild_all_by_attribute(ci_type_relation) CIRelationManager.rebuild_all_by_attribute(ci_type_relation, uid)
@celery.task(name="cmdb.batch_ci_cache", queue=CMDB_QUEUE) @celery.task(name="cmdb.batch_ci_cache", queue=CMDB_QUEUE)
@@ -82,7 +97,7 @@ def batch_ci_cache(ci_ids, ): # only for attribute change index
@celery.task(name="cmdb.ci_delete", queue=CMDB_QUEUE) @celery.task(name="cmdb.ci_delete", queue=CMDB_QUEUE)
@reconnect_db @reconnect_db
def ci_delete(ci_id): def ci_delete(ci_id, type_id):
current_app.logger.info(ci_id) current_app.logger.info(ci_id)
if current_app.config.get("USE_ES"): if current_app.config.get("USE_ES"):
@@ -97,6 +112,12 @@ def ci_delete(ci_id):
adt.update(updated_at=datetime.datetime.now()) adt.update(updated_at=datetime.datetime.now())
instance.delete() instance.delete()
for attr in Attribute.get_by(reference_type_id=type_id, to_dict=False):
table = TableMap(attr=attr).table
for i in getattr(table, 'get_by')(attr_id=attr.id, value=ci_id, to_dict=False):
i.delete()
ci_cache(i.ci_id, None, None)
current_app.logger.info("{0} delete..........".format(ci_id)) current_app.logger.info("{0} delete..........".format(ci_id))
@@ -186,7 +207,7 @@ def ci_relation_add(parent_dict, child_id, uid):
for ci in response: for ci in response:
try: try:
CIRelationManager.add(ci['_id'], child_id) CIRelationManager.add(ci['_id'], child_id)
ci_relation_cache(ci['_id'], child_id) ci_relation_cache(ci['_id'], child_id, None)
except Exception as e: except Exception as e:
current_app.logger.warning(e) current_app.logger.warning(e)
finally: finally:
@@ -277,3 +298,75 @@ def write_ad_rule_sync_history(rules, oneagent_id, oneagent_name, sync_at):
except Exception as e: except Exception as e:
current_app.logger.error("write auto discovery rule sync history failed: {}".format(e)) current_app.logger.error("write auto discovery rule sync history failed: {}".format(e))
db.session.rollback() db.session.rollback()
@celery.task(name="cmdb.build_relations_for_ad_accept", queue=CMDB_QUEUE)
@reconnect_db
def build_relations_for_ad_accept(adc, ci_id, ad_key2attr):
from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search as ci_search
current_app.test_request_context().push()
login_user(UserCache.get('worker'))
relation_ads = AutoDiscoveryCITypeRelation.get_by(ad_type_id=adc['type_id'], to_dict=False)
for r_adt in relation_ads:
ad_key = r_adt.ad_key
if not adc['instance'].get(ad_key):
continue
ad_key_values = [adc['instance'].get(ad_key)] if not isinstance(
adc['instance'].get(ad_key), list) else adc['instance'].get(ad_key)
for ad_key_value in ad_key_values:
query = "_type:{},{}:{}".format(r_adt.peer_type_id, r_adt.peer_attr_id, ad_key_value)
s = ci_search(query, use_ci_filter=False, count=1000000)
try:
response, _, _, _, _, _ = s.search()
except SearchError as e:
current_app.logger.error("build_relations_for_ad_accept failed: {}".format(e))
return
for relation_ci in response:
relation_ci_id = relation_ci['_id']
try:
CIRelationManager.add(ci_id, relation_ci_id,
valid=False,
source=RelationSourceEnum.AUTO_DISCOVERY)
except:
try:
CIRelationManager.add(relation_ci_id, ci_id,
valid=False,
source=RelationSourceEnum.AUTO_DISCOVERY)
except:
pass
# build relations in reverse
relation_ads = AutoDiscoveryCITypeRelation.get_by(peer_type_id=adc['type_id'], to_dict=False)
attr2ad_key = {v: k for k, v in ad_key2attr.items()}
for r_adt in relation_ads:
attr = AttributeCache.get(r_adt.peer_attr_id)
ad_key = attr2ad_key.get(attr and attr.name)
if not ad_key:
continue
ad_value = adc['instance'].get(ad_key)
peer_ad_key = r_adt.ad_key
peer_instances = AutoDiscoveryCI.get_by(type_id=r_adt.ad_type_id, to_dict=False)
for peer_instance in peer_instances:
peer_ad_values = peer_instance.instance.get(peer_ad_key)
peer_ad_values = [peer_ad_values] if not isinstance(peer_ad_values, list) else peer_ad_values
if ad_value in peer_ad_values and peer_instance.ci_id:
try:
CIRelationManager.add(peer_instance.ci_id, ci_id,
valid=False,
source=RelationSourceEnum.AUTO_DISCOVERY)
except:
try:
CIRelationManager.add(ci_id, peer_instance.ci_id,
valid=False,
source=RelationSourceEnum.AUTO_DISCOVERY)
except:
pass

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PROJECT VERSION\n" "Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2024-06-20 19:12+0800\n" "POT-Creation-Date: 2024-09-26 17:57+0800\n"
"PO-Revision-Date: 2023-12-25 20:21+0800\n" "PO-Revision-Date: 2023-12-25 20:21+0800\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: zh\n" "Language: zh\n"
@@ -16,7 +16,7 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.14.0\n" "Generated-By: Babel 2.16.0\n"
#: api/lib/resp_format.py:7 #: api/lib/resp_format.py:7
msgid "unauthorized" msgid "unauthorized"
@@ -169,8 +169,8 @@ msgstr "目前只允许 属性创建人、管理员 删除属性!"
#: api/lib/cmdb/resp_format.py:37 #: api/lib/cmdb/resp_format.py:37
msgid "" msgid ""
"Attribute field names cannot be built-in fields: id, _id, ci_id, type, " "Attribute field names cannot be built-in fields: id, _id, ci_id, type, "
"_type, ci_type" "_type, ci_type, ticket_id"
msgstr "属性字段名不能是内置字段: id, _id, ci_id, type, _type, ci_type" msgstr "属性字段名不能是内置字段: id, _id, ci_id, type, _type, ci_type, ci_type, ticket_id"
#: api/lib/cmdb/resp_format.py:39 #: api/lib/cmdb/resp_format.py:39
msgid "Predefined value: Other model request parameters are illegal!" msgid "Predefined value: Other model request parameters are illegal!"
@@ -197,289 +197,305 @@ msgid "CI already exists!"
msgstr "CI 已经存在!" msgstr "CI 已经存在!"
#: api/lib/cmdb/resp_format.py:47 #: api/lib/cmdb/resp_format.py:47
msgid "{}: CI reference {} does not exist!"
msgstr "{}: CI引用 {} 不存在!"
#: api/lib/cmdb/resp_format.py:48
msgid "{}: CI reference {} is illegal!"
msgstr "{}, CI引用 {} 不合法!"
#: api/lib/cmdb/resp_format.py:49
msgid "Relationship constraint: {}, verification failed" msgid "Relationship constraint: {}, verification failed"
msgstr "关系约束: {}, 校验失败" msgstr "关系约束: {}, 校验失败"
#: api/lib/cmdb/resp_format.py:49 #: api/lib/cmdb/resp_format.py:51
msgid "" msgid ""
"Many-to-many relationship constraint: Model {} <-> {} already has a many-" "Many-to-many relationship constraint: Model {} <-> {} already has a many-"
"to-many relationship!" "to-many relationship!"
msgstr "多对多关系 限制: 模型 {} <-> {} 已经存在多对多关系!" msgstr "多对多关系 限制: 模型 {} <-> {} 已经存在多对多关系!"
#: api/lib/cmdb/resp_format.py:52 #: api/lib/cmdb/resp_format.py:54
msgid "CI relationship: {} does not exist" msgid "CI relationship: {} does not exist"
msgstr "CI关系: {} 不存在" msgstr "CI关系: {} 不存在"
#: api/lib/cmdb/resp_format.py:55 #: api/lib/cmdb/resp_format.py:57
msgid "In search expressions, not supported before parentheses: or, not" msgid "In search expressions, not supported before parentheses: or, not"
msgstr "搜索表达式里小括号前不支持: 或、非" msgstr "搜索表达式里小括号前不支持: 或、非"
#: api/lib/cmdb/resp_format.py:57 #: api/lib/cmdb/resp_format.py:59
msgid "Model {} does not exist" msgid "Model {} does not exist"
msgstr "模型 {} 不存在" msgstr "模型 {} 不存在"
#: api/lib/cmdb/resp_format.py:58 #: api/lib/cmdb/resp_format.py:60
msgid "Model {} already exists" msgid "Model {} already exists"
msgstr "模型 {} 已经存在" msgstr "模型 {} 已经存在"
#: api/lib/cmdb/resp_format.py:59 #: api/lib/cmdb/resp_format.py:61
msgid "The primary key is undefined or has been deleted" msgid "The primary key is undefined or has been deleted"
msgstr "主键未定义或者已被删除" msgstr "主键未定义或者已被删除"
#: api/lib/cmdb/resp_format.py:60 #: api/lib/cmdb/resp_format.py:62
msgid "Only the creator can delete it!" msgid "Only the creator can delete it!"
msgstr "只有创建人才能删除它!" msgstr "只有创建人才能删除它!"
#: api/lib/cmdb/resp_format.py:61 #: api/lib/cmdb/resp_format.py:63
msgid "The model cannot be deleted because the CI already exists" msgid "The model cannot be deleted because the CI already exists"
msgstr "因为CI已经存在不能删除模型" msgstr "因为CI已经存在不能删除模型"
#: api/lib/cmdb/resp_format.py:63 #: api/lib/cmdb/resp_format.py:65
msgid "The inheritance cannot be deleted because the CI already exists" msgid "The inheritance cannot be deleted because the CI already exists"
msgstr "因为CI已经存在不能删除继承关系" msgstr "因为CI已经存在不能删除继承关系"
#: api/lib/cmdb/resp_format.py:65 #: api/lib/cmdb/resp_format.py:67
msgid "The model is inherited and cannot be deleted" msgid "The model is inherited and cannot be deleted"
msgstr "该模型被继承, 不能删除" msgstr "该模型被继承, 不能删除"
#: api/lib/cmdb/resp_format.py:68 #: api/lib/cmdb/resp_format.py:68
msgid "The model is referenced by attribute {} and cannot be deleted"
msgstr "该模型被属性 {} 引用, 不能删除"
#: api/lib/cmdb/resp_format.py:72
msgid "" msgid ""
"The model cannot be deleted because the model is referenced by the " "The model cannot be deleted because the model is referenced by the "
"relational view {}" "relational view {}"
msgstr "因为关系视图 {} 引用了该模型,不能删除模型" msgstr "因为关系视图 {} 引用了该模型,不能删除模型"
#: api/lib/cmdb/resp_format.py:70 #: api/lib/cmdb/resp_format.py:74
msgid "Model group {} does not exist" msgid "Model group {} does not exist"
msgstr "模型分组 {} 不存在" msgstr "模型分组 {} 不存在"
#: api/lib/cmdb/resp_format.py:71 #: api/lib/cmdb/resp_format.py:75
msgid "Model group {} already exists" msgid "Model group {} already exists"
msgstr "模型分组 {} 已经存在" msgstr "模型分组 {} 已经存在"
#: api/lib/cmdb/resp_format.py:72 #: api/lib/cmdb/resp_format.py:76
msgid "Model relationship {} does not exist" msgid "Model relationship {} does not exist"
msgstr "模型关系 {} 不存在" msgstr "模型关系 {} 不存在"
#: api/lib/cmdb/resp_format.py:73 #: api/lib/cmdb/resp_format.py:77
msgid "Attribute group {} already exists" msgid "Attribute group {} already exists"
msgstr "属性分组 {} 已存在" msgstr "属性分组 {} 已存在"
#: api/lib/cmdb/resp_format.py:74 #: api/lib/cmdb/resp_format.py:78
msgid "Attribute group {} does not exist" msgid "Attribute group {} does not exist"
msgstr "属性分组 {} 不存在" msgstr "属性分组 {} 不存在"
#: api/lib/cmdb/resp_format.py:76 #: api/lib/cmdb/resp_format.py:80
msgid "Attribute group <{0}> - attribute <{1}> does not exist" msgid "Attribute group <{0}> - attribute <{1}> does not exist"
msgstr "属性组<{0}> - 属性<{1}> 不存在" msgstr "属性组<{0}> - 属性<{1}> 不存在"
#: api/lib/cmdb/resp_format.py:77 #: api/lib/cmdb/resp_format.py:81
msgid "The unique constraint already exists!" msgid "The unique constraint already exists!"
msgstr "唯一约束已经存在!" msgstr "唯一约束已经存在!"
#: api/lib/cmdb/resp_format.py:79 #: api/lib/cmdb/resp_format.py:83
msgid "Uniquely constrained attributes cannot be JSON and multi-valued" msgid "Uniquely constrained attributes cannot be JSON and multi-valued"
msgstr "唯一约束的属性不能是 JSON 和 多值" msgstr "唯一约束的属性不能是 JSON 和 多值"
#: api/lib/cmdb/resp_format.py:80 #: api/lib/cmdb/resp_format.py:84
msgid "Duplicated trigger" msgid "Duplicated trigger"
msgstr "重复的触发器" msgstr "重复的触发器"
#: api/lib/cmdb/resp_format.py:81 #: api/lib/cmdb/resp_format.py:85
msgid "Trigger {} does not exist" msgid "Trigger {} does not exist"
msgstr "触发器 {} 不存在" msgstr "触发器 {} 不存在"
#: api/lib/cmdb/resp_format.py:82 #: api/lib/cmdb/resp_format.py:86
msgid "Duplicated reconciliation rule" msgid "Duplicated reconciliation rule"
msgstr "" msgstr ""
#: api/lib/cmdb/resp_format.py:83 #: api/lib/cmdb/resp_format.py:87
msgid "Reconciliation rule {} does not exist" msgid "Reconciliation rule {} does not exist"
msgstr "关系类型 {} 不存在" msgstr "关系类型 {} 不存在"
#: api/lib/cmdb/resp_format.py:85 #: api/lib/cmdb/resp_format.py:89
msgid "Operation record {} does not exist" msgid "Operation record {} does not exist"
msgstr "操作记录 {} 不存在" msgstr "操作记录 {} 不存在"
#: api/lib/cmdb/resp_format.py:86 #: api/lib/cmdb/resp_format.py:90
msgid "Unique identifier cannot be deleted" msgid "Unique identifier cannot be deleted"
msgstr "不能删除唯一标识" msgstr "不能删除唯一标识"
#: api/lib/cmdb/resp_format.py:87 #: api/lib/cmdb/resp_format.py:91
msgid "Cannot delete default sorted attributes" msgid "Cannot delete default sorted attributes"
msgstr "不能删除默认排序的属性" msgstr "不能删除默认排序的属性"
#: api/lib/cmdb/resp_format.py:89 #: api/lib/cmdb/resp_format.py:93
msgid "No node selected" msgid "No node selected"
msgstr "没有选择节点" msgstr "没有选择节点"
#: api/lib/cmdb/resp_format.py:90 #: api/lib/cmdb/resp_format.py:94
msgid "This search option does not exist!" msgid "This search option does not exist!"
msgstr "该搜索选项不存在!" msgstr "该搜索选项不存在!"
#: api/lib/cmdb/resp_format.py:91 #: api/lib/cmdb/resp_format.py:95
msgid "This search option has a duplicate name!" msgid "This search option has a duplicate name!"
msgstr "该搜索选项命名重复!" msgstr "该搜索选项命名重复!"
#: api/lib/cmdb/resp_format.py:93 #: api/lib/cmdb/resp_format.py:97
msgid "Relationship type {} already exists" msgid "Relationship type {} already exists"
msgstr "关系类型 {} 已经存在" msgstr "关系类型 {} 已经存在"
#: api/lib/cmdb/resp_format.py:94 #: api/lib/cmdb/resp_format.py:98
msgid "Relationship type {} does not exist" msgid "Relationship type {} does not exist"
msgstr "关系类型 {} 不存在" msgstr "关系类型 {} 不存在"
#: api/lib/cmdb/resp_format.py:96 #: api/lib/cmdb/resp_format.py:100
msgid "Invalid attribute value: {}" msgid "Invalid attribute value: {}"
msgstr "无效的属性值: {}" msgstr "无效的属性值: {}"
#: api/lib/cmdb/resp_format.py:97 #: api/lib/cmdb/resp_format.py:101
msgid "{} Invalid value: {}" msgid "{} Invalid value: {}"
msgstr "{} 无效的值: {}" msgstr "{} 无效的值: {}"
#: api/lib/cmdb/resp_format.py:98 #: api/lib/cmdb/resp_format.py:102
msgid "{} is not in the predefined values" msgid "{} is not in the predefined values"
msgstr "{} 不在预定义值里" msgstr "{} 不在预定义值里"
#: api/lib/cmdb/resp_format.py:100 #: api/lib/cmdb/resp_format.py:104
msgid "The value of attribute {} must be unique, {} already exists" msgid "The value of attribute {} must be unique, {} already exists"
msgstr "属性 {} 的值必须是唯一的, 当前值 {} 已存在" msgstr "属性 {} 的值必须是唯一的, 当前值 {} 已存在"
#: api/lib/cmdb/resp_format.py:101 #: api/lib/cmdb/resp_format.py:105
msgid "Attribute {} value must exist" msgid "Attribute {} value must exist"
msgstr "属性 {} 值必须存在" msgstr "属性 {} 值必须存在"
#: api/lib/cmdb/resp_format.py:102 #: api/lib/cmdb/resp_format.py:106
msgid "Out of range value, the maximum value is 2147483647" msgid "Out of range value, the maximum value is 2147483647"
msgstr "超过最大值限制, 最大值是2147483647" msgstr "超过最大值限制, 最大值是2147483647"
#: api/lib/cmdb/resp_format.py:104 #: api/lib/cmdb/resp_format.py:108
msgid "Unknown error when adding or modifying attribute value: {}" msgid "Unknown error when adding or modifying attribute value: {}"
msgstr "新增或者修改属性值未知错误: {}" msgstr "新增或者修改属性值未知错误: {}"
#: api/lib/cmdb/resp_format.py:106 #: api/lib/cmdb/resp_format.py:110
msgid "Duplicate custom name" msgid "Duplicate custom name"
msgstr "订制名重复" msgstr "订制名重复"
#: api/lib/cmdb/resp_format.py:108 #: api/lib/cmdb/resp_format.py:112
msgid "Number of models exceeds limit: {}" msgid "Number of models exceeds limit: {}"
msgstr "模型数超过限制: {}" msgstr "模型数超过限制: {}"
#: api/lib/cmdb/resp_format.py:109 #: api/lib/cmdb/resp_format.py:113
msgid "The number of CIs exceeds the limit: {}" msgid "The number of CIs exceeds the limit: {}"
msgstr "CI数超过限制: {}" msgstr "CI数超过限制: {}"
#: api/lib/cmdb/resp_format.py:111 #: api/lib/cmdb/resp_format.py:115
msgid "Auto-discovery rule: {} already exists!" msgid "Auto-discovery rule: {} already exists!"
msgstr "自动发现规则: {} 已经存在!" msgstr "自动发现规则: {} 已经存在!"
#: api/lib/cmdb/resp_format.py:112 #: api/lib/cmdb/resp_format.py:116
msgid "Auto-discovery rule: {} does not exist!" msgid "Auto-discovery rule: {} does not exist!"
msgstr "自动发现规则: {} 不存在!" msgstr "自动发现规则: {} 不存在!"
#: api/lib/cmdb/resp_format.py:114 #: api/lib/cmdb/resp_format.py:118
msgid "This auto-discovery rule is referenced by the model and cannot be deleted!" msgid "This auto-discovery rule is referenced by the model and cannot be deleted!"
msgstr "该自动发现规则被模型引用, 不能删除!" msgstr "该自动发现规则被模型引用, 不能删除!"
#: api/lib/cmdb/resp_format.py:116 #: api/lib/cmdb/resp_format.py:120
msgid "The application of auto-discovery rules cannot be defined repeatedly!" msgid "The application of auto-discovery rules cannot be defined repeatedly!"
msgstr "自动发现规则的应用不能重复定义!" msgstr "自动发现规则的应用不能重复定义!"
#: api/lib/cmdb/resp_format.py:117 #: api/lib/cmdb/resp_format.py:121
msgid "The auto-discovery you want to modify: {} does not exist!" msgid "The auto-discovery you want to modify: {} does not exist!"
msgstr "您要修改的自动发现: {} 不存在!" msgstr "您要修改的自动发现: {} 不存在!"
#: api/lib/cmdb/resp_format.py:118 #: api/lib/cmdb/resp_format.py:122
msgid "Attribute does not include unique identifier: {}" msgid "Attribute does not include unique identifier: {}"
msgstr "属性字段没有包括唯一标识: {}" msgstr "属性字段没有包括唯一标识: {}"
#: api/lib/cmdb/resp_format.py:119 #: api/lib/cmdb/resp_format.py:123
msgid "The auto-discovery instance does not exist!" msgid "The auto-discovery instance does not exist!"
msgstr "自动发现的实例不存在!" msgstr "自动发现的实例不存在!"
#: api/lib/cmdb/resp_format.py:120 #: api/lib/cmdb/resp_format.py:124
msgid "The model is not associated with this auto-discovery!" msgid "The model is not associated with this auto-discovery!"
msgstr "模型并未关联该自动发现!" msgstr "模型并未关联该自动发现!"
#: api/lib/cmdb/resp_format.py:121 #: api/lib/cmdb/resp_format.py:125
msgid "Only the creator can modify the Secret!" msgid "Only the creator can modify the Secret!"
msgstr "只有创建人才能修改Secret!" msgstr "只有创建人才能修改Secret!"
#: api/lib/cmdb/resp_format.py:123 #: api/lib/cmdb/resp_format.py:127
msgid "This rule already has auto-discovery instances and cannot be deleted!" msgid "This rule already has auto-discovery instances and cannot be deleted!"
msgstr "该规则已经有自动发现的实例, 不能被删除!" msgstr "该规则已经有自动发现的实例, 不能被删除!"
#: api/lib/cmdb/resp_format.py:125 #: api/lib/cmdb/resp_format.py:129
msgid "The default auto-discovery rule is already referenced by model {}!" msgid "The default auto-discovery rule is already referenced by model {}!"
msgstr "该默认的自动发现规则 已经被模型 {} 引用!" msgstr "该默认的自动发现规则 已经被模型 {} 引用!"
#: api/lib/cmdb/resp_format.py:127 #: api/lib/cmdb/resp_format.py:131
msgid "The unique_key method must return a non-empty string!" msgid "The unique_key method must return a non-empty string!"
msgstr "unique_key方法必须返回非空字符串!" msgstr "unique_key方法必须返回非空字符串!"
#: api/lib/cmdb/resp_format.py:128 #: api/lib/cmdb/resp_format.py:132
msgid "The attributes method must return a list" msgid "The attributes method must return a list"
msgstr "attributes方法必须返回的是list" msgstr "attributes方法必须返回的是list"
#: api/lib/cmdb/resp_format.py:130 #: api/lib/cmdb/resp_format.py:134
msgid "The list returned by the attributes method cannot be empty!" msgid "The list returned by the attributes method cannot be empty!"
msgstr "attributes方法返回的list不能为空!" msgstr "attributes方法返回的list不能为空!"
#: api/lib/cmdb/resp_format.py:132 #: api/lib/cmdb/resp_format.py:136
msgid "Only administrators can define execution targets as: all nodes!" msgid "Only administrators can define execution targets as: all nodes!"
msgstr "只有管理员才可以定义执行机器为: 所有节点!" msgstr "只有管理员才可以定义执行机器为: 所有节点!"
#: api/lib/cmdb/resp_format.py:133 #: api/lib/cmdb/resp_format.py:137
msgid "Execute targets permission check failed: {}" msgid "Execute targets permission check failed: {}"
msgstr "执行机器权限检查不通过: {}" msgstr "执行机器权限检查不通过: {}"
#: api/lib/cmdb/resp_format.py:135 #: api/lib/cmdb/resp_format.py:139
msgid "CI filter authorization must be named!" msgid "CI filter authorization must be named!"
msgstr "CI过滤授权 必须命名!" msgstr "CI过滤授权 必须命名!"
#: api/lib/cmdb/resp_format.py:136 #: api/lib/cmdb/resp_format.py:140
msgid "CI filter authorization is currently not supported or query" msgid "CI filter authorization is currently not supported or query"
msgstr "CI过滤授权 暂时不支持 或 查询" msgstr "CI过滤授权 暂时不支持 或 查询"
#: api/lib/cmdb/resp_format.py:139 #: api/lib/cmdb/resp_format.py:143
msgid "You do not have permission to operate attribute {}!" msgid "You do not have permission to operate attribute {}!"
msgstr "您没有属性 {} 的操作权限!" msgstr "您没有属性 {} 的操作权限!"
#: api/lib/cmdb/resp_format.py:140 #: api/lib/cmdb/resp_format.py:144
msgid "You do not have permission to operate this CI!" msgid "You do not have permission to operate this CI!"
msgstr "您没有该CI的操作权限!" msgstr "您没有该CI的操作权限!"
#: api/lib/cmdb/resp_format.py:142 #: api/lib/cmdb/resp_format.py:146
msgid "Failed to save password: {}" msgid "Failed to save password: {}"
msgstr "保存密码失败: {}" msgstr "保存密码失败: {}"
#: api/lib/cmdb/resp_format.py:143 #: api/lib/cmdb/resp_format.py:147
msgid "Failed to get password: {}" msgid "Failed to get password: {}"
msgstr "获取密码失败: {}" msgstr "获取密码失败: {}"
#: api/lib/cmdb/resp_format.py:145 #: api/lib/cmdb/resp_format.py:149
msgid "Scheduling time format error" msgid "Scheduling time format error"
msgstr "{}格式错误,应该为:%Y-%m-%d %H:%M:%S" msgstr "{}格式错误,应该为:%Y-%m-%d %H:%M:%S"
#: api/lib/cmdb/resp_format.py:146 #: api/lib/cmdb/resp_format.py:150
msgid "CMDB data reconciliation results" msgid "CMDB data reconciliation results"
msgstr "" msgstr ""
#: api/lib/cmdb/resp_format.py:147 #: api/lib/cmdb/resp_format.py:151
msgid "Number of {} illegal: {}" msgid "Number of {} illegal: {}"
msgstr "" msgstr ""
#: api/lib/cmdb/resp_format.py:149 #: api/lib/cmdb/resp_format.py:153
msgid "Topology view {} already exists" msgid "Topology view {} already exists"
msgstr "拓扑视图 {} 已经存在" msgstr "拓扑视图 {} 已经存在"
#: api/lib/cmdb/resp_format.py:150 #: api/lib/cmdb/resp_format.py:154
msgid "Topology group {} already exists" msgid "Topology group {} already exists"
msgstr "拓扑视图分组 {} 已经存在" msgstr "拓扑视图分组 {} 已经存在"
#: api/lib/cmdb/resp_format.py:152 #: api/lib/cmdb/resp_format.py:156
msgid "The group cannot be deleted because the topology view already exists" msgid "The group cannot be deleted because the topology view already exists"
msgstr "因为该分组下定义了拓扑视图,不能删除" msgstr "因为该分组下定义了拓扑视图,不能删除"
#: api/lib/cmdb/resp_format.py:158
msgid "Both the source model and the target model must be selected"
msgstr "源模型和目标模型不能为空!"
#: api/lib/common_setting/resp_format.py:8 #: api/lib/common_setting/resp_format.py:8
msgid "Company info already existed" msgid "Company info already existed"
msgstr "公司信息已存在,无法创建!" msgstr "公司信息已存在,无法创建!"

View File

@@ -1,7 +1,6 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
import datetime import datetime
import jwt import jwt
import six import six
from flask import abort from flask import abort
@@ -17,10 +16,12 @@ from api.lib.decorator import args_required
from api.lib.decorator import args_validate from api.lib.decorator import args_validate
from api.lib.perm.acl.acl import ACLManager from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.audit import AuditCRUD from api.lib.perm.acl.audit import AuditCRUD
from api.lib.perm.acl.cache import AppCache
from api.lib.perm.acl.cache import RoleCache from api.lib.perm.acl.cache import RoleCache
from api.lib.perm.acl.cache import User from api.lib.perm.acl.cache import User
from api.lib.perm.acl.cache import UserCache from api.lib.perm.acl.cache import UserCache
from api.lib.perm.acl.resp_format import ErrFormat from api.lib.perm.acl.resp_format import ErrFormat
from api.lib.perm.acl.role import RoleRelationCRUD
from api.lib.perm.auth import auth_abandoned from api.lib.perm.auth import auth_abandoned
from api.lib.perm.auth import auth_with_app_token from api.lib.perm.auth import auth_with_app_token
from api.models.acl import Role from api.models.acl import Role
@@ -124,10 +125,17 @@ class AuthWithKeyView(APIView):
if not user.get('username'): if not user.get('username'):
user['username'] = user.get('name') user['username'] = user.get('name')
return self.jsonify(user=user, result = dict(user=user,
authenticated=authenticated, authenticated=authenticated,
rid=role and role.id, rid=role and role.id,
can_proxy=can_proxy) can_proxy=can_proxy)
if request.values.get('need_parentRoles') in current_app.config.get('BOOL_TRUE'):
app_id = AppCache.get(request.values.get('app_id'))
parent_ids = RoleRelationCRUD.recursive_parent_ids(role and role.id, app_id and app_id.id)
result['user']['parentRoles'] = [RoleCache.get(rid).name for rid in set(parent_ids) if RoleCache.get(rid)]
return self.jsonify(result)
class AuthWithTokenView(APIView): class AuthWithTokenView(APIView):
@@ -184,6 +192,8 @@ class LogoutView(APIView):
def post(self): def post(self):
logout_user() logout_user()
AuditCRUD.add_login_log(None, None, None, _id=session.get('LOGIN_ID'), logout_at=datetime.datetime.now()) AuditCRUD.add_login_log(None, None, None,
_id=session.get('LOGIN_ID') or request.values.get('LOGIN_ID'),
logout_at=datetime.datetime.now())
self.jsonify(code=200) self.jsonify(code=200)

View File

@@ -11,6 +11,7 @@ from flask_login import current_user
from api.lib.decorator import args_required from api.lib.decorator import args_required
from api.lib.decorator import args_validate from api.lib.decorator import args_validate
from api.lib.perm.acl.acl import ACLManager from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import AuditCRUD
from api.lib.perm.acl.acl import role_required from api.lib.perm.acl.acl import role_required
from api.lib.perm.acl.cache import AppCache from api.lib.perm.acl.cache import AppCache
from api.lib.perm.acl.cache import UserCache from api.lib.perm.acl.cache import UserCache
@@ -48,6 +49,13 @@ class GetUserInfoView(APIView):
role=dict(permissions=user_info.get('parents')), role=dict(permissions=user_info.get('parents')),
avatar=user_info.get('avatar')) avatar=user_info.get('avatar'))
if request.values.get('channel'):
_id = AuditCRUD.add_login_log(name, True, ErrFormat.login_succeed,
ip=request.values.get('ip'),
browser=request.values.get('browser'))
session['LOGIN_ID'] = _id
result['LOGIN_ID'] = _id
current_app.logger.info("get user info for3: {}".format(result)) current_app.logger.info("get user info for3: {}".format(result))
return self.jsonify(result=result) return self.jsonify(result=result)

View File

@@ -8,6 +8,7 @@ from flask import request
from flask_login import current_user from flask_login import current_user
from io import BytesIO from io import BytesIO
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryAccountCRUD
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCICRUD from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCICRUD
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCITypeCRUD from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCITypeCRUD
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCITypeRelationCRUD from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCITypeRelationCRUD
@@ -20,6 +21,7 @@ from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryRuleSyncHist
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoverySNMPManager from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoverySNMPManager
from api.lib.cmdb.auto_discovery.const import DEFAULT_INNER from api.lib.cmdb.auto_discovery.const import DEFAULT_INNER
from api.lib.cmdb.auto_discovery.const import PRIVILEGED_USERS from api.lib.cmdb.auto_discovery.const import PRIVILEGED_USERS
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.const import PermEnum from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.resp_format import ErrFormat from api.lib.cmdb.resp_format import ErrFormat
@@ -111,6 +113,7 @@ class AutoDiscoveryRuleTemplateFileView(APIView):
class AutoDiscoveryRuleHTTPView(APIView): class AutoDiscoveryRuleHTTPView(APIView):
url_prefix = ("/adr/http/<string:name>/categories", url_prefix = ("/adr/http/<string:name>/categories",
"/adr/http/<string:name>/attributes", "/adr/http/<string:name>/attributes",
"/adr/http/<string:name>/mapping",
"/adr/snmp/<string:name>/attributes", "/adr/snmp/<string:name>/attributes",
"/adr/components/<string:name>/attributes",) "/adr/components/<string:name>/attributes",)
@@ -125,6 +128,10 @@ class AutoDiscoveryRuleHTTPView(APIView):
resource = request.values.get('resource') resource = request.values.get('resource')
return self.jsonify(AutoDiscoveryHTTPManager.get_attributes(name, resource)) return self.jsonify(AutoDiscoveryHTTPManager.get_attributes(name, resource))
if "mapping" in request.url:
resource = request.values.get('resource')
return self.jsonify(AutoDiscoveryHTTPManager.get_mapping(name, resource))
return self.jsonify(AutoDiscoveryHTTPManager.get_categories(name)) return self.jsonify(AutoDiscoveryHTTPManager.get_categories(name))
@@ -144,6 +151,11 @@ class AutoDiscoveryCITypeView(APIView):
i['extra_option'].pop('secret', None) i['extra_option'].pop('secret', None)
else: else:
i['extra_option']['secret'] = AESCrypto.decrypt(i['extra_option']['secret']) i['extra_option']['secret'] = AESCrypto.decrypt(i['extra_option']['secret'])
if isinstance(i.get("extra_option"), dict) and i['extra_option'].get('password'):
if not (current_user.username == "cmdb_agent" or current_user.uid == i['uid']):
i['extra_option'].pop('password', None)
else:
i['extra_option']['password'] = AESCrypto.decrypt(i['extra_option']['password'])
return self.jsonify(res) return self.jsonify(res)
@@ -213,6 +225,7 @@ class AutoDiscoveryCIView(APIView):
@args_required("type_id") @args_required("type_id")
@args_required("adt_id") @args_required("adt_id")
@args_required("instance") @args_required("instance")
@args_required("unique_value")
def post(self): def post(self):
request.values.pop("_key", None) request.values.pop("_key", None)
request.values.pop("_secret", None) request.values.pop("_secret", None)
@@ -262,14 +275,16 @@ class AutoDiscoveryRuleSyncView(APIView):
oneagent_id = request.values.get('oneagent_id') oneagent_id = request.values.get('oneagent_id')
last_update_at = request.values.get('last_update_at') last_update_at = request.values.get('last_update_at')
query = "oneagent_id:{}".format(oneagent_id) response = []
s = ci_search(query) if AttributeCache.get('oneagent_id'):
try: query = "oneagent_id:{}".format(oneagent_id)
response, _, _, _, _, _ = s.search() s = ci_search(query)
except SearchError as e: try:
import traceback response, _, _, _, _, _ = s.search()
current_app.logger.error(traceback.format_exc()) except SearchError as e:
return abort(400, str(e)) import traceback
current_app.logger.error(traceback.format_exc())
return abort(400, str(e))
for res in response: for res in response:
if res.get('{}_name'.format(res['ci_type'])) == oneagent_name or oneagent_name == res.get('oneagent_name'): if res.get('{}_name'.format(res['ci_type'])) == oneagent_name or oneagent_name == res.get('oneagent_name'):
@@ -318,8 +333,12 @@ class AutoDiscoveryExecHistoryView(APIView):
def get(self): def get(self):
page = get_page(request.values.pop('page', 1)) page = get_page(request.values.pop('page', 1))
page_size = get_page_size(request.values.pop('page_size', None)) page_size = get_page_size(request.values.pop('page_size', None))
last_size = request.values.pop('last_size', None)
if last_size and last_size.isdigit():
last_size = int(last_size)
numfound, res = AutoDiscoveryExecHistoryCRUD.search(page=page, numfound, res = AutoDiscoveryExecHistoryCRUD.search(page=page,
page_size=page_size, page_size=page_size,
last_size=last_size,
**request.values) **request.values)
return self.jsonify(page=page, return self.jsonify(page=page,
@@ -345,3 +364,31 @@ class AutoDiscoveryCounterView(APIView):
type_id = request.values.get('type_id') type_id = request.values.get('type_id')
return self.jsonify(AutoDiscoveryCounterCRUD().get(type_id)) return self.jsonify(AutoDiscoveryCounterCRUD().get(type_id))
class AutoDiscoveryAccountView(APIView):
url_prefix = ("/adr/accounts", "/adr/accounts/<int:account_id>")
@args_required('adr_id')
def get(self):
adr_id = request.values.get('adr_id')
return self.jsonify(AutoDiscoveryAccountCRUD().get(adr_id))
@args_required('adr_id')
@args_required('accounts', value_required=False)
def post(self):
AutoDiscoveryAccountCRUD().upsert(**request.values)
return self.jsonify(code=200)
@args_required('config')
def put(self, account_id):
res = AutoDiscoveryAccountCRUD().update(account_id, **request.values)
return self.jsonify(res.to_dict())
def delete(self, account_id):
AutoDiscoveryAccountCRUD().delete(account_id)
return self.jsonify(account_id=account_id)

View File

@@ -15,7 +15,7 @@ from api.lib.cmdb.const import ResourceTypeEnum, PermEnum
from api.lib.cmdb.const import RetKey from api.lib.cmdb.const import RetKey
from api.lib.cmdb.perms import has_perm_for_ci from api.lib.cmdb.perms import has_perm_for_ci
from api.lib.cmdb.search import SearchError from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search from api.lib.cmdb.search.ci import search as ci_search
from api.lib.decorator import args_required from api.lib.decorator import args_required
from api.lib.perm.acl.acl import has_perm_from_args from api.lib.perm.acl.acl import has_perm_from_args
from api.lib.utils import get_page from api.lib.utils import get_page
@@ -160,7 +160,7 @@ class CISearchView(APIView):
use_id_filter = request.values.get("use_id_filter", False) in current_app.config.get('BOOL_TRUE') use_id_filter = request.values.get("use_id_filter", False) in current_app.config.get('BOOL_TRUE')
start = time.time() start = time.time()
s = search(query, fl, facet, page, ret_key, count, sort, excludes, use_id_filter=use_id_filter) s = ci_search(query, fl, facet, page, ret_key, count, sort, excludes, use_id_filter=use_id_filter)
try: try:
response, counter, total, page, numfound, facet = s.search() response, counter, total, page, numfound, facet = s.search()
except SearchError as e: except SearchError as e:

View File

@@ -2,7 +2,6 @@
import time import time
from flask import abort from flask import abort
from flask import current_app from flask import current_app
from flask import request from flask import request
@@ -65,6 +64,42 @@ class CIRelationSearchView(APIView):
result=response) result=response)
class CIRelationSearchPathView(APIView):
url_prefix = ("/ci_relations/path/s", "/ci_relations/path/search")
@args_required("source", "target", "path")
def post(self):
"""@params: page: page number
page_size | count: page size
source: source CIType, e.g. {type_id: 1, q: `search expr`}
target: target CIType, e.g. {type_ids: [2], q: `search expr`}
path: Path from the Source CIType to the Target CIType, e.g. [1, ..., 2]
"""
page = get_page(request.values.get("page", 1))
count = get_page_size(request.values.get("count") or request.values.get("page_size"))
source = request.values.get("source")
target = request.values.get("target")
path = request.values.get("path")
s = Search(page=page, count=count)
try:
(response, counter, total, page, numfound, id2ci,
relation_types, type2show_key) = s.search_by_path(source, target, path)
except SearchError as e:
return abort(400, str(e))
return self.jsonify(numfound=numfound,
total=total,
page=page,
counter=counter,
paths=response,
id2ci=id2ci,
relation_types=relation_types,
type2show_key=type2show_key)
class CIRelationStatisticsView(APIView): class CIRelationStatisticsView(APIView):
url_prefix = "/ci_relations/statistics" url_prefix = "/ci_relations/statistics"

View File

@@ -48,16 +48,21 @@ class CITypeView(APIView):
if request.url.endswith("icons"): if request.url.endswith("icons"):
return self.jsonify(CITypeManager().get_icons()) return self.jsonify(CITypeManager().get_icons())
q = request.args.get("type_name") q = request.values.get("type_name")
type_ids = handle_arg_list(request.values.get("type_ids"))
type_ids = type_ids or (type_id and [type_id])
if type_ids:
ci_types = []
for _type_id in type_ids:
ci_type = CITypeCache.get(_type_id)
if ci_type is None:
return abort(404, ErrFormat.ci_type_not_found)
if type_id is not None: ci_type = ci_type.to_dict()
ci_type = CITypeCache.get(type_id) ci_type['parent_ids'] = CITypeInheritanceManager.get_parents(_type_id)
if ci_type is None: ci_type['show_name'] = ci_type.get('show_id') and AttributeCache.get(ci_type['show_id']).name
return abort(404, ErrFormat.ci_type_not_found) ci_type['unique_name'] = ci_type['unique_id'] and AttributeCache.get(ci_type['unique_id']).name
ci_types.append(ci_type)
ci_type = ci_type.to_dict()
ci_type['parent_ids'] = CITypeInheritanceManager.get_parents(type_id)
ci_types = [ci_type]
elif type_name is not None: elif type_name is not None:
ci_type = CITypeCache.get(type_name).to_dict() ci_type = CITypeCache.get(type_name).to_dict()
ci_type['parent_ids'] = CITypeInheritanceManager.get_parents(ci_type['id']) ci_type['parent_ids'] = CITypeInheritanceManager.get_parents(ci_type['id'])

View File

@@ -8,7 +8,6 @@ from api.lib.cmdb.ci_type import CITypeManager
from api.lib.cmdb.ci_type import CITypeRelationManager from api.lib.cmdb.ci_type import CITypeRelationManager
from api.lib.cmdb.const import PermEnum from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.preference import PreferenceManager from api.lib.cmdb.preference import PreferenceManager
from api.lib.cmdb.resp_format import ErrFormat from api.lib.cmdb.resp_format import ErrFormat
from api.lib.common_setting.decorator import perms_role_required from api.lib.common_setting.decorator import perms_role_required
@@ -17,7 +16,7 @@ from api.lib.decorator import args_required
from api.lib.perm.acl.acl import ACLManager from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import has_perm_from_args from api.lib.perm.acl.acl import has_perm_from_args
from api.lib.perm.acl.acl import is_app_admin from api.lib.perm.acl.acl import is_app_admin
from api.lib.perm.acl.acl import role_required from api.lib.utils import handle_arg_list
from api.resource import APIView from api.resource import APIView
app_cli = CMDBApp() app_cli = CMDBApp()
@@ -42,6 +41,19 @@ class GetParentsView(APIView):
return self.jsonify(parents=CITypeRelationManager.get_parents(child_id)) return self.jsonify(parents=CITypeRelationManager.get_parents(child_id))
class CITypeRelationPathView(APIView):
url_prefix = ("/ci_type_relations/path",)
@args_required("source_type_id", "target_type_ids")
def get(self):
source_type_id = request.values.get("source_type_id")
target_type_ids = handle_arg_list(request.values.get("target_type_ids"))
paths = CITypeRelationManager.find_path(source_type_id, target_type_ids)
return self.jsonify(paths=paths)
class CITypeRelationView(APIView): class CITypeRelationView(APIView):
url_prefix = ("/ci_type_relations", "/ci_type_relations/<int:parent_id>/<int:child_id>") url_prefix = ("/ci_type_relations", "/ci_type_relations/<int:parent_id>/<int:child_id>")

View File

@@ -55,3 +55,5 @@ pycryptodomex>=3.19.0
colorama>=0.4.6 colorama>=0.4.6
lz4>=4.3.2 lz4>=4.3.2
python-magic==0.4.27 python-magic==0.4.27
jsonpath==0.82.2
networkx>=3.1

View File

@@ -39,9 +39,9 @@ SQLALCHEMY_ENGINE_OPTIONS = {
# # cache # # cache
CACHE_TYPE = 'redis' CACHE_TYPE = 'redis'
CACHE_REDIS_HOST = '127.0.0.1' CACHE_REDIS_HOST = env.str('CACHE_REDIS_HOST', default='redis')
CACHE_REDIS_PORT = 6379 CACHE_REDIS_PORT = env.str('CACHE_REDIS_PORT', default='6379')
CACHE_REDIS_PASSWORD = '' CACHE_REDIS_PASSWORD = env.str('CACHE_REDIS_PASSWORD', default='')
CACHE_KEY_PREFIX = 'CMDB::' CACHE_KEY_PREFIX = 'CMDB::'
CACHE_DEFAULT_TIMEOUT = 3000 CACHE_DEFAULT_TIMEOUT = 3000

View File

@@ -3,3 +3,4 @@ VUE_APP_PREVIEW=false
VUE_APP_API_BASE_URL=http://127.0.0.1:5000/api VUE_APP_API_BASE_URL=http://127.0.0.1:5000/api
VUE_APP_BUILD_PACKAGES="ticket,calendar,acl" VUE_APP_BUILD_PACKAGES="ticket,calendar,acl"
VUE_APP_IS_OUTER=true VUE_APP_IS_OUTER=true
VUE_APP_IS_OPEN_SOURCE=true

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
@font-face { @font-face {
font-family: "iconfont"; /* Project id 3857903 */ font-family: "iconfont"; /* Project id 3857903 */
src: url('iconfont.woff2?t=1719487341033') format('woff2'), src: url('iconfont.woff2?t=1729157759723') format('woff2'),
url('iconfont.woff?t=1719487341033') format('woff'), url('iconfont.woff?t=1729157759723') format('woff'),
url('iconfont.ttf?t=1719487341033') format('truetype'); url('iconfont.ttf?t=1729157759723') format('truetype');
} }
.iconfont { .iconfont {
@@ -13,6 +13,510 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.oneterm-mysql:before {
content: "\e9e8";
}
.oneterm-redis:before {
content: "\e9e7";
}
.veops-sign_out:before {
content: "\e9e6";
}
.veops-company:before {
content: "\e9e5";
}
.veops-emails:before {
content: "\e9e4";
}
.veops-switch:before {
content: "\e9e3";
}
.qiyeweixin:before {
content: "\e9e2";
}
.veops-progress:before {
content: "\e9e1";
}
.veops-completed:before {
content: "\e9e0";
}
.itsm-ticketTime:before {
content: "\e9df";
}
.veops-notification:before {
content: "\e9dc";
}
.a-veops-account1:before {
content: "\e9dd";
}
.veops-personal:before {
content: "\e9de";
}
.itsm-customer_satisfaction2:before {
content: "\e9da";
}
.itsm-over2:before {
content: "\e9db";
}
.veops-search1:before {
content: "\e9d9";
}
.itsm-customer_satisfaction:before {
content: "\e9d8";
}
.itsm-over:before {
content: "\e9d7";
}
.itsm-request:before {
content: "\e9d6";
}
.itsm-release:before {
content: "\e9d5";
}
.veops-link:before {
content: "\e9d4";
}
.oneterm-command_record:before {
content: "\e9d3";
}
.ai-question:before {
content: "\e9d2";
}
.ai-sending:before {
content: "\e9d1";
}
.ai-dialogue:before {
content: "\e9d0";
}
.ai-report2:before {
content: "\e9cf";
}
.ai-delete:before {
content: "\e9cd";
}
.caise-knowledge:before {
content: "\e9ce";
}
.ai-article:before {
content: "\e9cc";
}
.ai-model_setup1:before {
content: "\e9cb";
}
.ai-report:before {
content: "\e9ca";
}
.ai-customer_service:before {
content: "\e9c9";
}
.oneterm-connect1:before {
content: "\e9c6";
}
.oneterm-session1:before {
content: "\e9c7";
}
.oneterm-assets:before {
content: "\e9c8";
}
.a-oneterm-ssh1:before {
content: "\e9c3";
}
.a-oneterm-ssh2:before {
content: "\e9c4";
}
.oneterm-rdp:before {
content: "\e9c5";
}
.caise-websphere:before {
content: "\e9c2";
}
.caise-vps:before {
content: "\e9c1";
}
.caise-F5:before {
content: "\e9c0";
}
.caise-HAProxy:before {
content: "\e9bf";
}
.caise-JBoss:before {
content: "\e9be";
}
.caise-dongfangtong:before {
content: "\e9bd";
}
.caise-kafka:before {
content: "\e9b7";
}
.caise-weblogic:before {
content: "\e9b8";
}
.caise-TDSQL:before {
content: "\e9b9";
}
.caise-kingbase:before {
content: "\e9ba";
}
.caise-dameng:before {
content: "\e9bb";
}
.caise-TIDB:before {
content: "\e9bc";
}
.veops-expand:before {
content: "\e9b6";
}
.caise-public_cloud:before {
content: "\e9b1";
}
.caise-system:before {
content: "\e9b2";
}
.caise-IPAM:before {
content: "\e9b3";
}
.caise-hyperV:before {
content: "\e9b4";
}
.caise-data_center2:before {
content: "\e9b5";
}
.caise-hardware:before {
content: "\e9ad";
}
.caise-computer:before {
content: "\e9ae";
}
.caise-network_devices:before {
content: "\e9af";
}
.caise-storage_device:before {
content: "\e9b0";
}
.caise-load_balancing:before {
content: "\e9ab";
}
.caise-message_queue:before {
content: "\e9ac";
}
.caise-websever:before {
content: "\e9aa";
}
.caise-middleware:before {
content: "\e9a9";
}
.caise-database:before {
content: "\e9a7";
}
.caise-business:before {
content: "\e9a8";
}
.caise-virtualization:before {
content: "\e9a6";
}
.caise-storage_pool:before {
content: "\e9a4";
}
.caise-storage_volume1:before {
content: "\e9a5";
}
.ciase-aix:before {
content: "\e9a3";
}
.caise_pool:before {
content: "\e99b";
}
.caise-ip_address:before {
content: "\e99c";
}
.caise-computer_room:before {
content: "\e99d";
}
.caise-rack:before {
content: "\e99e";
}
.caise-pc:before {
content: "\e99f";
}
.caise-bandwidth_line:before {
content: "\e9a0";
}
.caise-fiber:before {
content: "\e9a1";
}
.caise-disk_array:before {
content: "\e9a2";
}
.veops-group:before {
content: "\e99a";
}
.veops-inheritance:before {
content: "\e999";
}
.veops-department:before {
content: "\e998";
}
.duose-changwenben1:before {
content: "\e997";
}
.duose-quote:before {
content: "\e995";
}
.duose-boole:before {
content: "\e996";
}
.veops-rule1:before {
content: "\e994";
}
.veops-operation_report:before {
content: "\e993";
}
.veops-ranking1:before {
content: "\e992";
}
.veops-ranking2:before {
content: "\e98f";
}
.veops-ranking3:before {
content: "\e990";
}
.veops-ranking4:before {
content: "\e991";
}
.veops-title5:before {
content: "\e98d";
}
.veops-repair1:before {
content: "\e98e";
}
.veops-ticket:before {
content: "\e988";
}
.veops-model4:before {
content: "\e989";
}
.veops-resource21:before {
content: "\e98a";
}
.veops-relationship3:before {
content: "\e98b";
}
.veops-title6:before {
content: "\e98c";
}
.veops-resource11:before {
content: "\e97a";
}
.veops-model11:before {
content: "\e97b";
}
.veops-relationship1:before {
content: "\e97c";
}
.veops-title1:before {
content: "\e97d";
}
.veops-title2:before {
content: "\e97e";
}
.veops-model2:before {
content: "\e97f";
}
.veops-resource2:before {
content: "\e980";
}
.veops-warehousing:before {
content: "\e981";
}
.veops-relationship2:before {
content: "\e982";
}
.veops-title3:before {
content: "\e983";
}
.veops-rule2:before {
content: "\e984";
}
.veops-model3:before {
content: "\e985";
}
.veops-title4:before {
content: "\e986";
}
.veops-rule3:before {
content: "\e987";
}
.veops-decline:before {
content: "\e978";
}
.veops-rise:before {
content: "\e979";
}
.caise-data_center:before {
content: "\e96f";
}
.caise-folder:before {
content: "\e970";
}
.caise-resource_pool:before {
content: "\e971";
}
.caise-network:before {
content: "\e972";
}
.caise-distributed_switch:before {
content: "\e973";
}
.caise-standard_switch:before {
content: "\e974";
}
.caise-host_cluster:before {
content: "\e975";
}
.caise-storage_cluster:before {
content: "\e976";
}
.caise-data_storage:before {
content: "\e977";
}
.veops-account:before {
content: "\e96e";
}
.veops-collect:before {
content: "\e96d";
}
.veops-collected:before {
content: "\e96c";
}
.veops-text:before {
content: "\e96b";
}
.veops-markdown:before { .veops-markdown:before {
content: "\e96a"; content: "\e96a";
} }

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,888 @@
"css_prefix_text": "", "css_prefix_text": "",
"description": "", "description": "",
"glyphs": [ "glyphs": [
{
"icon_id": "42155223",
"name": "oneterm-mysql",
"font_class": "oneterm-mysql",
"unicode": "e9e8",
"unicode_decimal": 59880
},
{
"icon_id": "42155225",
"name": "oneterm-redis",
"font_class": "oneterm-redis",
"unicode": "e9e7",
"unicode_decimal": 59879
},
{
"icon_id": "42154436",
"name": "veops-sign_out",
"font_class": "veops-sign_out",
"unicode": "e9e6",
"unicode_decimal": 59878
},
{
"icon_id": "42154310",
"name": "veops-company",
"font_class": "veops-company",
"unicode": "e9e5",
"unicode_decimal": 59877
},
{
"icon_id": "42154325",
"name": "veops-emails",
"font_class": "veops-emails",
"unicode": "e9e4",
"unicode_decimal": 59876
},
{
"icon_id": "42154350",
"name": "veops-switch",
"font_class": "veops-switch",
"unicode": "e9e3",
"unicode_decimal": 59875
},
{
"icon_id": "42154370",
"name": "veops-qiyeweixin",
"font_class": "qiyeweixin",
"unicode": "e9e2",
"unicode_decimal": 59874
},
{
"icon_id": "42134185",
"name": "veops-progress",
"font_class": "veops-progress",
"unicode": "e9e1",
"unicode_decimal": 59873
},
{
"icon_id": "42134110",
"name": "veops-completed",
"font_class": "veops-completed",
"unicode": "e9e0",
"unicode_decimal": 59872
},
{
"icon_id": "42133882",
"name": "itsm-ticketTime",
"font_class": "itsm-ticketTime",
"unicode": "e9df",
"unicode_decimal": 59871
},
{
"icon_id": "42122869",
"name": "veops-notification",
"font_class": "veops-notification",
"unicode": "e9dc",
"unicode_decimal": 59868
},
{
"icon_id": "42122868",
"name": "veops-account_password",
"font_class": "a-veops-account1",
"unicode": "e9dd",
"unicode_decimal": 59869
},
{
"icon_id": "42122861",
"name": "veops-personal",
"font_class": "veops-personal",
"unicode": "e9de",
"unicode_decimal": 59870
},
{
"icon_id": "42101103",
"name": "itsm-evaluation2",
"font_class": "itsm-customer_satisfaction2",
"unicode": "e9da",
"unicode_decimal": 59866
},
{
"icon_id": "42101098",
"name": "itsm-over2",
"font_class": "itsm-over2",
"unicode": "e9db",
"unicode_decimal": 59867
},
{
"icon_id": "42065574",
"name": "veops-search",
"font_class": "veops-search1",
"unicode": "e9d9",
"unicode_decimal": 59865
},
{
"icon_id": "42063479",
"name": "itsm-evaluation",
"font_class": "itsm-customer_satisfaction",
"unicode": "e9d8",
"unicode_decimal": 59864
},
{
"icon_id": "42062436",
"name": "itsm-over",
"font_class": "itsm-over",
"unicode": "e9d7",
"unicode_decimal": 59863
},
{
"icon_id": "42050642",
"name": "itsm-requirement",
"font_class": "itsm-request",
"unicode": "e9d6",
"unicode_decimal": 59862
},
{
"icon_id": "42050622",
"name": "itsm-release",
"font_class": "itsm-release",
"unicode": "e9d5",
"unicode_decimal": 59861
},
{
"icon_id": "41903314",
"name": "veops-link",
"font_class": "veops-link",
"unicode": "e9d4",
"unicode_decimal": 59860
},
{
"icon_id": "41876664",
"name": "oneterm-command_record",
"font_class": "oneterm-command_record",
"unicode": "e9d3",
"unicode_decimal": 59859
},
{
"icon_id": "41859436",
"name": "ai-question",
"font_class": "ai-question",
"unicode": "e9d2",
"unicode_decimal": 59858
},
{
"icon_id": "41859414",
"name": "ai-sending",
"font_class": "ai-sending",
"unicode": "e9d1",
"unicode_decimal": 59857
},
{
"icon_id": "41859374",
"name": "ai-dialogue",
"font_class": "ai-dialogue",
"unicode": "e9d0",
"unicode_decimal": 59856
},
{
"icon_id": "41859191",
"name": "ai-report2",
"font_class": "ai-report2",
"unicode": "e9cf",
"unicode_decimal": 59855
},
{
"icon_id": "41858720",
"name": "ai-delete",
"font_class": "ai-delete",
"unicode": "e9cd",
"unicode_decimal": 59853
},
{
"icon_id": "41858484",
"name": "caise-knowledge",
"font_class": "caise-knowledge",
"unicode": "e9ce",
"unicode_decimal": 59854
},
{
"icon_id": "41833445",
"name": "ai-article",
"font_class": "ai-article",
"unicode": "e9cc",
"unicode_decimal": 59852
},
{
"icon_id": "41811974",
"name": "ai-model_setup (1)",
"font_class": "ai-model_setup1",
"unicode": "e9cb",
"unicode_decimal": 59851
},
{
"icon_id": "41811980",
"name": "ai-report",
"font_class": "ai-report",
"unicode": "e9ca",
"unicode_decimal": 59850
},
{
"icon_id": "41811915",
"name": "ai-customer_service",
"font_class": "ai-customer_service",
"unicode": "e9c9",
"unicode_decimal": 59849
},
{
"icon_id": "41735717",
"name": "oneterm-connect",
"font_class": "oneterm-connect1",
"unicode": "e9c6",
"unicode_decimal": 59846
},
{
"icon_id": "41735716",
"name": "oneterm-session",
"font_class": "oneterm-session1",
"unicode": "e9c7",
"unicode_decimal": 59847
},
{
"icon_id": "41735703",
"name": "oneterm-assets",
"font_class": "oneterm-assets",
"unicode": "e9c8",
"unicode_decimal": 59848
},
{
"icon_id": "41725683",
"name": "oneterm-RDP",
"font_class": "a-oneterm-ssh1",
"unicode": "e9c3",
"unicode_decimal": 59843
},
{
"icon_id": "41725684",
"name": "oneterm-SSH",
"font_class": "a-oneterm-ssh2",
"unicode": "e9c4",
"unicode_decimal": 59844
},
{
"icon_id": "41725685",
"name": "oneterm-VNC",
"font_class": "oneterm-rdp",
"unicode": "e9c5",
"unicode_decimal": 59845
},
{
"icon_id": "41724497",
"name": "caise-websphere",
"font_class": "caise-websphere",
"unicode": "e9c2",
"unicode_decimal": 59842
},
{
"icon_id": "41724575",
"name": "caise-vps",
"font_class": "caise-vps",
"unicode": "e9c1",
"unicode_decimal": 59841
},
{
"icon_id": "41724631",
"name": "caise-F5",
"font_class": "caise-F5",
"unicode": "e9c0",
"unicode_decimal": 59840
},
{
"icon_id": "41724653",
"name": "caise-HAProxy",
"font_class": "caise-HAProxy",
"unicode": "e9bf",
"unicode_decimal": 59839
},
{
"icon_id": "41722953",
"name": "caise-JBoss",
"font_class": "caise-JBoss",
"unicode": "e9be",
"unicode_decimal": 59838
},
{
"icon_id": "41722960",
"name": "caise-dongfangtong",
"font_class": "caise-dongfangtong",
"unicode": "e9bd",
"unicode_decimal": 59837
},
{
"icon_id": "41722681",
"name": "caise-kafka",
"font_class": "caise-kafka",
"unicode": "e9b7",
"unicode_decimal": 59831
},
{
"icon_id": "41722680",
"name": "caise-weblogic",
"font_class": "caise-weblogic",
"unicode": "e9b8",
"unicode_decimal": 59832
},
{
"icon_id": "41722679",
"name": "caise-TDSQL",
"font_class": "caise-TDSQL",
"unicode": "e9b9",
"unicode_decimal": 59833
},
{
"icon_id": "41722678",
"name": "caise-kingbase",
"font_class": "caise-kingbase",
"unicode": "e9ba",
"unicode_decimal": 59834
},
{
"icon_id": "41722677",
"name": "达梦",
"font_class": "caise-dameng",
"unicode": "e9bb",
"unicode_decimal": 59835
},
{
"icon_id": "41722675",
"name": "caise-TIDB",
"font_class": "caise-TIDB",
"unicode": "e9bc",
"unicode_decimal": 59836
},
{
"icon_id": "41681675",
"name": "veops-expand",
"font_class": "veops-expand",
"unicode": "e9b6",
"unicode_decimal": 59830
},
{
"icon_id": "41672951",
"name": "公有云",
"font_class": "caise-public_cloud",
"unicode": "e9b1",
"unicode_decimal": 59825
},
{
"icon_id": "41672952",
"name": "操作系统",
"font_class": "caise-system",
"unicode": "e9b2",
"unicode_decimal": 59826
},
{
"icon_id": "41673309",
"name": "IPAM",
"font_class": "caise-IPAM",
"unicode": "e9b3",
"unicode_decimal": 59827
},
{
"icon_id": "41673312",
"name": "hyperV",
"font_class": "caise-hyperV",
"unicode": "e9b4",
"unicode_decimal": 59828
},
{
"icon_id": "41673320",
"name": "数据中心",
"font_class": "caise-data_center2",
"unicode": "e9b5",
"unicode_decimal": 59829
},
{
"icon_id": "41669141",
"name": "硬件设备",
"font_class": "caise-hardware",
"unicode": "e9ad",
"unicode_decimal": 59821
},
{
"icon_id": "41669249",
"name": "计算机",
"font_class": "caise-computer",
"unicode": "e9ae",
"unicode_decimal": 59822
},
{
"icon_id": "41669250",
"name": "网络设备",
"font_class": "caise-network_devices",
"unicode": "e9af",
"unicode_decimal": 59823
},
{
"icon_id": "41669278",
"name": "存储设备",
"font_class": "caise-storage_device",
"unicode": "e9b0",
"unicode_decimal": 59824
},
{
"icon_id": "41659452",
"name": "负载均衡",
"font_class": "caise-load_balancing",
"unicode": "e9ab",
"unicode_decimal": 59819
},
{
"icon_id": "41659446",
"name": "消息队列",
"font_class": "caise-message_queue",
"unicode": "e9ac",
"unicode_decimal": 59820
},
{
"icon_id": "41659424",
"name": "websever",
"font_class": "caise-websever",
"unicode": "e9aa",
"unicode_decimal": 59818
},
{
"icon_id": "41655608",
"name": "中间件",
"font_class": "caise-middleware",
"unicode": "e9a9",
"unicode_decimal": 59817
},
{
"icon_id": "41655599",
"name": "数据库",
"font_class": "caise-database",
"unicode": "e9a7",
"unicode_decimal": 59815
},
{
"icon_id": "41655591",
"name": "业务",
"font_class": "caise-business",
"unicode": "e9a8",
"unicode_decimal": 59816
},
{
"icon_id": "41655550",
"name": "虚拟化",
"font_class": "caise-virtualization",
"unicode": "e9a6",
"unicode_decimal": 59814
},
{
"icon_id": "41654680",
"name": "存储池",
"font_class": "caise-storage_pool",
"unicode": "e9a4",
"unicode_decimal": 59812
},
{
"icon_id": "41654676",
"name": "存储卷",
"font_class": "caise-storage_volume1",
"unicode": "e9a5",
"unicode_decimal": 59813
},
{
"icon_id": "41654608",
"name": "aix",
"font_class": "ciase-aix",
"unicode": "e9a3",
"unicode_decimal": 59811
},
{
"icon_id": "41654233",
"name": "ip池",
"font_class": "caise_pool",
"unicode": "e99b",
"unicode_decimal": 59803
},
{
"icon_id": "41654237",
"name": "ip地址",
"font_class": "caise-ip_address",
"unicode": "e99c",
"unicode_decimal": 59804
},
{
"icon_id": "41654249",
"name": "机房",
"font_class": "caise-computer_room",
"unicode": "e99d",
"unicode_decimal": 59805
},
{
"icon_id": "41654271",
"name": "机柜",
"font_class": "caise-rack",
"unicode": "e99e",
"unicode_decimal": 59806
},
{
"icon_id": "41654276",
"name": "PC",
"font_class": "caise-pc",
"unicode": "e99f",
"unicode_decimal": 59807
},
{
"icon_id": "41654305",
"name": "带宽线路",
"font_class": "caise-bandwidth_line",
"unicode": "e9a0",
"unicode_decimal": 59808
},
{
"icon_id": "41654323",
"name": "光纤交换机",
"font_class": "caise-fiber",
"unicode": "e9a1",
"unicode_decimal": 59809
},
{
"icon_id": "41654369",
"name": "磁盘阵列",
"font_class": "caise-disk_array",
"unicode": "e9a2",
"unicode_decimal": 59810
},
{
"icon_id": "41643869",
"name": "veops-group",
"font_class": "veops-group",
"unicode": "e99a",
"unicode_decimal": 59802
},
{
"icon_id": "41637123",
"name": "veops-inheritance",
"font_class": "veops-inheritance",
"unicode": "e999",
"unicode_decimal": 59801
},
{
"icon_id": "41570722",
"name": "veops-department",
"font_class": "veops-department",
"unicode": "e998",
"unicode_decimal": 59800
},
{
"icon_id": "41437322",
"name": "duose-changwenben (1)",
"font_class": "duose-changwenben1",
"unicode": "e997",
"unicode_decimal": 59799
},
{
"icon_id": "41363381",
"name": "duose-quote",
"font_class": "duose-quote",
"unicode": "e995",
"unicode_decimal": 59797
},
{
"icon_id": "41363378",
"name": "duose-boole",
"font_class": "duose-boole",
"unicode": "e996",
"unicode_decimal": 59798
},
{
"icon_id": "41341306",
"name": "veops-rule1",
"font_class": "veops-rule1",
"unicode": "e994",
"unicode_decimal": 59796
},
{
"icon_id": "41337509",
"name": "veops-operation_report",
"font_class": "veops-operation_report",
"unicode": "e993",
"unicode_decimal": 59795
},
{
"icon_id": "41335526",
"name": "veops-ranking1",
"font_class": "veops-ranking1",
"unicode": "e992",
"unicode_decimal": 59794
},
{
"icon_id": "41335530",
"name": "veops-ranking2",
"font_class": "veops-ranking2",
"unicode": "e98f",
"unicode_decimal": 59791
},
{
"icon_id": "41335529",
"name": "veops-ranking3",
"font_class": "veops-ranking3",
"unicode": "e990",
"unicode_decimal": 59792
},
{
"icon_id": "41335528",
"name": "veops-ranking4",
"font_class": "veops-ranking4",
"unicode": "e991",
"unicode_decimal": 59793
},
{
"icon_id": "41334746",
"name": "veops-title5",
"font_class": "veops-title5",
"unicode": "e98d",
"unicode_decimal": 59789
},
{
"icon_id": "41334744",
"name": "veops-repair (1)",
"font_class": "veops-repair1",
"unicode": "e98e",
"unicode_decimal": 59790
},
{
"icon_id": "41334753",
"name": "veops-ticket",
"font_class": "veops-ticket",
"unicode": "e988",
"unicode_decimal": 59784
},
{
"icon_id": "41334751",
"name": "veops-model4",
"font_class": "veops-model4",
"unicode": "e989",
"unicode_decimal": 59785
},
{
"icon_id": "41334752",
"name": "veops-resource2 (1)",
"font_class": "veops-resource21",
"unicode": "e98a",
"unicode_decimal": 59786
},
{
"icon_id": "41334750",
"name": "veops-relationship3",
"font_class": "veops-relationship3",
"unicode": "e98b",
"unicode_decimal": 59787
},
{
"icon_id": "41334748",
"name": "veops-title6",
"font_class": "veops-title6",
"unicode": "e98c",
"unicode_decimal": 59788
},
{
"icon_id": "41334404",
"name": "veops-resource1 (1)",
"font_class": "veops-resource11",
"unicode": "e97a",
"unicode_decimal": 59770
},
{
"icon_id": "41334505",
"name": "veops-model1 (1)",
"font_class": "veops-model11",
"unicode": "e97b",
"unicode_decimal": 59771
},
{
"icon_id": "41334533",
"name": "veops-relationship1",
"font_class": "veops-relationship1",
"unicode": "e97c",
"unicode_decimal": 59772
},
{
"icon_id": "41334535",
"name": "veops-title1",
"font_class": "veops-title1",
"unicode": "e97d",
"unicode_decimal": 59773
},
{
"icon_id": "41334572",
"name": "veops-title2",
"font_class": "veops-title2",
"unicode": "e97e",
"unicode_decimal": 59774
},
{
"icon_id": "41334579",
"name": "veops-model2",
"font_class": "veops-model2",
"unicode": "e97f",
"unicode_decimal": 59775
},
{
"icon_id": "41334590",
"name": "veops-resource2",
"font_class": "veops-resource2",
"unicode": "e980",
"unicode_decimal": 59776
},
{
"icon_id": "41334594",
"name": "veops-warehousing",
"font_class": "veops-warehousing",
"unicode": "e981",
"unicode_decimal": 59777
},
{
"icon_id": "41334598",
"name": "veops-relationship2",
"font_class": "veops-relationship2",
"unicode": "e982",
"unicode_decimal": 59778
},
{
"icon_id": "41334602",
"name": "veops-title3",
"font_class": "veops-title3",
"unicode": "e983",
"unicode_decimal": 59779
},
{
"icon_id": "41334603",
"name": "veops-rule2",
"font_class": "veops-rule2",
"unicode": "e984",
"unicode_decimal": 59780
},
{
"icon_id": "41334604",
"name": "veops-model3",
"font_class": "veops-model3",
"unicode": "e985",
"unicode_decimal": 59781
},
{
"icon_id": "41334725",
"name": "veops-title4",
"font_class": "veops-title4",
"unicode": "e986",
"unicode_decimal": 59782
},
{
"icon_id": "41334739",
"name": "veops-rule3",
"font_class": "veops-rule3",
"unicode": "e987",
"unicode_decimal": 59783
},
{
"icon_id": "41334004",
"name": "veops-decline",
"font_class": "veops-decline",
"unicode": "e978",
"unicode_decimal": 59768
},
{
"icon_id": "41333990",
"name": "veops-rise",
"font_class": "veops-rise",
"unicode": "e979",
"unicode_decimal": 59769
},
{
"icon_id": "41143117",
"name": "caise-数据中心",
"font_class": "caise-data_center",
"unicode": "e96f",
"unicode_decimal": 59759
},
{
"icon_id": "41143118",
"name": "caise-文件夹",
"font_class": "caise-folder",
"unicode": "e970",
"unicode_decimal": 59760
},
{
"icon_id": "41143119",
"name": "caise-资源池",
"font_class": "caise-resource_pool",
"unicode": "e971",
"unicode_decimal": 59761
},
{
"icon_id": "41143120",
"name": "caise-网络",
"font_class": "caise-network",
"unicode": "e972",
"unicode_decimal": 59762
},
{
"icon_id": "41143121",
"name": "caise-分布式交换机",
"font_class": "caise-distributed_switch",
"unicode": "e973",
"unicode_decimal": 59763
},
{
"icon_id": "41143122",
"name": "caise-标准式交换机",
"font_class": "caise-standard_switch",
"unicode": "e974",
"unicode_decimal": 59764
},
{
"icon_id": "41143128",
"name": "caise-主机集群",
"font_class": "caise-host_cluster",
"unicode": "e975",
"unicode_decimal": 59765
},
{
"icon_id": "41143129",
"name": "caise-数据存储集群",
"font_class": "caise-storage_cluster",
"unicode": "e976",
"unicode_decimal": 59766
},
{
"icon_id": "41143143",
"name": "caise-数据存储",
"font_class": "caise-data_storage",
"unicode": "e977",
"unicode_decimal": 59767
},
{
"icon_id": "41141857",
"name": "veops-account",
"font_class": "veops-account",
"unicode": "e96e",
"unicode_decimal": 59758
},
{
"icon_id": "41128804",
"name": "veops-collect",
"font_class": "veops-collect",
"unicode": "e96d",
"unicode_decimal": 59757
},
{
"icon_id": "41128781",
"name": "veops-collected",
"font_class": "veops-collected",
"unicode": "e96c",
"unicode_decimal": 59756
},
{
"icon_id": "41106846",
"name": "veops-text",
"font_class": "veops-text",
"unicode": "e96b",
"unicode_decimal": 59755
},
{ {
"icon_id": "40896913", "icon_id": "40896913",
"name": "veops-markdown", "name": "veops-markdown",

Binary file not shown.

View File

@@ -61,12 +61,12 @@ export default {
) )
// 注册富文本自定义元素 // 注册富文本自定义元素
const resume = { // const resume = {
type: 'attachment', // type: 'attachment',
attachmentLabel: '', // attachmentLabel: '',
attachmentValue: '', // attachmentValue: '',
children: [{ text: '' }], // void 元素必须有一个 children 其中只有一个空字符串重要 // children: [{ text: '' }], // void 元素必须有一个 children 其中只有一个空字符串重要
} // }
function withAttachment(editor) { function withAttachment(editor) {
// JS 语法 // JS 语法

18
cmdb-ui/src/api/cmdb.js Normal file
View File

@@ -0,0 +1,18 @@
import { axios } from '@/utils/request'
export function searchCI(params, isShowMessage = true) {
return axios({
url: `/v0.1/ci/s`,
method: 'GET',
params: params,
isShowMessage
})
}
export function getCIType(CITypeName, parameter) {
return axios({
url: `/v0.1/ci_types/${CITypeName}`,
method: 'GET',
params: parameter
})
}

View File

@@ -1,346 +1,407 @@
<template> <template>
<div> <div>
<a-space :style="{ display: 'flex', marginBottom: '10px' }" v-for="(item, index) in ruleList" :key="item.id"> <a-space :style="{ display: 'flex', marginBottom: '10px' }" v-for="(item, index) in ruleList" :key="item.id">
<div :style="{ width: '70px', height: '24px', position: 'relative' }"> <div :style="{ width: '70px', height: '24px', position: 'relative' }">
<treeselect <treeselect
v-if="index" v-if="index"
class="custom-treeselect" class="custom-treeselect"
:style="{ width: '70px', '--custom-height': '24px', position: 'absolute', top: '-17px', left: 0 }" :style="{ width: '70px', '--custom-height': '24px', position: 'absolute', top: '-17px', left: 0 }"
v-model="item.type" v-model="item.type"
:multiple="false" :multiple="false"
:clearable="false" :clearable="false"
searchable searchable
:options="ruleTypeList" :options="ruleTypeList"
:normalizer=" :normalizer="
(node) => { (node) => {
return { return {
id: node.value, id: node.value,
label: node.label, label: node.label,
children: node.children, children: node.children,
} }
} }
" "
:disabled="disabled" :disabled="disabled"
> >
</treeselect> </treeselect>
</div> </div>
<treeselect <treeselect
class="custom-treeselect" class="custom-treeselect"
:style="{ width: '130px', '--custom-height': '24px' }" :style="{ width: '130px', '--custom-height': '24px' }"
v-model="item.property" v-model="item.property"
:multiple="false" :multiple="false"
:clearable="false" :clearable="false"
searchable searchable
:options="canSearchPreferenceAttrList" :options="canSearchPreferenceAttrList"
:normalizer=" :normalizer="
(node) => { (node) => {
return { return {
id: node.name, id: node.name,
label: node.alias || node.name, label: node.alias || node.name,
children: node.children, children: node.children,
} }
} }
" "
appendToBody appendToBody
:zIndex="1050" :zIndex="1050"
:disabled="disabled" :disabled="disabled"
> >
<div <div
:title="node.label" :title="node.label"
slot="option-label" slot="option-label"
slot-scope="{ node }" slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }" :style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
> >
<ValueTypeMapIcon :attr="node.raw" /> <ValueTypeMapIcon :attr="node.raw" />
{{ node.label }} {{ node.label }}
</div> </div>
<div <div
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }" :style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
slot="value-label" slot="value-label"
slot-scope="{ node }" slot-scope="{ node }"
> >
<ValueTypeMapIcon :attr="node.raw" /> {{ node.label }} <ValueTypeMapIcon :attr="node.raw" /> {{ node.label }}
</div> </div>
</treeselect> </treeselect>
<treeselect <treeselect
class="custom-treeselect" class="custom-treeselect"
:style="{ width: '100px', '--custom-height': '24px' }" :style="{ width: '100px', '--custom-height': '24px' }"
v-model="item.exp" v-model="item.exp"
:multiple="false" :multiple="false"
:clearable="false" :clearable="false"
searchable searchable
:options="[...getExpListByProperty(item.property), ...advancedExpList]" :options="[...getExpListByProperty(item.property), ...advancedExpList]"
:normalizer=" :normalizer="
(node) => { (node) => {
return { return {
id: node.value, id: node.value,
label: node.label, label: node.label,
children: node.children, children: node.children,
} }
} }
" "
@select="(value) => handleChangeExp(value, item, index)" @select="(value) => handleChangeExp(value, item, index)"
appendToBody appendToBody
:zIndex="1050" :zIndex="1050"
:disabled="disabled" :disabled="disabled"
> >
</treeselect> <div
<treeselect slot="option-label"
class="custom-treeselect" slot-scope="{ node }"
:style="{ width: '175px', '--custom-height': '24px' }" :style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
v-model="item.value" >
:multiple="false" <a-tooltip :title="node.label">
:clearable="false" {{ node.label }}
searchable </a-tooltip>
v-if="isChoiceByProperty(item.property) && (item.exp === 'is' || item.exp === '~is')" </div>
:options="getChoiceValueByProperty(item.property)" <div
:placeholder="$t('placeholder2')" :style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
:normalizer=" slot="value-label"
(node) => { slot-scope="{ node }"
return { >
id: node[0], <a-tooltip :title="node.label">
label: node[0], {{ node.label }}
children: node.children, </a-tooltip>
} </div>
} </treeselect>
" <CIReferenceAttr
appendToBody v-if="getAttr(item.property).is_reference && (item.exp === 'is' || item.exp === '~is')"
:zIndex="1050" :style="{ width: '175px' }"
:disabled="disabled" class="select-filter-component"
> :referenceTypeId="getAttr(item.property).reference_type_id"
<div :disabled="disabled"
:title="node.label" v-model="item.value"
slot="option-label" />
slot-scope="{ node }" <a-select
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }" v-else-if="getAttr(item.property).is_bool && (item.exp === 'is' || item.exp === '~is')"
> v-model="item.value"
{{ node.label }} class="select-filter-component"
</div> :style="{ width: '175px' }"
</treeselect> :disabled="disabled"
<a-input-group :placeholder="$t('placeholder2')"
size="small" >
compact <a-select-option key="1">
v-else-if="item.exp === 'range' || item.exp === '~range'" true
:style="{ width: '175px' }" </a-select-option>
> <a-select-option key="0">
<a-input false
class="ops-input" </a-select-option>
size="small" </a-select>
v-model="item.min" <treeselect
:style="{ width: '78px' }" class="custom-treeselect"
:placeholder="$t('min')" :style="{ width: '175px', '--custom-height': '24px' }"
:disabled="disabled" v-model="item.value"
/> :multiple="false"
~ :clearable="false"
<a-input searchable
class="ops-input" v-else-if="isChoiceByProperty(item.property) && (item.exp === 'is' || item.exp === '~is')"
size="small" :options="getChoiceValueByProperty(item.property)"
v-model="item.max" :placeholder="$t('placeholder2')"
:style="{ width: '78px' }" :normalizer="
:placeholder="$t('max')" (node) => {
:disabled="disabled" return {
/> id: String(node[0] || ''),
</a-input-group> label: node[1] ? node[1].label || node[0] : node[0],
<a-input-group size="small" compact v-else-if="item.exp === 'compare'" :style="{ width: '175px' }"> children: node.children && node.children.length ? node.children : undefined,
<treeselect }
class="custom-treeselect" }
:style="{ width: '60px', '--custom-height': '24px' }" "
v-model="item.compareType" appendToBody
:multiple="false" :zIndex="1050"
:clearable="false" :disabled="disabled"
searchable >
:options="compareTypeList" <div
:normalizer=" :title="node.label"
(node) => { slot="option-label"
return { slot-scope="{ node }"
id: node.value, :style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
label: node.label, >
children: node.children, {{ node.label }}
} </div>
} </treeselect>
" <a-input-group
appendToBody size="small"
:zIndex="1050" compact
:disabled="disabled" v-else-if="item.exp === 'range' || item.exp === '~range'"
> :style="{ width: '175px' }"
</treeselect> >
<a-input class="ops-input" v-model="item.value" size="small" style="width: 113px" /> <a-input
</a-input-group> class="ops-input"
<a-input size="small"
v-else-if="item.exp !== 'value' && item.exp !== '~value'" v-model="item.min"
size="small" :style="{ width: '78px' }"
v-model="item.value" :placeholder="$t('min')"
:placeholder="item.exp === 'in' || item.exp === '~in' ? $t('cmdbFilterComp.split', { separator: ';' }) : ''" :disabled="disabled"
class="ops-input" />
:style="{ width: '175px' }" ~
:disabled="disabled" <a-input
></a-input> class="ops-input"
<div v-else :style="{ width: '175px' }"></div> size="small"
<template v-if="!disabled"> v-model="item.max"
<a-tooltip :title="$t('copy')"> :style="{ width: '78px' }"
<a class="operation" @click="handleCopyRule(item)"><ops-icon type="veops-copy"/></a> :placeholder="$t('max')"
</a-tooltip> :disabled="disabled"
<a-tooltip :title="$t('delete')"> />
<a class="operation" @click="handleDeleteRule(item)"><ops-icon type="icon-xianxing-delete"/></a> </a-input-group>
</a-tooltip> <a-input-group size="small" compact v-else-if="item.exp === 'compare'" :style="{ width: '175px' }">
<a-tooltip :title="$t('cmdbFilterComp.addHere')" v-if="needAddHere"> <treeselect
<a class="operation" @click="handleAddRuleAt(item)"><a-icon type="plus-circle"/></a> class="custom-treeselect"
</a-tooltip> :style="{ width: '60px', '--custom-height': '24px' }"
</template> v-model="item.compareType"
</a-space> :multiple="false"
<div class="table-filter-add" v-if="!disabled"> :clearable="false"
<a @click="handleAddRule">+ {{ $t('new') }}</a> searchable
</div> :options="compareTypeList"
</div> :normalizer="
</template> (node) => {
return {
<script> id: node.value,
import _ from 'lodash' label: node.label,
import { v4 as uuidv4 } from 'uuid' children: node.children,
import { ruleTypeList, expList, advancedExpList, compareTypeList } from './constants' }
import ValueTypeMapIcon from '../CMDBValueTypeMapIcon' }
"
export default { appendToBody
name: 'Expression', :zIndex="1050"
components: { ValueTypeMapIcon }, :disabled="disabled"
model: { >
prop: 'value', </treeselect>
event: 'change', <a-input class="ops-input" v-model="item.value" size="small" style="width: 113px" />
}, </a-input-group>
props: { <a-input
value: { v-else-if="item.exp !== 'value' && item.exp !== '~value'"
type: Array, size="small"
default: () => [], v-model="item.value"
}, :placeholder="item.exp === 'in' || item.exp === '~in' ? $t('cmdbFilterComp.split', { separator: ';' }) : ''"
canSearchPreferenceAttrList: { class="ops-input"
type: Array, :style="{ width: '175px' }"
required: true, :disabled="disabled"
default: () => [], ></a-input>
}, <div v-else :style="{ width: '175px' }"></div>
needAddHere: { <template v-if="!disabled">
type: Boolean, <a-tooltip :title="$t('copy')">
default: false, <a class="operation" @click="handleCopyRule(item)"><ops-icon type="veops-copy"/></a>
}, </a-tooltip>
disabled: { <a-tooltip :title="$t('delete')">
type: Boolean, <a class="operation" @click="handleDeleteRule(item)"><ops-icon type="icon-xianxing-delete"/></a>
default: false, </a-tooltip>
}, <a-tooltip :title="$t('cmdbFilterComp.addHere')" v-if="needAddHere">
}, <a class="operation" @click="handleAddRuleAt(item)"><a-icon type="plus-circle"/></a>
data() { </a-tooltip>
return { </template>
compareTypeList, </a-space>
} <div class="table-filter-add" v-if="!disabled">
}, <a @click="handleAddRule">+ {{ $t('new') }}</a>
computed: { </div>
ruleList: { </div>
get() { </template>
return this.value
}, <script>
set(val) { import _ from 'lodash'
this.$emit('change', val) import { v4 as uuidv4 } from 'uuid'
return val import { ruleTypeList, expList, advancedExpList, compareTypeList } from './constants'
}, import ValueTypeMapIcon from '../CMDBValueTypeMapIcon'
}, import CIReferenceAttr from '../ciReferenceAttr/index.vue'
ruleTypeList() {
return ruleTypeList() export default {
}, name: 'Expression',
expList() { components: { ValueTypeMapIcon, CIReferenceAttr },
return expList() model: {
}, prop: 'value',
advancedExpList() { event: 'change',
return advancedExpList() },
}, props: {
}, value: {
methods: { type: Array,
getExpListByProperty(property) { default: () => [],
if (property) { },
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property) canSearchPreferenceAttrList: {
if (_find && ['0', '1', '3', '4', '5'].includes(_find.value_type)) { type: Array,
return [ required: true,
{ value: 'is', label: this.$t('cmdbFilterComp.is') }, default: () => [],
{ value: '~is', label: this.$t('cmdbFilterComp.~is') }, },
{ value: '~value', label: this.$t('cmdbFilterComp.~value') }, // 为空的定义有点绕 needAddHere: {
{ value: 'value', label: this.$t('cmdbFilterComp.value') }, type: Boolean,
] default: false,
} },
return this.expList disabled: {
} type: Boolean,
return this.expList default: false,
}, },
isChoiceByProperty(property) { },
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property) data() {
if (_find) { return {
return _find.is_choice compareTypeList,
} }
return false },
}, computed: {
handleAddRule() { ruleList: {
this.ruleList.push({ get() {
id: uuidv4(), return this.value
type: 'and', },
property: this.canSearchPreferenceAttrList[0]?.name, set(val) {
exp: 'is', this.$emit('change', val)
value: null, return val
}) },
this.$emit('change', this.ruleList) },
}, ruleTypeList() {
handleCopyRule(item) { return ruleTypeList()
this.ruleList.push({ ...item, id: uuidv4() }) },
this.$emit('change', this.ruleList) expList() {
}, return expList()
handleDeleteRule(item) { },
const idx = this.ruleList.findIndex((r) => r.id === item.id) advancedExpList() {
if (idx > -1) { return advancedExpList()
this.ruleList.splice(idx, 1) },
} },
this.$emit('change', this.ruleList) methods: {
}, getExpListByProperty(property) {
handleAddRuleAt(item) { if (property) {
const idx = this.ruleList.findIndex((r) => r.id === item.id) const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (idx > -1) { if (_find && (['0', '1', '3', '4', '5'].includes(_find.value_type) || _find.is_reference || _find.is_bool)) {
this.ruleList.splice(idx, 0, { return [
id: uuidv4(), { value: 'is', label: this.$t('cmdbFilterComp.is') },
type: 'and', { value: '~is', label: this.$t('cmdbFilterComp.~is') },
property: this.canSearchPreferenceAttrList[0]?.name, { value: '~value', label: this.$t('cmdbFilterComp.~value') }, // 为空的定义有点绕
exp: 'is', { value: 'value', label: this.$t('cmdbFilterComp.value') },
value: null, ]
}) }
} return this.expList
this.$emit('change', this.ruleList) }
}, return this.expList
getChoiceValueByProperty(property) { },
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property) isChoiceByProperty(property) {
if (_find) { const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
return _find.choice_value if (_find) {
} return _find.is_choice
return [] }
}, return false
handleChangeExp({ value }, item, index) { },
const _ruleList = _.cloneDeep(this.ruleList) handleAddRule() {
if (value === 'range') { this.ruleList.push({
_ruleList[index] = { id: uuidv4(),
..._ruleList[index], type: 'and',
min: '', property: this.canSearchPreferenceAttrList[0]?.name,
max: '', exp: 'is',
exp: value, value: null,
} })
} else if (value === 'compare') { this.$emit('change', this.ruleList)
_ruleList[index] = { },
..._ruleList[index], handleCopyRule(item) {
compareType: '1', this.ruleList.push({ ...item, id: uuidv4() })
exp: value, this.$emit('change', this.ruleList)
} },
} else { handleDeleteRule(item) {
_ruleList[index] = { const idx = this.ruleList.findIndex((r) => r.id === item.id)
..._ruleList[index], if (idx > -1) {
exp: value, this.ruleList.splice(idx, 1)
} }
} this.$emit('change', this.ruleList)
this.ruleList = _ruleList },
this.$emit('change', this.ruleList) handleAddRuleAt(item) {
}, const idx = this.ruleList.findIndex((r) => r.id === item.id)
}, if (idx > -1) {
} this.ruleList.splice(idx, 0, {
</script> id: uuidv4(),
type: 'and',
<style></style> property: this.canSearchPreferenceAttrList[0]?.name,
exp: 'is',
value: null,
})
}
this.$emit('change', this.ruleList)
},
getChoiceValueByProperty(property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find) {
return _find.choice_value
}
return []
},
getAttr(property) {
return this.canSearchPreferenceAttrList.find((item) => item.name === property) || {}
},
handleChangeExp({ value }, item, index) {
const _ruleList = _.cloneDeep(this.ruleList)
if (value === 'range') {
_ruleList[index] = {
..._ruleList[index],
min: '',
max: '',
exp: value,
}
} else if (value === 'compare') {
_ruleList[index] = {
..._ruleList[index],
compareType: '1',
exp: value,
}
} else {
_ruleList[index] = {
..._ruleList[index],
exp: value,
}
}
this.ruleList = _ruleList
this.$emit('change', this.ruleList)
},
},
}
</script>
<style lang="less" scoped>
.select-filter-component {
height: 24px;
/deep/ .ant-select-selection {
height: 24px;
background: #f7f8fa;
line-height: 24px;
border: none;
.ant-select-selection__rendered {
height: 24px;
line-height: 24px;
}
}
}
</style>

View File

@@ -1,49 +1,77 @@
<template> <template>
<span> <span>
<ops-icon :type="getPropertyIcon(attr)" /> <ops-icon :type="getPropertyIcon(attr)" />
</span> </span>
</template> </template>
<script> <script>
export default { export default {
name: 'ValueTypeIcon', name: 'ValueTypeIcon',
props: { props: {
attr: { attr: {
type: Object, type: Object,
default: () => {}, default: () => {},
}, },
}, },
methods: { methods: {
getPropertyIcon(attr) { getPropertyIcon(attr) {
switch (attr.value_type) { let valueType = attr.value_type
case '0':
return 'duose-shishu' if (valueType === '2') {
case '1': if (attr.is_password) {
return 'duose-fudianshu' valueType = '7'
case '2': } else if (attr.is_link) {
if (attr.is_password) { valueType = '8'
return 'duose-password' } else if (!attr.is_index) {
} valueType = '9'
if (attr.is_link) { }
return 'duose-link' }
}
return 'duose-wenben' if (
case '3': valueType === '7' &&
return 'duose-datetime' attr.is_bool
case '4': ) {
return 'duose-date' valueType = '10'
case '5': }
return 'duose-time'
case '6': if (
return 'duose-json' valueType === '0' &&
case '7': attr.is_reference
return 'duose-password' ) {
case '8': valueType = '11'
return 'duose-link' }
}
}, switch (valueType) {
}, case '0':
} return 'duose-shishu'
</script> case '1':
return 'duose-fudianshu'
<style></style> case '2':
return 'duose-wenben'
case '3':
return 'duose-datetime'
case '4':
return 'duose-date'
case '5':
return 'duose-time'
case '6':
return 'duose-json'
case '7':
return 'duose-password'
case '8':
return 'duose-link'
case '9':
return 'duose-changwenben1'
case '10':
return 'duose-boole'
case '11':
return 'duose-quote'
default:
return ''
}
},
},
}
</script>
<style></style>

View File

@@ -177,7 +177,7 @@ export const linearIconList = [
}] }]
}, { }, {
value: 'icon-xianxing-application', value: 'icon-xianxing-application',
label: '应用', label: '常用组件',
list: [{ list: [{
value: 'icon-xianxing-yilianjie', value: 'icon-xianxing-yilianjie',
label: '已连接' label: '已连接'
@@ -517,7 +517,7 @@ export const fillIconList = [
}] }]
}, { }, {
value: 'icon-shidi-application', value: 'icon-shidi-application',
label: '应用', label: '常用组件',
list: [{ list: [{
value: 'icon-shidi-yilianjie', value: 'icon-shidi-yilianjie',
label: '已连接' label: '已连接'
@@ -729,6 +729,18 @@ export const multicolorIconList = [
value: 'database', value: 'database',
label: '数据库', label: '数据库',
list: [{ list: [{
value: 'caise-TIDB',
label: 'TIDB'
}, {
value: 'caise-dameng',
label: '达梦'
}, {
value: 'caise-kingbase',
label: 'KingBase'
}, {
value: 'caise-TDSQL',
label: 'TDSQL'
}, {
value: 'caise-DB2', value: 'caise-DB2',
label: 'DB2' label: 'DB2'
}, { }, {
@@ -809,6 +821,9 @@ export const multicolorIconList = [
value: 'system', value: 'system',
label: '操作系统', label: '操作系统',
list: [{ list: [{
value: 'ciase-aix',
label: 'aix'
}, {
value: 'caise-Windows', value: 'caise-Windows',
label: 'Windows' label: 'Windows'
}, { }, {
@@ -903,8 +918,140 @@ export const multicolorIconList = [
}] }]
}, { }, {
value: 'caise-application', value: 'caise-application',
label: '应用', label: '常用组件',
list: [{ list: [{
value: 'caise-websphere',
label: 'WebSphere'
}, {
value: 'caise-vps',
label: 'VPS'
}, {
value: 'caise-F5',
label: 'F5'
}, {
value: 'caise-HAProxy',
label: 'HAProxy'
}, {
value: 'caise-kafka',
label: 'kafka'
}, {
value: 'caise-dongfangtong',
label: '东方通'
}, {
value: 'cmdb-vcenter',
label: 'VCenter'
}, {
value: 'ops-KVM',
label: 'KVM'
}, {
value: 'caise-JBoss',
label: 'JBoss'
}, {
value: 'caise-weblogic',
label: 'WebLogic'
}, {
value: 'caise-disk_array',
label: '磁盘阵列'
}, {
value: 'caise-fiber',
label: '光纤交换机'
}, {
value: 'caise-bandwidth_line',
label: '带宽线路'
}, {
value: 'caise-pc',
label: 'PC'
}, {
value: 'caise-rack',
label: '机柜'
}, {
value: 'caise-computer_room',
label: '机房'
}, {
value: 'caise-ip_address',
label: 'ip地址'
}, {
value: 'caise_pool',
label: 'ip池'
}, {
value: 'caise-storage_volume1',
label: '存储卷'
}, {
value: 'caise-virtualization',
label: '虚拟化'
}, {
value: 'caise-business',
label: '业务'
}, {
value: 'caise-database',
label: '数据库'
}, {
value: 'caise-middleware',
label: '中间件'
}, {
value: 'caise-websever',
label: 'websever'
}, {
value: 'caise-message_queue',
label: '消息队列'
}, {
value: 'caise-load_balancing',
label: '负载均衡'
}, {
value: 'caise-storage_device',
label: '存储设备'
}, {
value: 'caise-network_devices',
label: '网络设备'
}, {
value: 'caise-computer',
label: '计算机'
}, {
value: 'caise-hardware',
label: '硬件设备'
}, {
value: 'caise-data_center2',
label: '数据中心'
}, {
value: 'caise-hyperV',
label: 'hyperV'
}, {
value: 'caise-IPAM',
label: 'IPAM'
}, {
value: 'caise-system',
label: '操作系统'
}, {
value: 'caise-public_cloud',
label: '公有云'
}, {
value: 'caise-data_center',
label: '数据中心'
}, {
value: 'caise-folder',
label: '文件夹'
}, {
value: 'caise-resource_pool',
label: '资源池'
}, {
value: 'caise-network',
label: '网络'
}, {
value: 'caise-distributed_switch',
label: '分布式交换机'
}, {
value: 'caise-standard_switch',
label: '标准式交换机'
}, {
value: 'caise-host_cluster',
label: '主机集群'
}, {
value: 'caise-storage_cluster',
label: '数据存储集群'
}, {
value: 'caise-data_storage',
label: '数据存储'
}, {
value: 'caise-yilianjie', value: 'caise-yilianjie',
label: '已连接' label: '已连接'
}, { }, {

View File

@@ -0,0 +1,24 @@
.cmdb-side-menu-search {
background-color: #FFFFFF !important;
cursor: auto !important;
:global {
.ant-input-affix-wrapper {
max-width: 170px !important;
width: 170px;
border-radius: 30px;
}
.ant-input {
box-shadow: none;
border: none;
background-color: #F7F8FA;
height: 30px;
line-height: 30px;
}
.ant-input-suffix {
right: 0px !important;
}
}
}

View File

@@ -9,6 +9,8 @@ import {
import { searchResourceType } from '@/modules/acl/api/resource' import { searchResourceType } from '@/modules/acl/api/resource'
import { roleHasPermissionToGrant } from '@/modules/acl/api/permission' import { roleHasPermissionToGrant } from '@/modules/acl/api/permission'
import CMDBGrant from '@/modules/cmdb/components/cmdbGrant' import CMDBGrant from '@/modules/cmdb/components/cmdbGrant'
import styles from './index.module.less'
import { mapActions } from 'vuex'
const { Item, SubMenu } = Menu const { Item, SubMenu } = Menu
@@ -40,7 +42,8 @@ export default {
openKeys: [], openKeys: [],
selectedKeys: [], selectedKeys: [],
cachedOpenKeys: [], cachedOpenKeys: [],
resource_type: {} resource_type: {},
currentAppRoute: ''
} }
}, },
computed: { computed: {
@@ -64,6 +67,7 @@ export default {
searchResourceType({ page_size: 9999, app_id: 'cmdb' }).then(res => { searchResourceType({ page_size: 9999, app_id: 'cmdb' }).then(res => {
this.resource_type = { groups: res.groups, id2perms: res.id2perms } this.resource_type = { groups: res.groups, id2perms: res.id2perms }
}) })
this.currentAppRoute = this.$route?.matched?.[0]?.name || ''
this.updateMenu() this.updateMenu()
}, },
watch: { watch: {
@@ -75,12 +79,14 @@ export default {
this.openKeys = this.cachedOpenKeys this.openKeys = this.cachedOpenKeys
} }
}, },
$route: function () { $route: function (route) {
this.currentAppRoute = route?.matched?.[0]?.name
this.updateMenu() this.updateMenu()
}, },
}, },
inject: ['reload'], inject: ['reload'],
methods: { methods: {
...mapActions(['UpdateCMDBSEarchValue']),
cancelAttributes(e, menu) { cancelAttributes(e, menu) {
const that = this const that = this
e.preventDefault() e.preventDefault()
@@ -257,7 +263,7 @@ export default {
const props = {} const props = {}
if (this.$route.name === routeName && selectedIcon) { if (this.$route.name === routeName && selectedIcon) {
return <ops-icon type={selectedIcon}></ops-icon> return <ops-icon type={selectedIcon}></ops-icon>
} else if (icon.startsWith('ops-') || icon.startsWith('icon-xianxing') || icon.startsWith('icon-shidi')) { } else if (icon.startsWith('ops-') || icon.startsWith('icon-xianxing') || icon.startsWith('icon-shidi') || icon.startsWith('veops-')) {
return <ops-icon type={icon}></ops-icon> return <ops-icon type={icon}></ops-icon>
} else { } else {
typeof (icon) === 'object' ? props.component = icon : props.type = icon typeof (icon) === 'object' ? props.component = icon : props.type = icon
@@ -286,6 +292,47 @@ export default {
this.$message.error(this.$t('noPermission')) this.$message.error(this.$t('noPermission'))
} }
}) })
},
jumpCMDBSearch(value) {
this.UpdateCMDBSEarchValue(value)
if (this.$route.name !== 'cmdb_resource_search') {
this.$router.push({
name: 'cmdb_resource_search',
})
}
},
renderCMDBSearch() {
if (this.currentAppRoute !== 'cmdb' || this.collapsed) {
return null
}
return (
<Item class={styles['cmdb-side-menu-search']}>
<a-input
ref="cmdbSideMenuSearchInputRef"
class={styles['cmdb-side-menu-search-input']}
style={{
border: this.$route.name === 'cmdb_resource_search' ? 'solid 1px #B1C9FF' : ''
}}
placeholder={this.$t('cmdbSearch')}
onPressEnter={(e) => {
this.jumpCMDBSearch(e.target.value)
}}
>
<ops-icon
slot="suffix"
type="veops-search1"
onClick={() => {
const value = this.$refs?.cmdbSideMenuSearchInputRef?.$refs?.input?.value || ''
this.jumpCMDBSearch(value)
}}
/>
</a-input>
</Item>
)
} }
}, },
@@ -313,6 +360,7 @@ export default {
// {...{ props, on: on }} // {...{ props, on: on }}
return ( return (
<Menu class="ops-side-bar" selectedKeys={this.selectedKeys} {...{ props, on: on }}> <Menu class="ops-side-bar" selectedKeys={this.selectedKeys} {...{ props, on: on }}>
{this.renderCMDBSearch()}
{menuTree} {menuTree}
</Menu> </Menu>
) )

View File

@@ -61,7 +61,13 @@
</template> </template>
</div> </div>
</div> </div>
<a-input ref="regInput" :placeholder="$t('regexSelect.placeholder')" :value="current.label" @change="changeLabel"> <a-input
ref="regInput"
:placeholder="$t('regexSelect.placeholder')"
:value="current.label"
:disabled="disabled"
@change="changeLabel"
>
</a-input> </a-input>
</a-popover> </a-popover>
</template> </template>
@@ -88,6 +94,10 @@ export default {
type: Array, type: Array,
default: () => [], default: () => [],
}, },
disabled: {
type: Boolean,
default: false,
}
}, },
data() { data() {
return { return {

View File

@@ -1,183 +1,187 @@
<template> <template>
<div ref="splitPane" class="split-pane" :class="direction + ' ' + appName" :style="{ flexDirection: direction }"> <div ref="splitPane" class="split-pane" :class="direction + ' ' + appName" :style="{ flexDirection: direction }">
<div class="pane pane-one" ref="one" :style="lengthType + ':' + paneLengthValue1"> <div class="pane pane-one" ref="one" :style="lengthType + ':' + paneLengthValue1">
<slot name="one"></slot> <slot name="one"></slot>
</div> </div>
<div class="spliter-wrap"> <div class="spliter-wrap">
<a-button <a-button
v-show="collapsable" v-show="collapsable"
:icon="isExpanded ? 'left' : 'right'" :icon="isExpanded ? 'left' : 'right'"
class="collapse-btn" class="collapse-btn"
@click="handleExpand" @click="handleExpand"
></a-button> ></a-button>
<div <div
class="pane-trigger" class="pane-trigger"
@mousedown="handleMouseDown" @mousedown="handleMouseDown"
:style="{ backgroundColor: triggerColor, width: `${triggerLength}px` }" :style="{ backgroundColor: triggerColor, width: `${triggerLength}px` }"
></div> ></div>
</div> </div>
<div class="pane pane-two" ref="two" :style="lengthType + ':' + paneLengthValue2"> <div class="pane pane-two" ref="two" :style="lengthType + ':' + paneLengthValue2">
<slot name="two"></slot> <slot name="two"></slot>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
name: 'SplitPane', name: 'SplitPane',
props: { props: {
direction: { direction: {
type: String, type: String,
default: 'row', default: 'row',
}, },
min: { min: {
type: Number, type: Number,
default: 10, default: 10,
}, },
max: { max: {
type: Number, type: Number,
default: 90, default: 90,
}, },
paneLengthPixel: { paneLengthPixel: {
type: Number, type: Number,
default: 220, default: 220,
}, },
triggerLength: { triggerLength: {
type: Number, type: Number,
default: 8, default: 8,
}, },
appName: { appName: {
type: String, type: String,
default: 'viewer', default: 'viewer',
}, },
collapsable: { collapsable: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
triggerColor: { triggerColor: {
type: String, type: String,
default: '#f7f8fa', default: '#f7f8fa',
}, },
}, calcBasedParent: {
data() { type: Boolean,
return { defualt: false
triggerLeftOffset: 0, // 鼠标距滑动器左()侧偏移量 }
isExpanded: localStorage.getItem(`${this.appName}-isExpanded`) },
? JSON.parse(localStorage.getItem(`${this.appName}-isExpanded`)) data() {
: false, return {
parentContainer: null, triggerLeftOffset: 0, // 鼠标距滑动器左()侧偏移量
} isExpanded: localStorage.getItem(`${this.appName}-isExpanded`)
}, ? JSON.parse(localStorage.getItem(`${this.appName}-isExpanded`))
computed: { : false,
lengthType() { parentContainer: null,
return this.direction === 'row' ? 'width' : 'height' }
}, },
computed: {
minLengthType() { lengthType() {
return this.direction === 'row' ? 'minWidth' : 'minHeight' return this.direction === 'row' ? 'width' : 'height'
}, },
paneLengthValue1() { minLengthType() {
return `calc(${this.paneLengthPercent}% - ${this.triggerLength / 2 + 'px'})` return this.direction === 'row' ? 'minWidth' : 'minHeight'
}, },
paneLengthValue2() { paneLengthValue1() {
const rest = 100 - this.paneLengthPercent return `calc(${this.paneLengthPercent}% - ${this.triggerLength / 2 + 'px'})`
return `calc(${rest}% - ${this.triggerLength / 2 + 'px'})` },
},
paneLengthValue2() {
paneLengthPercent() { const rest = 100 - this.paneLengthPercent
const clientRectWidth = this.parentContainer return `calc(${rest}% - ${this.triggerLength / 2 + 'px'})`
? this.parentContainer.clientWidth },
: document.documentElement.getBoundingClientRect().width
return (this.paneLengthPixel / clientRectWidth) * 100 paneLengthPercent() {
}, const clientRectWidth = this.parentContainer && this.calcBasedParent
}, ? this.parentContainer.clientWidth
: document.documentElement.getBoundingClientRect().width
watch: { return (this.paneLengthPixel / clientRectWidth) * 100
isExpanded(newValue) { },
if (newValue) { },
document.querySelector(`.${this.appName} .pane-two`).style.display = 'none'
} else { watch: {
document.querySelector(`.${this.appName} .pane-two`).style.display = '' isExpanded(newValue) {
} if (newValue) {
}, document.querySelector(`.${this.appName} .pane-two`).style.display = 'none'
}, } else {
document.querySelector(`.${this.appName} .pane-two`).style.display = ''
mounted() { }
const paneLengthPixel = localStorage.getItem(`${this.appName}-paneLengthPixel`) },
if (paneLengthPixel) { },
this.$emit('update:paneLengthPixel', Number(paneLengthPixel))
} mounted() {
this.parentContainer = document.querySelector(`.${this.appName}`) const paneLengthPixel = localStorage.getItem(`${this.appName}-paneLengthPixel`)
if (this.isExpanded) { if (paneLengthPixel) {
document.querySelector(`.${this.appName} .pane-two`).style.display = 'none' this.$emit('update:paneLengthPixel', Number(paneLengthPixel))
} else { }
document.querySelector(`.${this.appName} .pane-two`).style.display = '' this.parentContainer = document.querySelector(`.${this.appName}`)
} if (this.isExpanded) {
}, document.querySelector(`.${this.appName} .pane-two`).style.display = 'none'
} else {
methods: { document.querySelector(`.${this.appName} .pane-two`).style.display = ''
// 按下滑动器 }
handleMouseDown(e) { },
document.addEventListener('mousemove', this.handleMouseMove)
document.addEventListener('mouseup', this.handleMouseUp) methods: {
if (this.direction === 'row') { // 按下滑动器
this.triggerLeftOffset = e.pageX - e.srcElement.getBoundingClientRect().left handleMouseDown(e) {
} else { document.addEventListener('mousemove', this.handleMouseMove)
this.triggerLeftOffset = e.pageY - e.srcElement.getBoundingClientRect().top document.addEventListener('mouseup', this.handleMouseUp)
} if (this.direction === 'row') {
}, this.triggerLeftOffset = e.pageX - e.srcElement.getBoundingClientRect().left
} else {
// 按下滑动器后移动鼠标 this.triggerLeftOffset = e.pageY - e.srcElement.getBoundingClientRect().top
handleMouseMove(e) { }
this.isExpanded = false },
this.$emit('expand', this.isExpanded)
const clientRect = this.$refs.splitPane.getBoundingClientRect() // 按下滑动器后移动鼠标
let paneLengthPixel = 0 handleMouseMove(e) {
this.isExpanded = false
if (this.direction === 'row') { this.$emit('expand', this.isExpanded)
const offset = e.pageX - clientRect.left - this.triggerLeftOffset + this.triggerLength / 2 const clientRect = this.$refs.splitPane.getBoundingClientRect()
paneLengthPixel = offset let paneLengthPixel = 0
} else {
const offset = e.pageY - clientRect.top - this.triggerLeftOffset + this.triggerLength / 2 if (this.direction === 'row') {
paneLengthPixel = offset const offset = e.pageX - clientRect.left - this.triggerLeftOffset + this.triggerLength / 2
} paneLengthPixel = offset
} else {
if (paneLengthPixel < this.min) { const offset = e.pageY - clientRect.top - this.triggerLeftOffset + this.triggerLength / 2
paneLengthPixel = this.min paneLengthPixel = offset
} }
if (paneLengthPixel > this.max) {
paneLengthPixel = this.max if (paneLengthPixel < this.min) {
} paneLengthPixel = this.min
}
this.$emit('update:paneLengthPixel', paneLengthPixel) if (paneLengthPixel > this.max) {
paneLengthPixel = this.max
localStorage.setItem(`${this.appName}-paneLengthPixel`, paneLengthPixel) }
},
this.$emit('update:paneLengthPixel', paneLengthPixel)
// 松开滑动器
handleMouseUp() { localStorage.setItem(`${this.appName}-paneLengthPixel`, paneLengthPixel)
document.removeEventListener('mousemove', this.handleMouseMove) },
},
// 松开滑动器
handleExpand() { handleMouseUp() {
this.isExpanded = !this.isExpanded document.removeEventListener('mousemove', this.handleMouseMove)
this.$emit('expand', this.isExpanded) },
localStorage.setItem(`${this.appName}-isExpanded`, this.isExpanded)
}, handleExpand() {
}, this.isExpanded = !this.isExpanded
} this.$emit('expand', this.isExpanded)
</script> localStorage.setItem(`${this.appName}-isExpanded`, this.isExpanded)
},
<style scoped lang="less"> },
@import './index.less'; }
</style> </script>
<style scoped lang="less">
@import './index.less';
</style>

View File

@@ -6,7 +6,8 @@
:paneLengthPixel.sync="paneLengthPixel" :paneLengthPixel.sync="paneLengthPixel"
:appName="appName" :appName="appName"
:triggerColor="triggerColor" :triggerColor="triggerColor"
:triggerLength="18" :triggerLength="triggerLength"
:calcBasedParent="calcBasedParent"
> >
<template #one> <template #one>
<div class="two-column-layout-sidebar"> <div class="two-column-layout-sidebar">
@@ -37,6 +38,14 @@ export default {
type: String, type: String,
default: '#f7f8fa', default: '#f7f8fa',
}, },
triggerLength: {
type: Number,
default: 18
},
calcBasedParent: {
type: Boolean,
defualt: false
}
}, },
data() { data() {
return { return {

View File

@@ -0,0 +1,178 @@
<template>
<div class="reference-attr-select-wrap">
<a-select
v-bind="$attrs"
v-model="selectCIIds"
optionFilterProp="title"
:mode="isList ? 'multiple' : 'default'"
showSearch
allowClear
:getPopupContainer="(trigger) => trigger.parentElement"
class="reference-attr-select"
:maxTagCount="2"
@dropdownVisibleChange="handleDropdownVisibleChange"
@search="handleSearch"
@change="handleChange"
>
<template v-if="!isInit">
<a-select-option
v-for="(item) in initSelectOption"
:key="item.key"
:title="item.title"
>
{{ item.title }}
</a-select-option>
</template>
<a-select-option
v-for="(item) in options"
:key="item.key"
:title="item.title"
>
{{ item.title }}
</a-select-option>
</a-select>
</div>
</template>
<script>
import _ from 'lodash'
import debounce from 'lodash/debounce'
import { searchCI, getCIType } from '@/api/cmdb'
export default {
name: 'CIReferenceAttr',
props: {
value: {
type: [Number, String, Array],
default: () => '',
},
isList: {
type: Boolean,
default: false,
},
referenceShowAttrName: {
type: String,
default: ''
},
referenceTypeId: {
type: [String, Number],
default: ''
},
initSelectOption: {
type: Array,
default: () => []
}
},
model: {
prop: 'value',
event: 'change',
},
data() {
return {
isInit: false,
options: [],
innerReferenceShowAttrName: ''
}
},
watch: {
referenceTypeId: {
immediate: true,
deep: true,
handler() {
this.isInit = false
}
}
},
computed: {
selectCIIds: {
get() {
if (this.isList) {
return this.value || []
} else {
return this.value ? Number(this.value) : ''
}
},
set(val) {
this.$emit('change', val ?? (this.isList ? [] : null))
return val
},
},
},
methods: {
async handleDropdownVisibleChange(open) {
if (!this.isInit && open && this.referenceTypeId) {
this.isInit = true
if (!this.referenceShowAttrName) {
const res = await getCIType(this.referenceTypeId)
const ciType = res?.ci_types?.[0]
this.innerReferenceShowAttrName = ciType?.show_name || ciType?.unique_name || ''
}
const attrName = this.referenceShowAttrName || this.innerReferenceShowAttrName || ''
if (!attrName) {
return
}
const res = await searchCI({
q: `_type:${this.referenceTypeId}`,
fl: attrName,
count: 25,
})
let options = res?.result?.map((item) => {
return {
key: item._id,
title: String(item?.[attrName] ?? '')
}
})
options = _.uniqBy([...this.initSelectOption, ...options], 'key')
this.options = options
}
},
handleSearch: debounce(async function(v) {
const attrName = this.referenceShowAttrName || this.innerReferenceShowAttrName || ''
if (!attrName || !this.referenceTypeId) {
return
}
const res = await searchCI({
q: `_type:${this.referenceTypeId}${v ? ',*' + v + '*' : ''}`,
fl: attrName,
count: v ? 100 : 25,
})
this.options = res?.result?.map((item) => {
return {
key: item._id,
title: String(item?.[attrName] ?? '')
}
})
}, 300),
handleChange(v) {
if (Array.isArray(v) ? !v.length : !v) {
this.handleSearch()
}
}
}
}
</script>
<style lang="less" scoped>
.reference-attr-select-wrap {
width: 100%;
.reference-attr-select {
width: 100%;
/deep/ .ant-select-dropdown {
z-index: 15;
}
}
}
</style>

View File

@@ -31,7 +31,7 @@ export default {
text-align: center; text-align: center;
border-radius: 4px; border-radius: 4px;
&:hover { &:hover {
background: linear-gradient(0deg, rgba(0, 80, 201, 0.2) 0%, rgba(174, 207, 255, 0.06) 86.76%); // background: linear-gradient(0deg, rgba(0, 80, 201, 0.2) 0%, rgba(174, 207, 255, 0.06) 86.76%);
color: @layout-header-font-selected-color; color: @layout-header-font-selected-color;
} }
} }

View File

@@ -5,7 +5,6 @@
v-for="route in defaultShowRoutes" v-for="route in defaultShowRoutes"
:key="route.name" :key="route.name"
@click="() => handleClick(route)" @click="() => handleClick(route)"
:title="$t(route.meta.title)"
> >
{{ route.meta.title }} {{ route.meta.title }}
</span> </span>
@@ -119,7 +118,9 @@ export default {
line-height: @layout-header-line-height; line-height: @layout-header-line-height;
display: inline-block; display: inline-block;
} }
> span:hover, > span:hover {
background-color: #f0f2f5;
}
.top-menu-selected { .top-menu-selected {
font-weight: bold; font-weight: bold;
color: @layout-header-font-selected-color; color: @layout-header-font-selected-color;

View File

@@ -5,15 +5,30 @@
<span <span
v-if="hasBackendPermission" v-if="hasBackendPermission"
@click="handleClick" @click="handleClick"
class="action" class="common-settings-btn"
style="width: 40px; display: flex; justify-content: center"
> >
<a-icon type="setting" /> <ops-icon class="common-settings-btn-icon" type="veops-setting" />
<span class="common-settings-btn-text">{{ $t('settings') }}</span>
</span> </span>
<span class="locale" @click="changeLang">{{ locale === 'zh' ? 'English' : '中文' }}</span>
<a-popover <a-popover
trigger="click" overlayClassName="lang-popover-wrap"
:overlayStyle="{ width: '150px' }" placement="bottomRight"
:getPopupContainer="(trigger) => trigger.parentNode"
>
<span class="locale">{{ languageList.find((lang) => lang.key === locale).title }}</span>
<div class="lang-menu" slot="content">
<a
v-for="(lang) in languageList"
:key="lang.key"
:class="['lang-menu-item', lang.key === locale ? 'lang-menu-item_active' : '']"
@click="changeLang(lang.key)"
>
{{ lang.title }}
</a>
</div>
</a-popover>
<a-popover
:overlayStyle="{ width: '130px' }"
placement="bottomRight" placement="bottomRight"
overlayClassName="custom-user" overlayClassName="custom-user"
> >
@@ -29,7 +44,7 @@
<span>{{ $t('topMenu.logout') }}</span> <span>{{ $t('topMenu.logout') }}</span>
</div> </div>
</template> </template>
<span class="action ant-dropdown-link user-dropdown-menu"> <span class="action ant-dropdown-link user-dropdown-menu user-info-wrap">
<a-avatar <a-avatar
v-if="avatar()" v-if="avatar()"
class="avatar" class="avatar"
@@ -54,10 +69,26 @@ export default {
components: { components: {
DocumentLink, DocumentLink,
}, },
data() {
return {
languageList: [
{
title: '简中',
key: 'zh'
},
{
title: 'EN',
key: 'en'
},
]
}
},
computed: { computed: {
...mapState(['user', 'locale']), ...mapState(['user', 'locale']),
hasBackendPermission() { hasBackendPermission() {
return this.user?.detailPermissions?.backend?.length const isAdmin = this?.user?.roles?.permissions?.includes('acl_admin')
return isAdmin || this.user?.detailPermissions?.backend?.length
}, },
}, },
methods: { methods: {
@@ -79,14 +110,9 @@ export default {
handleClick() { handleClick() {
this.$router.push('/setting') this.$router.push('/setting')
}, },
changeLang() { changeLang(lang) {
if (this.locale === 'zh') { this.SET_LOCALE(lang)
this.SET_LOCALE('en') this.$i18n.locale = lang
this.$i18n.locale = 'en'
} else {
this.SET_LOCALE('zh')
this.$i18n.locale = 'zh'
}
this.$nextTick(() => { this.$nextTick(() => {
setDocumentTitle(`${this.$t(this.$route.meta.title)} - ${domTitle}`) setDocumentTitle(`${this.$t(this.$route.meta.title)} - ${domTitle}`)
}) })
@@ -116,8 +142,88 @@ export default {
.locale { .locale {
cursor: pointer; cursor: pointer;
padding: 0 8px;
&:hover { &:hover {
color: @primary-color; color: @primary-color;
} }
} }
.lang-popover-wrap {
width: 70px;
padding: 0px;
.ant-popover-inner-content {
padding: 0px;
}
}
</style>
<style lang="less" scoped>
.user-wrapper {
.common-settings-btn {
cursor: pointer;
padding: 0px 18px;
background-color: #F0F5FF;
border-radius: 22px;
height: 26px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 8px;
&-icon {
font-size: 12px;
color: #2F54EB;
}
&-text {
margin-left: 4px;
font-size: 12px;
font-weight: 400;
color: #4E5969;
}
&:hover {
.commen-settings-btn-text {
color: #2F54EB;
}
}
}
.lang-menu {
width: 100%;
display: flex;
flex-direction: column;
&-item {
width: 100%;
padding: 5px 10px;
cursor: pointer;
color: #4E5969;
&_active {
color: #2F54EB;
background-color: #f0f5ff;
}
&:hover {
color: #2F54EB;
}
}
}
.user-info-wrap {
.avatar {
transition: all 0.2s;
border: solid 1px transparent;
}
&:hover {
.avatar {
border-color: #2F54EB;
}
}
}
}
</style> </style>

View File

@@ -69,6 +69,8 @@ Vue.prototype.$httpError = function (err, describe) {
window.$message = Vue.prototype.$message window.$message = Vue.prototype.$message
Vue.prototype.isOpenSource = process.env.VUE_APP_IS_OPEN_SOURCE === 'true'
Vue.use(Antd) Vue.use(Antd)
Vue.use(Viser) Vue.use(Viser)

View File

@@ -10,6 +10,7 @@ export default {
resourceType: 'Resource Types', resourceType: 'Resource Types',
trigger: 'Triggers', trigger: 'Triggers',
}, },
settings: 'Common Settings',
screen: 'Big Screen', screen: 'Big Screen',
dashboard: 'Dashboard', dashboard: 'Dashboard',
admin: 'Admin', admin: 'Admin',
@@ -107,6 +108,7 @@ export default {
visual: 'Visual', visual: 'Visual',
default: 'default', default: 'default',
tip: 'Tip', tip: 'Tip',
cmdbSearch: 'Search',
pagination: { pagination: {
total: '{range0}-{range1} of {total} items' total: '{range0}-{range1} of {total} items'
}, },

View File

@@ -10,6 +10,7 @@ export default {
resourceType: '资源类型', resourceType: '资源类型',
trigger: '触发器', trigger: '触发器',
}, },
settings: '通用设置',
screen: '大屏', screen: '大屏',
dashboard: '仪表盘', dashboard: '仪表盘',
admin: '管理员', admin: '管理员',
@@ -107,6 +108,7 @@ export default {
visual: '虚拟', visual: '虚拟',
default: '默认', default: '默认',
tip: '提示', tip: '提示',
cmdbSearch: '搜索一下',
pagination: { pagination: {
total: '当前展示 {range0}-{range1} 条数据, 共 {total} 条' total: '当前展示 {range0}-{range1} 条数据, 共 {total} 条'
}, },

View File

@@ -81,3 +81,11 @@ export function searchCIRelationFull(params) {
params, params,
}) })
} }
export function searchCIRelationPath(data) {
return axios({
url: `/v0.1/ci_relations/path/s`,
method: 'POST',
data,
})
}

View File

@@ -1,232 +1,257 @@
import { axios } from '@/utils/request' import { axios } from '@/utils/request'
/** /**
* 获取 所有的 ci_types * 获取 所有的 ci_types
* @param parameter * @param parameter
* @returns {AxiosPromise} * @returns {AxiosPromise}
*/ */
export function getCITypes(parameter) { export function getCITypes(parameter) {
return axios({ return axios({
url: '/v0.1/ci_types', url: '/v0.1/ci_types',
method: 'GET', method: 'GET',
params: parameter params: parameter
}) })
} }
/** /**
* 获取 某个 ci_types * 获取 某个 ci_types
* @param CITypeName * @param CITypeName
* @param parameter * @param parameter
* @returns {AxiosPromise} * @returns {AxiosPromise}
*/ */
export function getCIType(CITypeName, parameter) { export function getCIType(CITypeName, parameter) {
return axios({ return axios({
url: `/v0.1/ci_types/${CITypeName}`, url: `/v0.1/ci_types/${CITypeName}`,
method: 'GET', method: 'GET',
params: parameter params: parameter
}) })
} }
/** /**
* 创建 ci_type * 创建 ci_type
* @param data * @param data
* @returns {AxiosPromise} * @returns {AxiosPromise}
*/ */
export function createCIType(data) { export function createCIType(data) {
return axios({ return axios({
url: '/v0.1/ci_types', url: '/v0.1/ci_types',
method: 'POST', method: 'POST',
data: data data: data
}) })
} }
/** /**
* 更新 ci_type * 更新 ci_type
* @param CITypeId * @param CITypeId
* @param data * @param data
* @returns {AxiosPromise} * @returns {AxiosPromise}
*/ */
export function updateCIType(CITypeId, data) { export function updateCIType(CITypeId, data) {
return axios({ return axios({
url: `/v0.1/ci_types/${CITypeId}`, url: `/v0.1/ci_types/${CITypeId}`,
method: 'PUT', method: 'PUT',
data: data data: data
}) })
} }
/** /**
* 删除 ci_type * 删除 ci_type
* @param CITypeId * @param CITypeId
* @returns {AxiosPromise} * @returns {AxiosPromise}
*/ */
export function deleteCIType(CITypeId) { export function deleteCIType(CITypeId) {
return axios({ return axios({
url: `/v0.1/ci_types/${CITypeId}`, url: `/v0.1/ci_types/${CITypeId}`,
method: 'DELETE' method: 'DELETE'
}) })
} }
/** /**
* 获取 某个 ci_type 的分组 * 获取 某个 ci_type 的分组
* @param CITypeId * @param CITypeId
* @param data * @param data
* @returns {AxiosPromise} * @returns {AxiosPromise}
*/ */
export function getCITypeGroupById(CITypeId, data) { export function getCITypeGroupById(CITypeId, data) {
return axios({ return axios({
url: `/v0.1/ci_types/${CITypeId}/attribute_groups`, url: `/v0.1/ci_types/${CITypeId}/attribute_groups`,
method: 'GET', method: 'GET',
params: data params: data
}) })
} }
/** /**
* 保存 某个 ci_type 的分组 * 保存 某个 ci_type 的分组
* @param CITypeId * @param CITypeId
* @param data * @param data
* @returns {AxiosPromise} * @returns {AxiosPromise}
*/ */
export function createCITypeGroupById(CITypeId, data) { export function createCITypeGroupById(CITypeId, data) {
return axios({ return axios({
url: `/v0.1/ci_types/${CITypeId}/attribute_groups`, url: `/v0.1/ci_types/${CITypeId}/attribute_groups`,
method: 'POST', method: 'POST',
data: data data: data
}) })
} }
/** /**
* 修改 某个 ci_type 的分组 * 修改 某个 ci_type 的分组
* @param groupId * @param groupId
* @param data * @param data
* @returns {AxiosPromise} * @returns {AxiosPromise}
*/ */
export function updateCITypeGroupById(groupId, data) { export function updateCITypeGroupById(groupId, data) {
return axios({ return axios({
url: `/v0.1/ci_types/attribute_groups/${groupId}`, url: `/v0.1/ci_types/attribute_groups/${groupId}`,
method: 'PUT', method: 'PUT',
data: data data: data
}) })
} }
/** /**
* 删除 某个 ci_type 的分组 * 删除 某个 ci_type 的分组
* @param groupId * @param groupId
* @param data * @param data
* @returns {AxiosPromise} * @returns {AxiosPromise}
*/ */
export function deleteCITypeGroupById(groupId, data) { export function deleteCITypeGroupById(groupId, data) {
return axios({ return axios({
url: `/v0.1/ci_types/attribute_groups/${groupId}`, url: `/v0.1/ci_types/attribute_groups/${groupId}`,
method: 'delete', method: 'delete',
data: data data: data
}) })
} }
export function getUniqueConstraintList(type_id) { /**
return axios({ * 获取级联属性配置
url: `/v0.1/ci_types/${type_id}/unique_constraint`, * @param {*} typeId
method: 'get', * @returns
}) */
} export function getCITypeCascadeAttributes(typeId) {
return axios({
export function addUniqueConstraint(type_id, data) { url: `/v0.1/cascade_attributes/ci_types/${typeId}`,
return axios({ method: 'get'
url: `/v0.1/ci_types/${type_id}/unique_constraint`, })
method: 'post', }
data: data
}) /**
} * 获取级联属性数据
* @param {*} typeId
export function updateUniqueConstraint(type_id, id, data) { * @returns
return axios({ */
url: `/v0.1/ci_types/${type_id}/unique_constraint/${id}`, export function postCITypeCascadeAttributesValues(attrId, data) {
method: 'put', return axios({
data: data url: `/v0.1/cascade_attributes/${attrId}/values`,
}) method: 'post',
} data
})
export function deleteUniqueConstraint(type_id, id) { }
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint/${id}`, export function getUniqueConstraintList(type_id) {
method: 'delete', return axios({
}) url: `/v0.1/ci_types/${type_id}/unique_constraint`,
} method: 'get',
})
export function getTriggerList(type_id) { }
return axios({
url: `/v0.1/ci_types/${type_id}/triggers`, export function addUniqueConstraint(type_id, data) {
method: 'get', return axios({
}) url: `/v0.1/ci_types/${type_id}/unique_constraint`,
} method: 'post',
data: data
export function addTrigger(type_id, data) { })
return axios({ }
url: `/v0.1/ci_types/${type_id}/triggers`,
method: 'post', export function updateUniqueConstraint(type_id, id, data) {
data: data return axios({
}) url: `/v0.1/ci_types/${type_id}/unique_constraint/${id}`,
} method: 'put',
data: data
export function updateTrigger(type_id, id, data) { })
return axios({ }
url: `/v0.1/ci_types/${type_id}/triggers/${id}`,
method: 'put', export function deleteUniqueConstraint(type_id, id) {
data: data return axios({
}) url: `/v0.1/ci_types/${type_id}/unique_constraint/${id}`,
} method: 'delete',
})
export function deleteTrigger(type_id, id) { }
return axios({
url: `/v0.1/ci_types/${type_id}/triggers/${id}`, export function getTriggerList(type_id) {
method: 'delete', return axios({
}) url: `/v0.1/ci_types/${type_id}/triggers`,
} method: 'get',
})
// CMDB的模型和实例的授权接口 }
export function grantCiType(type_id, rid, data) {
return axios({ export function addTrigger(type_id, data) {
url: `/v0.1/ci_types/${type_id}/roles/${rid}/grant`, return axios({
method: 'post', url: `/v0.1/ci_types/${type_id}/triggers`,
data method: 'post',
}) data: data
} })
// CMDB的模型和实例的删除授权接口 }
export function revokeCiType(type_id, rid, data) {
return axios({ export function updateTrigger(type_id, id, data) {
url: `/v0.1/ci_types/${type_id}/roles/${rid}/revoke`, return axios({
method: 'post', url: `/v0.1/ci_types/${type_id}/triggers/${id}`,
data method: 'put',
}) data: data
} })
// CMDB的模型和实例的过滤的权限 }
export function ciTypeFilterPermissions(type_id) {
return axios({ export function deleteTrigger(type_id, id) {
url: `/v0.1/ci_types/${type_id}/filters/permissions`, return axios({
method: 'get', url: `/v0.1/ci_types/${type_id}/triggers/${id}`,
}) method: 'delete',
} })
}
// parent_ids, child_id
export function postCiTypeInheritance(data) { // CMDB的模型和实例的授权接口
return axios({ export function grantCiType(type_id, rid, data) {
url: `/v0.1/ci_types/inheritance`, return axios({
method: 'post', url: `/v0.1/ci_types/${type_id}/roles/${rid}/grant`,
data method: 'post',
}) data
} })
}
// parent_id, child_id // CMDB的模型和实例的删除授权接口
export function deleteCiTypeInheritance(data) { export function revokeCiType(type_id, rid, data) {
return axios({ return axios({
url: `/v0.1/ci_types/inheritance`, url: `/v0.1/ci_types/${type_id}/roles/${rid}/revoke`,
method: 'delete', method: 'post',
data data
}) })
} }
// CMDB的模型和实例的过滤的权限
export function getCITypeIcons() { export function ciTypeFilterPermissions(type_id) {
return axios({ return axios({
url: '/v0.1/ci_types/icons', url: `/v0.1/ci_types/${type_id}/filters/permissions`,
method: 'GET', method: 'get',
}) })
} }
// parent_ids, child_id
export function postCiTypeInheritance(data) {
return axios({
url: `/v0.1/ci_types/inheritance`,
method: 'post',
data
})
}
// parent_id, child_id
export function deleteCiTypeInheritance(data) {
return axios({
url: `/v0.1/ci_types/inheritance`,
method: 'delete',
data
})
}
export function getCITypeIcons() {
return axios({
url: '/v0.1/ci_types/icons',
method: 'GET',
})
}

View File

@@ -74,3 +74,11 @@ export function getCanEditByParentIdChildId(parent_id, child_id) {
method: 'GET' method: 'GET'
}) })
} }
export function getCITypeRelationPath(params) {
return axios({
url: `/v0.1/ci_type_relations/path`,
method: 'GET',
params
})
}

View File

@@ -52,6 +52,32 @@ export function getSnmpAttributes(type, name) {
}) })
} }
export function getHttpAttrMapping(name, resource) {
return axios({
url: `/v0.1/adr/http/${name}/mapping`,
method: 'GET',
params: {
resource
}
})
}
export function getHTTPAccounts(params) {
return axios({
url: `/v0.1/adr/accounts`,
method: 'GET',
params
})
}
export function postHTTPAccounts(data) {
return axios({
url: `/v0.1/adr/accounts`,
method: 'POST',
data
})
}
export function getCITypeDiscovery(type_id) { export function getCITypeDiscovery(type_id) {
return axios({ return axios({
url: `/v0.1/adt/ci_types/${type_id}`, url: `/v0.1/adt/ci_types/${type_id}`,

View File

@@ -1,89 +1,92 @@
import { axios } from '@/utils/request' import { axios } from '@/utils/request'
export function getCIHistory(ciId) { export function getCIHistory(ciId) {
return axios({ return axios({
url: `/v0.1/history/ci/${ciId}`, url: `/v0.1/history/ci/${ciId}`,
method: 'GET' method: 'GET'
}) })
} }
export function getCIHistoryTable(params) { export function getCIHistoryTable(params) {
return axios({ return axios({
url: `/v0.1/history/records/attribute`, url: `/v0.1/history/records/attribute`,
method: 'GET', method: 'GET',
params: params params: params,
}) timeout: 30 * 1000
} })
}
export function getRelationTable(params) {
return axios({ export function getRelationTable(params) {
url: `/v0.1/history/records/relation`, return axios({
method: 'GET', url: `/v0.1/history/records/relation`,
params: params method: 'GET',
}) params: params,
} timeout: 30 * 1000
})
export function getCITypesTable(params) { }
return axios({
url: `/v0.1/history/ci_types`, export function getCITypesTable(params) {
method: 'GET', return axios({
params: params url: `/v0.1/history/ci_types`,
}) method: 'GET',
} params: params,
timeout: 30 * 1000
export function getUsers(params) { })
return axios({ }
url: `/v1/acl/users/employee`,
method: 'GET', export function getUsers(params) {
params: params return axios({
}) url: `/v1/acl/users/employee`,
} method: 'GET',
params: params
export function getCiTriggers(params) { })
return axios({ }
url: `/v0.1/history/ci_triggers`,
method: 'GET', export function getCiTriggers(params) {
params: params return axios({
}) url: `/v0.1/history/ci_triggers`,
} method: 'GET',
params: params
export function getCiTriggersByCiId(ci_id, params) { })
return axios({ }
url: `/v0.1/history/ci_triggers/${ci_id}`,
method: 'GET', export function getCiTriggersByCiId(ci_id, params) {
params return axios({
}) url: `/v0.1/history/ci_triggers/${ci_id}`,
} method: 'GET',
params
export function getCiRelatedTickets(params) { })
return axios({ }
url: `/itsm/v1/process_ticket/get_tickets_by`,
method: 'POST', export function getCiRelatedTickets(params) {
data: params, return axios({
isShowMessage: false url: `/itsm/v1/process_ticket/get_tickets_by`,
}) method: 'POST',
} data: params,
isShowMessage: false
export function judgeItsmInstalled() { })
return axios({ }
url: `/itsm/v1/process_ticket/itsm_existed`,
method: 'GET', export function judgeItsmInstalled() {
isShowMessage: false return axios({
}) url: `/itsm/v1/process_ticket/itsm_existed`,
} method: 'GET',
isShowMessage: false
export function getCIsBaseline(params) { })
return axios({ }
url: `/v0.1/ci/baseline`,
method: 'GET', export function getCIsBaseline(params) {
params return axios({
}) url: `/v0.1/ci/baseline`,
} method: 'GET',
params
export function CIBaselineRollback(ciId, params) { })
return axios({ }
url: `/v0.1/ci/${ciId}/baseline/rollback`,
method: 'POST', export function CIBaselineRollback(ciId, params) {
data: params return axios({
}) url: `/v0.1/ci/${ciId}/baseline/rollback`,
} method: 'POST',
data: params
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 KiB

View File

@@ -21,15 +21,15 @@
> >
* *
</span> </span>
<vxe-select <a-select
filterable
clearable
v-model="row.attr" v-model="row.attr"
type="text"
:options="ciTypeAttributes"
transfer
:placeholder="$t('cmdb.ciType.attrMapTableAttrPlaceholder')" :placeholder="$t('cmdb.ciType.attrMapTableAttrPlaceholder')"
></vxe-select> showSearch
allowClear
:options="ciTypeAttributes"
style="width: 100%; height: 28px; line-height: 28px;"
class="attr-map-table-left-select"
></a-select>
</div> </div>
</template> </template>
</vxe-column> </vxe-column>
@@ -49,7 +49,7 @@
> >
<vxe-column field="name" :title="$t('name')"></vxe-column> <vxe-column field="name" :title="$t('name')"></vxe-column>
<vxe-column field="type" :title="$t('type')"></vxe-column> <vxe-column field="type" :title="$t('type')"></vxe-column>
<vxe-column v-if="ruleType !== 'agent'" field="example" :title="$t('cmdb.components.example')"> <vxe-column v-if="ruleType !== DISCOVERY_CATEGORY_TYPE.AGENT" field="example" :title="$t('cmdb.components.example')">
<template #default="{row}"> <template #default="{row}">
<span v-if="row.type === 'json'">{{ JSON.stringify(row.example) }}</span> <span v-if="row.type === 'json'">{{ JSON.stringify(row.example) }}</span>
<span v-else>{{ row.example }}</span> <span v-else>{{ row.example }}</span>
@@ -72,6 +72,8 @@
</template> </template>
<script> <script>
import { DISCOVERY_CATEGORY_TYPE } from '@/modules/cmdb/views/discovery/constants.js'
export default { export default {
name: 'AttrMapTable', name: 'AttrMapTable',
props: { props: {
@@ -93,7 +95,9 @@ export default {
} }
}, },
data() { data() {
return {} return {
DISCOVERY_CATEGORY_TYPE
}
}, },
methods: { methods: {
getTableData() { getTableData() {
@@ -123,6 +127,18 @@ export default {
&-left { &-left {
width: 30%; width: 30%;
&-select {
/deep/ .ant-select-selection {
height: 28px;
line-height: 28px;
.ant-select-selection__rendered {
height: 28px;
line-height: 28px;
}
}
}
} }
&-right { &-right {

View File

@@ -13,7 +13,10 @@
v-decorator="['filename', { rules: [{ required: true, message: $t('cmdb.components.filenameInputTips') }] }]" v-decorator="['filename', { rules: [{ required: true, message: $t('cmdb.components.filenameInputTips') }] }]"
/> />
</a-form-item> </a-form-item>
<a-form-item :label="$t('cmdb.components.saveType')"> <a-form-item
v-if="showFileTypeSelect"
:label="$t('cmdb.components.saveType')"
>
<a-select <a-select
:placeholder="$t('cmdb.components.saveTypeTips')" :placeholder="$t('cmdb.components.saveTypeTips')"
v-decorator="[ v-decorator="[
@@ -83,6 +86,10 @@ export default {
type: String, type: String,
default: 'default', default: 'default',
}, },
showFileTypeSelect: {
type: Boolean,
default: true
}
}, },
data() { data() {
return { return {

View File

@@ -0,0 +1,85 @@
<template>
<div
v-if="icon || title"
class="ci-icon"
:style="{
'--size': size + 'px'
}"
>
<template v-if="icon">
<img
v-if="icon.split('$$')[2]"
:src="`/api/common-setting/v1/file/${icon.split('$$')[3]}`"
/>
<ops-icon
v-else
:style="{
color: icon.split('$$')[1],
}"
:type="icon.split('$$')[0]"
/>
</template>
<span
class="ci-icon-letter"
v-else
>
<span>
{{ title[0].toUpperCase() }}
</span>
</span>
</div>
</template>
<script>
export default {
name: 'CIIcon',
props: {
icon: {
type: String,
default: ''
},
// 如果没有icon, 默认以title 的第一个字符
title: {
type: String,
default: ''
},
size: {
type: [String, Number],
default: '12'
}
}
}
</script>
<style lang="less" scoped>
.ci-icon {
font-size: var(--size);
width: var(--size);
height: var(--size);
display: flex;
align-items: center;
justify-content: center;
& > img {
width: var(--size);
height: var(--size);
}
&-letter {
background-color: #FFFFFF;
color: #2f54eb;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
border-radius: 2px;
box-shadow: 0px 1px 2px rgba(47, 84, 235, 0.2);
& > span {
transform-origin: center;
transform: scale(0.7);
}
}
}
</style>

View File

@@ -0,0 +1,578 @@
<template>
<div class="ci-table-wrap">
<ops-table
:id="id"
border
keep-source
show-overflow
resizable
ref="xTable"
size="small"
:data="data"
:loading="loading"
:row-config="{ useKey: true, keyField: '_id' }"
show-header-overflow
highlight-hover-row
:checkbox-config="{ reserve: true, highlight: true, range: true }"
:edit-config="{ trigger: 'dblclick', mode: 'row', showIcon: false }"
:sort-config="{ remote: true, trigger: 'cell' }"
:row-key="true"
:column-key="true"
:cell-style="getCellStyle"
:scroll-y="{ enabled: true, gt: 20 }"
:scroll-x="{ enabled: true, gt: 20 }"
class="ops-unstripe-table checkbox-hover-table"
:custom-config="{ storage: true }"
@checkbox-change="onSelectChange"
@checkbox-all="onSelectChange"
@checkbox-range-end="onSelectRangeEnd"
v-bind="$attrs"
v-on="$listeners"
>
<vxe-column
align="center"
type="checkbox"
width="60"
:fixed="isCheckboxFixed ? 'left' : ''"
v-if="showCheckbox"
>
<template #default="{row}">
{{ getRowSeq(row) }}
</template>
</vxe-column>
<vxe-table-column
v-for="(col, index) in columns"
:key="`${col.field}_${index}`"
:title="col.title"
:field="col.field"
:width="col.width"
:sortable="col.sortable"
:edit-render="getColumnsEditRender(col)"
:cell-type="col.value_type === '2' ? 'string' : 'auto'"
:fixed="col.is_fixed ? 'left' : ''"
>
<template #header>
<span class="vxe-handle">
<OpsMoveIcon class="header-move-icon" />
<span>{{ col.title }}</span>
</span>
</template>
<template v-if="col.is_choice || col.is_password || col.is_bool || col.is_reference" #edit="{ row }">
<CIReferenceAttr
v-if="col.is_reference"
:referenceTypeId="col.reference_type_id"
:isList="col.is_list"
:referenceShowAttrName="referenceShowAttrNameMap[col.reference_type_id] || ''"
:initSelectOption="getInitReferenceSelectOption(row[col.field], col)"
v-model="row[col.field]"
/>
<a-switch
v-else-if="col.is_bool"
v-model="row[col.field]"
/>
<vxe-input v-else-if="col.is_password" v-model="passwordValue[col.field]" />
<a-select
v-if="col.is_choice"
v-model="row[col.field]"
:getPopupContainer="(trigger) => trigger.parentElement"
:style="{ width: '100%', height: '32px' }"
:placeholder="$t('placeholder2')"
:showArrow="false"
:mode="col.is_list ? 'multiple' : 'default'"
class="ci-table-edit-select"
allowClear
showSearch
>
<a-select-option
v-for="(choice, idx) in col.filters"
:value="choice[0]"
:key="'edit_' + col.field + idx"
>
<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>
<a-tooltip placement="topLeft" :title="choice[1] ? choice[1].label || choice[0] : choice[0]">
<span>{{ choice[1] ? choice[1].label || choice[0] : choice[0] }}</span>
</a-tooltip>
</span>
</a-select-option>
</a-select>
</template>
<template
v-if="col.value_type === '6' || col.is_link || col.is_password || col.is_choice || col.is_reference"
#default="{ row }"
>
<template v-if="col.is_reference" >
<a
v-for="(ciId) in (col.is_list ? row[col.field] : [row[col.field]])"
:key="ciId"
:href="`/cmdb/cidetail/${col.reference_type_id}/${ciId}`"
target="_blank"
>
{{ getReferenceAttrValue(ciId, col) }}
</a>
</template>
<span v-else-if="col.value_type === '6' && row[col.field]">{{ row[col.field] }}</span>
<template v-else-if="col.is_link && row[col.field]">
<a
v-for="(item, linkIndex) in (col.is_list ? row[col.field] : [row[col.field]])"
:key="linkIndex"
:href="
item.startsWith('http') || item.startsWith('https')
? `${item}`
: `http://${item}`
"
target="_blank"
>
{{ getChoiceValueLabel(col, item) || item }}
</a>
</template>
<PasswordField
v-else-if="col.is_password && row[col.field]"
:ci_id="row._id"
:attr_id="col.attr_id"
></PasswordField>
<template v-else-if="col.is_choice">
<span
v-for="value in (col.is_list ? row[col.field] : [row[col.field]])"
:key="value"
:style="getChoiceValueStyle(col, value)"
class="column-default-choice"
>
<img
v-if="getChoiceValueIcon(col, value).id && getChoiceValueIcon(col, value).url"
:src="`/api/common-setting/v1/file/${getChoiceValueIcon(col, value).url}`"
:style="{ maxHeight: '13px', maxWidth: '13px', marginRight: '5px' }"
/>
<ops-icon
v-else-if="getChoiceValueIcon(col, value).name"
:style="{ color: getChoiceValueIcon(col, value).color, marginRight: '5px' }"
:type="getChoiceValueIcon(col, value).name"
/>
{{ getChoiceValueLabel(col, value) || value }}
</span>
</template>
</template>
</vxe-table-column>
<vxe-column align="left" field="operate" fixed="right" width="80">
<template #header>
<span>{{ $t('operation') }}</span>
</template>
<template #default="{ row }">
<a-space>
<a @click="openDetail(row.ci_id || row._id)">
<a-icon type="unordered-list" />
</a>
<a-tooltip :title="$t('cmdb.ci.viewRelation')">
<a @click="openDetail(row.ci_id || row._id, 'tab_2', '2')">
<a-icon type="retweet" />
</a>
</a-tooltip>
<a v-if="showDelete" @click="deleteCI(row)" :style="{ color: 'red' }">
<a-icon type="delete" />
</a>
</a-space>
</template>
</vxe-column>
<template #empty>
<div
v-if="loading"
class="ci-table-loading"
>
{{ loadingTip || $t('loading') }}
</div>
<div v-else>
<img :style="{ width: '200px' }" :src="require('@/assets/data_empty.png')" />
<div>{{ $t('noData') }}</div>
</div>
</template>
<template #loading>
<div class="ci-table-loading">{{ loadingTip || $t('loading') }}</div>
</template>
</ops-table>
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
</div>
</template>
<script>
import _ from 'lodash'
import { getCITypes } from '@/modules/cmdb/api/CIType'
import { searchCI } from '@/modules/cmdb/api/ci'
import JsonEditor from '../JsonEditor/jsonEditor.vue'
import PasswordField from '../passwordField/index.vue'
import { ops_move_icon as OpsMoveIcon } from '@/core/icons'
import CIReferenceAttr from '@/components/ciReferenceAttr/index.vue'
export default {
name: 'CITable',
components: {
JsonEditor,
PasswordField,
OpsMoveIcon,
CIReferenceAttr
},
props: {
// table ID
id: {
type: String,
default: ''
},
// table Loading
loading: {
type: Boolean,
default: false,
},
// ci 属性列表
attrList: {
type: Array,
default: () => []
},
// table column
columns: {
type: Array,
default: () => []
},
passwordValue: {
type: Object,
default: () => {}
},
// 加载提示
loadingTip: {
type: String,
default: ''
},
// 是否展示复选框
showCheckbox: {
type: Boolean,
default: true
},
// 是否展示删除按钮
showDelete: {
type: Boolean,
default: true
},
// 表格数据
data: {
type: Array,
default: () => []
}
},
data() {
return {
referenceShowAttrNameMap: {},
referenceCIIdMap: {},
}
},
computed: {
isCheckboxFixed() {
const idx = this.columns.findIndex((item) => item.is_fixed)
return idx > -1
},
tableDataWatch() {
return {
data: this.data,
columns: this.columns
}
},
referenceCIIdWatch() {
const referenceTypeCol = this.columns?.filter((col) => col?.is_reference && col?.reference_type_id) || []
if (!this.data?.length || !referenceTypeCol?.length) {
return []
}
const ids = []
this.data.forEach((row) => {
referenceTypeCol.forEach((col) => {
if (row[col.field]) {
ids.push(...(Array.isArray(row[col.field]) ? row[col.field] : [row[col.field]]))
}
})
})
return _.uniq(ids)
}
},
watch: {
columns: {
immediate: true,
deep: true,
handler(newVal) {
this.handleReferenceShowAttrName(newVal)
}
},
referenceCIIdWatch: {
immediate: true,
deep: true,
handler() {
this.handleReferenceCIIdMap()
}
}
},
methods: {
getVxetableRef() {
return this?.$refs?.['xTable']?.getVxetableRef?.() || null
},
onSelectChange() {
const xTable = this.getVxetableRef()
const records = [...xTable.getCheckboxRecords(), ...xTable.getCheckboxReserveRecords()]
this.$emit('onSelectChange', records)
},
onSelectRangeEnd({ records }) {
this.$emit('onSelectChange', records)
},
getCellStyle({ row, rowIndex, $rowIndex, column, columnIndex, $columnIndex }) {
const { property } = column
const _find = this.attrList.find((attr) => attr.name === property)
if (
_find &&
_find.option &&
_find.option.fontOptions &&
row[`${property}`] !== undefined &&
row[`${property}`] !== null
) {
return { ..._find.option.fontOptions }
}
},
getColumnsEditRender(col) {
const _editRender = {
...col.editRender,
}
if (col.value_type === '6') {
_editRender.events = { focus: this.handleFocusJson }
}
return _editRender
},
handleFocusJson({ column, row }) {
this.$refs.jsonEditor.open(column, row)
},
jsonEditorOk(row, column, jsonData) {
this.data.forEach((item) => {
if (item._id === row._id) {
item[column.property] = JSON.stringify(jsonData)
}
})
this.getVxetableRef().refreshColumn()
},
getChoiceValueStyle(col, colValue) {
const _find = col.filters.find((item) => String(item[0]) === String(colValue))
if (_find) {
return _find[1]?.style || {}
}
return {}
},
getChoiceValueIcon(col, colValue) {
const _find = col.filters.find((item) => String(item[0]) === String(colValue))
if (_find) {
return _find[1]?.icon || {}
}
return {}
},
getChoiceValueLabel(col, colValue) {
const _find = col?.filters?.find((item) => String(item[0]) === String(colValue))
if (_find) {
return _find[1]?.label || ''
}
return ''
},
/**
* 开启当前 ci 详情弹窗
*/
openDetail(id, activeTabKey, ciDetailRelationKey) {
this.$emit('openDetail', id, activeTabKey, ciDetailRelationKey)
},
deleteCI(row) {
this.$emit('deleteCI', row)
},
getRowSeq(row) {
return this.getVxetableRef().getRowSeq(row)
},
async handleReferenceShowAttrName(columns) {
const needRequiredCITypeIds = columns?.filter((col) => col?.is_reference && col?.reference_type_id).map((col) => col.reference_type_id) || []
if (!needRequiredCITypeIds.length) {
this.referenceShowAttrNameMap = {}
return
}
const res = await getCITypes({
type_ids: needRequiredCITypeIds.join(',')
})
const map = {}
res.ci_types.forEach((ciType) => {
map[ciType.id] = ciType?.show_name || ciType?.unique_name || ''
})
this.referenceShowAttrNameMap = map
},
async handleReferenceCIIdMap() {
const referenceTypeCol = this.columns.filter((col) => col?.is_reference && col?.reference_type_id) || []
if (!this.data?.length || !referenceTypeCol?.length) {
this.referenceCIIdMap = {}
return
}
const map = {}
this.data.forEach((row) => {
referenceTypeCol.forEach((col) => {
const ids = Array.isArray(row[col.field]) ? row[col.field] : row[col.field] ? [row[col.field]] : []
if (ids.length) {
if (!map?.[col.reference_type_id]) {
map[col.reference_type_id] = {}
}
ids.forEach((id) => {
map[col.reference_type_id][id] = {}
})
}
})
})
if (!Object.keys(map).length) {
this.referenceCIIdMap = {}
return
}
const allRes = await Promise.all(
Object.keys(map).map((key) => {
return searchCI({
q: `_type:${key},_id:(${Object.keys(map[key]).join(';')})`,
count: 9999
})
})
)
allRes.forEach((res) => {
res.result.forEach((item) => {
if (map?.[item._type]?.[item._id]) {
map[item._type][item._id] = item
}
})
})
this.referenceCIIdMap = map
},
getReferenceAttrValue(id, col) {
const ci = this?.referenceCIIdMap?.[col?.reference_type_id]?.[id]
if (!ci) {
return id
}
const attrName = this.referenceShowAttrNameMap?.[col.reference_type_id]
return ci?.[attrName] || id
},
getInitReferenceSelectOption(value, col) {
const ids = Array.isArray(value) ? value : value ? [value] : []
if (!ids.length) {
return []
}
const map = this?.referenceCIIdMap?.[col?.reference_type_id]
const attrName = this.referenceShowAttrNameMap?.[col?.reference_type_id]
const option = (Array.isArray(value) ? value : [value]).map((id) => {
return {
key: id,
title: map?.[id]?.[attrName] || id
}
})
return option
}
}
}
</script>
<style lang="less" scoped>
.ci-table-wrap {
.ci-table-loading {
width: 100%;
line-height: 200px;
}
.header-move-icon {
width: 17px;
height: 17px;
display: none;
position: absolute;
left: -3px;
top: 12px;
}
.column-default-choice {
border-radius: 4px;
padding: 1px 5px;
margin: 2px;
vertical-align: bottom;
display: inline-flex;
align-items: center;
}
}
.checkbox-hover-table {
/deep/ .vxe-table--body-wrapper {
.vxe-checkbox--label {
display: inline;
padding-left: 0px !important;
color: #bfbfbf;
}
.vxe-icon-checkbox-unchecked {
display: none;
}
.vxe-icon-checkbox-checked ~ .vxe-checkbox--label {
display: none;
}
.vxe-cell--checkbox {
&:hover {
.vxe-icon-checkbox-unchecked {
display: inline;
}
.vxe-checkbox--label {
display: none;
}
}
}
}
}
</style>

View File

@@ -1,212 +1,219 @@
<template> <template>
<a-modal :width="680" :title="title" :visible="visible" @ok="handleOk" @cancel="handleCancel"> <a-modal :width="680" :title="title" :visible="visible" @ok="handleOk" @cancel="handleCancel">
<CustomRadio <CustomRadio
:radioList="[ :radioList="[
{ value: 1, label: $t('cmdb.components.all') }, { value: 1, label: $t('cmdb.components.all') },
{ value: 2, label: $t('cmdb.components.customize'), layout: 'vertical' }, { value: 2, label: $t('cmdb.components.customize'), layout: 'vertical' },
{ value: 3, label: $t('cmdb.components.none') }, { value: 3, label: $t('cmdb.components.none') },
]" ]"
:value="radioValue" :value="radioValue"
@change="changeRadioValue" @change="changeRadioValue"
> >
<template slot="extra_2" v-if="radioValue === 2"> <template slot="extra_2" v-if="radioValue === 2">
<treeselect <treeselect
v-if="colType === 'read_attr'" v-if="colType === 'read_attr'"
v-model="selectedAttr" v-model="selectedAttr"
:multiple="true" :multiple="true"
:clearable="true" :clearable="true"
searchable searchable
:options="attrGroup" :options="attrGroup"
:placeholder="$t('cmdb.ciType.selectAttributes')" :placeholder="$t('cmdb.ciType.selectAttributes')"
value-consists-of="LEAF_PRIORITY" value-consists-of="LEAF_PRIORITY"
:limit="10" :limit="10"
:limitText="(count) => `+ ${count}`" :limitText="(count) => `+ ${count}`"
:normalizer=" :normalizer="
(node) => { (node) => {
return { return {
id: node.name || -1, id: node.name || -1,
label: node.alias || node.name || $t('other'), label: node.alias || node.name || $t('other'),
title: node.alias || node.name || $t('other'), title: node.alias || node.name || $t('other'),
children: node.attributes, children: node.attributes,
} }
} }
" "
appendToBody appendToBody
zIndex="1050" zIndex="1050"
> >
</treeselect> </treeselect>
<a-form-model <a-form-model
:model="form" :model="form"
:rules="rules" :rules="rules"
v-if="colType === 'read_ci'" v-if="colType === 'read_ci'"
:labelCol="{ span: 2 }" :labelCol="{ span: 2 }"
:wrapperCol="{ span: 10 }" :wrapperCol="{ span: 10 }"
ref="form" ref="form"
> >
<a-form-model-item :label="$t('name')" prop="name"> <a-form-model-item :label="$t('name')" prop="name">
<a-input v-model="form.name" /> <a-input v-model="form.name" />
</a-form-model-item> </a-form-model-item>
<FilterComp <FilterComp
ref="filterComp" ref="filterComp"
:isDropdown="false" :isDropdown="false"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList" :canSearchPreferenceAttrList="canSearchPreferenceAttrList"
@setExpFromFilter="setExpFromFilter" @setExpFromFilter="setExpFromFilter"
:expression="expression" :expression="expression"
/> />
</a-form-model> <div class="read-ci-tip">{{ $t('cmdb.ciType.ciGrantTip') }}</div>
</template> </a-form-model>
</CustomRadio> </template>
</a-modal> </CustomRadio>
</template> </a-modal>
</template>
<script>
import { grantCiType, revokeCiType } from '../../api/CIType' <script>
import { getCITypeAttributesByTypeIds } from '../../api/CITypeAttr' import { grantCiType, revokeCiType } from '../../api/CIType'
import FilterComp from '@/components/CMDBFilterComp' import { getCITypeAttributesByTypeIds } from '../../api/CITypeAttr'
import FilterComp from '@/components/CMDBFilterComp'
export default {
name: 'ReadGrantModal', export default {
components: { FilterComp }, name: 'ReadGrantModal',
props: { components: { FilterComp },
CITypeId: { props: {
type: Number, CITypeId: {
default: null, type: Number,
}, default: null,
}, },
inject: { },
provide_attrGroup: { inject: {
from: 'attrGroup', provide_attrGroup: {
}, from: 'attrGroup',
provide_filerPerimissions: { },
from: 'filerPerimissions', provide_filerPerimissions: {
}, from: 'filerPerimissions',
}, },
data() { },
return { data() {
visible: false, return {
colType: '', visible: false,
row: {}, colType: '',
radioValue: 1, row: {},
radioStyle: { radioValue: 1,
display: 'block', radioStyle: {
height: '30px', display: 'block',
lineHeight: '30px', height: '30px',
}, lineHeight: '30px',
selectedAttr: [], },
ruleList: [], selectedAttr: [],
canSearchPreferenceAttrList: [], ruleList: [],
expression: '', canSearchPreferenceAttrList: [],
form: { expression: '',
name: '', form: {
}, name: '',
rules: { },
name: [{ required: true, message: this.$t('cmdb.components.customizeFilterName') }], rules: {
}, name: [{ required: true, message: this.$t('cmdb.components.customizeFilterName') }],
} },
}, }
computed: { },
title() { computed: {
if (this.colType === 'read_attr') { title() {
return this.$t('cmdb.components.attributeGrant') if (this.colType === 'read_attr') {
} return this.$t('cmdb.components.attributeGrant')
return this.$t('cmdb.components.ciGrant') }
}, return this.$t('cmdb.components.ciGrant')
attrGroup() { },
return this.provide_attrGroup() attrGroup() {
}, return this.provide_attrGroup()
filerPerimissions() { },
return this.provide_filerPerimissions() filerPerimissions() {
}, return this.provide_filerPerimissions()
filterKey() { },
if (this.colType === 'read_attr') { filterKey() {
return 'attr_filter' if (this.colType === 'read_attr') {
} return 'attr_filter'
return 'ci_filter' }
}, return 'ci_filter'
}, },
methods: { },
async open(colType, row) { methods: {
this.visible = true async open(colType, row) {
this.colType = colType this.visible = true
this.row = row this.colType = colType
this.form = { this.row = row
name: '', this.form = {
} name: '',
if (this.colType === 'read_ci') { }
await getCITypeAttributesByTypeIds({ type_ids: this.CITypeId }).then((res) => { if (this.colType === 'read_ci') {
this.canSearchPreferenceAttrList = res.attributes.filter((item) => item.value_type !== '6') await getCITypeAttributesByTypeIds({ type_ids: this.CITypeId }).then((res) => {
}) this.canSearchPreferenceAttrList = res.attributes.filter((item) => item.value_type !== '6')
} })
if (this.filerPerimissions[row.rid]) { }
const _tempValue = this.filerPerimissions[row.rid][this.filterKey] if (this.filerPerimissions[row.rid]) {
if (_tempValue && _tempValue.length) { const _tempValue = this.filerPerimissions[row.rid][this.filterKey]
this.radioValue = 2 if (_tempValue && _tempValue.length) {
if (this.colType === 'read_attr') { this.radioValue = 2
this.selectedAttr = _tempValue if (this.colType === 'read_attr') {
} else { this.selectedAttr = _tempValue
this.expression = `q=${_tempValue}` } else {
this.form = { this.expression = `q=${_tempValue}`
name: this.filerPerimissions[row.rid].name || '', this.form = {
} name: this.filerPerimissions[row.rid].name || '',
this.$nextTick(() => { }
this.$refs.filterComp.visibleChange(true) this.$nextTick(() => {
}) this.$refs.filterComp.visibleChange(true)
} })
} }
} }
}, }
async handleOk() { },
if (this.radioValue === 1) { async handleOk() {
await grantCiType(this.CITypeId, this.row.rid, { if (this.radioValue === 1) {
perms: ['read'], await grantCiType(this.CITypeId, this.row.rid, {
attr_filter: this.colType === 'read_attr' ? [] : undefined, perms: ['read'],
ci_filter: this.colType === 'read_ci' ? '' : undefined, attr_filter: this.colType === 'read_attr' ? [] : undefined,
}) ci_filter: this.colType === 'read_ci' ? '' : undefined,
} else if (this.radioValue === 2) { })
if (this.colType === 'read_ci') { } else if (this.radioValue === 2) {
this.$refs.filterComp.handleSubmit() if (this.colType === 'read_ci') {
} this.$refs.filterComp.handleSubmit()
await grantCiType(this.CITypeId, this.row.rid, { }
perms: ['read'], await grantCiType(this.CITypeId, this.row.rid, {
attr_filter: this.colType === 'read_attr' ? this.selectedAttr : undefined, perms: ['read'],
ci_filter: this.colType === 'read_ci' ? this.expression.slice(2) : undefined, attr_filter: this.colType === 'read_attr' ? this.selectedAttr : undefined,
name: this.colType === 'read_ci' ? this.form.name : undefined, ci_filter: this.colType === 'read_ci' ? this.expression.slice(2) : undefined,
}) name: this.colType === 'read_ci' ? this.form.name : undefined,
} else { })
const _tempValue = this.filerPerimissions?.[this.row.rid]?.[this.filterKey] } else {
await revokeCiType(this.CITypeId, this.row.rid, { const _tempValue = this.filerPerimissions?.[this.row.rid]?.[this.filterKey]
perms: ['read'], await revokeCiType(this.CITypeId, this.row.rid, {
attr_filter: this.colType === 'read_attr' ? _tempValue : undefined, perms: ['read'],
ci_filter: this.colType === 'read_ci' ? _tempValue : undefined, attr_filter: this.colType === 'read_attr' ? _tempValue : undefined,
}) ci_filter: this.colType === 'read_ci' ? _tempValue : undefined,
} })
this.$emit('updateTableDataRead', this.row, this.radioValue === 1 || this.radioValue === 2) }
this.handleCancel() this.$emit('updateTableDataRead', this.row, this.radioValue === 1 || this.radioValue === 2)
}, this.handleCancel()
handleCancel() { },
this.radioValue = 1 handleCancel() {
this.selectedAttr = [] this.radioValue = 1
if (this.$refs.form) { this.selectedAttr = []
this.$refs.form.resetFields() if (this.$refs.form) {
} this.$refs.form.resetFields()
this.visible = false }
}, this.visible = false
setExpFromFilter(filterExp) { },
let expression = '' setExpFromFilter(filterExp) {
if (filterExp) { let expression = ''
expression = `q=${filterExp}` if (filterExp) {
} expression = `q=${filterExp}`
this.expression = expression }
}, this.expression = expression
changeRadioValue(value) { },
if (this.id_filter) { changeRadioValue(value) {
this.$message.warning(this.$t('cmdb.serviceTree.grantedByServiceTreeTips')) if (this.id_filter) {
} else { this.$message.warning(this.$t('cmdb.serviceTree.grantedByServiceTreeTips'))
this.radioValue = value } else {
} this.radioValue = value
}, }
}, },
} },
</script> }
</script>
<style></style>
<style lang="less" scoped>
.read-ci-tip {
font-size: 12px;
line-height: 22px;
color: #a5a9bc;
}
</style>

View File

@@ -0,0 +1,41 @@
import i18n from '@/lang'
export const ruleTypeList = () => {
return [
{ value: 'and', label: i18n.t('cmdbFilterComp.and') },
{ value: 'or', label: i18n.t('cmdbFilterComp.or') },
// { value: 'not', label: '非' },
]
}
export const expList = () => {
return [
{ value: 'is', label: i18n.t('cmdbFilterComp.is') },
{ value: '~is', label: i18n.t('cmdbFilterComp.~is') },
{ value: 'contain', label: i18n.t('cmdbFilterComp.contain') },
{ value: '~contain', label: i18n.t('cmdbFilterComp.~contain') },
{ value: 'start_with', label: i18n.t('cmdbFilterComp.start_with') },
{ value: '~start_with', label: i18n.t('cmdbFilterComp.~start_with') },
{ value: 'end_with', label: i18n.t('cmdbFilterComp.end_with') },
{ value: '~end_with', label: i18n.t('cmdbFilterComp.~end_with') },
{ value: '~value', label: i18n.t('cmdbFilterComp.~value') }, // 为空的定义有点绕
{ value: 'value', label: i18n.t('cmdbFilterComp.value') },
]
}
export const advancedExpList = () => {
return [
{ value: 'in', label: i18n.t('cmdbFilterComp.in') },
{ value: '~in', label: i18n.t('cmdbFilterComp.~in') },
{ value: 'range', label: i18n.t('cmdbFilterComp.range') },
{ value: '~range', label: i18n.t('cmdbFilterComp.~range') },
{ value: 'compare', label: i18n.t('cmdbFilterComp.compare') },
]
}
export const compareTypeList = [
{ value: '1', label: '>' },
{ value: '2', label: '>=' },
{ value: '3', label: '<' },
{ value: '4', label: '<=' },
]

View File

@@ -0,0 +1,320 @@
<template>
<div :style="{ lineHeight: rowHeight }">
<a-space :style="{ display: 'flex', marginBottom: '10px' }" v-for="(item, index) in ruleList" :key="item.id">
<div v-if="ruleList.length > 1" :style="{ width: '60px', height: rowHeight, position: 'relative' }">
<treeselect
v-if="index !== 0"
class="custom-treeselect"
:style="{ width: '60px', '--custom-height': rowHeight, position: 'absolute', top: '-24px' }"
v-model="item.type"
:multiple="false"
:clearable="false"
searchable
:options="ruleTypeList"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
:disabled="disabled"
>
</treeselect>
</div>
<treeselect
class="custom-treeselect"
:style="{ width: '120px', '--custom-height': rowHeight }"
v-model="item.property"
:multiple="false"
:clearable="false"
searchable
:options="canSearchPreferenceAttrList"
:normalizer="
(node) => {
return {
id: node.name,
label: node.alias || node.name,
children: node.children,
}
}
"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
<div
v-if="node.id !== '$count'"
:title="node.label"
slot="option-label"
slot-scope="{ node }"
class="property-label"
>
<ValueTypeMapIcon :attr="node.raw" />
{{ node.label }}
</div>
<div
v-else
:title="node.label"
slot="option-label"
slot-scope="{ node }"
class="property-label"
:style="{ borderBottom: '1px solid #E4E7ED', marginBottom: '8px' }"
>
<ValueTypeMapIcon :attr="node.raw" />
{{ node.label }}
</div>
<div
class="property-label"
slot="value-label"
slot-scope="{ node }"
>
<ValueTypeMapIcon :attr="node.raw" /> {{ node.label }}
</div>
</treeselect>
<treeselect
class="custom-treeselect"
:style="{ width: '90px', '--custom-height': rowHeight }"
v-model="item.exp"
:multiple="false"
:clearable="false"
searchable
:options="getExpListByProperty(item.property)"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
appendToBody
@select="(value) => handleChangeExp(value, item, index)"
:zIndex="1050"
:disabled="disabled"
>
</treeselect>
<ValueControls
:rule="ruleList[index]"
:attrList="canSearchPreferenceAttrList"
:disabled="disabled"
:curModelAttrList="curModelAttrList"
:rowHeight="rowHeight"
@change="(value) => handleChangeValue(value, index)"
/>
<template v-if="!disabled">
<a-tooltip :title="$t('copy')">
<a class="operation" @click="handleCopyRule(item)"><ops-icon type="veops-copy"/></a>
</a-tooltip>
<a-tooltip :title="$t('delete')">
<a class="operation" @click="handleDeleteRule(item)"><a-icon type="minus-circle"/></a>
</a-tooltip>
<a-tooltip :title="$t('cmdbFilterComp.addHere')">
<a class="operation" @click="handleAddRuleAt(item)"><a-icon type="plus-circle"/></a>
</a-tooltip>
</template>
</a-space>
<div class="table-filter-add" v-if="!disabled && ruleList.length === 0">
<a @click="handleAddRule">+ {{ $t('new') }}</a>
</div>
</div>
</template>
<script>
import _ from 'lodash'
import { v4 as uuidv4 } from 'uuid'
import { ruleTypeList, expList, advancedExpList, compareTypeList } from './constants.js'
import ValueTypeMapIcon from '@/components/CMDBValueTypeMapIcon'
import ValueControls from './valueControls.vue'
export default {
name: 'Expression',
components: {
ValueTypeMapIcon,
ValueControls
},
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: Array,
default: () => [],
},
canSearchPreferenceAttrList: {
type: Array,
required: true,
default: () => [],
},
disabled: {
type: Boolean,
default: false,
},
curModelAttrList: {
type: Array,
default: () => []
}
},
data() {
return {
compareTypeList,
rowHeight: '36px',
}
},
computed: {
ruleList: {
get() {
return this.value
},
set(val) {
this.$emit('change', val)
return val
},
},
ruleTypeList() {
return ruleTypeList()
},
expList() {
return expList()
},
advancedExpList() {
return advancedExpList()
}
},
methods: {
getExpListByProperty(property) {
if (property === '$count') {
return [
{ value: 'is', label: this.$t('cmdbFilterComp.is') },
{ value: '~is', label: this.$t('cmdbFilterComp.~is') },
{ value: 'compare', label: this.$t('cmdbFilterComp.compare') }
]
}
if (property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find && ['0', '1', '3', '4', '5'].includes(_find.value_type)) {
return [
{ value: 'is', label: this.$t('cmdbFilterComp.is') },
{ value: '~is', label: this.$t('cmdbFilterComp.~is') },
{ value: '~value', label: this.$t('cmdbFilterComp.~value') }, // 为空的定义有点绕
{ value: 'value', label: this.$t('cmdbFilterComp.value') },
...this.advancedExpList
]
}
}
return [...this.expList, ...this.advancedExpList]
},
isChoiceByProperty(property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find) {
return _find.is_choice
}
return false
},
handleAddRule() {
this.ruleList.push({
id: uuidv4(),
type: 'and',
property: this.canSearchPreferenceAttrList[0]?.name,
exp: 'is',
value: null,
})
this.$emit('change', this.ruleList)
},
handleCopyRule(item) {
this.ruleList.push({ ...item, id: uuidv4() })
this.$emit('change', this.ruleList)
},
handleDeleteRule(item) {
const idx = this.ruleList.findIndex((r) => r.id === item.id)
if (idx > -1) {
this.ruleList.splice(idx, 1)
}
this.$emit('change', this.ruleList)
},
handleAddRuleAt(item) {
const idx = this.ruleList.findIndex((r) => r.id === item.id)
if (idx > -1) {
this.ruleList.splice(idx + 1, 0, {
id: uuidv4(),
type: 'and',
property: this.canSearchPreferenceAttrList[0]?.name,
exp: 'is',
value: null,
})
}
this.$emit('change', this.ruleList)
},
getChoiceValueByProperty(property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find) {
return _find.choice_value
}
return []
},
handleChangeExp({ value }, item, index) {
const _ruleList = _.cloneDeep(this.ruleList)
if (value === 'range') {
_ruleList[index] = {
..._ruleList[index],
min: '',
max: '',
exp: value,
}
} else if (value === 'compare') {
_ruleList[index] = {
..._ruleList[index],
compareType: '1',
exp: value,
}
} else {
_ruleList[index] = {
..._ruleList[index],
exp: value,
}
}
this.ruleList = _ruleList
this.$emit('change', this.ruleList)
},
handleChangeValue(value, index) {
const _ruleList = _.cloneDeep(this.ruleList)
_ruleList[index] = value
this.$emit('change', _ruleList)
},
},
}
</script>
<style lang="less" scoped>
.input-group {
display: flex;
align-items: center;
width: 150px;
&-range-icon {
margin: 0 8px;
}
input {
height: 36px;
}
}
.property-label {
width: 100%;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden
}
.attr-filter-tip {
color: #86909C;
font-size: 12px;
font-weight: 400;
}
</style>

View File

@@ -0,0 +1,211 @@
<template>
<div>
<Expression
v-model="ruleList"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)"
:disabled="false"
:curModelAttrList="curModelAttrList"
/>
</div>
</template>
<script>
import { v4 as uuidv4 } from 'uuid'
import { compareTypeList } from './constants.js'
import Expression from './expression.vue'
export default {
name: 'AttrFilter',
components: {
Expression
},
props: {
canSearchPreferenceAttrList: {
type: Array,
required: true,
default: () => [],
},
expression: {
type: String,
default: '',
},
regQ: {
type: String,
default: '(?<=q=).+(?=&)|(?<=q=).+$',
},
CITypeIds: {
type: Array,
default: () => [],
},
curModelAttrList: {
type: Array,
default: () => []
}
},
data() {
return {
compareTypeList,
visible: false,
ruleList: [],
filterExp: '',
}
},
methods: {
init(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,
},
]
: []
}
},
handleSubmit() {
if (this.ruleList && this.ruleList.length) {
this.ruleList[0].type = 'and' // 增删后以防万一第一个不是and
this.filterExp = ''
const expList = this.ruleList.filter((rule) => rule?.property).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>
</style>

View File

@@ -0,0 +1,255 @@
<template>
<div class="control-group">
<CIReferenceAttr
v-if="getAttr(rule.property).is_reference && (rule.exp === 'is' || rule.exp === '~is')"
class="select-filter"
:referenceTypeId="getAttr(rule.property).reference_type_id"
:value="rule.value"
:disabled="disabled"
@change="(value) => handleChange('value', value)"
/>
<a-select
v-else-if="getAttr(rule.property).is_bool && (rule.exp === 'is' || rule.exp === '~is')"
class="select-filter"
:disabled="disabled"
:placeholder="$t('placeholder2')"
:value="rule.value"
@change="(value) => handleChange('value', value)"
>
<a-select-option key="1">
true
</a-select-option>
<a-select-option key="0">
false
</a-select-option>
</a-select>
<div
class="input-group"
v-else-if="isChoiceByProperty(rule.property) && (rule.exp === 'is' || rule.exp === '~is')"
>
<a-select
class="select-filter"
:style="{ width: '175px' }"
showSearch
:placeholder="$t('placeholder2')"
:disabled="disabled"
:value="rule.value"
@change="(value) => handleChange('value', value)"
>
<a-select-option
v-for="(node) in getChoiceValueByProperty(rule.property)"
:key="String(node[0])"
:title="node[1] ? node[1].label || node[0] : node[0]"
>
<a-tooltip placement="topLeft" :title="node[1] ? node[1].label || node[0] : node[0]" >
{{ node[1] ? node[1].label || node[0] : node[0] }}
</a-tooltip>
</a-select-option>
</a-select>
<!-- <treeselect
class="custom-treeselect"
:style="{ '--custom-height': rowHeight }"
:value="rule.value"
@input="(value) => handleChange('value', value)"
:multiple="false"
:clearable="false"
searchable
:options="getChoiceValueByProperty(rule.property)"
:placeholder="$t('placeholder2')"
:normalizer="
(node) => {
return {
id: node[0],
label: node[1] ? node[1].label || node[0] : node[0],
children: node.children,
}
}
"
:zIndex="1050"
:disabled="disabled"
appendToBody
>
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
{{ node.label }}
</div>
</treeselect> -->
</div>
<div
compact
v-else-if="rule.exp === 'range' || rule.exp === '~range'"
class="input-group"
>
<a-input
class="ops-input"
:placeholder="$t('min')"
:disabled="disabled"
:value="rule.min"
@change="(e) => handleChange('min', e.target.value)"
/>
<span class="input-group-range-icon">~</span>
<a-input
class="ops-input"
v-model="rule.max"
:placeholder="$t('max')"
:disabled="disabled"
:value="rule.max"
@change="(e) => handleChange('max', e.target.value)"
/>
</div>
<div class="input-group" compact v-else-if="rule.exp === 'compare'">
<treeselect
class="custom-treeselect"
:style="{ width: '70px', '--custom-height': rowHeight, 'flex-shrink': 0 }"
:value="rule.compareType"
@input="(value) => handleChange('compareType', value)"
:multiple="false"
:clearable="false"
searchable
:options="compareTypeList"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
:zIndex="1050"
:disabled="disabled"
appendToBody
>
</treeselect>
<a-input :value="rule.value" @change="(e) => handleChange('value', e.target.value)" class="ops-input"/>
</div>
<div class="input-group" v-else-if="rule.exp !== 'value' && rule.exp !== '~value'">
<a-input
:value="rule.value"
@change="(e) => handleChange('value', e.target.value)"
:placeholder="rule.exp === 'in' || rule.exp === '~in' ? $t('cmdbFilterComp.split', { separator: ';' }) : ''"
class="ops-input"
:disabled="disabled"
></a-input>
</div>
<div v-else :style="{ width: '136px' }"></div>
</div>
</template>
<script>
import { compareTypeList } from './constants.js'
import CIReferenceAttr from '@/components/ciReferenceAttr/index.vue'
export default {
name: 'ValueControls',
components: {
CIReferenceAttr,
},
props: {
rule: {
type: Object,
default: () => {},
},
attrList: {
type: Array,
default: () => [],
},
disabled: {
type: Boolean,
default: false,
},
// 当前模型属性
curModelAttrList: {
type: Array,
default: () => []
},
// 行高
rowHeight: {
type: String,
default: ''
},
},
data() {
return {
compareTypeList,
}
},
computed: {
choiceValue() {
const val = /\{\{([^}]+)\}\}/g.exec(this?.rule?.value || '')
return val ? val?.[1]?.trim() || '' : this?.value?.value
}
},
methods: {
isChoiceByProperty(property) {
const _find = this.attrList.find((item) => item.name === property)
if (_find) {
return _find.is_choice
}
return false
},
getChoiceValueByProperty(property) {
const _find = this.attrList.find((item) => item.name === property)
if (_find) {
return _find.choice_value
}
return []
},
handleChange(key, value) {
this.$emit('change', {
...this.rule,
[key]: value
})
},
getAttr(property) {
return this.attrList.find((item) => item.name === property) || {}
},
}
}
</script>
<style lang="less" scoped>
.control-group {
display: flex;
}
.input-group {
display: flex;
align-items: center;
width: 136px;
&-range-icon {
margin: 0 8px;
}
input {
height: 36px;
}
}
.select-filter {
height: 36px;
width: 136px;
/deep/ .ant-select-selection {
height: 36px;
background: #f7f8fa;
line-height: 36px;
border: none;
.ant-select-selection__rendered {
height: 36px;
line-height: 36px;
}
}
/deep/ .vue-treeselect__control {
background: #f7f8fa;
border: none;
}
}
</style>

View File

@@ -29,7 +29,7 @@
class="category-side-children-item-corporate" class="category-side-children-item-corporate"
v-if="ruleType === 'private_cloud' || (ruleType === 'http' && (categoryIndex !== 0 || itemIndex !== 0))" v-if="ruleType === 'private_cloud' || (ruleType === 'http' && (categoryIndex !== 0 || itemIndex !== 0))"
> >
{{ $t('cmdb.enterpriseVersionFlag') }}
</span> </span>
</div> </div>
</div> </div>
@@ -67,7 +67,7 @@
class="corporate-flag" class="corporate-flag"
v-if="ruleType === 'private_cloud' || (ruleType === 'http' && (categoryIndex !== 0 || itemIndex !== 0))" v-if="ruleType === 'private_cloud' || (ruleType === 'http' && (categoryIndex !== 0 || itemIndex !== 0))"
> >
<span class="corporate-flag-text"></span> <span class="corporate-flag-text">{{ $t('cmdb.enterpriseVersionFlag') }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -252,6 +252,8 @@ export default {
cursor: pointer; cursor: pointer;
position: relative; position: relative;
min-width: 100px;
text-align: center;
&:hover { &:hover {
background-color: @layout-sidebar-selected-color; background-color: @layout-sidebar-selected-color;

View File

@@ -9,7 +9,7 @@
@clickCategory="setCurrentCate" @clickCategory="setCurrentCate"
/> />
<template v-else> <template v-else>
<a-select v-if="isCloud" :style="{ marginBottom: '10px', minWidth: '120px' }" v-model="currentCate"> <a-select v-if="isCloud" :style="{ marginBottom: '10px', minWidth: '200px' }" v-model="currentCate">
<a-select-option v-for="cate in categoriesSelect" :key="cate" :value="cate">{{ cate }}</a-select-option> <a-select-option v-for="cate in categoriesSelect" :key="cate" :value="cate">{{ cate }}</a-select-option>
</a-select> </a-select>
<AttrMapTable <AttrMapTable
@@ -29,7 +29,9 @@
</template> </template>
<script> <script>
import { getHttpCategories, getHttpAttributes, getSnmpAttributes } from '../../api/discovery' import _ from 'lodash'
import { getHttpCategories, getHttpAttributes, getSnmpAttributes, getHttpAttrMapping } from '../../api/discovery'
import { DISCOVERY_CATEGORY_TYPE } from '@/modules/cmdb/views/discovery/constants.js'
import AttrMapTable from '@/modules/cmdb/components/attrMapTable/index.vue' import AttrMapTable from '@/modules/cmdb/components/attrMapTable/index.vue'
import ADPreviewTable from './adPreviewTable.vue' import ADPreviewTable from './adPreviewTable.vue'
import HttpADCategory from './httpADCategory.vue' import HttpADCategory from './httpADCategory.vue'
@@ -69,6 +71,10 @@ export default {
uniqueKey: { uniqueKey: {
type: String, type: String,
default: '', default: '',
},
currentAdt: {
type: Object,
default: () => {},
} }
}, },
data() { data() {
@@ -77,6 +83,7 @@ export default {
categoriesSelect: [], categoriesSelect: [],
currentCate: '', currentCate: '',
tableData: [], tableData: [],
httpAttrMap: {}
} }
}, },
computed: { computed: {
@@ -95,7 +102,7 @@ export default {
} }
}, },
isCloud() { isCloud() {
return ['http', 'private_cloud'].includes(this.ruleType) return [DISCOVERY_CATEGORY_TYPE.HTTP, DISCOVERY_CATEGORY_TYPE.PRIVATE_CLOUD].includes(this.ruleType)
} }
}, },
watch: { watch: {
@@ -103,13 +110,7 @@ export default {
immediate: true, immediate: true,
handler(newVal) { handler(newVal) {
if (newVal) { if (newVal) {
getHttpAttributes(this.ruleName, { resource: newVal }).then((res) => { this.getHttpAttr(newVal)
if (this.isEdit) {
this.formatTableData(res)
} else {
this.tableData = res
}
})
} }
}, },
}, },
@@ -119,7 +120,7 @@ export default {
this.currentCate = '' this.currentCate = ''
this.$nextTick(() => { this.$nextTick(() => {
const { ruleType, ruleName } = newVal const { ruleType, ruleName } = newVal
if (['snmp', 'components'].includes(ruleType) && ruleName) { if ([DISCOVERY_CATEGORY_TYPE.SNMP, DISCOVERY_CATEGORY_TYPE.COMPONENT].includes(ruleType) && ruleName) {
getSnmpAttributes(ruleType, ruleName).then((res) => { getSnmpAttributes(ruleType, ruleName).then((res) => {
if (this.isEdit) { if (this.isEdit) {
this.formatTableData(res) this.formatTableData(res)
@@ -140,7 +141,7 @@ export default {
}) })
this.categoriesSelect = categoriesSelect this.categoriesSelect = categoriesSelect
if (this.isEdit && categoriesSelect?.length) { if (this.isEdit && categoriesSelect?.length) {
this.currentCate = categoriesSelect[0] this.currentCate = this?.currentAdt?.extra_option?.category || categoriesSelect[0]
} }
}) })
} }
@@ -158,28 +159,54 @@ export default {
}, },
formatTableData(list) { formatTableData(list) {
const _findADT = this.adCITypeList.find((item) => Number(item.adr_id) === Number(this.currentTab)) const _findADT = this.adCITypeList.find((item) => Number(item.adr_id) === Number(this.currentTab))
this.tableData = (list || []).map((item) => { this.tableData = (list || []).map((val) => {
if (_findADT.attributes) { const item = _.cloneDeep(val)
return {
...item, if (_findADT?.attributes?.[item.name]) {
attr: _findADT.attributes[`${item.name}`], item.attr = _findADT.attributes[item.name]
} }
} else {
const attrMapName = this.httpAttrMap?.[item?.name]
if (
this.isEdit &&
!item.attr &&
attrMapName &&
this.ciTypeAttributes.some((ele) => ele.name === attrMapName)
) {
item.attr = attrMapName
}
if (!item.attr) {
const _find = this.ciTypeAttributes.find((ele) => ele.name === item.name) const _find = this.ciTypeAttributes.find((ele) => ele.name === item.name)
if (_find) { if (_find) {
return { item.attr = _find.name
...item,
attr: _find.name,
}
} }
return item
} }
return item
}) })
}, },
getTableData() { getTableData() {
const $table = this.$refs.attrMapTable const $table = this.$refs.attrMapTable
const { fullData } = $table.getTableData() const { fullData } = $table.getTableData()
return fullData || [] return fullData || []
},
async getHttpAttr(val) {
await this.getHttpAttrMapping(this.ruleName, val)
getHttpAttributes(this.ruleName, { resource: val }).then((res) => {
if (this.isEdit) {
this.formatTableData(res)
} else {
this.tableData = res
}
})
},
async getHttpAttrMapping(name, resource) {
const res = await getHttpAttrMapping(name, resource)
this.httpAttrMap = res || {}
} }
}, },
} }

View File

@@ -1,53 +1,27 @@
<template> <template>
<div class="node-setting-wrap"> <div class="node-setting-wrap">
<a-row v-for="(node) in nodes" :key="node.id"> <ops-table
<a-col :span="6"> :data="nodes"
<a-form-item :label="$t('cmdb.ciType.nodeSettingIp')"> size="mini"
<a-input show-header-overflow
allowClear :row-config="{ height: 42 }"
v-decorator="[ border
`node_ip_${node.id}`, :min-height="78"
{ >
rules: [ <vxe-column width="170" :title="$t('cmdb.ciType.nodeSettingIp')">
{ required: false, message: $t('cmdb.ciType.nodeSettingIpTip') }, <template #default="{ row }">
{ <a-input v-model="row.ip"></a-input>
pattern: </template>
'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$', </vxe-column>
message: $t('cmdb.ciType.nodeSettingIpTip1'), <vxe-column width="170" :title="$t('cmdb.ciType.nodeSettingCommunity')">
trigger: 'blur', <template #default="{ row }">
}, <a-input v-model="row.community"></a-input>
], </template>
}, </vxe-column>
]" <vxe-column width="170" :title="$t('cmdb.ciType.nodeSettingVersion')">
:placeholder="$t('cmdb.ciType.nodeSettingIpTip')" <template #default="{ row }">
/>
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item :label="$t('cmdb.ciType.nodeSettingCommunity')">
<a-input
allowClear
v-decorator="[
`node_community_${node.id}`,
{
rules: [{ required: false, message: $t('cmdb.ciType.nodeSettingCommunityTip') }],
},
]"
:placeholder="$t('cmdb.ciType.nodeSettingCommunityTip')"
/>
</a-form-item>
</a-col>
<a-col :span="5">
<a-form-item :label="$t('cmdb.ciType.nodeSettingVersion')">
<a-select <a-select
v-decorator="[ v-model="row.version"
`node_version_${node.id}`,
{
rules: [{ required: false, message: $t('cmdb.ciType.nodeSettingVersionTip') }],
},
]"
:placeholder="$t('cmdb.ciType.nodeSettingVersionTip')" :placeholder="$t('cmdb.ciType.nodeSettingVersionTip')"
allowClear allowClear
class="node-setting-select" class="node-setting-select"
@@ -58,26 +32,25 @@
<a-select-option value="2c"> <a-select-option value="2c">
v2c v2c
</a-select-option> </a-select-option>
<a-select-option value="3">
v3
</a-select-option>
</a-select> </a-select>
</a-form-item> </template>
</a-col> </vxe-column>
<a-col :span="3"> <vxe-column wdith="170">
<div class="action"> <template #default="{ row }">
<a @click="() => copyNode(node.id)"> <div class="action">
<a-icon type="copy" /> <a @click="() => copyNode(row.id)">
</a> <a-icon type="copy" />
<a @click="() => removeNode(node.id, 1)"> </a>
<a-icon type="minus-circle" /> <a @click="() => removeNode(row.id, 1)">
</a> <a-icon type="minus-circle" />
<a @click="addNode"> </a>
<a-icon type="plus-circle" /> <a @click="addNode">
</a> <a-icon type="plus-circle" />
</div> </a>
</a-col> </div>
</a-row> </template>
</vxe-column>
</ops-table>
</div> </div>
</template> </template>
@@ -110,17 +83,10 @@ export default {
const newNode = { const newNode = {
id: uuidv4(), id: uuidv4(),
ip: '', ip: '',
community: '', community: 'public',
version: '', version: '',
} }
this.nodes.push(newNode) this.nodes.push(newNode)
this.$nextTick(() => {
this.form.setFieldsValue({
[`node_ip_${newNode.id}`]: newNode.ip,
[`node_community_${newNode.id}`]: newNode.community,
[`node_version_${newNode.id}`]: newNode.version,
})
})
}, },
removeNode(removeId, minLength) { removeNode(removeId, minLength) {
if (this.nodes.length <= minLength) { if (this.nodes.length <= minLength) {
@@ -133,45 +99,20 @@ export default {
} }
}, },
copyNode(id) { copyNode(id) {
const newNode = { const copyNode = this.nodes.find((item) => item.id === id)
id: uuidv4(), if (copyNode) {
ip: this.form.getFieldValue(`node_ip_${id}`), const newNode = {
community: this.form.getFieldValue(`node_community_${id}`), ...copyNode,
version: this.form.getFieldValue(`node_version_${id}`), id: uuidv4(),
}
this.nodes.push(newNode)
this.$nextTick(() => {
this.form.setFieldsValue({
[`node_ip_${newNode.id}`]: newNode.ip,
[`node_community_${newNode.id}`]: newNode.community,
[`node_version_${newNode.id}`]: newNode.version,
})
})
},
getInfoValuesFromForm(values) {
return this.nodes.map((item) => {
return {
id: item.id,
ip: values[`node_ip_${item.id}`],
community: values[`node_community_${item.id}`],
version: values[`node_version_${item.id}`],
} }
}) this.nodes.push(newNode)
},
setNodeField() {
if (this.nodes && this.nodes.length) {
this.nodes.forEach((item) => {
this.form.setFieldsValue({
[`node_ip_${item.id}`]: item.ip,
[`node_community_${item.id}`]: item.community,
[`node_version_${item.id}`]: item.version,
})
})
} }
}, },
getNodeValue() { getNodeValue() {
const values = this.form.getFieldsValue() const nodes = this.nodes.map((node) => {
return this.getInfoValuesFromForm(values) return _.pick(node, ['ip', 'community', 'version'])
})
return nodes
}, },
}, },
} }
@@ -180,10 +121,9 @@ export default {
<style lang="less" scoped> <style lang="less" scoped>
.node-setting-wrap { .node-setting-wrap {
margin-left: 17px; margin-left: 17px;
width: 600px;
.ant-row { .ant-row {
// display: flex;
/deep/ .ant-input-clear-icon { /deep/ .ant-input-clear-icon {
color: rgba(0,0,0,.25); color: rgba(0,0,0,.25);

View File

@@ -113,6 +113,11 @@ export default {
this.editor.insertNode(node) this.editor.insertNode(node)
} }
}, },
destroy() {
const editor = this.editor
if (editor == null) return
editor.destroy()
}
}, },
} }
</script> </script>

View File

@@ -65,9 +65,7 @@
<a><a-icon type="question-circle"/></a> <a><a-icon type="question-circle"/></a>
</a-tooltip> </a-tooltip>
</a-input> </a-input>
<a-tooltip :title="$t('reset')"> <a-button @click="reset">{{ $t('reset') }}</a-button>
<a-button @click="reset">{{ $t('reset') }}</a-button>
</a-tooltip>
<FilterComp <FilterComp
ref="filterComp" ref="filterComp"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList" :canSearchPreferenceAttrList="canSearchPreferenceAttrList"
@@ -310,6 +308,11 @@ export default {
height: 32px; height: 32px;
.search-form-bar-filter { .search-form-bar-filter {
.ops_display_wrapper(transparent); .ops_display_wrapper(transparent);
&:hover {
color: @primary-color;
}
.search-form-bar-filter-icon { .search-form-bar-filter-icon {
color: @primary-color; color: @primary-color;
font-size: 12px; font-size: 12px;

View File

@@ -105,7 +105,7 @@ export default {
return { method, url, parameters, body, headers, authorization } return { method, url, parameters, body, headers, authorization }
}, },
setParams(params) { setParams(params) {
const { method, url, parameters, body, headers, authorization = {} } = params ?? {} const { method = 'GET', url = '', parameters = {}, body = '', headers = {}, authorization = {} } = params ?? {}
this.method = method this.method = method
this.url = url this.url = url
this.$refs.Parameters.parameters = this.$refs.Parameters.parameters =

View File

@@ -2,6 +2,8 @@ const cmdb_en = {
relation: 'Relation', relation: 'Relation',
attribute: 'Attributes', attribute: 'Attributes',
configTable: 'Config Table', configTable: 'Config Table',
enterpriseVersionFlag: 'Pro',
enterpriseVersionTip: 'Enterprise version only',
menu: { menu: {
views: 'Views', views: 'Views',
topologyView: 'Topology Views', topologyView: 'Topology Views',
@@ -60,7 +62,7 @@ const cmdb_en = {
desc: 'Reverse order', desc: 'Reverse order',
uniqueKey: 'Unique Identifies', uniqueKey: 'Unique Identifies',
uniqueKeySelect: 'Please select a unique identifier', uniqueKeySelect: 'Please select a unique identifier',
uniqueKeyTips: 'json/password/computed/choice can not be unique identifies', uniqueKeyTips: 'json/password/computed/selectList can not be unique identifies',
notfound: 'Can\'t find what you want?', notfound: 'Can\'t find what you want?',
cannotDeleteGroupTips: 'There is data under this group and cannot be deleted!', cannotDeleteGroupTips: 'There is data under this group and cannot be deleted!',
confirmDeleteGroup: 'Are you sure you want to delete group [{groupName}]?', confirmDeleteGroup: 'Are you sure you want to delete group [{groupName}]?',
@@ -115,7 +117,7 @@ const cmdb_en = {
advancedSettings: 'Advanced Settings', advancedSettings: 'Advanced Settings',
font: 'Font', font: 'Font',
color: 'Color', color: 'Color',
choiceValue: 'Predefined value', choiceValue: 'Select List',
computedAttribute: 'Computed Attribute', computedAttribute: 'Computed Attribute',
computedAttributeTips: 'The value of this attribute is calculated through an expression constructed from other attributes of the CIType or by executing a piece of code. The reference method of the attribute is: {{ attribute name }}', computedAttributeTips: 'The value of this attribute is calculated through an expression constructed from other attributes of the CIType or by executing a piece of code. The reference method of the attribute is: {{ attribute name }}',
addAttribute: 'New attribute', addAttribute: 'New attribute',
@@ -128,6 +130,7 @@ const cmdb_en = {
selectAttribute: 'Select Attribute', selectAttribute: 'Select Attribute',
groupExisted: 'Group name already exists', groupExisted: 'Group name already exists',
attributeSortedTips: 'Attributes in other groups cannot be sorted. If you need to sort, please drag them to a custom group first!', attributeSortedTips: 'Attributes in other groups cannot be sorted. If you need to sort, please drag them to a custom group first!',
attributeSortedTips2: 'Non-inherited attributes cannot be inserted before inherited attributes!',
buildinAttribute: 'built-in attributes', buildinAttribute: 'built-in attributes',
expr: 'Expression', expr: 'Expression',
code: 'Code', code: 'Code',
@@ -139,7 +142,7 @@ const cmdb_en = {
selectCIType: 'Please select a CMDB CIType', selectCIType: 'Please select a CMDB CIType',
selectCITypeAttributes: 'Please select CIType attributes', selectCITypeAttributes: 'Please select CIType attributes',
selectAttributes: 'Please select attributes', selectAttributes: 'Please select attributes',
choiceScriptDemo: 'class ChoiceValue(object):\n @staticmethod\n def values():\n """\n Execution entry, returns predefined value\n :return: Returns a list, the type of the value is the same as the type of the attribute\n For example:\n return ["online", "offline"]\n """\n return []', choiceScriptDemo: 'class ChoiceValue(object):\n @staticmethod\n def values():\n """\n Execution entry, returns select list\n :return: Returns a list, the type of the value is the same as the type of the attribute\n For example:\n return ["online", "offline"]\n """\n return []',
valueExisted: 'The current value already exists!', valueExisted: 'The current value already exists!',
addRelation: 'Add Relation', addRelation: 'Add Relation',
sourceCIType: 'Source CIType', sourceCIType: 'Source CIType',
@@ -187,6 +190,14 @@ const cmdb_en = {
confirmDeleteTrigger: 'Are you sure to delete this trigger?', confirmDeleteTrigger: 'Are you sure to delete this trigger?',
int: 'Integer', int: 'Integer',
float: 'Float', float: 'Float',
longText: 'Long Text',
shortText: 'Short Text',
shortTextTip: 'Text length <= 128',
referenceModel: 'Reference Model',
referenceModelTip: 'Please select reference model',
referenceModelTip1: 'For quick view of referenced model instances',
bool: 'Bool',
reference: 'Reference',
text: 'Text', text: 'Text',
datetime: 'DateTime', datetime: 'DateTime',
date: 'Date', date: 'Date',
@@ -204,7 +215,7 @@ const cmdb_en = {
otherGroupTips: 'Non sortable within the other group', otherGroupTips: 'Non sortable within the other group',
filterTips: 'click to show {name}', filterTips: 'click to show {name}',
attributeAssociation: 'Attribute Association', attributeAssociation: 'Attribute Association',
attributeAssociationTip1: 'Automatically establish relationships through the attributes except password, json and multiple of two models', attributeAssociationTip1: 'Automatically establish relationships through attribute values (except password, json, multi-value, long text, boolean, reference) of two models',
attributeAssociationTip2: 'Double click to edit', attributeAssociationTip2: 'Double click to edit',
attributeAssociationTip3: 'Two Attributes must be selected', attributeAssociationTip3: 'Two Attributes must be selected',
attributeAssociationTip4: 'Please select a attribute from Source CIType', attributeAssociationTip4: 'Please select a attribute from Source CIType',
@@ -236,7 +247,7 @@ const cmdb_en = {
checkModalColumn4: 'Last checkup time', checkModalColumn4: 'Last checkup time',
testModalTitle: 'Automated discovery testing', testModalTitle: 'Automated discovery testing',
attrMapTableAttrPlaceholder: 'Please edit the name', attrMapTableAttrPlaceholder: 'Please edit the name',
nodeSettingIp: 'IP Addresses', nodeSettingIp: 'Network device IP address',
nodeSettingIpTip: 'Please enter the ip address', nodeSettingIpTip: 'Please enter the ip address',
nodeSettingIpTip1: 'ip address format error', nodeSettingIpTip1: 'ip address format error',
nodeSettingCommunity: 'Community', nodeSettingCommunity: 'Community',
@@ -259,6 +270,56 @@ const cmdb_en = {
account: 'Account', account: 'Account',
insecure: 'Certificate Validation', insecure: 'Certificate Validation',
vcenterName: 'Platform Name', vcenterName: 'Platform Name',
resourceSearchTip1: 'Please use conditional filtering for CI filtering and copy and paste the filter expression into the fill-in box in the previous step.',
resourceSearchTip2: 'Note 1: Please use the green button to the right of the expression to copy it',
resourceSearchTip3: 'Note 2: If you do not need to filter, please click the grey button to copy and paste directly to configure for all nodes',
enable: 'Enable',
enableTip: 'Confirm switching on?',
portScanConfig: 'Port Scan Config',
portScanLabel1: 'CIDR',
portScanLabel2: 'Port Range',
portScanLabel3: 'AgentID',
viewAllAttr: 'View All Prop',
attrGroup: 'Attr Group',
attrName: 'Attr Name',
attrAlias: 'Attr Alias',
attrCode: 'Attr Code',
computedAttrTip1: 'Reference attributes follow jinja2 syntax',
computedAttrTip2: `Multi-valued attributes (lists) are rendered with [ ] included by default, if you want to remove it, the reference method is: """{{ attr_name | join(',') }}""" where commas are separators`,
computedAttrTip3: `Cannot refer to other computed attributes`,
example: 'Example',
attrFilterTip: `The third column of values allows you to select attributes of this model to cascade attributes`,
rule: 'Rule',
cascadeAttr: 'Cascade',
cascadeAttrTip: 'Cascading attributes note the order',
enumValue: 'Value',
label: 'Label',
valueInputTip: 'Please input value',
enumValueTip2: 'Enumeration values cannot be repeated',
builtin: 'Built In',
department: 'Department',
user: 'User',
userGroup: 'User Group',
departmentTip: 'Scroll down to select all departments in the company structure for common settings',
userGroupSelectTip: 'Please select user group',
displayValue: 'Display Value',
displayValueSelectTip: 'Please select Display Value',
departmentCascadeDisplay: 'Cascade Display',
filterUsers: 'Filter Users',
enum: 'Enum',
ciGrantTip: `Filter conditions can be changed dynamically using {{}} referenced variables, currently user variables are supported, such as {{user.uid}},{{user.username}},{{user.email}},{{user.nickname}}`,
searchInputTip: 'Please search for resource keywords',
resourceSearch: 'Resource Search',
recentSearch: 'Recent Search',
myCollection: 'My Collection',
keyword: 'Keywords',
CIType: 'CIType',
filterPopoverLabel: 'Filter',
conditionFilter: 'Condition Filter',
advancedFilter: 'Advanced Filter',
saveCondition: 'Save Condition',
confirmClear: 'Confirm to clear?',
currentPage: 'Current Page'
}, },
components: { components: {
unselectAttributes: 'Unselected', unselectAttributes: 'Unselected',
@@ -300,7 +361,7 @@ const cmdb_en = {
pleaseSearch: 'Please search', pleaseSearch: 'Please search',
conditionFilter: 'Conditional filtering', conditionFilter: 'Conditional filtering',
attributeDesc: 'Attribute Description', attributeDesc: 'Attribute Description',
ciSearchTips: '1. JSON/password/link attributes cannot be searched\n2. If the search content includes commas, they need to be escaped\n3. Only index attributes are searched, non-index attributes use conditional filtering', ciSearchTips: '1. JSON/password/link/longText/reference attributes cannot be searched\n2. If the search content includes commas, they need to be escaped\n3. Only index attributes are searched, non-index attributes use conditional filtering',
ciSearchTips2: 'For example: q=hostname:*0.0.0.0*', ciSearchTips2: 'For example: q=hostname:*0.0.0.0*',
subCIType: 'Subscription CIType', subCIType: 'Subscription CIType',
already: 'already', already: 'already',
@@ -308,6 +369,7 @@ const cmdb_en = {
sub: 'subscription', sub: 'subscription',
selectBelow: 'Please select below', selectBelow: 'Please select below',
subSuccess: 'Subscription successful', subSuccess: 'Subscription successful',
subFailed: 'Subscription failed, please try again later',
selectMethods: 'Please select a method', selectMethods: 'Please select a method',
noAuthRequest: 'No certification requested yet', noAuthRequest: 'No certification requested yet',
noParamRequest: 'No parameter certification yet', noParamRequest: 'No parameter certification yet',
@@ -343,7 +405,7 @@ const cmdb_en = {
tips2: '1. Click to download the template, and users can customize the header of the template file, including model properties and model associations', tips2: '1. Click to download the template, and users can customize the header of the template file, including model properties and model associations',
// eslint-disable-next-line no-template-curly-in-string // eslint-disable-next-line no-template-curly-in-string
tips3: '2. The red color in the template file represents the model relationship, such as the $Product. Product Name (${Model Name}. {Attribute Name}) column, which establishes the relationship with the product.', tips3: '2. The red color in the template file represents the model relationship, such as the $Product. Product Name (${Model Name}. {Attribute Name}) column, which establishes the relationship with the product.',
tips4: '3. In the download template Excel file, the predefined values of attributes will be set as dropdown options. Please note that due to the limitations of Excel itself, a single dropdown box is limited to a maximum of 255 characters. If it exceeds 255 characters, we will not set the dropdown options for this attribute', tips4: `3. The download template excel file will have the property's drop-down list enumeration configured as a drop-down option. Please note that due to the limitations of Excel itself, a single dropdown box is limited to a maximum of 255 characters. If it exceeds 255 characters, we will not set the dropdown options for this attribute`,
tips5: '4. When using Excel templates, please ensure that a single file does not exceed 5000 lines.', tips5: '4. When using Excel templates, please ensure that a single file does not exceed 5000 lines.',
}, },
preference: { preference: {
@@ -363,6 +425,8 @@ const cmdb_en = {
yearsAgo: 'years ago', yearsAgo: 'years ago',
just: 'just now', just: 'just now',
searchPlaceholder: 'Please search CIType', searchPlaceholder: 'Please search CIType',
subCITable: 'Data',
subCITree: 'Tree',
}, },
custom_dashboard: { custom_dashboard: {
charts: 'Chart', charts: 'Chart',
@@ -522,7 +586,7 @@ class AutoDiscovery(object):
""" """
Define attribute fields Define attribute fields
:return: Returns a list of attribute fields. The list items are (name, type, description). The name must be in English. :return: Returns a list of attribute fields. The list items are (name, type, description). The name must be in English.
type: String Integer Float Date DateTime Time JSON type: String Integer Float Date DateTime Time JSON Bool Reference
For example: For example:
return [ return [
("ci_type", "String", "CIType name"), ("ci_type", "String", "CIType name"),
@@ -566,12 +630,19 @@ if __name__ == "__main__":
discoveryCardResoureTip: 'Number of resource types automatically discovered', discoveryCardResoureTip: 'Number of resource types automatically discovered',
addPlugin: 'Add plugin', addPlugin: 'Add plugin',
pluginSearchTip: 'Please search the rules', pluginSearchTip: 'Please search the rules',
innerFlag: 'Inner' innerFlag: 'Inner',
defaultName: 'Default Name',
deleteTip: 'Cannot be deleted again.',
tabCustom: 'Custom',
tabConfig: 'Configured',
addConfig: 'Add Config',
configErrTip: 'Please select config'
}, },
ci: { ci: {
attributeDesc: 'Attribute Description', attributeDesc: 'Attribute Description',
selectRows: 'Select: {rows} items', selectRows: 'Select: {rows} items',
addRelation: 'Add Relation', addRelation: 'Add Relation',
viewRelation: 'View Relation',
all: 'All', all: 'All',
batchUpdate: 'Batch Update', batchUpdate: 'Batch Update',
batchUpdateConfirm: 'Are you sure you want to make batch updates?', batchUpdateConfirm: 'Are you sure you want to make batch updates?',
@@ -594,7 +665,7 @@ if __name__ == "__main__":
tips4: 'At least one field must be selected', tips4: 'At least one field must be selected',
tips5: 'Search name | alias', tips5: 'Search name | alias',
tips6: 'Speed up retrieval, full-text search possible, no need to use conditional filtering\n\n json/link/password currently does not support indexing \n\nText characters longer than 190 cannot be indexed', tips6: 'Speed up retrieval, full-text search possible, no need to use conditional filtering\n\n json/link/password currently does not support indexing \n\nText characters longer than 190 cannot be indexed',
tips7: 'The form of expression is a drop-down box, and the value must be in the predefined value', tips7: 'Whether to configure a select list',
tips8: 'Multiple values, such as intranet IP', tips8: 'Multiple values, such as intranet IP',
tips9: 'For front-end only', tips9: 'For front-end only',
tips10: 'Other attributes of the CIType are computed using expressions\n\nA code snippet computes the returned value.', tips10: 'Other attributes of the CIType are computed using expressions\n\nA code snippet computes the returned value.',
@@ -616,6 +687,7 @@ if __name__ == "__main__":
rollbackingTips: 'Rollbacking', rollbackingTips: 'Rollbacking',
batchRollbacking: 'Deleting {total} items in total, {successNum} items successful, {errorNum} items failed', batchRollbacking: 'Deleting {total} items in total, {successNum} items successful, {errorNum} items failed',
baselineTips: 'Changes at this point in time will also be rollbacked, Unique ID, password and dynamic attributes do not support', baselineTips: 'Changes at this point in time will also be rollbacked, Unique ID, password and dynamic attributes do not support',
cover: 'Cover',
}, },
serviceTree: { serviceTree: {
remove: 'Remove', remove: 'Remove',
@@ -669,5 +741,22 @@ if __name__ == "__main__":
topoViewSearchPlaceholder: 'Please enter the node name.', topoViewSearchPlaceholder: 'Please enter the node name.',
moreBtn: 'Show more({count})' moreBtn: 'Show more({count})'
}, },
relationSearch: {
relationSearch: 'Relation Search',
sourceCIType: 'Source CIType',
sourceCITypeTip: 'Please input or select',
sourceCITYpeInput: 'Please input keywords',
targetCIType: 'Target CIType',
targetCITypeTip: 'Please input or select, multiple choices available',
pathSelect: 'Path Select',
pathSelectTip: 'Please select source CIType and target CIType first',
saveCondition: 'Save Condition',
conditionFilter: 'Condition Filter',
level: 'Level',
returnPath: 'Return Path',
conditionName: 'Condition Name',
path: 'Path',
expandCondition: 'Expand Condition',
}
} }
export default cmdb_en export default cmdb_en

View File

@@ -2,6 +2,8 @@ const cmdb_zh = {
relation: '关系', relation: '关系',
attribute: '属性', attribute: '属性',
configTable: '配置表格', configTable: '配置表格',
enterpriseVersionFlag: '企',
enterpriseVersionTip: '仅限企业版',
menu: { menu: {
views: '视图', views: '视图',
topologyView: '拓扑视图', topologyView: '拓扑视图',
@@ -60,7 +62,7 @@ const cmdb_zh = {
desc: '倒序', desc: '倒序',
uniqueKey: '唯一标识', uniqueKey: '唯一标识',
uniqueKeySelect: '请选择唯一标识', uniqueKeySelect: '请选择唯一标识',
uniqueKeyTips: 'json、密码、计算属性、预定义值属性不能作为唯一标识', uniqueKeyTips: 'json、密码、计算属性、下拉列表属性不能作为唯一标识',
notfound: '找不到想要的?', notfound: '找不到想要的?',
cannotDeleteGroupTips: '该分组下有数据, 不能删除!', cannotDeleteGroupTips: '该分组下有数据, 不能删除!',
confirmDeleteGroup: '确定要删除分组 【{groupName}】 吗?', confirmDeleteGroup: '确定要删除分组 【{groupName}】 吗?',
@@ -115,7 +117,7 @@ const cmdb_zh = {
advancedSettings: '高级设置', advancedSettings: '高级设置',
font: '字体', font: '字体',
color: '颜色', color: '颜色',
choiceValue: '预定义值', choiceValue: '下拉列表',
computedAttribute: '计算属性', computedAttribute: '计算属性',
computedAttributeTips: '该属性的值是通过模型的其它属性构建的表达式或者执行一段代码的方式计算而来,属性的引用方法为: {{ 属性名 }}', computedAttributeTips: '该属性的值是通过模型的其它属性构建的表达式或者执行一段代码的方式计算而来,属性的引用方法为: {{ 属性名 }}',
addAttribute: '新增属性', addAttribute: '新增属性',
@@ -128,6 +130,7 @@ const cmdb_zh = {
selectAttribute: '添加属性', selectAttribute: '添加属性',
groupExisted: '分组名称已存在', groupExisted: '分组名称已存在',
attributeSortedTips: '其他分组中的属性不能进行排序,如需排序请先拖至自定义的分组!', attributeSortedTips: '其他分组中的属性不能进行排序,如需排序请先拖至自定义的分组!',
attributeSortedTips2: '非继承属性不能插入到继承属性前!',
buildinAttribute: '内置字段', buildinAttribute: '内置字段',
expr: '表达式', expr: '表达式',
code: '代码', code: '代码',
@@ -139,7 +142,7 @@ const cmdb_zh = {
selectCIType: '请选择CMDB模型', selectCIType: '请选择CMDB模型',
selectCITypeAttributes: '请选择模型属性', selectCITypeAttributes: '请选择模型属性',
selectAttributes: '请选择属性', selectAttributes: '请选择属性',
choiceScriptDemo: 'class ChoiceValue(object):\n @staticmethod\n def values():\n """\n 执行入口, 返回预定义值\n :return: 返回一个列表, 值的类型同属性的类型\n 例如:\n return ["在线", "下线"]\n """\n return []', choiceScriptDemo: 'class ChoiceValue(object):\n @staticmethod\n def values():\n """\n 执行入口, 返回下拉列表\n :return: 返回一个列表, 值的类型同属性的类型\n 例如:\n return ["在线", "下线"]\n """\n return []',
valueExisted: '当前值已存在!', valueExisted: '当前值已存在!',
addRelation: '新增关系', addRelation: '新增关系',
sourceCIType: '源模型', sourceCIType: '源模型',
@@ -187,6 +190,14 @@ const cmdb_zh = {
confirmDeleteTrigger: '确认删除该触发器吗?', confirmDeleteTrigger: '确认删除该触发器吗?',
int: '整数', int: '整数',
float: '浮点数', float: '浮点数',
longText: '长文本',
shortText: '短文本',
shortTextTip: '文本长度 <= 128',
referenceModel: '引用模型',
referenceModelTip: '请选择引用模型',
referenceModelTip1: '用于快捷查看引用模型实例',
bool: '布尔',
reference: '引用',
text: '文本', text: '文本',
datetime: '日期时间', datetime: '日期时间',
date: '日期', date: '日期',
@@ -204,7 +215,7 @@ const cmdb_zh = {
otherGroupTips: '其他分组属性不可排序', otherGroupTips: '其他分组属性不可排序',
filterTips: '点击可仅查看{name}属性', filterTips: '点击可仅查看{name}属性',
attributeAssociation: '属性关联', attributeAssociation: '属性关联',
attributeAssociationTip1: '通过2个模型的属性值(除密码、json、多值)来自动建立关系', attributeAssociationTip1: '通过2个模型的属性值(除密码、json、多值、长文本、布尔、引用)来自动建立关系',
attributeAssociationTip2: '双击可编辑', attributeAssociationTip2: '双击可编辑',
attributeAssociationTip3: '属性关联必须选择两个属性', attributeAssociationTip3: '属性关联必须选择两个属性',
attributeAssociationTip4: '请选择原模型属性', attributeAssociationTip4: '请选择原模型属性',
@@ -236,7 +247,7 @@ const cmdb_zh = {
checkModalColumn4: '最近检查时间', checkModalColumn4: '最近检查时间',
testModalTitle: '自动发现测试', testModalTitle: '自动发现测试',
attrMapTableAttrPlaceholder: '请编辑名称', attrMapTableAttrPlaceholder: '请编辑名称',
nodeSettingIp: 'ip地址', nodeSettingIp: '网络设备IP地址',
nodeSettingIpTip: '请输入 ip 地址', nodeSettingIpTip: '请输入 ip 地址',
nodeSettingIpTip1: 'ip地址格式错误', nodeSettingIpTip1: 'ip地址格式错误',
nodeSettingCommunity: 'Community', nodeSettingCommunity: 'Community',
@@ -259,6 +270,56 @@ const cmdb_zh = {
account: '账号', account: '账号',
insecure: '是否证书验证', insecure: '是否证书验证',
vcenterName: '虚拟平台名', vcenterName: '虚拟平台名',
resourceSearchTip1: '请使用条件过滤进行CI筛选并将过滤表达式复制粘贴到上一步填写框中。',
resourceSearchTip2: '注1请使用表达式右侧的绿色按钮进行复制',
resourceSearchTip3: '注2如不需要筛选请直接点击灰色按钮进行复制粘贴即可配置为所有节点',
enable: '开启',
enableTip: '确定切换开启状态吗',
portScanConfig: '端口扫描配置',
portScanLabel1: 'CIDR',
portScanLabel2: '端口范围',
portScanLabel3: 'AgentID',
viewAllAttr: '查看所有属性',
attrGroup: '属性分组',
attrName: '属性名称',
attrAlias: '属性别名',
attrCode: '属性代码',
computedAttrTip1: '引用属性遵循jinja2语法',
computedAttrTip2: `多值属性(列表)默认呈现包括[ ], 如果要去掉, 引用方法为: """{{ attr_name | join(',') }}""" 其中逗号为分隔符`,
computedAttrTip3: `不能引用其他计算属性`,
example: '例如',
attrFilterTip: '第三列值可选择本模型的属性,来实现级联属性的功能',
rule: '规则',
cascadeAttr: '级联',
cascadeAttrTip: '级联属性注意顺序',
enumValue: '枚举值',
label: '标签',
valueInputTip: '请输入枚举值',
enumValueTip2: '枚举值不能重复',
builtin: '内置',
department: '部门',
user: '用户',
userGroup: '用户组',
departmentTip: '下拉选择为通用设置公司架构里的所有部门',
userGroupSelectTip: '请选择用户组',
displayValue: '展示值',
displayValueSelectTip: '请选择展示值',
departmentCascadeDisplay: '部门级联显示',
filterUsers: '筛选用户',
enum: '枚举',
ciGrantTip: `筛选条件可使用{{}}引用变量实现动态变化,目前支持用户变量,如{{user.uid}},{{user.username}},{{user.email}},{{user.nickname}}`,
searchInputTip: '请搜索资源关键字',
resourceSearch: '资源搜索',
recentSearch: '最近搜索',
myCollection: '我的收藏',
keyword: '关键字',
CIType: '模型',
filterPopoverLabel: '条件过滤',
conditionFilter: '条件过滤',
advancedFilter: '高级筛选',
saveCondition: '保存条件',
confirmClear: '确认清空?',
currentPage: '当前页'
}, },
components: { components: {
unselectAttributes: '未选属性', unselectAttributes: '未选属性',
@@ -300,7 +361,7 @@ const cmdb_zh = {
pleaseSearch: '请查找', pleaseSearch: '请查找',
conditionFilter: '条件过滤', conditionFilter: '条件过滤',
attributeDesc: '属性说明', attributeDesc: '属性说明',
ciSearchTips: '1. json、密码、链接属性不能搜索\n2. 搜索内容包括逗号, 则需转义\n3. 只搜索索引属性, 非索引属性使用条件过滤', ciSearchTips: '1. json、密码、链接、长文本、引用属性不能搜索\n2. 搜索内容包括逗号, 则需转义\n3. 只搜索索引属性, 非索引属性使用条件过滤',
ciSearchTips2: '例: q=hostname:*0.0.0.0*', ciSearchTips2: '例: q=hostname:*0.0.0.0*',
subCIType: '订阅模型', subCIType: '订阅模型',
already: '已', already: '已',
@@ -308,6 +369,7 @@ const cmdb_zh = {
sub: '订阅', sub: '订阅',
selectBelow: '请在下方进行选择', selectBelow: '请在下方进行选择',
subSuccess: '订阅成功', subSuccess: '订阅成功',
subFailed: '订阅失败,请稍后再试',
selectMethods: '请选择方式', selectMethods: '请选择方式',
noAuthRequest: '暂无请求认证', noAuthRequest: '暂无请求认证',
noParamRequest: '暂无参数认证', noParamRequest: '暂无参数认证',
@@ -342,7 +404,7 @@ const cmdb_zh = {
tips2: '1. 点击下载模板,用户可以自定义模板文件的表头,包括模型属性、模型关联', tips2: '1. 点击下载模板,用户可以自定义模板文件的表头,包括模型属性、模型关联',
// eslint-disable-next-line no-template-curly-in-string // eslint-disable-next-line no-template-curly-in-string
tips3: '2. 模板文件中红色为模型关系,如$产品.产品名(${模型名}.{属性名})这一列就可建立和产品之间的关系', tips3: '2. 模板文件中红色为模型关系,如$产品.产品名(${模型名}.{属性名})这一列就可建立和产品之间的关系',
tips4: '3. 下载模板excel文件中会将属性的预定义值置为下拉选项请注意受excel本身的限制单个下拉框限制了最多255个字符如果超过255个字符我们不会设置该属性的下拉选项', tips4: '3. 下载模板excel文件中会将属性的下拉列表枚举配置置为下拉选项请注意受excel本身的限制单个下拉框限制了最多255个字符如果超过255个字符我们不会设置该属性的下拉选项',
tips5: '4. 在使用excel模板时请确保单个文件不超过5000行', tips5: '4. 在使用excel模板时请确保单个文件不超过5000行',
}, },
preference: { preference: {
@@ -362,6 +424,8 @@ const cmdb_zh = {
yearsAgo: '年前', yearsAgo: '年前',
just: '刚刚', just: '刚刚',
searchPlaceholder: '请搜索模型', searchPlaceholder: '请搜索模型',
subCITable: '数据订阅',
subCITree: '层级订阅',
}, },
custom_dashboard: { custom_dashboard: {
charts: '图表', charts: '图表',
@@ -521,7 +585,7 @@ class AutoDiscovery(object):
""" """
Define attribute fields Define attribute fields
:return: Returns a list of attribute fields. The list items are (name, type, description). The name must be in English. :return: Returns a list of attribute fields. The list items are (name, type, description). The name must be in English.
type: String Integer Float Date DateTime Time JSON type: String Integer Float Date DateTime Time JSON Bool Reference
For example: For example:
return [ return [
("ci_type", "String", "CIType name"), ("ci_type", "String", "CIType name"),
@@ -565,12 +629,19 @@ if __name__ == "__main__":
discoveryCardResoureTip: '自动发现的资源类型数', discoveryCardResoureTip: '自动发现的资源类型数',
addPlugin: '新增插件', addPlugin: '新增插件',
pluginSearchTip: '请搜索规则', pluginSearchTip: '请搜索规则',
innerFlag: '内置' innerFlag: '内置',
defaultName: '默认名称',
deleteTip: '不可再删除',
tabCustom: '自定义',
tabConfig: '已有配置',
addConfig: '添加配置',
configErrTip: '请选择配置'
}, },
ci: { ci: {
attributeDesc: '查看属性配置', attributeDesc: '查看属性配置',
selectRows: '选取:{rows} 项', selectRows: '选取:{rows} 项',
addRelation: '添加关系', addRelation: '添加关系',
viewRelation: '查看关系',
all: '全部', all: '全部',
batchUpdate: '批量修改', batchUpdate: '批量修改',
batchUpdateConfirm: '确认要批量修改吗?', batchUpdateConfirm: '确认要批量修改吗?',
@@ -593,7 +664,7 @@ if __name__ == "__main__":
tips4: '必须至少选择一个字段', tips4: '必须至少选择一个字段',
tips5: '搜索 名称 | 别名', tips5: '搜索 名称 | 别名',
tips6: '加快检索, 可以全文搜索, 无需使用条件过滤\n\n json、链接、密码目前不支持建索引 \n\n文本字符长度超过190不能建索引', tips6: '加快检索, 可以全文搜索, 无需使用条件过滤\n\n json、链接、密码目前不支持建索引 \n\n文本字符长度超过190不能建索引',
tips7: '表现形式是下拉框, 值必须在预定义值里', tips7: '是否配置下拉列表',
tips8: '多值, 比如内网IP', tips8: '多值, 比如内网IP',
tips9: '仅针对前端', tips9: '仅针对前端',
tips10: '模型的其他属性通过表达式的方式计算出来\n\n一个代码片段计算返回的值', tips10: '模型的其他属性通过表达式的方式计算出来\n\n一个代码片段计算返回的值',
@@ -615,6 +686,7 @@ if __name__ == "__main__":
rollbackingTips: '正在批量回滚中', rollbackingTips: '正在批量回滚中',
batchRollbacking: '正在回滚,共{total}个,成功{successNum}个,失败{errorNum}个', batchRollbacking: '正在回滚,共{total}个,成功{successNum}个,失败{errorNum}个',
baselineTips: '该时间点的变更也会被回滚, 唯一标识、密码属性、动态属性不支持回滚', baselineTips: '该时间点的变更也会被回滚, 唯一标识、密码属性、动态属性不支持回滚',
cover: '覆盖',
}, },
serviceTree: { serviceTree: {
remove: '移除', remove: '移除',
@@ -668,5 +740,22 @@ if __name__ == "__main__":
topoViewSearchPlaceholder: '请输入节点名字', topoViewSearchPlaceholder: '请输入节点名字',
moreBtn: '展示更多({count})' moreBtn: '展示更多({count})'
}, },
relationSearch: {
relationSearch: '关系搜索',
sourceCIType: '源模型',
sourceCITypeTip: '请输入或选择',
sourceCITYpeInput: '请输入关键词',
targetCIType: '目标模型',
targetCITypeTip: '请输入或选择,可多选',
pathSelect: '路径选择',
pathSelectTip: '请先选择源模型和目标模型',
saveCondition: '保存条件',
conditionFilter: '条件过滤',
level: '层级',
returnPath: '返回路径',
conditionName: '条件命名',
path: '路径',
expandCondition: '展开条件',
}
} }
export default cmdb_zh export default cmdb_zh

View File

@@ -53,8 +53,9 @@ const genCmdbRoutes = async () => {
{ {
path: '/cmdb/resourcesearch', path: '/cmdb/resourcesearch',
name: 'cmdb_resource_search', name: 'cmdb_resource_search',
hidden: true,
meta: { title: 'cmdb.menu.ciSearch', icon: 'ops-cmdb-search', selectedIcon: 'ops-cmdb-search', keepAlive: false }, meta: { title: 'cmdb.menu.ciSearch', icon: 'ops-cmdb-search', selectedIcon: 'ops-cmdb-search', keepAlive: false },
component: () => import('../views/resource_search/index.vue') component: () => import('../views/resource_search_2/index.vue')
}, },
{ {
path: '/cmdb/adc', path: '/cmdb/adc',
@@ -102,7 +103,7 @@ const genCmdbRoutes = async () => {
name: 'cmdb_ci_type', name: 'cmdb_ci_type',
component: RouteView, component: RouteView,
redirect: '/cmdb/ci_type', redirect: '/cmdb/ci_type',
meta: { title: 'cmdb.menu.backendManage', icon: 'setting', permission: ['cmdb_admin', 'OneOPS_Application_Admin', 'admin'], }, meta: { title: 'cmdb.menu.backendManage', icon: 'veops-setting2', selectedIcon: 'veops-setting2', permission: ['cmdb_admin', 'OneOPS_Application_Admin', 'admin'], },
children: [ children: [
{ {
path: '/cmdb/customdashboard', path: '/cmdb/customdashboard',
@@ -117,11 +118,10 @@ const genCmdbRoutes = async () => {
meta: { title: 'cmdb.menu.serviceTreeDefine', keepAlive: false, icon: 'ops-cmdb-preferencerelation', selectedIcon: 'ops-cmdb-preferencerelation-selected' } meta: { title: 'cmdb.menu.serviceTreeDefine', keepAlive: false, icon: 'ops-cmdb-preferencerelation', selectedIcon: 'ops-cmdb-preferencerelation-selected' }
}, },
{ {
path: '/cmdb/modelrelation', path: '/cmdb/discovery',
name: 'model_relation', name: 'discovery',
hideChildrenInMenu: true, component: () => import('../views/discovery/index'),
component: () => import('../views/model_relation/index'), meta: { title: 'cmdb.menu.ad', keepAlive: false, icon: 'ops-cmdb-adr', selectedIcon: 'ops-cmdb-adr-selected' }
meta: { title: 'cmdb.menu.citypeRelation', keepAlive: false, icon: 'ops-cmdb-modelrelation', selectedIcon: 'ops-cmdb-modelrelation-selected' }
}, },
{ {
path: '/cmdb/operationhistory', path: '/cmdb/operationhistory',
@@ -130,19 +130,20 @@ const genCmdbRoutes = async () => {
component: () => import('../views/operation_history/index'), component: () => import('../views/operation_history/index'),
meta: { title: 'cmdb.menu.operationHistory', keepAlive: false, icon: 'ops-cmdb-operation', selectedIcon: 'ops-cmdb-operation-selected' } meta: { title: 'cmdb.menu.operationHistory', keepAlive: false, icon: 'ops-cmdb-operation', selectedIcon: 'ops-cmdb-operation-selected' }
}, },
{
path: '/cmdb/modelrelation',
name: 'model_relation',
hideChildrenInMenu: true,
component: () => import('../views/model_relation/index'),
meta: { title: 'cmdb.menu.citypeRelation', keepAlive: false, icon: 'ops-cmdb-modelrelation', selectedIcon: 'ops-cmdb-modelrelation-selected' }
},
{ {
path: '/cmdb/relationtype', path: '/cmdb/relationtype',
name: 'relation_type', name: 'relation_type',
hideChildrenInMenu: true, hideChildrenInMenu: true,
component: () => import('../views/relation_type/index'), component: () => import('../views/relation_type/index'),
meta: { title: 'cmdb.menu.relationType', keepAlive: false, icon: 'ops-cmdb-relationtype', selectedIcon: 'ops-cmdb-relationtype-selected' } meta: { title: 'cmdb.menu.relationType', keepAlive: false, icon: 'ops-cmdb-relationtype', selectedIcon: 'ops-cmdb-relationtype-selected' }
}, }
{
path: '/cmdb/discovery',
name: 'discovery',
component: () => import('../views/discovery/index'),
meta: { title: 'cmdb.menu.ad', keepAlive: false, icon: 'ops-cmdb-adr', selectedIcon: 'ops-cmdb-adr-selected' }
},
] ]
} }
] ]

View File

@@ -4,13 +4,16 @@ export const valueTypeMap = () => {
return { return {
'0': i18n.t('cmdb.ciType.int'), '0': i18n.t('cmdb.ciType.int'),
'1': i18n.t('cmdb.ciType.float'), '1': i18n.t('cmdb.ciType.float'),
'2': i18n.t('cmdb.ciType.text'), '2': i18n.t('cmdb.ciType.shortText'),
'3': i18n.t('cmdb.ciType.datetime'), '3': i18n.t('cmdb.ciType.datetime'),
'4': i18n.t('cmdb.ciType.date'), '4': i18n.t('cmdb.ciType.date'),
'5': i18n.t('cmdb.ciType.time'), '5': i18n.t('cmdb.ciType.time'),
'6': 'JSON', '6': 'JSON',
'7': i18n.t('cmdb.ciType.password'), '7': i18n.t('cmdb.ciType.password'),
'8': i18n.t('cmdb.ciType.link') '8': i18n.t('cmdb.ciType.link'),
'9': i18n.t('cmdb.ciType.longText'),
'10': i18n.t('cmdb.ciType.bool'),
'11': i18n.t('cmdb.ciType.reference'),
} }
} }

View File

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

View File

@@ -74,202 +74,24 @@
</div> </div>
</SearchForm> </SearchForm>
<CiDetailDrawer ref="detail" :typeId="typeId" /> <CiDetailDrawer ref="detail" :typeId="typeId" />
<ops-table
:id="`cmdb-ci-${typeId}`" <CITable
border
keep-source
show-overflow
resizable
ref="xTable" ref="xTable"
size="small" :id="`cmdb-ci-${typeId}`"
:row-config="{ useKey: true, keyField: '_id' }" :loading="loading"
:height="tableHeight" :attrList="preferenceAttrList"
show-header-overflow :columns="columns"
highlight-hover-row :passwordValue="passwordValue"
:data="instanceList" :data="instanceList"
@checkbox-change="onSelectChange" :height="tableHeight"
@checkbox-all="onSelectChange" @onSelectChange="onSelectChange"
@checkbox-range-end="onSelectRangeEnd"
:checkbox-config="{ reserve: true, highlight: true, range: true }"
@edit-closed="handleEditClose" @edit-closed="handleEditClose"
@edit-actived="handleEditActived" @edit-actived="handleEditActived"
:edit-config="{ trigger: 'dblclick', mode: 'row', showIcon: false }"
:sort-config="{ remote: true, trigger: 'cell' }"
@sort-change="handleSortCol" @sort-change="handleSortCol"
:row-key="true" @openDetail="openDetail"
:column-key="true" @deleteCI="deleteCI"
:cell-style="getCellStyle" />
:scroll-y="{ enabled: true, gt: 20 }"
:scroll-x="{ enabled: true, gt: 0 }"
class="ops-unstripe-table checkbox-hover-table"
:custom-config="{ storage: true }"
>
<vxe-column align="center" type="checkbox" width="60" :fixed="isCheckboxFixed ? 'left' : ''">
<template #default="{row}">
{{ getRowSeq(row) }}
</template>
</vxe-column>
<vxe-table-column
v-for="(col, index) in columns"
:key="`${col.field}_${index}`"
:title="col.title"
:field="col.field"
:width="col.width"
:sortable="col.sortable"
:edit-render="getColumnsEditRender(col)"
:cell-type="col.value_type === '2' ? 'string' : 'auto'"
:fixed="col.is_fixed ? 'left' : ''"
>
<template #header>
<span class="vxe-handle">
<OpsMoveIcon
style="width: 17px; height: 17px; display: none; position: absolute; left: -3px; top: 12px"
/>
<span>{{ col.title }}</span>
</span>
</template>
<template v-if="col.is_choice || col.is_password" #edit="{ row }">
<vxe-input v-if="col.is_password" v-model="passwordValue[col.field]" />
<a-select
:getPopupContainer="(trigger) => trigger.parentElement"
:style="{ width: '100%', height: '32px' }"
v-model="row[col.field]"
:v-bind="$t('placeholder2')"
v-if="col.is_choice"
:showArrow="false"
:mode="col.is_list ? 'multiple' : 'default'"
class="ci-table-edit-select"
allowClear
>
<a-select-option
:value="choice[0]"
:key="'edit_' + col.field + idx"
v-for="(choice, idx) in col.filters"
>
<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>
</template>
<template
v-if="col.value_type === '6' || col.is_link || col.is_password || col.is_choice"
#default="{ row }"
>
<span v-if="col.value_type === '6' && row[col.field]">{{ row[col.field] }}</span>
<template v-else-if="col.is_link && row[col.field]">
<a
v-for="(item, linkIndex) in (col.is_list ? row[col.field] : [row[col.field]])"
:key="linkIndex"
:href="
item.startsWith('http') || item.startsWith('https')
? `${item}`
: `http://${item}`
"
target="_blank"
>
{{ item }}
</a>
</template>
<PasswordField
v-else-if="col.is_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">
<span
v-for="value in row[col.field]"
:key="value"
:style="{
borderRadius: '4px',
padding: '1px 5px',
margin: '2px',
...getChoiceValueStyle(col, value),
display: 'inline-flex',
alignItems: 'center',
}"
>
<img
v-if="getChoiceValueIcon(col, value).id && getChoiceValueIcon(col, value).url"
:src="`/api/common-setting/v1/file/${getChoiceValueIcon(col, value).url}`"
:style="{ maxHeight: '13px', maxWidth: '13px', marginRight: '5px' }"
/>
<ops-icon
v-else
:style="{ color: getChoiceValueIcon(col, value).color, marginRight: '5px' }"
:type="getChoiceValueIcon(col, value).name"
/>{{ value }}
</span>
</template>
<span
v-else
:style="{
borderRadius: '4px',
padding: '1px 5px',
margin: '2px 0',
...getChoiceValueStyle(col, row[col.field]),
display: 'inline-flex',
alignItems: 'center',
}"
>
<img
v-if="getChoiceValueIcon(col, row[col.field]).id && getChoiceValueIcon(col, row[col.field]).url"
:src="`/api/common-setting/v1/file/${getChoiceValueIcon(col, row[col.field]).url}`"
:style="{ maxHeight: '13px', maxWidth: '13px', marginRight: '5px' }"
/>
<ops-icon
v-else
:style="{ color: getChoiceValueIcon(col, row[col.field]).color, marginRight: '5px' }"
:type="getChoiceValueIcon(col, row[col.field]).name"
/>
{{ row[col.field] }}
</span>
</template>
</template>
</vxe-table-column>
<vxe-column align="left" field="operate" fixed="right" width="80">
<template #header>
<span>{{ $t('operation') }}</span>
</template>
<template #default="{ row }">
<a-space>
<a @click="$refs.detail.create(row.ci_id || row._id)">
<a-icon type="unordered-list" />
</a>
<a-tooltip :title="$t('cmdb.ci.addRelation')">
<a @click="$refs.detail.create(row.ci_id || row._id, 'tab_2', '2')">
<a-icon type="retweet" />
</a>
</a-tooltip>
<a @click="deleteCI(row)" :style="{ color: 'red' }">
<a-icon type="delete" />
</a>
</a-space>
</template>
</vxe-column>
<template #empty>
<div v-if="loading" style="height: 200px; line-height: 200px">{{ $t('loading') }}</div>
<div v-else>
<img :style="{ width: '200px' }" :src="require('@/assets/data_empty.png')" />
<div>{{ $t('noData') }}</div>
</div>
</template>
</ops-table>
<div :style="{ textAlign: 'right', marginTop: '4px' }"> <div :style="{ textAlign: 'right', marginTop: '4px' }">
<a-pagination <a-pagination
:showSizeChanger="true" :showSizeChanger="true"
@@ -301,7 +123,6 @@
</a-pagination> </a-pagination>
</div> </div>
<create-instance-form ref="create" @reload="reloadData" @submit="batchUpdate" /> <create-instance-form ref="create" @reload="reloadData" @submit="batchUpdate" />
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
<BatchDownload ref="batchDownload" @batchDownload="batchDownload" /> <BatchDownload ref="batchDownload" @batchDownload="batchDownload" />
<ci-rollback-form ref="ciRollbackForm" @batchRollbackAsync="batchRollbackAsync($event)" :ciIds="selectedRowKeys" /> <ci-rollback-form ref="ciRollbackForm" @batchRollbackAsync="batchRollbackAsync($event)" :ciIds="selectedRowKeys" />
<MetadataDrawer ref="metadataDrawer" /> <MetadataDrawer ref="metadataDrawer" />
@@ -321,7 +142,6 @@ import SearchForm from '../../components/searchForm/SearchForm.vue'
import CreateInstanceForm from './modules/CreateInstanceForm' import CreateInstanceForm from './modules/CreateInstanceForm'
import CiDetailDrawer from './modules/ciDetailDrawer.vue' import CiDetailDrawer from './modules/ciDetailDrawer.vue'
import EditAttrsPopover from './modules/editAttrsPopover' import EditAttrsPopover from './modules/editAttrsPopover'
import JsonEditor from '../../components/JsonEditor/jsonEditor.vue'
import { searchCI, updateCI, deleteCI } from '@/modules/cmdb/api/ci' import { searchCI, updateCI, deleteCI } from '@/modules/cmdb/api/ci'
import { getSubscribeAttributes, subscribeCIType, subscribeTreeView } from '@/modules/cmdb/api/preference' import { getSubscribeAttributes, subscribeCIType, subscribeTreeView } from '@/modules/cmdb/api/preference'
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr' import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
@@ -329,15 +149,14 @@ import { roleHasPermissionToGrant } from '@/modules/acl/api/permission'
import { searchResourceType } from '@/modules/acl/api/resource' import { searchResourceType } from '@/modules/acl/api/resource'
import { getCITableColumns } from '../../utils/helper' import { getCITableColumns } from '../../utils/helper'
import { intersection } from '@/utils/functions/set' import { intersection } from '@/utils/functions/set'
import PasswordField from '../../components/passwordField/index.vue'
import BatchDownload from '../../components/batchDownload/batchDownload.vue' import BatchDownload from '../../components/batchDownload/batchDownload.vue'
import PreferenceSearch from '../../components/preferenceSearch/preferenceSearch.vue' import PreferenceSearch from '../../components/preferenceSearch/preferenceSearch.vue'
import MetadataDrawer from './modules/MetadataDrawer.vue' import MetadataDrawer from './modules/MetadataDrawer.vue'
import CMDBGrant from '../../components/cmdbGrant' import CMDBGrant from '../../components/cmdbGrant'
import { ops_move_icon as OpsMoveIcon } from '@/core/icons'
import { getAttrPassword } from '../../api/CITypeAttr' import { getAttrPassword } from '../../api/CITypeAttr'
import CiRollbackForm from './modules/ciRollbackForm.vue' import CiRollbackForm from './modules/ciRollbackForm.vue'
import { CIBaselineRollback } from '../../api/history' import { CIBaselineRollback } from '../../api/history'
import CITable from '@/modules/cmdb/components/ciTable/index.vue'
export default { export default {
name: 'InstanceList', name: 'InstanceList',
@@ -345,24 +164,18 @@ export default {
SearchForm, SearchForm,
CreateInstanceForm, CreateInstanceForm,
CiDetailDrawer, CiDetailDrawer,
JsonEditor,
PasswordField,
EditAttrsPopover, EditAttrsPopover,
BatchDownload, BatchDownload,
PreferenceSearch, PreferenceSearch,
MetadataDrawer, MetadataDrawer,
CMDBGrant, CMDBGrant,
OpsMoveIcon,
CiRollbackForm, CiRollbackForm,
CITable
}, },
computed: { computed: {
windowHeight() { windowHeight() {
return this.$store.state.windowHeight return this.$store.state.windowHeight
}, },
isCheckboxFixed() {
const idx = this.columns.findIndex((item) => item.is_fixed)
return idx > -1
},
tableHeight() { tableHeight() {
// if (this.selectedRowKeys && this.selectedRowKeys.length) { // if (this.selectedRowKeys && this.selectedRowKeys.length) {
// return this.windowHeight - 246 // return this.windowHeight - 246
@@ -548,12 +361,7 @@ export default {
const subscribed = await getSubscribeAttributes(this.typeId) const subscribed = await getSubscribeAttributes(this.typeId)
this.preferenceAttrList = subscribed.attributes // All columns that have been subscribed this.preferenceAttrList = subscribed.attributes // All columns that have been subscribed
}, },
onSelectChange() { onSelectChange(records) {
const xTable = this.$refs.xTable.getVxetableRef()
const records = [...xTable.getCheckboxRecords(), ...xTable.getCheckboxReserveRecords()]
this.selectedRowKeys = records.map((i) => i.ci_id || i._id)
},
onSelectRangeEnd({ records }) {
this.selectedRowKeys = records.map((i) => i.ci_id || i._id) this.selectedRowKeys = records.map((i) => i.ci_id || i._id)
}, },
reloadData() { reloadData() {
@@ -609,7 +417,7 @@ export default {
async openBatchDownload() { async openBatchDownload() {
this.$refs.batchDownload.open({ this.$refs.batchDownload.open({
preferenceAttrList: this.preferenceAttrList, preferenceAttrList: this.preferenceAttrList.filter((attr) => !attr?.is_reference),
ciTypeName: this.$route.meta.title || this.$route.meta.name, ciTypeName: this.$route.meta.title || this.$route.meta.name,
}) })
}, },
@@ -657,13 +465,12 @@ export default {
this.loadTip = this.$t('cmdb.ci.batchUpdateInProgress') + '...' this.loadTip = this.$t('cmdb.ci.batchUpdateInProgress') + '...'
const payload = {} const payload = {}
Object.keys(values).forEach((key) => { Object.keys(values).forEach((key) => {
if (values[key] || values[key] === 0) {
payload[key] = values[key]
}
// Field values support blanking // Field values support blanking
// There are currently field values that do not support blanking and will be returned by the backend. // There are currently field values that do not support blanking and will be returned by the backend.
if (values[key] === undefined || values[key] === null) { if (values[key] === undefined || values[key] === null) {
payload[key] = null payload[key] = null
} else {
payload[key] = values[key]
} }
}) })
this.$refs.create.visible = false this.$refs.create.visible = false
@@ -817,28 +624,6 @@ export default {
await this.loadPreferenceAttrList() await this.loadPreferenceAttrList()
await this.loadTableData() await this.loadTableData()
}, },
getColumnsEditRender(col) {
const _editRender = {
...col.editRender,
}
if (col.value_type === '6') {
_editRender.events = { focus: this.handleFocusJson }
}
return _editRender
},
handleFocusJson({ column, row }) {
this.$refs.jsonEditor.open(column, row)
},
jsonEditorOk(row, column, jsonData) {
// The backend writes data at different speeds. You can modify the table data directly without pulling the interface.
// this.reloadData()
this.instanceList.forEach((item) => {
if (item._id === row._id) {
item[column.property] = JSON.stringify(jsonData)
}
})
this.$refs.xTable.getVxetableRef().refreshColumn()
},
onShowSizeChange(current, pageSize) { onShowSizeChange(current, pageSize) {
this.pageSize = pageSize this.pageSize = pageSize
if (this.currentPage === 1) { if (this.currentPage === 1) {
@@ -900,23 +685,6 @@ export default {
) )
}) })
}, },
// tableFilterChangeEvent({ column, property, values, datas, filterList, $event }) {
// console.log(111)
// },
getChoiceValueStyle(col, colValue) {
const _find = col.filters.find((item) => String(item[0]) === String(colValue))
if (_find) {
return _find[1]?.style || {}
}
return {}
},
getChoiceValueIcon(col, colValue) {
const _find = col.filters.find((item) => String(item[0]) === String(colValue))
if (_find) {
return _find[1]?.icon || {}
}
return {}
},
handleEditActived() { handleEditActived() {
this.isEditActive = true this.isEditActive = true
const passwordCol = this.columns.filter((col) => col.is_password) const passwordCol = this.columns.filter((col) => col.is_password)
@@ -942,19 +710,6 @@ export default {
this.lastEditCiId = row._id this.lastEditCiId = row._id
}) })
}, },
getCellStyle({ row, rowIndex, $rowIndex, column, columnIndex, $columnIndex }) {
const { property } = column
const _find = this.preferenceAttrList.find((attr) => attr.name === property)
if (
_find &&
_find.option &&
_find.option.fontOptions &&
row[`${property}`] !== undefined &&
row[`${property}`] !== null
) {
return { ..._find.option.fontOptions }
}
},
getQAndSort() { getQAndSort() {
const fuzzySearch = this.$refs['search'].fuzzySearch || '' const fuzzySearch = this.$refs['search'].fuzzySearch || ''
const expression = this.$refs['search'].expression || '' const expression = this.$refs['search'].expression || ''
@@ -1052,8 +807,8 @@ export default {
this.visible = false this.visible = false
} }
}, },
getRowSeq(row) { openDetail(id, activeTabKey, ciDetailRelationKey) {
return this.$refs.xTable.getVxetableRef().getRowSeq(row) this.$refs.detail.create(id, activeTabKey, ciDetailRelationKey)
} }
}, },
} }
@@ -1072,33 +827,4 @@ export default {
overflow: auto; overflow: auto;
margin-bottom: -24px; margin-bottom: -24px;
} }
.checkbox-hover-table {
/deep/ .vxe-table--body-wrapper {
.vxe-checkbox--label {
display: inline;
padding-left: 0px !important;
color: #bfbfbf;
}
.vxe-icon-checkbox-unchecked {
display: none;
}
.vxe-icon-checkbox-checked ~ .vxe-checkbox--label {
display: none;
}
.vxe-cell--checkbox {
&:hover {
.vxe-icon-checkbox-unchecked {
display: inline;
}
.vxe-checkbox--label {
display: none;
}
}
}
}
}
</style> </style>

View File

@@ -1,431 +1,547 @@
<template> <template>
<CustomDrawer <CustomDrawer
:title="title + CIType.alias" :title="title + CIType.alias"
width="800" width="800"
@close="handleClose" @close="handleClose"
:maskClosable="false" :maskClosable="false"
:visible="visible" :visible="visible"
wrapClassName="create-instance-form" wrapClassName="create-instance-form"
:bodyStyle="{ paddingTop: 0 }" :bodyStyle="{ paddingTop: 0 }"
:headerStyle="{ borderBottom: 'none' }" :headerStyle="{ borderBottom: 'none' }"
> >
<div class="custom-drawer-bottom-action"> <div class="custom-drawer-bottom-action">
<a-button @click="handleClose">{{ $t('cancel') }}</a-button> <a-button @click="handleClose">{{ $t('cancel') }}</a-button>
<a-button type="primary" @click="createInstance">{{ $t('submit') }}</a-button> <a-button type="primary" @click="createInstance">{{ $t('submit') }}</a-button>
</div> </div>
<template v-if="action === 'create'"> <template v-if="action === 'create'">
<template v-for="group in attributesByGroup"> <template v-for="group in attributesByGroup">
<CreateInstanceFormByGroup <CreateInstanceFormByGroup
:ref="`createInstanceFormByGroup_${group.id}`" :ref="`createInstanceFormByGroup_${group.id}`"
:key="group.id || group.name" :key="group.id || group.name"
:group="group" :group="group"
@handleFocusInput="handleFocusInput" :attributeList="attributeList"
:attributeList="attributeList" @handleFocusInput="handleFocusInput"
/> />
</template> </template>
<template v-if="parentsType && parentsType.length"> <template v-if="parentsType && parentsType.length">
<a-divider style="font-size:14px;margin:14px 0;font-weight:700;">{{ <a-divider style="font-size:14px;margin:14px 0;font-weight:700;">{{
$t('cmdb.menu.citypeRelation') $t('cmdb.menu.citypeRelation')
}}</a-divider> }}</a-divider>
<a-form> <a-form>
<a-row :gutter="24" align="top" type="flex"> <a-row :gutter="24" align="top" type="flex">
<a-col :span="12" v-for="item in parentsType" :key="item.id"> <a-col :span="12" v-for="item in parentsType" :key="item.id">
<a-form-item :label="item.alias || item.name" :colon="false"> <a-form-item :label="item.alias || item.name" :colon="false">
<a-input-group compact style="width: 100%"> <a-input-group compact style="width: 100%">
<a-select v-model="parentsForm[item.name].attr"> <a-select v-model="parentsForm[item.name].attr">
<a-select-option <a-select-option
:title="attr.alias || attr.name" :title="attr.alias || attr.name"
v-for="attr in item.attributes" v-for="attr in filterAttributes(item.attributes)"
:key="attr.name" :key="attr.name"
:value="attr.name" :value="attr.name"
> >
{{ attr.alias || attr.name }} {{ attr.alias || attr.name }}
</a-select-option> </a-select-option>
</a-select> </a-select>
<a-input <a-input
:placeholder="$t('cmdb.ci.tips1')" :placeholder="$t('cmdb.ci.tips1')"
v-model="parentsForm[item.name].value" v-model="parentsForm[item.name].value"
style="width: 50%" style="width: 50%"
/> />
</a-input-group> </a-input-group>
</a-form-item> </a-form-item>
</a-col> </a-col>
</a-row> </a-row>
</a-form> </a-form>
</template> </template>
</template> </template>
<template v-if="action === 'update'"> <template v-if="action === 'update'">
<a-form :form="form"> <a-form :form="form">
<p>{{ $t('cmdb.ci.tips2') }}</p> <p>{{ $t('cmdb.ci.tips2') }}</p>
<a-row :gutter="24" v-for="list in batchUpdateLists" :key="list.name"> <a-row :gutter="8" v-for="list in batchUpdateLists" :key="list.name">
<a-col :span="11"> <a-col :span="6">
<a-form-item> <a-form-item>
<el-select showSearch size="small" filterable v-model="list.name" :placeholder="$t('cmdb.ci.tips3')"> <el-select showSearch size="small" filterable v-model="list.name" :placeholder="$t('cmdb.ci.tips3')">
<el-option <el-option
v-for="attr in attributeList" v-for="attr in attributeList"
:key="attr.name" :key="attr.name"
:value="attr.name" :value="attr.name"
:disabled="batchUpdateLists.findIndex((item) => item.name === attr.name) > -1" :disabled="batchUpdateLists.findIndex((item) => item.name === attr.name) > -1"
:label="attr.alias || attr.name" :label="attr.alias || attr.name"
> >
</el-option> </el-option>
</el-select> </el-select>
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="11"> <a-col v-if="showListOperation(list.name)" :span="3">
<a-form-item> <a-form-item>
<a-select <el-select size="small" filterable v-model="list.operation" :placeholder="$t('placeholder2')">
:style="{ width: '100%' }" <el-option
v-decorator="[list.name, { rules: [{ required: false }] }]" v-for="(option) in listOperationOptions"
:placeholder="$t('placeholder2')" :key="option.value"
v-if="getFieldType(list.name).split('%%')[0] === 'select'" :value="option.value"
:mode="getFieldType(list.name).split('%%')[1] === 'multiple' ? 'multiple' : 'default'" :label="$t(option.label)"
showSearch >
allowClear </el-option>
> </el-select>
<a-select-option </a-form-item>
:value="choice[0]" </a-col>
:key="'New_' + choice + choice_idx" <a-col :span="showListOperation(list.name) ? 10 : 13">
v-for="(choice, choice_idx) in getSelectFieldOptions(list.name)" <a-form-item>
> <CIReferenceAttr
<span :style="choice[1] ? choice[1].style || {} : {}"> v-if="getAttr(list.name).is_reference"
<ops-icon :referenceTypeId="getAttr(list.name).reference_type_id"
:style="{ color: choice[1].icon.color }" :isList="getAttr(list.name).is_list"
v-if="choice[1] && choice[1].icon && choice[1].icon.name" v-decorator="[
:type="choice[1].icon.name" list.name,
/> {
{{ choice[0] }} initialValue: getAttr(list.name).is_list ? [] : ''
</span> }
</a-select-option> ]"
</a-select> />
<a-input-number <a-switch
v-decorator="[list.name, { rules: [{ required: false }] }]" v-else-if="getAttr(list.name).is_bool"
style="width: 100%" v-decorator="[
v-if="getFieldType(list.name) === 'input_number'" list.name,
/> {
<a-date-picker valuePropName: 'checked',
v-decorator="[list.name, { rules: [{ required: false }] }]" initialValue: false
style="width: 100%" }
:format="getFieldType(list.name) == '4' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'" ]"
:valueFormat="getFieldType(list.name) == '4' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'" />
v-if="getFieldType(list.name) === '4' || getFieldType(list.name) === '3'" <a-select
:showTime="getFieldType(list.name) === '4' ? false : { format: 'HH:mm:ss' }" :style="{ width: '100%' }"
/> v-decorator="[list.name, { rules: getDecoratorRules(list) }]"
<a-input :placeholder="$t('placeholder2')"
v-if="getFieldType(list.name) === 'input'" v-else-if="getFieldType(list.name).split('%%')[0] === 'select'"
@focus="(e) => handleFocusInput(e, list)" :mode="getFieldType(list.name).split('%%')[1] === 'multiple' ? 'multiple' : 'default'"
v-decorator="[list.name, { rules: [{ required: false }] }]" showSearch
/> allowClear
</a-form-item> >
</a-col> <a-select-option
<a-col :span="2"> :value="choice[0]"
<a-form-item> :key="'New_' + choice + choice_idx"
<a :style="{ color: 'red', marginTop: '2px' }" @click="handleDelete(list.name)"> v-for="(choice, choice_idx) in getSelectFieldOptions(list.name)"
<a-icon type="delete" /> >
</a> <span :style="choice[1] ? choice[1].style || {} : {}">
</a-form-item> <ops-icon
</a-col> :style="{ color: choice[1].icon.color }"
</a-row> v-if="choice[1] && choice[1].icon && choice[1].icon.name"
<a-button type="primary" ghost icon="plus" @click="handleAdd">{{ $t('cmdb.ci.newUpdateField') }}</a-button> :type="choice[1].icon.name"
</a-form> />
</template> <a-tooltip placement="topLeft" :title="choice[1] ? choice[1].label || choice[0] : choice[0]" >
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" /> {{ choice[1] ? choice[1].label || choice[0] : choice[0] }}
</CustomDrawer> </a-tooltip>
</template> </span>
</a-select-option>
<script> </a-select>
import _ from 'lodash' <a-input-number
import moment from 'moment' v-decorator="[list.name, { rules: getDecoratorRules(list) }]"
import { Select, Option } from 'element-ui' style="width: 100%"
import { getCIType, getCITypeGroupById } from '@/modules/cmdb/api/CIType' v-else-if="getFieldType(list.name) === 'input_number'"
import { addCI } from '@/modules/cmdb/api/ci' />
import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue' <a-date-picker
import { valueTypeMap } from '../../../utils/const' v-decorator="[list.name, { rules: getDecoratorRules(list) }]"
import CreateInstanceFormByGroup from './createInstanceFormByGroup.vue' style="width: 100%"
import { getCITypeParent, getCanEditByParentIdChildId } from '@/modules/cmdb/api/CITypeRelation' :format="getFieldType(list.name) == '4' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
:valueFormat="getFieldType(list.name) == '4' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
export default { v-else-if="getFieldType(list.name) === '4' || getFieldType(list.name) === '3'"
name: 'CreateInstanceForm', :showTime="getFieldType(list.name) === '4' ? false : { format: 'HH:mm:ss' }"
components: { />
ElSelect: Select, <a-input
ElOption: Option, v-else-if="getFieldType(list.name) === 'input'"
JsonEditor, @focus="(e) => handleFocusInput(e, list)"
CreateInstanceFormByGroup, v-decorator="[list.name, { rules: getDecoratorRules(list) }]"
}, />
props: { </a-form-item>
typeIdFromRelation: { </a-col>
type: Number, <a-col :span="2">
default: 0, <a-form-item>
}, <a :style="{ color: 'red', marginTop: '2px' }" @click="handleDelete(list.name)">
}, <a-icon type="delete" />
data() { </a>
return { </a-form-item>
action: '', </a-col>
form: this.$form.createForm(this), </a-row>
visible: false, <a-button type="primary" ghost icon="plus" @click="handleAdd">{{ $t('cmdb.ci.newUpdateField') }}</a-button>
attributeList: [], </a-form>
</template>
CIType: {}, <JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
</CustomDrawer>
batchUpdateLists: [], </template>
editAttr: null,
attributesByGroup: [], <script>
parentsType: [], import _ from 'lodash'
parentsForm: {}, import moment from 'moment'
canEdit: {}, import { Select, Option } from 'element-ui'
} import { getCIType, getCITypeGroupById } from '@/modules/cmdb/api/CIType'
}, import { addCI } from '@/modules/cmdb/api/ci'
computed: { import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue'
title() { import { valueTypeMap } from '../../../utils/const'
return this.action === 'create' ? this.$t('create') + ' ' : this.$t('cmdb.ci.batchUpdate') + ' ' import CreateInstanceFormByGroup from './createInstanceFormByGroup.vue'
}, import { getCITypeParent, getCanEditByParentIdChildId } from '@/modules/cmdb/api/CITypeRelation'
typeId() { import CIReferenceAttr from '@/components/ciReferenceAttr/index.vue'
if (this.typeIdFromRelation) {
return this.typeIdFromRelation export default {
} name: 'CreateInstanceForm',
return this.$router.currentRoute.meta.typeId components: {
}, ElSelect: Select,
valueTypeMap() { ElOption: Option,
return valueTypeMap() JsonEditor,
}, CreateInstanceFormByGroup,
}, CIReferenceAttr
provide() { },
return { props: {
getFieldType: this.getFieldType, typeIdFromRelation: {
} type: Number,
}, default: 0,
inject: ['attrList'], },
methods: { },
moment, data() {
async getCIType() { return {
await getCIType(this.typeId).then((res) => { action: '',
this.CIType = res.ci_types[0] form: this.$form.createForm(this),
}) visible: false,
}, attributeList: [],
async getAttributeList() {
const _attrList = this.attrList() CIType: {},
this.attributeList = _attrList.sort((x, y) => y.is_required - x.is_required)
await getCITypeGroupById(this.typeId).then((res1) => { batchUpdateLists: [],
const _attributesByGroup = res1.map((g) => { editAttr: null,
g.attributes = g.attributes.filter((attr) => !attr.is_computed) attributesByGroup: [],
return g parentsType: [],
}) parentsForm: {},
const attrHasGroupIds = [] canEdit: {},
res1.forEach((g) => { listOperationOptions: [
const id = g.attributes.map((attr) => attr.id) {
attrHasGroupIds.push(...id) value: 'cover',
}) label: 'cmdb.ci.cover'
const otherGroupAttr = this.attributeList.filter( },
(attr) => !attrHasGroupIds.includes(attr.id) && !attr.is_computed {
) value: 'add',
if (otherGroupAttr.length) { label: 'add'
_attributesByGroup.push({ id: -1, name: this.$t('other'), attributes: otherGroupAttr }) },
} {
console.log(otherGroupAttr, _attributesByGroup) value: 'delete',
this.attributesByGroup = _attributesByGroup label: 'delete'
}) }
}, ]
createInstance() { }
const _this = this },
if (_this.action === 'update') { computed: {
this.form.validateFields((err, values) => { title() {
if (err) { return this.action === 'create' ? this.$t('create') + ' ' : this.$t('cmdb.ci.batchUpdate') + ' '
return },
} typeId() {
Object.keys(values).forEach((k) => { if (this.typeIdFromRelation) {
const _tempFind = this.attributeList.find((item) => item.name === k) return this.typeIdFromRelation
if ( }
_tempFind.value_type === '3' && return this.$router.currentRoute.meta.typeId
values[k] && },
Object.prototype.toString.call(values[k]) === '[object Object]' valueTypeMap() {
) { return valueTypeMap()
values[k] = values[k].format('YYYY-MM-DD HH:mm:ss') },
} },
if ( provide() {
_tempFind.value_type === '4' && return {
values[k] && getFieldType: this.getFieldType,
Object.prototype.toString.call(values[k]) === '[object Object]' }
) { },
values[k] = values[k].format('YYYY-MM-DD') inject: ['attrList'],
} methods: {
if (_tempFind.value_type === '6') { moment,
values[k] = values[k] ? JSON.parse(values[k]) : undefined async getCIType() {
} await getCIType(this.typeId).then((res) => {
}) this.CIType = res.ci_types[0]
})
_this.$emit('submit', values) },
}) async getAttributeList() {
} else { const _attrList = this.attrList()
let values = {} this.attributeList = _attrList.sort((x, y) => y.is_required - x.is_required)
for (let i = 0; i < this.attributesByGroup.length; i++) { await getCITypeGroupById(this.typeId).then((res1) => {
const data = this.$refs[`createInstanceFormByGroup_${this.attributesByGroup[i].id}`][0].getData() const _attributesByGroup = res1.map((g) => {
if (data === 'error') { g.attributes = g.attributes.filter((attr) => !attr.is_computed)
return return g
} })
values = { ...values, ...data } const attrHasGroupIds = []
} res1.forEach((g) => {
const id = g.attributes.map((attr) => attr.id)
Object.keys(values).forEach((k) => { attrHasGroupIds.push(...id)
const _tempFind = this.attributeList.find((item) => item.name === k) })
if ( const otherGroupAttr = this.attributeList.filter(
_tempFind.value_type === '3' && (attr) => !attrHasGroupIds.includes(attr.id) && !attr.is_computed
values[k] && )
Object.prototype.toString.call(values[k]) === '[object Object]' if (otherGroupAttr.length) {
) { _attributesByGroup.push({ id: -1, name: this.$t('other'), attributes: otherGroupAttr })
values[k] = values[k].format('YYYY-MM-DD HH:mm:ss') }
} console.log(otherGroupAttr, _attributesByGroup)
if ( this.attributesByGroup = _attributesByGroup
_tempFind.value_type === '4' && })
values[k] && },
Object.prototype.toString.call(values[k]) === '[object Object]' createInstance() {
) { const _this = this
values[k] = values[k].format('YYYY-MM-DD') if (_this.action === 'update') {
} this.form.validateFields({ force: true }, (err, values) => {
if (_tempFind.value_type === '6') { if (err) {
values[k] = values[k] ? JSON.parse(values[k]) : undefined return
} }
}) Object.keys(values).forEach((k) => {
values.ci_type = _this.typeId const _tempFind = this.attributeList.find((item) => item.name === k)
console.log(this.parentsForm)
Object.keys(this.parentsForm).forEach((type) => { if (_tempFind.is_reference) {
if (this.parentsForm[type].value) { values[k] = values[k] ? values[k] : null
values[`$${type}.${this.parentsForm[type].attr}`] = this.parentsForm[type].value }
}
}) if (
addCI(values).then((res) => { _tempFind.value_type === '3' &&
_this.$message.success(this.$t('addSuccess')) values[k] &&
_this.visible = false Object.prototype.toString.call(values[k]) === '[object Object]'
_this.$emit('reload', { ci_id: res.ci_id }) ) {
}) values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
} }
if (
// this.form.validateFields((err, values) => { _tempFind.value_type === '4' &&
// if (err) { values[k] &&
// _this.$message.error('字段填写不符合要求!') Object.prototype.toString.call(values[k]) === '[object Object]'
// return ) {
// } values[k] = values[k].format('YYYY-MM-DD')
// Object.keys(values).forEach((k) => { }
// if (Object.prototype.toString.call(values[k]) === '[object Object]' && values[k]) { if (_tempFind.value_type === '6') {
// values[k] = values[k].format('YYYY-MM-DD HH:mm:ss') values[k] = values[k] ? JSON.parse(values[k]) : undefined
// } }
// const _tempFind = this.attributeList.find((item) => item.name === k)
// if (_tempFind.value_type === '6') { if (_tempFind.is_list) {
// values[k] = values[k] ? JSON.parse(values[k]) : undefined const operation = this.batchUpdateLists?.find((item) => item.name === k)?.operation || 'cover'
// } switch (operation) {
// }) case 'add':
case 'delete':
// if (_this.action === 'update') { values[k] = {
// _this.$emit('submit', values) op: operation,
// return v: values[k]
// } }
// values.ci_type = _this.typeId break
// console.log(values) default:
// this.attributesByGroup.forEach((group) => { break
// this.$refs[`createInstanceFormByGroup_${group.id}`][0].getData() }
// }) }
// console.log(1111) })
// // addCI(values).then((res) => {
// // _this.$message.success('新增成功!') _this.$emit('submit', values)
// // _this.visible = false })
// // _this.$emit('reload') } else {
// // }) let values = {}
// }) for (let i = 0; i < this.attributesByGroup.length; i++) {
}, const data = this.$refs[`createInstanceFormByGroup_${this.attributesByGroup[i].id}`][0].getData()
handleClose() { if (data === 'error') {
this.visible = false return
}, }
handleOpen(visible, action) { values = { ...values, ...data }
this.visible = visible }
this.action = action
this.$nextTick(() => { Object.keys(values).forEach((k) => {
this.form.resetFields() const _tempFind = this.attributeList.find((item) => item.name === k)
Promise.all([this.getCIType(), this.getAttributeList()]).then(() => {
this.batchUpdateLists = [{ name: this.attributeList[0].name }] if (_tempFind.is_reference) {
}) values[k] = values[k] ? values[k] : null
if (action === 'create') { }
getCITypeParent(this.typeId).then(async (res) => {
for (let i = 0; i < res.parents.length; i++) { if (
await getCanEditByParentIdChildId(res.parents[i].id, this.typeId).then((p_res) => { _tempFind.value_type === '3' &&
this.canEdit = { values[k] &&
..._.cloneDeep(this.canEdit), Object.prototype.toString.call(values[k]) === '[object Object]'
[res.parents[i].id]: p_res.result, ) {
} values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
}) }
} if (
this.parentsType = res.parents.filter((parent) => this.canEdit[parent.id]) _tempFind.value_type === '4' &&
const _parentsForm = {} values[k] &&
res.parents.forEach((item) => { Object.prototype.toString.call(values[k]) === '[object Object]'
const _find = item.attributes.find((attr) => attr.id === item.unique_id) ) {
_parentsForm[item.name] = { attr: _find.name, value: '' } values[k] = values[k].format('YYYY-MM-DD')
}) }
this.parentsForm = _parentsForm if (_tempFind.value_type === '6') {
}) values[k] = values[k] ? JSON.parse(values[k]) : undefined
} }
}) })
}, values.ci_type = _this.typeId
getFieldType(name) { console.log(this.parentsForm)
const _find = this.attributeList.find((item) => item.name === name) Object.keys(this.parentsForm).forEach((type) => {
if (_find) { if (this.parentsForm[type].value) {
if (_find.is_choice) { values[`$${type}.${this.parentsForm[type].attr}`] = this.parentsForm[type].value
if (_find.is_list) { }
return 'select%%multiple' })
} addCI(values).then((res) => {
return 'select' _this.$message.success(this.$t('addSuccess'))
} else if ((_find.value_type === '0' || _find.value_type === '1') && !_find.is_list) { _this.visible = false
return 'input_number' _this.$emit('reload', { ci_id: res.ci_id })
} else if (_find.value_type === '4' || _find.value_type === '3') { })
return _find.value_type }
} else {
return 'input' // this.form.validateFields((err, values) => {
} // if (err) {
} // _this.$message.error('字段填写不符合要求!')
return 'input' // return
}, // }
getSelectFieldOptions(name) { // Object.keys(values).forEach((k) => {
const _find = this.attributeList.find((item) => item.name === name) // if (Object.prototype.toString.call(values[k]) === '[object Object]' && values[k]) {
if (_find) { // values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
return _find.choice_value // }
} // const _tempFind = this.attributeList.find((item) => item.name === k)
return [] // if (_tempFind.value_type === '6') {
}, // values[k] = values[k] ? JSON.parse(values[k]) : undefined
handleAdd() { // }
this.batchUpdateLists.push({ name: undefined }) // })
},
handleDelete(name) { // if (_this.action === 'update') {
const _idx = this.batchUpdateLists.findIndex((item) => item.name === name) // _this.$emit('submit', values)
if (_idx > -1) { // return
this.batchUpdateLists.splice(_idx, 1) // }
} // values.ci_type = _this.typeId
}, // console.log(values)
// filterOption(input, option) { // this.attributesByGroup.forEach((group) => {
// return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0 // this.$refs[`createInstanceFormByGroup_${group.id}`][0].getData()
// }, // })
handleFocusInput(e, attr) { // console.log(1111)
console.log(attr) // // addCI(values).then((res) => {
const _tempFind = this.attributeList.find((item) => item.name === attr.name) // // _this.$message.success('新增成功!')
if (_tempFind.value_type === '6') { // // _this.visible = false
this.editAttr = attr // // _this.$emit('reload')
e.srcElement.blur() // // })
const jsonData = this.form.getFieldValue(attr.name) // })
this.$refs.jsonEditor.open(null, null, jsonData ? JSON.parse(jsonData) : {}) },
} else { handleClose() {
this.editAttr = null this.visible = false
} },
}, handleOpen(visible, action) {
jsonEditorOk(jsonData) { this.visible = visible
this.form.setFieldsValue({ [this.editAttr.name]: JSON.stringify(jsonData) }) this.action = action
}, this.$nextTick(() => {
}, this.form.resetFields()
} Promise.all([this.getCIType(), this.getAttributeList()]).then(() => {
</script> this.batchUpdateLists = [{
<style lang="less"> name: this.attributeList?.[0]?.name || undefined,
.create-instance-form { operation: 'cover'
.ant-form-item { }]
margin-bottom: 5px; })
} if (action === 'create') {
.ant-drawer-body { getCITypeParent(this.typeId).then(async (res) => {
overflow-y: auto; for (let i = 0; i < res.parents.length; i++) {
max-height: calc(100vh - 110px); await getCanEditByParentIdChildId(res.parents[i].id, this.typeId).then((p_res) => {
} this.canEdit = {
} ..._.cloneDeep(this.canEdit),
</style> [res.parents[i].id]: p_res.result,
}
})
}
this.parentsType = res.parents.filter((parent) => this.canEdit[parent.id])
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') && !_find.is_list) {
return 'input_number'
} else if (_find.value_type === '4' || _find.value_type === '3') {
return _find.value_type
} else {
return 'input'
}
}
return 'input'
},
getAttr(name) {
return this.attributeList.find((item) => item.name === name) ?? {}
},
getSelectFieldOptions(name) {
const _find = this.attributeList.find((item) => item.name === name)
if (_find) {
return _find.choice_value
}
return []
},
handleAdd() {
this.batchUpdateLists.push({
name: undefined,
operation: 'cover'
})
},
handleDelete(name) {
const _idx = this.batchUpdateLists.findIndex((item) => item.name === name)
if (_idx > -1) {
this.batchUpdateLists.splice(_idx, 1)
}
},
// filterOption(input, option) {
// return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
// },
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) })
},
showListOperation(name) {
if (!name) {
return false
}
const attr = this.attributeList.find((attr) => attr.name === name)
return attr && attr.is_list
},
getDecoratorRules(data) {
const { name, operation } = data
const isList = this.showListOperation(name)
const rules = [
{ required: false }
]
if (isList && ['delete', 'add'].includes(operation)) {
rules[0] = {
required: true,
message: this.$t('placeholder1')
}
}
return rules
},
filterAttributes(attributes) {
return attributes.filter((attr) => {
return !attr.is_bool && !attr.is_reference
})
},
},
}
</script>
<style lang="less">
.create-instance-form {
.ant-form-item {
margin-bottom: 5px;
}
.ant-drawer-body {
overflow-y: auto;
height: calc(100vh - 110px);
}
}
</style>

View File

@@ -79,6 +79,8 @@
import XEUtils from 'xe-utils' import XEUtils from 'xe-utils'
import { getCITypeAttributesByName } from '@/modules/cmdb/api/CITypeAttr' import { getCITypeAttributesByName } from '@/modules/cmdb/api/CITypeAttr'
import { valueTypeMap } from '@/modules/cmdb/utils/const' import { valueTypeMap } from '@/modules/cmdb/utils/const'
import { getPropertyType } from '@/modules/cmdb/utils/helper'
export default { export default {
name: 'MetadataDrawer', name: 'MetadataDrawer',
data() { data() {
@@ -187,12 +189,7 @@ export default {
this.loading = true this.loading = true
const { attributes = [] } = await getCITypeAttributesByName(this.typeId) const { attributes = [] } = await getCITypeAttributesByName(this.typeId)
this.tableData = attributes.map((attr) => { this.tableData = attributes.map((attr) => {
if (attr.is_password) { attr.value_type = getPropertyType(attr)
attr.value_type = '7'
}
if (attr.is_link) {
attr.value_type = '8'
}
return attr return attr
}) })
this.loading = false this.loading = false

View File

@@ -1,9 +1,19 @@
<template> <template>
<span :id="`ci-detail-attr-${attr.name}`"> <span :id="`ci-detail-attr-${attr.name}`">
<span v-if="!isEdit || attr.value_type === '6'"> <span v-if="!isEdit || attr.value_type === '6'">
<template v-if="attr.is_reference" >
<a
v-for="(ciId) in (attr.is_list ? ci[attr.name] : [ci[attr.name]])"
:key="ciId"
:href="`/cmdb/cidetail/${attr.reference_type_id}/${ciId}`"
target="_blank"
>
{{ attr.referenceShowAttrNameMap ? attr.referenceShowAttrNameMap[ciId] || ciId : ciId }}
</a>
</template>
<PasswordField <PasswordField
:style="{ display: 'inline-block' }" :style="{ display: 'inline-block' }"
v-if="attr.is_password && ci[attr.name]" v-else-if="attr.is_password && ci[attr.name]"
:ci_id="ci._id" :ci_id="ci._id"
:attr_id="attr.id" :attr_id="attr.id"
></PasswordField> ></PasswordField>
@@ -32,7 +42,7 @@
:style="{ color: getChoiceValueIcon(attr, value).color, marginRight: '5px' }" :style="{ color: getChoiceValueIcon(attr, value).color, marginRight: '5px' }"
:type="getChoiceValueIcon(attr, value).name" :type="getChoiceValueIcon(attr, value).name"
/> />
{{ value }}</span {{ getChoiceValueLabel(attr, value) || value }}</span
> >
</template> </template>
<span <span
@@ -56,7 +66,7 @@
:style="{ color: getChoiceValueIcon(attr, ci[attr.name]).color, marginRight: '5px' }" :style="{ color: getChoiceValueIcon(attr, ci[attr.name]).color, marginRight: '5px' }"
:type="getChoiceValueIcon(attr, ci[attr.name]).name" :type="getChoiceValueIcon(attr, ci[attr.name]).name"
/> />
{{ ci[attr.name] }} {{ getChoiceValueLabel(attr, ci[attr.name]) || ci[attr.name] }}
</span> </span>
</template> </template>
<template v-else-if="attr.is_list"> <template v-else-if="attr.is_list">
@@ -67,6 +77,29 @@
<template v-else> <template v-else>
<a-form :form="form"> <a-form :form="form">
<a-form-item label="" :colon="false"> <a-form-item label="" :colon="false">
<CIReferenceAttr
v-if="attr.is_reference"
:referenceTypeId="attr.reference_type_id"
:isList="attr.is_list"
:referenceShowAttrName="attr.showAttrName"
:initSelectOption="getInitReferenceSelectOption(attr)"
v-decorator="[
attr.name,
{
rules: [{ required: attr.is_required, message: $t('placeholder2') + `${attr.alias || attr.name}` }],
}
]"
/>
<a-switch
v-else-if="attr.is_bool"
v-decorator="[
attr.name,
{
rules: [{ required: attr.is_required }],
valuePropName: 'checked',
}
]"
/>
<a-select <a-select
:style="{ width: '100%' }" :style="{ width: '100%' }"
v-decorator="[ v-decorator="[
@@ -76,7 +109,7 @@
}, },
]" ]"
:placeholder="$t('placeholder2')" :placeholder="$t('placeholder2')"
v-if="attr.is_choice" v-else-if="attr.is_choice"
:mode="attr.is_list ? 'multiple' : 'default'" :mode="attr.is_list ? 'multiple' : 'default'"
showSearch showSearch
allowClear allowClear
@@ -101,7 +134,7 @@
:type="choice[1].icon.name" :type="choice[1].icon.name"
/> />
</template> </template>
{{ choice[0] }} {{ choice[1] ? choice[1].label || choice[0] : choice[0] }}
</span> </span>
</a-select-option> </a-select-option>
</a-select> </a-select>
@@ -157,10 +190,11 @@ import { updateCI } from '@/modules/cmdb/api/ci'
import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue' import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue'
import PasswordField from '../../../components/passwordField/index.vue' import PasswordField from '../../../components/passwordField/index.vue'
import { getAttrPassword } from '../../../api/CITypeAttr' import { getAttrPassword } from '../../../api/CITypeAttr'
import CIReferenceAttr from '@/components/ciReferenceAttr/index.vue'
export default { export default {
name: 'CiDetailAttrContent', name: 'CiDetailAttrContent',
components: { JsonEditor, PasswordField }, components: { JsonEditor, PasswordField, CIReferenceAttr },
props: { props: {
ci: { ci: {
type: Object, type: Object,
@@ -209,7 +243,7 @@ export default {
} }
this.isEdit = true this.isEdit = true
this.$nextTick(async () => { this.$nextTick(async () => {
if (this.attr.is_list && !this.attr.is_choice) { if (this.attr.is_list && !this.attr.is_choice && !this.attr.is_reference) {
this.form.setFieldsValue({ this.form.setFieldsValue({
[`${this.attr.name}`]: Array.isArray(this.ci[this.attr.name]) [`${this.attr.name}`]: Array.isArray(this.ci[this.attr.name])
? this.ci[this.attr.name].join(',') ? this.ci[this.attr.name].join(',')
@@ -237,6 +271,10 @@ export default {
.then(() => { .then(() => {
this.$message.success(this.$t('updateSuccess')) this.$message.success(this.$t('updateSuccess'))
this.$emit('updateCIByself', { [`${this.attr.name}`]: newData }, this.attr.name) this.$emit('updateCIByself', { [`${this.attr.name}`]: newData }, this.attr.name)
if (this.attr.is_reference) {
this.$emit('refreshReferenceAttr')
}
}) })
.catch(() => { .catch(() => {
this.$emit('refresh', this.attr.name) this.$emit('refresh', this.attr.name)
@@ -280,9 +318,28 @@ export default {
} }
return {} return {}
}, },
getChoiceValueLabel(col, colValue) {
const _find = col.choice_value.find((item) => String(item[0]) === String(colValue))
if (_find) {
return _find[1]?.label || ''
}
return ''
},
getName(name) { getName(name) {
return name ?? '' return name ?? ''
}, },
getInitReferenceSelectOption(attr) {
const option = Object.keys(attr?.referenceShowAttrNameMap || {}).map((key) => {
return {
key: Number(key),
title: attr?.referenceShowAttrNameMap?.[key] ?? ''
}
})
return option
}
}, },
} }
</script> </script>

View File

@@ -105,7 +105,7 @@ export default {
default: true, default: true,
}, },
attrList: { attrList: {
type: Array, type: Function,
default: () => [], default: () => [],
} }
}, },

View File

@@ -38,6 +38,16 @@
resizable resizable
class="ops-stripe-table" class="ops-stripe-table"
> >
<template #reference_default="{ row, column }">
<a
v-for="(id) in (column.params.attr.is_list ? row[column.field] : [row[column.field]])"
:key="id"
:href="`/cmdb/cidetail/${column.params.attr.reference_type_id}/${id}`"
target="_blank"
>
{{ id }}
</a>
</template>
<template #operation_default="{ row }"> <template #operation_default="{ row }">
<a-popconfirm <a-popconfirm
arrowPointAtCenter arrowPointAtCenter
@@ -85,6 +95,16 @@
resizable resizable
class="ops-stripe-table" class="ops-stripe-table"
> >
<template #reference_default="{ row, column }">
<a
v-for="(id) in (column.params.attr.is_list ? row[column.field] : [row[column.field]])"
:key="id"
:href="`/cmdb/cidetail/${column.params.attr.reference_type_id}/${id}`"
target="_blank"
>
{{ id }}
</a>
</template>
<template #operation_default="{ row }"> <template #operation_default="{ row }">
<a-popconfirm <a-popconfirm
arrowPointAtCenter arrowPointAtCenter
@@ -132,6 +152,10 @@ export default {
type: Object, type: Object,
default: () => {}, default: () => {},
}, },
initQueryLoading: {
type: Boolean,
default: false,
}
}, },
data() { data() {
return { return {
@@ -145,11 +169,260 @@ export default {
firstCIJsonAttr: {}, firstCIJsonAttr: {},
secondCIJsonAttr: {}, secondCIJsonAttr: {},
canEdit: {}, canEdit: {},
topoData: {
nodes: {},
edges: []
}
} }
}, },
computed: { computed: {
topoData() { exsited_ci() {
const _exsited_ci = [this.ciId]
this.parentCITypes.forEach((parent) => {
if (this.firstCIs[parent.name]) {
this.firstCIs[parent.name].forEach((parentCi) => {
_exsited_ci.push(parentCi._id)
})
}
})
this.childCITypes.forEach((child) => {
if (this.secondCIs[child.name]) {
this.secondCIs[child.name].forEach((childCi) => {
_exsited_ci.push(childCi._id)
})
}
})
return _exsited_ci
},
},
inject: {
attrList: { from: 'attrList' },
attributes: { from: 'attributes' },
ci_types: { from: 'ci_types' },
relationViewRefreshNumber: {
from: 'relationViewRefreshNumber',
default: () => null,
},
},
mounted() {
if (!this.initQueryLoading) {
this.init(true)
}
},
methods: {
async init(isFirst) {
await Promise.all([this.getParentCITypes(), this.getChildCITypes()])
Promise.all([this.getFirstCIs(), this.getSecondCIs()]).then(() => {
const ci_types_list = this.ci_types()
this.handleTopoData()
if (
isFirst &&
this.$refs.ciDetailRelationTopo &&
ci_types_list.length
) {
this.$refs.ciDetailRelationTopo.exsited_ci = this.exsited_ci
this.$refs.ciDetailRelationTopo.setTopoData(this.topoData)
}
})
},
async getFirstCIs() {
await searchCIRelation(`root_id=${Number(this.ciId)}&level=1&reverse=1&count=10000`)
.then((res) => {
const firstCIs = {}
res.result.forEach((item) => {
this.firstCIJsonAttr[item._type].forEach((attr) => {
item[`${attr}`] = item[`${attr}`] ? JSON.stringify(item[`${attr}`]) : ''
})
this.formatCI(item, this.firstCIColumns)
if (item.ci_type in firstCIs) {
firstCIs[item.ci_type].push(item)
} else {
firstCIs[item.ci_type] = [item]
}
})
this.firstCIs = firstCIs
})
.catch((e) => {})
},
async getSecondCIs() {
await searchCIRelation(`root_id=${Number(this.ciId)}&level=1&reverse=0&count=10000`)
.then((res) => {
const secondCIs = {}
res.result.forEach((item) => {
this.secondCIJsonAttr[item._type].forEach((attr) => {
item[`${attr}`] = item[`${attr}`] ? JSON.stringify(item[`${attr}`]) : ''
})
this.formatCI(item, this.secondCIColumns)
if (item.ci_type in secondCIs) {
secondCIs[item.ci_type].push(item)
} else {
secondCIs[item.ci_type] = [item]
}
})
this.secondCIs = secondCIs
})
.catch((e) => {})
},
formatCI(ci, columns) {
Object.keys(ci).forEach((key) => {
const attr = columns?.[ci?._type]?.find((item) => item?.params?.attr?.name === key)?.params?.attr
if (attr?.is_choice && attr?.choice_value?.length) {
if (attr?.is_list) {
ci[key] = ci[key].map((value) => {
const label = attr?.choice_value?.find((choice) => choice?.[0] === value)?.[1]?.label
return label || ci[key]
})
} else {
const label = attr?.choice_value?.find((choice) => choice?.[0] === ci[key])?.[1]?.label
ci[key] = label || ci[key]
}
}
})
return ci
},
async getParentCITypes() {
const res = await getCITypeParent(this.typeId)
this.parentCITypes = res.parents
for (let i = 0; i < res.parents.length; i++) {
await getCanEditByParentIdChildId(res.parents[i].id, this.typeId).then((p_res) => {
this.canEdit = {
..._.cloneDeep(this.canEdit),
[res.parents[i].id]: p_res.result,
}
})
}
const firstCIColumns = {}
const firstCIJsonAttr = {}
res.parents.forEach((item) => {
const columns = []
const jsonAttr = []
item.attributes.forEach((attr) => {
const column = {
key: 'p_' + attr.id,
field: attr.name,
title: attr.alias,
minWidth: '100px',
params: {
attr
},
}
if (attr.is_reference) {
column.slots = {
default: 'reference_default'
}
}
columns.push(column)
if (attr.value_type === '6') {
jsonAttr.push(attr.name)
}
})
firstCIJsonAttr[item.id] = jsonAttr
firstCIColumns[item.id] = columns
firstCIColumns[item.id].push({
key: 'p_operation',
field: 'operation',
title: this.$t('operation'),
width: '60px',
fixed: 'right',
slots: {
default: 'operation_default',
},
align: 'center',
})
})
this.firstCIColumns = firstCIColumns
this.firstCIJsonAttr = firstCIJsonAttr
},
async getChildCITypes() {
const res = await getCITypeChildren(this.typeId)
this.childCITypes = res.children
for (let i = 0; i < res.children.length; i++) {
await getCanEditByParentIdChildId(this.typeId, res.children[i].id).then((c_res) => {
this.canEdit = {
..._.cloneDeep(this.canEdit),
[res.children[i].id]: c_res.result,
}
})
}
const secondCIColumns = {}
const secondCIJsonAttr = {}
res.children.forEach((item) => {
const columns = []
const jsonAttr = []
item.attributes.forEach((attr) => {
const column = {
key: 'c_' + attr.id,
field: attr.name,
title: attr.alias,
minWidth: '100px',
params: {
attr
},
}
if (attr.is_reference) {
column.slots = {
default: 'reference_default'
}
}
columns.push(column)
if (attr.value_type === '6') {
jsonAttr.push(attr.name)
}
})
secondCIJsonAttr[item.id] = jsonAttr
secondCIColumns[item.id] = columns
secondCIColumns[item.id].push({
key: 'c_operation',
field: 'operation',
title: this.$t('operation'),
width: '60px',
fixed: 'right',
slots: {
default: 'operation_default',
},
align: 'center',
})
})
this.secondCIColumns = secondCIColumns
this.secondCIJsonAttr = secondCIJsonAttr
},
reload() {
this.init()
},
deleteRelation(first_ci_id, second_ci_id) {
deleteCIRelationView(first_ci_id, second_ci_id).then((res) => {
this.init()
if (this.relationViewRefreshNumber) {
this.relationViewRefreshNumber()
}
})
},
handleChangeActiveKey(e) {
if (e.target.value === '1') {
this.$nextTick(() => {
this.$refs.ciDetailRelationTopo.exsited_ci = this.exsited_ci
this.$refs.ciDetailRelationTopo.setTopoData(this.topoData)
})
}
},
handleTopoData() {
const ci_types_list = this.ci_types() const ci_types_list = this.ci_types()
if (!ci_types_list?.length) {
this.$set(this, 'topoData', {
nodes: {},
edges: []
})
return
}
const _findCiType = ci_types_list.find((item) => item.id === this.typeId) const _findCiType = ci_types_list.find((item) => item.id === this.typeId)
const unique_id = _findCiType.show_id || this.attributes().unique_id const unique_id = _findCiType.show_id || this.attributes().unique_id
const unique_name = _findCiType.show_name || this.attributes().unique const unique_name = _findCiType.show_name || this.attributes().unique
@@ -266,185 +539,11 @@ export default {
}) })
} }
}) })
return { nodes, edges } this.$set(this, 'topoData', {
}, nodes,
exsited_ci() { edges
const _exsited_ci = [this.ciId]
this.parentCITypes.forEach((parent) => {
if (this.firstCIs[parent.name]) {
this.firstCIs[parent.name].forEach((parentCi) => {
_exsited_ci.push(parentCi._id)
})
}
}) })
this.childCITypes.forEach((child) => { }
if (this.secondCIs[child.name]) {
this.secondCIs[child.name].forEach((childCi) => {
_exsited_ci.push(childCi._id)
})
}
})
return _exsited_ci
},
},
inject: {
attrList: { from: 'attrList' },
attributes: { from: 'attributes' },
ci_types: { from: 'ci_types' },
relationViewRefreshNumber: {
from: 'relationViewRefreshNumber',
default: () => null,
},
},
mounted() {
this.init(true)
},
methods: {
async init(isFirst) {
await Promise.all([this.getParentCITypes(), this.getChildCITypes()])
Promise.all([this.getFirstCIs(), this.getSecondCIs()]).then(() => {
if (isFirst && this.$refs.ciDetailRelationTopo) {
this.$refs.ciDetailRelationTopo.exsited_ci = this.exsited_ci
this.$refs.ciDetailRelationTopo.setTopoData(this.topoData)
}
})
},
async getFirstCIs() {
await searchCIRelation(`root_id=${Number(this.ciId)}&&level=1&&reverse=1&&count=10000`)
.then((res) => {
const firstCIs = {}
res.result.forEach((item) => {
this.firstCIJsonAttr[item._type].forEach((attr) => {
item[`${attr}`] = item[`${attr}`] ? JSON.stringify(item[`${attr}`]) : ''
})
if (item.ci_type in firstCIs) {
firstCIs[item.ci_type].push(item)
} else {
firstCIs[item.ci_type] = [item]
}
})
this.firstCIs = firstCIs
})
.catch((e) => {})
},
async getSecondCIs() {
await searchCIRelation(`root_id=${Number(this.ciId)}&&level=1&&reverse=0&&count=10000`)
.then((res) => {
const secondCIs = {}
res.result.forEach((item) => {
this.secondCIJsonAttr[item._type].forEach((attr) => {
item[`${attr}`] = item[`${attr}`] ? JSON.stringify(item[`${attr}`]) : ''
})
if (item.ci_type in secondCIs) {
secondCIs[item.ci_type].push(item)
} else {
secondCIs[item.ci_type] = [item]
}
})
this.secondCIs = secondCIs
})
.catch((e) => {})
},
async getParentCITypes() {
const res = await getCITypeParent(this.typeId)
this.parentCITypes = res.parents
for (let i = 0; i < res.parents.length; i++) {
await getCanEditByParentIdChildId(res.parents[i].id, this.typeId).then((p_res) => {
this.canEdit = {
..._.cloneDeep(this.canEdit),
[res.parents[i].id]: p_res.result,
}
})
}
const firstCIColumns = {}
const firstCIJsonAttr = {}
res.parents.forEach((item) => {
const columns = []
const jsonAttr = []
item.attributes.forEach((attr) => {
columns.push({ key: 'p_' + attr.id, field: attr.name, title: attr.alias, minWidth: '100px' })
if (attr.value_type === '6') {
jsonAttr.push(attr.name)
}
})
firstCIJsonAttr[item.id] = jsonAttr
firstCIColumns[item.id] = columns
firstCIColumns[item.id].push({
key: 'p_operation',
field: 'operation',
title: this.$t('operation'),
width: '60px',
fixed: 'right',
slots: {
default: 'operation_default',
},
align: 'center',
})
})
this.firstCIColumns = firstCIColumns
this.firstCIJsonAttr = firstCIJsonAttr
},
async getChildCITypes() {
const res = await getCITypeChildren(this.typeId)
this.childCITypes = res.children
for (let i = 0; i < res.children.length; i++) {
await getCanEditByParentIdChildId(this.typeId, res.children[i].id).then((c_res) => {
this.canEdit = {
..._.cloneDeep(this.canEdit),
[res.children[i].id]: c_res.result,
}
})
}
const secondCIColumns = {}
const secondCIJsonAttr = {}
res.children.forEach((item) => {
const columns = []
const jsonAttr = []
item.attributes.forEach((attr) => {
columns.push({ key: 'c_' + attr.id, field: attr.name, title: attr.alias, minWidth: '100px' })
if (attr.value_type === '6') {
jsonAttr.push(attr.name)
}
})
secondCIJsonAttr[item.id] = jsonAttr
secondCIColumns[item.id] = columns
secondCIColumns[item.id].push({
key: 'c_operation',
field: 'operation',
title: this.$t('operation'),
width: '60px',
fixed: 'right',
slots: {
default: 'operation_default',
},
align: 'center',
})
})
this.secondCIColumns = secondCIColumns
this.secondCIJsonAttr = secondCIJsonAttr
},
reload() {
this.init()
},
deleteRelation(first_ci_id, second_ci_id) {
deleteCIRelationView(first_ci_id, second_ci_id).then((res) => {
this.init()
if (this.relationViewRefreshNumber) {
this.relationViewRefreshNumber()
}
})
},
handleChangeActiveKey(e) {
if (e.target.value === '1') {
this.$nextTick(() => {
this.$refs.ciDetailRelationTopo.exsited_ci = this.exsited_ci
this.$refs.ciDetailRelationTopo.setTopoData(this.topoData)
})
}
},
}, },
} }
</script> </script>

View File

@@ -17,7 +17,7 @@
border: 1px solid #d9d9d9; border: 1px solid #d9d9d9;
border-radius: 2px; border-radius: 2px;
padding: 4px 8px; padding: 4px 8px;
width: 100px; width: auto;
text-align: center; text-align: center;
.title { .title {
font-size: 16px; font-size: 16px;
@@ -73,7 +73,7 @@
} }
} }
.root { .root {
width: 100px; width: auto;
border-color: @primary-color; border-color: @primary-color;
font-weight: 700; font-weight: 700;
padding: 4px 8px; padding: 4px 8px;

View File

@@ -29,6 +29,10 @@ export default {
methods: { methods: {
init() { init() {
const root = document.getElementById('ci-detail-relation-topo') const root = document.getElementById('ci-detail-relation-topo')
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
context.font = '16px'
this.canvas = new TreeCanvas({ this.canvas = new TreeCanvas({
root: root, root: root,
disLinkable: false, // 可删除连线 disLinkable: false, // 可删除连线
@@ -54,7 +58,15 @@ export default {
return 10 return 10
}, },
getWidth(d) { getWidth(d) {
return 40 const metrics = context.measureText(d?.title || '')
const width = metrics.width
/**
* width 文字宽度
* 20 icon 宽度
* 4 盒子内边距
* 40 节点间距
*/
return width + 20 + 4 + 40
}, },
getHGap(d) { getHGap(d) {
return 80 return 80
@@ -69,22 +81,27 @@ export default {
this.canvas.on('events', ({ type, data }) => { this.canvas.on('events', ({ type, data }) => {
const sourceNode = data?.id || null const sourceNode = data?.id || null
if (type === 'custom:clickLeft') { if (type === 'custom:clickLeft') {
searchCIRelation(`root_id=${Number(sourceNode)}&&level=1&&reverse=1&&count=10000`).then((res) => { searchCIRelation(`root_id=${Number(sourceNode)}&level=1&reverse=1&count=10000`).then((res) => {
this.redrawData(res, sourceNode, 'left') this.redrawData(res, sourceNode, 'left')
}) })
} }
if (type === 'custom:clickRight') { if (type === 'custom:clickRight') {
searchCIRelation(`root_id=${Number(sourceNode)}&&level=1&&reverse=0&&count=10000`).then((res) => { searchCIRelation(`root_id=${Number(sourceNode)}&level=1&reverse=0&count=10000`).then((res) => {
this.redrawData(res, sourceNode, 'right') this.redrawData(res, sourceNode, 'right')
}) })
} }
}) })
}, },
setTopoData(data) { setTopoData(data) {
const root = document.getElementById('ci-detail-relation-topo')
if (root && root?.innerHTML) {
root.innerHTML = ''
}
this.canvas = null this.canvas = null
this.init() this.init()
this.topoData = _.cloneDeep(data) this.topoData = _.cloneDeep(data)
this.canvas.draw(data, {}, () => {
this.canvas.redraw(data, {}, () => {
this.canvas.focusCenterWithAnimate() this.canvas.focusCenterWithAnimate()
}) })
}, },

View File

@@ -1,449 +1,537 @@
<template> <template>
<div :style="{ height: '100%' }"> <div :style="{ height: '100%' }">
<a-tabs v-if="hasPermission" class="ci-detail-tab" v-model="activeTabKey" @change="changeTab"> <a-tabs v-if="hasPermission" class="ci-detail-tab" v-model="activeTabKey" @change="changeTab">
<a @click="shareCi" slot="tabBarExtraContent" :style="{ marginRight: '24px' }"> <a @click="shareCi" slot="tabBarExtraContent" :style="{ marginRight: '24px' }">
<a-icon type="share-alt" /> <a-icon type="share-alt" />
{{ $t('cmdb.ci.share') }} {{ $t('cmdb.ci.share') }}
</a> </a>
<a-tab-pane key="tab_1"> <a-tab-pane key="tab_1">
<span slot="tab"><a-icon type="book" />{{ $t('cmdb.attribute') }}</span> <span slot="tab"><a-icon type="book" />{{ $t('cmdb.attribute') }}</span>
<div class="ci-detail-attr"> <div class="ci-detail-attr">
<el-descriptions <el-descriptions
:title="group.name || $t('other')" :title="group.name || $t('other')"
:key="group.name" :key="group.name"
v-for="group in attributeGroups" v-for="group in attributeGroups"
border border
:column="3" :column="3"
> >
<el-descriptions-item <el-descriptions-item
:label="`${attr.alias || attr.name}`" :label="`${attr.alias || attr.name}`"
:key="attr.name" :key="attr.name"
v-for="attr in group.attributes" v-for="attr in group.attributes"
> >
<ci-detail-attr-content :ci="ci" :attr="attr" @refresh="refresh" @updateCIByself="updateCIByself" /> <ci-detail-attr-content :ci="ci" :attr="attr" @refresh="refresh" @updateCIByself="updateCIByself" @refreshReferenceAttr="handleReferenceAttr" />
</el-descriptions-item> </el-descriptions-item>
</el-descriptions> </el-descriptions>
</div> </div>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="tab_2"> <a-tab-pane key="tab_2">
<span slot="tab"><a-icon type="branches" />{{ $t('cmdb.relation') }}</span> <span slot="tab"><a-icon type="branches" />{{ $t('cmdb.relation') }}</span>
<div :style="{ height: '100%', padding: '24px', overflow: 'auto' }"> <div :style="{ height: '100%', padding: '24px', overflow: 'auto' }">
<ci-detail-relation ref="ciDetailRelation" :ciId="ciId" :typeId="typeId" :ci="ci" /> <ci-detail-relation ref="ciDetailRelation" :ciId="ciId" :typeId="typeId" :ci="ci" :initQueryLoading="initQueryLoading" />
</div> </div>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="tab_3"> <a-tab-pane key="tab_3">
<span slot="tab"><a-icon type="clock-circle" />{{ $t('cmdb.ci.history') }}</span> <span slot="tab"><a-icon type="clock-circle" />{{ $t('cmdb.ci.history') }}</span>
<div :style="{ padding: '24px', height: '100%' }"> <div :style="{ padding: '24px', height: '100%' }">
<a-space :style="{ 'margin-bottom': '10px', display: 'flex' }"> <a-space :style="{ 'margin-bottom': '10px', display: 'flex' }">
<a-button type="primary" class="ops-button-ghost" ghost @click="handleRollbackCI()"> <a-button type="primary" class="ops-button-ghost" ghost @click="handleRollbackCI()">
<ops-icon type="shishizhuangtai" />{{ $t('cmdb.ci.rollback') }} <ops-icon type="shishizhuangtai" />{{ $t('cmdb.ci.rollback') }}
</a-button> </a-button>
</a-space> <a-button type="primary" class="ops-button-ghost" ghost @click="handleExport">
<ci-rollback-form ref="ciRollbackForm" :ciIds="[ciId]" @getCIHistory="getCIHistory" /> <ops-icon type="veops-export" />{{ $t('export') }}
<vxe-table </a-button>
ref="xTable" </a-space>
show-overflow <ci-rollback-form ref="ciRollbackForm" :ciIds="[ciId]" @getCIHistory="getCIHistory" />
show-header-overflow <vxe-table
:data="ciHistory" ref="xTable"
size="small" show-overflow
:height="tableHeight" show-header-overflow
highlight-hover-row :data="ciHistory"
:span-method="mergeRowMethod" size="small"
:scroll-y="{ enabled: false, gt: 20 }" :height="tableHeight"
:scroll-x="{ enabled: false, gt: 0 }" highlight-hover-row
border :span-method="mergeRowMethod"
resizable :scroll-y="{ enabled: false, gt: 20 }"
class="ops-unstripe-table" :scroll-x="{ enabled: false, gt: 0 }"
> border
<template #empty> resizable
<a-empty :image-style="{ height: '100px' }" :style="{ paddingTop: '10%' }"> class="ops-unstripe-table"
<img slot="image" :src="require('@/assets/data_empty.png')" /> >
<span slot="description"> {{ $t('noData') }} </span> <template #empty>
</a-empty> <a-empty :image-style="{ height: '100px' }" :style="{ paddingTop: '10%' }">
</template> <img slot="image" :src="require('@/assets/data_empty.png')" />
<vxe-table-column sortable field="created_at" :title="$t('created_at')"></vxe-table-column> <span slot="description"> {{ $t('noData') }} </span>
<vxe-table-column </a-empty>
field="username" </template>
:title="$t('user')" <vxe-table-column sortable field="created_at" :title="$t('created_at')"></vxe-table-column>
:filters="[]" <vxe-table-column
:filter-method="filterUsernameMethod" field="username"
></vxe-table-column> :title="$t('user')"
<vxe-table-column :filters="[]"
field="operate_type" :filter-method="filterUsernameMethod"
:filters="[ ></vxe-table-column>
{ value: 0, label: $t('new') }, <vxe-table-column
{ value: 1, label: $t('delete') }, field="operate_type"
{ value: 2, label: $t('update') }, :filters="[
]" { value: 0, label: $t('new') },
:filter-method="filterOperateMethod" { value: 1, label: $t('delete') },
:title="$t('operation')" { value: 2, label: $t('update') },
> ]"
<template #default="{ row }"> :filter-method="filterOperateMethod"
{{ operateTypeMap[row.operate_type] }} :title="$t('operation')"
</template> >
</vxe-table-column> <template #default="{ row }">
<vxe-table-column {{ operateTypeMap[row.operate_type] }}
field="attr_alias" </template>
:title="$t('cmdb.attribute')" </vxe-table-column>
:filters="[]" <vxe-table-column
:filter-method="filterAttrMethod" field="attr_alias"
></vxe-table-column> :title="$t('cmdb.attribute')"
<vxe-table-column field="old" :title="$t('cmdb.history.old')"> :filters="[]"
<template #default="{ row }"> :filter-method="filterAttrMethod"
<span v-if="row.value_type === '6'">{{ JSON.parse(row.old) }}</span> ></vxe-table-column>
<span v-else>{{ row.old }}</span> <vxe-table-column :cell-type="'string'" field="old" :title="$t('cmdb.history.old')">
</template> <template #default="{ row }">
</vxe-table-column> <span v-if="row.value_type === '6'">{{ JSON.parse(row.old) }}</span>
<vxe-table-column field="new" :title="$t('cmdb.history.new')"> <span v-else>{{ row.old }}</span>
<template #default="{ row }"> </template>
<span v-if="row.value_type === '6'">{{ JSON.parse(row.new) }}</span> </vxe-table-column>
<span v-else>{{ row.new }}</span> <vxe-table-column :cell-type="'string'" field="new" :title="$t('cmdb.history.new')">
</template> <template #default="{ row }">
</vxe-table-column> <span v-if="row.value_type === '6'">{{ JSON.parse(row.new) }}</span>
</vxe-table> <span v-else>{{ row.new }}</span>
</div> </template>
</a-tab-pane> </vxe-table-column>
<a-tab-pane key="tab_4"> </vxe-table>
<span slot="tab"><ops-icon type="itsm_auto_trigger" />{{ $t('cmdb.history.triggerHistory') }}</span> </div>
<div :style="{ padding: '24px', height: '100%' }"> </a-tab-pane>
<TriggerTable :ci_id="ci._id" /> <a-tab-pane key="tab_4">
</div> <span slot="tab"><ops-icon type="itsm_auto_trigger" />{{ $t('cmdb.history.triggerHistory') }}</span>
</a-tab-pane> <div :style="{ padding: '24px', height: '100%' }">
<a-tab-pane key="tab_5"> <TriggerTable :ci_id="ci._id" />
<span slot="tab"><ops-icon type="itsm-association" />{{ $t('cmdb.ci.relITSM') }}</span> </div>
<div :style="{ padding: '24px', height: '100%' }"> </a-tab-pane>
<RelatedItsmTable ref="relatedITSMTable" :ci_id="ci._id" :ciHistory="ciHistory" :itsmInstalled="itsmInstalled" :attrList="attrList" /> <a-tab-pane key="tab_5">
</div> <span slot="tab"><ops-icon type="itsm-association" />{{ $t('cmdb.ci.relITSM') }}</span>
</a-tab-pane> <div :style="{ padding: '24px', height: '100%' }">
</a-tabs> <RelatedItsmTable ref="relatedITSMTable" :ci_id="ci._id" :ciHistory="ciHistory" :itsmInstalled="itsmInstalled" :attrList="attrList" />
<a-empty </div>
v-else </a-tab-pane>
:image-style="{ </a-tabs>
height: '100px', <a-empty
}" v-else
:style="{ paddingTop: '20%' }" :image-style="{
> height: '100px',
<img slot="image" :src="require('@/assets/data_empty.png')" /> }"
<span slot="description"> {{ $t('cmdb.ci.noPermission') }} </span> :style="{ paddingTop: '20%' }"
</a-empty> >
</div> <img slot="image" :src="require('@/assets/data_empty.png')" />
</template> <span slot="description"> {{ $t('cmdb.ci.noPermission') }} </span>
</a-empty>
<script> </div>
import _ from 'lodash' </template>
import { Descriptions, DescriptionsItem } from 'element-ui'
import { getCITypeGroupById, getCITypes } from '@/modules/cmdb/api/CIType' <script>
import { getCIHistory, judgeItsmInstalled } from '@/modules/cmdb/api/history' import _ from 'lodash'
import { getCIById } from '@/modules/cmdb/api/ci' import { Descriptions, DescriptionsItem } from 'element-ui'
import CiDetailAttrContent from './ciDetailAttrContent.vue' import { getCITypeGroupById, getCITypes } from '@/modules/cmdb/api/CIType'
import CiDetailRelation from './ciDetailRelation.vue' import { getCIHistory, judgeItsmInstalled } from '@/modules/cmdb/api/history'
import TriggerTable from '../../operation_history/modules/triggerTable.vue' import { getCIById, searchCI } from '@/modules/cmdb/api/ci'
import RelatedItsmTable from './ciDetailRelatedItsmTable.vue' import CiDetailAttrContent from './ciDetailAttrContent.vue'
import CiRollbackForm from './ciRollbackForm.vue' import CiDetailRelation from './ciDetailRelation.vue'
export default { import TriggerTable from '../../operation_history/modules/triggerTable.vue'
name: 'CiDetailTab', import RelatedItsmTable from './ciDetailRelatedItsmTable.vue'
components: { import CiRollbackForm from './ciRollbackForm.vue'
ElDescriptions: Descriptions, export default {
ElDescriptionsItem: DescriptionsItem, name: 'CiDetailTab',
CiDetailAttrContent, components: {
CiDetailRelation, ElDescriptions: Descriptions,
TriggerTable, ElDescriptionsItem: DescriptionsItem,
RelatedItsmTable, CiDetailAttrContent,
CiRollbackForm, CiDetailRelation,
}, TriggerTable,
props: { RelatedItsmTable,
typeId: { CiRollbackForm,
type: Number, },
required: true, props: {
}, typeId: {
treeViewsLevels: { type: Number,
type: Array, required: true,
default: () => [], },
}, treeViewsLevels: {
attributeHistoryTableHeight: { type: Array,
type: Number, default: () => [],
default: null },
} attributeHistoryTableHeight: {
}, type: Number,
data() { default: null
return { }
ci: {}, },
item: [], data() {
attributeGroups: [], return {
activeTabKey: 'tab_1', ci: {},
rowSpanMap: {}, item: [],
ciHistory: [], attributeGroups: [],
ciId: null, activeTabKey: 'tab_1',
ci_types: [], rowSpanMap: {},
hasPermission: true, ciHistory: [],
itsmInstalled: true, ciId: null,
tableHeight: this.attributeHistoryTableHeight || (this.$store.state.windowHeight - 120), ci_types: [],
} hasPermission: true,
}, itsmInstalled: true,
computed: { tableHeight: this.attributeHistoryTableHeight || (this.$store.state.windowHeight - 120),
windowHeight() { initQueryLoading: true,
return this.$store.state.windowHeight }
}, },
operateTypeMap() { computed: {
return { windowHeight() {
0: this.$t('new'), return this.$store.state.windowHeight
1: this.$t('delete'), },
2: this.$t('update'), operateTypeMap() {
} return {
}, 0: this.$t('new'),
}, 1: this.$t('delete'),
provide() { 2: this.$t('update'),
return { }
ci_types: () => { },
return this.ci_types },
}, provide() {
} return {
}, ci_types: () => {
inject: { return this.ci_types
reload: { },
from: 'reload', }
default: null, },
}, inject: {
handleSearch: { reload: {
from: 'handleSearch', from: 'reload',
default: null, default: null,
}, },
attrList: { handleSearch: {
from: 'attrList', from: 'handleSearch',
default: () => [], default: null,
}, },
}, attrList: {
methods: { from: 'attrList',
async create(ciId, activeTabKey = 'tab_1', ciDetailRelationKey = '1') { default: () => [],
this.activeTabKey = activeTabKey },
if (activeTabKey === 'tab_2') { },
this.$nextTick(() => { methods: {
this.$refs.ciDetailRelation.activeKey = ciDetailRelationKey async create(ciId, activeTabKey = 'tab_1', ciDetailRelationKey = '1') {
}) this.initQueryLoading = true
} this.activeTabKey = activeTabKey
this.ciId = ciId if (activeTabKey === 'tab_2') {
await this.getCI() this.$nextTick(() => {
await this.judgeItsmInstalled() this.$refs.ciDetailRelation.activeKey = ciDetailRelationKey
if (this.hasPermission) { })
this.getAttributes() }
this.getCIHistory() this.ciId = ciId
getCITypes().then((res) => { await this.getCI()
this.ci_types = res.ci_types await this.judgeItsmInstalled()
}) if (this.hasPermission) {
} this.getAttributes()
}, this.getCIHistory()
getAttributes() { const ciTypeRes = await getCITypes()
getCITypeGroupById(this.typeId, { need_other: 1 }) this.ci_types = ciTypeRes.ci_types
.then((res) => { if (this.activeTabKey === 'tab_2') {
this.attributeGroups = res this.$refs.ciDetailRelation.init(true)
}) }
.catch((e) => {}) }
}, this.initQueryLoading = false
async getCI() { },
await getCIById(this.ciId) getAttributes() {
.then((res) => { getCITypeGroupById(this.typeId, { need_other: 1 })
if (res.result.length) { .then((res) => {
this.ci = res.result[0] this.attributeGroups = res
} else {
this.hasPermission = false this.handleReferenceAttr()
} })
}) .catch((e) => {})
.catch((e) => { },
if (e.response.status === 404) {
this.itsmInstalled = false async handleReferenceAttr() {
} const map = {}
}) this.attributeGroups.forEach((group) => {
}, group.attributes.forEach((attr) => {
async judgeItsmInstalled() { if (attr?.is_reference && attr?.reference_type_id && this.ci[attr.name]) {
await judgeItsmInstalled().catch((e) => { const ids = Array.isArray(this.ci[attr.name]) ? this.ci[attr.name] : this.ci[attr.name] ? [this.ci[attr.name]] : []
this.itsmInstalled = false if (ids.length) {
}) if (!map?.[attr.reference_type_id]) {
}, map[attr.reference_type_id] = {}
}
getCIHistory() { ids.forEach((id) => {
getCIHistory(this.ciId) map[attr.reference_type_id][id] = {}
.then((res) => { })
this.ciHistory = res }
}
const rowSpanMap = {} })
let startIndex = 0 })
let startCount = 1
res.forEach((item, index) => { if (!Object.keys(map).length) {
if (index === 0) { return
return }
}
if (res[index].record_id === res[startIndex].record_id) { const ciTypesRes = await getCITypes({
startCount += 1 type_ids: Object.keys(map).join(',')
rowSpanMap[index] = 0 })
if (index === res.length - 1) { const showAttrNameMap = {}
rowSpanMap[startIndex] = startCount ciTypesRes.ci_types.forEach((ciType) => {
} showAttrNameMap[ciType.id] = ciType?.show_name || ciType?.unique_name || ''
} else { })
rowSpanMap[startIndex] = startCount
startIndex = index const allRes = await Promise.all(
startCount = 1 Object.keys(map).map((key) => {
if (index === res.length - 1) { return searchCI({
rowSpanMap[index] = 1 q: `_type:${key},_id:(${Object.keys(map[key]).join(';')})`,
} count: 9999
} })
}) })
this.rowSpanMap = rowSpanMap )
})
.catch((e) => { const ciNameMap = {}
console.log(e) allRes.forEach((res) => {
}) res.result.forEach((item) => {
}, ciNameMap[item._id] = item
changeTab(key) { })
this.activeTabKey = key })
if (key === 'tab_3') {
this.$nextTick(() => { const newAttrGroups = _.cloneDeep(this.attributeGroups)
const $table = this.$refs.xTable
if ($table) { newAttrGroups.forEach((group) => {
const usernameColumn = $table.getColumnByField('username') group.attributes.forEach((attr) => {
const attrColumn = $table.getColumnByField('attr_alias') if (attr?.is_reference && attr?.reference_type_id) {
if (usernameColumn) { attr.showAttrName = showAttrNameMap?.[attr?.reference_type_id] || ''
const usernameList = [...new Set(this.ciHistory.map((item) => item.username))]
$table.setFilter( const referenceShowAttrNameMap = {}
usernameColumn, const referenceCIIds = this.ci[attr.name];
usernameList.map((item) => { (Array.isArray(referenceCIIds) ? referenceCIIds : referenceCIIds ? [referenceCIIds] : []).forEach((id) => {
return { referenceShowAttrNameMap[id] = ciNameMap?.[id]?.[attr.showAttrName] ?? id
value: item, })
label: item, attr.referenceShowAttrNameMap = referenceShowAttrNameMap
} }
}) })
) })
}
if (attrColumn) { this.$set(this, 'attributeGroups', newAttrGroups)
$table.setFilter( },
attrColumn,
this.attrList().map((attr) => { async getCI() {
return { value: attr.alias || attr.name, label: attr.alias || attr.name } await getCIById(this.ciId)
}) .then((res) => {
) if (res.result.length) {
} this.ci = res.result[0]
} } else {
}) this.hasPermission = false
} }
}, })
filterUsernameMethod({ value, row, column }) { .catch((e) => {
return row.username === value if (e.response.status === 404) {
}, this.itsmInstalled = false
filterOperateMethod({ value, row, column }) { }
return Number(row.operate_type) === Number(value) })
}, },
filterAttrMethod({ value, row, column }) { async judgeItsmInstalled() {
return row.attr_alias === value await judgeItsmInstalled().catch((e) => {
}, this.itsmInstalled = false
refresh(editAttrName) { })
this.getCI() },
const _find = this.treeViewsLevels.find((level) => level.name === editAttrName)
// 修改的字段为树形视图订阅的字段 则全部reload getCIHistory() {
setTimeout(() => { getCIHistory(this.ciId)
if (_find) { .then((res) => {
if (this.reload) { this.ciHistory = res
this.reload()
} const rowSpanMap = {}
} else { let startIndex = 0
if (this.handleSearch) { let startCount = 1
this.handleSearch() res.forEach((item, index) => {
} if (index === 0) {
} return
}, 500) }
}, if (res[index].record_id === res[startIndex].record_id) {
mergeRowMethod({ row, _rowIndex, column, visibleData }) { startCount += 1
const fields = ['created_at', 'username'] rowSpanMap[index] = 0
const cellValue1 = row['created_at'] if (index === res.length - 1) {
const cellValue2 = row['username'] rowSpanMap[startIndex] = startCount
if (cellValue1 && cellValue2 && fields.includes(column.property)) { }
const prevRow = visibleData[_rowIndex - 1] } else {
let nextRow = visibleData[_rowIndex + 1] rowSpanMap[startIndex] = startCount
if (prevRow && prevRow['created_at'] === cellValue1 && prevRow['username'] === cellValue2) { startIndex = index
return { rowspan: 0, colspan: 0 } startCount = 1
} else { if (index === res.length - 1) {
let countRowspan = 1 rowSpanMap[index] = 1
while (nextRow && nextRow['created_at'] === cellValue1 && nextRow['username'] === cellValue2) { }
nextRow = visibleData[++countRowspan + _rowIndex] }
} })
if (countRowspan > 1) { this.rowSpanMap = rowSpanMap
return { rowspan: countRowspan, colspan: 1 } })
} .catch((e) => {
} console.log(e)
} })
}, },
updateCIByself(params, editAttrName) { changeTab(key) {
const _ci = { ..._.cloneDeep(this.ci), ...params } this.activeTabKey = key
this.ci = _ci if (key === 'tab_3') {
const _find = this.treeViewsLevels.find((level) => level.name === editAttrName) this.$nextTick(() => {
// 修改的字段为树形视图订阅的字段 则全部reload const $table = this.$refs.xTable
setTimeout(() => { if ($table) {
if (_find) { const usernameColumn = $table.getColumnByField('username')
if (this.reload) { const attrColumn = $table.getColumnByField('attr_alias')
this.reload() if (usernameColumn) {
} const usernameList = [...new Set(this.ciHistory.map((item) => item.username))]
} else { $table.setFilter(
if (this.handleSearch) { usernameColumn,
this.handleSearch() usernameList.map((item) => {
} return {
} value: item,
}, 500) label: item,
}, }
shareCi() { })
const text = `${document.location.host}/cmdb/cidetail/${this.typeId}/${this.ciId}` )
this.$copyText(text) }
.then(() => { if (attrColumn) {
this.$message.success(this.$t('copySuccess')) $table.setFilter(
}) attrColumn,
.catch(() => { this.attrList().map((attr) => {
this.$message.error(this.$t('cmdb.ci.copyFailed')) return { value: attr.alias || attr.name, label: attr.alias || attr.name }
}) })
}, )
handleRollbackCI() { }
this.$nextTick(() => { }
this.$refs.ciRollbackForm.onOpen() })
}) }
}, },
}, filterUsernameMethod({ value, row, column }) {
} return row.username === value
</script> },
filterOperateMethod({ value, row, column }) {
<style lang="less"> return Number(row.operate_type) === Number(value)
.ci-detail-tab { },
height: 100%; filterAttrMethod({ value, row, column }) {
.ant-tabs-content { return row.attr_alias === value
height: calc(100% - 45px); },
.ant-tabs-tabpane { refresh(editAttrName) {
height: 100%; this.getCI()
} const _find = this.treeViewsLevels.find((level) => level.name === editAttrName)
} // 修改的字段为树形视图订阅的字段 则全部reload
.ant-tabs-bar { setTimeout(() => {
margin: 0; if (_find) {
} if (this.reload) {
.ant-tabs-extra-content { this.reload()
line-height: 44px; }
} } else {
.ci-detail-attr { if (this.handleSearch) {
height: 100%; this.handleSearch()
overflow: auto; }
padding: 24px; }
.el-descriptions-item__content { }, 500)
cursor: default; },
&:hover a { mergeRowMethod({ row, _rowIndex, column, visibleData }) {
opacity: 1 !important; const fields = ['created_at', 'username']
} const cellValue1 = row['created_at']
} const cellValue2 = row['username']
.el-descriptions:first-child > .el-descriptions__header { if (cellValue1 && cellValue2 && fields.includes(column.property)) {
margin-top: 0; const prevRow = visibleData[_rowIndex - 1]
} let nextRow = visibleData[_rowIndex + 1]
.el-descriptions__header { if (prevRow && prevRow['created_at'] === cellValue1 && prevRow['username'] === cellValue2) {
margin-bottom: 5px; return { rowspan: 0, colspan: 0 }
margin-top: 20px; } else {
} let countRowspan = 1
.ant-form-item { while (nextRow && nextRow['created_at'] === cellValue1 && nextRow['username'] === cellValue2) {
margin-bottom: 0; nextRow = visibleData[++countRowspan + _rowIndex]
} }
.ant-form-item-control { if (countRowspan > 1) {
line-height: 19px; return { rowspan: countRowspan, colspan: 1 }
} }
} }
} }
</style> },
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) {
if (this.reload) {
this.reload()
}
} else {
if (this.handleSearch) {
this.handleSearch()
}
}
}, 500)
},
shareCi() {
const text = `${document.location.host}/cmdb/cidetail/${this.typeId}/${this.ciId}`
this.$copyText(text)
.then(() => {
this.$message.success(this.$t('copySuccess'))
})
.catch(() => {
this.$message.error(this.$t('cmdb.ci.copyFailed'))
})
},
handleRollbackCI() {
this.$nextTick(() => {
this.$refs.ciRollbackForm.onOpen()
})
},
async handleExport() {
this.$refs.xTable.exportData({
filename: this.$t('cmdb.ci.history'),
sheetName: 'Sheet1',
type: 'xlsx',
types: ['xlsx', 'csv', 'html', 'xml', 'txt'],
data: this.ciHistory,
isMerge: true,
isColgroup: true,
})
}
},
}
</script>
<style lang="less">
.ci-detail-tab {
height: 100%;
.ant-tabs-content {
height: calc(100% - 45px);
.ant-tabs-tabpane {
height: 100%;
}
}
.ant-tabs-bar {
margin: 0;
}
.ant-tabs-extra-content {
line-height: 44px;
}
.ci-detail-attr {
height: 100%;
overflow: auto;
padding: 24px;
.el-descriptions-item__content {
cursor: default;
&:hover a {
opacity: 1 !important;
}
}
.el-descriptions:first-child > .el-descriptions__header {
margin-top: 0;
}
.el-descriptions__header {
margin-bottom: 5px;
margin-top: 20px;
}
.ant-form-item {
margin-bottom: 0;
}
.ant-form-item-control {
line-height: 19px;
}
}
}
</style>

View File

@@ -16,24 +16,40 @@
:key="attr.name + attr_idx" :key="attr.name + attr_idx"
> >
<a-form-item :label="attr.alias || attr.name" :colon="false"> <a-form-item :label="attr.alias || attr.name" :colon="false">
<CIReferenceAttr
v-if="attr.is_reference"
:referenceTypeId="attr.reference_type_id"
:isList="attr.is_list"
v-decorator="[
attr.name,
{
rules: [{ required: attr.is_required, message: $t('placeholder2') + `${attr.alias || attr.name}` }],
initialValue: attr.is_list ? [] : ''
}
]"
/>
<a-switch
v-else-if="attr.is_bool"
v-decorator="[
attr.name,
{
rules: [{ required: false }],
valuePropName: 'checked',
initialValue: attr.default ? Boolean(attr.default.default) : false
}
]"
/>
<a-select <a-select
:style="{ width: '100%' }" :style="{ width: '100%' }"
v-decorator="[ v-decorator="[
attr.name, attr.name,
{ {
rules: [{ required: attr.is_required, message: $t('placeholder2') + `${attr.alias || attr.name}` }], rules: [{ required: attr.is_required, message: $t('placeholder2') + `${attr.alias || attr.name}` }],
initialValue: initialValue: getChoiceDefault(attr),
attr.default && attr.default.default
? attr.is_list
? Array.isArray(attr.default.default)
? attr.default.default
: attr.default.default.split(',')
: attr.default.default
: attr.is_list ? [] : null,
}, },
]" ]"
:placeholder="$t('placeholder2')" :placeholder="$t('placeholder2')"
v-if="attr.is_choice" v-else-if="attr.is_choice"
:mode="attr.is_list ? 'multiple' : 'default'" :mode="attr.is_list ? 'multiple' : 'default'"
showSearch showSearch
allowClear allowClear
@@ -56,7 +72,9 @@
:type="choice[1].icon.name" :type="choice[1].icon.name"
/> />
</template> </template>
{{ choice[0] }} <a-tooltip placement="topLeft" :title="choice[1] ? choice[1].label || choice[0] : choice[0]">
{{ choice[1] ? choice[1].label || choice[0] : choice[0] }}
</a-tooltip>
</span> </span>
</a-select-option> </a-select-option>
</a-select> </a-select>
@@ -67,7 +85,7 @@
attr.name, attr.name,
{ {
rules: [{ required: attr.is_required, message: $t('placeholder2') + `${attr.alias || attr.name}` }], rules: [{ required: attr.is_required, message: $t('placeholder2') + `${attr.alias || attr.name}` }],
initialValue: attr.default && attr.default.default ? attr.default.default : '', initialValue: attr.default && attr.default.default ? Array.isArray(attr.default.default) ? attr.default.default.join(',') : attr.default.default : '',
}, },
]" ]"
> >
@@ -130,13 +148,16 @@
</template> </template>
<script> <script>
import _ from 'lodash'
import moment from 'moment' import moment from 'moment'
import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue' import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue'
import CIReferenceAttr from '@/components/ciReferenceAttr/index.vue'
export default { export default {
name: 'CreateInstanceFormByGroup', name: 'CreateInstanceFormByGroup',
components: { JsonEditor }, components: {
JsonEditor,
CIReferenceAttr
},
props: { props: {
group: { group: {
type: Object, type: Object,
@@ -146,6 +167,10 @@ export default {
type: Array, type: Array,
default: () => [], default: () => [],
}, },
ciTypeId: {
type: [Number, String],
default: ''
}
}, },
inject: ['getFieldType'], inject: ['getFieldType'],
data() { data() {
@@ -183,6 +208,35 @@ export default {
jsonEditorOk(jsonData) { jsonEditorOk(jsonData) {
this.form.setFieldsValue({ [this.editAttr.name]: JSON.stringify(jsonData) }) this.form.setFieldsValue({ [this.editAttr.name]: JSON.stringify(jsonData) })
}, },
getChoiceDefault(attr) {
if (!attr?.default?.default) {
return attr.is_list ? [] : null
}
if (attr.is_list) {
let defaultValue = []
if (Array.isArray(attr.default.default)) {
defaultValue = attr.default.default
} else {
defaultValue = String(attr.default.default).split(',')
}
if (['0', '1', '11'].includes(attr.value_type)) {
defaultValue = defaultValue?.map((item) => {
const numberValue = Number(item)
return Number.isNaN(numberValue) ? item : numberValue
})
}
return defaultValue
}
let defaultValue = attr.default.default
if (['0', '1', '11'].includes(attr.value_type)) {
const numberValue = Number(defaultValue)
defaultValue = Number.isNaN(numberValue) ? attr.default.default : numberValue
}
return defaultValue
}
}, },
} }
</script> </script>

View File

@@ -92,6 +92,10 @@ export default {
font-size: 14px; font-size: 14px;
font-weight: 400; font-weight: 400;
&:hover {
color: @primary-color;
}
&:not(:first-child) { &:not(:first-child) {
border-left: solid 1px @border-color-base; border-left: solid 1px @border-color-base;
} }
@@ -99,6 +103,10 @@ export default {
&_active { &_active {
background-color: @primary-color; background-color: @primary-color;
color: #FFFFFF; color: #FFFFFF;
&:hover {
color: #FFFFFF;
}
} }
} }
} }

View File

@@ -0,0 +1,147 @@
<template>
<CustomDrawer
:title="$t('cmdb.ciType.viewAllAttr')"
:visible="visible"
placement="right"
width="800"
:bodyStyle="{ height: '100vh' }"
@close="handleClose"
>
<vxe-table
resizable
size="mini"
:span-method="mergeRowMethod"
:data="tableData"
show-overflow
show-header-overflow
border
class="ops-stripe-table"
:height="windowHeight - 160"
>
<vxe-table-column align="center" field="groupId" :title="$t('cmdb.ciType.attrGroup')" :width="100">
<template #default="{row}">
<span>{{ row.groupName }}</span>
</template>
</vxe-table-column>
<vxe-table-column field="name" :title="$t('cmdb.ciType.attrName')" :width="150"></vxe-table-column>
<vxe-table-column field="alias" :title="$t('cmdb.ciType.attrAlias')" :width="150"></vxe-table-column>
<vxe-table-column field="typeText" :title="$t('type')" :width="100"></vxe-table-column>
<vxe-table-column field="code" :title="$t('cmdb.ciType.attrCode')">
<template #default="{row}">
<a @click="copyText(row.code)" >{{ row.code }}</a>
</template>
</vxe-table-column>
</vxe-table>
</CustomDrawer>
</template>
<script>
import { mapState } from 'vuex'
import _ from 'lodash'
import { valueTypeMap } from '@/modules/cmdb/utils/const'
import { getPropertyType } from '@/modules/cmdb/utils/helper'
export default {
name: 'AllAttrDrawer',
data() {
return {
visible: false,
tableData: [],
}
},
inject: {
providerGroupsData: {
default: () => {
return () => {
return {
CITypeGroups: [],
otherGroupAttributes: [],
}
}
}
}
},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
}),
},
methods: {
async open() {
this.visible = true
const tableData = []
const typeMap = valueTypeMap()
const providerGroupsData = _.cloneDeep(this.providerGroupsData() || {})
const groupsData = providerGroupsData?.CITypeGroups || []
const otherAttrData = providerGroupsData?.otherGroupAttributes || []
groupsData.forEach((group) => {
if (group?.attributes?.length) {
const attrArr = group.attributes.map((attr) => {
if (attr.is_password) {
attr.value_type = '7'
}
if (attr.is_link) {
attr.value_type = '8'
}
attr.groupId = group.id
attr.groupName = group.name
attr.code = ['0', '1', '6'].includes(attr.value_type) ? `{{ ${attr.name} }}` : `'''{{ ${attr.name} }}'''`
attr.typeText = typeMap?.[attr.value_type] ?? ''
return attr
})
tableData.push(...attrArr)
}
})
otherAttrData.forEach((attr) => {
attr.value_type = getPropertyType(attr)
attr.groupId = -1
attr.groupName = this.$t('other')
attr.code = `{{ ${attr.name} }}`
attr.typeText = typeMap?.[attr.value_type] ?? ''
})
tableData.push(...otherAttrData)
this.tableData = tableData
},
mergeRowMethod({ row, _rowIndex, column, visibleData }) {
const fields = ['groupId']
const currentValue = row.groupId
if (currentValue && fields.includes(column.property)) {
const prevRow = visibleData[_rowIndex - 1]
let nextRow = visibleData[_rowIndex + 1]
if (prevRow && prevRow.groupId === currentValue) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow.groupId === currentValue) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
},
handleClose() {
this.visible = false
},
copyText(text) {
this.$copyText(text)
.then(() => {
this.$message.success(this.$t('copySuccess'))
})
}
}
}
</script>
<style lang="less" scoped>
</style>

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