mirror of
https://github.com/veops/cmdb.git
synced 2025-09-03 03:06:56 +08:00
Compare commits
26 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
be4fc62218 | ||
|
ff4ce4dbe0 | ||
|
dda1fce46a | ||
|
fbf59e7b44 | ||
|
4ae67d1f0f | ||
|
b56cf5bb3d | ||
|
53e8d34c68 | ||
|
c62e4032e3 | ||
|
108c11071a | ||
|
debb25f65b | ||
|
f26dd65d07 | ||
|
cb2726c890 | ||
|
2003fd4a48 | ||
|
1bbf8c10b5 | ||
|
93e919b73f | ||
|
435bb2a2c8 | ||
|
5ceb8ff6f9 | ||
|
47332aca3c | ||
|
f24cb55585 | ||
|
f1594550e0 | ||
|
a025c844bc | ||
|
1a03a0b800 | ||
|
e35efea712 | ||
|
3f1e8beae8 | ||
|
3ea81987a1 | ||
|
6a20e2f578 |
13
CODE_OF_CONDUCT.md
Normal file
13
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Contributor Code of Conduct
|
||||
|
||||
As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
|
||||
|
||||
We are committed to making participation in this project a harassment-free experience for everyone, regardless of the level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion.
|
||||
|
||||
Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant](https://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
|
196
README.md
196
README.md
@@ -1,116 +1,138 @@
|
||||
|
||||
<p align="center">
|
||||
<a href="https://veops.cn"><img src="docs/images/logo.png" alt="维易CMDB" width="300"/></a>
|
||||
<a href="https://veops.cn">
|
||||
<img src="https://github.com/user-attachments/assets/c5cfb272-899b-418d-9e69-8e1dd07db0f6" alt="维易CMDB"/>
|
||||
</a>
|
||||
</p>
|
||||
<h3 align="center">简单、轻量、通用的运维配置管理数据库</h3>
|
||||
|
||||
<h4 align="center">简单、轻量、通用的运维配置管理数据库</h4>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/veops/cmdb/blob/master/LICENSE"><img src="https://img.shields.io/badge/License-AGPLv3-brightgreen" alt="License: GPLv3"></a>
|
||||
<a href="https:https://github.com/sendya/ant-design-pro-vue"><img src="https://img.shields.io/badge/UI-Ant%20Design%20Pro%20Vue-brightgreen" alt="UI"></a>
|
||||
<a href="https://github.com/pallets/flask"><img src="https://img.shields.io/badge/API-Flask-brightgreen" alt="API"></a>
|
||||
<a href="https://github.com/veops/cmdb/releases"><img alt="the latest release version" src="https://img.shields.io/github/v/release/veops/cmdb?color=75C1C4&include_prereleases&label=Release&logo=github&logoColor=white"></a>
|
||||
<a href="https:https://github.com/sendya/ant-design-pro-vue"><img src="https://img.shields.io/badge/UI-Ant%20Design%20Pro%20Vue-green" alt="UI"></a>
|
||||
<a href="https://github.com/pallets/flask"><img src="https://img.shields.io/badge/API-Flask-bright" alt="API"></a>
|
||||
<a href="https://github.com/veops/cmdb/stargazers"><img src="https://img.shields.io/github/stars/veops/cmdb" alt="Stars Badge"/></a>
|
||||
<a href="https://github.com/veops/cmdb"><img src="https://img.shields.io/github/forks/veops/cmdb" alt="Forks Badge"/></a>
|
||||
</p>
|
||||
<p align="center">
|
||||
中文(简体) · <a href="docs/README_en.md">English</a>
|
||||
</p>
|
||||
|
||||
|
||||
------------------------------
|
||||
|
||||
[English](docs/README_en.md) / [中文](README.md)
|
||||
- 产品文档:https://veops.cn/docs/
|
||||
- 在线体验:<a href="https://cmdb.veops.cn" target="_blank">CMDB</a>
|
||||
- username: demo 或者 admin
|
||||
- password: 123456
|
||||
|
||||
> **重要提示**: `master` 分支在开发过程中可能处于 _不稳定的状态_ 。
|
||||
> 请通过[releases](https://github.com/veops/cmdb/releases)获取
|
||||
|
||||
## 系统介绍
|
||||
|
||||
### 系统概览
|
||||
维易CMDB是一个简洁、轻量且高度可定制的运维配置管理数据库(CMDB)。它支持灵活的模型配置和资源自动发现,旨在为企业提供便捷的资产管理解决方案,帮助运维团队高效地管理 IT 基础设施和服务。
|
||||
|
||||
<img src=docs/images/dashboard.png />
|
||||
|
||||
|
||||
### 相关文章
|
||||
|
||||
- <a href="https://mp.weixin.qq.com/s/v3eANth64UBW5xdyOkK3tg" target="_blank">概要设计</a>
|
||||
- <a href="https://github.com/veops/cmdb/tree/master/docs/cmdb_api.md" target="_blank">API 文档</a>
|
||||
- <a href="https://mp.weixin.qq.com/s/rQaf4AES7YJsyNQG_MKOLg" target="_blank">自动发现</a>
|
||||
- 更多文章可以在公众号 **维易科技OneOps** 里查看
|
||||
|
||||
### 特点
|
||||
|
||||
- 灵活性
|
||||
1. 配置灵活,不设定任何运维场景,有内置模板
|
||||
2. 自动发现、入库 IT 资产
|
||||
- 安全性
|
||||
1. 细粒度权限控制
|
||||
2. 完备操作日志
|
||||
- 多应用
|
||||
1. 丰富视图展示维度
|
||||
2. API简单强大
|
||||
3. 支持定义属性触发器、计算属性
|
||||
- 产品文档:[https://veops.cn/docs/](https://veops.cn/docs/)
|
||||
- 在线体验:[https://cmdb.veops.cn](https://cmdb.veops.cn)
|
||||
- 用户名:demo 或者 admin
|
||||
- 密码:123456
|
||||
- **重要提示**:`master` 分支在开发过程中可能处于**不稳定的状态**。请通过 [releases](https://github.com/veops/cmdb/releases) 获取最新稳定版本。
|
||||
|
||||
### 主要功能
|
||||
|
||||
- 自定义模型和模型关系,模型属性支持下拉列表、字体颜色、计算属性等高级特性
|
||||
- 支持计算机、网络设备、存储设备、数据库、中间件、公有云资源等自动发现
|
||||
- 支持资源、层级、关系视图展示
|
||||
- 细粒度访问控制,完备的操作日志
|
||||
- 通用的资源搜索和关系搜索
|
||||
- 支持IP地址管理(IPAM), 数据中心基础设施管理(DCIM)
|
||||
- **自定义模型和模型关系**:支持模型属性的自定义,包括下拉列表、字体颜色、计算属性等高级功能,满足不同业务需求。
|
||||
- **自动发现资源**:支持计算机、网络设备、存储设备、数据库、中间件、公有云资源等自动发现。
|
||||
- **多维度视图展示**:包括资源视图、层级视图、关系视图等,帮助运维人员全面管理资源。
|
||||
- **细粒度权限控制**:通过精确的访问控制和完备的操作日志保障系统的安全性。
|
||||
- **全面的资源搜索功能**:支持灵活的资源和关系搜索,快速定位和操作资源。
|
||||
- **集成 IP 地址管理(IPAM)和数据中心基础设施管理(DCIM)**:简化网络资源和数据中心设备的管理。
|
||||
|
||||
更多详细功能,请移步 [维易科技官网](https://veops.cn) 进行了解。
|
||||
|
||||
### 系统优势
|
||||
|
||||
### 更多功能
|
||||
- 灵活性
|
||||
+ 无需指定固定运维场景,支持自由配置并内置多种模板
|
||||
+ 支持自动发现和入库 IT 资产,快速搭建资产管理系统
|
||||
- 安全性
|
||||
+ 细粒度的权限控制机制,确保资源管理的安全性
|
||||
+ 完整的操作日志记录,便于审计和问题追踪
|
||||
- 多应用
|
||||
+ 提供多种视图展示方式,满足不同场景的需求
|
||||
+ 强大的 API 接口,支持深度集成
|
||||
+ 支持定义属性触发器和计算属性,增强数据处理能力
|
||||
|
||||
> 也欢迎移步[维易科技官网](https://veops.cn),发现更多免费运维系统。
|
||||
### 技术栈
|
||||
|
||||
+ 后端:Python [3.8-3.11]
|
||||
+ 数据存储:MySQL、Redis
|
||||
+ 前端:Vue.js
|
||||
+ UI组件库:Ant Design Vue
|
||||
|
||||
### 系统概览
|
||||
|
||||
<table style="border-collapse: collapse;">
|
||||
<tr>
|
||||
<td style="padding: 5px;background-color:#fff;">
|
||||
<img width="400" src="https://github.com/user-attachments/assets/6d2df835-ae93-4d91-9bd9-213c270eca7a"/>
|
||||
</td>
|
||||
<td style="padding: 5px;background-color:#fff;">
|
||||
<img width="400" src="https://github.com/user-attachments/assets/cb8b598a-a1f9-4c74-adf1-6e59aea2c9b3"/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td style="padding: 5px;background-color:#fff;">
|
||||
<img width="400" src="https://github.com/user-attachments/assets/b440224f-53c3-4b7f-a9be-285d7a4b848f"/>
|
||||
</td>
|
||||
<td style="padding: 5px;background-color:#fff;">
|
||||
<img width="400" src="https://github.com/user-attachments/assets/f457d5a0-b60b-4949-b94e-020f4c61444b"/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 关注我们
|
||||
|
||||
欢迎 Star 加关注,第一时间获取更新动态!
|
||||
|
||||

|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 搭建
|
||||
|
||||
+ 方案一:Docker 一键快速构建
|
||||
|
||||
- 第1步: 安装 Docker 环境和 Docker Compose(v2)
|
||||
- 第2步: 拷贝项目代码, `git clone https://github.com/veops/cmdb.git`
|
||||
- 第3步:进入主目录并启动, `docker compose up -d`
|
||||
|
||||
+ 方案二:[本地开发环境搭建](docs/local.md)
|
||||
+ 方案三:[Makefile 安装](docs/makefile.md)
|
||||
|
||||
### 2. 访问
|
||||
- 打开浏览器并访问: [http://127.0.0.1:8000](http://127.0.0.1:8000)
|
||||
- 用户名: demo 或者 admin
|
||||
- 密码: 123456
|
||||
|
||||
## 接入公司
|
||||
|
||||
> 欢迎使用开源CMDB的公司,在 [#112](https://github.com/veops/cmdb/issues/112) 登记
|
||||
+ 欢迎使用开源CMDB的公司和团队,在 [#112](https://github.com/veops/cmdb/issues/112) 登记
|
||||
|
||||
## 安装
|
||||
## 代码贡献
|
||||
我们欢迎所有开发者贡献代码,改善和扩展这个项目。请先阅读我们的[贡献指南](docs/CONTRIBUTING.md)。此外,您还可以通过社交媒体、活动和分享来支持 Veops 的开源。
|
||||
|
||||
### Docker 一键快速构建
|
||||
<a href="https://github.com/veops/cmdb/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=veops/cmdb" />
|
||||
</a>
|
||||
|
||||
[//]: # (> 方法一)
|
||||
- 第一步: 先安装 Docker 环境, 以及Docker Compose (v2)
|
||||
- 第二步: 拷贝项目
|
||||
```shell
|
||||
git clone https://github.com/veops/cmdb.git
|
||||
```
|
||||
- 第三步:进入主目录,执行:
|
||||
```
|
||||
docker compose up -d
|
||||
```
|
||||
## 更多开源
|
||||
|
||||
[//]: # (> 方法二, 该方法适用于linux系统)
|
||||
- [OneTerm](https://github.com/veops/oneterm): 一款简单、轻量、灵活的堡垒机服务。
|
||||
- [messenger](https://github.com/veops/messenger): 一个简单轻量的消息发送服务。
|
||||
- [ACL](https://github.com/veops/acl): 一个简单通用的权限管理系统设计与实践。
|
||||
|
||||
[//]: # (- 第一步: 先安装 Docker 环境, 以及Docker Compose (v2))
|
||||
## 相关文章
|
||||
|
||||
[//]: # (- 第二步: 直接使用项目根目录下的install.sh 文件进行 `安装`、`启动`、`暂停`、`查状态`、`删除`、`卸载`)
|
||||
- <a href="https://mp.weixin.qq.com/s/v3eANth64UBW5xdyOkK3tg" target="_blank">尽可能通用的运维CMDB的设计与实践(Ⅰ) - 概览</a>
|
||||
- <a href="https://mp.weixin.qq.com/s/rQaf4AES7YJsyNQG_MKOLg" target="_blank">尽可能通用的运维CMDB的设计与实践(ⅠⅠ) - 自动发现</a>
|
||||
- <a href="https://github.com/veops/cmdb/tree/master/docs/cmdb_api.md" target="_blank">CMDB接口文档</a>
|
||||
|
||||
[//]: # (```shell)
|
||||
更多文章可以在公众号 **维易科技OneOps** 里查看
|
||||
|
||||
[//]: # (curl -so install.sh https://raw.githubusercontent.com/veops/cmdb/deploy_on_kylin_docker/install.sh)
|
||||
## 与我联系
|
||||
|
||||
[//]: # (sh install.sh install)
|
||||
|
||||
[//]: # (```)
|
||||
|
||||
### [本地开发环境搭建](docs/local.md)
|
||||
|
||||
### [Makefile 安装](docs/makefile.md)
|
||||
|
||||
## 验证
|
||||
- 浏览器打开: [http://127.0.0.1:8000](http://127.0.0.1:8000)
|
||||
- username: demo 或者 admin
|
||||
- password: 123456
|
||||
|
||||
|
||||
---
|
||||
|
||||
_**欢迎关注公众号(维易科技OneOps),关注后可加入微信群,进行产品和技术交流。**_
|
||||
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/wechat.png" alt="公众号: 维易科技OneOps" />
|
||||
</p>
|
||||
+ 邮箱: <a href="mailto:bd@veops.cn">bd@veops.cn</a>
|
||||
+ 公众号:**维易科技OneOps**。关注后可以加入微信群,参与产品和技术交流
|
||||
<img src="docs/images/wechat.png" alt="公众号: 维易科技OneOps" />
|
@@ -12,6 +12,7 @@ from sqlalchemy import func
|
||||
from api.extensions import db
|
||||
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 NET_DEVICE_NAMES
|
||||
from api.lib.cmdb.auto_discovery.const import PRIVILEGED_USERS
|
||||
from api.lib.cmdb.cache import AttributeCache
|
||||
from api.lib.cmdb.cache import AutoDiscoveryMappingCache
|
||||
@@ -252,6 +253,7 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
|
||||
:return:
|
||||
"""
|
||||
result = []
|
||||
db.session.commit()
|
||||
rules = cls.cls.get_by(to_dict=True)
|
||||
|
||||
for rule in rules:
|
||||
@@ -718,6 +720,12 @@ class AutoDiscoveryCICRUD(DBMixin):
|
||||
|
||||
build_relations_for_ad_accept.apply_async(args=(adc.to_dict(), ci_id, ad_key2attr), queue=CMDB_QUEUE)
|
||||
|
||||
ci_type = CITypeCache.get(adc.type_id)
|
||||
if ci_type and ci_type.name in NET_DEVICE_NAMES and 'ports' in adc.instance:
|
||||
from api.tasks.cmdb import add_net_device_ports
|
||||
add_net_device_ports.apply_async(args=(ci_id, adc.instance['ports']),
|
||||
queue=CMDB_QUEUE)
|
||||
|
||||
adc.update(is_accept=True,
|
||||
accept_by=nickname or current_user.nickname,
|
||||
accept_time=datetime.datetime.now(),
|
||||
|
@@ -4,6 +4,8 @@ from api.lib.cmdb.const import AutoDiscoveryType
|
||||
|
||||
PRIVILEGED_USERS = ("cmdb_agent", "worker", "admin")
|
||||
|
||||
NET_DEVICE_NAMES = {"switch", 'router', 'firewall', 'printer'}
|
||||
|
||||
DEFAULT_INNER = [
|
||||
dict(name="阿里云", en="aliyun", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
|
||||
option={'icon': {'name': 'caise-aliyun'}, "en": "aliyun"}),
|
||||
@@ -41,8 +43,12 @@ DEFAULT_INNER = [
|
||||
option={'icon': {'name': 'caise-luyouqi'}}),
|
||||
dict(name="防火墙", type=AutoDiscoveryType.SNMP, is_inner=True, is_plugin=False,
|
||||
option={'icon': {'name': 'caise-fanghuoqiang'}}),
|
||||
dict(name="打印机", type=AutoDiscoveryType.SNMP, is_inner=True, is_plugin=False,
|
||||
option={'icon': {'name': 'caise-dayinji'}}),
|
||||
# dict(name="打印机", type=AutoDiscoveryType.SNMP, is_inner=True, is_plugin=False,
|
||||
# option={'icon': {'name': 'caise-dayinji'}}),
|
||||
dict(name="光纤交换机", type=AutoDiscoveryType.SNMP, is_inner=True, is_plugin=False,
|
||||
option={'icon': {'name': 'caise-fiber'}}),
|
||||
dict(name="F5", type=AutoDiscoveryType.SNMP, is_inner=True, is_plugin=False,
|
||||
option={'icon': {'name': 'caise-F5'}}),
|
||||
]
|
||||
|
||||
CLOUD_MAP = {
|
||||
|
@@ -1,37 +1,74 @@
|
||||
[{
|
||||
"name":"manufacturer",
|
||||
"type": "文本",
|
||||
"example":"HUAWEI Technology Co.,Ltd",
|
||||
"desc":"制造产商"
|
||||
},{
|
||||
"name":"sn",
|
||||
"type": "文本",
|
||||
"example":"102030059898",
|
||||
"desc":"设备序列号"
|
||||
},{
|
||||
"name":"device_name",
|
||||
"type": "文本",
|
||||
"example":"USG6525E",
|
||||
"desc":"设备名称"
|
||||
},{
|
||||
"name":"device_model",
|
||||
"type": "文本",
|
||||
"example":"2011.2.321.1.205",
|
||||
"desc":"设备细分类型 结合相关产商获取相应的产品类型"
|
||||
},{
|
||||
"name":"description",
|
||||
"type": "文本",
|
||||
"example":"Huawei Vwersatile Routing Platform Software",
|
||||
"desc":"设备描述"
|
||||
},{
|
||||
"name":"manager_ip",
|
||||
"type": "文本",
|
||||
"example":"192.168.1.1",
|
||||
"desc":"管理ip"
|
||||
}, {
|
||||
"name":"ips",
|
||||
"type": "文本、多值",
|
||||
"example":"192.168.1.1, 192.168.1.2",
|
||||
"desc":"ips"
|
||||
}
|
||||
[
|
||||
{
|
||||
"name": "manufacturer",
|
||||
"type": "文本",
|
||||
"example": "Huawei",
|
||||
"desc": "制造产商"
|
||||
},
|
||||
{
|
||||
"name": "sn",
|
||||
"type": "文本",
|
||||
"example": "102030059898",
|
||||
"desc": "设备序列号"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"type": "文本",
|
||||
"example": "USG6525E",
|
||||
"desc": "设备名称"
|
||||
},
|
||||
{
|
||||
"name": "model",
|
||||
"type": "文本",
|
||||
"example": "2011.2.321.1.205",
|
||||
"desc": "设备细分类型 结合相关产商获取相应的产品类型"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"type": "文本",
|
||||
"example": "Huawei Vwersatile Routing Platform Software",
|
||||
"desc": "设备描述"
|
||||
},
|
||||
{
|
||||
"name": "manager_ip",
|
||||
"type": "文本",
|
||||
"example": "192.168.1.1",
|
||||
"desc": "管理ip"
|
||||
},
|
||||
{
|
||||
"name": "ips",
|
||||
"type": "文本、多值",
|
||||
"example": "192.168.1.1, 192.168.1.2",
|
||||
"desc": "ips"
|
||||
},
|
||||
{
|
||||
"name": "uptime",
|
||||
"type": "文本",
|
||||
"example": "2023-04-15 10:00:00",
|
||||
"desc": "启动时间"
|
||||
},
|
||||
{
|
||||
"name": "snmp_version",
|
||||
"type": "文本",
|
||||
"example": "v2c",
|
||||
"desc": "SNMP版本"
|
||||
},
|
||||
{
|
||||
"name": "port_num",
|
||||
"type": "整数",
|
||||
"example": 24,
|
||||
"desc": "端口数量"
|
||||
},
|
||||
{
|
||||
"name": "ports",
|
||||
"type": "json",
|
||||
"example": "",
|
||||
"desc": "设备的端口列表"
|
||||
},
|
||||
{
|
||||
"name": "neighbors",
|
||||
"type": "json",
|
||||
"example": "",
|
||||
"desc": "设备的邻居列表"
|
||||
}
|
||||
]
|
@@ -1399,6 +1399,7 @@ class CITypeTemplateManager(object):
|
||||
i.pop('order', None)
|
||||
i.pop('choice_web_hook', None)
|
||||
i.pop('choice_other', None)
|
||||
i.pop('choice_builtin', None)
|
||||
i.pop('order', None)
|
||||
i.pop('inherited_from', None)
|
||||
choice_value = i.pop('choice_value', None)
|
||||
|
@@ -3,7 +3,7 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import imp
|
||||
import importlib.util
|
||||
|
||||
import copy
|
||||
import jinja2
|
||||
@@ -180,14 +180,15 @@ class AttributeValueManager(object):
|
||||
|
||||
@staticmethod
|
||||
def _compute_attr_value_from_expr(expr, ci_dict):
|
||||
t = jinja2.Template(expr).render(ci_dict)
|
||||
|
||||
try:
|
||||
return eval(t)
|
||||
result = jinja2.Template(expr).render(ci_dict)
|
||||
return result
|
||||
except Exception as e:
|
||||
current_app.logger.warning(str(e))
|
||||
return t
|
||||
|
||||
current_app.logger.warning(
|
||||
f"Expression evaluation error - Expression: '{expr}'"
|
||||
f"Input parameters: {ci_dict}, Error type: {type(e).__name__}, Error message: {str(e)}"
|
||||
)
|
||||
return None
|
||||
@staticmethod
|
||||
def _compute_attr_value_from_script(script, ci_dict):
|
||||
script = jinja2.Template(script).render(ci_dict)
|
||||
@@ -198,11 +199,11 @@ class AttributeValueManager(object):
|
||||
|
||||
try:
|
||||
path = script_f.name
|
||||
dir_name, name = os.path.dirname(path), os.path.basename(path)[:-3]
|
||||
name = os.path.basename(path)[:-3]
|
||||
|
||||
fp, path, desc = imp.find_module(name, [dir_name])
|
||||
|
||||
mod = imp.load_module(name, fp, path, desc)
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
|
||||
if hasattr(mod, 'computed'):
|
||||
return mod.computed()
|
||||
|
@@ -376,6 +376,29 @@ def build_relations_for_ad_accept(adc, ci_id, ad_key2attr):
|
||||
pass
|
||||
|
||||
|
||||
@celery.task(name="cmdb.add_net_device_ports", queue=CMDB_QUEUE)
|
||||
@reconnect_db
|
||||
def add_net_device_ports(ci_id, ports):
|
||||
from api.lib.cmdb.ci import CIRelationManager
|
||||
from api.lib.cmdb.ci import CIManager
|
||||
from api.lib.cmdb.cache import CITypeCache
|
||||
|
||||
port_type = CITypeCache.get("net_port")
|
||||
if not port_type:
|
||||
current_app.logger.warning("CIType net port is not found")
|
||||
return
|
||||
|
||||
for port in ports:
|
||||
try:
|
||||
port_id = CIManager.add(port_type.id, is_auto_discovery=True, _is_admin=True, **port)
|
||||
|
||||
CIRelationManager.add(ci_id, port_id,
|
||||
valid=False,
|
||||
source=RelationSourceEnum.AUTO_DISCOVERY)
|
||||
except Exception as e:
|
||||
current_app.logger.warning("add_net_device_ports failed: {}".format(e))
|
||||
|
||||
|
||||
@celery.task(name="cmdb.dcim_calc_u_free_count", queue=CMDB_QUEUE)
|
||||
@reconnect_db
|
||||
def dcim_calc_u_free_count():
|
||||
|
@@ -30,6 +30,7 @@ from api.lib.cmdb.search import SearchError
|
||||
from api.lib.cmdb.search.ci import search as ci_search
|
||||
from api.lib.decorator import args_required
|
||||
from api.lib.decorator import args_validate
|
||||
from api.lib.exception import AbortException
|
||||
from api.lib.perm.acl.acl import has_perm_from_args
|
||||
from api.lib.utils import AESCrypto
|
||||
from api.lib.utils import get_page
|
||||
@@ -296,7 +297,10 @@ class AutoDiscoveryRuleSyncView(APIView):
|
||||
|
||||
rules, last_update_at1 = AutoDiscoveryCITypeCRUD.get(None, oneagent_id, oneagent_name, last_update_at)
|
||||
|
||||
subnet_scan_rules, last_update_at2 = SubnetManager().scan_rules(oneagent_id, last_update_at)
|
||||
try:
|
||||
subnet_scan_rules, last_update_at2 = SubnetManager().scan_rules(oneagent_id, last_update_at)
|
||||
except AbortException:
|
||||
subnet_scan_rules, last_update_at2 = [], ""
|
||||
|
||||
return self.jsonify(rules=rules,
|
||||
subnet_scan_rules=subnet_scan_rules,
|
||||
|
@@ -64,9 +64,13 @@ class CITypeView(APIView):
|
||||
ci_type['unique_name'] = ci_type['unique_id'] and AttributeCache.get(ci_type['unique_id']).name
|
||||
ci_types.append(ci_type)
|
||||
elif type_name is not None:
|
||||
ci_type = CITypeCache.get(type_name).to_dict()
|
||||
ci_type['parent_ids'] = CITypeInheritanceManager.get_parents(ci_type['id'])
|
||||
ci_types = [ci_type]
|
||||
ci_type = CITypeCache.get(type_name)
|
||||
if ci_type is not None:
|
||||
ci_type = ci_type.to_dict()
|
||||
ci_type['parent_ids'] = CITypeInheritanceManager.get_parents(ci_type['id'])
|
||||
ci_types = [ci_type]
|
||||
else:
|
||||
ci_types = []
|
||||
else:
|
||||
ci_types = CITypeManager().get_ci_types(q)
|
||||
count = len(ci_types)
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 249 KiB After Width: | Height: | Size: 1.1 MiB |
@@ -132,6 +132,7 @@ export default {
|
||||
/deep/ .ant-select-selection {
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
border: none;
|
||||
|
||||
.ant-select-selection__rendered {
|
||||
height: 28px;
|
||||
|
@@ -21,7 +21,7 @@
|
||||
</template>
|
||||
<span
|
||||
class="ci-icon-letter"
|
||||
v-else
|
||||
v-else-if="title"
|
||||
>
|
||||
<span>
|
||||
{{ title[0].toUpperCase() }}
|
||||
|
@@ -1,147 +0,0 @@
|
||||
<template>
|
||||
<div class="node-setting-wrap">
|
||||
<ops-table
|
||||
:data="nodes"
|
||||
size="mini"
|
||||
show-header-overflow
|
||||
:row-config="{ height: 42 }"
|
||||
border
|
||||
:min-height="78"
|
||||
>
|
||||
<vxe-column width="170" :title="$t('cmdb.ciType.nodeSettingIp')">
|
||||
<template #default="{ row }">
|
||||
<a-input v-model="row.ip"></a-input>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column width="170" :title="$t('cmdb.ciType.nodeSettingCommunity')">
|
||||
<template #default="{ row }">
|
||||
<a-input v-model="row.community"></a-input>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column width="170" :title="$t('cmdb.ciType.nodeSettingVersion')">
|
||||
<template #default="{ row }">
|
||||
<a-select
|
||||
v-model="row.version"
|
||||
:placeholder="$t('cmdb.ciType.nodeSettingVersionTip')"
|
||||
allowClear
|
||||
class="node-setting-select"
|
||||
>
|
||||
<a-select-option value="1">
|
||||
v1
|
||||
</a-select-option>
|
||||
<a-select-option value="2c">
|
||||
v2c
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column wdith="170">
|
||||
<template #default="{ row }">
|
||||
<div class="action">
|
||||
<a @click="() => copyNode(row.id)">
|
||||
<a-icon type="copy" />
|
||||
</a>
|
||||
<a @click="() => removeNode(row.id, 1)">
|
||||
<a-icon type="minus-circle" />
|
||||
</a>
|
||||
<a @click="addNode">
|
||||
<a-icon type="plus-circle" />
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
</vxe-column>
|
||||
</ops-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
export default {
|
||||
name: 'MonitorNodeSetting',
|
||||
props: {
|
||||
initNodes: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
form: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
nodes: [],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initNodesFunc() {
|
||||
this.nodes = _.cloneDeep(this.initNodes)
|
||||
},
|
||||
addNode() {
|
||||
const newNode = {
|
||||
id: uuidv4(),
|
||||
ip: '',
|
||||
community: 'public',
|
||||
version: '',
|
||||
}
|
||||
this.nodes.push(newNode)
|
||||
},
|
||||
removeNode(removeId, minLength) {
|
||||
if (this.nodes.length <= minLength) {
|
||||
this.$message.error('不可再删除!')
|
||||
return
|
||||
}
|
||||
const _idx = this.nodes.findIndex((item) => item.id === removeId)
|
||||
if (_idx > -1) {
|
||||
this.nodes.splice(_idx, 1)
|
||||
}
|
||||
},
|
||||
copyNode(id) {
|
||||
const copyNode = this.nodes.find((item) => item.id === id)
|
||||
if (copyNode) {
|
||||
const newNode = {
|
||||
...copyNode,
|
||||
id: uuidv4(),
|
||||
}
|
||||
this.nodes.push(newNode)
|
||||
}
|
||||
},
|
||||
getNodeValue() {
|
||||
const nodes = this.nodes.map((node) => {
|
||||
return _.pick(node, ['ip', 'community', 'version'])
|
||||
})
|
||||
return nodes
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.node-setting-wrap {
|
||||
margin-left: 17px;
|
||||
width: 600px;
|
||||
|
||||
.ant-row {
|
||||
/deep/ .ant-input-clear-icon {
|
||||
color: rgba(0,0,0,.25);
|
||||
|
||||
&:hover {
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.node-setting-select {
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
.action {
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
@@ -77,6 +77,7 @@ const cmdb_en = {
|
||||
confirmDeleteADT: 'Do you confirm to delete [{pluginName}]',
|
||||
attributeMap: 'Attribute mapping',
|
||||
nodeConfig: 'Node Configuration',
|
||||
scanningParameter: 'Scanning Parameter',
|
||||
autoDiscovery: 'AutoDiscovery',
|
||||
node: 'Node',
|
||||
adExecConfig: 'Execute configuration',
|
||||
@@ -253,6 +254,25 @@ const cmdb_en = {
|
||||
checkModalColumn4: 'Last checkup time',
|
||||
testModalTitle: 'Automated discovery testing',
|
||||
attrMapTableAttrPlaceholder: 'Please edit the name',
|
||||
SNMPConfiguration: 'SNMP Configuration',
|
||||
nodeList: 'Node List',
|
||||
defaultVersion: 'Default Version',
|
||||
defaultCommunity: 'Default Community',
|
||||
timeout: 'Timeout',
|
||||
retryCount: 'Retry Count',
|
||||
scanningConfiguration: 'Scanning Configuration',
|
||||
initialNode: 'Initial Node',
|
||||
defaultGateway: 'Default Gateway',
|
||||
recursiveOrNot: 'Recursive Or Not',
|
||||
recursiveTip: 'Scanning Configuration: When disabling recursion, the node list must be configured.',
|
||||
maximumDepth: 'Maximum Depth',
|
||||
snmpFormTip1: 'If SNMP is not the default, Community and version need to be configured separately',
|
||||
snmpFormTip2: 'Timeout for establishing SNMP connection',
|
||||
snmpFormTip3: 'Number of retries to establish an SNMP connection',
|
||||
snmpFormTip4: 'The first node to start scanning, or recursively from the default gateway if unconfigured',
|
||||
snmpFormTip5: 'Enabled by default to discover all network devices and topology relationships as much as possible, and disabled to scan only the devices in the node list',
|
||||
snmpFormTip6: 'Depth of network device topology',
|
||||
snmpFormTip7: 'The results of the scan are filtered with CIDR, not filtered if not configured. Format: 192.168.1.0/24',
|
||||
nodeSettingIp: 'Network device IP address',
|
||||
nodeSettingIpTip: 'Please enter the ip address',
|
||||
nodeSettingIpTip1: 'ip address format error',
|
||||
@@ -701,6 +721,7 @@ if __name__ == "__main__":
|
||||
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',
|
||||
cover: 'Cover',
|
||||
detail: 'Detail'
|
||||
},
|
||||
serviceTree: {
|
||||
remove: 'Remove',
|
||||
|
@@ -77,6 +77,7 @@ const cmdb_zh = {
|
||||
confirmDeleteADT: '确认删除 【{pluginName}】',
|
||||
attributeMap: '字段映射',
|
||||
nodeConfig: '节点配置',
|
||||
scanningParameter: '扫描参数',
|
||||
autoDiscovery: '自动发现属性',
|
||||
node: '节点',
|
||||
adExecConfig: '执行配置',
|
||||
@@ -253,6 +254,25 @@ const cmdb_zh = {
|
||||
checkModalColumn4: '最近检查时间',
|
||||
testModalTitle: '自动发现测试',
|
||||
attrMapTableAttrPlaceholder: '请编辑名称',
|
||||
SNMPConfiguration: 'SNMP配置',
|
||||
nodeList: '节点列表',
|
||||
defaultVersion: '默认版本',
|
||||
defaultCommunity: '默认 Community',
|
||||
timeout: '超时时间',
|
||||
retryCount: '重试次数',
|
||||
scanningConfiguration: '扫描配置',
|
||||
initialNode: '初始节点',
|
||||
defaultGateway: '默认网关',
|
||||
recursiveOrNot: '是否递归',
|
||||
recursiveTip: '扫描配置关闭递归时, 必须配置节点列表',
|
||||
maximumDepth: '最大深度',
|
||||
snmpFormTip1: '如果不是默认的SNMP, Community和版本需要单独配置',
|
||||
snmpFormTip2: '建立SNMP连接的超时时间',
|
||||
snmpFormTip3: '建立SNMP连接的重试次数',
|
||||
snmpFormTip4: '开始扫描的第一个节点,如果不配置则是从默认网关开始递归扫描',
|
||||
snmpFormTip5: '默认开启,表示尽可能发现所有网络设备和拓扑关系, 如果关闭,则仅扫描节点列表里的设备',
|
||||
snmpFormTip6: '网络设备拓扑的深度',
|
||||
snmpFormTip7: '扫描的结果用CIDR进行过滤,不配置则不会过滤。格式: 192.168.1.0/24',
|
||||
nodeSettingIp: '网络设备IP地址',
|
||||
nodeSettingIpTip: '请输入 ip 地址',
|
||||
nodeSettingIpTip1: 'ip地址格式错误',
|
||||
@@ -700,6 +720,7 @@ if __name__ == "__main__":
|
||||
batchRollbacking: '正在回滚,共{total}个,成功{successNum}个,失败{errorNum}个',
|
||||
baselineTips: '该时间点的变更也会被回滚, 唯一标识、密码属性、动态属性不支持回滚',
|
||||
cover: '覆盖',
|
||||
detail: '详情'
|
||||
},
|
||||
serviceTree: {
|
||||
remove: '移除',
|
||||
|
@@ -17,7 +17,15 @@
|
||||
:ci_id="ci._id"
|
||||
:attr_id="attr.id"
|
||||
></PasswordField>
|
||||
<template v-else-if="attr.value_type === '6'">{{ JSON.stringify(ci[attr.name] || {}) }}</template>
|
||||
<a-tooltip
|
||||
v-else-if="attr.value_type === '6'"
|
||||
:title="JSON.stringify(ci[attr.name] || {})"
|
||||
overlayClassName="ci-detail-attr-json-tooltip"
|
||||
>
|
||||
<span class="ci-detail-attr-json">
|
||||
{{ JSON.stringify(ci[attr.name] || {}) }}
|
||||
</span>
|
||||
</a-tooltip>
|
||||
<template v-else-if="attr.is_choice">
|
||||
<template v-if="attr.is_list">
|
||||
<span
|
||||
@@ -348,4 +356,22 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
<style lang="less" scoped>
|
||||
.ci-detail-attr-json {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.ci-detail-attr-json-tooltip {
|
||||
|
||||
.ant-tooltip-content {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div class="ci-detail-table-title">
|
||||
{{ title }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CIDetailTableTitle',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.ci-detail-table-title {
|
||||
height: 42px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: @text-color_1;
|
||||
padding: 0px 20px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: #EBF0F9;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
height: 44px;
|
||||
width: 300px;
|
||||
background: #F8F9FD60;
|
||||
transform: rotate(40deg);
|
||||
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 25%;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
height: 44px;
|
||||
width: 300px;
|
||||
background: #F8F9FD60;
|
||||
transform: rotate(40deg);
|
||||
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: calc(25% + 100px);
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div class="ci-detail-title">
|
||||
<CIIcon :icon="icon" size="20" />
|
||||
<span class="ci-detail-title-text">{{ title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CIIcon from '@/modules/cmdb/components/ciIcon'
|
||||
|
||||
export default {
|
||||
name: 'CIDetailTitle',
|
||||
components: {
|
||||
CIIcon
|
||||
},
|
||||
props: {
|
||||
ci: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
ci_types: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
findCIType() {
|
||||
return this.ci_types?.find?.((item) => item?.id === this.ci?._type)
|
||||
},
|
||||
icon() {
|
||||
return this?.findCiType?.icon || ''
|
||||
},
|
||||
title() {
|
||||
return this?.ci?.[this.findCIType?.show_name] || this?.ci?.[this.findCIType?.unique_key] || ''
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.ci-detail-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
column-gap: 9px;
|
||||
|
||||
&-text {
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: @text-color_1;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,607 @@
|
||||
<template>
|
||||
<div v-if="allCITypes.length" class="ci-relation-table">
|
||||
<CIDetailTableTitle :title="$t('cmdb.relation')" />
|
||||
|
||||
<div class="ci-relation-table-wrap">
|
||||
<div class="ci-relation-table-tab">
|
||||
<div
|
||||
v-for="(item) in tabList"
|
||||
:key="item.value"
|
||||
:class="`tab-item ${item.value === currentTab ? 'tab-item-active' : ''}`"
|
||||
@click="clickTab(item.value)"
|
||||
>
|
||||
<span class="tab-item-name">
|
||||
<a-tooltip :title="item.name">
|
||||
<span class="tab-item-name-text">{{ item.name }}</span>
|
||||
</a-tooltip>
|
||||
<span
|
||||
v-if="item.count"
|
||||
class="tab-item-name-count"
|
||||
>
|
||||
({{ item.count }})
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
v-if="item.value === currentTab && item.showAdd"
|
||||
class="tab-item-add"
|
||||
@click="openAddModal(item)"
|
||||
>
|
||||
<a-icon type="plus" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="ci-relation-table-container"
|
||||
v-if="tableIDList.length"
|
||||
>
|
||||
<div
|
||||
v-for="(item) in tableIDList"
|
||||
:key="item.id"
|
||||
class="ci-relation-table-item"
|
||||
>
|
||||
<div
|
||||
v-if="currentTab === 'all'"
|
||||
class="ci-relation-table-item-name"
|
||||
>
|
||||
<span class="ci-relation-table-item-name-text">{{ item.name }}</span>
|
||||
<span class="ci-relation-table-item-name-count">({{ item.count }})</span>
|
||||
</div>
|
||||
|
||||
<vxe-grid
|
||||
bordered
|
||||
size="mini"
|
||||
:columns="allColumns[item.id]"
|
||||
:data="allCIList[item.id]"
|
||||
overflow
|
||||
showOverflow="tooltip"
|
||||
showHeaderOverflow="tooltip"
|
||||
resizable
|
||||
class="ops-stripe-table"
|
||||
max-height="300px"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
{{ getReferenceName(id, column) }}
|
||||
</a>
|
||||
</template>
|
||||
<template #operation_default="{ row }">
|
||||
<a-popconfirm
|
||||
arrowPointAtCenter
|
||||
:title="$t('cmdb.ci.confirmDeleteRelation')"
|
||||
@confirm="deleteRelation(row)"
|
||||
>
|
||||
<a
|
||||
:disabled="!allCanEdit[item.id]"
|
||||
:style="{
|
||||
color: !allCanEdit[item.id] ? 'rgba(0, 0, 0, 0.25)' : 'red',
|
||||
}"
|
||||
>
|
||||
<a-icon type="delete" />
|
||||
</a>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
</vxe-grid>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AddTableModal ref="addTableModal" @reload="refreshTableData" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { getCanEditByParentIdChildId } from '@/modules/cmdb/api/CITypeRelation'
|
||||
import { deleteCIRelationView } from '@/modules/cmdb/api/CIRelation'
|
||||
import { searchCI } from '@/modules/cmdb/api/ci'
|
||||
import { getSubscribeAttributes } from '@/modules/cmdb/api/preference'
|
||||
|
||||
import CIDetailTableTitle from './ciDetailTableTitle.vue'
|
||||
import AddTableModal from '@/modules/cmdb/views/relation_views/modules/AddTableModal.vue'
|
||||
|
||||
export default {
|
||||
name: 'CIRelationTable',
|
||||
components: {
|
||||
CIDetailTableTitle,
|
||||
AddTableModal
|
||||
},
|
||||
inject: {
|
||||
ci_types: { from: 'ci_types' },
|
||||
relationViewRefreshNumber: {
|
||||
from: 'relationViewRefreshNumber',
|
||||
default: () => null,
|
||||
},
|
||||
},
|
||||
props: {
|
||||
ciId: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
typeId: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
ci: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
relationData: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
tabList: [],
|
||||
currentTab: 'all',
|
||||
allCITypes: [],
|
||||
allColumns: {},
|
||||
allJSONAttr: {},
|
||||
allCIList: {},
|
||||
allCanEdit: {},
|
||||
referenceCINameMap: {}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
tableIDList() {
|
||||
let baseIDs = []
|
||||
|
||||
switch (this.currentTab) {
|
||||
case 'all':
|
||||
baseIDs = this.tabList.filter((item) => item.value !== 'all').map((item) => item.value)
|
||||
break
|
||||
default:
|
||||
baseIDs = [this.currentTab]
|
||||
break
|
||||
}
|
||||
|
||||
return baseIDs.filter((id) => this.allCIList?.[id]?.length).map((id) => {
|
||||
const findTab = this.tabList.find((item) => item.value === id) || {}
|
||||
|
||||
return {
|
||||
id,
|
||||
name: findTab?.name || '',
|
||||
count: findTab?.count || ''
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
relationData: {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
handler(val) {
|
||||
this.init(val)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async init(relationData) {
|
||||
const ci_types_list = this.ci_types()
|
||||
const _findCiType = ci_types_list.find((item) => item.id === this.typeId)
|
||||
if (!_findCiType) {
|
||||
return
|
||||
}
|
||||
|
||||
const cloneRelationData = _.cloneDeep(relationData)
|
||||
|
||||
const allCITypes = [
|
||||
...cloneRelationData.parentCITypeList,
|
||||
...cloneRelationData.childCITypeList
|
||||
]
|
||||
await this.handleSubscribeAttributes(allCITypes)
|
||||
|
||||
const {
|
||||
columns: parentColumns,
|
||||
jsonAttr: parentJSONAttr,
|
||||
} = this.handleCITypeList(cloneRelationData.parentCITypeList, true)
|
||||
const {
|
||||
columns: childColumns,
|
||||
jsonAttr: childJSONAttr,
|
||||
} = this.handleCITypeList(cloneRelationData.childCITypeList, false)
|
||||
|
||||
this.allCITypes = allCITypes
|
||||
this.allColumns = {
|
||||
...parentColumns,
|
||||
...childColumns
|
||||
}
|
||||
this.allJSONAttr = {
|
||||
...parentJSONAttr,
|
||||
...childJSONAttr
|
||||
}
|
||||
|
||||
await this.getCanEditList(this.allCITypes)
|
||||
|
||||
const [parentCIs, childCIs] = await Promise.all([
|
||||
this.handleCIList(cloneRelationData.parentCIList, true),
|
||||
this.handleCIList(cloneRelationData.childCIList, false)
|
||||
])
|
||||
this.allCIList = {
|
||||
...parentCIs,
|
||||
...childCIs
|
||||
}
|
||||
|
||||
const tabList = this.allCITypes.map((item) => {
|
||||
return {
|
||||
name: item?.alias ?? item?.name ?? '',
|
||||
value: item.id,
|
||||
count: this.allCIList?.[item.id]?.length || 0,
|
||||
showAdd: this.allCanEdit?.[item.id] ?? false
|
||||
}
|
||||
})
|
||||
tabList.unshift({
|
||||
name: this.$t('all'),
|
||||
value: 'all',
|
||||
count: Object.values(this.allCIList).reduce((acc, cur) => acc + (cur?.length || 0), 0),
|
||||
showAdd: false
|
||||
})
|
||||
this.tabList = tabList
|
||||
|
||||
this.handleReferenceCINameMap()
|
||||
},
|
||||
|
||||
handleCITypeList(list, isParent) {
|
||||
const CIColumns = {}
|
||||
const CIJSONAttr = {}
|
||||
|
||||
list.forEach((item) => {
|
||||
const columns = []
|
||||
const jsonAttr = []
|
||||
|
||||
item.isParent = isParent
|
||||
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)
|
||||
}
|
||||
})
|
||||
CIJSONAttr[item.id] = jsonAttr
|
||||
CIColumns[item.id] = columns
|
||||
CIColumns[item.id].push({
|
||||
key: 'p_operation',
|
||||
field: 'operation',
|
||||
title: this.$t('operation'),
|
||||
width: '60px',
|
||||
fixed: 'right',
|
||||
slots: {
|
||||
default: 'operation_default',
|
||||
},
|
||||
align: 'center',
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
columns: CIColumns,
|
||||
jsonAttr: CIJSONAttr
|
||||
}
|
||||
},
|
||||
|
||||
async getCanEditList(allCITypes) {
|
||||
const promises = allCITypes.map((ciType) => {
|
||||
let parentId = ciType.id
|
||||
let childId = this.typeId
|
||||
|
||||
if (!ciType.isParent) {
|
||||
parentId = this.typeId
|
||||
childId = ciType.id
|
||||
}
|
||||
|
||||
return getCanEditByParentIdChildId(parentId, childId).then((res) => {
|
||||
return { id: ciType.id, canEdit: res.result }
|
||||
})
|
||||
})
|
||||
|
||||
const allCanEdit = {}
|
||||
|
||||
const res = await Promise.all(promises)
|
||||
if (res?.length) {
|
||||
res.map((item) => {
|
||||
allCanEdit[item.id] = item.canEdit
|
||||
})
|
||||
}
|
||||
|
||||
this.allCanEdit = allCanEdit
|
||||
},
|
||||
|
||||
async handleSubscribeAttributes(allCITypes) {
|
||||
const promises = allCITypes.map((ciType, index) => {
|
||||
return getSubscribeAttributes(ciType.id).then((res) => {
|
||||
return {
|
||||
...(res || {}),
|
||||
id: ciType.id,
|
||||
indexInAll: index
|
||||
}
|
||||
})
|
||||
})
|
||||
const res = await Promise.all(promises)
|
||||
|
||||
if (res?.length) {
|
||||
res.forEach((item) => {
|
||||
if (
|
||||
allCITypes?.[item.indexInAll]?.attributes &&
|
||||
item?.is_subscribed
|
||||
) {
|
||||
allCITypes[item.indexInAll].attributes = item.attributes
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return allCITypes
|
||||
},
|
||||
|
||||
async handleCIList(ciList, isParent) {
|
||||
const cis = {}
|
||||
ciList.forEach((item) => {
|
||||
this.allJSONAttr[item._type].forEach((attr) => {
|
||||
item[`${attr}`] = item[`${attr}`] ? JSON.stringify(item[`${attr}`]) : ''
|
||||
})
|
||||
this.formatCI(item)
|
||||
item.isParent = isParent
|
||||
|
||||
if (item._type in cis) {
|
||||
cis[item._type].push(item)
|
||||
} else {
|
||||
cis[item._type] = [item]
|
||||
}
|
||||
})
|
||||
|
||||
return cis
|
||||
},
|
||||
|
||||
formatCI(ci) {
|
||||
Object.keys(ci).forEach((key) => {
|
||||
const attr = this.allColumns?.[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 handleReferenceCINameMap() {
|
||||
const referenceCINameMap = {}
|
||||
this.allCITypes.forEach((CIType) => {
|
||||
CIType.attributes.forEach((attr) => {
|
||||
if (attr?.is_reference && attr?.reference_type_id) {
|
||||
const currentCIList = this.allCIList[CIType.id]
|
||||
if (currentCIList?.length) {
|
||||
currentCIList.forEach((ci) => {
|
||||
const ids = Array.isArray(ci[attr.name]) ? ci[attr.name] : ci[attr.name] ? [ci[attr.name]] : []
|
||||
|
||||
if (ids.length) {
|
||||
if (!referenceCINameMap?.[attr.reference_type_id]) {
|
||||
referenceCINameMap[attr.reference_type_id] = {}
|
||||
}
|
||||
ids.forEach((id) => {
|
||||
referenceCINameMap[attr.reference_type_id][id] = ''
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (!Object.keys(referenceCINameMap).length) {
|
||||
return
|
||||
}
|
||||
|
||||
const allRes = await Promise.all(
|
||||
Object.keys(referenceCINameMap).map((key) => {
|
||||
return searchCI({
|
||||
q: `_type:${key},_id:(${Object.keys(referenceCINameMap[key]).join(';')})`,
|
||||
count: 9999
|
||||
})
|
||||
})
|
||||
)
|
||||
const CITypeList = this.ci_types()
|
||||
const showNameMap = {}
|
||||
|
||||
Object.keys(referenceCINameMap).forEach((id) => {
|
||||
const CIType = CITypeList.find((CIType) => Number(CIType.id) === Number(id))
|
||||
|
||||
showNameMap[id] = {
|
||||
show_name: CIType?.show_name,
|
||||
unique_key: CIType?.unique_key
|
||||
}
|
||||
})
|
||||
|
||||
allRes.forEach((res) => {
|
||||
res.result.forEach((item) => {
|
||||
if (referenceCINameMap?.[item._type]?.[item._id] === '') {
|
||||
const showName = showNameMap?.[item._type]
|
||||
|
||||
referenceCINameMap[item._type][item._id] = item?.[showName?.show_name] ?? item?.[showName?.unique_key] ?? ''
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
this.referenceCINameMap = referenceCINameMap
|
||||
},
|
||||
|
||||
getReferenceName(id, column) {
|
||||
const typeId = column?.params?.attr?.reference_type_id
|
||||
return this.referenceCINameMap?.[typeId]?.[id] || id
|
||||
},
|
||||
|
||||
clickTab(value) {
|
||||
this.currentTab = value
|
||||
},
|
||||
|
||||
deleteRelation(row) {
|
||||
const first_ci_id = row?.isParent ? row?._id : this.ciId
|
||||
const second_ci_id = row?.isParent ? this.ciId : row?._id
|
||||
|
||||
deleteCIRelationView(first_ci_id, second_ci_id).then(() => {
|
||||
this.refreshTableData()
|
||||
if (this.relationViewRefreshNumber) {
|
||||
this.relationViewRefreshNumber()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
openAddModal(tabData) {
|
||||
const ciType = this.allCITypes.find((item) => item.id === tabData.value)
|
||||
|
||||
this.$refs.addTableModal.openModal(
|
||||
{
|
||||
[`${this.ci.unique}`]: this.ci?.[this.ci.unique]
|
||||
},
|
||||
this.ciId,
|
||||
ciType,
|
||||
ciType?.isParent ? 'parents' : 'children'
|
||||
)
|
||||
},
|
||||
|
||||
async refreshTableData() {
|
||||
this.$emit('refreshRelationCI')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.ci-relation-table {
|
||||
width: 100%;
|
||||
margin-top: 32px;
|
||||
|
||||
&-wrap {
|
||||
border: solid 1px #E4E7ED;
|
||||
border-top: none;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&-tab {
|
||||
flex-shrink: 0;
|
||||
width: 160px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 6px 0px;
|
||||
border-right: solid 1px #E4E7ED;
|
||||
|
||||
.tab-item {
|
||||
height: 32px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-left: 16px;
|
||||
padding-right: 10px;
|
||||
background-color: #FFFFFF;
|
||||
cursor: pointer;
|
||||
|
||||
&-name {
|
||||
font-size: 14px;
|
||||
color: @text-color_1;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
max-width: calc(100% - 16px);
|
||||
|
||||
&-text {
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
overflow: hidden;
|
||||
color: @text-color_2;
|
||||
}
|
||||
|
||||
&-count {
|
||||
color: @text-color_3;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&-add {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 14px;
|
||||
background-color: #FFFFFF;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: @primary-color;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&-active {
|
||||
background-color: #F0F5FF;
|
||||
|
||||
.tab-item-name-text {
|
||||
color: @text-color_1;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.tab-item-name-text {
|
||||
color: @text-color_1;
|
||||
}
|
||||
|
||||
.tab-item-add {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-container {
|
||||
width: 100%;
|
||||
padding: 15px 17px;
|
||||
overflow: hidden;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
&-item {
|
||||
margin-bottom: 16px;
|
||||
|
||||
&-name {
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: @text-color_1;
|
||||
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
&-count {
|
||||
font-size: 12px;
|
||||
color: @text-color_3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,95 @@
|
||||
import { getCITypeChildren, getCITypeParent } from '@/modules/cmdb/api/CITypeRelation'
|
||||
import { searchCIRelation } from '@/modules/cmdb/api/CIRelation'
|
||||
|
||||
const RelationMixin = {
|
||||
data() {
|
||||
return {
|
||||
relationData: {
|
||||
parentCITypeList: [],
|
||||
childCITypeList: [],
|
||||
parentCIList: [],
|
||||
childCIList: []
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async initRelationData(typeId, ciId) {
|
||||
const {
|
||||
parentCITypeList,
|
||||
childCITypeList
|
||||
} = await this.getRelationCITypeList(typeId)
|
||||
const {
|
||||
parentCIList,
|
||||
childCIList
|
||||
} = await this.getRelationCIList(ciId)
|
||||
this.relationData = {
|
||||
parentCITypeList,
|
||||
childCITypeList,
|
||||
parentCIList,
|
||||
childCIList
|
||||
}
|
||||
},
|
||||
|
||||
async getRelationCITypeList(typeId) {
|
||||
let parentCITypeList = []
|
||||
let childCITypeList = []
|
||||
|
||||
if (typeId) {
|
||||
parentCITypeList = await this.getParentCITypeList(typeId)
|
||||
childCITypeList = await this.getChildCITypeList(typeId)
|
||||
}
|
||||
|
||||
return {
|
||||
parentCITypeList,
|
||||
childCITypeList
|
||||
}
|
||||
},
|
||||
|
||||
async getRelationCIList(ciId) {
|
||||
let parentCIList = []
|
||||
let childCIList = []
|
||||
|
||||
if (ciId) {
|
||||
parentCIList = await this.getParentCIList(ciId)
|
||||
childCIList = await this.getChildCIList(ciId)
|
||||
}
|
||||
|
||||
return {
|
||||
parentCIList,
|
||||
childCIList
|
||||
}
|
||||
},
|
||||
|
||||
async refreshRelationCI(ciId) {
|
||||
const {
|
||||
parentCIList,
|
||||
childCIList
|
||||
} = await this.getRelationCIList(ciId)
|
||||
this.relationData.parentCIList = parentCIList
|
||||
this.relationData.childCIList = childCIList
|
||||
},
|
||||
|
||||
async getParentCITypeList(typeId) {
|
||||
const res = await getCITypeParent(typeId)
|
||||
return res?.parents || []
|
||||
},
|
||||
|
||||
async getChildCITypeList(typeId) {
|
||||
const res = await getCITypeChildren(typeId)
|
||||
return res.children || []
|
||||
},
|
||||
|
||||
async getParentCIList(ciId) {
|
||||
const res = await searchCIRelation(`root_id=${ciId}&level=1&reverse=1&count=10000`)
|
||||
return res?.result || []
|
||||
},
|
||||
|
||||
async getChildCIList(ciId) {
|
||||
const res = await searchCIRelation(`root_id=${ciId}&level=1&reverse=0&count=10000`)
|
||||
return res?.result || []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default RelationMixin
|
@@ -1,146 +1,16 @@
|
||||
<template>
|
||||
<div class="ci-detail-relation">
|
||||
<a-radio-group v-model="activeKey" size="small" @change="handleChangeActiveKey">
|
||||
<a-radio-button value="1">
|
||||
{{ $t('cmdb.ci.topo') }}
|
||||
</a-radio-button>
|
||||
<a-radio-button value="2">
|
||||
{{ $t('cmdb.ci.table') }}
|
||||
</a-radio-button>
|
||||
</a-radio-group>
|
||||
<CiDetailRelationTopo ref="ciDetailRelationTopo" v-if="activeKey === '1'" />
|
||||
<template v-if="activeKey === '2'">
|
||||
<template v-for="parent in parentCITypes">
|
||||
<div :key="'ctr_' + parent.ctr_id">
|
||||
<div class="ci-detail-relation-table-title">
|
||||
{{ parent.alias || parent.name }}
|
||||
<a
|
||||
:disabled="!canEdit[parent.id]"
|
||||
@click="
|
||||
() => {
|
||||
$refs.addTableModal.openModal({ [`${ci.unique}`]: ci[ci.unique] }, ci._id, parent, 'parents')
|
||||
}
|
||||
"
|
||||
><a-icon
|
||||
type="plus-square"
|
||||
/></a>
|
||||
<span v-if="!canEdit[parent.id]">({{ $t('cmdb.ci.m2mTips') }})</span>
|
||||
</div>
|
||||
<vxe-grid
|
||||
v-if="firstCIs[parent.name]"
|
||||
bordered
|
||||
size="mini"
|
||||
:columns="firstCIColumns[parent.id]"
|
||||
:data="firstCIs[parent.name]"
|
||||
overflow
|
||||
showOverflow="tooltip"
|
||||
showHeaderOverflow="tooltip"
|
||||
resizable
|
||||
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"
|
||||
>
|
||||
{{ getReferenceName(id, column) }}
|
||||
</a>
|
||||
</template>
|
||||
<template #operation_default="{ row }">
|
||||
<a-popconfirm
|
||||
arrowPointAtCenter
|
||||
:title="$t('cmdb.ci.confirmDeleteRelation')"
|
||||
@confirm="deleteRelation(row._id, ciId)"
|
||||
>
|
||||
<a
|
||||
:disabled="!canEdit[parent.id]"
|
||||
:style="{
|
||||
color: !canEdit[parent.id] ? 'rgba(0, 0, 0, 0.25)' : 'red',
|
||||
}"
|
||||
><a-icon
|
||||
type="delete"
|
||||
/></a>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
</vxe-grid>
|
||||
</div>
|
||||
</template>
|
||||
<a-divider />
|
||||
<template v-for="child in childCITypes">
|
||||
<div :key="'ctr_' + child.ctr_id">
|
||||
<div class="ci-detail-relation-table-title">
|
||||
{{ child.alias || child.name }}
|
||||
<a
|
||||
:disabled="!canEdit[child.id]"
|
||||
@click="
|
||||
() => {
|
||||
$refs.addTableModal.openModal({ [`${ci.unique}`]: ci[ci.unique] }, ci._id, child, 'children')
|
||||
}
|
||||
"
|
||||
><a-icon
|
||||
type="plus-square"
|
||||
/></a>
|
||||
<span v-if="!canEdit[child.id]">({{ $t('cmdb.ci.m2mTips') }})</span>
|
||||
</div>
|
||||
<vxe-grid
|
||||
v-if="secondCIs[child.name]"
|
||||
bordered
|
||||
size="mini"
|
||||
:columns="secondCIColumns[child.id]"
|
||||
:data="secondCIs[child.name]"
|
||||
showOverflow="tooltip"
|
||||
showHeaderOverflow="tooltip"
|
||||
resizable
|
||||
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"
|
||||
>
|
||||
{{ getReferenceName(id, column) }}
|
||||
</a>
|
||||
</template>
|
||||
<template #operation_default="{ row }">
|
||||
<a-popconfirm
|
||||
arrowPointAtCenter
|
||||
:title="$t('cmdb.ci.confirmDeleteRelation')"
|
||||
@confirm="deleteRelation(ciId, row._id)"
|
||||
>
|
||||
<a
|
||||
:disabled="!canEdit[child.id]"
|
||||
:style="{
|
||||
color: !canEdit[child.id] ? 'rgba(0, 0, 0, 0.25)' : 'red',
|
||||
}"
|
||||
><a-icon
|
||||
type="delete"
|
||||
/></a>
|
||||
</a-popconfirm>
|
||||
</template>
|
||||
</vxe-grid>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<AddTableModal ref="addTableModal" @reload="reload" />
|
||||
<CiDetailRelationTopo ref="ciDetailRelationTopo"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { getCITypeChildren, getCITypeParent, getCanEditByParentIdChildId } from '@/modules/cmdb/api/CITypeRelation'
|
||||
import { searchCIRelation, deleteCIRelationView } from '@/modules/cmdb/api/CIRelation'
|
||||
import { searchCI } from '@/modules/cmdb/api/ci'
|
||||
import CiDetailRelationTopo from './ciDetailRelationTopo/index.vue'
|
||||
import Node from './ciDetailRelationTopo/node.js'
|
||||
import AddTableModal from '../../relation_views/modules/AddTableModal.vue'
|
||||
|
||||
export default {
|
||||
name: 'CiDetailRelation',
|
||||
components: { CiDetailRelationTopo, AddTableModal },
|
||||
name: 'CIDetailRelation',
|
||||
components: { CiDetailRelationTopo },
|
||||
props: {
|
||||
ciId: {
|
||||
type: Number,
|
||||
@@ -154,41 +24,32 @@ export default {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
initQueryLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
relationData: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeKey: '1',
|
||||
parentCITypes: [],
|
||||
childCITypes: [],
|
||||
firstCIs: {},
|
||||
firstCIColumns: {},
|
||||
secondCIs: {},
|
||||
secondCIColumns: {},
|
||||
firstCIJsonAttr: {},
|
||||
secondCIJsonAttr: {},
|
||||
canEdit: {},
|
||||
topoData: {
|
||||
nodes: {},
|
||||
edges: []
|
||||
},
|
||||
referenceCINameMap: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
exsited_ci() {
|
||||
const _exsited_ci = [this.ciId]
|
||||
this.parentCITypes.forEach((parent) => {
|
||||
this.relationData.parentCITypeList.forEach((parent) => {
|
||||
if (this.firstCIs[parent.name]) {
|
||||
this.firstCIs[parent.name].forEach((parentCi) => {
|
||||
_exsited_ci.push(parentCi._id)
|
||||
})
|
||||
}
|
||||
})
|
||||
this.childCITypes.forEach((child) => {
|
||||
this.relationData.childCITypeList.forEach((child) => {
|
||||
if (this.secondCIs[child.name]) {
|
||||
this.secondCIs[child.name].forEach((childCi) => {
|
||||
_exsited_ci.push(childCi._id)
|
||||
@@ -207,314 +68,59 @@ export default {
|
||||
default: () => null,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (!this.initQueryLoading) {
|
||||
this.init(true)
|
||||
|
||||
watch: {
|
||||
relationData: {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
handler(val) {
|
||||
this.init(val)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async init(isFirst) {
|
||||
async init(relationData) {
|
||||
const ci_types_list = this.ci_types()
|
||||
const _findCiType = ci_types_list.find((item) => item.id === this.typeId)
|
||||
if (!_findCiType) {
|
||||
return
|
||||
}
|
||||
|
||||
await Promise.all([this.getParentCITypes(), this.getChildCITypes()])
|
||||
Promise.all([this.getFirstCIs(), this.getSecondCIs()]).then(() => {
|
||||
this.handleTopoData()
|
||||
if (
|
||||
isFirst &&
|
||||
this.$refs.ciDetailRelationTopo &&
|
||||
ci_types_list.length
|
||||
) {
|
||||
this.getFirstCIs(relationData.parentCIList)
|
||||
this.getSecondCIs(relationData.childCIList)
|
||||
|
||||
this.handleTopoData()
|
||||
this.$nextTick(() => {
|
||||
if (this.$refs.ciDetailRelationTopo) {
|
||||
this.$refs.ciDetailRelationTopo.exsited_ci = this.exsited_ci
|
||||
this.$refs.ciDetailRelationTopo.setTopoData(this.topoData)
|
||||
}
|
||||
|
||||
this.handleReferenceCINameMap()
|
||||
})
|
||||
},
|
||||
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]
|
||||
}
|
||||
async getFirstCIs(parentCIList) {
|
||||
const firstCIs = {}
|
||||
parentCIList.forEach((item) => {
|
||||
if (item.ci_type in firstCIs) {
|
||||
firstCIs[item.ci_type].push(item)
|
||||
} else {
|
||||
firstCIs[item.ci_type] = [item]
|
||||
}
|
||||
})
|
||||
|
||||
return ci
|
||||
this.firstCIs = firstCIs
|
||||
},
|
||||
|
||||
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
|
||||
},
|
||||
|
||||
async handleReferenceCINameMap() {
|
||||
const CITypes = _.unionBy(
|
||||
[
|
||||
...this.parentCITypes,
|
||||
...this.childCITypes
|
||||
],
|
||||
'id'
|
||||
)
|
||||
const CIList = _.unionBy(
|
||||
_.flatten(
|
||||
[
|
||||
...Object.values(this.firstCIs),
|
||||
...Object.values(this.secondCIs)
|
||||
]
|
||||
),
|
||||
'_id'
|
||||
)
|
||||
|
||||
const CIMap = {}
|
||||
CIList.forEach((ci) => {
|
||||
if (!CIMap[ci._type]) {
|
||||
CIMap[ci._type] = []
|
||||
}
|
||||
CIMap[ci._type].push(ci)
|
||||
})
|
||||
|
||||
const referenceCINameMap = {}
|
||||
CITypes.forEach((CIType) => {
|
||||
CIType.attributes.forEach((attr) => {
|
||||
if (attr?.is_reference && attr?.reference_type_id) {
|
||||
const currentCIList = CIMap[CIType.id]
|
||||
if (currentCIList?.length) {
|
||||
currentCIList.forEach((ci) => {
|
||||
const ids = Array.isArray(ci[attr.name]) ? ci[attr.name] : ci[attr.name] ? [ci[attr.name]] : []
|
||||
|
||||
if (ids.length) {
|
||||
if (!referenceCINameMap?.[attr.reference_type_id]) {
|
||||
referenceCINameMap[attr.reference_type_id] = {}
|
||||
}
|
||||
ids.forEach((id) => {
|
||||
referenceCINameMap[attr.reference_type_id][id] = ''
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (!Object.keys(referenceCINameMap).length) {
|
||||
return
|
||||
}
|
||||
|
||||
const allRes = await Promise.all(
|
||||
Object.keys(referenceCINameMap).map((key) => {
|
||||
return searchCI({
|
||||
q: `_type:${key},_id:(${Object.keys(referenceCINameMap[key]).join(';')})`,
|
||||
count: 9999
|
||||
})
|
||||
})
|
||||
)
|
||||
const CITypeList = this.ci_types()
|
||||
const showNameMap = {}
|
||||
|
||||
Object.keys(referenceCINameMap).forEach((id) => {
|
||||
const CIType = CITypeList.find((CIType) => Number(CIType.id) === Number(id))
|
||||
|
||||
showNameMap[id] = {
|
||||
show_name: CIType?.show_name,
|
||||
unique_key: CIType?.unique_key
|
||||
async getSecondCIs(childCIList) {
|
||||
const secondCIs = {}
|
||||
childCIList.forEach((item) => {
|
||||
if (item.ci_type in secondCIs) {
|
||||
secondCIs[item.ci_type].push(item)
|
||||
} else {
|
||||
secondCIs[item.ci_type] = [item]
|
||||
}
|
||||
})
|
||||
|
||||
allRes.forEach((res) => {
|
||||
res.result.forEach((item) => {
|
||||
if (referenceCINameMap?.[item._type]?.[item._id] === '') {
|
||||
const showName = showNameMap?.[item._type]
|
||||
|
||||
referenceCINameMap[item._type][item._id] = item?.[showName?.show_name] ?? item?.[showName?.unique_key] ?? ''
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
this.referenceCINameMap = referenceCINameMap
|
||||
this.secondCIs = secondCIs
|
||||
},
|
||||
|
||||
getReferenceName(id, column) {
|
||||
const typeId = column?.params?.attr?.reference_type_id
|
||||
return this.referenceCINameMap?.[typeId]?.[id] || id
|
||||
},
|
||||
|
||||
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()
|
||||
if (!ci_types_list?.length) {
|
||||
@@ -555,7 +161,7 @@ export default {
|
||||
children: [],
|
||||
}
|
||||
const edges = []
|
||||
this.parentCITypes.forEach((parent) => {
|
||||
this.relationData.parentCITypeList.forEach((parent) => {
|
||||
const _findCiType = ci_types_list.find((item) => item.id === parent.id)
|
||||
if (this.firstCIs[parent.name] && _findCiType) {
|
||||
const unique_id = _findCiType.show_id || _findCiType.unique_id
|
||||
@@ -598,7 +204,7 @@ export default {
|
||||
})
|
||||
}
|
||||
})
|
||||
this.childCITypes.forEach((child) => {
|
||||
this.relationData.childCITypeList.forEach((child) => {
|
||||
const _findCiType = ci_types_list.find((item) => item.id === child.id)
|
||||
if (this.secondCIs[child.name] && _findCiType) {
|
||||
const unique_id = _findCiType.show_id || _findCiType.unique_id
|
||||
@@ -653,12 +259,5 @@ export default {
|
||||
<style lang="less" scoped>
|
||||
.ci-detail-relation {
|
||||
height: 100%;
|
||||
.ci-detail-relation-table-title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 5px;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -2,7 +2,7 @@
|
||||
<div
|
||||
id="ci-detail-relation-topo"
|
||||
class="ci-detail-relation-topo"
|
||||
:style="{ width: '100%', marginTop: '20px', height: 'calc(100% - 44px)' }"
|
||||
:style="{ width: '100%', height: '100%' }"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
@@ -25,7 +25,6 @@ export default {
|
||||
}
|
||||
},
|
||||
inject: ['ci_types'],
|
||||
mounted() {},
|
||||
methods: {
|
||||
init() {
|
||||
const root = document.getElementById('ci-detail-relation-topo')
|
||||
|
@@ -6,29 +6,79 @@
|
||||
{{ $t('cmdb.ci.share') }}
|
||||
</a>
|
||||
<a-tab-pane key="tab_1">
|
||||
<span slot="tab"><a-icon type="book" />{{ $t('cmdb.attribute') }}</span>
|
||||
<div class="ci-detail-attr">
|
||||
<el-descriptions
|
||||
:title="group.name || $t('other')"
|
||||
:key="group.name"
|
||||
v-for="group in attributeGroups"
|
||||
border
|
||||
:column="3"
|
||||
>
|
||||
<el-descriptions-item
|
||||
:label="`${attr.alias || attr.name}`"
|
||||
:key="attr.name"
|
||||
v-for="attr in group.attributes"
|
||||
>
|
||||
<ci-detail-attr-content :ci="ci" :attr="attr" @refresh="refresh" @updateCIByself="updateCIByself" @refreshReferenceAttr="handleReferenceAttr" />
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<span slot="tab"><a-icon type="book" />{{ $t('cmdb.ci.detail') }}</span>
|
||||
|
||||
<div class="ci-detail-table">
|
||||
<CIDetailTitle :ci="ci" :ci_types="ci_types" />
|
||||
|
||||
<div class="ci-detail-table-attr">
|
||||
<CIDetailTableTitle :title="$t('cmdb.attribute')" />
|
||||
|
||||
<div class="ci-detail-table-attr-wrap">
|
||||
<div
|
||||
v-for="group in attributeGroups"
|
||||
:key="group.name"
|
||||
class="ci-detail-table-attr-group"
|
||||
>
|
||||
<div class="ci-detail-table-attr-group-name">
|
||||
{{ group.name || $t('other') }}
|
||||
</div>
|
||||
|
||||
<a-row :gutter="[18, 14]">
|
||||
<a-col
|
||||
v-for="attr in group.attributes"
|
||||
:key="attr.name"
|
||||
:span="8"
|
||||
>
|
||||
<a-row :gutter="[8, 0]">
|
||||
<a-col :span="8">
|
||||
<span class="ci-detail-table-attr-label">
|
||||
<a-tooltip :title="attr.alias || attr.name">
|
||||
<span class="ci-detail-table-attr-label-text">{{ attr.alias || attr.name }}</span>
|
||||
</a-tooltip>
|
||||
<span class="ci-detail-table-attr-label-colon">:</span>
|
||||
</span>
|
||||
</a-col>
|
||||
|
||||
<a-col
|
||||
:span="16"
|
||||
class="ci-detail-table-attr-content"
|
||||
>
|
||||
<CIDetailAttrContent
|
||||
:ci="ci"
|
||||
:attr="attr"
|
||||
:attributeGroups="attributeGroups"
|
||||
@updateChoiceValue="updateChoiceValue"
|
||||
@refresh="refresh"
|
||||
@updateCIByself="updateCIByself"
|
||||
@refreshReferenceAttr="handleReferenceAttr"
|
||||
/>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CIRelationTable
|
||||
:ciId="ciId"
|
||||
:typeId="typeId"
|
||||
:ci="ci"
|
||||
:relationData="relationData"
|
||||
@refreshRelationCI="refreshRelationCI(ciId)"
|
||||
/>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
<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.ci.topo') }}</span>
|
||||
<div :style="{ height: '100%', padding: '24px', overflow: 'auto' }">
|
||||
<ci-detail-relation ref="ciDetailRelation" :ciId="ciId" :typeId="typeId" :ci="ci" :initQueryLoading="initQueryLoading" />
|
||||
<CIDetailRelation
|
||||
:ciId="ciId"
|
||||
:typeId="typeId"
|
||||
:ci="ci"
|
||||
:relationData="relationData"
|
||||
/>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="tab_3">
|
||||
@@ -42,7 +92,7 @@
|
||||
<ops-icon type="veops-export" />{{ $t('export') }}
|
||||
</a-button>
|
||||
</a-space>
|
||||
<ci-rollback-form ref="ciRollbackForm" :ciIds="[ciId]" @getCIHistory="getCIHistory" />
|
||||
<CIRollbackForm ref="ciRollbackForm" :ciIds="[ciId]" @getCIHistory="getCIHistory" />
|
||||
<vxe-table
|
||||
ref="xTable"
|
||||
show-overflow
|
||||
@@ -134,25 +184,33 @@
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { Descriptions, DescriptionsItem } from 'element-ui'
|
||||
import { getCITypeGroupById, getCITypes } from '@/modules/cmdb/api/CIType'
|
||||
import { getCIHistory, judgeItsmInstalled } from '@/modules/cmdb/api/history'
|
||||
import { getCIById, searchCI } from '@/modules/cmdb/api/ci'
|
||||
import CiDetailAttrContent from './ciDetailAttrContent.vue'
|
||||
import CiDetailRelation from './ciDetailRelation.vue'
|
||||
|
||||
import RelationMixin from './ciDetailMixin/relationMixin.js'
|
||||
|
||||
import CIDetailTitle from './ciDetailComponent/ciDetailTitle.vue'
|
||||
import CIDetailTableTitle from './ciDetailComponent/ciDetailTableTitle.vue'
|
||||
import CIDetailAttrContent from './ciDetailAttrContent.vue'
|
||||
import CIRelationTable from './ciDetailComponent/ciRelationTable.vue'
|
||||
import CIDetailRelation from './ciDetailRelation.vue'
|
||||
import TriggerTable from '../../operation_history/modules/triggerTable.vue'
|
||||
import RelatedItsmTable from './ciDetailRelatedItsmTable.vue'
|
||||
import CiRollbackForm from './ciRollbackForm.vue'
|
||||
import CIRollbackForm from './ciRollbackForm.vue'
|
||||
|
||||
export default {
|
||||
name: 'CiDetailTab',
|
||||
mixins: [RelationMixin],
|
||||
components: {
|
||||
ElDescriptions: Descriptions,
|
||||
ElDescriptionsItem: DescriptionsItem,
|
||||
CiDetailAttrContent,
|
||||
CiDetailRelation,
|
||||
CIDetailAttrContent,
|
||||
CIDetailRelation,
|
||||
TriggerTable,
|
||||
RelatedItsmTable,
|
||||
CiRollbackForm,
|
||||
CIRollbackForm,
|
||||
CIDetailTitle,
|
||||
CIDetailTableTitle,
|
||||
CIRelationTable
|
||||
},
|
||||
props: {
|
||||
typeId: {
|
||||
@@ -218,15 +276,11 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async create(ciId, activeTabKey = 'tab_1', ciDetailRelationKey = '1') {
|
||||
async create(ciId, activeTabKey = 'tab_1') {
|
||||
this.initQueryLoading = true
|
||||
this.activeTabKey = activeTabKey
|
||||
if (activeTabKey === 'tab_2') {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.ciDetailRelation.activeKey = ciDetailRelationKey
|
||||
})
|
||||
}
|
||||
this.ciId = ciId
|
||||
|
||||
await this.getCI()
|
||||
await this.judgeItsmInstalled()
|
||||
if (this.hasPermission) {
|
||||
@@ -234,16 +288,15 @@ export default {
|
||||
this.getCIHistory()
|
||||
const ciTypeRes = await getCITypes()
|
||||
this.ci_types = ciTypeRes.ci_types
|
||||
if (this.activeTabKey === 'tab_2') {
|
||||
this.$refs.ciDetailRelation.init(true)
|
||||
}
|
||||
|
||||
this.initRelationData(this.typeId, this.ciId)
|
||||
}
|
||||
this.initQueryLoading = false
|
||||
},
|
||||
getAttributes() {
|
||||
getCITypeGroupById(this.typeId, { need_other: 1 })
|
||||
.then((res) => {
|
||||
this.attributeGroups = res
|
||||
this.attributeGroups = (res || []).filter((group) => group?.attributes?.length)
|
||||
|
||||
this.handleReferenceAttr()
|
||||
})
|
||||
@@ -509,23 +562,68 @@ export default {
|
||||
.ant-tabs-extra-content {
|
||||
line-height: 44px;
|
||||
}
|
||||
.ci-detail-attr {
|
||||
.ci-detail-table {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
.el-descriptions-item__content {
|
||||
cursor: default;
|
||||
&:hover a {
|
||||
opacity: 1 !important;
|
||||
|
||||
&-attr {
|
||||
width: 100%;
|
||||
margin-top: 14px;
|
||||
|
||||
&-wrap {
|
||||
padding: 13px;
|
||||
width: 100%;
|
||||
border: solid 1px #E4E7ED;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
&-group {
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
&-name {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: @text-color_1;
|
||||
margin-bottom: 7.5px;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
&-label {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: @text-color_3;
|
||||
display: inline-flex;
|
||||
max-width: 100%;
|
||||
|
||||
&-text {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
|
||||
&-colon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
overflow-wrap: break-word;
|
||||
|
||||
&: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;
|
||||
}
|
||||
|
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<a-form-model
|
||||
:model="formData"
|
||||
:labelCol="labelCol"
|
||||
:wrapperCol="{ span: 6 }"
|
||||
class="attr-ad-form"
|
||||
>
|
||||
<a-form-model-item
|
||||
:label="$t('cmdb.ciType.defaultVersion')"
|
||||
>
|
||||
<a-select
|
||||
v-model="formData.version"
|
||||
allowClear
|
||||
>
|
||||
<a-select-option value="1">
|
||||
v1
|
||||
</a-select-option>
|
||||
<a-select-option value="2c">
|
||||
v2c
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-model-item>
|
||||
|
||||
<a-form-model-item
|
||||
:label="$t('cmdb.ciType.defaultCommunity')"
|
||||
>
|
||||
<a-input v-model="formData.community" />
|
||||
</a-form-model-item>
|
||||
|
||||
<a-form-model-item
|
||||
:label="$t('cmdb.ciType.timeout')"
|
||||
:extra="$t('cmdb.ciType.snmpFormTip2')"
|
||||
>
|
||||
<a-input-number
|
||||
v-model="formData.timeout"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
/>
|
||||
</a-form-model-item>
|
||||
|
||||
<a-form-model-item
|
||||
:label="$t('cmdb.ciType.retryCount')"
|
||||
:extra="$t('cmdb.ciType.snmpFormTip3')"
|
||||
>
|
||||
<a-input-number
|
||||
v-model="formData.retries"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
/>
|
||||
</a-form-model-item>
|
||||
</a-form-model>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SNMPConfig',
|
||||
model: {
|
||||
prop: 'value',
|
||||
event: 'change',
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
inject: ['provide_labelCol'],
|
||||
computed: {
|
||||
formData: {
|
||||
get() {
|
||||
return this.value
|
||||
},
|
||||
set(newValue) {
|
||||
this.$emit('change', newValue)
|
||||
}
|
||||
},
|
||||
labelCol() {
|
||||
return this.provide_labelCol()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<a-form-model
|
||||
:model="formData"
|
||||
:labelCol="labelCol"
|
||||
:wrapperCol="{ span: 6 }"
|
||||
class="attr-ad-form"
|
||||
>
|
||||
<a-form-model-item
|
||||
:label="$t('cmdb.ciType.initialNode')"
|
||||
:extra="$t('cmdb.ciType.snmpFormTip4')"
|
||||
>
|
||||
<a-input
|
||||
v-model="formData.initial_node"
|
||||
:placeholder="$t('cmdb.ciType.defaultGateway')"
|
||||
/>
|
||||
</a-form-model-item>
|
||||
|
||||
<a-form-model-item
|
||||
:label="$t('cmdb.ciType.recursiveOrNot')"
|
||||
:extra="$t('cmdb.ciType.snmpFormTip5')"
|
||||
>
|
||||
<a-switch v-model="formData.recursive_scan" />
|
||||
</a-form-model-item>
|
||||
|
||||
<a-form-model-item
|
||||
:label="$t('cmdb.ciType.maximumDepth')"
|
||||
:extra="$t('cmdb.ciType.snmpFormTip6')"
|
||||
>
|
||||
<a-input-number
|
||||
v-model="formData.max_depth"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
/>
|
||||
</a-form-model-item>
|
||||
</a-form-model>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SNMPScanningConfig',
|
||||
model: {
|
||||
prop: 'value',
|
||||
event: 'change',
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
inject: ['provide_labelCol'],
|
||||
computed: {
|
||||
formData: {
|
||||
get() {
|
||||
return this.value
|
||||
},
|
||||
set(newValue) {
|
||||
this.$emit('change', newValue)
|
||||
}
|
||||
},
|
||||
labelCol() {
|
||||
return this.provide_labelCol()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
}
|
||||
}
|
||||
</script>
|
@@ -1,40 +1,35 @@
|
||||
<template>
|
||||
<a-row class="attr-ad-form">
|
||||
<a-col :span="24">
|
||||
<a-form-item
|
||||
label="CIDR"
|
||||
:labelCol="labelCol"
|
||||
:wrapperCol="{ span: 18 }"
|
||||
labelAlign="right"
|
||||
style="width: 100%; margin-top: 20px"
|
||||
<a-form-item
|
||||
label="CIDR"
|
||||
:labelCol="labelCol"
|
||||
:wrapperCol="{ span: 6 }"
|
||||
:extra="$t('cmdb.ciType.snmpFormTip7')"
|
||||
>
|
||||
<div class="cidr-tag">
|
||||
<div
|
||||
v-for="(item) in list"
|
||||
:key="item.id"
|
||||
class="cidr-tag-item"
|
||||
>
|
||||
<div class="cidr-tag">
|
||||
<div
|
||||
v-for="(item) in list"
|
||||
:key="item.id"
|
||||
class="cidr-tag-item"
|
||||
>
|
||||
<a-tooltip :title="item.value">
|
||||
<span class="cidr-tag-text">{{ item.value }}</span>
|
||||
</a-tooltip>
|
||||
<a-icon
|
||||
class="cidrv-tag-close"
|
||||
type="close"
|
||||
@click.stop="clickClose(item.id)"
|
||||
/>
|
||||
</div>
|
||||
<a-input
|
||||
v-if="showAddInput"
|
||||
class="cidr-tag-input"
|
||||
autofocus
|
||||
@blur="addPreValue"
|
||||
@pressEnter="showAddInput = false"
|
||||
></a-input>
|
||||
<a v-else class="cidr-tag-add" @click="showAddInput = true">+ {{ $t('new') }}</a>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-tooltip :title="item.value">
|
||||
<span class="cidr-tag-text">{{ item.value }}</span>
|
||||
</a-tooltip>
|
||||
<a-icon
|
||||
class="cidrv-tag-close"
|
||||
type="close"
|
||||
@click.stop="clickClose(item.id)"
|
||||
/>
|
||||
</div>
|
||||
<a-input
|
||||
v-if="showAddInput"
|
||||
class="cidr-tag-input"
|
||||
autofocus
|
||||
@blur="addPreValue"
|
||||
@pressEnter="showAddInput = false"
|
||||
></a-input>
|
||||
<a v-else class="cidr-tag-add" @click="showAddInput = true">+ {{ $t('new') }}</a>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<a-form-item
|
||||
:labelCol="labelCol"
|
||||
:wrapperCol="{ span: 18 }"
|
||||
>
|
||||
<span slot="label">
|
||||
{{ $t('cmdb.ciType.nodeList') }}
|
||||
<a-tooltip :title="$t('cmdb.ciType.snmpFormTip1')">
|
||||
<a-icon type="question-circle" />
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<div class="node-setting-wrap">
|
||||
<ops-table
|
||||
:data="nodes"
|
||||
size="mini"
|
||||
show-header-overflow
|
||||
:row-config="{ height: 42 }"
|
||||
border
|
||||
:min-height="78"
|
||||
>
|
||||
<vxe-column width="170" :title="$t('cmdb.ciType.nodeSettingIp')">
|
||||
<template #default="{ row }">
|
||||
<a-input v-model="row.ip"></a-input>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column width="170" :title="$t('cmdb.ciType.nodeSettingCommunity')">
|
||||
<template #default="{ row }">
|
||||
<a-input v-model="row.community"></a-input>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column width="170" :title="$t('cmdb.ciType.nodeSettingVersion')">
|
||||
<template #default="{ row }">
|
||||
<a-select
|
||||
v-model="row.version"
|
||||
:placeholder="$t('cmdb.ciType.nodeSettingVersionTip')"
|
||||
allowClear
|
||||
class="node-setting-select"
|
||||
>
|
||||
<a-select-option value="1">
|
||||
v1
|
||||
</a-select-option>
|
||||
<a-select-option value="2c">
|
||||
v2c
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column min-wdith="90">
|
||||
<template #default="{ row }">
|
||||
<div class="action">
|
||||
<a @click="() => copyNode(row.id)">
|
||||
<a-icon type="copy" />
|
||||
</a>
|
||||
<a @click="() => removeNode(row.id, 1)">
|
||||
<a-icon type="minus-circle" />
|
||||
</a>
|
||||
<a @click="addNode">
|
||||
<a-icon type="plus-circle" />
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
</vxe-column>
|
||||
</ops-table>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
export default {
|
||||
name: 'MonitorNodeSetting',
|
||||
inject: ['provide_labelCol'],
|
||||
props: {
|
||||
form: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
nodes: [],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
labelCol() {
|
||||
return this.provide_labelCol()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
initNodesFunc(nodes) {
|
||||
this.nodes = _.cloneDeep(nodes)
|
||||
},
|
||||
addNode() {
|
||||
const newNode = {
|
||||
id: uuidv4(),
|
||||
ip: '',
|
||||
community: 'public',
|
||||
version: '',
|
||||
}
|
||||
this.nodes.push(newNode)
|
||||
},
|
||||
removeNode(removeId, minLength) {
|
||||
if (this.nodes.length <= minLength) {
|
||||
this.$message.error('不可再删除!')
|
||||
return
|
||||
}
|
||||
const _idx = this.nodes.findIndex((item) => item.id === removeId)
|
||||
if (_idx > -1) {
|
||||
this.nodes.splice(_idx, 1)
|
||||
}
|
||||
},
|
||||
copyNode(id) {
|
||||
const copyNode = this.nodes.find((item) => item.id === id)
|
||||
if (copyNode) {
|
||||
const newNode = {
|
||||
...copyNode,
|
||||
id: uuidv4(),
|
||||
}
|
||||
this.nodes.push(newNode)
|
||||
}
|
||||
},
|
||||
|
||||
getNodeValue() {
|
||||
const nodes = this.nodes.map((node) => {
|
||||
return _.pick(node, ['ip', 'community', 'version'])
|
||||
})
|
||||
return nodes
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.node-setting-wrap {
|
||||
max-width: 600px;
|
||||
|
||||
.ant-row {
|
||||
/deep/ .ant-input-clear-icon {
|
||||
color: rgba(0,0,0,.25);
|
||||
|
||||
&:hover {
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.node-setting-select {
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
.action {
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
@@ -54,11 +54,20 @@
|
||||
/>
|
||||
</div>
|
||||
<template v-if="adrType === DISCOVERY_CATEGORY_TYPE.SNMP">
|
||||
<div class="attr-ad-header">{{ $t('cmdb.ciType.nodeConfig') }}</div>
|
||||
<a-form :form="nodeSettingForm" layout="inline" class="attr-ad-snmp-form">
|
||||
<NodeSetting ref="nodeSetting" :initNodes="nodes" />
|
||||
<CIDRTags v-model="cidrList" />
|
||||
</a-form>
|
||||
<div class="attr-ad-header">{{ $t('cmdb.ciType.scanningParameter') }}</div>
|
||||
<div class="attr-ad-form attr-ad-snmp-form">
|
||||
<div class="attr-ad-snmp-form-title">
|
||||
{{ $t('cmdb.ciType.SNMPConfiguration') }}
|
||||
</div>
|
||||
<NodeSetting ref="nodeSetting" />
|
||||
<SNMPConfig v-model="SNMPScanningConfigForm" />
|
||||
|
||||
<div class="attr-ad-snmp-form-title">
|
||||
{{ $t('cmdb.ciType.scanningConfiguration') }}
|
||||
</div>
|
||||
<SNMPScanningConfig v-model="SNMPScanningConfigForm" />
|
||||
<CIDRTags v-model="SNMPScanningConfigForm.cidr" />
|
||||
</div>
|
||||
</template>
|
||||
<div class="attr-ad-header">{{ $t('cmdb.ciType.adExecConfig') }}</div>
|
||||
<a-form-model
|
||||
@@ -177,13 +186,15 @@ import { TAB_KEY } from './attrAD/constants.js'
|
||||
import HttpSnmpAD from '../../components/httpSnmpAD'
|
||||
import AttrMapTable from '@/modules/cmdb/components/attrMapTable/index.vue'
|
||||
import CMDBExprDrawer from '@/components/CMDBExprDrawer'
|
||||
import NodeSetting from '@/modules/cmdb/components/nodeSetting/index.vue'
|
||||
import NodeSetting from './attrAD/nodeSetting/index.vue'
|
||||
import AttrADTest from './attrADTest.vue'
|
||||
import { Popover } from 'element-ui'
|
||||
import VcenterForm from './attrAD/privateCloud/vcenterForm.vue'
|
||||
import PublicCloud from './attrAD/publicCloud/index.vue'
|
||||
import PortScanConfig from './attrAD/portScanConfig/index.vue'
|
||||
import CIDRTags from './attrAD/cidrTags/index.vue'
|
||||
import SNMPScanningConfig from './attrAD/SNMPScanningConfig/index.vue'
|
||||
import SNMPConfig from './attrAD/SNMPConfig/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'AttrADTabpane',
|
||||
@@ -198,7 +209,9 @@ export default {
|
||||
VcenterForm,
|
||||
PublicCloud,
|
||||
PortScanConfig,
|
||||
CIDRTags
|
||||
CIDRTags,
|
||||
SNMPScanningConfig,
|
||||
SNMPConfig
|
||||
},
|
||||
props: {
|
||||
adr_id: {
|
||||
@@ -263,14 +276,6 @@ export default {
|
||||
cronVisible: false,
|
||||
intervalValue: 3,
|
||||
agent_type: 'agent_id',
|
||||
nodes: [
|
||||
{
|
||||
id: uuidv4(),
|
||||
ip: '',
|
||||
community: 'public',
|
||||
version: '',
|
||||
},
|
||||
],
|
||||
nodeSettingForm: this.$form.createForm(this, { name: 'snmp_form' }),
|
||||
uniqueKey: '',
|
||||
isPrivateCloud: false,
|
||||
@@ -278,7 +283,16 @@ export default {
|
||||
PRIVATE_CLOUD_NAME,
|
||||
DISCOVERY_CATEGORY_TYPE,
|
||||
isClient: false, // 是否前端新增临时数据
|
||||
cidrList: [],
|
||||
SNMPScanningConfigForm: {
|
||||
version: '2c',
|
||||
community: 'public',
|
||||
timeout: 5,
|
||||
retries: 3,
|
||||
initial_node: '',
|
||||
recursive_scan: true,
|
||||
max_depth: 5,
|
||||
cidr: []
|
||||
}, // snmp scanning config form data
|
||||
}
|
||||
},
|
||||
provide() {
|
||||
@@ -323,13 +337,13 @@ export default {
|
||||
const isEn = this.$i18n.locale === 'en'
|
||||
return {
|
||||
xl: {
|
||||
span: isEn ? 4 : 2
|
||||
span: isEn ? 4 : 3
|
||||
},
|
||||
lg: {
|
||||
span: isEn ? 5 : 3
|
||||
span: isEn ? 5 : 4
|
||||
},
|
||||
sm: {
|
||||
span: isEn ? 6 : 4
|
||||
span: isEn ? 6 : 5
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -404,7 +418,13 @@ export default {
|
||||
}
|
||||
|
||||
if (this.adrType === DISCOVERY_CATEGORY_TYPE.SNMP) {
|
||||
const nodes = _findADT?.extra_option?.nodes?.length ? _findADT?.extra_option?.nodes : [
|
||||
const extra_option = _findADT?.extra_option ?? {}
|
||||
const {
|
||||
nodes,
|
||||
cidr = []
|
||||
} = extra_option
|
||||
|
||||
const initializeNodes = nodes?.length ? nodes : [
|
||||
{
|
||||
id: uuidv4(),
|
||||
ip: '',
|
||||
@@ -412,13 +432,11 @@ export default {
|
||||
version: '',
|
||||
},
|
||||
]
|
||||
this.nodes = nodes
|
||||
this.$nextTick(() => {
|
||||
this.$refs.nodeSetting.initNodesFunc()
|
||||
this.$refs.nodeSetting.initNodesFunc(initializeNodes)
|
||||
})
|
||||
|
||||
let cidrList = []
|
||||
const cidr = _findADT?.extra_option?.cidr
|
||||
if (Array.isArray(cidr) && cidr?.length) {
|
||||
cidrList = cidr.map((v) => {
|
||||
return {
|
||||
@@ -427,7 +445,16 @@ export default {
|
||||
}
|
||||
})
|
||||
}
|
||||
this.cidrList = cidrList
|
||||
this.SNMPScanningConfigForm = {
|
||||
version: extra_option?.version ?? '2c',
|
||||
community: extra_option?.community ?? 'public',
|
||||
timeout: extra_option?.timeout ?? 5,
|
||||
retries: extra_option?.retries ?? 3,
|
||||
initial_node: extra_option?.initial_node ?? '',
|
||||
recursive_scan: extra_option?.recursive_scan ?? true,
|
||||
max_depth: extra_option?.max_depth ?? 5,
|
||||
cidr: cidrList
|
||||
}
|
||||
}
|
||||
if (this.adrType === DISCOVERY_CATEGORY_TYPE.AGENT) {
|
||||
this.tableData = (_find?.attributes || []).map((item) => {
|
||||
@@ -501,12 +528,27 @@ export default {
|
||||
}
|
||||
|
||||
if (this.adrType === DISCOVERY_CATEGORY_TYPE.SNMP) {
|
||||
const {
|
||||
cidr,
|
||||
...otherConfigForm
|
||||
} = this.SNMPScanningConfigForm
|
||||
const nodes = this.$refs.nodeSetting?.getNodeValue() ?? []
|
||||
|
||||
params = {
|
||||
extra_option: {
|
||||
nodes: this.$refs.nodeSetting?.getNodeValue() ?? [],
|
||||
cidr: this?.cidrList?.map((item) => item.value) || []
|
||||
...otherConfigForm,
|
||||
nodes,
|
||||
cidr: cidr?.map((item) => item.value) || []
|
||||
},
|
||||
}
|
||||
|
||||
if (
|
||||
!otherConfigForm?.recursive_scan &&
|
||||
nodes?.some((item) => !item?.ip)
|
||||
) {
|
||||
this.$message.error(this.$t('cmdb.ciType.recursiveTip'))
|
||||
return
|
||||
}
|
||||
}
|
||||
if (this.adrType === DISCOVERY_CATEGORY_TYPE.AGENT) {
|
||||
const $table = this.$refs.attrMapTable
|
||||
@@ -761,8 +803,18 @@ export default {
|
||||
}
|
||||
}
|
||||
.attr-ad-snmp-form {
|
||||
.ant-form-item {
|
||||
margin-bottom: 0;
|
||||
&-title {
|
||||
font-size: 16px;
|
||||
color: #000000;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/deep/ .ant-input-number {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/deep/ .ant-form-extra {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -179,7 +179,11 @@
|
||||
:filterOption="filterOption"
|
||||
@change="changeChild"
|
||||
>
|
||||
<a-select-option :value="CIType.id" :key="CIType.id" v-for="CIType in CITypes">
|
||||
<a-select-option
|
||||
:value="CIType.id"
|
||||
:key="CIType.id"
|
||||
v-for="CIType in CITypes"
|
||||
>
|
||||
{{ CIType.alias || CIType.name }}
|
||||
<span class="model-select-name">({{ CIType.name }})</span>
|
||||
</a-select-option>
|
||||
@@ -510,7 +514,11 @@ export default {
|
||||
})
|
||||
},
|
||||
filterOption(input, option) {
|
||||
return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
const inputValue = input.toLowerCase()
|
||||
const alias = option.componentOptions.children[0].text.toLowerCase()
|
||||
const name = option.componentOptions.children[1]?.elm?.innerHTML?.toLowerCase?.() ?? ''
|
||||
|
||||
return alias.indexOf(inputValue) >= 0 || name.indexOf(inputValue) >= 0
|
||||
},
|
||||
rowClass({ row }) {
|
||||
if (row.isDivider) return 'relation-table-divider'
|
||||
|
@@ -112,11 +112,18 @@ export default {
|
||||
})
|
||||
let CIList = res?.result || []
|
||||
|
||||
const {
|
||||
show_key = '',
|
||||
unique_id = '',
|
||||
attributes = []
|
||||
} = this?.currentCITYpe || {}
|
||||
const unique_key = attributes?.find((attr) => attr?.id === unique_id)?.name || ''
|
||||
|
||||
if (CIList.length) {
|
||||
CIList = CIList.map((item) => {
|
||||
return {
|
||||
value: item?._id,
|
||||
name: item?.[this?.currentCITYpe?.show_key] || item?._id || '',
|
||||
name: item?.[show_key] || item?.[unique_key] || item?._id || '',
|
||||
unitCount: item?.u_count ?? 0
|
||||
}
|
||||
})
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="ops-login">
|
||||
<div class="ops-login-left">
|
||||
<span>维易科技<br />让运维更简单</span>
|
||||
<span>维易科技 让运维变简单</span>
|
||||
</div>
|
||||
<div class="ops-login-right">
|
||||
<img src="../../assets/logo_VECMDB.png" />
|
||||
@@ -216,12 +216,12 @@ export default {
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
> span {
|
||||
color: white;
|
||||
color: @text-color_2;
|
||||
position: absolute;
|
||||
top: 10%;
|
||||
bottom: 10%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 1.75vw;
|
||||
font-size: 26px;
|
||||
}
|
||||
}
|
||||
.ops-login-right {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
cmdb-db:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-db:2.3
|
||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-db:2.5
|
||||
container_name: cmdb-db
|
||||
env_file:
|
||||
- .env
|
||||
@@ -24,7 +24,7 @@ services:
|
||||
- '23306:3306'
|
||||
|
||||
cmdb-cache:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-cache:2.3
|
||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-cache:2.5
|
||||
container_name: cmdb-cache
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
@@ -41,7 +41,7 @@ services:
|
||||
- redis
|
||||
|
||||
cmdb-api:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.4.17
|
||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.5.2
|
||||
container_name: cmdb-api
|
||||
env_file:
|
||||
- .env
|
||||
@@ -84,7 +84,7 @@ services:
|
||||
test: "ps aux|grep -v grep|grep -v '1 root'|grep gunicorn || exit 1"
|
||||
|
||||
cmdb-ui:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.4.17
|
||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.5.2
|
||||
container_name: cmdb-ui
|
||||
depends_on:
|
||||
cmdb-api:
|
||||
|
61
docs/CONTRIBUTING.md
Normal file
61
docs/CONTRIBUTING.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# 🎉 Contributing to CMDB 🥳
|
||||
|
||||
首先,非常感谢您考虑为我们的项目做出贡献!我们欢迎任何形式的贡献,无论是提出新功能、改进代码、修复 bug 还是改善文档。
|
||||
|
||||
本指南将为您提供所有相关信息,帮助您快速入门并开始参与本项目。请花几分钟阅读它,它将帮助我们更好地协作,共同创造一个更好的项目。
|
||||
|
||||
## ❖ 提交问题 (Issue)
|
||||
|
||||
在提交 PR 之前,请先搜索 现有的 [PR](https://github.com/veops/cmdb/pulls) 或 [Issue](https://github.com/veops/cmdb/issues),查看是否已经有相关的开放或关闭的提交。
|
||||
|
||||
如果是修复 bug,请首先提交一个 Issue。
|
||||
|
||||
对于新增功能,请先通过我们提供的联系方式与我们直接联系,以便更好的合作。
|
||||
|
||||
## ❖ 提交 PR 的步骤
|
||||
|
||||
1. 在 Github 上 fork 该项目的仓库。
|
||||
2. 在本地复制仓库后创建一个新分支,用于开发新功能、修复 bug 或进行其他贡献,命令:`git checkout -b feat/xxxx`。
|
||||
3. 提交您的更改:`git commit -am 'feat: add xxxxx'`。
|
||||
4. 推送您的分支:`git push origin feat/xxxx`。
|
||||
5. 提交 Pull Request 时,请确保您的源分支是刚刚推送的分支,目标分支是 CMDB 项目的 master 分支。
|
||||
6. 提交后,请留意与 Pull Request 相关的邮件和通知。待通过审核后,我们会按计划将其合并到 master 分支,并进行新一轮的版本发布。
|
||||
|
||||
## ❖ 开发环境
|
||||
- Python 版本 >= 3.8
|
||||
- Node.js 版本 >= 14.17.6
|
||||
- Docker
|
||||
|
||||
## ❖ 代码风格
|
||||
|
||||
**API**: 请遵循 [Python Style](https://google.github.io/styleguide/pyguide.html)
|
||||
|
||||
**UI**: 请遵循 [node-style-guide](https://github.com/felixge/node-style-guide)
|
||||
|
||||
## ❖ 提交信息
|
||||
|
||||
+ 请遵循 [Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular)
|
||||
|
||||
+ 提交时使用不同的范围
|
||||
- API: `feat(api): xxx`
|
||||
- UI: `feat(ui): xxx`
|
||||
|
||||
+ 为了确保所有开发者都能更好地理解,提交信息请使用英文。
|
||||
|
||||
- `feat` 添加新功能
|
||||
- `fix` 修复问题/BUG
|
||||
- `style` 代码风格相关,不影响运行结果
|
||||
- `perf` 优化/性能提升
|
||||
- `refactor` 代码重构
|
||||
- `revert` 撤销编辑
|
||||
- `test` 测试相关
|
||||
- `docs` 文档/注释
|
||||
- `chore` 依赖更新/脚手架配置修改等
|
||||
- `workflow` 工作流优化
|
||||
- `ci` 持续集成
|
||||
- `types` 类型定义文件变更
|
||||
- `wip` 开发中
|
||||
|
||||
## ❖ 代码内容
|
||||
|
||||
为了便于所有开发者理解,请确保代码注释和代码内容使用英文。
|
61
docs/CONTRIBUTING_en.md
Normal file
61
docs/CONTRIBUTING_en.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# 🎉 Contributing to CMDB 🥳
|
||||
|
||||
First of all, thank you very much for considering contributing to our project! We welcome any kind of contribution, whether it's proposing new functional features, improving code, fixing bugs, or improving documentation.
|
||||
|
||||
This guide will provide all the relevant information to help you get started working on this project. Please take a few minutes to read it, it will help us collaborate better and create a better project together.
|
||||
|
||||
## ❖ Submit Issue
|
||||
|
||||
Before jumping into a PR be sure to search [existing PRs](https://github.com/veops/cmdb/pulls) or [issues](https://github.com/veops/cmdb/issues) for an open or closed item that relates to your submission.
|
||||
|
||||
If it's a bug fix, please raise it in an Issue first.
|
||||
|
||||
For new feature additions, please first contact us directly via the contact information we provide.
|
||||
|
||||
## ❖ Pull Requests Steps
|
||||
|
||||
1. Fork this project's repo on Github
|
||||
2. Create a new branch on your local copy to develop new features, fix bugs, or make other contributions, `git checkout -b feat/xxxx`
|
||||
3. Submit your changes: `git commit -am 'feat: add xxxxx'`
|
||||
4. Push your branch: `git push origin feat/xxxx`
|
||||
5. To submit a `Pull Request`, make sure your source branch is the one you just pushed, and your target branch is the `master` branch of the CMDB project.
|
||||
6. After submitting, watch out for emails and notifications associated with the Pull request. Once it's approved, we'll merge it into the `master` branch as planned. Doing a new round of releases
|
||||
|
||||
## ❖ Development Environment
|
||||
- Python >= 3.8
|
||||
- node >= 14.17.6
|
||||
- docker
|
||||
|
||||
## ❖ Code Style
|
||||
|
||||
**API**: Please follow the [Python Style](https://google.github.io/styleguide/pyguide.html)
|
||||
|
||||
**UI**: Please follow the [node-style-guide](https://github.com/felixge/node-style-guide)
|
||||
|
||||
## ❖ Commit Messages
|
||||
|
||||
+ Please follow the [Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular)
|
||||
|
||||
+ Commit with different scopes
|
||||
- API: `feat(api): xxx`
|
||||
- UI: `feat(ui): xxx`
|
||||
|
||||
+ Please keep the commit message in English for better understanding by all developers.
|
||||
|
||||
- `feat` Add new features
|
||||
- `fix` Fix the problem/BUG
|
||||
- `style` The code style is related and does not affect the running result
|
||||
- `perf` Optimization/performance improvement
|
||||
- `refactor` Refactor
|
||||
- `revert` Undo edit
|
||||
- `test` Test related
|
||||
- `docs` Documentation/notes
|
||||
- `chore` Dependency update/scaffolding configuration modification etc.
|
||||
- `workflow` Workflow improvements
|
||||
- `ci` Continuous integration
|
||||
- `types` Type definition file changes
|
||||
- `wip` In development
|
||||
|
||||
## ❖ Code Content
|
||||
|
||||
Please keep the code comments and code content in English for better understanding by all developers.
|
@@ -1,101 +1,126 @@
|
||||
|
||||
<h2 align="center">Simple, lightweight, and versatile operational CMDB</h2>
|
||||
<p align="center">
|
||||
<a href="https://github.com/veops/cmdb/blob/master/LICENSE"><img src="https://img.shields.io/badge/License-AGPLv3-brightgreen" alt="License: GPLv3"></a>
|
||||
<a href="https:https://github.com/sendya/ant-design-pro-vue"><img src="https://img.shields.io/badge/UI-Ant%20Design%20Pro%20Vue-brightgreen" alt="UI"></a>
|
||||
<a href="https://github.com/pallets/flask"><img src="https://img.shields.io/badge/API-Flask-brightgreen" alt="API"></a>
|
||||
<a href="https://veops.cn">
|
||||
<img src="https://github.com/user-attachments/assets/c5cfb272-899b-418d-9e69-8e1dd07db0f6" alt="VE CMDB"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h4 align="center">Simple, lightweight, and versatile operational CMDB</h4>
|
||||
<p align="center">
|
||||
<a href="https://github.com/veops/cmdb/blob/master/LICENSE"><img src="https://img.shields.io/badge/License-AGPLv3-brightgreen" alt="License: GPLv3"></a>
|
||||
<a href="https://github.com/veops/cmdb/releases"><img alt="the latest release version" src="https://img.shields.io/github/v/release/veops/cmdb?color=75C1C4&include_prereleases&label=Release&logo=github&logoColor=white"></a>
|
||||
<a href="https:https://github.com/sendya/ant-design-pro-vue"><img src="https://img.shields.io/badge/UI-Ant%20Design%20Pro%20Vue-green" alt="UI"></a>
|
||||
<a href="https://github.com/pallets/flask"><img src="https://img.shields.io/badge/API-Flask-bright" alt="API"></a>
|
||||
<a href="https://github.com/veops/cmdb/stargazers"><img src="https://img.shields.io/github/stars/veops/cmdb" alt="Stars Badge"/></a>
|
||||
<a href="https://github.com/veops/cmdb"><img src="https://img.shields.io/github/forks/veops/cmdb" alt="Forks Badge"/></a>
|
||||
</p>
|
||||
|
||||
------------------------------
|
||||
<p align="center">
|
||||
English · <a href="../README.md">中文(简体)</a>
|
||||
</p>
|
||||
|
||||
[English](README_en.md) / [中文](../README.md)
|
||||
## Introduce
|
||||
VE CMDB is a simple, lightweight and highly customizable operations and maintenance configuration management database (CMDB). It supports flexible model configuration and resource auto-discovery, and is designed to provide organizations with a convenient asset management solution that helps operations teams efficiently manage IT infrastructure and services.
|
||||
|
||||
## DEMO ONLINE
|
||||
- Product document:https://veops.cn/docs/
|
||||
- Preview online: <a href="https://cmdb.veops.cn" target="_blank">CMDB</a>
|
||||
- Product document:[https://veops.cn/docs/](https://veops.cn/docs/)
|
||||
- Preview online: [https://cmdb.veops.cn](https://cmdb.veops.cn)
|
||||
- username: demo
|
||||
- password: 123456
|
||||
|
||||
> **ATTENTION**: branch `master` may be unstable as the result of continued development, please pull code from [releases](https://github.com/veops/cmdb/releases)
|
||||
|
||||
## Overview
|
||||
|
||||
- **ATTENTION**: branch `master` may be unstable as the result of continued development, Please use [releases](https://github.com/veops/cmdb/releases) to get the latest stable version
|
||||
|
||||
### Features
|
||||
|
||||
- **Custom Model and Model Relationships**: Supports customization of model attributes, including drop-down lists, font colors, calculated attributes and other advanced functions to meet different business needs.
|
||||
- **Auto-discovery of resources**: supports auto-discovery of computers, network devices, storage devices, databases, middleware, public cloud resources, etc.
|
||||
- **Multi-dimensional view display**: including resource view, hierarchical view, relationship view, etc., helping O&M personnel to comprehensively manage resources.
|
||||
- **Fine-grained privilege control**: ensure system security through precise access control and complete operation logs.
|
||||
- **Comprehensive Resource Search Function**: Supports flexible resource and relationship search to quickly locate and operate resources.
|
||||
- **Integrated IP Address Management (IPAM) and Data Center Infrastructure Management (DCIM) features**: Simplify the management of network resources and data center equipment.
|
||||
|
||||
For more detailed features, please visit the [official website](https://veops.cn).
|
||||
|
||||
### System Advantage
|
||||
|
||||
- Flexibility
|
||||
1. Standardize and manage complex data assets
|
||||
2. Automatically discover and inventory IT assets
|
||||
+ No need to specify fixed operation and maintenance scenarios, supports free configuration and built-in multiple templates.
|
||||
+ Support automatic discovery and inventory of IT assets, quickly set up an asset management system.
|
||||
- Security
|
||||
1. Fine-grained access control
|
||||
2. Comprehensive operation logs
|
||||
+ Fine-grained permission control mechanism to ensure the security of resource management.
|
||||
+ Complete operation logs for easy auditing and problem tracking.
|
||||
- Multi-application
|
||||
1. Rich view display dimensions
|
||||
2. Provide Restful API
|
||||
3. Custom field triggers
|
||||
+ Provides multiple views to meet the needs of different scenarios.
|
||||
+ Powerful API interface supports deep integration.
|
||||
+ Support for defining attribute triggers and calculating attributes to enhance data processing capabilities.
|
||||
|
||||
### Main Features
|
||||
### Tech Stack
|
||||
|
||||
- Custom models and model relationships, with model attributes supporting advanced features such as dropdown lists, font colors, and computed attributes.
|
||||
- Support for automatic discovery of computers, network devices, storage devices, databases, middleware, public cloud resources, etc.
|
||||
- Support for displaying resource, hierarchy, and relationship views.
|
||||
- Fine-grained access control and comprehensive operation logs.
|
||||
- General resource and relationship search capabilities.
|
||||
- Support for IP Address Management (IPAM) and Data Center Infrastructure Management (DCIM).
|
||||
+ Back-end: Python [3.8-3.11].
|
||||
+ Data Storage: MySQL, Redis
|
||||
+ Front-end: Vue.js
|
||||
+ UI component library: Ant Design Vue
|
||||
|
||||
### Overview
|
||||
|
||||
<table style="border-collapse: collapse;">
|
||||
<tr>
|
||||
<td style="padding: 5px;background-color:#fff;">
|
||||
<img width="400" src="https://github.com/user-attachments/assets/6d2df835-ae93-4d91-9bd9-213c270eca7a"/>
|
||||
</td>
|
||||
<td style="padding: 5px;background-color:#fff;">
|
||||
<img width="400" src="https://github.com/user-attachments/assets/cb8b598a-a1f9-4c74-adf1-6e59aea2c9b3"/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
### More Features
|
||||
<tr>
|
||||
<td style="padding: 5px;background-color:#fff;">
|
||||
<img width="400" src="https://github.com/user-attachments/assets/b440224f-53c3-4b7f-a9be-285d7a4b848f"/>
|
||||
</td>
|
||||
<td style="padding: 5px;background-color:#fff;">
|
||||
<img width="400" src="https://github.com/user-attachments/assets/f457d5a0-b60b-4949-b94e-020f4c61444b"/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
> Welcome to visit VeOps official website to discover more free operations and maintenance systems.
|
||||
## Getting started & staying tuned with us
|
||||
|
||||
## Installation
|
||||
Star us, and you will receive all releases notifications from GitHub without any delay!
|
||||
|
||||
### One-Click Docker Quick Build
|
||||

|
||||
|
||||
[//]: # (> Method 1)
|
||||
- step 1: **Prepare: install Docker and Docker Compose (v2)**
|
||||
- step 2: copy the repository
|
||||
```shell
|
||||
git clone https://github.com/veops/cmdb.git
|
||||
```
|
||||
- step 3: In directory cmdb:
|
||||
```
|
||||
docker compose up -d
|
||||
```
|
||||
## Quick Start
|
||||
|
||||
[//]: # (> M**ethod 2 Usefull for linux os.)
|
||||
### 1. Set up
|
||||
|
||||
[//]: # (- step 1: **Prepare: install Docker and Docker Compose (v2)**)
|
||||
+ Option 1: Docker One-Click Quick Builds
|
||||
|
||||
[//]: # (- step 2: directly use the install.sh file in the project's root directory to `install`, `start`, `pause`, `status`, `delete`, and `uninstall` the application. )
|
||||
- Step 1: Install Docker environment and Docker Compose (v2)
|
||||
- Step 2: Copy the project code, `git clone https://github.com/veops/cmdb.git`
|
||||
- Step 3: Enter the home directory and start, `docker compose up -d`
|
||||
|
||||
[//]: # (```shell)
|
||||
+ [Local Setup](local_en.md)
|
||||
+ [Installation with Makefile](makefile_en.md)
|
||||
|
||||
[//]: # (curl -so install.sh https://raw.githubusercontent.com/veops/cmdb/master/install.sh )
|
||||
### 2. Visit
|
||||
- Open your browser and visit: [http://127.0.0.1:8000](http://127.0.0.1:8000)
|
||||
- Username: demo or admin
|
||||
- Password: 123456
|
||||
|
||||
[//]: # (sh install.sh install)
|
||||
## Access Company
|
||||
|
||||
[//]: # (```**)
|
||||
|
||||
|
||||
### [Local Setup](local_en.md)
|
||||
|
||||
### [Installation with Makefile](makefile_en.md)
|
||||
|
||||
## Validation
|
||||
|
||||
- View: [http://127.0.0.1:8000](http://127.0.0.1:8000)
|
||||
- username: demo or admin
|
||||
- password: 123456
|
||||
+ Companies using the open source CMDB are welcome to record in [#112](https://github.com/veops/cmdb/issues/112)
|
||||
|
||||
## Contributing
|
||||
We welcome all developers to contribute code to improve and extend this project. Please read our [contribution guidelines](./CONTRIBUTING_en.md) first. Additionally, you can support Veops open source through social media, events, and sharing.
|
||||
|
||||
1. Fork it
|
||||
2. Create your feature branch (`git checkout -b my-feature`)
|
||||
3. Commit your changes (`git commit -am 'Add some feature'`)
|
||||
4. Push to the branch (`git push origin my-feature`)
|
||||
5. Create new Pull Request
|
||||
<a href="https://github.com/veops/cmdb/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=veops/cmdb" />
|
||||
</a>
|
||||
|
||||
---
|
||||
## More Open Source
|
||||
|
||||
- [OneTerm](https://github.com/veops/oneterm): Provide secure access and control over all infrastructure.
|
||||
- [messenger](https://github.com/veops/messenger): A simple and lightweight message sending service.
|
||||
- [ACL](https://github.com/veops/acl): A general permission control management system.
|
||||
|
||||
## Contact me
|
||||
|
||||
+ Email: <a href="mailto:bd@veops.cn">bd@veops.cn</a>
|
Binary file not shown.
Before Width: | Height: | Size: 51 KiB |
@@ -48,7 +48,7 @@ max_connections=1000
|
||||
slow_query_log = ON
|
||||
slow_query_log_file = /tmp/mysql_slow.log
|
||||
long_query_time = 1
|
||||
sql_mode="STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION"
|
||||
sql_mode="STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION"
|
||||
#log-error = /var/log/mysql/error.log
|
||||
# By default we only accept connections from localhost
|
||||
#bind-address = 127.0.0.1
|
||||
|
@@ -26,6 +26,8 @@ server {
|
||||
application/rss+xml
|
||||
image/svg+xml;
|
||||
|
||||
client_max_body_size 100m;
|
||||
|
||||
root /etc/nginx/html;
|
||||
location / {
|
||||
root /etc/nginx/html;
|
||||
|
Reference in New Issue
Block a user