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

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -52,7 +52,7 @@
: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>
@@ -97,6 +97,7 @@
</a-space> </a-space>
</div> </div>
<a-space> <a-space>
<slot name="extraContent"></slot>
<a-button @click="reset" size="small">{{ $t('reset') }}</a-button> <a-button @click="reset" size="small">{{ $t('reset') }}</a-button>
<a-tooltip :title="$t('cmdb.components.attributeDesc')" v-if="type === 'relationView'"> <a-tooltip :title="$t('cmdb.components.attributeDesc')" v-if="type === 'relationView'">
<a <a
@@ -191,6 +192,9 @@ export default {
} }
}, },
methods: { methods: {
// toggleAdvanced() {
// this.advanced = !this.advanced
// },
getCITypeGroups() { getCITypeGroups() {
getCITypeGroups({ need_other: true }).then((res) => { getCITypeGroups({ need_other: true }).then((res) => {
this.ciTypeGroup = res this.ciTypeGroup = res

View File

@@ -46,8 +46,9 @@ const cmdb_en = {
selectDefaultOrderAttr: 'Select default sorting attributes', selectDefaultOrderAttr: 'Select default sorting attributes',
asec: 'Forward order', asec: 'Forward order',
desc: 'Reverse order', desc: 'Reverse order',
uniqueKey: 'Uniquely Identifies', uniqueKey: 'Unique Identifies',
uniqueKeySelect: 'Please select a unique identifier', uniqueKeySelect: 'Please select a unique identifier',
uniqueKeyTips: 'json/password/computed/choice can not be unique identifies',
notfound: 'Can\'t find what you want?', notfound: 'Can\'t find what you want?',
cannotDeleteGroupTips: 'There is data under this group and cannot be deleted!', cannotDeleteGroupTips: 'There is data under this group and cannot be deleted!',
confirmDeleteGroup: 'Are you sure you want to delete group [{groupName}]?', confirmDeleteGroup: 'Are you sure you want to delete group [{groupName}]?',
@@ -223,7 +224,7 @@ const cmdb_en = {
pleaseSearch: 'Please search', pleaseSearch: 'Please search',
conditionFilter: 'Conditional filtering', conditionFilter: 'Conditional filtering',
attributeDesc: 'Attribute Description', attributeDesc: 'Attribute Description',
ciSearchTips: '1. JSON attributes cannot be searched<br />2. If the search content includes commas, they need to be escaped,<br />3. Only index attributes are searched, non-index attributes use conditional filtering', ciSearchTips: '1. JSON/password/link attributes cannot be searched\n2. If the search content includes commas, they need to be escaped\n3. Only index attributes are searched, non-index attributes use conditional filtering',
ciSearchTips2: 'For example: q=hostname:*0.0.0.0*', ciSearchTips2: 'For example: q=hostname:*0.0.0.0*',
subCIType: 'Subscription CIType', subCIType: 'Subscription CIType',
already: 'already', already: 'already',
@@ -466,7 +467,7 @@ const cmdb_en = {
tips3: 'Please select the fields that need to be modified', tips3: 'Please select the fields that need to be modified',
tips4: 'At least one field must be selected', tips4: 'At least one field must be selected',
tips5: 'Search name | alias', tips5: 'Search name | alias',
tips6: 'Speed up retrieval, full-text search possible, no need to use conditional filtering\n\n json currently does not support indexing \n\nText characters longer than 190 cannot be indexed', tips6: 'Speed up retrieval, full-text search possible, no need to use conditional filtering\n\n json/link/password currently does not support indexing \n\nText characters longer than 190 cannot be indexed',
tips7: 'The form of expression is a drop-down box, and the value must be in the predefined value', tips7: 'The form of expression is a drop-down box, and the value must be in the predefined value',
tips8: 'Multiple values, such as intranet IP', tips8: 'Multiple values, such as intranet IP',
tips9: 'For front-end only', tips9: 'For front-end only',
@@ -483,6 +484,16 @@ const cmdb_en = {
alert1: 'The administrator has not configured the ServiceTree(relation view), or you do not have permission to access it!', alert1: 'The administrator has not configured the ServiceTree(relation view), or you do not have permission to access it!',
copyFailed: 'Copy failed', copyFailed: 'Copy failed',
deleteRelationConfirm: 'Confirm to remove selected {name} from current relationship?', deleteRelationConfirm: 'Confirm to remove selected {name} from current relationship?',
batch: 'Batch',
grantTitle: 'Grant(read)',
userPlaceholder: 'Please select users',
rolePlaceholder: 'Please select roles',
grantedByServiceTree: 'Granted By Service Tree:',
grantedByServiceTreeTips: 'Please delete id_filter in Servive Tree',
peopleHasRead: 'Personnel authorized to read:',
authorizationPolicy: 'CI Authorization Policy:',
idAuthorizationPolicy: 'Authorized by node:',
view: 'View permissions'
}, },
tree: { tree: {
tips1: 'Please go to Preference page first to complete your subscription!', tips1: 'Please go to Preference page first to complete your subscription!',

View File

@@ -48,6 +48,7 @@ const cmdb_zh = {
desc: '倒序', desc: '倒序',
uniqueKey: '唯一标识', uniqueKey: '唯一标识',
uniqueKeySelect: '请选择唯一标识', uniqueKeySelect: '请选择唯一标识',
uniqueKeyTips: 'json、密码、计算属性、预定义值属性不能作为唯一标识',
notfound: '找不到想要的?', notfound: '找不到想要的?',
cannotDeleteGroupTips: '该分组下有数据, 不能删除!', cannotDeleteGroupTips: '该分组下有数据, 不能删除!',
confirmDeleteGroup: '确定要删除分组 【{groupName}】 吗?', confirmDeleteGroup: '确定要删除分组 【{groupName}】 吗?',
@@ -223,7 +224,7 @@ const cmdb_zh = {
pleaseSearch: '请查找', pleaseSearch: '请查找',
conditionFilter: '条件过滤', conditionFilter: '条件过滤',
attributeDesc: '属性说明', attributeDesc: '属性说明',
ciSearchTips: '1. json属性不能搜索<br />2. 搜索内容包括逗号, 则需转义 ,<br />3. 只搜索索引属性, 非索引属性使用条件过滤', ciSearchTips: '1. json、密码、链接属性不能搜索\n2. 搜索内容包括逗号, 则需转义\n3. 只搜索索引属性, 非索引属性使用条件过滤',
ciSearchTips2: '例: q=hostname:*0.0.0.0*', ciSearchTips2: '例: q=hostname:*0.0.0.0*',
subCIType: '订阅模型', subCIType: '订阅模型',
already: '已', already: '已',
@@ -465,7 +466,7 @@ const cmdb_zh = {
tips3: '请选择需要修改的字段', tips3: '请选择需要修改的字段',
tips4: '必须至少选择一个字段', tips4: '必须至少选择一个字段',
tips5: '搜索 名称 | 别名', tips5: '搜索 名称 | 别名',
tips6: '加快检索, 可以全文搜索, 无需使用条件过滤\n\n json目前不支持建索引 \n\n文本字符长度超过190不能建索引', tips6: '加快检索, 可以全文搜索, 无需使用条件过滤\n\n json、链接、密码目前不支持建索引 \n\n文本字符长度超过190不能建索引',
tips7: '表现形式是下拉框, 值必须在预定义值里', tips7: '表现形式是下拉框, 值必须在预定义值里',
tips8: '多值, 比如内网IP', tips8: '多值, 比如内网IP',
tips9: '仅针对前端', tips9: '仅针对前端',
@@ -482,6 +483,16 @@ const cmdb_zh = {
alert1: '管理员 还未配置业务关系, 或者你无权限访问!', alert1: '管理员 还未配置业务关系, 或者你无权限访问!',
copyFailed: '复制失败', copyFailed: '复制失败',
deleteRelationConfirm: '确认将选中的 {name} 从当前关系中删除?', deleteRelationConfirm: '确认将选中的 {name} 从当前关系中删除?',
batch: '批量操作',
grantTitle: '授权(查看权限)',
userPlaceholder: '请选择用户',
rolePlaceholder: '请选择角色',
grantedByServiceTree: '服务树授权:',
grantedByServiceTreeTips: '请先在服务树里删掉节点授权',
peopleHasRead: '当前有查看权限的人员:',
authorizationPolicy: '实例授权策略:',
idAuthorizationPolicy: '按节点授权的:',
view: '查看权限'
}, },
tree: { tree: {
tips1: '请先到 我的订阅 页面完成订阅!', tips1: '请先到 我的订阅 页面完成订阅!',

View File

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

View File

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

View File

@@ -270,7 +270,17 @@
</div> </div>
</el-select> </el-select>
</a-form-item> </a-form-item>
<a-form-item :label="$t('cmdb.ciType.uniqueKey')"> <a-form-item>
<template slot="label">
<a-tooltip :title="$t('cmdb.ciType.uniqueKeyTips')">
<a-icon
style="position:absolute;top:3px;left:-17px;color:#2f54eb;"
type="question-circle"
theme="filled"
/>
</a-tooltip>
<span>{{ $t('cmdb.ciType.uniqueKey') }}</span>
</template>
<el-select <el-select
size="small" size="small"
filterable filterable

View File

@@ -2,14 +2,55 @@
<div :style="{ marginBottom: '-24px', overflow: 'hidden' }"> <div :style="{ marginBottom: '-24px', overflow: 'hidden' }">
<div v-if="relationViews.name2id && relationViews.name2id.length" class="relation-views-wrapper"> <div v-if="relationViews.name2id && relationViews.name2id.length" class="relation-views-wrapper">
<div class="cmdb-views-header"> <div class="cmdb-views-header">
<span class="cmdb-views-header-title">{{ $route.meta.name }}</span> <span
class="cmdb-views-header-title"
>{{ $route.meta.name }}
<div
class="ops-list-batch-action"
:style="{ backgroundColor: '#c0ceeb' }"
v-if="showBatchLevel !== null && batchTreeKey && batchTreeKey.length"
>
<span
@click="
() => {
$refs.grantModal.open('depart')
}
"
>{{ $t('grant') }}</span
>
<a-divider type="vertical" />
<span
@click="
() => {
$refs.revokeModal.open()
}
"
>{{ $t('revoke') }}</span
>
<template v-if="showBatchLevel > 0">
<a-divider type="vertical" />
<span @click="batchDeleteCIRelationFromTree">{{ $t('delete') }}</span>
</template>
<a-divider type="vertical" />
<span
@click="
() => {
showBatchLevel = null
batchTreeKey = []
}
"
>{{ $t('cancel') }}</span
>
<span>{{ $t('selectRows', { rows: batchTreeKey.length }) }}</span>
</div>
</span>
<a-button size="small" icon="user-add" type="primary" ghost @click="handlePerm">{{ $t('grant') }}</a-button> <a-button size="small" icon="user-add" type="primary" ghost @click="handlePerm">{{ $t('grant') }}</a-button>
</div> </div>
<SplitPane <SplitPane
:min="200" :min="200"
:max="500" :max="500"
:paneLengthPixel.sync="paneLengthPixel" :paneLengthPixel.sync="paneLengthPixel"
appName="cmdb-relation-views" :appName="`cmdb-relation-views-${viewId}`"
triggerColor="#F0F5FF" triggerColor="#F0F5FF"
:triggerLength="18" :triggerLength="18"
> >
@@ -24,7 +65,6 @@
@drop="onDrop" @drop="onDrop"
:expandedKeys="expandedKeys" :expandedKeys="expandedKeys"
> >
<a-icon slot="switcherIcon" type="down" />
<template #title="{ key: treeKey, title, isLeaf }"> <template #title="{ key: treeKey, title, isLeaf }">
<ContextMenu <ContextMenu
:title="title" :title="title"
@@ -35,7 +75,10 @@
:id2type="relationViews.id2type" :id2type="relationViews.id2type"
@onContextMenuClick="onContextMenuClick" @onContextMenuClick="onContextMenuClick"
@onNodeClick="onNodeClick" @onNodeClick="onNodeClick"
:ciTypes="ciTypes" :ciTypeIcons="ciTypeIcons"
:showBatchLevel="showBatchLevel"
:batchTreeKey="batchTreeKey"
@clickCheckbox="clickCheckbox"
/> />
</template> </template>
</a-tree> </a-tree>
@@ -313,9 +356,8 @@
v-else-if="relationViews.name2id && !relationViews.name2id.length" v-else-if="relationViews.name2id && !relationViews.name2id.length"
></a-alert> ></a-alert>
<AddTableModal ref="addTableModal" @reload="reload" /> <AddTableModal ref="addTableModal" @reload="reload" />
<!-- <GrantDrawer ref="grantDrawer" resourceTypeName="RelationView" app_id="cmdb" /> -->
<CMDBGrant ref="cmdbGrant" resourceType="RelationView" app_id="cmdb" /> <CMDBGrant ref="cmdbGrant" resourceType="RelationView" app_id="cmdb" />
<GrantModal ref="grantModal" @handleOk="onRelationViewGrant" :customTitle="$t('cmdb.serviceTree.grantTitle')" />
<CiDetailDrawer ref="detail" :typeId="Number(currentTypeId[0])" /> <CiDetailDrawer ref="detail" :typeId="Number(currentTypeId[0])" />
<create-instance-form <create-instance-form
ref="create" ref="create"
@@ -325,11 +367,12 @@
/> />
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" /> <JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
<BatchDownload ref="batchDownload" @batchDownload="batchDownload" /> <BatchDownload ref="batchDownload" @batchDownload="batchDownload" />
<ReadPermissionsModal ref="readPermissionsModal" />
<RevokeModal ref="revokeModal" @handleRevoke="handleRevoke" />
</div> </div>
</template> </template>
<script> <script>
/* eslint-disable no-useless-escape */
import _ from 'lodash' import _ from 'lodash'
import { Tree } from 'element-ui' import { Tree } from 'element-ui'
import Sortable from 'sortablejs' import Sortable from 'sortablejs'
@@ -349,7 +392,7 @@ import {
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr' import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
import { searchCI2, updateCI, deleteCI } from '@/modules/cmdb/api/ci' import { searchCI2, updateCI, deleteCI } from '@/modules/cmdb/api/ci'
import { getCITypes } from '../../api/CIType' import { getCITypeIcons, grantCiType, revokeCiType } from '../../api/CIType'
import { roleHasPermissionToGrant } from '@/modules/acl/api/permission' import { roleHasPermissionToGrant } from '@/modules/acl/api/permission'
import { searchResourceType } from '@/modules/acl/api/resource' import { searchResourceType } from '@/modules/acl/api/resource'
import SplitPane from '@/components/SplitPane' import SplitPane from '@/components/SplitPane'
@@ -361,8 +404,11 @@ import BatchDownload from '../../components/batchDownload/batchDownload.vue'
import PasswordField from '../../components/passwordField/index.vue' import PasswordField from '../../components/passwordField/index.vue'
import PreferenceSearch from '../../components/preferenceSearch/preferenceSearch.vue' import PreferenceSearch from '../../components/preferenceSearch/preferenceSearch.vue'
import CMDBGrant from '../../components/cmdbGrant' import CMDBGrant from '../../components/cmdbGrant'
import GrantModal from '../../components/cmdbGrant/grantModal.vue'
import { ops_move_icon as OpsMoveIcon } from '@/core/icons' import { ops_move_icon as OpsMoveIcon } from '@/core/icons'
import { getAttrPassword } from '../../api/CITypeAttr' import { getAttrPassword } from '../../api/CITypeAttr'
import ReadPermissionsModal from './modules/ReadPermissionsModal.vue'
import RevokeModal from '../../components/cmdbGrant/revokeModal.vue'
export default { export default {
name: 'RelationViews', name: 'RelationViews',
@@ -370,8 +416,8 @@ export default {
SearchForm, SearchForm,
AddTableModal, AddTableModal,
ContextMenu, ContextMenu,
// GrantDrawer,
CMDBGrant, CMDBGrant,
GrantModal,
SplitPane, SplitPane,
ElTree: Tree, ElTree: Tree,
EditAttrsPopover, EditAttrsPopover,
@@ -382,13 +428,15 @@ export default {
PasswordField, PasswordField,
PreferenceSearch, PreferenceSearch,
OpsMoveIcon, OpsMoveIcon,
ReadPermissionsModal,
RevokeModal,
}, },
data() { data() {
return { return {
treeData: [], treeData: [],
triggerSelect: false, triggerSelect: false,
treeNode: null, treeNode: null,
ciTypes: [], ciTypeIcons: {},
relationViews: {}, relationViews: {},
levels: [], levels: [],
showTypeIds: [], showTypeIds: [],
@@ -430,6 +478,10 @@ export default {
passwordValue: {}, passwordValue: {},
lastEditCiId: null, lastEditCiId: null,
isContinueCloseEdit: true, isContinueCloseEdit: true,
contextMenuKey: null,
showBatchLevel: null,
batchTreeKey: [],
} }
}, },
@@ -452,6 +504,21 @@ export default {
isShowBatchIcon() { isShowBatchIcon() {
return !!this.selectedRowKeys.length return !!this.selectedRowKeys.length
}, },
topo_flatten() {
return this.relationViews?.views[this.$route.meta.name]?.topo_flatten ?? []
},
descendant_ids() {
return this.topo_flatten.slice(this.treeKeys.length).join(',')
},
descendant_ids_for_statistics() {
return this.topo_flatten.slice(this.treeKeys.length + 1).join(',')
},
root_parent_path() {
return this.treeKeys
.slice(0, this.treeKeys.length)
.map((item) => item.split('%')[0])
.join(',')
},
}, },
provide() { provide() {
return { return {
@@ -505,8 +572,8 @@ export default {
}) })
}, },
getCITypesList() { getCITypesList() {
getCITypes().then((res) => { getCITypeIcons().then((res) => {
this.ciTypes = res.ci_types this.ciTypeIcons = res
}) })
}, },
refreshTable() { refreshTable() {
@@ -572,33 +639,38 @@ export default {
q = q.slice(1) q = q.slice(1)
} }
if (this.treeKeys.length === 0) { if (this.treeKeys.length === 0) {
await this.judgeCITypes(q) // await this.judgeCITypes(q)
if (!refreshType) { if (!refreshType) {
this.loadRoot() await this.loadRoot()
} }
const fuzzySearch = (this.$refs['search'] || {}).fuzzySearch || '' // const fuzzySearch = (this.$refs['search'] || {}).fuzzySearch || ''
if (fuzzySearch) { // if (fuzzySearch) {
q = `q=_type:${this.currentTypeId[0]},*${fuzzySearch}*,` + q // q = `q=_type:${this.currentTypeId[0]},*${fuzzySearch}*,` + q
} else { // } else {
q = `q=_type:${this.currentTypeId[0]},` + q // q = `q=_type:${this.currentTypeId[0]},` + q
} // }
if (this.currentTypeId[0]) { // if (this.currentTypeId[0] && this.treeData && this.treeData.length) {
const res = await searchCI2(q) // // default select first node
this.pageNo = res.page // this.onNodeClick(this.treeData[0].key)
this.numfound = res.numfound // const res = await searchCI2(q)
res.result.forEach((item, index) => (item.key = item._id)) // const root_id = this.treeData.map((item) => item.id).join(',')
const jsonAttrList = this.preferenceAttrList.filter((attr) => attr.value_type === '6') // q += `&root_id=${root_id}`
console.log(jsonAttrList)
this.instanceList = res['result'].map((item) => { // this.pageNo = res.page
jsonAttrList.forEach( // this.numfound = res.numfound
(jsonAttr) => (item[jsonAttr.name] = item[jsonAttr.name] ? JSON.stringify(item[jsonAttr.name]) : '') // res.result.forEach((item, index) => (item.key = item._id))
) // const jsonAttrList = this.preferenceAttrList.filter((attr) => attr.value_type === '6')
return { ..._.cloneDeep(item) } // console.log(jsonAttrList)
}) // this.instanceList = res['result'].map((item) => {
this.initialInstanceList = _.cloneDeep(this.instanceList) // jsonAttrList.forEach(
this.calcColumns() // (jsonAttr) => (item[jsonAttr.name] = item[jsonAttr.name] ? JSON.stringify(item[jsonAttr.name]) : '')
} // )
// return { ..._.cloneDeep(item) }
// })
// this.initialInstanceList = _.cloneDeep(this.instanceList)
// this.calcColumns()
// }
} else { } else {
q += `&root_id=${this.treeKeys[this.treeKeys.length - 1].split('%')[0]}` q += `&root_id=${this.treeKeys[this.treeKeys.length - 1].split('%')[0]}`
@@ -634,10 +706,10 @@ export default {
level = [1] level = [1]
} }
q += `&level=${level.join(',')}` q += `&level=${level.join(',')}`
await this.judgeCITypes(q)
if (!refreshType) { if (!refreshType) {
this.loadNoRoot(this.treeKeys[this.treeKeys.length - 1], level) this.loadNoRoot(this.treeKeys[this.treeKeys.length - 1], level)
} }
await this.judgeCITypes(q)
const fuzzySearch = (this.$refs['search'] || {}).fuzzySearch || '' const fuzzySearch = (this.$refs['search'] || {}).fuzzySearch || ''
if (fuzzySearch) { if (fuzzySearch) {
q = `q=_type:${this.currentTypeId[0]},*${fuzzySearch}*,` + q q = `q=_type:${this.currentTypeId[0]},*${fuzzySearch}*,` + q
@@ -645,8 +717,12 @@ export default {
q = `q=_type:${this.currentTypeId[0]},` + q q = `q=_type:${this.currentTypeId[0]},` + q
} }
if (Object.values(this.level2constraint).includes('2')) { if (Object.values(this.level2constraint).includes('2')) {
q = q + `&&has_m2m=1` q = q + `&has_m2m=1`
} }
if (this.root_parent_path) {
q = q + `&root_parent_path=${this.root_parent_path}`
}
q = q + `&descendant_ids=${this.descendant_ids}`
if (this.currentTypeId[0]) { if (this.currentTypeId[0]) {
const res = await searchCIRelation(q) const res = await searchCIRelation(q)
@@ -666,7 +742,6 @@ export default {
this.calcColumns() this.calcColumns()
} }
if (refreshType === 'refreshNumber') { if (refreshType === 'refreshNumber') {
const promises = this.treeKeys.map((key, index) => { const promises = this.treeKeys.map((key, index) => {
let ancestor_ids let ancestor_ids
@@ -684,8 +759,9 @@ export default {
ancestor_ids, ancestor_ids,
root_ids: key.split('%')[0], root_ids: key.split('%')[0],
level: this.treeKeys.length - index, level: this.treeKeys.length - index,
type_ids: this.showTypes.map((type) => type.id).join(','), type_ids: this.leaf2showTypes[this.leaf[0]].join(','),
has_m2m: Number(Object.values(this.level2constraint).includes('2')), has_m2m: Number(Object.values(this.level2constraint).includes('2')),
descendant_ids: this.descendant_ids_for_statistics,
}).then((res) => { }).then((res) => {
let result let result
const getTreeItem = (data, id) => { const getTreeItem = (data, id) => {
@@ -741,22 +817,25 @@ export default {
const promises = _showTypeIds.map((typeId) => { const promises = _showTypeIds.map((typeId) => {
let _q = (`q=_type:${typeId},` + q).replace(/count=\d*/, 'count=1') let _q = (`q=_type:${typeId},` + q).replace(/count=\d*/, 'count=1')
if (Object.values(this.level2constraint).includes('2')) { if (Object.values(this.level2constraint).includes('2')) {
_q = _q + `&&has_m2m=1` _q = _q + `&has_m2m=1`
} }
console.log(_q) if (this.root_parent_path) {
if (this.treeKeys.length === 0) { _q = _q + `&root_parent_path=${this.root_parent_path}`
return searchCI2(_q).then((res) => {
if (res.numfound !== 0) {
showTypeIds.push(typeId)
}
})
} else {
return searchCIRelation(_q).then((res) => {
if (res.numfound !== 0) {
showTypeIds.push(typeId)
}
})
} }
// if (this.treeKeys.length === 0) {
// return searchCI2(_q).then((res) => {
// if (res.numfound !== 0) {
// showTypeIds.push(typeId)
// }
// })
// } else {
_q = _q + `&descendant_ids=${this.descendant_ids}`
return searchCIRelation(_q).then((res) => {
if (res.numfound !== 0) {
showTypeIds.push(typeId)
}
})
// }
}) })
await Promise.all(promises).then(async () => { await Promise.all(promises).then(async () => {
if (showTypeIds.length && showTypeIds.sort().join(',') !== this.showTypeIds.sort().join(',')) { if (showTypeIds.length && showTypeIds.sort().join(',') !== this.showTypeIds.sort().join(',')) {
@@ -780,7 +859,7 @@ export default {
}, },
async loadRoot() { async loadRoot() {
searchCI2(`q=_type:(${this.levels[0].join(';')})&count=10000`).then(async (res) => { await searchCI2(`q=_type:(${this.levels[0].join(';')})&count=10000&use_id_filter=1`).then(async (res) => {
const facet = [] const facet = []
const ciIds = [] const ciIds = []
res.result.forEach((item) => { res.result.forEach((item) => {
@@ -797,8 +876,9 @@ export default {
return statisticsCIRelation({ return statisticsCIRelation({
root_ids: ciIds.join(','), root_ids: ciIds.join(','),
level: level, level: level,
type_ids: this.showTypes.map((type) => type.id).join(','), type_ids: this.leaf2showTypes[this.leaf[0]].join(','),
has_m2m: Number(Object.values(this.level2constraint).includes('2')), has_m2m: Number(Object.values(this.level2constraint).includes('2')),
descendant_ids: this.descendant_ids_for_statistics,
}).then((num) => { }).then((num) => {
facet.forEach((item, idx) => { facet.forEach((item, idx) => {
item[1] += num[ciIds[idx] + ''] item[1] += num[ciIds[idx] + '']
@@ -806,16 +886,17 @@ export default {
}) })
}) })
await Promise.all(promises) await Promise.all(promises)
this.wrapTreeData(facet, 'loadRoot') this.wrapTreeData(facet)
// default select first node
this.onNodeClick(this.treeData[0].key)
}) })
}, },
async loadNoRoot(rootIdAndTypeId, level) { async loadNoRoot(rootIdAndTypeId, level) {
const rootId = rootIdAndTypeId.split('%')[0] const rootId = rootIdAndTypeId.split('%')[0]
const typeId = Number(rootIdAndTypeId.split('%')[1]) const typeId = Number(rootIdAndTypeId.split('%')[1])
const topo_flatten = this.relationViews?.views[this.$route.meta.name]?.topo_flatten ?? [] const index = this.topo_flatten.findIndex((id) => id === typeId)
const index = topo_flatten.findIndex((id) => id === typeId) const _type = this.topo_flatten[index + 1]
const _type = topo_flatten[index + 1]
if (_type) { if (_type) {
let q = `q=_type:${_type}&root_id=${rootId}&level=1&count=10000` let q = `q=_type:${_type}&root_id=${rootId}&level=1&count=10000`
if ( if (
@@ -829,8 +910,12 @@ export default {
.join(',')}` .join(',')}`
} }
if (Object.values(this.level2constraint).includes('2')) { if (Object.values(this.level2constraint).includes('2')) {
q = q + `&&has_m2m=1` q = q + `&has_m2m=1`
} }
if (this.root_parent_path) {
q = q + `&root_parent_path=${this.root_parent_path}`
}
q = q + `&descendant_ids=${this.descendant_ids}`
searchCIRelation(q).then(async (res) => { searchCIRelation(q).then(async (res) => {
const facet = [] const facet = []
const ciIds = [] const ciIds = []
@@ -852,8 +937,9 @@ export default {
ancestor_ids, ancestor_ids,
root_ids: ciIds.join(','), root_ids: ciIds.join(','),
level: _level - 1, level: _level - 1,
type_ids: this.showTypes.map((type) => type.id).join(','), type_ids: this.leaf2showTypes[this.leaf[0]].join(','),
has_m2m: Number(Object.values(this.level2constraint).includes('2')), has_m2m: Number(Object.values(this.level2constraint).includes('2')),
descendant_ids: this.descendant_ids_for_statistics,
}).then((num) => { }).then((num) => {
facet.forEach((item, idx) => { facet.forEach((item, idx) => {
item[1] += num[ciIds[idx] + ''] item[1] += num[ciIds[idx] + '']
@@ -862,7 +948,7 @@ export default {
} }
}) })
await Promise.all(promises) await Promise.all(promises)
this.wrapTreeData(facet, 'loadNoRoot') this.wrapTreeData(facet)
}) })
} }
}, },
@@ -917,6 +1003,7 @@ export default {
} }
this.treeKeys = treeNode.eventKey.split('@^@').filter((item) => item !== '') this.treeKeys = treeNode.eventKey.split('@^@').filter((item) => item !== '')
this.treeNode = treeNode this.treeNode = treeNode
// this.refreshTable()
resolve() resolve()
}) })
}, },
@@ -979,27 +1066,37 @@ export default {
this.$refs.xTable.refreshColumn() this.$refs.xTable.refreshColumn()
}) })
}, },
calculateParamsFromTreeKey(treeKey, menuKey) {
const splitTreeKey = treeKey.split('@^@')
const _tempTree = splitTreeKey[splitTreeKey.length - 1].split('%')
const firstCIObj = JSON.parse(_tempTree[2])
const firstCIId = _tempTree[0]
let ancestor_ids
if (
Object.keys(this.level2constraint).some(
(le) => le < Object.keys(this.level2constraint).length && this.level2constraint[le] === '2'
)
) {
const ancestor = treeKey
.split('@^@')
.slice(0, menuKey === 'delete' ? treeKey.split('@^@').length - 2 : treeKey.split('@^@').length - 1)
ancestor_ids = ancestor.map((item) => item.split('%')[0]).join(',')
}
return { splitTreeKey, firstCIObj, firstCIId, _tempTree, ancestor_ids }
},
onContextMenuClick(treeKey, menuKey) { onContextMenuClick(treeKey, menuKey) {
if (treeKey) { if (treeKey) {
const splitTreeKey = treeKey.split('@^@') if (!['batchGrant', 'batchRevoke', 'batchDelete', 'batchCancel'].includes(menuKey)) {
const _tempTree = splitTreeKey[splitTreeKey.length - 1].split('%') this.contextMenuKey = treeKey
const firstCIObj = JSON.parse(_tempTree[2])
const firstCIId = _tempTree[0]
let ancestor_ids
if (
Object.keys(this.level2constraint).some(
(le) => le < Object.keys(this.level2constraint).length && this.level2constraint[le] === '2'
)
) {
const ancestor = treeKey
.split('@^@')
.slice(0, menuKey === 'delete' ? treeKey.split('@^@').length - 2 : treeKey.split('@^@').length - 1)
ancestor_ids = ancestor.map((item) => item.split('%')[0]).join(',')
} }
const { splitTreeKey, firstCIObj, firstCIId, _tempTree, ancestor_ids } = this.calculateParamsFromTreeKey(
treeKey,
menuKey
)
if (menuKey === 'delete') { if (menuKey === 'delete') {
const _tempTreeParent = splitTreeKey[splitTreeKey.length - 2].split('%') const _tempTreeParent = splitTreeKey[splitTreeKey.length - 2].split('%')
const that = this const that = this
this.$confirm({ this.$confirm({
title: that.$t('warning'), title: that.$t('warning'),
content: (h) => <div>{that.$t('confirmDelete2', { name: Object.values(firstCIObj)[0] })}</div>, content: (h) => <div>{that.$t('confirmDelete2', { name: Object.values(firstCIObj)[0] })}</div>,
@@ -1012,6 +1109,24 @@ export default {
}) })
}, },
}) })
} else if (menuKey === 'grant') {
this.$refs.grantModal.open('depart')
} else if (menuKey === 'revoke') {
this.$refs.revokeModal.open()
} else if (menuKey === 'view') {
this.$refs.readPermissionsModal.open(treeKey)
} else if (menuKey === 'batch') {
this.showBatchLevel = splitTreeKey.filter((item) => !!item).length - 1
this.batchTreeKey = []
} else if (menuKey === 'batchGrant') {
this.$refs.grantModal.open('depart')
} else if (menuKey === 'batchRevoke') {
this.$refs.revokeModal.open()
} else if (menuKey === 'batchDelete') {
this.batchDeleteCIRelationFromTree()
} else if (menuKey === 'batchCancel') {
this.showBatchLevel = null
this.batchTreeKey = []
} else { } else {
const childTypeId = menuKey const childTypeId = menuKey
this.$refs.addTableModal.openModal(firstCIObj, firstCIId, childTypeId, 'children', ancestor_ids) this.$refs.addTableModal.openModal(firstCIObj, firstCIId, childTypeId, 'children', ancestor_ids)
@@ -1066,8 +1181,10 @@ export default {
const _splitTargetKey = targetKey.split('@^@').filter((item) => item !== '') const _splitTargetKey = targetKey.split('@^@').filter((item) => item !== '')
if (_splitDragKey.length - 1 === _splitTargetKey.length) { if (_splitDragKey.length - 1 === _splitTargetKey.length) {
const dragId = _splitDragKey[_splitDragKey.length - 1].split('%')[0] const dragId = _splitDragKey[_splitDragKey.length - 1].split('%')[0]
// const targetObj = JSON.parse(_splitTargetKey[_splitTargetKey.length - 1].split('%')[2])
const targetId = _splitTargetKey[_splitTargetKey.length - 1].split('%')[0] const targetId = _splitTargetKey[_splitTargetKey.length - 1].split('%')[0]
console.log(_splitDragKey) console.log(_splitDragKey)
// TODO 拖拽这里不造咋弄 等等再说吧
batchUpdateCIRelationChildren([dragId], [targetId]).then((res) => { batchUpdateCIRelationChildren([dragId], [targetId]).then((res) => {
this.reload() this.reload()
}) })
@@ -1438,6 +1555,138 @@ export default {
this.$message.error(this.$t('cmdb.serviceTreecopyFailed')) this.$message.error(this.$t('cmdb.serviceTreecopyFailed'))
}) })
}, },
async onRelationViewGrant({ department, user }, type) {
const result = []
if (this.showBatchLevel !== null && this.batchTreeKey && this.batchTreeKey.length) {
for (let i = 0; i < this.batchTreeKey.length; i++) {
await this.relationViewGrant({ department, user }, this.batchTreeKey[i], (_result) => {
result.push(..._result)
})
}
this.showBatchLevel = null
this.batchTreeKey = []
} else {
await this.relationViewGrant({ department, user }, this.contextMenuKey, (_result) => {
result.push(..._result)
})
}
if (result.every((r) => r.status === 'fulfilled')) {
this.$message.success(this.$t('operateSuccess'))
}
},
async relationViewGrant({ department, user }, nodeKey, callback) {
const needGrantNodes = nodeKey
.split('@^@')
.filter((item) => !!item)
.reverse()
console.log(needGrantNodes)
const needGrantRids = [...department, ...user]
const floor = Math.ceil(needGrantRids.length / 6)
const result = []
for (let i = 0; i < needGrantNodes.length; i++) {
const grantNode = needGrantNodes[i]
const _grantNode = grantNode.split('%')
const ciId = _grantNode[0]
const typeId = _grantNode[1]
const uniqueValue = Object.entries(JSON.parse(_grantNode[2]))[0][1]
const parent_path = needGrantNodes
.slice(i + 1)
.map((item) => {
return Number(item.split('%')[0])
})
.reverse()
.join(',')
for (let j = 0; j < floor; j++) {
const itemList = needGrantRids.slice(6 * j, 6 * j + 6)
const promises = itemList.map((rid) =>
grantCiType(typeId, rid, {
id_filter: { [ciId]: { name: uniqueValue, parent_path } },
is_recursive: Number(i > 0),
})
)
const _result = await Promise.allSettled(promises)
result.push(..._result)
}
}
callback(result)
},
clickCheckbox(treeKey) {
const _idx = this.batchTreeKey.findIndex((item) => item === treeKey)
if (_idx > -1) {
this.batchTreeKey.splice(_idx, 1)
} else {
this.batchTreeKey.push(treeKey)
}
},
batchDeleteCIRelationFromTree() {
const that = this
this.$confirm({
title: that.$t('warning'),
content: (h) => <div>{that.$t('confirmDelete')}</div>,
async onOk() {
for (let i = 0; i < that.batchTreeKey.length; i++) {
const { splitTreeKey, firstCIObj, firstCIId, _tempTree, ancestor_ids } = that.calculateParamsFromTreeKey(
that.batchTreeKey[i],
'delete'
)
const _tempTreeParent = splitTreeKey[splitTreeKey.length - 2].split('%')
await deleteCIRelationView(_tempTreeParent[0], _tempTree[0], { ancestor_ids }).then((res) => {})
}
that.$message.success(that.$t('deleteSuccess'))
that.showBatchLevel = null
that.batchTreeKey = []
setTimeout(() => {
that.reload()
}, 500)
},
})
},
async handleSingleRevoke({ users = [], roles = [] }, treeKey, callback) {
const rids = [...users.map((item) => Number(item.split('-')[1])), ...roles]
const treeKeyPath = treeKey.split('@^@').filter((item) => !!item)
const _treeKey = treeKeyPath.pop(-1).split('%')
const id_filter = {}
const typeId = _treeKey[1]
const ciId = _treeKey[0]
const uniqueValue = Object.entries(JSON.parse(_treeKey[2]))[0][1]
const parent_path = treeKeyPath
.map((item) => {
return Number(item.split('%')[0])
})
.join(',')
id_filter[ciId] = { name: uniqueValue, parent_path }
const floor = Math.ceil(rids.length / 6)
const result = []
for (let j = 0; j < floor; j++) {
const itemList = rids.slice(6 * j, 6 * j + 6)
const promises = itemList.map((rid) => revokeCiType(typeId, rid, { id_filter, perms: ['read'], parent_path }))
const _result = await Promise.allSettled(promises)
result.push(..._result)
}
callback(result)
},
async handleRevoke({ users = [], roles = [] }) {
const result = []
if (this.showBatchLevel !== null && this.batchTreeKey && this.batchTreeKey.length) {
for (let i = 0; i < this.batchTreeKey.length; i++) {
const treeKey = this.batchTreeKey[i]
await this.handleSingleRevoke({ users, roles }, treeKey, (_result) => {
result.push(..._result)
})
}
} else {
await this.handleSingleRevoke({ users, roles }, this.contextMenuKey, (_result) => {
result.push(..._result)
})
}
if (result.every((r) => r.status === 'fulfilled')) {
this.$message.success(this.$t('operateSuccess'))
}
this.showBatchLevel = null
this.batchTreeKey = []
},
}, },
} }
</script> </script>

View File

@@ -11,24 +11,24 @@
> >
<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
v-model="expression"
class="ci-searchform-expression"
:style="{ width, marginBottom: '10px' }"
:placeholder="placeholder"
@focus="
() => {
isFocusExpression = true
}
"
/> -->
<SearchForm <SearchForm
ref="searchForm" ref="searchForm"
:typeId="addTypeId" :typeId="addTypeId"
:preferenceAttrList="preferenceAttrList" :preferenceAttrList="preferenceAttrList"
@refresh="handleSearch" @refresh="handleSearch"
/> >
<!-- <a @click="handleSearch"><a-icon type="search"/></a> --> <a-button
@click="
() => {
$refs.createInstanceForm.handleOpen(true, 'create')
}
"
slot="extraContent"
type="primary"
size="small"
>新增</a-button
>
</SearchForm>
<vxe-table <vxe-table
ref="xTable" ref="xTable"
row-id="_id" row-id="_id"
@@ -77,19 +77,31 @@
/> />
</a-spin> </a-spin>
</div> </div>
<CreateInstanceForm
ref="createInstanceForm"
:typeIdFromRelation="addTypeId"
@reload="
() => {
currentPage = 1
getTableData(true)
}
"
/>
</a-modal> </a-modal>
</template> </template>
<script> <script>
/* eslint-disable no-useless-escape */
import { searchCI } from '@/modules/cmdb/api/ci' import { searchCI } from '@/modules/cmdb/api/ci'
import { getSubscribeAttributes } from '@/modules/cmdb/api/preference' import { getSubscribeAttributes } from '@/modules/cmdb/api/preference'
import { batchUpdateCIRelationChildren, batchUpdateCIRelationParents } from '@/modules/cmdb/api/CIRelation' import { batchUpdateCIRelationChildren, batchUpdateCIRelationParents } from '@/modules/cmdb/api/CIRelation'
import { getCITableColumns } from '../../../utils/helper' import { getCITableColumns } from '../../../utils/helper'
import SearchForm from '../../../components/searchForm/SearchForm.vue' import SearchForm from '../../../components/searchForm/SearchForm.vue'
import CreateInstanceForm from '../../ci/modules/CreateInstanceForm.vue'
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
export default { export default {
name: 'AddTableModal', name: 'AddTableModal',
components: { SearchForm }, components: { SearchForm, CreateInstanceForm },
data() { data() {
return { return {
visible: false, visible: false,
@@ -106,6 +118,7 @@ export default {
type: 'children', type: 'children',
preferenceAttrList: [], preferenceAttrList: [],
ancestor_ids: undefined, ancestor_ids: undefined,
attrList1: [],
} }
}, },
computed: { computed: {
@@ -119,6 +132,13 @@ export default {
return this.isFocusExpression ? '500px' : '100px' return this.isFocusExpression ? '500px' : '100px'
}, },
}, },
provide() {
return {
attrList: () => {
return this.attrList
},
}
},
watch: {}, watch: {},
methods: { methods: {
async openModal(ciObj, ciId, addTypeId, type, ancestor_ids = undefined) { async openModal(ciObj, ciId, addTypeId, type, ancestor_ids = undefined) {
@@ -132,6 +152,9 @@ export default {
await getSubscribeAttributes(addTypeId).then((res) => { await getSubscribeAttributes(addTypeId).then((res) => {
this.preferenceAttrList = res.attributes // 已经订阅的全部列 this.preferenceAttrList = res.attributes // 已经订阅的全部列
}) })
getCITypeAttributesById(addTypeId).then((res) => {
this.attrList = res.attributes
})
this.getTableData(true) this.getTableData(true)
}, },
async getTableData(isInit) { async getTableData(isInit) {
@@ -207,6 +230,9 @@ export default {
this.handleClose() this.handleClose()
this.$emit('reload') this.$emit('reload')
}, 500) }, 500)
} else {
this.handleClose()
this.$emit('reload')
} }
}, },
handleSearch() { handleSearch() {

View File

@@ -1,63 +1,81 @@
<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',
width: '100%',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
alignItems: 'center',
}"
>
<template v-if="icon">
<img
v-if="icon.split('$$')[2]"
:src="`/api/common-setting/v1/file/${icon.split('$$')[3]}`"
:style="{ maxHeight: '14px', maxWidth: '14px' }"
/>
<ops-icon
v-else
:style="{
color: icon.split('$$')[1],
fontSize: '14px',
}"
:type="icon.split('$$')[0]"
/>
</template>
<span
:style="{ :style="{
display: 'inline-block', color: icon.split('$$')[1],
width: '16px', fontSize: '14px',
height: '16px',
borderRadius: '50%',
backgroundColor: '#d3d3d3',
color: '#fff',
textAlign: 'center',
lineHeight: '16px',
fontSize: '12px',
}" }"
v-else :type="icon.split('$$')[0]"
>{{ ciTypeName ? ciTypeName[0].toUpperCase() : 'i' }}</span />
> <span class="relation-views-node-icon" v-else>{{ icon ? icon[0].toUpperCase() : 'i' }}</span>
<span :style="{ marginLeft: '5px' }">{{ this.title }}</span> </template>
</span> <span class="relation-views-node-title">{{ this.title }}</span>
<a-icon :style="{ fontSize: '10px' }" v-if="childLength && !isLeaf" :type="switchIcon"></a-icon> </span>
</div> <a-dropdown>
</a-dropdown> <a-menu slot="overlay" @click="({ key: menuKey }) => this.onContextMenuClick(this.treeKey, menuKey)">
<template v-if="showBatchLevel === null">
<a-menu-item
v-for="item in menuList"
:key="item.id"
><a-icon type="plus-circle" />{{ $t('new') }} {{ item.alias }}</a-menu-item
>
<a-menu-item
v-if="showDelete"
key="delete"
><ops-icon type="icon-xianxing-delete" />{{ $t('cmdb.serviceTree.deleteNode') }}</a-menu-item
>
<a-menu-divider />
<a-menu-item key="grant"><a-icon type="user-add" />{{ $t('grant') }}</a-menu-item>
<a-menu-item key="revoke"><a-icon type="user-delete" />{{ $t('revoke') }}</a-menu-item>
<a-menu-item key="view"><a-icon type="eye" />{{ $t('cmdb.serviceTree.view') }}</a-menu-item>
<a-menu-divider />
<a-menu-item
key="batch"
><ops-icon type="icon-xianxing-copy" />{{ $t('cmdb.serviceTree.batch') }}</a-menu-item
>
</template>
<template v-else>
<a-menu-item
:disabled="!batchTreeKey || !batchTreeKey.length"
key="batchGrant"
><a-icon type="user-add" />{{ $t('grant') }}</a-menu-item
>
<a-menu-item
:disabled="!batchTreeKey || !batchTreeKey.length"
key="batchRevoke"
><a-icon type="user-delete" />{{ $t('revoke') }}</a-menu-item
>
<a-menu-divider />
<template v-if="showBatchLevel > 0">
<a-menu-item
:disabled="!batchTreeKey || !batchTreeKey.length"
key="batchDelete"
><ops-icon type="icon-xianxing-delete" />{{ $t('delete') }}</a-menu-item
>
<a-menu-divider />
</template>
<a-menu-item key="batchCancel"><a-icon type="close-circle" />{{ $t('cancel') }}</a-menu-item>
</template>
</a-menu>
<a-icon class="relation-views-node-operation" type="ellipsis" />
</a-dropdown>
<a-icon :style="{ fontSize: '10px' }" v-if="childLength && !isLeaf" :type="switchIcon"></a-icon>
</div>
</template> </template>
<script> <script>
@@ -88,7 +106,15 @@ export default {
type: Boolean, type: Boolean,
default: () => false, default: () => false,
}, },
ciTypes: { ciTypeIcons: {
type: Object,
default: () => {},
},
showBatchLevel: {
type: Number,
default: null,
},
batchTreeKey: {
type: Array, type: Array,
default: () => [], default: () => [],
}, },
@@ -141,14 +167,10 @@ export default {
icon() { icon() {
const _split = this.treeKey.split('@^@') const _split = this.treeKey.split('@^@')
const currentNodeTypeId = _split[_split.length - 1].split('%')[1] const currentNodeTypeId = _split[_split.length - 1].split('%')[1]
const _find = this.ciTypes.find((type) => type.id === Number(currentNodeTypeId)) return this.ciTypeIcons[Number(currentNodeTypeId)] ?? null
return _find?.icon || null
}, },
ciTypeName() { showCheckbox() {
const _split = this.treeKey.split('@^@') return this.showBatchLevel === this.treeKey.split('@^@').filter((item) => !!item).length - 1
const currentNodeTypeId = _split[_split.length - 1].split('%')[1]
const _find = this.ciTypes.find((type) => type.id === Number(currentNodeTypeId))
return _find?.name || ''
}, },
}, },
methods: { methods: {
@@ -159,8 +181,73 @@ export default {
this.$emit('onNodeClick', this.treeKey) this.$emit('onNodeClick', this.treeKey)
this.switchIcon = this.switchIcon === 'down' ? 'up' : 'down' this.switchIcon = this.switchIcon === 'down' ? 'up' : 'down'
}, },
clickCheckbox() {
this.$emit('clickCheckbox', this.treeKey)
},
}, },
} }
</script> </script>
<style></style> <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>

View File

@@ -284,6 +284,10 @@ export default {
const regSort = /(?<=sort=).+/g const regSort = /(?<=sort=).+/g
const exp = expression.match(regQ) ? expression.match(regQ)[0] : null const exp = expression.match(regQ) ? expression.match(regQ)[0] : null
// if (exp) {
// exp = exp.replace(/(\:)/g, '$1*')
// exp = exp.replace(/(\,)/g, '*$1')
// }
// 如果是表格点击的排序 以表格为准 // 如果是表格点击的排序 以表格为准
let sort let sort
if (sortByTable) { if (sortByTable) {
@@ -314,7 +318,9 @@ export default {
this.columnsGroup = [] this.columnsGroup = []
this.instanceList = [] this.instanceList = []
this.totalNumber = res['numfound'] this.totalNumber = res['numfound']
if (!res['numfound']) {
return
}
const { attributes: resAllAttributes } = await getCITypeAttributesByTypeIds({ const { attributes: resAllAttributes } = await getCITypeAttributesByTypeIds({
type_ids: Object.keys(res.counter).join(','), type_ids: Object.keys(res.counter).join(','),
}) })

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

@@ -10,11 +10,12 @@
: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> </treeselect>
</template> </template>
@@ -60,6 +61,14 @@ export default {
type: String, type: String,
default: 'employee_id', default: 'employee_id',
}, },
limit: {
type: Number,
default: 20,
},
flat: {
type: Boolean,
default: false,
},
}, },
data() { data() {
return {} return {}

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`