Compare commits

..

63 Commits

Author SHA1 Message Date
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
pycook
f6de9b42ab chore: release v2.4.17 2024-12-26 15:30:08 +08:00
Leo Song
25f6bbcc3e Merge pull request #664 from veops/dev_ui_241226
feat(ui): update relative views menu display
2024-12-26 13:59:26 +08:00
songlh
bc3201656c feat(ui): update relative views menu display 2024-12-26 13:58:53 +08:00
pycook
89db5a060e fix(api): search for attr filter 2024-12-26 13:57:05 +08:00
Leo Song
b669775cd6 Merge pull request #663 from veops/dev_ui_241224
feat(ui): update style
2024-12-24 15:23:33 +08:00
songlh
655b642930 feat(ui): update style 2024-12-24 15:22:36 +08:00
pycook
b253fdfea0 fix(api): multi-line search 2024-12-23 16:52:06 +08:00
pycook
d0129439cd fix(api): update CI for unique identifier 2024-12-23 16:36:45 +08:00
pycook
aaa4ca1327 fix(api): date trigger 2024-12-23 16:32:27 +08:00
thexqn
747475b6a6 Fix bug 1217 (#656)
* fix(api): move soft delete of PreferenceShowAttributes to correct location

* chore: add manage.sh to .gitignore

* fix(api): correct type_id reference in CITypeAttributeManager for PreferenceShowAttributes soft delete

* Update .gitignore

---------

Co-authored-by: pycook <pycook@126.com>
2024-12-20 16:47:25 +08:00
thexqn
ada23262bb feat(search): implement column search mode and enhance search input functionality (#658)
* chore: update .gitignore to include manage.sh and .env files

* feat(api): add new SQL query for CI by no attribute in

* feat(api): enhance search functionality with new IN clause support for queries

* feat(lang): add new search tips and modes in English and Chinese language files

* feat(search): implement column search mode and enhance search input functionality
2024-12-18 17:21:24 +08:00
simontigers
0c57b2b83d Merge pull request #660 from veops/config_ruff
fix: ci_type find_path _graph
2024-12-18 16:42:31 +08:00
simontigers
082724e7bd fix: ci_type find_path _graph 2024-12-18 16:42:03 +08:00
simontigers
d782ceddab Merge pull request #659 from veops/config_ruff
fix: code linter
2024-12-18 14:35:56 +08:00
simontigers
510ea5dc2d fix: code linter 2024-12-18 06:24:38 +00:00
Leo Song
41ce5db1c7 Merge pull request #657 from veops/dev_ui_241217
fix(ui): ci - number type attr default value display error
2024-12-17 15:13:18 +08:00
songlh
c3aab86844 fix(ui): ci - number type attr default value display error 2024-12-17 15:12:44 +08:00
simontigers
d1e40b4e5e Merge pull request #654 from veops/fix_acl_date_joined_column
fix: acl user date_joined timezone
2024-12-16 13:58:46 +08:00
simontigers
ea4ea9d6b6 fix: acl user date_joined timezone 2024-12-16 13:58:20 +08:00
pycook
1c5d2c8e9e fix(api): date trigger 2024-12-13 16:57:24 +08:00
Leo Song
6bd3de8951 Merge pull request #652 from veops/dev_ui_241211
feat: update style
2024-12-11 15:48:23 +08:00
songlh
a0ff3d69cb feat: update style 2024-12-11 15:47:45 +08:00
pycook
fccf5db886 fix(ui): restore ui .env 2024-12-09 20:21:52 +08:00
pycook
95b55d2963 feat(api): set the default expire for redis lock 2024-12-09 19:50:04 +08:00
163 changed files with 6814 additions and 3249 deletions

1
.gitignore vendored
View File

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

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" />

79
cmdb-api/.ruff.toml Normal file
View File

@@ -0,0 +1,79 @@
line-length = 120
cache-dir = ".ruff_cache"
target-version = "py310"
unsafe-fixes = true
show-fixes = true
[lint]
select = [
"E",
"F",
"I",
"TCH",
# W
"W505",
# PT
"PT018",
# SIM
"SIM101",
"SIM114",
# PGH
"PGH004",
# PL
"PLE1142",
# RUF
"RUF100",
# UP
"UP007"
]
preview = true
ignore = ["FURB101"]
[lint.flake8-pytest-style]
mark-parentheses = false
parametrize-names-type = "list"
parametrize-values-row-type = "list"
parametrize-values-type = "tuple"
[lint.flake8-unused-arguments]
ignore-variadic-names = true
[lint.isort]
lines-between-types = 1
order-by-type = true
[lint.per-file-ignores]
"**/api/v1/*.py" = ["TCH"]
"**/model/*.py" = ["TCH003"]
"**/models/__init__.py" = ["F401", "F403"]
"**/tests/*.py" = ["E402"]
"celery_worker.py" = ["F401"]
"api/views/entry.py" = ["I001"]
"migrations/*.py" = ["I001", "E402"]
"*.py" = ["I001"]
"api/views/common_setting/department.py" = ["F841"]
"api/lib/common_setting/upload_file.py" = ["F841"]
"api/lib/common_setting/acl.py" = ["F841"]
"**/__init__.py" = ["F822"]
"api/tasks/*.py" = ["E722"]
"api/views/cmdb/*.py" = ["E722"]
"api/views/acl/*.py" = ["E722"]
"api/lib/secrets/*.py" = ["E722", "F841"]
"api/lib/utils.py" = ["E722", "E731"]
"api/lib/perm/authentication/cas/*" = ["E113", "F841"]
"api/lib/perm/acl/*" = ["E722"]
"api/lib/*" = ["E721", "F722"]
"api/lib/cmdb/*" = ["F722", "E722"]
"api/lib/cmdb/search/ci/es/search.py" = ["F841", "SIM114"]
"api/lib/cmdb/search/ci/db/search.py" = ["F841"]
"api/lib/cmdb/value.py" = ["F841"]
"api/lib/cmdb/history.py" = ["E501"]
"api/commands/common.py" = ["E722"]
"api/commands/click_cmdb.py" = ["E722"]
"api/lib/perm/auth.py" = ["SIM114"]
[format]
preview = true
quote-style = "single"
docstring-code-format = true
skip-magic-trailing-comma = false

View File

@@ -228,6 +228,12 @@ def cmdb_trigger():
"""
from api.lib.cmdb.ci import CITriggerManager
current_app.test_request_context().push()
if not UserCache.get('worker'):
from api.lib.perm.acl.user import UserCRUD
UserCRUD.add(username='worker', password=uuid.uuid4().hex, email='worker@xxx.com')
login_user(UserCache.get('worker'))
current_day = datetime.datetime.today().strftime("%Y-%m-%d")
trigger2cis = dict()
trigger2completed = dict()
@@ -256,10 +262,10 @@ def cmdb_trigger():
trigger2cis[trigger.id] = (trigger, ready_cis)
else:
cur = trigger2cis[trigger.id]
cur_ci_ids = {i.ci_id for i in cur[1]}
cur_ci_ids = {_ci.ci_id for _ci in cur[1]}
trigger2cis[trigger.id] = (
trigger, cur[1] + [i for i in ready_cis if i.ci_id not in cur_ci_ids
and i.ci_id not in trigger2completed.get(trigger.id, {})])
trigger, cur[1] + [_ci for _ci in ready_cis if _ci.ci_id not in cur_ci_ids
and _ci.ci_id not in trigger2completed.get(trigger.id, {})])
for tid in trigger2cis:
trigger, cis = trigger2cis[tid]
@@ -346,7 +352,7 @@ def cmdb_inner_secrets_init(address):
if valid_address(address):
token = current_app.config.get("INNER_TRIGGER_TOKEN", "") if not token else token
if not token:
token = click.prompt(f'Enter root token', hide_input=True, confirmation_prompt=False)
token = click.prompt('Enter root token', hide_input=True, confirmation_prompt=False)
assert token is not None
resp = requests.post("{}/api/v0.1/secrets/auto_seal".format(address.strip("/")),
headers={"Inner-Token": token})

View File

@@ -415,7 +415,7 @@ class AttributeManager(object):
db.session.rollback()
current_app.logger.error("update attribute error, {0}".format(str(e)))
return abort(400, ErrFormat.update_attribute_failed.format(("id=".format(_id))))
return abort(400, ErrFormat.update_attribute_failed.format(("id={}".format(_id))))
new = attr.to_dict()
if not new['choice_web_hook'] and new['is_choice']:

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

@@ -295,7 +295,7 @@ class CIManager(object):
db.session.commit()
value_table = TableMap(attr_name=attr.name).table
with redis_lock.Lock(rd.r, "auto_inc_id_{}".format(attr.name)):
with redis_lock.Lock(rd.r, "auto_inc_id_{}".format(attr.name), expire=10):
max_v = value_table.get_by(attr_id=attr.id, only_query=True).order_by(
getattr(value_table, 'value').desc()).first()
if max_v is not None:
@@ -393,7 +393,7 @@ class CIManager(object):
ci = None
record_id = None
password_dict = {}
with redis_lock.Lock(rd.r, ci_type.name):
with redis_lock.Lock(rd.r, ci_type.name, expire=10):
db.session.commit()
if (unique_key.default and unique_key.default.get('default') == AttributeDefaultValueEnum.AUTO_INC_ID and
@@ -524,10 +524,14 @@ class CIManager(object):
raw_dict = copy.deepcopy(ci_dict)
ci_attr2type_attr = {type_attr.attr_id: type_attr for type_attr, _ in attrs}
unique_name = None
for _, attr in attrs:
if attr.default and attr.default.get('default') == AttributeDefaultValueEnum.UPDATED_AT:
ci_dict[attr.name] = now
if attr.id == ci_type.unique_id:
unique_name = attr.name
value_manager = AttributeValueManager()
password_dict = dict()
@@ -550,14 +554,15 @@ class CIManager(object):
limit_attrs = self._valid_ci_for_no_read(ci) if not _is_admin else {}
record_id = None
with redis_lock.Lock(rd.r, ci_type.name):
with redis_lock.Lock(rd.r, ci_type.name, expire=10):
db.session.commit()
self._valid_unique_constraint(ci.type_id, ci_dict, ci_id)
ci_dict = {k: v for k, v in ci_dict.items() if k in ci_type_attrs_name}
key2attr = value_manager.valid_attr_value(ci_dict, ci.type_id, ci.id, ci_type_attrs_name,
ci_attr2type_attr=ci_attr2type_attr)
ci_attr2type_attr=ci_attr2type_attr,
unique_name=unique_name)
if computed_attrs:
value_manager.handle_ci_compute_attributes(ci_dict, computed_attrs, ci)
@@ -1268,7 +1273,9 @@ class CIRelationManager(object):
else:
type_relation = CITypeRelation.get_by_id(relation_type_id)
with redis_lock.Lock(rd.r, "ci_relation_add_{}_{}".format(first_ci.type_id, second_ci.type_id)):
with redis_lock.Lock(rd.r,
"ci_relation_add_{}_{}".format(first_ci.type_id, second_ci.type_id),
expire=10):
cls._check_constraint(first_ci_id, first_ci.type_id, second_ci_id, second_ci.type_id, type_relation)

View File

@@ -722,9 +722,6 @@ class CITypeAttributeManager(object):
ci_cache.apply_async(args=(ci.id, None, None), queue=CMDB_QUEUE)
for item in PreferenceShowAttributes.get_by(type_id=type_id, attr_id=attr_id, to_dict=False):
item.soft_delete(commit=False)
child_ids = CITypeInheritanceManager.recursive_children(type_id)
for _type_id in [type_id] + child_ids:
for item in CITypeUniqueConstraint.get_by(type_id=_type_id, to_dict=False):
@@ -740,6 +737,9 @@ class CITypeAttributeManager(object):
item = CITypeTrigger.get_by(type_id=_type_id, attr_id=attr_id, to_dict=False, first=True)
item and item.soft_delete(commit=False)
for item in PreferenceShowAttributes.get_by(type_id=_type_id, attr_id=attr_id, to_dict=False):
item.soft_delete(commit=False)
for item in (CITypeRelation.get_by(parent_id=type_id, to_dict=False) +
CITypeRelation.get_by(child_id=type_id, to_dict=False)):
if item.parent_id == type_id and attr_id in (item.parent_attr_ids or []):
@@ -862,15 +862,15 @@ class CITypeRelationManager(object):
graph = nx.DiGraph()
def get_children(_id):
def get_children(_id, _graph):
children = CITypeRelation.get_by(parent_id=_id, to_dict=False)
for i in children:
if i.child_id != _id:
graph.add_edge(i.parent_id, i.child_id)
get_children(i.child_id)
_graph.add_edge(i.parent_id, i.child_id)
get_children(i.child_id, _graph)
get_children(source_type_id)
get_children(source_type_id, graph)
paths = list(nx.all_simple_paths(graph, source_type_id, target_type_ids))
@@ -1145,13 +1145,14 @@ class CITypeAttributeGroupManager(object):
else:
group_pos = group2pos[group['name']]
attr = None
for i in items:
if i.attr_id in id2attr:
attr = id2attr[i.attr_id]
attr['inherited'] = group['inherited']
attr['inherited_from'] = group.get('inherited_from')
result[group_pos]['attributes'].append(attr)
else:
continue
if i.attr_id in attr2pos:
result[attr2pos[i.attr_id][0]]['attributes'].remove(attr2pos[i.attr_id][1])
@@ -1398,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

@@ -122,7 +122,7 @@ class RackManager(DCIMBase):
CIManager().update(rack['_id'], **payload)
def add_device(self, rack_id, device_id, u_start, u_count=None):
with (redis_lock.Lock(rd.r, "DCIM_RACK_OPERATE_{}".format(rack_id))):
with (redis_lock.Lock(rd.r, "DCIM_RACK_OPERATE_{}".format(rack_id), expire=10)):
self.calc_u_free_count(rack_id, device_id, u_start, u_count)
self.add_relation(rack_id, device_id)
@@ -139,7 +139,7 @@ class RackManager(DCIMBase):
OperateHistoryManager().add(operate_type=OperateTypeEnum.ADD_DEVICE, rack_id=rack_id, ci_id=device_id)
def remove_device(self, rack_id, device_id):
with (redis_lock.Lock(rd.r, "DCIM_RACK_OPERATE_{}".format(rack_id))):
with (redis_lock.Lock(rd.r, "DCIM_RACK_OPERATE_{}".format(rack_id), expire=10)):
CIRelationManager.delete_3(rack_id, device_id, apply_async=False, valid=False)
payload = {RackBuiltinAttributes.FREE_U_COUNT: self.calc_u_free_count(rack_id)}
@@ -151,7 +151,7 @@ class RackManager(DCIMBase):
OperateHistoryManager().add(operate_type=OperateTypeEnum.REMOVE_DEVICE, rack_id=rack_id, ci_id=device_id)
def move_device(self, rack_id, device_id, to_u_start):
with (redis_lock.Lock(rd.r, "DCIM_RACK_OPERATE_{}".format(rack_id))):
with (redis_lock.Lock(rd.r, "DCIM_RACK_OPERATE_{}".format(rack_id), expire=10)):
payload = {RackBuiltinAttributes.FREE_U_COUNT: self.calc_u_free_count(rack_id, device_id, to_u_start)}
CIManager().update(rack_id, _sync=True, **payload)
@@ -160,7 +160,7 @@ class RackManager(DCIMBase):
OperateHistoryManager().add(operate_type=OperateTypeEnum.MOVE_DEVICE, rack_id=rack_id, ci_id=device_id)
def migrate_device(self, rack_id, device_id, to_rack_id, to_u_start):
with (redis_lock.Lock(rd.r, "DCIM_RACK_OPERATE_{}".format(rack_id))):
with (redis_lock.Lock(rd.r, "DCIM_RACK_OPERATE_{}".format(rack_id), expire=10)):
self.calc_u_free_count(to_rack_id, device_id, to_u_start)
if rack_id != to_rack_id:

View File

@@ -92,7 +92,7 @@ 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))):
with (redis_lock.Lock(rd.r, "IPAM_ASSIGN_ADDRESS_{}".format(subnet_id), expire=10)):
cis = self._get_cis(subnet_id, ips)
ip2ci = {ci[IPAddressBuiltinAttributes.IP]: ci for ci in cis}

View File

@@ -43,7 +43,7 @@ class ScanHistoryManager(DBMixin):
if kwargs.get('ips'):
from api.lib.cmdb.ipam.address import IpAddressManager
IpAddressManager().assign_ips(kwargs['ips'], None, kwargs.get('cidr'),
IpAddressManager().assign_ips(kwargs['ips'], ci_id, kwargs.get('cidr'),
**{IPAddressBuiltinAttributes.IS_USED: 1})
scan_rule = IPAMSubnetScan.get_by(ci_id=ci_id, first=True, to_dict=False)

View File

@@ -163,7 +163,7 @@ class CIFilterPermsCRUD(DBMixin):
def add(self, **kwargs):
kwargs = self._can_add(**kwargs) or kwargs
with redis_lock.Lock(rd.r, 'CMDB_FILTER_{}_{}'.format(kwargs['type_id'], kwargs['rid'])):
with redis_lock.Lock(rd.r, 'CMDB_FILTER_{}_{}'.format(kwargs['type_id'], kwargs['rid']), expire=10):
request_id_filter = {}
if kwargs.get('id_filter'):
obj = self.cls.get_by(type_id=kwargs.get('type_id'),
@@ -232,7 +232,7 @@ class CIFilterPermsCRUD(DBMixin):
pass
def delete(self, **kwargs):
with redis_lock.Lock(rd.r, 'CMDB_FILTER_{}_{}'.format(kwargs['type_id'], kwargs['rid'])):
with redis_lock.Lock(rd.r, 'CMDB_FILTER_{}_{}'.format(kwargs['type_id'], kwargs['rid']), expire=10):
obj = self.cls.get_by(type_id=kwargs.get('type_id'),
rid=kwargs.get('rid'),
id_filter=None,
@@ -249,7 +249,7 @@ class CIFilterPermsCRUD(DBMixin):
def delete2(self, **kwargs):
with redis_lock.Lock(rd.r, 'CMDB_FILTER_{}_{}'.format(kwargs['type_id'], kwargs['rid'])):
with redis_lock.Lock(rd.r, 'CMDB_FILTER_{}_{}'.format(kwargs['type_id'], kwargs['rid']), expire=10):
obj = self.cls.get_by(type_id=kwargs.get('type_id'),
rid=kwargs.get('rid'),
ci_filter=None,

View File

@@ -16,8 +16,9 @@ class ErrFormat(CommonErrFormat):
argument_file_not_found = _l("The file doesn't seem to be uploaded") # 文件似乎并未上传
attribute_not_found = _l("Attribute {} does not exist!") # 属性 {} 不存在!
# 该属性是模型的唯一标识,不能被删除!
attribute_is_unique_id = _l(
"This attribute is the unique identifier of the model and cannot be deleted!") # 该属性是模型的唯一标识,不能被删除!
"This attribute is the unique identifier of the model and cannot be deleted!")
attribute_is_ref_by_type = _l(
"This attribute is referenced by model {} and cannot be deleted!") # 该属性被模型 {} 引用, 不能删除!
attribute_value_type_cannot_change = _l(
@@ -129,7 +130,8 @@ class ErrFormat(CommonErrFormat):
adr_default_ref_once = _l("The default auto-discovery rule is already referenced by model {}!")
# unique_key方法必须返回非空字符串!
adr_unique_key_required = _l("The unique_key method must return a non-empty string!")
adr_plugin_attributes_list_required = _l("The attributes method must return a list") # attributes方法必须返回的是list
# attributes方法必须返回的是list
adr_plugin_attributes_list_required = _l("The attributes method must return a list")
# attributes方法返回的list不能为空!
adr_plugin_attributes_list_no_empty = _l("The list returned by the attributes method cannot be empty!")
# 只有管理员才可以定义执行机器为: 所有节点!

View File

@@ -107,3 +107,12 @@ FROM
WHERE c_value_index_datetime.value LIKE "{0}") AS {1}
GROUP BY {1}.ci_id
"""
QUERY_CI_BY_NO_ATTR_IN = """
SELECT *
FROM
(SELECT c_value_index_texts.ci_id
FROM c_value_index_texts
WHERE c_value_index_texts.value in ({0})) AS {1}
GROUP BY {1}.ci_id
"""

View File

@@ -6,6 +6,7 @@ from __future__ import unicode_literals
import copy
import six
import time
from flask import abort
from flask import current_app
from flask_login import current_user
from jinja2 import Template
@@ -27,6 +28,7 @@ from api.lib.cmdb.search.ci.db.query_sql import FACET_QUERY
from api.lib.cmdb.search.ci.db.query_sql import QUERY_CI_BY_ATTR_NAME
from api.lib.cmdb.search.ci.db.query_sql import QUERY_CI_BY_ID
from api.lib.cmdb.search.ci.db.query_sql import QUERY_CI_BY_NO_ATTR
from api.lib.cmdb.search.ci.db.query_sql import QUERY_CI_BY_NO_ATTR_IN
from api.lib.cmdb.search.ci.db.query_sql import QUERY_CI_BY_TYPE
from api.lib.cmdb.search.ci.db.query_sql import QUERY_UNION_CI_ATTRIBUTE_IS_NULL
from api.lib.cmdb.utils import TableMap
@@ -141,7 +143,7 @@ class Search(object):
if str(ci_type.id) in self.type_id_list:
self.type_id_list.remove(str(ci_type.id))
type_id_list.remove(str(ci_type.id))
sub.extend([i for i in queries[1:] if isinstance(i, six.string_types)])
sub.extend([i for i in queries[1:] if isinstance(i, (six.string_types, list))])
sub.insert(0, "_type:{}".format(ci_type.id))
queries.append(dict(operator="|", queries=sub))
@@ -151,7 +153,9 @@ class Search(object):
if not self.fl:
self.fl = set(self.type2filter_perms[ci_type.id]['attr_filter'])
else:
self.fl = set(self.fl) & set(self.type2filter_perms[ci_type.id]['attr_filter'])
fl = set(self.fl) & set(self.type2filter_perms[ci_type.id]['attr_filter'])
not fl and abort(400, ErrFormat.ci_filter_perm_attr_no_permission.format(self.fl))
self.fl = fl
else:
self.fl = self.fl or {}
if not self.fl or isinstance(self.fl, dict):
@@ -433,11 +437,14 @@ class Search(object):
if not q.startswith("("):
raise SearchError(ErrFormat.ci_search_Parentheses_invalid)
operator, q = self._operator_proc(q)
if q.endswith(")"):
result.append(dict(operator=operator, queries=[q[1:-1]]))
if ":" not in q: # multi-line search
result.append(q[1:-1].split(';'))
else:
operator, q = self._operator_proc(q)
if q.endswith(")"):
result.append(dict(operator=operator, queries=[q[1:-1]]))
sub = dict(operator=operator, queries=[q[1:]])
sub = dict(operator=operator, queries=[q[1:]])
elif q.endswith(")") and sub:
sub['queries'].append(q[:-1])
result.append(copy.deepcopy(sub))
@@ -525,22 +532,31 @@ class Search(object):
query_sql = ""
for q in queries:
# current_app.logger.debug(q)
_query_sql = ""
if isinstance(q, dict):
alias, _query_sql, operator = self.__query_build_by_field(q['queries'], True, True, alias, is_sub=True)
# current_app.logger.info(_query_sql)
# current_app.logger.info((operator, is_first, alias))
operator = q['operator']
if len(q['queries']) == 1 and ";" in q['queries'][0]:
values = q['queries'][0].split(";")
in_values = ",".join("'{0}'".format(v) for v in values)
_query_sql = QUERY_CI_BY_NO_ATTR_IN.format(in_values, alias)
operator = q['operator']
else:
alias, _query_sql, operator = self.__query_build_by_field(q['queries'], True, True, alias,
is_sub=True)
operator = q['operator']
elif ":" in q and not q.startswith("*"):
alias, _query_sql, operator = self.__query_by_attr(q, queries, alias, is_sub)
elif q == "*":
continue
elif q:
q = q.replace("'", "\\'")
q = q.replace('"', '\\"')
q = q.replace("*", "%").replace('\\n', '%')
_query_sql = QUERY_CI_BY_NO_ATTR.format(q, alias)
if not isinstance(q, list):
q = q.replace("'", "\\'")
q = q.replace('"', '\\"')
q = q.replace("*", "%").replace('\\n', '%')
_query_sql = QUERY_CI_BY_NO_ATTR.format(q, alias)
else:
_query_sql = QUERY_CI_BY_NO_ATTR_IN.format(",".join("'{0}'".format(v) for v in q), alias)
if is_first and _query_sql and not self.only_type_query:
query_sql = "SELECT * FROM ({0}) AS {1}".format(_query_sql, alias)

View File

@@ -55,7 +55,6 @@ def str2datetime(x):
return datetime.datetime.strptime(x, "%Y-%m-%d %H:%M")
class ValueTypeMap(object):
deserialize = {
ValueTypeEnum.INT: string2int,

View File

@@ -3,7 +3,7 @@
from __future__ import unicode_literals
import imp
import importlib.util
import copy
import jinja2
@@ -136,7 +136,7 @@ class AttributeValueManager(object):
if not re.compile(expr).match(str(value)):
return abort(400, ErrFormat.attribute_value_invalid2.format(alias, value))
def _validate(self, attr, value, value_table, ci=None, type_id=None, ci_id=None, type_attr=None):
def _validate(self, attr, value, value_table, ci=None, type_id=None, ci_id=None, type_attr=None, unique_name=None):
if not attr.is_reference:
ci = ci or {}
v = self._deserialize_value(attr.alias, attr.value_type, value)
@@ -146,7 +146,7 @@ class AttributeValueManager(object):
else:
v = value or None
attr.is_unique and self._check_is_unique(
(attr.is_unique or attr.name == unique_name) and self._check_is_unique(
value_table, attr, ci and ci.id or ci_id, ci and ci.type_id or type_id, v)
self._check_is_required(ci and ci.type_id or type_id, attr, v, type_attr=type_attr)
if attr.is_reference:
@@ -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()
@@ -237,7 +238,10 @@ class AttributeValueManager(object):
if computed_value is not None:
ci_dict[attr['name']] = computed_value
def valid_attr_value(self, ci_dict, type_id, ci_id, name2attr, alias2attr=None, ci_attr2type_attr=None):
def valid_attr_value(self, ci_dict, type_id, ci_id, name2attr,
alias2attr=None,
ci_attr2type_attr=None,
unique_name=None):
key2attr = dict()
alias2attr = alias2attr or {}
ci_attr2type_attr = ci_attr2type_attr or {}
@@ -268,7 +272,8 @@ class AttributeValueManager(object):
else:
value = self._validate(attr, value, value_table, ci=None, type_id=type_id, ci_id=ci_id,
type_attr=ci_attr2type_attr.get(attr.id))
type_attr=ci_attr2type_attr.get(attr.id),
unique_name=unique_name)
ci_dict[key] = value
except BadRequest as e:
raise

View File

@@ -1,6 +1,6 @@
import functools
from flask import abort, session
from flask import abort, session, current_app
from api.lib.common_setting.acl import ACLManager
from api.lib.common_setting.resp_format import ErrFormat
from api.lib.perm.acl.acl import is_app_admin
@@ -15,6 +15,7 @@ def perms_role_required(app_name, resource_type_name, resource_name, perm, role_
try:
has_perms = acl.role_has_perms(session["acl"]['rid'], resource_name, resource_type_name, perm)
except Exception as e:
current_app.logger.error(f"acl role_has_perms err: {e}")
# resource_type not exist, continue check role
if role_name:
if role_name not in session.get("acl", {}).get("parentRoles", []) and not is_app_admin(app_name):

View File

@@ -476,7 +476,7 @@ class EditDepartmentInACL(object):
for employee in e_list:
employee_acl_rid = employee.get('e_acl_rid')
if employee_acl_rid == 0:
result.append(f"employee_acl_rid == 0")
result.append("employee_acl_rid == 0")
continue
cls.remove_single_employee_from_old_department(acl, employee, result)
@@ -501,8 +501,8 @@ class EditDepartmentInACL(object):
acl.remove_user_from_role(employee.get('e_acl_rid'), payload)
current_app.logger.info(f"remove {employee.get('e_acl_rid')} from {d_acl_rid}")
except Exception as e:
result.append(
f"remove_user_from_role employee_acl_rid: {employee.get('e_acl_rid')}, parent_id: {d_acl_rid}, err: {e}")
err = f"remove_user_from_role e_acl_rid: {employee.get('e_acl_rid')}, parent_id: {d_acl_rid}, err: {e}"
result.append(err)
return True
@@ -548,7 +548,7 @@ class EditDepartmentInACL(object):
for employee in e_list:
employee_acl_rid = employee.get('e_acl_rid')
if employee_acl_rid == 0:
result.append(f"employee_acl_rid == 0")
result.append("employee_acl_rid == 0")
continue
cls.remove_single_employee_from_old_department(acl, employee, result)

View File

@@ -4,7 +4,7 @@ import traceback
from datetime import datetime
import requests
from flask import abort
from flask import abort, current_app
from flask_login import current_user
from sqlalchemy import or_, literal_column, func, not_, and_
from werkzeug.datastructures import MultiDict
@@ -478,7 +478,7 @@ class EmployeeCRUD(object):
Employee.deleted == 0,
Employee.block == block,
]
if type(department_id) == list:
if isinstance(department_id, list):
if len(department_id) == 0:
return []
else:
@@ -702,6 +702,7 @@ class EmployeeCRUD(object):
try:
last_login = datetime.strptime(last_login, '%Y-%m-%d %H:%M:%S')
except Exception as e:
current_app.logger.error(f"strptime {last_login} err: {e}")
last_login = datetime.now()
else:
last_login = datetime.now()
@@ -712,6 +713,7 @@ class EmployeeCRUD(object):
)
return last_login
except Exception as e:
current_app.logger.error(f"update last_login err: {e}")
return

View File

@@ -2,7 +2,7 @@ import requests
from api.lib.common_setting.const import BotNameMap
from api.lib.common_setting.resp_format import ErrFormat
from api.models.common_setting import CompanyInfo, NoticeConfig
from api.models.common_setting import NoticeConfig
from wtforms import Form
from wtforms import StringField
from wtforms import validators

View File

@@ -48,7 +48,9 @@ class CMDBApp(BaseApp):
{"page": "Model_Relationships", "page_cn": "模型关系", "perms": ["read"]},
{"page": "Operation_Audit", "page_cn": "操作审计", "perms": ["read"]},
{"page": "Relationship_Types", "page_cn": "关系类型", "perms": ["read"]},
{"page": "Auto_Discovery", "page_cn": "自动发现", "perms": ["read", "create_plugin", "update_plugin", "delete_plugin"]},
{"page": "Auto_Discovery", "page_cn": "自动发现",
"perms": ["read", "create_plugin", "update_plugin", "delete_plugin"]
},
{"page": "TopologyView", "page_cn": "拓扑视图",
"perms": ["read", "create_topology_group", "update_topology_group", "delete_topology_group",
"create_topology_view"],

View File

@@ -6,7 +6,7 @@ from functools import wraps
from flask import abort
from flask import request
from api.lib.perm.acl.cache import AppCache, AppAccessTokenCache
from api.lib.perm.acl.cache import AppCache
from api.lib.perm.acl.resp_format import ErrFormat

View File

@@ -138,14 +138,14 @@ class HasResourceRoleCache(object):
@classmethod
def add(cls, rid, app_id):
with redis_lock.Lock(rd.r, 'HasResourceRoleCache'):
with redis_lock.Lock(rd.r, 'HasResourceRoleCache', expire=10):
c = cls.get(app_id)
c[rid] = 1
cache.set(cls.PREFIX_KEY.format(app_id), c, timeout=0)
@classmethod
def remove(cls, rid, app_id):
with redis_lock.Lock(rd.r, 'HasResourceRoleCache'):
with redis_lock.Lock(rd.r, 'HasResourceRoleCache', expire=10):
c = cls.get(app_id)
c.pop(rid, None)
cache.set(cls.PREFIX_KEY.format(app_id), c, timeout=0)

View File

@@ -1,8 +1,5 @@
# -*- coding:utf-8 -*-
import time
import redis_lock
import six
from flask import abort
@@ -145,7 +142,7 @@ class RoleRelationCRUD(object):
@classmethod
def add(cls, role, parent_id, child_ids, app_id):
with redis_lock.Lock(rd.r, "ROLE_RELATION_ADD"):
with redis_lock.Lock(rd.r, "ROLE_RELATION_ADD", expire=10):
db.session.commit()
result = []

View File

@@ -1,7 +1,6 @@
# -*- coding:utf-8 -*-
import base64
from typing import Set
import elasticsearch
import redis

View File

@@ -105,8 +105,8 @@ class User(CRUDModel, SoftDeleteMixin):
_password = db.Column("password", db.String(80))
key = db.Column(db.String(32), nullable=False)
secret = db.Column(db.String(32), nullable=False)
date_joined = db.Column(db.DateTime, default=datetime.utcnow)
last_login = db.Column(db.DateTime, default=datetime.utcnow)
date_joined = db.Column(db.DateTime, default=datetime.now)
last_login = db.Column(db.DateTime, default=datetime.now)
block = db.Column(db.Boolean, default=False)
has_logined = db.Column(db.Boolean, default=False)
wx_id = db.Column(db.String(32))

View File

@@ -32,7 +32,7 @@ from api.models.acl import Trigger
def role_rebuild(rids, app_id):
rids = rids if isinstance(rids, list) else [rids]
for rid in rids:
with redis_lock.Lock(rd.r, "ROLE_REBUILD_{}_{}".format(rid, app_id)):
with redis_lock.Lock(rd.r, "ROLE_REBUILD_{}_{}".format(rid, app_id), expire=10):
RoleRelationCache.rebuild(rid, app_id)
current_app.logger.info("Role {0} App {1} rebuild..........".format(rids, app_id))

View File

@@ -145,7 +145,7 @@ def ci_delete_trigger(trigger, operate_type, ci_dict):
@flush_db
@reconnect_db
def ci_relation_cache(parent_id, child_id, ancestor_ids):
with redis_lock.Lock(rd.r, "CIRelation_{}".format(parent_id)):
with redis_lock.Lock(rd.r, "CIRelation_{}".format(parent_id), expire=10):
children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0]
children = json.loads(children) if children is not None else {}
@@ -223,7 +223,7 @@ def ci_relation_add(parent_dict, child_id, uid):
@celery.task(name="cmdb.ci_relation_delete", queue=CMDB_QUEUE)
@reconnect_db
def ci_relation_delete(parent_id, child_id, ancestor_ids):
with redis_lock.Lock(rd.r, "CIRelation_{}".format(parent_id)):
with redis_lock.Lock(rd.r, "CIRelation_{}".format(parent_id), expire=10):
children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0]
children = json.loads(children) if children is not None else {}
@@ -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

@@ -131,7 +131,7 @@ class EmployeeChangePasswordWithACLID(APIView):
if not password:
abort(400, ErrFormat.password_is_required)
data = EmployeeCRUD.change_password_by_uid(_uid, password)
EmployeeCRUD.change_password_by_uid(_uid, password)
return self.jsonify(200)

View File

@@ -6,7 +6,7 @@ import magic
from api.lib.common_setting.const import MIMEExtMap
from api.lib.common_setting.resp_format import ErrFormat
from api.lib.common_setting.upload_file import allowed_file, generate_new_file_name, CommonFileCRUD
from api.lib.common_setting.upload_file import generate_new_file_name, CommonFileCRUD
from api.resource import APIView
prefix = '/file'

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

@@ -58,3 +58,4 @@ python-magic==0.4.27
jsonpath==0.82.2
networkx>=3.1
ipaddress>=1.0.23
ruff==0.8.3

View File

@@ -1,6 +1,6 @@
NODE_ENV=production
VUE_APP_PREVIEW=false
VUE_APP_API_BASE_URL=/api
VUE_APP_API_BASE_URL=http://127.0.0.1:5000/api
VUE_APP_BUILD_PACKAGES="ticket,calendar,acl"
VUE_APP_IS_OUTER=true
VUE_APP_IS_OPEN_SOURCE=true

View File

@@ -54,6 +54,198 @@
<div class="content unicode" style="display: block;">
<ul class="icon_lists dib-box">
<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>
<div class="code-name">&amp;#xea0b;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea0a;</span>
<div class="name">veops-switch (1)</div>
<div class="code-name">&amp;#xea0a;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea09;</span>
<div class="name">veops-label</div>
<div class="code-name">&amp;#xea09;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea08;</span>
<div class="name">top_acl</div>
<div class="code-name">&amp;#xea08;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea06;</span>
<div class="name">top_ticket</div>
<div class="code-name">&amp;#xea06;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea07;</span>
<div class="name">top_agent</div>
<div class="code-name">&amp;#xea07;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea05;</span>
<div class="name">itsm-table_download</div>
<div class="code-name">&amp;#xea05;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea04;</span>
<div class="name">itsm-image_download</div>
<div class="code-name">&amp;#xea04;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea02;</span>
<div class="name">veops-rear</div>
@@ -6162,9 +6354,9 @@
<pre><code class="language-css"
>@font-face {
font-family: 'iconfont';
src: url('iconfont.woff2?t=1732673294759') format('woff2'),
url('iconfont.woff?t=1732673294759') format('woff'),
url('iconfont.ttf?t=1732673294759') format('truetype');
src: url('iconfont.woff2?t=1749393321370') format('woff2'),
url('iconfont.woff?t=1749393321370') format('woff'),
url('iconfont.ttf?t=1749393321370') format('truetype');
}
</code></pre>
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@@ -6190,6 +6382,294 @@
<div class="content font-class">
<ul class="icon_lists dib-box">
<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 a-command_interception1"></span>
<div class="name">
command_interception (1)
</div>
<div class="code-name">.a-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">
veops-servicetree
</div>
<div class="code-name">.veops-servicetree
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-switch1"></span>
<div class="name">
veops-switch (1)
</div>
<div class="code-name">.veops-switch1
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-label"></span>
<div class="name">
veops-label
</div>
<div class="code-name">.veops-label
</div>
</li>
<li class="dib">
<span class="icon iconfont top_acl"></span>
<div class="name">
top_acl
</div>
<div class="code-name">.top_acl
</div>
</li>
<li class="dib">
<span class="icon iconfont top_ticket"></span>
<div class="name">
top_ticket
</div>
<div class="code-name">.top_ticket
</div>
</li>
<li class="dib">
<span class="icon iconfont top_agent"></span>
<div class="name">
top_agent
</div>
<div class="code-name">.top_agent
</div>
</li>
<li class="dib">
<span class="icon iconfont itsm-table_download"></span>
<div class="name">
itsm-table_download
</div>
<div class="code-name">.itsm-table_download
</div>
</li>
<li class="dib">
<span class="icon iconfont itsm-image_download"></span>
<div class="name">
itsm-image_download
</div>
<div class="code-name">.itsm-image_download
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-rear"></span>
<div class="name">
@@ -15352,6 +15832,262 @@
<div class="content symbol">
<ul class="icon_lists dib-box">
<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="#a-command_interception1"></use>
</svg>
<div class="name">command_interception (1)</div>
<div class="code-name">#a-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>
</svg>
<div class="name">veops-servicetree</div>
<div class="code-name">#veops-servicetree</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-switch1"></use>
</svg>
<div class="name">veops-switch (1)</div>
<div class="code-name">#veops-switch1</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-label"></use>
</svg>
<div class="name">veops-label</div>
<div class="code-name">#veops-label</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#top_acl"></use>
</svg>
<div class="name">top_acl</div>
<div class="code-name">#top_acl</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#top_ticket"></use>
</svg>
<div class="name">top_ticket</div>
<div class="code-name">#top_ticket</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#top_agent"></use>
</svg>
<div class="name">top_agent</div>
<div class="code-name">#top_agent</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-table_download"></use>
</svg>
<div class="name">itsm-table_download</div>
<div class="code-name">#itsm-table_download</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-image_download"></use>
</svg>
<div class="name">itsm-image_download</div>
<div class="code-name">#itsm-image_download</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-rear"></use>

View File

@@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 3857903 */
src: url('iconfont.woff2?t=1732673294759') format('woff2'),
url('iconfont.woff?t=1732673294759') format('woff'),
url('iconfont.ttf?t=1732673294759') format('truetype');
src: url('iconfont.woff2?t=1749393321370') format('woff2'),
url('iconfont.woff?t=1749393321370') format('woff'),
url('iconfont.ttf?t=1749393321370') format('truetype');
}
.iconfont {
@@ -13,6 +13,134 @@
-moz-osx-font-smoothing: grayscale;
}
.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";
}
.a-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";
}
.veops-switch1:before {
content: "\ea0a";
}
.veops-label:before {
content: "\ea09";
}
.top_acl:before {
content: "\ea08";
}
.top_ticket:before {
content: "\ea06";
}
.top_agent:before {
content: "\ea07";
}
.itsm-table_download:before {
content: "\ea05";
}
.itsm-image_download:before {
content: "\ea04";
}
.veops-rear:before {
content: "\ea02";
}

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,230 @@
"css_prefix_text": "",
"description": "",
"glyphs": [
{
"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": "a-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",
"font_class": "veops-servicetree",
"unicode": "ea0b",
"unicode_decimal": 59915
},
{
"icon_id": "42921461",
"name": "veops-switch (1)",
"font_class": "veops-switch1",
"unicode": "ea0a",
"unicode_decimal": 59914
},
{
"icon_id": "42857659",
"name": "veops-label",
"font_class": "veops-label",
"unicode": "ea09",
"unicode_decimal": 59913
},
{
"icon_id": "42790685",
"name": "top_acl",
"font_class": "top_acl",
"unicode": "ea08",
"unicode_decimal": 59912
},
{
"icon_id": "42790687",
"name": "top_ticket",
"font_class": "top_ticket",
"unicode": "ea06",
"unicode_decimal": 59910
},
{
"icon_id": "42790686",
"name": "top_agent",
"font_class": "top_agent",
"unicode": "ea07",
"unicode_decimal": 59911
},
{
"icon_id": "42732510",
"name": "itsm-table_download",
"font_class": "itsm-table_download",
"unicode": "ea05",
"unicode_decimal": 59909
},
{
"icon_id": "42732515",
"name": "itsm-image_download",
"font_class": "itsm-image_download",
"unicode": "ea04",
"unicode_decimal": 59908
},
{
"icon_id": "42510712",
"name": "veops-rear",

Binary file not shown.

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)
@@ -200,6 +200,28 @@ 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
}
},
}
</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

@@ -106,7 +106,7 @@
<CIReferenceAttr
v-if="getAttr(item.property).is_reference && (item.exp === 'is' || item.exp === '~is')"
:style="{ width: '175px' }"
class="select-filter-component"
class="select-filter-component ops-select-bg"
:referenceTypeId="getAttr(item.property).reference_type_id"
:disabled="disabled"
v-model="item.value"
@@ -114,7 +114,7 @@
<a-select
v-else-if="getAttr(item.property).is_bool && (item.exp === 'is' || item.exp === '~is')"
v-model="item.value"
class="select-filter-component"
class="select-filter-component ops-select-bg"
:style="{ width: '175px' }"
:disabled="disabled"
:placeholder="$t('placeholder2')"
@@ -398,7 +398,6 @@ export default {
/deep/ .ant-select-selection {
height: 24px;
background: #f7f8fa;
line-height: 24px;
border: none;

View File

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

View File

@@ -341,7 +341,7 @@ export default {
},
}
</script>
<style scoped>
<style lang="less" scoped>
.pop_btn {
text-align: right;
margin-top: 24px;

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

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

@@ -11,10 +11,19 @@
.ant-input {
box-shadow: none;
border: none;
background-color: #F7F8FA;
height: 30px;
line-height: 30px;
border-radius: 30px;
&:focus {
border: solid 1px #B1C9FF;
}
}
.cmdb-side-menu-search-focused {
.ant-input {
border: solid 1px #B1C9FF;
}
}
.ant-input-suffix {

View File

@@ -92,7 +92,7 @@ export default {
e.preventDefault()
e.stopPropagation()
this.$confirm({
title: this.$t('alert'),
title: this.$t('warning'),
content: this.$t('cmdb.preference.confirmcancelSub2', { name: menu.meta.title }),
onOk() {
const citypeId = menu.meta.typeId
@@ -171,7 +171,6 @@ export default {
},
renderMenuItem(menu) {
const isShowDot = menu.path.substr(0, 22) === '/cmdb/instances/types/'
const isShowGrant = menu.path.substr(0, 20) === '/cmdb/relationviews/'
const target = menu.meta.target || null
const tag = target && 'a' || 'router-link'
const props = { to: { name: menu.name } }
@@ -205,11 +204,9 @@ export default {
<a-icon type="menu" ref="extraEllipsis" class="custom-menu-extra-ellipsis"></a-icon>
</a-popover>
}
{isShowGrant && <a-icon class="custom-menu-extra-ellipsis" onClick={e => this.handlePerm(e, menu, 'RelationView')} type="user-add" />}
</span>
</tag>
{isShowDot && <CMDBGrant ref="cmdbGrantCIType" resourceType="CIType" app_id="cmdb" />}
{isShowGrant && <CMDBGrant ref="cmdbGrantRelationView" resourceType="RelationView" app_id="cmdb" />}
</Item>
)
},
@@ -313,10 +310,7 @@ export default {
<Item class={styles['cmdb-side-menu-search']}>
<a-input
ref="cmdbSideMenuSearchInputRef"
class={styles['cmdb-side-menu-search-input']}
style={{
border: this.$route.name === 'cmdb_resource_search' ? 'solid 1px #B1C9FF' : ''
}}
class={`ops-input ${this.$route.name === 'cmdb_resource_search' ? 'cmdb-side-menu-search-focused' : ''}`}
placeholder={this.$t('cmdbSearch')}
onPressEnter={(e) => {
this.jumpCMDBSearch(e.target.value)

View File

@@ -1,121 +1,121 @@
<template>
<vxe-table v-bind="$attrs" v-on="new$listeners" ref="xTable">
<slot></slot>
<template #empty>
<slot name="empty">
<div :style="{ paddingTop: '10px' }">
<img :style="{ width: '140px', height: '90px' }" :src="require('@/assets/data_empty.png')" />
<div>{{ $t('noData') }}</div>
</div>
</slot>
</template>
<template #loading>
<slot name="loading"></slot>
</template>
</vxe-table>
</template>
<script>
import _ from 'lodash'
// 该组件使用方法与vxe-table一致但调用它的方法时需先调用getVxetableRef()获取到vxe-table实体
export default {
name: 'OpsTable',
data() {
return {
// isShifting: false,
// lastIndex: -1,
lastSelected: [],
currentSelected: [],
}
},
computed: {
new$listeners() {
if (!Object.keys(this.$listeners).length) {
return this.$listeners
}
return Object.assign(this.$listeners, {
// 在这里覆盖原有的change事件
// 'checkbox-change': this.selectChangeEvent,
'checkbox-range-change': this.checkboxRangeChange,
'checkbox-range-start': this.checkboxRangeStart,
'checkbox-range-end': this.checkboxRangeEnd,
})
},
},
mounted() {
// window.onkeydown = (e) => {
// if (e.key === 'Shift') {
// this.isShifting = true
// }
// }
// window.onkeyup = (e) => {
// if (e.key === 'Shift') {
// this.isShifting = false
// this.lastIndex = -1
// }
// }
},
beforeDestroy() {
// window.onkeydown = ''
// window.onkeyup = ''
},
methods: {
getVxetableRef() {
return this.$refs.xTable
},
// selectChangeEvent(e) {
// const xTable = this.$refs.xTable
// const { lastIndex } = this
// const currentIndex = e.rowIndex
// const { tableData } = xTable.getTableData()
// if (lastIndex > -1 && this.isShifting) {
// let start = lastIndex
// let end = currentIndex
// if (lastIndex > currentIndex) {
// start = currentIndex
// end = lastIndex
// }
// const rangeData = tableData.slice(start, end + 1)
// xTable.setCheckboxRow(rangeData, true)
// }
// this.lastIndex = currentIndex
// this.$emit('checkbox-change', { ...e, records: xTable.getCheckboxRecords() })
// },
checkboxRangeStart(e) {
const xTable = this.$refs.xTable
const lastSelected = xTable.getCheckboxRecords()
const selectedReserve = xTable.getCheckboxReserveRecords()
this.lastSelected = [...lastSelected, ...selectedReserve]
this.$emit('checkbox-range-start', e)
},
checkboxRangeChange(e) {
const xTable = this.$refs.xTable
xTable.setCheckboxRow(this.lastSelected, true)
this.currentSelected = e.records
// this.lastSelected = [...new Set([...this.lastSelected, ...e.records])]
this.$emit('checkbox-range-change', {
...e,
records: [...xTable.getCheckboxRecords(), ...xTable.getCheckboxReserveRecords()],
})
},
checkboxRangeEnd(e) {
const xTable = this.$refs.xTable
const isAllSelected = this.currentSelected.every((item) => {
const _idx = this.lastSelected.findIndex((ele) => _.isEqual(ele, item))
return _idx > -1
})
if (isAllSelected) {
xTable.setCheckboxRow(this.currentSelected, false)
}
this.currentSelected = []
this.lastSelected = []
this.$emit('checkbox-range-end', {
...e,
records: [...xTable.getCheckboxRecords(), ...xTable.getCheckboxReserveRecords()],
})
},
},
}
</script>
<style lang="less"></style>
<template>
<vxe-table v-bind="$attrs" v-on="new$listeners" ref="xTable">
<slot></slot>
<template #empty>
<slot name="empty">
<div :style="{ paddingTop: '10px' }">
<img :style="{ width: '140px', height: '120px' }" :src="require('@/assets/data_empty.png')" />
<div>{{ $t('noData') }}</div>
</div>
</slot>
</template>
<template #loading>
<slot name="loading"></slot>
</template>
</vxe-table>
</template>
<script>
import _ from 'lodash'
// 该组件使用方法与vxe-table一致但调用它的方法时需先调用getVxetableRef()获取到vxe-table实体
export default {
name: 'OpsTable',
data() {
return {
// isShifting: false,
// lastIndex: -1,
lastSelected: [],
currentSelected: [],
}
},
computed: {
new$listeners() {
if (!Object.keys(this.$listeners).length) {
return this.$listeners
}
return Object.assign(this.$listeners, {
// 在这里覆盖原有的change事件
// 'checkbox-change': this.selectChangeEvent,
'checkbox-range-change': this.checkboxRangeChange,
'checkbox-range-start': this.checkboxRangeStart,
'checkbox-range-end': this.checkboxRangeEnd,
})
},
},
mounted() {
// window.onkeydown = (e) => {
// if (e.key === 'Shift') {
// this.isShifting = true
// }
// }
// window.onkeyup = (e) => {
// if (e.key === 'Shift') {
// this.isShifting = false
// this.lastIndex = -1
// }
// }
},
beforeDestroy() {
// window.onkeydown = ''
// window.onkeyup = ''
},
methods: {
getVxetableRef() {
return this.$refs.xTable
},
// selectChangeEvent(e) {
// const xTable = this.$refs.xTable
// const { lastIndex } = this
// const currentIndex = e.rowIndex
// const { tableData } = xTable.getTableData()
// if (lastIndex > -1 && this.isShifting) {
// let start = lastIndex
// let end = currentIndex
// if (lastIndex > currentIndex) {
// start = currentIndex
// end = lastIndex
// }
// const rangeData = tableData.slice(start, end + 1)
// xTable.setCheckboxRow(rangeData, true)
// }
// this.lastIndex = currentIndex
// this.$emit('checkbox-change', { ...e, records: xTable.getCheckboxRecords() })
// },
checkboxRangeStart(e) {
const xTable = this.$refs.xTable
const lastSelected = xTable.getCheckboxRecords()
const selectedReserve = xTable.getCheckboxReserveRecords()
this.lastSelected = [...lastSelected, ...selectedReserve]
this.$emit('checkbox-range-start', e)
},
checkboxRangeChange(e) {
const xTable = this.$refs.xTable
xTable.setCheckboxRow(this.lastSelected, true)
this.currentSelected = e.records
// this.lastSelected = [...new Set([...this.lastSelected, ...e.records])]
this.$emit('checkbox-range-change', {
...e,
records: [...xTable.getCheckboxRecords(), ...xTable.getCheckboxReserveRecords()],
})
},
checkboxRangeEnd(e) {
const xTable = this.$refs.xTable
const isAllSelected = this.currentSelected.every((item) => {
const _idx = this.lastSelected.findIndex((ele) => _.isEqual(ele, item))
return _idx > -1
})
if (isAllSelected) {
xTable.setCheckboxRow(this.currentSelected, false)
}
this.currentSelected = []
this.lastSelected = []
this.$emit('checkbox-range-end', {
...e,
records: [...xTable.getCheckboxRecords(), ...xTable.getCheckboxReserveRecords()],
})
},
},
}
</script>
<style lang="less"></style>

View File

@@ -109,9 +109,25 @@ export default {
default: 'default',
tip: 'Tip',
cmdbSearch: 'Search',
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,25 @@ export default {
default: '默认',
tip: '提示',
cmdbSearch: '搜索一下',
exception: {
backToHome: '返回首页',
desc1: '抱歉,你无权访问该页面',
desc2: '抱歉,你访问的页面不存在或仍在开发中',
desc3: '抱歉,服务器出错了'
},
pagination: {
total: '当前展示 {range0}-{range1} 条数据, 共 {total} 条'
},
components: {
colorTagSelectTip: '选择或输入(回车确定)标签',
database: '数据库',
system: '操作系统',
language: '语言',
status: '状态',
commonComponent: '常用组件',
data: '数据',
cloud: '云'
},
topMenu: {
personalCenter: '个人中心',
logout: '退出登录',

View File

@@ -226,16 +226,16 @@ export default {
right: 4px;
display: none;
&:hover {
color: #1f78d1;
color: @primary-color;
}
}
&:hover .ant-transfer-list-icon {
display: inline;
background-color: #c0eaff;
background-color: @primary-color_4;
border-radius: 4px;
}
}
.ant-transfer-list-content-item-selected {
background-color: #f0faff;
background-color: ~'@{primary-color_8}35';
}
</style>

View File

@@ -195,11 +195,11 @@ export default {
background-color: #fff;
height: calc(100vh - 64px);
margin-bottom: -24px;
padding: 24px;
padding: 20px;
.acl-resource-types-header {
width: 100%;
display: inline-flex;
margin-bottom: 15px;
margin-bottom: 20px;
align-items: center;
}
}

View File

@@ -358,11 +358,11 @@ export default {
background-color: #fff;
height: calc(100vh - 64px);
margin-bottom: -24px;
padding: 12px 24px 24px 24px;
padding: 8px 20px 20px 20px;
.acl-resources-header {
width: 100%;
display: inline-flex;
margin-bottom: 15px;
margin-bottom: 20px;
align-items: center;
justify-content: space-between;
.ant-switch {

View File

@@ -58,10 +58,7 @@
:title="$t('acl.visualRole')"
:width="120"
align="center"
:filters="[
{ label: $t('yes'), value: 1 },
{ label: $t('no'), value: 0 },
]"
:filters="visualRoleFilters"
:filterMultiple="false"
:filter-method="
({ value, row }) => {
@@ -155,6 +152,10 @@ export default {
pageSizeOptions: ['20', '50', '100', '200'],
searchName: '',
filterTableValue: { user_role: 1, user_only: 0 },
visualRoleFilters: [
{ label: this.$t('yes'), value: 1 },
{ label: this.$t('no'), value: 0 }
]
}
},
computed: {
@@ -291,11 +292,11 @@ export default {
background-color: #fff;
height: calc(100vh - 64px);
margin-bottom: -24px;
padding: 24px;
padding: 20px;
.acl-roles-header {
width: 100%;
display: inline-flex;
margin-bottom: 15px;
margin-bottom: 20px;
align-items: center;
.ant-checkbox-wrapper {
margin-left: auto;

View File

@@ -326,11 +326,11 @@ export default {
background-color: #fff;
height: calc(100vh - 64px);
margin-bottom: -24px;
padding: 24px;
padding: 20px;
.acl-trigger-header {
width: 100%;
display: inline-flex;
margin-bottom: 15px;
margin-bottom: 20px;
align-items: center;
}
}

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;
@@ -187,7 +188,7 @@ export default {
width: 12px;
height: 12px;
background-color: @primary-color;
border: solid 3px #E2E7FC;
border: solid 3px @primary-color_4;
border-radius: 50%;
}

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,150 +1,156 @@
<template>
<div class="ci-type-grant">
<vxe-table
ref="xTable"
size="mini"
stripe
class="ops-stripe-table"
:data="filterTableData"
:max-height="`${tableHeight}px`"
:row-style="(params) => getCurrentRowStyle(params, addedRids)"
>
<vxe-column field="name"></vxe-column>
<vxe-column v-for="col in columns" :key="col" :field="col" :title="permMap[col]">
<template #default="{row}">
<ReadCheckbox
v-if="['read'].includes(col.split('_')[0])"
:value="row[col.split('_')[0]]"
:valueKey="col"
:rid="row.rid"
@openReadGrantModal="() => openReadGrantModal(col, row)"
/>
<a-checkbox v-else-if="col === 'grant'" :checked="row[col]" @click="clickGrant(col, row)"></a-checkbox>
<a-checkbox @change="(e) => handleChange(e, col, row)" v-else v-model="row[col]"></a-checkbox>
</template>
</vxe-column>
<template #empty>
<div v-if="loading()" style="height: 200px; line-height: 200px;color:#2F54EB">
<a-icon type="loading" /> {{ $t('loading') }}
</div>
<div v-else>
<img :style="{ width: '100px' }" :src="require('@/assets/data_empty.png')" />
<div>{{ $t('noData') }}</div>
</div>
</template>
</vxe-table>
<a-space>
<span class="grant-button" @click="grantDepart">{{ $t('cmdb.components.grantUser') }}</span>
<span class="grant-button" @click="grantRole">{{ $t('cmdb.components.grantRole') }}</span>
</a-space>
</div>
</template>
<script>
import _ from 'lodash'
import { permMap } from './constants.js'
import { grantCiType, revokeCiType } from '../../api/CIType'
import ReadCheckbox from './readCheckbox.vue'
import { getCurrentRowStyle } from './utils'
export default {
name: 'CiTypeGrant',
components: { ReadCheckbox },
inject: ['loading', 'isModal'],
props: {
CITypeId: {
type: Number,
default: null,
},
tableData: {
type: Array,
default: () => [],
},
grantType: {
type: String,
default: 'ci_type',
},
addedRids: {
type: Array,
default: () => [],
},
},
computed: {
filterTableData() {
const _tableData = this.tableData.filter((data) => {
const _intersection = _.intersection(
Object.keys(data),
this.columns.map((col) => col.split('_')[0])
)
return _intersection && _intersection.length
})
return _.uniqBy(_tableData, (item) => item.rid)
},
columns() {
if (this.grantType === 'ci_type') {
return ['config', 'grant']
}
return ['read_attr', 'read_ci', 'create', 'update', 'delete']
},
windowHeight() {
return this.$store.state.windowHeight
},
tableHeight() {
if (this.isModal) {
return (this.windowHeight - 104) / 2
}
return (this.windowHeight - 104) / 2 - 116
},
permMap() {
return permMap()
}
},
methods: {
getCurrentRowStyle,
async handleChange(e, col, row) {
if (e.target.checked) {
await grantCiType(this.CITypeId, row.rid, { perms: [col] }).catch(() => {
this.$emit('getTableData')
})
} else {
await revokeCiType(this.CITypeId, row.rid, { perms: [col] }).catch(() => {
this.$emit('getTableData')
})
}
},
grantDepart() {
this.$emit('grantDepart', this.grantType)
},
grantRole() {
this.$emit('grantRole', this.grantType)
},
openReadGrantModal(col, row) {
this.$emit('openReadGrantModal', col, row)
},
clickGrant(col, row, rowIndex) {
if (!row[col]) {
this.handleChange({ target: { checked: true } }, col, row)
const _idx = this.tableData.findIndex((item) => item.rid === row.rid)
this.$set(this.tableData, _idx, { ...this.tableData[_idx], grant: true })
} else {
const that = this
this.$confirm({
title: that.$t('warning'),
content: that.$t('cmdb.components.confirmRevoke', { name: `${row.name}` }),
onOk() {
that.handleChange({ target: { checked: false } }, col, row)
const _idx = that.tableData.findIndex((item) => item.rid === row.rid)
that.$set(that.tableData, _idx, { ...that.tableData[_idx], grant: false })
},
})
}
},
},
}
</script>
<style lang="less" scoped>
.ci-type-grant {
padding: 10px 0;
}
</style>
<template>
<div class="ci-type-grant">
<vxe-table
ref="xTable"
size="mini"
stripe
class="ops-stripe-table"
:data="filterTableData"
:max-height="`${tableHeight}px`"
:row-class-name="(params) => getCurrentRowClass(params, addedRids)"
>
<vxe-column field="name"></vxe-column>
<vxe-column v-for="col in columns" :key="col" :field="col" :title="permMap[col]">
<template #default="{row}">
<ReadCheckbox
v-if="['read'].includes(col.split('_')[0])"
:value="row[col.split('_')[0]]"
:valueKey="col"
:rid="row.rid"
@openReadGrantModal="() => openReadGrantModal(col, row)"
/>
<a-checkbox v-else-if="col === 'grant'" :checked="row[col]" @click="clickGrant(col, row)"></a-checkbox>
<a-checkbox @change="(e) => handleChange(e, col, row)" v-else v-model="row[col]"></a-checkbox>
</template>
</vxe-column>
<template #empty>
<div v-if="loading()" class="ci-type-grant-loading">
<a-icon type="loading" /> {{ $t('loading') }}
</div>
<div v-else>
<img :style="{ width: '100px' }" :src="require('@/assets/data_empty.png')" />
<div>{{ $t('noData') }}</div>
</div>
</template>
</vxe-table>
<a-space>
<span class="grant-button" @click="grantDepart">{{ $t('cmdb.components.grantUser') }}</span>
<span class="grant-button" @click="grantRole">{{ $t('cmdb.components.grantRole') }}</span>
</a-space>
</div>
</template>
<script>
import _ from 'lodash'
import { permMap } from './constants.js'
import { grantCiType, revokeCiType } from '../../api/CIType'
import ReadCheckbox from './readCheckbox.vue'
import { getCurrentRowClass } from './utils'
export default {
name: 'CiTypeGrant',
components: { ReadCheckbox },
inject: ['loading', 'isModal'],
props: {
CITypeId: {
type: Number,
default: null,
},
tableData: {
type: Array,
default: () => [],
},
grantType: {
type: String,
default: 'ci_type',
},
addedRids: {
type: Array,
default: () => [],
},
},
computed: {
filterTableData() {
const _tableData = this.tableData.filter((data) => {
const _intersection = _.intersection(
Object.keys(data),
this.columns.map((col) => col.split('_')[0])
)
return _intersection && _intersection.length
})
return _.uniqBy(_tableData, (item) => item.rid)
},
columns() {
if (this.grantType === 'ci_type') {
return ['config', 'grant']
}
return ['read_attr', 'read_ci', 'create', 'update', 'delete']
},
windowHeight() {
return this.$store.state.windowHeight
},
tableHeight() {
if (this.isModal) {
return (this.windowHeight - 104) / 2
}
return (this.windowHeight - 104) / 2 - 116
},
permMap() {
return permMap()
}
},
methods: {
getCurrentRowClass,
async handleChange(e, col, row) {
if (e.target.checked) {
await grantCiType(this.CITypeId, row.rid, { perms: [col] }).catch(() => {
this.$emit('getTableData')
})
} else {
await revokeCiType(this.CITypeId, row.rid, { perms: [col] }).catch(() => {
this.$emit('getTableData')
})
}
},
grantDepart() {
this.$emit('grantDepart', this.grantType)
},
grantRole() {
this.$emit('grantRole', this.grantType)
},
openReadGrantModal(col, row) {
this.$emit('openReadGrantModal', col, row)
},
clickGrant(col, row, rowIndex) {
if (!row[col]) {
this.handleChange({ target: { checked: true } }, col, row)
const _idx = this.tableData.findIndex((item) => item.rid === row.rid)
this.$set(this.tableData, _idx, { ...this.tableData[_idx], grant: true })
} else {
const that = this
this.$confirm({
title: that.$t('warning'),
content: that.$t('cmdb.components.confirmRevoke', { name: `${row.name}` }),
onOk() {
that.handleChange({ target: { checked: false } }, col, row)
const _idx = that.tableData.findIndex((item) => item.rid === row.rid)
that.$set(that.tableData, _idx, { ...that.tableData[_idx], grant: false })
},
})
}
},
},
}
</script>
<style lang="less" scoped>
.ci-type-grant {
padding: 10px 0;
&-loading {
height: 200px;
line-height: 200px;
color: @primary-color;
}
}
</style>

View File

@@ -1,371 +1,382 @@
<template>
<div class="cmdb-grant" :style="{ }">
<template v-if="cmdbGrantType.includes('ci_type')">
<div class="cmdb-grant-title">{{ $t('cmdb.components.ciTypeGrant') }}</div>
<CiTypeGrant
:CITypeId="CITypeId"
:tableData="tableData"
grantType="ci_type"
@grantDepart="grantDepart"
@grantRole="grantRole"
@getTableData="getTableData"
ref="grant_ci_type"
:addedRids="addedRids"
/>
</template>
<template
v-if="
cmdbGrantType.includes('ci_type,ci') || (cmdbGrantType.includes('ci') && !cmdbGrantType.includes('ci_type'))
"
>
<div class="cmdb-grant-title">{{ $t('cmdb.components.ciGrant') }}</div>
<CiTypeGrant
:CITypeId="CITypeId"
:tableData="tableData"
grantType="ci"
@grantDepart="grantDepart"
@grantRole="grantRole"
@getTableData="getTableData"
@openReadGrantModal="openReadGrantModal"
ref="grant_ci"
:addedRids="addedRids"
/>
</template>
<template v-if="cmdbGrantType.includes('type_relation')">
<div class="cmdb-grant-title">{{ $t('cmdb.components.relationGrant') }}</div>
<TypeRelationGrant
:typeRelationIds="typeRelationIds"
:tableData="tableData"
grantType="type_relation"
@grantDepart="grantDepart"
@grantRole="grantRole"
@getTableData="getTableData"
ref="grant_type_relation"
:addedRids="addedRids"
/>
</template>
<template v-if="cmdbGrantType.includes('relation_view')">
<div class="cmdb-grant-title">{{ resourceTypeName }}{{ $t('cmdb.components.perm') }}</div>
<RelationViewGrant
:resourceTypeName="resourceTypeName"
:tableData="tableData"
grantType="relation_view"
@grantDepart="grantDepart"
@grantRole="grantRole"
@getTableData="getTableData"
ref="grant_relation_view"
:addedRids="addedRids"
/>
</template>
<template v-if="cmdbGrantType.includes('TopologyView')">
<div class="cmdb-grant-title">{{ resourceTypeName }}{{ $t('cmdb.components.perm') }}</div>
<TopologyViewGrant
:resourceTypeName="resourceTypeName"
:tableData="tableData"
:viewId="CITypeId"
grantType="TopologyView"
@grantDepart="grantDepart"
@grantRole="grantRole"
@getTableData="getTableData"
ref="grantTopologyView"
:addedRids="addedRids"
/>
</template>
<GrantModal ref="grantModal" @handleOk="handleOk" />
<ReadGrantModal ref="readGrantModal" :CITypeId="CITypeId" @updateTableDataRead="updateTableDataRead" />
</div>
</template>
<script>
import { mapState } from 'vuex'
import CiTypeGrant from './ciTypeGrant.vue'
import TypeRelationGrant from './typeRelationGrant.vue'
import { searchResource } from '@/modules/acl/api/resource'
import { getResourcePerms } from '@/modules/acl/api/permission'
import GrantModal from './grantModal.vue'
import ReadGrantModal from './readGrantModal'
import RelationViewGrant from './relationViewGrant.vue'
import TopologyViewGrant from './topologyViewGrant.vue'
import { getCITypeGroupById, ciTypeFilterPermissions } from '../../api/CIType'
export default {
name: 'GrantComp',
components: { CiTypeGrant, TypeRelationGrant, RelationViewGrant, TopologyViewGrant, GrantModal, ReadGrantModal },
props: {
CITypeId: {
type: Number,
default: null,
},
resourceTypeName: {
type: String,
default: '',
},
resourceType: {
type: String,
default: 'CIType',
},
app_id: {
type: String,
default: 'cmdb',
},
cmdbGrantType: {
type: String,
default: 'ci_type,ci',
},
typeRelationIds: {
type: Array,
default: null,
},
isModal: {
type: Boolean,
default: false,
},
},
inject: ['resource_type'],
data() {
return {
tableData: [],
grantType: '',
resource_id: null,
attrGroup: [],
filerPerimissions: {},
loading: false,
addedRids: [], // added rid this time
}
},
computed: {
...mapState({
allEmployees: (state) => state.user.allEmployees,
allDepartments: (state) => state.user.allDepartments,
}),
child_resource_type() {
return this.resource_type()
},
windowHeight() {
return this.$store.state.windowHeight
},
},
provide() {
return {
attrGroup: () => {
return this.attrGroup
},
filerPerimissions: () => {
return this.filerPerimissions
},
loading: () => {
return this.loading
},
isModal: this.isModal,
}
},
watch: {
resourceTypeName: {
immediate: true,
handler() {
this.init()
},
},
CITypeId: {
immediate: true,
handler() {
if (this.CITypeId && this.cmdbGrantType.includes('ci')) {
this.getFilterPermissions()
this.getAttrGroup()
}
},
},
},
mounted() {},
methods: {
getAttrGroup() {
getCITypeGroupById(this.CITypeId, { need_other: true }).then((res) => {
this.attrGroup = res
})
},
getFilterPermissions() {
ciTypeFilterPermissions(this.CITypeId).then((res) => {
this.filerPerimissions = res
})
},
async init() {
const _find = this.child_resource_type.groups.find((item) => item.name === this.resourceType)
const resource_type_id = _find?.id ?? 0
const res = await searchResource({
app_id: this.app_id,
resource_type_id,
page_size: 9999,
})
const _tempFind = res.resources.find((item) => item.name === this.resourceTypeName)
console.log(this.resourceTypeName)
this.resource_id = _tempFind?.id || 0
this.getTableData()
},
async getTableData() {
this.loading = true
const _tableData = await getResourcePerms(this.resource_id, { need_users: 0 })
const perms = []
for (const key in _tableData) {
const obj = {}
obj.name = key
_tableData[key].perms.forEach((perm) => {
obj[`${perm.name}`] = true
obj.rid = perm?.rid ?? null
})
perms.push(obj)
}
this.tableData = perms
this.loading = false
},
// Grant the department in common-setting and get the roleid from it
grantDepart(grantType) {
this.$refs.grantModal.open('depart')
this.grantType = grantType
},
// Grant the oldest role permissions
grantRole(grantType) {
this.$refs.grantModal.open('role')
this.grantType = grantType
},
handleOk(params, type) {
const { grantType } = this
let rids
if (type === 'depart') {
rids = [
...params.department.map((rid) => {
const _find = this.allDepartments.find((dep) => dep.acl_rid === rid)
return { rid, name: _find?.department_name ?? rid }
}),
...params.user.map((rid) => {
const _find = this.allEmployees.find((dep) => dep.acl_rid === rid)
return { rid, name: _find?.nickname ?? rid }
}),
]
}
if (type === 'role') {
rids = [
...params.map((role) => {
return { rid: role.id, name: role.name }
}),
]
}
if (grantType === 'ci_type') {
this.tableData.unshift(
...rids.map(({ rid, name }) => {
const _find = this.tableData.find((item) => item.rid === rid)
return {
rid,
name,
conifg: false,
grant: false,
..._find,
}
})
)
}
if (grantType === 'ci') {
this.tableData.unshift(
...rids.map(({ rid, name }) => {
const _find = this.tableData.find((item) => item.rid === rid)
return {
rid,
name,
read_attr: false,
read_ci: false,
create: false,
update: false,
delete: false,
..._find,
}
})
)
}
if (grantType === 'type_relation') {
this.tableData.unshift(
...rids.map(({ rid, name }) => {
return {
rid,
name,
create: false,
grant: false,
delete: false,
}
})
)
}
if (grantType === 'relation_view') {
this.tableData.unshift(
...rids.map(({ rid, name }) => {
return {
rid,
name,
read: false,
grant: false,
}
})
)
}
if (grantType === 'TopologyView') {
this.tableData.unshift(
...rids.map(({ rid, name }) => {
return {
rid,
name,
read: false,
update: false,
delete: false,
grant: false,
}
})
)
}
this.addedRids = rids
this.$nextTick(() => {
setTimeout(() => {
this.$refs[`grant_${grantType}`].$refs.xTable.elemStore['main-body-wrapper'].scrollTo(0, 0)
}, 300)
})
},
openReadGrantModal(col, row) {
this.$refs.readGrantModal.open(col, row)
},
updateTableDataRead(row, hasRead) {
const _idx = this.tableData.findIndex((item) => item.rid === row.rid)
this.$set(this.tableData, _idx, { ...this.tableData[_idx], read: hasRead })
this.getFilterPermissions()
},
},
}
</script>
<style lang="less" scoped>
.cmdb-grant {
position: relative;
padding: 0 20px;
overflow: auto;
.cmdb-grant-title {
border-left: 4px solid @primary-color;
padding-left: 10px;
}
}
</style>
<style lang="less">
.cmdb-grant {
.grant-button {
padding: 6px 8px;
color: @primary-color;
background-color: @primary-color_5;
border-radius: 2px;
cursor: pointer;
margin: 15px 0;
display: inline-block;
transition: all 0.3s;
&:hover {
box-shadow: 2px 3px 4px @primary-color_5;
}
}
}
</style>
<template>
<div class="cmdb-grant" :style="{ }">
<template v-if="cmdbGrantType.includes('ci_type')">
<div class="cmdb-grant-title">{{ $t('cmdb.components.ciTypeGrant') }}</div>
<CiTypeGrant
:CITypeId="CITypeId"
:tableData="tableData"
grantType="ci_type"
@grantDepart="grantDepart"
@grantRole="grantRole"
@getTableData="getTableData"
ref="grant_ci_type"
:addedRids="addedRids"
/>
</template>
<template
v-if="
cmdbGrantType.includes('ci_type,ci') || (cmdbGrantType.includes('ci') && !cmdbGrantType.includes('ci_type'))
"
>
<div class="cmdb-grant-title">{{ $t('cmdb.components.ciGrant') }}</div>
<CiTypeGrant
:CITypeId="CITypeId"
:tableData="tableData"
grantType="ci"
@grantDepart="grantDepart"
@grantRole="grantRole"
@getTableData="getTableData"
@openReadGrantModal="openReadGrantModal"
ref="grant_ci"
:addedRids="addedRids"
/>
</template>
<template v-if="cmdbGrantType.includes('type_relation')">
<div class="cmdb-grant-title">{{ $t('cmdb.components.relationGrant') }}</div>
<TypeRelationGrant
:typeRelationIds="typeRelationIds"
:tableData="tableData"
grantType="type_relation"
@grantDepart="grantDepart"
@grantRole="grantRole"
@getTableData="getTableData"
ref="grant_type_relation"
:addedRids="addedRids"
/>
</template>
<template v-if="cmdbGrantType.includes('relation_view')">
<div class="cmdb-grant-title">{{ resourceTypeName }}{{ $t('cmdb.components.perm') }}</div>
<RelationViewGrant
:resourceTypeName="resourceTypeName"
:tableData="tableData"
grantType="relation_view"
@grantDepart="grantDepart"
@grantRole="grantRole"
@getTableData="getTableData"
ref="grant_relation_view"
:addedRids="addedRids"
/>
</template>
<template v-if="cmdbGrantType.includes('TopologyView')">
<div class="cmdb-grant-title">{{ resourceTypeName }}{{ $t('cmdb.components.perm') }}</div>
<TopologyViewGrant
:resourceTypeName="resourceTypeName"
:tableData="tableData"
:viewId="CITypeId"
grantType="TopologyView"
@grantDepart="grantDepart"
@grantRole="grantRole"
@getTableData="getTableData"
ref="grantTopologyView"
:addedRids="addedRids"
/>
</template>
<GrantModal ref="grantModal" @handleOk="handleOk" />
<ReadGrantModal ref="readGrantModal" :CITypeId="CITypeId" @updateTableDataRead="updateTableDataRead" />
</div>
</template>
<script>
import { mapState } from 'vuex'
import CiTypeGrant from './ciTypeGrant.vue'
import TypeRelationGrant from './typeRelationGrant.vue'
import { searchResource } from '@/modules/acl/api/resource'
import { getResourcePerms } from '@/modules/acl/api/permission'
import GrantModal from './grantModal.vue'
import ReadGrantModal from './readGrantModal'
import RelationViewGrant from './relationViewGrant.vue'
import TopologyViewGrant from './topologyViewGrant.vue'
import { getCITypeGroupById, ciTypeFilterPermissions } from '../../api/CIType'
import { CI_DEFAULT_ATTR } from '@/modules/cmdb/utils/const.js'
export default {
name: 'GrantComp',
components: { CiTypeGrant, TypeRelationGrant, RelationViewGrant, TopologyViewGrant, GrantModal, ReadGrantModal },
props: {
CITypeId: {
type: Number,
default: null,
},
resourceTypeName: {
type: String,
default: '',
},
resourceType: {
type: String,
default: 'CIType',
},
app_id: {
type: String,
default: 'cmdb',
},
cmdbGrantType: {
type: String,
default: 'ci_type,ci',
},
typeRelationIds: {
type: Array,
default: null,
},
isModal: {
type: Boolean,
default: false,
},
},
inject: ['resource_type'],
data() {
return {
tableData: [],
grantType: '',
resource_id: null,
attrGroup: [],
filerPerimissions: {},
loading: false,
addedRids: [], // added rid this time
}
},
computed: {
...mapState({
allEmployees: (state) => state.user.allEmployees,
allDepartments: (state) => state.user.allDepartments,
}),
child_resource_type() {
return this.resource_type()
},
windowHeight() {
return this.$store.state.windowHeight
},
},
provide() {
return {
attrGroup: () => {
return this.attrGroup
},
filerPerimissions: () => {
return this.filerPerimissions
},
loading: () => {
return this.loading
},
isModal: this.isModal,
}
},
watch: {
resourceTypeName: {
immediate: true,
handler() {
this.init()
},
},
CITypeId: {
immediate: true,
handler() {
if (this.CITypeId && this.cmdbGrantType.includes('ci')) {
this.getFilterPermissions()
this.getAttrGroup()
}
},
},
},
mounted() {},
methods: {
getAttrGroup() {
getCITypeGroupById(this.CITypeId, { need_other: true }).then((res) => {
this.attrGroup = res
})
},
getFilterPermissions() {
ciTypeFilterPermissions(this.CITypeId).then((res) => {
Object.keys(res).forEach((key) => {
const attr_filter = res?.[key]?.attr_filter
if (attr_filter?.length) {
res[key].attr_filter = attr_filter.filter((item) => ![CI_DEFAULT_ATTR.UPDATE_USER, CI_DEFAULT_ATTR.UPDATE_TIME].includes(item))
}
})
this.filerPerimissions = res
})
},
async init() {
const _find = this.child_resource_type.groups.find((item) => item.name === this.resourceType)
const resource_type_id = _find?.id ?? 0
const res = await searchResource({
app_id: this.app_id,
resource_type_id,
page_size: 9999,
})
const _tempFind = res.resources.find((item) => item.name === this.resourceTypeName)
console.log(this.resourceTypeName)
this.resource_id = _tempFind?.id || 0
this.getTableData()
},
async getTableData() {
this.loading = true
const _tableData = await getResourcePerms(this.resource_id, { need_users: 0 })
const perms = []
for (const key in _tableData) {
const obj = {}
obj.name = key
_tableData[key].perms.forEach((perm) => {
obj[`${perm.name}`] = true
obj.rid = perm?.rid ?? null
})
perms.push(obj)
}
this.tableData = perms
this.loading = false
},
// Grant the department in common-setting and get the roleid from it
grantDepart(grantType) {
this.$refs.grantModal.open('depart')
this.grantType = grantType
},
// Grant the oldest role permissions
grantRole(grantType) {
this.$refs.grantModal.open('role')
this.grantType = grantType
},
handleOk(params, type) {
const { grantType } = this
let rids
if (type === 'depart') {
rids = [
...params.department.map((rid) => {
const _find = this.allDepartments.find((dep) => dep.acl_rid === rid)
return { rid, name: _find?.department_name ?? rid }
}),
...params.user.map((rid) => {
const _find = this.allEmployees.find((dep) => dep.acl_rid === rid)
return { rid, name: _find?.nickname ?? rid }
}),
]
}
if (type === 'role') {
rids = [
...params.map((role) => {
return { rid: role.id, name: role.name }
}),
]
}
if (grantType === 'ci_type') {
this.tableData.unshift(
...rids.map(({ rid, name }) => {
const _find = this.tableData.find((item) => item.rid === rid)
return {
rid,
name,
conifg: false,
grant: false,
..._find,
}
})
)
}
if (grantType === 'ci') {
this.tableData.unshift(
...rids.map(({ rid, name }) => {
const _find = this.tableData.find((item) => item.rid === rid)
return {
rid,
name,
read_attr: false,
read_ci: false,
create: false,
update: false,
delete: false,
..._find,
}
})
)
}
if (grantType === 'type_relation') {
this.tableData.unshift(
...rids.map(({ rid, name }) => {
return {
rid,
name,
create: false,
grant: false,
delete: false,
}
})
)
}
if (grantType === 'relation_view') {
this.tableData.unshift(
...rids.map(({ rid, name }) => {
return {
rid,
name,
read: false,
grant: false,
}
})
)
}
if (grantType === 'TopologyView') {
this.tableData.unshift(
...rids.map(({ rid, name }) => {
return {
rid,
name,
read: false,
update: false,
delete: false,
grant: false,
}
})
)
}
this.addedRids = rids
this.$nextTick(() => {
setTimeout(() => {
this.$refs[`grant_${grantType}`].$refs.xTable.elemStore['main-body-wrapper'].scrollTo(0, 0)
}, 300)
})
},
openReadGrantModal(col, row) {
this.$refs.readGrantModal.open(col, row)
},
updateTableDataRead(row, hasRead) {
const _idx = this.tableData.findIndex((item) => item.rid === row.rid)
this.$set(this.tableData, _idx, { ...this.tableData[_idx], read: hasRead })
this.getFilterPermissions()
},
},
}
</script>
<style lang="less" scoped>
.cmdb-grant {
position: relative;
padding: 0 20px;
overflow: auto;
.cmdb-grant-title {
border-left: 4px solid @primary-color;
padding-left: 10px;
}
}
</style>
<style lang="less">
.cmdb-grant {
.grant-button {
padding: 6px 8px;
color: @primary-color;
background-color: @primary-color_5;
border-radius: 2px;
cursor: pointer;
margin: 15px 0;
display: inline-block;
transition: all 0.3s;
z-index: 1;
.btn-wave-hover(@primary-color_4, -1);
}
.grant-table-row-focus {
background-color: @primary-color_8;
}
}
</style>

View File

@@ -1,98 +1,98 @@
<template>
<div class="ci-relation-grant">
<vxe-table
ref="xTable"
size="mini"
stripe
class="ops-stripe-table"
:data="tableData"
:max-height="`${tableHeight}px`"
:row-style="(params) => getCurrentRowStyle(params, addedRids)"
>
<vxe-column field="name"></vxe-column>
<vxe-column v-for="col in columns" :key="col" :field="col" :title="permMap[col]">
<template #default="{row}">
<a-checkbox @change="(e) => handleChange(e, col, row)" v-model="row[col]"></a-checkbox>
</template>
</vxe-column>
</vxe-table>
<a-space>
<span class="grant-button" @click="grantDepart">{{ $t('cmdb.components.grantUser') }}</span>
<span class="grant-button" @click="grantRole">{{ $t('cmdb.components.grantRole') }}</span>
</a-space>
</div>
</template>
<script>
import { permMap } from './constants.js'
import { grantRelationView, revokeRelationView } from '../../api/preference.js'
import { getCurrentRowStyle } from './utils'
export default {
name: 'RelationViewGrant',
inject: ['loading', 'isModal'],
props: {
resourceTypeName: {
type: String,
default: '',
},
tableData: {
type: Array,
default: () => [],
},
grantType: {
type: String,
default: 'relation_view',
},
addedRids: {
type: Array,
default: () => [],
},
},
data() {
return {
columns: ['read', 'grant'],
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
tableHeight() {
if (this.isModal) {
return (this.windowHeight - 104) / 2
}
return (this.windowHeight - 104) / 2 - 116
},
permMap() {
return permMap()
}
},
methods: {
getCurrentRowStyle,
grantDepart() {
this.$emit('grantDepart', this.grantType)
},
grantRole() {
this.$emit('grantRole', this.grantType)
},
handleChange(e, col, row) {
if (e.target.checked) {
grantRelationView(row.rid, { perms: [col], name: this.resourceTypeName }).catch(() => {
this.$emit('getTableData')
})
} else {
revokeRelationView(row.rid, { perms: [col], name: this.resourceTypeName }).catch(() => {
this.$emit('getTableData')
})
}
},
},
}
</script>
<style lang="less" scoped>
.ci-relation-grant {
padding: 10px 0;
}
</style>
<template>
<div class="ci-relation-grant">
<vxe-table
ref="xTable"
size="mini"
stripe
class="ops-stripe-table"
:data="tableData"
:max-height="`${tableHeight}px`"
:row-class-name="(params) => getCurrentRowClass(params, addedRids)"
>
<vxe-column field="name"></vxe-column>
<vxe-column v-for="col in columns" :key="col" :field="col" :title="permMap[col]">
<template #default="{row}">
<a-checkbox @change="(e) => handleChange(e, col, row)" v-model="row[col]"></a-checkbox>
</template>
</vxe-column>
</vxe-table>
<a-space>
<span class="grant-button" @click="grantDepart">{{ $t('cmdb.components.grantUser') }}</span>
<span class="grant-button" @click="grantRole">{{ $t('cmdb.components.grantRole') }}</span>
</a-space>
</div>
</template>
<script>
import { permMap } from './constants.js'
import { grantRelationView, revokeRelationView } from '../../api/preference.js'
import { getCurrentRowClass } from './utils'
export default {
name: 'RelationViewGrant',
inject: ['loading', 'isModal'],
props: {
resourceTypeName: {
type: String,
default: '',
},
tableData: {
type: Array,
default: () => [],
},
grantType: {
type: String,
default: 'relation_view',
},
addedRids: {
type: Array,
default: () => [],
},
},
data() {
return {
columns: ['read', 'grant'],
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
tableHeight() {
if (this.isModal) {
return (this.windowHeight - 104) / 2
}
return (this.windowHeight - 104) / 2 - 116
},
permMap() {
return permMap()
}
},
methods: {
getCurrentRowClass,
grantDepart() {
this.$emit('grantDepart', this.grantType)
},
grantRole() {
this.$emit('grantRole', this.grantType)
},
handleChange(e, col, row) {
if (e.target.checked) {
grantRelationView(row.rid, { perms: [col], name: this.resourceTypeName }).catch(() => {
this.$emit('getTableData')
})
} else {
revokeRelationView(row.rid, { perms: [col], name: this.resourceTypeName }).catch(() => {
this.$emit('getTableData')
})
}
},
},
}
</script>
<style lang="less" scoped>
.ci-relation-grant {
padding: 10px 0;
}
</style>

View File

@@ -1,122 +1,118 @@
<template>
<a-modal :visible="visible" @cancel="handleCancel" @ok="handleOK" :title="$t('revoke')">
<a-form-model :model="form" :label-col="{ span: 4 }" :wrapper-col="{ span: 16 }">
<a-form-model-item :label="$t('user')">
<EmployeeTreeSelect
class="custom-treeselect custom-treeselect-bgcAndBorder"
:style="{
'--custom-height': '32px',
lineHeight: '32px',
'--custom-bg-color': '#fff',
'--custom-border': '1px solid #d9d9d9',
'--custom-multiple-lineHeight': '18px',
}"
:multiple="true"
v-model="form.users"
:placeholder="$t('cmdb.serviceTree.userPlaceholder')"
:idType="2"
departmentKey="acl_rid"
employeeKey="acl_rid"
/>
</a-form-model-item>
<a-form-model-item :label="$t('role')">
<treeselect
v-model="form.roles"
:multiple="true"
:options="filterAllRoles"
class="custom-treeselect custom-treeselect-bgcAndBorder"
:style="{
'--custom-height': '32px',
lineHeight: '32px',
'--custom-bg-color': '#fff',
'--custom-border': '1px solid #d9d9d9',
'--custom-multiple-lineHeight': '18px',
}"
:limit="10"
:limitText="(count) => `+ ${count}`"
:normalizer="
(node) => {
return {
id: node.id,
label: node.name,
}
}
"
appendToBody
zIndex="1050"
:placeholder="$t('cmdb.serviceTree.rolePlaceholder')"
@search-change="searchRole"
/>
</a-form-model-item>
</a-form-model>
</a-modal>
</template>
<script>
import EmployeeTreeSelect from '@/views/setting/components/employeeTreeSelect.vue'
import { getAllDepAndEmployee } from '@/api/company'
import { searchRole } from '@/modules/acl/api/role'
export default {
name: 'RevokeModal',
components: { EmployeeTreeSelect },
data() {
return {
visible: false,
form: {
users: undefined,
roles: undefined,
},
allTreeDepAndEmp: [],
allRoles: [],
filterAllRoles: [],
}
},
provide() {
return {
provide_allTreeDepAndEmp: () => {
return this.allTreeDepAndEmp
},
}
},
mounted() {
this.getAllDepAndEmployee()
this.loadRoles()
},
methods: {
async loadRoles() {
const res = await searchRole({ page_size: 9999, app_id: 'cmdb', is_all: true })
this.allRoles = res.roles
this.filterAllRoles = this.allRoles.slice(0, 100)
},
getAllDepAndEmployee() {
getAllDepAndEmployee({ block: 0 }).then((res) => {
this.allTreeDepAndEmp = res
})
},
open() {
this.visible = true
this.$nextTick(() => {
this.form = {
users: undefined,
roles: undefined,
}
})
},
handleCancel() {
this.visible = false
},
searchRole(searchQuery) {
this.filterAllRoles = this.allRoles
.filter((item) => item.name.toLowerCase().includes(searchQuery.toLowerCase()))
.slice(0, 100)
},
handleOK() {
this.$emit('handleRevoke', this.form)
this.handleCancel()
},
},
}
</script>
<style></style>
<template>
<a-modal :visible="visible" @cancel="handleCancel" @ok="handleOK" :title="$t('revoke')">
<a-form-model :model="form" :label-col="{ span: 4 }" :wrapper-col="{ span: 16 }">
<a-form-model-item :label="$t('user')">
<EmployeeTreeSelect
class="custom-treeselect custom-treeselect-white"
:style="{
'--custom-height': '32px',
lineHeight: '32px',
'--custom-multiple-lineHeight': '18px',
}"
:multiple="true"
v-model="form.users"
:placeholder="$t('cmdb.serviceTree.userPlaceholder')"
:idType="2"
departmentKey="acl_rid"
employeeKey="acl_rid"
/>
</a-form-model-item>
<a-form-model-item :label="$t('role')">
<treeselect
v-model="form.roles"
:multiple="true"
:options="filterAllRoles"
class="custom-treeselect custom-treeselect-white"
:style="{
'--custom-height': '32px',
lineHeight: '32px',
'--custom-multiple-lineHeight': '18px',
}"
:limit="10"
:limitText="(count) => `+ ${count}`"
:normalizer="
(node) => {
return {
id: node.id,
label: node.name,
}
}
"
appendToBody
zIndex="1050"
:placeholder="$t('cmdb.serviceTree.rolePlaceholder')"
@search-change="searchRole"
/>
</a-form-model-item>
</a-form-model>
</a-modal>
</template>
<script>
import EmployeeTreeSelect from '@/views/setting/components/employeeTreeSelect.vue'
import { getAllDepAndEmployee } from '@/api/company'
import { searchRole } from '@/modules/acl/api/role'
export default {
name: 'RevokeModal',
components: { EmployeeTreeSelect },
data() {
return {
visible: false,
form: {
users: undefined,
roles: undefined,
},
allTreeDepAndEmp: [],
allRoles: [],
filterAllRoles: [],
}
},
provide() {
return {
provide_allTreeDepAndEmp: () => {
return this.allTreeDepAndEmp
},
}
},
mounted() {
this.getAllDepAndEmployee()
this.loadRoles()
},
methods: {
async loadRoles() {
const res = await searchRole({ page_size: 9999, app_id: 'cmdb', is_all: true })
this.allRoles = res.roles
this.filterAllRoles = this.allRoles.slice(0, 100)
},
getAllDepAndEmployee() {
getAllDepAndEmployee({ block: 0 }).then((res) => {
this.allTreeDepAndEmp = res
})
},
open() {
this.visible = true
this.$nextTick(() => {
this.form = {
users: undefined,
roles: undefined,
}
})
},
handleCancel() {
this.visible = false
},
searchRole(searchQuery) {
this.filterAllRoles = this.allRoles
.filter((item) => item.name.toLowerCase().includes(searchQuery.toLowerCase()))
.slice(0, 100)
},
handleOK() {
this.$emit('handleRevoke', this.form)
this.handleCancel()
},
},
}
</script>
<style></style>

View File

@@ -8,7 +8,7 @@
:data="tableData"
:max-height="`${tableHeight}px`"
:scroll-y="{enabled: true}"
:row-style="(params) => getCurrentRowStyle(params, addedRids)"
:row-class-name="(params) => getCurrentRowClass(params, addedRids)"
>
<vxe-column field="name"></vxe-column>
<vxe-column v-for="col in columns" :key="col" :field="col" :title="permMap[col]">
@@ -27,7 +27,7 @@
<script>
import { permMap } from './constants.js'
import { grantTopologyView, revokeTopologyView } from '@/modules/cmdb/api/topology.js'
import { getCurrentRowStyle } from './utils'
import { getCurrentRowClass } from './utils'
export default {
name: 'TopologyViewGrant',
inject: ['loading', 'isModal'],
@@ -73,7 +73,7 @@ export default {
}
},
methods: {
getCurrentRowStyle,
getCurrentRowClass,
grantDepart() {
this.$emit('grantDepart', this.grantType)
},

View File

@@ -1,100 +1,100 @@
<template>
<div class="ci-relation-grant">
<vxe-table
ref="xTable"
size="mini"
stripe
class="ops-stripe-table"
:data="tableData"
:max-height="`${tableHeight}px`"
:row-style="(params) => getCurrentRowStyle(params, addedRids)"
>
<vxe-column field="name"></vxe-column>
<vxe-column v-for="col in columns" :key="col" :field="col" :title="permMap[col]">
<template #default="{row}">
<a-checkbox @change="(e) => handleChange(e, col, row)" v-model="row[col]"></a-checkbox>
</template>
</vxe-column>
</vxe-table>
<a-space>
<span class="grant-button" @click="grantDepart">{{ $t('cmdb.components.grantUser') }}</span>
<span class="grant-button" @click="grantRole">{{ $t('cmdb.components.grantRole') }}</span>
</a-space>
</div>
</template>
<script>
import { permMap } from './constants.js'
import { grantTypeRelation, revokeTypeRelation } from '../../api/CITypeRelation.js'
import { getCurrentRowStyle } from './utils'
export default {
name: 'TypeRelationGrant',
inject: ['loading', 'isModal'],
props: {
tableData: {
type: Array,
default: () => [],
},
grantType: {
type: String,
default: 'type_relation',
},
typeRelationIds: {
type: Array,
default: null,
},
addedRids: {
type: Array,
default: () => [],
},
},
data() {
return {
columns: ['create', 'grant', 'delete'],
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
tableHeight() {
if (this.isModal) {
return (this.windowHeight - 104) / 2
}
return (this.windowHeight - 104) / 2 - 116
},
permMap() {
return permMap()
}
},
methods: {
getCurrentRowStyle,
grantDepart() {
this.$emit('grantDepart', this.grantType)
},
grantRole() {
this.$emit('grantRole', this.grantType)
},
handleChange(e, col, row) {
const first = this.typeRelationIds[0]
const second = this.typeRelationIds[1]
if (e.target.checked) {
grantTypeRelation(first, second, row.rid, { perms: [col] }).catch(() => {
this.$emit('getTableData')
})
} else {
revokeTypeRelation(first, second, row.rid, { perms: [col] }).catch(() => {
this.$emit('getTableData')
})
}
},
},
}
</script>
<style lang="less" scoped>
.ci-relation-grant {
padding: 10px 0;
}
</style>
<template>
<div class="ci-relation-grant">
<vxe-table
ref="xTable"
size="mini"
stripe
class="ops-stripe-table"
:data="tableData"
:max-height="`${tableHeight}px`"
:row-class-name="(params) => getCurrentRowClass(params, addedRids)"
>
<vxe-column field="name"></vxe-column>
<vxe-column v-for="col in columns" :key="col" :field="col" :title="permMap[col]">
<template #default="{row}">
<a-checkbox @change="(e) => handleChange(e, col, row)" v-model="row[col]"></a-checkbox>
</template>
</vxe-column>
</vxe-table>
<a-space>
<span class="grant-button" @click="grantDepart">{{ $t('cmdb.components.grantUser') }}</span>
<span class="grant-button" @click="grantRole">{{ $t('cmdb.components.grantRole') }}</span>
</a-space>
</div>
</template>
<script>
import { permMap } from './constants.js'
import { grantTypeRelation, revokeTypeRelation } from '../../api/CITypeRelation.js'
import { getCurrentRowClass } from './utils'
export default {
name: 'TypeRelationGrant',
inject: ['loading', 'isModal'],
props: {
tableData: {
type: Array,
default: () => [],
},
grantType: {
type: String,
default: 'type_relation',
},
typeRelationIds: {
type: Array,
default: null,
},
addedRids: {
type: Array,
default: () => [],
},
},
data() {
return {
columns: ['create', 'grant', 'delete'],
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
tableHeight() {
if (this.isModal) {
return (this.windowHeight - 104) / 2
}
return (this.windowHeight - 104) / 2 - 116
},
permMap() {
return permMap()
}
},
methods: {
getCurrentRowClass,
grantDepart() {
this.$emit('grantDepart', this.grantType)
},
grantRole() {
this.$emit('grantRole', this.grantType)
},
handleChange(e, col, row) {
const first = this.typeRelationIds[0]
const second = this.typeRelationIds[1]
if (e.target.checked) {
grantTypeRelation(first, second, row.rid, { perms: [col] }).catch(() => {
this.$emit('getTableData')
})
} else {
revokeTypeRelation(first, second, row.rid, { perms: [col] }).catch(() => {
this.$emit('getTableData')
})
}
},
},
}
</script>
<style lang="less" scoped>
.ci-relation-grant {
padding: 10px 0;
}
</style>

View File

@@ -3,12 +3,10 @@
:disabled="disabled"
ref="cmdb_type_select"
:disable-branch-nodes="true"
class="custom-treeselect custom-treeselect-bgcAndBorder"
class="custom-treeselect custom-treeselect-white"
:style="{
'--custom-height': '30px',
lineHeight: '30px',
'--custom-bg-color': '#fff',
'--custom-border': '1px solid #d9d9d9',
lineHeight: '30px'
}"
v-model="currenCiType"
:multiple="multiple"

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

@@ -16,12 +16,8 @@
<template v-for="(item, index) in preferenceSearchList.slice(0, 3)">
<span
v-if="item.name.length > 6"
class="preference-search-tag"
:class="['preference-search-tag', item.id === currentPreferenceSearch ? 'preference-search-tag-focus' : '']"
:key="`${item.id}_${index}`"
:style="{
backgroundColor: item.id === currentPreferenceSearch ? '#2f54eb' : '',
color: item.id === currentPreferenceSearch ? '#fff' : '',
}"
>
<a-tooltip :title="item.name">
<span @click="clickPreferenceSearch(item)">{{ `${item.name.slice(0, 6)}...` }}</span>
@@ -33,11 +29,7 @@
<span
v-else
:key="`${item.id}_${index}`"
class="preference-search-tag"
:style="{
backgroundColor: item.id === currentPreferenceSearch ? '#2f54eb' : '#fafafa',
color: item.id === currentPreferenceSearch ? '#fff' : '#000000a6',
}"
:class="['preference-search-tag', item.id === currentPreferenceSearch ? 'preference-search-tag-focus' : '']"
>
<span @click="clickPreferenceSearch(item)">{{ item.name }}</span>
<a-popconfirm :title="$t('cmdb.ciType.confirmDelete2')" @confirm="deletePreferenceSearch(item)">
@@ -189,6 +181,15 @@ export default {
> i {
font-size: 12px;
}
&:hover {
color: @primary-color;
}
&-focus {
background-color: @primary-color;
color: #FFFFFF !important;
}
}
.preference-search-delete {
color: #a9a9a9;

View File

@@ -5,13 +5,11 @@
<a-space>
<treeselect
v-if="type === 'resourceSearch'"
class="custom-treeselect custom-treeselect-bgcAndBorder"
class="custom-treeselect"
:style="{
width: '200px',
marginRight: '10px',
'--custom-height': '32px',
'--custom-bg-color': '#fff',
'--custom-border': '1px solid #d9d9d9',
'--custom-multiple-lineHeight': '16px',
}"
v-model="currenCiType"
@@ -55,7 +53,7 @@
<a-icon
type="search"
slot="suffix"
:style="{ color: fuzzySearch ? '#2f54eb' : '#d9d9d9', cursor: 'pointer' }"
:class="['search-form-bar-input-icon', fuzzySearch ? 'search-form-bar-input-icon-focus' : '']"
@click="emitRefresh"
/>
<a-tooltip slot="prefix" placement="bottom" :overlayStyle="{ maxWidth: '550px', whiteSpace: 'pre-line' }">
@@ -310,6 +308,16 @@ export default {
justify-content: space-between;
align-items: center;
height: 32px;
&-input-icon {
cursor: pointer;
color: #d9d9d9;
&-focus {
color: @primary-color;
}
}
.search-form-bar-filter {
.ops_display_wrapper(transparent);

View File

@@ -135,9 +135,15 @@ export default {
border: 1px solid #f3f4f6;
}
.authorization-input {
border: none;
border: 1px solid transparent;
&:focus {
box-shadow: none;
border-color: @primary-color;
}
&:hover {
border-color: @primary-color;
}
}
}

View File

@@ -29,7 +29,7 @@
<tr v-for="(item, index) in headers" :key="item.id">
<td><a-input class="headers-input" v-model="item.key" :placeholder="$t('cmdb.components.param', { param: `${index + 1}` })" /></td>
<td><a-input class="headers-input" v-model="item.value" :placeholder="$t('cmdb.components.value', { value: `${index + 1}` })" /></td>
<td>
<td class="headers-delete">
<a style="color:red">
<ops-icon type="icon-xianxing-delete" @click="deleteParam(index)" />
</a>
@@ -92,10 +92,20 @@ export default {
border: 1px solid #f3f4f6;
}
.headers-input {
border: none;
border: 1px solid transparent;
&:focus {
box-shadow: none;
border-color: @primary-color;
}
&:hover {
border-color: @primary-color;
}
}
.headers-delete {
text-align: center;
}
}
</style>

View File

@@ -3,12 +3,10 @@
<a-input-group compact>
<treeselect
:disable-branch-nodes="true"
class="custom-treeselect custom-treeselect-bgcAndBorder"
class="custom-treeselect custom-treeselect-white"
:style="{
'--custom-height': '30px',
lineHeight: '30px',
'--custom-bg-color': '#fff',
'--custom-border': '1px solid #d9d9d9',
display: 'inline-block',
width: '100px',
}"

View File

@@ -23,7 +23,7 @@
<tr v-for="(item, index) in parameters" :key="item.id">
<td><a-input class="parameters-input" v-model="item.key" :placeholder="$t('cmdb.components.param', { param: `${index + 1}` })" /></td>
<td><a-input class="parameters-input" v-model="item.value" :placeholder="$t('cmdb.components.value', { value: `${index + 1}` })" /></td>
<td>
<td class="parameters-delete">
<a style="color:red">
<ops-icon type="icon-xianxing-delete" @click="deleteParam(index)" />
</a>
@@ -91,10 +91,20 @@ export default {
border: 1px solid #f3f4f6;
}
.parameters-input {
border: none;
border: 1px solid transparent;
&:focus {
box-shadow: none;
border-color: @primary-color;
}
&:hover {
border-color: @primary-color;
}
}
.parameters-delete {
text-align: center;
}
}
</style>

View File

@@ -26,7 +26,8 @@ const cmdb_en = {
ad: 'AutoDiscovery',
cidetail: 'CI Detail',
scene: 'Scene',
dcim: 'DCIM'
dcim: 'DCIM',
serviceTree: 'Service Tree'
},
ciType: {
ciType: 'CIType',
@@ -76,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',
@@ -252,6 +254,25 @@ const cmdb_en = {
checkModalColumn4: 'Last checkup time',
testModalTitle: 'Automated discovery testing',
attrMapTableAttrPlaceholder: 'Please edit the name',
SNMPConfiguration: 'SNMP Configuration',
nodeList: 'Node List',
defaultVersion: 'Default Version',
defaultCommunity: 'Default Community',
timeout: 'Timeout',
retryCount: 'Retry Count',
scanningConfiguration: 'Scanning Configuration',
initialNode: 'Initial Node',
defaultGateway: 'Default Gateway',
recursiveOrNot: 'Recursive Or Not',
recursiveTip: 'Scanning Configuration: When disabling recursion, the node list must be configured.',
maximumDepth: 'Maximum Depth',
snmpFormTip1: 'If SNMP is not the default, Community and version need to be configured separately',
snmpFormTip2: 'Timeout for establishing SNMP connection',
snmpFormTip3: 'Number of retries to establish an SNMP connection',
snmpFormTip4: 'The first node to start scanning, or recursively from the default gateway if unconfigured',
snmpFormTip5: 'Enabled by default to discover all network devices and topology relationships as much as possible, and disabled to scan only the devices in the node list',
snmpFormTip6: 'Depth of network device topology',
snmpFormTip7: 'The results of the scan are filtered with CIDR, not filtered if not configured. Format: 192.168.1.0/24',
nodeSettingIp: 'Network device IP address',
nodeSettingIpTip: 'Please enter the ip address',
nodeSettingIpTip1: 'ip address format error',
@@ -314,6 +335,10 @@ const cmdb_en = {
enum: 'Enum',
ciGrantTip: `Filter conditions can be changed dynamically using {{}} referenced variables, currently user variables are supported, such as {{user.uid}},{{user.username}},{{user.email}},{{user.nickname}}`,
searchInputTip: 'Please search for resource keywords',
columnSearchInputTip: '192.168.1.1\n192.168.1.2\n192.168.1.3',
rowSearchMode: 'Single Row',
columnSearchMode: 'Multi Row',
columnSearchTip: 'Supports retrieval of short texts only',
resourceSearch: 'Resource Search',
recentSearch: 'Recent Search',
myCollection: 'My Collection',
@@ -696,6 +721,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',

View File

@@ -26,7 +26,8 @@ const cmdb_zh = {
ad: '自动发现',
cidetail: 'CI 详情',
scene: '场景',
dcim: '数据中心'
dcim: '数据中心',
serviceTree: '服务树'
},
ciType: {
ciType: '模型',
@@ -76,6 +77,7 @@ const cmdb_zh = {
confirmDeleteADT: '确认删除 【{pluginName}】',
attributeMap: '字段映射',
nodeConfig: '节点配置',
scanningParameter: '扫描参数',
autoDiscovery: '自动发现属性',
node: '节点',
adExecConfig: '执行配置',
@@ -252,6 +254,25 @@ const cmdb_zh = {
checkModalColumn4: '最近检查时间',
testModalTitle: '自动发现测试',
attrMapTableAttrPlaceholder: '请编辑名称',
SNMPConfiguration: 'SNMP配置',
nodeList: '节点列表',
defaultVersion: '默认版本',
defaultCommunity: '默认 Community',
timeout: '超时时间',
retryCount: '重试次数',
scanningConfiguration: '扫描配置',
initialNode: '初始节点',
defaultGateway: '默认网关',
recursiveOrNot: '是否递归',
recursiveTip: '扫描配置关闭递归时, 必须配置节点列表',
maximumDepth: '最大深度',
snmpFormTip1: '如果不是默认的SNMP, Community和版本需要单独配置',
snmpFormTip2: '建立SNMP连接的超时时间',
snmpFormTip3: '建立SNMP连接的重试次数',
snmpFormTip4: '开始扫描的第一个节点,如果不配置则是从默认网关开始递归扫描',
snmpFormTip5: '默认开启,表示尽可能发现所有网络设备和拓扑关系, 如果关闭,则仅扫描节点列表里的设备',
snmpFormTip6: '网络设备拓扑的深度',
snmpFormTip7: '扫描的结果用CIDR进行过滤不配置则不会过滤。格式: 192.168.1.0/24',
nodeSettingIp: '网络设备IP地址',
nodeSettingIpTip: '请输入 ip 地址',
nodeSettingIpTip1: 'ip地址格式错误',
@@ -314,6 +335,10 @@ const cmdb_zh = {
enum: '枚举',
ciGrantTip: `筛选条件可使用{{}}引用变量实现动态变化,目前支持用户变量,如{{user.uid}},{{user.username}},{{user.email}},{{user.nickname}}`,
searchInputTip: '请搜索资源关键字',
columnSearchInputTip: '192.168.1.1\n192.168.1.2\n192.168.1.3',
rowSearchMode: '单行',
columnSearchMode: '多行',
columnSearchTip: '仅支持检索短文本',
resourceSearch: '资源搜索',
recentSearch: '最近搜索',
myCollection: '我的收藏',
@@ -695,6 +720,9 @@ if __name__ == "__main__":
batchRollbacking: '正在回滚,共{total}个,成功{successNum}个,失败{errorNum}个',
baselineTips: '该时间点的变更也会被回滚, 唯一标识、密码属性、动态属性不支持回滚',
cover: '覆盖',
detail: '详情',
upstream: '上游',
downstream: '下游'
},
serviceTree: {
remove: '移除',

View File

@@ -27,6 +27,17 @@ const genCmdbRoutes = async () => {
name: 'cmdb_disabled1',
meta: { title: 'cmdb.menu.resources', disabled: true },
},
{
path: '/cmdb/relationviews/:viewId?',
name: 'cmdb_relation_views',
component: () => import('../views/relation_views/index'),
meta: {
title: 'cmdb.menu.serviceTree',
appName: 'cmdb',
icon: 'veops-servicetree',
keepAlive: false
},
},
{
path: '/cmdb/resourceviews',
name: 'cmdb_resource_views',
@@ -60,7 +71,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')
},
{
@@ -194,15 +205,14 @@ const genCmdbRoutes = async () => {
} else {
routes.redirect = '/cmdb/dashboard'
}
const relationViews = relation.name2id.map(item => {
return {
path: `/cmdb/relationviews/${item[1]}`,
name: `cmdb_relation_views_${item[1]}`,
component: () => import('../views/relation_views/index'),
meta: { title: item[0], icon: 'ops-cmdb-relation', selectedIcon: 'ops-cmdb-relation', keepAlive: false, name: item[0] },
if (relation?.name2id?.length === 0) {
const relationViewRouteIndex = routes.children?.findIndex?.((route) => route.name === 'cmdb_relation_views')
if (relationViewRouteIndex >= 0) {
routes.children.splice(relationViewRouteIndex, 1)
}
})
routes.children.splice(resourceViewsIndex, 0, ...relationViews)
}
return routes
}

View File

@@ -14,7 +14,7 @@
<p v-html="$t('cmdb.batch.drawTips1')"></p>
<p v-html="$t('cmdb.batch.drawTips2')"></p>
<div v-for="item in fileList" :key="item.name" class="cmdb-batch-upload-dragger-file">
<span><a-icon type="file" :style="{ color: '#2F54EB', marginRight: '5px' }" />{{ item.name }}</span>
<span><a-icon type="file" class="cmdb-batch-upload-dragger-file-icon"/>{{ item.name }}</span>
<a-progress :status="progressStatus" :percent="percent" />
</div>
</a-upload-dragger>
@@ -92,18 +92,17 @@ export default {
}
.ant-upload.ant-upload-drag {
border: none;
background: linear-gradient(90deg, @text-color_5 50%, transparent 0) repeat-x,
linear-gradient(90deg, @text-color_5 50%, transparent 0) repeat-x,
linear-gradient(0deg, @text-color_5 50%, transparent 0) repeat-y,
linear-gradient(0deg, @text-color_5 50%, transparent 0) repeat-y;
background-size: 15px 1px, 15px 1px, 1px 15px, 1px 15px;
background-position: 0 0, 0 100%, 0 0, 100% 0;
background: ~'linear-gradient(90deg, @{text-color_5} 50%, transparent 0) repeat-x 0 0 / 15px 1px, linear-gradient(90deg, @{text-color_5} 50%, transparent 0) repeat-x 0 100% / 15px 1px, linear-gradient(0deg, @{text-color_5} 50%, transparent 0) repeat-y 0 0 / 1px 15px, linear-gradient(0deg, @{text-color_5} 50%, transparent 0) repeat-y 100% 0 / 1px 15px';
.ant-upload-drag-container > i {
font-size: 60px;
}
.cmdb-batch-upload-tips {
color: @primary-color;
}
&:hover {
background: ~'linear-gradient(90deg, @{primary-color_2} 50%, transparent 0) repeat-x 0 0 / 15px 1px, linear-gradient(90deg, @{primary-color_2} 50%, transparent 0) repeat-x 0 100% / 15px 1px, linear-gradient(0deg, @{primary-color_2} 50%, transparent 0) repeat-y 0 0 / 1px 15px, linear-gradient(0deg, @{primary-color_2} 50%, transparent 0) repeat-y 100% 0 / 1px 15px, @{primary-color_7}';
}
}
.ant-upload.ant-upload-drag .ant-upload-drag-container {
vertical-align: baseline;
@@ -129,6 +128,11 @@ export default {
white-space: nowrap;
margin-right: 10px;
}
&-icon {
color: @primary-color;
margin-right: 5px;
}
}
.cmdb-batch-upload-tips {
width: 50%;

View File

@@ -9,22 +9,18 @@
"
:title="$t('cmdb.ci.attributeDesc')"
width="72%"
:bodyStyle="{ height: '100vh' }"
:bodyStyle="{ height: '100vh', paddingTop: '16px' }"
>
<vxe-toolbar>
<template #buttons>
<a-input
v-model="searchKey"
:style="{ display: 'inline-block', width: '244px' }"
class="ops-input ops-input-radius"
type="search"
:placeholder="$t('cmdb.ci.tips5')"
@keyup="searchAttributes"
>
<a-icon type="search" slot="suffix" />
</a-input>
</template>
</vxe-toolbar>
<a-input
v-model="searchKey"
:style="{ display: 'inline-block', width: '244px', marginBottom: '16px' }"
class="ops-input ops-input-radius"
type="search"
:placeholder="$t('cmdb.ci.tips5')"
@keyup="searchAttributes"
>
<a-icon type="search" slot="suffix" />
</a-input>
<a-spin :spinning="loading">
<vxe-table

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,35 +6,85 @@
{{ $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">
<span slot="tab"><a-icon type="clock-circle" />{{ $t('cmdb.ci.history') }}</span>
<div :style="{ padding: '24px', height: '100%' }">
<a-space :style="{ 'margin-bottom': '10px', display: 'flex' }">
<div :style="{ padding: '16px 24px 24px', height: '100%' }">
<a-space :style="{ marginBottom: '16px', display: 'flex' }">
<a-button type="primary" class="ops-button-ghost" ghost @click="handleRollbackCI()">
<ops-icon type="shishizhuangtai" />{{ $t('cmdb.ci.rollback') }}
</a-button>
@@ -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: {
@@ -180,7 +238,7 @@ export default {
ci_types: [],
hasPermission: true,
itsmInstalled: true,
tableHeight: this.attributeHistoryTableHeight || (this.$store.state.windowHeight - 120),
tableHeight: this.attributeHistoryTableHeight || (this.$store.state.windowHeight - 130),
initQueryLoading: true,
}
},
@@ -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

@@ -95,7 +95,7 @@
attr.name,
{
rules: [{ required: attr.is_required, message: $t('placeholder1') + `${attr.alias || attr.name}` }],
initialValue: attr.default && attr.default.default ? attr.default.default : null,
initialValue: attr.default && attr.default.default !== undefined && attr.default.default !== null ? attr.default.default : null,
},
]"
style="width: 100%"
@@ -148,6 +148,7 @@
</template>
<script>
import _ from 'lodash'
import moment from 'moment'
import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue'
import CIReferenceAttr from '@/components/ciReferenceAttr/index.vue'
@@ -210,7 +211,7 @@ export default {
},
getChoiceDefault(attr) {
if (!attr?.default?.default) {
if (_.isNil(attr?.default?.default)) {
return attr.is_list ? [] : null
}

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

@@ -61,16 +61,20 @@ export default {
justify-content: center;
font-size: 12px;
font-weight: 400;
color: #4E5969;
background-color: #F7F8FA;
color: @text-color_2;
background-color: @primary-color_7;
width: 105px;
height: 32px;
cursor: pointer;
&-active {
border: solid 1px #B1C9FF;
background-color: #E1EFFF;
color: #2F54EB;
border: solid 1px @primary-color_8;
background-color: @primary-color_4;
color: @primary-color;
}
&:hover {
color: @primary-color;
}
}
}

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