Compare commits

...

55 Commits

Author SHA1 Message Date
pycook
a4e686f9ae chore: release v2.5.4 2025-08-15 21:54:46 +08:00
LH_R
45bb3867da fix(ui): resource views - fix page display during initialization 2025-08-15 21:37:07 +08:00
LH_R
bb467030e2 feat(ui): attribute association - many-to-many cannot all be multi-value 2025-08-15 21:34:01 +08:00
pycook
0b1dfa4538 feat(api): multi-value attribute handling in relation building 2025-08-15 20:32:22 +08:00
LH_R
56a310c667 feat(ui): record the page path when logout 2025-08-15 16:09:41 +08:00
LH_R
290a79860c feat(ui): update tree views page empty data display 2025-08-15 16:07:35 +08:00
LH_R
0144ee6508 feat(ui): add automatic subscription #285 2025-08-15 16:06:23 +08:00
LH_R
035171cbe8 feat(ui): update resource view menu display and CMDB route redirection 2025-08-15 16:04:10 +08:00
LH_R
f7273c96dc feat(ui): support multi-value attribute associations 2025-08-15 15:57:59 +08:00
LH_R
73bdd99829 feat(ui): update iconfont 2025-08-15 15:52:39 +08:00
pycook
f46214aaf8 fix(api): click command: add-user #675 2025-08-14 20:46:57 +08:00
pycook
a809933a5f feat(api): Add auto subscription config 2025-08-14 20:19:49 +08:00
LH_R
5048f2a788 feat(ui): topology view & batch import - update CI Type select component 2025-07-09 16:23:18 +08:00
LH_R
40a53a0213 feat(ui): add CMDBTypeSelectAntd component 2025-07-07 12:17:30 +08:00
LH_R
1947af5693 feat(ui): update i18n 2025-06-23 16:26:49 +08:00
LH_R
6ec7caf5ea feat(ui): remove unused code and unused files 2025-06-23 16:26:38 +08:00
pycook
e93d894f04 chore: release v2.5.3 2025-06-20 21:30:16 +08:00
LH_R
081f35816f feat(ui): ci relation table - add upstream and downstream grouping 2025-06-20 16:27:51 +08:00
pycook
a8fadb2785 feat(api): add system language api 2025-06-19 12:46:24 +08:00
LH_R
72c37c995d feat(ui): update i18n 2025-06-18 15:35:45 +08:00
LH_R
f8fbbe4b9a feat(ui): i18n - init language add getSystemLanguage request 2025-06-17 21:19:51 +08:00
LH_R
155ba67ecc fix(ui): bug (#702) 2025-06-09 15:08:38 +08:00
LH_R
9c67b1e56a feat(ui): update iconfont 2025-06-08 23:45:44 +08:00
LH_R
88df3355d8 feat(ui): update adc permission 2025-05-16 17:34:14 +08:00
LH_R
549056a42d feat(ui): IPAM - ipSearch and subnetList add batch action group 2025-04-25 16:33:21 +08:00
LH_R
365fdf2bab feat(ui): RelationView - AddTableModal checkbox config add range 2025-04-25 16:32:38 +08:00
LH_R
6bf01786d8 fix(ui): CI - relation table repeat CIType 2025-04-25 16:32:06 +08:00
LH_R
e180f549c8 fix(ui): CI - ci detail title display 2025-04-25 16:31:43 +08:00
LH_R
3a7f4a31d0 fix(ui): Login - LDAP checkbox display condition 2025-04-22 21:33:17 +08:00
pycook
be4fc62218 chore: release v2.5.2 2025-04-17 21:19:46 +08:00
LH_R
ff4ce4dbe0 feat(ui): CI - update JSON type attr tooltip display 2025-04-16 20:54:11 +08:00
LH_R
dda1fce46a feat(ui): CI - relation table by subscribed attr 2025-04-16 20:53:54 +08:00
pycook
fbf59e7b44 perf(api): net device auto discovery 2025-04-16 19:55:02 +08:00
LH_R
4ae67d1f0f feat(ui): CI - hide groups without attr 2025-04-15 19:01:31 +08:00
LH_R
b56cf5bb3d feat(ui): CI - update default style of attr(JSON type) 2025-04-15 16:36:24 +08:00
Leo Song
53e8d34c68 Merge pull request #693 from veops/dev_ui_250411
fix(ui): dcim - device select name display
2025-04-11 15:51:50 +08:00
LH_R
c62e4032e3 fix(ui): dcim - device select name display 2025-04-11 15:51:12 +08:00
Leo Song
108c11071a Merge pull request #692 from veops/dev_ui_250410
dev_ ui_250410
2025-04-10 16:42:21 +08:00
LH_R
debb25f65b feat(ui): CI - update relation layout style 2025-04-10 16:38:42 +08:00
LH_R
f26dd65d07 feat(ui): CIType[relation] - update select filterOption 2025-04-10 16:14:00 +08:00
Leo Song
cb2726c890 Merge pull request #691 from veops/dev_ui_250409
feat(ui): CI Type[AD] - update snmp configuration style
2025-04-09 10:02:01 +08:00
LH_R
2003fd4a48 feat(ui): CI Type[AD] - update snmp configuration style 2025-04-09 10:01:35 +08:00
Leo Song
1bbf8c10b5 Merge pull request #689 from veops/dev_ui_250408
feat(ui): CI Type[AD] - update scanning configuration
2025-04-08 15:53:03 +08:00
LH_R
93e919b73f feat(ui): CI Type[AD] - update scanning configuration 2025-04-08 15:52:30 +08:00
thexqn
435bb2a2c8 fix(api): 使用 ast.literal_eval 代替 eval,取消不正确的计算属性值返回。 (#688)
* fix(api): 使用 ast.literal_eval 代替 eval,取消不正确的计算属性值返回。

* fix(api): 修复属性值计算逻辑,直接返回渲染结果。
2025-04-04 19:19:42 +08:00
pycook
5ceb8ff6f9 feat: update nginx configuration client_max_body_size 2025-03-26 21:02:28 +08:00
pycook
47332aca3c feat(api): Replace imp with importlib 2025-02-23 22:08:38 +08:00
LH_R
f24cb55585 docs: add CODE_OF_CONDUCT 2025-02-13 16:48:07 +08:00
LH_R
f1594550e0 docs: update README 2025-02-13 16:44:57 +08:00
pycook
a025c844bc chore: release v2.5.1 2025-02-10 20:22:17 +08:00
pycook
1a03a0b800 fix(api): get citype 2025-02-10 20:20:22 +08:00
songlh
e35efea712 feat(ui): login page - update background image 2025-01-22 16:39:13 +08:00
songlh
3f1e8beae8 docs: update README 2025-01-21 11:27:45 +08:00
songlh
3ea81987a1 docs: update README 2025-01-16 17:39:43 +08:00
pycook
6a20e2f578 fix(api): auto discovery rules sync 2025-01-14 16:24:20 +08:00
121 changed files with 6230 additions and 3169 deletions

13
CODE_OF_CONDUCT.md Normal file
View 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
View File

@@ -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 加关注,第一时间获取更新动态!
![star us](https://github.com/user-attachments/assets/f9056d5a-171c-4f53-9fec-d40c9e5ff94d)
## 快速开始
### 1. 搭建
+ 方案一Docker 一键快速构建
- 第1步: 安装 Docker 环境和 Docker Composev2
- 第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 &#40;v2&#41;)
## 相关文章
[//]: # (- 第二步: 直接使用项目根目录下的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" />

View File

@@ -1,6 +1,8 @@
import click
from flask import current_app
from flask.cli import with_appcontext
from api.lib.exception import AbortException
from api.lib.perm.acl.user import UserCRUD
@@ -46,11 +48,18 @@ def add_user():
email = click.prompt('Enter email ', confirmation_prompt=False)
is_admin = click.prompt('Admin (Y/N) ', confirmation_prompt=False, type=bool, default=False)
UserCRUD.add(username=username, password=password, email=email)
current_app.test_request_context().push()
if is_admin:
app = AppCache.get('acl') or App.create(name='acl')
acl_admin = RoleCache.get_by_name(app.id, 'acl_admin') or RoleCRUD.add_role('acl_admin', app.id, True)
rid = RoleCache.get_by_name(None, username).id
try:
RoleRelationCRUD.add(acl_admin, acl_admin.id, [rid], app.id)
UserCRUD.add(username=username, password=password, email=email)
if is_admin:
app = AppCache.get('acl') or App.create(name='acl')
acl_admin = RoleCache.get_by_name(None, 'acl_admin') or RoleCRUD.add_role('acl_admin', app.id, True)
rid = RoleCache.get_by_name(None, username).id
RoleRelationCRUD.add(acl_admin, acl_admin.id, [rid], app.id)
except AbortException as e:
print(f"Failed: {e}")

View File

@@ -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(),

View File

@@ -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 = {

View File

@@ -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": "设备的邻居列表"
}
]

View File

@@ -360,16 +360,28 @@ class CIManager(object):
_sync=False,
**ci_dict):
"""
add ci
:param ci_type_name:
:param exist_policy: replace or reject or need
:param _no_attribute_policy: ignore or reject
:param is_auto_discovery: default is False
:param _is_admin: default is False
:param ticket_id:
:param _sync:
:param ci_dict:
:return:
Create a new Configuration Item (CI) or update existing based on unique constraints.
Handles complete CI creation workflow including validation, uniqueness checks,
password encryption, computed attributes, relationship creation, and caching.
Args:
ci_type_name (str): Name of the CI type to create
exist_policy (ExistPolicy): How to handle existing CIs (REPLACE/REJECT/NEED)
_no_attribute_policy (ExistPolicy): How to handle unknown attributes (IGNORE/REJECT)
is_auto_discovery (bool): Whether CI is created by auto-discovery process
_is_admin (bool): Whether to skip permission checks
ticket_id (int, optional): Associated ticket ID for audit trail
_sync (bool): Whether to execute cache/relation tasks synchronously
**ci_dict: CI attribute values as key-value pairs
Returns:
int: ID of the created or updated CI
Raises:
400: If unique constraints violated, required attributes missing, or validation fails
403: If user lacks permissions for restricted attributes
404: If CI type not found or referenced CI not exists
"""
now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
ci_type = CITypeManager.check_is_existed(ci_type_name)
@@ -512,6 +524,24 @@ class CIManager(object):
return ci.id
def update(self, ci_id, _is_admin=False, ticket_id=None, _sync=False, **ci_dict):
"""
Update an existing Configuration Item with new attribute values.
Performs comprehensive CI update including validation, constraint checks,
password handling, computed attributes processing, and change tracking.
Args:
ci_id (int): ID of the CI to update
_is_admin (bool): Whether to skip permission checks
ticket_id (int, optional): Associated ticket ID for audit trail
_sync (bool): Whether to execute cache/relation tasks synchronously
**ci_dict: CI attribute values to update as key-value pairs
Raises:
400: If unique constraints violated or validation fails
403: If user lacks permissions for restricted attributes
404: If CI not found
"""
now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
ci = self.confirm_ci_existed(ci_id)
ci_type = ci.ci_type
@@ -1420,14 +1450,31 @@ class CIRelationManager(object):
parent_attr = AttributeCache.get(parent_attr_id)
child_attr = AttributeCache.get(child_attr_id)
attr_value = ci_dict.get(parent_attr.name)
if attr_value != 0 and not attr_value:
continue
value_table = TableMap(attr=child_attr).table
for child in value_table.get_by(attr_id=child_attr.id, value=attr_value, only_query=True).join(
CI, CI.id == value_table.ci_id).filter(CI.type_id == item.child_id):
_relations.add((ci_dict['_id'], child.ci_id))
attr_value_list = [attr_value] if not isinstance(attr_value, list) else attr_value
matching_cis = value_table.get_by(
attr_id=child_attr.id,
only_query=True
).join(
CI, CI.id == value_table.ci_id
).filter(
CI.type_id == item.child_id,
value_table.value.in_(attr_value_list)
).all()
for ci in matching_cis:
_relations.add((ci_dict['_id'], ci.ci_id))
if relations is None:
relations = _relations
else:
relations &= _relations
if item.constraint == ConstraintEnum.Many2Many:
relations |= _relations
else:
relations &= _relations
cls.delete_relations_by_source(RelationSourceEnum.ATTRIBUTE_VALUES,
first_ci_id=ci_dict['_id'],
@@ -1447,14 +1494,31 @@ class CIRelationManager(object):
parent_attr = AttributeCache.get(parent_attr_id)
child_attr = AttributeCache.get(child_attr_id)
attr_value = ci_dict.get(child_attr.name)
if attr_value != 0 and not attr_value:
continue
value_table = TableMap(attr=parent_attr).table
for parent in value_table.get_by(attr_id=parent_attr.id, value=attr_value, only_query=True).join(
CI, CI.id == value_table.ci_id).filter(CI.type_id == item.parent_id):
_relations.add((parent.ci_id, ci_dict['_id']))
attr_value_list = [attr_value] if not isinstance(attr_value, list) else attr_value
matching_cis = value_table.get_by(
attr_id=parent_attr.id,
only_query=True
).join(
CI, CI.id == value_table.ci_id
).filter(
CI.type_id == item.parent_id,
value_table.value.in_(attr_value_list)
).all()
for ci in matching_cis:
_relations.add((ci.ci_id, ci_dict['_id']))
if relations is None:
relations = _relations
else:
relations &= _relations
if item.constraint == ConstraintEnum.Many2Many:
relations |= _relations
else:
relations &= _relations
cls.delete_relations_by_source(RelationSourceEnum.ATTRIBUTE_VALUES,
second_ci_id=ci_dict['_id'],

View File

@@ -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)

View File

@@ -92,7 +92,8 @@ class IpAddressManager(object):
else:
return abort(400, ErrFormat.ipam_address_model_not_found)
with (redis_lock.Lock(rd.r, "IPAM_ASSIGN_ADDRESS_{}".format(subnet_id), expire=10)):
with (redis_lock.Lock(rd.r, "IPAM_ASSIGN_ADDRESS_{}".format(subnet_id),
expire=60, auto_renewal=True)):
cis = self._get_cis(subnet_id, ips)
ip2ci = {ci[IPAddressBuiltinAttributes.IP]: ci for ci in cis}

View File

@@ -1,9 +1,8 @@
# -*- coding:utf-8 -*-
import copy
from collections import defaultdict
import copy
import six
import toposort
from flask import abort
from flask import current_app
@@ -16,6 +15,7 @@ from api.lib.cmdb.cache import CITypeAttributesCache
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.cache import CMDBCounterCache
from api.lib.cmdb.ci_type import CITypeAttributeManager
from api.lib.cmdb.ci_type import CITypeManager
from api.lib.cmdb.const import BUILTIN_ATTRIBUTES
from api.lib.cmdb.const import ConstraintEnum
from api.lib.cmdb.const import PermEnum
@@ -29,6 +29,7 @@ from api.lib.perm.acl.acl import ACLManager
from api.models.cmdb import CITypeGroup
from api.models.cmdb import CITypeGroupItem
from api.models.cmdb import CITypeRelation
from api.models.cmdb import PreferenceAutoSubscriptionConfig
from api.models.cmdb import PreferenceCITypeOrder
from api.models.cmdb import PreferenceRelationView
from api.models.cmdb import PreferenceSearchOption
@@ -49,15 +50,27 @@ class PreferenceManager(object):
type2group = {}
for i in db.session.query(CITypeGroupItem, CITypeGroup).join(
CITypeGroup, CITypeGroup.id == CITypeGroupItem.group_id).filter(
CITypeGroup.deleted.is_(False)).filter(CITypeGroupItem.deleted.is_(False)):
CITypeGroup.deleted.is_(False)).filter(CITypeGroupItem.deleted.is_(False)):
type2group[i.CITypeGroupItem.type_id] = i.CITypeGroup.to_dict()
types = db.session.query(PreferenceShowAttributes.type_id).filter(
PreferenceShowAttributes.uid == current_user.uid).filter(
PreferenceShowAttributes.deleted.is_(False)).group_by(
PreferenceShowAttributes.type_id).all() if instance else []
types = sorted(types, key=lambda x: {i.type_id: idx for idx, i in enumerate(
ci_type_order) if not i.is_tree}.get(x.type_id, 1))
if instance:
auto_types = PreferenceManager.get_auto_subscription_types(current_user.uid)
if auto_types is not None:
class TypeIdObj:
def __init__(self, type_id):
self.type_id = type_id
types = [TypeIdObj(t) for t in auto_types]
else:
types = db.session.query(PreferenceShowAttributes.type_id).filter(
PreferenceShowAttributes.uid == current_user.uid).filter(
PreferenceShowAttributes.deleted.is_(False)).group_by(
PreferenceShowAttributes.type_id).all()
else:
types = []
types = sorted(types, key=lambda x: {i.type_id: idx for idx, i in enumerate(ci_type_order)
if not i.is_tree}.get(x.type_id, 1))
group_types = []
other_types = []
group2idx = {}
@@ -104,19 +117,26 @@ class PreferenceManager(object):
ci_type_order = sorted(PreferenceCITypeOrder.get_by(uid=current_user.uid, to_dict=False), key=lambda x: x.order)
if instance:
types = db.session.query(PreferenceShowAttributes.type_id,
PreferenceShowAttributes.uid, PreferenceShowAttributes.created_at).filter(
PreferenceShowAttributes.deleted.is_(False)).filter(
PreferenceShowAttributes.uid == current_user.uid).group_by(
PreferenceShowAttributes.uid, PreferenceShowAttributes.type_id)
for i in types:
result['self']['instance'].append(i.type_id)
if str(i.created_at) > str(result['self']['type_id2subs_time'].get(i.type_id, "")):
result['self']['type_id2subs_time'][i.type_id] = i.created_at
# Try auto subscription first, fallback to manual if not configured
auto_types = PreferenceManager.get_auto_subscription_types(current_user.uid)
if auto_types is not None:
result['self']['instance'] = auto_types
for type_id in auto_types:
result['self']['type_id2subs_time'][type_id] = ""
else:
types = db.session.query(PreferenceShowAttributes.type_id,
PreferenceShowAttributes.uid, PreferenceShowAttributes.created_at).filter(
PreferenceShowAttributes.deleted.is_(False)).filter(
PreferenceShowAttributes.uid == current_user.uid).group_by(
PreferenceShowAttributes.uid, PreferenceShowAttributes.type_id)
for i in types:
result['self']['instance'].append(i.type_id)
if str(i.created_at) > str(result['self']['type_id2subs_time'].get(i.type_id, "")):
result['self']['type_id2subs_time'][i.type_id] = i.created_at
instance_order = [i.type_id for i in ci_type_order if not i.is_tree]
if len(instance_order) == len(result['self']['instance']):
result['self']['instance'] = instance_order
instance_order = [i.type_id for i in ci_type_order if not i.is_tree]
if len(instance_order) == len(result['self']['instance']):
result['self']['instance'] = instance_order
if tree:
types = PreferenceTreeView.get_by(uid=current_user.uid, to_dict=False)
@@ -131,15 +151,73 @@ class PreferenceManager(object):
return result
@staticmethod
def get_auto_subscription_types(uid):
"""Get user's auto-subscribed CI types based on config rules"""
config = PreferenceAutoSubscriptionConfig.get_by(
uid=uid, enabled=True, first=True, to_dict=False
)
if not config:
return None
all_permitted_types = PreferenceManager._get_permitted_ci_types()
result_types = PreferenceManager._apply_subscription_config(config, all_permitted_types)
return result_types
@staticmethod
def _get_permitted_ci_types():
"""Get CI types that user has read permission for"""
from api.lib.perm.acl.acl import is_app_admin
if not current_app.config.get('USE_ACL') or is_app_admin('cmdb'):
return [t['id'] for t in CITypeManager.get_ci_types()]
# Regular user: filter by permissions
permitted_resources = ACLManager('cmdb').get_resources(ResourceTypeEnum.CI)
permitted_names = {r.get('name') for r in permitted_resources}
return [ci_type_dict['id'] for ci_type_dict in CITypeManager.get_ci_types()
if ci_type_dict['name'] in permitted_names]
@staticmethod
def _apply_subscription_config(config, all_permitted_types):
"""Apply subscription rules: 'all' mode excludes, 'none' mode includes"""
result_types = set()
if config.base_strategy == 'all':
# Start with all types, then exclude
result_types = set(all_permitted_types)
if config.group_ids:
exclude_group_type_ids = PreferenceManager._get_types_by_group_ids(config.group_ids)
result_types.difference_update(exclude_group_type_ids)
if config.type_ids:
result_types.difference_update(config.type_ids)
else: # base_strategy == 'none'
# Start empty, then include
if config.group_ids:
include_group_type_ids = PreferenceManager._get_types_by_group_ids(config.group_ids)
result_types.update(t for t in include_group_type_ids if t in all_permitted_types)
if config.type_ids:
result_types.update(t for t in config.type_ids if t in all_permitted_types)
return list(result_types)
@staticmethod
def _get_types_by_group_ids(group_ids):
return [i.type_id for i in CITypeGroupItem.get_by(
__func_in___key_group_id=group_ids, to_dict=False, fl=['type_id'])]
@staticmethod
def get_show_attributes(type_id):
_type = CITypeCache.get(type_id) or abort(404, ErrFormat.ci_type_not_found)
type_id = _type and _type.id
if not isinstance(type_id, six.integer_types):
_type = CITypeCache.get(type_id)
type_id = _type and _type.id
attrs = PreferenceShowAttributes.get_by(uid=current_user.uid, type_id=type_id, to_dict=False)
result = []
@@ -170,11 +248,11 @@ class PreferenceManager(object):
i.update(dict(choice_value=AttributeManager.get_choice_values(
i["id"], i["value_type"], i.get("choice_web_hook"), i.get("choice_other"))))
if (_type.name in SysComputedAttributes.type2attr and
i['name'] in SysComputedAttributes.type2attr[_type.name]):
i['sys_computed'] = True
else:
i['sys_computed'] = False
if (_type.name in SysComputedAttributes.type2attr and
i['name'] in SysComputedAttributes.type2attr[_type.name]):
i['sys_computed'] = True
else:
i['sys_computed'] = False
return is_subscribed, result
@@ -523,3 +601,54 @@ class PreferenceManager(object):
db.session.rollback()
current_app.logger.error("upsert citype order failed: {}".format(e))
return abort(400, ErrFormat.unknown_error)
@staticmethod
def get_auto_subscription_config():
"""Get user's auto subscription configuration"""
config = PreferenceAutoSubscriptionConfig.get_by(
uid=current_user.uid, first=True, to_dict=True
)
return config
@staticmethod
def create_or_update_auto_subscription_config(base_strategy, group_ids=None, type_ids=None,
enabled=True, description=None):
"""Create or update user's auto subscription config"""
config = PreferenceAutoSubscriptionConfig.get_by(
uid=current_user.uid, first=True, to_dict=False
)
data = {
'base_strategy': base_strategy,
'group_ids': group_ids or [],
'type_ids': type_ids or [],
'enabled': enabled,
'description': description
}
if config:
return config.update(**data)
else:
data['uid'] = current_user.uid
return PreferenceAutoSubscriptionConfig.create(**data)
@staticmethod
def delete_auto_subscription_config():
"""Delete user's auto subscription configuration"""
config = PreferenceAutoSubscriptionConfig.get_by(
uid=current_user.uid, first=True, to_dict=False
)
if config:
config.soft_delete()
return True
@staticmethod
def toggle_auto_subscription_config(enabled):
"""Enable or disable user's auto subscription config"""
config = PreferenceAutoSubscriptionConfig.get_by(
uid=current_user.uid, first=True, to_dict=False
)
if not config:
return abort(404, "Auto subscription config not found")
return config.update(enabled=enabled)

View File

@@ -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()

View File

@@ -525,6 +525,17 @@ class PreferenceCITypeOrder(Model):
is_tree = db.Column(db.Boolean, default=False) # True is tree view, False is resource view
class PreferenceAutoSubscriptionConfig(Model):
__tablename__ = "c_pasc"
uid = db.Column(db.Integer, index=True, nullable=False, unique=True)
base_strategy = db.Column(db.Enum('all', 'none'), default='none', nullable=False)
group_ids = db.Column(db.JSON)
type_ids = db.Column(db.JSON)
enabled = db.Column(db.Boolean, default=True, nullable=False)
description = db.Column(db.Text)
# custom
class CustomDashboard(Model):
__tablename__ = "c_c_d"

View File

@@ -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():

View File

@@ -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,

View File

@@ -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)

View File

@@ -211,3 +211,59 @@ class PreferenceCITypeOrderView(APIView):
PreferenceManager.upsert_ci_type_order(type_ids, is_tree)
return self.jsonify(type_ids=type_ids, is_tree=is_tree)
class PreferenceAutoSubscriptionView(APIView):
url_prefix = "/preference/auto_subscription"
def get(self):
config = PreferenceManager.get_auto_subscription_config()
return self.jsonify(config or {})
@args_required("base_strategy")
def put(self):
base_strategy = request.values.get("base_strategy")
group_ids = request.values.get("group_ids")
type_ids = request.values.get("type_ids")
enabled = request.values.get("enabled", 1) in current_app.config.get('BOOL_TRUE')
description = request.values.get("description")
if base_strategy not in ['all', 'none']:
return abort(400, "base_strategy must be 'all' or 'none'")
if group_ids:
try:
group_ids = [int(x) for x in group_ids.split(',') if x.strip()]
except ValueError:
return abort(400, "Invalid group_ids format")
if type_ids:
try:
type_ids = [int(x) for x in type_ids.split(',') if x.strip()]
except ValueError:
return abort(400, "Invalid type_ids format")
result = PreferenceManager.create_or_update_auto_subscription_config(
base_strategy=base_strategy,
group_ids=group_ids,
type_ids=type_ids,
enabled=enabled,
description=description
)
return self.jsonify(result.to_dict())
def delete(self):
PreferenceManager.delete_auto_subscription_config()
return self.jsonify(message="Auto subscription config deleted")
class PreferenceAutoSubscriptionToggleView(APIView):
url_prefix = "/preference/auto_subscription/toggle"
@args_required("enabled")
def patch(self):
enabled = request.values.get("enabled") in current_app.config.get('BOOL_TRUE')
result = PreferenceManager.toggle_auto_subscription_config(enabled)
return self.jsonify(result.to_dict())

View File

@@ -0,0 +1,37 @@
import os
from api.resource import APIView
from api.lib.perm.auth import auth_abandoned
prefix = "/system"
class SystemLanguageView(APIView):
url_prefix = (f"{prefix}/language",)
method_decorators = []
@auth_abandoned
def get(self):
"""Get system default language
Read from environment variable SYSTEM_DEFAULT_LANGUAGE, default to Chinese if not set
"""
default_language = os.environ.get("SYSTEM_DEFAULT_LANGUAGE", "")
return self.jsonify(
{
"language": default_language,
"language_name": self._get_language_name(default_language),
}
)
def _get_language_name(self, language_code):
"""Return language name based on language code"""
language_mapping = {
"zh-CN": "中文(简体)",
"zh-TW": "中文(繁体)",
"en-US": "English",
"ja-JP": "日本語",
"ko-KR": "한국어",
}
return language_mapping.get(language_code, "")

View File

@@ -5,14 +5,6 @@ if (IS_PROD) {
plugins.push('transform-remove-console')
}
// lazy load ant-design-vue
// if your use import on Demand, Use this code
// plugins.push(['import', {
// 'libraryName': 'ant-design-vue',
// 'libraryDirectory': 'es',
// 'style': true // `style: true` 会加载 less 文件
// }])
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset',

View File

@@ -2,7 +2,7 @@ const ThemeColorReplacer = require('webpack-theme-color-replacer')
const generate = require('@ant-design/colors/lib/generate').default
const getAntdSerials = (color) => {
// 淡化(即lesstint
// Lighten (similar to less's tint)
const lightens = new Array(9).fill().map((t, i) => {
return ThemeColorReplacer.varyColor.lighten(color, i / 10)
})
@@ -13,8 +13,8 @@ const getAntdSerials = (color) => {
const themePluginOption = {
fileName: 'css/theme-colors-[contenthash:8].css',
matchColors: getAntdSerials('#2f54eb'), // 主色系列
// 改变样式选择器,解决样式覆盖问题
matchColors: getAntdSerials('#2f54eb'), // primary color series
// change style selectors to solve style override issues
changeSelector (selector) {
switch (selector) {
case '.ant-calendar-today .ant-calendar-date':

View File

@@ -54,6 +54,180 @@
<div class="content unicode" style="display: block;">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont">&#xea28;</span>
<div class="name">auto</div>
<div class="code-name">&amp;#xea28;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea26;</span>
<div class="name">oneterm-http</div>
<div class="code-name">&amp;#xea26;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea27;</span>
<div class="name">oneterm-https</div>
<div class="code-name">&amp;#xea27;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea25;</span>
<div class="name">access_period</div>
<div class="code-name">&amp;#xea25;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea24;</span>
<div class="name">authorization</div>
<div class="code-name">&amp;#xea24;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea23;</span>
<div class="name">onterm-symbolic_link</div>
<div class="code-name">&amp;#xea23;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea20;</span>
<div class="name">oneterm-batch_execution</div>
<div class="code-name">&amp;#xea20;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea21;</span>
<div class="name">oneterm-file_log-selected</div>
<div class="code-name">&amp;#xea21;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea22;</span>
<div class="name">oneterm-file_log</div>
<div class="code-name">&amp;#xea22;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea1f;</span>
<div class="name">file</div>
<div class="code-name">&amp;#xea1f;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea1e;</span>
<div class="name">folder</div>
<div class="code-name">&amp;#xea1e;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea1b;</span>
<div class="name">mongoDB (1)</div>
<div class="code-name">&amp;#xea1b;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea1c;</span>
<div class="name">postgreSQL (1)</div>
<div class="code-name">&amp;#xea1c;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea1d;</span>
<div class="name">telnet (1)</div>
<div class="code-name">&amp;#xea1d;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea17;</span>
<div class="name">command_interception (1)</div>
<div class="code-name">&amp;#xea17;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea18;</span>
<div class="name">quick_commands</div>
<div class="code-name">&amp;#xea18;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea19;</span>
<div class="name">terminal_settings</div>
<div class="code-name">&amp;#xea19;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea1a;</span>
<div class="name">basic_settings</div>
<div class="code-name">&amp;#xea1a;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea16;</span>
<div class="name">asset_management</div>
<div class="code-name">&amp;#xea16;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea15;</span>
<div class="name">ai-seek</div>
<div class="code-name">&amp;#xea15;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea13;</span>
<div class="name">ai-hate1</div>
<div class="code-name">&amp;#xea13;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea14;</span>
<div class="name">ai-like1</div>
<div class="code-name">&amp;#xea14;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea11;</span>
<div class="name">ai-like2</div>
<div class="code-name">&amp;#xea11;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea12;</span>
<div class="name">ai-hate2</div>
<div class="code-name">&amp;#xea12;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea10;</span>
<div class="name">ai-top_up</div>
<div class="code-name">&amp;#xea10;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea0f;</span>
<div class="name">ai-top_down</div>
<div class="code-name">&amp;#xea0f;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea0d;</span>
<div class="name">autoflow-script</div>
<div class="code-name">&amp;#xea0d;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea0e;</span>
<div class="name">autoflow-dag</div>
<div class="code-name">&amp;#xea0e;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea0c;</span>
<div class="name">itsm-default_line</div>
<div class="code-name">&amp;#xea0c;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea0b;</span>
<div class="name">veops-servicetree</div>
@@ -6210,9 +6384,9 @@
<pre><code class="language-css"
>@font-face {
font-family: 'iconfont';
src: url('iconfont.woff2?t=1735191938771') format('woff2'),
url('iconfont.woff?t=1735191938771') format('woff'),
url('iconfont.ttf?t=1735191938771') format('truetype');
src: url('iconfont.woff2?t=1755240492206') format('woff2'),
url('iconfont.woff?t=1755240492206') format('woff'),
url('iconfont.ttf?t=1755240492206') format('truetype');
}
</code></pre>
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@@ -6238,6 +6412,267 @@
<div class="content font-class">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont auto"></span>
<div class="name">
auto
</div>
<div class="code-name">.auto
</div>
</li>
<li class="dib">
<span class="icon iconfont oneterm-http"></span>
<div class="name">
oneterm-http
</div>
<div class="code-name">.oneterm-http
</div>
</li>
<li class="dib">
<span class="icon iconfont oneterm-https"></span>
<div class="name">
oneterm-https
</div>
<div class="code-name">.oneterm-https
</div>
</li>
<li class="dib">
<span class="icon iconfont ops-oneterm-access_period"></span>
<div class="name">
access_period
</div>
<div class="code-name">.ops-oneterm-access_period
</div>
</li>
<li class="dib">
<span class="icon iconfont ops-oneterm-authorization"></span>
<div class="name">
authorization
</div>
<div class="code-name">.ops-oneterm-authorization
</div>
</li>
<li class="dib">
<span class="icon iconfont onterm-symbolic_link"></span>
<div class="name">
onterm-symbolic_link
</div>
<div class="code-name">.onterm-symbolic_link
</div>
</li>
<li class="dib">
<span class="icon iconfont oneterm-batch_execution"></span>
<div class="name">
oneterm-batch_execution
</div>
<div class="code-name">.oneterm-batch_execution
</div>
</li>
<li class="dib">
<span class="icon iconfont ops-oneterm-file_log-selected"></span>
<div class="name">
oneterm-file_log-selected
</div>
<div class="code-name">.ops-oneterm-file_log-selected
</div>
</li>
<li class="dib">
<span class="icon iconfont ops-oneterm-file_log"></span>
<div class="name">
oneterm-file_log
</div>
<div class="code-name">.ops-oneterm-file_log
</div>
</li>
<li class="dib">
<span class="icon iconfont file"></span>
<div class="name">
file
</div>
<div class="code-name">.file
</div>
</li>
<li class="dib">
<span class="icon iconfont folder1"></span>
<div class="name">
folder
</div>
<div class="code-name">.folder1
</div>
</li>
<li class="dib">
<span class="icon iconfont a-mongoDB1"></span>
<div class="name">
mongoDB (1)
</div>
<div class="code-name">.a-mongoDB1
</div>
</li>
<li class="dib">
<span class="icon iconfont a-postgreSQL1"></span>
<div class="name">
postgreSQL (1)
</div>
<div class="code-name">.a-postgreSQL1
</div>
</li>
<li class="dib">
<span class="icon iconfont a-telnet1"></span>
<div class="name">
telnet (1)
</div>
<div class="code-name">.a-telnet1
</div>
</li>
<li class="dib">
<span class="icon iconfont ops-oneterm-command_interception1"></span>
<div class="name">
command_interception (1)
</div>
<div class="code-name">.ops-oneterm-command_interception1
</div>
</li>
<li class="dib">
<span class="icon iconfont quick_commands"></span>
<div class="name">
quick_commands
</div>
<div class="code-name">.quick_commands
</div>
</li>
<li class="dib">
<span class="icon iconfont terminal_settings"></span>
<div class="name">
terminal_settings
</div>
<div class="code-name">.terminal_settings
</div>
</li>
<li class="dib">
<span class="icon iconfont basic_settings"></span>
<div class="name">
basic_settings
</div>
<div class="code-name">.basic_settings
</div>
</li>
<li class="dib">
<span class="icon iconfont ops-oneterm-asset-management"></span>
<div class="name">
asset_management
</div>
<div class="code-name">.ops-oneterm-asset-management
</div>
</li>
<li class="dib">
<span class="icon iconfont ai-seek"></span>
<div class="name">
ai-seek
</div>
<div class="code-name">.ai-seek
</div>
</li>
<li class="dib">
<span class="icon iconfont ai-hate1"></span>
<div class="name">
ai-hate1
</div>
<div class="code-name">.ai-hate1
</div>
</li>
<li class="dib">
<span class="icon iconfont ai-like1"></span>
<div class="name">
ai-like1
</div>
<div class="code-name">.ai-like1
</div>
</li>
<li class="dib">
<span class="icon iconfont ai-like2"></span>
<div class="name">
ai-like2
</div>
<div class="code-name">.ai-like2
</div>
</li>
<li class="dib">
<span class="icon iconfont ai-hate2"></span>
<div class="name">
ai-hate2
</div>
<div class="code-name">.ai-hate2
</div>
</li>
<li class="dib">
<span class="icon iconfont ai-top_up"></span>
<div class="name">
ai-top_up
</div>
<div class="code-name">.ai-top_up
</div>
</li>
<li class="dib">
<span class="icon iconfont ai-top_down"></span>
<div class="name">
ai-top_down
</div>
<div class="code-name">.ai-top_down
</div>
</li>
<li class="dib">
<span class="icon iconfont autoflow-script"></span>
<div class="name">
autoflow-script
</div>
<div class="code-name">.autoflow-script
</div>
</li>
<li class="dib">
<span class="icon iconfont autoflow-dag"></span>
<div class="name">
autoflow-dag
</div>
<div class="code-name">.autoflow-dag
</div>
</li>
<li class="dib">
<span class="icon iconfont itsm-default_line"></span>
<div class="name">
itsm-default_line
</div>
<div class="code-name">.itsm-default_line
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-servicetree"></span>
<div class="name">
@@ -15472,6 +15907,238 @@
<div class="content symbol">
<ul class="icon_lists dib-box">
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#auto"></use>
</svg>
<div class="name">auto</div>
<div class="code-name">#auto</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#oneterm-http"></use>
</svg>
<div class="name">oneterm-http</div>
<div class="code-name">#oneterm-http</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#oneterm-https"></use>
</svg>
<div class="name">oneterm-https</div>
<div class="code-name">#oneterm-https</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ops-oneterm-access_period"></use>
</svg>
<div class="name">access_period</div>
<div class="code-name">#ops-oneterm-access_period</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ops-oneterm-authorization"></use>
</svg>
<div class="name">authorization</div>
<div class="code-name">#ops-oneterm-authorization</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#onterm-symbolic_link"></use>
</svg>
<div class="name">onterm-symbolic_link</div>
<div class="code-name">#onterm-symbolic_link</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#oneterm-batch_execution"></use>
</svg>
<div class="name">oneterm-batch_execution</div>
<div class="code-name">#oneterm-batch_execution</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ops-oneterm-file_log-selected"></use>
</svg>
<div class="name">oneterm-file_log-selected</div>
<div class="code-name">#ops-oneterm-file_log-selected</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ops-oneterm-file_log"></use>
</svg>
<div class="name">oneterm-file_log</div>
<div class="code-name">#ops-oneterm-file_log</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#file"></use>
</svg>
<div class="name">file</div>
<div class="code-name">#file</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#folder1"></use>
</svg>
<div class="name">folder</div>
<div class="code-name">#folder1</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#a-mongoDB1"></use>
</svg>
<div class="name">mongoDB (1)</div>
<div class="code-name">#a-mongoDB1</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#a-postgreSQL1"></use>
</svg>
<div class="name">postgreSQL (1)</div>
<div class="code-name">#a-postgreSQL1</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#a-telnet1"></use>
</svg>
<div class="name">telnet (1)</div>
<div class="code-name">#a-telnet1</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ops-oneterm-command_interception1"></use>
</svg>
<div class="name">command_interception (1)</div>
<div class="code-name">#ops-oneterm-command_interception1</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#quick_commands"></use>
</svg>
<div class="name">quick_commands</div>
<div class="code-name">#quick_commands</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#terminal_settings"></use>
</svg>
<div class="name">terminal_settings</div>
<div class="code-name">#terminal_settings</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#basic_settings"></use>
</svg>
<div class="name">basic_settings</div>
<div class="code-name">#basic_settings</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ops-oneterm-asset-management"></use>
</svg>
<div class="name">asset_management</div>
<div class="code-name">#ops-oneterm-asset-management</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ai-seek"></use>
</svg>
<div class="name">ai-seek</div>
<div class="code-name">#ai-seek</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ai-hate1"></use>
</svg>
<div class="name">ai-hate1</div>
<div class="code-name">#ai-hate1</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ai-like1"></use>
</svg>
<div class="name">ai-like1</div>
<div class="code-name">#ai-like1</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ai-like2"></use>
</svg>
<div class="name">ai-like2</div>
<div class="code-name">#ai-like2</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ai-hate2"></use>
</svg>
<div class="name">ai-hate2</div>
<div class="code-name">#ai-hate2</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ai-top_up"></use>
</svg>
<div class="name">ai-top_up</div>
<div class="code-name">#ai-top_up</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ai-top_down"></use>
</svg>
<div class="name">ai-top_down</div>
<div class="code-name">#ai-top_down</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#autoflow-script"></use>
</svg>
<div class="name">autoflow-script</div>
<div class="code-name">#autoflow-script</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#autoflow-dag"></use>
</svg>
<div class="name">autoflow-dag</div>
<div class="code-name">#autoflow-dag</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-default_line"></use>
</svg>
<div class="name">itsm-default_line</div>
<div class="code-name">#itsm-default_line</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-servicetree"></use>

View File

@@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 3857903 */
src: url('iconfont.woff2?t=1735191938771') format('woff2'),
url('iconfont.woff?t=1735191938771') format('woff'),
url('iconfont.ttf?t=1735191938771') format('truetype');
src: url('iconfont.woff2?t=1755240492206') format('woff2'),
url('iconfont.woff?t=1755240492206') format('woff'),
url('iconfont.ttf?t=1755240492206') format('truetype');
}
.iconfont {
@@ -13,6 +13,122 @@
-moz-osx-font-smoothing: grayscale;
}
.auto:before {
content: "\ea28";
}
.oneterm-http:before {
content: "\ea26";
}
.oneterm-https:before {
content: "\ea27";
}
.ops-oneterm-access_period:before {
content: "\ea25";
}
.ops-oneterm-authorization:before {
content: "\ea24";
}
.onterm-symbolic_link:before {
content: "\ea23";
}
.oneterm-batch_execution:before {
content: "\ea20";
}
.ops-oneterm-file_log-selected:before {
content: "\ea21";
}
.ops-oneterm-file_log:before {
content: "\ea22";
}
.file:before {
content: "\ea1f";
}
.folder1:before {
content: "\ea1e";
}
.a-mongoDB1:before {
content: "\ea1b";
}
.a-postgreSQL1:before {
content: "\ea1c";
}
.a-telnet1:before {
content: "\ea1d";
}
.ops-oneterm-command_interception1:before {
content: "\ea17";
}
.quick_commands:before {
content: "\ea18";
}
.terminal_settings:before {
content: "\ea19";
}
.basic_settings:before {
content: "\ea1a";
}
.ops-oneterm-asset-management:before {
content: "\ea16";
}
.ai-seek:before {
content: "\ea15";
}
.ai-hate1:before {
content: "\ea13";
}
.ai-like1:before {
content: "\ea14";
}
.ai-like2:before {
content: "\ea11";
}
.ai-hate2:before {
content: "\ea12";
}
.ai-top_up:before {
content: "\ea10";
}
.ai-top_down:before {
content: "\ea0f";
}
.autoflow-script:before {
content: "\ea0d";
}
.autoflow-dag:before {
content: "\ea0e";
}
.itsm-default_line:before {
content: "\ea0c";
}
.veops-servicetree:before {
content: "\ea0b";
}

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,209 @@
"css_prefix_text": "",
"description": "",
"glyphs": [
{
"icon_id": "45254419",
"name": "auto",
"font_class": "auto",
"unicode": "ea28",
"unicode_decimal": 59944
},
{
"icon_id": "45069619",
"name": "oneterm-http",
"font_class": "oneterm-http",
"unicode": "ea26",
"unicode_decimal": 59942
},
{
"icon_id": "45069616",
"name": "oneterm-https",
"font_class": "oneterm-https",
"unicode": "ea27",
"unicode_decimal": 59943
},
{
"icon_id": "44939355",
"name": "access_period",
"font_class": "ops-oneterm-access_period",
"unicode": "ea25",
"unicode_decimal": 59941
},
{
"icon_id": "44939354",
"name": "authorization",
"font_class": "ops-oneterm-authorization",
"unicode": "ea24",
"unicode_decimal": 59940
},
{
"icon_id": "44501032",
"name": "onterm-symbolic_link",
"font_class": "onterm-symbolic_link",
"unicode": "ea23",
"unicode_decimal": 59939
},
{
"icon_id": "44497221",
"name": "oneterm-batch_execution",
"font_class": "oneterm-batch_execution",
"unicode": "ea20",
"unicode_decimal": 59936
},
{
"icon_id": "44497220",
"name": "oneterm-file_log-selected",
"font_class": "ops-oneterm-file_log-selected",
"unicode": "ea21",
"unicode_decimal": 59937
},
{
"icon_id": "44497219",
"name": "oneterm-file_log",
"font_class": "ops-oneterm-file_log",
"unicode": "ea22",
"unicode_decimal": 59938
},
{
"icon_id": "44455092",
"name": "file",
"font_class": "file",
"unicode": "ea1f",
"unicode_decimal": 59935
},
{
"icon_id": "44455100",
"name": "folder",
"font_class": "folder1",
"unicode": "ea1e",
"unicode_decimal": 59934
},
{
"icon_id": "44315758",
"name": "mongoDB (1)",
"font_class": "a-mongoDB1",
"unicode": "ea1b",
"unicode_decimal": 59931
},
{
"icon_id": "44315757",
"name": "postgreSQL (1)",
"font_class": "a-postgreSQL1",
"unicode": "ea1c",
"unicode_decimal": 59932
},
{
"icon_id": "44315755",
"name": "telnet (1)",
"font_class": "a-telnet1",
"unicode": "ea1d",
"unicode_decimal": 59933
},
{
"icon_id": "44276353",
"name": "command_interception (1)",
"font_class": "ops-oneterm-command_interception1",
"unicode": "ea17",
"unicode_decimal": 59927
},
{
"icon_id": "44276352",
"name": "quick_commands",
"font_class": "quick_commands",
"unicode": "ea18",
"unicode_decimal": 59928
},
{
"icon_id": "44276351",
"name": "terminal_settings",
"font_class": "terminal_settings",
"unicode": "ea19",
"unicode_decimal": 59929
},
{
"icon_id": "44276350",
"name": "basic_settings",
"font_class": "basic_settings",
"unicode": "ea1a",
"unicode_decimal": 59930
},
{
"icon_id": "44276278",
"name": "asset_management",
"font_class": "ops-oneterm-asset-management",
"unicode": "ea16",
"unicode_decimal": 59926
},
{
"icon_id": "43267802",
"name": "ai-seek",
"font_class": "ai-seek",
"unicode": "ea15",
"unicode_decimal": 59925
},
{
"icon_id": "43213714",
"name": "ai-hate1",
"font_class": "ai-hate1",
"unicode": "ea13",
"unicode_decimal": 59923
},
{
"icon_id": "43213712",
"name": "ai-like1",
"font_class": "ai-like1",
"unicode": "ea14",
"unicode_decimal": 59924
},
{
"icon_id": "43213717",
"name": "ai-like2",
"font_class": "ai-like2",
"unicode": "ea11",
"unicode_decimal": 59921
},
{
"icon_id": "43213716",
"name": "ai-hate2",
"font_class": "ai-hate2",
"unicode": "ea12",
"unicode_decimal": 59922
},
{
"icon_id": "43139007",
"name": "ai-top_up",
"font_class": "ai-top_up",
"unicode": "ea10",
"unicode_decimal": 59920
},
{
"icon_id": "43139017",
"name": "ai-top_down",
"font_class": "ai-top_down",
"unicode": "ea0f",
"unicode_decimal": 59919
},
{
"icon_id": "43029539",
"name": "autoflow-script",
"font_class": "autoflow-script",
"unicode": "ea0d",
"unicode_decimal": 59917
},
{
"icon_id": "43029538",
"name": "autoflow-dag",
"font_class": "autoflow-dag",
"unicode": "ea0e",
"unicode_decimal": 59918
},
{
"icon_id": "42960865",
"name": "itsm-default_line",
"font_class": "itsm-default_line",
"unicode": "ea0c",
"unicode_decimal": 59916
},
{
"icon_id": "42930714",
"name": "veops-servicetree",

Binary file not shown.

View File

@@ -1,7 +1,6 @@
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
@@ -12,17 +11,19 @@
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %>
<link rel="stylesheet" href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" />
<% } %>
<script>
  const userAgent = navigator.userAgent
<script>
const userAgent = navigator.userAgent
const isEdge = userAgent.indexOf("Edge") > -1
  const isChrome = userAgent.indexOf("Chrome") > -1 && userAgent.indexOf("Safari") > -1 && !isEdge
  if (!isChrome) {
alert("推荐使用Chrome浏览器 其他环境下未做严格测试!")
const isChrome = userAgent.indexOf("Chrome") > -1 && userAgent.indexOf("Safari") > -1 && !isEdge
const lang = (navigator.language || navigator.userLanguage || '').toLowerCase()
if (!isChrome) {
if (lang.startsWith('zh')) {
alert("推荐使用Chrome浏览器其他环境下未做严格测试");
} else {
alert("It is recommended to use Chrome browser. Other environments are not strictly tested!");
}
}
</script>
</head>
<body>
<noscript>
@@ -30,10 +31,10 @@
</noscript>
<div id="app">
<div id="loading-mask">
<div class="loading-wrapper">
<span class="loading-dot loading-dot-spin"><i></i><i></i><i></i><i></i></span>
</div>
<div class="loading-wrapper">
<span class="loading-dot loading-dot-spin"><i></i><i></i><i></i><i></i></span>
</div>
</div>
</div>
<!-- require cdn assets js -->
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>

View File

@@ -12,6 +12,7 @@ import zhCN from 'ant-design-vue/lib/locale-provider/zh_CN'
import enUS from 'ant-design-vue/lib/locale-provider/en_US'
import { AppDeviceEnquire } from '@/utils/mixin'
import { debounce } from './utils/util'
import { getSystemLanguage } from '@/api/system.js'
import { h } from 'snabbdom'
import { DomEditor, Boot } from '@wangeditor/editor'
@@ -45,8 +46,7 @@ export default {
},
},
created() {
this.SET_LOCALE(localStorage.getItem('ops_locale') || 'zh')
this.$i18n.locale = localStorage.getItem('ops_locale') || 'zh'
this.initLanguage()
this.timer = setInterval(() => {
this.setTime(new Date().getTime())
}, 1000)
@@ -60,133 +60,7 @@ export default {
})
)
// 注册富文本自定义元素
// const resume = {
// type: 'attachment',
// attachmentLabel: '',
// attachmentValue: '',
// children: [{ text: '' }], // void 元素必须有一个 children 其中只有一个空字符串重要
// }
function withAttachment(editor) {
// JS 语法
const { isInline, isVoid } = editor
const newEditor = editor
newEditor.isInline = (elem) => {
const type = DomEditor.getNodeType(elem)
if (type === 'attachment') return true // 针对 type: attachment 设置为 inline
return isInline(elem)
}
newEditor.isVoid = (elem) => {
const type = DomEditor.getNodeType(elem)
if (type === 'attachment') return true // 针对 type: attachment 设置为 void
return isVoid(elem)
}
return newEditor // 返回 newEditor 重要
}
Boot.registerPlugin(withAttachment)
/**
* 渲染附件元素到编辑器
* @param elem 附件元素即上文的 myResume
* @param children 元素子节点void 元素可忽略
* @param editor 编辑器实例
* @returns vnode 节点通过 snabbdom.js h 函数生成
*/
function renderAttachment(elem, children, editor) {
// JS 语法
// 获取附件的数据参考上文 myResume 数据结构
const { attachmentLabel = '', attachmentValue = '' } = elem
// 附件元素 vnode
const attachVnode = h(
// HTML tag
'span',
// HTML 属性样式事件
{
props: { contentEditable: false }, // HTML 属性驼峰式写法
style: {
display: 'inline-block',
margin: '0 3px',
padding: '0 3px',
backgroundColor: '#e6f7ff',
border: '1px solid #91d5ff',
borderRadius: '2px',
color: '#1890ff',
}, // style 驼峰式写法
on: {
click() {
console.log('clicked', attachmentValue)
} /* 其他... */,
},
},
// 子节点
[attachmentLabel]
)
return attachVnode
}
const renderElemConf = {
type: 'attachment', // 新元素 type 重要
renderElem: renderAttachment,
}
Boot.registerRenderElem(renderElemConf)
/**
* 生成附件元素的 HTML
* @param elem 附件元素即上文的 myResume
* @param childrenHtml 子节点的 HTML 代码void 元素可忽略
* @returns 附件元素的 HTML 字符串
*/
function attachmentToHtml(elem, childrenHtml) {
// JS 语法
// 获取附件元素的数据
const { attachmentValue = '', attachmentLabel = '' } = elem
// 生成 HTML 代码
const html = `<span data-w-e-type="attachment" data-w-e-is-void data-w-e-is-inline data-attachmentValue="${attachmentValue}" data-attachmentLabel="${attachmentLabel}">${attachmentLabel}</span>`
return html
}
const elemToHtmlConf = {
type: 'attachment', // 新元素的 type 重要
elemToHtml: attachmentToHtml,
}
Boot.registerElemToHtml(elemToHtmlConf)
/**
* 解析 HTML 字符串生成附件元素
* @param domElem HTML 对应的 DOM Element
* @param children 子节点
* @param editor editor 实例
* @returns 附件元素如上文的 myResume
*/
function parseAttachmentHtml(domElem, children, editor) {
// JS 语法
// DOM element 中获取附件的信息
const attachmentValue = domElem.getAttribute('data-attachmentValue') || ''
const attachmentLabel = domElem.getAttribute('data-attachmentLabel') || ''
// 生成附件元素按照此前约定的数据结构
const myResume = {
type: 'attachment',
attachmentValue,
attachmentLabel,
children: [{ text: '' }], // void node 必须有 children 其中有一个空字符串重要
}
return myResume
}
const parseHtmlConf = {
selector: 'span[data-w-e-type="attachment"]', // CSS 选择器匹配特定的 HTML 标签
parseElemHtml: parseAttachmentHtml,
}
Boot.registerParseElemHtml(parseHtmlConf)
this.handleEditor()
},
beforeDestroy() {
clearInterval(this.timer)
@@ -200,6 +74,141 @@ export default {
this.alive = true
})
},
async initLanguage() {
let saveLocale = localStorage.getItem('ops_locale')
if (!saveLocale) {
let requestLanguage = ''
try {
const languageRes = await getSystemLanguage()
requestLanguage = languageRes?.language || ''
} catch (e) {
console.error('getSystemLanguage error:', e)
}
// request language variable || user local system language
const userLanguage = requestLanguage || navigator.language || navigator.userLanguage
if (userLanguage.includes('zh')) {
saveLocale = 'zh'
} else {
saveLocale = 'en'
}
}
this.SET_LOCALE(saveLocale)
this.$i18n.locale = saveLocale
},
handleEditor() {
// register custom rich text element: attachment
function withAttachment(editor) {
const { isInline, isVoid } = editor
const newEditor = editor
newEditor.isInline = (elem) => {
const type = DomEditor.getNodeType(elem)
if (type === 'attachment') return true // For type: attachment, set to inline
return isInline(elem)
}
newEditor.isVoid = (elem) => {
const type = DomEditor.getNodeType(elem)
if (type === 'attachment') return true // For type: attachment set to void
return isVoid(elem)
}
return newEditor // Must return, important!!!
}
Boot.registerPlugin(withAttachment)
/**
* Render "attachment" element in editor
* @param elem Attachment element
* @param children Child nodes (ignored for void elements)
* @param editor Editor instance
* @returns vnode (generated by snabbdom's h function)
*/
function renderAttachment(elem, children, editor) {
const { attachmentLabel = '', attachmentValue = '' } = elem
const attachVnode = h(
// HTML tag
'span',
// HTML attr, style, event
{
props: { contentEditable: false },
style: {
display: 'inline-block',
margin: '0 3px',
padding: '0 3px',
backgroundColor: '#e6f7ff',
border: '1px solid #91d5ff',
borderRadius: '2px',
color: '#1890ff',
},
on: {
click() {
console.log('clicked', attachmentValue)
}
},
},
// child node
[attachmentLabel]
)
return attachVnode
}
const renderElemConf = {
type: 'attachment',
renderElem: renderAttachment,
}
Boot.registerRenderElem(renderElemConf)
/**
* Generate HTML for "attachment" element
* @param elem Attachment element
* @param childrenHtml Child HTML (ignored for void elements)
* @returns HTML string
*/
function attachmentToHtml(elem, childrenHtml) {
// Getting data for attached elements
const { attachmentValue = '', attachmentLabel = '' } = elem
// generate HTML
const html = `<span data-w-e-type="attachment" data-w-e-is-void data-w-e-is-inline data-attachmentValue="${attachmentValue}" data-attachmentLabel="${attachmentLabel}">${attachmentLabel}</span>`
return html
}
const elemToHtmlConf = {
type: 'attachment',
elemToHtml: attachmentToHtml,
}
Boot.registerElemToHtml(elemToHtmlConf)
/**
* Parse HTML to generate "attachment" element
* @param domElem DOM element
* @param children Children
* @param editor Editor instance
* @returns Attachment element
*/
function parseAttachmentHtml(domElem, children, editor) {
// Getting attachment information from DOM element
const attachmentValue = domElem.getAttribute('data-attachmentValue') || ''
const attachmentLabel = domElem.getAttribute('data-attachmentLabel') || ''
const myResume = {
type: 'attachment',
attachmentValue,
attachmentLabel,
children: [{ text: '' }], // The void node must have children with an empty string in it, important!!!!
}
return myResume
}
const parseHtmlConf = {
selector: 'span[data-w-e-type="attachment"]', // CSS selector to match specific HTML tags
parseElemHtml: parseAttachmentHtml,
}
Boot.registerParseElemHtml(parseHtmlConf)
}
},
}
</script>

View File

@@ -0,0 +1,8 @@
import { axios } from '@/utils/request'
export function getSystemLanguage() {
return axios({
url: '/common-setting/v1/system/language',
method: 'get',
})
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

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

View File

@@ -301,7 +301,7 @@ export default {
return [
{ value: 'is', label: this.$t('cmdbFilterComp.is') },
{ value: '~is', label: this.$t('cmdbFilterComp.~is') },
{ value: '~value', label: this.$t('cmdbFilterComp.~value') }, // 为空的定义有点绕
{ value: '~value', label: this.$t('cmdbFilterComp.~value') },
{ value: 'value', label: this.$t('cmdbFilterComp.value') },
]
}

View File

@@ -87,9 +87,11 @@ export default {
},
methods: {
/**
* @param isInitOne When the initialization exp is null, does the ruleList default to giving one
*/
visibleChange(open, isInitOne = true) {
// isInitOne 初始化exp为空时ruleList是否默认给一条
// const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
// const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
const exp = this.expression.match(new RegExp(this.regQ, 'g'))
? this.expression.match(new RegExp(this.regQ, 'g'))[0]
: null
@@ -204,7 +206,7 @@ export default {
},
handleSubmit() {
if (this.ruleList && this.ruleList.length) {
this.ruleList[0].type = 'and' // 增删后以防万一第一个不是and
this.ruleList[0].type = 'and' // after add/delete, just in case the first one is not 'and'
this.filterExp = ''
const expList = this.ruleList.map((rule) => {
let singleRuleExp = ''

View File

@@ -14,7 +14,7 @@
<script>
/**
* 元素折叠过度效果
* Collapse transition effect for elements
*/
export default {
name: 'CollapseTransition',
@@ -33,20 +33,17 @@ export default {
},
methods: {
collapseBeforeEnter(el) {
// console.log('11, collapseBeforeEnter');
this.oldPaddingBottom = el.style.paddingBottom
this.oldPaddingTop = el.style.paddingTop
// 过渡效果开始前设置元素的maxHeight为0让元素maxHeight有一个初始值
// set the element's maxHeight to 0 before the transition effect starts so that the element's maxHeight has an initial value
el.style.paddingTop = '0'
el.style.paddingBottom = '0'
el.style.maxHeight = '0'
},
collapseEnter(el, done) {
// console.log('22, collapseEnter');
//
this.oldOverflow = el.style.overflow
const elHeight = el.scrollHeight
// 过渡效果进入后将元素的maxHeight设置为元素本身的高度将元素maxHeight设置为auto不会有过渡效果
// After entering, set maxHeight to the element's height; setting maxHeight to auto will not have a transition effect
if (elHeight > 0) {
el.style.maxHeight = elHeight + 'px'
} else {
@@ -59,24 +56,20 @@ export default {
// done();
const onTransitionDone = function() {
done()
// console.log('enter onTransitionDone');
el.removeEventListener('transitionend', onTransitionDone, false)
el.removeEventListener('transitioncancel', onTransitionDone, false)
}
// 绑定元素的transition完成事件在transition完成后立即完成vue的过度动效
// Bind transition end event to finish Vue's transition immediately after the CSS transition
el.addEventListener('transitionend', onTransitionDone, false)
el.addEventListener('transitioncancel', onTransitionDone, false)
},
collapseAfterEnter(el) {
// console.log('33, collapseAfterEnter');
// 过渡效果完成后恢复元素的maxHeight
// Restore maxHeight after transition is complete
el.style.maxHeight = ''
el.style.overflow = this.oldOverflow
},
collapseBeforeLeave(el) {
// console.log('44, collapseBeforeLeave', el.scrollHeight);
this.oldPaddingBottom = el.style.paddingBottom
this.oldPaddingTop = el.style.paddingTop
this.oldOverflow = el.style.overflow
@@ -85,8 +78,6 @@ export default {
el.style.overflow = 'hidden'
},
collapseLeave(el, done) {
// console.log('55, collapseLeave', el.scrollHeight);
if (el.scrollHeight !== 0) {
el.style.maxHeight = '0'
el.style.paddingBottom = '0'
@@ -95,16 +86,14 @@ export default {
// done();
const onTransitionDone = function() {
done()
// console.log('leave onTransitionDone');
el.removeEventListener('transitionend', onTransitionDone, false)
el.removeEventListener('transitioncancel', onTransitionDone, false)
}
// 绑定元素的transition完成事件在transition完成后立即完成vue的过度动效
// Bind transition end event to finish Vue's transition immediately after the CSS transition
el.addEventListener('transitionend', onTransitionDone, false)
el.addEventListener('transitioncancel', onTransitionDone, false)
},
collapseAfterLeave(el) {
// console.log('66, collapseAfterLeave');
el.style.maxHeight = ''
el.style.overflow = this.oldOverflow
el.style.paddingBottom = this.oldPaddingBottom

View File

@@ -1,9 +1,6 @@
/* eslint-disable */
/*
!!!!!!!
以下为凶残的cron表达式验证胆小肾虚及心脏病者慎入!!!
不听劝告者后果自负T T
!!!!!!!
cron表达式验证
cron表达式为秒
判断正误方法错误的话返回错误信息正确的话返回true
*/

View File

@@ -47,7 +47,7 @@ export const commonIconList = ['changyong-ubuntu',
export const linearIconList = [
{
value: 'database',
label: '数据库',
label: 'components.database',
list: [{
value: 'icon-xianxing-DB2',
label: 'DB2'
@@ -81,7 +81,7 @@ export const linearIconList = [
}]
}, {
value: 'system',
label: '操作系统',
label: 'components.system',
list: [{
value: 'icon-xianxing-Windows',
label: 'Windows'
@@ -106,7 +106,7 @@ export const linearIconList = [
}]
}, {
value: 'language',
label: '语言',
label: 'components.language',
list: [{
value: 'icon-xianxing-python',
label: 'python'
@@ -137,7 +137,7 @@ export const linearIconList = [
}]
}, {
value: 'status',
label: '状态',
label: 'components.status',
list: [{
value: 'icon-xianxing-yiwen',
label: '疑问'
@@ -177,7 +177,7 @@ export const linearIconList = [
}]
}, {
value: 'icon-xianxing-application',
label: '常用组件',
label: 'components.commonComponent',
list: [{
value: 'icon-xianxing-yilianjie',
label: '已连接'
@@ -310,7 +310,7 @@ export const linearIconList = [
}]
}, {
value: 'data',
label: '数据',
label: 'components.data',
list: [{
value: 'icon-xianxing-bingzhuangtu',
label: '饼状图'
@@ -387,7 +387,7 @@ export const linearIconList = [
export const fillIconList = [
{
value: 'database',
label: '数据库',
label: 'components.database',
list: [{
value: 'icon-shidi-DB2',
label: 'DB2'
@@ -421,7 +421,7 @@ export const fillIconList = [
}]
}, {
value: 'system',
label: '操作系统',
label: 'components.system',
list: [{
value: 'icon-shidi-Windows',
label: 'Windows'
@@ -446,7 +446,7 @@ export const fillIconList = [
}]
}, {
value: 'language',
label: '语言',
label: 'components.language',
list: [{
value: 'icon-shidi-python',
label: 'python'
@@ -477,7 +477,7 @@ export const fillIconList = [
}]
}, {
value: 'status',
label: '状态',
label: 'components.status',
list: [{
value: 'icon-shidi-yiwen',
label: '疑问'
@@ -517,7 +517,7 @@ export const fillIconList = [
}]
}, {
value: 'icon-shidi-application',
label: '常用组件',
label: 'components.commonComponent',
list: [{
value: 'icon-shidi-yilianjie',
label: '已连接'
@@ -650,7 +650,7 @@ export const fillIconList = [
}]
}, {
value: 'data',
label: '数据',
label: 'components.data',
list: [{
value: 'icon-shidi-bingzhuangtu',
label: '饼状图'
@@ -727,7 +727,7 @@ export const fillIconList = [
export const multicolorIconList = [
{
value: 'database',
label: '数据库',
label: 'components.database',
list: [{
value: 'caise-TIDB',
label: 'TIDB'
@@ -773,7 +773,7 @@ export const multicolorIconList = [
}]
}, {
value: 'cloud',
label: '',
label: 'components.cloud',
list: [{
value: 'AWS',
label: 'AWS'
@@ -819,7 +819,7 @@ export const multicolorIconList = [
}]
}, {
value: 'system',
label: '操作系统',
label: 'components.system',
list: [{
value: 'ciase-aix',
label: 'aix'
@@ -847,7 +847,7 @@ export const multicolorIconList = [
}]
}, {
value: 'language',
label: '语言',
label: 'components.language',
list: [{
value: 'caise-python',
label: 'python'
@@ -878,7 +878,7 @@ export const multicolorIconList = [
}]
}, {
value: 'status',
label: '状态',
label: 'components.status',
list: [{
value: 'caise-yiwen',
label: '疑问'
@@ -918,7 +918,7 @@ export const multicolorIconList = [
}]
}, {
value: 'caise-application',
label: '常用组件',
label: 'components.commonComponent',
list: [{
value: 'caise-websphere',
label: 'WebSphere'
@@ -1180,7 +1180,7 @@ export const multicolorIconList = [
}]
}, {
value: 'data',
label: '数据',
label: 'components.data',
list: [{
value: 'caise-bingzhuangtu',
label: '饼状图'

View File

@@ -33,7 +33,7 @@
<template v-if="iconList && iconList.length">
<template v-if="currentIconType !== '4'">
<div v-for="category in iconList" :key="category.value">
<h4 class="category">{{ category.label }}</h4>
<h4 class="category">{{ $t(category.label) }}</h4>
<div class="custom-icon-select-popover-content-wrapper">
<div
v-for="name in category.list"

View File

@@ -0,0 +1,60 @@
<script>
import Tooltip from 'ant-design-vue/es/tooltip'
import { cutStrByFullLength, getStrFullLength } from '@/components/_util/util'
/*
const isSupportLineClamp = document.body.style.webkitLineClamp !== undefined;
const TooltipOverlayStyle = {
overflowWrap: 'break-word',
wordWrap: 'break-word',
};
*/
export default {
name: 'Ellipsis',
components: {
Tooltip,
},
props: {
prefixCls: {
type: String,
default: 'ant-pro-ellipsis',
},
tooltip: {
type: Boolean,
},
length: {
type: Number,
required: true,
},
lines: {
type: Number,
default: 1,
},
fullWidthRecognition: {
type: Boolean,
default: false,
},
},
methods: {
getStrDom(str, fullLength) {
return <span>{cutStrByFullLength(str, this.length) + (fullLength > this.length ? '...' : '')}</span>
},
getTooltip(fullStr, fullLength) {
return (
<Tooltip overlayStyle={{ maxWidth: '700px' }}>
<template slot="title">{fullStr}</template>
{this.getStrDom(fullStr, fullLength)}
</Tooltip>
)
},
},
render() {
const { tooltip, length } = this.$props
const str = this.$slots.default.map((vNode) => vNode.text).join('')
const fullLength = getStrFullLength(str)
const strDom = tooltip && fullLength > length ? this.getTooltip(str, fullLength) : this.getStrDom(str, fullLength)
return strDom
},
}
</script>

View File

@@ -0,0 +1,3 @@
import Ellipsis from './Ellipsis'
export default Ellipsis

View File

@@ -0,0 +1,38 @@
# Ellipsis 文本自动省略号
文本过长自动处理省略号,支持按照文本长度和最大行数两种方式截取。
引用方式:
```javascript
import Ellipsis from '@/components/Ellipsis'
export default {
components: {
Ellipsis
}
}
```
## 代码演示 [demo](https://pro.loacg.com/test/home)
```html
<ellipsis :length="100" tooltip>
There were injuries alleged in three cases in 2015, and a
fourth incident in September, according to the safety recall report. After meeting with US regulators in October, the firm decided to issue a voluntary recall.
</ellipsis>
```
## API
参数 | 说明 | 类型 | 默认值
----|------|-----|------
tooltip | 移动到文本展示完整内容的提示 | boolean | -
length | 在按照长度截取下的文本最大字符数,超过则截取省略 | number | -

View File

@@ -6,9 +6,9 @@
</div>
<div class="content">
<h1>{{ config[type].title }}</h1>
<div class="desc">{{ config[type].desc }}</div>
<div class="desc">{{ $t(config[type].desc) }}</div>
<div class="actions">
<a-button type="primary" @click="handleToHome">返回首页</a-button>
<a-button type="primary" @click="handleToHome">{{ $t('exception.backToHome') }}</a-button>
</div>
</div>
</div>

View File

@@ -2,17 +2,17 @@ const types = {
403: {
img: 'https://gw.alipayobjects.com/zos/rmsportal/wZcnGqRDyhPOEYFcZDnb.svg',
title: '403',
desc: '抱歉,你无权访问该页面'
desc: 'exception.desc1'
},
404: {
img: 'https://gw.alipayobjects.com/zos/rmsportal/KpnpchXsobRgLElEozzI.svg',
title: '404',
desc: '抱歉,你访问的页面不存在或仍在开发中'
desc: 'exception.desc2'
},
500: {
img: 'https://gw.alipayobjects.com/zos/rmsportal/RVRUAYdCGeYNBWoKiIwB.svg',
title: '500',
desc: '抱歉,服务器出错了'
desc: 'exception.desc3'
}
}

View File

@@ -1,58 +0,0 @@
<template>
<div class="footer">
<div class="links">
<a
href="https://veops.cn/"
target="_blank"
>维易科技</a>
<a
href="https://github.com/sendya/ant-design-pro-vue"
target="_blank"
>
<a-icon type="github" />
</a>
</div>
<div class="copyright">
Copyright
<a-icon type="copyright" /> 2021-2023 <span>@维易科技</span>
</div>
</div>
</template>
<script>
export default {
name: 'GlobalFooter',
data () {
return {}
}
}
</script>
<style lang="less" scoped>
.footer {
padding: 0 16px;
margin: 48px 0 24px;
text-align: center;
.links {
margin-bottom: 8px;
a {
color: rgba(0, 0, 0, 0.45);
&:hover {
color: rgba(0, 0, 0, 0.65);
}
&:not(:last-child) {
margin-right: 40px;
}
}
}
.copyright {
color: rgba(0, 0, 0, 0.45);
font-size: 14px;
}
}
</style>

View File

@@ -1,2 +0,0 @@
import GlobalFooter from './GlobalFooter'
export default GlobalFooter

View File

@@ -1,14 +1,6 @@
import router, { resetRouter } from '@/router'
import Menu from 'ant-design-vue/es/menu'
import Icon from 'ant-design-vue/es/icon'
import store from '@/store'
import {
subscribeCIType,
subscribeTreeView,
} from '@/modules/cmdb/api/preference'
import { searchResourceType } from '@/modules/acl/api/resource'
import { roleHasPermissionToGrant } from '@/modules/acl/api/permission'
import CMDBGrant from '@/modules/cmdb/components/cmdbGrant'
import styles from './index.module.less'
import { mapActions } from 'vuex'
@@ -87,40 +79,6 @@ export default {
inject: ['reload'],
methods: {
...mapActions(['UpdateCMDBSEarchValue']),
cancelAttributes(e, menu) {
const that = this
e.preventDefault()
e.stopPropagation()
this.$confirm({
title: this.$t('warning'),
content: this.$t('cmdb.preference.confirmcancelSub2', { name: menu.meta.title }),
onOk() {
const citypeId = menu.meta.typeId
const unsubCIType = subscribeCIType(citypeId, '')
const unsubTree = subscribeTreeView(citypeId, '')
Promise.all([unsubCIType, unsubTree]).then(() => {
that.$message.success(that.$t('cmdb.preference.cancelSubSuccess'))
const lastTypeId = window.localStorage.getItem('ops_ci_typeid') || undefined
if (Number(citypeId) === Number(lastTypeId)) {
localStorage.setItem('ops_ci_typeid', '')
}
const href = window.location.href
const hrefSplit = href.split('/')
if (Number(hrefSplit[hrefSplit.length - 1]) === Number(citypeId)) {
that.$router.push('/cmdb/preference')
}
const roles = store.getters.roles
resetRouter()
store.dispatch('GenerateRoutes', { roles }, { root: true }).then(() => {
router.addRoutes(store.getters.appRoutes)
})
if (hrefSplit[hrefSplit.length - 1] === 'preference') {
that.reload()
}
})
},
})
},
// select menu item
onOpenChange(openKeys) {
if (this.mode === 'horizontal') {
@@ -170,7 +128,6 @@ export default {
return this.$t(`${title}`)
},
renderMenuItem(menu) {
const isShowDot = menu.path.substr(0, 22) === '/cmdb/instances/types/'
const target = menu.meta.target || null
const tag = target && 'a' || 'router-link'
const props = { to: { name: menu.name } }
@@ -187,26 +144,11 @@ export default {
<tag {...{ props, attrs }}>
{this.renderIcon({ icon: menu.meta.icon, customIcon: menu.meta.customIcon, name: menu.meta.name, typeId: menu.meta.typeId, routeName: menu.name, selectedIcon: menu.meta.selectedIcon, })}
<span>
<span style={menu.meta.style} class={this.renderI18n(menu.meta.title).length > 10 ? 'scroll' : ''}>{this.renderI18n(menu.meta.title)}</span>
{isShowDot && !menu.meta.disabled &&
<a-popover
overlayClassName="custom-menu-extra-submenu"
placement="rightTop"
arrowPointAtCenter
autoAdjustOverflow={false}
getPopupContainer={(trigger) => trigger}
content={() =>
<div>
<div onClick={e => this.handlePerm(e, menu, 'CIType')} class="custom-menu-extra-submenu-item"><a-icon type="user-add" />{ this.renderI18n('grant') }</div>
<div onClick={e => this.cancelAttributes(e, menu)} class="custom-menu-extra-submenu-item"><a-icon type="star" />{ this.renderI18n('cmdb.preference.cancelSub') }</div>
</div>}
>
<a-icon type="menu" ref="extraEllipsis" class="custom-menu-extra-ellipsis"></a-icon>
</a-popover>
}
<span style={menu.meta.style} class={this.renderI18n(menu.meta.title).length > 10 ? 'scroll' : ''}>
{this.renderI18n(menu.meta.title)}
</span>
</span>
</tag>
{isShowDot && <CMDBGrant ref="cmdbGrantCIType" resourceType="CIType" app_id="cmdb" />}
</Item>
)
},
@@ -269,27 +211,6 @@ export default {
)
}
},
handlePerm(e, menu, resource_type_name) {
e.stopPropagation()
e.preventDefault()
roleHasPermissionToGrant({
app_id: 'cmdb',
resource_type_name,
perm: 'grant',
resource_name: menu.meta.name,
}).then(res => {
if (res.result) {
console.log(menu)
if (resource_type_name === 'CIType') {
this.$refs.cmdbGrantCIType.open({ name: menu.meta.name, cmdbGrantType: 'ci', CITypeId: menu.meta?.typeId })
} else {
this.$refs.cmdbGrantRelationView.open({ name: menu.meta.name, cmdbGrantType: 'relation_view' })
}
} else {
this.$message.error(this.$t('noPermission'))
}
})
},
jumpCMDBSearch(value) {
this.UpdateCMDBSEarchValue(value)

View File

@@ -1,10 +0,0 @@
import { Spin } from 'ant-design-vue'
export default {
name: 'PageLoading',
render () {
return (<div style={{ paddingTop: 100, textAlign: 'center' }}>
<Spin size="large" />
</div>)
}
}

View File

@@ -1,352 +0,0 @@
<template>
<div class="setting-drawer" ref="settingDrawer">
<a-drawer
width="300"
placement="right"
@close="onClose"
:closable="false"
:visible="visible"
>
<div class="setting-drawer-index-content">
<div :style="{ marginBottom: '24px' }">
<h3 class="setting-drawer-index-title">整体风格设置</h3>
<div class="setting-drawer-index-blockChecbox">
<a-tooltip>
<template slot="title">
暗色菜单风格
</template>
<div class="setting-drawer-index-item" @click="handleMenuTheme('dark')">
<img src="https://gw.alipayobjects.com/zos/rmsportal/LCkqqYNmvBEbokSDscrm.svg" alt="dark">
<div class="setting-drawer-index-selectIcon" v-if="navTheme === 'dark'">
<a-icon type="check"/>
</div>
</div>
</a-tooltip>
<a-tooltip>
<template slot="title">
亮色菜单风格
</template>
<div class="setting-drawer-index-item" @click="handleMenuTheme('light')">
<img src="https://gw.alipayobjects.com/zos/rmsportal/jpRkZQMyYRryryPNtyIC.svg" alt="light">
<div class="setting-drawer-index-selectIcon" v-if="navTheme !== 'dark'">
<a-icon type="check"/>
</div>
</div>
</a-tooltip>
</div>
</div>
<div :style="{ marginBottom: '24px' }">
<h3 class="setting-drawer-index-title">主题色</h3>
<div style="height: 20px">
<a-tooltip class="setting-drawer-theme-color-colorBlock" v-for="(item, index) in colorList" :key="index">
<template slot="title">
{{ item.key }}
</template>
<a-tag :color="item.color" @click="changeColor(item.color)">
<a-icon type="check" v-if="item.color === primaryColor"></a-icon>
</a-tag>
</a-tooltip>
</div>
</div>
<a-divider />
<div :style="{ marginBottom: '24px' }">
<h3 class="setting-drawer-index-title">导航模式</h3>
<div class="setting-drawer-index-blockChecbox">
<a-tooltip>
<template slot="title">
侧边栏导航
</template>
<div class="setting-drawer-index-item" @click="handleLayout('sidemenu')">
<img src="https://gw.alipayobjects.com/zos/rmsportal/JopDzEhOqwOjeNTXkoje.svg" alt="sidemenu">
<div class="setting-drawer-index-selectIcon" v-if="layoutMode === 'sidemenu'">
<a-icon type="check"/>
</div>
</div>
</a-tooltip>
<a-tooltip>
<template slot="title">
顶部栏导航
</template>
<div class="setting-drawer-index-item" @click="handleLayout('topmenu')">
<img src="https://gw.alipayobjects.com/zos/rmsportal/KDNDBbriJhLwuqMoxcAr.svg" alt="topmenu">
<div class="setting-drawer-index-selectIcon" v-if="layoutMode !== 'sidemenu'">
<a-icon type="check"/>
</div>
</div>
</a-tooltip>
</div>
<div :style="{ marginTop: '24px' }">
<a-list :split="false">
<a-list-item>
<a-tooltip slot="actions">
<template slot="title">
该设定仅 [顶部栏导航] 时有效
</template>
<a-select size="small" style="width: 80px;" :defaultValue="contentWidth" @change="handleContentWidthChange">
<a-select-option value="Fixed">固定</a-select-option>
<a-select-option value="Fluid" v-if="layoutMode !== 'sidemenu'">流式</a-select-option>
</a-select>
</a-tooltip>
<a-list-item-meta>
<div slot="title">内容区域宽度</div>
</a-list-item-meta>
</a-list-item>
<a-list-item>
<a-switch slot="actions" size="small" :defaultChecked="fixedHeader" @change="handleFixedHeader" />
<a-list-item-meta>
<div slot="title">固定 Header</div>
</a-list-item-meta>
</a-list-item>
<a-list-item>
<a-switch slot="actions" size="small" :disabled="!fixedHeader" :defaultChecked="autoHideHeader" @change="handleFixedHeaderHidden" />
<a-list-item-meta>
<a-tooltip slot="title" placement="left">
<template slot="title">固定 Header 时可配置</template>
<div :style="{ opacity: !fixedHeader ? '0.5' : '1' }">下滑时隐藏 Header</div>
</a-tooltip>
</a-list-item-meta>
</a-list-item>
<a-list-item >
<a-switch slot="actions" size="small" :disabled="(layoutMode === 'topmenu')" :defaultChecked="fixSiderbar" @change="handleFixSiderbar" />
<a-list-item-meta>
<div slot="title" :style="{ textDecoration: layoutMode === 'topmenu' ? 'line-through' : 'unset' }">固定侧边菜单</div>
</a-list-item-meta>
</a-list-item>
</a-list>
</div>
</div>
<a-divider />
<div :style="{ marginBottom: '24px' }">
<h3 class="setting-drawer-index-title">其他设置</h3>
<div>
<a-list :split="false">
<a-list-item>
<a-switch slot="actions" size="small" :defaultChecked="colorWeak" @change="onColorWeak" />
<a-list-item-meta>
<div slot="title">色弱模式</div>
</a-list-item-meta>
</a-list-item>
<a-list-item>
<a-switch slot="actions" size="small" :defaultChecked="multiTab" @change="onMultiTab" />
<a-list-item-meta>
<div slot="title">多页签模式</div>
</a-list-item-meta>
</a-list-item>
</a-list>
</div>
</div>
<a-divider />
<div :style="{ marginBottom: '24px' }">
<a-button
@click="doCopy"
icon="copy"
block
>拷贝设置</a-button>
<a-alert type="warning" :style="{ marginTop: '24px' }">
<span slot="message">
配置栏只在开发环境用于预览生产环境不会展现请手动修改配置文件
<a href="https://github.com/sendya/ant-design-pro-vue/blob/master/src/config/setting.js" target="_blank">src/config/setting.js</a>
</span>
</a-alert>
</div>
</div>
<div class="setting-drawer-index-handle" @click="toggle">
<a-icon type="setting" v-if="!visible"/>
<a-icon type="close" v-else/>
</div>
</a-drawer>
</div>
</template>
<script>
import SettingItem from './SettingItem'
import config from '@/config/setting'
import { updateTheme, updateColorWeak, colorList } from './settingConfig'
import { mixin, mixinDevice } from '@/utils/mixin'
export default {
components: {
SettingItem
},
mixins: [mixin, mixinDevice],
data () {
return {
visible: true,
colorList
}
},
watch: {
},
mounted () {
const vm = this
setTimeout(() => {
vm.visible = false
}, 16)
updateTheme(this.primaryColor)
if (this.colorWeak !== config.colorWeak) {
updateColorWeak(this.colorWeak)
}
},
methods: {
showDrawer () {
this.visible = true
},
onClose () {
this.visible = false
},
toggle () {
this.visible = !this.visible
},
onColorWeak (checked) {
this.$store.dispatch('ToggleWeak', checked)
updateColorWeak(checked)
},
onMultiTab (checked) {
this.$store.dispatch('ToggleMultiTab', checked)
},
handleMenuTheme (theme) {
this.$store.dispatch('ToggleTheme', theme)
},
doCopy () {
// get current settings from mixin or this.$store.state.app, pay attention to the property name
const text = `export default {
primaryColor: '${this.primaryColor}', // primary color of ant design
navTheme: '${this.navTheme}', // theme for nav menu
layout: '${this.layoutMode}', // nav menu position: sidemenu or topmenu
contentWidth: '${this.contentWidth}', // layout of content: Fluid or Fixed, only works when layout is topmenu
fixedHeader: ${this.fixedHeader}, // sticky header
fixSiderbar: ${this.fixSiderbar}, // sticky siderbar
autoHideHeader: ${this.autoHideHeader}, // auto hide header
colorWeak: ${this.colorWeak},
multiTab: ${this.multiTab},
production: process.env.NODE_ENV === 'production' && process.env.VUE_APP_PREVIEW !== 'true',
// vue-ls options
storageOptions: {
namespace: 'pro__',
name: 'ls',
storage: 'local',
}
}`
this.$copyText(text).then(message => {
console.log('copy', message)
this.$message.success('复制完毕')
}).catch(err => {
console.log('copy.err', err)
this.$message.error('复制失败')
})
},
handleLayout (mode) {
this.$store.dispatch('ToggleLayoutMode', mode)
// 因为顶部菜单不能固定左侧菜单栏所以强制关闭
this.handleFixSiderbar(false)
},
handleContentWidthChange (type) {
this.$store.dispatch('ToggleContentWidth', type)
},
changeColor (color) {
if (this.primaryColor !== color) {
this.$store.dispatch('ToggleColor', color)
updateTheme(color)
}
},
handleFixedHeader (fixed) {
this.$store.dispatch('ToggleFixedHeader', fixed)
},
handleFixedHeaderHidden (autoHidden) {
this.$store.dispatch('ToggleFixedHeaderHidden', autoHidden)
},
handleFixSiderbar (fixed) {
if (this.layoutMode === 'topmenu') {
this.$store.dispatch('ToggleFixSiderbar', false)
return
}
this.$store.dispatch('ToggleFixSiderbar', fixed)
}
}
}
</script>
<style lang="less" scoped>
.setting-drawer-index-content {
.setting-drawer-index-blockChecbox {
display: flex;
.setting-drawer-index-item {
margin-right: 16px;
position: relative;
border-radius: 4px;
cursor: pointer;
img {
width: 48px;
}
.setting-drawer-index-selectIcon {
position: absolute;
top: 0;
right: 0;
width: 100%;
padding-top: 15px;
padding-left: 24px;
height: 100%;
color: #1890ff;
font-size: 14px;
font-weight: 700;
}
}
}
.setting-drawer-theme-color-colorBlock {
width: 20px;
height: 20px;
border-radius: 2px;
float: left;
cursor: pointer;
margin-right: 8px;
padding-left: 0px;
padding-right: 0px;
text-align: center;
color: #fff;
font-weight: 700;
i {
font-size: 14px;
}
}
}
.setting-drawer-index-handle {
position: absolute;
top: 240px;
background: #1890ff;
width: 48px;
height: 48px;
right: 300px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
pointer-events: auto;
z-index: 1001;
text-align: center;
font-size: 16px;
border-radius: 4px 0 0 4px;
i {
color: rgb(255, 255, 255);
font-size: 20px;
}
}
</style>

View File

@@ -1,38 +0,0 @@
<template>
<div class="setting-drawer-index-item">
<h3 class="setting-drawer-index-title">{{ title }}</h3>
<slot></slot>
<a-divider v-if="divider"/>
</div>
</template>
<script>
export default {
name: 'SettingItem',
props: {
title: {
type: String,
default: ''
},
divider: {
type: Boolean,
default: false
}
}
}
</script>
<style lang="less" scoped>
.setting-drawer-index-item {
margin-bottom: 24px;
.setting-drawer-index-title {
font-size: 14px;
color: rgba(0, 0, 0, .85);
line-height: 22px;
margin-bottom: 12px;
}
}
</style>

View File

@@ -1,2 +0,0 @@
import SettingDrawer from './SettingDrawer'
export default SettingDrawer

View File

@@ -1,105 +0,0 @@
import { message } from 'ant-design-vue/es'
// import setting from '../setting';
import themeColor from './themeColor.js'
// let lessNodesAppended
const colorList = [
{
key: '薄暮', color: '#F5222D'
},
{
key: '火山', color: '#FA541C'
},
{
key: '日暮', color: '#FAAD14'
},
{
key: '明青', color: '#13C2C2'
},
{
key: '极光绿', color: '#52C41A'
},
{
key: '拂晓蓝(默认)', color: '#1890FF'
},
{
key: '极客蓝', color: '#2F54EB'
},
{
key: '酱紫', color: '#722ED1'
}
]
const updateTheme = newPrimaryColor => {
const hideMessage = message.loading('正在切换主题!', 0)
themeColor.changeColor(newPrimaryColor).finally(t => {
hideMessage()
})
}
/*
const updateTheme = primaryColor => {
// Don't compile less in production!
/* if (process.env.NODE_ENV === 'production') {
return;
} * /
// Determine if the component is remounted
if (!primaryColor) {
return
}
const hideMessage = message.loading('正在编译主题!', 0)
function buildIt () {
if (!window.less) {
return
}
setTimeout(() => {
window.less
.modifyVars({
'@primary-color': primaryColor
})
.then(() => {
hideMessage()
})
.catch(() => {
message.error('Failed to update theme')
hideMessage()
})
}, 200)
}
if (!lessNodesAppended) {
// insert less.js and color.less
const lessStyleNode = document.createElement('link')
const lessConfigNode = document.createElement('script')
const lessScriptNode = document.createElement('script')
lessStyleNode.setAttribute('rel', 'stylesheet/less')
lessStyleNode.setAttribute('href', '/color.less')
lessConfigNode.innerHTML = `
window.less = {
async: true,
env: 'production',
javascriptEnabled: true
};
`
lessScriptNode.src = 'https://gw.alipayobjects.com/os/lib/less.js/3.8.1/less.min.js'
lessScriptNode.async = true
lessScriptNode.onload = () => {
buildIt()
lessScriptNode.onload = null
}
document.body.appendChild(lessStyleNode)
document.body.appendChild(lessConfigNode)
document.body.appendChild(lessScriptNode)
lessNodesAppended = true
} else {
buildIt()
}
}
*/
const updateColorWeak = colorWeak => {
// document.body.className = colorWeak ? 'colorWeak' : '';
colorWeak ? document.body.classList.add('colorWeak') : document.body.classList.remove('colorWeak')
}
export { updateTheme, colorList, updateColorWeak }

View File

@@ -1,23 +0,0 @@
import client from 'webpack-theme-color-replacer/client'
import generate from '@ant-design/colors/lib/generate'
export default {
getAntdSerials (color) {
// 淡化即less的tint
const lightens = new Array(9).fill().map((t, i) => {
return client.varyColor.lighten(color, i / 10)
})
// colorPalette变换得到颜色值
const colorPalettes = generate(color)
return lightens.concat(colorPalettes)
},
changeColor (newColor) {
var options = {
newColors: this.getAntdSerials(newColor), // new colors array, one-to-one corresponde with `matchColors`
changeUrl (cssUrl) {
return `/${cssUrl}` // while router is not `hash` mode, it needs absolute path
}
}
return client.changer.changeColor(options, Promise)
}
}

View File

@@ -2,10 +2,12 @@ import MultiTab from '@/components/MultiTab'
import Result from '@/components/Result'
import TagSelect from '@/components/TagSelect'
import ExceptionPage from '@/components/Exception'
import Ellipsis from '@/components/Ellipsis'
export {
MultiTab,
Result,
ExceptionPage,
TagSelect
TagSelect,
Ellipsis
}

View File

@@ -1,6 +1,6 @@
<template>
<a-tooltip>
<template slot="title">文档中心</template>
<template slot="title">{{ $t('documentCenter') }}</template>
<span class="document-link">
<a-icon type="question-circle" @click="handleClick" />
</span>

View File

@@ -38,7 +38,7 @@
<script>
import store from '@/store'
import { gridSvg, top_agent, top_acl } from '@/core/icons'
import { getPreference } from '@/modules/cmdb/api/preference'
export default {
name: 'TopMenu',
components: { gridSvg, top_agent, top_acl },
@@ -77,18 +77,7 @@ export default {
async handleClick(route) {
this.visible = false
if (route.name !== this.current) {
if (route.name === 'cmdb') {
const preference = await getPreference()
const lastTypeId = window.localStorage.getItem('ops_ci_typeid') || undefined
if (lastTypeId && preference.type_ids.some((item) => item === Number(lastTypeId))) {
this.$router.push(`/cmdb/instances/types/${lastTypeId}`)
} else {
this.$router.push('/cmdb/dashboard')
}
} else {
this.$router.push(route.redirect)
}
// this.current = route.name
this.$router.push(route.redirect)
}
},
},

View File

@@ -1,4 +1,3 @@
/* eslint-disable */
import Vue from 'vue'
import router from './router'
import store from './store'
@@ -11,16 +10,23 @@ import i18n from '@/lang'
NProgress.configure({ showSpinner: false })
// 不用认证的页面
const whitePath = ['/user/login', '/user/logout', '/user/register', '/api/sso/login', '/api/sso/logout', '/user/forgetPassword']
// pages that do not require authentication
const whitePath = [
'/user/login',
'/user/logout',
'/user/register',
'/api/sso/login',
'/api/sso/logout',
'/user/forgetPassword'
]
// 此处不处理登录, 只处理 是否有用户信息的认证 前端permission的处理 axios处理401 -> 登录
// 登录页面处理处理 是否使用单点登录
// Only handle user info authentication here, not login logic.
// Frontend permission handling; axios handles 401 -> login.
// Login page handles whether to use SSO.
router.beforeEach(async (to, from, next) => {
NProgress.start() // start progress bar
to.meta && (!!to.meta.title && setDocumentTitle(`${i18n.t(to.meta.title)} - ${domTitle}`))
const authed = store.state.authed
const auth_type = localStorage.getItem('ops_auth_type')
if (whitePath.includes(to.path)) {
next()
@@ -28,17 +34,17 @@ router.beforeEach(async (to, from, next) => {
store.dispatch('GetAuthDataEnable')
store.dispatch('GetInfo').then(res => {
const roles = res.result && res.result.role
store.dispatch("loadAllUsers")
store.dispatch("loadAllEmployees")
store.dispatch("loadAllDepartments")
store.dispatch('loadAllUsers')
store.dispatch('loadAllEmployees')
store.dispatch('loadAllDepartments')
store.dispatch('GenerateRoutes', { roles }).then(() => {
router.addRoutes(store.getters.appRoutes)
const redirect = decodeURIComponent(from.query.redirect || to.path)
if (to.path === redirect) {
// hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
// Ensure addRoutes is complete, set replace: true so navigation will not leave a history record
next({ ...to, replace: true })
} else {
// 跳转到目的路由
// Redirect to the target route
next({ path: redirect })
}
})

View File

@@ -109,9 +109,29 @@ export default {
default: 'default',
tip: 'Tip',
cmdbSearch: 'Search',
requestError: 'An error occurred, please try again later',
requestServiceError: 'Unknown error on the server, please contact the administrator',
requestWait: 'The modification has been submitted, please wait for review ({time} seconds)',
documentCenter: 'Document Center',
exception: {
backToHome: 'Back to home page',
desc1: 'Sorry, you are not authorized to access this page',
desc2: 'Sorry, the page you are visiting does not exist or is still under development',
desc3: 'Sorry, server error'
},
pagination: {
total: '{range0}-{range1} of {total} items'
},
components: {
colorTagSelectTip: 'Enter or select tags',
database: 'Database',
system: 'System',
language: 'Language',
status: 'Status',
commonComponent: 'Common Component',
data: 'Data',
cloud: 'Cloud'
},
topMenu: {
personalCenter: 'My Profile',
logout: 'Logout',

View File

@@ -109,9 +109,29 @@ export default {
default: '默认',
tip: '提示',
cmdbSearch: '搜索一下',
requestError: '出现错误,请稍后再试',
requestServiceError: '服务端未知错误, 请联系管理员!',
requestWait: '修改已提交,请等待审核({time}s',
documentCenter: '文档中心',
exception: {
backToHome: '返回首页',
desc1: '抱歉,你无权访问该页面',
desc2: '抱歉,你访问的页面不存在或仍在开发中',
desc3: '抱歉,服务器出错了'
},
pagination: {
total: '当前展示 {range0}-{range1} 条数据, 共 {total} 条'
},
components: {
colorTagSelectTip: '选择或输入(回车确定)标签',
database: '数据库',
system: '操作系统',
language: '语言',
status: '状态',
commonComponent: '常用组件',
data: '数据',
cloud: '云'
},
topMenu: {
personalCenter: '个人中心',
logout: '退出登录',

View File

@@ -63,8 +63,6 @@ import RouteView from './RouteView'
import MultiTab from '@/components/MultiTab'
import SideMenu from '@/components/Menu/SideMenu'
import GlobalHeader from '@/components/GlobalHeader'
import GlobalFooter from '@/components/GlobalFooter'
import SettingDrawer from '@/components/SettingDrawer'
export default {
name: 'BasicLayout',
@@ -74,8 +72,6 @@ export default {
MultiTab,
SideMenu,
GlobalHeader,
GlobalFooter,
SettingDrawer,
},
data() {
return {

View File

@@ -1,4 +1,3 @@
/* eslint-disable */
import '@babel/polyfill'
import Vue from 'vue'
import App from './App.vue'
@@ -14,10 +13,9 @@ import i18n from './lang'
import iconFont from '../public/iconfont/iconfont'
// 存在直接crash的风险 还未到
const customIcon = Icon.createFromIconfontCN(iconFont)
Vue.component('ops-icon', customIcon)
var vue;
var vue
async function start() {
const _vue = new Vue({

View File

@@ -63,7 +63,14 @@
show-quick-jumper
:current="tablePage.currentPage"
:total="tablePage.total"
:show-total="(total, range) => `当前展示 ${range[0]}-${range[1]} 条数据, 共 ${total} 条`"
:show-total="
(total, range) =>
$t('pagination.total', {
range0: range[0],
range1: range[1],
total,
})
"
:page-size="tablePage.pageSize"
:default-current="1"
:page-size-options="pageSizeOptions"

View File

@@ -138,7 +138,14 @@
show-quick-jumper
:current="tablePage.currentPage"
:total="tablePage.total"
:show-total="(total, range) => `当前展示 ${range[0]}-${range[1]} 条数据, 共 ${total} 条`"
:show-total="
(total, range) =>
$t('pagination.total', {
range0: range[0],
range1: range[1],
total,
})
"
:page-size="tablePage.pageSize"
:default-current="1"
:page-size-options="pageSizeOptions"

View File

@@ -107,7 +107,14 @@
show-quick-jumper
:current="tablePage.currentPage"
:total="tablePage.total"
:show-total="(total, range) => `当前展示 ${range[0]}-${range[1]} 条数据, 共 ${total} 条`"
:show-total="
(total, range) =>
$t('pagination.total', {
range0: range[0],
range1: range[1],
total,
})
"
:page-size="tablePage.pageSize"
:default-current="1"
:page-size-options="pageSizeOptions"

View File

@@ -157,3 +157,18 @@ export function preferenceCitypeOrder(data) {
data: data
})
}
export function getAutoSubscription() {
return axios({
url: '/v0.1/preference/auto_subscription',
method: 'get',
})
}
export function putAutoSubscription(data) {
return axios({
url: '/v0.1/preference/auto_subscription',
method: 'put',
data
})
}

View File

@@ -132,6 +132,7 @@ export default {
/deep/ .ant-select-selection {
height: 28px;
line-height: 28px;
border: none;
.ant-select-selection__rendered {
height: 28px;

View File

@@ -21,7 +21,7 @@
</template>
<span
class="ci-icon-letter"
v-else
v-else-if="title"
>
<span>
{{ title[0].toUpperCase() }}

View File

@@ -1,4 +1,9 @@
export const getCurrentRowStyle = ({ row }, addedRids) => {
const idx = addedRids.findIndex(item => item.rid === row.rid)
return idx > -1 ? 'background-color:#E0E7FF!important' : ''
}
export const getCurrentRowStyle = ({ row }, addedRids) => {
const idx = addedRids.findIndex(item => item.rid === row.rid)
return idx > -1 ? 'background-color:#E0E7FF!important' : ''
}
export const getCurrentRowClass = ({ row }, addedRids) => {
const idx = addedRids.findIndex(item => item.rid === row.rid)
return idx > -1 ? 'grant-table-row-focus' : ''
}

View File

@@ -0,0 +1,108 @@
<template>
<a-select
v-bind="$attrs"
v-model="currenCIType"
style="width: 100%"
:showSearch="true"
:filterOption="filterOption"
:placeholder="placeholder || $t('placeholder2')"
>
<a-select-opt-group
v-for="group in selectOptions"
:key="group.id"
:label="group.name || $t('other')"
>
<a-select-option
v-for="type in group.ci_types"
:key="type.id"
:value="type.id"
:data-alias="type.alias"
:data-name="type.name"
>
{{ type.alias || type.name || $t('other') }}
<span v-if="type.name" class="select-option-name">({{ type.name }})</span>
</a-select-option>
</a-select-opt-group>
</a-select>
</template>
<script>
import _ from 'lodash'
import { getCITypeGroupsConfig } from '@/modules/cmdb/api/ciTypeGroup'
export default {
name: 'CMDBTypeSelectAntd',
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: [String, Number, Array],
default: undefined,
},
CITypeGroup: {
type: Array,
default: undefined
},
placeholder: {
type: String,
default: '',
}
},
data() {
return {
selectOptions: []
}
},
computed: {
currenCIType: {
get() {
return this.value
},
set(newVal, oldVal) {
if (newVal !== oldVal) {
this.$emit('change', newVal)
}
return newVal
},
},
},
watch: {
CITypeGroup: {
deep: true,
handler() {
this.handleSelectOptions()
}
}
},
async mounted() {
this.handleSelectOptions()
},
methods: {
async handleSelectOptions() {
let rawCITypeGroup = []
if (this.CITypeGroup) {
rawCITypeGroup = this.CITypeGroup
} else {
rawCITypeGroup = await getCITypeGroupsConfig({ need_other: true })
}
this.selectOptions = _.cloneDeep(rawCITypeGroup).filter((group) => group?.ci_types?.length)
},
filterOption(input, option) {
const attrs = option?.data?.attrs || {}
const alias = attrs?.['data-alias']?.toLowerCase?.() || ''
const name = attrs?.['data-name']?.toLowerCase?.() || ''
return alias.indexOf(input.toLowerCase()) >= 0 || name.indexOf(input.toLowerCase()) >= 0
}
},
}
</script>
<style lang="less" scoped>
.select-option-name {
font-size: 12px;
color: #A5A9BC;
}
</style>

View File

@@ -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>

View File

@@ -81,8 +81,6 @@
<script>
import { mapState } from 'vuex'
import router, { resetRouter } from '@/router'
import store from '@/store'
import {
subscribeCIType,
getSubscribeAttributes,
@@ -223,9 +221,8 @@ export default {
selectedAttrList.map((item) => {
return [item, !!this.fixedList.includes(item)]
})
).then((res) => {
).then(() => {
this.$message.success(this.$t('cmdb.components.subSuccess'))
this.resetRoute()
if (this.selectedAttrList.length > 0) {
this.instanceSubscribed = true
} else {
@@ -233,13 +230,7 @@ export default {
}
})
},
resetRoute() {
resetRouter()
const roles = store.getters.roles
store.dispatch('GenerateRoutes', { roles }, { root: true }).then(() => {
router.addRoutes(store.getters.appRoutes)
})
},
setTargetKeys(targetKeys) {
this.selectedAttrList = targetKeys
},

View File

@@ -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',
@@ -221,12 +222,15 @@ const cmdb_en = {
otherGroupTips: 'Non sortable within the other group',
filterTips: 'click to show {name}',
attributeAssociation: 'Attribute Association',
attributeAssociationTip1: 'Automatically establish relationships through attribute values (except password, json, multi-value, long text, boolean, reference) of two models',
attributeAssociationTip1: 'Automatically establish relationships through attribute values (except password, json, long text, boolean, reference) of two models',
attributeAssociationTip2: 'Double click to edit',
attributeAssociationTip3: 'Two Attributes must be selected',
attributeAssociationTip4: 'Please select a attribute from Source CIType',
attributeAssociationTip5: 'Please select a attribute from Target CIType',
attributeAssociationTip6: 'Cannot be deleted again.',
attributeAssociationTip7: '1. The attribute value types of the source model and target model must be consistent.',
attributeAssociationTip8: '2. One To Many: Source model can select multiple value attributes',
attributeAssociationTip9: '3. Many To Many: Either the source model or the target model can be a multi-valued attribute.',
show: 'show attribute',
setAsShow: 'Set as show attribute',
cancelSetAsShow: 'Cancel show attribute',
@@ -253,6 +257,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',
@@ -440,6 +463,20 @@ const cmdb_en = {
searchPlaceholder: 'Please search CIType',
subCITable: 'Data',
subCITree: 'Tree',
autoSub: 'Auto Subscription',
autoSub2: 'Click To Enable Auto Subscribe',
autoSubScope: 'Auto Subscription Scope',
subscribeAllModel: 'Subscribe All Models',
selectiveSubscription: 'Selective Subscription',
excludeGroup: 'Exclude Group',
excludeModel: 'Exclude Model',
selectGroup: 'Select Group',
selectModel: 'Select Model',
isEnable: 'Is Enable',
enableAutoSubTip: 'After enabling automatic subscription, the model list in the resource data menu will only be based on the results of automatic subscription.',
tips1: 'Please go to',
tips2: ' Preference ',
tips3: 'page first to complete your subscription!'
},
custom_dashboard: {
charts: 'Chart',
@@ -701,6 +738,9 @@ 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',
upstream: 'Upstream',
downstream: 'Downstream'
},
serviceTree: {
remove: 'Remove',
@@ -725,7 +765,6 @@ if __name__ == "__main__":
searchTips: 'Search in service tree'
},
tree: {
tips1: 'Please go to Preference page first to complete your subscription!',
subSettings: 'Settings',
},
topo: {

View File

@@ -77,6 +77,7 @@ const cmdb_zh = {
confirmDeleteADT: '确认删除 【{pluginName}】',
attributeMap: '字段映射',
nodeConfig: '节点配置',
scanningParameter: '扫描参数',
autoDiscovery: '自动发现属性',
node: '节点',
adExecConfig: '执行配置',
@@ -221,12 +222,15 @@ const cmdb_zh = {
otherGroupTips: '其他分组属性不可排序',
filterTips: '点击可仅查看{name}属性',
attributeAssociation: '属性关联',
attributeAssociationTip1: '通过2个模型的属性值(除密码、json、多值、长文本、布尔、引用)来自动建立关系',
attributeAssociationTip1: '通过2个模型的属性值(除密码、json、长文本、布尔、引用)来自动建立关系',
attributeAssociationTip2: '双击可编辑',
attributeAssociationTip3: '属性关联必须选择两个属性',
attributeAssociationTip4: '请选择原模型属性',
attributeAssociationTip5: '请选择目标模型属性',
attributeAssociationTip6: '不可再删除',
attributeAssociationTip7: '1. 源模型和目标模型的属性值类型必须保持一致',
attributeAssociationTip8: '2. 一对多:源模型可选多值属性',
attributeAssociationTip9: '3. 多对多:源模型和目标模型其中任何一个可为多值属性',
show: '展示属性',
setAsShow: '设置为展示属性',
cancelSetAsShow: '取消设置为展示属性',
@@ -253,6 +257,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地址格式错误',
@@ -439,6 +462,20 @@ const cmdb_zh = {
searchPlaceholder: '请搜索模型',
subCITable: '数据订阅',
subCITree: '层级订阅',
autoSub: '自动订阅',
autoSub2: '点击开启自动订阅',
autoSubScope: '自动订阅范围',
subscribeAllModel: '订阅所有模型',
selectiveSubscription: '选择性订阅',
excludeGroup: '排除分组',
excludeModel: '排除模型',
selectGroup: '选择分组',
selectModel: '选择模型',
isEnable: '是否启用',
enableAutoSubTip: '开启自动订阅后,资源数据菜单的模型列表只以自动订阅的结果为准',
tips1: '请先到',
tips2: ' 我的订阅 ',
tips3: '页面完成订阅!',
},
custom_dashboard: {
charts: '图表',
@@ -700,6 +737,9 @@ if __name__ == "__main__":
batchRollbacking: '正在回滚,共{total}个,成功{successNum}个,失败{errorNum}个',
baselineTips: '该时间点的变更也会被回滚, 唯一标识、密码属性、动态属性不支持回滚',
cover: '覆盖',
detail: '详情',
upstream: '上游',
downstream: '下游'
},
serviceTree: {
remove: '移除',
@@ -724,7 +764,6 @@ if __name__ == "__main__":
searchTips: '在服务树中筛选'
},
tree: {
tips1: '请先到 我的订阅 页面完成订阅!',
subSettings: '订阅设置',
},
topo: {

View File

@@ -1,5 +1,5 @@
import { RouteView, BasicLayout } from '@/layouts'
import { getPreference, getRelationView } from '@/modules/cmdb/api/preference'
import { getRelationView } from '@/modules/cmdb/api/preference'
const genCmdbRoutes = async () => {
const routes = {
@@ -7,6 +7,7 @@ const genCmdbRoutes = async () => {
name: 'cmdb',
component: BasicLayout,
meta: { title: 'CMDB', keepAlive: false },
redirect: '/cmdb/instances/types',
children: [
// preference
// views
@@ -39,12 +40,10 @@ const genCmdbRoutes = async () => {
},
},
{
path: '/cmdb/resourceviews',
path: '/cmdb/instances/types/:typeId?',
name: 'cmdb_resource_views',
component: RouteView,
meta: { title: 'cmdb.menu.ciTable', icon: 'ops-cmdb-resource', selectedIcon: 'ops-cmdb-resource', keepAlive: true },
hideChildrenInMenu: false,
children: []
component: () => import(`../views/ci/index`),
meta: { title: 'cmdb.menu.ciTable', icon: 'ops-cmdb-resource', selectedIcon: 'ops-cmdb-resource', keepAlive: false }
},
{
path: '/cmdb/tree_views',
@@ -71,7 +70,7 @@ const genCmdbRoutes = async () => {
{
path: '/cmdb/adc',
name: 'cmdb_auto_discovery_ci',
meta: { title: 'cmdb.menu.adCIs', icon: 'ops-cmdb-adc', selectedIcon: 'ops-cmdb-adc', keepAlive: false },
meta: { title: 'cmdb.menu.adCIs', icon: 'ops-cmdb-adc', selectedIcon: 'ops-cmdb-adc', keepAlive: false, permission: ['admin', 'cmdb_admin'] },
component: () => import('../views/discoveryCI/index.vue')
},
{
@@ -176,35 +175,8 @@ const genCmdbRoutes = async () => {
}
]
}
// Dynamically add subscription items and business relationships
const [preference, relation] = await Promise.all([getPreference(), getRelationView()])
const resourceViewsIndex = routes.children.findIndex(item => item.name === 'cmdb_resource_views')
preference.group_types.forEach(group => {
if (preference.group_types.length > 1) {
routes.children[resourceViewsIndex].children.push({
path: `/cmdb/instances/types/group${group.id}`,
name: `cmdb_instances_group_${group.id}`,
meta: { title: group.name || 'other', disabled: true, style: 'margin-left: 12px' },
})
}
group.ci_types.forEach(item => {
routes.children[resourceViewsIndex].children.push({
path: `/cmdb/instances/types/${item.id}`,
component: () => import(`../views/ci/index`),
name: `cmdb_${item.id}`,
meta: { title: item.alias, keepAlive: false, typeId: item.id, name: item.name, customIcon: item.icon },
// hideChildrenInMenu: true // Force display of MenuItem instead of SubMenu
})
})
})
const lastTypeId = window.localStorage.getItem('ops_ci_typeid') || undefined
if (lastTypeId && preference.type_ids.some(item => item === Number(lastTypeId))) {
routes.redirect = `/cmdb/instances/types/${lastTypeId}`
} else if (routes.children[resourceViewsIndex]?.children?.length > 0) {
routes.redirect = routes.children[resourceViewsIndex].children.find(item => !item.hidden && !item.meta.disabled)?.path
} else {
routes.redirect = '/cmdb/dashboard'
}
// get service tree dynamic display menu
const relation = await getRelationView()
if (relation?.name2id?.length === 0) {
const relationViewRouteIndex = routes.children?.findIndex?.((route) => route.name === 'cmdb_relation_views')

View File

@@ -1,19 +1,14 @@
<template>
<div>
<p class="cmdb-batch-upload-label"><span>*</span>1. {{ $t('cmdb.batch.selectCIType') }}</p>
<a-select
showSearch
:placeholder="$t('cmdb.batch.selectCITypeTips')"
@change="selectCiType"
:style="{ width: '50%', marginBottom: '1em' }"
class="ops-select"
:filter-option="filterOption"
<CMDBTypeSelectAntd
v-model="selectNum"
>
<a-select-option v-for="ciType in ciTypeList" :key="ciType.name" :value="ciType.id">{{
ciType.alias
}}</a-select-option>
</a-select>
ref="CMDBTypeSelectAntd"
:placeholder="$t('cmdb.batch.selectCITypeTips')"
:style="{ width: '50%', marginBottom: '1em' }"
:CITypeGroup="CITypeGroup"
@change="selectCiType"
/>
<p class="cmdb-batch-upload-label">&nbsp;&nbsp;2. {{ $t('cmdb.batch.downloadTemplate') }}</p>
<a-button
:style="{ marginBottom: '1em' }"
@@ -99,16 +94,21 @@ import _ from 'lodash'
import { mapState } from 'vuex'
import ExcelJS from 'exceljs'
import FileSaver from 'file-saver'
import { getCITypes } from '@/modules/cmdb/api/CIType'
import { getCITypeGroupsConfig } from '@/modules/cmdb/api/ciTypeGroup'
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
import { getCITypeParent, getCanEditByParentIdChildId } from '@/modules/cmdb/api/CITypeRelation'
import { searchPermResourceByRoleId } from '@/modules/acl/api/permission'
import CMDBTypeSelectAntd from '@/modules/cmdb/components/cmdbTypeSelect/cmdbTypeSelectAntd'
export default {
name: 'CiTypeChoice',
components: {
CMDBTypeSelectAntd
},
data() {
return {
ciTypeList: [],
CITypeGroup: [],
ciTypeName: '',
selectNum: undefined,
selectCiTypeAttrList: [],
@@ -132,11 +132,16 @@ export default {
resource_type_id: 'CIType',
app_id: 'cmdb',
})
getCITypes().then((res) => {
this.ciTypeList = res.ci_types.filter((type) => {
const _findRe = resources.find((re) => re.name === type.name)
return _findRe?.permissions.includes('create') ?? false
getCITypeGroupsConfig({ need_other: true }).then((res) => {
const CITypeGroup = res || []
CITypeGroup.forEach((group) => {
group.ci_types = (group.ci_types || []).filter((type) => {
const _find = resources.find((resource) => resource.name === type.name)
return _find?.permissions?.includes?.('create') ?? false
})
})
this.CITypeGroup = CITypeGroup.filter((group) => group?.ci_types?.length)
})
},
watch: {
@@ -152,18 +157,27 @@ export default {
},
},
methods: {
selectCiType(el) {
selectCiType(id) {
// Callback function when a template type is selected
getCITypeAttributesById(el).then((res) => {
getCITypeAttributesById(id).then((res) => {
this.$emit('getCiTypeAttr', res)
this.selectCiTypeAttrList = res
})
this.ciTypeList.forEach((item) => {
if (this.selectNum === item.id) {
this.ciTypeName = item.alias || item.name
}
})
const selectOptions = this.$refs?.CMDBTypeSelectAntd?.selectOptions
let ciTypeName = ''
if (selectOptions?.length) {
selectOptions.forEach((option) => {
if (option?.ci_types?.length) {
option.ci_types.forEach((type) => {
if (type?.id === id) {
ciTypeName = type.alias || type.name
}
})
}
})
}
this.ciTypeName = ciTypeName
},
openModal() {
@@ -188,9 +202,6 @@ export default {
this.checkedAttrs = this.selectCiTypeAttrList.attributes.map((item) => item.alias || item.name)
})
},
filterOption(input, option) {
return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
},
handleCancel() {
this.visible = false
},

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,833 @@
<template>
<div id="ciIndex" class="cmdb-ci">
<a-spin :tip="loadTip" :spinning="loading" >
<div class="cmdb-views-header">
<span>
<span class="cmdb-views-header-title">{{ CIType.alias || CIType.name }}</span>
<span
@click="
() => {
$refs.metadataDrawer.open(typeId)
}
"
class="cmdb-views-header-metadata"
>
<a-icon type="info-circle" />{{ $t('cmdb.ci.attributeDesc') }}
</span>
</span>
<a-space>
<a-button
type="primary"
class="ops-button-ghost"
ghost
@click="$refs.create.handleOpen(true, 'create')"
><ops-icon type="veops-increase" />
{{ $t('create') }}
</a-button>
<EditAttrsPopover :typeId="typeId" class="operation-icon" @refresh="refreshAfterEditAttrs">
<a-button
type="primary"
ghost
class="ops-button-ghost"
><ops-icon type="veops-configuration_table" />{{ $t('cmdb.configTable') }}</a-button
>
</EditAttrsPopover>
<a-dropdown v-model="visible">
<a-button type="primary" ghost class="ops-button-ghost">···</a-button>
<a-menu slot="overlay" @click="handleMenuClick">
<a-menu-item @click="handlePerm" key="grant">
<a-icon type="user-add" />
{{ $t('grant') }}
</a-menu-item>
<a-menu-item
v-if="!autoSub.enabled"
key="cancelSub"
@click="unsubscribe"
>
<a-icon type="star" />
{{ $t('cmdb.preference.cancelSub') }}
</a-menu-item>
</a-menu>
</a-dropdown>
</a-space>
</div>
<div class="cmdb-ci-main">
<SearchForm
ref="search"
@refresh="handleSearch"
:preferenceAttrList="preferenceAttrList"
:typeId="typeId"
:selectedRowKeys="selectedRowKeys"
@copyExpression="copyExpression"
>
<PreferenceSearch
ref="preferenceSearch"
v-show="!selectedRowKeys.length"
@getQAndSort="getQAndSort"
@setParamsFromPreferenceSearch="setParamsFromPreferenceSearch"
/>
<div class="ops-list-batch-action" v-show="!!selectedRowKeys.length">
<span @click="$refs.create.handleOpen(true, 'update')">{{ $t('update') }}</span>
<a-divider type="vertical" />
<span @click="openBatchDownload">{{ $t('download') }}</span>
<a-divider type="vertical" />
<span @click="batchDelete">{{ $t('delete') }}</span>
<a-divider type="vertical" />
<span @click="batchRollback">{{ $t('cmdb.ci.rollback') }}</span>
<span>{{ $t('cmdb.ci.selectRows', { rows: selectedRowKeys.length }) }}</span>
</div>
</SearchForm>
<CiDetailDrawer ref="detail" :typeId="typeId" />
<CITable
ref="xTable"
:id="`cmdb-ci-${typeId}`"
:loading="loading"
:attrList="preferenceAttrList"
:columns="columns"
:passwordValue="passwordValue"
:data="instanceList"
:height="tableHeight"
@onSelectChange="onSelectChange"
@edit-closed="handleEditClose"
@edit-actived="handleEditActived"
@sort-change="handleSortCol"
@openDetail="openDetail"
@deleteCI="deleteCI"
/>
<div :style="{ textAlign: 'right', marginTop: '4px' }">
<a-pagination
:showSizeChanger="true"
:current="currentPage"
size="small"
:total="totalNumber"
show-quick-jumper
:page-size="pageSize"
:page-size-options="pageSizeOptions"
@showSizeChange="onShowSizeChange"
:show-total="
(total, range) =>
$t('pagination.total', {
range0: range[0],
range1: range[1],
total,
})
"
@change="
(page) => {
currentPage = page
}
"
>
<template slot="buildOptionText" slot-scope="props">
<span v-if="props.value !== '100000'">{{ props.value }}{{ $t('itemsPerPage') }}</span>
<span v-if="props.value === '100000'">{{ $t('cmdb.ci.all') }}</span>
</template>
</a-pagination>
</div>
<create-instance-form
ref="create"
:typeIdFromProp="typeId"
@reload="reloadData"
@submit="batchUpdate"
/>
<BatchDownload ref="batchDownload" @batchDownload="batchDownload" />
<CiRollbackForm ref="ciRollbackForm" @batchRollbackAsync="batchRollbackAsync($event)" :ciIds="selectedRowKeys" />
<MetadataDrawer ref="metadataDrawer" />
<CMDBGrant ref="cmdbGrant" resourceTypeName="CIType" app_id="cmdb" />
</div>
</a-spin>
</div>
</template>
<script>
import _ from 'lodash'
import Sortable from 'sortablejs'
import { searchCI, updateCI, deleteCI } from '@/modules/cmdb/api/ci'
import { getSubscribeAttributes, subscribeCIType, subscribeTreeView } from '@/modules/cmdb/api/preference'
import { getCITypeAttributesById, getAttrPassword } from '@/modules/cmdb/api/CITypeAttr'
import { roleHasPermissionToGrant } from '@/modules/acl/api/permission'
import { searchResourceType } from '@/modules/acl/api/resource'
import { CIBaselineRollback } from '@/modules/cmdb/api/history'
import { getCITableColumns } from '../../utils/helper'
import { intersection } from '@/utils/functions/set'
import BatchDownload from '../../components/batchDownload/batchDownload.vue'
import PreferenceSearch from '../../components/preferenceSearch/preferenceSearch.vue'
import MetadataDrawer from './modules/MetadataDrawer.vue'
import CMDBGrant from '../../components/cmdbGrant'
import CiRollbackForm from './modules/ciRollbackForm.vue'
import SearchForm from '@/modules/cmdb/components/searchForm/SearchForm.vue'
import CreateInstanceForm from './modules/CreateInstanceForm'
import CiDetailDrawer from './modules/ciDetailDrawer.vue'
import EditAttrsPopover from './modules/editAttrsPopover'
import CITable from '@/modules/cmdb/components/ciTable/index.vue'
export default {
name: 'InstanceList',
components: {
SearchForm,
CreateInstanceForm,
CiDetailDrawer,
EditAttrsPopover,
BatchDownload,
PreferenceSearch,
MetadataDrawer,
CMDBGrant,
CiRollbackForm,
CITable
},
props: {
typeId: {
type: Number,
default: undefined
},
CIType: {
type: Object,
default: () => {}
},
autoSub: {
type: Object,
default: () => {}
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
tableHeight() {
// if (this.selectedRowKeys && this.selectedRowKeys.length) {
// return this.windowHeight - 246
// }
return this.windowHeight - 240
},
},
data() {
return {
tableData: [],
loading: false,
currentPage: 1,
pageSizeOptions: ['50', '100', '200', '100000'],
pageSize: 50,
totalNumber: 0,
loadTip: '',
form: this.$form.createForm(this),
preferenceAttrList: [],
instanceList: [],
columns: [],
// custom table alert & rowSelection
selectedRowKeys: [],
// Check whether to edit
initialInstanceList: [],
sortByTable: undefined,
isEditActive: false,
attrList: [],
attributes: {},
// Table drag parameters
tableDragClassName: [],
resource_type: {},
initialPasswordValue: {},
passwordValue: {},
lastEditCiId: null,
isContinueCloseEdit: true,
visible: false,
}
},
watch: {
currentPage: function(newVal, oldVal) {
this.loadTableData(this.sortByTable)
},
},
provide() {
return {
handleSearch: this.handleSearch,
setPreferenceSearchCurrent: this.setPreferenceSearchCurrent,
attrList: () => {
return this.attrList
},
attributes: () => {
return this.attributes
},
filterCompPreferenceSearch: () => {
return { type_id: this.typeId }
},
resource_type: () => {
return this.resource_type
}
}
},
async mounted() {
this.loading = true
await this.getAttributeList()
await this.loadPreferenceAttrList()
await this.loadTableData()
this.loading = false
this.$nextTick(() => {
const loadingNode = document.getElementsByClassName('ant-drawer-mask')
if (loadingNode?.style) {
loadingNode.style.zIndex = 8
}
})
setTimeout(() => {
this.columnDrop()
}, 1000)
},
beforeDestroy() {
// window.onkeypress = null
if (this.sortable) {
this.sortable.destroy()
}
},
methods: {
async getAttributeList() {
await getCITypeAttributesById(this.typeId).then((res) => {
this.attrList = res.attributes
this.attributes = res
})
},
handleSearch() {
this.$refs.xTable.getVxetableRef().clearSort()
this.sortByTable = undefined
this.$nextTick(() => {
if (this.currentPage === 1) {
this.reloadData()
} else {
this.currentPage = 1
}
})
},
async loadTableData(sortByTable = undefined) {
try {
this.loading = true
// If fuzzy search is possible, queryParam can be deleted later.
// const queryParams = this.$refs['search'].queryParam || {}
const fuzzySearch = this.$refs['search'].fuzzySearch
const expression = this.$refs['search'].expression || ''
const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
const regSort = /(?<=sort=).+/g
const exp = expression.match(regQ) ? expression.match(regQ)[0] : null
let sort
if (sortByTable) {
sort = sortByTable
} else {
sort = expression.match(regSort) ? expression.match(regSort)[0] : undefined
}
const res = await searchCI({
q: `_type:${this.typeId}${exp ? `,${exp}` : ''}${fuzzySearch ? `,*${fuzzySearch}*` : ''}`,
count: this.pageSize,
page: this.currentPage,
sort,
})
this.totalNumber = res['numfound']
this.columns = this.getColumns(res.result, this.preferenceAttrList)
this.columns.forEach((col) => {
if (col.is_password) {
this.initialPasswordValue[col.field] = ''
this.passwordValue[col.field] = ''
}
})
const jsonAttrList = this.attrList.filter((attr) => attr.value_type === '6')
this.instanceList = res['result'].map((item) => {
jsonAttrList.forEach(
(jsonAttr) => (item[jsonAttr.name] = item[jsonAttr.name] ? JSON.stringify(item[jsonAttr.name]) : '')
)
return { ..._.cloneDeep(item) }
})
this.initialInstanceList = _.cloneDeep(this.instanceList)
this.$nextTick(() => {
// this.setSelectRows()
this.$refs.xTable.getVxetableRef().refreshColumn()
})
} finally {
this.loading = false
}
},
getColumns(data, attrList) {
const width = document.getElementById('ciIndex').clientWidth - 50
return getCITableColumns(data, attrList, width)
},
setSelectRows() {
const cached = new Set(this.selectedRowKeys)
const loaded = new Set(this.instanceList.map((i) => i.ci_id || i._id))
const inter = Array.from(intersection(cached, loaded))
if (inter.length === this.instanceList.length) {
this.$refs['xTable'].getVxetableRef().setAllCheckboxRow(true)
} else {
const rows = []
inter.forEach((rid) => {
rows.push(this.$refs['xTable'].getVxetableRef().getRowById(rid))
})
this.$refs['xTable'].getVxetableRef().setCheckboxRow(rows, true)
}
},
async loadPreferenceAttrList() {
const subscribed = await getSubscribeAttributes(this.typeId)
this.preferenceAttrList = subscribed.attributes // All columns that have been subscribed
},
onSelectChange(records) {
this.selectedRowKeys = records.map((i) => i.ci_id || i._id)
},
reloadData() {
this.loadTableData()
},
handleEditClose({ row, rowIndex, column }) {
if (!this.isContinueCloseEdit) {
return
}
const $table = this.$refs['xTable'].getVxetableRef()
const data = {}
this.columns.forEach((item) => {
if (
!(item.field in this.initialPasswordValue) &&
!_.isEqual(row[item.field], this.initialInstanceList[rowIndex][item.field])
) {
data[item.field] = row[item.field] ?? null
}
})
Object.keys(this.initialPasswordValue).forEach((key) => {
if (this.initialPasswordValue[key] !== this.passwordValue[key]) {
data[key] = this.passwordValue[key]
row[key] = this.passwordValue[key]
}
})
this.isEditActive = false
this.lastEditCiId = null
if (JSON.stringify(data) !== '{}') {
updateCI(row.ci_id || row._id, data)
.then(() => {
this.$message.success(this.$t('saveSuccess'))
$table.reloadRow(row, null)
const _initialInstanceList = _.cloneDeep(this.initialInstanceList)
_initialInstanceList[rowIndex] = {
..._initialInstanceList[rowIndex],
...data,
}
this.initialInstanceList = _initialInstanceList
})
.catch((err) => {
console.log(err)
this.loadTableData()
})
}
this.columns.forEach((col) => {
if (col.is_password) {
this.initialPasswordValue[col.field] = ''
this.passwordValue[col.field] = ''
}
})
},
async openBatchDownload() {
this.$refs.batchDownload.open({
preferenceAttrList: this.preferenceAttrList.filter((attr) => !attr?.is_reference),
ciTypeName: this.CIType.alias || this.CIType.name,
})
},
batchDownload({ filename, type, checkedKeys }) {
const jsonAttrList = []
checkedKeys.forEach((key) => {
const _find = this.attrList.find((attr) => attr.name === key)
if (_find && _find.value_type === '6') jsonAttrList.push(key)
})
const data = _.cloneDeep([
...this.$refs.xTable.getVxetableRef().getCheckboxReserveRecords(),
...this.$refs.xTable.getVxetableRef().getCheckboxRecords(true),
])
this.$refs.xTable.getVxetableRef().exportData({
filename,
type,
columnFilterMethod({ column }) {
return checkedKeys.includes(column.property)
},
data: [
...data.map((item) => {
jsonAttrList.forEach((jsonAttr) => (item[jsonAttr] = item[jsonAttr] ? JSON.stringify(item[jsonAttr]) : ''))
return { ...item }
}),
],
})
this.selectedRowKeys = []
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
},
batchUpdate(values) {
const that = this
this.$confirm({
title: that.$t('warning'),
content: that.$t('cmdb.ci.batchUpdateConfirm'),
async onOk() {
that.batchUpdateAsync(values)
},
})
},
async batchUpdateAsync(values) {
let successNum = 0
let errorNum = 0
this.loading = true
this.loadTip = this.$t('cmdb.ci.batchUpdateInProgress') + '...'
const payload = {}
Object.keys(values).forEach((key) => {
// Field values support blanking
// There are currently field values that do not support blanking and will be returned by the backend.
if (values[key] === undefined || values[key] === null) {
payload[key] = null
} else {
payload[key] = values[key]
}
})
this.$refs.create.visible = false
const key = 'updatable'
let errorMsg = ''
for (let i = 0; i < this.selectedRowKeys.length; i++) {
await updateCI(this.selectedRowKeys[i], payload, false)
.then(() => {
successNum += 1
})
.catch((error) => {
errorMsg = errorMsg + '\n' + `${this.selectedRowKeys[i]}:${error.response?.data?.message ?? ''}`
this.$notification.warning({
key,
message: this.$t('warning'),
description: errorMsg,
duration: 0,
style: { whiteSpace: 'break-spaces', overflow: 'auto', maxHeight: this.windowHeight - 80 + 'px' },
})
errorNum += 1
})
.finally(() => {
this.loadTip = this.$t('cmdb.ci.batchUpdateInProgress2', {
total: this.selectedRowKeys.length,
successNum: successNum,
errorNum: errorNum,
})
})
}
this.loading = false
this.loadTip = ''
this.selectedRowKeys = []
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
this.reloadData()
},
batchDelete() {
const that = this
this.$confirm({
title: that.$t('warning'),
content: that.$t('confirmDelete'),
onOk() {
that.batchDeleteAsync()
},
})
},
async batchDeleteAsync() {
let successNum = 0
let errorNum = 0
this.loading = true
this.loadTip = this.$t('cmdb.ci.batchDeleting')
const floor = Math.ceil(this.selectedRowKeys.length / 6)
for (let i = 0; i < floor; i++) {
const itemList = this.selectedRowKeys.slice(6 * i, 6 * i + 6)
const promises = itemList.map((x) => deleteCI(x, false))
await Promise.allSettled(promises)
.then((res) => {
res.forEach((r) => {
if (r.status === 'fulfilled') {
successNum += 1
} else {
errorNum += 1
}
})
})
.finally(() => {
this.loadTip = this.$t('cmdb.ci.batchDeleting2', {
total: this.selectedRowKeys.length,
successNum: successNum,
errorNum: errorNum,
})
})
}
this.loading = false
this.loadTip = ''
this.selectedRowKeys = []
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
this.$nextTick(() => {
if (this.currentPage === 1) {
this.loadTableData()
} else {
this.currentPage = 1
}
})
},
deleteCI(record) {
const that = this
this.$confirm({
title: that.$t('warning'),
content: that.$t('confirmDelete'),
onOk() {
deleteCI(record.ci_id || record._id).then((res) => {
// that.$refs.table.refresh(true)
that.$message.success(that.$t('deleteSuccess'))
that.reloadData()
})
},
})
},
batchRollback() {
this.$nextTick(() => {
this.$refs.ciRollbackForm.onOpen(true)
})
},
async batchRollbackAsync(params) {
const mask = document.querySelector('.ant-drawer-mask')
const oldValue = mask.style.zIndex
mask.style.zIndex = 2
let successNum = 0
let errorNum = 0
this.loading = true
this.loadTip = this.$t('cmdb.ci.rollbackingTips')
const floor = Math.ceil(this.selectedRowKeys.length / 6)
for (let i = 0; i < floor; i++) {
const itemList = this.selectedRowKeys.slice(6 * i, 6 * i + 6)
const promises = itemList.map((x) => CIBaselineRollback(x, params))
await Promise.allSettled(promises)
.then((res) => {
res.forEach((r) => {
if (r.status === 'fulfilled') {
successNum += 1
} else {
errorNum += 1
}
})
})
.finally(() => {
this.loadTip = this.$t('cmdb.ci.batchRollbacking', {
total: this.selectedRowKeys.length,
successNum: successNum,
errorNum: errorNum,
})
})
}
this.loading = false
this.loadTip = ''
mask.style.zIndex = oldValue
this.selectedRowKeys = []
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
this.$nextTick(() => {
if (this.currentPage === 1) {
this.loadTableData()
} else {
this.currentPage = 1
}
})
},
async refreshAfterEditAttrs() {
await this.loadPreferenceAttrList()
await this.loadTableData()
},
onShowSizeChange(current, pageSize) {
this.pageSize = pageSize
if (this.currentPage === 1) {
this.reloadData()
} else {
this.currentPage = 1
}
setTimeout(() => {
// this.setSelectRows()
}, 500)
},
handleSortCol({ column, property, order, sortBy, sortList, $event }) {
let sortByTable
if (order === 'asc') {
sortByTable = property
} else if (order === 'desc') {
sortByTable = `-${property}`
}
this.sortByTable = sortByTable
this.$nextTick(() => {
if (this.currentPage === 1) {
this.loadTableData(sortByTable)
} else {
this.currentPage = 1
}
})
},
columnDrop() {
this.$nextTick(() => {
const xTable = this.$refs?.xTable?.getVxetableRef?.()
if (!xTable) {
return
}
this.sortable = Sortable.create(
xTable.$el.querySelector('.body--wrapper>.vxe-table--header .vxe-header--row'),
{
handle: '.vxe-handle',
onChoose: () => {
const header = xTable.$el.querySelector('.body--wrapper>.vxe-table--header .vxe-header--row')
const classNameList = []
header.childNodes.forEach((item) => {
classNameList.push(item.classList[1])
})
this.tableDragClassName = classNameList
},
onEnd: (params) => {
// 由于开启了虚拟滚动newIndex和oldIndex是虚拟的
const { newIndex, oldIndex } = params
// 从tableDragClassName拿到colid
const fromColid = this.tableDragClassName[oldIndex]
const toColid = this.tableDragClassName[newIndex]
const fromColumn = xTable.getColumnById(fromColid)
const toColumn = xTable.getColumnById(toColid)
const fromIndex = xTable.getColumnIndex(fromColumn)
const toIndex = xTable.getColumnIndex(toColumn)
const tableColumn = xTable.getColumns()
const currRow = tableColumn.splice(fromIndex, 1)[0]
tableColumn.splice(toIndex, 0, currRow)
xTable.loadColumn(tableColumn)
},
}
)
})
},
handleEditActived() {
this.isEditActive = true
const passwordCol = this.columns.filter((col) => col.is_password)
this.$nextTick(() => {
const editRecord = this.$refs.xTable.getVxetableRef().getEditRecord()
const { row, column } = editRecord
if (passwordCol.length && this.lastEditCiId !== row._id) {
this.$nextTick(async () => {
for (let i = 0; i < passwordCol.length; i++) {
await getAttrPassword(row._id, passwordCol[i].attr_id).then((res) => {
this.initialPasswordValue[passwordCol[i].field] = res.value
this.passwordValue[passwordCol[i].field] = res.value
})
}
this.isContinueCloseEdit = false
await this.$refs.xTable.getVxetableRef().clearEdit()
this.isContinueCloseEdit = true
this.$nextTick(() => {
this.$refs.xTable.getVxetableRef().setEditCell(row, column.field)
})
})
}
this.lastEditCiId = row._id
})
},
getQAndSort() {
const fuzzySearch = this.$refs['search'].fuzzySearch || ''
const expression = this.$refs['search'].expression || ''
this.$refs.preferenceSearch.savePreference({ fuzzySearch, expression })
},
setParamsFromPreferenceSearch(item) {
const { fuzzySearch, expression } = item.option
this.$refs.search.fuzzySearch = fuzzySearch
this.$refs.search.expression = expression
this.selectedRowKeys = []
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
this.$refs.xTable.getVxetableRef().clearSort()
this.sortByTable = undefined
this.$nextTick(() => {
if (this.currentPage === 1) {
this.loadTableData()
} else {
this.currentPage = 1
}
})
},
setPreferenceSearchCurrent(id = null) {
this.$refs.preferenceSearch.currentPreferenceSearch = id
},
copyExpression() {
const expression = this.$refs['search'].expression || ''
const fuzzySearch = this.$refs['search'].fuzzySearch
const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
const exp = expression.match(regQ) ? expression.match(regQ)[0] : null
const text = `q=_type:${this.typeId}${exp ? `,${exp}` : ''}${fuzzySearch ? `,*${fuzzySearch}*` : ''}`
this.$copyText(text)
.then(() => {
this.$message.success(this.$t('copySuccess'))
})
.catch(() => {
this.$message.error(this.$t('cmdb.ci.copyFailed'))
})
},
unsubscribe() {
this.$confirm({
title: this.$t('warning'),
content: this.$t('cmdb.preference.confirmcancelSub2', {
name: `${this.CIType.alias || this.CIType.name}`,
}),
onOk: () => {
const promises = [subscribeCIType(this.typeId, ''), subscribeTreeView(this.typeId, '')]
Promise.all(promises).then(() => {
this.$message.success(this.$t('cmdb.preference.cancelSubSuccess'))
this.$emit('unSubscribe')
})
},
})
},
handlePerm() {
roleHasPermissionToGrant({
app_id: 'cmdb',
resource_type_name: 'CIType',
perm: 'grant',
resource_name: this.CIType.name,
}).then((res) => {
if (res.result) {
searchResourceType({ page_size: 9999, app_id: 'cmdb' }).then((res) => {
this.resource_type = { groups: res.groups, id2perms: res.id2perms }
this.$nextTick(() => {
this.$refs.cmdbGrant.open({
name: this.CIType.name,
cmdbGrantType: 'ci',
CITypeId: this.typeId,
})
})
})
} else {
this.$message.error(this.$t('noPermission'))
}
})
},
handleMenuClick(e) {
if (e.key === 'grant') {
this.visible = false
}
},
openDetail(id, activeTabKey, ciDetailRelationKey) {
this.$refs.detail.create(id, activeTabKey, ciDetailRelationKey)
}
},
}
</script>
<style lang="less">
@import '../index.less';
</style>
<style lang="less" scoped>
.cmdb-ci {
background-color: #fff;
padding: 20px;
border-radius: @border-radius-box;
height: calc(100vh - 64px);
overflow: auto;
margin-bottom: -24px;
}
</style>

View File

@@ -191,6 +191,10 @@ export default {
CIReferenceAttr
},
props: {
typeIdFromProp: {
type: Number,
default: 0,
},
typeIdFromRelation: {
type: Number,
default: 0,
@@ -235,7 +239,7 @@ export default {
if (this.typeIdFromRelation) {
return this.typeIdFromRelation
}
return this.$router.currentRoute.meta.typeId
return this.typeIdFromProp
},
valueTypeMap() {
return valueTypeMap()

View File

@@ -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>

View File

@@ -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>

View File

@@ -0,0 +1,64 @@
<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: () => []
}
},
data() {
return {
icon: '',
title: ''
}
},
computed: {
findCIType() {
return this.ci_types?.find?.((item) => item?.id === this.ci?._type) || {}
}
},
watch: {
findCIType: {
deep: true,
immediate: true,
handler(val) {
this.icon = val?.icon || ''
this.title = this?.ci?.[val?.show_name] || this?.ci?.[val?.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>

View File

@@ -0,0 +1,673 @@
<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="(group) in tabList"
:key="group.key"
class="tab-group"
>
<div
v-if="group.name"
class="tab-group-name"
>
{{ group.name }}
</div>
<div
v-for="(item) in group.list"
:key="item.key"
:class="`tab-item ${item.key === currentTab ? 'tab-item-active' : ''}`"
:style="{
paddingLeft: item.key === 'all' ? '8px' : '16px'
}"
@click="clickTab(item.key)"
>
<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.key === currentTab && item.showAdd"
class="tab-item-add"
@click="openAddModal(item)"
>
<a-icon type="plus" />
</span>
</div>
</div>
</div>
<div
class="ci-relation-table-container"
v-if="tableIDList.length"
>
<div
v-for="(item) in tableIDList"
:key="item.key"
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.value]"
:data="allCIList[item.key]"
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.value]"
:style="{
color: !allCanEdit[item.value] ? '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'
const PARENT_KEY = 'parents'
const CHILDREN_KEY = 'children'
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: {
tabListFlat() {
return this.tabList.reduce((list, group) => list.concat(group.list), [])
},
tableIDList() {
const baseKeys = this.currentTab === 'all'
? this.tabListFlat.filter(item => item.value !== 'all').map(item => item.key)
: [this.currentTab]
return baseKeys.filter((key) => this.allCIList?.[key]?.length).map((key) => {
const findTab = this.tabListFlat.find((item) => item.key === key) || {}
let name = findTab?.name || ''
if (name && findTab?.value === this.ci._type) {
name = `${findTab?.isParent ? this.$t('cmdb.ci.upstream') : this.$t('cmdb.ci.downstream')} - ${name}`
}
return {
key,
value: findTab?.value || '',
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 = _.uniqBy(
[
...cloneRelationData.parentCITypeList,
...cloneRelationData.childCITypeList
],
'id'
)
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 = []
tabList[0] = {
name: '',
key: 'all',
list: [{
name: this.$t('all'),
key: 'all',
value: 'all',
count: Object.values(this.allCIList).reduce((acc, cur) => acc + (cur?.length || 0), 0),
showAdd: false
}]
}
tabList[1] = {
name: this.$t('cmdb.ci.upstream'),
key: PARENT_KEY,
list: this.buildTabList(cloneRelationData.parentCITypeList, PARENT_KEY, true)
}
tabList[2] = {
name: this.$t('cmdb.ci.downstream'),
key: CHILDREN_KEY,
list: this.buildTabList(cloneRelationData.childCITypeList, CHILDREN_KEY, false)
}
this.tabList = tabList
this.handleReferenceCINameMap()
},
buildTabList(list, keyPrefix, isParent) {
return list.map((item) => {
const key = `${keyPrefix}-${item.id}`
return {
name: item?.alias ?? item?.name ?? '',
key,
isParent,
value: item.id,
count: this.allCIList?.[key]?.length || 0,
showAdd: this.allCanEdit?.[item.id] ?? false
}
})
},
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
const CIKey = `${isParent ? PARENT_KEY : CHILDREN_KEY}-${item._type}`
if (CIKey in cis) {
cis[CIKey].push(item)
} else {
cis[CIKey] = [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) => {
const CIKey = `${CIType.isParent ? PARENT_KEY : CHILDREN_KEY}-${CIType.id}`
CIType.attributes.forEach((attr) => {
if (attr?.is_reference && attr?.reference_type_id) {
const currentCIList = this.allCIList[CIKey]
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(key) {
this.currentTab = key
},
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,
tabData?.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;
min-height: 300px;
max-height: 600px;
overflow-y: auto;
overflow-x: hidden;
padding: 6px 0px;
border-right: solid 1px #E4E7ED;
.tab-group {
width: 100%;
&-name {
padding-left: 8px;
height: 32px;
line-height: 32px;
width: 100%;
font-weight: 600;
color: rgba(0, 0, 0, .45);
}
}
.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;
max-height: 600px;
overflow-y: auto;
overflow-x: hidden;
}
&-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>

View File

@@ -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

View File

@@ -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) {
@@ -526,10 +132,11 @@ export default {
}
const _findCiType = ci_types_list.find((item) => item.id === this.typeId)
const unique_id = _findCiType.show_id || this.attributes().unique_id
const unique_name = _findCiType.show_name || this.attributes().unique
const unique_id = _findCiType.show_id || _findCiType.unique_id
const _findUnique = this.attrList().find((attr) => attr.id === unique_id)
const unique_name = _findUnique?.name
const unique_alias = _findUnique?.alias || _findUnique?.name || ''
const nodes = {
isRoot: true,
id: `Root_${this.typeId}`,
@@ -555,7 +162,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 +205,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 +260,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>

View File

@@ -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')

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -50,13 +50,19 @@
<vxe-column :width="300" field="attributeAssociation" :edit-render="{}">
<template #header>
<span>
<a-tooltip :title="$t('cmdb.ciType.attributeAssociationTip1')">
<a-tooltip>
<template #title>
<div>{{ $t('cmdb.ciType.attributeAssociationTip1') }}</div>
<div>{{ $t('cmdb.ciType.attributeAssociationTip7') }}</div>
<div>{{ $t('cmdb.ciType.attributeAssociationTip8') }}</div>
<div>{{ $t('cmdb.ciType.attributeAssociationTip9') }}</div>
</template>
<a><a-icon type="question-circle"/></a>
</a-tooltip>
{{ $t('cmdb.ciType.attributeAssociation') }}
<span :style="{ fontSize: '10px', fontWeight: 'normal' }" class="text-color-4">{{
$t('cmdb.ciType.attributeAssociationTip2')
}}</span>
<span :style="{ fontSize: '10px', fontWeight: 'normal' }" class="text-color-4">
{{ $t('cmdb.ciType.attributeAssociationTip2') }}
</span>
</span>
</template>
<template #default="{row}">
@@ -88,7 +94,13 @@
optionFilterProp="title"
>
<a-select-option
v-for="attr in filterAttributes(row.isParent ? row.attributes : attributes)"
v-for="attr in filterAttributes(
row.isParent ? row.attributes : attributes,
item.childAttrId,
row.isParent ? attributes : row.attributes,
'parent',
row.constraint
)"
:key="attr.id"
:value="attr.id"
:title="attr.alias || attr.name"
@@ -107,7 +119,13 @@
optionFilterProp="title"
>
<a-select-option
v-for="attr in filterAttributes(row.isParent ? attributes : row.attributes)"
v-for="attr in filterAttributes(
row.isParent ? attributes : row.attributes,
item.parentAttrId,
row.isParent ? row.attributes : attributes,
'child',
row.constraint
)"
:key="attr.id"
:value="attr.id"
:title="attr.alias || attr.name"
@@ -171,19 +189,12 @@
</a-select>
</a-form-item>
<a-form-item :label="$t('cmdb.ciType.dstCIType')">
<a-select
showSearch
<CMDBTypeSelectAntd
v-decorator="['ci_type_id', { rules: [{ required: true, message: $t('cmdb.ciType.dstCITypeTips') }] }]"
name="ci_type_id"
:placeholder="$t('cmdb.ciType.dstCITypeTips')"
v-decorator="['ci_type_id', { rules: [{ required: true, message: $t('cmdb.ciType.dstCITypeTips') }] }]"
:filterOption="filterOption"
@change="changeChild"
>
<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>
</a-select>
/>
</a-form-item>
<a-form-item :label="$t('cmdb.ciType.relationType')">
@@ -207,6 +218,7 @@
'constraint',
{ rules: [{ required: true, message: $t('cmdb.ciType.relationConstraintTips') }] },
]"
@change="handleFormConstraintChange"
>
<a-select-option value="0">{{ $t('cmdb.ciType.one2Many') }}</a-select-option>
<a-select-option value="1">{{ $t('cmdb.ciType.one2One') }}</a-select-option>
@@ -214,6 +226,11 @@
</a-select>
</a-form-item>
<a-form-item :label="$t('cmdb.ciType.attributeAssociation')">
<template #extra>
<div>{{ $t('cmdb.ciType.attributeAssociationTip7') }}</div>
<div>{{ $t('cmdb.ciType.attributeAssociationTip8') }}</div>
<div>{{ $t('cmdb.ciType.attributeAssociationTip9') }}</div>
</template>
<a-row
v-for="item in modalAttrList"
:key="item.id"
@@ -225,7 +242,15 @@
allowClear
v-model="item.parentAttrId"
>
<a-select-option v-for="attr in filterAttributes(attributes)" :key="attr.id">
<a-select-option
v-for="attr in filterAttributes(
attributes,
item.childAttrId,
modalChildAttributes,
'parent'
)"
:key="attr.id"
>
{{ attr.alias || attr.name }}
</a-select-option>
</a-select>
@@ -241,7 +266,15 @@
allowClear
v-model="item.childAttrId"
>
<a-select-option v-for="attr in filterAttributes(modalChildAttributes)" :key="attr.id">
<a-select-option
v-for="attr in filterAttributes(
modalChildAttributes,
item.parentAttrId,
attributes,
'child'
)"
:key="attr.id"
>
{{ attr.alias || attr.name }}
</a-select-option>
</a-select>
@@ -281,12 +314,14 @@ import { getCITypes } from '@/modules/cmdb/api/CIType'
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
import { v4 as uuidv4 } from 'uuid'
import CMDBGrant from '../../components/cmdbGrant'
import CMDBGrant from '@/modules/cmdb/components/cmdbGrant'
import CMDBTypeSelectAntd from '@/modules/cmdb/components/cmdbTypeSelect/cmdbTypeSelectAntd'
export default {
name: 'RelationTable',
components: {
CMDBGrant,
CMDBTypeSelectAntd
},
props: {
CITypeId: {
@@ -509,9 +544,6 @@ export default {
cmdbGrantType: 'type_relation',
})
},
filterOption(input, option) {
return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
},
rowClass({ row }) {
if (row.isDivider) return 'relation-table-divider'
if (row.isParent) return 'relation-table-parent'
@@ -596,20 +628,41 @@ export default {
this.modalAttrList.forEach((item) => {
item.childAttrId = undefined
})
getCITypeAttributesById(value).then((res) => {
this.modalChildAttributes = res?.attributes ?? []
})
if (value) {
getCITypeAttributesById(value).then((res) => {
this.modalChildAttributes = res?.attributes ?? []
})
}
},
filterAttributes(attributes) {
// filter password/json/is_list/longText/bool/reference
return attributes.filter((attr) => {
filterAttributes(attributes, relationAttrId, relationAttrs, type, constraint) {
const relationAttr = relationAttrs.find((attr) => attr.id === relationAttrId)
// filter password/json/longText/bool/reference
let filterAttrs = attributes.filter((attr) => {
if (attr.value_type === '2' && !attr.is_index) {
return false
}
return !attr.is_password && !attr.is_list && attr.value_type !== '6' && !attr.is_bool && !attr.is_reference
return !attr.is_password && attr.value_type !== '6' && !attr.is_bool && !attr.is_reference
})
if (relationAttr) {
filterAttrs = filterAttrs.filter((attr) => attr.value_type === relationAttr?.value_type)
}
const constraintValue = Number(constraint ?? this.form.getFieldValue('constraint'))
if (
(constraintValue === 0 && type === 'child') ||
constraintValue === 1 ||
(constraintValue === 2 && relationAttr?.is_list)
) {
return filterAttrs.filter((attr) => !attr.is_list)
}
return filterAttrs
},
addTableAttr() {
this.tableAttrList.push({
id: uuidv4(),
@@ -645,6 +698,13 @@ export default {
if (index !== -1) {
this.modalAttrList.splice(index, 1)
}
},
handleFormConstraintChange() {
this.modalAttrList.forEach((item) => {
item.parentAttrId = undefined
item.childAttrId = undefined
})
}
},
}

View File

@@ -51,31 +51,21 @@
:label="$t('cmdb.ciType.ciType')"
prop="type_ids"
>
<a-select
show-search
optionFilterProp="children"
@change="changeCIType"
<CMDBTypeSelectAntd
v-model="form.type_ids"
:placeholder="$t('cmdb.ciType.selectCIType')"
mode="multiple"
>
<a-select-option v-for="ci_type in ci_types" :key="ci_type.id" :value="ci_type.id">{{
ci_type.alias || ci_type.name
}}</a-select-option>
</a-select>
:CITypeGroup="CITypeGroup"
:placeholder="$t('cmdb.ciType.selectCIType')"
@change="changeCIType"
/>
</a-form-model-item>
<a-form-model-item v-else :label="$t('cmdb.ciType.ciType')" prop="type_id">
<a-select
show-search
optionFilterProp="children"
@change="changeCIType"
<CMDBTypeSelectAntd
v-model="form.type_id"
:CITypeGroup="CITypeGroup"
:placeholder="$t('cmdb.ciType.selectCIType')"
>
<a-select-option v-for="ci_type in ci_types" :key="ci_type.id" :value="ci_type.id">{{
ci_type.alias || ci_type.name
}}</a-select-option>
</a-select>
@change="changeCIType"
/>
</a-form-model-item>
<a-form-model-item
:label="$t('cmdb.custom_dashboard.dimensions')"
@@ -309,17 +299,26 @@
<script>
import Chart from './chart.vue'
import { dashboardCategory } from './constant'
import { postCustomDashboard, putCustomDashboard, postCustomDashboardPreview } from '../../api/customDashboard'
import { getCITypeAttributesByTypeIds, getCITypeCommonAttributesByTypeIds } from '../../api/CITypeAttr'
import { getRecursive_level2children } from '../../api/CITypeRelation'
import { getLastLayout } from '../../utils/helper'
import { postCustomDashboard, putCustomDashboard, postCustomDashboardPreview } from '@/modules/cmdb/api/customDashboard'
import { getCITypeAttributesByTypeIds, getCITypeCommonAttributesByTypeIds } from '@/modules/cmdb/api/CITypeAttr'
import { getRecursive_level2children } from '@/modules/cmdb/api/CITypeRelation'
import { getCITypeGroupsConfig } from '@/modules/cmdb/api/ciTypeGroup'
import { getLastLayout } from '@/modules/cmdb/utils/helper'
import FilterComp from '@/components/CMDBFilterComp'
import ColorPicker from './colorPicker.vue'
import ColorListPicker from './colorListPicker.vue'
import CMDBTypeSelectAntd from '@/modules/cmdb/components/cmdbTypeSelect/cmdbTypeSelectAntd'
export default {
name: 'ChartForm',
components: { Chart, FilterComp, ColorPicker, ColorListPicker },
components: {
Chart,
FilterComp,
ColorPicker,
ColorListPicker,
CMDBTypeSelectAntd
},
props: {
ci_types: {
type: Array,
@@ -365,6 +364,7 @@ export default {
level2children: {},
isShadow: false,
changeCITypeRequestValue: null,
CITypeGroup: []
}
},
computed: {
@@ -484,6 +484,7 @@ export default {
showIcon,
tableCategory: ret === 'cis' ? 2 : 1,
}
this.getCITypeGroup()
},
handleclose() {
this.attributes = []
@@ -491,6 +492,9 @@ export default {
this.isShowPreview = false
this.visible = false
},
async getCITypeGroup() {
this.CITypeGroup = await getCITypeGroupsConfig({ need_other: true })
},
changeCIType(value) {
this.form.attr_ids = []
this.commonAttributes = []

View File

@@ -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
}
})

View File

@@ -1,79 +1,82 @@
<template>
<div ref="wrapRef">
<div class="table-header">
<SearchForm
ref="search"
:preferenceAttrList="preferenceAttrList"
:typeId="addressCITypeId"
@copyExpression="copyExpression"
@refresh="handleSearch"
<a-spin :tip="loadTip" :spinning="loading" >
<div class="table-header">
<SearchForm
ref="search"
:preferenceAttrList="preferenceAttrList"
:typeId="addressCITypeId"
:selectedRowKeys="selectedRowKeys"
@copyExpression="copyExpression"
@refresh="handleSearch"
>
<div class="ops-list-batch-action" v-show="!!selectedRowKeys.length">
<span @click="$refs.create.handleOpen(true, 'update')">{{ $t('update') }}</span>
<a-divider type="vertical" />
<span @click="openBatchDownload">{{ $t('download') }}</span>
<a-divider type="vertical" />
<span @click="batchDelete">{{ $t('delete') }}</span>
<span>{{ $t('cmdb.ci.selectRows', { rows: selectedRowKeys.length }) }}</span>
</div>
</SearchForm>
<div class="table-header-right">
<EditAttrsPopover
:typeId="addressCITypeId"
@refresh="refreshAfterEditAttrs"
>
<a-button
type="primary"
ghost
class="ops-button-ghost"
>
<ops-icon type="veops-configuration_table" />
{{ $t('cmdb.configTable') }}
</a-button>
</EditAttrsPopover>
</div>
</div>
<CITable
ref="xTable"
:loading="loading"
:attrList="preferenceAttrList"
:columns="columns"
:data="instanceList"
:height="tableHeight"
@sort-change="handleSortCol"
@openDetail="openDetail"
@deleteCI="deleteCI"
@onSelectChange="onSelectChange"
/>
<div class="table-header-right">
<EditAttrsPopover
:typeId="addressCITypeId"
@refresh="refreshAfterEditAttrs"
<div class="table-pagination">
<a-pagination
:showSizeChanger="true"
:current="page"
size="small"
:total="totalNumber"
show-quick-jumper
:page-size="pageSize"
:page-size-options="pageSizeOptions"
:show-total="
(total, range) =>
$t('pagination.total', {
range0: range[0],
range1: range[1],
total,
})
"
@change="handleChangePage"
@showSizeChange="onShowSizeChange"
>
<a-button
type="primary"
ghost
class="ops-button-ghost"
>
<ops-icon type="veops-configuration_table" />
{{ $t('cmdb.configTable') }}
</a-button>
</EditAttrsPopover>
<a-button
v-if="instanceList && instanceList.length"
type="primary"
class="ops-button-ghost"
ghost
@click="handleExport"
>
<ops-icon type="veops-export" />
{{ $t('export') }}
</a-button>
<template slot="buildOptionText" slot-scope="props">
<span v-if="props.value !== '100000'">{{ props.value }}{{ $t('itemsPerPage') }}</span>
<span v-if="props.value === '100000'">{{ $t('cmdb.ci.all') }}</span>
</template>
</a-pagination>
</div>
</div>
<CITable
ref="xTable"
:loading="loading"
:attrList="preferenceAttrList"
:columns="columns"
:data="instanceList"
:height="tableHeight"
@sort-change="handleSortCol"
@openDetail="openDetail"
@deleteCI="deleteCI"
/>
<div class="table-pagination">
<a-pagination
:showSizeChanger="true"
:current="page"
size="small"
:total="totalNumber"
show-quick-jumper
:page-size="pageSize"
:page-size-options="pageSizeOptions"
:show-total="
(total, range) =>
$t('pagination.total', {
range0: range[0],
range1: range[1],
total,
})
"
@change="handleChangePage"
@showSizeChange="onShowSizeChange"
>
<template slot="buildOptionText" slot-scope="props">
<span v-if="props.value !== '100000'">{{ props.value }}{{ $t('itemsPerPage') }}</span>
<span v-if="props.value === '100000'">{{ $t('cmdb.ci.all') }}</span>
</template>
</a-pagination>
</div>
</a-spin>
<BatchDownload
ref="batchDownload"
@@ -82,6 +85,12 @@
/>
<CIDetailDrawer ref="detail" :typeId="addressCITypeId" />
<CreateInstanceForm
ref="create"
:typeIdFromRelation="addressCITypeId"
@submit="batchUpdate"
/>
</div>
</template>
@@ -90,7 +99,7 @@ import _ from 'lodash'
import { mapState } from 'vuex'
import ExcelJS from 'exceljs'
import FileSaver from 'file-saver'
import { searchCI, deleteCI } from '@/modules/cmdb/api/ci'
import { searchCI, deleteCI, updateCI } from '@/modules/cmdb/api/ci'
import { getSubscribeAttributes } from '@/modules/cmdb/api/preference'
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
import { getCITableColumns } from '@/modules/cmdb/utils/helper'
@@ -100,6 +109,7 @@ import CITable from '@/modules/cmdb/components/ciTable/index.vue'
import BatchDownload from '@/modules/cmdb/components/batchDownload/batchDownload.vue'
import CIDetailDrawer from '@/modules/cmdb/views/ci/modules/ciDetailDrawer.vue'
import EditAttrsPopover from '@/modules/cmdb/views/ci/modules/editAttrsPopover.vue'
import CreateInstanceForm from '@/modules/cmdb/views/ci/modules/CreateInstanceForm'
export default {
name: 'IPSearch',
@@ -108,7 +118,8 @@ export default {
CITable,
BatchDownload,
CIDetailDrawer,
EditAttrsPopover
EditAttrsPopover,
CreateInstanceForm
},
props: {
addressCIType: {
@@ -122,6 +133,7 @@ export default {
pageSize: 50,
pageSizeOptions: ['50', '100', '200'],
loading: false,
loadTip: '',
sortByTable: undefined,
instanceList: [],
@@ -130,6 +142,7 @@ export default {
preferenceAttrList: [],
attrList: [],
attributes: {},
selectedRowKeys: [],
}
},
computed: {
@@ -275,7 +288,7 @@ export default {
})
},
handleExport() {
openBatchDownload() {
this.$refs.batchDownload.open({
preferenceAttrList: this.preferenceAttrList,
ciTypeName: this.$t('cmdb.ipam.ipSearch') || '',
@@ -336,6 +349,7 @@ export default {
FileSaver.saveAs(file, `${filename}.xlsx`)
})
this.selectedRowKeys = []
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
},
@@ -361,6 +375,120 @@ export default {
},
})
},
onSelectChange(records) {
this.selectedRowKeys = records.map((i) => i.ci_id || i._id)
},
batchDelete() {
this.$confirm({
title: this.$t('warning'),
content: this.$t('confirmDelete'),
onOk: () => {
this.batchDeleteAsync()
},
})
},
async batchDeleteAsync() {
let successNum = 0
let errorNum = 0
this.loading = true
this.loadTip = this.$t('cmdb.ci.batchDeleting')
const floor = Math.ceil(this.selectedRowKeys.length / 6)
for (let i = 0; i < floor; i++) {
const itemList = this.selectedRowKeys.slice(6 * i, 6 * i + 6)
const promises = itemList.map((x) => deleteCI(x, false))
await Promise.allSettled(promises)
.then((res) => {
res.forEach((r) => {
if (r.status === 'fulfilled') {
successNum += 1
} else {
errorNum += 1
}
})
})
.finally(() => {
this.loadTip = this.$t('cmdb.ci.batchDeleting2', {
total: this.selectedRowKeys.length,
successNum: successNum,
errorNum: errorNum,
})
})
}
this.loading = false
this.loadTip = ''
this.selectedRowKeys = []
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
this.$nextTick(() => {
this.page = 1
this.getTableData()
})
},
batchUpdate(values) {
this.$confirm({
title: this.$t('warning'),
content: this.$t('cmdb.ci.batchUpdateConfirm'),
onOk: () => {
this.batchUpdateAsync(values)
},
})
},
async batchUpdateAsync(values) {
let successNum = 0
let errorNum = 0
this.loading = true
this.loadTip = this.$t('cmdb.ci.batchUpdateInProgress') + '...'
const payload = {}
Object.keys(values).forEach((key) => {
if (values[key] === undefined || values[key] === null) {
payload[key] = null
} else {
payload[key] = values[key]
}
})
this.$refs.create.visible = false
const key = 'updatable'
let errorMsg = ''
for (let i = 0; i < this.selectedRowKeys.length; i++) {
await updateCI(this.selectedRowKeys[i], payload, false)
.then(() => {
successNum += 1
})
.catch((error) => {
errorMsg = errorMsg + '\n' + `${this.selectedRowKeys[i]}:${error.response?.data?.message ?? ''}`
this.$notification.warning({
key,
message: this.$t('warning'),
description: errorMsg,
duration: 0,
style: { whiteSpace: 'break-spaces', overflow: 'auto', maxHeight: this.windowHeight - 80 + 'px' },
})
errorNum += 1
})
.finally(() => {
this.loadTip = this.$t('cmdb.ci.batchUpdateInProgress2', {
total: this.selectedRowKeys.length,
successNum: successNum,
errorNum: errorNum,
})
})
}
this.loading = false
this.loadTip = ''
this.selectedRowKeys = []
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
this.getTableData()
},
}
}
</script>

View File

@@ -1,79 +1,82 @@
<template>
<div ref="wrapRef">
<div class="table-header">
<SearchForm
ref="search"
:preferenceAttrList="preferenceAttrList"
:typeId="subnetCITypeId"
@copyExpression="copyExpression"
@refresh="handleSearch"
<a-spin :tip="loadTip" :spinning="loading" >
<div class="table-header">
<SearchForm
ref="search"
:preferenceAttrList="preferenceAttrList"
:typeId="subnetCITypeId"
:selectedRowKeys="selectedRowKeys"
@copyExpression="copyExpression"
@refresh="handleSearch"
>
<div class="ops-list-batch-action" v-show="!!selectedRowKeys.length">
<span @click="$refs.create.handleOpen(true, 'update')">{{ $t('update') }}</span>
<a-divider type="vertical" />
<span @click="openBatchDownload">{{ $t('download') }}</span>
<a-divider type="vertical" />
<span @click="batchDelete">{{ $t('delete') }}</span>
<span>{{ $t('cmdb.ci.selectRows', { rows: selectedRowKeys.length }) }}</span>
</div>
</SearchForm>
<div class="table-header-right">
<EditAttrsPopover
:typeId="subnetCITypeId"
@refresh="refreshAfterEditAttrs"
>
<a-button
type="primary"
ghost
class="ops-button-ghost"
>
<ops-icon type="veops-configuration_table" />
{{ $t('cmdb.configTable') }}
</a-button>
</EditAttrsPopover>
</div>
</div>
<CITable
ref="xTable"
:loading="loading"
:attrList="preferenceAttrList"
:columns="columns"
:data="instanceList"
:height="tableHeight"
@sort-change="handleSortCol"
@openDetail="openDetail"
@deleteCI="deleteCI"
@onSelectChange="onSelectChange"
/>
<div class="table-header-right">
<EditAttrsPopover
:typeId="subnetCITypeId"
@refresh="refreshAfterEditAttrs"
<div class="table-pagination">
<a-pagination
:showSizeChanger="true"
:current="page"
size="small"
:total="totalNumber"
show-quick-jumper
:page-size="pageSize"
:page-size-options="pageSizeOptions"
:show-total="
(total, range) =>
$t('pagination.total', {
range0: range[0],
range1: range[1],
total,
})
"
@change="handleChangePage"
@showSizeChange="onShowSizeChange"
>
<a-button
type="primary"
ghost
class="ops-button-ghost"
>
<ops-icon type="veops-configuration_table" />
{{ $t('cmdb.configTable') }}
</a-button>
</EditAttrsPopover>
<a-button
v-if="instanceList && instanceList.length"
type="primary"
class="ops-button-ghost"
ghost
@click="handleExport"
>
<ops-icon type="veops-export" />
{{ $t('export') }}
</a-button>
<template slot="buildOptionText" slot-scope="props">
<span v-if="props.value !== '100000'">{{ props.value }}{{ $t('itemsPerPage') }}</span>
<span v-if="props.value === '100000'">{{ $t('cmdb.ci.all') }}</span>
</template>
</a-pagination>
</div>
</div>
<CITable
ref="xTable"
:loading="loading"
:attrList="preferenceAttrList"
:columns="columns"
:data="instanceList"
:height="tableHeight"
@sort-change="handleSortCol"
@openDetail="openDetail"
@deleteCI="deleteCI"
/>
<div class="table-pagination">
<a-pagination
:showSizeChanger="true"
:current="page"
size="small"
:total="totalNumber"
show-quick-jumper
:page-size="pageSize"
:page-size-options="pageSizeOptions"
:show-total="
(total, range) =>
$t('pagination.total', {
range0: range[0],
range1: range[1],
total,
})
"
@change="handleChangePage"
@showSizeChange="onShowSizeChange"
>
<template slot="buildOptionText" slot-scope="props">
<span v-if="props.value !== '100000'">{{ props.value }}{{ $t('itemsPerPage') }}</span>
<span v-if="props.value === '100000'">{{ $t('cmdb.ci.all') }}</span>
</template>
</a-pagination>
</div>
</a-spin>
<BatchDownload
ref="batchDownload"
@@ -82,6 +85,12 @@
/>
<CIDetailDrawer ref="detail" :typeId="subnetCITypeId" />
<CreateInstanceForm
ref="create"
:typeIdFromRelation="subnetCITypeId"
@submit="batchUpdate"
/>
</div>
</template>
@@ -90,7 +99,7 @@ import _ from 'lodash'
import { mapState } from 'vuex'
import ExcelJS from 'exceljs'
import FileSaver from 'file-saver'
import { searchCI, deleteCI } from '@/modules/cmdb/api/ci'
import { searchCI, deleteCI, updateCI } from '@/modules/cmdb/api/ci'
import { getSubscribeAttributes } from '@/modules/cmdb/api/preference'
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
import { getCITableColumns } from '@/modules/cmdb/utils/helper'
@@ -100,6 +109,7 @@ import CITable from '@/modules/cmdb/components/ciTable/index.vue'
import BatchDownload from '@/modules/cmdb/components/batchDownload/batchDownload.vue'
import CIDetailDrawer from '@/modules/cmdb/views/ci/modules/ciDetailDrawer.vue'
import EditAttrsPopover from '@/modules/cmdb/views/ci/modules/editAttrsPopover.vue'
import CreateInstanceForm from '@/modules/cmdb/views/ci/modules/CreateInstanceForm'
export default {
name: 'SubnetList',
@@ -108,7 +118,8 @@ export default {
CITable,
BatchDownload,
CIDetailDrawer,
EditAttrsPopover
EditAttrsPopover,
CreateInstanceForm
},
props: {
subnetCIType: {
@@ -122,6 +133,7 @@ export default {
pageSize: 50,
pageSizeOptions: ['50', '100', '200'],
loading: false,
loadTip: '',
sortByTable: undefined,
instanceList: [],
@@ -130,6 +142,7 @@ export default {
preferenceAttrList: [],
attrList: [],
attributes: {},
selectedRowKeys: [],
}
},
computed: {
@@ -275,7 +288,7 @@ export default {
})
},
handleExport() {
openBatchDownload() {
this.$refs.batchDownload.open({
preferenceAttrList: this.preferenceAttrList,
ciTypeName: this.$t('cmdb.ipam.subnetList') || '',
@@ -336,6 +349,7 @@ export default {
FileSaver.saveAs(file, `${filename}.xlsx`)
})
this.selectedRowKeys = []
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
},
@@ -362,6 +376,120 @@ export default {
},
})
},
onSelectChange(records) {
this.selectedRowKeys = records.map((i) => i.ci_id || i._id)
},
batchDelete() {
this.$confirm({
title: this.$t('warning'),
content: this.$t('confirmDelete'),
onOk: () => {
this.batchDeleteAsync()
},
})
},
async batchDeleteAsync() {
let successNum = 0
let errorNum = 0
this.loading = true
this.loadTip = this.$t('cmdb.ci.batchDeleting')
const floor = Math.ceil(this.selectedRowKeys.length / 6)
for (let i = 0; i < floor; i++) {
const itemList = this.selectedRowKeys.slice(6 * i, 6 * i + 6)
const promises = itemList.map((x) => deleteCI(x, false))
await Promise.allSettled(promises)
.then((res) => {
res.forEach((r) => {
if (r.status === 'fulfilled') {
successNum += 1
} else {
errorNum += 1
}
})
})
.finally(() => {
this.loadTip = this.$t('cmdb.ci.batchDeleting2', {
total: this.selectedRowKeys.length,
successNum: successNum,
errorNum: errorNum,
})
})
}
this.loading = false
this.loadTip = ''
this.selectedRowKeys = []
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
this.$nextTick(() => {
this.page = 1
this.getTableData()
})
},
batchUpdate(values) {
this.$confirm({
title: this.$t('warning'),
content: this.$t('cmdb.ci.batchUpdateConfirm'),
onOk: () => {
this.batchUpdateAsync(values)
},
})
},
async batchUpdateAsync(values) {
let successNum = 0
let errorNum = 0
this.loading = true
this.loadTip = this.$t('cmdb.ci.batchUpdateInProgress') + '...'
const payload = {}
Object.keys(values).forEach((key) => {
if (values[key] === undefined || values[key] === null) {
payload[key] = null
} else {
payload[key] = values[key]
}
})
this.$refs.create.visible = false
const key = 'updatable'
let errorMsg = ''
for (let i = 0; i < this.selectedRowKeys.length; i++) {
await updateCI(this.selectedRowKeys[i], payload, false)
.then(() => {
successNum += 1
})
.catch((error) => {
errorMsg = errorMsg + '\n' + `${this.selectedRowKeys[i]}:${error.response?.data?.message ?? ''}`
this.$notification.warning({
key,
message: this.$t('warning'),
description: errorMsg,
duration: 0,
style: { whiteSpace: 'break-spaces', overflow: 'auto', maxHeight: this.windowHeight - 80 + 'px' },
})
errorNum += 1
})
.finally(() => {
this.loadTip = this.$t('cmdb.ci.batchUpdateInProgress2', {
total: this.selectedRowKeys.length,
successNum: successNum,
errorNum: errorNum,
})
})
}
this.loading = false
this.loadTip = ''
this.selectedRowKeys = []
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
this.getTableData()
},
}
}
</script>

View File

@@ -14,35 +14,26 @@
>
<a-form :form="form" @submit="handleSubmit" :label-col="{ span: 6 }" :wrapper-col="{ span: 14 }">
<a-form-item :label="$t('cmdb.ciType.sourceCIType')">
<a-select
showSearch
name="source_ci_type_id"
<CMDBTypeSelectAntd
v-decorator="[
'source_ci_type_id',
{ rules: [{ required: true, message: $t('cmdb.ciType.sourceCITypeTips') }] },
]"
name="source_ci_type_id"
:CITypeGroup="CITypeGroups"
@change="handleSourceTypeChange"
:filterOption="filterOption"
>
<a-select-option :value="CIType.id" :key="CIType.id" v-for="CIType in displayCITypes">
{{ CIType.alias || CIType.name }}
<span class="model-select-name">({{ CIType.name }})</span>
</a-select-option>
</a-select>
/>
</a-form-item>
<a-form-item :label="$t('cmdb.ciType.dstCIType')">
<a-select
showSearch
<CMDBTypeSelectAntd
v-decorator="[
'ci_type_id',
{ rules: [{ required: true, message: $t('cmdb.ciType.dstCITypeTips') }] },
]"
name="ci_type_id"
v-decorator="['ci_type_id', { rules: [{ required: true, message: $t('cmdb.ciType.dstCITypeTips') }] }]"
:CITypeGroup="CITypeGroups"
@change="handleTargetTypeChange"
:filterOption="filterOption"
>
<a-select-option :value="CIType.id" :key="CIType.id" v-for="CIType in displayTargetCITypes">
{{ CIType.alias || CIType.name }}
<span class="model-select-name">({{ CIType.name }})</span>
</a-select-option>
</a-select>
/>
</a-form-item>
<a-form-item :label="$t('cmdb.ciType.relationType')">
@@ -65,6 +56,7 @@
'constraint',
{ rules: [{ required: true, message: $t('cmdb.ciType.relationConstraintTips') }] },
]"
@change="handleConstraintChange"
>
<a-select-option value="0">{{ $t('cmdb.ciType.one2Many') }}</a-select-option>
<a-select-option value="1">{{ $t('cmdb.ciType.one2One') }}</a-select-option>
@@ -72,6 +64,11 @@
</a-select>
</a-form-item>
<a-form-item :label="$t('cmdb.ciType.attributeAssociation')">
<template #extra>
<div>{{ $t('cmdb.ciType.attributeAssociationTip7') }}</div>
<div>{{ $t('cmdb.ciType.attributeAssociationTip8') }}</div>
<div>{{ $t('cmdb.ciType.attributeAssociationTip9') }}</div>
</template>
<a-row
v-for="item in modalAttrList"
:key="item.id"
@@ -83,7 +80,10 @@
allowClear
v-model="item.parentAttrId"
>
<a-select-option v-for="attr in filterAttributes(modalParentAttributes)" :key="attr.id">
<a-select-option
v-for="attr in filterAttributes(modalParentAttributes, item.childAttrId, modalChildAttributes, 'parent')"
:key="attr.id"
>
{{ attr.alias || attr.name }}
</a-select-option>
</a-select>
@@ -99,7 +99,10 @@
allowClear
v-model="item.childAttrId"
>
<a-select-option v-for="attr in filterAttributes(modalChildAttributes)" :key="attr.id">
<a-select-option
v-for="attr in filterAttributes(modalChildAttributes, item.parentAttrId, modalParentAttributes, 'child')"
:key="attr.id"
>
{{ attr.alias || attr.name }}
</a-select-option>
</a-select>
@@ -127,7 +130,6 @@
</template>
<script>
import ModelRelationTable from './modules/modelRelationTable.vue'
import { searchResourceType } from '@/modules/acl/api/resource'
import { getCITypeGroupsConfig } from '@/modules/cmdb/api/ciTypeGroup'
import { getCITypes } from '@/modules/cmdb/api/CIType'
@@ -135,10 +137,14 @@ import { createRelation, deleteRelation, getRelationTypes } from '@/modules/cmdb
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
import { v4 as uuidv4 } from 'uuid'
import ModelRelationTable from './modules/modelRelationTable.vue'
import CMDBTypeSelectAntd from '@/modules/cmdb/components/cmdbTypeSelect/cmdbTypeSelectAntd'
export default {
name: 'Index',
components: {
ModelRelationTable,
CMDBTypeSelectAntd
},
data() {
return {
@@ -189,7 +195,7 @@ export default {
'1': this.$t('cmdb.ciType.one2One'),
'2': this.$t('cmdb.ciType.many2Many'),
}
},
}
},
provide() {
return {
@@ -362,18 +368,33 @@ export default {
this.modalChildAttributes = res?.attributes ?? []
})
},
filterOption(input, option) {
return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
},
filterAttributes(attributes) {
// filter password/json/is_list/longText/bool/reference
return attributes.filter((attr) => {
filterAttributes(attributes, relationAttrId, relationAttrs, type) {
const relationAttr = relationAttrs.find((attr) => attr.id === relationAttrId)
// filter password/json/longText/bool/reference
let filterAttrs = attributes.filter((attr) => {
if (attr.value_type === '2' && !attr.is_index) {
return false
}
return !attr.is_password && !attr.is_list && attr.value_type !== '6' && !attr.is_bool && !attr.is_reference
return !attr.is_password && attr.value_type !== '6' && !attr.is_bool && !attr.is_reference
})
if (relationAttr) {
filterAttrs = filterAttrs.filter((attr) => attr.value_type === relationAttr?.value_type)
}
const constraintValue = Number(this.form.getFieldValue('constraint'))
if (
(constraintValue === 0 && type === 'child') ||
constraintValue === 1 ||
(constraintValue === 2 && relationAttr?.is_list)
) {
return filterAttrs.filter((attr) => !attr.is_list)
}
return filterAttrs
},
addModalAttr() {
@@ -393,6 +414,13 @@ export default {
if (index !== -1) {
this.modalAttrList.splice(index, 1)
}
},
handleConstraintChange() {
this.modalAttrList.forEach((item) => {
item.parentAttrId = undefined
item.childAttrId = undefined
})
}
},
}

View File

@@ -15,7 +15,7 @@
@edit-closed="handleEditClose"
@edit-actived="handleEditActived"
>
<vxe-column field="created_at" :title="$t('created_at')" sortable width="159px"></vxe-column>
<vxe-column field="created_at" :title="$t('created_at')" sortable width="170"></vxe-column>
<vxe-column field="parent.alias" :title="$t('cmdb.ciType.sourceCIType')"></vxe-column>
<vxe-column
field="relation_type_id"
@@ -38,7 +38,13 @@
<vxe-column :width="300" field="attributeAssociation" :edit-render="{}">
<template #header>
<span>
<a-tooltip :title="$t('cmdb.ciType.attributeAssociationTip1')">
<a-tooltip>
<template #title>
<div>{{ $t('cmdb.ciType.attributeAssociationTip1') }}</div>
<div>{{ $t('cmdb.ciType.attributeAssociationTip7') }}</div>
<div>{{ $t('cmdb.ciType.attributeAssociationTip8') }}</div>
<div>{{ $t('cmdb.ciType.attributeAssociationTip9') }}</div>
</template>
<a><a-icon type="question-circle"/></a>
</a-tooltip>
{{ $t('cmdb.ciType.attributeAssociation') }}
@@ -76,7 +82,7 @@
optionFilterProp="title"
>
<a-select-option
v-for="attr in filterAttributes(type2attributes[row.parent_id])"
v-for="attr in filterAttributes(row, item.childAttrId, 'parent')"
:key="attr.id"
:value="attr.id"
:title="attr.alias || attr.name"
@@ -95,7 +101,7 @@
optionFilterProp="title"
>
<a-select-option
v-for="attr in filterAttributes(type2attributes[row.child_id])"
v-for="attr in filterAttributes(row, item.parentAttrId, 'child')"
:key="attr.id"
:value="attr.id"
:title="attr.alias || attr.name"
@@ -298,15 +304,37 @@ export default {
const _find = attributes.find((attr) => attr.id === id)
return _find?.alias ?? _find?.name ?? id
},
filterAttributes(attributes) {
// filter password/json/is_list/longText/bool/reference
return attributes.filter((attr) => {
filterAttributes(row, relationAttrId, type) {
const { parent_id, child_id, constraint } = row
const currentAttrs = this.type2attributes?.[child_id] || []
const relationAttrs = this.type2attributes?.[parent_id] || []
const relationAttr = relationAttrs.find((attr) => attr.id === relationAttrId)
// filter password/json/longText/bool/reference
let filterAttrs = currentAttrs.filter((attr) => {
if (attr.value_type === '2' && !attr.is_index) {
return false
}
return !attr.is_password && !attr.is_list && attr.value_type !== '6' && !attr.is_bool && !attr.is_reference
return !attr.is_password && attr.value_type !== '6' && !attr.is_bool && !attr.is_reference
})
if (relationAttr) {
filterAttrs = filterAttrs.filter((attr) => attr.value_type === relationAttr?.value_type)
}
const constraintValue = Number(constraint)
if (
(constraintValue === 0 && type === 'child') ||
constraintValue === 1 ||
(constraintValue === 2 && relationAttr?.is_list)
) {
return filterAttrs.filter((attr) => !attr.is_list)
}
return filterAttrs
},
addTableAttr() {

View File

@@ -0,0 +1,177 @@
<template>
<a-modal
:title="$t('cmdb.preference.autoSub')"
:visible="visible"
:width="600"
@cancel="handleCancel"
@ok="handleOk"
>
<a-form-model
ref="autuSubFormRef"
:model="form"
:rules="rules"
:label-col="{ span: 7 }"
:wrapper-col="{ span: 15 }"
>
<a-form-model-item
:label="$t('cmdb.preference.autoSubScope')"
prop="base_strategy"
>
<a-radio-group
v-model="form.base_strategy"
:options="baseStrategyOptions"
/>
</a-form-model-item>
<a-form-model-item
:label="form.base_strategy === 'all' ? $t('cmdb.preference.excludeGroup') : $t('cmdb.preference.selectGroup')"
prop="group_ids"
>
<a-select
v-model="form.group_ids"
mode="multiple"
optionFilterProp="title"
:options="groupSelectOptions"
/>
</a-form-model-item>
<a-form-model-item
:label="form.base_strategy === 'all' ? $t('cmdb.preference.excludeModel') : $t('cmdb.preference.selectModel')"
prop="type_ids"
>
<a-select
v-model="form.type_ids"
mode="multiple"
optionFilterProp="title"
>
<a-select-opt-group
v-for="(group) in modelSelectOptions"
:key="group.value"
:title="group.label"
>
<span slot="label">{{ group.label }}</span>
<a-select-option
v-for="(type) in group.children"
:key="type.value"
:value="type.value"
:title="type.label"
>
{{ type.label }}
</a-select-option>
</a-select-opt-group>
</a-select>
</a-form-model-item>
<a-form-model-item
:label="$t('cmdb.preference.isEnable')"
prop="enabled"
:extra="$t('cmdb.preference.enableAutoSubTip')"
>
<a-switch v-model="form.enabled" />
</a-form-model-item>
</a-form-model>
</a-modal>
</template>
<script>
import { putAutoSubscription } from '@/modules/cmdb/api/preference.js'
export default {
name: 'AutoSubscribe',
props: {
ciType: {
type: Array,
default: () => []
},
autoSub: {
type: Object,
default: () => {}
}
},
data() {
return {
visible: false,
form: {
base_strategy: 'all',
group_ids: [],
type_ids: [],
enabled: true,
},
rules: {
base_strategy: [{ required: true, message: this.$t('placeholder2') }],
},
baseStrategyOptions: [
{
label: this.$t('cmdb.preference.subscribeAllModel'),
value: 'all'
},
{
label: this.$t('cmdb.preference.selectiveSubscription'),
value: 'none'
}
],
groupSelectOptions: [],
modelSelectOptions: []
}
},
methods: {
async open() {
this.form = {
base_strategy: this.autoSub?.base_strategy || 'all',
group_ids: this.autoSub?.group_ids || [],
type_ids: this.autoSub?.type_ids || [],
enabled: this.autoSub?.enabled ?? true
}
this.groupSelectOptions = this.ciType.map((group) => {
return {
label: group.name,
title: group.name,
value: group.id
}
})
const modelSelectOptions = this.ciType.filter((group) => group?.ci_types?.length)
this.modelSelectOptions = modelSelectOptions.map((group) => {
return {
label: group.name,
value: group.id,
children: group.ci_types.map((type) => {
return {
label: type.alias || type.name,
value: type.id
}
})
}
})
this.visible = true
},
handleCancel() {
this.visible = false
},
handleOk() {
this.$refs.autuSubFormRef.validate(async (valid) => {
if (valid) {
const { base_strategy, group_ids, type_ids, enabled } = this.form
const params = {
base_strategy: base_strategy,
group_ids: group_ids.join(','),
type_ids: type_ids.join(','),
enabled: enabled
}
putAutoSubscription(params).then(() => {
this.$message.success(this.$t('saveSuccess'))
this.handleCancel()
this.$emit('ok')
})
}
})
}
}
}
</script>
<style lang="less" scoped>
</style>

View File

@@ -86,13 +86,15 @@
</div>
<span class="cmdb-preference-group-content-title">{{ ciType.alias || ciType.name }}</span>
<span class="cmdb-preference-group-content-action">
<a-tooltip :title="$t('cmdb.preference.cancelSub')">
<span
@click="unsubscribe(ciType, group.type)"
><ops-icon type="cmdb-preference-cancel-subscribe" />
</span>
</a-tooltip>
<a-divider type="vertical" :style="{ margin: '0 3px' }" />
<template v-if="!enableAutoSub || subType.type === 'tree'">
<a-tooltip :title="$t('cmdb.preference.cancelSub')">
<span
@click="unsubscribe(ciType, group.type)"
><ops-icon type="cmdb-preference-cancel-subscribe" />
</span>
</a-tooltip>
<a-divider type="vertical" :style="{ margin: '0 3px' }" />
</template>
<a-tooltip :title="$t('cmdb.preference.editSub')">
<span
@click="openSubscribeSetting(ciType, `${index + 1}`)"
@@ -108,11 +110,23 @@
</div>
</div>
<div class="cmdb-preference-right">
<a-input-search
v-model="searchValue"
:style="{ width: '300px', marginBottom: '20px' }"
:placeholder="$t('cmdb.preference.searchPlaceholder')"
/>
<div class="cmdb-preference-right-header">
<a-input-search
v-model="searchValue"
class="cmdb-preference-right-header-search"
:placeholder="$t('cmdb.preference.searchPlaceholder')"
/>
<div
:class="[
'cmdb-preference-right-header-auto',
enableAutoSub ? 'cmdb-preference-right-header-auto_enable' : ''
]"
@click="openAutoSubModal"
>
<ops-icon type="auto" />
<span>{{ enableAutoSub ? $t('cmdb.preference.autoSub') : $t('cmdb.preference.autoSub2') }}</span>
</div>
</div>
<div v-for="group in filterCiTypeData" :key="group.id">
<p
@click="changeGroupExpand(group)"
@@ -154,14 +168,6 @@
{{ item.alias || item.name }}</span
>
</div>
<div class="cmdb-preference-colleague">
<span
v-if="type_id2users[item.id] && type_id2users[item.id].length"
>{{ type_id2users[item.id].length > 99 ? '99+' : type_id2users[item.id].length
}}{{ $t('cmdb.preference.peopleSub') }}</span
>
<span v-else>{{ $t('cmdb.preference.noSub') }}</span>
</div>
<div class="cmdb-preference-progress">
<div class="cmdb-preference-progress-info">
<span>{{ $t('cmdb.menu.ad') }}</span>
@@ -173,11 +179,20 @@
</div>
<a-divider :style="{ margin: '10px 0 3px 0' }" />
<div class="cmdb-preference-footor-subscribed" v-if="item.is_subscribed">
<span><a-icon type="clock-circle" :style="{ marginRight: '3px' }" />{{ getsubscribedDays(item) }}</span>
<span
:style="{
opacity: enableAutoSub ? 0 : 1
}"
>
<a-icon type="clock-circle" :style="{ marginRight: '3px' }" />{{ getsubscribedDays(item) }}
</span>
<span>
<a-tooltip :title="$t('cmdb.preference.cancelSub')">
<span @click="unsubscribe(item)"><ops-icon type="cmdb-preference-cancel-subscribe" /> </span>
</a-tooltip>
<template v-if="!enableAutoSub">
<a-tooltip :title="$t('cmdb.preference.cancelSub')">
<span @click="unsubscribe(item)"><ops-icon type="cmdb-preference-cancel-subscribe" /> </span>
</a-tooltip>
<a-divider type="vertical" :style="{ margin: '0 3px' }" />
</template>
<a-divider type="vertical" :style="{ margin: '0 3px' }" />
<a-tooltip :title="$t('cmdb.preference.editSub')">
<span @click="openSubscribeSetting(item)"><ops-icon type="cmdb-preference-subscribe"/></span>
@@ -185,13 +200,15 @@
</span>
</div>
<div v-else class="cmdb-preference-footor-unsubscribed">
<a
@click="handleSubscribeCIType(item)"
class="cmdb-preference-footor-unsubscribed-item"
>
<ops-icon type="cmdb-ci" />{{ $t('cmdb.preference.subCITable') }}
</a>
<span class="cmdb-preference-footor-unsubscribed-gap"></span>
<template v-if="!enableAutoSub">
<a
@click="handleSubscribeCIType(item)"
class="cmdb-preference-footor-unsubscribed-item"
>
<ops-icon type="cmdb-ci" />{{ $t('cmdb.preference.subCITable') }}
</a>
<span class="cmdb-preference-footor-unsubscribed-gap"></span>
</template>
<a
@click="openSubscribeSetting(item, '2')"
class="cmdb-preference-footor-unsubscribed-item"
@@ -209,17 +226,21 @@
ref="subscribeSetting"
@reload="
() => {
resetRoute()
initData()
}
"
/>
<AutoSubscribe
ref="autoSubRef"
:ciType="citypeData"
:autoSub="autoSub"
@ok="initData"
/>
</div>
</template>
<script>
import _ from 'lodash'
import router, { resetRouter } from '@/router'
import store from '@/store'
import { mapState } from 'vuex'
import moment from 'moment'
import draggable from 'vuedraggable'
@@ -238,10 +259,12 @@ import SubscribeSetting from '../../components/subscribeSetting/subscribeSetting
import { getCIAdcStatistics } from '../../api/ci'
import { ops_move_icon as OpsMoveIcon } from '@/core/icons'
import { SUB_NET_CITYPE_NAME, SCOPE_CITYPE_NAME, ADDRESS_CITYPE_NAME } from '../ipam/constants.js'
import AutoSubscribe from './components/autoSubscribe.vue'
import { getAutoSubscription } from '@/modules/cmdb/api/preference.js'
export default {
name: 'Preference',
components: { CollapseTransition, SubscribeSetting, draggable, OpsMoveIcon, Ellipsis },
components: { CollapseTransition, SubscribeSetting, draggable, OpsMoveIcon, Ellipsis, AutoSubscribe },
data() {
return {
citypeData: [],
@@ -253,6 +276,7 @@ export default {
type_id2users: {},
myPreferences: [],
searchValue: '',
autoSub: {}
}
},
computed: {
@@ -275,11 +299,19 @@ export default {
}
return this.citypeData
},
enableAutoSub() {
return this?.autoSub?.enabled ?? false
}
},
mounted() {
this.getCITypes(true)
this.getAutoSubscription()
},
methods: {
initData() {
this.getCITypes()
this.getAutoSubscription()
},
async getCITypes(isInit = false) {
const [ciTypeGroup, pref, pref2, statistics] = await Promise.all([
getCITypeGroups({ need_other: true }),
@@ -350,6 +382,12 @@ export default {
}, 300)
}
},
async getAutoSubscription() {
const res = await getAutoSubscription()
this.autoSub = res || {}
},
getsubscribedDays(item) {
const subscribedTime = this.self.type_id2subs_time[item.id]
moment.duration(moment().diff(moment(subscribedTime)))
@@ -396,21 +434,11 @@ export default {
}
}
that.$message.success(that.$t('cmdb.preference.cancelSubSuccess'))
that.resetRoute()
that.initData()
})
},
})
},
resetRoute() {
const roles = store.getters.roles
store.dispatch('GenerateRoutes', { roles }, { root: true }).then(() => {
resetRouter()
this.$nextTick(() => {
router.addRoutes(store.getters.appRoutes)
this.getCITypes()
})
})
},
async handleSubscribeCIType(ciType) {
try {
@@ -433,7 +461,7 @@ export default {
subscribeList
)
this.$message.success(this.$t('cmdb.components.subSuccess'))
this.resetRoute()
this.initData()
} catch (error) {
console.error('handleSubscribeCIType failed', error)
this.$message.success(this.$t('cmdb.components.subFailed'))
@@ -461,7 +489,7 @@ export default {
})
preferenceCitypeOrder({ type_ids: typeIds, is_tree: false })
.then(() => {
this.resetRoute()
this.initData()
})
.catch(() => {
this.getCITypes(false)
@@ -487,13 +515,17 @@ export default {
preferenceCitypeOrder({ type_ids: typeIds, is_tree: isTree })
.then(() => {
if (!isTree) {
this.resetRoute()
this.initData()
}
})
.catch(() => {
this.getCITypes(false)
})
},
openAutoSubModal() {
this.$refs.autoSubRef.open()
}
},
}
</script>
@@ -629,6 +661,45 @@ export default {
height: 100%;
padding-top: 24px;
&-header {
margin-bottom: 20px;
display: flex;
align-items: center;
&-search {
width: 300px;
margin-right: 14px;
}
&-auto {
background: linear-gradient(90deg, #16D9E3 0%, #30C7EC 47%, #46AEF7 100%);
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
color: #FFFFFF;
cursor: pointer;
padding: 0 12px;
opacity: 0.5;
transition: opacity 0.2s;
span {
margin-left: 4px;
font-size: 14px;
font-weight: 600;
}
&_enable {
opacity: 1;
}
&:hover {
opacity: 1;
}
}
}
&-group-title {
width: 300px;
margin-bottom: 20px;
@@ -651,7 +722,7 @@ export default {
.cmdb-preference-type {
display: inline-block;
width: 195px;
height: 155px;
height: 127px;
border-radius: @border-radius-box;
background-color: #fff;
box-shadow: ~'0px 2px 8px @{primary-color}15';

View File

@@ -30,13 +30,13 @@
>新增</a-button
>
</SearchForm>
<vxe-table
<ops-table
ref="xTable"
row-id="_id"
:data="tableData"
:height="tableHeight"
highlight-hover-row
:checkbox-config="{ reserve: true }"
:checkbox-config="{ reserve: true, highlight: true, range: true }"
@checkbox-change="onSelectChange"
@checkbox-all="onSelectChange"
show-overflow="tooltip"
@@ -76,7 +76,7 @@
<span v-if="col.value_type == '6' && row[col.field]">{{ JSON.stringify(row[col.field]) }}</span>
</template>
</vxe-table-column>
</vxe-table>
</ops-table>
<a-pagination
v-model="currentPage"
size="small"
@@ -216,7 +216,7 @@ export default {
this.totalNumber = res.numfound
this.columns = this.getColumns(res.result, this.preferenceAttrList)
this.$nextTick(() => {
const _table = this.$refs.xTable
const _table = this.$refs.xTable?.getVxetableRef?.()
if (_table) {
_table.refreshColumn()
}
@@ -316,7 +316,11 @@ export default {
onSelectChange() {},
handleClose() {
this.$refs.xTable.clearCheckboxRow()
const _table = this.$refs.xTable?.getVxetableRef?.()
if (_table) {
_table.clearCheckboxRow()
}
this.currentPage = 1
this.expression = ''
this.isFocusExpression = false
@@ -324,8 +328,10 @@ export default {
this.showCreateBtn = true
},
async handleOk() {
const selectRecordsCurrent = this.$refs.xTable.getCheckboxRecords()
const selectRecordsReserved = this.$refs.xTable.getCheckboxReserveRecords()
const _table = this.$refs.xTable?.getVxetableRef?.()
const selectRecordsCurrent = _table?.getCheckboxRecords?.() || []
const selectRecordsReserved = _table?.getCheckboxReserveRecords?.() || []
const ciIds = [...selectRecordsCurrent, ...selectRecordsReserved].map((record) => record._id)
if (ciIds.length) {
if (this.type === 'children') {

View File

@@ -238,9 +238,11 @@
/>
</a-form-item>
<a-form-item :label="$t('cmdb.topo.centralNodeType')" prop="central_node_type">
<a-select @change="CITypeChange" v-decorator="['central_node_type', { rules: [{ required: true, message: $t('cmdb.topo.typeRequired') }]}]" :showSearch="true" optionFilterProp="label">
<a-select-option v-for="t in ciTypes" :key="t.id" :value="t.id" :label="t.alias">{{ t.alias }}</a-select-option>
</a-select>
<CMDBTypeSelectAntd
v-decorator="['central_node_type', { rules: [{ required: true, message: $t('cmdb.topo.typeRequired') }]}]"
:placeholder="$t('cmdb.ciType.selectModel')"
@change="CITypeChange"
/>
</a-form-item>
<a-form-item :label="$t('cmdb.topo.filterInstances')" prop="central_node_instances">
<a-input
@@ -325,7 +327,6 @@ import draggable from 'vuedraggable'
import emptyImage from '@/assets/data_empty.png'
import SplitPane from '@/components/SplitPane'
import { ops_move_icon as OpsMoveIcon } from '@/core/icons'
import { getCITypeGroups } from '@/modules/cmdb/api/ciTypeGroup'
import { roleHasPermissionToGrant } from '@/modules/acl/api/permission'
import { searchResourceType } from '@/modules/acl/api/resource'
import SeeksRelationGraph from '@/modules/cmdb/3rd/relation-graph'
@@ -336,6 +337,7 @@ import { searchCI } from '@/modules/cmdb/api/ci'
import { getTopoGroups, postTopoGroup, putTopoGroupByGId, putTopoGroupsOrder, deleteTopoGroup, getTopoView, addTopoView, updateTopoView, deleteTopoView, getRelationsByTypeId, previewTopoView, showTopoView } from '@/modules/cmdb/api/topology'
import CMDBExprDrawer from '@/components/CMDBExprDrawer'
import { v4 as uuidv4 } from 'uuid'
import CMDBTypeSelectAntd from '@/modules/cmdb/components/cmdbTypeSelect/cmdbTypeSelectAntd'
const currentTopoKey = 'ops_cmdb_topo_currentId'
export default {
@@ -348,6 +350,7 @@ export default {
SeeksRelationGraph,
RelationGraph,
CMDBGrant,
CMDBTypeSelectAntd
},
data() {
const defaultOptions = {
@@ -421,7 +424,6 @@ export default {
currentId: null,
topoGroups: [],
CITypeId: null,
ciTypes: [],
startId: null,
endId: null,
@@ -500,15 +502,6 @@ export default {
this.currentId = _currentId
}
this.loadTopoViews(!_currentId)
let ciTypes = []
getCITypeGroups({ need_other: true }).then((res) => {
res.forEach((item) => {
if (item.ci_types && item.ci_types.length) {
ciTypes = ciTypes.concat(item.ci_types)
}
})
this.ciTypes = ciTypes
})
this.showTopoView(this.currentId.split('%')[1])
},
computed: {

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