Compare commits

..

11 Commits

Author SHA1 Message Date
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
57 changed files with 9023 additions and 7931 deletions

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

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,16 +278,16 @@ 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:
return int(max_v.value) + 1 return int(max_v.value) + 1
return 1 return 1
@classmethod @classmethod
def add(cls, ci_type_name, def add(cls, ci_type_name,
@@ -312,12 +312,12 @@ 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 = ci_dict.get(unique_key.name) or ci_dict.get(unique_key.alias) or ci_dict.get(unique_key.id) unique_value = None
unique_value = unique_value or abort(400, ErrFormat.unique_key_required.format(unique_key.name)) 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 = unique_value or abort(400, ErrFormat.unique_key_required.format(unique_key.name))
attrs = CITypeAttributeManager.get_all_attributes(ci_type.id) attrs = CITypeAttributeManager.get_all_attributes(ci_type.id)
ci_type_attrs_name = {attr.name: attr for _, attr in attrs} ci_type_attrs_name = {attr.name: attr for _, attr in attrs}
@@ -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,

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,36 +163,67 @@ 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'),
rid=kwargs.get('rid'),
ci_filter=None,
attr_filter=None,
first=True, to_dict=False)
obj = self.cls.get_by(type_id=kwargs.get('type_id'), for _id, v in (kwargs.get('id_filter') or {}).items():
rid=kwargs.get('rid'), key = ",".join(([v['parent_path']] if v.get('parent_path') else []) + [str(_id)])
first=True, to_dict=False) request_id_filter[key] = v['name']
if obj is not None:
obj = obj.update(filter_none=False, **kwargs)
if not obj.attr_filter and not obj.ci_filter:
if current_app.config.get('USE_ACL'):
ACLManager().del_resource(str(obj.id), ResourceTypeEnum.CI_FILTER)
obj.soft_delete() else:
obj = self.cls.get_by(type_id=kwargs.get('type_id'),
rid=kwargs.get('rid'),
id_filter=None,
first=True, to_dict=False)
return obj is_recursive = kwargs.pop('is_recursive', 0)
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)
if not obj.attr_filter and not obj.ci_filter and not obj.id_filter:
if current_app.config.get('USE_ACL'):
ACLManager().del_resource(str(obj.id), ResourceTypeEnum.CI_FILTER, rebuild=False)
obj.soft_delete()
if not is_recursive and request_id_filter:
self._revoke_children(obj.rid, request_id_filter, rebuild=False)
else:
if not kwargs.get('ci_filter') and not kwargs.get('attr_filter'):
return return
obj = self.cls.create(**kwargs) else:
if not kwargs.get('ci_filter') and not kwargs.get('attr_filter') and not kwargs.get('id_filter'):
return
if current_app.config.get('USE_ACL'): if request_id_filter:
try: kwargs['id_filter'] = request_id_filter
ACLManager().add_resource(obj.id, ResourceTypeEnum.CI_FILTER)
except:
pass
ACLManager().grant_resource_to_role_by_rid(obj.id,
kwargs.get('rid'),
ResourceTypeEnum.CI_FILTER)
return obj obj = self.cls.create(**kwargs)
if current_app.config.get('USE_ACL'): # new resource
try:
ACLManager().add_resource(obj.id, ResourceTypeEnum.CI_FILTER)
except:
pass
ACLManager().grant_resource_to_role_by_rid(obj.id,
kwargs.get('rid'),
ResourceTypeEnum.CI_FILTER)
return obj
def _can_update(self, **kwargs): def _can_update(self, **kwargs):
pass pass
@@ -140,19 +232,84 @@ class CIFilterPermsCRUD(DBMixin):
pass pass
def delete(self, **kwargs): def delete(self, **kwargs):
obj = self.cls.get_by(type_id=kwargs.get('type_id'), with redis_lock.Lock(rd.r, 'CMDB_FILTER_{}_{}'.format(kwargs['type_id'], kwargs['rid'])):
rid=kwargs.get('rid'), obj = self.cls.get_by(type_id=kwargs.get('type_id'),
first=True, to_dict=False) rid=kwargs.get('rid'),
id_filter=None,
first=True, to_dict=False)
if obj is not None:
resource = None
if current_app.config.get('USE_ACL'):
resource = ACLManager().del_resource(str(obj.id), ResourceTypeEnum.CI_FILTER)
obj.soft_delete()
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']
if obj is not None:
resource = None resource = None
if current_app.config.get('USE_ACL'): if obj is not None:
resource = ACLManager().del_resource(str(obj.id), ResourceTypeEnum.CI_FILTER)
obj.soft_delete() 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 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

@@ -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=True):
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: else:
key = ["{},{}".format(self.ancestor_ids, _id) for _id in ids]
if not self.ancestor_ids: if not self.ancestor_ids:
key, prefix = ids, REDIS_PREFIX_CI_RELATION key, prefix = [str(i) for i in ids], REDIS_PREFIX_CI_RELATION
else: else:
key = ["{},{}".format(self.ancestor_ids, _id) for _id in ids]
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,15 +301,22 @@ 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 = []

View File

@@ -302,9 +302,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

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

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

@@ -569,6 +569,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,21 +49,20 @@ 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 )
) d_acl_rid = old_department.acl_rid if old_d_rid_in_acl == old_department.acl_rid else 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 = {
payload = { 'app_id': 'acl',
'app_id': 'acl', 'parent_id': d_acl_rid,
'parent_id': d_acl_rid, }
} try:
try: acl.remove_user_from_role(employee_acl_rid, payload)
acl.remove_user_from_role(employee_acl_rid, payload) except Exception as e:
except Exception as e: result.append(ErrFormat.acl_remove_user_from_role_failed.format(str(e)))
result.append(ErrFormat.acl_remove_user_from_role_failed.format(str(e)))
payload = { payload = {
'app_id': 'acl', 'app_id': 'acl',

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))
acl.grant_resource_to_role_by_rid(type_name, rid, ResourceTypeEnum.CI_TYPE, perms, rebuild=False) 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)
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

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

View File

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

View File

@@ -1,332 +1,346 @@
<template> <template>
<div> <div>
<a-space :style="{ display: 'flex', marginBottom: '10px' }" v-for="(item, index) in ruleList" :key="item.id"> <a-space :style="{ display: 'flex', marginBottom: '10px' }" v-for="(item, index) in ruleList" :key="item.id">
<div :style="{ width: '70px', height: '24px', position: 'relative' }"> <div :style="{ width: '70px', height: '24px', position: 'relative' }">
<treeselect <treeselect
v-if="index" v-if="index"
class="custom-treeselect" class="custom-treeselect"
:style="{ width: '70px', '--custom-height': '24px', position: 'absolute', top: '-17px', left: 0 }" :style="{ width: '70px', '--custom-height': '24px', position: 'absolute', top: '-17px', left: 0 }"
v-model="item.type" v-model="item.type"
:multiple="false" :multiple="false"
:clearable="false" :clearable="false"
searchable searchable
:options="ruleTypeList" :options="ruleTypeList"
:normalizer=" :normalizer="
(node) => { (node) => {
return { return {
id: node.value, id: node.value,
label: node.label, label: node.label,
children: node.children, children: node.children,
} }
} }
" "
> :disabled="disabled"
</treeselect> >
</div> </treeselect>
<treeselect </div>
class="custom-treeselect" <treeselect
:style="{ width: '130px', '--custom-height': '24px' }" class="custom-treeselect"
v-model="item.property" :style="{ width: '130px', '--custom-height': '24px' }"
:multiple="false" v-model="item.property"
:clearable="false" :multiple="false"
searchable :clearable="false"
:options="canSearchPreferenceAttrList" searchable
:normalizer=" :options="canSearchPreferenceAttrList"
(node) => { :normalizer="
return { (node) => {
id: node.name, return {
label: node.alias || node.name, id: node.name,
children: node.children, label: node.alias || node.name,
} children: node.children,
} }
" }
appendToBody "
:zIndex="1050" appendToBody
> :zIndex="1050"
<div :disabled="disabled"
:title="node.label" >
slot="option-label" <div
slot-scope="{ node }" :title="node.label"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }" slot="option-label"
> slot-scope="{ node }"
<ValueTypeMapIcon :attr="node.raw" /> :style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
{{ node.label }} >
</div> <ValueTypeMapIcon :attr="node.raw" />
<div {{ node.label }}
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }" </div>
slot="value-label" <div
slot-scope="{ node }" :style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
> slot="value-label"
<ValueTypeMapIcon :attr="node.raw" /> {{ node.label }} slot-scope="{ node }"
</div> >
</treeselect> <ValueTypeMapIcon :attr="node.raw" /> {{ node.label }}
<treeselect </div>
class="custom-treeselect" </treeselect>
:style="{ width: '100px', '--custom-height': '24px' }" <treeselect
v-model="item.exp" class="custom-treeselect"
:multiple="false" :style="{ width: '100px', '--custom-height': '24px' }"
:clearable="false" v-model="item.exp"
searchable :multiple="false"
:options="[...getExpListByProperty(item.property), ...advancedExpList]" :clearable="false"
:normalizer=" searchable
(node) => { :options="[...getExpListByProperty(item.property), ...advancedExpList]"
return { :normalizer="
id: node.value, (node) => {
label: node.label, return {
children: node.children, id: node.value,
} label: node.label,
} children: node.children,
" }
@select="(value) => handleChangeExp(value, item, index)" }
appendToBody "
:zIndex="1050" @select="(value) => handleChangeExp(value, item, index)"
> appendToBody
</treeselect> :zIndex="1050"
<treeselect :disabled="disabled"
class="custom-treeselect" >
:style="{ width: '175px', '--custom-height': '24px' }" </treeselect>
v-model="item.value" <treeselect
:multiple="false" class="custom-treeselect"
:clearable="false" :style="{ width: '175px', '--custom-height': '24px' }"
searchable v-model="item.value"
v-if="isChoiceByProperty(item.property) && (item.exp === 'is' || item.exp === '~is')" :multiple="false"
:options="getChoiceValueByProperty(item.property)" :clearable="false"
:placeholder="$t('placeholder2')" searchable
:normalizer=" v-if="isChoiceByProperty(item.property) && (item.exp === 'is' || item.exp === '~is')"
(node) => { :options="getChoiceValueByProperty(item.property)"
return { :placeholder="$t('placeholder2')"
id: node[0], :normalizer="
label: node[0], (node) => {
children: node.children, return {
} id: node[0],
} label: node[0],
" children: node.children,
appendToBody }
:zIndex="1050" }
> "
<div appendToBody
:title="node.label" :zIndex="1050"
slot="option-label" :disabled="disabled"
slot-scope="{ node }" >
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }" <div
> :title="node.label"
{{ node.label }} slot="option-label"
</div> slot-scope="{ node }"
</treeselect> :style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
<a-input-group >
size="small" {{ node.label }}
compact </div>
v-else-if="item.exp === 'range' || item.exp === '~range'" </treeselect>
:style="{ width: '175px' }" <a-input-group
> size="small"
<a-input compact
class="ops-input" v-else-if="item.exp === 'range' || item.exp === '~range'"
size="small" :style="{ width: '175px' }"
v-model="item.min" >
:style="{ width: '78px' }" <a-input
:placeholder="$t('min')" class="ops-input"
/> size="small"
~ v-model="item.min"
<a-input :style="{ width: '78px' }"
class="ops-input" :placeholder="$t('min')"
size="small" :disabled="disabled"
v-model="item.max" />
:style="{ width: '78px' }" ~
:placeholder="$t('max')" <a-input
/> class="ops-input"
</a-input-group> size="small"
<a-input-group size="small" compact v-else-if="item.exp === 'compare'" :style="{ width: '175px' }"> v-model="item.max"
<treeselect :style="{ width: '78px' }"
class="custom-treeselect" :placeholder="$t('max')"
:style="{ width: '60px', '--custom-height': '24px' }" :disabled="disabled"
v-model="item.compareType" />
:multiple="false" </a-input-group>
:clearable="false" <a-input-group size="small" compact v-else-if="item.exp === 'compare'" :style="{ width: '175px' }">
searchable <treeselect
:options="compareTypeList" class="custom-treeselect"
:normalizer=" :style="{ width: '60px', '--custom-height': '24px' }"
(node) => { v-model="item.compareType"
return { :multiple="false"
id: node.value, :clearable="false"
label: node.label, searchable
children: node.children, :options="compareTypeList"
} :normalizer="
} (node) => {
" return {
appendToBody id: node.value,
:zIndex="1050" label: node.label,
> children: node.children,
</treeselect> }
<a-input class="ops-input" v-model="item.value" size="small" style="width: 113px" /> }
</a-input-group> "
<a-input appendToBody
v-else-if="item.exp !== 'value' && item.exp !== '~value'" :zIndex="1050"
size="small" :disabled="disabled"
v-model="item.value" >
:placeholder="item.exp === 'in' || item.exp === '~in' ? $t('cmdbFilterComp.split', { separator: ';' }) : ''" </treeselect>
class="ops-input" <a-input class="ops-input" v-model="item.value" size="small" style="width: 113px" />
:style="{ width: '175px' }" </a-input-group>
></a-input> <a-input
<div v-else :style="{ width: '175px' }"></div> v-else-if="item.exp !== 'value' && item.exp !== '~value'"
<a-tooltip :title="$t('copy')"> size="small"
<a class="operation" @click="handleCopyRule(item)"><ops-icon type="icon-xianxing-copy"/></a> v-model="item.value"
</a-tooltip> :placeholder="item.exp === 'in' || item.exp === '~in' ? $t('cmdbFilterComp.split', { separator: ';' }) : ''"
<a-tooltip :title="$t('delete')"> class="ops-input"
<a class="operation" @click="handleDeleteRule(item)"><ops-icon type="icon-xianxing-delete"/></a> :style="{ width: '175px' }"
</a-tooltip> :disabled="disabled"
<a-tooltip :title="$t('cmdbFilterComp.addHere')" v-if="needAddHere"> ></a-input>
<a class="operation" @click="handleAddRuleAt(item)"><a-icon type="plus-circle"/></a> <div v-else :style="{ width: '175px' }"></div>
</a-tooltip> <template v-if="!disabled">
</a-space> <a-tooltip :title="$t('copy')">
<div class="table-filter-add"> <a class="operation" @click="handleCopyRule(item)"><ops-icon type="icon-xianxing-copy"/></a>
<a @click="handleAddRule">+ {{ $t('new') }}</a> </a-tooltip>
</div> <a-tooltip :title="$t('delete')">
</div> <a class="operation" @click="handleDeleteRule(item)"><ops-icon type="icon-xianxing-delete"/></a>
</template> </a-tooltip>
<a-tooltip :title="$t('cmdbFilterComp.addHere')" v-if="needAddHere">
<script> <a class="operation" @click="handleAddRuleAt(item)"><a-icon type="plus-circle"/></a>
import _ from 'lodash' </a-tooltip>
import { v4 as uuidv4 } from 'uuid' </template>
import { ruleTypeList, expList, advancedExpList, compareTypeList } from './constants' </a-space>
import ValueTypeMapIcon from '../CMDBValueTypeMapIcon' <div class="table-filter-add" v-if="!disabled">
<a @click="handleAddRule">+ {{ $t('new') }}</a>
export default { </div>
name: 'Expression', </div>
components: { ValueTypeMapIcon }, </template>
model: {
prop: 'value', <script>
event: 'change', import _ from 'lodash'
}, import { v4 as uuidv4 } from 'uuid'
props: { import { ruleTypeList, expList, advancedExpList, compareTypeList } from './constants'
value: { import ValueTypeMapIcon from '../CMDBValueTypeMapIcon'
type: Array,
default: () => [], export default {
}, name: 'Expression',
canSearchPreferenceAttrList: { components: { ValueTypeMapIcon },
type: Array, model: {
required: true, prop: 'value',
default: () => [], event: 'change',
}, },
needAddHere: { props: {
type: Boolean, value: {
default: false, type: Array,
}, default: () => [],
}, },
data() { canSearchPreferenceAttrList: {
return { type: Array,
compareTypeList, required: true,
} default: () => [],
}, },
computed: { needAddHere: {
ruleList: { type: Boolean,
get() { default: false,
return this.value },
}, disabled: {
set(val) { type: Boolean,
this.$emit('change', val) default: false,
return val },
}, },
}, data() {
ruleTypeList() { return {
return ruleTypeList() compareTypeList,
}, }
expList() { },
return expList() computed: {
}, ruleList: {
advancedExpList() { get() {
return advancedExpList() return this.value
}, },
}, set(val) {
methods: { this.$emit('change', val)
getExpListByProperty(property) { return val
if (property) { },
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property) },
if (_find && ['0', '1', '3', '4', '5'].includes(_find.value_type)) { ruleTypeList() {
return [ return ruleTypeList()
{ value: 'is', label: this.$t('cmdbFilterComp.is') }, },
{ value: '~is', label: this.$t('cmdbFilterComp.~is') }, expList() {
{ value: '~value', label: this.$t('cmdbFilterComp.~value') }, // 为空的定义有点绕 return expList()
{ value: 'value', label: this.$t('cmdbFilterComp.value') }, },
] advancedExpList() {
} return advancedExpList()
return this.expList },
} },
return this.expList methods: {
}, getExpListByProperty(property) {
isChoiceByProperty(property) { if (property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property) const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find) { if (_find && ['0', '1', '3', '4', '5'].includes(_find.value_type)) {
return _find.is_choice return [
} { value: 'is', label: this.$t('cmdbFilterComp.is') },
return false { value: '~is', label: this.$t('cmdbFilterComp.~is') },
}, { value: '~value', label: this.$t('cmdbFilterComp.~value') }, // 为空的定义有点绕
handleAddRule() { { value: 'value', label: this.$t('cmdbFilterComp.value') },
this.ruleList.push({ ]
id: uuidv4(), }
type: 'and', return this.expList
property: this.canSearchPreferenceAttrList[0]?.name, }
exp: 'is', return this.expList
value: null, },
}) isChoiceByProperty(property) {
this.$emit('change', this.ruleList) const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
}, if (_find) {
handleCopyRule(item) { return _find.is_choice
this.ruleList.push({ ...item, id: uuidv4() }) }
this.$emit('change', this.ruleList) return false
}, },
handleDeleteRule(item) { handleAddRule() {
const idx = this.ruleList.findIndex((r) => r.id === item.id) this.ruleList.push({
if (idx > -1) { id: uuidv4(),
this.ruleList.splice(idx, 1) type: 'and',
} property: this.canSearchPreferenceAttrList[0]?.name,
this.$emit('change', this.ruleList) exp: 'is',
}, value: null,
handleAddRuleAt(item) { })
const idx = this.ruleList.findIndex((r) => r.id === item.id) this.$emit('change', this.ruleList)
if (idx > -1) { },
this.ruleList.splice(idx, 0, { handleCopyRule(item) {
id: uuidv4(), this.ruleList.push({ ...item, id: uuidv4() })
type: 'and', this.$emit('change', this.ruleList)
property: this.canSearchPreferenceAttrList[0]?.name, },
exp: 'is', handleDeleteRule(item) {
value: null, const idx = this.ruleList.findIndex((r) => r.id === item.id)
}) if (idx > -1) {
} this.ruleList.splice(idx, 1)
this.$emit('change', this.ruleList) }
}, this.$emit('change', this.ruleList)
getChoiceValueByProperty(property) { },
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property) handleAddRuleAt(item) {
if (_find) { const idx = this.ruleList.findIndex((r) => r.id === item.id)
return _find.choice_value if (idx > -1) {
} this.ruleList.splice(idx, 0, {
return [] id: uuidv4(),
}, type: 'and',
handleChangeExp({ value }, item, index) { property: this.canSearchPreferenceAttrList[0]?.name,
const _ruleList = _.cloneDeep(this.ruleList) exp: 'is',
if (value === 'range') { value: null,
_ruleList[index] = { })
..._ruleList[index], }
min: '', this.$emit('change', this.ruleList)
max: '', },
exp: value, getChoiceValueByProperty(property) {
} const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
} else if (value === 'compare') { if (_find) {
_ruleList[index] = { return _find.choice_value
..._ruleList[index], }
compareType: '1', return []
exp: value, },
} handleChangeExp({ value }, item, index) {
} else { const _ruleList = _.cloneDeep(this.ruleList)
_ruleList[index] = { if (value === 'range') {
..._ruleList[index], _ruleList[index] = {
exp: value, ..._ruleList[index],
} min: '',
} max: '',
this.ruleList = _ruleList exp: value,
this.$emit('change', this.ruleList) }
}, } else if (value === 'compare') {
}, _ruleList[index] = {
} ..._ruleList[index],
</script> compareType: '1',
exp: value,
<style></style> }
} else {
_ruleList[index] = {
..._ruleList[index],
exp: value,
}
}
this.ruleList = _ruleList
this.$emit('change', this.ruleList)
},
},
}
</script>
<style></style>

View File

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

View File

@@ -1,179 +1,183 @@
<template> <template>
<div ref="splitPane" class="split-pane" :class="direction + ' ' + appName" :style="{ flexDirection: direction }"> <div ref="splitPane" class="split-pane" :class="direction + ' ' + appName" :style="{ flexDirection: direction }">
<div class="pane pane-one" ref="one" :style="lengthType + ':' + paneLengthValue1"> <div class="pane pane-one" ref="one" :style="lengthType + ':' + paneLengthValue1">
<slot name="one"></slot> <slot name="one"></slot>
</div> </div>
<div class="spliter-wrap"> <div class="spliter-wrap">
<a-button <a-button
v-show="collapsable" v-show="collapsable"
:icon="isExpanded ? 'left' : 'right'" :icon="isExpanded ? 'left' : 'right'"
class="collapse-btn" class="collapse-btn"
@click="handleExpand" @click="handleExpand"
></a-button> ></a-button>
<div <div
class="pane-trigger" class="pane-trigger"
@mousedown="handleMouseDown" @mousedown="handleMouseDown"
:style="{ backgroundColor: triggerColor, width: `${triggerLength}px` }" :style="{ backgroundColor: triggerColor, width: `${triggerLength}px` }"
></div> ></div>
</div> </div>
<div class="pane pane-two" ref="two" :style="lengthType + ':' + paneLengthValue2"> <div class="pane pane-two" ref="two" :style="lengthType + ':' + paneLengthValue2">
<slot name="two"></slot> <slot name="two"></slot>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
name: 'SplitPane', name: 'SplitPane',
props: { props: {
direction: { direction: {
type: String, type: String,
default: 'row', default: 'row',
}, },
min: { min: {
type: Number, type: Number,
default: 10, default: 10,
}, },
max: { max: {
type: Number, type: Number,
default: 90, default: 90,
}, },
paneLengthPixel: { paneLengthPixel: {
type: Number, type: Number,
default: 220, default: 220,
}, },
triggerLength: { triggerLength: {
type: Number, type: Number,
default: 8, default: 8,
}, },
appName: { appName: {
type: String, type: String,
default: 'viewer', default: 'viewer',
}, },
collapsable: { collapsable: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
triggerColor: { triggerColor: {
type: String, type: String,
default: '#f0f2f5', default: '#f0f2f5',
}, },
}, },
data() { data() {
return { return {
triggerLeftOffset: 0, // 鼠标距滑动器左()侧偏移量 triggerLeftOffset: 0, // 鼠标距滑动器左()侧偏移量
isExpanded: localStorage.getItem(`${this.appName}-isExpanded`) isExpanded: localStorage.getItem(`${this.appName}-isExpanded`)
? JSON.parse(localStorage.getItem(`${this.appName}-isExpanded`)) ? JSON.parse(localStorage.getItem(`${this.appName}-isExpanded`))
: false, : false,
parentContainer: null, parentContainer: null,
} }
}, },
computed: { computed: {
lengthType() { lengthType() {
return this.direction === 'row' ? 'width' : 'height' return this.direction === 'row' ? 'width' : 'height'
}, },
minLengthType() { minLengthType() {
return this.direction === 'row' ? 'minWidth' : 'minHeight' return this.direction === 'row' ? 'minWidth' : 'minHeight'
}, },
paneLengthValue1() { paneLengthValue1() {
return `calc(${this.paneLengthPercent}% - ${this.triggerLength / 2 + 'px'})` return `calc(${this.paneLengthPercent}% - ${this.triggerLength / 2 + 'px'})`
}, },
paneLengthValue2() { paneLengthValue2() {
const rest = 100 - this.paneLengthPercent const rest = 100 - this.paneLengthPercent
return `calc(${rest}% - ${this.triggerLength / 2 + 'px'})` return `calc(${rest}% - ${this.triggerLength / 2 + 'px'})`
}, },
paneLengthPercent() { paneLengthPercent() {
const clientRectWidth = this.parentContainer const clientRectWidth = this.parentContainer
? this.parentContainer.clientWidth ? this.parentContainer.clientWidth
: document.documentElement.getBoundingClientRect().width : document.documentElement.getBoundingClientRect().width
return (this.paneLengthPixel / clientRectWidth) * 100 return (this.paneLengthPixel / clientRectWidth) * 100
}, },
}, },
watch: { watch: {
isExpanded(newValue) { isExpanded(newValue) {
if (newValue) { if (newValue) {
document.querySelector(`.${this.appName} .pane-two`).style.display = 'none' document.querySelector(`.${this.appName} .pane-two`).style.display = 'none'
} else { } else {
document.querySelector(`.${this.appName} .pane-two`).style.display = '' document.querySelector(`.${this.appName} .pane-two`).style.display = ''
} }
}, },
}, },
mounted() { mounted() {
this.parentContainer = document.querySelector(`.${this.appName}`) const paneLengthPixel = localStorage.getItem(`${this.appName}-paneLengthPixel`)
if (this.isExpanded) { if (paneLengthPixel) {
document.querySelector(`.${this.appName} .pane-two`).style.display = 'none' this.$emit('update:paneLengthPixel', Number(paneLengthPixel))
} else { }
document.querySelector(`.${this.appName} .pane-two`).style.display = '' this.parentContainer = document.querySelector(`.${this.appName}`)
} if (this.isExpanded) {
}, document.querySelector(`.${this.appName} .pane-two`).style.display = 'none'
} else {
methods: { document.querySelector(`.${this.appName} .pane-two`).style.display = ''
// 按下滑动器 }
handleMouseDown(e) { },
document.addEventListener('mousemove', this.handleMouseMove)
document.addEventListener('mouseup', this.handleMouseUp) methods: {
if (this.direction === 'row') { // 按下滑动器
this.triggerLeftOffset = e.pageX - e.srcElement.getBoundingClientRect().left handleMouseDown(e) {
} else { document.addEventListener('mousemove', this.handleMouseMove)
this.triggerLeftOffset = e.pageY - e.srcElement.getBoundingClientRect().top document.addEventListener('mouseup', this.handleMouseUp)
} if (this.direction === 'row') {
}, this.triggerLeftOffset = e.pageX - e.srcElement.getBoundingClientRect().left
} else {
// 按下滑动器后移动鼠标 this.triggerLeftOffset = e.pageY - e.srcElement.getBoundingClientRect().top
handleMouseMove(e) { }
this.isExpanded = false },
this.$emit('expand', this.isExpanded)
const clientRect = this.$refs.splitPane.getBoundingClientRect() // 按下滑动器后移动鼠标
let paneLengthPixel = 0 handleMouseMove(e) {
this.isExpanded = false
if (this.direction === 'row') { this.$emit('expand', this.isExpanded)
const offset = e.pageX - clientRect.left - this.triggerLeftOffset + this.triggerLength / 2 const clientRect = this.$refs.splitPane.getBoundingClientRect()
paneLengthPixel = offset let paneLengthPixel = 0
} else {
const offset = e.pageY - clientRect.top - this.triggerLeftOffset + this.triggerLength / 2 if (this.direction === 'row') {
paneLengthPixel = offset const offset = e.pageX - clientRect.left - this.triggerLeftOffset + this.triggerLength / 2
} paneLengthPixel = offset
} else {
if (paneLengthPixel < this.min) { const offset = e.pageY - clientRect.top - this.triggerLeftOffset + this.triggerLength / 2
paneLengthPixel = this.min paneLengthPixel = offset
} }
if (paneLengthPixel > this.max) {
paneLengthPixel = this.max if (paneLengthPixel < this.min) {
} paneLengthPixel = this.min
}
this.$emit('update:paneLengthPixel', paneLengthPixel) if (paneLengthPixel > this.max) {
paneLengthPixel = this.max
localStorage.setItem(`${this.appName}-paneLengthPixel`, paneLengthPixel) }
},
this.$emit('update:paneLengthPixel', paneLengthPixel)
// 松开滑动器
handleMouseUp() { localStorage.setItem(`${this.appName}-paneLengthPixel`, paneLengthPixel)
document.removeEventListener('mousemove', this.handleMouseMove) },
},
// 松开滑动器
handleExpand() { handleMouseUp() {
this.isExpanded = !this.isExpanded document.removeEventListener('mousemove', this.handleMouseMove)
this.$emit('expand', this.isExpanded) },
localStorage.setItem(`${this.appName}-isExpanded`, this.isExpanded)
}, handleExpand() {
}, this.isExpanded = !this.isExpanded
} this.$emit('expand', this.isExpanded)
</script> localStorage.setItem(`${this.appName}-isExpanded`, this.isExpanded)
},
<style scoped lang="less"> },
@import './index.less'; }
</style> </script>
<style scoped lang="less">
@import './index.less';
</style>

View File

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

View File

@@ -1,48 +1,48 @@
.split-pane { .split-pane {
height: 100%; height: 100%;
display: flex; display: flex;
} }
.split-pane .pane-two { .split-pane .pane-two {
flex: 1; flex: 1;
} }
.split-pane .pane-trigger { .split-pane .pane-trigger {
user-select: none; user-select: none;
} }
.split-pane.row .pane-one { .split-pane.row .pane-one {
width: 20%; width: 20%;
height: 100%; height: 100%;
// overflow-y: auto; // overflow-y: auto;
} }
.split-pane.column .pane { .split-pane.column .pane {
width: 100%; width: 100%;
} }
.split-pane.row .pane-trigger { .split-pane.row .pane-trigger {
width: 8px; width: 8px;
height: 100%; height: 100%;
cursor: e-resize; cursor: e-resize;
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAPCAYAAADDNm69AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAAeSURBVBhXY/4PBMzMzA379u1rANFMDGhgGAswMAAAn6EH6K9ktYAAAAAASUVORK5CYII=') background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAPCAYAAADDNm69AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAAeSURBVBhXY/4PBMzMzA379u1rANFMDGhgGAswMAAAn6EH6K9ktYAAAAAASUVORK5CYII=')
1px 50% no-repeat #f0f2f5; 1px 50% no-repeat #f0f2f5;
} }
.split-pane .collapse-btn { .split-pane .collapse-btn {
width: 25px; width: 25px;
height: 70px; height: 70px;
position: absolute; position: absolute;
right: 8px; right: 8px;
top: calc(50% - 35px); top: calc(50% - 35px);
background-color: #f0f2f5; background-color: #f0f2f5;
border-color: transparent; border-color: transparent;
border-radius: 8px 0px 0px 8px; border-radius: 8px 0px 0px 8px;
.anticon { .anticon {
color: #7cb0fe; color: #7cb0fe;
} }
} }
.split-pane .spliter-wrap { .split-pane .spliter-wrap {
position: relative; position: relative;
} }

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

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

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

View File

@@ -1,15 +1,15 @@
import i18n from '@/lang' import i18n from '@/lang'
export const permMap = () => { export const permMap = () => {
return { return {
read: i18n.t('view'), read: i18n.t('view'),
add: i18n.t('new'), add: i18n.t('new'),
create: i18n.t('new'), create: i18n.t('new'),
update: i18n.t('update'), update: i18n.t('update'),
delete: i18n.t('delete'), delete: i18n.t('delete'),
config: i18n.t('cmdb.components.config'), config: i18n.t('cmdb.components.config'),
grant: i18n.t('grant'), grant: i18n.t('grant'),
'read_attr': i18n.t('cmdb.components.readAttribute'), 'read_attr': i18n.t('cmdb.components.readAttribute'),
'read_ci': i18n.t('cmdb.components.readCI') 'read_ci': i18n.t('cmdb.components.readCI')
} }
} }

View File

@@ -1,343 +1,343 @@
<template> <template>
<div class="cmdb-grant" :style="{ maxHeight: `${windowHeight - 104}px` }"> <div class="cmdb-grant" :style="{ maxHeight: `${windowHeight - 104}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
:CITypeId="CITypeId" :CITypeId="CITypeId"
:tableData="tableData" :tableData="tableData"
grantType="ci_type" grantType="ci_type"
@grantDepart="grantDepart" @grantDepart="grantDepart"
@grantRole="grantRole" @grantRole="grantRole"
@getTableData="getTableData" @getTableData="getTableData"
ref="grant_ci_type" ref="grant_ci_type"
:addedRids="addedRids" :addedRids="addedRids"
/> />
</template> </template>
<template <template
v-if=" v-if="
cmdbGrantType.includes('ci_type,ci') || (cmdbGrantType.includes('ci') && !cmdbGrantType.includes('ci_type')) cmdbGrantType.includes('ci_type,ci') || (cmdbGrantType.includes('ci') && !cmdbGrantType.includes('ci_type'))
" "
> >
<div class="cmdb-grant-title">{{ $t('cmdb.components.ciGrant') }}</div> <div class="cmdb-grant-title">{{ $t('cmdb.components.ciGrant') }}</div>
<CiTypeGrant <CiTypeGrant
:CITypeId="CITypeId" :CITypeId="CITypeId"
:tableData="tableData" :tableData="tableData"
grantType="ci" grantType="ci"
@grantDepart="grantDepart" @grantDepart="grantDepart"
@grantRole="grantRole" @grantRole="grantRole"
@getTableData="getTableData" @getTableData="getTableData"
@openReadGrantModal="openReadGrantModal" @openReadGrantModal="openReadGrantModal"
ref="grant_ci" ref="grant_ci"
:addedRids="addedRids" :addedRids="addedRids"
/> />
</template> </template>
<template v-if="cmdbGrantType.includes('type_relation')"> <template v-if="cmdbGrantType.includes('type_relation')">
<div class="cmdb-grant-title">{{ $t('cmdb.components.relationGrant') }}</div> <div class="cmdb-grant-title">{{ $t('cmdb.components.relationGrant') }}</div>
<TypeRelationGrant <TypeRelationGrant
:typeRelationIds="typeRelationIds" :typeRelationIds="typeRelationIds"
:tableData="tableData" :tableData="tableData"
grantType="type_relation" grantType="type_relation"
@grantDepart="grantDepart" @grantDepart="grantDepart"
@grantRole="grantRole" @grantRole="grantRole"
@getTableData="getTableData" @getTableData="getTableData"
ref="grant_type_relation" ref="grant_type_relation"
:addedRids="addedRids" :addedRids="addedRids"
/> />
</template> </template>
<template v-if="cmdbGrantType.includes('relation_view')"> <template v-if="cmdbGrantType.includes('relation_view')">
<div class="cmdb-grant-title">{{ resourceTypeName }}{{ $t('cmdb.components.perm') }}</div> <div class="cmdb-grant-title">{{ resourceTypeName }}{{ $t('cmdb.components.perm') }}</div>
<RelationViewGrant <RelationViewGrant
:resourceTypeName="resourceTypeName" :resourceTypeName="resourceTypeName"
:tableData="tableData" :tableData="tableData"
grantType="relation_view" grantType="relation_view"
@grantDepart="grantDepart" @grantDepart="grantDepart"
@grantRole="grantRole" @grantRole="grantRole"
@getTableData="getTableData" @getTableData="getTableData"
ref="grant_relation_view" ref="grant_relation_view"
:addedRids="addedRids" :addedRids="addedRids"
/> />
</template> </template>
<GrantModal ref="grantModal" @handleOk="handleOk" /> <GrantModal ref="grantModal" @handleOk="handleOk" />
<ReadGrantModal ref="readGrantModal" :CITypeId="CITypeId" @updateTableDataRead="updateTableDataRead" /> <ReadGrantModal ref="readGrantModal" :CITypeId="CITypeId" @updateTableDataRead="updateTableDataRead" />
</div> </div>
</template> </template>
<script> <script>
import { mapState } from 'vuex' import { mapState } from 'vuex'
import CiTypeGrant from './ciTypeGrant.vue' import CiTypeGrant from './ciTypeGrant.vue'
import TypeRelationGrant from './typeRelationGrant.vue' import TypeRelationGrant from './typeRelationGrant.vue'
import { searchResource } from '@/modules/acl/api/resource' import { searchResource } from '@/modules/acl/api/resource'
import { getResourcePerms } from '@/modules/acl/api/permission' import { getResourcePerms } from '@/modules/acl/api/permission'
import GrantModal from './grantModal.vue' import GrantModal from './grantModal.vue'
import ReadGrantModal from './readGrantModal' import ReadGrantModal from './readGrantModal'
import RelationViewGrant from './relationViewGrant.vue' import RelationViewGrant from './relationViewGrant.vue'
import { getCITypeGroupById, ciTypeFilterPermissions } from '../../api/CIType' import { getCITypeGroupById, ciTypeFilterPermissions } from '../../api/CIType'
export default { export default {
name: 'GrantComp', name: 'GrantComp',
components: { CiTypeGrant, TypeRelationGrant, RelationViewGrant, GrantModal, ReadGrantModal }, components: { CiTypeGrant, TypeRelationGrant, RelationViewGrant, GrantModal, ReadGrantModal },
props: { props: {
CITypeId: { CITypeId: {
type: Number, type: Number,
default: null, default: null,
}, },
resourceTypeName: { resourceTypeName: {
type: String, type: String,
default: '', default: '',
}, },
resourceType: { resourceType: {
type: String, type: String,
default: 'CIType', default: 'CIType',
}, },
app_id: { app_id: {
type: String, type: String,
default: 'cmdb', default: 'cmdb',
}, },
cmdbGrantType: { cmdbGrantType: {
type: String, type: String,
default: 'ci_type,ci', default: 'ci_type,ci',
}, },
typeRelationIds: { typeRelationIds: {
type: Array, type: Array,
default: null, default: null,
}, },
isModal: { isModal: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
}, },
inject: ['resource_type'], inject: ['resource_type'],
data() { data() {
return { return {
tableData: [], tableData: [],
grantType: '', grantType: '',
resource_id: null, resource_id: null,
attrGroup: [], attrGroup: [],
filerPerimissions: {}, filerPerimissions: {},
loading: false, loading: false,
addedRids: [], // added rid this time addedRids: [], // added rid this time
} }
}, },
computed: { computed: {
...mapState({ ...mapState({
allEmployees: (state) => state.user.allEmployees, allEmployees: (state) => state.user.allEmployees,
allDepartments: (state) => state.user.allDepartments, allDepartments: (state) => state.user.allDepartments,
}), }),
child_resource_type() { child_resource_type() {
return this.resource_type() return this.resource_type()
}, },
windowHeight() { windowHeight() {
return this.$store.state.windowHeight return this.$store.state.windowHeight
}, },
}, },
provide() { provide() {
return { return {
attrGroup: () => { attrGroup: () => {
return this.attrGroup return this.attrGroup
}, },
filerPerimissions: () => { filerPerimissions: () => {
return this.filerPerimissions return this.filerPerimissions
}, },
loading: () => { loading: () => {
return this.loading return this.loading
}, },
isModal: this.isModal, isModal: this.isModal,
} }
}, },
watch: { watch: {
resourceTypeName: { resourceTypeName: {
immediate: true, immediate: true,
handler() { handler() {
this.init() this.init()
}, },
}, },
CITypeId: { CITypeId: {
immediate: true, immediate: true,
handler() { handler() {
if (this.CITypeId && this.cmdbGrantType.includes('ci')) { if (this.CITypeId && this.cmdbGrantType.includes('ci')) {
this.getFilterPermissions() this.getFilterPermissions()
this.getAttrGroup() this.getAttrGroup()
} }
}, },
}, },
}, },
mounted() {}, mounted() {},
methods: { methods: {
getAttrGroup() { getAttrGroup() {
getCITypeGroupById(this.CITypeId, { need_other: true }).then((res) => { getCITypeGroupById(this.CITypeId, { need_other: true }).then((res) => {
this.attrGroup = res this.attrGroup = res
}) })
}, },
getFilterPermissions() { getFilterPermissions() {
ciTypeFilterPermissions(this.CITypeId).then((res) => { ciTypeFilterPermissions(this.CITypeId).then((res) => {
this.filerPerimissions = res this.filerPerimissions = res
}) })
}, },
async init() { async init() {
const _find = this.child_resource_type.groups.find((item) => item.name === this.resourceType) const _find = this.child_resource_type.groups.find((item) => item.name === this.resourceType)
const resource_type_id = _find?.id ?? 0 const resource_type_id = _find?.id ?? 0
const res = await searchResource({ const res = await searchResource({
app_id: this.app_id, app_id: this.app_id,
resource_type_id, resource_type_id,
page_size: 9999, page_size: 9999,
}) })
const _tempFind = res.resources.find((item) => item.name === this.resourceTypeName) const _tempFind = res.resources.find((item) => item.name === this.resourceTypeName)
console.log(this.resourceTypeName) console.log(this.resourceTypeName)
this.resource_id = _tempFind?.id || 0 this.resource_id = _tempFind?.id || 0
this.getTableData() this.getTableData()
}, },
async getTableData() { async getTableData() {
this.loading = true this.loading = true
const _tableData = await getResourcePerms(this.resource_id, { need_users: 0 }) const _tableData = await getResourcePerms(this.resource_id, { need_users: 0 })
const perms = [] const perms = []
for (const key in _tableData) { for (const key in _tableData) {
const obj = {} const obj = {}
obj.name = key obj.name = key
_tableData[key].perms.forEach((perm) => { _tableData[key].perms.forEach((perm) => {
obj[`${perm.name}`] = true obj[`${perm.name}`] = true
obj.rid = perm?.rid ?? null obj.rid = perm?.rid ?? null
}) })
perms.push(obj) perms.push(obj)
} }
this.tableData = perms this.tableData = perms
this.loading = false this.loading = false
}, },
// Grant the department in common-setting and get the roleid from it // Grant the department in common-setting and get the roleid from it
grantDepart(grantType) { grantDepart(grantType) {
this.$refs.grantModal.open('depart') this.$refs.grantModal.open('depart')
this.grantType = grantType this.grantType = grantType
}, },
// Grant the oldest role permissions // Grant the oldest role permissions
grantRole(grantType) { grantRole(grantType) {
this.$refs.grantModal.open('role') this.$refs.grantModal.open('role')
this.grantType = grantType this.grantType = grantType
}, },
handleOk(params, type) { handleOk(params, type) {
const { grantType } = this const { grantType } = this
let rids let rids
if (type === 'depart') { if (type === 'depart') {
rids = [ rids = [
...params.department.map((rid) => { ...params.department.map((rid) => {
const _find = this.allDepartments.find((dep) => dep.acl_rid === rid) const _find = this.allDepartments.find((dep) => dep.acl_rid === rid)
return { rid, name: _find?.department_name ?? rid } return { rid, name: _find?.department_name ?? rid }
}), }),
...params.user.map((rid) => { ...params.user.map((rid) => {
const _find = this.allEmployees.find((dep) => dep.acl_rid === rid) const _find = this.allEmployees.find((dep) => dep.acl_rid === rid)
return { rid, name: _find?.nickname ?? rid } return { rid, name: _find?.nickname ?? rid }
}), }),
] ]
} }
if (type === 'role') { if (type === 'role') {
rids = [ rids = [
...params.map((role) => { ...params.map((role) => {
return { rid: role.id, name: role.name } return { rid: role.id, name: role.name }
}), }),
] ]
} }
if (grantType === 'ci_type') { if (grantType === 'ci_type') {
this.tableData.unshift( this.tableData.unshift(
...rids.map(({ rid, name }) => { ...rids.map(({ rid, name }) => {
const _find = this.tableData.find((item) => item.rid === rid) const _find = this.tableData.find((item) => item.rid === rid)
return { return {
rid, rid,
name, name,
conifg: false, conifg: false,
grant: false, grant: false,
..._find, ..._find,
} }
}) })
) )
} }
if (grantType === 'ci') { if (grantType === 'ci') {
this.tableData.unshift( this.tableData.unshift(
...rids.map(({ rid, name }) => { ...rids.map(({ rid, name }) => {
const _find = this.tableData.find((item) => item.rid === rid) const _find = this.tableData.find((item) => item.rid === rid)
return { return {
rid, rid,
name, name,
read_attr: false, read_attr: false,
read_ci: false, read_ci: false,
create: false, create: false,
update: false, update: false,
delete: false, delete: false,
..._find, ..._find,
} }
}) })
) )
} }
if (grantType === 'type_relation') { if (grantType === 'type_relation') {
this.tableData.unshift( this.tableData.unshift(
...rids.map(({ rid, name }) => { ...rids.map(({ rid, name }) => {
return { return {
rid, rid,
name, name,
create: false, create: false,
grant: false, grant: false,
delete: false, delete: false,
} }
}) })
) )
} }
if (grantType === 'relation_view') { if (grantType === 'relation_view') {
this.tableData.unshift( this.tableData.unshift(
...rids.map(({ rid, name }) => { ...rids.map(({ rid, name }) => {
return { return {
rid, rid,
name, name,
read: false, read: false,
grant: false, grant: false,
} }
}) })
) )
} }
this.addedRids = rids this.addedRids = rids
this.$nextTick(() => { this.$nextTick(() => {
setTimeout(() => { setTimeout(() => {
this.$refs[`grant_${grantType}`].$refs.xTable.elemStore['main-body-wrapper'].scrollTo(0, 0) this.$refs[`grant_${grantType}`].$refs.xTable.elemStore['main-body-wrapper'].scrollTo(0, 0)
}, 300) }, 300)
}) })
}, },
openReadGrantModal(col, row) { openReadGrantModal(col, row) {
this.$refs.readGrantModal.open(col, row) this.$refs.readGrantModal.open(col, row)
}, },
updateTableDataRead(row, hasRead) { updateTableDataRead(row, hasRead) {
const _idx = this.tableData.findIndex((item) => item.rid === row.rid) const _idx = this.tableData.findIndex((item) => item.rid === row.rid)
this.$set(this.tableData, _idx, { ...this.tableData[_idx], read: hasRead }) this.$set(this.tableData, _idx, { ...this.tableData[_idx], read: hasRead })
this.getFilterPermissions() this.getFilterPermissions()
}, },
}, },
} }
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@import '~@/style/static.less'; @import '~@/style/static.less';
.cmdb-grant { .cmdb-grant {
position: relative; position: relative;
padding: 24px 24px 0 24px; padding: 24px 24px 0 24px;
overflow: auto; overflow: auto;
.cmdb-grant-title { .cmdb-grant-title {
border-left: 4px solid #custom_colors[color_1]; border-left: 4px solid #custom_colors[color_1];
padding-left: 10px; padding-left: 10px;
} }
} }
</style> </style>
<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: #custom_colors[color_1];
background-color: #custom_colors[color_2]; background-color: #custom_colors[color_2];
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 #custom_colors[color_2];
} }
} }
} }
</style> </style>

View File

@@ -1,57 +1,67 @@
<template> <template>
<a-modal :title="title" :visible="visible" @ok="handleOk" @cancel="handleCancel" destroyOnClose> <a-modal :title="title" :visible="visible" @ok="handleOk" @cancel="handleCancel" destroyOnClose>
<EmployeeTransfer <EmployeeTransfer
:isDisabledAllCompany="true" :isDisabledAllCompany="true"
v-if="type === 'depart'" v-if="type === 'depart'"
uniqueKey="acl_rid" uniqueKey="acl_rid"
ref="employeeTransfer" ref="employeeTransfer"
:height="350" :height="350"
/> />
<RoleTransfer app_id="cmdb" :height="350" ref="roleTransfer" v-if="type === 'role'" /> <RoleTransfer app_id="cmdb" :height="350" ref="roleTransfer" v-if="type === 'role'" />
</a-modal> </a-modal>
</template> </template>
<script> <script>
import EmployeeTransfer from '@/components/EmployeeTransfer' import EmployeeTransfer from '@/components/EmployeeTransfer'
import RoleTransfer from '@/components/RoleTransfer' import RoleTransfer from '@/components/RoleTransfer'
export default {
name: 'GrantModal', export default {
components: { EmployeeTransfer, RoleTransfer }, name: 'GrantModal',
data() { components: { EmployeeTransfer, RoleTransfer },
return { props: {
visible: false, customTitle: {
type: 'depart', type: String,
} default: '',
}, },
computed: { },
title() { data() {
if (this.type === 'depart') { return {
return this.$t('cmdb.components.grantUser') visible: false,
} type: 'depart',
return this.$t('cmdb.components.grantRole') }
}, },
}, computed: {
methods: { title() {
open(type) { if (this.customTitle) {
this.visible = true return this.customTitle
this.type = type }
}, if (this.type === 'depart') {
handleOk() { return this.$t('cmdb.components.grantUser')
let params }
if (this.type === 'depart') { return this.$t('cmdb.components.grantRole')
params = this.$refs.employeeTransfer.getValues() },
} },
if (this.type === 'role') { methods: {
params = this.$refs.roleTransfer.getValues() open(type) {
} this.visible = true
this.handleCancel() this.type = type
this.$emit('handleOk', params, this.type) },
}, handleOk() {
handleCancel() { let params
this.visible = false if (this.type === 'depart') {
}, params = this.$refs.employeeTransfer.getValues()
}, }
} if (this.type === 'role') {
</script> params = this.$refs.roleTransfer.getValues()
}
<style></style> this.handleCancel()
this.$emit('handleOk', params, this.type)
},
handleCancel() {
this.visible = false
},
},
}
</script>
<style></style>

View File

@@ -1,57 +1,57 @@
<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 }">
<GrantComp <GrantComp
:resourceType="resourceType" :resourceType="resourceType"
:app_id="app_id" :app_id="app_id"
:cmdbGrantType="cmdbGrantType" :cmdbGrantType="cmdbGrantType"
:resourceTypeName="resourceTypeName" :resourceTypeName="resourceTypeName"
:typeRelationIds="typeRelationIds" :typeRelationIds="typeRelationIds"
:CITypeId="CITypeId" :CITypeId="CITypeId"
:isModal="true" :isModal="true"
/> />
</a-modal> </a-modal>
</template> </template>
<script> <script>
import GrantComp from './grantComp.vue' import GrantComp from './grantComp.vue'
export default { export default {
name: 'CMDBGrant', name: 'CMDBGrant',
components: { GrantComp }, components: { GrantComp },
props: { props: {
resourceType: { resourceType: {
type: String, type: String,
default: 'CIType', default: 'CIType',
}, },
app_id: { app_id: {
type: String, type: String,
default: '', default: '',
}, },
}, },
data() { data() {
return { return {
visible: false, visible: false,
resourceTypeName: '', resourceTypeName: '',
typeRelationIds: [], typeRelationIds: [],
cmdbGrantType: '', cmdbGrantType: '',
CITypeId: null, CITypeId: null,
} }
}, },
methods: { methods: {
open({ name, typeRelationIds = [], cmdbGrantType, CITypeId }) { open({ name, typeRelationIds = [], cmdbGrantType, CITypeId }) {
this.visible = true this.visible = true
this.resourceTypeName = name this.resourceTypeName = name
this.typeRelationIds = typeRelationIds this.typeRelationIds = typeRelationIds
this.cmdbGrantType = cmdbGrantType this.cmdbGrantType = cmdbGrantType
this.CITypeId = CITypeId this.CITypeId = CITypeId
}, },
handleOk() { handleOk() {
this.handleCancel() this.handleCancel()
}, },
handleCancel() { handleCancel() {
this.visible = false this.visible = false
}, },
}, },
} }
</script> </script>
<style></style> <style></style>

View File

@@ -1,89 +1,89 @@
<template> <template>
<div :class="{ 'read-checkbox': true, 'ant-checkbox-wrapper': isHalfChecked }" @click="openReadGrantModal"> <div :class="{ 'read-checkbox': true, 'ant-checkbox-wrapper': isHalfChecked }" @click="openReadGrantModal">
<a-tooltip <a-tooltip
v-if="value && isHalfChecked" v-if="value && isHalfChecked"
:title="valueKey === 'read_ci' ? filerPerimissions[this.rid].name || '' : ''" :title="valueKey === 'read_ci' ? filerPerimissions[this.rid].name || '' : ''"
> >
<div v-if="value && isHalfChecked" :class="{ 'read-checkbox-half-checked': true, 'ant-checkbox': true }"></div> <div v-if="value && isHalfChecked" :class="{ 'read-checkbox-half-checked': true, 'ant-checkbox': true }"></div>
</a-tooltip> </a-tooltip>
<a-checkbox v-else :checked="value" /> <a-checkbox v-else :checked="value" />
</div> </div>
</template> </template>
<script> <script>
export default { export default {
name: 'ReadCheckbox', name: 'ReadCheckbox',
inject: { inject: {
provide_filerPerimissions: { provide_filerPerimissions: {
from: 'filerPerimissions', from: 'filerPerimissions',
}, },
}, },
props: { props: {
value: { value: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
valueKey: { valueKey: {
type: String, type: String,
default: 'read_attr', default: 'read_attr',
}, },
rid: { rid: {
type: Number, type: Number,
default: 0, default: 0,
}, },
}, },
computed: { computed: {
filerPerimissions() { filerPerimissions() {
return this.provide_filerPerimissions() return this.provide_filerPerimissions()
}, },
filterKey() { filterKey() {
if (this.valueKey === 'read_attr') { if (this.valueKey === 'read_attr') {
return 'attr_filter' return 'attr_filter'
} }
return 'ci_filter' return 'ci_filter'
}, },
isHalfChecked() { isHalfChecked() {
if (this.filerPerimissions[this.rid]) { if (this.filerPerimissions[this.rid]) {
const _tempValue = this.filerPerimissions[this.rid][this.filterKey] const _tempValue = this.filerPerimissions[this.rid][this.filterKey]
return !!(_tempValue && _tempValue.length) return !!(_tempValue && _tempValue.length)
} }
return false return false
}, },
}, },
methods: { methods: {
openReadGrantModal() { openReadGrantModal() {
this.$emit('openReadGrantModal') this.$emit('openReadGrantModal')
}, },
}, },
} }
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@import '~@/style/static.less'; @import '~@/style/static.less';
.read-checkbox { .read-checkbox {
.read-checkbox-half-checked { .read-checkbox-half-checked {
width: 16px; width: 16px;
height: 16px; height: 16px;
border: 1px solid #d9d9d9; border: 1px solid #d9d9d9;
border-radius: 2px; border-radius: 2px;
cursor: pointer; cursor: pointer;
margin: 0; margin: 0;
padding: 0; padding: 0;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
&::after { &::after {
content: ''; content: '';
position: absolute; position: absolute;
width: 0; width: 0;
height: 0; height: 0;
// background-color: #custom_colors[color_1]; // background-color: #custom_colors[color_1];
border-radius: 2px; border-radius: 2px;
border: 14px solid transparent; border: 14px solid transparent;
border-left-color: #custom_colors[color_1]; border-left-color: #custom_colors[color_1];
transform: rotate(225deg); transform: rotate(225deg);
top: -16px; top: -16px;
left: -17px; left: -17px;
} }
} }
} }
</style> </style>

View File

@@ -1,205 +1,212 @@
<template> <template>
<a-modal :width="680" :title="title" :visible="visible" @ok="handleOk" @cancel="handleCancel"> <a-modal :width="680" :title="title" :visible="visible" @ok="handleOk" @cancel="handleCancel">
<CustomRadio <CustomRadio
:radioList="[ :radioList="[
{ value: 1, label: $t('cmdb.components.all') }, { value: 1, label: $t('cmdb.components.all') },
{ 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"> >
<treeselect <template slot="extra_2" v-if="radioValue === 2">
v-if="colType === 'read_attr'" <treeselect
v-model="selectedAttr" v-if="colType === 'read_attr'"
:multiple="true" v-model="selectedAttr"
:clearable="true" :multiple="true"
searchable :clearable="true"
:options="attrGroup" searchable
:placeholder="$t('cmdb.ciType.selectAttributes')" :options="attrGroup"
value-consists-of="LEAF_PRIORITY" :placeholder="$t('cmdb.ciType.selectAttributes')"
:limit="10" value-consists-of="LEAF_PRIORITY"
:limitText="(count) => `+ ${count}`" :limit="10"
:normalizer=" :limitText="(count) => `+ ${count}`"
(node) => { :normalizer="
return { (node) => {
id: node.name || -1, return {
label: node.alias || node.name || $t('other'), id: node.name || -1,
title: node.alias || node.name || $t('other'), label: node.alias || node.name || $t('other'),
children: node.attributes, title: node.alias || node.name || $t('other'),
} children: node.attributes,
} }
" }
appendToBody "
zIndex="1050" appendToBody
> zIndex="1050"
</treeselect> >
<a-form-model </treeselect>
:model="form" <a-form-model
:rules="rules" :model="form"
v-if="colType === 'read_ci'" :rules="rules"
:labelCol="{ span: 2 }" v-if="colType === 'read_ci'"
:wrapperCol="{ span: 10 }" :labelCol="{ span: 2 }"
ref="form" :wrapperCol="{ span: 10 }"
> ref="form"
<a-form-model-item :label="$t('name')" prop="name"> >
<a-input v-model="form.name" /> <a-form-model-item :label="$t('name')" prop="name">
</a-form-model-item> <a-input v-model="form.name" />
<FilterComp </a-form-model-item>
ref="filterComp" <FilterComp
:isDropdown="false" ref="filterComp"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList" :isDropdown="false"
@setExpFromFilter="setExpFromFilter" :canSearchPreferenceAttrList="canSearchPreferenceAttrList"
:expression="expression" @setExpFromFilter="setExpFromFilter"
/> :expression="expression"
</a-form-model> />
</template> </a-form-model>
</CustomRadio> </template>
</a-modal> </CustomRadio>
</template> </a-modal>
</template>
<script>
import { grantCiType, revokeCiType } from '../../api/CIType' <script>
import { getCITypeAttributesByTypeIds } from '../../api/CITypeAttr' import { grantCiType, revokeCiType } from '../../api/CIType'
import FilterComp from '@/components/CMDBFilterComp' import { getCITypeAttributesByTypeIds } from '../../api/CITypeAttr'
import FilterComp from '@/components/CMDBFilterComp'
export default {
name: 'ReadGrantModal', export default {
components: { FilterComp }, name: 'ReadGrantModal',
props: { components: { FilterComp },
CITypeId: { props: {
type: Number, CITypeId: {
default: null, type: Number,
}, default: null,
}, },
inject: { },
provide_attrGroup: { inject: {
from: 'attrGroup', provide_attrGroup: {
}, from: 'attrGroup',
provide_filerPerimissions: { },
from: 'filerPerimissions', provide_filerPerimissions: {
}, from: 'filerPerimissions',
}, },
data() { },
return { data() {
visible: false, return {
colType: '', visible: false,
row: {}, colType: '',
radioValue: 1, row: {},
radioStyle: { radioValue: 1,
display: 'block', radioStyle: {
height: '30px', display: 'block',
lineHeight: '30px', height: '30px',
}, lineHeight: '30px',
selectedAttr: [], },
ruleList: [], selectedAttr: [],
canSearchPreferenceAttrList: [], ruleList: [],
expression: '', canSearchPreferenceAttrList: [],
form: { expression: '',
name: '', form: {
}, name: '',
rules: { },
name: [{ required: true, message: this.$t('cmdb.components.customizeFilterName') }], rules: {
}, name: [{ required: true, message: this.$t('cmdb.components.customizeFilterName') }],
} },
}, }
computed: { },
title() { computed: {
if (this.colType === 'read_attr') { title() {
return this.$t('cmdb.components.attributeGrant') if (this.colType === 'read_attr') {
} return this.$t('cmdb.components.attributeGrant')
return this.$t('cmdb.components.ciGrant') }
}, return this.$t('cmdb.components.ciGrant')
attrGroup() { },
return this.provide_attrGroup() attrGroup() {
}, return this.provide_attrGroup()
filerPerimissions() { },
return this.provide_filerPerimissions() filerPerimissions() {
}, return this.provide_filerPerimissions()
filterKey() { },
if (this.colType === 'read_attr') { filterKey() {
return 'attr_filter' if (this.colType === 'read_attr') {
} return 'attr_filter'
return 'ci_filter' }
}, return 'ci_filter'
}, },
methods: { },
async open(colType, row) { methods: {
this.visible = true async open(colType, row) {
this.colType = colType this.visible = true
this.row = row this.colType = colType
if (this.colType === 'read_ci') { this.row = row
await getCITypeAttributesByTypeIds({ type_ids: this.CITypeId }).then((res) => { this.form = {
this.canSearchPreferenceAttrList = res.attributes.filter((item) => item.value_type !== '6') name: '',
}) }
} if (this.colType === 'read_ci') {
if (this.filerPerimissions[row.rid]) { await getCITypeAttributesByTypeIds({ type_ids: this.CITypeId }).then((res) => {
const _tempValue = this.filerPerimissions[row.rid][this.filterKey] this.canSearchPreferenceAttrList = res.attributes.filter((item) => item.value_type !== '6')
if (_tempValue && _tempValue.length) { })
this.radioValue = 2 }
if (this.colType === 'read_attr') { if (this.filerPerimissions[row.rid]) {
this.selectedAttr = _tempValue const _tempValue = this.filerPerimissions[row.rid][this.filterKey]
} else { if (_tempValue && _tempValue.length) {
this.expression = `q=${_tempValue}` this.radioValue = 2
this.form = { if (this.colType === 'read_attr') {
name: this.filerPerimissions[row.rid].name || '', this.selectedAttr = _tempValue
} } else {
this.$nextTick(() => { this.expression = `q=${_tempValue}`
this.$refs.filterComp.visibleChange(true) this.form = {
}) name: this.filerPerimissions[row.rid].name || '',
} }
} this.$nextTick(() => {
} else { this.$refs.filterComp.visibleChange(true)
this.form = { })
name: '', }
} }
} }
}, },
async handleOk() { async handleOk() {
if (this.radioValue === 1) { if (this.radioValue === 1) {
await grantCiType(this.CITypeId, this.row.rid, { await grantCiType(this.CITypeId, this.row.rid, {
perms: ['read'], perms: ['read'],
attr_filter: this.colType === 'read_attr' ? [] : undefined, attr_filter: this.colType === 'read_attr' ? [] : undefined,
ci_filter: this.colType === 'read_ci' ? '' : undefined, ci_filter: this.colType === 'read_ci' ? '' : undefined,
}) })
} else if (this.radioValue === 2) { } else if (this.radioValue === 2) {
if (this.colType === 'read_ci') { if (this.colType === 'read_ci') {
this.$refs.filterComp.handleSubmit() this.$refs.filterComp.handleSubmit()
} }
await grantCiType(this.CITypeId, this.row.rid, { await grantCiType(this.CITypeId, this.row.rid, {
perms: ['read'], perms: ['read'],
attr_filter: this.colType === 'read_attr' ? this.selectedAttr : undefined, attr_filter: this.colType === 'read_attr' ? this.selectedAttr : undefined,
ci_filter: this.colType === 'read_ci' ? this.expression.slice(2) : undefined, ci_filter: this.colType === 'read_ci' ? this.expression.slice(2) : undefined,
name: this.colType === 'read_ci' ? this.form.name : undefined, name: this.colType === 'read_ci' ? this.form.name : undefined,
}) })
} else { } else {
const _tempValue = this.filerPerimissions?.[this.row.rid]?.[this.filterKey] const _tempValue = this.filerPerimissions?.[this.row.rid]?.[this.filterKey]
await revokeCiType(this.CITypeId, this.row.rid, { await revokeCiType(this.CITypeId, this.row.rid, {
perms: ['read'], perms: ['read'],
attr_filter: this.colType === 'read_attr' ? _tempValue : undefined, attr_filter: this.colType === 'read_attr' ? _tempValue : undefined,
ci_filter: this.colType === 'read_ci' ? _tempValue : undefined, ci_filter: this.colType === 'read_ci' ? _tempValue : undefined,
}) })
} }
this.$emit('updateTableDataRead', this.row, this.radioValue === 1 || this.radioValue === 2) this.$emit('updateTableDataRead', this.row, this.radioValue === 1 || this.radioValue === 2)
this.handleCancel() this.handleCancel()
}, },
handleCancel() { handleCancel() {
this.radioValue = 1 this.radioValue = 1
this.selectedAttr = [] this.selectedAttr = []
if (this.$refs.form) { if (this.$refs.form) {
this.$refs.form.resetFields() this.$refs.form.resetFields()
} }
this.visible = false this.visible = false
}, },
setExpFromFilter(filterExp) { setExpFromFilter(filterExp) {
let expression = '' let expression = ''
if (filterExp) { if (filterExp) {
expression = `q=${filterExp}` expression = `q=${filterExp}`
} }
this.expression = expression this.expression = expression
}, },
}, changeRadioValue(value) {
} if (this.id_filter) {
</script> this.$message.warning(this.$t('cmdb.serviceTree.grantedByServiceTreeTips'))
} else {
<style></style> this.radioValue = value
}
},
},
}
</script>
<style></style>

View File

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

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

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

View File

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

View File

@@ -1,301 +1,305 @@
<template> <template>
<div> <div>
<div id="search-form-bar" class="search-form-bar"> <div id="search-form-bar" class="search-form-bar">
<div :style="{ display: 'inline-flex', alignItems: 'center' }"> <div :style="{ display: 'inline-flex', alignItems: 'center' }">
<a-space> <a-space>
<treeselect <treeselect
v-if="type === 'resourceSearch'" v-if="type === 'resourceSearch'"
class="custom-treeselect" class="custom-treeselect"
:style="{ width: '250px', marginRight: '10px', '--custom-height': '32px' }" :style="{ width: '250px', marginRight: '10px', '--custom-height': '32px' }"
v-model="currenCiType" v-model="currenCiType"
:multiple="true" :multiple="true"
:clearable="true" :clearable="true"
searchable searchable
:options="ciTypeGroup" :options="ciTypeGroup"
:limit="1" :limit="1"
:limitText="(count) => `+ ${count}`" :limitText="(count) => `+ ${count}`"
value-consists-of="LEAF_PRIORITY" value-consists-of="LEAF_PRIORITY"
:placeholder="$t('cmdb.ciType.ciType')" :placeholder="$t('cmdb.ciType.ciType')"
@close="closeCiTypeGroup" @close="closeCiTypeGroup"
@open="openCiTypeGroup" @open="openCiTypeGroup"
@input="inputCiTypeGroup" @input="inputCiTypeGroup"
:normalizer=" :normalizer="
(node) => { (node) => {
return { return {
id: node.id || -1, id: node.id || -1,
label: node.alias || node.name || $t('other'), label: node.alias || node.name || $t('other'),
title: node.alias || node.name || $t('other'), title: node.alias || node.name || $t('other'),
children: node.ci_types, children: node.ci_types,
} }
} }
" "
> >
<div <div
:title="node.label" :title="node.label"
slot="option-label" slot="option-label"
slot-scope="{ node }" slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }" :style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
> >
{{ node.label }} {{ node.label }}
</div> </div>
</treeselect> </treeselect>
<a-input <a-input
v-model="fuzzySearch" v-model="fuzzySearch"
:style="{ display: 'inline-block', width: '244px' }" :style="{ display: 'inline-block', width: '244px' }"
:placeholder="$t('cmdb.components.pleaseSearch')" :placeholder="$t('cmdb.components.pleaseSearch')"
@pressEnter="emitRefresh" @pressEnter="emitRefresh"
class="ops-input ops-input-radius" 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' : '', 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>
<FilterComp <FilterComp
ref="filterComp" ref="filterComp"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList" :canSearchPreferenceAttrList="canSearchPreferenceAttrList"
@setExpFromFilter="setExpFromFilter" @setExpFromFilter="setExpFromFilter"
:expression="expression" :expression="expression"
placement="bottomLeft" placement="bottomLeft"
> >
<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" />
</div> </div>
</FilterComp> </FilterComp>
<a-input <a-input
v-if="isShowExpression" v-if="isShowExpression"
v-model="expression" v-model="expression"
v-show="!selectedRowKeys.length" v-show="!selectedRowKeys.length"
@focus=" @focus="
() => { () => {
isFocusExpression = true isFocusExpression = true
} }
" "
@blur=" @blur="
() => { () => {
isFocusExpression = false isFocusExpression = false
} }
" "
class="ci-searchform-expression" class="ci-searchform-expression"
:style="{ width }" :style="{ width }"
:placeholder="placeholder" :placeholder="placeholder"
@keyup.enter="emitRefresh" @keyup.enter="emitRefresh"
> >
<a-icon slot="suffix" type="copy" @click="handleCopyExpression" /> <a-icon slot="suffix" type="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-button @click="reset" size="small">{{ $t('reset') }}</a-button>
<a <a-tooltip :title="$t('cmdb.components.attributeDesc')" v-if="type === 'relationView'">
@click=" <a
() => { @click="
$refs.metadataDrawer.open(typeId) () => {
} $refs.metadataDrawer.open(typeId)
" }
><a-icon "
v-if="type === 'relationView'" ><a-icon
type="question-circle" v-if="type === 'relationView'"
/></a> type="question-circle"
</a-tooltip> /></a>
</a-space> </a-tooltip>
</div> </a-space>
<MetadataDrawer ref="metadataDrawer" /> </div>
</div> <MetadataDrawer ref="metadataDrawer" />
</template> </div>
</template>
<script>
import _ from 'lodash' <script>
import Treeselect from '@riophae/vue-treeselect' import _ from 'lodash'
import MetadataDrawer from '../../views/ci/modules/MetadataDrawer.vue' import Treeselect from '@riophae/vue-treeselect'
import FilterComp from '@/components/CMDBFilterComp' import MetadataDrawer from '../../views/ci/modules/MetadataDrawer.vue'
import { getCITypeGroups } from '../../api/ciTypeGroup' import FilterComp from '@/components/CMDBFilterComp'
export default { import { getCITypeGroups } from '../../api/ciTypeGroup'
name: 'SearchForm', export default {
components: { MetadataDrawer, FilterComp, Treeselect }, name: 'SearchForm',
props: { components: { MetadataDrawer, FilterComp, Treeselect },
preferenceAttrList: { props: {
type: Array, preferenceAttrList: {
required: true, type: Array,
}, required: true,
isShowExpression: { },
type: Boolean, isShowExpression: {
default: true, type: Boolean,
}, default: true,
typeId: { },
type: Number, typeId: {
default: null, type: Number,
}, default: null,
type: { },
type: String, type: {
default: '', type: String,
}, default: '',
selectedRowKeys: { },
type: Array, selectedRowKeys: {
default: () => [], type: Array,
}, default: () => [],
}, },
data() { },
return { data() {
// Advanced Search Expand/Close return {
advanced: false, // Advanced Search Expand/Close
queryParam: {}, advanced: false,
isFocusExpression: false, queryParam: {},
expression: '', isFocusExpression: false,
fuzzySearch: '', expression: '',
currenCiType: [], fuzzySearch: '',
ciTypeGroup: [], currenCiType: [],
lastCiType: [], ciTypeGroup: [],
} lastCiType: [],
}, }
},
computed: {
placeholder() { computed: {
return this.isFocusExpression ? this.$t('cmdb.components.ciSearchTips2') : this.$t('cmdb.ciType.expr') placeholder() {
}, return this.isFocusExpression ? this.$t('cmdb.components.ciSearchTips2') : this.$t('cmdb.ciType.expr')
width() { },
return '200px' width() {
}, return '200px'
canSearchPreferenceAttrList() { },
return this.preferenceAttrList.filter((item) => item.value_type !== '6') canSearchPreferenceAttrList() {
}, return this.preferenceAttrList.filter((item) => item.value_type !== '6')
}, },
watch: { },
'$route.path': function(newValue, oldValue) { watch: {
this.queryParam = {} '$route.path': function(newValue, oldValue) {
this.expression = '' this.queryParam = {}
this.fuzzySearch = '' this.expression = ''
}, this.fuzzySearch = ''
}, },
inject: { },
setPreferenceSearchCurrent: { inject: {
from: 'setPreferenceSearchCurrent', setPreferenceSearchCurrent: {
default: null, from: 'setPreferenceSearchCurrent',
}, default: null,
}, },
mounted() { },
if (this.type === 'resourceSearch') { mounted() {
this.getCITypeGroups() if (this.type === 'resourceSearch') {
} this.getCITypeGroups()
}, }
methods: { },
getCITypeGroups() { methods: {
getCITypeGroups({ need_other: true }).then((res) => { // toggleAdvanced() {
this.ciTypeGroup = res // this.advanced = !this.advanced
.filter((item) => item.ci_types && item.ci_types.length) // },
.map((item) => { getCITypeGroups() {
item.id = `parent_${item.id || -1}` getCITypeGroups({ need_other: true }).then((res) => {
return { ..._.cloneDeep(item) } this.ciTypeGroup = res
}) .filter((item) => item.ci_types && item.ci_types.length)
}) .map((item) => {
}, item.id = `parent_${item.id || -1}`
reset() { return { ..._.cloneDeep(item) }
this.queryParam = {} })
this.expression = '' })
this.fuzzySearch = '' },
this.currenCiType = [] reset() {
this.emitRefresh() this.queryParam = {}
}, this.expression = ''
setExpFromFilter(filterExp) { this.fuzzySearch = ''
const regSort = /(?<=sort=).+/g this.currenCiType = []
const expSort = this.expression.match(regSort) ? this.expression.match(regSort)[0] : undefined this.emitRefresh()
let expression = '' },
if (filterExp) { setExpFromFilter(filterExp) {
expression = `q=${filterExp}` const regSort = /(?<=sort=).+/g
} const expSort = this.expression.match(regSort) ? this.expression.match(regSort)[0] : undefined
if (expSort) { let expression = ''
expression += `&sort=${expSort}` if (filterExp) {
} expression = `q=${filterExp}`
this.expression = expression }
this.emitRefresh() if (expSort) {
}, expression += `&sort=${expSort}`
handleSubmit() { }
this.$refs.filterComp.handleSubmit() this.expression = expression
}, this.emitRefresh()
openCiTypeGroup() { },
this.lastCiType = _.cloneDeep(this.currenCiType) handleSubmit() {
}, this.$refs.filterComp.handleSubmit()
closeCiTypeGroup(value) { },
if (!_.isEqual(value, this.lastCiType)) { openCiTypeGroup() {
this.$emit('updateAllAttributesList', value) this.lastCiType = _.cloneDeep(this.currenCiType)
} },
}, closeCiTypeGroup(value) {
inputCiTypeGroup(value) { if (!_.isEqual(value, this.lastCiType)) {
console.log(value) this.$emit('updateAllAttributesList', value)
if (!value || !value.length) { }
this.$emit('updateAllAttributesList', value) },
} inputCiTypeGroup(value) {
}, console.log(value)
emitRefresh() { if (!value || !value.length) {
if (this.setPreferenceSearchCurrent) { this.$emit('updateAllAttributesList', value)
this.setPreferenceSearchCurrent(null) }
} },
this.$nextTick(() => { emitRefresh() {
this.$emit('refresh', true) if (this.setPreferenceSearchCurrent) {
}) this.setPreferenceSearchCurrent(null)
}, }
handleCopyExpression() { this.$nextTick(() => {
this.$emit('copyExpression') this.$emit('refresh', true)
}, })
}, },
} handleCopyExpression() {
</script> this.$emit('copyExpression')
<style lang="less"> },
@import '../../views/index.less'; },
.ci-searchform-expression { }
> input { </script>
border-bottom: 2px solid #d9d9d9; <style lang="less">
border-top: none; @import '../../views/index.less';
border-left: none; .ci-searchform-expression {
border-right: none; > input {
&:hover, border-bottom: 2px solid #d9d9d9;
&:focus { border-top: none;
border-bottom: 2px solid #2f54eb; border-left: none;
} border-right: none;
&:focus { &:hover,
box-shadow: 0 2px 2px -2px #1f78d133; &:focus {
} border-bottom: 2px solid #2f54eb;
} }
.ant-input-suffix { &:focus {
color: #2f54eb; box-shadow: 0 2px 2px -2px #1f78d133;
cursor: pointer; }
} }
} .ant-input-suffix {
.cmdb-search-form { color: #2f54eb;
.ant-form-item-label { cursor: pointer;
overflow: hidden; }
text-overflow: ellipsis; }
white-space: nowrap; .cmdb-search-form {
} .ant-form-item-label {
} overflow: hidden;
</style> text-overflow: ellipsis;
white-space: nowrap;
<style lang="less" scoped> }
@import '~@/style/static.less'; }
</style>
.search-form-bar {
margin-bottom: 10px; <style lang="less" scoped>
display: flex; @import '~@/style/static.less';
justify-content: space-between;
align-items: center; .search-form-bar {
.search-form-bar-filter { margin-bottom: 10px;
.ops_display_wrapper(); display: flex;
.search-form-bar-filter-icon { justify-content: space-between;
color: #custom_colors[color_1]; align-items: center;
font-size: 12px; .search-form-bar-filter {
} .ops_display_wrapper();
} .search-form-bar-filter-icon {
} color: #custom_colors[color_1];
</style> font-size: 12px;
}
}
}
</style>

View File

@@ -1,492 +1,503 @@
const cmdb_en = { const cmdb_en = {
relation: 'Relation', relation: 'Relation',
attribute: 'Attributes', attribute: 'Attributes',
menu: { menu: {
views: 'Views', views: 'Views',
config: 'Configuration', config: 'Configuration',
backend: 'Management', backend: 'Management',
ciTable: 'Resource Views', ciTable: 'Resource Views',
ciTree: 'Tree Views', ciTree: 'Tree Views',
ciSearch: 'Search', ciSearch: 'Search',
adCIs: 'AutoDiscovery Pool', adCIs: 'AutoDiscovery Pool',
preference: 'Preference', preference: 'Preference',
batchUpload: 'Batch Import', batchUpload: 'Batch Import',
citypeManage: 'Modeling', citypeManage: 'Modeling',
backendManage: 'Backend', backendManage: 'Backend',
customDashboard: 'Custom Dashboard', customDashboard: 'Custom Dashboard',
serviceTreeDefine: 'Service Tree', serviceTreeDefine: 'Service Tree',
citypeRelation: 'CIType Relation', citypeRelation: 'CIType Relation',
operationHistory: 'Operation Audit', operationHistory: 'Operation Audit',
relationType: 'Relation Type', relationType: 'Relation Type',
ad: 'AutoDiscovery', ad: 'AutoDiscovery',
cidetail: 'CI Detail' cidetail: 'CI Detail'
}, },
ciType: { ciType: {
ciType: 'CIType', ciType: 'CIType',
attributes: 'Attributes', attributes: 'Attributes',
relation: 'Relation', relation: 'Relation',
trigger: 'Triggers', trigger: 'Triggers',
attributeAD: 'Attributes AutoDiscovery', attributeAD: 'Attributes AutoDiscovery',
relationAD: 'Relation AutoDiscovery', relationAD: 'Relation AutoDiscovery',
grant: 'Grant', grant: 'Grant',
addGroup: 'New Group', addGroup: 'New Group',
editGroup: 'Edit Group', editGroup: 'Edit Group',
group: 'Group', group: 'Group',
attributeLibray: 'Attribute Library', attributeLibray: 'Attribute Library',
addCITypeInGroup: 'Add a new CIType to the group', addCITypeInGroup: 'Add a new CIType to the group',
addCIType: 'Add CIType', addCIType: 'Add CIType',
editGroupName: 'Edit group name', editGroupName: 'Edit group name',
deleteGroup: 'Delete this group', deleteGroup: 'Delete this group',
CITypeName: 'Name(English)', CITypeName: 'Name(English)',
English: 'English', English: 'English',
inputAttributeName: 'Please enter the attribute name', inputAttributeName: 'Please enter the attribute name',
attributeNameTips: 'It cannot start with a number, it can be English numbers and underscores (_)', attributeNameTips: 'It cannot start with a number, it can be English numbers and underscores (_)',
editCIType: 'Edit CIType', editCIType: 'Edit CIType',
defaultSort: 'Default sort', defaultSort: 'Default sort',
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',
notfound: 'Can\'t find what you want?', uniqueKeyTips: 'json/password/computed/choice can not be unique identifies',
cannotDeleteGroupTips: 'There is data under this group and cannot be deleted!', notfound: 'Can\'t find what you want?',
confirmDeleteGroup: 'Are you sure you want to delete group [{groupName}]?', cannotDeleteGroupTips: 'There is data under this group and cannot be deleted!',
confirmDeleteCIType: 'Are you sure you want to delete model [{typeName}]?', confirmDeleteGroup: 'Are you sure you want to delete group [{groupName}]?',
uploading: 'Uploading', confirmDeleteCIType: 'Are you sure you want to delete model [{typeName}]?',
uploadFailed: 'Upload failed, please try again later', uploading: 'Uploading',
addPlugin: 'New plugin', uploadFailed: 'Upload failed, please try again later',
deletePlugin: 'Delete plugin', addPlugin: 'New plugin',
confirmDeleteADT: 'Do you confirm to delete [{pluginName}]', deletePlugin: 'Delete plugin',
attributeMap: 'Attribute mapping', confirmDeleteADT: 'Do you confirm to delete [{pluginName}]',
autoDiscovery: 'AutoDiscovery', attributeMap: 'Attribute mapping',
node: 'Node', autoDiscovery: 'AutoDiscovery',
adExecConfig: 'Execute configuration', node: 'Node',
adExecTarget: 'Execute targets', adExecConfig: 'Execute configuration',
oneagentIdTips: 'Please enter the hexadecimal OneAgent ID starting with 0x', adExecTarget: 'Execute targets',
selectFromCMDBTips: 'Select from CMDB ', oneagentIdTips: 'Please enter the hexadecimal OneAgent ID starting with 0x',
adAutoInLib: 'Save as CI auto', selectFromCMDBTips: 'Select from CMDB ',
adInterval: 'Collection frequency', adAutoInLib: 'Save as CI auto',
byInterval: 'by interval', adInterval: 'Collection frequency',
allNodes: 'All nodes', byInterval: 'by interval',
specifyNodes: 'Specify Node', allNodes: 'All nodes',
specifyNodesTips: 'Please fill in the specify node!', specifyNodes: 'Specify Node',
username: 'Username', specifyNodesTips: 'Please fill in the specify node!',
password: 'Password', username: 'Username',
link: 'Link', password: 'Password',
list: 'List', link: 'Link',
listTips: 'The value of the field is one or more, and the type of the value returned by the interface is list.', list: 'List',
computeForAllCITips: 'All CI trigger computes', listTips: 'The value of the field is one or more, and the type of the value returned by the interface is list.',
confirmcomputeForAllCITips: 'Confirm triggering computes for all CIs?', computeForAllCITips: 'All CI trigger computes',
isUnique: 'Is it unique', confirmcomputeForAllCITips: 'Confirm triggering computes for all CIs?',
unique: 'Unique', isUnique: 'Is it unique',
isChoice: 'Choiced', unique: 'Unique',
defaultShow: 'Default Display', isChoice: 'Choiced',
defaultShowTips: 'The CI instance table displays this field by default', defaultShow: 'Default Display',
isSortable: 'Sortable', defaultShowTips: 'The CI instance table displays this field by default',
isIndex: 'Indexed', isSortable: 'Sortable',
index: 'Index', isIndex: 'Indexed',
indexTips: 'Fields can be used for retrieval to speed up queries', index: 'Index',
confirmDelete: 'Confirm to delete [{name}]?', indexTips: 'Fields can be used for retrieval to speed up queries',
confirmDelete2: 'Confirm to delete?', confirmDelete: 'Confirm to delete [{name}]?',
computeSuccess: 'Triggered successfully!', confirmDelete2: 'Confirm to delete?',
basicConfig: 'Basic Settings', computeSuccess: 'Triggered successfully!',
AttributeName: 'Name(English)', basicConfig: 'Basic Settings',
DataType: 'Data Type', AttributeName: 'Name(English)',
defaultValue: 'Default value', DataType: 'Data Type',
autoIncID: 'Auto-increment ID', defaultValue: 'Default value',
customTime: 'Custom time', autoIncID: 'Auto-increment ID',
advancedSettings: 'Advanced Settings', customTime: 'Custom time',
font: 'Font', advancedSettings: 'Advanced Settings',
color: 'Color', font: 'Font',
choiceValue: 'Predefined value', color: 'Color',
computedAttribute: 'Computed Attribute', choiceValue: 'Predefined value',
computedAttributeTips: 'The value of this attribute is calculated through an expression constructed from other attributes of the CIType or by executing a piece of code. The reference method of the attribute is: {{ attribute name }}', computedAttribute: 'Computed Attribute',
addAttribute: 'New attribute', computedAttributeTips: 'The value of this attribute is calculated through an expression constructed from other attributes of the CIType or by executing a piece of code. The reference method of the attribute is: {{ attribute name }}',
existedAttributes: 'Already have attributes', addAttribute: 'New attribute',
editAttribute: 'Edit attribute', existedAttributes: 'Already have attributes',
addAttributeTips1: 'If sorting is selected, it must also be selected!', editAttribute: 'Edit attribute',
uniqueConstraint: 'Unique Constraint', addAttributeTips1: 'If sorting is selected, it must also be selected!',
up: 'Move up', uniqueConstraint: 'Unique Constraint',
down: 'Move down', up: 'Move up',
selectAttribute: 'Select Attribute', down: 'Move down',
groupExisted: 'Group name already exists', selectAttribute: 'Select Attribute',
attributeSortedTips: 'Attributes in other groups cannot be sorted. If you need to sort, please drag them to a custom group first!', groupExisted: 'Group name already exists',
buildinAttribute: 'built-in attributes', attributeSortedTips: 'Attributes in other groups cannot be sorted. If you need to sort, please drag them to a custom group first!',
expr: 'Expression', buildinAttribute: 'built-in attributes',
code: 'Code', expr: 'Expression',
apply: 'apply', code: 'Code',
continueAdd: 'Keep adding', apply: 'apply',
filter: 'Filter', continueAdd: 'Keep adding',
choiceOther: 'Other CIType Attributes', filter: 'Filter',
choiceWebhookTips: 'The returned results are filtered by fields, and the hierarchical nesting is separated by ##, such as k1##k2. The web request returns {k1: [{k2: 1}, {k2: 2}]}, and the parsing result is [1, 2 ]', choiceOther: 'Other CIType Attributes',
selectCIType: 'Please select a CMDB CIType', choiceWebhookTips: 'The returned results are filtered by fields, and the hierarchical nesting is separated by ##, such as k1##k2. The web request returns {k1: [{k2: 1}, {k2: 2}]}, and the parsing result is [1, 2 ]',
selectCITypeAttributes: 'Please select CIType attributes', selectCIType: 'Please select a CMDB CIType',
selectAttributes: 'Please select attributes', selectCITypeAttributes: 'Please select CIType attributes',
choiceScriptDemo: 'class ChoiceValue(object):\n @staticmethod\n def values():\n """\n Execution entry, returns predefined value\n :return: Returns a list, the type of the value is the same as the type of the attribute\n For example:\n return ["online", "offline"]\n """\n return []', selectAttributes: 'Please select attributes',
valueExisted: 'The current value already exists!', choiceScriptDemo: 'class ChoiceValue(object):\n @staticmethod\n def values():\n """\n Execution entry, returns predefined value\n :return: Returns a list, the type of the value is the same as the type of the attribute\n For example:\n return ["online", "offline"]\n """\n return []',
addRelation: 'Add Relation', valueExisted: 'The current value already exists!',
sourceCIType: 'Source CIType', addRelation: 'Add Relation',
sourceCITypeTips: 'Please select Source CIType', sourceCIType: 'Source CIType',
dstCIType: 'Target CIType', sourceCITypeTips: 'Please select Source CIType',
dstCITypeTips: 'Please select target CIType', dstCIType: 'Target CIType',
relationType: 'Relation Type', dstCITypeTips: 'Please select target CIType',
relationTypeTips: 'Please select relation type', relationType: 'Relation Type',
isParent: 'is parent', relationTypeTips: 'Please select relation type',
relationConstraint: 'Constraints', isParent: 'is parent',
relationConstraintTips: 'please select a relationship constraint', relationConstraint: 'Constraints',
one2Many: 'One to Many', relationConstraintTips: 'please select a relationship constraint',
one2One: 'One to One', one2Many: 'One to Many',
many2Many: 'Many to Many', one2One: 'One to One',
basicInfo: 'Basic Information', many2Many: 'Many to Many',
nameInputTips: 'Please enter name', basicInfo: 'Basic Information',
triggerDataChange: 'Data changes', nameInputTips: 'Please enter name',
triggerDate: 'Date attribute', triggerDataChange: 'Data changes',
triggerEnable: 'Turn on', triggerDate: 'Date attribute',
descInput: 'Please enter remarks', triggerEnable: 'Turn on',
triggerCondition: 'Triggering conditions', descInput: 'Please enter remarks',
addInstance: 'Add new instance', triggerCondition: 'Triggering conditions',
deleteInstance: 'Delete instance', addInstance: 'Add new instance',
changeInstance: 'Instance changes', deleteInstance: 'Delete instance',
selectMutipleAttributes: 'Please select attributes (multiple selections)', changeInstance: 'Instance changes',
selectSingleAttribute: 'Please select an attribute (single choice)', selectMutipleAttributes: 'Please select attributes (multiple selections)',
beforeDays: 'ahead of time', selectSingleAttribute: 'Please select an attribute (single choice)',
days: 'Days', beforeDays: 'ahead of time',
notifyAt: 'Send time', days: 'Days',
notify: 'Notify', notifyAt: 'Send time',
triggerAction: 'Trigger action', notify: 'Notify',
receivers: 'Recipients', triggerAction: 'Trigger action',
emailTips: 'Please enter your email address, separate multiple email addresses with ;', receivers: 'Recipients',
customEmail: 'Custom recipients', emailTips: 'Please enter your email address, separate multiple email addresses with ;',
notifySubject: 'Notification title', customEmail: 'Custom recipients',
notifySubjectTips: 'Please enter notification title', notifySubject: 'Notification title',
notifyContent: 'Content', notifySubjectTips: 'Please enter notification title',
notifyMethod: 'Notify methods', notifyContent: 'Content',
botSelect: 'Please select a robot', notifyMethod: 'Notify methods',
refAttributeTips: 'The title and content can reference the attribute value of the CIType. The reference method is: {{ attr_name }}', botSelect: 'Please select a robot',
webhookRefAttributeTips: 'Request parameters can reference the attribute value of the model. The reference method is: {{ attr_name }}', refAttributeTips: 'The title and content can reference the attribute value of the CIType. The reference method is: {{ attr_name }}',
newTrigger: 'Add trigger', webhookRefAttributeTips: 'Request parameters can reference the attribute value of the model. The reference method is: {{ attr_name }}',
editTriggerTitle: 'Edit trigger {name}', newTrigger: 'Add trigger',
newTriggerTitle: 'Add trigger {name}', editTriggerTitle: 'Edit trigger {name}',
confirmDeleteTrigger: 'Are you sure to delete this trigger?', newTriggerTitle: 'Add trigger {name}',
int: 'Integer', confirmDeleteTrigger: 'Are you sure to delete this trigger?',
float: 'Float', int: 'Integer',
text: 'Text', float: 'Float',
datetime: 'DateTime', text: 'Text',
date: 'Date', datetime: 'DateTime',
time: 'Time', date: 'Date',
json: 'JSON', time: 'Time',
event: 'Event', json: 'JSON',
reg: 'Regex', event: 'Event',
isInherit: 'Inherit', reg: 'Regex',
inheritType: 'Inherit Type', isInherit: 'Inherit',
inheritTypePlaceholder: 'Please select inherit types', inheritType: 'Inherit Type',
inheritFrom: 'inherit from {name}', inheritTypePlaceholder: 'Please select inherit types',
groupInheritFrom: 'Please go to the {name} for modification' inheritFrom: 'inherit from {name}',
}, groupInheritFrom: 'Please go to the {name} for modification'
components: { },
unselectAttributes: 'Unselected', components: {
selectAttributes: 'Selected', unselectAttributes: 'Unselected',
downloadCI: 'Export data', selectAttributes: 'Selected',
filename: 'Filename', downloadCI: 'Export data',
filenameInputTips: 'Please enter filename', filename: 'Filename',
saveType: 'Save type', filenameInputTips: 'Please enter filename',
saveTypeTips: 'Please select save type', saveType: 'Save type',
xlsx: 'Excel workbook (*.xlsx)', saveTypeTips: 'Please select save type',
csv: 'CSV (comma separated) (*.csv)', xlsx: 'Excel workbook (*.xlsx)',
html: 'Web page (*.html)', csv: 'CSV (comma separated) (*.csv)',
xml: 'XML data (*.xml)', html: 'Web page (*.html)',
txt: 'Text file (tab delimited) (*.txt)', xml: 'XML data (*.xml)',
grantUser: 'Grant User/Department', txt: 'Text file (tab delimited) (*.txt)',
grantRole: 'Grant Role', grantUser: 'Grant User/Department',
confirmRevoke: 'Confirm to delete the [Authorization] permission of [{name}]?', grantRole: 'Grant Role',
readAttribute: 'View Attributes', confirmRevoke: 'Confirm to delete the [Authorization] permission of [{name}]?',
readCI: 'View CIs', readAttribute: 'View Attributes',
config: 'Configuration', readCI: 'View CIs',
ciTypeGrant: 'Grant CIType', config: 'Configuration',
ciGrant: 'Grant CI', ciTypeGrant: 'Grant CIType',
attributeGrant: 'Grant Attribute', ciGrant: 'Grant CI',
relationGrant: 'Grant Relation', attributeGrant: 'Grant Attribute',
perm: 'Permissions', relationGrant: 'Grant Relation',
all: 'All', perm: 'Permissions',
customize: 'Customize', all: 'All',
none: 'None', customize: 'Customize',
customizeFilterName: 'Please enter a custom filter name', none: 'None',
colorPickerError: 'Initialization color format error, use #fff or rgb format', customizeFilterName: 'Please enter a custom filter name',
example: 'Example value', colorPickerError: 'Initialization color format error, use #fff or rgb format',
aliyun: 'aliyun', example: 'Example value',
tencentcloud: 'Tencent Cloud', aliyun: 'aliyun',
huaweicloud: 'Huawei Cloud', tencentcloud: 'Tencent Cloud',
beforeChange: 'Before change', huaweicloud: 'Huawei Cloud',
afterChange: 'After change', beforeChange: 'Before change',
noticeContentTips: 'Please enter notification content', afterChange: 'After change',
saveQuery: 'Save Filters', noticeContentTips: 'Please enter notification content',
pleaseSearch: 'Please search', saveQuery: 'Save Filters',
conditionFilter: 'Conditional filtering', pleaseSearch: 'Please search',
attributeDesc: 'Attribute Description', conditionFilter: 'Conditional filtering',
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', attributeDesc: 'Attribute Description',
ciSearchTips2: 'For example: q=hostname:*0.0.0.0*', 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',
subCIType: 'Subscription CIType', ciSearchTips2: 'For example: q=hostname:*0.0.0.0*',
already: 'already', subCIType: 'Subscription CIType',
not: 'not', already: 'already',
sub: 'subscription', not: 'not',
selectBelow: 'Please select below', sub: 'subscription',
subSuccess: 'Subscription successful', selectBelow: 'Please select below',
selectMethods: 'Please select a method', subSuccess: 'Subscription successful',
noAuthRequest: 'No certification requested yet', selectMethods: 'Please select a method',
noParamRequest: 'No parameter certification yet', noAuthRequest: 'No certification requested yet',
requestParam: 'Request parameters', noParamRequest: 'No parameter certification yet',
param: 'Parameter{param}', requestParam: 'Request parameters',
value: 'Value{value}', param: 'Parameter{param}',
clear: 'Clear', value: 'Value{value}',
}, clear: 'Clear',
batch: { },
downloadFailed: 'Download failed', batch: {
unselectCIType: 'No CIType selected yet', downloadFailed: 'Download failed',
pleaseUploadFile: 'Please upload files', unselectCIType: 'No CIType selected yet',
batchUploadCanceled: 'Batch upload canceled', pleaseUploadFile: 'Please upload files',
selectCITypeTips: 'Please select CIType', batchUploadCanceled: 'Batch upload canceled',
downloadTemplate: 'Download Template', selectCITypeTips: 'Please select CIType',
drawTips: 'Click or drag files here to upload!', downloadTemplate: 'Download Template',
supportFileTypes: 'Supported file types: xls, xlsx', drawTips: 'Click or drag files here to upload!',
uploadResult: 'Upload results', supportFileTypes: 'Supported file types: xls, xlsx',
total: 'total', uploadResult: 'Upload results',
successItems: 'items, succeeded', total: 'total',
failedItems: 'items, failed', successItems: 'items, succeeded',
items: 'items', failedItems: 'items, failed',
errorTips: 'Error message', items: 'items',
requestFailedTips: 'An error occurred with the request, please try again later', errorTips: 'Error message',
requestSuccessTips: 'Upload completed', requestFailedTips: 'An error occurred with the request, please try again later',
}, requestSuccessTips: 'Upload completed',
preference: { },
mySub: 'My Subscription', preference: {
sub: 'Subscribe', mySub: 'My Subscription',
cancelSub: 'Unsubscribe', sub: 'Subscribe',
editSub: 'Edit subscription', cancelSub: 'Unsubscribe',
peopleSub: ' people subscribed', editSub: 'Edit subscription',
noSub: 'No subscribed', peopleSub: ' people subscribed',
cancelSubSuccess: 'Unsubscribe successfully', noSub: 'No subscribed',
confirmcancelSub: 'Are you sure to cancel your subscription?', cancelSubSuccess: 'Unsubscribe successfully',
confirmcancelSub2: 'Are you sure you want to unsubscribe {name}?', confirmcancelSub: 'Are you sure to cancel your subscription?',
of: 'of', confirmcancelSub2: 'Are you sure you want to unsubscribe {name}?',
hoursAgo: 'hours ago', of: 'of',
daysAgo: 'days ago', hoursAgo: 'hours ago',
monthsAgo: 'month ago', daysAgo: 'days ago',
yearsAgo: 'years ago', monthsAgo: 'month ago',
just: 'just now', yearsAgo: 'years ago',
}, just: 'just now',
custom_dashboard: { },
charts: 'Chart', custom_dashboard: {
newChart: 'Add Chart', charts: 'Chart',
editChart: 'Edit Chart', newChart: 'Add Chart',
title: 'Title', editChart: 'Edit Chart',
titleTips: 'Please enter a chart title', title: 'Title',
calcIndicators: 'Counter', titleTips: 'Please enter a chart title',
dimensions: 'Dimensions', calcIndicators: 'Counter',
selectDimensions: 'Please select a dimension', dimensions: 'Dimensions',
quantity: 'Quantity', selectDimensions: 'Please select a dimension',
childCIType: 'Relational CIType', quantity: 'Quantity',
level: 'Level', childCIType: 'Relational CIType',
levelTips: 'Please enter the relationship level', level: 'Level',
preview: 'Preview', levelTips: 'Please enter the relationship level',
showIcon: 'Display icon', preview: 'Preview',
chartType: 'Chart Type', showIcon: 'Display icon',
dataFilter: 'Data Filtering', chartType: 'Chart Type',
format: 'Formats', dataFilter: 'Data Filtering',
fontColor: 'Font Color', format: 'Formats',
backgroundColor: 'Background', fontColor: 'Font Color',
chartColor: 'Chart Color', backgroundColor: 'Background',
chartLength: 'Length', chartColor: 'Chart Color',
barType: 'Bar Type', chartLength: 'Length',
stackedBar: 'Stacked Bar', barType: 'Bar Type',
multipleSeriesBar: 'Multiple Series Bar ', stackedBar: 'Stacked Bar',
axis: 'Axis', multipleSeriesBar: 'Multiple Series Bar ',
direction: 'Direction', axis: 'Axis',
lowerShadow: 'Lower Shadow', direction: 'Direction',
count: 'Indicator', lowerShadow: 'Lower Shadow',
bar: 'Bar', count: 'Indicator',
line: 'Line', bar: 'Bar',
pie: 'Pie', line: 'Line',
table: 'Table', pie: 'Pie',
default: 'default', table: 'Table',
relation: 'Relation', default: 'default',
noCustomDashboard: 'The administrator has not customized the dashboard yet', relation: 'Relation',
}, noCustomDashboard: 'The administrator has not customized the dashboard yet',
preference_relation: { },
newServiceTree: 'Add ServiceTree', preference_relation: {
serviceTreeName: 'Name', newServiceTree: 'Add ServiceTree',
public: 'Public', serviceTreeName: 'Name',
saveLayout: 'Save Layout', public: 'Public',
childNodesNotFound: 'There are no child nodes and no business relationship can be formed. Please select again!', saveLayout: 'Save Layout',
tips1: 'Cannot form a view with the currently selected node, please select again!', childNodesNotFound: 'There are no child nodes and no business relationship can be formed. Please select again!',
tips2: 'Please enter the new serviceTree name!', tips1: 'Cannot form a view with the currently selected node, please select again!',
tips3: 'Please select at least two nodes!', tips2: 'Please enter the new serviceTree name!',
}, tips3: 'Please select at least two nodes!',
history: { },
ciChange: 'CI', history: {
relationChange: 'Relation', ciChange: 'CI',
ciTypeChange: 'CIType', relationChange: 'Relation',
triggerHistory: 'Triggers', ciTypeChange: 'CIType',
opreateTime: 'Operate Time', triggerHistory: 'Triggers',
user: 'User', opreateTime: 'Operate Time',
userTips: 'Enter filter username', user: 'User',
filter: 'Search', userTips: 'Enter filter username',
filterOperate: 'fitler operation', filter: 'Search',
attribute: 'Attribute', filterOperate: 'fitler operation',
old: 'Old', attribute: 'Attribute',
new: 'New', old: 'Old',
noUpdate: 'No update', new: 'New',
itemsPerPage: '/page', noUpdate: 'No update',
triggerName: 'Name', itemsPerPage: '/page',
event: 'Event', triggerName: 'Name',
action: 'Actoin', event: 'Event',
status: 'Status', action: 'Actoin',
done: 'Done', status: 'Status',
undone: 'Undone', done: 'Done',
triggerTime: 'Trigger Time', undone: 'Undone',
totalItems: '{total} records in total', triggerTime: 'Trigger Time',
pleaseSelect: 'Please select', totalItems: '{total} records in total',
startTime: 'Start Time', pleaseSelect: 'Please select',
endTime: 'End Time', startTime: 'Start Time',
deleteCIType: 'Delete CIType', endTime: 'End Time',
addCIType: 'Add CIType', deleteCIType: 'Delete CIType',
updateCIType: 'Update CIType', addCIType: 'Add CIType',
addAttribute: 'Add Attribute', updateCIType: 'Update CIType',
updateAttribute: 'Update Attribute', addAttribute: 'Add Attribute',
deleteAttribute: 'Delete Attribute', updateAttribute: 'Update Attribute',
addTrigger: 'Add Trigger', deleteAttribute: 'Delete Attribute',
updateTrigger: 'Update Trigger', addTrigger: 'Add Trigger',
deleteTrigger: 'Delete Trigger', updateTrigger: 'Update Trigger',
addUniqueConstraint: 'Add Unique Constraint', deleteTrigger: 'Delete Trigger',
updateUniqueConstraint: 'Update Unique Constraint', addUniqueConstraint: 'Add Unique Constraint',
deleteUniqueConstraint: 'Delete Unique Constraint', updateUniqueConstraint: 'Update Unique Constraint',
addRelation: 'Add Relation', deleteUniqueConstraint: 'Delete Unique Constraint',
deleteRelation: 'Delete Relation', addRelation: 'Add Relation',
noModifications: 'No Modifications', deleteRelation: 'Delete Relation',
attr: 'attribute', noModifications: 'No Modifications',
attrId: 'attribute id', attr: 'attribute',
changeDescription: 'attribute id: {attr_id}, {before_days} day(s) in advance, Subject: {subject}\nContent: {body}\nNotify At: {notify_at}' attrId: 'attribute id',
}, changeDescription: 'attribute id: {attr_id}, {before_days} day(s) in advance, Subject: {subject}\nContent: {body}\nNotify At: {notify_at}'
relation_type: { },
addRelationType: 'New', relation_type: {
nameTips: 'Please enter a type name', addRelationType: 'New',
}, nameTips: 'Please enter a type name',
ad: { },
upload: 'Import', ad: {
download: 'Export', upload: 'Import',
accept: 'Accept', download: 'Export',
acceptBy: 'Accept By', accept: 'Accept',
acceptTime: 'Accept Time', acceptBy: 'Accept By',
confirmAccept: 'Confirm Accept?', acceptTime: 'Accept Time',
acceptSuccess: 'Accept successfully', confirmAccept: 'Confirm Accept?',
isAccept: 'Is accept', acceptSuccess: 'Accept successfully',
deleteADC: 'Confirm to delete this data?', isAccept: 'Is accept',
batchDelete: 'Confirm to delete this data?', deleteADC: 'Confirm to delete this data?',
agent: 'Built-in & Plug-ins', batchDelete: 'Confirm to delete this data?',
snmp: 'Network Devices', agent: 'Built-in & Plug-ins',
http: 'Public Clouds', snmp: 'Network Devices',
rule: 'AutoDiscovery Rules', http: 'Public Clouds',
timeout: 'Timeout error', rule: 'AutoDiscovery Rules',
mode: 'Mode', timeout: 'Timeout error',
collectSettings: 'Collection Settings', mode: 'Mode',
updateFields: 'Update Field', collectSettings: 'Collection Settings',
pluginScript: `# -*- coding:utf-8 -*- updateFields: 'Update Field',
pluginScript: `# -*- coding:utf-8 -*-
import json
import json
class AutoDiscovery(object):
class AutoDiscovery(object):
@property
def unique_key(self): @property
""" def unique_key(self):
"""
:return: Returns the name of a unique attribute
""" :return: Returns the name of a unique attribute
return """
return
@staticmethod
def attributes(): @staticmethod
""" def attributes():
Define attribute fields """
:return: Returns a list of attribute fields. The list items are (name, type, description). The name must be in English. Define attribute fields
type: String Integer Float Date DateTime Time JSON :return: Returns a list of attribute fields. The list items are (name, type, description). The name must be in English.
For example: type: String Integer Float Date DateTime Time JSON
return [ For example:
("ci_type", "String", "CIType name"), return [
("private_ip", "String", "Internal IP, multiple values separated by commas") ("ci_type", "String", "CIType name"),
] ("private_ip", "String", "Internal IP, multiple values separated by commas")
""" ]
return [] """
return []
@staticmethod
def run(): @staticmethod
""" def run():
Execution entry, returns collected attribute values """
:return: Execution entry, returns collected attribute values
Returns a list, the list item is a dictionary, the dictionary key is the attribute name, and the value is the attribute value :return:
For example: Returns a list, the list item is a dictionary, the dictionary key is the attribute name, and the value is the attribute value
return [dict(ci_type="server", private_ip="192.168.1.1")] For example:
""" return [dict(ci_type="server", private_ip="192.168.1.1")]
return [] """
return []
if __name__ == "__main__":
result = AutoDiscovery().run() if __name__ == "__main__":
if isinstance(result, list): result = AutoDiscovery().run()
print("AutoDiscovery::Result::{}".format(json.dumps(result))) if isinstance(result, list):
else: print("AutoDiscovery::Result::{}".format(json.dumps(result)))
print("ERROR: The collection return must be a list") else:
`, print("ERROR: The collection return must be a list")
server: 'Server', `,
vserver: 'VServer', server: 'Server',
nic: 'NIC', vserver: 'VServer',
disk: 'harddisk', nic: 'NIC',
}, disk: 'harddisk',
ci: { },
attributeDesc: 'Attribute Description', ci: {
selectRows: 'Select: {rows} items', attributeDesc: 'Attribute Description',
addRelation: 'Add Relation', selectRows: 'Select: {rows} items',
all: 'All', addRelation: 'Add Relation',
batchUpdate: 'Batch Update', all: 'All',
batchUpdateConfirm: 'Are you sure you want to make batch updates?', batchUpdate: 'Batch Update',
batchUpdateInProgress: 'Currently being updated in batches', batchUpdateConfirm: 'Are you sure you want to make batch updates?',
batchUpdateInProgress2: 'Updating in batches, {total} in total, {successNum} successful, {errorNum} failed', batchUpdateInProgress: 'Currently being updated in batches',
batchDeleting: 'Deleting...', batchUpdateInProgress2: 'Updating in batches, {total} in total, {successNum} successful, {errorNum} failed',
batchDeleting2: 'Deleting {total} items in total, {successNum} items successful, {errorNum} items failed', batchDeleting: 'Deleting...',
copyFailed: 'Copy failed', batchDeleting2: 'Deleting {total} items in total, {successNum} items successful, {errorNum} items failed',
noLevel: 'No hierarchical relationship!', copyFailed: 'Copy failed',
batchAddRelation: 'Batch Add Relation', noLevel: 'No hierarchical relationship!',
history: 'History', batchAddRelation: 'Batch Add Relation',
topo: 'Topology', history: 'History',
table: 'Table', topo: 'Topology',
m2mTips: 'The current CIType relationship is many-to-many, please go to the SerivceTree(relation view) to add or delete', table: 'Table',
confirmDeleteRelation: 'Confirm to delete the relationship?', m2mTips: 'The current CIType relationship is many-to-many, please go to the SerivceTree(relation view) to add or delete',
tips1: 'Use commas to separate multiple values', confirmDeleteRelation: 'Confirm to delete the relationship?',
tips2: 'The field can be modified as needed. When the value is empty, the field will be left empty.', tips1: 'Use commas to separate multiple values',
tips3: 'Please select the fields that need to be modified', tips2: 'The field can be modified as needed. When the value is empty, the field will be left empty.',
tips4: 'At least one field must be selected', tips3: 'Please select the fields that need to be modified',
tips5: 'Search name | alias', tips4: 'At least one field must be selected',
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', tips5: 'Search name | alias',
tips7: 'The form of expression is a drop-down box, and the value must be in the predefined value', 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',
tips8: 'Multiple values, such as intranet IP', tips7: 'The form of expression is a drop-down box, and the value must be in the predefined value',
tips9: 'For front-end only', tips8: 'Multiple values, such as intranet IP',
tips10: 'Other attributes of the CIType are computed using expressions\n\nA code snippet computes the returned value.', tips9: 'For front-end only',
newUpdateField: 'Add a Attribute', tips10: 'Other attributes of the CIType are computed using expressions\n\nA code snippet computes the returned value.',
attributeSettings: 'Attribute Settings', newUpdateField: 'Add a Attribute',
share: 'Share', attributeSettings: 'Attribute Settings',
noPermission: 'No Permission' share: 'Share',
}, noPermission: 'No Permission'
serviceTree: { },
deleteNode: 'Delete Node', serviceTree: {
tips1: 'For example: q=os_version:centos&sort=os_version', deleteNode: 'Delete Node',
tips2: 'Expression search', tips1: 'For example: q=os_version:centos&sort=os_version',
alert1: 'The administrator has not configured the ServiceTree(relation view), or you do not have permission to access it!', tips2: 'Expression search',
copyFailed: 'Copy failed', alert1: 'The administrator has not configured the ServiceTree(relation view), or you do not have permission to access it!',
deleteRelationConfirm: 'Confirm to remove selected {name} from current relationship?', copyFailed: 'Copy failed',
}, deleteRelationConfirm: 'Confirm to remove selected {name} from current relationship?',
tree: { batch: 'Batch',
tips1: 'Please go to Preference page first to complete your subscription!', grantTitle: 'Grant(read)',
subSettings: 'Settings', userPlaceholder: 'Please select users',
} rolePlaceholder: 'Please select roles',
} grantedByServiceTree: 'Granted By Service Tree:',
export default cmdb_en 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: {
tips1: 'Please go to Preference page first to complete your subscription!',
subSettings: 'Settings',
}
}
export default cmdb_en

View File

@@ -1,491 +1,502 @@
const cmdb_zh = { const cmdb_zh = {
relation: '关系', relation: '关系',
attribute: '属性', attribute: '属性',
menu: { menu: {
views: '视图', views: '视图',
config: '配置', config: '配置',
backend: '管理端', backend: '管理端',
ciTable: '资源数据', ciTable: '资源数据',
ciTree: '资源层级', ciTree: '资源层级',
ciSearch: '资源搜索', ciSearch: '资源搜索',
adCIs: '自动发现池', adCIs: '自动发现池',
preference: '我的订阅', preference: '我的订阅',
batchUpload: '批量导入', batchUpload: '批量导入',
citypeManage: '模型配置', citypeManage: '模型配置',
backendManage: '后台管理', backendManage: '后台管理',
customDashboard: '定制仪表盘', customDashboard: '定制仪表盘',
serviceTreeDefine: '服务树定义', serviceTreeDefine: '服务树定义',
citypeRelation: '模型关系', citypeRelation: '模型关系',
operationHistory: '操作审计', operationHistory: '操作审计',
relationType: '关系类型', relationType: '关系类型',
ad: '自动发现', ad: '自动发现',
cidetail: 'CI 详情' cidetail: 'CI 详情'
}, },
ciType: { ciType: {
ciType: '模型', ciType: '模型',
attributes: '模型属性', attributes: '模型属性',
relation: '模型关联', relation: '模型关联',
trigger: '触发器', trigger: '触发器',
attributeAD: '属性自动发现', attributeAD: '属性自动发现',
relationAD: '关系自动发现', relationAD: '关系自动发现',
grant: '权限配置', grant: '权限配置',
addGroup: '新增分组', addGroup: '新增分组',
editGroup: '修改分组', editGroup: '修改分组',
group: '分组', group: '分组',
attributeLibray: '属性库', attributeLibray: '属性库',
addCITypeInGroup: '在该组中新增CI模型', addCITypeInGroup: '在该组中新增CI模型',
addCIType: '新增CI模型', addCIType: '新增CI模型',
editGroupName: '编辑组名称', editGroupName: '编辑组名称',
deleteGroup: '删除该组', deleteGroup: '删除该组',
CITypeName: '模型名(英文)', CITypeName: '模型名(英文)',
English: '英文', English: '英文',
inputAttributeName: '请输入属性名', inputAttributeName: '请输入属性名',
attributeNameTips: '不能以数字开头,可以是英文 数字以及下划线 (_)', attributeNameTips: '不能以数字开头,可以是英文 数字以及下划线 (_)',
editCIType: '编辑模型', editCIType: '编辑模型',
defaultSort: '默认排序', defaultSort: '默认排序',
selectDefaultOrderAttr: '选择默认排序属性', selectDefaultOrderAttr: '选择默认排序属性',
asec: '正序', asec: '正序',
desc: '倒序', desc: '倒序',
uniqueKey: '唯一标识', uniqueKey: '唯一标识',
uniqueKeySelect: '请选择唯一标识', uniqueKeySelect: '请选择唯一标识',
notfound: '找不到想要的?', uniqueKeyTips: 'json、密码、计算属性、预定义值属性不能作为唯一标识',
cannotDeleteGroupTips: '该分组下有数据, 不能删除!', notfound: '找不到想要的?',
confirmDeleteGroup: '确定要删除分组 【{groupName}】 吗?', cannotDeleteGroupTips: '该分组下有数据, 不能删除!',
confirmDeleteCIType: '确定要删除模型 【{typeName}】 吗?', confirmDeleteGroup: '确定要删除分组 【{groupName}】 吗?',
uploading: '正在导入中', confirmDeleteCIType: '确定要删除模型 【{typeName}】 吗?',
uploadFailed: '导入失败,请稍后重试', uploading: '正在导入中',
addPlugin: '新建plugin', uploadFailed: '导入失败,请稍后重试',
deletePlugin: '删除plugin', addPlugin: '新建plugin',
confirmDeleteADT: '确认删除 【{pluginName}】', deletePlugin: '删除plugin',
attributeMap: '字段映射', confirmDeleteADT: '确认删除 【{pluginName}】',
autoDiscovery: '自动发现', attributeMap: '字段映射',
node: '节点', autoDiscovery: '自动发现',
adExecConfig: '执行配置', node: '节点',
adExecTarget: '执行机器', adExecConfig: '执行配置',
oneagentIdTips: '请输入以0x开头的16进制OneAgent ID', adExecTarget: '执行机器',
selectFromCMDBTips: '从CMDB中选择 ', oneagentIdTips: '请输入以0x开头的16进制OneAgent ID',
adAutoInLib: '自动入库', selectFromCMDBTips: '从CMDB中选择 ',
adInterval: '采集频率', adAutoInLib: '自动入库',
byInterval: '按间隔', adInterval: '采集频率',
allNodes: '所有节点', byInterval: '按间隔',
specifyNodes: '指定节点', allNodes: '所有节点',
specifyNodesTips: '请填写指定节点', specifyNodes: '指定节点',
username: '用户名', specifyNodesTips: '请填写指定节点!',
password: '密码', username: '用户名',
link: '链接', password: '密码',
list: '多值', link: '链接',
listTips: '字段的值是1个或者多个接口返回的值的类型是list', list: '多值',
computeForAllCITips: '所有CI触发计算', listTips: '字段的值是1个或者多个接口返回的值的类型是list',
confirmcomputeForAllCITips: '确认触发所有CI计算', computeForAllCITips: '所有CI触发计算',
isUnique: '是否唯一', confirmcomputeForAllCITips: '确认触发所有CI的计算',
unique: '唯一', isUnique: '是否唯一',
isChoice: '是否选择', unique: '唯一',
defaultShow: '默认显示', isChoice: '是否选择',
defaultShowTips: 'CI实例表格默认展示该字段', defaultShow: '默认显示',
isSortable: '可排序', defaultShowTips: 'CI实例表格默认展示该字段',
isIndex: '是否索引', isSortable: '可排序',
index: '索引', isIndex: '是否索引',
indexTips: '字段可被用于检索,加速查询', index: '索引',
confirmDelete: '确认删除【{name}】?', indexTips: '字段可被用于检索,加速查询',
confirmDelete2: '确认删除?', confirmDelete: '确认删除【{name}】?',
computeSuccess: '触发成功!', confirmDelete2: '确认删除?',
basicConfig: '基础设置', computeSuccess: '触发成功!',
AttributeName: '属性名(英文)', basicConfig: '基础设置',
DataType: '数据类型', AttributeName: '属性名(英文)',
defaultValue: '默认值', DataType: '数据类型',
autoIncID: '自增ID', defaultValue: '默认值',
customTime: '自定义时间', autoIncID: '自增ID',
advancedSettings: '高级设置', customTime: '自定义时间',
font: '字体', advancedSettings: '高级设置',
color: '颜色', font: '字体',
choiceValue: '预定义值', color: '颜色',
computedAttribute: '计算属性', choiceValue: '预定义值',
computedAttributeTips: '该属性的值是通过模型的其它属性构建的表达式或者执行一段代码的方式计算而来,属性的引用方法为: {{ 属性名 }}', computedAttribute: '计算属性',
addAttribute: '新增属性', computedAttributeTips: '该属性的值是通过模型的其它属性构建的表达式或者执行一段代码的方式计算而来,属性的引用方法为: {{ 属性名 }}',
existedAttributes: '已有属性', addAttribute: '新增属性',
editAttribute: '编辑属性', existedAttributes: '已有属性',
addAttributeTips1: '选中排序,则必须也要选中!', editAttribute: '编辑属性',
uniqueConstraint: '唯一校验', addAttributeTips1: '选中排序,则必须也要选中!',
up: '上移', uniqueConstraint: '唯一校验',
down: '移', up: '移',
selectAttribute: '添加属性', down: '下移',
groupExisted: '分组名称已存在', selectAttribute: '添加属性',
attributeSortedTips: '其他分组中的属性不能进行排序,如需排序请先拖至自定义的分组!', groupExisted: '分组名称已存在',
buildinAttribute: '内置字段', attributeSortedTips: '其他分组中的属性不能进行排序,如需排序请先拖至自定义的分组!',
expr: '表达式', buildinAttribute: '内置字段',
code: '代码', expr: '表达式',
apply: '应用', code: '代码',
continueAdd: '继续添加', apply: '应用',
filter: '过滤', continueAdd: '继续添加',
choiceOther: '其他模型属性', filter: '过滤',
choiceWebhookTips: '返回的结果按字段来过滤,层级嵌套用##分隔比如k1##k2web请求返回{k1: [{k2: 1}, {k2: 2}]}, 解析结果为[1, 2]', choiceOther: '其他模型属性',
selectCIType: '请选择CMDB模型', choiceWebhookTips: '返回的结果按字段来过滤,层级嵌套用##分隔比如k1##k2web请求返回{k1: [{k2: 1}, {k2: 2}]}, 解析结果为[1, 2]',
selectCITypeAttributes: '请选择模型属性', selectCIType: '请选择CMDB模型',
selectAttributes: '请选择属性', selectCITypeAttributes: '请选择模型属性',
choiceScriptDemo: 'class ChoiceValue(object):\n @staticmethod\n def values():\n """\n 执行入口, 返回预定义值\n :return: 返回一个列表, 值的类型同属性的类型\n 例如:\n return ["在线", "下线"]\n """\n return []', selectAttributes: '请选择属性',
valueExisted: '当前值已存在!', choiceScriptDemo: 'class ChoiceValue(object):\n @staticmethod\n def values():\n """\n 执行入口, 返回预定义值\n :return: 返回一个列表, 值的类型同属性的类型\n 例如:\n return ["在线", "下线"]\n """\n return []',
addRelation: '新增关系', valueExisted: '当前值已存在!',
sourceCIType: '源模型', addRelation: '新增关系',
sourceCITypeTips: '请选择源模型', sourceCIType: '源模型',
dstCIType: '目标模型', sourceCITypeTips: '请选择源模型',
dstCITypeTips: '请选择目标模型', dstCIType: '目标模型',
relationType: '关联类型', dstCITypeTips: '请选择目标模型',
relationTypeTips: '请选择关联类型', relationType: '关联类型',
isParent: '被', relationTypeTips: '请选择关联类型',
relationConstraint: '关系约束', isParent: '',
relationConstraintTips: '请选择关系约束', relationConstraint: '关系约束',
one2Many: '一对多', relationConstraintTips: '请选择关系约束',
one2One: '一对', one2Many: '一对',
many2Many: '多对多', one2One: '一对一',
basicInfo: '基本信息', many2Many: '多对多',
nameInputTips: '请输入名称', basicInfo: '基本信息',
triggerDataChange: '数据变更', nameInputTips: '请输入名称',
triggerDate: '日期属性', triggerDataChange: '数据变更',
triggerEnable: '开启', triggerDate: '日期属性',
descInput: '请输入备注', triggerEnable: '开启',
triggerCondition: '触发条件', descInput: '请输入备注',
addInstance: '新增实例', triggerCondition: '触发条件',
deleteInstance: '删除实例', addInstance: '新增实例',
changeInstance: '实例变更', deleteInstance: '删除实例',
selectMutipleAttributes: '请选择属性(多选)', changeInstance: '实例变更',
selectSingleAttribute: '请选择属性(选)', selectMutipleAttributes: '请选择属性(选)',
beforeDays: '提前', selectSingleAttribute: '请选择属性(单选)',
days: '', beforeDays: '提前',
notifyAt: '发送时间', days: '',
notify: '通知', notifyAt: '发送时间',
triggerAction: '触发动作', notify: '通知',
receivers: '收件人', triggerAction: '触发动作',
emailTips: '请输入邮箱,多个邮箱用;分隔', receivers: '收件人',
customEmail: '自定义收件人', emailTips: '请输入邮箱,多个邮箱用;分隔',
notifySubject: '通知标题', customEmail: '自定义收件人',
notifySubjectTips: '请输入通知标题', notifySubject: '通知标题',
notifyContent: '内容', notifySubjectTips: '请输入通知标题',
notifyMethod: '通知方式', notifyContent: '内容',
botSelect: '请选择机器人', notifyMethod: '通知方式',
refAttributeTips: '标题、内容可以引用该模型的属性值,引用方法为: {{ attr_name }}', botSelect: '请选择机器人',
webhookRefAttributeTips: '请求参数可以引用该模型的属性值,引用方法为: {{ attr_name }}', refAttributeTips: '标题、内容可以引用该模型的属性值,引用方法为: {{ attr_name }}',
newTrigger: '新增触发器', webhookRefAttributeTips: '请求参数可以引用该模型的属性值,引用方法为: {{ attr_name }}',
editTriggerTitle: '编辑触发器 {name}', newTrigger: '新增触发器',
newTriggerTitle: '新增触发器 {name}', editTriggerTitle: '编辑触发器 {name}',
confirmDeleteTrigger: '确认删除该触发器吗?', newTriggerTitle: '新增触发器 {name}',
int: '整数', confirmDeleteTrigger: '确认删除该触发器吗?',
float: '浮点数', int: '数',
text: '文本', float: '浮点数',
datetime: '日期时间', text: '文本',
date: '日期', datetime: '日期时间',
time: '时间', date: '日期',
json: 'JSON', time: '时间',
event: '事件', json: 'JSON',
reg: '正则校验', event: '事件',
isInherit: '是否继承', reg: '正则校验',
inheritType: '继承模型', isInherit: '是否继承',
inheritTypePlaceholder: '请选择继承模型(多选)', inheritType: '继承模型',
inheritFrom: '属性继承自{name}', inheritTypePlaceholder: '请选择继承模型(多选)',
groupInheritFrom: '请至{name}进行修改' inheritFrom: '属性继承自{name}',
}, groupInheritFrom: '请至{name}进行修改'
components: { },
unselectAttributes: '未选属性', components: {
selectAttributes: '选属性', unselectAttributes: '选属性',
downloadCI: '导出数据', selectAttributes: '已选属性',
filename: '文件名', downloadCI: '导出数据',
filenameInputTips: '请输入文件名', filename: '文件名',
saveType: '保存类型', filenameInputTips: '请输入文件名',
saveTypeTips: '请选择保存类型', saveType: '保存类型',
xlsx: 'Excel工作簿(*.xlsx)', saveTypeTips: '请选择保存类型',
csv: 'CSV(逗号分隔)(*.csv)', xlsx: 'Excel工作簿(*.xlsx)',
html: '网页(*.html)', csv: 'CSV(逗号分隔)(*.csv)',
xml: 'XML数据(*.xml)', html: '网页(*.html)',
txt: '文本文件(制表符分隔)(*.txt)', xml: 'XML数据(*.xml)',
grantUser: '授权用户/部门', txt: '文本文件(制表符分隔)(*.txt)',
grantRole: '授权角色', grantUser: '授权用户/部门',
confirmRevoke: '确认删除 【{name}】 的 【授权】 权限?', grantRole: '授权角色',
readAttribute: '查看字段', confirmRevoke: '确认删除 【{name}】 的 【授权】 权限?',
readCI: '查看实例', readAttribute: '查看字段',
config: '配置', readCI: '查看实例',
ciTypeGrant: '模型权限', config: '配置',
ciGrant: '实例权限', ciTypeGrant: '模型权限',
attributeGrant: '字段权限', ciGrant: '实例权限',
relationGrant: '关系权限', attributeGrant: '字段权限',
perm: '权限', relationGrant: '关系权限',
all: '全部', perm: '权限',
customize: '自定义', all: '全部',
none: '', customize: '自定义',
customizeFilterName: '请输入自定义筛选条件名', none: '无',
colorPickerError: '初始化颜色格式错误,使用#fff或rgb格式', customizeFilterName: '请输入自定义筛选条件名',
example: '示例值', colorPickerError: '初始化颜色格式错误,使用#fff或rgb格式',
aliyun: '阿里云', example: '示例值',
tencentcloud: '腾讯云', aliyun: '阿里云',
huaweicloud: '华为云', tencentcloud: '腾讯云',
beforeChange: '变更前', huaweicloud: '华为云',
afterChange: '变更', beforeChange: '变更',
noticeContentTips: '请输入通知内容', afterChange: '变更后',
saveQuery: '保存筛选条件', noticeContentTips: '请输入通知内容',
pleaseSearch: '请查找', saveQuery: '保存筛选条件',
conditionFilter: '条件过滤', pleaseSearch: '请查找',
attributeDesc: '属性说明', conditionFilter: '条件过滤',
ciSearchTips: '1. json属性不能搜索<br />2. 搜索内容包括逗号, 则需转义 ,<br />3. 只搜索索引属性, 非索引属性使用条件过滤', attributeDesc: '属性说明',
ciSearchTips2: '例: q=hostname:*0.0.0.0*', ciSearchTips: '1. json、密码、链接属性不能搜索\n2. 搜索内容包括逗号, 则需转义\n3. 只搜索索引属性, 非索引属性使用条件过滤',
subCIType: '订阅模型', ciSearchTips2: '例: q=hostname:*0.0.0.0*',
already: '', subCIType: '订阅模型',
not: '', already: '',
sub: '订阅', not: '',
selectBelow: '请在下方进行选择', sub: '订阅',
subSuccess: '订阅成功', selectBelow: '请在下方进行选择',
selectMethods: '请选择方式', subSuccess: '订阅成功',
noAuthRequest: '暂无请求认证', selectMethods: '请选择方式',
noParamRequest: '暂无参数认证', noAuthRequest: '暂无请求认证',
requestParam: '请求参数', noParamRequest: '暂无参数认证',
param: '参数{param}', requestParam: '请求参数',
value: '值{value}', param: '参数{param}',
clear: '清空', value: '值{value}',
}, clear: '清空',
batch: { },
downloadFailed: '失败下载', batch: {
unselectCIType: '尚未选择模板类型', downloadFailed: '失败下载',
pleaseUploadFile: '请上传文件', unselectCIType: '尚未选择模板类型',
batchUploadCanceled: '批量上传已取消', pleaseUploadFile: '请上传文件',
selectCITypeTips: '请选择模板类型', batchUploadCanceled: '批量上传已取消',
downloadTemplate: '下载模板', selectCITypeTips: '请选择模板类型',
drawTips: '点击或拖拽文件至此上传!', downloadTemplate: '下载模板',
supportFileTypes: '支持文件类型xlsxlsx', drawTips: '点击或拖拽文件至此上传!',
uploadResult: '上传结果', supportFileTypes: '支持文件类型xlsxlsx',
total: '', uploadResult: '上传结果',
successItems: '条,已成功', total: '',
failedItems: '条,失败', successItems: '条,已成功',
items: '条', failedItems: '条,失败',
errorTips: '错误信息', items: '',
requestFailedTips: '请求出现错误,请稍后再试', errorTips: '错误信息',
requestSuccessTips: '批量上传已完成', requestFailedTips: '请求出现错误,请稍后再试',
}, requestSuccessTips: '批量上传已完成',
preference: { },
mySub: '我的订阅', preference: {
sub: '订阅', mySub: '我的订阅',
cancelSub: '取消订阅', sub: '订阅',
editSub: '编辑订阅', cancelSub: '取消订阅',
peopleSub: '位同事已订阅', editSub: '编辑订阅',
noSub: '暂无同事订阅', peopleSub: '同事订阅',
cancelSubSuccess: '取消订阅成功', noSub: '暂无同事订阅',
confirmcancelSub: '确认取消订阅', cancelSubSuccess: '取消订阅成功',
confirmcancelSub2: '确认取消订阅 {name} 吗?', confirmcancelSub: '确认取消订阅',
of: '的', confirmcancelSub2: '确认取消订阅 {name} 吗?',
hoursAgo: '小时前', of: '',
daysAgo: '前', hoursAgo: '小时前',
monthsAgo: '前', daysAgo: '前',
yearsAgo: '前', monthsAgo: '前',
just: '刚刚', yearsAgo: '年前',
}, just: '刚刚',
custom_dashboard: { },
charts: '图表', custom_dashboard: {
newChart: '新增图表', charts: '图表',
editChart: '编辑图表', newChart: '新增图表',
title: '标题', editChart: '编辑图表',
titleTips: '请输入图表标题', title: '标题',
calcIndicators: '计算指标', titleTips: '请输入图表标题',
dimensions: '维度', calcIndicators: '计算指标',
selectDimensions: '请选择维度', dimensions: '维度',
quantity: '数量', selectDimensions: '请选择维度',
childCIType: '关系模型', quantity: '数量',
level: '层级', childCIType: '关系模型',
levelTips: '请输入关系层级', level: '层级',
preview: '预览', levelTips: '请输入关系层级',
showIcon: '是否显示icon', preview: '预览',
chartType: '图表类型', showIcon: '是否显示icon',
dataFilter: '数据筛选', chartType: '图表类型',
format: '格式', dataFilter: '数据筛选',
fontColor: '字体颜色', format: '格式',
backgroundColor: '背景颜色', fontColor: '字体颜色',
chartColor: '图表颜色', backgroundColor: '背景颜色',
chartLength: '图表长度', chartColor: '图表颜色',
barType: '柱状图类型', chartLength: '图表长度',
stackedBar: '堆积柱状图', barType: '柱状图类型',
multipleSeriesBar: '多系列柱状图', stackedBar: '堆积柱状图',
axis: '', multipleSeriesBar: '多系列柱状图',
direction: '方向', axis: '',
lowerShadow: '下方阴影', direction: '方向',
count: '指标', lowerShadow: '下方阴影',
bar: '柱状图', count: '指标',
line: '折线图', bar: '柱状图',
pie: '饼状图', line: '折线图',
table: '表格', pie: '饼状图',
default: '默认', table: '表格',
relation: '关系', default: '默认',
noCustomDashboard: '管理员暂未定制仪表盘', relation: '关系',
}, noCustomDashboard: '管理员暂未定制仪表盘',
preference_relation: { },
newServiceTree: '新增服务树', preference_relation: {
serviceTreeName: '服务树', newServiceTree: '新增服务树',
public: '公开', serviceTreeName: '服务树名',
saveLayout: '保存布局', public: '公开',
childNodesNotFound: '不存在子节点,不能形成业务关系,请重新选择!', saveLayout: '保存布局',
tips1: '不能与当前选中节点形成视图,请重新选择!', childNodesNotFound: '不存在子节点,不能形成业务关系,请重新选择!',
tips2: '请输入新增服务树名', tips1: '不能与当前选中节点形成视图,请重新选择',
tips3: '请选择至少两个节点', tips2: '请输入新增服务树名',
}, tips3: '请选择至少两个节点!',
history: { },
ciChange: 'CI变更', history: {
relationChange: '关系变更', ciChange: 'CI变更',
ciTypeChange: '模型变更', relationChange: '关系变更',
triggerHistory: '触发历史', ciTypeChange: '模型变更',
opreateTime: '操作时间', triggerHistory: '触发历史',
user: '用户', opreateTime: '操作时间',
userTips: '输入筛选用户', user: '用户',
filter: '筛选', userTips: '输入筛选用户名',
filterOperate: '筛选操作', filter: '筛选',
attribute: '属性', filterOperate: '筛选操作',
old: '', attribute: '属性',
new: '', old: '',
noUpdate: '没有修改', new: '',
itemsPerPage: '/页', noUpdate: '没有修改',
triggerName: '触发器名称', itemsPerPage: '/页',
event: '事件', triggerName: '触发器名称',
action: '动作', event: '事件',
status: '状态', action: '动作',
done: '已完成', status: '状态',
undone: '完成', done: '完成',
triggerTime: '触发时间', undone: '未完成',
totalItems: '共 {total} 条记录', triggerTime: '触发时间',
pleaseSelect: '请选择', totalItems: '共 {total} 条记录',
startTime: '开始时间', pleaseSelect: '请选择',
endTime: '结束时间', startTime: '开始时间',
deleteCIType: '删除模型', endTime: '结束时间',
addCIType: '新增模型', deleteCIType: '删除模型',
updateCIType: '修改模型', addCIType: '新增模型',
addAttribute: '新增属性', updateCIType: '修改模型',
updateAttribute: '修改属性', addAttribute: '新增属性',
deleteAttribute: '删除属性', updateAttribute: '修改属性',
addTrigger: '新增触发器', deleteAttribute: '删除属性',
updateTrigger: '修改触发器', addTrigger: '新增触发器',
deleteTrigger: '删除触发器', updateTrigger: '修改触发器',
addUniqueConstraint: '新增联合唯一', deleteTrigger: '删除触发器',
updateUniqueConstraint: '修改联合唯一', addUniqueConstraint: '新增联合唯一',
deleteUniqueConstraint: '删除联合唯一', updateUniqueConstraint: '修改联合唯一',
addRelation: '新增关系', deleteUniqueConstraint: '删除联合唯一',
deleteRelation: '删除关系', addRelation: '新增关系',
noModifications: '没有修改', deleteRelation: '删除关系',
attr: '属性名', noModifications: '没有修改',
attrId: '属性ID', attr: '属性',
changeDescription: '属性ID{attr_id},提前:{before_days}天,主题:{subject}\n内容{body}\n通知时间{notify_at}' attrId: '属性ID',
}, changeDescription: '属性ID{attr_id},提前:{before_days}天,主题:{subject}\n内容{body}\n通知时间{notify_at}'
relation_type: { },
addRelationType: '新增关系类型', relation_type: {
nameTips: '请输入类型', addRelationType: '新增关系类型',
}, nameTips: '请输入类型名',
ad: { },
upload: '规则导入', ad: {
download: '规则导', upload: '规则导',
accept: '入库', download: '规则导出',
acceptBy: '入库', accept: '入库',
acceptTime: '入库时间', acceptBy: '入库',
confirmAccept: '确认入库?', acceptTime: '入库时间',
acceptSuccess: '入库成功', confirmAccept: '确认入库?',
isAccept: '是否入库', acceptSuccess: '入库成功',
deleteADC: '确认删除该条数据?', isAccept: '是否入库',
batchDelete: '确认删除这些数据?', deleteADC: '确认删除该条数据?',
agent: '内置 & 插件', batchDelete: '确认删除这些数据?',
snmp: '网络设备', agent: '内置 & 插件',
http: '公有云资源', snmp: '网络设备',
rule: '自动发现规则', http: '公有云资源',
timeout: '超时错误', rule: '自动发现规则',
mode: '模式', timeout: '超时错误',
collectSettings: '采集设置', mode: '模式',
updateFields: '更新字段', collectSettings: '采集设置',
pluginScript: `# -*- coding:utf-8 -*- updateFields: '更新字段',
pluginScript: `# -*- coding:utf-8 -*-
import json
import json
class AutoDiscovery(object):
class AutoDiscovery(object):
@property
def unique_key(self): @property
""" def unique_key(self):
"""
:return: 返回唯一属性的名字
""" :return: 返回唯一属性的名字
return """
return
@staticmethod
def attributes(): @staticmethod
""" def attributes():
定义属性字段 """
:return: 返回属性字段列表, 列表项是(名称, 类型, 描述), 名称必须是英文 定义属性字段
类型: String Integer Float Date DateTime Time JSON :return: 返回属性字段列表, 列表项是(名称, 类型, 描述), 名称必须是英文
例如: 类型: String Integer Float Date DateTime Time JSON
return [ 例如:
("ci_type", "String", "模型名称"), return [
("private_ip", "String", "内网IP, 多值逗号分隔") ("ci_type", "String", "模型名称"),
] ("private_ip", "String", "内网IP, 多值逗号分隔")
""" ]
return [] """
return []
@staticmethod
def run(): @staticmethod
""" def run():
执行入口, 返回采集的属性值 """
:return: 返回一个列表, 列表项是字典, 字典key是属性名称, value是属性值 执行入口, 返回采集的属性值
例如: :return: 返回一个列表, 列表项是字典, 字典key是属性名称, value是属性值
return [dict(ci_type="server", private_ip="192.168.1.1")] 例如:
""" return [dict(ci_type="server", private_ip="192.168.1.1")]
return [] """
return []
if __name__ == "__main__":
result = AutoDiscovery().run() if __name__ == "__main__":
if isinstance(result, list): result = AutoDiscovery().run()
print("AutoDiscovery::Result::{}".format(json.dumps(result))) if isinstance(result, list):
else: print("AutoDiscovery::Result::{}".format(json.dumps(result)))
print("ERROR: 采集返回必须是列表") else:
`, print("ERROR: 采集返回必须是列表")
server: '物理机', `,
vserver: '虚拟机', server: '物理机',
nic: '网卡', vserver: '虚拟机',
disk: '硬盘', nic: '网卡',
}, disk: '硬盘',
ci: { },
attributeDesc: '属性说明', ci: {
selectRows: '选取:{rows} 项', attributeDesc: '属性说明',
addRelation: '添加关系', selectRows: '选取:{rows} 项',
all: '全部', addRelation: '添加关系',
batchUpdate: '批量修改', all: '全部',
batchUpdateConfirm: '确认要批量修改吗?', batchUpdate: '批量修改',
batchUpdateInProgress: '正在批量修改', batchUpdateConfirm: '确认要批量修改吗?',
batchUpdateInProgress2: '正在批量修改,共{total}个,成功{successNum}个,失败{errorNum}个', batchUpdateInProgress: '正在批量修改',
batchDeleting: '正在删除...', batchUpdateInProgress2: '正在批量修改,共{total}个,成功{successNum}个,失败{errorNum}个',
batchDeleting2: '正在删除,共{total}个,成功{successNum}个,失败{errorNum}个', batchDeleting: '正在删除...',
copyFailed: '复制失败!', batchDeleting2: '正在删除,共{total}个,成功{successNum}个,失败{errorNum}个',
noLevel: '无层级关系', copyFailed: '复制失败',
batchAddRelation: '批量添加关系', noLevel: '无层级关系',
history: '操作历史', batchAddRelation: '批量添加关系',
topo: '拓扑', history: '操作历史',
table: '表格', topo: '拓扑',
m2mTips: '当前模型关系为多对多,请前往关系视图进行增删操作', table: '表格',
confirmDeleteRelation: '确认删除关系?', m2mTips: '当前模型关系为多对多,请前往关系视图进行增删操作',
tips1: '多个值使用,分割', confirmDeleteRelation: '确认删除关系?',
tips2: '可根据需要修改字段,当值为 空 时,则该字段 置空', tips1: '多个值使用,分割',
tips3: '请选择需要修改字段', tips2: '可根据需要修改字段,当值为 空 时,则该字段 置空',
tips4: '必须至少选择一个字段', tips3: '请选择需要修改的字段',
tips5: '搜索 名称 | 别名', tips4: '必须至少选择一个字段',
tips6: '加快检索, 可以全文搜索, 无需使用条件过滤\n\n json目前不支持建索引 \n\n文本字符长度超过190不能建索引', tips5: '搜索 名称 | 别名',
tips7: '表现形式是下拉框, 值必须在预定义值里', tips6: '加快检索, 可以全文搜索, 无需使用条件过滤\n\n json、链接、密码目前不支持建索引 \n\n文本字符长度超过190不能建索引',
tips8: '多值, 比如内网IP', tips7: '表现形式是下拉框, 值必须在预定义值里',
tips9: '仅针对前端', tips8: '多值, 比如内网IP',
tips10: '模型的其他属性通过表达式的方式计算出来\n\n一个代码片段计算返回的值', tips9: '仅针对前端',
newUpdateField: '新增修改字段', tips10: '模型的其他属性通过表达式的方式计算出来\n\n一个代码片段计算返回的值',
attributeSettings: '字段设置', newUpdateField: '新增修改字段',
share: '分享', attributeSettings: '字段设置',
noPermission: '暂无权限' share: '分享',
}, noPermission: '暂无权限'
serviceTree: { },
deleteNode: '删除节点', serviceTree: {
tips1: '例q=os_version:centos&sort=os_version', deleteNode: '删除节点',
tips2: '表达式搜索', tips1: 'q=os_version:centos&sort=os_version',
alert1: '管理员 还未配置业务关系, 或者你无权限访问!', tips2: '表达式搜索',
copyFailed: '复制失败', alert1: '管理员 还未配置业务关系, 或者你无权限访问!',
deleteRelationConfirm: '确认将选中的 {name} 从当前关系中删除?', copyFailed: '复制失败',
}, deleteRelationConfirm: '确认将选中的 {name} 从当前关系中删除?',
tree: { batch: '批量操作',
tips1: '请先到 我的订阅 页面完成订阅!', grantTitle: '授权(查看权限)',
subSettings: '订阅设置', userPlaceholder: '请选择用户',
} rolePlaceholder: '请选择角色',
} grantedByServiceTree: '服务树授权:',
export default cmdb_zh grantedByServiceTreeTips: '请先在服务树里删掉节点授权',
peopleHasRead: '当前有查看权限的人员:',
authorizationPolicy: '实例授权策略:',
idAuthorizationPolicy: '按节点授权的:',
view: '查看权限'
},
tree: {
tips1: '请先到 我的订阅 页面完成订阅!',
subSettings: '订阅设置',
}
}
export default cmdb_zh

View File

@@ -1,394 +1,430 @@
<template> <template>
<CustomDrawer <CustomDrawer
:title="title + CIType.alias" :title="title + CIType.alias"
width="800" width="800"
@close="handleClose" @close="handleClose"
:maskClosable="false" :maskClosable="false"
:visible="visible" :visible="visible"
wrapClassName="create-instance-form" wrapClassName="create-instance-form"
:bodyStyle="{ paddingTop: 0 }" :bodyStyle="{ paddingTop: 0 }"
:headerStyle="{ borderBottom: 'none' }" :headerStyle="{ borderBottom: 'none' }"
> >
<div class="custom-drawer-bottom-action"> <div class="custom-drawer-bottom-action">
<a-button @click="handleClose">{{ $t('cancel') }}</a-button> <a-button @click="handleClose">{{ $t('cancel') }}</a-button>
<a-button type="primary" @click="createInstance">{{ $t('submit') }}</a-button> <a-button type="primary" @click="createInstance">{{ $t('submit') }}</a-button>
</div> </div>
<template v-if="action === 'create'"> <template v-if="action === 'create'">
<template v-for="group in attributesByGroup"> <template v-for="group in attributesByGroup">
<CreateInstanceFormByGroup <CreateInstanceFormByGroup
:ref="`createInstanceFormByGroup_${group.id}`" :ref="`createInstanceFormByGroup_${group.id}`"
:key="group.id || group.name" :key="group.id || group.name"
:group="group" :group="group"
@handleFocusInput="handleFocusInput" @handleFocusInput="handleFocusInput"
:attributeList="attributeList" :attributeList="attributeList"
/> />
</template> </template>
<template v-if="parentsType && parentsType.length"> <template v-if="parentsType && parentsType.length">
<a-divider style="font-size:14px;margin:14px 0;font-weight:700;">{{ <a-divider style="font-size:14px;margin:14px 0;font-weight:700;">{{
$t('cmdb.menu.citypeRelation') $t('cmdb.menu.citypeRelation')
}}</a-divider> }}</a-divider>
<a-form> <a-form>
<a-row :gutter="24" align="top" type="flex"> <a-row :gutter="24" align="top" type="flex">
<a-col :span="12" v-for="item in parentsType" :key="item.id"> <a-col :span="12" v-for="item in parentsType" :key="item.id">
<a-form-item :label="item.alias || item.name" :colon="false"> <a-form-item :label="item.alias || item.name" :colon="false">
<a-input-group compact style="width: 100%"> <a-input-group compact style="width: 100%">
<a-select v-model="parentsForm[item.name].attr"> <a-select v-model="parentsForm[item.name].attr">
<a-select-option <a-select-option
:title="attr.alias || attr.name" :title="attr.alias || attr.name"
v-for="attr in item.attributes" v-for="attr in item.attributes"
:key="attr.name" :key="attr.name"
:value="attr.name" :value="attr.name"
> >
{{ attr.alias || attr.name }} {{ attr.alias || attr.name }}
</a-select-option> </a-select-option>
</a-select> </a-select>
<a-input <a-input
:placeholder="$t('cmdb.ci.tips1')" :placeholder="$t('cmdb.ci.tips1')"
v-model="parentsForm[item.name].value" v-model="parentsForm[item.name].value"
style="width: 50%" style="width: 50%"
/> />
</a-input-group> </a-input-group>
</a-form-item> </a-form-item>
</a-col> </a-col>
</a-row> </a-row>
</a-form> </a-form>
</template> </template>
</template> </template>
<template v-if="action === 'update'"> <template v-if="action === 'update'">
<a-form :form="form"> <a-form :form="form">
<p>{{ $t('cmdb.ci.tips2') }}</p> <p>{{ $t('cmdb.ci.tips2') }}</p>
<a-row :gutter="24" v-for="list in batchUpdateLists" :key="list.name"> <a-row :gutter="24" v-for="list in batchUpdateLists" :key="list.name">
<a-col :span="11"> <a-col :span="11">
<a-form-item> <a-form-item>
<el-select showSearch size="small" filterable v-model="list.name" :placeholder="$t('cmdb.ci.tips3')"> <el-select showSearch size="small" filterable v-model="list.name" :placeholder="$t('cmdb.ci.tips3')">
<el-option <el-option
v-for="attr in attributeList" v-for="attr in attributeList"
:key="attr.name" :key="attr.name"
:value="attr.name" :value="attr.name"
:disabled="batchUpdateLists.findIndex((item) => item.name === attr.name) > -1" :disabled="batchUpdateLists.findIndex((item) => item.name === attr.name) > -1"
:label="attr.alias || attr.name" :label="attr.alias || attr.name"
> >
</el-option> </el-option>
</el-select> </el-select>
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="11"> <a-col :span="11">
<a-form-item> <a-form-item>
<a-select <a-select
:style="{ width: '100%' }" :style="{ width: '100%' }"
v-decorator="[list.name, { rules: [{ required: false }] }]" v-decorator="[list.name, { rules: [{ required: false }] }]"
:placeholder="$t('placeholder2')" :placeholder="$t('placeholder2')"
v-if="getFieldType(list.name).split('%%')[0] === 'select'" v-if="getFieldType(list.name).split('%%')[0] === 'select'"
:mode="getFieldType(list.name).split('%%')[1] === 'multiple' ? 'multiple' : 'default'" :mode="getFieldType(list.name).split('%%')[1] === 'multiple' ? 'multiple' : 'default'"
showSearch showSearch
allowClear allowClear
> >
<a-select-option <a-select-option
:value="choice[0]" :value="choice[0]"
:key="'New_' + choice + choice_idx" :key="'New_' + choice + choice_idx"
v-for="(choice, choice_idx) in getSelectFieldOptions(list.name)" v-for="(choice, choice_idx) in getSelectFieldOptions(list.name)"
> >
<span :style="choice[1] ? choice[1].style || {} : {}"> <span :style="choice[1] ? choice[1].style || {} : {}">
<ops-icon <ops-icon
:style="{ color: choice[1].icon.color }" :style="{ color: choice[1].icon.color }"
v-if="choice[1] && choice[1].icon && choice[1].icon.name" v-if="choice[1] && choice[1].icon && choice[1].icon.name"
:type="choice[1].icon.name" :type="choice[1].icon.name"
/> />
{{ choice[0] }} {{ choice[0] }}
</span> </span>
</a-select-option> </a-select-option>
</a-select> </a-select>
<a-input-number <a-input-number
v-decorator="[list.name, { rules: [{ required: false }] }]" v-decorator="[list.name, { rules: [{ required: false }] }]"
style="width: 100%" style="width: 100%"
v-if="getFieldType(list.name) === 'input_number'" v-if="getFieldType(list.name) === 'input_number'"
/> />
<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) == 'date' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
v-if="getFieldType(list.name) === 'date' || getFieldType(list.name) === 'datetime'" v-if="getFieldType(list.name) === 'date' || getFieldType(list.name) === 'datetime'"
:showTime="getFieldType(list.name) === 'date' ? false : { format: 'HH:mm:ss' }" :showTime="getFieldType(list.name) === 'date' ? false : { format: 'HH:mm:ss' }"
/> />
<a-input <a-input
v-if="getFieldType(list.name) === 'input'" v-if="getFieldType(list.name) === 'input'"
@focus="(e) => handleFocusInput(e, list)" @focus="(e) => handleFocusInput(e, list)"
v-decorator="[list.name, { rules: [{ required: false }] }]" v-decorator="[list.name, { rules: [{ required: false }] }]"
/> />
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="2"> <a-col :span="2">
<a-form-item> <a-form-item>
<a :style="{ color: 'red', marginTop: '2px' }" @click="handleDelete(list.name)"> <a :style="{ color: 'red', marginTop: '2px' }" @click="handleDelete(list.name)">
<a-icon type="delete" /> <a-icon type="delete" />
</a> </a>
</a-form-item> </a-form-item>
</a-col> </a-col>
</a-row> </a-row>
<a-button type="primary" ghost icon="plus" @click="handleAdd">{{ $t('cmdb.ci.newUpdateField') }}</a-button> <a-button type="primary" ghost icon="plus" @click="handleAdd">{{ $t('cmdb.ci.newUpdateField') }}</a-button>
</a-form> </a-form>
</template> </template>
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" /> <JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
</CustomDrawer> </CustomDrawer>
</template> </template>
<script> <script>
import _ from 'lodash' import _ from 'lodash'
import moment from 'moment' import moment from 'moment'
import { Select, Option } from 'element-ui' import { Select, Option } from 'element-ui'
import { getCIType, getCITypeGroupById } from '@/modules/cmdb/api/CIType' import { getCIType, getCITypeGroupById } from '@/modules/cmdb/api/CIType'
import { addCI } from '@/modules/cmdb/api/ci' import { addCI } from '@/modules/cmdb/api/ci'
import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue' import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue'
import { valueTypeMap } from '../../../utils/const' import { valueTypeMap } from '../../../utils/const'
import CreateInstanceFormByGroup from './createInstanceFormByGroup.vue' import CreateInstanceFormByGroup from './createInstanceFormByGroup.vue'
import { getCITypeParent, getCanEditByParentIdChildId } from '@/modules/cmdb/api/CITypeRelation' import { getCITypeParent, getCanEditByParentIdChildId } from '@/modules/cmdb/api/CITypeRelation'
export default { export default {
name: 'CreateInstanceForm', name: 'CreateInstanceForm',
components: { components: {
ElSelect: Select, ElSelect: Select,
ElOption: Option, ElOption: Option,
JsonEditor, JsonEditor,
CreateInstanceFormByGroup, CreateInstanceFormByGroup,
}, },
props: { props: {
typeIdFromRelation: { typeIdFromRelation: {
type: Number, type: Number,
default: 0, default: 0,
}, },
}, },
data() { data() {
return { return {
action: '', action: '',
form: this.$form.createForm(this), form: this.$form.createForm(this),
visible: false, visible: false,
attributeList: [], attributeList: [],
CIType: {}, CIType: {},
batchUpdateLists: [], batchUpdateLists: [],
editAttr: null, editAttr: null,
attributesByGroup: [], attributesByGroup: [],
parentsType: [], parentsType: [],
parentsForm: {}, parentsForm: {},
canEdit: {}, canEdit: {},
} }
}, },
computed: { computed: {
title() { title() {
return this.action === 'create' ? this.$t('create') + ' ' : this.$t('cmdb.ci.batchUpdate') + ' ' return this.action === 'create' ? this.$t('create') + ' ' : this.$t('cmdb.ci.batchUpdate') + ' '
}, },
typeId() { typeId() {
if (this.typeIdFromRelation) { if (this.typeIdFromRelation) {
return this.typeIdFromRelation return this.typeIdFromRelation
} }
return this.$router.currentRoute.meta.typeId return this.$router.currentRoute.meta.typeId
}, },
valueTypeMap() { valueTypeMap() {
return valueTypeMap() return valueTypeMap()
}, },
}, },
provide() { provide() {
return { return {
getFieldType: this.getFieldType, getFieldType: this.getFieldType,
} }
}, },
inject: ['attrList'], inject: ['attrList'],
methods: { methods: {
moment, moment,
async getCIType() { async getCIType() {
await getCIType(this.typeId).then((res) => { await getCIType(this.typeId).then((res) => {
this.CIType = res.ci_types[0] this.CIType = res.ci_types[0]
}) })
}, },
async getAttributeList() { async getAttributeList() {
const _attrList = this.attrList() const _attrList = this.attrList()
this.attributeList = _attrList.sort((x, y) => y.is_required - x.is_required) this.attributeList = _attrList.sort((x, y) => y.is_required - x.is_required)
await getCITypeGroupById(this.typeId).then((res1) => { await getCITypeGroupById(this.typeId).then((res1) => {
const _attributesByGroup = res1.map((g) => { const _attributesByGroup = res1.map((g) => {
g.attributes = g.attributes.filter((attr) => !attr.is_computed) g.attributes = g.attributes.filter((attr) => !attr.is_computed)
return g return g
}) })
const attrHasGroupIds = [] const attrHasGroupIds = []
res1.forEach((g) => { res1.forEach((g) => {
const id = g.attributes.map((attr) => attr.id) const id = g.attributes.map((attr) => attr.id)
attrHasGroupIds.push(...id) attrHasGroupIds.push(...id)
}) })
const otherGroupAttr = this.attributeList.filter( const otherGroupAttr = this.attributeList.filter(
(attr) => !attrHasGroupIds.includes(attr.id) && !attr.is_computed (attr) => !attrHasGroupIds.includes(attr.id) && !attr.is_computed
) )
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 })
} }
this.attributesByGroup = _attributesByGroup console.log(otherGroupAttr, _attributesByGroup)
}) this.attributesByGroup = _attributesByGroup
}, })
createInstance() { },
const _this = this createInstance() {
if (_this.action === 'update') { const _this = this
this.form.validateFields((err, values) => { if (_this.action === 'update') {
if (err) { this.form.validateFields((err, values) => {
return if (err) {
} return
Object.keys(values).forEach((k) => { }
const _tempFind = this.attributeList.find((item) => item.name === k) Object.keys(values).forEach((k) => {
if ( const _tempFind = this.attributeList.find((item) => item.name === k)
_tempFind.value_type === '3' && if (
values[k] && _tempFind.value_type === '3' &&
Object.prototype.toString.call(values[k]) === '[object Object]' values[k] &&
) { Object.prototype.toString.call(values[k]) === '[object Object]'
values[k] = values[k].format('YYYY-MM-DD HH:mm:ss') ) {
} values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
if ( }
_tempFind.value_type === '4' && if (
values[k] && _tempFind.value_type === '4' &&
Object.prototype.toString.call(values[k]) === '[object Object]' values[k] &&
) { Object.prototype.toString.call(values[k]) === '[object Object]'
values[k] = values[k].format('YYYY-MM-DD') ) {
} values[k] = values[k].format('YYYY-MM-DD')
if (_tempFind.value_type === '6') { }
values[k] = values[k] ? JSON.parse(values[k]) : undefined if (_tempFind.value_type === '6') {
} values[k] = values[k] ? JSON.parse(values[k]) : undefined
}) }
})
_this.$emit('submit', values)
}) _this.$emit('submit', values)
} else { })
let values = {} } else {
for (let i = 0; i < this.attributesByGroup.length; i++) { let values = {}
const data = this.$refs[`createInstanceFormByGroup_${this.attributesByGroup[i].id}`][0].getData() for (let i = 0; i < this.attributesByGroup.length; i++) {
if (data === 'error') { const data = this.$refs[`createInstanceFormByGroup_${this.attributesByGroup[i].id}`][0].getData()
return if (data === 'error') {
} return
values = { ...values, ...data } }
} values = { ...values, ...data }
}
Object.keys(values).forEach((k) => {
const _tempFind = this.attributeList.find((item) => item.name === k) Object.keys(values).forEach((k) => {
if ( const _tempFind = this.attributeList.find((item) => item.name === k)
_tempFind.value_type === '3' && if (
values[k] && _tempFind.value_type === '3' &&
Object.prototype.toString.call(values[k]) === '[object Object]' values[k] &&
) { Object.prototype.toString.call(values[k]) === '[object Object]'
values[k] = values[k].format('YYYY-MM-DD HH:mm:ss') ) {
} values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
if ( }
_tempFind.value_type === '4' && if (
values[k] && _tempFind.value_type === '4' &&
Object.prototype.toString.call(values[k]) === '[object Object]' values[k] &&
) { Object.prototype.toString.call(values[k]) === '[object Object]'
values[k] = values[k].format('YYYY-MM-DD') ) {
} values[k] = values[k].format('YYYY-MM-DD')
if (_tempFind.value_type === '6') { }
values[k] = values[k] ? JSON.parse(values[k]) : undefined if (_tempFind.value_type === '6') {
} values[k] = values[k] ? JSON.parse(values[k]) : undefined
}) }
values.ci_type = _this.typeId })
console.log(this.parentsForm) values.ci_type = _this.typeId
Object.keys(this.parentsForm).forEach((type) => { console.log(this.parentsForm)
if (this.parentsForm[type].value) { Object.keys(this.parentsForm).forEach((type) => {
values[`$${type}.${this.parentsForm[type].attr}`] = this.parentsForm[type].value if (this.parentsForm[type].value) {
} values[`$${type}.${this.parentsForm[type].attr}`] = this.parentsForm[type].value
}) }
addCI(values).then((res) => { })
_this.$message.success(this.$t('addSuccess')) addCI(values).then((res) => {
_this.visible = false _this.$message.success(this.$t('addSuccess'))
_this.$emit('reload', { ci_id: res.ci_id }) _this.visible = false
}) _this.$emit('reload', { ci_id: res.ci_id })
} })
}, }
handleClose() {
this.visible = false // this.form.validateFields((err, values) => {
}, // if (err) {
handleOpen(visible, action) { // _this.$message.error('字段填写不符合要求!')
this.visible = visible // return
this.action = action // }
this.$nextTick(() => { // Object.keys(values).forEach((k) => {
this.form.resetFields() // if (Object.prototype.toString.call(values[k]) === '[object Object]' && values[k]) {
Promise.all([this.getCIType(), this.getAttributeList()]).then(() => { // values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
this.batchUpdateLists = [{ name: this.attributeList[0].name }] // }
}) // const _tempFind = this.attributeList.find((item) => item.name === k)
if (action === 'create') { // if (_tempFind.value_type === '6') {
getCITypeParent(this.typeId).then(async (res) => { // values[k] = values[k] ? JSON.parse(values[k]) : undefined
for (let i = 0; i < res.parents.length; i++) { // }
await getCanEditByParentIdChildId(res.parents[i].id, this.typeId).then((p_res) => { // })
this.canEdit = {
..._.cloneDeep(this.canEdit), // if (_this.action === 'update') {
[res.parents[i].id]: p_res.result, // _this.$emit('submit', values)
} // return
}) // }
} // values.ci_type = _this.typeId
this.parentsType = res.parents.filter((parent) => this.canEdit[parent.id]) // console.log(values)
const _parentsForm = {} // this.attributesByGroup.forEach((group) => {
res.parents.forEach((item) => { // this.$refs[`createInstanceFormByGroup_${group.id}`][0].getData()
const _find = item.attributes.find((attr) => attr.id === item.unique_id) // })
_parentsForm[item.name] = { attr: _find.name, value: '' } // console.log(1111)
}) // // addCI(values).then((res) => {
this.parentsForm = _parentsForm // // _this.$message.success('新增成功!')
}) // // _this.visible = false
} // // _this.$emit('reload')
}) // // })
}, // })
getFieldType(name) { },
const _find = this.attributeList.find((item) => item.name === name) handleClose() {
if (_find) { this.visible = false
if (_find.is_choice) { },
if (_find.is_list) { handleOpen(visible, action) {
return 'select%%multiple' this.visible = visible
} this.action = action
return 'select' this.$nextTick(() => {
} else if (_find.value_type === '0' || _find.value_type === '1') { this.form.resetFields()
return 'input_number' Promise.all([this.getCIType(), this.getAttributeList()]).then(() => {
} else if (_find.value_type === '4' || _find.value_type === '3') { this.batchUpdateLists = [{ name: this.attributeList[0].name }]
return this.valueTypeMap[_find.value_type] })
} else { if (action === 'create') {
return 'input' getCITypeParent(this.typeId).then(async (res) => {
} for (let i = 0; i < res.parents.length; i++) {
} await getCanEditByParentIdChildId(res.parents[i].id, this.typeId).then((p_res) => {
return 'input' this.canEdit = {
}, ..._.cloneDeep(this.canEdit),
getSelectFieldOptions(name) { [res.parents[i].id]: p_res.result,
const _find = this.attributeList.find((item) => item.name === name) }
if (_find) { })
return _find.choice_value }
} this.parentsType = res.parents.filter((parent) => this.canEdit[parent.id])
return [] const _parentsForm = {}
}, res.parents.forEach((item) => {
handleAdd() { const _find = item.attributes.find((attr) => attr.id === item.unique_id)
this.batchUpdateLists.push({ name: undefined }) _parentsForm[item.name] = { attr: _find.name, value: '' }
}, })
handleDelete(name) { this.parentsForm = _parentsForm
const _idx = this.batchUpdateLists.findIndex((item) => item.name === name) })
if (_idx > -1) { }
this.batchUpdateLists.splice(_idx, 1) })
} },
}, getFieldType(name) {
handleFocusInput(e, attr) { const _find = this.attributeList.find((item) => item.name === name)
console.log(attr) if (_find) {
const _tempFind = this.attributeList.find((item) => item.name === attr.name) if (_find.is_choice) {
if (_tempFind.value_type === '6') { if (_find.is_list) {
this.editAttr = attr return 'select%%multiple'
e.srcElement.blur() }
const jsonData = this.form.getFieldValue(attr.name) return 'select'
this.$refs.jsonEditor.open(null, null, jsonData ? JSON.parse(jsonData) : {}) } else if (_find.value_type === '0' || _find.value_type === '1') {
} else { return 'input_number'
this.editAttr = null } else if (_find.value_type === '4' || _find.value_type === '3') {
} return this.valueTypeMap[_find.value_type]
}, } else {
jsonEditorOk(jsonData) { return 'input'
this.form.setFieldsValue({ [this.editAttr.name]: JSON.stringify(jsonData) }) }
}, }
}, return 'input'
} },
</script> getSelectFieldOptions(name) {
<style lang="less"> const _find = this.attributeList.find((item) => item.name === name)
.create-instance-form { if (_find) {
.ant-form-item { return _find.choice_value
margin-bottom: 5px; }
} return []
.ant-drawer-body { },
overflow-y: auto; handleAdd() {
max-height: calc(100vh - 110px); this.batchUpdateLists.push({ name: undefined })
} },
} handleDelete(name) {
</style> const _idx = this.batchUpdateLists.findIndex((item) => item.name === name)
if (_idx > -1) {
this.batchUpdateLists.splice(_idx, 1)
}
},
// filterOption(input, option) {
// return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
// },
handleFocusInput(e, attr) {
console.log(attr)
const _tempFind = this.attributeList.find((item) => item.name === attr.name)
if (_tempFind.value_type === '6') {
this.editAttr = attr
e.srcElement.blur()
const jsonData = this.form.getFieldValue(attr.name)
this.$refs.jsonEditor.open(null, null, jsonData ? JSON.parse(jsonData) : {})
} else {
this.editAttr = null
}
},
jsonEditorOk(jsonData) {
this.form.setFieldsValue({ [this.editAttr.name]: JSON.stringify(jsonData) })
},
},
}
</script>
<style lang="less">
.create-instance-form {
.ant-form-item {
margin-bottom: 5px;
}
.ant-drawer-body {
overflow-y: auto;
max-height: calc(100vh - 110px);
}
}
</style>

View File

@@ -1,390 +1,390 @@
<template> <template>
<div :style="{ height: '100%' }"> <div :style="{ height: '100%' }">
<a-tabs v-if="hasPermission" class="ci-detail-tab" v-model="activeTabKey" @change="changeTab"> <a-tabs v-if="hasPermission" class="ci-detail-tab" v-model="activeTabKey" @change="changeTab">
<a @click="shareCi" slot="tabBarExtraContent" :style="{ marginRight: '24px' }"> <a @click="shareCi" slot="tabBarExtraContent" :style="{ marginRight: '24px' }">
<a-icon type="share-alt" /> <a-icon type="share-alt" />
{{ $t('cmdb.ci.share') }} {{ $t('cmdb.ci.share') }}
</a> </a>
<a-tab-pane key="tab_1"> <a-tab-pane key="tab_1">
<span slot="tab"><a-icon type="book" />{{ $t('cmdb.attribute') }}</span> <span slot="tab"><a-icon type="book" />{{ $t('cmdb.attribute') }}</span>
<div class="ci-detail-attr"> <div class="ci-detail-attr">
<el-descriptions <el-descriptions
:title="group.name || $t('other')" :title="group.name || $t('other')"
:key="group.name" :key="group.name"
v-for="group in attributeGroups" v-for="group in attributeGroups"
border border
:column="3" :column="3"
> >
<el-descriptions-item <el-descriptions-item
:label="`${attr.alias || attr.name}`" :label="`${attr.alias || attr.name}`"
:key="attr.name" :key="attr.name"
v-for="attr in group.attributes" v-for="attr in group.attributes"
> >
<CiDetailAttrContent :ci="ci" :attr="attr" @refresh="refresh" @updateCIByself="updateCIByself" /> <CiDetailAttrContent :ci="ci" :attr="attr" @refresh="refresh" @updateCIByself="updateCIByself" />
</el-descriptions-item> </el-descriptions-item>
</el-descriptions> </el-descriptions>
</div> </div>
</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>
<a-tab-pane key="tab_3"> <a-tab-pane key="tab_3">
<span slot="tab"><a-icon type="clock-circle" />{{ $t('cmdb.ci.history') }}</span> <span slot="tab"><a-icon type="clock-circle" />{{ $t('cmdb.ci.history') }}</span>
<div :style="{ padding: '24px', height: '100%' }"> <div :style="{ padding: '24px', height: '100%' }">
<vxe-table <vxe-table
ref="xTable" ref="xTable"
:data="ciHistory" :data="ciHistory"
size="small" size="small"
height="auto" height="auto"
:span-method="mergeRowMethod" :span-method="mergeRowMethod"
border border
:scroll-y="{ enabled: false }" :scroll-y="{ enabled: false }"
class="ops-stripe-table" class="ops-stripe-table"
> >
<vxe-table-column sortable field="created_at" :title="$t('created_at')"></vxe-table-column> <vxe-table-column sortable field="created_at" :title="$t('created_at')"></vxe-table-column>
<vxe-table-column <vxe-table-column
field="username" field="username"
:title="$t('user')" :title="$t('user')"
:filters="[]" :filters="[]"
:filter-method="filterUsernameMethod" :filter-method="filterUsernameMethod"
></vxe-table-column> ></vxe-table-column>
<vxe-table-column <vxe-table-column
field="operate_type" field="operate_type"
:filters="[ :filters="[
{ value: 0, label: $t('new') }, { value: 0, label: $t('new') },
{ value: 1, label: $t('delete') }, { value: 1, label: $t('delete') },
{ value: 3, label: $t('update') }, { value: 3, label: $t('update') },
]" ]"
:filter-method="filterOperateMethod" :filter-method="filterOperateMethod"
:title="$t('operation')" :title="$t('operation')"
> >
<template #default="{ row }"> <template #default="{ row }">
{{ operateTypeMap[row.operate_type] }} {{ operateTypeMap[row.operate_type] }}
</template> </template>
</vxe-table-column> </vxe-table-column>
<vxe-table-column <vxe-table-column
field="attr_alias" field="attr_alias"
:title="$t('cmdb.attribute')" :title="$t('cmdb.attribute')"
:filters="[]" :filters="[]"
:filter-method="filterAttrMethod" :filter-method="filterAttrMethod"
></vxe-table-column> ></vxe-table-column>
<vxe-table-column field="old" :title="$t('cmdb.history.old')"></vxe-table-column> <vxe-table-column field="old" :title="$t('cmdb.history.old')"></vxe-table-column>
<vxe-table-column field="new" :title="$t('cmdb.history.new')"></vxe-table-column> <vxe-table-column field="new" :title="$t('cmdb.history.new')"></vxe-table-column>
</vxe-table> </vxe-table>
</div> </div>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="tab_4"> <a-tab-pane key="tab_4">
<span slot="tab"><ops-icon type="itsm_auto_trigger" />{{ $t('cmdb.history.triggerHistory') }}</span> <span slot="tab"><ops-icon type="itsm_auto_trigger" />{{ $t('cmdb.history.triggerHistory') }}</span>
<div :style="{ padding: '24px', height: '100%' }"> <div :style="{ padding: '24px', height: '100%' }">
<TriggerTable :ci_id="ci._id" /> <TriggerTable :ci_id="ci._id" />
</div> </div>
</a-tab-pane> </a-tab-pane>
</a-tabs> </a-tabs>
<a-empty <a-empty
v-else v-else
:image-style="{ :image-style="{
height: '100px', height: '100px',
}" }"
:style="{ paddingTop: '20%' }" :style="{ paddingTop: '20%' }"
> >
<img slot="image" :src="require('@/assets/data_empty.png')" /> <img slot="image" :src="require('@/assets/data_empty.png')" />
<span slot="description"> {{ $t('cmdb.ci.noPermission') }} </span> <span slot="description"> {{ $t('cmdb.ci.noPermission') }} </span>
</a-empty> </a-empty>
</div> </div>
</template> </template>
<script> <script>
import _ from 'lodash' import _ from 'lodash'
import { Descriptions, DescriptionsItem } from 'element-ui' import { Descriptions, DescriptionsItem } from 'element-ui'
import { getCITypeGroupById, getCITypes } from '@/modules/cmdb/api/CIType' import { getCITypeGroupById, getCITypes } from '@/modules/cmdb/api/CIType'
import { getCIHistory } from '@/modules/cmdb/api/history' import { getCIHistory } from '@/modules/cmdb/api/history'
import { getCIById } from '@/modules/cmdb/api/ci' import { getCIById } from '@/modules/cmdb/api/ci'
import CiDetailAttrContent from './ciDetailAttrContent.vue' import CiDetailAttrContent from './ciDetailAttrContent.vue'
import CiDetailRelation from './ciDetailRelation.vue' import CiDetailRelation from './ciDetailRelation.vue'
import TriggerTable from '../../operation_history/modules/triggerTable.vue' import TriggerTable from '../../operation_history/modules/triggerTable.vue'
export default { export default {
name: 'CiDetailTab', name: 'CiDetailTab',
components: { components: {
ElDescriptions: Descriptions, ElDescriptions: Descriptions,
ElDescriptionsItem: DescriptionsItem, ElDescriptionsItem: DescriptionsItem,
CiDetailAttrContent, CiDetailAttrContent,
CiDetailRelation, CiDetailRelation,
TriggerTable, TriggerTable,
}, },
props: { props: {
typeId: { typeId: {
type: Number, type: Number,
required: true, required: true,
}, },
treeViewsLevels: { treeViewsLevels: {
type: Array, type: Array,
default: () => [], default: () => [],
}, },
}, },
data() { data() {
return { return {
ci: {}, ci: {},
attributeGroups: [], attributeGroups: [],
activeTabKey: 'tab_1', activeTabKey: 'tab_1',
rowSpanMap: {}, rowSpanMap: {},
ciHistory: [], ciHistory: [],
ciId: null, ciId: null,
ci_types: [], ci_types: [],
hasPermission: true, hasPermission: true,
} }
}, },
computed: { computed: {
windowHeight() { windowHeight() {
return this.$store.state.windowHeight return this.$store.state.windowHeight
}, },
operateTypeMap() { operateTypeMap() {
return { return {
0: this.$t('new'), 0: this.$t('new'),
1: this.$t('delete'), 1: this.$t('delete'),
2: this.$t('update'), 2: this.$t('update'),
} }
}, },
}, },
provide() { provide() {
return { return {
ci_types: () => { ci_types: () => {
return this.ci_types return this.ci_types
}, },
} }
}, },
inject: { inject: {
reload: { reload: {
from: 'reload', from: 'reload',
default: null, default: null,
}, },
handleSearch: { handleSearch: {
from: 'handleSearch', from: 'handleSearch',
default: null, default: null,
}, },
attrList: { attrList: {
from: 'attrList', from: 'attrList',
default: () => [], default: () => [],
}, },
}, },
methods: { methods: {
async create(ciId, activeTabKey = 'tab_1', ciDetailRelationKey = '1') { async create(ciId, activeTabKey = 'tab_1', ciDetailRelationKey = '1') {
this.activeTabKey = activeTabKey this.activeTabKey = activeTabKey
if (activeTabKey === 'tab_2') { if (activeTabKey === 'tab_2') {
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.ciDetailRelation.activeKey = ciDetailRelationKey this.$refs.ciDetailRelation.activeKey = ciDetailRelationKey
}) })
} }
this.ciId = ciId this.ciId = ciId
await this.getCI() await this.getCI()
if (this.hasPermission) { if (this.hasPermission) {
this.getAttributes() this.getAttributes()
this.getCIHistory() this.getCIHistory()
getCITypes().then((res) => { getCITypes().then((res) => {
this.ci_types = res.ci_types this.ci_types = res.ci_types
}) })
} }
}, },
getAttributes() { getAttributes() {
getCITypeGroupById(this.typeId, { need_other: 1 }) getCITypeGroupById(this.typeId, { need_other: 1 })
.then((res) => { .then((res) => {
this.attributeGroups = res this.attributeGroups = res
}) })
.catch((e) => {}) .catch((e) => {})
}, },
async getCI() { async getCI() {
await getCIById(this.ciId) await getCIById(this.ciId)
.then((res) => { .then((res) => {
if (res.result.length) { if (res.result.length) {
this.ci = res.result[0] this.ci = res.result[0]
} else { } else {
this.hasPermission = false this.hasPermission = false
} }
}) })
.catch((e) => {}) .catch((e) => {})
}, },
getCIHistory() { getCIHistory() {
getCIHistory(this.ciId) getCIHistory(this.ciId)
.then((res) => { .then((res) => {
this.ciHistory = res this.ciHistory = res
const rowSpanMap = {} const rowSpanMap = {}
let startIndex = 0 let startIndex = 0
let startCount = 1 let startCount = 1
res.forEach((item, index) => { res.forEach((item, index) => {
if (index === 0) { if (index === 0) {
return return
} }
if (res[index].record_id === res[startIndex].record_id) { if (res[index].record_id === res[startIndex].record_id) {
startCount += 1 startCount += 1
rowSpanMap[index] = 0 rowSpanMap[index] = 0
if (index === res.length - 1) { if (index === res.length - 1) {
rowSpanMap[startIndex] = startCount rowSpanMap[startIndex] = startCount
} }
} else { } else {
rowSpanMap[startIndex] = startCount rowSpanMap[startIndex] = startCount
startIndex = index startIndex = index
startCount = 1 startCount = 1
if (index === res.length - 1) { if (index === res.length - 1) {
rowSpanMap[index] = 1 rowSpanMap[index] = 1
} }
} }
}) })
this.rowSpanMap = rowSpanMap this.rowSpanMap = rowSpanMap
}) })
.catch((e) => { .catch((e) => {
console.log(e) console.log(e)
}) })
}, },
changeTab(key) { changeTab(key) {
this.activeTabKey = key this.activeTabKey = key
if (key === 'tab_3') { if (key === 'tab_3') {
this.$nextTick(() => { this.$nextTick(() => {
const $table = this.$refs.xTable const $table = this.$refs.xTable
if ($table) { if ($table) {
const usernameColumn = $table.getColumnByField('username') const usernameColumn = $table.getColumnByField('username')
const attrColumn = $table.getColumnByField('attr_alias') const attrColumn = $table.getColumnByField('attr_alias')
if (usernameColumn) { if (usernameColumn) {
const usernameList = [...new Set(this.ciHistory.map((item) => item.username))] const usernameList = [...new Set(this.ciHistory.map((item) => item.username))]
$table.setFilter( $table.setFilter(
usernameColumn, usernameColumn,
usernameList.map((item) => { usernameList.map((item) => {
return { return {
value: item, value: item,
label: item, label: item,
} }
}) })
) )
} }
if (attrColumn) { if (attrColumn) {
$table.setFilter( $table.setFilter(
attrColumn, attrColumn,
this.attrList().map((attr) => { this.attrList().map((attr) => {
return { value: attr.alias || attr.name, label: attr.alias || attr.name } return { value: attr.alias || attr.name, label: attr.alias || attr.name }
}) })
) )
} }
} }
}) })
} }
}, },
filterUsernameMethod({ value, row, column }) { filterUsernameMethod({ value, row, column }) {
return row.username === value return row.username === value
}, },
filterOperateMethod({ value, row, column }) { filterOperateMethod({ value, row, column }) {
return Number(row.operate_type) === Number(value) return Number(row.operate_type) === Number(value)
}, },
filterAttrMethod({ value, row, column }) { filterAttrMethod({ value, row, column }) {
return row.attr_alias === value return row.attr_alias === value
}, },
refresh(editAttrName) { refresh(editAttrName) {
this.getCI() this.getCI()
const _find = this.treeViewsLevels.find((level) => level.name === editAttrName) const _find = this.treeViewsLevels.find((level) => level.name === editAttrName)
// 修改的字段为树形视图订阅的字段 则全部reload // 修改的字段为树形视图订阅的字段 则全部reload
setTimeout(() => { setTimeout(() => {
if (_find) { if (_find) {
if (this.reload) { if (this.reload) {
this.reload() this.reload()
} }
} else { } else {
if (this.handleSearch) { if (this.handleSearch) {
this.handleSearch() this.handleSearch()
} }
} }
}, 500) }, 500)
}, },
mergeRowMethod({ row, _rowIndex, column, visibleData }) { mergeRowMethod({ row, _rowIndex, column, visibleData }) {
const fields = ['created_at', 'username'] const fields = ['created_at', 'username']
const cellValue1 = row['created_at'] const cellValue1 = row['created_at']
const cellValue2 = row['username'] const cellValue2 = row['username']
if (cellValue1 && cellValue2 && fields.includes(column.property)) { if (cellValue1 && cellValue2 && fields.includes(column.property)) {
const prevRow = visibleData[_rowIndex - 1] const prevRow = visibleData[_rowIndex - 1]
let nextRow = visibleData[_rowIndex + 1] let nextRow = visibleData[_rowIndex + 1]
if (prevRow && prevRow['created_at'] === cellValue1 && prevRow['username'] === cellValue2) { if (prevRow && prevRow['created_at'] === cellValue1 && prevRow['username'] === cellValue2) {
return { rowspan: 0, colspan: 0 } return { rowspan: 0, colspan: 0 }
} else { } else {
let countRowspan = 1 let countRowspan = 1
while (nextRow && nextRow['created_at'] === cellValue1 && nextRow['username'] === cellValue2) { while (nextRow && nextRow['created_at'] === cellValue1 && nextRow['username'] === cellValue2) {
nextRow = visibleData[++countRowspan + _rowIndex] nextRow = visibleData[++countRowspan + _rowIndex]
} }
if (countRowspan > 1) { if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 } return { rowspan: countRowspan, colspan: 1 }
} }
} }
} }
}, },
updateCIByself(params, editAttrName) { updateCIByself(params, editAttrName) {
const _ci = { ..._.cloneDeep(this.ci), ...params } const _ci = { ..._.cloneDeep(this.ci), ...params }
this.ci = _ci this.ci = _ci
const _find = this.treeViewsLevels.find((level) => level.name === editAttrName) const _find = this.treeViewsLevels.find((level) => level.name === editAttrName)
// 修改的字段为树形视图订阅的字段 则全部reload // 修改的字段为树形视图订阅的字段 则全部reload
setTimeout(() => { setTimeout(() => {
if (_find) { if (_find) {
if (this.reload) { if (this.reload) {
this.reload() this.reload()
} }
} else { } else {
if (this.handleSearch) { if (this.handleSearch) {
this.handleSearch() this.handleSearch()
} }
} }
}, 500) }, 500)
}, },
shareCi() { shareCi() {
const text = `${document.location.host}/cmdb/cidetail/${this.typeId}/${this.ciId}` const text = `${document.location.host}/cmdb/cidetail/${this.typeId}/${this.ciId}`
this.$copyText(text) this.$copyText(text)
.then(() => { .then(() => {
this.$message.success(this.$t('copySuccess')) this.$message.success(this.$t('copySuccess'))
}) })
.catch(() => { .catch(() => {
this.$message.error(this.$t('cmdb.ci.copyFailed')) this.$message.error(this.$t('cmdb.ci.copyFailed'))
}) })
}, },
}, },
} }
</script> </script>
<style lang="less"> <style lang="less">
.ci-detail-tab { .ci-detail-tab {
height: 100%; height: 100%;
.ant-tabs-content { .ant-tabs-content {
height: calc(100% - 45px); height: calc(100% - 45px);
.ant-tabs-tabpane { .ant-tabs-tabpane {
height: 100%; height: 100%;
} }
} }
.ant-tabs-bar { .ant-tabs-bar {
margin: 0; margin: 0;
} }
.ant-tabs-extra-content { .ant-tabs-extra-content {
line-height: 44px; line-height: 44px;
} }
.ci-detail-attr { .ci-detail-attr {
height: 100%; height: 100%;
overflow: auto; overflow: auto;
padding: 24px; padding: 24px;
.el-descriptions-item__content { .el-descriptions-item__content {
cursor: default; cursor: default;
&:hover a { &:hover a {
opacity: 1 !important; opacity: 1 !important;
} }
} }
.el-descriptions:first-child > .el-descriptions__header { .el-descriptions:first-child > .el-descriptions__header {
margin-top: 0; margin-top: 0;
} }
.el-descriptions__header { .el-descriptions__header {
margin-bottom: 5px; margin-bottom: 5px;
margin-top: 20px; margin-top: 20px;
} }
.ant-form-item { .ant-form-item {
margin-bottom: 0; margin-bottom: 0;
} }
.ant-form-item-control { .ant-form-item-control {
line-height: 19px; line-height: 19px;
} }
} }
} }
</style> </style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,224 +1,250 @@
<template> <template>
<a-modal <a-modal
v-model="visible" v-model="visible"
width="90%" width="90%"
:closable="false" :closable="false"
:centered="true" :centered="true"
:maskClosable="false" :maskClosable="false"
:destroyOnClose="true" :destroyOnClose="true"
@cancel="handleClose" @cancel="handleClose"
@ok="handleOk" @ok="handleOk"
> >
<div :style="{ width: '100%' }" id="add-table-modal"> <div :style="{ width: '100%' }" id="add-table-modal">
<a-spin :spinning="loading"> <a-spin :spinning="loading">
<!-- <a-input <SearchForm
v-model="expression" ref="searchForm"
class="ci-searchform-expression" :typeId="addTypeId"
:style="{ width, marginBottom: '10px' }" :preferenceAttrList="preferenceAttrList"
:placeholder="placeholder" @refresh="handleSearch"
@focus=" >
() => { <a-button
isFocusExpression = true @click="
} () => {
" $refs.createInstanceForm.handleOpen(true, 'create')
/> --> }
<SearchForm "
ref="searchForm" slot="extraContent"
:typeId="addTypeId" type="primary"
:preferenceAttrList="preferenceAttrList" size="small"
@refresh="handleSearch" >新增</a-button
/> >
<!-- <a @click="handleSearch"><a-icon type="search"/></a> --> </SearchForm>
<vxe-table <vxe-table
ref="xTable" ref="xTable"
row-id="_id" row-id="_id"
:data="tableData" :data="tableData"
:height="tableHeight" :height="tableHeight"
highlight-hover-row highlight-hover-row
:checkbox-config="{ reserve: true }" :checkbox-config="{ reserve: true }"
@checkbox-change="onSelectChange" @checkbox-change="onSelectChange"
@checkbox-all="onSelectChange" @checkbox-all="onSelectChange"
show-overflow="tooltip" show-overflow="tooltip"
show-header-overflow="tooltip" show-header-overflow="tooltip"
:scroll-y="{ enabled: true, gt: 50 }" :scroll-y="{ enabled: true, gt: 50 }"
:scroll-x="{ enabled: true, gt: 0 }" :scroll-x="{ enabled: true, gt: 0 }"
class="ops-stripe-table" class="ops-stripe-table"
> >
<vxe-column align="center" type="checkbox" width="60" fixed="left"></vxe-column> <vxe-column align="center" type="checkbox" width="60" fixed="left"></vxe-column>
<vxe-table-column <vxe-table-column
v-for="col in columns" v-for="col in columns"
:key="col.field" :key="col.field"
:title="col.title" :title="col.title"
:field="col.field" :field="col.field"
:width="col.width" :width="col.width"
:sortable="col.sortable" :sortable="col.sortable"
> >
<template #default="{row}" v-if="col.value_type === '6'"> <template #default="{row}" v-if="col.value_type === '6'">
<span v-if="col.value_type === '6' && row[col.field]">{{ JSON.stringify(row[col.field]) }}</span> <span v-if="col.value_type === '6' && row[col.field]">{{ JSON.stringify(row[col.field]) }}</span>
</template> </template>
</vxe-table-column> </vxe-table-column>
</vxe-table> </vxe-table>
<a-pagination <a-pagination
v-model="currentPage" v-model="currentPage"
size="small" size="small"
:total="totalNumber" :total="totalNumber"
show-quick-jumper show-quick-jumper
:page-size="50" :page-size="50"
:show-total=" :show-total="
(total, range) => (total, range) =>
$t('pagination.total', { $t('pagination.total', {
range0: range[0], range0: range[0],
range1: range[1], range1: range[1],
total, total,
}) })
" "
:style="{ textAlign: 'right', marginTop: '10px' }" :style="{ textAlign: 'right', marginTop: '10px' }"
@change="handleChangePage" @change="handleChangePage"
/> />
</a-spin> </a-spin>
</div> </div>
</a-modal> <CreateInstanceForm
</template> ref="createInstanceForm"
:typeIdFromRelation="addTypeId"
<script> @reload="
/* eslint-disable no-useless-escape */ () => {
import { searchCI } from '@/modules/cmdb/api/ci' currentPage = 1
import { getSubscribeAttributes } from '@/modules/cmdb/api/preference' getTableData(true)
import { batchUpdateCIRelationChildren, batchUpdateCIRelationParents } from '@/modules/cmdb/api/CIRelation' }
import { getCITableColumns } from '../../../utils/helper' "
import SearchForm from '../../../components/searchForm/SearchForm.vue' />
export default { </a-modal>
name: 'AddTableModal', </template>
components: { SearchForm },
data() { <script>
return { import { searchCI } from '@/modules/cmdb/api/ci'
visible: false, import { getSubscribeAttributes } from '@/modules/cmdb/api/preference'
currentPage: 1, import { batchUpdateCIRelationChildren, batchUpdateCIRelationParents } from '@/modules/cmdb/api/CIRelation'
totalNumber: 0, import { getCITableColumns } from '../../../utils/helper'
tableData: [], import SearchForm from '../../../components/searchForm/SearchForm.vue'
columns: [], import CreateInstanceForm from '../../ci/modules/CreateInstanceForm.vue'
ciObj: {}, import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
ciId: null,
addTypeId: null, export default {
loading: false, name: 'AddTableModal',
expression: '', components: { SearchForm, CreateInstanceForm },
isFocusExpression: false, data() {
type: 'children', return {
preferenceAttrList: [], visible: false,
ancestor_ids: undefined, currentPage: 1,
} totalNumber: 0,
}, tableData: [],
computed: { columns: [],
tableHeight() { ciObj: {},
return this.$store.state.windowHeight - 250 ciId: null,
}, addTypeId: null,
placeholder() { loading: false,
return this.isFocusExpression ? this.$t('cmdb.serviceTreetips1') : this.$t('cmdb.serviceTreetips2') expression: '',
}, isFocusExpression: false,
width() { type: 'children',
return this.isFocusExpression ? '500px' : '100px' preferenceAttrList: [],
}, ancestor_ids: undefined,
}, attrList1: [],
watch: {}, }
methods: { },
async openModal(ciObj, ciId, addTypeId, type, ancestor_ids = undefined) { computed: {
console.log(ciObj, ciId, addTypeId, type) tableHeight() {
this.visible = true return this.$store.state.windowHeight - 250
this.ciObj = ciObj },
this.ciId = ciId placeholder() {
this.addTypeId = addTypeId return this.isFocusExpression ? this.$t('cmdb.serviceTreetips1') : this.$t('cmdb.serviceTreetips2')
this.type = type },
this.ancestor_ids = ancestor_ids width() {
await getSubscribeAttributes(addTypeId).then((res) => { return this.isFocusExpression ? '500px' : '100px'
this.preferenceAttrList = res.attributes // 已经订阅的全部列 },
}) },
this.getTableData(true) provide() {
}, return {
async getTableData(isInit) { attrList: () => {
if (this.addTypeId) { return this.attrList
await this.fetchData(isInit) },
} }
}, },
async fetchData(isInit) { watch: {},
this.loading = true methods: {
// if (isInit) { async openModal(ciObj, ciId, addTypeId, type, ancestor_ids = undefined) {
// const subscribed = await getSubscribeAttributes(this.addTypeId) console.log(ciObj, ciId, addTypeId, type)
// this.preferenceAttrList = subscribed.attributes // 已经订阅的全部列 this.visible = true
// } this.ciObj = ciObj
let sort, fuzzySearch, expression, exp this.ciId = ciId
if (!isInit) { this.addTypeId = addTypeId
fuzzySearch = this.$refs['searchForm'].fuzzySearch this.type = type
expression = this.$refs['searchForm'].expression || '' this.ancestor_ids = ancestor_ids
const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g await getSubscribeAttributes(addTypeId).then((res) => {
this.preferenceAttrList = res.attributes // 已经订阅的全部列
exp = expression.match(regQ) ? expression.match(regQ)[0] : null })
} getCITypeAttributesById(addTypeId).then((res) => {
this.attrList = res.attributes
await searchCI({ })
q: `_type:${this.addTypeId}${exp ? `,${exp}` : ''}${fuzzySearch ? `,*${fuzzySearch}*` : ''}`, this.getTableData(true)
count: 50, },
page: this.currentPage, async getTableData(isInit) {
sort, if (this.addTypeId) {
}) await this.fetchData(isInit)
.then((res) => { }
this.tableData = res.result },
this.totalNumber = res.numfound async fetchData(isInit) {
this.columns = this.getColumns(res.result, this.preferenceAttrList) this.loading = true
this.$nextTick(() => { // if (isInit) {
const _table = this.$refs.xTable // const subscribed = await getSubscribeAttributes(this.addTypeId)
if (_table) { // this.preferenceAttrList = subscribed.attributes // 已经订阅的全部列
_table.refreshColumn() // }
} let sort, fuzzySearch, expression, exp
this.loading = false if (!isInit) {
}) fuzzySearch = this.$refs['searchForm'].fuzzySearch
}) expression = this.$refs['searchForm'].expression || ''
.catch(() => { const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
this.loading = false
}) exp = expression.match(regQ) ? expression.match(regQ)[0] : null
}, }
getColumns(data, attrList) {
const modalDom = document.getElementById('add-table-modal') await searchCI({
if (modalDom) { q: `_type:${this.addTypeId}${exp ? `,${exp}` : ''}${fuzzySearch ? `,*${fuzzySearch}*` : ''}`,
const width = modalDom.clientWidth - 50 count: 50,
return getCITableColumns(data, attrList, width) page: this.currentPage,
} sort,
return [] })
}, .then((res) => {
onSelectChange() {}, this.tableData = res.result
handleClose() { this.totalNumber = res.numfound
this.$refs.xTable.clearCheckboxRow() this.columns = this.getColumns(res.result, this.preferenceAttrList)
this.currentPage = 1 this.$nextTick(() => {
this.expression = '' const _table = this.$refs.xTable
this.isFocusExpression = false if (_table) {
this.visible = false _table.refreshColumn()
}, }
async handleOk() { this.loading = false
const selectRecordsCurrent = this.$refs.xTable.getCheckboxRecords() })
const selectRecordsReserved = this.$refs.xTable.getCheckboxReserveRecords() })
const ciIds = [...selectRecordsCurrent, ...selectRecordsReserved].map((record) => record._id) .catch(() => {
if (ciIds.length) { this.loading = false
if (this.type === 'children') { })
await batchUpdateCIRelationChildren(ciIds, [this.ciId], this.ancestor_ids) },
} else { getColumns(data, attrList) {
await batchUpdateCIRelationParents(ciIds, [this.ciId]) const modalDom = document.getElementById('add-table-modal')
} if (modalDom) {
setTimeout(() => { const width = modalDom.clientWidth - 50
this.$message.success(this.$t('addSuccess')) return getCITableColumns(data, attrList, width)
this.handleClose() }
this.$emit('reload') return []
}, 500) },
} onSelectChange() {},
}, handleClose() {
handleSearch() { this.$refs.xTable.clearCheckboxRow()
this.currentPage = 1 this.currentPage = 1
this.fetchData() this.expression = ''
}, this.isFocusExpression = false
handleChangePage(page, pageSize) { this.visible = false
this.currentPage = page },
this.fetchData() async handleOk() {
}, const selectRecordsCurrent = this.$refs.xTable.getCheckboxRecords()
}, const selectRecordsReserved = this.$refs.xTable.getCheckboxReserveRecords()
} const ciIds = [...selectRecordsCurrent, ...selectRecordsReserved].map((record) => record._id)
</script> if (ciIds.length) {
if (this.type === 'children') {
<style lang="less" scoped></style> await batchUpdateCIRelationChildren(ciIds, [this.ciId], this.ancestor_ids)
} else {
await batchUpdateCIRelationParents(ciIds, [this.ciId])
}
setTimeout(() => {
this.$message.success(this.$t('addSuccess'))
this.handleClose()
this.$emit('reload')
}, 500)
} else {
this.handleClose()
this.$emit('reload')
}
},
handleSearch() {
this.currentPage = 1
this.fetchData()
},
handleChangePage(page, pageSize) {
this.currentPage = page
this.fetchData()
},
},
}
</script>
<style lang="less" scoped></style>

View File

@@ -1,166 +1,253 @@
<template> <template>
<a-dropdown :trigger="['contextmenu']"> <div
<a-menu slot="overlay" @click="({ key: menuKey }) => this.onContextMenuClick(this.treeKey, menuKey)"> :class="{
<a-menu-item v-for="item in menuList" :key="item.id">{{ $t('new') }} {{ item.alias }}</a-menu-item> 'relation-views-node': true,
<a-menu-item v-if="showDelete" key="delete">{{ $t('cmdb.serviceTree.deleteNode') }}</a-menu-item> 'relation-views-node-checkbox': showCheckbox,
</a-menu> }"
<div @click="clickNode"
:style="{ >
width: '100%', <span>
display: 'inline-flex', <a-checkbox @click.stop="clickCheckbox" class="relation-views-node-checkbox" v-if="showCheckbox" />
justifyContent: 'space-between', <template v-if="icon">
alignItems: 'center', <img
}" v-if="icon.includes('$$') && icon.split('$$')[2]"
@click="clickNode" :src="`/api/common-setting/v1/file/${icon.split('$$')[3]}`"
> :style="{ maxHeight: '14px', maxWidth: '14px' }"
<span />
:style="{ <ops-icon
display: 'flex', v-else-if="icon.includes('$$') && icon.split('$$')[0]"
overflow: 'hidden', :style="{
width: '100%', color: icon.split('$$')[1],
textOverflow: 'ellipsis', fontSize: '14px',
whiteSpace: 'nowrap', }"
alignItems: 'center', :type="icon.split('$$')[0]"
}" />
> <span class="relation-views-node-icon" v-else>{{ icon ? icon[0].toUpperCase() : 'i' }}</span>
<template v-if="icon"> </template>
<img <span class="relation-views-node-title">{{ this.title }}</span>
v-if="icon.split('$$')[2]" </span>
:src="`/api/common-setting/v1/file/${icon.split('$$')[3]}`" <a-dropdown>
:style="{ maxHeight: '14px', maxWidth: '14px' }" <a-menu slot="overlay" @click="({ key: menuKey }) => this.onContextMenuClick(this.treeKey, menuKey)">
/> <template v-if="showBatchLevel === null">
<ops-icon <a-menu-item
v-else v-for="item in menuList"
:style="{ :key="item.id"
color: icon.split('$$')[1], ><a-icon type="plus-circle" />{{ $t('new') }} {{ item.alias }}</a-menu-item
fontSize: '14px', >
}" <a-menu-item
:type="icon.split('$$')[0]" v-if="showDelete"
/> key="delete"
</template> ><ops-icon type="icon-xianxing-delete" />{{ $t('cmdb.serviceTree.deleteNode') }}</a-menu-item
<span >
:style="{ <a-menu-divider />
display: 'inline-block', <a-menu-item key="grant"><a-icon type="user-add" />{{ $t('grant') }}</a-menu-item>
width: '16px', <a-menu-item key="revoke"><a-icon type="user-delete" />{{ $t('revoke') }}</a-menu-item>
height: '16px', <a-menu-item key="view"><a-icon type="eye" />{{ $t('cmdb.serviceTree.view') }}</a-menu-item>
borderRadius: '50%', <a-menu-divider />
backgroundColor: '#d3d3d3', <a-menu-item
color: '#fff', key="batch"
textAlign: 'center', ><ops-icon type="icon-xianxing-copy" />{{ $t('cmdb.serviceTree.batch') }}</a-menu-item
lineHeight: '16px', >
fontSize: '12px', </template>
}" <template v-else>
v-else <a-menu-item
>{{ ciTypeName ? ciTypeName[0].toUpperCase() : 'i' }}</span :disabled="!batchTreeKey || !batchTreeKey.length"
> key="batchGrant"
<span :style="{ marginLeft: '5px' }">{{ this.title }}</span> ><a-icon type="user-add" />{{ $t('grant') }}</a-menu-item
</span> >
<a-icon :style="{ fontSize: '10px' }" v-if="childLength && !isLeaf" :type="switchIcon"></a-icon> <a-menu-item
</div> :disabled="!batchTreeKey || !batchTreeKey.length"
</a-dropdown> key="batchRevoke"
</template> ><a-icon type="user-delete" />{{ $t('revoke') }}</a-menu-item
>
<script> <a-menu-divider />
export default { <template v-if="showBatchLevel > 0">
name: 'ContextMenu', <a-menu-item
props: { :disabled="!batchTreeKey || !batchTreeKey.length"
title: { key="batchDelete"
type: String, ><ops-icon type="icon-xianxing-delete" />{{ $t('delete') }}</a-menu-item
default: '', >
}, <a-menu-divider />
treeKey: { </template>
type: String, <a-menu-item key="batchCancel"><a-icon type="close-circle" />{{ $t('cancel') }}</a-menu-item>
default: '', </template>
}, </a-menu>
levels: { <a-icon class="relation-views-node-operation" type="ellipsis" />
type: Array, </a-dropdown>
default: () => [], <a-icon :style="{ fontSize: '10px' }" v-if="childLength && !isLeaf" :type="switchIcon"></a-icon>
}, </div>
currentViews: { </template>
type: Object,
default: () => {}, <script>
}, export default {
id2type: { name: 'ContextMenu',
type: Object, props: {
default: () => {}, title: {
}, type: String,
isLeaf: { default: '',
type: Boolean, },
default: () => false, treeKey: {
}, type: String,
ciTypes: { default: '',
type: Array, },
default: () => [], levels: {
}, type: Array,
}, default: () => [],
data() { },
return { currentViews: {
switchIcon: 'down', type: Object,
} default: () => {},
}, },
computed: { id2type: {
childLength() { type: Object,
const reg = /(?<=\()\S+(?=\))/g default: () => {},
return Number(this.title.match(reg)[0]) },
}, isLeaf: {
splitTreeKey() { type: Boolean,
return this.treeKey.split('@^@') default: () => false,
}, },
_tempTree() { ciTypeIcons: {
return this.splitTreeKey[this.splitTreeKey.length - 1].split('%') type: Object,
}, default: () => {},
_typeIdIdx() { },
return this.levels.findIndex((level) => level[0] === Number(this._tempTree[1])) // 当前节点在levels中的index showBatchLevel: {
}, type: Number,
showDelete() { default: null,
if (this._typeIdIdx === 0) { },
// 如果是第一层节点则不能删除 batchTreeKey: {
return false type: Array,
} default: () => [],
return true },
}, },
menuList() { data() {
let _menuList = [] return {
if (this._typeIdIdx > -1 && this._typeIdIdx < this.levels.length - 1) { switchIcon: 'down',
// 不是叶子节点 }
const id = Number(this.levels[this._typeIdIdx + 1]) },
_menuList = [ computed: {
{ childLength() {
id, const reg = /(?<=\()\S+(?=\))/g
alias: this.id2type[id].alias || this.id2type[id].name, return Number(this.title.match(reg)[0])
}, },
] splitTreeKey() {
} else { return this.treeKey.split('@^@')
// 叶子节点 },
_menuList = this.currentViews.node2show_types[this._tempTree[1]].map((item) => { _tempTree() {
return { id: item.id, alias: item.alias || item.name } return this.splitTreeKey[this.splitTreeKey.length - 1].split('%')
}) },
} _typeIdIdx() {
return _menuList return this.levels.findIndex((level) => level[0] === Number(this._tempTree[1])) // 当前节点在levels中的index
}, },
icon() { showDelete() {
const _split = this.treeKey.split('@^@') if (this._typeIdIdx === 0) {
const currentNodeTypeId = _split[_split.length - 1].split('%')[1] // 如果是第一层节点则不能删除
const _find = this.ciTypes.find((type) => type.id === Number(currentNodeTypeId)) return false
return _find?.icon || null }
}, return true
ciTypeName() { },
const _split = this.treeKey.split('@^@') menuList() {
const currentNodeTypeId = _split[_split.length - 1].split('%')[1] let _menuList = []
const _find = this.ciTypes.find((type) => type.id === Number(currentNodeTypeId)) if (this._typeIdIdx > -1 && this._typeIdIdx < this.levels.length - 1) {
return _find?.name || '' // 不是叶子节点
}, const id = Number(this.levels[this._typeIdIdx + 1])
}, _menuList = [
methods: { {
onContextMenuClick(treeKey, menuKey) { id,
this.$emit('onContextMenuClick', treeKey, menuKey) alias: this.id2type[id].alias || this.id2type[id].name,
}, },
clickNode() { ]
this.$emit('onNodeClick', this.treeKey) } else {
this.switchIcon = this.switchIcon === 'down' ? 'up' : 'down' // 叶子节点
}, _menuList = this.currentViews.node2show_types[this._tempTree[1]].map((item) => {
}, return { id: item.id, alias: item.alias || item.name }
} })
</script> }
return _menuList
<style></style> },
icon() {
const _split = this.treeKey.split('@^@')
const currentNodeTypeId = _split[_split.length - 1].split('%')[1]
return this.ciTypeIcons[Number(currentNodeTypeId)] ?? null
},
showCheckbox() {
return this.showBatchLevel === this.treeKey.split('@^@').filter((item) => !!item).length - 1
},
},
methods: {
onContextMenuClick(treeKey, menuKey) {
this.$emit('onContextMenuClick', treeKey, menuKey)
},
clickNode() {
this.$emit('onNodeClick', this.treeKey)
this.switchIcon = this.switchIcon === 'down' ? 'up' : 'down'
},
clickCheckbox() {
this.$emit('clickCheckbox', this.treeKey)
},
},
}
</script>
<style lang="less" scoped>
.relation-views-node {
width: 100%;
display: inline-flex;
justify-content: space-between;
align-items: center;
> span {
display: flex;
overflow: hidden;
align-items: center;
width: 100%;
.relation-views-node-icon {
display: inline-block;
width: 16px;
height: 16px;
border-radius: 50%;
background-color: #d3d3d3;
color: #fff;
text-align: center;
line-height: 16px;
font-size: 12px;
}
.relation-views-node-title {
padding-left: 5px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
width: calc(100% - 16px);
}
}
.relation-views-node-operation {
display: none;
margin-right: 5px;
}
}
.relation-views-node-checkbox,
.relation-views-node-moveright {
> span {
.relation-views-node-checkbox {
margin-right: 10px;
}
.relation-views-node-title {
width: calc(100% - 42px);
}
}
}
</style>
<style lang="less">
.relation-views-left .ant-tree-node-content-wrapper:hover {
.relation-views-node-operation {
display: inline-block;
}
}
.relation-views-left {
ul:has(.relation-views-node-checkbox) > li > ul {
margin-left: 26px;
}
ul:has(.relation-views-node-checkbox) {
margin-left: 0 !important;
}
}
</style>

View File

@@ -0,0 +1,105 @@
<template>
<a-modal
width="600px"
:bodyStyle="{
paddingTop: 0,
}"
:visible="visible"
:footer="null"
@cancel="handleCancel"
:title="$t('view')"
>
<div>
<template v-if="readCIIdFilterPermissions && readCIIdFilterPermissions.length">
<p>
<strong>{{ $t('cmdb.serviceTree.idAuthorizationPolicy') }}</strong>
<a
@click="
() => {
showAllReadCIIdFilterPermissions = !showAllReadCIIdFilterPermissions
}
"
v-if="readCIIdFilterPermissions.length > 10"
><a-icon
:type="showAllReadCIIdFilterPermissions ? 'caret-down' : 'caret-up'"
/></a>
</p>
<a-tag
v-for="item in showAllReadCIIdFilterPermissions
? readCIIdFilterPermissions
: readCIIdFilterPermissions.slice(0, 10)"
:key="item.name"
color="blue"
:style="{ marginBottom: '5px' }"
>{{ item.name }}</a-tag
>
<a-tag
:style="{ marginBottom: '5px' }"
v-if="readCIIdFilterPermissions.length > 10 && !showAllReadCIIdFilterPermissions"
>+{{ readCIIdFilterPermissions.length - 10 }}</a-tag
>
</template>
<a-empty v-else>
<img slot="image" :src="require('@/assets/data_empty.png')" />
<span slot="description"> {{ $t('noData') }} </span>
</a-empty>
</div>
</a-modal>
</template>
<script>
import { ciTypeFilterPermissions, getCIType } from '../../../api/CIType'
import FilterComp from '@/components/CMDBFilterComp'
import { searchRole } from '@/modules/acl/api/role'
export default {
name: 'ReadPermissionsModal',
components: { FilterComp },
data() {
return {
visible: false,
filerPerimissions: {},
readCIIdFilterPermissions: [],
canSearchPreferenceAttrList: [],
showAllReadCIIdFilterPermissions: false,
allRoles: [],
}
},
mounted() {
this.loadRoles()
},
methods: {
async loadRoles() {
const res = await searchRole({ page_size: 9999, app_id: 'cmdb', is_all: true })
this.allRoles = res.roles
},
async open(treeKey) {
this.visible = true
const _splitTreeKey = treeKey.split('@^@').filter((item) => !!item)
const _treeKey = _splitTreeKey.slice(_splitTreeKey.length - 1, _splitTreeKey.length)[0].split('%')
const typeId = _treeKey[1]
const _treeKeyPath = _splitTreeKey.map((item) => item.split('%')[0]).join(',')
await ciTypeFilterPermissions(typeId).then((res) => {
this.filerPerimissions = res
})
const readCIIdFilterPermissions = []
Object.entries(this.filerPerimissions).forEach(([k, v]) => {
const { id_filter } = v
if (id_filter && Object.keys(id_filter).includes(_treeKeyPath)) {
const _find = this.allRoles.find((item) => item.id === Number(k))
readCIIdFilterPermissions.push({ name: _find?.name ?? k, rid: k })
}
})
this.readCIIdFilterPermissions = readCIIdFilterPermissions
console.log(readCIIdFilterPermissions)
},
handleCancel() {
this.showAllReadCIIdFilterPermissions = false
this.visible = false
},
},
}
</script>
<style></style>

File diff suppressed because it is too large Load Diff

View File

@@ -32,10 +32,25 @@ body {
&.userLayout { &.userLayout {
overflow: auto; overflow: auto;
} }
.text-color-1 {
color: @text-color_1;
}
.text-color-2 {
color: @text-color_2;
}
.text-color-3 {
color: @text-color_3;
}
.text-color-4 {
color: @text-color_4;
}
.border-radius-box {
border-radius: @border-radius-box;
}
} }
.ant-layout { .ant-layout {
background-color: #custom_colors() [color_2]; background-color: #f7f8fa;
} }
.layout.ant-layout { .layout.ant-layout {
@@ -352,19 +367,8 @@ body {
} }
// 内容区 // 内容区
// .layout-content {
// margin: 24px 24px 0px;
// height: 100%;
// height: 64px;
// padding: 0 12px 0 0;
// }
.ant-layout-content { .ant-layout-content {
padding: 0 24px; padding: 0 24px;
// background: @layout-background-color-light;
//按钮样式
.ant-btn {
// border-radius: 2px;
}
} }
// footer // footer
@@ -504,12 +508,7 @@ body {
transition: none; transition: none;
} }
.ops-side-bar.ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected { .ops-side-bar.ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected {
// background-image: url('../assets/sidebar_selected.png') !important;
// background-repeat: no-repeat !important;
background: @layout-sidebar-selected-color; background: @layout-sidebar-selected-color;
// background-size: 228px 38px;
// background-position-x: -10px;
// background-position-y: center;
transition: none; transition: none;
} }
.ops-side-bar.ant-menu .ant-menu-submenu .ant-menu-item.ant-menu-item-selected { .ops-side-bar.ant-menu .ant-menu-submenu .ant-menu-item.ant-menu-item-selected {
@@ -518,24 +517,16 @@ body {
@keyframes wordsLoop { @keyframes wordsLoop {
0% { 0% {
// transform: translateX(100%);
// -webkit-transform: translateX(100%);
margin-left: 0; margin-left: 0;
} }
100% { 100% {
// transform: translateX(-100%);
// -webkit-transform: translateX(-100%);
margin-left: -300%; margin-left: -300%;
} }
} }
.ops-side-bar.ant-menu-light { .ops-side-bar.ant-menu-light {
border-right-color: transparent; border-right-color: transparent;
// background: @layout-background-light-color;
// background: url('../assets/sidebar_background.png');
background: @layout-sidebar-color; background: @layout-sidebar-color;
// background-position-x: center;
// background-position-y: center;
background-repeat: no-repeat !important; background-repeat: no-repeat !important;
background-size: cover; background-size: cover;
.ant-menu-inline.ant-menu-sub { .ant-menu-inline.ant-menu-sub {
@@ -543,14 +534,12 @@ body {
} }
.ant-menu-submenu-content .ant-menu-item, .ant-menu-submenu-content .ant-menu-item,
.ant-menu-item { .ant-menu-item {
// margin: 0;
> a { > a {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
color: @layout-sidebar-font-color; color: @layout-sidebar-font-color;
} }
&:hover { &:hover {
// background: #0000000a;
.scroll { .scroll {
animation: 5s wordsLoop linear infinite normal; animation: 5s wordsLoop linear infinite normal;
} }
@@ -828,13 +817,13 @@ body {
.el-button.is-plain:hover, .el-button.is-plain:hover,
.el-input.is-active .el-input__inner, .el-input.is-active .el-input__inner,
.el-input__inner:focus { .el-input__inner:focus {
border-color: #custom_colors() [color_1] !important; border-color: @primary-color !important;
} }
.el-button--text, .el-button--text,
.el-select-dropdown__item.selected, .el-select-dropdown__item.selected,
.el-button.is-plain:focus, .el-button.is-plain:focus,
.el-button.is-plain:hover { .el-button.is-plain:hover {
color: #custom_colors() [color_1] !important; color: @primary-color !important;
} }
.ant-tabs-nav .ant-tabs-tab { .ant-tabs-nav .ant-tabs-tab {
@@ -863,7 +852,7 @@ body {
.ant-layout-sider { .ant-layout-sider {
box-shadow: none; box-shadow: none;
.ant-layout-sider-children { .ant-layout-sider-children {
background: #custom_colors[color_2]; background: @primary-color_5;
.ant-menu { .ant-menu {
display: none; display: none;
} }
@@ -894,7 +883,7 @@ body {
} }
} }
.custom-vue-treeselect__control(@bgColor:#custom_colors()[color_2],@border:none) { .custom-vue-treeselect__control(@bgColor:@primary-color_5,@border:none) {
background-color: @bgColor; background-color: @bgColor;
border: @border; border: @border;
} }
@@ -913,6 +902,9 @@ body {
border: none; border: none;
box-shadow: 0px 4px 6px rgba(78, 94, 160, 0.25) !important; box-shadow: 0px 4px 6px rgba(78, 94, 160, 0.25) !important;
} }
.vue-treeselect__limit-tip-text {
margin: 0;
}
} }
// 自定义背景颜色和border // 自定义背景颜色和border
@@ -934,30 +926,30 @@ body {
} }
.vue-treeselect__option--highlight, .vue-treeselect__option--highlight,
.vue-treeselect__option--selected { .vue-treeselect__option--selected {
color: #custom_colors[color_1]; color: @primary-color;
background-color: #custom_colors() [color_2] !important; background-color: @primary-color_5 !important;
} }
.vue-treeselect__checkbox--checked, .vue-treeselect__checkbox--checked,
.vue-treeselect__checkbox--indeterminate { .vue-treeselect__checkbox--indeterminate {
border-color: #custom_colors() [color_1] !important; border-color: @primary-color !important;
background: #custom_colors() [color_1] !important; background: @primary-color !important;
} }
.vue-treeselect__label-container:hover { .vue-treeselect__label-container:hover {
.vue-treeselect__checkbox--checked, .vue-treeselect__checkbox--checked,
.vue-treeselect__checkbox--indeterminate { .vue-treeselect__checkbox--indeterminate {
border-color: #custom_colors() [color_1] !important; border-color: @primary-color !important;
background: #custom_colors() [color_1] !important; background: @primary-color !important;
} }
} }
.vue-treeselect__multi-value-item { .vue-treeselect__multi-value-item {
background: #custom_colors() [color_2] !important; background: @primary-color_5 !important;
color: #custom_colors() [color_1] !important; color: @primary-color !important;
} }
.vue-treeselect__value-remove { .vue-treeselect__value-remove {
color: #custom_colors() [color_1] !important; color: @primary-color !important;
} }
.vue-treeselect__label-container:hover .vue-treeselect__checkbox--unchecked { .vue-treeselect__label-container:hover .vue-treeselect__checkbox--unchecked {
border-color: #custom_colors() [color_1] !important; border-color: @primary-color !important;
} }
//表格样式 //表格样式
@@ -967,7 +959,7 @@ body {
border: none !important; border: none !important;
} }
.vxe-table--header-wrapper { .vxe-table--header-wrapper {
background-color: #custom_colors() [color_2] !important; background-color: @primary-color_5 !important;
} }
.vxe-header--row .vxe-header--column:hover { .vxe-header--row .vxe-header--column:hover {
background: #2f54eb1f !important; background: #2f54eb1f !important;
@@ -991,7 +983,7 @@ body {
border: none !important; border: none !important;
} }
.vxe-table--header-wrapper { .vxe-table--header-wrapper {
background-color: #custom_colors() [color_2] !important; background-color: @primary-color_5 !important;
} }
// .vxe-table--header-wrapper.body--wrapper { // .vxe-table--header-wrapper.body--wrapper {
// border-radius: 8px !important; // border-radius: 8px !important;
@@ -1025,12 +1017,12 @@ body {
.ops-input { .ops-input {
.ant-input, .ant-input,
.ant-time-picker-input { .ant-time-picker-input {
background-color: #custom_colors[color_2]; background-color: @primary-color_5;
border: none; border: none;
} }
} }
.ops-input.ant-input { .ops-input.ant-input {
background-color: #custom_colors[color_2]; background-color: @primary-color_5;
border: none; border: none;
} }
.ops-input.ant-input[disabled] { .ops-input.ant-input[disabled] {
@@ -1101,7 +1093,7 @@ body {
.vxe-pager .vxe-pager--prev-btn:not(.is--disabled):focus, .vxe-pager .vxe-pager--prev-btn:not(.is--disabled):focus,
.vxe-button.type--text:not(.is--disabled):hover, .vxe-button.type--text:not(.is--disabled):hover,
.vxe-table--filter-footer > button:hover { .vxe-table--filter-footer > button:hover {
color: #custom_colors() [color_1] !important; color: @primary-color !important;
} }
.vxe-cell .vxe-default-input:focus, .vxe-cell .vxe-default-input:focus,
@@ -1112,13 +1104,13 @@ body {
.vxe-table--filter-wrapper .vxe-default-textarea:focus, .vxe-table--filter-wrapper .vxe-default-textarea:focus,
.vxe-select.is--active:not(.is--filter) > .vxe-input .vxe-input--inner, .vxe-select.is--active:not(.is--filter) > .vxe-input .vxe-input--inner,
.vxe-input:not(.is--disabled).is--active .vxe-input--inner { .vxe-input:not(.is--disabled).is--active .vxe-input--inner {
border-color: #custom_colors() [color_1] !important; border-color: @primary-color !important;
} }
//批量操作 //批量操作
.ops-list-batch-action { .ops-list-batch-action {
display: inline-block; display: inline-block;
background-color: #custom_colors[color_2]; background-color: @primary-color_5;
font-size: 12px; font-size: 12px;
color: rgba(0, 0, 0, 0.55); color: rgba(0, 0, 0, 0.55);
> span { > span {
@@ -1126,11 +1118,11 @@ body {
padding: 4px 8px; padding: 4px 8px;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
color: #custom_colors[color_1]; color: @primary-color;
} }
} }
> span:last-child { > span:last-child {
color: #custom_colors[color_1]; color: @primary-color;
cursor: default; cursor: default;
} }
} }
@@ -1139,17 +1131,17 @@ body {
.ops-tab.ant-tabs.ant-tabs-card { .ops-tab.ant-tabs.ant-tabs-card {
.ant-tabs-card-bar { .ant-tabs-card-bar {
margin: 0; margin: 0;
border-bottom: none;
.ant-tabs-nav-container { .ant-tabs-nav-container {
background-color: #fff;
.ant-tabs-tab { .ant-tabs-tab {
border: none; border: none;
border-top-left-radius: 4px; border-top-left-radius: 4px;
border-top-right-radius: 4px; border-top-right-radius: 4px;
background: rgba(255, 255, 255, 0.5); background: @primary-color_6;
margin-right: 5px; margin-right: 5px;
} }
.ant-tabs-tab-active { .ant-tabs-tab-active {
background: #custom_colors[color_2]; background: #fff;
} }
} }
} }
@@ -1157,9 +1149,10 @@ body {
//button //button
.ops-button-primary { .ops-button-primary {
background-color: #custom_colors[color_2]; background-color: @primary-color_4;
border-color: #custom_colors[color_2]; border-color: @primary-color_4;
color: #custom_colors[color_1]; color: @primary-color;
box-shadow: none;
} }
//select //select
@@ -1181,8 +1174,8 @@ body {
} }
} }
.ant-select-selection { .ant-select-selection {
background-color: #custom_colors[color_2]; background-color: @primary-color_5;
border-color: #custom_colors[color_2]; border-color: @primary-color_5;
} }
} }
@@ -1190,8 +1183,8 @@ body {
.ops-dropdown { .ops-dropdown {
.ant-dropdown-menu-item:hover, .ant-dropdown-menu-item:hover,
.ant-dropdown-menu-submenu-title:hover { .ant-dropdown-menu-submenu-title:hover {
background-color: #custom_colors[color_2]; background-color: @primary-color_5;
color: #custom_colors[color_1]; color: @primary-color;
} }
} }
@@ -1203,7 +1196,7 @@ body {
border-bottom: none; border-bottom: none;
.ant-modal-title { .ant-modal-title {
padding-left: 10px; padding-left: 10px;
border-left: 4px solid #custom_colors[color_1]; border-left: 4px solid @primary-color;
} }
} }
.ant-modal-footer { .ant-modal-footer {
@@ -1216,7 +1209,7 @@ body {
width: 276px; width: 276px;
} }
.ant-tooltip-inner { .ant-tooltip-inner {
background-color: #custom_colors[color_3]; background-color: @primary-color_3;
border-radius: '4px'; border-radius: '4px';
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
@@ -1231,7 +1224,7 @@ body {
.ant-tooltip-arrow::before { .ant-tooltip-arrow::before {
width: 7px; width: 7px;
height: 7px; height: 7px;
background-color: #custom_colors[color_3]; background-color: @primary-color_3;
} }
} }
@@ -1241,7 +1234,7 @@ body {
.el-tabs__header { .el-tabs__header {
border-bottom: none; border-bottom: none;
background-color: #custom_colors[color_2]; background-color: @primary-color_5;
border-radius: 8px 8px 0px 0px; border-radius: 8px 8px 0px 0px;
} }
@@ -1252,7 +1245,7 @@ body {
.el-tabs__header .el-tabs__item.is-active { .el-tabs__header .el-tabs__item.is-active {
background-color: white; background-color: white;
color: #custom_colors[color_1]; color: @primary-color;
} }
.el-tabs__header .el-tabs__item:first-child.is-active { .el-tabs__header .el-tabs__item:first-child.is-active {
border-top-left-radius: 8px; border-top-left-radius: 8px;
@@ -1263,12 +1256,12 @@ body {
} }
.el-radio__input.is-checked .el-radio__inner { .el-radio__input.is-checked .el-radio__inner {
background-color: #custom_colors[color_1]; background-color: @primary-color;
border-color: #custom_colors[color_1]; border-color: @primary-color;
} }
.el-radio__input.is-checked + .el-radio__label { .el-radio__input.is-checked + .el-radio__label {
color: #custom_colors[color_1]; color: @primary-color;
} }
.el-tab-pane { .el-tab-pane {
@@ -1306,14 +1299,14 @@ body {
// a-drop-down // a-drop-down
.ant-dropdown-menu-item-active { .ant-dropdown-menu-item-active {
color: #custom_colors[color_1]; color: @primary-color;
} }
.ant-tag { .ant-tag {
&.ops-perm-tag { &.ops-perm-tag {
border: none; border: none;
background-color: #custom_colors[color_2]; background-color: @primary-color_5;
color: #custom_colors[color_1]; color: @primary-color;
} }
} }
@@ -1332,7 +1325,7 @@ body {
border: none; border: none;
} }
div.jsoneditor-menu { div.jsoneditor-menu {
border-bottom-color: #custom_colors[color_1]; border-bottom-color: @primary-color;
} }
} }
// .ant-menu.ant-menu-light { // .ant-menu.ant-menu-light {

View File

@@ -1,5 +1,20 @@
@border-radius-base: 2px; // 组件/浮层圆角 @border-radius-base: 2px; // 组件/浮层圆角
@primary-color: #2f54eb; // 全局主色 @border-radius-box: 4px; // big box radius
@primary-color: #2f54eb; // 全局主色 六大品牌色
@primary-color_2: #7f97fa;
@primary-color_3: #d3e3fd;
@primary-color_4: #e1efff;
@primary-color_5: #f0f5ff;
@primary-color_6: #f9fbff;
@text-color_1: #1d2119;
@text-color_2: #4e5969;
@text-color_3: #86909c;
@text-color_4: #a5a9bc;
@border-color: #e4e7ed;
@scrollbar-color: rgba(47, 122, 235, 0.2); @scrollbar-color: rgba(47, 122, 235, 0.2);
@layout-header-background: #fff; @layout-header-background: #fff;
@@ -28,7 +43,7 @@
color_3: #d2e2ff; color_3: #d2e2ff;
} }
.ops_display_wrapper(@backgroundColor:#custom_colors()[color_2]) { .ops_display_wrapper(@backgroundColor:@primary-color_5) {
cursor: pointer; cursor: pointer;
padding: 5px 8px; padding: 5px 8px;
background-color: @backgroundColor; background-color: @backgroundColor;
@@ -42,10 +57,10 @@
cursor: pointer; cursor: pointer;
padding: 5px 10px; padding: 5px 10px;
&:hover { &:hover {
background-color: #custom_colors[color_2]; background-color: @primary-color_5;
} }
} }
.ops_popover_item_selected() { .ops_popover_item_selected() {
background-color: #custom_colors[color_2]; background-color: @primary-color_5;
color: #custom_colors[color_1]; color: @primary-color;
} }

View File

@@ -1,97 +1,106 @@
<template> <template>
<treeselect <treeselect
:disable-branch-nodes="multiple ? false : true" :disable-branch-nodes="multiple ? false : true"
:multiple="multiple" :multiple="multiple"
:options="employeeTreeSelectOption" :options="employeeTreeSelectOption"
:placeholder="readOnly ? '' : placeholder || $t('cs.components.selectEmployee')" :placeholder="readOnly ? '' : placeholder || $t('cs.components.selectEmployee')"
v-model="treeValue" v-model="treeValue"
:max-height="200" :max-height="200"
:noChildrenText="$t('cs.components.empty')" :noChildrenText="$t('cs.components.empty')"
:noOptionsText="$t('cs.components.empty')" :noOptionsText="$t('cs.components.empty')"
:class="className ? className : 'ops-setting-treeselect'" :class="className ? className : 'ops-setting-treeselect'"
value-consists-of="LEAF_PRIORITY" value-consists-of="LEAF_PRIORITY"
:limit="20" :limit="limit"
:limitText="(count) => `+ ${count}`" :limitText="(count) => `+ ${count}`"
v-bind="$attrs" v-bind="$attrs"
appendToBody appendToBody
:zIndex="1050" :zIndex="1050"
> :flat="flat"
</treeselect> >
</template> </treeselect>
</template>
<script>
import Treeselect from '@riophae/vue-treeselect' <script>
import { formatOption } from '@/utils/util' import Treeselect from '@riophae/vue-treeselect'
export default { import { formatOption } from '@/utils/util'
name: 'EmployeeTreeSelect', export default {
components: { name: 'EmployeeTreeSelect',
Treeselect, components: {
}, Treeselect,
model: { },
prop: 'value', model: {
event: 'change', prop: 'value',
}, event: 'change',
props: { },
value: { props: {
type: [String, Array, Number, null], value: {
default: null, type: [String, Array, Number, null],
}, default: null,
multiple: { },
type: Boolean, multiple: {
default: false, type: Boolean,
}, default: false,
className: { },
type: String, className: {
default: 'ops-setting-treeselect', type: String,
}, default: 'ops-setting-treeselect',
placeholder: { },
type: String, placeholder: {
default: '', type: String,
}, default: '',
idType: { },
type: Number, idType: {
default: 1, type: Number,
}, default: 1,
departmentKey: { },
type: String, departmentKey: {
default: 'department_id', type: String,
}, default: 'department_id',
employeeKey: { },
type: String, employeeKey: {
default: 'employee_id', type: String,
}, default: 'employee_id',
}, },
data() { limit: {
return {} type: Number,
}, default: 20,
inject: { },
provide_allTreeDepAndEmp: { flat: {
from: 'provide_allTreeDepAndEmp', type: Boolean,
}, default: false,
readOnly: { },
from: 'readOnly', },
default: false, data() {
}, return {}
}, },
computed: { inject: {
treeValue: { provide_allTreeDepAndEmp: {
get() { from: 'provide_allTreeDepAndEmp',
return this.value },
}, readOnly: {
set(val) { from: 'readOnly',
this.$emit('change', val) default: false,
return val },
}, },
}, computed: {
allTreeDepAndEmp() { treeValue: {
return this.provide_allTreeDepAndEmp() get() {
}, return this.value
employeeTreeSelectOption() { },
return formatOption(this.allTreeDepAndEmp, this.idType, false, this.departmentKey, this.employeeKey) set(val) {
}, this.$emit('change', val)
}, return val
methods: {}, },
} },
</script> allTreeDepAndEmp() {
return this.provide_allTreeDepAndEmp()
<style scoped></style> },
employeeTreeSelectOption() {
return formatOption(this.allTreeDepAndEmp, this.idType, false, this.departmentKey, this.employeeKey)
},
},
methods: {},
}
</script>
<style scoped></style>

View File

@@ -33,7 +33,7 @@ services:
- redis - redis
cmdb-api: cmdb-api:
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.3.12 image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.3.13
# build: # build:
# context: . # context: .
# target: cmdb-api # target: cmdb-api
@@ -70,7 +70,7 @@ services:
- cmdb-api - cmdb-api
cmdb-ui: cmdb-ui:
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.3.12 image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.3.13
# build: # build:
# context: . # context: .
# target: cmdb-ui # target: cmdb-ui

File diff suppressed because one or more lines are too long

View File

@@ -13,7 +13,7 @@
**set database in config file cmdb-api/settings.py** **set database in config file cmdb-api/settings.py**
- In cmdb directory,start in order as follows: - In cmdb directory,start in order as follows:
- enviroment: `make env` - environment: `make env`
- start API: `make api` - start API: `make api`
- start UI: `make ui` - start UI: `make ui`
- start worker: `make worker` - start worker: `make worker`