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"
PyMySQL = "==1.1.0"
redis = "==4.6.0"
python-redis-lock = "==4.0.0"
# Migrations
Flask-Migrate = "==2.5.2"
# Deployment

View File

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

View File

@@ -6,6 +6,7 @@ import datetime
import json
import threading
import redis_lock
from flask import abort
from flask import current_app
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.secrets.inner import InnerCrypt
from api.lib.secrets.vault import VaultClient
from api.lib.utils import Lock
from api.lib.utils import handle_arg_list
from api.lib.webhook import webhook_request
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_cache
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 = "******"
@@ -278,16 +278,16 @@ class CIManager(object):
@staticmethod
def _auto_inc_id(attr):
db.session.remove()
db.session.commit()
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(
getattr(value_table, 'value').desc()).first()
if max_v is not None:
return int(max_v.value) + 1
return 1
return 1
@classmethod
def add(cls, ci_type_name,
@@ -312,12 +312,12 @@ class CIManager(object):
unique_key = AttributeCache.get(ci_type.unique_id) or abort(
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 = unique_value or abort(400, ErrFormat.unique_key_required.format(unique_key.name))
unique_value = None
if not (unique_key.default and unique_key.default.get('default') == AttributeDefaultValueEnum.AUTO_INC_ID and
not ci_dict.get(unique_key.name)): # primary key is not auto inc id
unique_value = ci_dict.get(unique_key.name) or ci_dict.get(unique_key.alias) or ci_dict.get(unique_key.id)
unique_value = unique_value or abort(400, ErrFormat.unique_key_required.format(unique_key.name))
attrs = CITypeAttributeManager.get_all_attributes(ci_type.id)
ci_type_attrs_name = {attr.name: attr for _, attr in attrs}
@@ -327,8 +327,15 @@ class CIManager(object):
ci = None
record_id = None
password_dict = {}
need_lock = current_user.username not in current_app.config.get('PRIVILEGED_USERS', PRIVILEGED_USERS)
with Lock(ci_type_name, need_lock=need_lock):
with redis_lock.Lock(rd.r, ci_type.name):
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)
if existed is not None:
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 {}
record_id = None
need_lock = current_user.username not in current_app.config.get('PRIVILEGED_USERS', PRIVILEGED_USERS)
with Lock(ci.ci_type.name, need_lock=need_lock):
with redis_lock.Lock(rd.r, ci.ci_type.name):
db.session.commit()
self._valid_unique_constraint(ci.type_id, ci_dict, ci_id)
ci_dict = {k: v for k, v in ci_dict.items() if k in ci_type_attrs_name}
@@ -551,6 +559,7 @@ class CIManager(object):
AttributeHistoryManger.add(None, ci_id, [(None, OperateType.DELETE, ci_dict, None)], ci.type_id)
ci_delete.apply_async(args=(ci_id,), queue=CMDB_QUEUE)
delete_id_filter.apply_async(args=(ci_id,), queue=CMDB_QUEUE)
return ci_id
@@ -857,6 +866,20 @@ class CIRelationManager(object):
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
def _sort_handler(sort_by, query_sql):
@@ -912,7 +935,7 @@ class CIRelationManager(object):
@staticmethod
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:
return
@@ -972,7 +995,7 @@ class CIRelationManager(object):
else:
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)
@@ -1008,6 +1031,7 @@ class CIRelationManager(object):
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)
delete_id_filter.apply_async(args=(cr.second_ci_id,), queue=CMDB_QUEUE)
return cr_id
@@ -1019,9 +1043,13 @@ class CIRelationManager(object):
to_dict=False,
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
def batch_update(cls, ci_ids, parents, children, ancestor_ids=None):
@@ -1062,7 +1090,7 @@ class CIRelationManager(object):
class CITriggerManager(object):
@staticmethod
def get(type_id):
db.session.remove()
db.session.commit()
return CITypeTrigger.get_by(type_id=type_id, to_dict=True)
@staticmethod

View File

@@ -76,6 +76,10 @@ class CITypeManager(object):
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
def get_ci_types(type_name=None, like=True):
resources = None
@@ -223,10 +227,12 @@ class CITypeManager(object):
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))
for item in CITypeRelation.get_by(parent_id=type_id, to_dict=False):
item.soft_delete(commit=False)
for item in (CITypeRelation.get_by(parent_id=type_id, to_dict=False) +
CITypeRelation.get_by(child_id=type_id, to_dict=False)):
if 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)
for table in [PreferenceTreeView, PreferenceShowAttributes, PreferenceSearchOption, CustomDashboard,
@@ -644,10 +650,30 @@ class CITypeAttributeManager(object):
existed.soft_delete()
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)
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)
CITypeHistoryManager.add(CITypeOperateType.DELETE_ATTRIBUTE, type_id, attr_id=attr.id,

View File

@@ -1,12 +1,15 @@
# -*- coding:utf-8 -*-
import copy
import functools
import redis_lock
from flask import abort
from flask import current_app
from flask import request
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.resp_format import ErrFormat
from api.lib.mixin import DBMixin
@@ -40,6 +43,11 @@ class CIFilterPermsCRUD(DBMixin):
result[i['rid']]['ci_filter'] = ""
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
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'] += (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
@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)
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):
ci_filter = kwargs.get('ci_filter')
attr_filter = kwargs.get('attr_filter') or ""
@@ -102,36 +163,67 @@ class CIFilterPermsCRUD(DBMixin):
def add(self, **kwargs):
kwargs = self._can_add(**kwargs) or kwargs
with redis_lock.Lock(rd.r, 'CMDB_FILTER_{}_{}'.format(kwargs['type_id'], kwargs['rid'])):
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'),
rid=kwargs.get('rid'),
first=True, to_dict=False)
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)
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']
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
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'):
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)
if request_id_filter:
kwargs['id_filter'] = request_id_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):
pass
@@ -140,19 +232,84 @@ class CIFilterPermsCRUD(DBMixin):
pass
def delete(self, **kwargs):
obj = self.cls.get_by(type_id=kwargs.get('type_id'),
rid=kwargs.get('rid'),
first=True, to_dict=False)
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'),
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
if current_app.config.get('USE_ACL'):
resource = ACLManager().del_resource(str(obj.id), ResourceTypeEnum.CI_FILTER)
if obj is not None:
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
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 decorator_has_perm(func):

View File

@@ -16,10 +16,11 @@ def search(query=None,
ret_key=RetKey.NAME,
count=1,
sort=None,
excludes=None):
excludes=None,
use_id_filter=True):
if current_app.config.get("USE_ES"):
s = SearchFromES(query, fl, facet, page, ret_key, count, sort)
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

View File

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

View File

@@ -44,7 +44,10 @@ class Search(object):
count=1,
sort=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.fl = fl or []
self.excludes = excludes or []
@@ -54,12 +57,17 @@ class Search(object):
self.count = count
self.sort = sort
self.ci_ids = ci_ids or []
self.raw_ci_ids = copy.deepcopy(self.ci_ids)
self.query_sql = ""
self.type_id_list = []
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.type2filter_perms = dict()
self.is_app_admin = is_app_admin('cmdb') or current_user.username == "worker"
@staticmethod
def _operator_proc(key):
@@ -106,7 +114,7 @@ class Search(object):
self.type_id_list.append(str(ci_type.id))
if ci_type.id in self.type2filter_perms:
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 = []
ci_filter = Template(ci_filter).render(user=current_user)
for i in ci_filter.split(','):
@@ -122,6 +130,14 @@ class Search(object):
self.fl = set(self.type2filter_perms[ci_type.id]['attr_filter'])
else:
self.fl = set(self.fl) & set(self.type2filter_perms[ci_type.id]['attr_filter'])
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:
raise SearchError(ErrFormat.no_permission.format(ci_type.alias, PermEnum.READ))
else:
@@ -138,7 +154,10 @@ class Search(object):
@staticmethod
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
def _in_query_handler(attr, v, is_not):
@@ -152,6 +171,7 @@ class Search(object):
"NOT LIKE" if is_not else "LIKE",
_v.replace("*", "%")) for _v in new_v])
_query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, in_query)
return _query_sql
@staticmethod
@@ -167,6 +187,7 @@ class Search(object):
"NOT BETWEEN" if is_not else "BETWEEN",
start.replace("*", "%"), end.replace("*", "%"))
_query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, range_query)
return _query_sql
@staticmethod
@@ -183,6 +204,7 @@ class Search(object):
comparison_query = "{0} '{1}'".format(v[0], v[1:].replace("*", "%"))
_query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, comparison_query)
return _query_sql
@staticmethod
@@ -194,6 +216,7 @@ class Search(object):
elif field.startswith("-"):
field = field[1:]
sort_type = "DESC"
return field, sort_type
def __sort_by_id(self, sort_type, query_sql):
@@ -322,6 +345,11 @@ class Search(object):
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):
"""
: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']}
res2 = acl.get_resources(ResourceTypeEnum.CI_FILTER)
if res2:
self.type2filter_perms = CIFilterPermsCRUD().get_by_ids(list(map(int, [i['name'] for i in res2])))
self.__get_type2filter_perms()
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))
def __confirm_type_first(self, queries):
has_type = False
result = []
@@ -371,8 +408,10 @@ class Search(object):
else:
result.append(q)
_is_app_admin = is_app_admin('cmdb') or current_user.username == "worker"
if result and not has_type and not _is_app_admin:
if self.parent_node_perm_passed:
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()
if id_query:
ci = CIManager.get_by_id(id_query)
@@ -381,13 +420,11 @@ class Search(object):
result.insert(0, "_type:{}".format(ci.type_id))
else:
result.insert(0, type_q)
elif _is_app_admin:
elif self.is_app_admin:
self.valid_type_names = "ALL"
else:
self.__get_types_has_read()
current_app.logger.warning(result)
return result
def __query_by_attr(self, q, queries, alias):
@@ -479,7 +516,7 @@ class Search(object):
def _filter_ids(self, query_sql):
if self.ci_ids:
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
@@ -511,6 +548,9 @@ class Search(object):
s = time.time()
if 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
# current_app.logger.debug(query_sql)
numfound, res = self._execute_sql(query_sql)
@@ -569,3 +609,8 @@ class Search(object):
total = len(response)
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 -*-
import json
import sys
from collections import Counter
from flask import abort
from flask import current_app
from flask_login import current_user
from api.extensions import rd
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 REDIS_PREFIX_CI_RELATION
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.search.ci.db.search import Search as SearchFromDB
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 CIRelation
class Search(object):
@@ -29,7 +34,9 @@ class Search(object):
sort=None,
reverse=False,
ancestor_ids=None,
has_m2m=None):
descendant_ids=None,
has_m2m=None,
root_parent_path=None):
self.orig_query = query
self.fl = fl
self.facet_field = facet_field
@@ -46,6 +53,8 @@ class Search(object):
level[0] if isinstance(level, list) and level else level)
self.ancestor_ids = ancestor_ids
self.descendant_ids = descendant_ids
self.root_parent_path = root_parent_path
self.has_m2m = has_m2m or False
if not self.has_m2m:
if self.ancestor_ids:
@@ -56,27 +65,23 @@ class Search(object):
if _l < int(level) and c == ConstraintEnum.Many2Many:
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):
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 = []
key = []
_tmp = []
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:
_tmp = map(lambda x: json.loads(x).keys(),
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
key, prefix = list(map(str, ids)), REDIS_PREFIX_CI_RELATION
else:
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]]))
prefix = REDIS_PREFIX_CI_RELATION2
_tmp = list(map(lambda x: json.loads(x).keys() if x else [], rd.get(key, prefix) or []))
ids = [j for i in _tmp for j in i]
if not key:
if not key or id_filter_limit is None:
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:
merge_ids.extend(ids)
@@ -120,7 +129,28 @@ class Search(object):
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):
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
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,
count=self.count,
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)
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
_tmp = []
level2ids = {}
for lv in range(1, self.level + 1):
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 not self.has_m2m:
key, prefix = ids, REDIS_PREFIX_CI_RELATION
key, prefix = [str(i) for i in ids], REDIS_PREFIX_CI_RELATION
else:
key = ["{},{}".format(self.ancestor_ids, _id) for _id in 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:
key = ["{},{}".format(self.ancestor_ids, _id) for _id in ids]
prefix = REDIS_PREFIX_CI_RELATION2
level2ids[lv] = [[i] for i in key]
if not key:
_tmp = []
if not key or id_filter_limit is None:
_tmp = [[]] * len(ids)
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:
_tmp = list(map(lambda x: [i for i in x if i[1] in type_ids],
(map(lambda x: list(json.loads(x).items()),
[i or '{}' for i in rd.get(key, prefix) or []]))))
_tmp = [[i for i in x if i[1] in type_ids and
(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)]
else:
_tmp = list(map(lambda x: list(json.loads(x).items()),
[i or '{}' for i in rd.get(key, prefix) or []]))
_tmp = [[i 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)]
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:
for idx, item in enumerate(_tmp):
if item:
if not self.has_m2m:
@@ -208,15 +301,22 @@ class Search(object):
level2ids[lv].append(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:
__tmp = map(lambda x: [(_id, type_id) for _id, type_id in json.loads(x).items()
if type_id in type_ids],
filter(lambda x: x is not None,
rd.get(key, prefix) or []))
__tmp = [[i for i in x if i[1] in type_ids and
(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)]
else:
__tmp = map(lambda x: list(json.loads(x).items()),
filter(lambda x: x is not None,
rd.get(key, prefix) or []))
__tmp = [[i 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)]
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:
__tmp = []

View File

@@ -302,9 +302,9 @@ class AttributeValueManager(object):
return self.write_change2(changed)
@staticmethod
def delete_attr_value(attr_id, ci_id):
def delete_attr_value(attr_id, ci_id, commit=True):
attr = AttributeCache.get(attr_id)
if attr is not None:
value_table = TableMap(attr=attr).table
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 redis_lock
from api.extensions import cache
from api.extensions import rd
from api.lib.decorator import flush_db
from api.lib.utils import Lock
from api.models.acl import App
from api.models.acl import Permission
from api.models.acl import Resource
@@ -136,14 +137,14 @@ class HasResourceRoleCache(object):
@classmethod
def add(cls, rid, app_id):
with Lock('HasResourceRoleCache'):
with redis_lock.Lock(rd.r, 'HasResourceRoleCache'):
c = cls.get(app_id)
c[rid] = 1
cache.set(cls.PREFIX_KEY.format(app_id), c, timeout=0)
@classmethod
def remove(cls, rid, app_id):
with Lock('HasResourceRoleCache'):
with redis_lock.Lock(rd.r, 'HasResourceRoleCache'):
c = cls.get(app_id)
c.pop(rid, None)
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):
"""
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
whitespace) between tags. Return "" if tag not found.
"""

View File

@@ -1,8 +1,6 @@
# -*- coding:utf-8 -*-
import base64
import sys
import time
from typing import Set
import elasticsearch
@@ -213,52 +211,6 @@ class ESHandler(object):
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):
BLOCK_SIZE = 16 # Bytes
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'))
ci_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)

View File

@@ -4,6 +4,7 @@
import json
import time
import redis_lock
from flask import current_app
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_RELATION
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 reconnect_db
from api.lib.perm.acl.cache import UserCache
from api.lib.utils import Lock
from api.lib.utils import handle_arg_list
from api.models.cmdb import CI
from api.models.cmdb import CIRelation
@@ -83,6 +84,13 @@ def ci_delete(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)
@reconnect_db
def ci_delete_trigger(trigger, operate_type, ci_dict):
@@ -99,7 +107,7 @@ def ci_delete_trigger(trigger, operate_type, ci_dict):
@flush_db
@reconnect_db
def ci_relation_cache(parent_id, child_id, ancestor_ids):
with Lock("CIRelation_{}".format(parent_id)):
with redis_lock.Lock(rd.r, "CIRelation_{}".format(parent_id)):
if ancestor_ids is None:
children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0]
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)
@reconnect_db
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:
children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0]
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
old_d_rid_in_acl = role_map.get(old_department.department_name, 0)
if old_d_rid_in_acl == 0:
return
if old_d_rid_in_acl != old_department.acl_rid:
old_department.update(
acl_rid=old_d_rid_in_acl
)
d_acl_rid = old_department.acl_rid if old_d_rid_in_acl == old_department.acl_rid else old_d_rid_in_acl
payload = {
'app_id': 'acl',
'parent_id': d_acl_rid,
}
try:
acl.remove_user_from_role(employee_acl_rid, payload)
except Exception as e:
result.append(ErrFormat.acl_remove_user_from_role_failed.format(str(e)))
if old_d_rid_in_acl > 0:
if old_d_rid_in_acl != old_department.acl_rid:
old_department.update(
acl_rid=old_d_rid_in_acl
)
d_acl_rid = old_department.acl_rid if old_d_rid_in_acl == old_department.acl_rid else old_d_rid_in_acl
payload = {
'app_id': 'acl',
'parent_id': d_acl_rid,
}
try:
acl.remove_user_from_role(employee_acl_rid, payload)
except Exception as e:
result.append(ErrFormat.acl_remove_user_from_role_failed.format(str(e)))
payload = {
'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 CIRelationManager
from api.lib.cmdb.const import ExistPolicy
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import ResourceTypeEnum, PermEnum
from api.lib.cmdb.const import RetKey
from api.lib.cmdb.perms import has_perm_for_ci
from api.lib.cmdb.search import SearchError
@@ -152,9 +151,10 @@ class CISearchView(APIView):
ret_key = RetKey.NAME
facet = handle_arg_list(request.values.get("facet", ""))
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()
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:
response, counter, total, page, numfound, facet = s.search()
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.ci_relation.search import Search
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_size
from api.lib.utils import handle_arg_list
@@ -36,6 +35,8 @@ class CIRelationSearchView(APIView):
root_id = request.values.get('root_id')
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'))))
query = request.values.get('q', "")
@@ -47,7 +48,8 @@ class CIRelationSearchView(APIView):
start = time.time()
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:
response, counter, total, page, numfound, facet = s.search()
except SearchError as e:
@@ -65,16 +67,16 @@ class CIRelationSearchView(APIView):
class CIRelationStatisticsView(APIView):
url_prefix = "/ci_relations/statistics"
@auth_abandoned
def get(self):
root_ids = list(map(int, handle_arg_list(request.values.get('root_ids'))))
level = request.values.get('level', 1)
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
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')
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:
result = s.statistics(type_ids)
except SearchError as e:

View File

@@ -38,9 +38,13 @@ from api.resource import 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):
if request.url.endswith("icons"):
return self.jsonify(CITypeManager().get_icons())
q = request.args.get("type_name")
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'):
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
if 'ci_filter' in request.values or 'attr_filter' in request.values:
resource = CIFilterPermsCRUD().add(type_id=type_id, rid=rid, **request.values)
new_resource = None
if 'ci_filter' in request.values or 'attr_filter' in request.values or 'id_filter' in 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.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'):
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
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:
resource = CIFilterPermsCRUD().delete(type_id=type_id, rid=rid)

View File

@@ -37,6 +37,7 @@ PyMySQL==1.1.0
ldap3==2.9.1
PyYAML==6.0.1
redis==4.6.0
python-redis-lock==4.0.0
requests==2.31.0
requests_oauthlib==1.3.1
markdownify==0.11.6

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,5 +1,20 @@
@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);
@layout-header-background: #fff;
@@ -28,7 +43,7 @@
color_3: #d2e2ff;
}
.ops_display_wrapper(@backgroundColor:#custom_colors()[color_2]) {
.ops_display_wrapper(@backgroundColor:@primary-color_5) {
cursor: pointer;
padding: 5px 8px;
background-color: @backgroundColor;
@@ -42,10 +57,10 @@
cursor: pointer;
padding: 5px 10px;
&:hover {
background-color: #custom_colors[color_2];
background-color: @primary-color_5;
}
}
.ops_popover_item_selected() {
background-color: #custom_colors[color_2];
color: #custom_colors[color_1];
background-color: @primary-color_5;
color: @primary-color;
}

View File

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

View File

@@ -33,7 +33,7 @@ services:
- redis
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:
# context: .
# target: cmdb-api
@@ -70,7 +70,7 @@ services:
- cmdb-api
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:
# context: .
# 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**
- In cmdb directory,start in order as follows:
- enviroment: `make env`
- environment: `make env`
- start API: `make api`
- start UI: `make ui`
- start worker: `make worker`