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

|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 搭建
|
||||
|
||||
+ 方案一:Docker 一键快速构建
|
||||
|
||||
- 第1步: 安装 Docker 环境和 Docker Compose(v2)
|
||||
- 第2步: 拷贝项目代码, `git clone https://github.com/veops/cmdb.git`
|
||||
- 第3步:进入主目录并启动, `docker compose up -d`
|
||||
|
||||
+ 方案二:[本地开发环境搭建](docs/local.md)
|
||||
+ 方案三:[Makefile 安装](docs/makefile.md)
|
||||
|
||||
### 2. 访问
|
||||
- 打开浏览器并访问: [http://127.0.0.1:8000](http://127.0.0.1:8000)
|
||||
- 用户名: demo 或者 admin
|
||||
- 密码: 123456
|
||||
|
||||
## 接入公司
|
||||
|
||||
> 欢迎使用开源CMDB的公司,在 [#112](https://github.com/veops/cmdb/issues/112) 登记
|
||||
+ 欢迎使用开源CMDB的公司和团队,在 [#112](https://github.com/veops/cmdb/issues/112) 登记
|
||||
|
||||
## 安装
|
||||
## 代码贡献
|
||||
我们欢迎所有开发者贡献代码,改善和扩展这个项目。请先阅读我们的[贡献指南](docs/CONTRIBUTING.md)。此外,您还可以通过社交媒体、活动和分享来支持 Veops 的开源。
|
||||
|
||||
### Docker 一键快速构建
|
||||
<a href="https://github.com/veops/cmdb/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=veops/cmdb" />
|
||||
</a>
|
||||
|
||||
[//]: # (> 方法一)
|
||||
- 第一步: 先安装 Docker 环境, 以及Docker Compose (v2)
|
||||
- 第二步: 拷贝项目
|
||||
```shell
|
||||
git clone https://github.com/veops/cmdb.git
|
||||
```
|
||||
- 第三步:进入主目录,执行:
|
||||
```
|
||||
docker compose up -d
|
||||
```
|
||||
## 更多开源
|
||||
|
||||
[//]: # (> 方法二, 该方法适用于linux系统)
|
||||
- [OneTerm](https://github.com/veops/oneterm): 一款简单、轻量、灵活的堡垒机服务。
|
||||
- [messenger](https://github.com/veops/messenger): 一个简单轻量的消息发送服务。
|
||||
- [ACL](https://github.com/veops/acl): 一个简单通用的权限管理系统设计与实践。
|
||||
|
||||
[//]: # (- 第一步: 先安装 Docker 环境, 以及Docker Compose (v2))
|
||||
## 相关文章
|
||||
|
||||
[//]: # (- 第二步: 直接使用项目根目录下的install.sh 文件进行 `安装`、`启动`、`暂停`、`查状态`、`删除`、`卸载`)
|
||||
- <a href="https://mp.weixin.qq.com/s/v3eANth64UBW5xdyOkK3tg" target="_blank">尽可能通用的运维CMDB的设计与实践(Ⅰ) - 概览</a>
|
||||
- <a href="https://mp.weixin.qq.com/s/rQaf4AES7YJsyNQG_MKOLg" target="_blank">尽可能通用的运维CMDB的设计与实践(ⅠⅠ) - 自动发现</a>
|
||||
- <a href="https://github.com/veops/cmdb/tree/master/docs/cmdb_api.md" target="_blank">CMDB接口文档</a>
|
||||
|
||||
[//]: # (```shell)
|
||||
更多文章可以在公众号 **维易科技OneOps** 里查看
|
||||
|
||||
[//]: # (curl -so install.sh https://raw.githubusercontent.com/veops/cmdb/deploy_on_kylin_docker/install.sh)
|
||||
## 与我联系
|
||||
|
||||
[//]: # (sh install.sh install)
|
||||
|
||||
[//]: # (```)
|
||||
|
||||
### [本地开发环境搭建](docs/local.md)
|
||||
|
||||
### [Makefile 安装](docs/makefile.md)
|
||||
|
||||
## 验证
|
||||
- 浏览器打开: [http://127.0.0.1:8000](http://127.0.0.1:8000)
|
||||
- username: demo 或者 admin
|
||||
- password: 123456
|
||||
|
||||
|
||||
---
|
||||
|
||||
_**欢迎关注公众号(维易科技OneOps),关注后可加入微信群,进行产品和技术交流。**_
|
||||
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/wechat.png" alt="公众号: 维易科技OneOps" />
|
||||
</p>
|
||||
+ 邮箱: <a href="mailto:bd@veops.cn">bd@veops.cn</a>
|
||||
+ 公众号:**维易科技OneOps**。关注后可以加入微信群,参与产品和技术交流
|
||||
<img src="docs/images/wechat.png" alt="公众号: 维易科技OneOps" />
|
79
cmdb-api/.ruff.toml
Normal file
79
cmdb-api/.ruff.toml
Normal 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
|
@@ -1,6 +1,8 @@
|
||||
import click
|
||||
from flask import current_app
|
||||
from flask.cli import with_appcontext
|
||||
|
||||
from api.lib.exception import AbortException
|
||||
from api.lib.perm.acl.user import UserCRUD
|
||||
|
||||
|
||||
@@ -46,11 +48,18 @@ def add_user():
|
||||
email = click.prompt('Enter email ', confirmation_prompt=False)
|
||||
is_admin = click.prompt('Admin (Y/N) ', confirmation_prompt=False, type=bool, default=False)
|
||||
|
||||
UserCRUD.add(username=username, password=password, email=email)
|
||||
current_app.test_request_context().push()
|
||||
|
||||
if is_admin:
|
||||
app = AppCache.get('acl') or App.create(name='acl')
|
||||
acl_admin = RoleCache.get_by_name(app.id, 'acl_admin') or RoleCRUD.add_role('acl_admin', app.id, True)
|
||||
rid = RoleCache.get_by_name(None, username).id
|
||||
try:
|
||||
|
||||
RoleRelationCRUD.add(acl_admin, acl_admin.id, [rid], app.id)
|
||||
UserCRUD.add(username=username, password=password, email=email)
|
||||
|
||||
if is_admin:
|
||||
app = AppCache.get('acl') or App.create(name='acl')
|
||||
acl_admin = RoleCache.get_by_name(None, 'acl_admin') or RoleCRUD.add_role('acl_admin', app.id, True)
|
||||
rid = RoleCache.get_by_name(None, username).id
|
||||
|
||||
RoleRelationCRUD.add(acl_admin, acl_admin.id, [rid], app.id)
|
||||
|
||||
except AbortException as e:
|
||||
print(f"Failed: {e}")
|
||||
|
@@ -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})
|
||||
|
@@ -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']:
|
||||
|
@@ -12,6 +12,7 @@ from sqlalchemy import func
|
||||
from api.extensions import db
|
||||
from api.lib.cmdb.auto_discovery.const import CLOUD_MAP
|
||||
from api.lib.cmdb.auto_discovery.const import DEFAULT_INNER
|
||||
from api.lib.cmdb.auto_discovery.const import NET_DEVICE_NAMES
|
||||
from api.lib.cmdb.auto_discovery.const import PRIVILEGED_USERS
|
||||
from api.lib.cmdb.cache import AttributeCache
|
||||
from api.lib.cmdb.cache import AutoDiscoveryMappingCache
|
||||
@@ -252,6 +253,7 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
|
||||
:return:
|
||||
"""
|
||||
result = []
|
||||
db.session.commit()
|
||||
rules = cls.cls.get_by(to_dict=True)
|
||||
|
||||
for rule in rules:
|
||||
@@ -718,6 +720,12 @@ class AutoDiscoveryCICRUD(DBMixin):
|
||||
|
||||
build_relations_for_ad_accept.apply_async(args=(adc.to_dict(), ci_id, ad_key2attr), queue=CMDB_QUEUE)
|
||||
|
||||
ci_type = CITypeCache.get(adc.type_id)
|
||||
if ci_type and ci_type.name in NET_DEVICE_NAMES and 'ports' in adc.instance:
|
||||
from api.tasks.cmdb import add_net_device_ports
|
||||
add_net_device_ports.apply_async(args=(ci_id, adc.instance['ports']),
|
||||
queue=CMDB_QUEUE)
|
||||
|
||||
adc.update(is_accept=True,
|
||||
accept_by=nickname or current_user.nickname,
|
||||
accept_time=datetime.datetime.now(),
|
||||
|
@@ -4,6 +4,8 @@ from api.lib.cmdb.const import AutoDiscoveryType
|
||||
|
||||
PRIVILEGED_USERS = ("cmdb_agent", "worker", "admin")
|
||||
|
||||
NET_DEVICE_NAMES = {"switch", 'router', 'firewall', 'printer'}
|
||||
|
||||
DEFAULT_INNER = [
|
||||
dict(name="阿里云", en="aliyun", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
|
||||
option={'icon': {'name': 'caise-aliyun'}, "en": "aliyun"}),
|
||||
@@ -41,8 +43,12 @@ DEFAULT_INNER = [
|
||||
option={'icon': {'name': 'caise-luyouqi'}}),
|
||||
dict(name="防火墙", type=AutoDiscoveryType.SNMP, is_inner=True, is_plugin=False,
|
||||
option={'icon': {'name': 'caise-fanghuoqiang'}}),
|
||||
dict(name="打印机", type=AutoDiscoveryType.SNMP, is_inner=True, is_plugin=False,
|
||||
option={'icon': {'name': 'caise-dayinji'}}),
|
||||
# dict(name="打印机", type=AutoDiscoveryType.SNMP, is_inner=True, is_plugin=False,
|
||||
# option={'icon': {'name': 'caise-dayinji'}}),
|
||||
dict(name="光纤交换机", type=AutoDiscoveryType.SNMP, is_inner=True, is_plugin=False,
|
||||
option={'icon': {'name': 'caise-fiber'}}),
|
||||
dict(name="F5", type=AutoDiscoveryType.SNMP, is_inner=True, is_plugin=False,
|
||||
option={'icon': {'name': 'caise-F5'}}),
|
||||
]
|
||||
|
||||
CLOUD_MAP = {
|
||||
|
@@ -1,37 +1,74 @@
|
||||
[{
|
||||
"name":"manufacturer",
|
||||
"type": "文本",
|
||||
"example":"HUAWEI Technology Co.,Ltd",
|
||||
"desc":"制造产商"
|
||||
},{
|
||||
"name":"sn",
|
||||
"type": "文本",
|
||||
"example":"102030059898",
|
||||
"desc":"设备序列号"
|
||||
},{
|
||||
"name":"device_name",
|
||||
"type": "文本",
|
||||
"example":"USG6525E",
|
||||
"desc":"设备名称"
|
||||
},{
|
||||
"name":"device_model",
|
||||
"type": "文本",
|
||||
"example":"2011.2.321.1.205",
|
||||
"desc":"设备细分类型 结合相关产商获取相应的产品类型"
|
||||
},{
|
||||
"name":"description",
|
||||
"type": "文本",
|
||||
"example":"Huawei Vwersatile Routing Platform Software",
|
||||
"desc":"设备描述"
|
||||
},{
|
||||
"name":"manager_ip",
|
||||
"type": "文本",
|
||||
"example":"192.168.1.1",
|
||||
"desc":"管理ip"
|
||||
}, {
|
||||
"name":"ips",
|
||||
"type": "文本、多值",
|
||||
"example":"192.168.1.1, 192.168.1.2",
|
||||
"desc":"ips"
|
||||
}
|
||||
[
|
||||
{
|
||||
"name": "manufacturer",
|
||||
"type": "文本",
|
||||
"example": "Huawei",
|
||||
"desc": "制造产商"
|
||||
},
|
||||
{
|
||||
"name": "sn",
|
||||
"type": "文本",
|
||||
"example": "102030059898",
|
||||
"desc": "设备序列号"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"type": "文本",
|
||||
"example": "USG6525E",
|
||||
"desc": "设备名称"
|
||||
},
|
||||
{
|
||||
"name": "model",
|
||||
"type": "文本",
|
||||
"example": "2011.2.321.1.205",
|
||||
"desc": "设备细分类型 结合相关产商获取相应的产品类型"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"type": "文本",
|
||||
"example": "Huawei Vwersatile Routing Platform Software",
|
||||
"desc": "设备描述"
|
||||
},
|
||||
{
|
||||
"name": "manager_ip",
|
||||
"type": "文本",
|
||||
"example": "192.168.1.1",
|
||||
"desc": "管理ip"
|
||||
},
|
||||
{
|
||||
"name": "ips",
|
||||
"type": "文本、多值",
|
||||
"example": "192.168.1.1, 192.168.1.2",
|
||||
"desc": "ips"
|
||||
},
|
||||
{
|
||||
"name": "uptime",
|
||||
"type": "文本",
|
||||
"example": "2023-04-15 10:00:00",
|
||||
"desc": "启动时间"
|
||||
},
|
||||
{
|
||||
"name": "snmp_version",
|
||||
"type": "文本",
|
||||
"example": "v2c",
|
||||
"desc": "SNMP版本"
|
||||
},
|
||||
{
|
||||
"name": "port_num",
|
||||
"type": "整数",
|
||||
"example": 24,
|
||||
"desc": "端口数量"
|
||||
},
|
||||
{
|
||||
"name": "ports",
|
||||
"type": "json",
|
||||
"example": "",
|
||||
"desc": "设备的端口列表"
|
||||
},
|
||||
{
|
||||
"name": "neighbors",
|
||||
"type": "json",
|
||||
"example": "",
|
||||
"desc": "设备的邻居列表"
|
||||
}
|
||||
]
|
@@ -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:
|
||||
@@ -360,16 +360,28 @@ class CIManager(object):
|
||||
_sync=False,
|
||||
**ci_dict):
|
||||
"""
|
||||
add ci
|
||||
:param ci_type_name:
|
||||
:param exist_policy: replace or reject or need
|
||||
:param _no_attribute_policy: ignore or reject
|
||||
:param is_auto_discovery: default is False
|
||||
:param _is_admin: default is False
|
||||
:param ticket_id:
|
||||
:param _sync:
|
||||
:param ci_dict:
|
||||
:return:
|
||||
Create a new Configuration Item (CI) or update existing based on unique constraints.
|
||||
|
||||
Handles complete CI creation workflow including validation, uniqueness checks,
|
||||
password encryption, computed attributes, relationship creation, and caching.
|
||||
|
||||
Args:
|
||||
ci_type_name (str): Name of the CI type to create
|
||||
exist_policy (ExistPolicy): How to handle existing CIs (REPLACE/REJECT/NEED)
|
||||
_no_attribute_policy (ExistPolicy): How to handle unknown attributes (IGNORE/REJECT)
|
||||
is_auto_discovery (bool): Whether CI is created by auto-discovery process
|
||||
_is_admin (bool): Whether to skip permission checks
|
||||
ticket_id (int, optional): Associated ticket ID for audit trail
|
||||
_sync (bool): Whether to execute cache/relation tasks synchronously
|
||||
**ci_dict: CI attribute values as key-value pairs
|
||||
|
||||
Returns:
|
||||
int: ID of the created or updated CI
|
||||
|
||||
Raises:
|
||||
400: If unique constraints violated, required attributes missing, or validation fails
|
||||
403: If user lacks permissions for restricted attributes
|
||||
404: If CI type not found or referenced CI not exists
|
||||
"""
|
||||
now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
ci_type = CITypeManager.check_is_existed(ci_type_name)
|
||||
@@ -393,7 +405,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
|
||||
@@ -512,6 +524,24 @@ class CIManager(object):
|
||||
return ci.id
|
||||
|
||||
def update(self, ci_id, _is_admin=False, ticket_id=None, _sync=False, **ci_dict):
|
||||
"""
|
||||
Update an existing Configuration Item with new attribute values.
|
||||
|
||||
Performs comprehensive CI update including validation, constraint checks,
|
||||
password handling, computed attributes processing, and change tracking.
|
||||
|
||||
Args:
|
||||
ci_id (int): ID of the CI to update
|
||||
_is_admin (bool): Whether to skip permission checks
|
||||
ticket_id (int, optional): Associated ticket ID for audit trail
|
||||
_sync (bool): Whether to execute cache/relation tasks synchronously
|
||||
**ci_dict: CI attribute values to update as key-value pairs
|
||||
|
||||
Raises:
|
||||
400: If unique constraints violated or validation fails
|
||||
403: If user lacks permissions for restricted attributes
|
||||
404: If CI not found
|
||||
"""
|
||||
now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
ci = self.confirm_ci_existed(ci_id)
|
||||
ci_type = ci.ci_type
|
||||
@@ -524,10 +554,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 +584,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 +1303,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)
|
||||
|
||||
@@ -1413,14 +1450,31 @@ class CIRelationManager(object):
|
||||
parent_attr = AttributeCache.get(parent_attr_id)
|
||||
child_attr = AttributeCache.get(child_attr_id)
|
||||
attr_value = ci_dict.get(parent_attr.name)
|
||||
if attr_value != 0 and not attr_value:
|
||||
continue
|
||||
value_table = TableMap(attr=child_attr).table
|
||||
for child in value_table.get_by(attr_id=child_attr.id, value=attr_value, only_query=True).join(
|
||||
CI, CI.id == value_table.ci_id).filter(CI.type_id == item.child_id):
|
||||
_relations.add((ci_dict['_id'], child.ci_id))
|
||||
attr_value_list = [attr_value] if not isinstance(attr_value, list) else attr_value
|
||||
|
||||
matching_cis = value_table.get_by(
|
||||
attr_id=child_attr.id,
|
||||
only_query=True
|
||||
).join(
|
||||
CI, CI.id == value_table.ci_id
|
||||
).filter(
|
||||
CI.type_id == item.child_id,
|
||||
value_table.value.in_(attr_value_list)
|
||||
).all()
|
||||
|
||||
for ci in matching_cis:
|
||||
_relations.add((ci_dict['_id'], ci.ci_id))
|
||||
|
||||
if relations is None:
|
||||
relations = _relations
|
||||
else:
|
||||
relations &= _relations
|
||||
if item.constraint == ConstraintEnum.Many2Many:
|
||||
relations |= _relations
|
||||
else:
|
||||
relations &= _relations
|
||||
|
||||
cls.delete_relations_by_source(RelationSourceEnum.ATTRIBUTE_VALUES,
|
||||
first_ci_id=ci_dict['_id'],
|
||||
@@ -1440,14 +1494,31 @@ class CIRelationManager(object):
|
||||
parent_attr = AttributeCache.get(parent_attr_id)
|
||||
child_attr = AttributeCache.get(child_attr_id)
|
||||
attr_value = ci_dict.get(child_attr.name)
|
||||
if attr_value != 0 and not attr_value:
|
||||
continue
|
||||
value_table = TableMap(attr=parent_attr).table
|
||||
for parent in value_table.get_by(attr_id=parent_attr.id, value=attr_value, only_query=True).join(
|
||||
CI, CI.id == value_table.ci_id).filter(CI.type_id == item.parent_id):
|
||||
_relations.add((parent.ci_id, ci_dict['_id']))
|
||||
attr_value_list = [attr_value] if not isinstance(attr_value, list) else attr_value
|
||||
|
||||
matching_cis = value_table.get_by(
|
||||
attr_id=parent_attr.id,
|
||||
only_query=True
|
||||
).join(
|
||||
CI, CI.id == value_table.ci_id
|
||||
).filter(
|
||||
CI.type_id == item.parent_id,
|
||||
value_table.value.in_(attr_value_list)
|
||||
).all()
|
||||
|
||||
for ci in matching_cis:
|
||||
_relations.add((ci.ci_id, ci_dict['_id']))
|
||||
|
||||
if relations is None:
|
||||
relations = _relations
|
||||
else:
|
||||
relations &= _relations
|
||||
if item.constraint == ConstraintEnum.Many2Many:
|
||||
relations |= _relations
|
||||
else:
|
||||
relations &= _relations
|
||||
|
||||
cls.delete_relations_by_source(RelationSourceEnum.ATTRIBUTE_VALUES,
|
||||
second_ci_id=ci_dict['_id'],
|
||||
|
@@ -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)
|
||||
|
@@ -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:
|
||||
|
@@ -92,7 +92,8 @@ class IpAddressManager(object):
|
||||
else:
|
||||
return abort(400, ErrFormat.ipam_address_model_not_found)
|
||||
|
||||
with (redis_lock.Lock(rd.r, "IPAM_ASSIGN_ADDRESS_{}".format(subnet_id))):
|
||||
with (redis_lock.Lock(rd.r, "IPAM_ASSIGN_ADDRESS_{}".format(subnet_id),
|
||||
expire=60, auto_renewal=True)):
|
||||
cis = self._get_cis(subnet_id, ips)
|
||||
ip2ci = {ci[IPAddressBuiltinAttributes.IP]: ci for ci in cis}
|
||||
|
||||
|
@@ -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)
|
||||
|
@@ -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,
|
||||
|
@@ -1,9 +1,8 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
import copy
|
||||
from collections import defaultdict
|
||||
|
||||
import copy
|
||||
import six
|
||||
import toposort
|
||||
from flask import abort
|
||||
from flask import current_app
|
||||
@@ -16,6 +15,7 @@ from api.lib.cmdb.cache import CITypeAttributesCache
|
||||
from api.lib.cmdb.cache import CITypeCache
|
||||
from api.lib.cmdb.cache import CMDBCounterCache
|
||||
from api.lib.cmdb.ci_type import CITypeAttributeManager
|
||||
from api.lib.cmdb.ci_type import CITypeManager
|
||||
from api.lib.cmdb.const import BUILTIN_ATTRIBUTES
|
||||
from api.lib.cmdb.const import ConstraintEnum
|
||||
from api.lib.cmdb.const import PermEnum
|
||||
@@ -29,6 +29,7 @@ from api.lib.perm.acl.acl import ACLManager
|
||||
from api.models.cmdb import CITypeGroup
|
||||
from api.models.cmdb import CITypeGroupItem
|
||||
from api.models.cmdb import CITypeRelation
|
||||
from api.models.cmdb import PreferenceAutoSubscriptionConfig
|
||||
from api.models.cmdb import PreferenceCITypeOrder
|
||||
from api.models.cmdb import PreferenceRelationView
|
||||
from api.models.cmdb import PreferenceSearchOption
|
||||
@@ -49,15 +50,27 @@ class PreferenceManager(object):
|
||||
type2group = {}
|
||||
for i in db.session.query(CITypeGroupItem, CITypeGroup).join(
|
||||
CITypeGroup, CITypeGroup.id == CITypeGroupItem.group_id).filter(
|
||||
CITypeGroup.deleted.is_(False)).filter(CITypeGroupItem.deleted.is_(False)):
|
||||
CITypeGroup.deleted.is_(False)).filter(CITypeGroupItem.deleted.is_(False)):
|
||||
type2group[i.CITypeGroupItem.type_id] = i.CITypeGroup.to_dict()
|
||||
|
||||
types = db.session.query(PreferenceShowAttributes.type_id).filter(
|
||||
PreferenceShowAttributes.uid == current_user.uid).filter(
|
||||
PreferenceShowAttributes.deleted.is_(False)).group_by(
|
||||
PreferenceShowAttributes.type_id).all() if instance else []
|
||||
types = sorted(types, key=lambda x: {i.type_id: idx for idx, i in enumerate(
|
||||
ci_type_order) if not i.is_tree}.get(x.type_id, 1))
|
||||
if instance:
|
||||
auto_types = PreferenceManager.get_auto_subscription_types(current_user.uid)
|
||||
if auto_types is not None:
|
||||
class TypeIdObj:
|
||||
def __init__(self, type_id):
|
||||
self.type_id = type_id
|
||||
|
||||
types = [TypeIdObj(t) for t in auto_types]
|
||||
else:
|
||||
types = db.session.query(PreferenceShowAttributes.type_id).filter(
|
||||
PreferenceShowAttributes.uid == current_user.uid).filter(
|
||||
PreferenceShowAttributes.deleted.is_(False)).group_by(
|
||||
PreferenceShowAttributes.type_id).all()
|
||||
else:
|
||||
types = []
|
||||
|
||||
types = sorted(types, key=lambda x: {i.type_id: idx for idx, i in enumerate(ci_type_order)
|
||||
if not i.is_tree}.get(x.type_id, 1))
|
||||
group_types = []
|
||||
other_types = []
|
||||
group2idx = {}
|
||||
@@ -104,19 +117,26 @@ class PreferenceManager(object):
|
||||
|
||||
ci_type_order = sorted(PreferenceCITypeOrder.get_by(uid=current_user.uid, to_dict=False), key=lambda x: x.order)
|
||||
if instance:
|
||||
types = db.session.query(PreferenceShowAttributes.type_id,
|
||||
PreferenceShowAttributes.uid, PreferenceShowAttributes.created_at).filter(
|
||||
PreferenceShowAttributes.deleted.is_(False)).filter(
|
||||
PreferenceShowAttributes.uid == current_user.uid).group_by(
|
||||
PreferenceShowAttributes.uid, PreferenceShowAttributes.type_id)
|
||||
for i in types:
|
||||
result['self']['instance'].append(i.type_id)
|
||||
if str(i.created_at) > str(result['self']['type_id2subs_time'].get(i.type_id, "")):
|
||||
result['self']['type_id2subs_time'][i.type_id] = i.created_at
|
||||
# Try auto subscription first, fallback to manual if not configured
|
||||
auto_types = PreferenceManager.get_auto_subscription_types(current_user.uid)
|
||||
if auto_types is not None:
|
||||
result['self']['instance'] = auto_types
|
||||
for type_id in auto_types:
|
||||
result['self']['type_id2subs_time'][type_id] = ""
|
||||
else:
|
||||
types = db.session.query(PreferenceShowAttributes.type_id,
|
||||
PreferenceShowAttributes.uid, PreferenceShowAttributes.created_at).filter(
|
||||
PreferenceShowAttributes.deleted.is_(False)).filter(
|
||||
PreferenceShowAttributes.uid == current_user.uid).group_by(
|
||||
PreferenceShowAttributes.uid, PreferenceShowAttributes.type_id)
|
||||
for i in types:
|
||||
result['self']['instance'].append(i.type_id)
|
||||
if str(i.created_at) > str(result['self']['type_id2subs_time'].get(i.type_id, "")):
|
||||
result['self']['type_id2subs_time'][i.type_id] = i.created_at
|
||||
|
||||
instance_order = [i.type_id for i in ci_type_order if not i.is_tree]
|
||||
if len(instance_order) == len(result['self']['instance']):
|
||||
result['self']['instance'] = instance_order
|
||||
instance_order = [i.type_id for i in ci_type_order if not i.is_tree]
|
||||
if len(instance_order) == len(result['self']['instance']):
|
||||
result['self']['instance'] = instance_order
|
||||
|
||||
if tree:
|
||||
types = PreferenceTreeView.get_by(uid=current_user.uid, to_dict=False)
|
||||
@@ -131,15 +151,73 @@ class PreferenceManager(object):
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def get_auto_subscription_types(uid):
|
||||
"""Get user's auto-subscribed CI types based on config rules"""
|
||||
config = PreferenceAutoSubscriptionConfig.get_by(
|
||||
uid=uid, enabled=True, first=True, to_dict=False
|
||||
)
|
||||
|
||||
if not config:
|
||||
return None
|
||||
|
||||
all_permitted_types = PreferenceManager._get_permitted_ci_types()
|
||||
result_types = PreferenceManager._apply_subscription_config(config, all_permitted_types)
|
||||
|
||||
return result_types
|
||||
|
||||
@staticmethod
|
||||
def _get_permitted_ci_types():
|
||||
"""Get CI types that user has read permission for"""
|
||||
from api.lib.perm.acl.acl import is_app_admin
|
||||
|
||||
if not current_app.config.get('USE_ACL') or is_app_admin('cmdb'):
|
||||
return [t['id'] for t in CITypeManager.get_ci_types()]
|
||||
|
||||
# Regular user: filter by permissions
|
||||
permitted_resources = ACLManager('cmdb').get_resources(ResourceTypeEnum.CI)
|
||||
permitted_names = {r.get('name') for r in permitted_resources}
|
||||
|
||||
return [ci_type_dict['id'] for ci_type_dict in CITypeManager.get_ci_types()
|
||||
if ci_type_dict['name'] in permitted_names]
|
||||
|
||||
@staticmethod
|
||||
def _apply_subscription_config(config, all_permitted_types):
|
||||
"""Apply subscription rules: 'all' mode excludes, 'none' mode includes"""
|
||||
result_types = set()
|
||||
|
||||
if config.base_strategy == 'all':
|
||||
# Start with all types, then exclude
|
||||
result_types = set(all_permitted_types)
|
||||
|
||||
if config.group_ids:
|
||||
exclude_group_type_ids = PreferenceManager._get_types_by_group_ids(config.group_ids)
|
||||
result_types.difference_update(exclude_group_type_ids)
|
||||
|
||||
if config.type_ids:
|
||||
result_types.difference_update(config.type_ids)
|
||||
|
||||
else: # base_strategy == 'none'
|
||||
# Start empty, then include
|
||||
if config.group_ids:
|
||||
include_group_type_ids = PreferenceManager._get_types_by_group_ids(config.group_ids)
|
||||
result_types.update(t for t in include_group_type_ids if t in all_permitted_types)
|
||||
|
||||
if config.type_ids:
|
||||
result_types.update(t for t in config.type_ids if t in all_permitted_types)
|
||||
|
||||
return list(result_types)
|
||||
|
||||
@staticmethod
|
||||
def _get_types_by_group_ids(group_ids):
|
||||
return [i.type_id for i in CITypeGroupItem.get_by(
|
||||
__func_in___key_group_id=group_ids, to_dict=False, fl=['type_id'])]
|
||||
|
||||
@staticmethod
|
||||
def get_show_attributes(type_id):
|
||||
_type = CITypeCache.get(type_id) or abort(404, ErrFormat.ci_type_not_found)
|
||||
type_id = _type and _type.id
|
||||
|
||||
if not isinstance(type_id, six.integer_types):
|
||||
_type = CITypeCache.get(type_id)
|
||||
type_id = _type and _type.id
|
||||
|
||||
attrs = PreferenceShowAttributes.get_by(uid=current_user.uid, type_id=type_id, to_dict=False)
|
||||
|
||||
result = []
|
||||
@@ -170,11 +248,11 @@ class PreferenceManager(object):
|
||||
i.update(dict(choice_value=AttributeManager.get_choice_values(
|
||||
i["id"], i["value_type"], i.get("choice_web_hook"), i.get("choice_other"))))
|
||||
|
||||
if (_type.name in SysComputedAttributes.type2attr and
|
||||
i['name'] in SysComputedAttributes.type2attr[_type.name]):
|
||||
i['sys_computed'] = True
|
||||
else:
|
||||
i['sys_computed'] = False
|
||||
if (_type.name in SysComputedAttributes.type2attr and
|
||||
i['name'] in SysComputedAttributes.type2attr[_type.name]):
|
||||
i['sys_computed'] = True
|
||||
else:
|
||||
i['sys_computed'] = False
|
||||
|
||||
return is_subscribed, result
|
||||
|
||||
@@ -523,3 +601,54 @@ class PreferenceManager(object):
|
||||
db.session.rollback()
|
||||
current_app.logger.error("upsert citype order failed: {}".format(e))
|
||||
return abort(400, ErrFormat.unknown_error)
|
||||
|
||||
@staticmethod
|
||||
def get_auto_subscription_config():
|
||||
"""Get user's auto subscription configuration"""
|
||||
config = PreferenceAutoSubscriptionConfig.get_by(
|
||||
uid=current_user.uid, first=True, to_dict=True
|
||||
)
|
||||
return config
|
||||
|
||||
@staticmethod
|
||||
def create_or_update_auto_subscription_config(base_strategy, group_ids=None, type_ids=None,
|
||||
enabled=True, description=None):
|
||||
"""Create or update user's auto subscription config"""
|
||||
config = PreferenceAutoSubscriptionConfig.get_by(
|
||||
uid=current_user.uid, first=True, to_dict=False
|
||||
)
|
||||
|
||||
data = {
|
||||
'base_strategy': base_strategy,
|
||||
'group_ids': group_ids or [],
|
||||
'type_ids': type_ids or [],
|
||||
'enabled': enabled,
|
||||
'description': description
|
||||
}
|
||||
|
||||
if config:
|
||||
return config.update(**data)
|
||||
else:
|
||||
data['uid'] = current_user.uid
|
||||
return PreferenceAutoSubscriptionConfig.create(**data)
|
||||
|
||||
@staticmethod
|
||||
def delete_auto_subscription_config():
|
||||
"""Delete user's auto subscription configuration"""
|
||||
config = PreferenceAutoSubscriptionConfig.get_by(
|
||||
uid=current_user.uid, first=True, to_dict=False
|
||||
)
|
||||
if config:
|
||||
config.soft_delete()
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def toggle_auto_subscription_config(enabled):
|
||||
"""Enable or disable user's auto subscription config"""
|
||||
config = PreferenceAutoSubscriptionConfig.get_by(
|
||||
uid=current_user.uid, first=True, to_dict=False
|
||||
)
|
||||
if not config:
|
||||
return abort(404, "Auto subscription config not found")
|
||||
|
||||
return config.update(enabled=enabled)
|
||||
|
@@ -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!")
|
||||
# 只有管理员才可以定义执行机器为: 所有节点!
|
||||
|
@@ -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
|
||||
"""
|
@@ -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)
|
||||
|
@@ -55,7 +55,6 @@ def str2datetime(x):
|
||||
return datetime.datetime.strptime(x, "%Y-%m-%d %H:%M")
|
||||
|
||||
|
||||
|
||||
class ValueTypeMap(object):
|
||||
deserialize = {
|
||||
ValueTypeEnum.INT: string2int,
|
||||
|
@@ -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
|
||||
|
@@ -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):
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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"],
|
||||
|
@@ -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
|
||||
|
||||
|
||||
|
@@ -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)
|
||||
|
@@ -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 = []
|
||||
|
@@ -1,7 +1,6 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
import base64
|
||||
from typing import Set
|
||||
|
||||
import elasticsearch
|
||||
import redis
|
||||
|
@@ -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))
|
||||
|
@@ -525,6 +525,17 @@ class PreferenceCITypeOrder(Model):
|
||||
is_tree = db.Column(db.Boolean, default=False) # True is tree view, False is resource view
|
||||
|
||||
|
||||
class PreferenceAutoSubscriptionConfig(Model):
|
||||
__tablename__ = "c_pasc"
|
||||
|
||||
uid = db.Column(db.Integer, index=True, nullable=False, unique=True)
|
||||
base_strategy = db.Column(db.Enum('all', 'none'), default='none', nullable=False)
|
||||
group_ids = db.Column(db.JSON)
|
||||
type_ids = db.Column(db.JSON)
|
||||
enabled = db.Column(db.Boolean, default=True, nullable=False)
|
||||
description = db.Column(db.Text)
|
||||
|
||||
|
||||
# custom
|
||||
class CustomDashboard(Model):
|
||||
__tablename__ = "c_c_d"
|
||||
|
@@ -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))
|
||||
|
@@ -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():
|
||||
|
@@ -30,6 +30,7 @@ from api.lib.cmdb.search import SearchError
|
||||
from api.lib.cmdb.search.ci import search as ci_search
|
||||
from api.lib.decorator import args_required
|
||||
from api.lib.decorator import args_validate
|
||||
from api.lib.exception import AbortException
|
||||
from api.lib.perm.acl.acl import has_perm_from_args
|
||||
from api.lib.utils import AESCrypto
|
||||
from api.lib.utils import get_page
|
||||
@@ -296,7 +297,10 @@ class AutoDiscoveryRuleSyncView(APIView):
|
||||
|
||||
rules, last_update_at1 = AutoDiscoveryCITypeCRUD.get(None, oneagent_id, oneagent_name, last_update_at)
|
||||
|
||||
subnet_scan_rules, last_update_at2 = SubnetManager().scan_rules(oneagent_id, last_update_at)
|
||||
try:
|
||||
subnet_scan_rules, last_update_at2 = SubnetManager().scan_rules(oneagent_id, last_update_at)
|
||||
except AbortException:
|
||||
subnet_scan_rules, last_update_at2 = [], ""
|
||||
|
||||
return self.jsonify(rules=rules,
|
||||
subnet_scan_rules=subnet_scan_rules,
|
||||
|
@@ -64,9 +64,13 @@ class CITypeView(APIView):
|
||||
ci_type['unique_name'] = ci_type['unique_id'] and AttributeCache.get(ci_type['unique_id']).name
|
||||
ci_types.append(ci_type)
|
||||
elif type_name is not None:
|
||||
ci_type = CITypeCache.get(type_name).to_dict()
|
||||
ci_type['parent_ids'] = CITypeInheritanceManager.get_parents(ci_type['id'])
|
||||
ci_types = [ci_type]
|
||||
ci_type = CITypeCache.get(type_name)
|
||||
if ci_type is not None:
|
||||
ci_type = ci_type.to_dict()
|
||||
ci_type['parent_ids'] = CITypeInheritanceManager.get_parents(ci_type['id'])
|
||||
ci_types = [ci_type]
|
||||
else:
|
||||
ci_types = []
|
||||
else:
|
||||
ci_types = CITypeManager().get_ci_types(q)
|
||||
count = len(ci_types)
|
||||
|
@@ -211,3 +211,59 @@ class PreferenceCITypeOrderView(APIView):
|
||||
PreferenceManager.upsert_ci_type_order(type_ids, is_tree)
|
||||
|
||||
return self.jsonify(type_ids=type_ids, is_tree=is_tree)
|
||||
|
||||
|
||||
class PreferenceAutoSubscriptionView(APIView):
|
||||
url_prefix = "/preference/auto_subscription"
|
||||
|
||||
def get(self):
|
||||
config = PreferenceManager.get_auto_subscription_config()
|
||||
return self.jsonify(config or {})
|
||||
|
||||
@args_required("base_strategy")
|
||||
def put(self):
|
||||
base_strategy = request.values.get("base_strategy")
|
||||
group_ids = request.values.get("group_ids")
|
||||
type_ids = request.values.get("type_ids")
|
||||
enabled = request.values.get("enabled", 1) in current_app.config.get('BOOL_TRUE')
|
||||
description = request.values.get("description")
|
||||
|
||||
if base_strategy not in ['all', 'none']:
|
||||
return abort(400, "base_strategy must be 'all' or 'none'")
|
||||
|
||||
if group_ids:
|
||||
try:
|
||||
group_ids = [int(x) for x in group_ids.split(',') if x.strip()]
|
||||
except ValueError:
|
||||
return abort(400, "Invalid group_ids format")
|
||||
|
||||
if type_ids:
|
||||
try:
|
||||
type_ids = [int(x) for x in type_ids.split(',') if x.strip()]
|
||||
except ValueError:
|
||||
return abort(400, "Invalid type_ids format")
|
||||
|
||||
result = PreferenceManager.create_or_update_auto_subscription_config(
|
||||
base_strategy=base_strategy,
|
||||
group_ids=group_ids,
|
||||
type_ids=type_ids,
|
||||
enabled=enabled,
|
||||
description=description
|
||||
)
|
||||
|
||||
return self.jsonify(result.to_dict())
|
||||
|
||||
def delete(self):
|
||||
PreferenceManager.delete_auto_subscription_config()
|
||||
return self.jsonify(message="Auto subscription config deleted")
|
||||
|
||||
|
||||
class PreferenceAutoSubscriptionToggleView(APIView):
|
||||
url_prefix = "/preference/auto_subscription/toggle"
|
||||
|
||||
@args_required("enabled")
|
||||
def patch(self):
|
||||
enabled = request.values.get("enabled") in current_app.config.get('BOOL_TRUE')
|
||||
|
||||
result = PreferenceManager.toggle_auto_subscription_config(enabled)
|
||||
return self.jsonify(result.to_dict())
|
||||
|
@@ -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)
|
||||
|
||||
|
||||
|
@@ -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'
|
||||
|
37
cmdb-api/api/views/common_setting/system_language.py
Normal file
37
cmdb-api/api/views/common_setting/system_language.py
Normal 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, "")
|
@@ -58,3 +58,4 @@ python-magic==0.4.27
|
||||
jsonpath==0.82.2
|
||||
networkx>=3.1
|
||||
ipaddress>=1.0.23
|
||||
ruff==0.8.3
|
||||
|
@@ -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
|
||||
|
@@ -5,14 +5,6 @@ if (IS_PROD) {
|
||||
plugins.push('transform-remove-console')
|
||||
}
|
||||
|
||||
// lazy load ant-design-vue
|
||||
// if your use import on Demand, Use this code
|
||||
// plugins.push(['import', {
|
||||
// 'libraryName': 'ant-design-vue',
|
||||
// 'libraryDirectory': 'es',
|
||||
// 'style': true // `style: true` 会加载 less 文件
|
||||
// }])
|
||||
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset',
|
||||
|
@@ -2,7 +2,7 @@ const ThemeColorReplacer = require('webpack-theme-color-replacer')
|
||||
const generate = require('@ant-design/colors/lib/generate').default
|
||||
|
||||
const getAntdSerials = (color) => {
|
||||
// 淡化(即less的tint)
|
||||
// Lighten (similar to less's tint)
|
||||
const lightens = new Array(9).fill().map((t, i) => {
|
||||
return ThemeColorReplacer.varyColor.lighten(color, i / 10)
|
||||
})
|
||||
@@ -13,8 +13,8 @@ const getAntdSerials = (color) => {
|
||||
|
||||
const themePluginOption = {
|
||||
fileName: 'css/theme-colors-[contenthash:8].css',
|
||||
matchColors: getAntdSerials('#2f54eb'), // 主色系列
|
||||
// 改变样式选择器,解决样式覆盖问题
|
||||
matchColors: getAntdSerials('#2f54eb'), // primary color series
|
||||
// change style selectors to solve style override issues
|
||||
changeSelector (selector) {
|
||||
switch (selector) {
|
||||
case '.ant-calendar-today .ant-calendar-date':
|
||||
|
@@ -54,6 +54,228 @@
|
||||
<div class="content unicode" style="display: block;">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">auto</div>
|
||||
<div class="code-name">&#xea28;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">oneterm-http</div>
|
||||
<div class="code-name">&#xea26;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">oneterm-https</div>
|
||||
<div class="code-name">&#xea27;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">access_period</div>
|
||||
<div class="code-name">&#xea25;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">authorization</div>
|
||||
<div class="code-name">&#xea24;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">onterm-symbolic_link</div>
|
||||
<div class="code-name">&#xea23;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">oneterm-batch_execution</div>
|
||||
<div class="code-name">&#xea20;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">oneterm-file_log-selected</div>
|
||||
<div class="code-name">&#xea21;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">oneterm-file_log</div>
|
||||
<div class="code-name">&#xea22;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">file</div>
|
||||
<div class="code-name">&#xea1f;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">folder</div>
|
||||
<div class="code-name">&#xea1e;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">mongoDB (1)</div>
|
||||
<div class="code-name">&#xea1b;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">postgreSQL (1)</div>
|
||||
<div class="code-name">&#xea1c;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">telnet (1)</div>
|
||||
<div class="code-name">&#xea1d;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">command_interception (1)</div>
|
||||
<div class="code-name">&#xea17;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">quick_commands</div>
|
||||
<div class="code-name">&#xea18;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">terminal_settings</div>
|
||||
<div class="code-name">&#xea19;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">basic_settings</div>
|
||||
<div class="code-name">&#xea1a;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">asset_management</div>
|
||||
<div class="code-name">&#xea16;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">ai-seek</div>
|
||||
<div class="code-name">&#xea15;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">ai-hate1</div>
|
||||
<div class="code-name">&#xea13;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">ai-like1</div>
|
||||
<div class="code-name">&#xea14;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">ai-like2</div>
|
||||
<div class="code-name">&#xea11;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">ai-hate2</div>
|
||||
<div class="code-name">&#xea12;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">ai-top_up</div>
|
||||
<div class="code-name">&#xea10;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">ai-top_down</div>
|
||||
<div class="code-name">&#xea0f;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">autoflow-script</div>
|
||||
<div class="code-name">&#xea0d;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">autoflow-dag</div>
|
||||
<div class="code-name">&#xea0e;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">itsm-default_line</div>
|
||||
<div class="code-name">&#xea0c;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">veops-servicetree</div>
|
||||
<div class="code-name">&#xea0b;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">veops-switch (1)</div>
|
||||
<div class="code-name">&#xea0a;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">veops-label</div>
|
||||
<div class="code-name">&#xea09;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">top_acl</div>
|
||||
<div class="code-name">&#xea08;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">top_ticket</div>
|
||||
<div class="code-name">&#xea06;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">top_agent</div>
|
||||
<div class="code-name">&#xea07;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">itsm-table_download</div>
|
||||
<div class="code-name">&#xea05;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">itsm-image_download</div>
|
||||
<div class="code-name">&#xea04;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">veops-rear</div>
|
||||
@@ -6162,9 +6384,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=1755240492206') format('woff2'),
|
||||
url('iconfont.woff?t=1755240492206') format('woff'),
|
||||
url('iconfont.ttf?t=1755240492206') format('truetype');
|
||||
}
|
||||
</code></pre>
|
||||
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
|
||||
@@ -6190,6 +6412,339 @@
|
||||
<div class="content font-class">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont auto"></span>
|
||||
<div class="name">
|
||||
auto
|
||||
</div>
|
||||
<div class="code-name">.auto
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont oneterm-http"></span>
|
||||
<div class="name">
|
||||
oneterm-http
|
||||
</div>
|
||||
<div class="code-name">.oneterm-http
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont oneterm-https"></span>
|
||||
<div class="name">
|
||||
oneterm-https
|
||||
</div>
|
||||
<div class="code-name">.oneterm-https
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont ops-oneterm-access_period"></span>
|
||||
<div class="name">
|
||||
access_period
|
||||
</div>
|
||||
<div class="code-name">.ops-oneterm-access_period
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont ops-oneterm-authorization"></span>
|
||||
<div class="name">
|
||||
authorization
|
||||
</div>
|
||||
<div class="code-name">.ops-oneterm-authorization
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont onterm-symbolic_link"></span>
|
||||
<div class="name">
|
||||
onterm-symbolic_link
|
||||
</div>
|
||||
<div class="code-name">.onterm-symbolic_link
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont oneterm-batch_execution"></span>
|
||||
<div class="name">
|
||||
oneterm-batch_execution
|
||||
</div>
|
||||
<div class="code-name">.oneterm-batch_execution
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont ops-oneterm-file_log-selected"></span>
|
||||
<div class="name">
|
||||
oneterm-file_log-selected
|
||||
</div>
|
||||
<div class="code-name">.ops-oneterm-file_log-selected
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont ops-oneterm-file_log"></span>
|
||||
<div class="name">
|
||||
oneterm-file_log
|
||||
</div>
|
||||
<div class="code-name">.ops-oneterm-file_log
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont file"></span>
|
||||
<div class="name">
|
||||
file
|
||||
</div>
|
||||
<div class="code-name">.file
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont folder1"></span>
|
||||
<div class="name">
|
||||
folder
|
||||
</div>
|
||||
<div class="code-name">.folder1
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont a-mongoDB1"></span>
|
||||
<div class="name">
|
||||
mongoDB (1)
|
||||
</div>
|
||||
<div class="code-name">.a-mongoDB1
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont a-postgreSQL1"></span>
|
||||
<div class="name">
|
||||
postgreSQL (1)
|
||||
</div>
|
||||
<div class="code-name">.a-postgreSQL1
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont a-telnet1"></span>
|
||||
<div class="name">
|
||||
telnet (1)
|
||||
</div>
|
||||
<div class="code-name">.a-telnet1
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont ops-oneterm-command_interception1"></span>
|
||||
<div class="name">
|
||||
command_interception (1)
|
||||
</div>
|
||||
<div class="code-name">.ops-oneterm-command_interception1
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont quick_commands"></span>
|
||||
<div class="name">
|
||||
quick_commands
|
||||
</div>
|
||||
<div class="code-name">.quick_commands
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont terminal_settings"></span>
|
||||
<div class="name">
|
||||
terminal_settings
|
||||
</div>
|
||||
<div class="code-name">.terminal_settings
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont basic_settings"></span>
|
||||
<div class="name">
|
||||
basic_settings
|
||||
</div>
|
||||
<div class="code-name">.basic_settings
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont ops-oneterm-asset-management"></span>
|
||||
<div class="name">
|
||||
asset_management
|
||||
</div>
|
||||
<div class="code-name">.ops-oneterm-asset-management
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont ai-seek"></span>
|
||||
<div class="name">
|
||||
ai-seek
|
||||
</div>
|
||||
<div class="code-name">.ai-seek
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont ai-hate1"></span>
|
||||
<div class="name">
|
||||
ai-hate1
|
||||
</div>
|
||||
<div class="code-name">.ai-hate1
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont ai-like1"></span>
|
||||
<div class="name">
|
||||
ai-like1
|
||||
</div>
|
||||
<div class="code-name">.ai-like1
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont ai-like2"></span>
|
||||
<div class="name">
|
||||
ai-like2
|
||||
</div>
|
||||
<div class="code-name">.ai-like2
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont ai-hate2"></span>
|
||||
<div class="name">
|
||||
ai-hate2
|
||||
</div>
|
||||
<div class="code-name">.ai-hate2
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont ai-top_up"></span>
|
||||
<div class="name">
|
||||
ai-top_up
|
||||
</div>
|
||||
<div class="code-name">.ai-top_up
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont ai-top_down"></span>
|
||||
<div class="name">
|
||||
ai-top_down
|
||||
</div>
|
||||
<div class="code-name">.ai-top_down
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont autoflow-script"></span>
|
||||
<div class="name">
|
||||
autoflow-script
|
||||
</div>
|
||||
<div class="code-name">.autoflow-script
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont autoflow-dag"></span>
|
||||
<div class="name">
|
||||
autoflow-dag
|
||||
</div>
|
||||
<div class="code-name">.autoflow-dag
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont itsm-default_line"></span>
|
||||
<div class="name">
|
||||
itsm-default_line
|
||||
</div>
|
||||
<div class="code-name">.itsm-default_line
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont veops-servicetree"></span>
|
||||
<div class="name">
|
||||
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 +15907,302 @@
|
||||
<div class="content symbol">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#auto"></use>
|
||||
</svg>
|
||||
<div class="name">auto</div>
|
||||
<div class="code-name">#auto</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#oneterm-http"></use>
|
||||
</svg>
|
||||
<div class="name">oneterm-http</div>
|
||||
<div class="code-name">#oneterm-http</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#oneterm-https"></use>
|
||||
</svg>
|
||||
<div class="name">oneterm-https</div>
|
||||
<div class="code-name">#oneterm-https</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#ops-oneterm-access_period"></use>
|
||||
</svg>
|
||||
<div class="name">access_period</div>
|
||||
<div class="code-name">#ops-oneterm-access_period</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#ops-oneterm-authorization"></use>
|
||||
</svg>
|
||||
<div class="name">authorization</div>
|
||||
<div class="code-name">#ops-oneterm-authorization</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#onterm-symbolic_link"></use>
|
||||
</svg>
|
||||
<div class="name">onterm-symbolic_link</div>
|
||||
<div class="code-name">#onterm-symbolic_link</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#oneterm-batch_execution"></use>
|
||||
</svg>
|
||||
<div class="name">oneterm-batch_execution</div>
|
||||
<div class="code-name">#oneterm-batch_execution</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#ops-oneterm-file_log-selected"></use>
|
||||
</svg>
|
||||
<div class="name">oneterm-file_log-selected</div>
|
||||
<div class="code-name">#ops-oneterm-file_log-selected</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#ops-oneterm-file_log"></use>
|
||||
</svg>
|
||||
<div class="name">oneterm-file_log</div>
|
||||
<div class="code-name">#ops-oneterm-file_log</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#file"></use>
|
||||
</svg>
|
||||
<div class="name">file</div>
|
||||
<div class="code-name">#file</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#folder1"></use>
|
||||
</svg>
|
||||
<div class="name">folder</div>
|
||||
<div class="code-name">#folder1</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#a-mongoDB1"></use>
|
||||
</svg>
|
||||
<div class="name">mongoDB (1)</div>
|
||||
<div class="code-name">#a-mongoDB1</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#a-postgreSQL1"></use>
|
||||
</svg>
|
||||
<div class="name">postgreSQL (1)</div>
|
||||
<div class="code-name">#a-postgreSQL1</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#a-telnet1"></use>
|
||||
</svg>
|
||||
<div class="name">telnet (1)</div>
|
||||
<div class="code-name">#a-telnet1</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#ops-oneterm-command_interception1"></use>
|
||||
</svg>
|
||||
<div class="name">command_interception (1)</div>
|
||||
<div class="code-name">#ops-oneterm-command_interception1</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#quick_commands"></use>
|
||||
</svg>
|
||||
<div class="name">quick_commands</div>
|
||||
<div class="code-name">#quick_commands</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#terminal_settings"></use>
|
||||
</svg>
|
||||
<div class="name">terminal_settings</div>
|
||||
<div class="code-name">#terminal_settings</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#basic_settings"></use>
|
||||
</svg>
|
||||
<div class="name">basic_settings</div>
|
||||
<div class="code-name">#basic_settings</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#ops-oneterm-asset-management"></use>
|
||||
</svg>
|
||||
<div class="name">asset_management</div>
|
||||
<div class="code-name">#ops-oneterm-asset-management</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#ai-seek"></use>
|
||||
</svg>
|
||||
<div class="name">ai-seek</div>
|
||||
<div class="code-name">#ai-seek</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#ai-hate1"></use>
|
||||
</svg>
|
||||
<div class="name">ai-hate1</div>
|
||||
<div class="code-name">#ai-hate1</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#ai-like1"></use>
|
||||
</svg>
|
||||
<div class="name">ai-like1</div>
|
||||
<div class="code-name">#ai-like1</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#ai-like2"></use>
|
||||
</svg>
|
||||
<div class="name">ai-like2</div>
|
||||
<div class="code-name">#ai-like2</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#ai-hate2"></use>
|
||||
</svg>
|
||||
<div class="name">ai-hate2</div>
|
||||
<div class="code-name">#ai-hate2</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#ai-top_up"></use>
|
||||
</svg>
|
||||
<div class="name">ai-top_up</div>
|
||||
<div class="code-name">#ai-top_up</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#ai-top_down"></use>
|
||||
</svg>
|
||||
<div class="name">ai-top_down</div>
|
||||
<div class="code-name">#ai-top_down</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#autoflow-script"></use>
|
||||
</svg>
|
||||
<div class="name">autoflow-script</div>
|
||||
<div class="code-name">#autoflow-script</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#autoflow-dag"></use>
|
||||
</svg>
|
||||
<div class="name">autoflow-dag</div>
|
||||
<div class="code-name">#autoflow-dag</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#itsm-default_line"></use>
|
||||
</svg>
|
||||
<div class="name">itsm-default_line</div>
|
||||
<div class="code-name">#itsm-default_line</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#veops-servicetree"></use>
|
||||
</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>
|
||||
|
@@ -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=1755240492206') format('woff2'),
|
||||
url('iconfont.woff?t=1755240492206') format('woff'),
|
||||
url('iconfont.ttf?t=1755240492206') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
@@ -13,6 +13,154 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.auto:before {
|
||||
content: "\ea28";
|
||||
}
|
||||
|
||||
.oneterm-http:before {
|
||||
content: "\ea26";
|
||||
}
|
||||
|
||||
.oneterm-https:before {
|
||||
content: "\ea27";
|
||||
}
|
||||
|
||||
.ops-oneterm-access_period:before {
|
||||
content: "\ea25";
|
||||
}
|
||||
|
||||
.ops-oneterm-authorization:before {
|
||||
content: "\ea24";
|
||||
}
|
||||
|
||||
.onterm-symbolic_link:before {
|
||||
content: "\ea23";
|
||||
}
|
||||
|
||||
.oneterm-batch_execution:before {
|
||||
content: "\ea20";
|
||||
}
|
||||
|
||||
.ops-oneterm-file_log-selected:before {
|
||||
content: "\ea21";
|
||||
}
|
||||
|
||||
.ops-oneterm-file_log:before {
|
||||
content: "\ea22";
|
||||
}
|
||||
|
||||
.file:before {
|
||||
content: "\ea1f";
|
||||
}
|
||||
|
||||
.folder1:before {
|
||||
content: "\ea1e";
|
||||
}
|
||||
|
||||
.a-mongoDB1:before {
|
||||
content: "\ea1b";
|
||||
}
|
||||
|
||||
.a-postgreSQL1:before {
|
||||
content: "\ea1c";
|
||||
}
|
||||
|
||||
.a-telnet1:before {
|
||||
content: "\ea1d";
|
||||
}
|
||||
|
||||
.ops-oneterm-command_interception1:before {
|
||||
content: "\ea17";
|
||||
}
|
||||
|
||||
.quick_commands:before {
|
||||
content: "\ea18";
|
||||
}
|
||||
|
||||
.terminal_settings:before {
|
||||
content: "\ea19";
|
||||
}
|
||||
|
||||
.basic_settings:before {
|
||||
content: "\ea1a";
|
||||
}
|
||||
|
||||
.ops-oneterm-asset-management:before {
|
||||
content: "\ea16";
|
||||
}
|
||||
|
||||
.ai-seek:before {
|
||||
content: "\ea15";
|
||||
}
|
||||
|
||||
.ai-hate1:before {
|
||||
content: "\ea13";
|
||||
}
|
||||
|
||||
.ai-like1:before {
|
||||
content: "\ea14";
|
||||
}
|
||||
|
||||
.ai-like2:before {
|
||||
content: "\ea11";
|
||||
}
|
||||
|
||||
.ai-hate2:before {
|
||||
content: "\ea12";
|
||||
}
|
||||
|
||||
.ai-top_up:before {
|
||||
content: "\ea10";
|
||||
}
|
||||
|
||||
.ai-top_down:before {
|
||||
content: "\ea0f";
|
||||
}
|
||||
|
||||
.autoflow-script:before {
|
||||
content: "\ea0d";
|
||||
}
|
||||
|
||||
.autoflow-dag:before {
|
||||
content: "\ea0e";
|
||||
}
|
||||
|
||||
.itsm-default_line:before {
|
||||
content: "\ea0c";
|
||||
}
|
||||
|
||||
.veops-servicetree:before {
|
||||
content: "\ea0b";
|
||||
}
|
||||
|
||||
.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
@@ -5,6 +5,265 @@
|
||||
"css_prefix_text": "",
|
||||
"description": "",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "45254419",
|
||||
"name": "auto",
|
||||
"font_class": "auto",
|
||||
"unicode": "ea28",
|
||||
"unicode_decimal": 59944
|
||||
},
|
||||
{
|
||||
"icon_id": "45069619",
|
||||
"name": "oneterm-http",
|
||||
"font_class": "oneterm-http",
|
||||
"unicode": "ea26",
|
||||
"unicode_decimal": 59942
|
||||
},
|
||||
{
|
||||
"icon_id": "45069616",
|
||||
"name": "oneterm-https",
|
||||
"font_class": "oneterm-https",
|
||||
"unicode": "ea27",
|
||||
"unicode_decimal": 59943
|
||||
},
|
||||
{
|
||||
"icon_id": "44939355",
|
||||
"name": "access_period",
|
||||
"font_class": "ops-oneterm-access_period",
|
||||
"unicode": "ea25",
|
||||
"unicode_decimal": 59941
|
||||
},
|
||||
{
|
||||
"icon_id": "44939354",
|
||||
"name": "authorization",
|
||||
"font_class": "ops-oneterm-authorization",
|
||||
"unicode": "ea24",
|
||||
"unicode_decimal": 59940
|
||||
},
|
||||
{
|
||||
"icon_id": "44501032",
|
||||
"name": "onterm-symbolic_link",
|
||||
"font_class": "onterm-symbolic_link",
|
||||
"unicode": "ea23",
|
||||
"unicode_decimal": 59939
|
||||
},
|
||||
{
|
||||
"icon_id": "44497221",
|
||||
"name": "oneterm-batch_execution",
|
||||
"font_class": "oneterm-batch_execution",
|
||||
"unicode": "ea20",
|
||||
"unicode_decimal": 59936
|
||||
},
|
||||
{
|
||||
"icon_id": "44497220",
|
||||
"name": "oneterm-file_log-selected",
|
||||
"font_class": "ops-oneterm-file_log-selected",
|
||||
"unicode": "ea21",
|
||||
"unicode_decimal": 59937
|
||||
},
|
||||
{
|
||||
"icon_id": "44497219",
|
||||
"name": "oneterm-file_log",
|
||||
"font_class": "ops-oneterm-file_log",
|
||||
"unicode": "ea22",
|
||||
"unicode_decimal": 59938
|
||||
},
|
||||
{
|
||||
"icon_id": "44455092",
|
||||
"name": "file",
|
||||
"font_class": "file",
|
||||
"unicode": "ea1f",
|
||||
"unicode_decimal": 59935
|
||||
},
|
||||
{
|
||||
"icon_id": "44455100",
|
||||
"name": "folder",
|
||||
"font_class": "folder1",
|
||||
"unicode": "ea1e",
|
||||
"unicode_decimal": 59934
|
||||
},
|
||||
{
|
||||
"icon_id": "44315758",
|
||||
"name": "mongoDB (1)",
|
||||
"font_class": "a-mongoDB1",
|
||||
"unicode": "ea1b",
|
||||
"unicode_decimal": 59931
|
||||
},
|
||||
{
|
||||
"icon_id": "44315757",
|
||||
"name": "postgreSQL (1)",
|
||||
"font_class": "a-postgreSQL1",
|
||||
"unicode": "ea1c",
|
||||
"unicode_decimal": 59932
|
||||
},
|
||||
{
|
||||
"icon_id": "44315755",
|
||||
"name": "telnet (1)",
|
||||
"font_class": "a-telnet1",
|
||||
"unicode": "ea1d",
|
||||
"unicode_decimal": 59933
|
||||
},
|
||||
{
|
||||
"icon_id": "44276353",
|
||||
"name": "command_interception (1)",
|
||||
"font_class": "ops-oneterm-command_interception1",
|
||||
"unicode": "ea17",
|
||||
"unicode_decimal": 59927
|
||||
},
|
||||
{
|
||||
"icon_id": "44276352",
|
||||
"name": "quick_commands",
|
||||
"font_class": "quick_commands",
|
||||
"unicode": "ea18",
|
||||
"unicode_decimal": 59928
|
||||
},
|
||||
{
|
||||
"icon_id": "44276351",
|
||||
"name": "terminal_settings",
|
||||
"font_class": "terminal_settings",
|
||||
"unicode": "ea19",
|
||||
"unicode_decimal": 59929
|
||||
},
|
||||
{
|
||||
"icon_id": "44276350",
|
||||
"name": "basic_settings",
|
||||
"font_class": "basic_settings",
|
||||
"unicode": "ea1a",
|
||||
"unicode_decimal": 59930
|
||||
},
|
||||
{
|
||||
"icon_id": "44276278",
|
||||
"name": "asset_management",
|
||||
"font_class": "ops-oneterm-asset-management",
|
||||
"unicode": "ea16",
|
||||
"unicode_decimal": 59926
|
||||
},
|
||||
{
|
||||
"icon_id": "43267802",
|
||||
"name": "ai-seek",
|
||||
"font_class": "ai-seek",
|
||||
"unicode": "ea15",
|
||||
"unicode_decimal": 59925
|
||||
},
|
||||
{
|
||||
"icon_id": "43213714",
|
||||
"name": "ai-hate1",
|
||||
"font_class": "ai-hate1",
|
||||
"unicode": "ea13",
|
||||
"unicode_decimal": 59923
|
||||
},
|
||||
{
|
||||
"icon_id": "43213712",
|
||||
"name": "ai-like1",
|
||||
"font_class": "ai-like1",
|
||||
"unicode": "ea14",
|
||||
"unicode_decimal": 59924
|
||||
},
|
||||
{
|
||||
"icon_id": "43213717",
|
||||
"name": "ai-like2",
|
||||
"font_class": "ai-like2",
|
||||
"unicode": "ea11",
|
||||
"unicode_decimal": 59921
|
||||
},
|
||||
{
|
||||
"icon_id": "43213716",
|
||||
"name": "ai-hate2",
|
||||
"font_class": "ai-hate2",
|
||||
"unicode": "ea12",
|
||||
"unicode_decimal": 59922
|
||||
},
|
||||
{
|
||||
"icon_id": "43139007",
|
||||
"name": "ai-top_up",
|
||||
"font_class": "ai-top_up",
|
||||
"unicode": "ea10",
|
||||
"unicode_decimal": 59920
|
||||
},
|
||||
{
|
||||
"icon_id": "43139017",
|
||||
"name": "ai-top_down",
|
||||
"font_class": "ai-top_down",
|
||||
"unicode": "ea0f",
|
||||
"unicode_decimal": 59919
|
||||
},
|
||||
{
|
||||
"icon_id": "43029539",
|
||||
"name": "autoflow-script",
|
||||
"font_class": "autoflow-script",
|
||||
"unicode": "ea0d",
|
||||
"unicode_decimal": 59917
|
||||
},
|
||||
{
|
||||
"icon_id": "43029538",
|
||||
"name": "autoflow-dag",
|
||||
"font_class": "autoflow-dag",
|
||||
"unicode": "ea0e",
|
||||
"unicode_decimal": 59918
|
||||
},
|
||||
{
|
||||
"icon_id": "42960865",
|
||||
"name": "itsm-default_line",
|
||||
"font_class": "itsm-default_line",
|
||||
"unicode": "ea0c",
|
||||
"unicode_decimal": 59916
|
||||
},
|
||||
{
|
||||
"icon_id": "42930714",
|
||||
"name": "veops-servicetree",
|
||||
"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.
Binary file not shown.
Binary file not shown.
@@ -1,7 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-cmn-Hans">
|
||||
<head>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
@@ -12,17 +11,19 @@
|
||||
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %>
|
||||
<link rel="stylesheet" href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" />
|
||||
<% } %>
|
||||
<script>
|
||||
|
||||
const userAgent = navigator.userAgent
|
||||
<script>
|
||||
const userAgent = navigator.userAgent
|
||||
const isEdge = userAgent.indexOf("Edge") > -1
|
||||
const isChrome = userAgent.indexOf("Chrome") > -1 && userAgent.indexOf("Safari") > -1 && !isEdge
|
||||
if (!isChrome) {
|
||||
alert("推荐使用Chrome浏览器, 其他环境下未做严格测试!")
|
||||
const isChrome = userAgent.indexOf("Chrome") > -1 && userAgent.indexOf("Safari") > -1 && !isEdge
|
||||
const lang = (navigator.language || navigator.userLanguage || '').toLowerCase()
|
||||
if (!isChrome) {
|
||||
if (lang.startsWith('zh')) {
|
||||
alert("推荐使用Chrome浏览器,其他环境下未做严格测试!");
|
||||
} else {
|
||||
alert("It is recommended to use Chrome browser. Other environments are not strictly tested!");
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
@@ -30,10 +31,10 @@
|
||||
</noscript>
|
||||
<div id="app">
|
||||
<div id="loading-mask">
|
||||
<div class="loading-wrapper">
|
||||
<span class="loading-dot loading-dot-spin"><i></i><i></i><i></i><i></i></span>
|
||||
</div>
|
||||
<div class="loading-wrapper">
|
||||
<span class="loading-dot loading-dot-spin"><i></i><i></i><i></i><i></i></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- require cdn assets js -->
|
||||
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
|
||||
|
@@ -12,6 +12,7 @@ import zhCN from 'ant-design-vue/lib/locale-provider/zh_CN'
|
||||
import enUS from 'ant-design-vue/lib/locale-provider/en_US'
|
||||
import { AppDeviceEnquire } from '@/utils/mixin'
|
||||
import { debounce } from './utils/util'
|
||||
import { getSystemLanguage } from '@/api/system.js'
|
||||
|
||||
import { h } from 'snabbdom'
|
||||
import { DomEditor, Boot } from '@wangeditor/editor'
|
||||
@@ -45,8 +46,7 @@ export default {
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.SET_LOCALE(localStorage.getItem('ops_locale') || 'zh')
|
||||
this.$i18n.locale = localStorage.getItem('ops_locale') || 'zh'
|
||||
this.initLanguage()
|
||||
this.timer = setInterval(() => {
|
||||
this.setTime(new Date().getTime())
|
||||
}, 1000)
|
||||
@@ -60,133 +60,7 @@ export default {
|
||||
})
|
||||
)
|
||||
|
||||
// 注册富文本自定义元素
|
||||
// const resume = {
|
||||
// type: 'attachment',
|
||||
// attachmentLabel: '',
|
||||
// attachmentValue: '',
|
||||
// children: [{ text: '' }], // void 元素必须有一个 children ,其中只有一个空字符串,重要!!!
|
||||
// }
|
||||
|
||||
function withAttachment(editor) {
|
||||
// JS 语法
|
||||
const { isInline, isVoid } = editor
|
||||
const newEditor = editor
|
||||
|
||||
newEditor.isInline = (elem) => {
|
||||
const type = DomEditor.getNodeType(elem)
|
||||
if (type === 'attachment') return true // 针对 type: attachment ,设置为 inline
|
||||
return isInline(elem)
|
||||
}
|
||||
|
||||
newEditor.isVoid = (elem) => {
|
||||
const type = DomEditor.getNodeType(elem)
|
||||
if (type === 'attachment') return true // 针对 type: attachment ,设置为 void
|
||||
return isVoid(elem)
|
||||
}
|
||||
|
||||
return newEditor // 返回 newEditor ,重要!!!
|
||||
}
|
||||
Boot.registerPlugin(withAttachment)
|
||||
/**
|
||||
* 渲染“附件”元素到编辑器
|
||||
* @param elem 附件元素,即上文的 myResume
|
||||
* @param children 元素子节点,void 元素可忽略
|
||||
* @param editor 编辑器实例
|
||||
* @returns vnode 节点(通过 snabbdom.js 的 h 函数生成)
|
||||
*/
|
||||
function renderAttachment(elem, children, editor) {
|
||||
// JS 语法
|
||||
|
||||
// 获取“附件”的数据,参考上文 myResume 数据结构
|
||||
const { attachmentLabel = '', attachmentValue = '' } = elem
|
||||
|
||||
// 附件元素 vnode
|
||||
const attachVnode = h(
|
||||
// HTML tag
|
||||
'span',
|
||||
// HTML 属性、样式、事件
|
||||
{
|
||||
props: { contentEditable: false }, // HTML 属性,驼峰式写法
|
||||
style: {
|
||||
display: 'inline-block',
|
||||
margin: '0 3px',
|
||||
padding: '0 3px',
|
||||
backgroundColor: '#e6f7ff',
|
||||
border: '1px solid #91d5ff',
|
||||
borderRadius: '2px',
|
||||
color: '#1890ff',
|
||||
}, // style ,驼峰式写法
|
||||
on: {
|
||||
click() {
|
||||
console.log('clicked', attachmentValue)
|
||||
} /* 其他... */,
|
||||
},
|
||||
},
|
||||
// 子节点
|
||||
[attachmentLabel]
|
||||
)
|
||||
|
||||
return attachVnode
|
||||
}
|
||||
const renderElemConf = {
|
||||
type: 'attachment', // 新元素 type ,重要!!!
|
||||
renderElem: renderAttachment,
|
||||
}
|
||||
Boot.registerRenderElem(renderElemConf)
|
||||
|
||||
/**
|
||||
* 生成“附件”元素的 HTML
|
||||
* @param elem 附件元素,即上文的 myResume
|
||||
* @param childrenHtml 子节点的 HTML 代码,void 元素可忽略
|
||||
* @returns “附件”元素的 HTML 字符串
|
||||
*/
|
||||
function attachmentToHtml(elem, childrenHtml) {
|
||||
// JS 语法
|
||||
|
||||
// 获取附件元素的数据
|
||||
const { attachmentValue = '', attachmentLabel = '' } = elem
|
||||
|
||||
// 生成 HTML 代码
|
||||
const html = `<span data-w-e-type="attachment" data-w-e-is-void data-w-e-is-inline data-attachmentValue="${attachmentValue}" data-attachmentLabel="${attachmentLabel}">${attachmentLabel}</span>`
|
||||
|
||||
return html
|
||||
}
|
||||
const elemToHtmlConf = {
|
||||
type: 'attachment', // 新元素的 type ,重要!!!
|
||||
elemToHtml: attachmentToHtml,
|
||||
}
|
||||
Boot.registerElemToHtml(elemToHtmlConf)
|
||||
|
||||
/**
|
||||
* 解析 HTML 字符串,生成“附件”元素
|
||||
* @param domElem HTML 对应的 DOM Element
|
||||
* @param children 子节点
|
||||
* @param editor editor 实例
|
||||
* @returns “附件”元素,如上文的 myResume
|
||||
*/
|
||||
function parseAttachmentHtml(domElem, children, editor) {
|
||||
// JS 语法
|
||||
|
||||
// 从 DOM element 中获取“附件”的信息
|
||||
const attachmentValue = domElem.getAttribute('data-attachmentValue') || ''
|
||||
const attachmentLabel = domElem.getAttribute('data-attachmentLabel') || ''
|
||||
|
||||
// 生成“附件”元素(按照此前约定的数据结构)
|
||||
const myResume = {
|
||||
type: 'attachment',
|
||||
attachmentValue,
|
||||
attachmentLabel,
|
||||
children: [{ text: '' }], // void node 必须有 children ,其中有一个空字符串,重要!!!
|
||||
}
|
||||
|
||||
return myResume
|
||||
}
|
||||
const parseHtmlConf = {
|
||||
selector: 'span[data-w-e-type="attachment"]', // CSS 选择器,匹配特定的 HTML 标签
|
||||
parseElemHtml: parseAttachmentHtml,
|
||||
}
|
||||
Boot.registerParseElemHtml(parseHtmlConf)
|
||||
this.handleEditor()
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.timer)
|
||||
@@ -200,6 +74,141 @@ export default {
|
||||
this.alive = true
|
||||
})
|
||||
},
|
||||
async initLanguage() {
|
||||
let saveLocale = localStorage.getItem('ops_locale')
|
||||
if (!saveLocale) {
|
||||
let requestLanguage = ''
|
||||
try {
|
||||
const languageRes = await getSystemLanguage()
|
||||
requestLanguage = languageRes?.language || ''
|
||||
} catch (e) {
|
||||
console.error('getSystemLanguage error:', e)
|
||||
}
|
||||
|
||||
// request language variable || user local system language
|
||||
const userLanguage = requestLanguage || navigator.language || navigator.userLanguage
|
||||
if (userLanguage.includes('zh')) {
|
||||
saveLocale = 'zh'
|
||||
} else {
|
||||
saveLocale = 'en'
|
||||
}
|
||||
}
|
||||
this.SET_LOCALE(saveLocale)
|
||||
this.$i18n.locale = saveLocale
|
||||
},
|
||||
|
||||
handleEditor() {
|
||||
// register custom rich text element: attachment
|
||||
function withAttachment(editor) {
|
||||
const { isInline, isVoid } = editor
|
||||
const newEditor = editor
|
||||
|
||||
newEditor.isInline = (elem) => {
|
||||
const type = DomEditor.getNodeType(elem)
|
||||
if (type === 'attachment') return true // For type: attachment, set to inline
|
||||
return isInline(elem)
|
||||
}
|
||||
|
||||
newEditor.isVoid = (elem) => {
|
||||
const type = DomEditor.getNodeType(elem)
|
||||
if (type === 'attachment') return true // For type: attachment ,set to void
|
||||
return isVoid(elem)
|
||||
}
|
||||
|
||||
return newEditor // Must return, important!!!
|
||||
}
|
||||
Boot.registerPlugin(withAttachment)
|
||||
/**
|
||||
* Render "attachment" element in editor
|
||||
* @param elem Attachment element
|
||||
* @param children Child nodes (ignored for void elements)
|
||||
* @param editor Editor instance
|
||||
* @returns vnode (generated by snabbdom's h function)
|
||||
*/
|
||||
function renderAttachment(elem, children, editor) {
|
||||
const { attachmentLabel = '', attachmentValue = '' } = elem
|
||||
|
||||
const attachVnode = h(
|
||||
// HTML tag
|
||||
'span',
|
||||
// HTML attr, style, event
|
||||
{
|
||||
props: { contentEditable: false },
|
||||
style: {
|
||||
display: 'inline-block',
|
||||
margin: '0 3px',
|
||||
padding: '0 3px',
|
||||
backgroundColor: '#e6f7ff',
|
||||
border: '1px solid #91d5ff',
|
||||
borderRadius: '2px',
|
||||
color: '#1890ff',
|
||||
},
|
||||
on: {
|
||||
click() {
|
||||
console.log('clicked', attachmentValue)
|
||||
}
|
||||
},
|
||||
},
|
||||
// child node
|
||||
[attachmentLabel]
|
||||
)
|
||||
|
||||
return attachVnode
|
||||
}
|
||||
const renderElemConf = {
|
||||
type: 'attachment',
|
||||
renderElem: renderAttachment,
|
||||
}
|
||||
Boot.registerRenderElem(renderElemConf)
|
||||
|
||||
/**
|
||||
* Generate HTML for "attachment" element
|
||||
* @param elem Attachment element
|
||||
* @param childrenHtml Child HTML (ignored for void elements)
|
||||
* @returns HTML string
|
||||
*/
|
||||
function attachmentToHtml(elem, childrenHtml) {
|
||||
// Getting data for attached elements
|
||||
const { attachmentValue = '', attachmentLabel = '' } = elem
|
||||
|
||||
// generate HTML
|
||||
const html = `<span data-w-e-type="attachment" data-w-e-is-void data-w-e-is-inline data-attachmentValue="${attachmentValue}" data-attachmentLabel="${attachmentLabel}">${attachmentLabel}</span>`
|
||||
|
||||
return html
|
||||
}
|
||||
const elemToHtmlConf = {
|
||||
type: 'attachment',
|
||||
elemToHtml: attachmentToHtml,
|
||||
}
|
||||
Boot.registerElemToHtml(elemToHtmlConf)
|
||||
|
||||
/**
|
||||
* Parse HTML to generate "attachment" element
|
||||
* @param domElem DOM element
|
||||
* @param children Children
|
||||
* @param editor Editor instance
|
||||
* @returns Attachment element
|
||||
*/
|
||||
function parseAttachmentHtml(domElem, children, editor) {
|
||||
// Getting “attachment” information from DOM element
|
||||
const attachmentValue = domElem.getAttribute('data-attachmentValue') || ''
|
||||
const attachmentLabel = domElem.getAttribute('data-attachmentLabel') || ''
|
||||
|
||||
const myResume = {
|
||||
type: 'attachment',
|
||||
attachmentValue,
|
||||
attachmentLabel,
|
||||
children: [{ text: '' }], // The void node must have children with an empty string in it, important!!!!
|
||||
}
|
||||
|
||||
return myResume
|
||||
}
|
||||
const parseHtmlConf = {
|
||||
selector: 'span[data-w-e-type="attachment"]', // CSS selector to match specific HTML tags
|
||||
parseElemHtml: parseAttachmentHtml,
|
||||
}
|
||||
Boot.registerParseElemHtml(parseHtmlConf)
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
8
cmdb-ui/src/api/system.js
Normal file
8
cmdb-ui/src/api/system.js
Normal 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 |
@@ -1,41 +1,40 @@
|
||||
import i18n from '@/lang'
|
||||
|
||||
export const ruleTypeList = () => {
|
||||
return [
|
||||
{ value: 'and', label: i18n.t('cmdbFilterComp.and') },
|
||||
{ value: 'or', label: i18n.t('cmdbFilterComp.or') },
|
||||
// { value: 'not', label: '非' },
|
||||
]
|
||||
}
|
||||
|
||||
export const expList = () => {
|
||||
return [
|
||||
{ value: 'is', label: i18n.t('cmdbFilterComp.is') },
|
||||
{ value: '~is', label: i18n.t('cmdbFilterComp.~is') },
|
||||
{ value: 'contain', label: i18n.t('cmdbFilterComp.contain') },
|
||||
{ value: '~contain', label: i18n.t('cmdbFilterComp.~contain') },
|
||||
{ value: 'start_with', label: i18n.t('cmdbFilterComp.start_with') },
|
||||
{ value: '~start_with', label: i18n.t('cmdbFilterComp.~start_with') },
|
||||
{ value: 'end_with', label: i18n.t('cmdbFilterComp.end_with') },
|
||||
{ value: '~end_with', label: i18n.t('cmdbFilterComp.~end_with') },
|
||||
{ value: '~value', label: i18n.t('cmdbFilterComp.~value') }, // 为空的定义有点绕
|
||||
{ value: 'value', label: i18n.t('cmdbFilterComp.value') },
|
||||
]
|
||||
}
|
||||
|
||||
export const advancedExpList = () => {
|
||||
return [
|
||||
{ value: 'in', label: i18n.t('cmdbFilterComp.in') },
|
||||
{ value: '~in', label: i18n.t('cmdbFilterComp.~in') },
|
||||
{ value: 'range', label: i18n.t('cmdbFilterComp.range') },
|
||||
{ value: '~range', label: i18n.t('cmdbFilterComp.~range') },
|
||||
{ value: 'compare', label: i18n.t('cmdbFilterComp.compare') },
|
||||
]
|
||||
}
|
||||
|
||||
export const compareTypeList = [
|
||||
{ value: '1', label: '>' },
|
||||
{ value: '2', label: '>=' },
|
||||
{ value: '3', label: '<' },
|
||||
{ value: '4', label: '<=' },
|
||||
]
|
||||
import i18n from '@/lang'
|
||||
|
||||
export const ruleTypeList = () => {
|
||||
return [
|
||||
{ value: 'and', label: i18n.t('cmdbFilterComp.and') },
|
||||
{ value: 'or', label: i18n.t('cmdbFilterComp.or') },
|
||||
]
|
||||
}
|
||||
|
||||
export const expList = () => {
|
||||
return [
|
||||
{ value: 'is', label: i18n.t('cmdbFilterComp.is') },
|
||||
{ value: '~is', label: i18n.t('cmdbFilterComp.~is') },
|
||||
{ value: 'contain', label: i18n.t('cmdbFilterComp.contain') },
|
||||
{ value: '~contain', label: i18n.t('cmdbFilterComp.~contain') },
|
||||
{ value: 'start_with', label: i18n.t('cmdbFilterComp.start_with') },
|
||||
{ value: '~start_with', label: i18n.t('cmdbFilterComp.~start_with') },
|
||||
{ value: 'end_with', label: i18n.t('cmdbFilterComp.end_with') },
|
||||
{ value: '~end_with', label: i18n.t('cmdbFilterComp.~end_with') },
|
||||
{ value: '~value', label: i18n.t('cmdbFilterComp.~value') },
|
||||
{ value: 'value', label: i18n.t('cmdbFilterComp.value') },
|
||||
]
|
||||
}
|
||||
|
||||
export const advancedExpList = () => {
|
||||
return [
|
||||
{ value: 'in', label: i18n.t('cmdbFilterComp.in') },
|
||||
{ value: '~in', label: i18n.t('cmdbFilterComp.~in') },
|
||||
{ value: 'range', label: i18n.t('cmdbFilterComp.range') },
|
||||
{ value: '~range', label: i18n.t('cmdbFilterComp.~range') },
|
||||
{ value: 'compare', label: i18n.t('cmdbFilterComp.compare') },
|
||||
]
|
||||
}
|
||||
|
||||
export const compareTypeList = [
|
||||
{ value: '1', label: '>' },
|
||||
{ value: '2', label: '>=' },
|
||||
{ value: '3', label: '<' },
|
||||
{ value: '4', label: '<=' },
|
||||
]
|
||||
|
@@ -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')"
|
||||
@@ -301,7 +301,7 @@ export default {
|
||||
return [
|
||||
{ value: 'is', label: this.$t('cmdbFilterComp.is') },
|
||||
{ value: '~is', label: this.$t('cmdbFilterComp.~is') },
|
||||
{ value: '~value', label: this.$t('cmdbFilterComp.~value') }, // 为空的定义有点绕
|
||||
{ value: '~value', label: this.$t('cmdbFilterComp.~value') },
|
||||
{ value: 'value', label: this.$t('cmdbFilterComp.value') },
|
||||
]
|
||||
}
|
||||
@@ -398,7 +398,6 @@ export default {
|
||||
|
||||
/deep/ .ant-select-selection {
|
||||
height: 24px;
|
||||
background: #f7f8fa;
|
||||
line-height: 24px;
|
||||
border: none;
|
||||
|
||||
|
@@ -1,302 +1,304 @@
|
||||
<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: {
|
||||
/**
|
||||
* @param isInitOne When the initialization exp is null, does the ruleList default to giving one
|
||||
*/
|
||||
visibleChange(open, isInitOne = true) {
|
||||
// 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' // after add/delete, just in case the first one is not '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>
|
||||
|
@@ -14,7 +14,7 @@
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 元素折叠过度效果
|
||||
* Collapse transition effect for elements
|
||||
*/
|
||||
export default {
|
||||
name: 'CollapseTransition',
|
||||
@@ -33,20 +33,17 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
collapseBeforeEnter(el) {
|
||||
// console.log('11, collapseBeforeEnter');
|
||||
this.oldPaddingBottom = el.style.paddingBottom
|
||||
this.oldPaddingTop = el.style.paddingTop
|
||||
// 过渡效果开始前设置元素的maxHeight为0,让元素maxHeight有一个初始值
|
||||
// set the element's maxHeight to 0 before the transition effect starts so that the element's maxHeight has an initial value
|
||||
el.style.paddingTop = '0'
|
||||
el.style.paddingBottom = '0'
|
||||
el.style.maxHeight = '0'
|
||||
},
|
||||
collapseEnter(el, done) {
|
||||
// console.log('22, collapseEnter');
|
||||
//
|
||||
this.oldOverflow = el.style.overflow
|
||||
const elHeight = el.scrollHeight
|
||||
// 过渡效果进入后将元素的maxHeight设置为元素本身的高度,将元素maxHeight设置为auto不会有过渡效果
|
||||
// After entering, set maxHeight to the element's height; setting maxHeight to auto will not have a transition effect
|
||||
if (elHeight > 0) {
|
||||
el.style.maxHeight = elHeight + 'px'
|
||||
} else {
|
||||
@@ -59,24 +56,20 @@ export default {
|
||||
// done();
|
||||
const onTransitionDone = function() {
|
||||
done()
|
||||
// console.log('enter onTransitionDone');
|
||||
el.removeEventListener('transitionend', onTransitionDone, false)
|
||||
el.removeEventListener('transitioncancel', onTransitionDone, false)
|
||||
}
|
||||
// 绑定元素的transition完成事件,在transition完成后立即完成vue的过度动效
|
||||
// Bind transition end event to finish Vue's transition immediately after the CSS transition
|
||||
el.addEventListener('transitionend', onTransitionDone, false)
|
||||
el.addEventListener('transitioncancel', onTransitionDone, false)
|
||||
},
|
||||
collapseAfterEnter(el) {
|
||||
// console.log('33, collapseAfterEnter');
|
||||
// 过渡效果完成后恢复元素的maxHeight
|
||||
// Restore maxHeight after transition is complete
|
||||
el.style.maxHeight = ''
|
||||
el.style.overflow = this.oldOverflow
|
||||
},
|
||||
|
||||
collapseBeforeLeave(el) {
|
||||
// console.log('44, collapseBeforeLeave', el.scrollHeight);
|
||||
|
||||
this.oldPaddingBottom = el.style.paddingBottom
|
||||
this.oldPaddingTop = el.style.paddingTop
|
||||
this.oldOverflow = el.style.overflow
|
||||
@@ -85,8 +78,6 @@ export default {
|
||||
el.style.overflow = 'hidden'
|
||||
},
|
||||
collapseLeave(el, done) {
|
||||
// console.log('55, collapseLeave', el.scrollHeight);
|
||||
|
||||
if (el.scrollHeight !== 0) {
|
||||
el.style.maxHeight = '0'
|
||||
el.style.paddingBottom = '0'
|
||||
@@ -95,16 +86,14 @@ export default {
|
||||
// done();
|
||||
const onTransitionDone = function() {
|
||||
done()
|
||||
// console.log('leave onTransitionDone');
|
||||
el.removeEventListener('transitionend', onTransitionDone, false)
|
||||
el.removeEventListener('transitioncancel', onTransitionDone, false)
|
||||
}
|
||||
// 绑定元素的transition完成事件,在transition完成后立即完成vue的过度动效
|
||||
// Bind transition end event to finish Vue's transition immediately after the CSS transition
|
||||
el.addEventListener('transitionend', onTransitionDone, false)
|
||||
el.addEventListener('transitioncancel', onTransitionDone, false)
|
||||
},
|
||||
collapseAfterLeave(el) {
|
||||
// console.log('66, collapseAfterLeave');
|
||||
el.style.maxHeight = ''
|
||||
el.style.overflow = this.oldOverflow
|
||||
el.style.paddingBottom = this.oldPaddingBottom
|
||||
|
@@ -341,7 +341,7 @@ export default {
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
<style lang="less" scoped>
|
||||
.pop_btn {
|
||||
text-align: right;
|
||||
margin-top: 24px;
|
||||
|
@@ -1,9 +1,6 @@
|
||||
/* eslint-disable */
|
||||
/*
|
||||
!!!!!!!
|
||||
以下为凶残的cron表达式验证,胆小肾虚及心脏病者慎入!!!
|
||||
不听劝告者后果自负T T
|
||||
!!!!!!!
|
||||
cron表达式验证
|
||||
cron表达式为秒,分,时,日,月,周,年
|
||||
判断正误方法:错误的话返回错误信息,正确的话返回true
|
||||
*/
|
||||
|
@@ -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: '饼状图'
|
||||
|
@@ -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"
|
||||
|
60
cmdb-ui/src/components/Ellipsis/Ellipsis.vue
Normal file
60
cmdb-ui/src/components/Ellipsis/Ellipsis.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<script>
|
||||
import Tooltip from 'ant-design-vue/es/tooltip'
|
||||
import { cutStrByFullLength, getStrFullLength } from '@/components/_util/util'
|
||||
/*
|
||||
const isSupportLineClamp = document.body.style.webkitLineClamp !== undefined;
|
||||
|
||||
const TooltipOverlayStyle = {
|
||||
overflowWrap: 'break-word',
|
||||
wordWrap: 'break-word',
|
||||
};
|
||||
*/
|
||||
|
||||
export default {
|
||||
name: 'Ellipsis',
|
||||
components: {
|
||||
Tooltip,
|
||||
},
|
||||
props: {
|
||||
prefixCls: {
|
||||
type: String,
|
||||
default: 'ant-pro-ellipsis',
|
||||
},
|
||||
tooltip: {
|
||||
type: Boolean,
|
||||
},
|
||||
length: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
lines: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
fullWidthRecognition: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getStrDom(str, fullLength) {
|
||||
return <span>{cutStrByFullLength(str, this.length) + (fullLength > this.length ? '...' : '')}</span>
|
||||
},
|
||||
getTooltip(fullStr, fullLength) {
|
||||
return (
|
||||
<Tooltip overlayStyle={{ maxWidth: '700px' }}>
|
||||
<template slot="title">{fullStr}</template>
|
||||
{this.getStrDom(fullStr, fullLength)}
|
||||
</Tooltip>
|
||||
)
|
||||
},
|
||||
},
|
||||
render() {
|
||||
const { tooltip, length } = this.$props
|
||||
const str = this.$slots.default.map((vNode) => vNode.text).join('')
|
||||
const fullLength = getStrFullLength(str)
|
||||
const strDom = tooltip && fullLength > length ? this.getTooltip(str, fullLength) : this.getStrDom(str, fullLength)
|
||||
return strDom
|
||||
},
|
||||
}
|
||||
</script>
|
3
cmdb-ui/src/components/Ellipsis/index.js
Normal file
3
cmdb-ui/src/components/Ellipsis/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import Ellipsis from './Ellipsis'
|
||||
|
||||
export default Ellipsis
|
38
cmdb-ui/src/components/Ellipsis/index.md
Normal file
38
cmdb-ui/src/components/Ellipsis/index.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Ellipsis 文本自动省略号
|
||||
|
||||
文本过长自动处理省略号,支持按照文本长度和最大行数两种方式截取。
|
||||
|
||||
|
||||
|
||||
引用方式:
|
||||
|
||||
```javascript
|
||||
import Ellipsis from '@/components/Ellipsis'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Ellipsis
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 代码演示 [demo](https://pro.loacg.com/test/home)
|
||||
|
||||
```html
|
||||
<ellipsis :length="100" tooltip>
|
||||
There were injuries alleged in three cases in 2015, and a
|
||||
fourth incident in September, according to the safety recall report. After meeting with US regulators in October, the firm decided to issue a voluntary recall.
|
||||
</ellipsis>
|
||||
```
|
||||
|
||||
|
||||
|
||||
## API
|
||||
|
||||
|
||||
参数 | 说明 | 类型 | 默认值
|
||||
----|------|-----|------
|
||||
tooltip | 移动到文本展示完整内容的提示 | boolean | -
|
||||
length | 在按照长度截取下的文本最大字符数,超过则截取省略 | number | -
|
@@ -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>
|
||||
|
@@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,58 +0,0 @@
|
||||
<template>
|
||||
<div class="footer">
|
||||
<div class="links">
|
||||
<a
|
||||
href="https://veops.cn/"
|
||||
target="_blank"
|
||||
>维易科技</a>
|
||||
<a
|
||||
href="https://github.com/sendya/ant-design-pro-vue"
|
||||
target="_blank"
|
||||
>
|
||||
<a-icon type="github" />
|
||||
</a>
|
||||
|
||||
</div>
|
||||
<div class="copyright">
|
||||
Copyright
|
||||
<a-icon type="copyright" /> 2021-2023 <span>@维易科技</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'GlobalFooter',
|
||||
data () {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.footer {
|
||||
padding: 0 16px;
|
||||
margin: 48px 0 24px;
|
||||
text-align: center;
|
||||
|
||||
.links {
|
||||
margin-bottom: 8px;
|
||||
|
||||
a {
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
|
||||
&:hover {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.copyright {
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,2 +0,0 @@
|
||||
import GlobalFooter from './GlobalFooter'
|
||||
export default GlobalFooter
|
@@ -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 {
|
||||
|
@@ -1,14 +1,6 @@
|
||||
import router, { resetRouter } from '@/router'
|
||||
import Menu from 'ant-design-vue/es/menu'
|
||||
import Icon from 'ant-design-vue/es/icon'
|
||||
import store from '@/store'
|
||||
import {
|
||||
subscribeCIType,
|
||||
subscribeTreeView,
|
||||
} from '@/modules/cmdb/api/preference'
|
||||
import { searchResourceType } from '@/modules/acl/api/resource'
|
||||
import { roleHasPermissionToGrant } from '@/modules/acl/api/permission'
|
||||
import CMDBGrant from '@/modules/cmdb/components/cmdbGrant'
|
||||
import styles from './index.module.less'
|
||||
import { mapActions } from 'vuex'
|
||||
|
||||
@@ -87,40 +79,6 @@ export default {
|
||||
inject: ['reload'],
|
||||
methods: {
|
||||
...mapActions(['UpdateCMDBSEarchValue']),
|
||||
cancelAttributes(e, menu) {
|
||||
const that = this
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
this.$confirm({
|
||||
title: this.$t('alert'),
|
||||
content: this.$t('cmdb.preference.confirmcancelSub2', { name: menu.meta.title }),
|
||||
onOk() {
|
||||
const citypeId = menu.meta.typeId
|
||||
const unsubCIType = subscribeCIType(citypeId, '')
|
||||
const unsubTree = subscribeTreeView(citypeId, '')
|
||||
Promise.all([unsubCIType, unsubTree]).then(() => {
|
||||
that.$message.success(that.$t('cmdb.preference.cancelSubSuccess'))
|
||||
const lastTypeId = window.localStorage.getItem('ops_ci_typeid') || undefined
|
||||
if (Number(citypeId) === Number(lastTypeId)) {
|
||||
localStorage.setItem('ops_ci_typeid', '')
|
||||
}
|
||||
const href = window.location.href
|
||||
const hrefSplit = href.split('/')
|
||||
if (Number(hrefSplit[hrefSplit.length - 1]) === Number(citypeId)) {
|
||||
that.$router.push('/cmdb/preference')
|
||||
}
|
||||
const roles = store.getters.roles
|
||||
resetRouter()
|
||||
store.dispatch('GenerateRoutes', { roles }, { root: true }).then(() => {
|
||||
router.addRoutes(store.getters.appRoutes)
|
||||
})
|
||||
if (hrefSplit[hrefSplit.length - 1] === 'preference') {
|
||||
that.reload()
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
// select menu item
|
||||
onOpenChange(openKeys) {
|
||||
if (this.mode === 'horizontal') {
|
||||
@@ -170,8 +128,6 @@ export default {
|
||||
return this.$t(`${title}`)
|
||||
},
|
||||
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 } }
|
||||
@@ -188,28 +144,11 @@ export default {
|
||||
<tag {...{ props, attrs }}>
|
||||
{this.renderIcon({ icon: menu.meta.icon, customIcon: menu.meta.customIcon, name: menu.meta.name, typeId: menu.meta.typeId, routeName: menu.name, selectedIcon: menu.meta.selectedIcon, })}
|
||||
<span>
|
||||
<span style={menu.meta.style} class={this.renderI18n(menu.meta.title).length > 10 ? 'scroll' : ''}>{this.renderI18n(menu.meta.title)}</span>
|
||||
{isShowDot && !menu.meta.disabled &&
|
||||
<a-popover
|
||||
overlayClassName="custom-menu-extra-submenu"
|
||||
placement="rightTop"
|
||||
arrowPointAtCenter
|
||||
autoAdjustOverflow={false}
|
||||
getPopupContainer={(trigger) => trigger}
|
||||
content={() =>
|
||||
<div>
|
||||
<div onClick={e => this.handlePerm(e, menu, 'CIType')} class="custom-menu-extra-submenu-item"><a-icon type="user-add" />{ this.renderI18n('grant') }</div>
|
||||
<div onClick={e => this.cancelAttributes(e, menu)} class="custom-menu-extra-submenu-item"><a-icon type="star" />{ this.renderI18n('cmdb.preference.cancelSub') }</div>
|
||||
</div>}
|
||||
>
|
||||
<a-icon type="menu" ref="extraEllipsis" class="custom-menu-extra-ellipsis"></a-icon>
|
||||
</a-popover>
|
||||
}
|
||||
{isShowGrant && <a-icon class="custom-menu-extra-ellipsis" onClick={e => this.handlePerm(e, menu, 'RelationView')} type="user-add" />}
|
||||
<span style={menu.meta.style} class={this.renderI18n(menu.meta.title).length > 10 ? 'scroll' : ''}>
|
||||
{this.renderI18n(menu.meta.title)}
|
||||
</span>
|
||||
</span>
|
||||
</tag>
|
||||
{isShowDot && <CMDBGrant ref="cmdbGrantCIType" resourceType="CIType" app_id="cmdb" />}
|
||||
{isShowGrant && <CMDBGrant ref="cmdbGrantRelationView" resourceType="RelationView" app_id="cmdb" />}
|
||||
</Item>
|
||||
)
|
||||
},
|
||||
@@ -272,27 +211,6 @@ export default {
|
||||
)
|
||||
}
|
||||
},
|
||||
handlePerm(e, menu, resource_type_name) {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
roleHasPermissionToGrant({
|
||||
app_id: 'cmdb',
|
||||
resource_type_name,
|
||||
perm: 'grant',
|
||||
resource_name: menu.meta.name,
|
||||
}).then(res => {
|
||||
if (res.result) {
|
||||
console.log(menu)
|
||||
if (resource_type_name === 'CIType') {
|
||||
this.$refs.cmdbGrantCIType.open({ name: menu.meta.name, cmdbGrantType: 'ci', CITypeId: menu.meta?.typeId })
|
||||
} else {
|
||||
this.$refs.cmdbGrantRelationView.open({ name: menu.meta.name, cmdbGrantType: 'relation_view' })
|
||||
}
|
||||
} else {
|
||||
this.$message.error(this.$t('noPermission'))
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
jumpCMDBSearch(value) {
|
||||
this.UpdateCMDBSEarchValue(value)
|
||||
@@ -313,10 +231,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)
|
||||
|
@@ -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>
|
||||
|
@@ -1,10 +0,0 @@
|
||||
import { Spin } from 'ant-design-vue'
|
||||
|
||||
export default {
|
||||
name: 'PageLoading',
|
||||
render () {
|
||||
return (<div style={{ paddingTop: 100, textAlign: 'center' }}>
|
||||
<Spin size="large" />
|
||||
</div>)
|
||||
}
|
||||
}
|
@@ -1,352 +0,0 @@
|
||||
<template>
|
||||
<div class="setting-drawer" ref="settingDrawer">
|
||||
<a-drawer
|
||||
width="300"
|
||||
placement="right"
|
||||
@close="onClose"
|
||||
:closable="false"
|
||||
:visible="visible"
|
||||
>
|
||||
<div class="setting-drawer-index-content">
|
||||
|
||||
<div :style="{ marginBottom: '24px' }">
|
||||
<h3 class="setting-drawer-index-title">整体风格设置</h3>
|
||||
|
||||
<div class="setting-drawer-index-blockChecbox">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
暗色菜单风格
|
||||
</template>
|
||||
<div class="setting-drawer-index-item" @click="handleMenuTheme('dark')">
|
||||
<img src="https://gw.alipayobjects.com/zos/rmsportal/LCkqqYNmvBEbokSDscrm.svg" alt="dark">
|
||||
<div class="setting-drawer-index-selectIcon" v-if="navTheme === 'dark'">
|
||||
<a-icon type="check"/>
|
||||
</div>
|
||||
</div>
|
||||
</a-tooltip>
|
||||
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
亮色菜单风格
|
||||
</template>
|
||||
<div class="setting-drawer-index-item" @click="handleMenuTheme('light')">
|
||||
<img src="https://gw.alipayobjects.com/zos/rmsportal/jpRkZQMyYRryryPNtyIC.svg" alt="light">
|
||||
<div class="setting-drawer-index-selectIcon" v-if="navTheme !== 'dark'">
|
||||
<a-icon type="check"/>
|
||||
</div>
|
||||
</div>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :style="{ marginBottom: '24px' }">
|
||||
<h3 class="setting-drawer-index-title">主题色</h3>
|
||||
|
||||
<div style="height: 20px">
|
||||
<a-tooltip class="setting-drawer-theme-color-colorBlock" v-for="(item, index) in colorList" :key="index">
|
||||
<template slot="title">
|
||||
{{ item.key }}
|
||||
</template>
|
||||
<a-tag :color="item.color" @click="changeColor(item.color)">
|
||||
<a-icon type="check" v-if="item.color === primaryColor"></a-icon>
|
||||
</a-tag>
|
||||
</a-tooltip>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<a-divider />
|
||||
|
||||
<div :style="{ marginBottom: '24px' }">
|
||||
<h3 class="setting-drawer-index-title">导航模式</h3>
|
||||
|
||||
<div class="setting-drawer-index-blockChecbox">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
侧边栏导航
|
||||
</template>
|
||||
<div class="setting-drawer-index-item" @click="handleLayout('sidemenu')">
|
||||
<img src="https://gw.alipayobjects.com/zos/rmsportal/JopDzEhOqwOjeNTXkoje.svg" alt="sidemenu">
|
||||
<div class="setting-drawer-index-selectIcon" v-if="layoutMode === 'sidemenu'">
|
||||
<a-icon type="check"/>
|
||||
</div>
|
||||
</div>
|
||||
</a-tooltip>
|
||||
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
顶部栏导航
|
||||
</template>
|
||||
<div class="setting-drawer-index-item" @click="handleLayout('topmenu')">
|
||||
<img src="https://gw.alipayobjects.com/zos/rmsportal/KDNDBbriJhLwuqMoxcAr.svg" alt="topmenu">
|
||||
<div class="setting-drawer-index-selectIcon" v-if="layoutMode !== 'sidemenu'">
|
||||
<a-icon type="check"/>
|
||||
</div>
|
||||
</div>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<div :style="{ marginTop: '24px' }">
|
||||
<a-list :split="false">
|
||||
<a-list-item>
|
||||
<a-tooltip slot="actions">
|
||||
<template slot="title">
|
||||
该设定仅 [顶部栏导航] 时有效
|
||||
</template>
|
||||
<a-select size="small" style="width: 80px;" :defaultValue="contentWidth" @change="handleContentWidthChange">
|
||||
<a-select-option value="Fixed">固定</a-select-option>
|
||||
<a-select-option value="Fluid" v-if="layoutMode !== 'sidemenu'">流式</a-select-option>
|
||||
</a-select>
|
||||
</a-tooltip>
|
||||
<a-list-item-meta>
|
||||
<div slot="title">内容区域宽度</div>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
<a-switch slot="actions" size="small" :defaultChecked="fixedHeader" @change="handleFixedHeader" />
|
||||
<a-list-item-meta>
|
||||
<div slot="title">固定 Header</div>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
<a-switch slot="actions" size="small" :disabled="!fixedHeader" :defaultChecked="autoHideHeader" @change="handleFixedHeaderHidden" />
|
||||
<a-list-item-meta>
|
||||
<a-tooltip slot="title" placement="left">
|
||||
<template slot="title">固定 Header 时可配置</template>
|
||||
<div :style="{ opacity: !fixedHeader ? '0.5' : '1' }">下滑时隐藏 Header</div>
|
||||
</a-tooltip>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
<a-list-item >
|
||||
<a-switch slot="actions" size="small" :disabled="(layoutMode === 'topmenu')" :defaultChecked="fixSiderbar" @change="handleFixSiderbar" />
|
||||
<a-list-item-meta>
|
||||
<div slot="title" :style="{ textDecoration: layoutMode === 'topmenu' ? 'line-through' : 'unset' }">固定侧边菜单</div>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
</div>
|
||||
</div>
|
||||
<a-divider />
|
||||
|
||||
<div :style="{ marginBottom: '24px' }">
|
||||
<h3 class="setting-drawer-index-title">其他设置</h3>
|
||||
<div>
|
||||
<a-list :split="false">
|
||||
<a-list-item>
|
||||
<a-switch slot="actions" size="small" :defaultChecked="colorWeak" @change="onColorWeak" />
|
||||
<a-list-item-meta>
|
||||
<div slot="title">色弱模式</div>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
<a-switch slot="actions" size="small" :defaultChecked="multiTab" @change="onMultiTab" />
|
||||
<a-list-item-meta>
|
||||
<div slot="title">多页签模式</div>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
</div>
|
||||
</div>
|
||||
<a-divider />
|
||||
<div :style="{ marginBottom: '24px' }">
|
||||
<a-button
|
||||
@click="doCopy"
|
||||
icon="copy"
|
||||
block
|
||||
>拷贝设置</a-button>
|
||||
<a-alert type="warning" :style="{ marginTop: '24px' }">
|
||||
<span slot="message">
|
||||
配置栏只在开发环境用于预览,生产环境不会展现,请手动修改配置文件
|
||||
<a href="https://github.com/sendya/ant-design-pro-vue/blob/master/src/config/setting.js" target="_blank">src/config/setting.js</a>
|
||||
</span>
|
||||
</a-alert>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-drawer-index-handle" @click="toggle">
|
||||
<a-icon type="setting" v-if="!visible"/>
|
||||
<a-icon type="close" v-else/>
|
||||
</div>
|
||||
</a-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SettingItem from './SettingItem'
|
||||
import config from '@/config/setting'
|
||||
import { updateTheme, updateColorWeak, colorList } from './settingConfig'
|
||||
import { mixin, mixinDevice } from '@/utils/mixin'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SettingItem
|
||||
},
|
||||
mixins: [mixin, mixinDevice],
|
||||
data () {
|
||||
return {
|
||||
visible: true,
|
||||
colorList
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
||||
},
|
||||
mounted () {
|
||||
const vm = this
|
||||
setTimeout(() => {
|
||||
vm.visible = false
|
||||
}, 16)
|
||||
updateTheme(this.primaryColor)
|
||||
if (this.colorWeak !== config.colorWeak) {
|
||||
updateColorWeak(this.colorWeak)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showDrawer () {
|
||||
this.visible = true
|
||||
},
|
||||
onClose () {
|
||||
this.visible = false
|
||||
},
|
||||
toggle () {
|
||||
this.visible = !this.visible
|
||||
},
|
||||
onColorWeak (checked) {
|
||||
this.$store.dispatch('ToggleWeak', checked)
|
||||
updateColorWeak(checked)
|
||||
},
|
||||
onMultiTab (checked) {
|
||||
this.$store.dispatch('ToggleMultiTab', checked)
|
||||
},
|
||||
handleMenuTheme (theme) {
|
||||
this.$store.dispatch('ToggleTheme', theme)
|
||||
},
|
||||
doCopy () {
|
||||
// get current settings from mixin or this.$store.state.app, pay attention to the property name
|
||||
const text = `export default {
|
||||
primaryColor: '${this.primaryColor}', // primary color of ant design
|
||||
navTheme: '${this.navTheme}', // theme for nav menu
|
||||
layout: '${this.layoutMode}', // nav menu position: sidemenu or topmenu
|
||||
contentWidth: '${this.contentWidth}', // layout of content: Fluid or Fixed, only works when layout is topmenu
|
||||
fixedHeader: ${this.fixedHeader}, // sticky header
|
||||
fixSiderbar: ${this.fixSiderbar}, // sticky siderbar
|
||||
autoHideHeader: ${this.autoHideHeader}, // auto hide header
|
||||
colorWeak: ${this.colorWeak},
|
||||
multiTab: ${this.multiTab},
|
||||
production: process.env.NODE_ENV === 'production' && process.env.VUE_APP_PREVIEW !== 'true',
|
||||
// vue-ls options
|
||||
storageOptions: {
|
||||
namespace: 'pro__',
|
||||
name: 'ls',
|
||||
storage: 'local',
|
||||
}
|
||||
}`
|
||||
this.$copyText(text).then(message => {
|
||||
console.log('copy', message)
|
||||
this.$message.success('复制完毕')
|
||||
}).catch(err => {
|
||||
console.log('copy.err', err)
|
||||
this.$message.error('复制失败')
|
||||
})
|
||||
},
|
||||
handleLayout (mode) {
|
||||
this.$store.dispatch('ToggleLayoutMode', mode)
|
||||
// 因为顶部菜单不能固定左侧菜单栏,所以强制关闭
|
||||
this.handleFixSiderbar(false)
|
||||
},
|
||||
handleContentWidthChange (type) {
|
||||
this.$store.dispatch('ToggleContentWidth', type)
|
||||
},
|
||||
changeColor (color) {
|
||||
if (this.primaryColor !== color) {
|
||||
this.$store.dispatch('ToggleColor', color)
|
||||
updateTheme(color)
|
||||
}
|
||||
},
|
||||
handleFixedHeader (fixed) {
|
||||
this.$store.dispatch('ToggleFixedHeader', fixed)
|
||||
},
|
||||
handleFixedHeaderHidden (autoHidden) {
|
||||
this.$store.dispatch('ToggleFixedHeaderHidden', autoHidden)
|
||||
},
|
||||
handleFixSiderbar (fixed) {
|
||||
if (this.layoutMode === 'topmenu') {
|
||||
this.$store.dispatch('ToggleFixSiderbar', false)
|
||||
return
|
||||
}
|
||||
this.$store.dispatch('ToggleFixSiderbar', fixed)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.setting-drawer-index-content {
|
||||
|
||||
.setting-drawer-index-blockChecbox {
|
||||
display: flex;
|
||||
|
||||
.setting-drawer-index-item {
|
||||
margin-right: 16px;
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
width: 48px;
|
||||
}
|
||||
|
||||
.setting-drawer-index-selectIcon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
padding-top: 15px;
|
||||
padding-left: 24px;
|
||||
height: 100%;
|
||||
color: #1890ff;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
.setting-drawer-theme-color-colorBlock {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 2px;
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
margin-right: 8px;
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
|
||||
i {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.setting-drawer-index-handle {
|
||||
position: absolute;
|
||||
top: 240px;
|
||||
background: #1890ff;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
right: 300px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
z-index: 1001;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
border-radius: 4px 0 0 4px;
|
||||
|
||||
i {
|
||||
color: rgb(255, 255, 255);
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,38 +0,0 @@
|
||||
<template>
|
||||
<div class="setting-drawer-index-item">
|
||||
<h3 class="setting-drawer-index-title">{{ title }}</h3>
|
||||
<slot></slot>
|
||||
<a-divider v-if="divider"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SettingItem',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
divider: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.setting-drawer-index-item {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.setting-drawer-index-title {
|
||||
font-size: 14px;
|
||||
color: rgba(0, 0, 0, .85);
|
||||
line-height: 22px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
@@ -1,2 +0,0 @@
|
||||
import SettingDrawer from './SettingDrawer'
|
||||
export default SettingDrawer
|
@@ -1,105 +0,0 @@
|
||||
import { message } from 'ant-design-vue/es'
|
||||
// import setting from '../setting';
|
||||
import themeColor from './themeColor.js'
|
||||
|
||||
// let lessNodesAppended
|
||||
|
||||
const colorList = [
|
||||
{
|
||||
key: '薄暮', color: '#F5222D'
|
||||
},
|
||||
{
|
||||
key: '火山', color: '#FA541C'
|
||||
},
|
||||
{
|
||||
key: '日暮', color: '#FAAD14'
|
||||
},
|
||||
{
|
||||
key: '明青', color: '#13C2C2'
|
||||
},
|
||||
{
|
||||
key: '极光绿', color: '#52C41A'
|
||||
},
|
||||
{
|
||||
key: '拂晓蓝(默认)', color: '#1890FF'
|
||||
},
|
||||
{
|
||||
key: '极客蓝', color: '#2F54EB'
|
||||
},
|
||||
{
|
||||
key: '酱紫', color: '#722ED1'
|
||||
}
|
||||
]
|
||||
|
||||
const updateTheme = newPrimaryColor => {
|
||||
const hideMessage = message.loading('正在切换主题!', 0)
|
||||
themeColor.changeColor(newPrimaryColor).finally(t => {
|
||||
hideMessage()
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
const updateTheme = primaryColor => {
|
||||
// Don't compile less in production!
|
||||
/* if (process.env.NODE_ENV === 'production') {
|
||||
return;
|
||||
} * /
|
||||
// Determine if the component is remounted
|
||||
if (!primaryColor) {
|
||||
return
|
||||
}
|
||||
const hideMessage = message.loading('正在编译主题!', 0)
|
||||
function buildIt () {
|
||||
if (!window.less) {
|
||||
return
|
||||
}
|
||||
setTimeout(() => {
|
||||
window.less
|
||||
.modifyVars({
|
||||
'@primary-color': primaryColor
|
||||
})
|
||||
.then(() => {
|
||||
hideMessage()
|
||||
})
|
||||
.catch(() => {
|
||||
message.error('Failed to update theme')
|
||||
hideMessage()
|
||||
})
|
||||
}, 200)
|
||||
}
|
||||
if (!lessNodesAppended) {
|
||||
// insert less.js and color.less
|
||||
const lessStyleNode = document.createElement('link')
|
||||
const lessConfigNode = document.createElement('script')
|
||||
const lessScriptNode = document.createElement('script')
|
||||
lessStyleNode.setAttribute('rel', 'stylesheet/less')
|
||||
lessStyleNode.setAttribute('href', '/color.less')
|
||||
lessConfigNode.innerHTML = `
|
||||
window.less = {
|
||||
async: true,
|
||||
env: 'production',
|
||||
javascriptEnabled: true
|
||||
};
|
||||
`
|
||||
lessScriptNode.src = 'https://gw.alipayobjects.com/os/lib/less.js/3.8.1/less.min.js'
|
||||
lessScriptNode.async = true
|
||||
lessScriptNode.onload = () => {
|
||||
buildIt()
|
||||
lessScriptNode.onload = null
|
||||
}
|
||||
document.body.appendChild(lessStyleNode)
|
||||
document.body.appendChild(lessConfigNode)
|
||||
document.body.appendChild(lessScriptNode)
|
||||
lessNodesAppended = true
|
||||
} else {
|
||||
buildIt()
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
const updateColorWeak = colorWeak => {
|
||||
// document.body.className = colorWeak ? 'colorWeak' : '';
|
||||
colorWeak ? document.body.classList.add('colorWeak') : document.body.classList.remove('colorWeak')
|
||||
}
|
||||
|
||||
export { updateTheme, colorList, updateColorWeak }
|
@@ -1,23 +0,0 @@
|
||||
import client from 'webpack-theme-color-replacer/client'
|
||||
import generate from '@ant-design/colors/lib/generate'
|
||||
|
||||
export default {
|
||||
getAntdSerials (color) {
|
||||
// 淡化(即less的tint)
|
||||
const lightens = new Array(9).fill().map((t, i) => {
|
||||
return client.varyColor.lighten(color, i / 10)
|
||||
})
|
||||
// colorPalette变换得到颜色值
|
||||
const colorPalettes = generate(color)
|
||||
return lightens.concat(colorPalettes)
|
||||
},
|
||||
changeColor (newColor) {
|
||||
var options = {
|
||||
newColors: this.getAntdSerials(newColor), // new colors array, one-to-one corresponde with `matchColors`
|
||||
changeUrl (cssUrl) {
|
||||
return `/${cssUrl}` // while router is not `hash` mode, it needs absolute path
|
||||
}
|
||||
}
|
||||
return client.changer.changeColor(options, Promise)
|
||||
}
|
||||
}
|
@@ -2,10 +2,12 @@ import MultiTab from '@/components/MultiTab'
|
||||
import Result from '@/components/Result'
|
||||
import TagSelect from '@/components/TagSelect'
|
||||
import ExceptionPage from '@/components/Exception'
|
||||
import Ellipsis from '@/components/Ellipsis'
|
||||
|
||||
export {
|
||||
MultiTab,
|
||||
Result,
|
||||
ExceptionPage,
|
||||
TagSelect
|
||||
TagSelect,
|
||||
Ellipsis
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<a-tooltip>
|
||||
<template slot="title">文档中心</template>
|
||||
<template slot="title">{{ $t('documentCenter') }}</template>
|
||||
<span class="document-link">
|
||||
<a-icon type="question-circle" @click="handleClick" />
|
||||
</span>
|
||||
|
@@ -38,7 +38,7 @@
|
||||
<script>
|
||||
import store from '@/store'
|
||||
import { gridSvg, top_agent, top_acl } from '@/core/icons'
|
||||
import { getPreference } from '@/modules/cmdb/api/preference'
|
||||
|
||||
export default {
|
||||
name: 'TopMenu',
|
||||
components: { gridSvg, top_agent, top_acl },
|
||||
@@ -77,18 +77,7 @@ export default {
|
||||
async handleClick(route) {
|
||||
this.visible = false
|
||||
if (route.name !== this.current) {
|
||||
if (route.name === 'cmdb') {
|
||||
const preference = await getPreference()
|
||||
const lastTypeId = window.localStorage.getItem('ops_ci_typeid') || undefined
|
||||
if (lastTypeId && preference.type_ids.some((item) => item === Number(lastTypeId))) {
|
||||
this.$router.push(`/cmdb/instances/types/${lastTypeId}`)
|
||||
} else {
|
||||
this.$router.push('/cmdb/dashboard')
|
||||
}
|
||||
} else {
|
||||
this.$router.push(route.redirect)
|
||||
}
|
||||
// this.current = route.name
|
||||
this.$router.push(route.redirect)
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable */
|
||||
import Vue from 'vue'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
@@ -11,16 +10,23 @@ import i18n from '@/lang'
|
||||
|
||||
NProgress.configure({ showSpinner: false })
|
||||
|
||||
// 不用认证的页面
|
||||
const whitePath = ['/user/login', '/user/logout', '/user/register', '/api/sso/login', '/api/sso/logout', '/user/forgetPassword']
|
||||
// pages that do not require authentication
|
||||
const whitePath = [
|
||||
'/user/login',
|
||||
'/user/logout',
|
||||
'/user/register',
|
||||
'/api/sso/login',
|
||||
'/api/sso/logout',
|
||||
'/user/forgetPassword'
|
||||
]
|
||||
|
||||
// 此处不处理登录, 只处理 是否有用户信息的认证 前端permission的处理 axios处理401 -> 登录
|
||||
// 登录页面处理处理 是否使用单点登录
|
||||
// Only handle user info authentication here, not login logic.
|
||||
// Frontend permission handling; axios handles 401 -> login.
|
||||
// Login page handles whether to use SSO.
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
NProgress.start() // start progress bar
|
||||
to.meta && (!!to.meta.title && setDocumentTitle(`${i18n.t(to.meta.title)} - ${domTitle}`))
|
||||
|
||||
const authed = store.state.authed
|
||||
const auth_type = localStorage.getItem('ops_auth_type')
|
||||
if (whitePath.includes(to.path)) {
|
||||
next()
|
||||
@@ -28,17 +34,17 @@ router.beforeEach(async (to, from, next) => {
|
||||
store.dispatch('GetAuthDataEnable')
|
||||
store.dispatch('GetInfo').then(res => {
|
||||
const roles = res.result && res.result.role
|
||||
store.dispatch("loadAllUsers")
|
||||
store.dispatch("loadAllEmployees")
|
||||
store.dispatch("loadAllDepartments")
|
||||
store.dispatch('loadAllUsers')
|
||||
store.dispatch('loadAllEmployees')
|
||||
store.dispatch('loadAllDepartments')
|
||||
store.dispatch('GenerateRoutes', { roles }).then(() => {
|
||||
router.addRoutes(store.getters.appRoutes)
|
||||
const redirect = decodeURIComponent(from.query.redirect || to.path)
|
||||
if (to.path === redirect) {
|
||||
// hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
|
||||
// Ensure addRoutes is complete, set replace: true so navigation will not leave a history record
|
||||
next({ ...to, replace: true })
|
||||
} else {
|
||||
// 跳转到目的路由
|
||||
// Redirect to the target route
|
||||
next({ path: redirect })
|
||||
}
|
||||
})
|
||||
|
@@ -109,9 +109,29 @@ export default {
|
||||
default: 'default',
|
||||
tip: 'Tip',
|
||||
cmdbSearch: 'Search',
|
||||
requestError: 'An error occurred, please try again later',
|
||||
requestServiceError: 'Unknown error on the server, please contact the administrator',
|
||||
requestWait: 'The modification has been submitted, please wait for review ({time} seconds)',
|
||||
documentCenter: 'Document Center',
|
||||
exception: {
|
||||
backToHome: 'Back to home page',
|
||||
desc1: 'Sorry, you are not authorized to access this page',
|
||||
desc2: 'Sorry, the page you are visiting does not exist or is still under development',
|
||||
desc3: 'Sorry, server error'
|
||||
},
|
||||
pagination: {
|
||||
total: '{range0}-{range1} of {total} items'
|
||||
},
|
||||
components: {
|
||||
colorTagSelectTip: 'Enter or select tags',
|
||||
database: 'Database',
|
||||
system: 'System',
|
||||
language: 'Language',
|
||||
status: 'Status',
|
||||
commonComponent: 'Common Component',
|
||||
data: 'Data',
|
||||
cloud: 'Cloud'
|
||||
},
|
||||
topMenu: {
|
||||
personalCenter: 'My Profile',
|
||||
logout: 'Logout',
|
||||
|
@@ -109,9 +109,29 @@ export default {
|
||||
default: '默认',
|
||||
tip: '提示',
|
||||
cmdbSearch: '搜索一下',
|
||||
requestError: '出现错误,请稍后再试',
|
||||
requestServiceError: '服务端未知错误, 请联系管理员!',
|
||||
requestWait: '修改已提交,请等待审核({time}s)',
|
||||
documentCenter: '文档中心',
|
||||
exception: {
|
||||
backToHome: '返回首页',
|
||||
desc1: '抱歉,你无权访问该页面',
|
||||
desc2: '抱歉,你访问的页面不存在或仍在开发中',
|
||||
desc3: '抱歉,服务器出错了'
|
||||
},
|
||||
pagination: {
|
||||
total: '当前展示 {range0}-{range1} 条数据, 共 {total} 条'
|
||||
},
|
||||
components: {
|
||||
colorTagSelectTip: '选择或输入(回车确定)标签',
|
||||
database: '数据库',
|
||||
system: '操作系统',
|
||||
language: '语言',
|
||||
status: '状态',
|
||||
commonComponent: '常用组件',
|
||||
data: '数据',
|
||||
cloud: '云'
|
||||
},
|
||||
topMenu: {
|
||||
personalCenter: '个人中心',
|
||||
logout: '退出登录',
|
||||
|
@@ -63,8 +63,6 @@ import RouteView from './RouteView'
|
||||
import MultiTab from '@/components/MultiTab'
|
||||
import SideMenu from '@/components/Menu/SideMenu'
|
||||
import GlobalHeader from '@/components/GlobalHeader'
|
||||
import GlobalFooter from '@/components/GlobalFooter'
|
||||
import SettingDrawer from '@/components/SettingDrawer'
|
||||
|
||||
export default {
|
||||
name: 'BasicLayout',
|
||||
@@ -74,8 +72,6 @@ export default {
|
||||
MultiTab,
|
||||
SideMenu,
|
||||
GlobalHeader,
|
||||
GlobalFooter,
|
||||
SettingDrawer,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable */
|
||||
import '@babel/polyfill'
|
||||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
@@ -14,10 +13,9 @@ import i18n from './lang'
|
||||
|
||||
import iconFont from '../public/iconfont/iconfont'
|
||||
|
||||
// 存在直接crash的风险 还未到
|
||||
const customIcon = Icon.createFromIconfontCN(iconFont)
|
||||
Vue.component('ops-icon', customIcon)
|
||||
var vue;
|
||||
var vue
|
||||
|
||||
async function start() {
|
||||
const _vue = new Vue({
|
||||
|
@@ -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>
|
||||
|
@@ -63,7 +63,14 @@
|
||||
show-quick-jumper
|
||||
:current="tablePage.currentPage"
|
||||
:total="tablePage.total"
|
||||
:show-total="(total, range) => `当前展示 ${range[0]}-${range[1]} 条数据, 共 ${total} 条`"
|
||||
:show-total="
|
||||
(total, range) =>
|
||||
$t('pagination.total', {
|
||||
range0: range[0],
|
||||
range1: range[1],
|
||||
total,
|
||||
})
|
||||
"
|
||||
:page-size="tablePage.pageSize"
|
||||
:default-current="1"
|
||||
:page-size-options="pageSizeOptions"
|
||||
@@ -195,11 +202,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;
|
||||
}
|
||||
}
|
||||
|
@@ -138,7 +138,14 @@
|
||||
show-quick-jumper
|
||||
:current="tablePage.currentPage"
|
||||
:total="tablePage.total"
|
||||
:show-total="(total, range) => `当前展示 ${range[0]}-${range[1]} 条数据, 共 ${total} 条`"
|
||||
:show-total="
|
||||
(total, range) =>
|
||||
$t('pagination.total', {
|
||||
range0: range[0],
|
||||
range1: range[1],
|
||||
total,
|
||||
})
|
||||
"
|
||||
:page-size="tablePage.pageSize"
|
||||
:default-current="1"
|
||||
:page-size-options="pageSizeOptions"
|
||||
@@ -358,11 +365,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 {
|
||||
|
@@ -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 }) => {
|
||||
@@ -110,7 +107,14 @@
|
||||
show-quick-jumper
|
||||
:current="tablePage.currentPage"
|
||||
:total="tablePage.total"
|
||||
:show-total="(total, range) => `当前展示 ${range[0]}-${range[1]} 条数据, 共 ${total} 条`"
|
||||
:show-total="
|
||||
(total, range) =>
|
||||
$t('pagination.total', {
|
||||
range0: range[0],
|
||||
range1: range[1],
|
||||
total,
|
||||
})
|
||||
"
|
||||
:page-size="tablePage.pageSize"
|
||||
:default-current="1"
|
||||
:page-size-options="pageSizeOptions"
|
||||
@@ -155,6 +159,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 +299,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;
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -157,3 +157,18 @@ export function preferenceCitypeOrder(data) {
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function getAutoSubscription() {
|
||||
return axios({
|
||||
url: '/v0.1/preference/auto_subscription',
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
export function putAutoSubscription(data) {
|
||||
return axios({
|
||||
url: '/v0.1/preference/auto_subscription',
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
@@ -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%;
|
||||
}
|
||||
|
||||
|
@@ -21,7 +21,7 @@
|
||||
</template>
|
||||
<span
|
||||
class="ci-icon-letter"
|
||||
v-else
|
||||
v-else-if="title"
|
||||
>
|
||||
<span>
|
||||
{{ title[0].toUpperCase() }}
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user