Compare commits

...

41 Commits

Author SHA1 Message Date
pycook
cf594f04ba fix(api): import CIType 2024-03-29 15:50:07 +08:00
ivonGwy
4232094aed fix: discover scripts (#458)
Co-authored-by: wang-liang0615 <dhuwl0615@163.com>
2024-03-29 15:48:46 +08:00
dagongren
d08827d086 style and 文案变更 (#457) 2024-03-29 15:02:18 +08:00
pycook
d25ae532cd fix(api): CIType template import 2024-03-29 14:20:56 +08:00
pycook
9fbb6ee64d docs: docker-compose changed to docker compose 2024-03-29 13:27:23 +08:00
pycook
b62f0e96fd Merge branch 'master' of github.com:veops/cmdb 2024-03-29 13:14:00 +08:00
pycook
c1bcd0ce45 fix(acl): del resource 2024-03-29 13:13:38 +08:00
dagongren
c8b55c34eb i18n (#456) 2024-03-29 13:11:52 +08:00
pycook
4b5906770f release: v2.4.1 2024-03-29 12:47:23 +08:00
dagongren
4188ac7252 i18n (#455) 2024-03-29 12:23:23 +08:00
dagongren
2efbc6474a style && service tree define (#454) 2024-03-29 11:53:43 +08:00
pycook
03eac0c4d2 pref(api): error tips for out of range value (#453) 2024-03-29 11:46:50 +08:00
dagongren
2a861250eb fix:icon/filter/router...and some bugs (#451) 2024-03-29 10:50:14 +08:00
dagongren
8fc19d8b7c icon font && opsTable (#450) 2024-03-28 20:59:32 +08:00
dagongren
430d2ff6d0 fix:cmdbgrant (#449) 2024-03-28 20:16:47 +08:00
dagongren
2517009d70 fix:Login (#448) 2024-03-28 19:56:54 +08:00
dagongren
67da360d80 Dev UI 240328 (#447)
* feat:ui 全面升级

* feat:ui全面升级
2024-03-28 19:53:54 +08:00
dagongren
24c56fb259 feat:ui 全面升级 (#446)
* feat:ui 全面升级

* feat:ui全面升级
2024-03-28 19:47:46 +08:00
pycook
37d5da65de Dev api 240328 (#445)
* feat(api): login api supports parameter auth_with_ldap

* fix(api): transfer attribute
2024-03-28 19:12:47 +08:00
dagongren
2224ebd533 feat:ui 全面升级 (#444) 2024-03-28 18:38:15 +08:00
pycook
bf6331d215 fix(api): batch import ci relation 2024-03-26 20:38:39 +08:00
pycook
b18b90ab4e fix(api): import CIType
fix(api): import CIType
2024-03-26 16:53:10 +08:00
pycook
702e17a7a4 fix(api): revoke service tree node permissions
fix(api): revoke service tree node permissions
2024-03-26 12:05:22 +08:00
pycook
a7586aa140 feat(api): support service tree editing (#437) 2024-03-26 10:58:11 +08:00
simontigers
ad3f96431c Merge pull request #431 from simontigers/common_employee_edit_department_in_acl
fix(api): common_employee_edit department in acl role
2024-03-25 11:46:30 +08:00
hu.sima
1515820713 fix(api): common_employee_edit department in acl role 2024-03-25 11:46:04 +08:00
simontigers
7728b57878 Merge pull request #430 from simontigers/common_file_ext_check
fix(api): check file ext with magic
2024-03-25 11:17:36 +08:00
hu.sima
a419eefd72 fix(api): check file ext with magic 2024-03-25 11:16:04 +08:00
simontigers
a44e5f6cf1 Merge pull request #429 from simontigers/common_check_new_columns
fix(api): common check new columns
2024-03-22 17:52:19 +08:00
simontigers
7d46e92c2d fix(api): common check new columns 2024-03-22 16:48:16 +08:00
pycook
4117cf87ec Merge branch 'master' of github.com:veops/cmdb 2024-03-20 11:56:49 +08:00
pycook
9e0fe0b818 fix: custom dashboard 2024-03-20 11:56:39 +08:00
dagongren
2a8f1ab9a4 style:update global.less (#426) 2024-03-19 10:01:38 +08:00
pycook
c0fe99b8c7 release: 2.3.13 2024-03-18 20:35:51 +08:00
dagongren
42feb4b862 feat(cmdb-ui):service tree grant (#425) 2024-03-18 19:59:16 +08:00
pycook
482d34993b Dev api 0308 (#424)
* feat(api): grant by node in relation view

* fix(api): When removing attributes, remove the unique constraint

* feat(api): grant by service tree
2024-03-18 19:57:25 +08:00
simontigers
7ff309b8b8 fix(api): edit employee depart with rid=0 (#420) 2024-03-12 17:46:50 +08:00
rustrover
98eb47d44f fix: some typos (#415)
Signed-off-by: gcmutator <329964069@qq.com>
Co-authored-by: gcmutator <329964069@qq.com>
2024-03-11 15:04:38 +08:00
pycook
9ab0f624ef fix(api): remove ACL resources when deleting CIType (#414) 2024-03-08 16:31:03 +08:00
pycook
3f3eda8b3c fix(api): issule #412, unique value restrictions (#413) 2024-03-05 16:21:27 +08:00
pycook
f788adc8cf feat(api): multi-id search (#411)
_id:(id1;id2)
2024-03-04 15:15:34 +08:00
135 changed files with 12685 additions and 8796 deletions

View File

@@ -74,17 +74,17 @@
### Docker 一键快速构建 ### Docker 一键快速构建
> 方法一 > 方法一
- 第一步: 先安装 docker 环境, 以及docker-compose - 第一步: 先安装 Docker 环境, 以及Docker Compose (v2)
- 第二步: 拷贝项目 - 第二步: 拷贝项目
```shell ```shell
git clone https://github.com/veops/cmdb.git git clone https://github.com/veops/cmdb.git
``` ```
- 第三步:进入主目录,执行: - 第三步:进入主目录,执行:
``` ```
docker-compose up -d docker compose up -d
``` ```
> 方法二, 该方法适用于linux系统 > 方法二, 该方法适用于linux系统
- 第一步: 先安装 docker 环境, 以及docker-compose - 第一步: 先安装 Docker 环境, 以及Docker Compose (v2)
- 第二步: 直接使用项目根目录下的install.sh 文件进行 `安装`、`启动`、`暂停`、`查状态`、`删除`、`卸载` - 第二步: 直接使用项目根目录下的install.sh 文件进行 `安装`、`启动`、`暂停`、`查状态`、`删除`、`卸载`
```shell ```shell
curl -so install.sh https://raw.githubusercontent.com/veops/cmdb/master/install.sh curl -so install.sh https://raw.githubusercontent.com/veops/cmdb/master/install.sh

View File

@@ -15,6 +15,7 @@ Flask-SQLAlchemy = "==2.5.0"
SQLAlchemy = "==1.4.49" SQLAlchemy = "==1.4.49"
PyMySQL = "==1.1.0" PyMySQL = "==1.1.0"
redis = "==4.6.0" redis = "==4.6.0"
python-redis-lock = "==4.0.0"
# Migrations # Migrations
Flask-Migrate = "==2.5.2" Flask-Migrate = "==2.5.2"
# Deployment # Deployment
@@ -65,6 +66,7 @@ hvac = "==2.0.0"
colorama = ">=0.4.6" colorama = ">=0.4.6"
pycryptodomex = ">=3.19.0" pycryptodomex = ">=3.19.0"
lz4 = ">=4.3.2" lz4 = ">=4.3.2"
python-magic = "==0.4.27"
[dev-packages] [dev-packages]
# Testing # Testing

View File

@@ -224,7 +224,7 @@ def common_check_new_columns():
column_type = new_column.type.compile(engine.dialect) column_type = new_column.type.compile(engine.dialect)
default_value = new_column.default.arg if new_column.default else None default_value = new_column.default.arg if new_column.default else None
sql = "ALTER TABLE " + target_table_name + " ADD COLUMN " + new_column.name + " " + column_type sql = "ALTER TABLE " + target_table_name + " ADD COLUMN " + f"`{new_column.name}`" + " " + column_type
if new_column.comment: if new_column.comment:
sql += f" comment '{new_column.comment}'" sql += f" comment '{new_column.comment}'"

View File

@@ -331,14 +331,14 @@ class AutoDiscoveryCICRUD(DBMixin):
@staticmethod @staticmethod
def get_attributes_by_type_id(type_id): def get_attributes_by_type_id(type_id):
from api.lib.cmdb.ci_type import CITypeAttributeManager from api.lib.cmdb.ci_type import CITypeAttributeManager
attributes = [i[1] for i in CITypeAttributeManager.get_all_attributes(type_id) or []] attributes = [i for i in CITypeAttributeManager.get_attributes_by_type_id(type_id) or []]
attr_names = set() attr_names = set()
adts = AutoDiscoveryCITypeCRUD.get_by_type_id(type_id) adts = AutoDiscoveryCITypeCRUD.get_by_type_id(type_id)
for adt in adts: for adt in adts:
attr_names |= set((adt.attributes or {}).values()) attr_names |= set((adt.attributes or {}).values())
return [attr.to_dict() for attr in attributes if attr.name in attr_names] return [attr for attr in attributes if attr['name'] in attr_names]
@classmethod @classmethod
def search(cls, page, page_size, fl=None, **kwargs): def search(cls, page, page_size, fl=None, **kwargs):

View File

@@ -309,7 +309,7 @@ class CMDBCounterCache(object):
s = RelSearch([i[0] for i in type_id_names], level, other_filer or '') s = RelSearch([i[0] for i in type_id_names], level, other_filer or '')
try: try:
stats = s.statistics(type_ids) stats = s.statistics(type_ids, need_filter=False)
except SearchError as e: except SearchError as e:
current_app.logger.error(e) current_app.logger.error(e)
return return

View File

@@ -6,6 +6,7 @@ import datetime
import json import json
import threading import threading
import redis_lock
from flask import abort from flask import abort
from flask import current_app from flask import current_app
from flask_login import current_user from flask_login import current_user
@@ -45,7 +46,6 @@ from api.lib.perm.acl.acl import is_app_admin
from api.lib.perm.acl.acl import validate_permission from api.lib.perm.acl.acl import validate_permission
from api.lib.secrets.inner import InnerCrypt from api.lib.secrets.inner import InnerCrypt
from api.lib.secrets.vault import VaultClient from api.lib.secrets.vault import VaultClient
from api.lib.utils import Lock
from api.lib.utils import handle_arg_list from api.lib.utils import handle_arg_list
from api.lib.webhook import webhook_request from api.lib.webhook import webhook_request
from api.models.cmdb import AttributeHistory from api.models.cmdb import AttributeHistory
@@ -60,8 +60,8 @@ from api.tasks.cmdb import ci_delete_trigger
from api.tasks.cmdb import ci_relation_add from api.tasks.cmdb import ci_relation_add
from api.tasks.cmdb import ci_relation_cache from api.tasks.cmdb import ci_relation_cache
from api.tasks.cmdb import ci_relation_delete from api.tasks.cmdb import ci_relation_delete
from api.tasks.cmdb import delete_id_filter
PRIVILEGED_USERS = {"worker", "cmdb_agent", "agent"}
PASSWORD_DEFAULT_SHOW = "******" PASSWORD_DEFAULT_SHOW = "******"
@@ -278,10 +278,10 @@ class CIManager(object):
@staticmethod @staticmethod
def _auto_inc_id(attr): def _auto_inc_id(attr):
db.session.remove() db.session.commit()
value_table = TableMap(attr_name=attr.name).table value_table = TableMap(attr_name=attr.name).table
with Lock("auto_inc_id_{}".format(attr.name), need_lock=True): with redis_lock.Lock(rd.r, "auto_inc_id_{}".format(attr.name)):
max_v = value_table.get_by(attr_id=attr.id, only_query=True).order_by( max_v = value_table.get_by(attr_id=attr.id, only_query=True).order_by(
getattr(value_table, 'value').desc()).first() getattr(value_table, 'value').desc()).first()
if max_v is not None: if max_v is not None:
@@ -312,10 +312,10 @@ class CIManager(object):
unique_key = AttributeCache.get(ci_type.unique_id) or abort( unique_key = AttributeCache.get(ci_type.unique_id) or abort(
400, ErrFormat.unique_value_not_found.format("unique_id={}".format(ci_type.unique_id))) 400, ErrFormat.unique_value_not_found.format("unique_id={}".format(ci_type.unique_id)))
if (unique_key.default and unique_key.default.get('default') == AttributeDefaultValueEnum.AUTO_INC_ID and
not ci_dict.get(unique_key.name)):
ci_dict[unique_key.name] = cls._auto_inc_id(unique_key)
unique_value = None
if not (unique_key.default and unique_key.default.get('default') == AttributeDefaultValueEnum.AUTO_INC_ID and
not ci_dict.get(unique_key.name)): # primary key is not auto inc id
unique_value = ci_dict.get(unique_key.name) or ci_dict.get(unique_key.alias) or ci_dict.get(unique_key.id) unique_value = ci_dict.get(unique_key.name) or ci_dict.get(unique_key.alias) or ci_dict.get(unique_key.id)
unique_value = unique_value or abort(400, ErrFormat.unique_key_required.format(unique_key.name)) unique_value = unique_value or abort(400, ErrFormat.unique_key_required.format(unique_key.name))
@@ -327,8 +327,15 @@ class CIManager(object):
ci = None ci = None
record_id = None record_id = None
password_dict = {} password_dict = {}
need_lock = current_user.username not in current_app.config.get('PRIVILEGED_USERS', PRIVILEGED_USERS) with redis_lock.Lock(rd.r, ci_type.name):
with Lock(ci_type_name, need_lock=need_lock): db.session.commit()
if (unique_key.default and unique_key.default.get('default') == AttributeDefaultValueEnum.AUTO_INC_ID and
not ci_dict.get(unique_key.name)):
ci_dict[unique_key.name] = cls._auto_inc_id(unique_key)
current_app.logger.info(ci_dict[unique_key.name])
unique_value = ci_dict[unique_key.name]
existed = cls.ci_is_exist(unique_key, unique_value, ci_type.id) existed = cls.ci_is_exist(unique_key, unique_value, ci_type.id)
if existed is not None: if existed is not None:
if exist_policy == ExistPolicy.REJECT: if exist_policy == ExistPolicy.REJECT:
@@ -463,8 +470,9 @@ class CIManager(object):
limit_attrs = self._valid_ci_for_no_read(ci) if not _is_admin else {} limit_attrs = self._valid_ci_for_no_read(ci) if not _is_admin else {}
record_id = None record_id = None
need_lock = current_user.username not in current_app.config.get('PRIVILEGED_USERS', PRIVILEGED_USERS) with redis_lock.Lock(rd.r, ci.ci_type.name):
with Lock(ci.ci_type.name, need_lock=need_lock): db.session.commit()
self._valid_unique_constraint(ci.type_id, ci_dict, ci_id) self._valid_unique_constraint(ci.type_id, ci_dict, ci_id)
ci_dict = {k: v for k, v in ci_dict.items() if k in ci_type_attrs_name} ci_dict = {k: v for k, v in ci_dict.items() if k in ci_type_attrs_name}
@@ -551,6 +559,7 @@ class CIManager(object):
AttributeHistoryManger.add(None, ci_id, [(None, OperateType.DELETE, ci_dict, None)], ci.type_id) AttributeHistoryManger.add(None, ci_id, [(None, OperateType.DELETE, ci_dict, None)], ci.type_id)
ci_delete.apply_async(args=(ci_id,), queue=CMDB_QUEUE) ci_delete.apply_async(args=(ci_id,), queue=CMDB_QUEUE)
delete_id_filter.apply_async(args=(ci_id,), queue=CMDB_QUEUE)
return ci_id return ci_id
@@ -857,6 +866,20 @@ class CIRelationManager(object):
return numfound, len(ci_ids), result return numfound, len(ci_ids), result
@staticmethod
def recursive_children(ci_id):
result = []
def _get_children(_id):
children = CIRelation.get_by(first_ci_id=_id, to_dict=False)
result.extend([i.second_ci_id for i in children])
for child in children:
_get_children(child.second_ci_id)
_get_children(ci_id)
return result
@staticmethod @staticmethod
def _sort_handler(sort_by, query_sql): def _sort_handler(sort_by, query_sql):
@@ -912,7 +935,7 @@ class CIRelationManager(object):
@staticmethod @staticmethod
def _check_constraint(first_ci_id, first_type_id, second_ci_id, second_type_id, type_relation): def _check_constraint(first_ci_id, first_type_id, second_ci_id, second_type_id, type_relation):
db.session.remove() db.session.commit()
if type_relation.constraint == ConstraintEnum.Many2Many: if type_relation.constraint == ConstraintEnum.Many2Many:
return return
@@ -972,7 +995,7 @@ class CIRelationManager(object):
else: else:
type_relation = CITypeRelation.get_by_id(relation_type_id) type_relation = CITypeRelation.get_by_id(relation_type_id)
with Lock("ci_relation_add_{}_{}".format(first_ci.type_id, second_ci.type_id), need_lock=True): with redis_lock.Lock(rd.r, "ci_relation_add_{}_{}".format(first_ci.type_id, second_ci.type_id)):
cls._check_constraint(first_ci_id, first_ci.type_id, second_ci_id, second_ci.type_id, type_relation) cls._check_constraint(first_ci_id, first_ci.type_id, second_ci_id, second_ci.type_id, type_relation)
@@ -1008,6 +1031,7 @@ class CIRelationManager(object):
his_manager.add(cr, operate_type=OperateType.DELETE) his_manager.add(cr, operate_type=OperateType.DELETE)
ci_relation_delete.apply_async(args=(cr.first_ci_id, cr.second_ci_id, cr.ancestor_ids), queue=CMDB_QUEUE) ci_relation_delete.apply_async(args=(cr.first_ci_id, cr.second_ci_id, cr.ancestor_ids), queue=CMDB_QUEUE)
delete_id_filter.apply_async(args=(cr.second_ci_id,), queue=CMDB_QUEUE)
return cr_id return cr_id
@@ -1019,9 +1043,13 @@ class CIRelationManager(object):
to_dict=False, to_dict=False,
first=True) first=True)
ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id, ancestor_ids), queue=CMDB_QUEUE) if cr is not None:
cls.delete(cr.id)
return cr and cls.delete(cr.id) ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id, ancestor_ids), queue=CMDB_QUEUE)
delete_id_filter.apply_async(args=(second_ci_id,), queue=CMDB_QUEUE)
return cr
@classmethod @classmethod
def batch_update(cls, ci_ids, parents, children, ancestor_ids=None): def batch_update(cls, ci_ids, parents, children, ancestor_ids=None):
@@ -1062,7 +1090,7 @@ class CIRelationManager(object):
class CITriggerManager(object): class CITriggerManager(object):
@staticmethod @staticmethod
def get(type_id): def get(type_id):
db.session.remove() db.session.commit()
return CITypeTrigger.get_by(type_id=type_id, to_dict=True) return CITypeTrigger.get_by(type_id=type_id, to_dict=True)
@staticmethod @staticmethod

View File

@@ -76,6 +76,10 @@ class CITypeManager(object):
return CIType.get_by_id(ci_type.id) return CIType.get_by_id(ci_type.id)
def get_icons(self):
return {i.id: i.icon or i.name for i in db.session.query(
self.cls.id, self.cls.icon, self.cls.name).filter(self.cls.deleted.is_(False))}
@staticmethod @staticmethod
def get_ci_types(type_name=None, like=True): def get_ci_types(type_name=None, like=True):
resources = None resources = None
@@ -223,10 +227,12 @@ class CITypeManager(object):
if item.get('parent_id') == type_id or item.get('child_id') == type_id: if item.get('parent_id') == type_id or item.get('child_id') == type_id:
return abort(400, ErrFormat.ci_relation_view_exists_and_cannot_delete_type.format(rv.name)) return abort(400, ErrFormat.ci_relation_view_exists_and_cannot_delete_type.format(rv.name))
for item in CITypeRelation.get_by(parent_id=type_id, to_dict=False): for item in (CITypeRelation.get_by(parent_id=type_id, to_dict=False) +
item.soft_delete(commit=False) CITypeRelation.get_by(child_id=type_id, to_dict=False)):
if current_app.config.get('USE_ACL'):
resource_name = CITypeRelationManager.acl_resource_name(item.parent.name, item.child.name)
ACLManager().del_resource(resource_name, ResourceTypeEnum.CI_TYPE_RELATION)
for item in CITypeRelation.get_by(child_id=type_id, to_dict=False):
item.soft_delete(commit=False) item.soft_delete(commit=False)
for table in [PreferenceTreeView, PreferenceShowAttributes, PreferenceSearchOption, CustomDashboard, for table in [PreferenceTreeView, PreferenceShowAttributes, PreferenceSearchOption, CustomDashboard,
@@ -644,10 +650,30 @@ class CITypeAttributeManager(object):
existed.soft_delete() existed.soft_delete()
for ci in CI.get_by(type_id=type_id, to_dict=False): for ci in CI.get_by(type_id=type_id, to_dict=False):
AttributeValueManager.delete_attr_value(attr_id, ci.id) AttributeValueManager.delete_attr_value(attr_id, ci.id, commit=False)
ci_cache.apply_async(args=(ci.id, None, None), queue=CMDB_QUEUE) 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):
if attr_id in item.attr_ids:
attr_ids = copy.deepcopy(item.attr_ids)
attr_ids.remove(attr_id)
if attr_ids:
item.update(attr_ids=attr_ids, commit=False)
else:
item.soft_delete(commit=False)
item = CITypeTrigger.get_by(type_id=_type_id, attr_id=attr_id, to_dict=False, first=True)
item and item.soft_delete(commit=False)
db.session.commit()
CITypeAttributeCache.clean(type_id, attr_id) CITypeAttributeCache.clean(type_id, attr_id)
CITypeHistoryManager.add(CITypeOperateType.DELETE_ATTRIBUTE, type_id, attr_id=attr.id, CITypeHistoryManager.add(CITypeOperateType.DELETE_ATTRIBUTE, type_id, attr_id=attr.id,
@@ -673,6 +699,10 @@ class CITypeAttributeManager(object):
to_group = CITypeAttributeGroup.get_by(type_id=type_id, name=to_group_name, first=True, to_dict=False) to_group = CITypeAttributeGroup.get_by(type_id=type_id, name=to_group_name, first=True, to_dict=False)
to_group_id = to_group and to_group.id to_group_id = to_group and to_group.id
if not to_group_id and CITypeInheritance.get_by(child_id=type_id, to_dict=False):
to_group = CITypeAttributeGroup.create(type_id=type_id, name=to_group_name)
to_group_id = to_group and to_group.id
if from_group_id != to_group_id: if from_group_id != to_group_id:
if from_group_id is not None: if from_group_id is not None:
CITypeAttributeGroupManager.delete_item(from_group_id, attr_id) CITypeAttributeGroupManager.delete_item(from_group_id, attr_id)
@@ -1162,6 +1192,8 @@ class CITypeTemplateManager(object):
i.pop('choice_web_hook', None) i.pop('choice_web_hook', None)
i.pop('choice_other', None) i.pop('choice_other', None)
i.pop('order', None) i.pop('order', None)
i.pop('inherited', None)
i.pop('inherited_from', None)
choice_value = i.pop('choice_value', None) choice_value = i.pop('choice_value', None)
if not choice_value: if not choice_value:
i['is_choice'] = False i['is_choice'] = False
@@ -1273,6 +1305,8 @@ class CITypeTemplateManager(object):
_group = copy.deepcopy(group) _group = copy.deepcopy(group)
_group.pop('attributes', None) _group.pop('attributes', None)
_group.pop('id', None) _group.pop('id', None)
_group.pop('inherited', None)
_group.pop('inherited_from', None)
existed = CITypeAttributeGroup.get_by(name=_group['name'], existed = CITypeAttributeGroup.get_by(name=_group['name'],
type_id=type_id_map.get(_group['type_id'], _group['type_id']), type_id=type_id_map.get(_group['type_id'], _group['type_id']),
first=True, to_dict=False) first=True, to_dict=False)
@@ -1328,9 +1362,13 @@ class CITypeTemplateManager(object):
rule.pop("id", None) rule.pop("id", None)
rule.pop("created_at", None) rule.pop("created_at", None)
rule.pop("updated_at", None) rule.pop("updated_at", None)
rule.pop("relation", None)
rule['uid'] = current_user.uid rule['uid'] = current_user.uid
if not rule.get('attributes'):
continue
existed = False existed = False
for i in AutoDiscoveryCIType.get_by(type_id=ci_type.id, adr_id=rule['adr_id'], to_dict=False): for i in AutoDiscoveryCIType.get_by(type_id=ci_type.id, adr_id=rule['adr_id'], to_dict=False):
if ((i.extra_option or {}).get('alias') or None) == ( if ((i.extra_option or {}).get('alias') or None) == (

View File

@@ -1,12 +1,15 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
import copy
import functools import functools
import redis_lock
from flask import abort from flask import abort
from flask import current_app from flask import current_app
from flask import request from flask import request
from flask_login import current_user from flask_login import current_user
from api.extensions import db
from api.extensions import rd
from api.lib.cmdb.const import ResourceTypeEnum from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.resp_format import ErrFormat from api.lib.cmdb.resp_format import ErrFormat
from api.lib.mixin import DBMixin from api.lib.mixin import DBMixin
@@ -40,6 +43,11 @@ class CIFilterPermsCRUD(DBMixin):
result[i['rid']]['ci_filter'] = "" result[i['rid']]['ci_filter'] = ""
result[i['rid']]['ci_filter'] += (i['ci_filter'] or "") result[i['rid']]['ci_filter'] += (i['ci_filter'] or "")
if i['id_filter']:
if not result[i['rid']]['id_filter']:
result[i['rid']]['id_filter'] = {}
result[i['rid']]['id_filter'].update(i['id_filter'] or {})
return result return result
def get_by_ids(self, _ids, type_id=None): def get_by_ids(self, _ids, type_id=None):
@@ -70,6 +78,11 @@ class CIFilterPermsCRUD(DBMixin):
result[i['type_id']]['ci_filter'] = "" result[i['type_id']]['ci_filter'] = ""
result[i['type_id']]['ci_filter'] += (i['ci_filter'] or "") result[i['type_id']]['ci_filter'] += (i['ci_filter'] or "")
if i['id_filter']:
if not result[i['type_id']]['id_filter']:
result[i['type_id']]['id_filter'] = {}
result[i['type_id']]['id_filter'].update(i['id_filter'] or {})
return result return result
@classmethod @classmethod
@@ -82,6 +95,54 @@ class CIFilterPermsCRUD(DBMixin):
type2filter_perms = cls().get_by_ids(list(map(int, [i['name'] for i in res2])), type_id=type_id) type2filter_perms = cls().get_by_ids(list(map(int, [i['name'] for i in res2])), type_id=type_id)
return type2filter_perms.get(type_id, {}).get('attr_filter') or [] return type2filter_perms.get(type_id, {}).get('attr_filter') or []
def _revoke_children(self, rid, id_filter, rebuild=True):
items = self.cls.get_by(rid=rid, ci_filter=None, attr_filter=None, to_dict=False)
for item in items:
changed, item_id_filter = False, copy.deepcopy(item.id_filter)
for prefix in id_filter:
for k, v in copy.deepcopy((item.id_filter or {})).items():
if k.startswith(prefix) and k != prefix:
item_id_filter.pop(k)
changed = True
if not item_id_filter and current_app.config.get('USE_ACL'):
item.soft_delete(commit=False)
ACLManager().del_resource(str(item.id), ResourceTypeEnum.CI_FILTER, rebuild=rebuild)
elif changed:
item.update(id_filter=item_id_filter, commit=False)
db.session.commit()
def _revoke_parent(self, rid, parent_path, rebuild=True):
parent_path = [i for i in parent_path.split(',') if i] or []
revoke_nodes = [','.join(parent_path[:i]) for i in range(len(parent_path), 0, -1)]
for node_path in revoke_nodes:
delete_item, can_deleted = None, True
items = self.cls.get_by(rid=rid, ci_filter=None, attr_filter=None, to_dict=False)
for item in items:
if node_path in item.id_filter:
delete_item = item
if any(filter(lambda x: x.startswith(node_path) and x != node_path, item.id_filter.keys())):
can_deleted = False
break
if can_deleted and delete_item:
id_filter = copy.deepcopy(delete_item.id_filter)
id_filter.pop(node_path)
delete_item = delete_item.update(id_filter=id_filter, filter_none=False)
if current_app.config.get('USE_ACL') and not id_filter:
ACLManager().del_resource(str(delete_item.id), ResourceTypeEnum.CI_FILTER, rebuild=False)
delete_item.soft_delete()
items.remove(delete_item)
if rebuild:
from api.tasks.acl import role_rebuild
from api.lib.perm.acl.const import ACL_QUEUE
from api.lib.perm.acl.cache import AppCache
role_rebuild.apply_async(args=(rid, AppCache.get('cmdb').id), queue=ACL_QUEUE)
def _can_add(self, **kwargs): def _can_add(self, **kwargs):
ci_filter = kwargs.get('ci_filter') ci_filter = kwargs.get('ci_filter')
attr_filter = kwargs.get('attr_filter') or "" attr_filter = kwargs.get('attr_filter') or ""
@@ -102,27 +163,58 @@ class CIFilterPermsCRUD(DBMixin):
def add(self, **kwargs): def add(self, **kwargs):
kwargs = self._can_add(**kwargs) or kwargs kwargs = self._can_add(**kwargs) or kwargs
with redis_lock.Lock(rd.r, 'CMDB_FILTER_{}_{}'.format(kwargs['type_id'], kwargs['rid'])):
request_id_filter = {}
if kwargs.get('id_filter'):
obj = self.cls.get_by(type_id=kwargs.get('type_id'), obj = self.cls.get_by(type_id=kwargs.get('type_id'),
rid=kwargs.get('rid'), rid=kwargs.get('rid'),
ci_filter=None,
attr_filter=None,
first=True, to_dict=False) first=True, to_dict=False)
for _id, v in (kwargs.get('id_filter') or {}).items():
key = ",".join(([v['parent_path']] if v.get('parent_path') else []) + [str(_id)])
request_id_filter[key] = v['name']
else:
obj = self.cls.get_by(type_id=kwargs.get('type_id'),
rid=kwargs.get('rid'),
id_filter=None,
first=True, to_dict=False)
is_recursive = kwargs.pop('is_recursive', 0)
if obj is not None: if obj is not None:
if obj.id_filter and isinstance(kwargs.get('id_filter'), dict):
obj_id_filter = copy.deepcopy(obj.id_filter)
for k, v in request_id_filter.items():
obj_id_filter[k] = v
kwargs['id_filter'] = obj_id_filter
obj = obj.update(filter_none=False, **kwargs) obj = obj.update(filter_none=False, **kwargs)
if not obj.attr_filter and not obj.ci_filter:
if not obj.attr_filter and not obj.ci_filter and not obj.id_filter:
if current_app.config.get('USE_ACL'): if current_app.config.get('USE_ACL'):
ACLManager().del_resource(str(obj.id), ResourceTypeEnum.CI_FILTER) ACLManager().del_resource(str(obj.id), ResourceTypeEnum.CI_FILTER, rebuild=False)
obj.soft_delete() obj.soft_delete()
return obj if not is_recursive and request_id_filter:
self._revoke_children(obj.rid, request_id_filter, rebuild=False)
return
else: else:
if not kwargs.get('ci_filter') and not kwargs.get('attr_filter'): if not kwargs.get('ci_filter') and not kwargs.get('attr_filter') and not kwargs.get('id_filter'):
return return
if request_id_filter:
kwargs['id_filter'] = request_id_filter
obj = self.cls.create(**kwargs) obj = self.cls.create(**kwargs)
if current_app.config.get('USE_ACL'): if current_app.config.get('USE_ACL'): # new resource
try: try:
ACLManager().add_resource(obj.id, ResourceTypeEnum.CI_FILTER) ACLManager().add_resource(obj.id, ResourceTypeEnum.CI_FILTER)
except: except:
@@ -140,8 +232,10 @@ class CIFilterPermsCRUD(DBMixin):
pass pass
def delete(self, **kwargs): def delete(self, **kwargs):
with redis_lock.Lock(rd.r, 'CMDB_FILTER_{}_{}'.format(kwargs['type_id'], kwargs['rid'])):
obj = self.cls.get_by(type_id=kwargs.get('type_id'), obj = self.cls.get_by(type_id=kwargs.get('type_id'),
rid=kwargs.get('rid'), rid=kwargs.get('rid'),
id_filter=None,
first=True, to_dict=False) first=True, to_dict=False)
if obj is not None: if obj is not None:
@@ -153,6 +247,69 @@ class CIFilterPermsCRUD(DBMixin):
return resource return resource
def delete2(self, **kwargs):
with redis_lock.Lock(rd.r, 'CMDB_FILTER_{}_{}'.format(kwargs['type_id'], kwargs['rid'])):
obj = self.cls.get_by(type_id=kwargs.get('type_id'),
rid=kwargs.get('rid'),
ci_filter=None,
attr_filter=None,
first=True, to_dict=False)
request_id_filter = {}
for _id, v in (kwargs.get('id_filter') or {}).items():
key = ",".join(([v['parent_path']] if v.get('parent_path') else []) + [str(_id)])
request_id_filter[key] = v['name']
resource = None
if obj is not None:
id_filter = {}
for k, v in copy.deepcopy(obj.id_filter or {}).items(): # important
if k not in request_id_filter:
id_filter[k] = v
if not id_filter and current_app.config.get('USE_ACL'):
resource = ACLManager().del_resource(str(obj.id), ResourceTypeEnum.CI_FILTER, rebuild=False)
obj.soft_delete()
db.session.commit()
else:
obj.update(id_filter=id_filter)
self._revoke_children(kwargs.get('rid'), request_id_filter, rebuild=False)
self._revoke_parent(kwargs.get('rid'), kwargs.get('parent_path'))
return resource
def delete_id_filter_by_ci_id(self, ci_id):
items = self.cls.get_by(ci_filter=None, attr_filter=None, to_dict=False)
rebuild_roles = set()
for item in items:
id_filter = copy.deepcopy(item.id_filter)
changed = False
for node_path in item.id_filter:
if str(ci_id) in node_path:
id_filter.pop(node_path)
changed = True
if changed:
rebuild_roles.add(item.rid)
if not id_filter:
item.soft_delete(commit=False)
else:
item.update(id_filter=id_filter, commit=False)
db.session.commit()
if rebuild_roles:
from api.tasks.acl import role_rebuild
from api.lib.perm.acl.const import ACL_QUEUE
from api.lib.perm.acl.cache import AppCache
for rid in rebuild_roles:
role_rebuild.apply_async(args=(rid, AppCache.get('cmdb').id), queue=ACL_QUEUE)
def has_perm_for_ci(arg_name, resource_type, perm, callback=None, app=None): def has_perm_for_ci(arg_name, resource_type, perm, callback=None, app=None):
def decorator_has_perm(func): def decorator_has_perm(func):

View File

@@ -238,11 +238,13 @@ class PreferenceManager(object):
views = _views views = _views
view2cr_ids = dict() view2cr_ids = dict()
name2view = dict()
result = dict() result = dict()
name2id = list() name2id = list()
for view in views: for view in views:
view2cr_ids.setdefault(view['name'], []).extend(view['cr_ids']) view2cr_ids.setdefault(view['name'], []).extend(view['cr_ids'])
name2id.append([view['name'], view['id']]) name2id.append([view['name'], view['id']])
name2view[view['name']] = view
id2type = dict() id2type = dict()
for view_name in view2cr_ids: for view_name in view2cr_ids:
@@ -286,6 +288,8 @@ class PreferenceManager(object):
topo_flatten=topo_flatten, topo_flatten=topo_flatten,
level2constraint=level2constraint, level2constraint=level2constraint,
leaf=leaf, leaf=leaf,
option=name2view[view_name]['option'],
is_public=name2view[view_name]['is_public'],
leaf2show_types=leaf2show_types, leaf2show_types=leaf2show_types,
node2show_types=node2show_types, node2show_types=node2show_types,
show_types=[CITypeCache.get(j).to_dict() show_types=[CITypeCache.get(j).to_dict()
@@ -297,14 +301,18 @@ class PreferenceManager(object):
return result, id2type, sorted(name2id, key=lambda x: x[1]) return result, id2type, sorted(name2id, key=lambda x: x[1])
@classmethod @classmethod
def create_or_update_relation_view(cls, name, cr_ids, is_public=False): def create_or_update_relation_view(cls, name=None, cr_ids=None, _id=None, is_public=False, option=None):
if not cr_ids: if not cr_ids:
return abort(400, ErrFormat.preference_relation_view_node_required) return abort(400, ErrFormat.preference_relation_view_node_required)
if _id is None:
existed = PreferenceRelationView.get_by(name=name, to_dict=False, first=True) existed = PreferenceRelationView.get_by(name=name, to_dict=False, first=True)
else:
existed = PreferenceRelationView.get_by_id(_id)
current_app.logger.debug(existed) current_app.logger.debug(existed)
if existed is None: if existed is None:
PreferenceRelationView.create(name=name, cr_ids=cr_ids, uid=current_user.uid, is_public=is_public) PreferenceRelationView.create(name=name, cr_ids=cr_ids, uid=current_user.uid,
is_public=is_public, option=option)
if current_app.config.get("USE_ACL"): if current_app.config.get("USE_ACL"):
ACLManager().add_resource(name, ResourceTypeEnum.RELATION_VIEW) ACLManager().add_resource(name, ResourceTypeEnum.RELATION_VIEW)
@@ -312,6 +320,11 @@ class PreferenceManager(object):
RoleEnum.CMDB_READ_ALL, RoleEnum.CMDB_READ_ALL,
ResourceTypeEnum.RELATION_VIEW, ResourceTypeEnum.RELATION_VIEW,
permissions=[PermEnum.READ]) permissions=[PermEnum.READ])
else:
if existed.name != name and current_app.config.get("USE_ACL"):
ACLManager().update_resource(existed.name, name, ResourceTypeEnum.RELATION_VIEW)
existed.update(name=name, cr_ids=cr_ids, is_public=is_public, option=option)
return cls.get_relation_view() return cls.get_relation_view()

View File

@@ -96,7 +96,7 @@ class ErrFormat(CommonErrFormat):
# 属性 {} 的值必须是唯一的, 当前值 {} 已存在 # 属性 {} 的值必须是唯一的, 当前值 {} 已存在
attribute_value_unique_required = _l("The value of attribute {} must be unique, {} already exists") attribute_value_unique_required = _l("The value of attribute {} must be unique, {} already exists")
attribute_value_required = _l("Attribute {} value must exist") # 属性 {} 值必须存在 attribute_value_required = _l("Attribute {} value must exist") # 属性 {} 值必须存在
attribute_value_out_of_range = _l("Out of range value, the maximum value is 2147483647")
# 新增或者修改属性值未知错误: {} # 新增或者修改属性值未知错误: {}
attribute_value_unknown_error = _l("Unknown error when adding or modifying attribute value: {}") attribute_value_unknown_error = _l("Unknown error when adding or modifying attribute value: {}")

View File

@@ -16,10 +16,11 @@ def search(query=None,
ret_key=RetKey.NAME, ret_key=RetKey.NAME,
count=1, count=1,
sort=None, sort=None,
excludes=None): excludes=None,
use_id_filter=False):
if current_app.config.get("USE_ES"): if current_app.config.get("USE_ES"):
s = SearchFromES(query, fl, facet, page, ret_key, count, sort) s = SearchFromES(query, fl, facet, page, ret_key, count, sort)
else: else:
s = SearchFromDB(query, fl, facet, page, ret_key, count, sort, excludes=excludes) s = SearchFromDB(query, fl, facet, page, ret_key, count, sort, excludes=excludes, use_id_filter=use_id_filter)
return s return s

View File

@@ -62,7 +62,7 @@ QUERY_CI_BY_ATTR_NAME = """
QUERY_CI_BY_ID = """ QUERY_CI_BY_ID = """
SELECT c_cis.id as ci_id SELECT c_cis.id as ci_id
FROM c_cis FROM c_cis
WHERE c_cis.id={} WHERE c_cis.id {}
""" """
QUERY_CI_BY_TYPE = """ QUERY_CI_BY_TYPE = """

View File

@@ -44,7 +44,10 @@ class Search(object):
count=1, count=1,
sort=None, sort=None,
ci_ids=None, ci_ids=None,
excludes=None): excludes=None,
parent_node_perm_passed=False,
use_id_filter=False,
use_ci_filter=True):
self.orig_query = query self.orig_query = query
self.fl = fl or [] self.fl = fl or []
self.excludes = excludes or [] self.excludes = excludes or []
@@ -54,12 +57,17 @@ class Search(object):
self.count = count self.count = count
self.sort = sort self.sort = sort
self.ci_ids = ci_ids or [] self.ci_ids = ci_ids or []
self.raw_ci_ids = copy.deepcopy(self.ci_ids)
self.query_sql = "" self.query_sql = ""
self.type_id_list = [] self.type_id_list = []
self.only_type_query = False self.only_type_query = False
self.parent_node_perm_passed = parent_node_perm_passed
self.use_id_filter = use_id_filter
self.use_ci_filter = use_ci_filter
self.valid_type_names = [] self.valid_type_names = []
self.type2filter_perms = dict() self.type2filter_perms = dict()
self.is_app_admin = is_app_admin('cmdb') or current_user.username == "worker"
@staticmethod @staticmethod
def _operator_proc(key): def _operator_proc(key):
@@ -106,7 +114,7 @@ class Search(object):
self.type_id_list.append(str(ci_type.id)) self.type_id_list.append(str(ci_type.id))
if ci_type.id in self.type2filter_perms: if ci_type.id in self.type2filter_perms:
ci_filter = self.type2filter_perms[ci_type.id].get('ci_filter') ci_filter = self.type2filter_perms[ci_type.id].get('ci_filter')
if ci_filter: if ci_filter and self.use_ci_filter and not self.use_id_filter:
sub = [] sub = []
ci_filter = Template(ci_filter).render(user=current_user) ci_filter = Template(ci_filter).render(user=current_user)
for i in ci_filter.split(','): for i in ci_filter.split(','):
@@ -122,6 +130,14 @@ class Search(object):
self.fl = set(self.type2filter_perms[ci_type.id]['attr_filter']) self.fl = set(self.type2filter_perms[ci_type.id]['attr_filter'])
else: else:
self.fl = set(self.fl) & set(self.type2filter_perms[ci_type.id]['attr_filter']) self.fl = set(self.fl) & set(self.type2filter_perms[ci_type.id]['attr_filter'])
if self.type2filter_perms[ci_type.id].get('id_filter') and self.use_id_filter:
if not self.raw_ci_ids:
self.ci_ids = list(self.type2filter_perms[ci_type.id]['id_filter'].keys())
if self.use_id_filter and not self.ci_ids and not self.is_app_admin:
self.raw_ci_ids = [0]
else: else:
raise SearchError(ErrFormat.no_permission.format(ci_type.alias, PermEnum.READ)) raise SearchError(ErrFormat.no_permission.format(ci_type.alias, PermEnum.READ))
else: else:
@@ -138,7 +154,10 @@ class Search(object):
@staticmethod @staticmethod
def _id_query_handler(v): def _id_query_handler(v):
return QUERY_CI_BY_ID.format(v) if ";" in v:
return QUERY_CI_BY_ID.format("in {}".format(v.replace(';', ',')))
else:
return QUERY_CI_BY_ID.format("= {}".format(v))
@staticmethod @staticmethod
def _in_query_handler(attr, v, is_not): def _in_query_handler(attr, v, is_not):
@@ -152,6 +171,7 @@ class Search(object):
"NOT LIKE" if is_not else "LIKE", "NOT LIKE" if is_not else "LIKE",
_v.replace("*", "%")) for _v in new_v]) _v.replace("*", "%")) for _v in new_v])
_query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, in_query) _query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, in_query)
return _query_sql return _query_sql
@staticmethod @staticmethod
@@ -167,6 +187,7 @@ class Search(object):
"NOT BETWEEN" if is_not else "BETWEEN", "NOT BETWEEN" if is_not else "BETWEEN",
start.replace("*", "%"), end.replace("*", "%")) start.replace("*", "%"), end.replace("*", "%"))
_query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, range_query) _query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, range_query)
return _query_sql return _query_sql
@staticmethod @staticmethod
@@ -183,6 +204,7 @@ class Search(object):
comparison_query = "{0} '{1}'".format(v[0], v[1:].replace("*", "%")) comparison_query = "{0} '{1}'".format(v[0], v[1:].replace("*", "%"))
_query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, comparison_query) _query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, comparison_query)
return _query_sql return _query_sql
@staticmethod @staticmethod
@@ -194,6 +216,7 @@ class Search(object):
elif field.startswith("-"): elif field.startswith("-"):
field = field[1:] field = field[1:]
sort_type = "DESC" sort_type = "DESC"
return field, sort_type return field, sort_type
def __sort_by_id(self, sort_type, query_sql): def __sort_by_id(self, sort_type, query_sql):
@@ -322,6 +345,11 @@ class Search(object):
return numfound, res return numfound, res
def __get_type2filter_perms(self):
res2 = ACLManager('cmdb').get_resources(ResourceTypeEnum.CI_FILTER)
if res2:
self.type2filter_perms = CIFilterPermsCRUD().get_by_ids(list(map(int, [i['name'] for i in res2])))
def __get_types_has_read(self): def __get_types_has_read(self):
""" """
:return: _type:(type1;type2) :return: _type:(type1;type2)
@@ -331,14 +359,23 @@ class Search(object):
self.valid_type_names = {i['name'] for i in res if PermEnum.READ in i['permissions']} self.valid_type_names = {i['name'] for i in res if PermEnum.READ in i['permissions']}
res2 = acl.get_resources(ResourceTypeEnum.CI_FILTER) self.__get_type2filter_perms()
if res2:
self.type2filter_perms = CIFilterPermsCRUD().get_by_ids(list(map(int, [i['name'] for i in res2]))) for type_id in self.type2filter_perms:
ci_type = CITypeCache.get(type_id)
if ci_type:
if self.type2filter_perms[type_id].get('id_filter'):
if self.use_id_filter:
self.valid_type_names.add(ci_type.name)
elif self.type2filter_perms[type_id].get('ci_filter'):
if self.use_ci_filter:
self.valid_type_names.add(ci_type.name)
else:
self.valid_type_names.add(ci_type.name)
return "_type:({})".format(";".join(self.valid_type_names)) return "_type:({})".format(";".join(self.valid_type_names))
def __confirm_type_first(self, queries): def __confirm_type_first(self, queries):
has_type = False has_type = False
result = [] result = []
@@ -371,8 +408,10 @@ class Search(object):
else: else:
result.append(q) result.append(q)
_is_app_admin = is_app_admin('cmdb') or current_user.username == "worker" if self.parent_node_perm_passed:
if result and not has_type and not _is_app_admin: self.__get_type2filter_perms()
self.valid_type_names = "ALL"
elif result and not has_type and not self.is_app_admin:
type_q = self.__get_types_has_read() type_q = self.__get_types_has_read()
if id_query: if id_query:
ci = CIManager.get_by_id(id_query) ci = CIManager.get_by_id(id_query)
@@ -381,13 +420,11 @@ class Search(object):
result.insert(0, "_type:{}".format(ci.type_id)) result.insert(0, "_type:{}".format(ci.type_id))
else: else:
result.insert(0, type_q) result.insert(0, type_q)
elif _is_app_admin: elif self.is_app_admin:
self.valid_type_names = "ALL" self.valid_type_names = "ALL"
else: else:
self.__get_types_has_read() self.__get_types_has_read()
current_app.logger.warning(result)
return result return result
def __query_by_attr(self, q, queries, alias): def __query_by_attr(self, q, queries, alias):
@@ -479,7 +516,7 @@ class Search(object):
def _filter_ids(self, query_sql): def _filter_ids(self, query_sql):
if self.ci_ids: if self.ci_ids:
return "SELECT * FROM ({0}) AS IN_QUERY WHERE IN_QUERY.ci_id IN ({1})".format( return "SELECT * FROM ({0}) AS IN_QUERY WHERE IN_QUERY.ci_id IN ({1})".format(
query_sql, ",".join(list(map(str, self.ci_ids)))) query_sql, ",".join(list(set(map(str, self.ci_ids)))))
return query_sql return query_sql
@@ -511,6 +548,9 @@ class Search(object):
s = time.time() s = time.time()
if query_sql: if query_sql:
query_sql = self._filter_ids(query_sql) query_sql = self._filter_ids(query_sql)
if self.raw_ci_ids and not self.ci_ids:
return 0, []
self.query_sql = query_sql self.query_sql = query_sql
# current_app.logger.debug(query_sql) # current_app.logger.debug(query_sql)
numfound, res = self._execute_sql(query_sql) numfound, res = self._execute_sql(query_sql)
@@ -569,3 +609,8 @@ class Search(object):
total = len(response) total = len(response)
return response, counter, total, self.page, numfound, facet return response, counter, total, self.page, numfound, facet
def get_ci_ids(self):
_, ci_ids = self._query_build_raw()
return ci_ids

View File

@@ -1,9 +1,11 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
import json import json
import sys
from collections import Counter from collections import Counter
from flask import abort from flask import abort
from flask import current_app from flask import current_app
from flask_login import current_user
from api.extensions import rd from api.extensions import rd
from api.lib.cmdb.ci import CIRelationManager from api.lib.cmdb.ci import CIRelationManager
@@ -11,11 +13,14 @@ from api.lib.cmdb.ci_type import CITypeRelationManager
from api.lib.cmdb.const import ConstraintEnum from api.lib.cmdb.const import ConstraintEnum
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2 from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.cmdb.resp_format import ErrFormat from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB
from api.lib.cmdb.search.ci.es.search import Search as SearchFromES from api.lib.cmdb.search.ci.es.search import Search as SearchFromES
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import is_app_admin
from api.models.cmdb import CI from api.models.cmdb import CI
from api.models.cmdb import CIRelation
class Search(object): class Search(object):
@@ -29,7 +34,9 @@ class Search(object):
sort=None, sort=None,
reverse=False, reverse=False,
ancestor_ids=None, ancestor_ids=None,
has_m2m=None): descendant_ids=None,
has_m2m=None,
root_parent_path=None):
self.orig_query = query self.orig_query = query
self.fl = fl self.fl = fl
self.facet_field = facet_field self.facet_field = facet_field
@@ -46,6 +53,8 @@ class Search(object):
level[0] if isinstance(level, list) and level else level) level[0] if isinstance(level, list) and level else level)
self.ancestor_ids = ancestor_ids self.ancestor_ids = ancestor_ids
self.descendant_ids = descendant_ids
self.root_parent_path = root_parent_path
self.has_m2m = has_m2m or False self.has_m2m = has_m2m or False
if not self.has_m2m: if not self.has_m2m:
if self.ancestor_ids: if self.ancestor_ids:
@@ -56,27 +65,23 @@ class Search(object):
if _l < int(level) and c == ConstraintEnum.Many2Many: if _l < int(level) and c == ConstraintEnum.Many2Many:
self.has_m2m = True self.has_m2m = True
self.type2filter_perms = None
self.is_app_admin = is_app_admin('cmdb') or current_user.username == "worker"
def _get_ids(self, ids): def _get_ids(self, ids):
if self.level[-1] == 1 and len(ids) == 1:
if self.ancestor_ids is None:
return [i.second_ci_id for i in CIRelation.get_by(first_ci_id=ids[0], to_dict=False)]
else:
seconds = {i.second_ci_id for i in CIRelation.get_by(first_ci_id=ids[0],
ancestor_ids=self.ancestor_ids,
to_dict=False)}
return list(seconds)
merge_ids = [] merge_ids = []
key = [] key = []
_tmp = [] _tmp = []
for level in range(1, sorted(self.level)[-1] + 1): for level in range(1, sorted(self.level)[-1] + 1):
if len(self.descendant_ids) >= level and self.type2filter_perms.get(self.descendant_ids[level - 1]):
id_filter_limit, _ = self._get_ci_filter(self.type2filter_perms[self.descendant_ids[level - 1]])
else:
id_filter_limit = {}
if not self.has_m2m: if not self.has_m2m:
_tmp = map(lambda x: json.loads(x).keys(), key, prefix = list(map(str, ids)), REDIS_PREFIX_CI_RELATION
filter(lambda x: x is not None, rd.get(ids, REDIS_PREFIX_CI_RELATION) or []))
ids = [j for i in _tmp for j in i]
key, prefix = ids, REDIS_PREFIX_CI_RELATION
else: else:
if not self.ancestor_ids: if not self.ancestor_ids:
@@ -92,12 +97,16 @@ class Search(object):
key = list(set(["{},{}".format(i, j) for idx, i in enumerate(key) for j in _tmp[idx]])) key = list(set(["{},{}".format(i, j) for idx, i in enumerate(key) for j in _tmp[idx]]))
prefix = REDIS_PREFIX_CI_RELATION2 prefix = REDIS_PREFIX_CI_RELATION2
_tmp = list(map(lambda x: json.loads(x).keys() if x else [], rd.get(key, prefix) or [])) if not key or id_filter_limit is None:
ids = [j for i in _tmp for j in i]
if not key:
return [] return []
res = [json.loads(x).items() for x in [i or '{}' for i in rd.get(key, prefix) or []]]
_tmp = [[i[0] for i in x if (not id_filter_limit or (
key[idx] not in id_filter_limit or int(i[0]) in id_filter_limit[key[idx]]) or
int(i[0]) in id_filter_limit)] for idx, x in enumerate(res)]
ids = [j for i in _tmp for j in i]
if level in self.level: if level in self.level:
merge_ids.extend(ids) merge_ids.extend(ids)
@@ -120,7 +129,28 @@ class Search(object):
return merge_ids return merge_ids
def _has_read_perm_from_parent_nodes(self):
self.root_parent_path = list(map(str, self.root_parent_path))
if str(self.root_id).isdigit() and str(self.root_id) not in self.root_parent_path:
self.root_parent_path.append(str(self.root_id))
self.root_parent_path = set(self.root_parent_path)
if self.is_app_admin:
self.type2filter_perms = {}
return True
res = ACLManager().get_resources(ResourceTypeEnum.CI_FILTER) or {}
self.type2filter_perms = CIFilterPermsCRUD().get_by_ids(list(map(int, [i['name'] for i in res]))) or {}
for _, filters in self.type2filter_perms.items():
if set((filters.get('id_filter') or {}).keys()) & self.root_parent_path:
return True
return True
def search(self): def search(self):
use_ci_filter = len(self.descendant_ids) == self.level[0] - 1
parent_node_perm_passed = self._has_read_perm_from_parent_nodes()
ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id
cis = [CI.get_by_id(_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(_id))) for _id in ids] cis = [CI.get_by_id(_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(_id))) for _id in ids]
@@ -161,42 +191,105 @@ class Search(object):
page=self.page, page=self.page,
count=self.count, count=self.count,
sort=self.sort, sort=self.sort,
ci_ids=merge_ids).search() ci_ids=merge_ids,
parent_node_perm_passed=parent_node_perm_passed,
use_ci_filter=use_ci_filter).search()
def statistics(self, type_ids): def _get_ci_filter(self, filter_perms, ci_filters=None):
ci_filters = ci_filters or []
if ci_filters:
result = {}
for item in ci_filters:
res = SearchFromDB('_type:{},{}'.format(item['type_id'], item['ci_filter']),
count=sys.maxsize, parent_node_perm_passed=True).get_ci_ids()
if res:
result[item['type_id']] = set(res)
return {}, result if result else None
result = dict()
if filter_perms.get('id_filter'):
for k in filter_perms['id_filter']:
node_path = k.split(',')
if len(node_path) == 1:
result[int(node_path[0])] = 1
elif not self.has_m2m:
result.setdefault(node_path[-2], set()).add(int(node_path[-1]))
else:
result.setdefault(','.join(node_path[:-1]), set()).add(int(node_path[-1]))
if result:
return result, None
else:
return None, None
return {}, None
def statistics(self, type_ids, need_filter=True):
self.level = int(self.level) self.level = int(self.level)
acl = ACLManager('cmdb')
type2filter_perms = dict()
if not self.is_app_admin:
res2 = acl.get_resources(ResourceTypeEnum.CI_FILTER)
if res2:
type2filter_perms = CIFilterPermsCRUD().get_by_ids(list(map(int, [i['name'] for i in res2])))
ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id
_tmp = [] _tmp = []
level2ids = {} level2ids = {}
for lv in range(1, self.level + 1): for lv in range(1, self.level + 1):
level2ids[lv] = [] level2ids[lv] = []
if need_filter:
id_filter_limit, ci_filter_limit = None, None
if len(self.descendant_ids or []) >= lv and type2filter_perms.get(self.descendant_ids[lv - 1]):
id_filter_limit, _ = self._get_ci_filter(type2filter_perms[self.descendant_ids[lv - 1]])
elif type_ids and self.level == lv:
ci_filters = [type2filter_perms[type_id] for type_id in type_ids if type_id in type2filter_perms]
if ci_filters:
id_filter_limit, ci_filter_limit = self._get_ci_filter({}, ci_filters=ci_filters)
else:
id_filter_limit = {}
else:
id_filter_limit = {}
else:
id_filter_limit, ci_filter_limit = {}, {}
if lv == 1: if lv == 1:
if not self.has_m2m: if not self.has_m2m:
key, prefix = ids, REDIS_PREFIX_CI_RELATION key, prefix = [str(i) for i in ids], REDIS_PREFIX_CI_RELATION
else:
if not self.ancestor_ids:
key, prefix = ids, REDIS_PREFIX_CI_RELATION
else: else:
key = ["{},{}".format(self.ancestor_ids, _id) for _id in ids] key = ["{},{}".format(self.ancestor_ids, _id) for _id in ids]
if not self.ancestor_ids:
key, prefix = [str(i) for i in ids], REDIS_PREFIX_CI_RELATION
else:
prefix = REDIS_PREFIX_CI_RELATION2 prefix = REDIS_PREFIX_CI_RELATION2
level2ids[lv] = [[i] for i in key] level2ids[lv] = [[i] for i in key]
if not key: if not key or id_filter_limit is None:
_tmp = [] _tmp = [[]] * len(ids)
continue continue
res = [json.loads(x).items() for x in [i or '{}' for i in rd.get(key, prefix) or []]]
_tmp = []
if type_ids and lv == self.level: if type_ids and lv == self.level:
_tmp = list(map(lambda x: [i for i in x if i[1] in type_ids], _tmp = [[i for i in x if i[1] in type_ids and
(map(lambda x: list(json.loads(x).items()), (not id_filter_limit or (key[idx] not in id_filter_limit or
[i or '{}' for i in rd.get(key, prefix) or []])))) int(i[0]) in id_filter_limit[key[idx]]) or
int(i[0]) in id_filter_limit)] for idx, x in enumerate(res)]
else: else:
_tmp = list(map(lambda x: list(json.loads(x).items()), _tmp = [[i for i in x if (not id_filter_limit or (key[idx] not in id_filter_limit or
[i or '{}' for i in rd.get(key, prefix) or []])) int(i[0]) in id_filter_limit[key[idx]]) or
int(i[0]) in id_filter_limit)] for idx, x in enumerate(res)]
if ci_filter_limit:
_tmp = [[j for j in i if j[1] not in ci_filter_limit or int(j[0]) in ci_filter_limit[j[1]]]
for i in _tmp]
else: else:
for idx, item in enumerate(_tmp): for idx, item in enumerate(_tmp):
if item: if item:
if not self.has_m2m: if not self.has_m2m:
@@ -208,18 +301,26 @@ class Search(object):
level2ids[lv].append(key) level2ids[lv].append(key)
if key: if key:
res = [json.loads(x).items() for x in [i or '{}' for i in rd.get(key, prefix) or []]]
if type_ids and lv == self.level: if type_ids and lv == self.level:
__tmp = map(lambda x: [(_id, type_id) for _id, type_id in json.loads(x).items() __tmp = [[i for i in x if i[1] in type_ids and
if type_id in type_ids], (not id_filter_limit or (
filter(lambda x: x is not None, key[idx] not in id_filter_limit or
rd.get(key, prefix) or [])) int(i[0]) in id_filter_limit[key[idx]]) or
int(i[0]) in id_filter_limit)] for idx, x in enumerate(res)]
else: else:
__tmp = map(lambda x: list(json.loads(x).items()), __tmp = [[i for i in x if (not id_filter_limit or (
filter(lambda x: x is not None, key[idx] not in id_filter_limit or
rd.get(key, prefix) or [])) int(i[0]) in id_filter_limit[key[idx]]) or
int(i[0]) in id_filter_limit)] for idx, x in enumerate(res)]
if ci_filter_limit:
__tmp = [[j for j in i if j[1] not in ci_filter_limit or
int(j[0]) in ci_filter_limit[j[1]]] for i in __tmp]
else: else:
__tmp = [] __tmp = []
if __tmp:
_tmp[idx] = [j for i in __tmp for j in i] _tmp[idx] = [j for i in __tmp for j in i]
else: else:
_tmp[idx] = [] _tmp[idx] = []

View File

@@ -11,12 +11,21 @@ import six
import api.models.cmdb as model import api.models.cmdb as model
from api.lib.cmdb.cache import AttributeCache from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.const import ValueTypeEnum from api.lib.cmdb.const import ValueTypeEnum
from api.lib.cmdb.resp_format import ErrFormat
TIME_RE = re.compile(r'(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d') TIME_RE = re.compile(r'(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d')
class ValueDeserializeError(Exception):
pass
def string2int(x): def string2int(x):
return int(float(x)) v = int(float(x))
if v > 2147483647:
raise ValueDeserializeError(ErrFormat.attribute_value_out_of_range)
return v
def str2datetime(x): def str2datetime(x):

View File

@@ -5,14 +5,16 @@ from __future__ import unicode_literals
import copy import copy
import imp import imp
import jinja2
import os import os
import re import re
import tempfile import tempfile
import jinja2
from flask import abort from flask import abort
from flask import current_app from flask import current_app
from jinja2schema import infer from jinja2schema import infer
from jinja2schema import to_json_schema from jinja2schema import to_json_schema
from werkzeug.exceptions import BadRequest
from api.extensions import db from api.extensions import db
from api.lib.cmdb.attribute import AttributeManager from api.lib.cmdb.attribute import AttributeManager
@@ -23,6 +25,7 @@ from api.lib.cmdb.const import ValueTypeEnum
from api.lib.cmdb.history import AttributeHistoryManger from api.lib.cmdb.history import AttributeHistoryManger
from api.lib.cmdb.resp_format import ErrFormat from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.utils import TableMap from api.lib.cmdb.utils import TableMap
from api.lib.cmdb.utils import ValueDeserializeError
from api.lib.cmdb.utils import ValueTypeMap from api.lib.cmdb.utils import ValueTypeMap
from api.lib.utils import handle_arg_list from api.lib.utils import handle_arg_list
from api.models.cmdb import CI from api.models.cmdb import CI
@@ -80,7 +83,7 @@ class AttributeValueManager(object):
return res return res
@staticmethod @staticmethod
def _deserialize_value(value_type, value): def _deserialize_value(alias, value_type, value):
if not value: if not value:
return value return value
@@ -88,6 +91,8 @@ class AttributeValueManager(object):
try: try:
v = deserialize(value) v = deserialize(value)
return v return v
except ValueDeserializeError as e:
return abort(400, ErrFormat.attribute_value_invalid2.format(alias, e))
except ValueError: except ValueError:
return abort(400, ErrFormat.attribute_value_invalid.format(value)) return abort(400, ErrFormat.attribute_value_invalid.format(value))
@@ -124,7 +129,7 @@ class AttributeValueManager(object):
def _validate(self, attr, value, value_table, ci=None, type_id=None, ci_id=None, type_attr=None): def _validate(self, attr, value, value_table, ci=None, type_id=None, ci_id=None, type_attr=None):
ci = ci or {} ci = ci or {}
v = self._deserialize_value(attr.value_type, value) v = self._deserialize_value(attr.alias, attr.value_type, value)
attr.is_choice and value and self._check_is_choice(attr, attr.value_type, v) attr.is_choice and value and self._check_is_choice(attr, attr.value_type, v)
attr.is_unique and self._check_is_unique( attr.is_unique and self._check_is_unique(
@@ -240,6 +245,8 @@ class AttributeValueManager(object):
value = self._validate(attr, value, value_table, ci=None, type_id=type_id, ci_id=ci_id, 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))
ci_dict[key] = value ci_dict[key] = value
except BadRequest as e:
raise
except Exception as e: except Exception as e:
current_app.logger.warning(str(e)) current_app.logger.warning(str(e))
@@ -302,9 +309,9 @@ class AttributeValueManager(object):
return self.write_change2(changed) return self.write_change2(changed)
@staticmethod @staticmethod
def delete_attr_value(attr_id, ci_id): def delete_attr_value(attr_id, ci_id, commit=True):
attr = AttributeCache.get(attr_id) attr = AttributeCache.get(attr_id)
if attr is not None: if attr is not None:
value_table = TableMap(attr=attr).table value_table = TableMap(attr=attr).table
for item in value_table.get_by(attr_id=attr.id, ci_id=ci_id, to_dict=False): for item in value_table.get_by(attr_id=attr.id, ci_id=ci_id, to_dict=False):
item.delete() item.delete(commit=commit)

View File

@@ -35,3 +35,32 @@ AuthCommonConfigAutoRedirect = 'auto_redirect'
class TestType(BaseEnum): class TestType(BaseEnum):
Connect = 'connect' Connect = 'connect'
Login = 'login' Login = 'login'
MIMEExtMap = {
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
'application/msword': '.doc',
'application/vnd.ms-word.document.macroEnabled.12': '.docm',
'application/vnd.ms-excel': '.xls',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',
'application/vnd.ms-excel.sheet.macroEnabled.12': '.xlsm',
'application/vnd.ms-powerpoint': '.ppt',
'application/vnd.openxmlformats-officedocument.presentationml.presentation': '.pptx',
'application/vnd.ms-powerpoint.presentation.macroEnabled.12': '.pptm',
'application/zip': '.zip',
'application/x-7z-compressed': '.7z',
'application/json': '.json',
'application/pdf': '.pdf',
'image/png': '.png',
'image/bmp': '.bmp',
'image/prs.btif': '.btif',
'image/gif': '.gif',
'image/jpeg': '.jpg',
'image/tiff': '.tif',
'image/vnd.microsoft.icon': '.ico',
'image/webp': '.webp',
'image/svg+xml': '.svg',
'image/vnd.adobe.photoshop': '.psd',
'text/plain': '.txt',
'text/csv': '.csv',
}

View File

@@ -470,8 +470,58 @@ class EditDepartmentInACL(object):
return f"edit_department_name_in_acl, rid: {d_rid}, success" return f"edit_department_name_in_acl, rid: {d_rid}, success"
@classmethod
def remove_from_old_department_role(cls, e_list, acl):
result = []
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")
continue
cls.remove_single_employee_from_old_department(acl, employee, result)
@staticmethod @staticmethod
def edit_employee_department_in_acl(e_list: list, new_d_id: int, op_uid: int): def remove_single_employee_from_old_department(acl, employee, result):
from api.models.acl import Role
old_department = DepartmentCRUD.get_department_by_id(employee.get('department_id'), False)
if not old_department:
return False
old_role = Role.get_by(first=True, name=old_department.department_name, app_id=None)
old_d_rid_in_acl = old_role.get('id') if old_role else 0
if old_d_rid_in_acl == 0:
return False
d_acl_rid = old_department.acl_rid if old_d_rid_in_acl == old_department.acl_rid else old_d_rid_in_acl
payload = {
'app_id': 'acl',
'parent_id': d_acl_rid,
}
try:
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}")
return True
@staticmethod
def add_employee_to_new_department(acl, employee_acl_rid, new_department_acl_rid, result):
payload = {
'app_id': 'acl',
'child_ids': [employee_acl_rid],
}
try:
acl.add_user_to_role(new_department_acl_rid, payload)
current_app.logger.info(f"add {employee_acl_rid} to {new_department_acl_rid}")
except Exception as e:
result.append(
f"add_user_to_role employee_acl_rid: {employee_acl_rid}, parent_id: {new_department_acl_rid}, \
err: {e}")
@classmethod
def edit_employee_department_in_acl(cls, e_list: list, new_d_id: int, op_uid: int):
result = [] result = []
new_department = DepartmentCRUD.get_department_by_id(new_d_id, False) new_department = DepartmentCRUD.get_department_by_id(new_d_id, False)
if not new_department: if not new_department:
@@ -481,7 +531,11 @@ class EditDepartmentInACL(object):
from api.models.acl import Role from api.models.acl import Role
new_role = Role.get_by(first=True, name=new_department.department_name, app_id=None) new_role = Role.get_by(first=True, name=new_department.department_name, app_id=None)
new_d_rid_in_acl = new_role.get('id') if new_role else 0 new_d_rid_in_acl = new_role.get('id') if new_role else 0
acl = ACLManager('acl', str(op_uid))
if new_d_rid_in_acl == 0: if new_d_rid_in_acl == 0:
# only remove from old department role
cls.remove_from_old_department_role(e_list, acl)
return return
if new_d_rid_in_acl != new_department.acl_rid: if new_d_rid_in_acl != new_department.acl_rid:
@@ -491,43 +545,15 @@ class EditDepartmentInACL(object):
new_department_acl_rid = new_department.acl_rid if new_d_rid_in_acl == new_department.acl_rid else \ new_department_acl_rid = new_department.acl_rid if new_d_rid_in_acl == new_department.acl_rid else \
new_d_rid_in_acl new_d_rid_in_acl
acl = ACLManager('acl', str(op_uid))
for employee in e_list: for employee in e_list:
old_department = DepartmentCRUD.get_department_by_id(employee.get('department_id'), False)
if not old_department:
continue
employee_acl_rid = employee.get('e_acl_rid') employee_acl_rid = employee.get('e_acl_rid')
if employee_acl_rid == 0: if employee_acl_rid == 0:
result.append(f"employee_acl_rid == 0") result.append(f"employee_acl_rid == 0")
continue continue
old_role = Role.get_by(first=True, name=old_department.department_name, app_id=None) cls.remove_single_employee_from_old_department(acl, employee, result)
old_d_rid_in_acl = old_role.get('id') if old_role else 0
if old_d_rid_in_acl == 0:
return
if old_d_rid_in_acl != old_department.acl_rid:
old_department.update(
acl_rid=old_d_rid_in_acl
)
d_acl_rid = old_department.acl_rid if old_d_rid_in_acl == old_department.acl_rid else old_d_rid_in_acl
payload = {
'app_id': 'acl',
'parent_id': d_acl_rid,
}
try:
acl.remove_user_from_role(employee_acl_rid, payload)
except Exception as e:
result.append(
f"remove_user_from_role employee_acl_rid: {employee_acl_rid}, parent_id: {d_acl_rid}, err: {e}")
payload = { # 在新部门中添加员工
'app_id': 'acl', cls.add_employee_to_new_department(acl, employee_acl_rid, new_department_acl_rid, result)
'child_ids': [employee_acl_rid],
}
try:
acl.add_user_to_role(new_department_acl_rid, payload)
except Exception as e:
result.append(
f"add_user_to_role employee_acl_rid: {employee_acl_rid}, parent_id: {d_acl_rid}, err: {e}")
return result return result

View File

@@ -148,10 +148,10 @@ class ACLManager(object):
if group: if group:
PermissionCRUD.revoke(rid, permissions, group_id=group.id, rebuild=rebuild) PermissionCRUD.revoke(rid, permissions, group_id=group.id, rebuild=rebuild)
def del_resource(self, name, resource_type_name=None): def del_resource(self, name, resource_type_name=None, rebuild=True):
resource = self._get_resource(name, resource_type_name) resource = self._get_resource(name, resource_type_name)
if resource: if resource:
return ResourceCRUD.delete(resource.id) return ResourceCRUD.delete(resource.id, rebuild=rebuild)
def has_permission(self, resource_name, resource_type, perm, resource_id=None): def has_permission(self, resource_name, resource_type, perm, resource_id=None):
if is_app_admin(self.app_id): if is_app_admin(self.app_id):

View File

@@ -2,10 +2,11 @@
import msgpack import msgpack
import redis_lock
from api.extensions import cache from api.extensions import cache
from api.extensions import rd
from api.lib.decorator import flush_db from api.lib.decorator import flush_db
from api.lib.utils import Lock
from api.models.acl import App from api.models.acl import App
from api.models.acl import Permission from api.models.acl import Permission
from api.models.acl import Resource from api.models.acl import Resource
@@ -136,14 +137,14 @@ class HasResourceRoleCache(object):
@classmethod @classmethod
def add(cls, rid, app_id): def add(cls, rid, app_id):
with Lock('HasResourceRoleCache'): with redis_lock.Lock(rd.r, 'HasResourceRoleCache'):
c = cls.get(app_id) c = cls.get(app_id)
c[rid] = 1 c[rid] = 1
cache.set(cls.PREFIX_KEY.format(app_id), c, timeout=0) cache.set(cls.PREFIX_KEY.format(app_id), c, timeout=0)
@classmethod @classmethod
def remove(cls, rid, app_id): def remove(cls, rid, app_id):
with Lock('HasResourceRoleCache'): with redis_lock.Lock(rd.r, 'HasResourceRoleCache'):
c = cls.get(app_id) c = cls.get(app_id)
c.pop(rid, None) c.pop(rid, None)
cache.set(cls.PREFIX_KEY.format(app_id), c, timeout=0) cache.set(cls.PREFIX_KEY.format(app_id), c, timeout=0)

View File

@@ -309,7 +309,7 @@ class ResourceCRUD(object):
return resource return resource
@staticmethod @staticmethod
def delete(_id): def delete(_id, rebuild=True):
resource = Resource.get_by_id(_id) or abort(404, ErrFormat.resource_not_found.format("id={}".format(_id))) resource = Resource.get_by_id(_id) or abort(404, ErrFormat.resource_not_found.format("id={}".format(_id)))
origin = resource.to_dict() origin = resource.to_dict()
@@ -322,6 +322,7 @@ class ResourceCRUD(object):
i.soft_delete() i.soft_delete()
rebuilds.append((i.rid, i.app_id)) rebuilds.append((i.rid, i.app_id))
if rebuild:
for rid, app_id in set(rebuilds): for rid, app_id in set(rebuilds):
role_rebuild.apply_async(args=(rid, app_id), queue=ACL_QUEUE) role_rebuild.apply_async(args=(rid, app_id), queue=ACL_QUEUE)

View File

@@ -194,7 +194,7 @@ def validate(ticket):
def _parse_tag(string, tag): def _parse_tag(string, tag):
""" """
Used for parsing xml. Search string for the first occurence of Used for parsing xml. Search string for the first occurrence of
<tag>.....</tag> and return text (stripped of leading and tailing <tag>.....</tag> and return text (stripped of leading and tailing
whitespace) between tags. Return "" if tag not found. whitespace) between tags. Return "" if tag not found.
""" """

View File

@@ -1,8 +1,6 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
import base64 import base64
import sys
import time
from typing import Set from typing import Set
import elasticsearch import elasticsearch
@@ -213,52 +211,6 @@ class ESHandler(object):
return 0, [], {} return 0, [], {}
class Lock(object):
def __init__(self, name, timeout=10, app=None, need_lock=True):
self.lock_key = name
self.need_lock = need_lock
self.timeout = timeout
if not app:
app = current_app
self.app = app
try:
self.redis = redis.Redis(host=self.app.config.get('CACHE_REDIS_HOST'),
port=self.app.config.get('CACHE_REDIS_PORT'),
password=self.app.config.get('CACHE_REDIS_PASSWORD'))
except:
self.app.logger.error("cannot connect redis")
raise Exception("cannot connect redis")
def lock(self, timeout=None):
if not timeout:
timeout = self.timeout
retry = 0
while retry < 100:
timestamp = time.time() + timeout + 1
_lock = self.redis.setnx(self.lock_key, timestamp)
if _lock == 1 or (
time.time() > float(self.redis.get(self.lock_key) or sys.maxsize) and
time.time() > float(self.redis.getset(self.lock_key, timestamp) or sys.maxsize)):
break
else:
retry += 1
time.sleep(0.6)
if retry >= 100:
raise Exception("get lock failed...")
def release(self):
if time.time() < float(self.redis.get(self.lock_key)):
self.redis.delete(self.lock_key)
def __enter__(self):
if self.need_lock:
self.lock()
def __exit__(self, exc_type, exc_val, exc_tb):
if self.need_lock:
self.release()
class AESCrypto(object): class AESCrypto(object):
BLOCK_SIZE = 16 # Bytes BLOCK_SIZE = 16 # Bytes
pad = lambda s: s + ((AESCrypto.BLOCK_SIZE - len(s) % AESCrypto.BLOCK_SIZE) * pad = lambda s: s + ((AESCrypto.BLOCK_SIZE - len(s) % AESCrypto.BLOCK_SIZE) *

View File

@@ -460,6 +460,7 @@ class PreferenceRelationView(Model):
name = db.Column(db.String(64), index=True, nullable=False) name = db.Column(db.String(64), index=True, nullable=False)
cr_ids = db.Column(db.JSON) # [{parent_id: x, child_id: y}] cr_ids = db.Column(db.JSON) # [{parent_id: x, child_id: y}]
is_public = db.Column(db.Boolean, default=False) is_public = db.Column(db.Boolean, default=False)
option = db.Column(db.JSON)
class PreferenceSearchOption(Model): class PreferenceSearchOption(Model):
@@ -569,6 +570,7 @@ class CIFilterPerms(Model):
type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id')) type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'))
ci_filter = db.Column(db.Text) ci_filter = db.Column(db.Text)
attr_filter = db.Column(db.Text) attr_filter = db.Column(db.Text)
id_filter = db.Column(db.JSON) # {node_path: unique_value}
rid = db.Column(db.Integer, index=True) rid = db.Column(db.Integer, index=True)

View File

@@ -4,6 +4,7 @@
import json import json
import time import time
import redis_lock
from flask import current_app from flask import current_app
from flask_login import login_user from flask_login import login_user
@@ -17,10 +18,10 @@ from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.cmdb.const import REDIS_PREFIX_CI from api.lib.cmdb.const import REDIS_PREFIX_CI
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2 from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2
from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.decorator import flush_db from api.lib.decorator import flush_db
from api.lib.decorator import reconnect_db from api.lib.decorator import reconnect_db
from api.lib.perm.acl.cache import UserCache from api.lib.perm.acl.cache import UserCache
from api.lib.utils import Lock
from api.lib.utils import handle_arg_list from api.lib.utils import handle_arg_list
from api.models.cmdb import CI from api.models.cmdb import CI
from api.models.cmdb import CIRelation from api.models.cmdb import CIRelation
@@ -83,6 +84,13 @@ def ci_delete(ci_id):
current_app.logger.info("{0} delete..........".format(ci_id)) current_app.logger.info("{0} delete..........".format(ci_id))
@celery.task(name="cmdb.delete_id_filter", queue=CMDB_QUEUE)
@reconnect_db
def delete_id_filter(ci_id):
CIFilterPermsCRUD().delete_id_filter_by_ci_id(ci_id)
@celery.task(name="cmdb.ci_delete_trigger", queue=CMDB_QUEUE) @celery.task(name="cmdb.ci_delete_trigger", queue=CMDB_QUEUE)
@reconnect_db @reconnect_db
def ci_delete_trigger(trigger, operate_type, ci_dict): def ci_delete_trigger(trigger, operate_type, ci_dict):
@@ -99,7 +107,7 @@ def ci_delete_trigger(trigger, operate_type, ci_dict):
@flush_db @flush_db
@reconnect_db @reconnect_db
def ci_relation_cache(parent_id, child_id, ancestor_ids): def ci_relation_cache(parent_id, child_id, ancestor_ids):
with Lock("CIRelation_{}".format(parent_id)): with redis_lock.Lock(rd.r, "CIRelation_{}".format(parent_id)):
if ancestor_ids is None: if ancestor_ids is None:
children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0] children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0]
children = json.loads(children) if children is not None else {} children = json.loads(children) if children is not None else {}
@@ -177,7 +185,7 @@ def ci_relation_add(parent_dict, child_id, uid):
@celery.task(name="cmdb.ci_relation_delete", queue=CMDB_QUEUE) @celery.task(name="cmdb.ci_relation_delete", queue=CMDB_QUEUE)
@reconnect_db @reconnect_db
def ci_relation_delete(parent_id, child_id, ancestor_ids): def ci_relation_delete(parent_id, child_id, ancestor_ids):
with Lock("CIRelation_{}".format(parent_id)): with redis_lock.Lock(rd.r, "CIRelation_{}".format(parent_id)):
if ancestor_ids is None: if ancestor_ids is None:
children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0] children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0]
children = json.loads(children) if children is not None else {} children = json.loads(children) if children is not None else {}

View File

@@ -49,8 +49,7 @@ def edit_employee_department_in_acl(e_list, new_d_id, op_uid):
continue continue
old_d_rid_in_acl = role_map.get(old_department.department_name, 0) old_d_rid_in_acl = role_map.get(old_department.department_name, 0)
if old_d_rid_in_acl == 0: if old_d_rid_in_acl > 0:
return
if old_d_rid_in_acl != old_department.acl_rid: if old_d_rid_in_acl != old_department.acl_rid:
old_department.update( old_department.update(
acl_rid=old_d_rid_in_acl acl_rid=old_d_rid_in_acl

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PROJECT VERSION\n" "Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2024-03-01 13:49+0800\n" "POT-Creation-Date: 2024-03-29 10:42+0800\n"
"PO-Revision-Date: 2023-12-25 20:21+0800\n" "PO-Revision-Date: 2023-12-25 20:21+0800\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: zh\n" "Language: zh\n"
@@ -322,7 +322,7 @@ msgstr "无效的属性值: {}"
#: api/lib/cmdb/resp_format.py:94 #: api/lib/cmdb/resp_format.py:94
msgid "{} Invalid value: {}" msgid "{} Invalid value: {}"
msgstr "无效的值: {}" msgstr "{} 无效的值: {}"
#: api/lib/cmdb/resp_format.py:95 #: api/lib/cmdb/resp_format.py:95
msgid "{} is not in the predefined values" msgid "{} is not in the predefined values"
@@ -336,6 +336,10 @@ msgstr "属性 {} 的值必须是唯一的, 当前值 {} 已存在"
msgid "Attribute {} value must exist" msgid "Attribute {} value must exist"
msgstr "属性 {} 值必须存在" msgstr "属性 {} 值必须存在"
#: api/lib/cmdb/resp_format.py:99
msgid "Out of range value, the maximum value is 2147483647"
msgstr "超过最大值限制, 最大值是2147483647"
#: api/lib/cmdb/resp_format.py:101 #: api/lib/cmdb/resp_format.py:101
msgid "Unknown error when adding or modifying attribute value: {}" msgid "Unknown error when adding or modifying attribute value: {}"
msgstr "新增或者修改属性值未知错误: {}" msgstr "新增或者修改属性值未知错误: {}"

View File

@@ -38,8 +38,9 @@ class LoginView(APIView):
username = request.values.get("username") or request.values.get("email") username = request.values.get("username") or request.values.get("email")
password = request.values.get("password") password = request.values.get("password")
_role = None _role = None
auth_with_ldap = request.values.get('auth_with_ldap', True)
config = AuthenticateDataCRUD(AuthenticateType.LDAP).get() config = AuthenticateDataCRUD(AuthenticateType.LDAP).get()
if config.get('enabled') or config.get('enable'): if (config.get('enabled') or config.get('enable')) and auth_with_ldap:
from api.lib.perm.authentication.ldap import authenticate_with_ldap from api.lib.perm.authentication.ldap import authenticate_with_ldap
user, authenticated = authenticate_with_ldap(username, password) user, authenticated = authenticate_with_ldap(username, password)
else: else:

View File

@@ -11,8 +11,7 @@ from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIManager from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.ci import CIRelationManager from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.const import ExistPolicy from api.lib.cmdb.const import ExistPolicy
from api.lib.cmdb.const import PermEnum from api.lib.cmdb.const import ResourceTypeEnum, PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RetKey from api.lib.cmdb.const import RetKey
from api.lib.cmdb.perms import has_perm_for_ci from api.lib.cmdb.perms import has_perm_for_ci
from api.lib.cmdb.search import SearchError from api.lib.cmdb.search import SearchError
@@ -152,9 +151,10 @@ class CISearchView(APIView):
ret_key = RetKey.NAME ret_key = RetKey.NAME
facet = handle_arg_list(request.values.get("facet", "")) facet = handle_arg_list(request.values.get("facet", ""))
sort = request.values.get("sort") sort = request.values.get("sort")
use_id_filter = request.values.get("use_id_filter", False) in current_app.config.get('BOOL_TRUE')
start = time.time() start = time.time()
s = search(query, fl, facet, page, ret_key, count, sort, excludes) s = search(query, fl, facet, page, ret_key, count, sort, excludes, use_id_filter=use_id_filter)
try: try:
response, counter, total, page, numfound, facet = s.search() response, counter, total, page, numfound, facet = s.search()
except SearchError as e: except SearchError as e:

View File

@@ -13,7 +13,6 @@ from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.search import SearchError from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci_relation.search import Search from api.lib.cmdb.search.ci_relation.search import Search
from api.lib.decorator import args_required from api.lib.decorator import args_required
from api.lib.perm.auth import auth_abandoned
from api.lib.utils import get_page from api.lib.utils import get_page
from api.lib.utils import get_page_size from api.lib.utils import get_page_size
from api.lib.utils import handle_arg_list from api.lib.utils import handle_arg_list
@@ -36,6 +35,8 @@ class CIRelationSearchView(APIView):
root_id = request.values.get('root_id') root_id = request.values.get('root_id')
ancestor_ids = request.values.get('ancestor_ids') or None # only for many to many ancestor_ids = request.values.get('ancestor_ids') or None # only for many to many
root_parent_path = handle_arg_list(request.values.get('root_parent_path') or '')
descendant_ids = list(map(int, handle_arg_list(request.values.get('descendant_ids', []))))
level = list(map(int, handle_arg_list(request.values.get('level', '1')))) level = list(map(int, handle_arg_list(request.values.get('level', '1'))))
query = request.values.get('q', "") query = request.values.get('q', "")
@@ -47,7 +48,8 @@ class CIRelationSearchView(APIView):
start = time.time() start = time.time()
s = Search(root_id, level, query, fl, facet, page, count, sort, reverse, s = Search(root_id, level, query, fl, facet, page, count, sort, reverse,
ancestor_ids=ancestor_ids, has_m2m=has_m2m) ancestor_ids=ancestor_ids, has_m2m=has_m2m, root_parent_path=root_parent_path,
descendant_ids=descendant_ids)
try: try:
response, counter, total, page, numfound, facet = s.search() response, counter, total, page, numfound, facet = s.search()
except SearchError as e: except SearchError as e:
@@ -65,16 +67,16 @@ class CIRelationSearchView(APIView):
class CIRelationStatisticsView(APIView): class CIRelationStatisticsView(APIView):
url_prefix = "/ci_relations/statistics" url_prefix = "/ci_relations/statistics"
@auth_abandoned
def get(self): def get(self):
root_ids = list(map(int, handle_arg_list(request.values.get('root_ids')))) root_ids = list(map(int, handle_arg_list(request.values.get('root_ids'))))
level = request.values.get('level', 1) level = request.values.get('level', 1)
type_ids = set(map(int, handle_arg_list(request.values.get('type_ids', [])))) type_ids = set(map(int, handle_arg_list(request.values.get('type_ids', []))))
ancestor_ids = request.values.get('ancestor_ids') or None # only for many to many ancestor_ids = request.values.get('ancestor_ids') or None # only for many to many
descendant_ids = list(map(int, handle_arg_list(request.values.get('descendant_ids', []))))
has_m2m = request.values.get("has_m2m") in current_app.config.get('BOOL_TRUE') has_m2m = request.values.get("has_m2m") in current_app.config.get('BOOL_TRUE')
start = time.time() start = time.time()
s = Search(root_ids, level, ancestor_ids=ancestor_ids, has_m2m=has_m2m) s = Search(root_ids, level, ancestor_ids=ancestor_ids, descendant_ids=descendant_ids, has_m2m=has_m2m)
try: try:
result = s.statistics(type_ids) result = s.statistics(type_ids)
except SearchError as e: except SearchError as e:

View File

@@ -38,9 +38,13 @@ from api.resource import APIView
class CITypeView(APIView): class CITypeView(APIView):
url_prefix = ("/ci_types", "/ci_types/<int:type_id>", "/ci_types/<string:type_name>") url_prefix = ("/ci_types", "/ci_types/<int:type_id>", "/ci_types/<string:type_name>",
"/ci_types/icons")
def get(self, type_id=None, type_name=None): def get(self, type_id=None, type_name=None):
if request.url.endswith("icons"):
return self.jsonify(CITypeManager().get_icons())
q = request.args.get("type_name") q = request.args.get("type_name")
if type_id is not None: if type_id is not None:
@@ -490,13 +494,14 @@ class CITypeGrantView(APIView):
if not acl.has_permission(type_name, ResourceTypeEnum.CI_TYPE, PermEnum.GRANT) and not is_app_admin('cmdb'): if not acl.has_permission(type_name, ResourceTypeEnum.CI_TYPE, PermEnum.GRANT) and not is_app_admin('cmdb'):
return abort(403, ErrFormat.no_permission.format(type_name, PermEnum.GRANT)) return abort(403, ErrFormat.no_permission.format(type_name, PermEnum.GRANT))
if perms and not request.values.get('id_filter'):
acl.grant_resource_to_role_by_rid(type_name, rid, ResourceTypeEnum.CI_TYPE, perms, rebuild=False) acl.grant_resource_to_role_by_rid(type_name, rid, ResourceTypeEnum.CI_TYPE, perms, rebuild=False)
resource = None new_resource = None
if 'ci_filter' in request.values or 'attr_filter' in request.values: if 'ci_filter' in request.values or 'attr_filter' in request.values or 'id_filter' in request.values:
resource = CIFilterPermsCRUD().add(type_id=type_id, rid=rid, **request.values) new_resource = CIFilterPermsCRUD().add(type_id=type_id, rid=rid, **request.values)
if not resource: if not new_resource:
from api.tasks.acl import role_rebuild from api.tasks.acl import role_rebuild
from api.lib.perm.acl.const import ACL_QUEUE from api.lib.perm.acl.const import ACL_QUEUE
@@ -522,10 +527,18 @@ class CITypeRevokeView(APIView):
if not acl.has_permission(type_name, ResourceTypeEnum.CI_TYPE, PermEnum.GRANT) and not is_app_admin('cmdb'): if not acl.has_permission(type_name, ResourceTypeEnum.CI_TYPE, PermEnum.GRANT) and not is_app_admin('cmdb'):
return abort(403, ErrFormat.no_permission.format(type_name, PermEnum.GRANT)) return abort(403, ErrFormat.no_permission.format(type_name, PermEnum.GRANT))
acl.revoke_resource_from_role_by_rid(type_name, rid, ResourceTypeEnum.CI_TYPE, perms, rebuild=False)
app_id = AppCache.get('cmdb').id app_id = AppCache.get('cmdb').id
resource = None resource = None
if request.values.get('id_filter'):
CIFilterPermsCRUD().delete2(
type_id=type_id, rid=rid, id_filter=request.values['id_filter'],
parent_path=request.values.get('parent_path'))
return self.jsonify(type_id=type_id, rid=rid)
acl.revoke_resource_from_role_by_rid(type_name, rid, ResourceTypeEnum.CI_TYPE, perms, rebuild=False)
if PermEnum.READ in perms or not perms: if PermEnum.READ in perms or not perms:
resource = CIFilterPermsCRUD().delete(type_id=type_id, rid=rid) resource = CIFilterPermsCRUD().delete(type_id=type_id, rid=rid)

View File

@@ -97,7 +97,7 @@ class PreferenceTreeApiView(APIView):
class PreferenceRelationApiView(APIView): class PreferenceRelationApiView(APIView):
url_prefix = "/preference/relation/view" url_prefix = ("/preference/relation/view", "/preference/relation/view/<int:_id>")
def get(self): def get(self):
views, id2type, name2id = PreferenceManager.get_relation_view() views, id2type, name2id = PreferenceManager.get_relation_view()
@@ -110,14 +110,20 @@ class PreferenceRelationApiView(APIView):
@args_validate(PreferenceManager.pref_rel_cls) @args_validate(PreferenceManager.pref_rel_cls)
def post(self): def post(self):
name = request.values.get("name") name = request.values.get("name")
is_public = request.values.get("is_public") in current_app.config.get('BOOL_TRUE')
cr_ids = request.values.get("cr_ids") cr_ids = request.values.get("cr_ids")
views, id2type, name2id = PreferenceManager.create_or_update_relation_view(name, cr_ids) option = request.values.get("option") or None
views, id2type, name2id = PreferenceManager.create_or_update_relation_view(name, cr_ids, is_public=is_public,
option=option)
return self.jsonify(views=views, id2type=id2type, name2id=name2id) return self.jsonify(views=views, id2type=id2type, name2id=name2id)
@role_required(RoleEnum.CONFIG) @role_required(RoleEnum.CONFIG)
def put(self): @args_required("name")
return self.post() def put(self, _id):
views, id2type, name2id = PreferenceManager.create_or_update_relation_view(_id=_id, **request.values)
return self.jsonify(views=views, id2type=id2type, name2id=name2id)
@role_required(RoleEnum.CONFIG) @role_required(RoleEnum.CONFIG)
@args_required("name") @args_required("name")

View File

@@ -1,10 +1,10 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
import os from flask import request, abort, current_app
from flask import request, abort, current_app, send_from_directory
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
import lz4.frame import lz4.frame
import magic
from api.lib.common_setting.const import MIMEExtMap
from api.lib.common_setting.resp_format import ErrFormat 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 allowed_file, generate_new_file_name, CommonFileCRUD
from api.resource import APIView from api.resource import APIView
@@ -45,18 +45,23 @@ class PostFileView(APIView):
if not file: if not file:
abort(400, ErrFormat.file_is_required) abort(400, ErrFormat.file_is_required)
extension = file.mimetype.split('/')[-1]
if '+' in extension:
extension = file.filename.split('.')[-1]
if file.filename == '':
filename = f'.{extension}'
else:
if extension not in file.filename:
filename = file.filename + f".{extension}"
else:
filename = file.filename
if allowed_file(filename, current_app.config.get('ALLOWED_EXTENSIONS', ALLOWED_EXTENSIONS)): m_type = magic.from_buffer(file.read(2048), mime=True)
file.seek(0)
if m_type == 'application/octet-stream':
m_type = file.mimetype
elif m_type == 'text/plain':
# https://github.com/ahupp/python-magic/issues/193
m_type = m_type if file.mimetype == m_type else file.mimetype
extension = MIMEExtMap.get(m_type, None)
if extension is None:
abort(400, f"不支持的文件类型: {m_type}")
filename = file.filename if file.filename and file.filename.endswith(extension) else file.filename + extension
new_filename = generate_new_file_name(filename) new_filename = generate_new_file_name(filename)
new_filename = secure_filename(new_filename) new_filename = secure_filename(new_filename)
file_content = file.read() file_content = file.read()
@@ -72,5 +77,3 @@ class PostFileView(APIView):
except Exception as e: except Exception as e:
current_app.logger.error(e) current_app.logger.error(e)
abort(400, ErrFormat.upload_failed.format(e)) abort(400, ErrFormat.upload_failed.format(e))
abort(400, ErrFormat.file_type_not_allowed.format(filename))

View File

@@ -37,6 +37,7 @@ PyMySQL==1.1.0
ldap3==2.9.1 ldap3==2.9.1
PyYAML==6.0.1 PyYAML==6.0.1
redis==4.6.0 redis==4.6.0
python-redis-lock==4.0.0
requests==2.31.0 requests==2.31.0
requests_oauthlib==1.3.1 requests_oauthlib==1.3.1
markdownify==0.11.6 markdownify==0.11.6
@@ -52,3 +53,4 @@ shamir~=17.12.0
pycryptodomex>=3.19.0 pycryptodomex>=3.19.0
colorama>=0.4.6 colorama>=0.4.6
lz4>=4.3.2 lz4>=4.3.2
python-magic==0.4.27

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
@font-face { @font-face {
font-family: "iconfont"; /* Project id 3857903 */ font-family: "iconfont"; /* Project id 3857903 */
src: url('iconfont.woff2?t=1702544951995') format('woff2'), src: url('iconfont.woff2?t=1711618200417') format('woff2'),
url('iconfont.woff?t=1702544951995') format('woff'), url('iconfont.woff?t=1711618200417') format('woff'),
url('iconfont.ttf?t=1702544951995') format('truetype'); url('iconfont.ttf?t=1711618200417') format('truetype');
} }
.iconfont { .iconfont {
@@ -13,6 +13,222 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.Google_Cloud_Platform:before {
content: "\e90b";
}
.Ctyun:before {
content: "\e90c";
}
.Alibaba_Cloud:before {
content: "\e90d";
}
.Azure:before {
content: "\e90e";
}
.ZStack:before {
content: "\e904";
}
.Tencent_Cloud:before {
content: "\e905";
}
.Nutanix:before {
content: "\e906";
}
.OpenStack:before {
content: "\e907";
}
.Huawei_Cloud:before {
content: "\e908";
}
.Bytecloud:before {
content: "\e909";
}
.UCloud:before {
content: "\e90a";
}
.AWS:before {
content: "\e901";
}
.ECloud:before {
content: "\e902";
}
.JDCloud:before {
content: "\e903";
}
.veops-more:before {
content: "\e900";
}
.duose-date:before {
content: "\e8ff";
}
.duose-shishu:before {
content: "\e8fd";
}
.duose-wenben:before {
content: "\e8fe";
}
.duose-json:before {
content: "\e8f7";
}
.duose-fudianshu:before {
content: "\e8f8";
}
.duose-time:before {
content: "\e8f9";
}
.duose-password:before {
content: "\e8fa";
}
.duose-link:before {
content: "\e8fb";
}
.duose-datetime:before {
content: "\e8fc";
}
.veops-setting2:before {
content: "\e8f6";
}
.veops-search:before {
content: "\e8f5";
}
.veops-delete:before {
content: "\e8f4";
}
.veops-refresh:before {
content: "\e8f3";
}
.veops-filter:before {
content: "\e8f2";
}
.veops-reduce:before {
content: "\e8ed";
}
.veops-increase:before {
content: "\e8ee";
}
.veops-configuration_table:before {
content: "\e8ef";
}
.veops-copy:before {
content: "\e8f0";
}
.veops-save:before {
content: "\e8f1";
}
.veops-setting:before {
content: "\e8ec";
}
.veops-default_avatar:before {
content: "\e8ea";
}
.veops-notice:before {
content: "\e8eb";
}
.itsm-quickStart:before {
content: "\e8e9";
}
.itsm-associatedWith:before {
content: "\e8e8";
}
.itsm-folder:before {
content: "\e8e7";
}
.report:before {
content: "\e8e5";
}
.folder:before {
content: "\e8e6";
}
.itsm-refresh:before {
content: "\e8e4";
}
.itsm-add_table:before {
content: "\e8e2";
}
.itsm-delete_page:before {
content: "\e8e3";
}
.oneterm-secret_key:before {
content: "\e8e0";
}
.oneterm-password:before {
content: "\e8e1";
}
.itsm-sla_timeout_not_handled:before {
content: "\e8dd";
}
.itsm-sla_not_timeout:before {
content: "\e8de";
}
.itsm-SLA:before {
content: "\e8df";
}
.itsm-sla_timeout_handled:before {
content: "\e8dc";
}
.itsm-sla_all:before {
content: "\e8da";
}
.itsm-generate_by_node_id:before {
content: "\e8db";
}
.cmdb-MySQL:before {
content: "\e8d9";
}
.OAUTH2:before { .OAUTH2:before {
content: "\e8d8"; content: "\e8d8";
} }
@@ -33,7 +249,7 @@
content: "\e8d4"; content: "\e8d4";
} }
.a-itsm-knowledge2:before { .itsm-knowledge2:before {
content: "\e8d2"; content: "\e8d2";
} }

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,384 @@
"css_prefix_text": "", "css_prefix_text": "",
"description": "", "description": "",
"glyphs": [ "glyphs": [
{
"icon_id": "39729980",
"name": "Google Cloud Platform",
"font_class": "Google_Cloud_Platform",
"unicode": "e90b",
"unicode_decimal": 59659
},
{
"icon_id": "39729978",
"name": "Ctyun",
"font_class": "Ctyun",
"unicode": "e90c",
"unicode_decimal": 59660
},
{
"icon_id": "39729977",
"name": "Alibaba Cloud",
"font_class": "Alibaba_Cloud",
"unicode": "e90d",
"unicode_decimal": 59661
},
{
"icon_id": "39729976",
"name": "Azure",
"font_class": "Azure",
"unicode": "e90e",
"unicode_decimal": 59662
},
{
"icon_id": "39729985",
"name": "ZStack",
"font_class": "ZStack",
"unicode": "e904",
"unicode_decimal": 59652
},
{
"icon_id": "39729986",
"name": "Tencent Cloud",
"font_class": "Tencent_Cloud",
"unicode": "e905",
"unicode_decimal": 59653
},
{
"icon_id": "39729981",
"name": "Nutanix",
"font_class": "Nutanix",
"unicode": "e906",
"unicode_decimal": 59654
},
{
"icon_id": "39729983",
"name": "OpenStack",
"font_class": "OpenStack",
"unicode": "e907",
"unicode_decimal": 59655
},
{
"icon_id": "39729982",
"name": "Huawei Cloud",
"font_class": "Huawei_Cloud",
"unicode": "e908",
"unicode_decimal": 59656
},
{
"icon_id": "39729979",
"name": "Bytecloud",
"font_class": "Bytecloud",
"unicode": "e909",
"unicode_decimal": 59657
},
{
"icon_id": "39729984",
"name": "UCloud",
"font_class": "UCloud",
"unicode": "e90a",
"unicode_decimal": 59658
},
{
"icon_id": "39729988",
"name": "AWS",
"font_class": "AWS",
"unicode": "e901",
"unicode_decimal": 59649
},
{
"icon_id": "39729989",
"name": "ECloud",
"font_class": "ECloud",
"unicode": "e902",
"unicode_decimal": 59650
},
{
"icon_id": "39729987",
"name": "JDCloud",
"font_class": "JDCloud",
"unicode": "e903",
"unicode_decimal": 59651
},
{
"icon_id": "39713390",
"name": "veops-more",
"font_class": "veops-more",
"unicode": "e900",
"unicode_decimal": 59648
},
{
"icon_id": "39712276",
"name": "duose-date",
"font_class": "duose-date",
"unicode": "e8ff",
"unicode_decimal": 59647
},
{
"icon_id": "39712289",
"name": "duose-shishu",
"font_class": "duose-shishu",
"unicode": "e8fd",
"unicode_decimal": 59645
},
{
"icon_id": "39712286",
"name": "duose-wenben",
"font_class": "duose-wenben",
"unicode": "e8fe",
"unicode_decimal": 59646
},
{
"icon_id": "39712314",
"name": "duose-json",
"font_class": "duose-json",
"unicode": "e8f7",
"unicode_decimal": 59639
},
{
"icon_id": "39712315",
"name": "duose-fudianshu",
"font_class": "duose-fudianshu",
"unicode": "e8f8",
"unicode_decimal": 59640
},
{
"icon_id": "39712312",
"name": "duose-time",
"font_class": "duose-time",
"unicode": "e8f9",
"unicode_decimal": 59641
},
{
"icon_id": "39712313",
"name": "duose-password",
"font_class": "duose-password",
"unicode": "e8fa",
"unicode_decimal": 59642
},
{
"icon_id": "39712311",
"name": "duose-link",
"font_class": "duose-link",
"unicode": "e8fb",
"unicode_decimal": 59643
},
{
"icon_id": "39712310",
"name": "duose-datetime",
"font_class": "duose-datetime",
"unicode": "e8fc",
"unicode_decimal": 59644
},
{
"icon_id": "39705895",
"name": "veops-setting2",
"font_class": "veops-setting2",
"unicode": "e8f6",
"unicode_decimal": 59638
},
{
"icon_id": "39692404",
"name": "veops-search",
"font_class": "veops-search",
"unicode": "e8f5",
"unicode_decimal": 59637
},
{
"icon_id": "39680289",
"name": "veops-delete",
"font_class": "veops-delete",
"unicode": "e8f4",
"unicode_decimal": 59636
},
{
"icon_id": "39677378",
"name": "veops-refresh",
"font_class": "veops-refresh",
"unicode": "e8f3",
"unicode_decimal": 59635
},
{
"icon_id": "39677152",
"name": "veops-filter",
"font_class": "veops-filter",
"unicode": "e8f2",
"unicode_decimal": 59634
},
{
"icon_id": "39677216",
"name": "veops-reduce",
"font_class": "veops-reduce",
"unicode": "e8ed",
"unicode_decimal": 59629
},
{
"icon_id": "39677215",
"name": "veops-increase",
"font_class": "veops-increase",
"unicode": "e8ee",
"unicode_decimal": 59630
},
{
"icon_id": "39677211",
"name": "veops-configuration_table",
"font_class": "veops-configuration_table",
"unicode": "e8ef",
"unicode_decimal": 59631
},
{
"icon_id": "39677210",
"name": "veops-copy",
"font_class": "veops-copy",
"unicode": "e8f0",
"unicode_decimal": 59632
},
{
"icon_id": "39677207",
"name": "veops-save",
"font_class": "veops-save",
"unicode": "e8f1",
"unicode_decimal": 59633
},
{
"icon_id": "39664281",
"name": "veops-setting",
"font_class": "veops-setting",
"unicode": "e8ec",
"unicode_decimal": 59628
},
{
"icon_id": "39664295",
"name": "veops-default_avatar",
"font_class": "veops-default_avatar",
"unicode": "e8ea",
"unicode_decimal": 59626
},
{
"icon_id": "39664288",
"name": "veops-notice",
"font_class": "veops-notice",
"unicode": "e8eb",
"unicode_decimal": 59627
},
{
"icon_id": "39603135",
"name": "quickly_initiate",
"font_class": "itsm-quickStart",
"unicode": "e8e9",
"unicode_decimal": 59625
},
{
"icon_id": "39588554",
"name": "itsm-associated",
"font_class": "itsm-associatedWith",
"unicode": "e8e8",
"unicode_decimal": 59624
},
{
"icon_id": "39517726",
"name": "itsm-folder",
"font_class": "itsm-folder",
"unicode": "e8e7",
"unicode_decimal": 59623
},
{
"icon_id": "39517542",
"name": "report",
"font_class": "report",
"unicode": "e8e5",
"unicode_decimal": 59621
},
{
"icon_id": "39517539",
"name": "folder",
"font_class": "folder",
"unicode": "e8e6",
"unicode_decimal": 59622
},
{
"icon_id": "39123642",
"name": "itsm-refresh (1)",
"font_class": "itsm-refresh",
"unicode": "e8e4",
"unicode_decimal": 59620
},
{
"icon_id": "39123405",
"name": "itsm-add_table (1)",
"font_class": "itsm-add_table",
"unicode": "e8e2",
"unicode_decimal": 59618
},
{
"icon_id": "39123409",
"name": "itsm-delete_page",
"font_class": "itsm-delete_page",
"unicode": "e8e3",
"unicode_decimal": 59619
},
{
"icon_id": "39117681",
"name": "oneterm-secret_key",
"font_class": "oneterm-secret_key",
"unicode": "e8e0",
"unicode_decimal": 59616
},
{
"icon_id": "39117679",
"name": "oneterm-password",
"font_class": "oneterm-password",
"unicode": "e8e1",
"unicode_decimal": 59617
},
{
"icon_id": "39079529",
"name": "itsm-unprocessed",
"font_class": "itsm-sla_timeout_not_handled",
"unicode": "e8dd",
"unicode_decimal": 59613
},
{
"icon_id": "39079522",
"name": "itsm-not_timeout",
"font_class": "itsm-sla_not_timeout",
"unicode": "e8de",
"unicode_decimal": 59614
},
{
"icon_id": "39079520",
"name": "itsm-SLA",
"font_class": "itsm-SLA",
"unicode": "e8df",
"unicode_decimal": 59615
},
{
"icon_id": "39079538",
"name": "itsm-processed",
"font_class": "itsm-sla_timeout_handled",
"unicode": "e8dc",
"unicode_decimal": 59612
},
{
"icon_id": "39079519",
"name": "itsm-all_SLA",
"font_class": "itsm-sla_all",
"unicode": "e8da",
"unicode_decimal": 59610
},
{
"icon_id": "38970103",
"name": "itsm-generate_by_node_id",
"font_class": "itsm-generate_by_node_id",
"unicode": "e8db",
"unicode_decimal": 59611
},
{
"icon_id": "38806676",
"name": "cmdb-MySQL",
"font_class": "cmdb-MySQL",
"unicode": "e8d9",
"unicode_decimal": 59609
},
{ {
"icon_id": "38566548", "icon_id": "38566548",
"name": "OAuth2.0", "name": "OAuth2.0",
@@ -43,7 +421,7 @@
{ {
"icon_id": "38533133", "icon_id": "38533133",
"name": "itsm-knowledge (2)", "name": "itsm-knowledge (2)",
"font_class": "a-itsm-knowledge2", "font_class": "itsm-knowledge2",
"unicode": "e8d2", "unicode": "e8d2",
"unicode_decimal": 59602 "unicode_decimal": 59602
}, },

Binary file not shown.

View File

@@ -30,9 +30,9 @@ export function getAuthDataEnable() {
}) })
} }
export function testLDAP(test_type, data) { export function testLDAP(data) {
return axios({ return axios({
url: `/common-setting/v1/auth_config/LDAP/test?test_type=${test_type}`, url: `/common-setting/v1/auth_config/LDAP/test`,
method: 'post', method: 'post',
data, data,
}) })

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@@ -20,6 +20,7 @@
} }
} }
" "
:disabled="disabled"
> >
</treeselect> </treeselect>
</div> </div>
@@ -42,6 +43,7 @@
" "
appendToBody appendToBody
:zIndex="1050" :zIndex="1050"
:disabled="disabled"
> >
<div <div
:title="node.label" :title="node.label"
@@ -80,6 +82,7 @@
@select="(value) => handleChangeExp(value, item, index)" @select="(value) => handleChangeExp(value, item, index)"
appendToBody appendToBody
:zIndex="1050" :zIndex="1050"
:disabled="disabled"
> >
</treeselect> </treeselect>
<treeselect <treeselect
@@ -103,6 +106,7 @@
" "
appendToBody appendToBody
:zIndex="1050" :zIndex="1050"
:disabled="disabled"
> >
<div <div
:title="node.label" :title="node.label"
@@ -125,6 +129,7 @@
v-model="item.min" v-model="item.min"
:style="{ width: '78px' }" :style="{ width: '78px' }"
:placeholder="$t('min')" :placeholder="$t('min')"
:disabled="disabled"
/> />
~ ~
<a-input <a-input
@@ -133,6 +138,7 @@
v-model="item.max" v-model="item.max"
:style="{ width: '78px' }" :style="{ width: '78px' }"
:placeholder="$t('max')" :placeholder="$t('max')"
:disabled="disabled"
/> />
</a-input-group> </a-input-group>
<a-input-group size="small" compact v-else-if="item.exp === 'compare'" :style="{ width: '175px' }"> <a-input-group size="small" compact v-else-if="item.exp === 'compare'" :style="{ width: '175px' }">
@@ -155,6 +161,7 @@
" "
appendToBody appendToBody
:zIndex="1050" :zIndex="1050"
:disabled="disabled"
> >
</treeselect> </treeselect>
<a-input class="ops-input" v-model="item.value" size="small" style="width: 113px" /> <a-input class="ops-input" v-model="item.value" size="small" style="width: 113px" />
@@ -166,10 +173,12 @@
:placeholder="item.exp === 'in' || item.exp === '~in' ? $t('cmdbFilterComp.split', { separator: ';' }) : ''" :placeholder="item.exp === 'in' || item.exp === '~in' ? $t('cmdbFilterComp.split', { separator: ';' }) : ''"
class="ops-input" class="ops-input"
:style="{ width: '175px' }" :style="{ width: '175px' }"
:disabled="disabled"
></a-input> ></a-input>
<div v-else :style="{ width: '175px' }"></div> <div v-else :style="{ width: '175px' }"></div>
<template v-if="!disabled">
<a-tooltip :title="$t('copy')"> <a-tooltip :title="$t('copy')">
<a class="operation" @click="handleCopyRule(item)"><ops-icon type="icon-xianxing-copy"/></a> <a class="operation" @click="handleCopyRule(item)"><ops-icon type="veops-copy"/></a>
</a-tooltip> </a-tooltip>
<a-tooltip :title="$t('delete')"> <a-tooltip :title="$t('delete')">
<a class="operation" @click="handleDeleteRule(item)"><ops-icon type="icon-xianxing-delete"/></a> <a class="operation" @click="handleDeleteRule(item)"><ops-icon type="icon-xianxing-delete"/></a>
@@ -177,8 +186,9 @@
<a-tooltip :title="$t('cmdbFilterComp.addHere')" v-if="needAddHere"> <a-tooltip :title="$t('cmdbFilterComp.addHere')" v-if="needAddHere">
<a class="operation" @click="handleAddRuleAt(item)"><a-icon type="plus-circle"/></a> <a class="operation" @click="handleAddRuleAt(item)"><a-icon type="plus-circle"/></a>
</a-tooltip> </a-tooltip>
</template>
</a-space> </a-space>
<div class="table-filter-add"> <div class="table-filter-add" v-if="!disabled">
<a @click="handleAddRule">+ {{ $t('new') }}</a> <a @click="handleAddRule">+ {{ $t('new') }}</a>
</div> </div>
</div> </div>
@@ -211,6 +221,10 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
disabled: {
type: Boolean,
default: false,
},
}, },
data() { data() {
return { return {

View File

@@ -16,6 +16,7 @@
:needAddHere="needAddHere" :needAddHere="needAddHere"
v-model="ruleList" v-model="ruleList"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)" :canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)"
:disabled="disabled"
/> />
<a-divider :style="{ margin: '10px 0' }" /> <a-divider :style="{ margin: '10px 0' }" />
<div style="width:554px"> <div style="width:554px">
@@ -31,6 +32,7 @@
v-else v-else
v-model="ruleList" v-model="ruleList"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)" :canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)"
:disabled="disabled"
/> />
</div> </div>
</template> </template>
@@ -69,6 +71,10 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
disabled: {
type: Boolean,
default: false,
},
}, },
data() { data() {
return { return {

View File

@@ -17,25 +17,29 @@ export default {
getPropertyIcon(attr) { getPropertyIcon(attr) {
switch (attr.value_type) { switch (attr.value_type) {
case '0': case '0':
return 'icon-xianxing-shishu' return 'duose-shishu'
case '1': case '1':
return 'icon-xianxing-fudianshu' return 'duose-fudianshu'
case '2': case '2':
if (attr.is_password) { if (attr.is_password) {
return 'icon-xianxing-password' return 'duose-password'
} }
if (attr.is_link) { if (attr.is_link) {
return 'icon-xianxing-link' return 'duose-link'
} }
return 'icon-xianxing-wenben' return 'duose-wenben'
case '3': case '3':
return 'icon-xianxing-datetime' return 'duose-datetime'
case '4': case '4':
return 'icon-xianxing-date' return 'duose-date'
case '5': case '5':
return 'icon-xianxing-time' return 'duose-time'
case '6': case '6':
return 'icon-xianxing-json' return 'duose-json'
case '7':
return 'duose-password'
case '8':
return 'duose-link'
} }
}, },
}, },

View File

@@ -759,6 +759,52 @@ export const multicolorIconList = [
value: 'caise-redis', value: 'caise-redis',
label: 'redis' label: 'redis'
}] }]
}, {
value: 'cloud',
label: '云',
list: [{
value: 'AWS',
label: 'AWS'
}, {
value: 'Azure',
label: 'Azure'
}, {
value: 'Google_Cloud_Platform',
label: 'Google Cloud Platform'
}, {
value: 'Alibaba_Cloud',
label: '阿里云'
}, {
value: 'Huawei_Cloud',
label: '华为云'
}, {
value: 'Tencent_Cloud',
label: '腾讯云'
}, {
value: 'UCloud',
label: 'UCloud'
}, {
value: 'Ctyun',
label: '天翼云'
}, {
value: 'ECloud',
label: '移动云'
}, {
value: 'JDCloud',
label: '京东云'
}, {
value: 'Bytecloud',
label: '字节云'
}, {
value: 'OpenStack',
label: 'OpenStack'
}, {
value: 'ZStack',
label: 'ZStack'
}, {
value: 'Nutanix',
label: 'Nutanix'
}]
}, { }, {
value: 'system', value: 'system',
label: '操作系统', label: '操作系统',

View File

@@ -1,7 +1,7 @@
<template> <template>
<a-layout-sider <a-layout-sider
:class="['sider', isDesktop() ? null : 'shadow', theme, fixSiderbar ? 'ant-fixed-sidemenu' : null]" :class="['sider', isDesktop() ? null : 'shadow', theme, fixSiderbar ? 'ant-fixed-sidemenu' : null]"
width="200px" width="220px"
:collapsible="collapsible" :collapsible="collapsible"
v-model="collapsed" v-model="collapsed"
:trigger="null" :trigger="null"
@@ -15,6 +15,7 @@
@select="onSelect" @select="onSelect"
style="padding: 16px 0px;" style="padding: 16px 0px;"
></s-menu> ></s-menu>
<!-- <OpsDocs :collapsed="collapsed" /> -->
</a-layout-sider> </a-layout-sider>
</template> </template>
@@ -22,10 +23,13 @@
import Logo from '@/components/tools/Logo' import Logo from '@/components/tools/Logo'
import SMenu from './index' import SMenu from './index'
import { mixin, mixinDevice } from '@/utils/mixin' import { mixin, mixinDevice } from '@/utils/mixin'
// import OpsDocs from '@/modules/docs/index.vue'
export default { export default {
name: 'SideMenu', name: 'SideMenu',
components: { Logo, SMenu }, components: { Logo, SMenu,
// OpsDocs
},
mixins: [mixin, mixinDevice], mixins: [mixin, mixinDevice],
props: { props: {
mode: { mode: {

View File

@@ -4,7 +4,7 @@
<template #empty> <template #empty>
<slot name="empty"> <slot name="empty">
<div :style="{ paddingTop: '10px' }"> <div :style="{ paddingTop: '10px' }">
<img :style="{ width: '100px', height: '90px' }" :src="require('@/assets/data_empty.png')" /> <img :style="{ width: '140px', height: '90px' }" :src="require('@/assets/data_empty.png')" />
<div>{{ $t('noData') }}</div> <div>{{ $t('noData') }}</div>
</div> </div>
</slot> </slot>

View File

@@ -64,7 +64,7 @@ export default {
}, },
triggerColor: { triggerColor: {
type: String, type: String,
default: '#f0f2f5', default: '#f7f8fa',
}, },
}, },
data() { data() {
@@ -113,6 +113,10 @@ export default {
}, },
mounted() { mounted() {
const paneLengthPixel = localStorage.getItem(`${this.appName}-paneLengthPixel`)
if (paneLengthPixel) {
this.$emit('update:paneLengthPixel', Number(paneLengthPixel))
}
this.parentContainer = document.querySelector(`.${this.appName}`) this.parentContainer = document.querySelector(`.${this.appName}`)
if (this.isExpanded) { if (this.isExpanded) {
document.querySelector(`.${this.appName} .pane-two`).style.display = 'none' document.querySelector(`.${this.appName} .pane-two`).style.display = 'none'

View File

@@ -35,7 +35,7 @@ export default {
}, },
triggerColor: { triggerColor: {
type: String, type: String,
default: '#F0F5FF', default: '#f7f8fa',
}, },
}, },
data() { data() {
@@ -52,22 +52,22 @@ export default {
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@import '~@/style/static.less';
.two-column-layout { .two-column-layout {
margin-bottom: -24px; margin-bottom: -24px;
width: 100%; width: 100%;
.two-column-layout-sidebar { .two-column-layout-sidebar {
height: 100%; height: 100%;
padding: 15px 7px;
border-radius: 15px; border-radius: 15px;
overflow-y: auto; overflow-y: auto;
background-color: #fff;
} }
.two-column-layout-main { .two-column-layout-main {
height: 100%; height: 100%;
padding: 12px; padding: 12px;
background-color: #fff; background-color: #fff;
overflow-y: auto; overflow-y: auto;
border-radius: 15px; border-radius: @border-radius-box;
} }
} }
</style> </style>

View File

@@ -43,6 +43,7 @@
<script> <script>
import store from '@/store' import store from '@/store'
import { gridSvg, top_agent, top_acl } from '@/core/icons' import { gridSvg, top_agent, top_acl } from '@/core/icons'
import { getPreference } from '@/modules/cmdb/api/preference'
export default { export default {
name: 'TopMenu', name: 'TopMenu',
components: { gridSvg, top_agent, top_acl }, components: { gridSvg, top_agent, top_acl },
@@ -78,10 +79,20 @@ export default {
this.current = this.$route.matched[0].name this.current = this.$route.matched[0].name
}, },
methods: { methods: {
handleClick(route) { async handleClick(route) {
this.visible = false this.visible = false
if (route.name !== this.current) { 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.some((item) => item.id === Number(lastTypeId))) {
this.$router.push(`/cmdb/instances/types/${lastTypeId}`)
} else {
this.$router.push('/cmdb/dashboard')
}
} else {
this.$router.push(route.redirect) this.$router.push(route.redirect)
}
// this.current = route.name // this.current = route.name
} }
}, },
@@ -110,33 +121,21 @@ export default {
line-height: @layout-header-icon-height; line-height: @layout-header-icon-height;
border-radius: 4px !important; border-radius: 4px !important;
display: inline-flex; display: inline-flex;
align-items: flex-end; align-items: center;
} }
> span { > span {
cursor: pointer; cursor: pointer;
padding: 4px 10px; padding: 4px 10px;
margin: 0 5px; margin: 0 5px;
border-radius: 4px;
color: @layout-header-font-color; color: @layout-header-font-color;
height: @layout-header-height; height: @layout-header-height;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
&:hover {
background: linear-gradient(0deg, rgba(0, 80, 201, 0.2) 0%, rgba(174, 207, 255, 0.06) 86.76%);
color: @layout-header-font-selected-color;
border-radius: 3px 3px 0px 0px;
}
} }
> span:hover,
.top-menu-selected { .top-menu-selected {
background: linear-gradient(0deg, rgba(0, 80, 201, 0.2) 0%, rgba(174, 207, 255, 0.06) 86.76%); font-weight: bold;
color: @layout-header-font-selected-color; color: @layout-header-font-selected-color;
border-radius: 3px 3px 0px 0px;
border-bottom: 3px solid @layout-header-font-selected-color;
&:hover {
background: linear-gradient(0deg, rgba(0, 80, 201, 0.2) 0%, rgba(174, 207, 255, 0.06) 86.76%);
color: @layout-header-font-selected-color;
border-radius: 3px 3px 0px 0px;
}
} }
} }

View File

@@ -25,6 +25,7 @@ export default {
deleting: 'Deleting', deleting: 'Deleting',
deletingTip: 'Deleting, total of {total}, {successNum} succeeded, {errorNum} failed', deletingTip: 'Deleting, total of {total}, {successNum} succeeded, {errorNum} failed',
grant: 'Grant', grant: 'Grant',
revoke: 'Revoke',
login_at: 'Login At', login_at: 'Login At',
logout_at: 'Logout At', logout_at: 'Logout At',
createSuccess: 'Create Success', createSuccess: 'Create Success',

View File

@@ -25,6 +25,7 @@ export default {
deleting: '正在删除', deleting: '正在删除',
deletingTip: '正在删除,共{total}个,成功{successNum}个,失败{errorNum}个', deletingTip: '正在删除,共{total}个,成功{successNum}个,失败{errorNum}个',
grant: '授权', grant: '授权',
revoke: '回收',
login_at: '登录时间', login_at: '登录时间',
logout_at: '登出时间', logout_at: '登出时间',
createSuccess: '创建成功', createSuccess: '创建成功',

View File

@@ -87,21 +87,21 @@ export default {
computed: { computed: {
...mapState({ ...mapState({
// 动态主路由 // 动态主路由
mainMenu: state => state.routes.appRoutes, mainMenu: (state) => state.routes.appRoutes,
}), }),
contentPaddingLeft() { contentPaddingLeft() {
if (!this.fixSidebar || this.isMobile()) { if (!this.fixSidebar || this.isMobile()) {
return '0' return '0'
} }
if (this.sidebarOpened) { if (this.sidebarOpened) {
return '200px' return '220px'
} }
return '80px' return '80px'
}, },
sideBarMenu() { sideBarMenu() {
const sideMenus = this.mainMenu.filter(item => this.$route.path.startsWith(item.path)) const sideMenus = this.mainMenu.filter((item) => this.$route.path.startsWith(item.path))
if (sideMenus.length > 1) { if (sideMenus.length > 1) {
return sideMenus.find(item => item.path !== '/').children return sideMenus.find((item) => item.path !== '/').children
} else { } else {
return sideMenus[0].children return sideMenus[0].children
} }
@@ -110,6 +110,9 @@ export default {
provide() { provide() {
return { return {
reloadBoard: this.reload, reloadBoard: this.reload,
collapsed: () => {
return this.collapsed
},
} }
}, },
watch: { watch: {
@@ -146,15 +149,6 @@ export default {
this.alive = true this.alive = true
}) })
}, },
paddingCalc() {
let left = ''
if (this.sidebarOpened) {
left = this.isDesktop() ? '200px' : '80px'
} else {
left = (this.isMobile() && '0') || (this.fixSidebar && '80px') || '0'
}
return left
},
menuSelect() { menuSelect() {
if (!this.isDesktop()) { if (!this.isDesktop()) {
this.collapsed = false this.collapsed = false

View File

@@ -233,8 +233,10 @@ export default {
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@import '~@/style/static.less';
.acl-history { .acl-history {
border-radius: 15px; border-radius: @border-radius-box;
height: calc(100vh - 64px); height: calc(100vh - 64px);
margin-bottom: -24px; margin-bottom: -24px;
padding: 24px; padding: 24px;

View File

@@ -290,7 +290,6 @@ export default {
const str = ` ${key} : -> ${newVal} ` const str = ` ${key} : -> ${newVal} `
item.description += str item.description += str
} else { } else {
const str = ` ${key} : ${oldVal} -> ${newVal} `
item.description += ` ${key} : ${oldVal} -> ${newVal} ` item.description += ` ${key} : ${oldVal} -> ${newVal} `
} }
} }

View File

@@ -241,7 +241,6 @@ export default {
const str = ` ${key} : -> ${newVal} ` const str = ` ${key} : -> ${newVal} `
item.changeDescription += str item.changeDescription += str
} else { } else {
const str = ` ${key} : ${oldVal} -> ${newVal} `
item.changeDescription += ` ${key} : ${oldVal} -> ${newVal} ` item.changeDescription += ` ${key} : ${oldVal} -> ${newVal} `
} }
} }

View File

@@ -32,8 +32,10 @@ export default {
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@import '~@/style/static.less';
.acl-operation-history { .acl-operation-history {
border-radius: 15px; border-radius: @border-radius-box;
height: calc(100vh - 64px); height: calc(100vh - 64px);
margin-bottom: -24px; margin-bottom: -24px;
padding: 24px; padding: 24px;

View File

@@ -189,8 +189,10 @@ export default {
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@import '~@/style/static.less';
.acl-resource-types { .acl-resource-types {
border-radius: 15px; border-radius: @border-radius-box;
background-color: #fff; background-color: #fff;
height: calc(100vh - 64px); height: calc(100vh - 64px);
margin-bottom: -24px; margin-bottom: -24px;

View File

@@ -352,8 +352,10 @@ export default {
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@import '~@/style/static.less';
.acl-resources { .acl-resources {
border-radius: 15px; border-radius: @border-radius-box;
background-color: #fff; background-color: #fff;
height: calc(100vh - 64px); height: calc(100vh - 64px);
margin-bottom: -24px; margin-bottom: -24px;

View File

@@ -285,8 +285,10 @@ export default {
</script> </script>
<style lang="less"> <style lang="less">
@import '~@/style/static.less';
.acl-roles { .acl-roles {
border-radius: 15px; border-radius: @border-radius-box;
background-color: #fff; background-color: #fff;
height: calc(100vh - 64px); height: calc(100vh - 64px);
margin-bottom: -24px; margin-bottom: -24px;

View File

@@ -88,10 +88,12 @@ export default {
</script> </script>
<style lang="less"> <style lang="less">
@import '~@/style/static.less';
.acl-secret-key { .acl-secret-key {
background-color: #fff; background-color: #fff;
padding: 24px; padding: 24px;
border-radius: 15px; border-radius: @border-radius-box;
height: calc(100% + 24px); height: calc(100% + 24px);
.ant-input[disabled] { .ant-input[disabled] {
color: rgba(0, 0, 0, 0.5); color: rgba(0, 0, 0, 0.5);

View File

@@ -320,8 +320,10 @@ export default {
} }
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@import '~@/style/static.less';
.acl-trigger { .acl-trigger {
border-radius: 15px; border-radius: @border-radius-box;
background-color: #fff; background-color: #fff;
height: calc(100vh - 64px); height: calc(100vh - 64px);
margin-bottom: -24px; margin-bottom: -24px;

View File

@@ -188,8 +188,10 @@ export default {
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@import '~@/style/static.less';
.acl-users { .acl-users {
border-radius: 15px; border-radius: @border-radius-box;
background-color: #fff; background-color: #fff;
height: calc(100vh - 64px); height: calc(100vh - 64px);
margin-bottom: -24px; margin-bottom: -24px;

View File

@@ -223,3 +223,10 @@ export function deleteCiTypeInheritance(data) {
data data
}) })
} }
export function getCITypeIcons() {
return axios({
url: '/v0.1/ci_types/icons',
method: 'GET',
})
}

View File

@@ -71,6 +71,14 @@ export function subscribeRelationView(payload) {
}) })
} }
export function putRelationView(id, data) {
return axios({
url: `/v0.1/preference/relation/view/${id}`,
method: 'put',
data
})
}
// 用户保存条件过滤选项 // 用户保存条件过滤选项
export function getPreferenceSearch(payload) { export function getPreferenceSearch(payload) {
// 参数有prv_id: 关系视图的id ptv_id: 层级视图的id, type_id: 模型id // 参数有prv_id: 关系视图的id ptv_id: 层级视图的id, type_id: 模型id

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="cmdb-grant" :style="{ maxHeight: `${windowHeight - 104}px` }"> <div class="cmdb-grant" :style="{ maxHeight: `${windowHeight - 130}px` }">
<template v-if="cmdbGrantType.includes('ci_type')"> <template v-if="cmdbGrantType.includes('ci_type')">
<div class="cmdb-grant-title">{{ $t('cmdb.components.ciTypeGrant') }}</div> <div class="cmdb-grant-title">{{ $t('cmdb.components.ciTypeGrant') }}</div>
<CiTypeGrant <CiTypeGrant
@@ -314,10 +314,10 @@ export default {
@import '~@/style/static.less'; @import '~@/style/static.less';
.cmdb-grant { .cmdb-grant {
position: relative; position: relative;
padding: 24px 24px 0 24px; padding: 0 20px;
overflow: auto; overflow: auto;
.cmdb-grant-title { .cmdb-grant-title {
border-left: 4px solid #custom_colors[color_1]; border-left: 4px solid @primary-color;
padding-left: 10px; padding-left: 10px;
} }
} }
@@ -325,18 +325,19 @@ export default {
<style lang="less"> <style lang="less">
@import '~@/style/static.less'; @import '~@/style/static.less';
.cmdb-grant { .cmdb-grant {
.grant-button { .grant-button {
padding: 6px 8px; padding: 6px 8px;
color: #custom_colors[color_1]; color: @primary-color;
background-color: #custom_colors[color_2]; background-color: @primary-color_5;
border-radius: 2px; border-radius: 2px;
cursor: pointer; cursor: pointer;
margin: 15px 0; margin: 15px 0;
display: inline-block; display: inline-block;
transition: all 0.3s; transition: all 0.3s;
&:hover { &:hover {
box-shadow: 2px 3px 4px #custom_colors[color_2]; box-shadow: 2px 3px 4px @primary-color_5;
} }
} }
} }

View File

@@ -14,9 +14,16 @@
<script> <script>
import EmployeeTransfer from '@/components/EmployeeTransfer' import EmployeeTransfer from '@/components/EmployeeTransfer'
import RoleTransfer from '@/components/RoleTransfer' import RoleTransfer from '@/components/RoleTransfer'
export default { export default {
name: 'GrantModal', name: 'GrantModal',
components: { EmployeeTransfer, RoleTransfer }, components: { EmployeeTransfer, RoleTransfer },
props: {
customTitle: {
type: String,
default: '',
},
},
data() { data() {
return { return {
visible: false, visible: false,
@@ -25,6 +32,9 @@ export default {
}, },
computed: { computed: {
title() { title() {
if (this.customTitle) {
return this.customTitle
}
if (this.type === 'depart') { if (this.type === 'depart') {
return this.$t('cmdb.components.grantUser') return this.$t('cmdb.components.grantUser')
} }

View File

@@ -1,5 +1,11 @@
<template> <template>
<a-modal width="800px" :visible="visible" @ok="handleOk" @cancel="handleCancel" :bodyStyle="{ padding: 0 }"> <a-modal
width="800px"
:visible="visible"
@ok="handleOk"
@cancel="handleCancel"
:bodyStyle="{ padding: 0, paddingTop: '20px' }"
>
<GrantComp <GrantComp
:resourceType="resourceType" :resourceType="resourceType"
:app_id="app_id" :app_id="app_id"

View File

@@ -6,7 +6,8 @@
{ value: 2, label: $t('cmdb.components.customize'), layout: 'vertical' }, { value: 2, label: $t('cmdb.components.customize'), layout: 'vertical' },
{ value: 3, label: $t('cmdb.components.none') }, { value: 3, label: $t('cmdb.components.none') },
]" ]"
v-model="radioValue" :value="radioValue"
@change="changeRadioValue"
> >
<template slot="extra_2" v-if="radioValue === 2"> <template slot="extra_2" v-if="radioValue === 2">
<treeselect <treeselect
@@ -128,6 +129,9 @@ export default {
this.visible = true this.visible = true
this.colType = colType this.colType = colType
this.row = row this.row = row
this.form = {
name: '',
}
if (this.colType === 'read_ci') { if (this.colType === 'read_ci') {
await getCITypeAttributesByTypeIds({ type_ids: this.CITypeId }).then((res) => { await getCITypeAttributesByTypeIds({ type_ids: this.CITypeId }).then((res) => {
this.canSearchPreferenceAttrList = res.attributes.filter((item) => item.value_type !== '6') this.canSearchPreferenceAttrList = res.attributes.filter((item) => item.value_type !== '6')
@@ -149,10 +153,6 @@ export default {
}) })
} }
} }
} else {
this.form = {
name: '',
}
} }
}, },
async handleOk() { async handleOk() {
@@ -198,6 +198,13 @@ export default {
} }
this.expression = expression this.expression = expression
}, },
changeRadioValue(value) {
if (this.id_filter) {
this.$message.warning(this.$t('cmdb.serviceTree.grantedByServiceTreeTips'))
} else {
this.radioValue = value
}
},
}, },
} }
</script> </script>

View File

@@ -0,0 +1,122 @@
<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>

View File

@@ -11,7 +11,7 @@
@blur="handleInputConfirm" @blur="handleInputConfirm"
@keyup.enter="handleInputConfirm" @keyup.enter="handleInputConfirm"
/> />
<a-button v-else type="primary" size="small" ghost @click="showInput">{{ $t('cmdb.components.saveQuery') }}</a-button> <a v-else @click="showInput"> {{ $t('cmdb.components.saveQuery') }}</a>
</span> </span>
<template v-for="(item, index) in preferenceSearchList.slice(0, 3)"> <template v-for="(item, index) in preferenceSearchList.slice(0, 3)">
<span <span
@@ -178,10 +178,10 @@ export default {
<style lang="less" scoped> <style lang="less" scoped>
.preference-search-tag { .preference-search-tag {
cursor: pointer; cursor: pointer;
border-radius: 5px; border-radius: 2px;
border: none; border: 1px solid #d9d9d9;
display: inline-block; display: inline-block;
padding: 0 7px; padding: 2px 7px;
margin-right: 8px; margin-right: 8px;
> span { > span {
margin-right: 4px; margin-right: 4px;

View File

@@ -5,8 +5,15 @@
<a-space> <a-space>
<treeselect <treeselect
v-if="type === 'resourceSearch'" v-if="type === 'resourceSearch'"
class="custom-treeselect" class="custom-treeselect custom-treeselect-bgcAndBorder"
:style="{ width: '250px', marginRight: '10px', '--custom-height': '32px' }" :style="{
width: '200px',
marginRight: '10px',
'--custom-height': '32px',
'--custom-bg-color': '#fff',
'--custom-border': '1px solid #d9d9d9',
'--custom-multiple-lineHeight': '16px',
}"
v-model="currenCiType" v-model="currenCiType"
:multiple="true" :multiple="true"
:clearable="true" :clearable="true"
@@ -41,24 +48,26 @@
</treeselect> </treeselect>
<a-input <a-input
v-model="fuzzySearch" v-model="fuzzySearch"
:style="{ display: 'inline-block', width: '244px' }" :style="{ display: 'inline-block', width: '200px' }"
:placeholder="$t('cmdb.components.pleaseSearch')" :placeholder="$t('cmdb.components.pleaseSearch')"
@pressEnter="emitRefresh" @pressEnter="emitRefresh"
class="ops-input ops-input-radius"
> >
<a-icon <a-icon
type="search" type="search"
slot="suffix" slot="suffix"
:style="{ color: fuzzySearch ? '#2f54eb' : '', cursor: 'pointer' }" :style="{ color: fuzzySearch ? '#2f54eb' : '#d9d9d9', cursor: 'pointer' }"
@click="emitRefresh" @click="emitRefresh"
/> />
<a-tooltip slot="prefix" placement="bottom" :overlayStyle="{ maxWidth: '550px' }"> <a-tooltip slot="prefix" placement="bottom" :overlayStyle="{ maxWidth: '550px', whiteSpace: 'pre-line' }">
<template slot="title"> <template slot="title">
{{ $t('cmdb.components.ciSearchTips') }} {{ $t('cmdb.components.ciSearchTips') }}
</template> </template>
<a><a-icon type="question-circle"/></a> <a><a-icon type="question-circle"/></a>
</a-tooltip> </a-tooltip>
</a-input> </a-input>
<a-tooltip :title="$t('reset')">
<a-button @click="reset">{{ $t('reset') }}</a-button>
</a-tooltip>
<FilterComp <FilterComp
ref="filterComp" ref="filterComp"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList" :canSearchPreferenceAttrList="canSearchPreferenceAttrList"
@@ -69,7 +78,7 @@
<div slot="popover_item" class="search-form-bar-filter"> <div slot="popover_item" class="search-form-bar-filter">
<a-icon class="search-form-bar-filter-icon" type="filter" /> <a-icon class="search-form-bar-filter-icon" type="filter" />
{{ $t('cmdb.components.conditionFilter') }} {{ $t('cmdb.components.conditionFilter') }}
<a-icon class="search-form-bar-filter-icon" type="down" /> <a-icon class="search-form-bar-filter-icon" type="down" :style="{ color: '#d9d9d9' }" />
</div> </div>
</FilterComp> </FilterComp>
<a-input <a-input
@@ -91,13 +100,13 @@
:placeholder="placeholder" :placeholder="placeholder"
@keyup.enter="emitRefresh" @keyup.enter="emitRefresh"
> >
<a-icon slot="suffix" type="copy" @click="handleCopyExpression" /> <ops-icon slot="suffix" type="veops-copy" @click="handleCopyExpression" />
</a-input> </a-input>
<slot></slot> <slot></slot>
</a-space> </a-space>
</div> </div>
<a-space> <a-space>
<a-button @click="reset" size="small">{{ $t('reset') }}</a-button> <slot name="extraContent"></slot>
<a-tooltip :title="$t('cmdb.components.attributeDesc')" v-if="type === 'relationView'"> <a-tooltip :title="$t('cmdb.components.attributeDesc')" v-if="type === 'relationView'">
<a <a
@click=" @click="
@@ -191,6 +200,9 @@ export default {
} }
}, },
methods: { methods: {
// toggleAdvanced() {
// this.advanced = !this.advanced
// },
getCITypeGroups() { getCITypeGroups() {
getCITypeGroups({ need_other: true }).then((res) => { getCITypeGroups({ need_other: true }).then((res) => {
this.ciTypeGroup = res this.ciTypeGroup = res
@@ -233,7 +245,6 @@ export default {
} }
}, },
inputCiTypeGroup(value) { inputCiTypeGroup(value) {
console.log(value)
if (!value || !value.length) { if (!value || !value.length) {
this.$emit('updateAllAttributesList', value) this.$emit('updateAllAttributesList', value)
} }
@@ -253,6 +264,7 @@ export default {
} }
</script> </script>
<style lang="less"> <style lang="less">
@import '~@/style/static.less';
@import '../../views/index.less'; @import '../../views/index.less';
.ci-searchform-expression { .ci-searchform-expression {
> input { > input {
@@ -262,14 +274,14 @@ export default {
border-right: none; border-right: none;
&:hover, &:hover,
&:focus { &:focus {
border-bottom: 2px solid #2f54eb; border-bottom: 2px solid @primary-color;
} }
&:focus { &:focus {
box-shadow: 0 2px 2px -2px #1f78d133; box-shadow: 0 2px 2px -2px #1f78d133;
} }
} }
.ant-input-suffix { .ant-input-suffix {
color: #2f54eb; color: #d9d9d9;
cursor: pointer; cursor: pointer;
} }
} }
@@ -286,14 +298,15 @@ export default {
@import '~@/style/static.less'; @import '~@/style/static.less';
.search-form-bar { .search-form-bar {
margin-bottom: 10px; margin-bottom: 20px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
height: 32px;
.search-form-bar-filter { .search-form-bar-filter {
.ops_display_wrapper(); .ops_display_wrapper(transparent);
.search-form-bar-filter-icon { .search-form-bar-filter-icon {
color: #custom_colors[color_1]; color: @primary-color;
font-size: 12px; font-size: 12px;
} }
} }

View File

@@ -1,6 +1,7 @@
const cmdb_en = { const cmdb_en = {
relation: 'Relation', relation: 'Relation',
attribute: 'Attributes', attribute: 'Attributes',
configTable: 'Config Table',
menu: { menu: {
views: 'Views', views: 'Views',
config: 'Configuration', config: 'Configuration',
@@ -46,8 +47,9 @@ const cmdb_en = {
selectDefaultOrderAttr: 'Select default sorting attributes', selectDefaultOrderAttr: 'Select default sorting attributes',
asec: 'Forward order', asec: 'Forward order',
desc: 'Reverse order', desc: 'Reverse order',
uniqueKey: 'Uniquely Identifies', uniqueKey: 'Unique Identifies',
uniqueKeySelect: 'Please select a unique identifier', uniqueKeySelect: 'Please select a unique identifier',
uniqueKeyTips: 'json/password/computed/choice can not be unique identifies',
notfound: 'Can\'t find what you want?', notfound: 'Can\'t find what you want?',
cannotDeleteGroupTips: 'There is data under this group and cannot be deleted!', cannotDeleteGroupTips: 'There is data under this group and cannot be deleted!',
confirmDeleteGroup: 'Are you sure you want to delete group [{groupName}]?', confirmDeleteGroup: 'Are you sure you want to delete group [{groupName}]?',
@@ -181,7 +183,11 @@ const cmdb_en = {
inheritType: 'Inherit Type', inheritType: 'Inherit Type',
inheritTypePlaceholder: 'Please select inherit types', inheritTypePlaceholder: 'Please select inherit types',
inheritFrom: 'inherit from {name}', inheritFrom: 'inherit from {name}',
groupInheritFrom: 'Please go to the {name} for modification' groupInheritFrom: 'Please go to the {name} for modification',
downloadType: 'Download CIType',
deleteCIType: 'Delete CIType',
otherGroupTips: 'Non sortable within the other group',
filterTips: 'click to show {name}'
}, },
components: { components: {
unselectAttributes: 'Unselected', unselectAttributes: 'Unselected',
@@ -223,7 +229,7 @@ const cmdb_en = {
pleaseSearch: 'Please search', pleaseSearch: 'Please search',
conditionFilter: 'Conditional filtering', conditionFilter: 'Conditional filtering',
attributeDesc: 'Attribute Description', attributeDesc: 'Attribute Description',
ciSearchTips: '1. JSON attributes cannot be searched<br />2. If the search content includes commas, they need to be escaped,<br />3. Only index attributes are searched, non-index attributes use conditional filtering', ciSearchTips: '1. JSON/password/link attributes cannot be searched\n2. If the search content includes commas, they need to be escaped\n3. Only index attributes are searched, non-index attributes use conditional filtering',
ciSearchTips2: 'For example: q=hostname:*0.0.0.0*', ciSearchTips2: 'For example: q=hostname:*0.0.0.0*',
subCIType: 'Subscription CIType', subCIType: 'Subscription CIType',
already: 'already', already: 'already',
@@ -244,8 +250,10 @@ const cmdb_en = {
unselectCIType: 'No CIType selected yet', unselectCIType: 'No CIType selected yet',
pleaseUploadFile: 'Please upload files', pleaseUploadFile: 'Please upload files',
batchUploadCanceled: 'Batch upload canceled', batchUploadCanceled: 'Batch upload canceled',
selectCITypeTips: 'Please select CIType', selectCIType: 'Select CIType',
selectCITypeTips: 'Please select a CIType and then download',
downloadTemplate: 'Download Template', downloadTemplate: 'Download Template',
clickDownload: 'Click to Download',
drawTips: 'Click or drag files here to upload!', drawTips: 'Click or drag files here to upload!',
supportFileTypes: 'Supported file types: xls, xlsx', supportFileTypes: 'Supported file types: xls, xlsx',
uploadResult: 'Upload results', uploadResult: 'Upload results',
@@ -256,6 +264,16 @@ const cmdb_en = {
errorTips: 'Error message', errorTips: 'Error message',
requestFailedTips: 'An error occurred with the request, please try again later', requestFailedTips: 'An error occurred with the request, please try again later',
requestSuccessTips: 'Upload completed', requestSuccessTips: 'Upload completed',
uploadFile: 'Upload File',
drawTips1: 'Please <span class="cmdb-batch-upload-tips">select a CIType</span>, and then <span class="cmdb-batch-upload-tips">download</span> ,',
drawTips2: '<span class="cmdb-batch-upload-tips">click or drag file</span> to upload',
dataPreview: 'Preview data and upload',
tips1: 'Kind Reminder :',
tips2: '1. Click to download the template, and users can customize the header of the template file, including model properties and model associations',
// eslint-disable-next-line no-template-curly-in-string
tips3: '2. The red color in the template file represents the model relationship, such as the $Product. Product Name (${Model Name}. {Attribute Name}) column, which establishes the relationship with the product.',
tips4: '3. In the download template Excel file, the predefined values of attributes will be set as dropdown options. Please note that due to the limitations of Excel itself, a single dropdown box is limited to a maximum of 255 characters. If it exceeds 255 characters, we will not set the dropdown options for this attribute',
tips5: '4. When using Excel templates, please ensure that a single file does not exceed 5000 lines.',
}, },
preference: { preference: {
mySub: 'My Subscription', mySub: 'My Subscription',
@@ -273,6 +291,7 @@ const cmdb_en = {
monthsAgo: 'month ago', monthsAgo: 'month ago',
yearsAgo: 'years ago', yearsAgo: 'years ago',
just: 'just now', just: 'just now',
searchPlaceholder: 'Please search CIType',
}, },
custom_dashboard: { custom_dashboard: {
charts: 'Chart', charts: 'Chart',
@@ -313,13 +332,22 @@ const cmdb_en = {
}, },
preference_relation: { preference_relation: {
newServiceTree: 'Add Service Tree', newServiceTree: 'Add Service Tree',
editServiceTree: 'Edit Service Tree',
serviceTreeName: 'Name', serviceTreeName: 'Name',
serviceTreeNamePlaceholder: 'Please enter the service tree name',
public: 'Public', public: 'Public',
saveLayout: 'Save Layout', saveLayout: 'Save Layout',
childNodesNotFound: 'There are no child nodes and no business relationship can be formed. Please select again!', childNodesNotFound: 'There are no child nodes and no business relationship can be formed. Please select again!',
tips1: 'Cannot form a view with the currently selected node, please select again!', tips1: 'Cannot form a view with the currently selected node, please select again!',
tips2: 'Please enter the new serviceTree name!', tips2: 'Please enter the new serviceTree name!',
tips3: 'Please select at least two nodes!', tips3: 'Please select at least two nodes!',
tips4: 'Leaf node must be selected',
tips5: 'Select the tree directory node and display the service tree sub nodes as a Table',
showLeafNode: 'Show Leaf Node',
showTreeNode: 'Show Tree Node',
sort: 'Sort',
sort1: 'Leaf node information comes first',
sort2: 'Tree node information comes first'
}, },
history: { history: {
ciChange: 'CI', ciChange: 'CI',
@@ -466,7 +494,7 @@ const cmdb_en = {
tips3: 'Please select the fields that need to be modified', tips3: 'Please select the fields that need to be modified',
tips4: 'At least one field must be selected', tips4: 'At least one field must be selected',
tips5: 'Search name | alias', tips5: 'Search name | alias',
tips6: 'Speed up retrieval, full-text search possible, no need to use conditional filtering\n\n json currently does not support indexing \n\nText characters longer than 190 cannot be indexed', tips6: 'Speed up retrieval, full-text search possible, no need to use conditional filtering\n\n json/link/password currently does not support indexing \n\nText characters longer than 190 cannot be indexed',
tips7: 'The form of expression is a drop-down box, and the value must be in the predefined value', tips7: 'The form of expression is a drop-down box, and the value must be in the predefined value',
tips8: 'Multiple values, such as intranet IP', tips8: 'Multiple values, such as intranet IP',
tips9: 'For front-end only', tips9: 'For front-end only',
@@ -477,12 +505,25 @@ const cmdb_en = {
noPermission: 'No Permission' noPermission: 'No Permission'
}, },
serviceTree: { serviceTree: {
deleteNode: 'Delete Node', remove: 'Remove',
deleteNode: 'Delete {name}',
tips1: 'For example: q=os_version:centos&sort=os_version', tips1: 'For example: q=os_version:centos&sort=os_version',
tips2: 'Expression search', tips2: 'Expression search',
alert1: 'The administrator has not configured the ServiceTree(relation view), or you do not have permission to access it!', alert1: 'The administrator has not configured the ServiceTree(relation view), or you do not have permission to access it!',
copyFailed: 'Copy failed', copyFailed: 'Copy failed',
deleteRelationConfirm: 'Confirm to remove selected {name} from current relationship?', deleteRelationConfirm: 'Confirm to remove selected {name} from current relationship?',
batch: 'Batch',
editNode: 'Edit Node',
editNodeName: 'Edit Node Name',
grantTitle: 'Grant(read)',
userPlaceholder: 'Please select users',
rolePlaceholder: 'Please select roles',
grantedByServiceTree: 'Granted By Service Tree:',
grantedByServiceTreeTips: 'Please delete id_filter in Servive Tree',
peopleHasRead: 'Personnel authorized to read:',
authorizationPolicy: 'CI Authorization Policy:',
idAuthorizationPolicy: 'Authorized by node:',
view: 'View permissions'
}, },
tree: { tree: {
tips1: 'Please go to Preference page first to complete your subscription!', tips1: 'Please go to Preference page first to complete your subscription!',

View File

@@ -1,6 +1,7 @@
const cmdb_zh = { const cmdb_zh = {
relation: '关系', relation: '关系',
attribute: '属性', attribute: '属性',
configTable: '配置表格',
menu: { menu: {
views: '视图', views: '视图',
config: '配置', config: '配置',
@@ -35,7 +36,7 @@ const cmdb_zh = {
attributeLibray: '属性库', attributeLibray: '属性库',
addCITypeInGroup: '在该组中新增CI模型', addCITypeInGroup: '在该组中新增CI模型',
addCIType: '新增CI模型', addCIType: '新增CI模型',
editGroupName: '编辑组名称', editGroupName: '重命名分组',
deleteGroup: '删除该组', deleteGroup: '删除该组',
CITypeName: '模型名(英文)', CITypeName: '模型名(英文)',
English: '英文', English: '英文',
@@ -48,6 +49,7 @@ const cmdb_zh = {
desc: '倒序', desc: '倒序',
uniqueKey: '唯一标识', uniqueKey: '唯一标识',
uniqueKeySelect: '请选择唯一标识', uniqueKeySelect: '请选择唯一标识',
uniqueKeyTips: 'json、密码、计算属性、预定义值属性不能作为唯一标识',
notfound: '找不到想要的?', notfound: '找不到想要的?',
cannotDeleteGroupTips: '该分组下有数据, 不能删除!', cannotDeleteGroupTips: '该分组下有数据, 不能删除!',
confirmDeleteGroup: '确定要删除分组 【{groupName}】 吗?', confirmDeleteGroup: '确定要删除分组 【{groupName}】 吗?',
@@ -181,7 +183,11 @@ const cmdb_zh = {
inheritType: '继承模型', inheritType: '继承模型',
inheritTypePlaceholder: '请选择继承模型(多选)', inheritTypePlaceholder: '请选择继承模型(多选)',
inheritFrom: '属性继承自{name}', inheritFrom: '属性继承自{name}',
groupInheritFrom: '请至{name}进行修改' groupInheritFrom: '请至{name}进行修改',
downloadType: '下载模型',
deleteCIType: '删除模型',
otherGroupTips: '其他分组属性不可排序',
filterTips: '点击可仅查看{name}属性'
}, },
components: { components: {
unselectAttributes: '未选属性', unselectAttributes: '未选属性',
@@ -219,11 +225,11 @@ const cmdb_zh = {
beforeChange: '变更前', beforeChange: '变更前',
afterChange: '变更后', afterChange: '变更后',
noticeContentTips: '请输入通知内容', noticeContentTips: '请输入通知内容',
saveQuery: '保存筛选条件', saveQuery: '保存条件',
pleaseSearch: '请查找', pleaseSearch: '请查找',
conditionFilter: '条件过滤', conditionFilter: '条件过滤',
attributeDesc: '属性说明', attributeDesc: '属性说明',
ciSearchTips: '1. json属性不能搜索<br />2. 搜索内容包括逗号, 则需转义 ,<br />3. 只搜索索引属性, 非索引属性使用条件过滤', ciSearchTips: '1. json、密码、链接属性不能搜索\n2. 搜索内容包括逗号, 则需转义\n3. 只搜索索引属性, 非索引属性使用条件过滤',
ciSearchTips2: '例: q=hostname:*0.0.0.0*', ciSearchTips2: '例: q=hostname:*0.0.0.0*',
subCIType: '订阅模型', subCIType: '订阅模型',
already: '已', already: '已',
@@ -244,9 +250,10 @@ const cmdb_zh = {
unselectCIType: '尚未选择模板类型', unselectCIType: '尚未选择模板类型',
pleaseUploadFile: '请上传文件', pleaseUploadFile: '请上传文件',
batchUploadCanceled: '批量上传已取消', batchUploadCanceled: '批量上传已取消',
selectCITypeTips: '选择模板类型', selectCIType: '选择模型',
selectCITypeTips: '请选择模型后下载模板',
downloadTemplate: '下载模板', downloadTemplate: '下载模板',
drawTips: '点击或拖拽文件至此上传!', clickDownload: '点击下载',
supportFileTypes: '支持文件类型xlsxlsx', supportFileTypes: '支持文件类型xlsxlsx',
uploadResult: '上传结果', uploadResult: '上传结果',
total: '共', total: '共',
@@ -256,6 +263,16 @@ const cmdb_zh = {
errorTips: '错误信息', errorTips: '错误信息',
requestFailedTips: '请求出现错误,请稍后再试', requestFailedTips: '请求出现错误,请稍后再试',
requestSuccessTips: '批量上传已完成', requestSuccessTips: '批量上传已完成',
uploadFile: '文件上传',
drawTips1: '请先<span class="cmdb-batch-upload-tips">选择模型</span><span class="cmdb-batch-upload-tips">下载模板</span>后',
drawTips2: '<span class="cmdb-batch-upload-tips">点击或拖拽文件</span>至此上传',
dataPreview: '数据预览并导入',
tips1: '温馨提示:',
tips2: '1. 点击下载模板,用户可以自定义模板文件的表头,包括模型属性、模型关联',
// eslint-disable-next-line no-template-curly-in-string
tips3: '2. 模板文件中红色为模型关系,如$产品.产品名(${模型名}.{属性名})这一列就可建立和产品之间的关系',
tips4: '3. 下载模板excel文件中会将属性的预定义值置为下拉选项请注意受excel本身的限制单个下拉框限制了最多255个字符如果超过255个字符我们不会设置该属性的下拉选项',
tips5: '4. 在使用excel模板时请确保单个文件不超过5000行',
}, },
preference: { preference: {
mySub: '我的订阅', mySub: '我的订阅',
@@ -273,6 +290,7 @@ const cmdb_zh = {
monthsAgo: '月前', monthsAgo: '月前',
yearsAgo: '年前', yearsAgo: '年前',
just: '刚刚', just: '刚刚',
searchPlaceholder: '请搜索模型',
}, },
custom_dashboard: { custom_dashboard: {
charts: '图表', charts: '图表',
@@ -313,13 +331,23 @@ const cmdb_zh = {
}, },
preference_relation: { preference_relation: {
newServiceTree: '新增服务树', newServiceTree: '新增服务树',
editServiceTree: '编辑服务树',
serviceTreeName: '服务树名', serviceTreeName: '服务树名',
serviceTreeNamePlaceholder: '请输入服务树名',
public: '公开', public: '公开',
saveLayout: '保存布局', saveLayout: '保存布局',
childNodesNotFound: '不存在子节点,不能形成业务关系,请重新选择!', childNodesNotFound: '不存在子节点,不能形成业务关系,请重新选择!',
tips1: '不能与当前选中节点形成视图,请重新选择!', tips1: '不能与当前选中节点形成视图,请重新选择!',
tips2: '请输入新增服务树名!', tips2: '请输入新增服务树名!',
tips3: '请选择至少两个节点!', tips3: '请选择至少两个节点!',
tips4: '树子节点为必选',
tips5: '选中树目录节点服务树子节点展示成Table',
showLeafNode: '树的子节点展示成Table',
showTreeNode: '展示树节点信息',
sort: '顺序',
sort1: '树子节点信息在前',
sort2: '树节点信息在前'
}, },
history: { history: {
ciChange: 'CI变更', ciChange: 'CI变更',
@@ -400,20 +428,20 @@ const cmdb_zh = {
def unique_key(self): def unique_key(self):
""" """
:return: 返回唯一属性的名字 :return: Returns the name of a unique attribute
""" """
return return
@staticmethod @staticmethod
def attributes(): def attributes():
""" """
定义属性字段 Define attribute fields
:return: 返回属性字段列表, 列表项是(名称, 类型, 描述), 名称必须是英文 :return: Returns a list of attribute fields. The list items are (name, type, description). The name must be in English.
类型: String Integer Float Date DateTime Time JSON type: String Integer Float Date DateTime Time JSON
例如: For example:
return [ return [
("ci_type", "String", "模型名称"), ("ci_type", "String", "CIType name"),
("private_ip", "String", "内网IP, 多值逗号分隔") ("private_ip", "String", "Internal IP, multiple values separated by commas")
] ]
""" """
return [] return []
@@ -421,9 +449,10 @@ const cmdb_zh = {
@staticmethod @staticmethod
def run(): def run():
""" """
执行入口, 返回采集的属性值 Execution entry, returns collected attribute values
:return: 返回一个列表, 列表项是字典, 字典key是属性名称, value是属性值 :return:
例如: Returns a list, the list item is a dictionary, the dictionary key is the attribute name, and the value is the attribute value
For example:
return [dict(ci_type="server", private_ip="192.168.1.1")] return [dict(ci_type="server", private_ip="192.168.1.1")]
""" """
return [] return []
@@ -434,7 +463,7 @@ const cmdb_zh = {
if isinstance(result, list): if isinstance(result, list):
print("AutoDiscovery::Result::{}".format(json.dumps(result))) print("AutoDiscovery::Result::{}".format(json.dumps(result)))
else: else:
print("ERROR: 采集返回必须是列表") print("ERROR: The collection return must be a list")
`, `,
server: '物理机', server: '物理机',
vserver: '虚拟机', vserver: '虚拟机',
@@ -442,7 +471,7 @@ const cmdb_zh = {
disk: '硬盘', disk: '硬盘',
}, },
ci: { ci: {
attributeDesc: '属性说明', attributeDesc: '查看属性配置',
selectRows: '选取:{rows} 项', selectRows: '选取:{rows} 项',
addRelation: '添加关系', addRelation: '添加关系',
all: '全部', all: '全部',
@@ -465,7 +494,7 @@ const cmdb_zh = {
tips3: '请选择需要修改的字段', tips3: '请选择需要修改的字段',
tips4: '必须至少选择一个字段', tips4: '必须至少选择一个字段',
tips5: '搜索 名称 | 别名', tips5: '搜索 名称 | 别名',
tips6: '加快检索, 可以全文搜索, 无需使用条件过滤\n\n json目前不支持建索引 \n\n文本字符长度超过190不能建索引', tips6: '加快检索, 可以全文搜索, 无需使用条件过滤\n\n json、链接、密码目前不支持建索引 \n\n文本字符长度超过190不能建索引',
tips7: '表现形式是下拉框, 值必须在预定义值里', tips7: '表现形式是下拉框, 值必须在预定义值里',
tips8: '多值, 比如内网IP', tips8: '多值, 比如内网IP',
tips9: '仅针对前端', tips9: '仅针对前端',
@@ -476,12 +505,25 @@ const cmdb_zh = {
noPermission: '暂无权限' noPermission: '暂无权限'
}, },
serviceTree: { serviceTree: {
deleteNode: '删除节点', remove: '移除',
deleteNode: '移除 {name}',
tips1: '例q=os_version:centos&sort=os_version', tips1: '例q=os_version:centos&sort=os_version',
tips2: '表达式搜索', tips2: '表达式搜索',
alert1: '管理员 还未配置业务关系, 或者你无权限访问!', alert1: '管理员 还未配置业务关系, 或者你无权限访问!',
copyFailed: '复制失败', copyFailed: '复制失败',
deleteRelationConfirm: '确认将选中的 {name} 从当前关系中删除?', deleteRelationConfirm: '确认将选中的 {name} 从当前关系中删除?',
batch: '批量操作',
editNode: '编辑节点',
editNodeName: '修改节点名',
grantTitle: '授权(查看权限)',
userPlaceholder: '请选择用户',
rolePlaceholder: '请选择角色',
grantedByServiceTree: '服务树授权:',
grantedByServiceTreeTips: '请先在服务树里删掉节点授权',
peopleHasRead: '当前有查看权限的人员:',
authorizationPolicy: '实例授权策略:',
idAuthorizationPolicy: '按节点授权的:',
view: '查看权限'
}, },
tree: { tree: {
tips1: '请先到 我的订阅 页面完成订阅!', tips1: '请先到 我的订阅 页面完成订阅!',

View File

@@ -156,8 +156,8 @@ const genCmdbRoutes = async () => {
const lastTypeId = window.localStorage.getItem('ops_ci_typeid') || undefined const lastTypeId = window.localStorage.getItem('ops_ci_typeid') || undefined
if (lastTypeId && preference.some(item => item.id === Number(lastTypeId))) { if (lastTypeId && preference.some(item => item.id === Number(lastTypeId))) {
routes.redirect = `/cmdb/instances/types/${lastTypeId}` routes.redirect = `/cmdb/instances/types/${lastTypeId}`
} else if (routes.children[2].children.length > 0) { } else if (routes.children[2]?.children?.length > 0) {
routes.redirect = routes.children[2].children.find(item => !item.hidden).path routes.redirect = routes.children[2].children.find(item => !item.hidden)?.path
} else { } else {
routes.redirect = '/cmdb/dashboard' routes.redirect = '/cmdb/dashboard'
} }

View File

@@ -49,7 +49,6 @@ export function getCITableColumns(data, attrList, width = 1600, height) {
const _attrList = _.orderBy(attrList, ['is_fixed'], ['desc']) const _attrList = _.orderBy(attrList, ['is_fixed'], ['desc'])
const columns = [] const columns = []
for (let attr of _attrList) { for (let attr of _attrList) {
const editRender = { name: 'input' } const editRender = { name: 'input' }
switch (attr.value_type) { switch (attr.value_type) {
case '0': case '0':
@@ -135,6 +134,35 @@ export const getPropertyStyle = (attr) => {
} }
} }
export const getPropertyIcon = (attr) => {
switch (attr.value_type) {
case '0':
return 'duose-shishu'
case '1':
return 'duose-fudianshu'
case '2':
if (attr.is_password) {
return 'duose-password'
}
if (attr.is_link) {
return 'duose-link'
}
return 'duose-wenben'
case '3':
return 'duose-datetime'
case '4':
return 'duose-date'
case '5':
return 'duose-time'
case '6':
return 'duose-json'
case '7':
return 'duose-password'
case '8':
return 'duose-link'
}
}
export const getLastLayout = (data, x1 = 0, y1 = 0, w1 = 0) => { export const getLastLayout = (data, x1 = 0, y1 = 0, w1 = 0) => {
const _tempData = _.orderBy(data, ['y', 'x'], ['asc', 'asc']) const _tempData = _.orderBy(data, ['y', 'x'], ['asc', 'asc'])
if (!_tempData.length) { if (!_tempData.length) {

View File

@@ -1,29 +1,33 @@
<template> <template>
<div class="cmdb-batch-upload" :style="{ height: `${windowHeight - 64}px` }"> <div class="cmdb-batch-upload" :style="{ height: `${windowHeight - 64}px` }">
<div id="title"> <div class="cmdb-views-header">
<ci-type-choice ref="ciTypeChoice" @getCiTypeAttr="showCiType" /> <span>
<span class="cmdb-views-header-title">{{ $t('cmdb.menu.batchUpload') }}</span>
</span>
</div> </div>
<a-row> <CiTypeChoice ref="ciTypeChoice" @getCiTypeAttr="showCiType" />
<a-col :span="12"> <p class="cmdb-batch-upload-label"><span>*</span>3. {{ $t('cmdb.batch.uploadFile') }}</p>
<upload-file-form <UploadFileForm
:isUploading="isUploading" :isUploading="isUploading"
:ciType="ciType" :ciType="ciType"
ref="uploadFileForm" ref="uploadFileForm"
@uploadDone="uploadDone" @uploadDone="uploadDone"
></upload-file-form> ></UploadFileForm>
</a-col> <p class="cmdb-batch-upload-label">4. {{ $t('cmdb.batch.dataPreview') }}</p>
<a-col :span="24" v-if="ciType && uploadData.length">
<CiUploadTable :ciTypeAttrs="ciTypeAttrs" ref="ciUploadTable" :uploadData="uploadData"></CiUploadTable> <CiUploadTable :ciTypeAttrs="ciTypeAttrs" ref="ciUploadTable" :uploadData="uploadData"></CiUploadTable>
<div class="cmdb-batch-upload-action"> <div class="cmdb-batch-upload-action">
<a-space size="large"> <a-space size="large">
<a-button type="primary" ghost @click="handleCancel">{{ $t('cancel') }}</a-button> <a-button :disabled="!(ciType && uploadData.length)" @click="handleUpload" type="primary">{{
<a-button @click="handleUpload" type="primary">{{ $t('upload') }}</a-button> $t('upload')
<a-button v-if="hasError && !isUploading" @click="downloadError" type="primary">{{ $t('cmdb.batch.downloadFailed') }}</a-button> }}</a-button>
<a-button @click="handleCancel">{{ $t('cancel') }}</a-button>
<a-button v-if="hasError && !isUploading" @click="downloadError" type="primary">{{
$t('cmdb.batch.downloadFailed')
}}</a-button>
</a-space> </a-space>
</div> </div>
</a-col> <UploadResult
<a-col :span="24" v-if="ciType"> v-if="ciType"
<upload-result
ref="uploadResult" ref="uploadResult"
:upLoadData="uploadData" :upLoadData="uploadData"
:ciType="ciType" :ciType="ciType"
@@ -31,9 +35,7 @@
:isUploading="isUploading" :isUploading="isUploading"
@uploadResultDone="uploadResultDone" @uploadResultDone="uploadResultDone"
@uploadResultError="uploadResultError" @uploadResultError="uploadResultError"
></upload-result> ></UploadResult>
</a-col>
</a-row>
</div> </div>
</template> </template>
@@ -124,7 +126,7 @@ export default {
handleCancel() { handleCancel() {
if (!this.isUploading) { if (!this.isUploading) {
this.showCiType(null) this.showCiType(null)
this.$refs.ciTypeChoice.selectNum = null this.$refs.ciTypeChoice.selectNum = undefined
this.hasError = false this.hasError = false
} else { } else {
this.$message.warning(this.$t('cmdb.batch.batchUploadCanceled')) this.$message.warning(this.$t('cmdb.batch.batchUploadCanceled'))
@@ -144,16 +146,29 @@ export default {
}, },
} }
</script> </script>
<style lang="less">
@import '~@/style/static.less';
@import '../index.less';
.cmdb-batch-upload-label {
color: @text-color_1;
font-weight: bold;
white-space: pre;
> span {
color: red;
}
}
</style>
<style lang="less" scoped> <style lang="less" scoped>
@import '~@/style/static.less';
.cmdb-batch-upload { .cmdb-batch-upload {
margin-bottom: -24px; margin-bottom: -24px;
padding: 24px; padding: 20px;
background-color: #fff; background-color: #fff;
border-radius: 20px; border-radius: @border-radius-box;
overflow: auto; overflow: auto;
.cmdb-batch-upload-action { .cmdb-batch-upload-action {
width: 50%; width: 50%;
text-align: center;
margin: 12px 0; margin: 12px 0;
} }
} }

View File

@@ -1,11 +1,11 @@
<template> <template>
<a-space> <div>
<span>{{ $t('cmdb.ciType.ciType') }}: </span> <p class="cmdb-batch-upload-label"><span>*</span>1. {{ $t('cmdb.batch.selectCIType') }}</p>
<a-select <a-select
showSearch showSearch
:placeholder="$t('cmdb.batch.selectCITypeTips')" :placeholder="$t('cmdb.batch.selectCITypeTips')"
@change="selectCiType" @change="selectCiType"
:style="{ width: '300px' }" :style="{ width: '50%', marginBottom: '1em' }"
class="ops-select" class="ops-select"
:filter-option="filterOption" :filter-option="filterOption"
v-model="selectNum" v-model="selectNum"
@@ -14,13 +14,16 @@
ciType.alias ciType.alias
}}</a-select-option> }}</a-select-option>
</a-select> </a-select>
<p class="cmdb-batch-upload-label">&nbsp;&nbsp;2. {{ $t('cmdb.batch.downloadTemplate') }}</p>
<a-button <a-button
:style="{ marginBottom: '1em' }"
@click="openModal" @click="openModal"
:disabled="!selectNum" :disabled="!selectNum"
type="primary" type="primary"
class="ops-button-primary" ghost
class="ops-button-ghost"
icon="download" icon="download"
>{{ $t('cmdb.batch.downloadTemplate') }}</a-button >{{ $t('cmdb.batch.clickDownload') }}</a-button
> >
<a-modal <a-modal
:bodyStyle="{ paddingTop: 0 }" :bodyStyle="{ paddingTop: 0 }"
@@ -88,7 +91,7 @@
</a-row> </a-row>
</template> </template>
</a-modal> </a-modal>
</a-space> </div>
</template> </template>
<script> <script>
@@ -107,7 +110,7 @@ export default {
return { return {
ciTypeList: [], ciTypeList: [],
ciTypeName: '', ciTypeName: '',
selectNum: null, selectNum: undefined,
selectCiTypeAttrList: [], selectCiTypeAttrList: [],
visible: false, visible: false,
checkedAttrs: [], checkedAttrs: [],
@@ -238,6 +241,7 @@ export default {
for (let row = 2; row < 5000; row++) { for (let row = 2; row < 5000; row++) {
Object.keys(choice_value_obj).forEach((key) => { Object.keys(choice_value_obj).forEach((key) => {
const formulae = `"${choice_value_obj[key].choice_value.map((value) => value[0]).join(',')}"` const formulae = `"${choice_value_obj[key].choice_value.map((value) => value[0]).join(',')}"`
console.log(formulae)
if (formulae.length <= 255) { if (formulae.length <= 255) {
ws.getCell(row, choice_value_obj[key].columnIdx).dataValidation = { ws.getCell(row, choice_value_obj[key].columnIdx).dataValidation = {
type: 'list', type: 'list',

View File

@@ -1,13 +1,14 @@
<template> <template>
<div class="cmdb-batch-upload-table"> <div class="cmdb-batch-upload-table">
<vxe-table <vxe-table
v-if="uploadData && uploadData.length"
ref="xTable" ref="xTable"
stripe stripe
show-header-overflow show-header-overflow
show-overflow="" show-overflow=""
size="small" size="small"
class="ops-stripe-table" class="ops-stripe-table"
:max-height="200" height="auto"
:data="dataSource" :data="dataSource"
resizable resizable
:row-style="rowStyle" :row-style="rowStyle"
@@ -21,6 +22,19 @@
:min-width="100" :min-width="100"
></vxe-column> ></vxe-column>
</vxe-table> </vxe-table>
<a-empty
v-else
:image-style="{
height: '80px',
marginTop: '10px',
}"
>
<img slot="image" :src="require('@/assets/data_empty.png')" />
<template slot="description">
<p>{{ $t('noData') }}</p>
<p>{{ $t('cmdb.batch.pleaseUploadFile') }}</p>
</template>
</a-empty>
</div> </div>
</template> </template>
@@ -99,7 +113,21 @@ export default {
} }
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@import '~@/style/static.less';
.cmdb-batch-upload-table { .cmdb-batch-upload-table {
overflow: auto; height: 200px;
padding: 20px;
background: linear-gradient(90deg, @text-color_5 50%, transparent 0) repeat-x,
linear-gradient(90deg, @text-color_5 50%, transparent 0) repeat-x,
linear-gradient(0deg, @text-color_5 50%, transparent 0) repeat-y,
linear-gradient(0deg, @text-color_5 50%, transparent 0) repeat-y;
background-size: 15px 1px, 15px 1px, 1px 15px, 1px 15px;
background-position: 0 0, 0 100%, 0 0, 100% 0;
.ant-empty-description {
p:last-child {
color: @primary-color;
}
}
} }
</style> </style>

View File

@@ -9,14 +9,22 @@
:fileList="fileList" :fileList="fileList"
:disabled="!ciType || isUploading" :disabled="!ciType || isUploading"
> >
<img :style="{ width: '80px', height: '80px' }" src="@/assets/file_upload.png" /> <ops-icon type="itsm-folder" />
<p class="ant-upload-text">{{ $t('cmdb.batch.drawTips') }}</p>
<p class="ant-upload-hint">{{ $t('cmdb.batch.supportFileTypes') }}</p> <p class="ant-upload-hint">{{ $t('cmdb.batch.supportFileTypes') }}</p>
</a-upload-dragger> <p v-html="$t('cmdb.batch.drawTips1')"></p>
<p v-html="$t('cmdb.batch.drawTips2')"></p>
<div v-for="item in fileList" :key="item.name" class="cmdb-batch-upload-dragger-file"> <div v-for="item in fileList" :key="item.name" class="cmdb-batch-upload-dragger-file">
<span><a-icon type="file" :style="{ color: '#2F54EB', marginRight: '5px' }" />{{ item.name }}</span> <span><a-icon type="file" :style="{ color: '#2F54EB', marginRight: '5px' }" />{{ item.name }}</span>
<a-progress :status="progressStatus" :percent="percent" /> <a-progress :status="progressStatus" :percent="percent" />
</div> </div>
</a-upload-dragger>
<div class="cmdb-batch-upload-tips">
<p>{{ $t('cmdb.batch.tips1') }}</p>
<div>{{ $t('cmdb.batch.tips2') }}</div>
<div>{{ $t('cmdb.batch.tips3') }}</div>
<div>{{ $t('cmdb.batch.tips4') }}</div>
<div>{{ $t('cmdb.batch.tips5') }}</div>
</div>
</div> </div>
</template> </template>
@@ -46,15 +54,13 @@ export default {
}, },
watch: { watch: {
ciType: { ciType: {
handler(newValue) { handler() {
if (!newValue) {
this.ciItemNum = 0 this.ciItemNum = 0
this.fileList = [] this.fileList = []
this.dataList = [] this.dataList = []
this.progressStatus = 'active' this.progressStatus = 'active'
this.percent = 0 this.percent = 0
this.$emit('uploadDone', this.dataList) this.$emit('uploadDone', this.dataList)
}
}, },
}, },
}, },
@@ -77,12 +83,28 @@ export default {
</script> </script>
<style lang="less"> <style lang="less">
@import '~@/style/static.less';
.cmdb-batch-upload-dragger { .cmdb-batch-upload-dragger {
height: 220px; height: auto;
margin: 16px 0; margin: 16px 0;
.ant-upload p {
margin-bottom: 5px;
}
.ant-upload.ant-upload-drag { .ant-upload.ant-upload-drag {
background: rgba(240, 245, 255, 0.35);
border: none; border: none;
background: linear-gradient(90deg, @text-color_5 50%, transparent 0) repeat-x,
linear-gradient(90deg, @text-color_5 50%, transparent 0) repeat-x,
linear-gradient(0deg, @text-color_5 50%, transparent 0) repeat-y,
linear-gradient(0deg, @text-color_5 50%, transparent 0) repeat-y;
background-size: 15px 1px, 15px 1px, 1px 15px, 1px 15px;
background-position: 0 0, 0 100%, 0 0, 100% 0;
.ant-upload-drag-container > i {
font-size: 60px;
}
.cmdb-batch-upload-tips {
color: @primary-color;
}
} }
.ant-upload.ant-upload-drag .ant-upload-drag-container { .ant-upload.ant-upload-drag .ant-upload-drag-container {
vertical-align: baseline; vertical-align: baseline;
@@ -90,23 +112,37 @@ export default {
} }
</style> </style>
<style lang="less" scoped> <style lang="less" scoped>
@import '~@/style/static.less';
.cmdb-batch-upload-dragger { .cmdb-batch-upload-dragger {
position: relative; position: relative;
display: flex;
> span {
display: inline-block;
width: 50%;
}
.cmdb-batch-upload-dragger-file { .cmdb-batch-upload-dragger-file {
background-color: #fff; background-color: @primary-color_7;
box-shadow: 0px 2px 5px rgba(78, 94, 160, 0.2); border-radius: 2px;
border-radius: 4px;
position: absolute;
width: 80%; width: 80%;
left: 50%;
bottom: 24px;
padding: 2px 8px; padding: 2px 8px;
transform: translate(-50%);
display: inline-flex; display: inline-flex;
> span { > span {
white-space: nowrap; white-space: nowrap;
margin-right: 10px; margin-right: 10px;
} }
} }
.cmdb-batch-upload-tips {
width: 50%;
padding-left: 20px;
color: @text-color_3;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
p:first-child {
color: @text-color_1;
}
}
} }
</style> </style>

View File

@@ -1,10 +1,11 @@
<template> <template>
<div class="cmdb-batch-upload-result" v-if="visible"> <div class="cmdb-batch-upload-result" v-if="visible">
<h3 class="cmdb-batch-upload-result-title">{{ $t('cmdb.batch.uploadResult') }}</h3> <p class="cmdb-batch-upload-label">5. {{ $t('cmdb.batch.uploadResult') }}</p>
<div class="cmdb-batch-upload-result-content"> <div class="cmdb-batch-upload-result-content">
<h4> <h4>
{{ $t('cmdb.batch.total') }}&nbsp;<span style="color: blue">{{ total }}</span> {{ $t('cmdb.batch.successItems') }} {{ $t('cmdb.batch.total') }}&nbsp;<span style="color: blue">{{ total }}</span>
<span style="color: lightgreen">{{ success }}</span> {{ $t('cmdb.batch.failedItems') }} <span style="color: red">{{ errorNum }} </span>{{ $t('cmdb.batch.items') }} {{ $t('cmdb.batch.successItems') }} <span style="color: lightgreen">{{ success }}</span>
{{ $t('cmdb.batch.failedItems') }} <span style="color: red">{{ errorNum }} </span>{{ $t('cmdb.batch.items') }}
</h4> </h4>
<div> <div>
<span>{{ $t('cmdb.batch.errorTips') }}: </span> <span>{{ $t('cmdb.batch.errorTips') }}: </span>
@@ -39,7 +40,7 @@ export default {
default: false, default: false,
}, },
}, },
data: function() { data() {
return { return {
visible: false, visible: false,
complete: 0, complete: 0,
@@ -54,6 +55,13 @@ export default {
return this.upLoadData.length || 0 return this.upLoadData.length || 0
}, },
}, },
watch: {
ciType: {
handler() {
this.visible = false
},
},
},
methods: { methods: {
async upload2Server() { async upload2Server() {
this.visible = true this.visible = true
@@ -96,10 +104,6 @@ export default {
@import '~@/style/static.less'; @import '~@/style/static.less';
.cmdb-batch-upload-result { .cmdb-batch-upload-result {
.cmdb-batch-upload-result-title {
border-left: 4px solid #custom_colors[color_1];
padding-left: 10px;
}
.cmdb-batch-upload-result-content { .cmdb-batch-upload-result-content {
background-color: rgba(240, 245, 255, 0.35); background-color: rgba(240, 245, 255, 0.35);
border-radius: 5px; border-radius: 5px;

View File

@@ -15,21 +15,35 @@
</span> </span>
</span> </span>
<a-space> <a-space>
<a-button size="small" icon="plus" type="primary" @click="$refs.create.handleOpen(true, 'create')"> <a-button
type="primary"
class="ops-button-ghost"
ghost
@click="$refs.create.handleOpen(true, 'create')"
><ops-icon type="veops-increase" />
{{ $t('create') }} {{ $t('create') }}
</a-button> </a-button>
<a-button size="small" icon="user-add" type="primary" ghost @click="handlePerm">{{ $t('grant') }}</a-button> <EditAttrsPopover :typeId="typeId" class="operation-icon" @refresh="refreshAfterEditAttrs">
<a-popconfirm <a-button
:title=" type="primary"
$t('cmdb.preference.confirmcancelSub2', { name: `${this.$route.meta.title || this.$route.meta.name}` }) ghost
" class="ops-button-ghost"
:ok-text="$t('confirm')" ><ops-icon type="veops-configuration_table" />{{ $t('cmdb.configTable') }}</a-button
:cancel-text="$t('cancel')"
@confirm="unsubscribe"
placement="bottomRight"
> >
<a-button size="small" icon="star" type="primary" ghost>{{ $t('cmdb.preference.cancelSub') }}</a-button> </EditAttrsPopover>
</a-popconfirm> <a-dropdown v-model="visible">
<a-button type="primary" ghost class="ops-button-ghost">···</a-button>
<a-menu slot="overlay" @click="handleMenuClick">
<a-menu-item @click="handlePerm" key="grant">
<a-icon type="user-add" />
{{ $t('grant') }}
</a-menu-item>
<a-menu-item key="cancelSub" @click="unsubscribe">
<a-icon type="star" />
{{ $t('cmdb.preference.cancelSub') }}
</a-menu-item>
</a-menu>
</a-dropdown>
</a-space> </a-space>
</div> </div>
<div class="cmdb-ci-main"> <div class="cmdb-ci-main">
@@ -86,7 +100,6 @@
:scroll-y="{ enabled: true, gt: 20 }" :scroll-y="{ enabled: true, gt: 20 }"
:scroll-x="{ enabled: true, gt: 0 }" :scroll-x="{ enabled: true, gt: 0 }"
class="ops-unstripe-table" class="ops-unstripe-table"
:style="{ margin: '0 -12px' }"
:custom-config="{ storage: true }" :custom-config="{ storage: true }"
> >
<vxe-column align="center" type="checkbox" width="60" :fixed="isCheckboxFixed ? 'left' : ''"></vxe-column> <vxe-column align="center" type="checkbox" width="60" :fixed="isCheckboxFixed ? 'left' : ''"></vxe-column>
@@ -219,11 +232,9 @@
</template> </template>
</template> </template>
</vxe-table-column> </vxe-table-column>
<vxe-column align="left" field="operate" fixed="right" width="120"> <vxe-column align="left" field="operate" fixed="right" width="80">
<template #header> <template #header>
<span>{{ $t('operation') }}</span> <span>{{ $t('operation') }}</span>
<EditAttrsPopover :typeId="typeId" class="operation-icon" @refresh="refreshAfterEditAttrs" />
<!-- <a-icon class="operation-icon" type="control" /> -->
</template> </template>
<template #default="{ row }"> <template #default="{ row }">
<a-space> <a-space>
@@ -342,7 +353,7 @@ export default {
// if (this.selectedRowKeys && this.selectedRowKeys.length) { // if (this.selectedRowKeys && this.selectedRowKeys.length) {
// return this.windowHeight - 246 // return this.windowHeight - 246
// } // }
return this.windowHeight - 210 return this.windowHeight - 240
}, },
}, },
data() { data() {
@@ -377,6 +388,7 @@ export default {
passwordValue: {}, passwordValue: {},
lastEditCiId: null, lastEditCiId: null,
isContinueCloseEdit: true, isContinueCloseEdit: true,
visible: false,
} }
}, },
watch: { watch: {
@@ -916,15 +928,24 @@ export default {
}) })
}, },
unsubscribe(ciType, type = 'all') { unsubscribe(ciType, type = 'all') {
const promises = [subscribeCIType(this.typeId, ''), subscribeTreeView(this.typeId, '')] const that = this
this.$confirm({
title: that.$t('warning'),
content: that.$t('cmdb.preference.confirmcancelSub2', {
name: `${that.$route.meta.title || that.$route.meta.name}`,
}),
onOk() {
const promises = [subscribeCIType(that.typeId, ''), subscribeTreeView(that.typeId, '')]
Promise.all(promises).then(() => { Promise.all(promises).then(() => {
const lastTypeId = window.localStorage.getItem('ops_ci_typeid') || undefined const lastTypeId = window.localStorage.getItem('ops_ci_typeid') || undefined
if (Number(ciType) === Number(lastTypeId)) { if (Number(ciType) === Number(lastTypeId)) {
localStorage.setItem('ops_ci_typeid', '') localStorage.setItem('ops_ci_typeid', '')
} }
this.$message.success(this.$t('cmdb.preference.cancelSubSuccess')) that.$message.success(that.$t('cmdb.preference.cancelSubSuccess'))
this.resetRoute() that.resetRoute()
this.$router.push('/cmdb/preference') that.$router.push('/cmdb/preference')
})
},
}) })
}, },
resetRoute() { resetRoute() {
@@ -957,6 +978,11 @@ export default {
} }
}) })
}, },
handleMenuClick(e) {
if (e.key === 'grant') {
this.visible = false
}
},
}, },
} }
</script> </script>
@@ -968,11 +994,11 @@ export default {
<style lang="less" scoped> <style lang="less" scoped>
@import '~@/style/static.less'; @import '~@/style/static.less';
.cmdb-ci { .cmdb-ci {
margin-bottom: -24px;
.cmdb-ci-main {
background-color: #fff; background-color: #fff;
border-radius: 15px; padding: 20px;
padding: 12px; border-radius: @border-radius-box;
} height: calc(100vh - 64px);
overflow: auto;
margin-bottom: -24px;
} }
</style> </style>

View File

@@ -106,9 +106,9 @@
<a-date-picker <a-date-picker
v-decorator="[list.name, { rules: [{ required: false }] }]" v-decorator="[list.name, { rules: [{ required: false }] }]"
style="width: 100%" style="width: 100%"
:format="getFieldType(list.name) == 'date' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'" :format="getFieldType(list.name) == '4' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
v-if="getFieldType(list.name) === 'date' || getFieldType(list.name) === 'datetime'" v-if="getFieldType(list.name) === '4' || getFieldType(list.name) === '3'"
:showTime="getFieldType(list.name) === 'date' ? false : { format: 'HH:mm:ss' }" :showTime="getFieldType(list.name) === '4' ? false : { format: 'HH:mm:ss' }"
/> />
<a-input <a-input
v-if="getFieldType(list.name) === 'input'" v-if="getFieldType(list.name) === 'input'"
@@ -220,6 +220,7 @@ export default {
if (otherGroupAttr.length) { if (otherGroupAttr.length) {
_attributesByGroup.push({ id: -1, name: this.$t('other'), attributes: otherGroupAttr }) _attributesByGroup.push({ id: -1, name: this.$t('other'), attributes: otherGroupAttr })
} }
console.log(otherGroupAttr, _attributesByGroup)
this.attributesByGroup = _attributesByGroup this.attributesByGroup = _attributesByGroup
}) })
}, },
@@ -296,6 +297,38 @@ export default {
_this.$emit('reload', { ci_id: res.ci_id }) _this.$emit('reload', { ci_id: res.ci_id })
}) })
} }
// this.form.validateFields((err, values) => {
// if (err) {
// _this.$message.error('字段填写不符合要求!')
// return
// }
// Object.keys(values).forEach((k) => {
// if (Object.prototype.toString.call(values[k]) === '[object Object]' && values[k]) {
// values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
// }
// const _tempFind = this.attributeList.find((item) => item.name === k)
// if (_tempFind.value_type === '6') {
// values[k] = values[k] ? JSON.parse(values[k]) : undefined
// }
// })
// if (_this.action === 'update') {
// _this.$emit('submit', values)
// return
// }
// values.ci_type = _this.typeId
// console.log(values)
// this.attributesByGroup.forEach((group) => {
// this.$refs[`createInstanceFormByGroup_${group.id}`][0].getData()
// })
// console.log(1111)
// // addCI(values).then((res) => {
// // _this.$message.success('新增成功!')
// // _this.visible = false
// // _this.$emit('reload')
// // })
// })
}, },
handleClose() { handleClose() {
this.visible = false this.visible = false
@@ -340,7 +373,7 @@ export default {
} else if (_find.value_type === '0' || _find.value_type === '1') { } else if (_find.value_type === '0' || _find.value_type === '1') {
return 'input_number' return 'input_number'
} else if (_find.value_type === '4' || _find.value_type === '3') { } else if (_find.value_type === '4' || _find.value_type === '3') {
return this.valueTypeMap[_find.value_type] return _find.value_type
} else { } else {
return 'input' return 'input'
} }
@@ -363,6 +396,9 @@ export default {
this.batchUpdateLists.splice(_idx, 1) this.batchUpdateLists.splice(_idx, 1)
} }
}, },
// filterOption(input, option) {
// return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
// },
handleFocusInput(e, attr) { handleFocusInput(e, attr) {
console.log(attr) console.log(attr)
const _tempFind = this.attributeList.find((item) => item.name === attr.name) const _tempFind = this.attributeList.find((item) => item.name === attr.name)

View File

@@ -27,7 +27,7 @@
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="tab_2"> <a-tab-pane key="tab_2">
<span slot="tab"><a-icon type="branches" />{{ $t('cmdb.relation') }}</span> <span slot="tab"><a-icon type="branches" />{{ $t('cmdb.relation') }}</span>
<div :style="{ height: '100%', padding: '24px' }"> <div :style="{ height: '100%', padding: '24px', overflow: 'auto' }">
<CiDetailRelation ref="ciDetailRelation" :ciId="ciId" :typeId="typeId" :ci="ci" /> <CiDetailRelation ref="ciDetailRelation" :ciId="ciId" :typeId="typeId" :ci="ci" />
</div> </div>
</a-tab-pane> </a-tab-pane>

View File

@@ -11,9 +11,11 @@
@setFixedList="setFixedList" @setFixedList="setFixedList"
/> />
</template> </template>
<slot>
<div :style="{ height: '100%', width: '30px', float: 'right', borderLeft: '1px solid #e8eaec' }"> <div :style="{ height: '100%', width: '30px', float: 'right', borderLeft: '1px solid #e8eaec' }">
<a-icon :style="{ margin: '13px 0 0 10px ' }" type="control" /> <a-icon :style="{ margin: '13px 0 0 10px ' }" type="control" />
</div> </div>
</slot>
</a-popover> </a-popover>
</template> </template>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="attr-ad" :style="{ height: `${windowHeight - 104}px` }"> <div class="attr-ad" :style="{ height: `${windowHeight - 130}px` }">
<div v-if="adCITypeList && adCITypeList.length"> <div v-if="adCITypeList && adCITypeList.length">
<a-tabs size="small" v-model="currentTab"> <a-tabs size="small" v-model="currentTab">
<a-tab-pane v-for="item in adCITypeList" :key="item.id"> <a-tab-pane v-for="item in adCITypeList" :key="item.id">
@@ -207,7 +207,7 @@ export default {
@import '~@/style/static.less'; @import '~@/style/static.less';
.attr-ad { .attr-ad {
position: relative; position: relative;
padding: 0 12px; padding: 0 20px;
.attr-ad-header { .attr-ad-header {
width: 100%; width: 100%;
display: inline-flex; display: inline-flex;

View File

@@ -1,5 +1,5 @@
<template> <template>
<div :style="{ height: `${windowHeight - 156}px`, overflow: 'auto', position: 'relative' }"> <div :style="{ height: `${windowHeight - 187}px`, overflow: 'auto', position: 'relative' }">
<a <a
v-if="!adrIsInner" v-if="!adrIsInner"
:style="{ position: 'absolute', right: 0, top: 0 }" :style="{ position: 'absolute', right: 0, top: 0 }"

View File

@@ -1,11 +1,19 @@
<template> <template>
<div :class="{ 'attribute-card': true, 'attribute-card-inherited': inherited }"> <div
@click="
() => {
if (isAdd) {
$emit('add')
}
}
"
:class="{ 'attribute-card': true, 'attribute-card-add': isAdd, 'attribute-card-inherited': inherited }"
>
<div class="attribute-card-uniqueKey" v-if="isUnique">{{ $t('cmdb.ciType.uniqueKey') }}</div>
<template v-if="!isAdd">
<a-tooltip :title="inherited ? $t('cmdb.ciType.inheritFrom', { name: property.inherited_from }) : ''"> <a-tooltip :title="inherited ? $t('cmdb.ciType.inheritFrom', { name: property.inherited_from }) : ''">
<div class="attribute-card-content"> <div class="attribute-card-content">
<div <div :class="{ 'attribute-card-value-type-icon': true, handle: !inherited }">
:class="{ 'attribute-card-value-type-icon': true, handle: !inherited }"
:style="{ ...getPropertyStyle(property) }"
>
<ValueTypeIcon :attr="property" /> <ValueTypeIcon :attr="property" />
</div> </div>
<div :class="{ 'attribute-card-content-inner': true, 'attribute-card-name-required': property.is_required }"> <div :class="{ 'attribute-card-content-inner': true, 'attribute-card-name-required': property.is_required }">
@@ -38,21 +46,20 @@
</h3> </h3>
<a-descriptions layout="horizontal" bordered size="small" :column="2"> <a-descriptions layout="horizontal" bordered size="small" :column="2">
<a-descriptions-item v-for="item in propertyList" :key="item.property" :label="item.label"> <a-descriptions-item v-for="item in propertyList" :key="item.property" :label="item.label">
<components <ops-icon
:is="`ops_${item.property}`" :style="{ color: property[item.property] ? '#7f97fa' : '', fontSize: '10px' }"
v-if="property[item.property]" :type="`ops-${item.property}-disabled`"
:style="{ width: '1em', height: '1em' }"
/> />
<ops-icon v-else :type="`ops-${item.property}-disabled`" />
</a-descriptions-item> </a-descriptions-item>
<a-descriptions-item label></a-descriptions-item> <a-descriptions-item label></a-descriptions-item>
</a-descriptions> </a-descriptions>
</div> </div>
<a-space :style="{ cursor: 'pointer' }"> <a-space :style="{ cursor: 'pointer' }">
<components <ops-icon
v-for="item in propertyList.filter((p) => property[p.property])" v-for="item in propertyList.filter((p) => property[p.property])"
:key="item.property" :key="item.property"
:is="`ops_${item.property}`" :style="{ color: '#7f97fa', fontSize: '10px' }"
:type="`ops-${item.property}-disabled`"
/> />
</a-space> </a-space>
</a-popover> </a-popover>
@@ -62,10 +69,15 @@
<a-tooltip :title="$t('cmdb.ciType.computeForAllCITips')"> <a-tooltip :title="$t('cmdb.ciType.computeForAllCITips')">
<a v-if="!isStore && property.is_computed"><a-icon type="redo" @click="handleCalcComputed"/></a> <a v-if="!isStore && property.is_computed"><a-icon type="redo" @click="handleCalcComputed"/></a>
</a-tooltip> </a-tooltip>
<a style="color:red;"><a-icon type="delete" @click="handleDelete"/></a> <a v-if="!isUnique" style="color:red;"><a-icon type="delete" @click="handleDelete"/></a>
</a-space> </a-space>
</div> </div>
<TriggerForm ref="triggerForm" :CITypeId="CITypeId" /> <TriggerForm ref="triggerForm" :CITypeId="CITypeId" />
</template>
<template v-else>
<a><a-icon type="plus"/></a>
<div>{{ $t('cmdb.ciType.addAttribute') }}</div>
</template>
</div> </div>
</template> </template>
@@ -82,10 +94,15 @@ import {
ops_is_unique, ops_is_unique,
} from '@/core/icons' } from '@/core/icons'
import { valueTypeMap } from '../../utils/const' import { valueTypeMap } from '../../utils/const'
import { getPropertyStyle } from '../../utils/helper'
import TriggerForm from './triggerForm.vue' import TriggerForm from './triggerForm.vue'
export default { export default {
name: 'AttributeCard', name: 'AttributeCard',
inject: {
unique: {
from: 'unique',
default: () => undefined,
},
},
components: { components: {
ValueTypeIcon, ValueTypeIcon,
TriggerForm, TriggerForm,
@@ -114,11 +131,21 @@ export default {
type: Array, type: Array,
default: () => [], default: () => [],
}, },
isAdd: {
type: Boolean,
default: false,
},
}, },
data() { data() {
return {} return {}
}, },
computed: { computed: {
isUnique() {
if (this.unique) {
return this.property?.name === this.unique()
}
return false
},
valueTypeMap() { valueTypeMap() {
return valueTypeMap() return valueTypeMap()
}, },
@@ -147,11 +174,10 @@ export default {
] ]
}, },
inherited() { inherited() {
return this.property.inherited || false return this.property?.inherited || false
}, },
}, },
methods: { methods: {
getPropertyStyle,
handleEdit() { handleEdit() {
this.$emit('edit') this.$emit('edit')
}, },
@@ -196,11 +222,12 @@ export default {
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@import '~@/style/static.less';
.attribute-card { .attribute-card {
width: 182px; width: 182px;
height: 80px; height: 80px;
background: #f8faff; background: @primary-color_6;
border-radius: 5px; border-radius: 2px;
position: relative; position: relative;
margin-bottom: 16px; margin-bottom: 16px;
transition: all 0.3s; transition: all 0.3s;
@@ -219,7 +246,7 @@ export default {
.attribute-card-value-type-icon { .attribute-card-value-type-icon {
width: 32px; width: 32px;
height: 32px; height: 32px;
font-size: 12px; font-size: 16px;
background: #ffffff !important; background: #ffffff !important;
box-shadow: 0px 1px 2px rgba(47, 84, 235, 0.2); box-shadow: 0px 1px 2px rgba(47, 84, 235, 0.2);
border-radius: 2px; border-radius: 2px;
@@ -243,7 +270,7 @@ export default {
white-space: nowrap; white-space: nowrap;
} }
.attribute-card-name-default-show { .attribute-card-name-default-show {
color: #2f54eb; color: @primary-color;
} }
.attribute-card_value-type { .attribute-card_value-type {
font-size: 10px; font-size: 10px;
@@ -270,20 +297,67 @@ export default {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
left: 0; left: 0;
background: linear-gradient(180deg, #96abd6 0%, #ecf2ff 0.01%, #ffffff 143.33%); background: @primary-color_5;
border-radius: 0px 0px 5px 5px; border-radius: 0px 0px 2px 2px;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
border-top: 1px solid @primary-color_3;
.attribute-card-operation { .attribute-card-operation {
visibility: hidden; visibility: hidden;
} }
} }
.attribute-card-uniqueKey {
position: absolute;
right: -12px;
top: 0;
color: @func-color_2;
background: url('../../assets/unique_card.png');
font-size: 10px;
z-index: 1;
background-size: 100% 100%;
background-repeat: no-repeat;
min-width: 55px;
padding: 2px 0 2px 5px;
}
} }
.attribute-card-inherited { .attribute-card-inherited {
background: #f3f4f7; background: @primary-color_7;
.attribute-card-footer { .attribute-card-footer {
background: #eaedf3; background: @text-color_7;
}
}
.attribute-card-add {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
position: relative;
background-color: inherit;
&:hover {
box-shadow: none;
background-color: @primary-color_6;
}
&:after {
content: '';
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 1;
background: linear-gradient(90deg, @text-color_5 50%, transparent 0) repeat-x,
linear-gradient(90deg, @text-color_5 50%, transparent 0) repeat-x,
linear-gradient(0deg, @text-color_5 50%, transparent 0) repeat-y,
linear-gradient(0deg, @text-color_5 50%, transparent 0) repeat-y;
background-size: 15px 1px, 15px 1px, 1px 15px, 1px 15px;
background-position: 0 0, 0 100%, 0 0, 100% 0;
}
div {
color: @text-color_4;
font-size: 12px;
} }
} }
</style> </style>

View File

@@ -172,8 +172,6 @@ export default {
margin-right: 60px; margin-right: 60px;
.ant-input-group.ant-input-group-compact > *:first-child, .ant-input-group.ant-input-group-compact > *:first-child,
.ant-input-group.ant-input-group-compact > .ant-select:first-child > .ant-select-selection { .ant-input-group.ant-input-group-compact > .ant-select:first-child > .ant-select-selection {
border-top-left-radius: 20px !important;
border-bottom-left-radius: 20px !important;
background-color: #custom_colors[color_1]; background-color: #custom_colors[color_1];
color: #fff; color: #fff;
border: none; border: none;
@@ -202,7 +200,6 @@ export default {
.ant-input { .ant-input {
background-color: #f0f5ff; background-color: #f0f5ff;
border: none; border: none;
border-radius: 20px;
&:focus { &:focus {
box-shadow: none; box-shadow: none;
} }

View File

@@ -12,14 +12,28 @@
</a-form-item> </a-form-item>
</span> </span>
</a-modal> </a-modal>
<div class="ci-types-attributes" :style="{ maxHeight: `${windowHeight - 104}px` }"> <div class="ci-types-attributes" :style="{ height: `${windowHeight - 130}px` }">
<a-space style="margin-bottom: 10px"> <a-space style="margin-bottom: 10px">
<a-button type="primary" @click="handleAddGroup" size="small" class="ops-button-primary" icon="plus">{{ <a-button @click="handleAddGroup" size="small" icon="plus">{{ $t('cmdb.ciType.group') }}</a-button>
$t('cmdb.ciType.group') <a-button @click="handleOpenUniqueConstraint" size="small">{{ $t('cmdb.ciType.uniqueConstraint') }}</a-button>
}}</a-button> <div>
<a-button type="primary" @click="handleOpenUniqueConstraint" size="small" class="ops-button-primary">{{ <a-tooltip
$t('cmdb.ciType.uniqueConstraint') v-for="type in Object.keys(valueTypeMap)"
}}</a-button> :key="type"
:title="$t('cmdb.ciType.filterTips', { name: valueTypeMap[type] })"
>
<span
@click="handleFilterType(type)"
:class="{
'ci-types-attributes-filter': true,
'ci-types-attributes-filter-selected': attrTypeFilter.includes(type),
}"
>
<ops-icon :type="getPropertyIcon({ value_type: type })" />
{{ valueTypeMap[type] }}
</span>
</a-tooltip>
</div>
</a-space> </a-space>
<div :key="CITypeGroup.id" v-for="(CITypeGroup, index) in CITypeGroups"> <div :key="CITypeGroup.id" v-for="(CITypeGroup, index) in CITypeGroups">
<div> <div>
@@ -29,13 +43,6 @@
> >
<span style="font-weight:700">{{ CITypeGroup.name }}</span> <span style="font-weight:700">{{ CITypeGroup.name }}</span>
<span style="color: #c3cdd7;margin:0 5px;">({{ CITypeGroup.attributes.length }})</span> <span style="color: #c3cdd7;margin:0 5px;">({{ CITypeGroup.attributes.length }})</span>
<a v-if="!CITypeGroup.inherited" @click="handleEditGroupName(index, CITypeGroup)">
<a-icon type="edit" />
</a>
<a v-else :style="{ cursor: 'not-allowed', color: 'gray' }">
<a-icon type="edit" />
</a>
</div> </div>
<template v-else> <template v-else>
<span> <span>
@@ -64,21 +71,24 @@
@click="handleMoveGroup(index, index + 1)" @click="handleMoveGroup(index, index + 1)"
/></a> /></a>
</a-tooltip> </a-tooltip>
<a-dropdown>
<a-tooltip> <a><ops-icon type="veops-more"/></a>
<template slot="title">{{ $t('cmdb.ciType.selectAttribute') }}</template> <a-menu slot="overlay">
<a><a-icon type="plus" @click="handleAddGroupAttr(index)"/></a> <a-menu-item @click="handleAddGroupAttr(index)">
</a-tooltip> <template slot="title"></template>
<a-tooltip> <a-icon type="plus" />
<template slot="title">{{ $t('cmdb.ciType.deleteGroup') }}</template> {{ $t('cmdb.ciType.addAttribute') }}
<a </a-menu-item>
:style="{ color: CITypeGroup.inherited ? 'gray' : 'red' }" <a-menu-item @click="handleEditGroupName(index, CITypeGroup)" :disabled="CITypeGroup.inherited">
:disabled="CITypeGroup.inherited" <a-icon type="edit" />
><a-icon {{ $t('cmdb.ciType.editGroupName') }}
type="delete" </a-menu-item>
@click="handleDeleteGroup(CITypeGroup)" <a-menu-item @click="handleDeleteGroup(CITypeGroup)" :disabled="CITypeGroup.inherited">
/></a> <a-icon type="delete" />
</a-tooltip> {{ $t('cmdb.ciType.deleteGroup') }}
</a-menu-item>
</a-menu>
</a-dropdown>
</a-space> </a-space>
</div> </div>
<div class="ci-types-attributes-wrapper"> <div class="ci-types-attributes-wrapper">
@@ -98,7 +108,7 @@
handle=".handle" handle=".handle"
> >
<AttributeCard <AttributeCard
v-for="item in CITypeGroup.attributes" v-for="item in filterValueType(CITypeGroup.attributes)"
:key="item.id" :key="item.id"
@edit="handleEditProperty(item)" @edit="handleEditProperty(item)"
:property="item" :property="item"
@@ -106,6 +116,7 @@
:CITypeId="CITypeId" :CITypeId="CITypeId"
:attributes="attributes" :attributes="attributes"
/> />
<AttributeCard isAdd @add="handleAddGroupAttr(index)" />
<i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i>
</draggable> </draggable>
</div> </div>
@@ -114,10 +125,11 @@
<div :style="{ height: '32px', lineHeight: '32px', display: 'inline-block', fontSize: '14px' }"> <div :style="{ height: '32px', lineHeight: '32px', display: 'inline-block', fontSize: '14px' }">
<span style="font-weight:700">{{ $t('other') }}</span> <span style="font-weight:700">{{ $t('other') }}</span>
<span style="color: #c3cdd7;margin-left:5px;">({{ otherGroupAttributes.length }})</span> <span style="color: #c3cdd7;margin-left:5px;">({{ otherGroupAttributes.length }})</span>
<span style="color: #c3cdd7;margin-left:5px;font-size:10px;">{{ $t('cmdb.ciType.otherGroupTips') }}</span>
</div> </div>
<div style="float: right"> <div style="float: right">
<a-tooltip> <a-tooltip>
<template slot="title">{{ $t('cmdb.ciType.selectAttribute') }}</template> <template slot="title">{{ $t('cmdb.ciType.addAttribute') }}</template>
<a @click="handleAddGroupAttr(undefined)"><a-icon type="plus"/></a> <a @click="handleAddGroupAttr(undefined)"><a-icon type="plus"/></a>
</a-tooltip> </a-tooltip>
</div> </div>
@@ -138,7 +150,7 @@
handle=".handle" handle=".handle"
> >
<AttributeCard <AttributeCard
v-for="item in otherGroupAttributes" v-for="item in filterValueType(otherGroupAttributes)"
:key="item.id" :key="item.id"
@edit="handleEditProperty(item)" @edit="handleEditProperty(item)"
:property="item" :property="item"
@@ -146,6 +158,7 @@
:CITypeId="CITypeId" :CITypeId="CITypeId"
:attributes="attributes" :attributes="attributes"
/> />
<AttributeCard isAdd @add="handleAddGroupAttr(undefined)" />
<i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i>
</draggable> </draggable>
</div> </div>
@@ -185,6 +198,8 @@ import AttributeCard from './attributeCard.vue'
import AttributeEditForm from './attributeEditForm.vue' import AttributeEditForm from './attributeEditForm.vue'
import NewCiTypeAttrModal from './newCiTypeAttrModal.vue' import NewCiTypeAttrModal from './newCiTypeAttrModal.vue'
import UniqueConstraint from './uniqueConstraint.vue' import UniqueConstraint from './uniqueConstraint.vue'
import { valueTypeMap } from '../../utils/const'
import { getPropertyIcon } from '../../utils/helper'
export default { export default {
name: 'AttributesTable', name: 'AttributesTable',
@@ -214,6 +229,8 @@ export default {
otherGroupAttributes: [], otherGroupAttributes: [],
addGroupModal: false, addGroupModal: false,
newGroupName: '', newGroupName: '',
attrTypeFilter: [],
unique: '',
} }
}, },
computed: { computed: {
@@ -223,9 +240,17 @@ export default {
windowHeight() { windowHeight() {
return this.$store.state.windowHeight return this.$store.state.windowHeight
}, },
valueTypeMap() {
return valueTypeMap()
},
}, },
provide() { provide() {
return { refresh: this.getCITypeGroupData } return {
refresh: this.getCITypeGroupData,
unique: () => {
return this.unique
},
}
}, },
beforeCreate() {}, beforeCreate() {},
created() {}, created() {},
@@ -233,6 +258,7 @@ export default {
this.getCITypeGroupData() this.getCITypeGroupData()
}, },
methods: { methods: {
getPropertyIcon,
handleEditProperty(property) { handleEditProperty(property) {
this.$refs.attributeEditForm.handleEdit(property, this.attributes) this.$refs.attributeEditForm.handleEdit(property, this.attributes)
}, },
@@ -260,6 +286,7 @@ export default {
group.editable = false group.editable = false
group.originOrder = group.order group.originOrder = group.order
group.originName = group.name group.originName = group.name
// group.attributes = group.attributes.sort((a, b) => a.order - b.order)
}) })
this.otherGroupAttributes = this.attributes this.otherGroupAttributes = this.attributes
@@ -282,6 +309,7 @@ export default {
Promise.all(promises).then((values) => { Promise.all(promises).then((values) => {
console.log(values) console.log(values)
this.attributes = values[0].attributes this.attributes = values[0].attributes
this.unique = values[0].unique
const temp = {} const temp = {}
this.attributes.forEach((attr) => { this.attributes.forEach((attr) => {
temp[attr.id] = attr temp[attr.id] = attr
@@ -387,6 +415,7 @@ export default {
group.attributes = group.attributes.filter((x) => !values.checkedAttributes.includes(x.id)) group.attributes = group.attributes.filter((x) => !values.checkedAttributes.includes(x.id))
} }
}) })
// this.CITypeGroups = this.CITypeGroups
this.otherGroupAttributes.forEach((attributes) => { this.otherGroupAttributes.forEach((attributes) => {
if (values.groupId === null) { if (values.groupId === null) {
@@ -523,20 +552,66 @@ export default {
handleOpenUniqueConstraint() { handleOpenUniqueConstraint() {
this.$refs.uniqueConstraint.open(this.attributes) this.$refs.uniqueConstraint.open(this.attributes)
}, },
handleFilterType(type) {
const _idx = this.attrTypeFilter.findIndex((item) => item === type)
if (_idx > -1) {
this.attrTypeFilter.splice(_idx, 1)
} else {
this.attrTypeFilter.push(type)
}
},
filterValueType(array) {
const { attrTypeFilter } = this
return array.filter((attr) => {
if (!attrTypeFilter.length) {
return true
} else {
if (attrTypeFilter.includes('7') && attr.is_password) {
return true
}
if (attrTypeFilter.includes('8') && attr.is_link) {
return true
}
if (
attrTypeFilter.includes(attr.value_type) &&
attr.value_type === '2' &&
(attr.is_password || attr.is_link)
) {
return false
}
if (attrTypeFilter.includes(attr.value_type)) {
return true
}
return false
}
})
},
}, },
watch: {},
} }
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@import '~@/style/static.less';
.fold { .fold {
width: calc(100% - 216px); width: calc(100% - 216px);
display: inline-block; display: inline-block;
} }
.ci-types-attributes { .ci-types-attributes {
padding: 16px 24px 24px; padding: 0 20px;
overflow-y: auto; overflow-y: auto;
.ci-types-attributes-filter {
color: @text-color_4;
cursor: pointer;
padding: 3px 8px;
white-space: nowrap;
margin-right: 5px;
}
.ci-types-attributes-filter:hover,
.ci-types-attributes-filter-selected {
background-color: @primary-color_5;
}
.property-item-empty { .property-item-empty {
color: #40a9ff; color: #40a9ff;
width: calc(100% - 20px); width: calc(100% - 20px);

View File

@@ -1,6 +1,6 @@
<template> <template>
<a-card :bordered="false" :bodyStyle="{ padding: '0' }"> <a-card :bordered="false" :bodyStyle="{ padding: '0' }">
<a-tabs :activeKey="activeKey" @change="changeTab" class="ops-tab" type="card"> <a-tabs :activeKey="activeKey" @change="changeTab" class="ops-tab">
<a-tab-pane key="1" :tab="$t('cmdb.ciType.attributes')"> <a-tab-pane key="1" :tab="$t('cmdb.ciType.attributes')">
<AttributesTable ref="attributesTable" :CITypeId="CITypeId" :CITypeName="CITypeName"></AttributesTable> <AttributesTable ref="attributesTable" :CITypeId="CITypeId" :CITypeName="CITypeName"></AttributesTable>
</a-tab-pane> </a-tab-pane>
@@ -18,6 +18,8 @@
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="6" :tab="$t('cmdb.ciType.grant')"> <a-tab-pane key="6" :tab="$t('cmdb.ciType.grant')">
<GrantComp :CITypeId="CITypeId" resourceType="CIType" :resourceTypeName="CITypeName"></GrantComp> <GrantComp :CITypeId="CITypeId" resourceType="CIType" :resourceTypeName="CITypeName"></GrantComp>
<div class="citype-detail-title">{{ $t('cmdb.components.relationGrant') }}</div>
<RelationTable isInGrantComp :CITypeId="CITypeId" :CITypeName="CITypeName"></RelationTable>
</a-tab-pane> </a-tab-pane>
</a-tabs> </a-tabs>
</a-card> </a-card>
@@ -74,4 +76,13 @@ export default {
} }
</script> </script>
<style lang="less" scoped></style> <style lang="less" scoped>
@import '~@/style/static.less';
.citype-detail-title {
border-left: 4px solid @primary-color;
padding-left: 10px;
margin-left: 20px;
margin-bottom: 10px;
}
</style>

View File

@@ -8,11 +8,10 @@
</div> </div>
<SplitPane <SplitPane
v-else v-else
:min="280" :min="220"
:max="500" :max="500"
:paneLengthPixel.sync="paneLengthPixel" :paneLengthPixel.sync="paneLengthPixel"
appName="cmdb-ci-types" appName="cmdb-ci-types"
triggerColor="#F0F5FF"
:triggerLength="18" :triggerLength="18"
> >
<template #one> <template #one>
@@ -22,22 +21,23 @@
:disabled="!permissions.includes('admin') && !permissions.includes('cmdb_admin')" :disabled="!permissions.includes('admin') && !permissions.includes('cmdb_admin')"
type="primary" type="primary"
size="small" size="small"
icon="plus" ghost
@click="handleClickAddGroup" @click="handleClickAddGroup"
class="ops-button-primary" class="ops-button-ghost"
>{{ $t('cmdb.ciType.group') }}</a-button ><ops-icon type="veops-increase" />{{ $t('cmdb.ciType.group') }}</a-button
> >
<a-space> <a-space>
<a <span
:style="{ cursor: 'pointer' }"
@click=" @click="
() => { () => {
$refs.attributeStore.open() $refs.attributeStore.open()
} }
" "
>{{ $t('cmdb.ciType.attributeLibray') }}</a >{{ $t('cmdb.ciType.attributeLibray') }}
> </span>
<a-dropdown v-if="permissions.includes('admin') || permissions.includes('cmdb_admin')"> <a-dropdown v-if="permissions.includes('admin') || permissions.includes('cmdb_admin')">
<a><ops-icon type="ops-menu"/></a> <ops-icon type="ops-menu" :style="{ cursor: 'pointer' }" />
<a-menu slot="overlay"> <a-menu slot="overlay">
<a-menu-item key="0"> <a-menu-item key="0">
<a-upload <a-upload
@@ -83,7 +83,7 @@
<a-space> <a-space>
<a-tooltip> <a-tooltip>
<template slot="title">{{ $t('cmdb.ciType.addCITypeInGroup') }}</template> <template slot="title">{{ $t('cmdb.ciType.addCITypeInGroup') }}</template>
<a><a-icon type="plus" @click="handleCreate(g)"/></a> <a><ops-icon type="veops-increase" @click="handleCreate(g)"/></a>
</a-tooltip> </a-tooltip>
<template v-if="g.id !== -1"> <template v-if="g.id !== -1">
<a-tooltip> <a-tooltip>
@@ -113,7 +113,7 @@
> >
<div> <div>
<OpsMoveIcon <OpsMoveIcon
style="width: 17px; height: 17px; display: none; position: absolute; left: 15px; top: 5px" style="width: 17px; height: 17px; display: none; position: absolute; left: -1px; top: 8px"
/> />
<span class="ci-types-left-detail-icon"> <span class="ci-types-left-detail-icon">
<template v-if="ci.icon"> <template v-if="ci.icon">
@@ -134,18 +134,33 @@
</span> </span>
</div> </div>
<span class="ci-types-left-detail-title">{{ ci.alias || ci.name }}</span> <span class="ci-types-left-detail-title">{{ ci.alias || ci.name }}</span>
<a-space class="ci-types-left-detail-action"> <a-dropdown :getPopupContainer="(trigger) => trigger">
<a><a-icon type="user-add" @click="(e) => handlePerm(e, ci)"/></a> <a class="ci-types-left-detail-action">
<a><a-icon type="edit" @click="(e) => handleEdit(e, ci)"/></a> <ops-icon type="veops-more" />
<a </a>
<a-menu slot="overlay">
<a-menu-item @click="(e) => handlePerm(e, ci)">
<a-icon type="user-add" />
{{ $t('grant') }}
</a-menu-item>
<a-menu-item @click="(e) => handleEdit(e, ci)">
<a-icon type="edit" />
{{ $t('cmdb.ciType.editCIType') }}
</a-menu-item>
<a-menu-item
v-if="permissions.includes('admin') || permissions.includes('cmdb_admin')" v-if="permissions.includes('admin') || permissions.includes('cmdb_admin')"
:disabled="ci.inherited" :disabled="ci.inherited"
@click="(e) => handleDownloadCiType(e, ci)" @click="(e) => handleDownloadCiType(e, ci)"
> >
<a-icon type="download" /> <a-icon type="download" />
</a> {{ $t('cmdb.ciType.downloadType') }}
<a style="color: red" @click="(e) => handleDelete(e, ci)"><a-icon type="delete"/></a> </a-menu-item>
</a-space> <a-menu-item @click="(e) => handleDelete(e, ci)">
<a-icon type="delete" />
{{ $t('cmdb.ciType.deleteCIType') }}
</a-menu-item>
</a-menu>
</a-dropdown>
</div> </div>
</draggable> </draggable>
</div> </div>
@@ -270,7 +285,7 @@
</div> </div>
</el-select> </el-select>
</a-form-item> </a-form-item>
<a-form-item :label="$t('cmdb.ciType.uniqueKey')"> <a-form-item :help="$t('cmdb.ciType.uniqueKeyTips')" :label="$t('cmdb.ciType.uniqueKey')">
<el-select <el-select
size="small" size="small"
filterable filterable
@@ -446,7 +461,6 @@ export default {
}, },
currentCName() { currentCName() {
if (this.currentId) { if (this.currentId) {
console.log(this.currentId)
if (this.currentId.split('%')[2] !== 'null') { if (this.currentId.split('%')[2] !== 'null') {
return this.currentId.split('%')[2] return this.currentId.split('%')[2]
} }
@@ -807,8 +821,8 @@ export default {
}) })
}, },
handleDelete(e, record) { handleDelete(e, record) {
e.preventDefault() e.domEvent.preventDefault()
e.stopPropagation() e.domEvent.stopPropagation()
const that = this const that = this
this.$confirm({ this.$confirm({
title: that.$t('warning'), title: that.$t('warning'),
@@ -823,8 +837,8 @@ export default {
}) })
}, },
handleDownloadCiType(e, ci) { handleDownloadCiType(e, ci) {
e.preventDefault() e.domEvent.preventDefault()
e.stopPropagation() e.domEvent.stopPropagation()
const x = new XMLHttpRequest() const x = new XMLHttpRequest()
x.open('GET', `/api/v0.1/ci_types/${ci.id}/template/export`, true) x.open('GET', `/api/v0.1/ci_types/${ci.id}/template/export`, true)
x.responseType = 'blob' x.responseType = 'blob'
@@ -845,8 +859,8 @@ export default {
}) })
}, },
async handleEdit(e, record) { async handleEdit(e, record) {
e.preventDefault() e.domEvent.preventDefault()
e.stopPropagation() e.domEvent.stopPropagation()
this.drawerTitle = this.$t('cmdb.ciType.editCIType') this.drawerTitle = this.$t('cmdb.ciType.editCIType')
this.drawerVisible = true this.drawerVisible = true
await getCITypeAttributesById(record.id).then((res) => { await getCITypeAttributesById(record.id).then((res) => {
@@ -899,8 +913,8 @@ export default {
} }
}, },
handlePerm(e, ci) { handlePerm(e, ci) {
e.preventDefault() e.domEvent.preventDefault()
e.stopPropagation() e.domEvent.stopPropagation()
roleHasPermissionToGrant({ roleHasPermissionToGrant({
app_id: 'cmdb', app_id: 'cmdb',
resource_type_name: 'CIType', resource_type_name: 'CIType',
@@ -932,6 +946,8 @@ export default {
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@import '~@/style/static.less';
.ci-types-wrap { .ci-types-wrap {
margin: 0 0 -24px 0; margin: 0 0 -24px 0;
.ci-types-empty { .ci-types-empty {
@@ -945,21 +961,24 @@ export default {
width: 100%; width: 100%;
overflow: auto; overflow: auto;
float: left; float: left;
border-top-left-radius: 20px;
border-top-right-radius: 20px;
.ci-types-left-content { .ci-types-left-content {
max-height: calc(100% - 45px); max-height: calc(100% - 45px);
overflow: hidden;
&:hover {
overflow: auto; overflow: auto;
} }
}
.ci-types-left-title { .ci-types-left-title {
padding: 10px 15px; padding: 10px 0;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
color: @text-color_3;
} }
.ci-types-left-group { .ci-types-left-group {
position: relative; position: relative;
padding: 8px 15px; padding: 8px 0 8px 14px;
color: rgb(99, 99, 99); color: rgb(99, 99, 99);
cursor: pointer; cursor: pointer;
font-size: 14px; font-size: 14px;
@@ -972,7 +991,7 @@ export default {
display: none; display: none;
} }
&:hover { &:hover {
background-color: #e1efff; background-color: @primary-color_3;
> div:nth-child(2) { > div:nth-child(2) {
display: inline-flex; display: inline-flex;
} }
@@ -982,17 +1001,25 @@ export default {
} }
} }
.ci-types-left-detail { .ci-types-left-detail {
padding: 3px 14px 3px 36px; padding: 3px 14px;
cursor: pointer; cursor: pointer;
position: relative; position: relative;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: flex-start; justify-content: flex-start;
align-items: center;
margin-bottom: 4px; margin-bottom: 4px;
height: 32px;
line-height: 32px;
.ci-types-left-detail-action { .ci-types-left-detail-action {
display: none; display: none;
margin-left: auto; margin-left: auto;
} }
.ci-types-left-detail-title {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.ci-types-left-detail-icon { .ci-types-left-detail-icon {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -1009,7 +1036,7 @@ export default {
} }
} }
&:hover { &:hover {
background-color: #e1efff; background-color: @primary-color_3;
svg { svg {
display: inline !important; display: inline !important;
} }
@@ -1019,7 +1046,7 @@ export default {
} }
} }
.selected { .selected {
background-color: #e1efff; background-color: @primary-color_3;
.ci-types-left-detail-title { .ci-types-left-detail-title {
font-weight: 700; font-weight: 700;
} }
@@ -1028,6 +1055,7 @@ export default {
.ci-types-right { .ci-types-right {
width: 100%; width: 100%;
position: relative; position: relative;
background-color: #fff;
.ci-types-right-empty { .ci-types-right-empty {
position: absolute; position: absolute;
text-align: center; text-align: center;
@@ -1039,7 +1067,6 @@ export default {
.ci-types-left, .ci-types-left,
.ci-types-right { .ci-types-right {
height: 100%; height: 100%;
background-color: #fff;
} }
} }
</style> </style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="relation-ad" :style="{ height: `${windowHeight - 104}px` }"> <div class="relation-ad" :style="{ height: `${windowHeight - 130}px` }">
<div class="relation-ad-item" v-for="item in relationList" :key="item.id"> <div class="relation-ad-item" v-for="item in relationList" :key="item.id">
<treeselect <treeselect
class="custom-treeselect" class="custom-treeselect"
@@ -253,7 +253,7 @@ export default {
<style lang="less" scoped> <style lang="less" scoped>
.relation-ad { .relation-ad {
overflow: auto; overflow: auto;
padding: 24px; padding: 0 20px;
.relation-ad-item { .relation-ad-item {
display: inline-flex; display: inline-flex;
justify-content: flex-start; justify-content: flex-start;

View File

@@ -1,6 +1,7 @@
<template> <template>
<div :style="{ padding: '16px 24px 24px' }"> <div :style="{ padding: '0 20px 20px' }">
<a-button <a-button
v-if="!isInGrantComp"
style="margin-bottom: 10px" style="margin-bottom: 10px"
@click="handleCreate" @click="handleCreate"
type="primary" type="primary"
@@ -43,7 +44,7 @@
<template #default="{row}"> <template #default="{row}">
<a-space v-if="!row.isParent && row.source_ci_type_id"> <a-space v-if="!row.isParent && row.source_ci_type_id">
<a @click="handleOpenGrant(row)"><a-icon type="user-add"/></a> <a @click="handleOpenGrant(row)"><a-icon type="user-add"/></a>
<a-popconfirm :title="$t('cmdb.ciType.confirmDelete2')" @confirm="handleDelete(row)"> <a-popconfirm v-if="!isInGrantComp" :title="$t('cmdb.ciType.confirmDelete2')" @confirm="handleDelete(row)">
<a style="color: red;"><a-icon type="delete"/></a> <a style="color: red;"><a-icon type="delete"/></a>
</a-popconfirm> </a-popconfirm>
</a-space> </a-space>
@@ -148,6 +149,10 @@ export default {
type: String, type: String,
default: '', default: '',
}, },
isInGrantComp: {
type: Boolean,
default: false,
},
}, },
data() { data() {
return { return {
@@ -178,7 +183,9 @@ export default {
async mounted() { async mounted() {
this.getCITypes() this.getCITypes()
this.getRelationTypes() this.getRelationTypes()
if (!this.isInGrantComp) {
await this.getCITypeParent() await this.getCITypeParent()
}
this.getData() this.getData()
}, },
methods: { methods: {

View File

@@ -6,6 +6,7 @@
:visible="visible" :visible="visible"
@close="handleCancel" @close="handleCancel"
@ok="handleOk" @ok="handleOk"
destroyOnClose
> >
<div class="custom-drawer-bottom-action"> <div class="custom-drawer-bottom-action">
<a-button type="primary" ghost @click="handleCancel">{{ $t('cancel') }}</a-button> <a-button type="primary" ghost @click="handleCancel">{{ $t('cancel') }}</a-button>

View File

@@ -134,6 +134,6 @@ export default {
<style lang="less" scoped> <style lang="less" scoped>
.ci-types-triggers { .ci-types-triggers {
padding: 16px 24px 24px; padding: 0 20px 20px;
} }
</style> </style>

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