mirror of
				https://github.com/veops/cmdb.git
				synced 2025-10-31 11:09:21 +08:00 
			
		
		
		
	Merge branch 'master' of github.com:veops/cmdb into dev_ui
This commit is contained in:
		| @@ -48,6 +48,7 @@ six = "==1.12.0" | ||||
| bs4 = ">=0.0.1" | ||||
| toposort = ">=1.5" | ||||
| requests = ">=2.22.0" | ||||
| requests_oauthlib = "==1.3.1" | ||||
| PyJWT = "==2.4.0" | ||||
| elasticsearch = "==7.17.9" | ||||
| future = "==0.18.3" | ||||
|   | ||||
| @@ -9,6 +9,7 @@ import time | ||||
| import click | ||||
| from flask import current_app | ||||
| from flask.cli import with_appcontext | ||||
| from flask_login import login_user | ||||
|  | ||||
| import api.lib.cmdb.ci | ||||
| from api.extensions import db | ||||
| @@ -24,6 +25,7 @@ from api.lib.cmdb.const import ValueTypeEnum | ||||
| from api.lib.exception import AbortException | ||||
| from api.lib.perm.acl.acl import ACLManager | ||||
| from api.lib.perm.acl.cache import AppCache | ||||
| from api.lib.perm.acl.cache import UserCache | ||||
| from api.lib.perm.acl.resource import ResourceCRUD | ||||
| from api.lib.perm.acl.resource import ResourceTypeCRUD | ||||
| from api.lib.perm.acl.role import RoleCRUD | ||||
| @@ -207,6 +209,8 @@ def cmdb_counter(): | ||||
|     """ | ||||
|     from api.lib.cmdb.cache import CMDBCounterCache | ||||
|  | ||||
|     current_app.test_request_context().push() | ||||
|     login_user(UserCache.get('worker')) | ||||
|     while True: | ||||
|         try: | ||||
|             db.session.remove() | ||||
|   | ||||
| @@ -161,6 +161,55 @@ class InitDepartment(object): | ||||
|             info = f"update department acl_rid: {acl_rid}" | ||||
|             current_app.logger.info(info) | ||||
| 
 | ||||
|     def init_backend_resource(self): | ||||
|         acl = self.check_app('backend') | ||||
|         resources_types = acl.get_all_resources_types() | ||||
| 
 | ||||
|         results = list(filter(lambda t: t['name'] == '操作权限', resources_types['groups'])) | ||||
|         if len(results) == 0: | ||||
|             payload = dict( | ||||
|                 app_id=acl.app_name, | ||||
|                 name='操作权限', | ||||
|                 description='', | ||||
|                 perms=['read', 'grant', 'delete', 'update'] | ||||
|             ) | ||||
|             resource_type = acl.create_resources_type(payload) | ||||
|         else: | ||||
|             resource_type = results[0] | ||||
| 
 | ||||
|         for name in ['公司信息']: | ||||
|             payload = dict( | ||||
|                 type_id=resource_type['id'], | ||||
|                 app_id=acl.app_name, | ||||
|                 name=name, | ||||
|             ) | ||||
|             try: | ||||
|                 acl.create_resource(payload) | ||||
|             except Exception as e: | ||||
|                 if '已经存在' in str(e): | ||||
|                     pass | ||||
|                 else: | ||||
|                     raise Exception(e) | ||||
| 
 | ||||
|     def check_app(self, app_name): | ||||
|         acl = ACLManager(app_name) | ||||
|         payload = dict( | ||||
|             name=app_name, | ||||
|             description=app_name | ||||
|         ) | ||||
|         try: | ||||
|             app = acl.validate_app() | ||||
|             if app: | ||||
|                 return acl | ||||
| 
 | ||||
|             acl.create_app(payload) | ||||
|         except Exception as e: | ||||
|             current_app.logger.error(e) | ||||
|             if '不存在' in str(e): | ||||
|                 acl.create_app(payload) | ||||
|                 return acl | ||||
|             raise Exception(e) | ||||
| 
 | ||||
| 
 | ||||
| @click.command() | ||||
| @with_appcontext | ||||
| @@ -177,5 +226,7 @@ def init_department(): | ||||
|     """ | ||||
|     Department initialization | ||||
|     """ | ||||
|     InitDepartment().init() | ||||
|     InitDepartment().create_acl_role_with_department() | ||||
|     cli = InitDepartment() | ||||
|     cli.init_wide_company() | ||||
|     cli.create_acl_role_with_department() | ||||
|     cli.init_backend_resource() | ||||
| @@ -163,13 +163,15 @@ class AttributeManager(object): | ||||
|         if RoleEnum.CONFIG not in session.get("acl", {}).get("parentRoles", []) and not is_app_admin('cmdb'): | ||||
|             return abort(403, ErrFormat.role_required.format(RoleEnum.CONFIG)) | ||||
|  | ||||
|     @staticmethod | ||||
|     def calc_computed_attribute(attr_id): | ||||
|     @classmethod | ||||
|     def calc_computed_attribute(cls, attr_id): | ||||
|         """ | ||||
|         calculate computed attribute for all ci | ||||
|         :param attr_id: | ||||
|         :return: | ||||
|         """ | ||||
|         cls.can_create_computed_attribute() | ||||
|  | ||||
|         from api.tasks.cmdb import calc_computed_attribute | ||||
|  | ||||
|         calc_computed_attribute.apply_async(args=(attr_id, current_user.uid), queue=CMDB_QUEUE) | ||||
|   | ||||
| @@ -2,14 +2,11 @@ | ||||
|  | ||||
| from __future__ import unicode_literals | ||||
|  | ||||
| import requests | ||||
| from flask import current_app | ||||
|  | ||||
| from api.extensions import cache | ||||
| from api.extensions import db | ||||
| from api.lib.cmdb.custom_dashboard import CustomDashboardManager | ||||
| from api.models.cmdb import Attribute | ||||
| from api.models.cmdb import CI | ||||
| from api.models.cmdb import CIType | ||||
| from api.models.cmdb import CITypeAttribute | ||||
| from api.models.cmdb import RelationType | ||||
| @@ -210,7 +207,6 @@ class CITypeAttributeCache(object): | ||||
|  | ||||
|     @classmethod | ||||
|     def get(cls, type_id, attr_id): | ||||
|  | ||||
|         attr = cache.get(cls.PREFIX_ID.format(type_id, attr_id)) | ||||
|         attr = attr or cache.get(cls.PREFIX_ID.format(type_id, attr_id)) | ||||
|         attr = attr or CITypeAttribute.get_by(type_id=type_id, attr_id=attr_id, first=True, to_dict=False) | ||||
| @@ -251,53 +247,72 @@ class CMDBCounterCache(object): | ||||
|         result = {} | ||||
|         for custom in customs: | ||||
|             if custom['category'] == 0: | ||||
|                 result[custom['id']] = cls.summary_counter(custom['type_id']) | ||||
|                 res = cls.sum_counter(custom) | ||||
|             elif custom['category'] == 1: | ||||
|                 result[custom['id']] = cls.attribute_counter(custom['type_id'], custom['attr_id']) | ||||
|             elif custom['category'] == 2: | ||||
|                 result[custom['id']] = cls.relation_counter(custom['type_id'], custom['level']) | ||||
|                 res = cls.attribute_counter(custom) | ||||
|             else: | ||||
|                 res = cls.relation_counter(custom.get('type_id'), | ||||
|                                            custom.get('level'), | ||||
|                                            custom.get('options', {}).get('filter', ''), | ||||
|                                            custom.get('options', {}).get('type_ids', '')) | ||||
|  | ||||
|             if res: | ||||
|                 result[custom['id']] = res | ||||
|  | ||||
|         cls.set(result) | ||||
|  | ||||
|         return result | ||||
|  | ||||
|     @classmethod | ||||
|     def update(cls, custom): | ||||
|     def update(cls, custom, flush=True): | ||||
|         result = cache.get(cls.KEY) or {} | ||||
|         if not result: | ||||
|             result = cls.reset() | ||||
|  | ||||
|         if custom['category'] == 0: | ||||
|             result[custom['id']] = cls.summary_counter(custom['type_id']) | ||||
|             res = cls.sum_counter(custom) | ||||
|         elif custom['category'] == 1: | ||||
|             result[custom['id']] = cls.attribute_counter(custom['type_id'], custom['attr_id']) | ||||
|         elif custom['category'] == 2: | ||||
|             result[custom['id']] = cls.relation_counter(custom['type_id'], custom['level']) | ||||
|             res = cls.attribute_counter(custom) | ||||
|         else: | ||||
|             res = cls.relation_counter(custom.get('type_id'), | ||||
|                                        custom.get('level'), | ||||
|                                        custom.get('options', {}).get('filter', ''), | ||||
|                                        custom.get('options', {}).get('type_ids', '')) | ||||
|  | ||||
|         cls.set(result) | ||||
|         if res and flush: | ||||
|             result[custom['id']] = res | ||||
|             cls.set(result) | ||||
|  | ||||
|         return res | ||||
|  | ||||
|     @staticmethod | ||||
|     def summary_counter(type_id): | ||||
|         return db.session.query(CI.id).filter(CI.deleted.is_(False)).filter(CI.type_id == type_id).count() | ||||
|     def relation_counter(type_id, level, other_filer, type_ids): | ||||
|         from api.lib.cmdb.search.ci_relation.search import Search as RelSearch | ||||
|         from api.lib.cmdb.search import SearchError | ||||
|         from api.lib.cmdb.search.ci import search | ||||
|  | ||||
|     @staticmethod | ||||
|     def relation_counter(type_id, level): | ||||
|         query = "_type:{}".format(type_id) | ||||
|         s = search(query, count=1000000) | ||||
|         try: | ||||
|             type_names, _, _, _, _, _ = s.search() | ||||
|         except SearchError as e: | ||||
|             current_app.logger.error(e) | ||||
|             return | ||||
|  | ||||
|         uri = current_app.config.get('CMDB_API') | ||||
|  | ||||
|         type_names = requests.get("{}/ci/s?q=_type:{}&count=10000".format(uri, type_id)).json().get('result') | ||||
|         type_id_names = [(str(i.get('_id')), i.get(i.get('unique'))) for i in type_names] | ||||
|  | ||||
|         url = "{}/ci_relations/statistics?root_ids={}&level={}".format( | ||||
|             uri, ','.join([i[0] for i in type_id_names]), level) | ||||
|         stats = requests.get(url).json() | ||||
|         s = RelSearch([i[0] for i in type_id_names], level, other_filer or '') | ||||
|         try: | ||||
|             stats = s.statistics(type_ids) | ||||
|         except SearchError as e: | ||||
|             current_app.logger.error(e) | ||||
|             return | ||||
|  | ||||
|         id2name = dict(type_id_names) | ||||
|         type_ids = set() | ||||
|         for i in (stats.get('detail') or []): | ||||
|             for j in stats['detail'][i]: | ||||
|                 type_ids.add(j) | ||||
|  | ||||
|         for type_id in type_ids: | ||||
|             _type = CITypeCache.get(type_id) | ||||
|             id2name[type_id] = _type and _type.alias | ||||
| @@ -317,9 +332,100 @@ class CMDBCounterCache(object): | ||||
|         return result | ||||
|  | ||||
|     @staticmethod | ||||
|     def attribute_counter(type_id, attr_id): | ||||
|         uri = current_app.config.get('CMDB_API') | ||||
|         url = "{}/ci/s?q=_type:{}&fl={}&facet={}".format(uri, type_id, attr_id, attr_id) | ||||
|         res = requests.get(url).json() | ||||
|         if res.get('facet'): | ||||
|             return dict([i[:2] for i in list(res.get('facet').values())[0]]) | ||||
|     def attribute_counter(custom): | ||||
|         from api.lib.cmdb.search import SearchError | ||||
|         from api.lib.cmdb.search.ci import search | ||||
|         from api.lib.cmdb.utils import ValueTypeMap | ||||
|  | ||||
|         custom.setdefault('options', {}) | ||||
|         type_id = custom.get('type_id') | ||||
|         attr_id = custom.get('attr_id') | ||||
|         type_ids = custom['options'].get('type_ids') or (type_id and [type_id]) | ||||
|         attr_ids = list(map(str, custom['options'].get('attr_ids') or (attr_id and [attr_id]))) | ||||
|         try: | ||||
|             attr2value_type = [AttributeCache.get(i).value_type for i in attr_ids] | ||||
|         except AttributeError: | ||||
|             return | ||||
|  | ||||
|         other_filter = custom['options'].get('filter') | ||||
|         other_filter = "{}".format(other_filter) if other_filter else '' | ||||
|  | ||||
|         if custom['options'].get('ret') == 'cis': | ||||
|             query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter) | ||||
|             s = search(query, fl=attr_ids, ret_key='alias', count=100) | ||||
|             try: | ||||
|                 cis, _, _, _, _, _ = s.search() | ||||
|             except SearchError as e: | ||||
|                 current_app.logger.error(e) | ||||
|                 return | ||||
|  | ||||
|             return cis | ||||
|  | ||||
|         result = dict() | ||||
|         # level = 1 | ||||
|         query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter) | ||||
|         s = search(query, fl=attr_ids, facet=[attr_ids[0]], count=1) | ||||
|         try: | ||||
|             _, _, _, _, _, facet = s.search() | ||||
|         except SearchError as e: | ||||
|             current_app.logger.error(e) | ||||
|             return | ||||
|         for i in (list(facet.values()) or [[]])[0]: | ||||
|             result[ValueTypeMap.serialize2[attr2value_type[0]](str(i[0]))] = i[1] | ||||
|         if len(attr_ids) == 1: | ||||
|             return result | ||||
|  | ||||
|         # level = 2 | ||||
|         for v in result: | ||||
|             query = "_type:({}),{},{}:{}".format(";".join(map(str, type_ids)), other_filter, attr_ids[0], v) | ||||
|             s = search(query, fl=attr_ids, facet=[attr_ids[1]], count=1) | ||||
|             try: | ||||
|                 _, _, _, _, _, facet = s.search() | ||||
|             except SearchError as e: | ||||
|                 current_app.logger.error(e) | ||||
|                 return | ||||
|             result[v] = dict() | ||||
|             for i in (list(facet.values()) or [[]])[0]: | ||||
|                 result[v][ValueTypeMap.serialize2[attr2value_type[1]](str(i[0]))] = i[1] | ||||
|  | ||||
|         if len(attr_ids) == 2: | ||||
|             return result | ||||
|  | ||||
|         # level = 3 | ||||
|         for v1 in result: | ||||
|             if not isinstance(result[v1], dict): | ||||
|                 continue | ||||
|             for v2 in result[v1]: | ||||
|                 query = "_type:({}),{},{}:{},{}:{}".format(";".join(map(str, type_ids)), other_filter, | ||||
|                                                            attr_ids[0], v1, attr_ids[1], v2) | ||||
|                 s = search(query, fl=attr_ids, facet=[attr_ids[2]], count=1) | ||||
|                 try: | ||||
|                     _, _, _, _, _, facet = s.search() | ||||
|                 except SearchError as e: | ||||
|                     current_app.logger.error(e) | ||||
|                     return | ||||
|                 result[v1][v2] = dict() | ||||
|                 for i in (list(facet.values()) or [[]])[0]: | ||||
|                     result[v1][v2][ValueTypeMap.serialize2[attr2value_type[2]](str(i[0]))] = i[1] | ||||
|  | ||||
|         return result | ||||
|  | ||||
|     @staticmethod | ||||
|     def sum_counter(custom): | ||||
|         from api.lib.cmdb.search import SearchError | ||||
|         from api.lib.cmdb.search.ci import search | ||||
|  | ||||
|         custom.setdefault('options', {}) | ||||
|         type_id = custom.get('type_id') | ||||
|         type_ids = custom['options'].get('type_ids') or (type_id and [type_id]) | ||||
|         other_filter = custom['options'].get('filter') or '' | ||||
|  | ||||
|         query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter) | ||||
|         s = search(query, count=1) | ||||
|         try: | ||||
|             _, _, _, _, numfound, _ = s.search() | ||||
|         except SearchError as e: | ||||
|             current_app.logger.error(e) | ||||
|             return | ||||
|  | ||||
|         return numfound | ||||
|   | ||||
| @@ -4,6 +4,7 @@ | ||||
| import copy | ||||
| import datetime | ||||
| import json | ||||
| import threading | ||||
|  | ||||
| from flask import abort | ||||
| from flask import current_app | ||||
| @@ -24,27 +25,33 @@ from api.lib.cmdb.const import CMDB_QUEUE | ||||
| from api.lib.cmdb.const import ConstraintEnum | ||||
| from api.lib.cmdb.const import ExistPolicy | ||||
| from api.lib.cmdb.const import OperateType | ||||
| from api.lib.cmdb.const import PermEnum, ResourceTypeEnum | ||||
| from api.lib.cmdb.const import PermEnum | ||||
| from api.lib.cmdb.const import REDIS_PREFIX_CI | ||||
| from api.lib.cmdb.const import ResourceTypeEnum | ||||
| from api.lib.cmdb.const import RetKey | ||||
| from api.lib.cmdb.history import AttributeHistoryManger | ||||
| from api.lib.cmdb.history import CIRelationHistoryManager | ||||
| from api.lib.cmdb.history import CITriggerHistoryManager | ||||
| from api.lib.cmdb.perms import CIFilterPermsCRUD | ||||
| from api.lib.cmdb.resp_format import ErrFormat | ||||
| from api.lib.cmdb.utils import TableMap | ||||
| from api.lib.cmdb.utils import ValueTypeMap | ||||
| from api.lib.cmdb.value import AttributeValueManager | ||||
| from api.lib.decorator import kwargs_required | ||||
| from api.lib.notify import notify_send | ||||
| from api.lib.perm.acl.acl import ACLManager | ||||
| from api.lib.perm.acl.acl import is_app_admin | ||||
| from api.lib.perm.acl.acl import validate_permission | ||||
| 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 | ||||
| from api.models.cmdb import AutoDiscoveryCI | ||||
| from api.models.cmdb import CI | ||||
| from api.models.cmdb import CIRelation | ||||
| from api.models.cmdb import CITypeAttribute | ||||
| from api.models.cmdb import CITypeRelation | ||||
| from api.models.cmdb import CITypeTrigger | ||||
| from api.tasks.cmdb import ci_cache | ||||
| from api.tasks.cmdb import ci_delete | ||||
| from api.tasks.cmdb import ci_relation_add | ||||
| @@ -378,16 +385,17 @@ class CIManager(object): | ||||
|             key2attr = value_manager.valid_attr_value(ci_dict, ci_type.id, ci and ci.id, | ||||
|                                                       ci_type_attrs_name, ci_type_attrs_alias, ci_attr2type_attr) | ||||
|  | ||||
|             operate_type = OperateType.UPDATE if ci is not None else OperateType.ADD | ||||
|             try: | ||||
|                 ci = ci or CI.create(type_id=ci_type.id, is_auto_discovery=is_auto_discovery) | ||||
|                 record_id = value_manager.create_or_update_attr_value2(ci, ci_dict, key2attr) | ||||
|                 record_id = value_manager.create_or_update_attr_value(ci, ci_dict, key2attr) | ||||
|             except BadRequest as e: | ||||
|                 if existed is None: | ||||
|                     cls.delete(ci.id) | ||||
|                 raise e | ||||
|  | ||||
|         if record_id:  # has change | ||||
|             ci_cache.apply_async([ci.id], queue=CMDB_QUEUE) | ||||
|             ci_cache.apply_async(args=(ci.id, operate_type, record_id), queue=CMDB_QUEUE) | ||||
|  | ||||
|         if ref_ci_dict:  # add relations | ||||
|             ci_relation_add.apply_async(args=(ref_ci_dict, ci.id, current_user.uid), queue=CMDB_QUEUE) | ||||
| @@ -427,12 +435,12 @@ class CIManager(object): | ||||
|                         return abort(403, ErrFormat.ci_filter_perm_attr_no_permission.format(k)) | ||||
|  | ||||
|             try: | ||||
|                 record_id = value_manager.create_or_update_attr_value2(ci, ci_dict, key2attr) | ||||
|                 record_id = value_manager.create_or_update_attr_value(ci, ci_dict, key2attr) | ||||
|             except BadRequest as e: | ||||
|                 raise e | ||||
|  | ||||
|         if record_id:  # has change | ||||
|             ci_cache.apply_async([ci_id], queue=CMDB_QUEUE) | ||||
|             ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE) | ||||
|  | ||||
|         ref_ci_dict = {k: v for k, v in ci_dict.items() if k.startswith("$") and "." in k} | ||||
|         if ref_ci_dict: | ||||
| @@ -442,9 +450,10 @@ class CIManager(object): | ||||
|     def update_unique_value(ci_id, unique_name, unique_value): | ||||
|         ci = CI.get_by_id(ci_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(ci_id))) | ||||
|  | ||||
|         AttributeValueManager().create_or_update_attr_value(unique_name, unique_value, ci) | ||||
|         key2attr = {unique_name: AttributeCache.get(unique_name)} | ||||
|         record_id = AttributeValueManager().create_or_update_attr_value(ci, {unique_name: unique_value}, key2attr) | ||||
|  | ||||
|         ci_cache.apply_async([ci_id], queue=CMDB_QUEUE) | ||||
|         ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE) | ||||
|  | ||||
|     @classmethod | ||||
|     def delete(cls, ci_id): | ||||
| @@ -477,9 +486,9 @@ class CIManager(object): | ||||
|  | ||||
|         db.session.commit() | ||||
|  | ||||
|         AttributeHistoryManger.add(None, ci_id, [(None, OperateType.DELETE, ci_dict, None)], ci.type_id) | ||||
|         record_id = AttributeHistoryManger.add(None, ci_id, [(None, OperateType.DELETE, ci_dict, None)], ci.type_id) | ||||
|  | ||||
|         ci_delete.apply_async([ci.id], queue=CMDB_QUEUE) | ||||
|         ci_delete.apply_async(args=(ci_dict, OperateType.DELETE, record_id), queue=CMDB_QUEUE) | ||||
|  | ||||
|         return ci_id | ||||
|  | ||||
| @@ -896,3 +905,128 @@ class CIRelationManager(object): | ||||
|             for parent_id in parents: | ||||
|                 for ci_id in ci_ids: | ||||
|                     cls.delete_2(parent_id, ci_id) | ||||
|  | ||||
|  | ||||
| class CITriggerManager(object): | ||||
|     @staticmethod | ||||
|     def get(type_id): | ||||
|         return CITypeTrigger.get_by(type_id=type_id, to_dict=False) | ||||
|  | ||||
|     @staticmethod | ||||
|     def _exec_webhook(operate_type, webhook, ci_dict, trigger_id, record_id): | ||||
|         try: | ||||
|             response = webhook_request(webhook, ci_dict).text | ||||
|             is_ok = True | ||||
|         except Exception as e: | ||||
|             current_app.logger.warning("exec webhook failed: {}".format(e)) | ||||
|             response = e | ||||
|             is_ok = False | ||||
|  | ||||
|         CITriggerHistoryManager.add(operate_type, | ||||
|                                     record_id, | ||||
|                                     ci_dict.get('_id'), | ||||
|                                     trigger_id, | ||||
|                                     is_ok=is_ok, | ||||
|                                     webhook=response) | ||||
|  | ||||
|         return is_ok | ||||
|  | ||||
|     @staticmethod | ||||
|     def _exec_notify(operate_type, notify, ci_dict, trigger_id, record_id, ci_id=None): | ||||
|  | ||||
|         if ci_id is not None: | ||||
|             ci_dict = CIManager().get_ci_by_id_from_db(ci_id, need_children=False, use_master=False) | ||||
|  | ||||
|         try: | ||||
|             response = notify_send(notify.get('subject'), notify.get('body'), notify.get('tos'), ci_dict) | ||||
|             is_ok = True | ||||
|         except Exception as e: | ||||
|             current_app.logger.warning("send notify failed: {}".format(e)) | ||||
|             response = e | ||||
|             is_ok = False | ||||
|  | ||||
|         CITriggerHistoryManager.add(operate_type, | ||||
|                                     record_id, | ||||
|                                     ci_dict.get('_id'), | ||||
|                                     trigger_id, | ||||
|                                     is_ok=is_ok, | ||||
|                                     notify=response) | ||||
|  | ||||
|         return is_ok | ||||
|  | ||||
|     @staticmethod | ||||
|     def ci_filter(ci_id, other_filter): | ||||
|         from api.lib.cmdb.search import SearchError | ||||
|         from api.lib.cmdb.search.ci import search | ||||
|  | ||||
|         query = "_id:{},{}".format(ci_id, other_filter) | ||||
|  | ||||
|         try: | ||||
|             _, _, _, _, numfound, _ = search(query).search() | ||||
|             return numfound | ||||
|         except SearchError as e: | ||||
|             current_app.logger.warning("ci search failed: {}".format(e)) | ||||
|  | ||||
|     @classmethod | ||||
|     def fire(cls, operate_type, ci_dict, record_id): | ||||
|         type_id = ci_dict.get('_type') | ||||
|         triggers = cls.get(type_id) or [] | ||||
|  | ||||
|         for trigger in triggers: | ||||
|             if not trigger.option.get('enable'): | ||||
|                 continue | ||||
|  | ||||
|             if trigger.option.get('filter') and not cls.ci_filter(ci_dict.get('_id'), trigger.option['filter']): | ||||
|                 continue | ||||
|  | ||||
|             if trigger.option.get('attr_ids') and isinstance(trigger.option['attr_ids'], list): | ||||
|                 if not (set(trigger.option['attr_ids']) & | ||||
|                         set([i.attr_id for i in AttributeHistory.get_by(record_id=record_id, to_dict=False)])): | ||||
|                     continue | ||||
|  | ||||
|             if trigger.option.get('action') == operate_type: | ||||
|                 if trigger.option.get('webhooks'): | ||||
|                     cls._exec_webhook(operate_type, trigger.option['webhooks'], ci_dict, trigger.id, record_id) | ||||
|                 elif trigger.option.get('notifies'): | ||||
|                     cls._exec_notify(operate_type, trigger.option['notifies'], ci_dict, trigger.id, record_id) | ||||
|  | ||||
|     @classmethod | ||||
|     def waiting_cis(cls, trigger): | ||||
|         now = datetime.datetime.today() | ||||
|  | ||||
|         delta_time = datetime.timedelta(days=(trigger.option.get('before_days', 0) or 0)) | ||||
|  | ||||
|         attr = AttributeCache.get(trigger.attr_id) | ||||
|  | ||||
|         value_table = TableMap(attr=attr).table | ||||
|  | ||||
|         values = value_table.get_by(attr_id=attr.id, to_dict=False) | ||||
|  | ||||
|         result = [] | ||||
|         for v in values: | ||||
|             if (isinstance(v.value, (datetime.date, datetime.datetime)) and | ||||
|                     (v.value - delta_time).strftime('%Y%m%d') == now.strftime("%Y%m%d")): | ||||
|  | ||||
|                 if trigger.option.get('filter') and not cls.ci_filter(v.ci_id, trigger.option['filter']): | ||||
|                     continue | ||||
|  | ||||
|                 result.append(v) | ||||
|  | ||||
|         return result | ||||
|  | ||||
|     @classmethod | ||||
|     def trigger_notify(cls, trigger, ci): | ||||
|         """ | ||||
|         only for date attribute | ||||
|         :param trigger: | ||||
|         :param ci: | ||||
|         :return: | ||||
|         """ | ||||
|         if (trigger.notify.get('notify_at') == datetime.datetime.now().strftime("%H:%M") or | ||||
|                 not trigger.option.get('notify_at')): | ||||
|             threading.Thread(target=cls._exec_notify, args=( | ||||
|                 None, trigger.option['notifies'], None, trigger.id, None, ci.id)).start() | ||||
|  | ||||
|             return True | ||||
|  | ||||
|         return False | ||||
|   | ||||
| @@ -3,9 +3,11 @@ | ||||
| import copy | ||||
| import datetime | ||||
|  | ||||
| import toposort | ||||
| from flask import abort | ||||
| from flask import current_app | ||||
| from flask_login import current_user | ||||
| from toposort import toposort_flatten | ||||
|  | ||||
| from api.extensions import db | ||||
| from api.lib.cmdb.attribute import AttributeManager | ||||
| @@ -114,7 +116,7 @@ class CITypeManager(object): | ||||
|     @kwargs_required("name") | ||||
|     def add(cls, **kwargs): | ||||
|  | ||||
|         unique_key = kwargs.pop("unique_key", None) | ||||
|         unique_key = kwargs.pop("unique_key", None) or kwargs.pop("unique_id", None) | ||||
|         unique_key = AttributeCache.get(unique_key) or abort(404, ErrFormat.unique_key_not_define) | ||||
|  | ||||
|         kwargs["alias"] = kwargs["name"] if not kwargs.get("alias") else kwargs["alias"] | ||||
| @@ -370,6 +372,16 @@ class CITypeAttributeManager(object): | ||||
|  | ||||
|         return result | ||||
|  | ||||
|     @staticmethod | ||||
|     def get_common_attributes(type_ids): | ||||
|         result = CITypeAttribute.get_by(__func_in___key_type_id=list(map(int, type_ids)), to_dict=False) | ||||
|         attr2types = {} | ||||
|         for i in result: | ||||
|             attr2types.setdefault(i.attr_id, []).append(i.type_id) | ||||
|  | ||||
|         return [AttributeCache.get(attr_id).to_dict() for attr_id in attr2types | ||||
|                 if len(attr2types[attr_id]) == len(type_ids)] | ||||
|  | ||||
|     @staticmethod | ||||
|     def _check(type_id, attr_ids): | ||||
|         ci_type = CITypeManager.check_is_existed(type_id) | ||||
| @@ -564,6 +576,23 @@ class CITypeRelationManager(object): | ||||
|  | ||||
|         return [cls._wrap_relation_type_dict(child.child_id, child) for child in children] | ||||
|  | ||||
|     @classmethod | ||||
|     def recursive_level2children(cls, parent_id): | ||||
|         result = dict() | ||||
|  | ||||
|         def get_children(_id, level): | ||||
|             children = CITypeRelation.get_by(parent_id=_id, to_dict=False) | ||||
|             if children: | ||||
|                 result.setdefault(level + 1, []).extend([i.child.to_dict() for i in children]) | ||||
|  | ||||
|             for i in children: | ||||
|                 if i.child_id != _id: | ||||
|                     get_children(i.child_id, level + 1) | ||||
|  | ||||
|         get_children(parent_id, 0) | ||||
|  | ||||
|         return result | ||||
|  | ||||
|     @classmethod | ||||
|     def get_parents(cls, child_id): | ||||
|         parents = CITypeRelation.get_by(child_id=child_id, to_dict=False) | ||||
| @@ -586,6 +615,17 @@ class CITypeRelationManager(object): | ||||
|         p = CITypeManager.check_is_existed(parent) | ||||
|         c = CITypeManager.check_is_existed(child) | ||||
|  | ||||
|         rels = {} | ||||
|         for i in CITypeRelation.get_by(to_dict=False): | ||||
|             rels.setdefault(i.child_id, set()).add(i.parent_id) | ||||
|         rels.setdefault(c.id, set()).add(p.id) | ||||
|  | ||||
|         try: | ||||
|             toposort_flatten(rels) | ||||
|         except toposort.CircularDependencyError as e: | ||||
|             current_app.logger.warning(str(e)) | ||||
|             return abort(400, ErrFormat.circular_dependency_error) | ||||
|  | ||||
|         existed = cls._get(p.id, c.id) | ||||
|         if existed is not None: | ||||
|             existed.update(relation_type_id=relation_type_id, | ||||
| @@ -823,6 +863,12 @@ class CITypeTemplateManager(object): | ||||
|         for added_id in set(id2obj_dicts.keys()) - set(existed_ids): | ||||
|             if cls == CIType: | ||||
|                 CITypeManager.add(**id2obj_dicts[added_id]) | ||||
|             elif cls == CITypeRelation: | ||||
|                 CITypeRelationManager.add(id2obj_dicts[added_id].get('parent_id'), | ||||
|                                           id2obj_dicts[added_id].get('child_id'), | ||||
|                                           id2obj_dicts[added_id].get('relation_type_id'), | ||||
|                                           id2obj_dicts[added_id].get('constraint'), | ||||
|                                           ) | ||||
|             else: | ||||
|                 cls.create(flush=True, **id2obj_dicts[added_id]) | ||||
|  | ||||
| @@ -1120,16 +1166,18 @@ class CITypeUniqueConstraintManager(object): | ||||
|  | ||||
| class CITypeTriggerManager(object): | ||||
|     @staticmethod | ||||
|     def get(type_id): | ||||
|         return CITypeTrigger.get_by(type_id=type_id, to_dict=True) | ||||
|     def get(type_id, to_dict=True): | ||||
|         return CITypeTrigger.get_by(type_id=type_id, to_dict=to_dict) | ||||
|  | ||||
|     @staticmethod | ||||
|     def add(type_id, attr_id, notify): | ||||
|         CITypeTrigger.get_by(type_id=type_id, attr_id=attr_id) and abort(400, ErrFormat.ci_type_trigger_duplicate) | ||||
|     def add(type_id, attr_id, option): | ||||
|         for i in CITypeTrigger.get_by(type_id=type_id, attr_id=attr_id, to_dict=False): | ||||
|             if i.option == option: | ||||
|                 return abort(400, ErrFormat.ci_type_trigger_duplicate) | ||||
|  | ||||
|         not isinstance(notify, dict) and abort(400, ErrFormat.argument_invalid.format("notify")) | ||||
|         not isinstance(option, dict) and abort(400, ErrFormat.argument_invalid.format("option")) | ||||
|  | ||||
|         trigger = CITypeTrigger.create(type_id=type_id, attr_id=attr_id, notify=notify) | ||||
|         trigger = CITypeTrigger.create(type_id=type_id, attr_id=attr_id, option=option) | ||||
|  | ||||
|         CITypeHistoryManager.add(CITypeOperateType.ADD_TRIGGER, | ||||
|                                  type_id, | ||||
| @@ -1139,12 +1187,12 @@ class CITypeTriggerManager(object): | ||||
|         return trigger.to_dict() | ||||
|  | ||||
|     @staticmethod | ||||
|     def update(_id, notify): | ||||
|     def update(_id, option): | ||||
|         existed = (CITypeTrigger.get_by_id(_id) or | ||||
|                    abort(404, ErrFormat.ci_type_trigger_not_found.format("id={}".format(_id)))) | ||||
|  | ||||
|         existed2 = existed.to_dict() | ||||
|         new = existed.update(notify=notify) | ||||
|         new = existed.update(option=option) | ||||
|  | ||||
|         CITypeHistoryManager.add(CITypeOperateType.UPDATE_TRIGGER, | ||||
|                                  existed.type_id, | ||||
| @@ -1164,35 +1212,3 @@ class CITypeTriggerManager(object): | ||||
|                                  existed.type_id, | ||||
|                                  trigger_id=_id, | ||||
|                                  change=existed.to_dict()) | ||||
|  | ||||
|     @staticmethod | ||||
|     def waiting_cis(trigger): | ||||
|         now = datetime.datetime.today() | ||||
|  | ||||
|         delta_time = datetime.timedelta(days=(trigger.notify.get('before_days', 0) or 0)) | ||||
|  | ||||
|         attr = AttributeCache.get(trigger.attr_id) | ||||
|  | ||||
|         value_table = TableMap(attr=attr).table | ||||
|  | ||||
|         values = value_table.get_by(attr_id=attr.id, to_dict=False) | ||||
|  | ||||
|         result = [] | ||||
|         for v in values: | ||||
|             if (isinstance(v.value, (datetime.date, datetime.datetime)) and | ||||
|                     (v.value - delta_time).strftime('%Y%m%d') == now.strftime("%Y%m%d")): | ||||
|                 result.append(v) | ||||
|  | ||||
|         return result | ||||
|  | ||||
|     @staticmethod | ||||
|     def trigger_notify(trigger, ci): | ||||
|         if (trigger.notify.get('notify_at') == datetime.datetime.now().strftime("%H:%M") or | ||||
|                 not trigger.notify.get('notify_at')): | ||||
|             from api.tasks.cmdb import trigger_notify | ||||
|  | ||||
|             trigger_notify.apply_async(args=(trigger.notify, ci.ci_id), queue=CMDB_QUEUE) | ||||
|  | ||||
|             return True | ||||
|  | ||||
|         return False | ||||
|   | ||||
| @@ -14,6 +14,14 @@ class CustomDashboardManager(object): | ||||
|     def get(): | ||||
|         return sorted(CustomDashboard.get_by(to_dict=True), key=lambda x: (x["category"], x['order'])) | ||||
|  | ||||
|     @staticmethod | ||||
|     def preview(**kwargs): | ||||
|         from api.lib.cmdb.cache import CMDBCounterCache | ||||
|  | ||||
|         res = CMDBCounterCache.update(kwargs, flush=False) | ||||
|  | ||||
|         return res | ||||
|  | ||||
|     @staticmethod | ||||
|     def add(**kwargs): | ||||
|         from api.lib.cmdb.cache import CMDBCounterCache | ||||
| @@ -23,9 +31,9 @@ class CustomDashboardManager(object): | ||||
|  | ||||
|         new = CustomDashboard.create(**kwargs) | ||||
|  | ||||
|         CMDBCounterCache.update(new.to_dict()) | ||||
|         res = CMDBCounterCache.update(new.to_dict()) | ||||
|  | ||||
|         return new | ||||
|         return new, res | ||||
|  | ||||
|     @staticmethod | ||||
|     def update(_id, **kwargs): | ||||
| @@ -35,9 +43,9 @@ class CustomDashboardManager(object): | ||||
|  | ||||
|         new = existed.update(**kwargs) | ||||
|  | ||||
|         CMDBCounterCache.update(new.to_dict()) | ||||
|         res = CMDBCounterCache.update(new.to_dict()) | ||||
|  | ||||
|         return new | ||||
|         return new, res | ||||
|  | ||||
|     @staticmethod | ||||
|     def batch_update(id2options): | ||||
|   | ||||
| @@ -16,6 +16,7 @@ from api.lib.perm.acl.cache import UserCache | ||||
| from api.models.cmdb import Attribute | ||||
| from api.models.cmdb import AttributeHistory | ||||
| from api.models.cmdb import CIRelationHistory | ||||
| from api.models.cmdb import CITriggerHistory | ||||
| from api.models.cmdb import CITypeHistory | ||||
| from api.models.cmdb import CITypeTrigger | ||||
| from api.models.cmdb import CITypeUniqueConstraint | ||||
| @@ -286,3 +287,67 @@ class CITypeHistoryManager(object): | ||||
|                            change=change) | ||||
|  | ||||
|             CITypeHistory.create(**payload) | ||||
|  | ||||
|  | ||||
| class CITriggerHistoryManager(object): | ||||
|     @staticmethod | ||||
|     def get(page, page_size, type_id=None, trigger_id=None, operate_type=None): | ||||
|         query = CITriggerHistory.get_by(only_query=True) | ||||
|         if type_id is not None: | ||||
|             query = query.filter(CITriggerHistory.type_id == type_id) | ||||
|  | ||||
|         if trigger_id: | ||||
|             query = query.filter(CITriggerHistory.trigger_id == trigger_id) | ||||
|  | ||||
|         if operate_type is not None: | ||||
|             query = query.filter(CITriggerHistory.operate_type == operate_type) | ||||
|  | ||||
|         numfound = query.count() | ||||
|  | ||||
|         query = query.order_by(CITriggerHistory.id.desc()) | ||||
|         result = query.offset((page - 1) * page_size).limit(page_size) | ||||
|         result = [i.to_dict() for i in result] | ||||
|         for res in result: | ||||
|             if res.get('trigger_id'): | ||||
|                 trigger = CITypeTrigger.get_by_id(res['trigger_id']) | ||||
|                 res['trigger'] = trigger and trigger.to_dict() | ||||
|  | ||||
|         return numfound, result | ||||
|  | ||||
|     @staticmethod | ||||
|     def get_by_ci_id(ci_id): | ||||
|         res = db.session.query(CITriggerHistory, CITypeTrigger, OperationRecord).join( | ||||
|             CITypeTrigger, CITypeTrigger.id == CITriggerHistory.trigger_id).join( | ||||
|             OperationRecord, OperationRecord.id == CITriggerHistory.record_id).filter( | ||||
|             CITriggerHistory.ci_id == ci_id).order_by(CITriggerHistory.id.desc()) | ||||
|  | ||||
|         result = [] | ||||
|         id2trigger = dict() | ||||
|         for i in res: | ||||
|             hist = i.CITriggerHistory | ||||
|             record = i.OperationRecord | ||||
|             item = dict(is_ok=hist.is_ok, | ||||
|                         operate_type=hist.operate_type, | ||||
|                         notify=hist.notify, | ||||
|                         webhook=hist.webhook, | ||||
|                         created_at=record.created_at.strftime('%Y-%m-%d %H:%M:%S'), | ||||
|                         record_id=record.id, | ||||
|                         hid=hist.id | ||||
|                         ) | ||||
|             if i.CITypeTrigger.id not in id2trigger: | ||||
|                 id2trigger[i.CITypeTrigger.id] = i.CITypeTrigger.to_dict() | ||||
|  | ||||
|             result.append(item) | ||||
|  | ||||
|         return dict(items=result, id2trigger=id2trigger) | ||||
|  | ||||
|     @staticmethod | ||||
|     def add(operate_type, record_id, ci_id, trigger_id, is_ok=False, notify=None, webhook=None): | ||||
|  | ||||
|         CITriggerHistory.create(operate_type=operate_type, | ||||
|                                 record_id=record_id, | ||||
|                                 ci_id=ci_id, | ||||
|                                 trigger_id=trigger_id, | ||||
|                                 is_ok=is_ok, | ||||
|                                 notify=notify, | ||||
|                                 webhook=webhook) | ||||
|   | ||||
| @@ -42,7 +42,7 @@ FACET_QUERY1 = """ | ||||
|  | ||||
| FACET_QUERY = """ | ||||
|     SELECT {0}.value, | ||||
|            count({0}.ci_id) | ||||
|            count(distinct({0}.ci_id)) | ||||
|     FROM {0} | ||||
|     INNER JOIN ({1}) AS F ON F.ci_id={0}.ci_id | ||||
|     WHERE {0}.attr_id={2:d} | ||||
|   | ||||
| @@ -141,6 +141,10 @@ class Search(object): | ||||
|     @staticmethod | ||||
|     def _in_query_handler(attr, v, is_not): | ||||
|         new_v = v[1:-1].split(";") | ||||
|  | ||||
|         if attr.value_type == ValueTypeEnum.DATE: | ||||
|             new_v = ["{} 00:00:00".format(i) for i in new_v if len(i) == 10] | ||||
|  | ||||
|         table_name = TableMap(attr=attr).table_name | ||||
|         in_query = " OR {0}.value ".format(table_name).join(['{0} "{1}"'.format( | ||||
|             "NOT LIKE" if is_not else "LIKE", | ||||
| @@ -151,6 +155,11 @@ class Search(object): | ||||
|     @staticmethod | ||||
|     def _range_query_handler(attr, v, is_not): | ||||
|         start, end = [x.strip() for x in v[1:-1].split("_TO_")] | ||||
|  | ||||
|         if attr.value_type == ValueTypeEnum.DATE: | ||||
|             start = "{} 00:00:00".format(start) if len(start) == 10 else start | ||||
|             end = "{} 00:00:00".format(end) if len(end) == 10 else end | ||||
|  | ||||
|         table_name = TableMap(attr=attr).table_name | ||||
|         range_query = "{0} '{1}' AND '{2}'".format( | ||||
|             "NOT BETWEEN" if is_not else "BETWEEN", | ||||
| @@ -162,8 +171,14 @@ class Search(object): | ||||
|     def _comparison_query_handler(attr, v): | ||||
|         table_name = TableMap(attr=attr).table_name | ||||
|         if v.startswith(">=") or v.startswith("<="): | ||||
|             if attr.value_type == ValueTypeEnum.DATE and len(v[2:]) == 10: | ||||
|                 v = "{} 00:00:00".format(v) | ||||
|  | ||||
|             comparison_query = "{0} '{1}'".format(v[:2], v[2:].replace("*", "%")) | ||||
|         else: | ||||
|             if attr.value_type == ValueTypeEnum.DATE and len(v[1:]) == 10: | ||||
|                 v = "{} 00:00:00".format(v) | ||||
|  | ||||
|             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 | ||||
| @@ -295,7 +310,7 @@ class Search(object): | ||||
|  | ||||
|         start = time.time() | ||||
|         execute = db.session.execute | ||||
|         current_app.logger.debug(v_query_sql) | ||||
|         # current_app.logger.debug(v_query_sql) | ||||
|         res = execute(v_query_sql).fetchall() | ||||
|         end_time = time.time() | ||||
|         current_app.logger.debug("query ci ids time is: {0}".format(end_time - start)) | ||||
| @@ -391,6 +406,9 @@ class Search(object): | ||||
|  | ||||
|             is_not = True if operator == "|~" else False | ||||
|  | ||||
|             if field_type == ValueTypeEnum.DATE and len(v) == 10: | ||||
|                 v = "{} 00:00:00".format(v) | ||||
|  | ||||
|             # in query | ||||
|             if v.startswith("(") and v.endswith(")"): | ||||
|                 _query_sql = self._in_query_handler(attr, v, is_not) | ||||
| @@ -506,7 +524,7 @@ class Search(object): | ||||
|             if k: | ||||
|                 table_name = TableMap(attr=attr).table_name | ||||
|                 query_sql = FACET_QUERY.format(table_name, self.query_sql, attr.id) | ||||
|                 # current_app.logger.debug(query_sql) | ||||
|                 # current_app.logger.warning(query_sql) | ||||
|                 result = db.session.execute(query_sql).fetchall() | ||||
|                 facet[k] = result | ||||
|  | ||||
|   | ||||
| @@ -18,7 +18,6 @@ from api.extensions import db | ||||
| from api.lib.cmdb.attribute import AttributeManager | ||||
| from api.lib.cmdb.cache import AttributeCache | ||||
| from api.lib.cmdb.cache import CITypeAttributeCache | ||||
| from api.lib.cmdb.const import ExistPolicy | ||||
| from api.lib.cmdb.const import OperateType | ||||
| from api.lib.cmdb.const import ValueTypeEnum | ||||
| from api.lib.cmdb.history import AttributeHistoryManger | ||||
| @@ -140,6 +139,7 @@ class AttributeValueManager(object): | ||||
|         try: | ||||
|             db.session.commit() | ||||
|         except Exception as e: | ||||
|             db.session.rollback() | ||||
|             current_app.logger.error("write change failed: {}".format(str(e))) | ||||
|  | ||||
|         return record_id | ||||
| @@ -235,7 +235,7 @@ class AttributeValueManager(object): | ||||
|  | ||||
|         return key2attr | ||||
|  | ||||
|     def create_or_update_attr_value2(self, ci, ci_dict, key2attr): | ||||
|     def create_or_update_attr_value(self, ci, ci_dict, key2attr): | ||||
|         """ | ||||
|         add or update attribute value, then write history | ||||
|         :param ci: instance object | ||||
| @@ -288,66 +288,6 @@ class AttributeValueManager(object): | ||||
|  | ||||
|         return self._write_change2(changed) | ||||
|  | ||||
|     def create_or_update_attr_value(self, key, value, ci, _no_attribute_policy=ExistPolicy.IGNORE, record_id=None): | ||||
|         """ | ||||
|         add or update attribute value, then write history | ||||
|         :param key: id, name or alias | ||||
|         :param value: | ||||
|         :param ci: instance object | ||||
|         :param _no_attribute_policy: ignore or reject | ||||
|         :param record_id: op record | ||||
|         :return: | ||||
|         """ | ||||
|         attr = self._get_attr(key) | ||||
|         if attr is None: | ||||
|             if _no_attribute_policy == ExistPolicy.IGNORE: | ||||
|                 return | ||||
|             if _no_attribute_policy == ExistPolicy.REJECT: | ||||
|                 return abort(400, ErrFormat.attribute_not_found.format(key)) | ||||
|  | ||||
|         value_table = TableMap(attr=attr).table | ||||
|  | ||||
|         try: | ||||
|             if attr.is_list: | ||||
|                 value_list = [self._validate(attr, i, value_table, ci) for i in handle_arg_list(value)] | ||||
|                 if not value_list: | ||||
|                     self._check_is_required(ci.type_id, attr, '') | ||||
|  | ||||
|                 existed_attrs = value_table.get_by(attr_id=attr.id, ci_id=ci.id, to_dict=False) | ||||
|                 existed_values = [i.value for i in existed_attrs] | ||||
|                 added = set(value_list) - set(existed_values) | ||||
|                 deleted = set(existed_values) - set(value_list) | ||||
|                 for v in added: | ||||
|                     value_table.create(ci_id=ci.id, attr_id=attr.id, value=v) | ||||
|                     record_id = self._write_change(ci.id, attr.id, OperateType.ADD, None, v, record_id, ci.type_id) | ||||
|  | ||||
|                 for v in deleted: | ||||
|                     existed_attr = existed_attrs[existed_values.index(v)] | ||||
|                     existed_attr.delete() | ||||
|                     record_id = self._write_change(ci.id, attr.id, OperateType.DELETE, v, None, record_id, ci.type_id) | ||||
|             else: | ||||
|                 value = self._validate(attr, value, value_table, ci) | ||||
|                 existed_attr = value_table.get_by(attr_id=attr.id, ci_id=ci.id, first=True, to_dict=False) | ||||
|                 existed_value = existed_attr and existed_attr.value | ||||
|                 if existed_value is None and value is not None: | ||||
|                     value_table.create(ci_id=ci.id, attr_id=attr.id, value=value) | ||||
|  | ||||
|                     record_id = self._write_change(ci.id, attr.id, OperateType.ADD, None, value, record_id, ci.type_id) | ||||
|                 else: | ||||
|                     if existed_value != value: | ||||
|                         if value is None: | ||||
|                             existed_attr.delete() | ||||
|                         else: | ||||
|                             existed_attr.update(value=value) | ||||
|  | ||||
|                         record_id = self._write_change(ci.id, attr.id, OperateType.UPDATE, | ||||
|                                                        existed_value, value, record_id, ci.type_id) | ||||
|  | ||||
|             return record_id | ||||
|         except Exception as e: | ||||
|             current_app.logger.warning(str(e)) | ||||
|             return abort(400, ErrFormat.attribute_value_invalid2.format("{}({})".format(attr.alias, attr.name), value)) | ||||
|  | ||||
|     @staticmethod | ||||
|     def delete_attr_value(attr_id, ci_id): | ||||
|         attr = AttributeCache.get(attr_id) | ||||
|   | ||||
| @@ -6,6 +6,7 @@ from api.lib.common_setting.resp_format import ErrFormat | ||||
| from api.lib.perm.acl.cache import RoleCache, AppCache | ||||
| from api.lib.perm.acl.role import RoleCRUD, RoleRelationCRUD | ||||
| from api.lib.perm.acl.user import UserCRUD | ||||
| from api.lib.perm.acl.resource import ResourceTypeCRUD, ResourceCRUD | ||||
|  | ||||
|  | ||||
| class ACLManager(object): | ||||
| @@ -94,3 +95,22 @@ class ACLManager(object): | ||||
|                       avatar=user_info.get('avatar')) | ||||
|  | ||||
|         return result | ||||
|  | ||||
|     def validate_app(self): | ||||
|         return AppCache.get(self.app_name) | ||||
|  | ||||
|     def get_all_resources_types(self, q=None, page=1, page_size=999999): | ||||
|         app_id = self.validate_app().id | ||||
|         numfound, res, id2perms = ResourceTypeCRUD.search(q, app_id, page, page_size) | ||||
|  | ||||
|         return dict( | ||||
|             numfound=numfound, | ||||
|             groups=[i.to_dict() for i in res], | ||||
|             id2perms=id2perms | ||||
|         ) | ||||
|  | ||||
|     def create_resource(self, payload): | ||||
|         payload['app_id'] = self.validate_app().id | ||||
|         resource = ResourceCRUD.add(**payload) | ||||
|  | ||||
|         return resource.to_dict() | ||||
|   | ||||
							
								
								
									
										45
									
								
								cmdb-api/api/lib/notify.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								cmdb-api/api/lib/notify.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| # -*- coding:utf-8 -*- | ||||
|  | ||||
| import json | ||||
|  | ||||
| import requests | ||||
| from flask import current_app | ||||
| from jinja2 import Template | ||||
|  | ||||
| from api.lib.mail import send_mail | ||||
|  | ||||
|  | ||||
| def _request_messenger(subject, body, tos, sender): | ||||
|     params = dict(sender=sender, title=subject, | ||||
|                   tos=[to[sender] for to in tos if to.get(sender)]) | ||||
|  | ||||
|     if not params['tos']: | ||||
|         raise Exception("no receivers") | ||||
|  | ||||
|     if sender == "email": | ||||
|         params['msgtype'] = 'text/html' | ||||
|         params['content'] = body | ||||
|     else: | ||||
|         params['msgtype'] = 'text' | ||||
|         params['content'] = json.dumps(dict(content=subject or body)) | ||||
|  | ||||
|     resp = requests.post(current_app.config.get('MESSENGER_URL'), json=params) | ||||
|     if resp.status_code != 200: | ||||
|         raise Exception(resp.text) | ||||
|  | ||||
|     return resp.text | ||||
|  | ||||
|  | ||||
| def notify_send(subject, body, methods, tos, payload=None): | ||||
|     payload = payload or {} | ||||
|     subject = Template(subject).render(payload) | ||||
|     body = Template(body).render(payload) | ||||
|  | ||||
|     res = '' | ||||
|     for method in methods: | ||||
|         if method == "email" and not current_app.config.get('USE_MESSENGER', True): | ||||
|             send_mail(None, [to.get('email') for to in tos], subject, body) | ||||
|  | ||||
|         res += _request_messenger(subject, body, tos, method) + "\n" | ||||
|  | ||||
|     return res | ||||
| @@ -9,6 +9,8 @@ class CommonErrFormat(object): | ||||
|  | ||||
|     not_found = "不存在" | ||||
|  | ||||
|     circular_dependency_error = "存在循环依赖!" | ||||
|  | ||||
|     unknown_search_error = "未知搜索错误" | ||||
|  | ||||
|     invalid_json = "json格式似乎不正确了, 请仔细确认一下!" | ||||
|   | ||||
							
								
								
									
										105
									
								
								cmdb-api/api/lib/webhook.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								cmdb-api/api/lib/webhook.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,105 @@ | ||||
| # -*- coding:utf-8 -*- | ||||
|  | ||||
| import json | ||||
| from functools import partial | ||||
|  | ||||
| import requests | ||||
| from jinja2 import Template | ||||
| from requests.auth import HTTPBasicAuth | ||||
| from requests_oauthlib import OAuth2Session | ||||
|  | ||||
|  | ||||
| class BearerAuth(requests.auth.AuthBase): | ||||
|     def __init__(self, token): | ||||
|         self.token = token | ||||
|  | ||||
|     def __call__(self, r): | ||||
|         r.headers["authorization"] = "Bearer {}".format(self.token) | ||||
|         return r | ||||
|  | ||||
|  | ||||
| def _wrap_auth(**kwargs): | ||||
|     auth_type = (kwargs.get('type') or "").lower() | ||||
|     if auth_type == "basicauth": | ||||
|         return HTTPBasicAuth(kwargs.get('username'), kwargs.get('password')) | ||||
|  | ||||
|     elif auth_type == "bearer": | ||||
|         return BearerAuth(kwargs.get('token')) | ||||
|  | ||||
|     elif auth_type == 'oauth2.0': | ||||
|         client_id = kwargs.get('client_id') | ||||
|         client_secret = kwargs.get('client_secret') | ||||
|         authorization_base_url = kwargs.get('authorization_base_url') | ||||
|         token_url = kwargs.get('token_url') | ||||
|         redirect_url = kwargs.get('redirect_url') | ||||
|         scope = kwargs.get('scope') | ||||
|  | ||||
|         oauth2_session = OAuth2Session(client_id, scope=scope or None) | ||||
|         oauth2_session.authorization_url(authorization_base_url) | ||||
|  | ||||
|         oauth2_session.fetch_token(token_url, client_secret=client_secret, authorization_response=redirect_url) | ||||
|  | ||||
|         return oauth2_session | ||||
|  | ||||
|     elif auth_type == "apikey": | ||||
|         return HTTPBasicAuth(kwargs.get('key'), kwargs.get('value')) | ||||
|  | ||||
|  | ||||
| def webhook_request(webhook, payload): | ||||
|     """ | ||||
|  | ||||
|     :param webhook: | ||||
|     { | ||||
|         "url": "https://veops.cn" | ||||
|         "method": "GET|POST|PUT|DELETE" | ||||
|         "body": {}, | ||||
|         "headers": { | ||||
|             "Content-Type": "Application/json" | ||||
|         }, | ||||
|         "parameters": { | ||||
|             "key": "value" | ||||
|         }, | ||||
|         "authorization": { | ||||
|             "type": "BasicAuth|Bearer|OAuth2.0|APIKey", | ||||
|             "password": "mmmm",  # BasicAuth | ||||
|             "username": "bbb",   # BasicAuth | ||||
|  | ||||
|             "token": "xxx",  # Bearer | ||||
|  | ||||
|             "key": "xxx",    # APIKey | ||||
|             "value": "xxx",  # APIKey | ||||
|  | ||||
|             "client_id": "xxx",               # OAuth2.0 | ||||
|             "client_secret": "xxx",           # OAuth2.0 | ||||
|             "authorization_base_url": "xxx",  # OAuth2.0 | ||||
|             "token_url": "xxx",               # OAuth2.0 | ||||
|             "redirect_url": "xxx",            # OAuth2.0 | ||||
|             "scope": "xxx"                    # OAuth2.0 | ||||
|         } | ||||
|     } | ||||
|     :param payload: | ||||
|     :return: | ||||
|     """ | ||||
|     assert webhook.get('url') is not None | ||||
|  | ||||
|     url = Template(webhook['url']).render(payload) | ||||
|  | ||||
|     params = webhook.get('parameters') or None | ||||
|     if isinstance(params, dict): | ||||
|         params = json.loads(Template(json.dumps(params)).render(payload)) | ||||
|  | ||||
|     data = Template(json.dumps(webhook.get('body', ''))).render(payload) | ||||
|     auth = _wrap_auth(**webhook.get('authorization', {})) | ||||
|  | ||||
|     if (webhook.get('authorization', {}).get("type") or '').lower() == 'oauth2.0': | ||||
|         request = getattr(auth, webhook.get('method', 'GET').lower()) | ||||
|     else: | ||||
|         request = partial(requests.request, webhook.get('method', 'GET')) | ||||
|  | ||||
|     return request( | ||||
|         url, | ||||
|         params=params, | ||||
|         headers=webhook.get('headers') or None, | ||||
|         data=data, | ||||
|         auth=auth | ||||
|     ) | ||||
| @@ -125,16 +125,26 @@ class CITypeAttributeGroupItem(Model): | ||||
|  | ||||
|  | ||||
| class CITypeTrigger(Model): | ||||
|     # __tablename__ = "c_ci_type_triggers" | ||||
|     __tablename__ = "c_c_t_t" | ||||
|  | ||||
|     type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'), nullable=False) | ||||
|     attr_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id"), nullable=False) | ||||
|     notify = db.Column(db.JSON)  # {subject: x, body: x, wx_to: [], mail_to: [], before_days: 0, notify_at: 08:00} | ||||
|     attr_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id")) | ||||
|     option = db.Column('notify', db.JSON) | ||||
|  | ||||
|  | ||||
| class CITriggerHistory(Model): | ||||
|     __tablename__ = "c_ci_trigger_histories" | ||||
|  | ||||
|     operate_type = db.Column(db.Enum(*OperateType.all(), name="operate_type")) | ||||
|     record_id = db.Column(db.Integer, db.ForeignKey("c_records.id")) | ||||
|     ci_id = db.Column(db.Integer, index=True, nullable=False) | ||||
|     trigger_id = db.Column(db.Integer, db.ForeignKey("c_c_t_t.id")) | ||||
|     is_ok = db.Column(db.Boolean, default=False) | ||||
|     notify = db.Column(db.Text) | ||||
|     webhook = db.Column(db.Text) | ||||
|  | ||||
|  | ||||
| class CITypeUniqueConstraint(Model): | ||||
|     # __tablename__ = "c_ci_type_unique_constraints" | ||||
|     __tablename__ = "c_c_t_u_c" | ||||
|  | ||||
|     type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'), nullable=False) | ||||
| @@ -363,7 +373,6 @@ class CITypeHistory(Model): | ||||
|  | ||||
| # preference | ||||
| class PreferenceShowAttributes(Model): | ||||
|     # __tablename__ = "c_preference_show_attributes" | ||||
|     __tablename__ = "c_psa" | ||||
|  | ||||
|     uid = db.Column(db.Integer, index=True, nullable=False) | ||||
| @@ -377,7 +386,6 @@ class PreferenceShowAttributes(Model): | ||||
|  | ||||
|  | ||||
| class PreferenceTreeView(Model): | ||||
|     # __tablename__ = "c_preference_tree_views" | ||||
|     __tablename__ = "c_ptv" | ||||
|  | ||||
|     uid = db.Column(db.Integer, index=True, nullable=False) | ||||
| @@ -386,7 +394,6 @@ class PreferenceTreeView(Model): | ||||
|  | ||||
|  | ||||
| class PreferenceRelationView(Model): | ||||
|     # __tablename__ = "c_preference_relation_views" | ||||
|     __tablename__ = "c_prv" | ||||
|  | ||||
|     uid = db.Column(db.Integer, index=True, nullable=False) | ||||
|   | ||||
| @@ -56,12 +56,7 @@ class AttributeView(APIView): | ||||
|  | ||||
|     @args_required("name") | ||||
|     @args_validate(AttributeManager.cls) | ||||
|     def post(self, attr_id=None): | ||||
|         if request.url.endswith("/calc_computed_attribute"): | ||||
|             AttributeManager.calc_computed_attribute(attr_id) | ||||
|  | ||||
|             return self.jsonify(attr_id=attr_id) | ||||
|  | ||||
|     def post(self): | ||||
|         choice_value = handle_arg_list(request.values.get("choice_value")) | ||||
|         params = request.values | ||||
|         params["choice_value"] = choice_value | ||||
| @@ -74,6 +69,11 @@ class AttributeView(APIView): | ||||
|  | ||||
|     @args_validate(AttributeManager.cls) | ||||
|     def put(self, attr_id): | ||||
|         if request.url.endswith("/calc_computed_attribute"): | ||||
|             AttributeManager.calc_computed_attribute(attr_id) | ||||
|  | ||||
|             return self.jsonify(attr_id=attr_id) | ||||
|  | ||||
|         choice_value = handle_arg_list(request.values.get("choice_value")) | ||||
|         params = request.values | ||||
|         params["choice_value"] = choice_value | ||||
|   | ||||
| @@ -185,8 +185,8 @@ class CIUnique(APIView): | ||||
|     @has_perm_from_args("ci_id", ResourceTypeEnum.CI, PermEnum.UPDATE, CIManager.get_type_name) | ||||
|     def put(self, ci_id): | ||||
|         params = request.values | ||||
|         unique_name = params.keys()[0] | ||||
|         unique_value = params.values()[0] | ||||
|         unique_name = list(params.keys())[0] | ||||
|         unique_value = list(params.values())[0] | ||||
|  | ||||
|         CIManager.update_unique_value(ci_id, unique_name, unique_value) | ||||
|  | ||||
|   | ||||
| @@ -154,9 +154,15 @@ class EnableCITypeView(APIView): | ||||
|  | ||||
|  | ||||
| class CITypeAttributeView(APIView): | ||||
|     url_prefix = ("/ci_types/<int:type_id>/attributes", "/ci_types/<string:type_name>/attributes") | ||||
|     url_prefix = ("/ci_types/<int:type_id>/attributes", "/ci_types/<string:type_name>/attributes", | ||||
|                   "/ci_types/common_attributes") | ||||
|  | ||||
|     def get(self, type_id=None, type_name=None): | ||||
|         if request.path.endswith("/common_attributes"): | ||||
|             type_ids = handle_arg_list(request.values.get('type_ids')) | ||||
|  | ||||
|             return self.jsonify(attributes=CITypeAttributeManager.get_common_attributes(type_ids)) | ||||
|  | ||||
|         t = CITypeCache.get(type_id) or CITypeCache.get(type_name) or abort(404, ErrFormat.ci_type_not_found) | ||||
|         type_id = t.id | ||||
|         unique_id = t.unique_id | ||||
| @@ -413,22 +419,21 @@ class CITypeTriggerView(APIView): | ||||
|         return self.jsonify(CITypeTriggerManager.get(type_id)) | ||||
|  | ||||
|     @has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id) | ||||
|     @args_required("attr_id") | ||||
|     @args_required("notify") | ||||
|     @args_required("option") | ||||
|     def post(self, type_id): | ||||
|         attr_id = request.values.get('attr_id') | ||||
|         notify = request.values.get('notify') | ||||
|         attr_id = request.values.get('attr_id') or None | ||||
|         option = request.values.get('option') | ||||
|  | ||||
|         return self.jsonify(CITypeTriggerManager().add(type_id, attr_id, notify)) | ||||
|         return self.jsonify(CITypeTriggerManager().add(type_id, attr_id, option)) | ||||
|  | ||||
|     @has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id) | ||||
|     @args_required("notify") | ||||
|     @args_required("option") | ||||
|     def put(self, type_id, _id): | ||||
|         assert type_id is not None | ||||
|  | ||||
|         notify = request.values.get('notify') | ||||
|         option = request.values.get('option') | ||||
|  | ||||
|         return self.jsonify(CITypeTriggerManager().update(_id, notify)) | ||||
|         return self.jsonify(CITypeTriggerManager().update(_id, option)) | ||||
|  | ||||
|     @has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id) | ||||
|     def delete(self, type_id, _id): | ||||
| @@ -500,3 +505,4 @@ class CITypeFilterPermissionView(APIView): | ||||
|     @auth_with_app_token | ||||
|     def get(self, type_id): | ||||
|         return self.jsonify(CIFilterPermsCRUD().get(type_id)) | ||||
|  | ||||
|   | ||||
| @@ -19,9 +19,14 @@ from api.resource import APIView | ||||
|  | ||||
|  | ||||
| class GetChildrenView(APIView): | ||||
|     url_prefix = "/ci_type_relations/<int:parent_id>/children" | ||||
|     url_prefix = ("/ci_type_relations/<int:parent_id>/children", | ||||
|                   "/ci_type_relations/<int:parent_id>/recursive_level2children", | ||||
|                   ) | ||||
|  | ||||
|     def get(self, parent_id): | ||||
|         if request.url.endswith("recursive_level2children"): | ||||
|             return self.jsonify(CITypeRelationManager.recursive_level2children(parent_id)) | ||||
|  | ||||
|         return self.jsonify(children=CITypeRelationManager.get_children(parent_id)) | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -13,7 +13,8 @@ from api.resource import APIView | ||||
|  | ||||
|  | ||||
| class CustomDashboardApiView(APIView): | ||||
|     url_prefix = ("/custom_dashboard", "/custom_dashboard/<int:_id>", "/custom_dashboard/batch") | ||||
|     url_prefix = ("/custom_dashboard", "/custom_dashboard/<int:_id>", "/custom_dashboard/batch", | ||||
|                   "/custom_dashboard/preview") | ||||
|  | ||||
|     def get(self): | ||||
|         return self.jsonify(CustomDashboardManager.get()) | ||||
| @@ -21,17 +22,26 @@ class CustomDashboardApiView(APIView): | ||||
|     @role_required(RoleEnum.CONFIG) | ||||
|     @args_validate(CustomDashboardManager.cls) | ||||
|     def post(self): | ||||
|         cm = CustomDashboardManager.add(**request.values) | ||||
|         if request.url.endswith("/preview"): | ||||
|             return self.jsonify(counter=CustomDashboardManager.preview(**request.values)) | ||||
|  | ||||
|         return self.jsonify(cm.to_dict()) | ||||
|         cm, counter = CustomDashboardManager.add(**request.values) | ||||
|  | ||||
|         res = cm.to_dict() | ||||
|         res.update(counter=counter) | ||||
|  | ||||
|         return self.jsonify(res) | ||||
|  | ||||
|     @role_required(RoleEnum.CONFIG) | ||||
|     @args_validate(CustomDashboardManager.cls) | ||||
|     def put(self, _id=None): | ||||
|         if _id is not None: | ||||
|             cm = CustomDashboardManager.update(_id, **request.values) | ||||
|             cm, counter = CustomDashboardManager.update(_id, **request.values) | ||||
|  | ||||
|             return self.jsonify(cm.to_dict()) | ||||
|             res = cm.to_dict() | ||||
|             res.update(counter=counter) | ||||
|  | ||||
|             return self.jsonify(res) | ||||
|  | ||||
|         CustomDashboardManager.batch_update(request.values.get("id2options")) | ||||
|  | ||||
|   | ||||
| @@ -5,15 +5,18 @@ import datetime | ||||
|  | ||||
| from flask import abort | ||||
| from flask import request | ||||
| from flask import session | ||||
|  | ||||
| from api.lib.cmdb.ci import CIManager | ||||
| from api.lib.cmdb.const import PermEnum | ||||
| from api.lib.cmdb.const import ResourceTypeEnum | ||||
| from api.lib.cmdb.const import RoleEnum | ||||
| from api.lib.cmdb.history import AttributeHistoryManger | ||||
| from api.lib.cmdb.history import CITriggerHistoryManager | ||||
| from api.lib.cmdb.history import CITypeHistoryManager | ||||
| from api.lib.cmdb.resp_format import ErrFormat | ||||
| from api.lib.perm.acl.acl import has_perm_from_args | ||||
| from api.lib.perm.acl.acl import is_app_admin | ||||
| from api.lib.perm.acl.acl import role_required | ||||
| from api.lib.utils import get_page | ||||
| from api.lib.utils import get_page_size | ||||
| @@ -76,6 +79,39 @@ class CIHistoryView(APIView): | ||||
|         return self.jsonify(result) | ||||
|  | ||||
|  | ||||
| class CITriggerHistoryView(APIView): | ||||
|     url_prefix = ("/history/ci_triggers/<int:ci_id>", "/history/ci_triggers") | ||||
|  | ||||
|     @has_perm_from_args("ci_id", ResourceTypeEnum.CI, PermEnum.READ, CIManager.get_type_name) | ||||
|     def get(self, ci_id=None): | ||||
|         if ci_id is not None: | ||||
|             result = CITriggerHistoryManager.get_by_ci_id(ci_id) | ||||
|  | ||||
|             return self.jsonify(result) | ||||
|  | ||||
|         if RoleEnum.CONFIG not in session.get("acl", {}).get("parentRoles", []) and not is_app_admin("cmdb"): | ||||
|             return abort(403, ErrFormat.role_required.format(RoleEnum.CONFIG)) | ||||
|  | ||||
|         type_id = request.values.get("type_id") | ||||
|         trigger_id = request.values.get("trigger_id") | ||||
|         operate_type = request.values.get("operate_type") | ||||
|  | ||||
|         page = get_page(request.values.get('page', 1)) | ||||
|         page_size = get_page_size(request.values.get('page_size', 1)) | ||||
|  | ||||
|         numfound, result = CITriggerHistoryManager.get(page, | ||||
|                                                        page_size, | ||||
|                                                        type_id=type_id, | ||||
|                                                        trigger_id=trigger_id, | ||||
|                                                        operate_type=operate_type) | ||||
|  | ||||
|         return self.jsonify(page=page, | ||||
|                             page_size=page_size, | ||||
|                             numfound=numfound, | ||||
|                             total=len(result), | ||||
|                             result=result) | ||||
|  | ||||
|  | ||||
| class CITypeHistoryView(APIView): | ||||
|     url_prefix = "/history/ci_types" | ||||
|  | ||||
|   | ||||
| @@ -11,7 +11,7 @@ from api.resource import APIView | ||||
| prefix = '/file' | ||||
|  | ||||
| ALLOWED_EXTENSIONS = { | ||||
|     'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'xls', 'xlsx', 'doc', 'docx', 'ppt', 'pptx', 'csv' | ||||
|     'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'xls', 'xlsx', 'doc', 'docx', 'ppt', 'pptx', 'csv', 'svg' | ||||
| } | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -36,6 +36,7 @@ python-ldap==3.4.0 | ||||
| PyYAML==6.0 | ||||
| redis==4.6.0 | ||||
| requests==2.31.0 | ||||
| requests_oauthlib==1.3.1 | ||||
| six==1.12.0 | ||||
| SQLAlchemy==1.4.49 | ||||
| supervisor==4.0.3 | ||||
|   | ||||
| @@ -94,5 +94,3 @@ ES_HOST = '127.0.0.1' | ||||
| USE_ES = False | ||||
|  | ||||
| BOOL_TRUE = ['true', 'TRUE', 'True', True, '1', 1, "Yes", "YES", "yes", 'Y', 'y'] | ||||
|  | ||||
| CMDB_API = "http://127.0.0.1:5000/api/v0.1" | ||||
|   | ||||
| @@ -54,6 +54,84 @@ | ||||
|       <div class="content unicode" style="display: block;"> | ||||
|           <ul class="icon_lists dib-box"> | ||||
|            | ||||
|             <li class="dib"> | ||||
|               <span class="icon iconfont"></span> | ||||
|                 <div class="name">cmdb-histogram</div> | ||||
|                 <div class="code-name">&#xe886;</div> | ||||
|               </li> | ||||
|            | ||||
|             <li class="dib"> | ||||
|               <span class="icon iconfont"></span> | ||||
|                 <div class="name">cmdb-index</div> | ||||
|                 <div class="code-name">&#xe883;</div> | ||||
|               </li> | ||||
|            | ||||
|             <li class="dib"> | ||||
|               <span class="icon iconfont"></span> | ||||
|                 <div class="name">cmdb-piechart</div> | ||||
|                 <div class="code-name">&#xe884;</div> | ||||
|               </li> | ||||
|            | ||||
|             <li class="dib"> | ||||
|               <span class="icon iconfont"></span> | ||||
|                 <div class="name">cmdb-line</div> | ||||
|                 <div class="code-name">&#xe885;</div> | ||||
|               </li> | ||||
|            | ||||
|             <li class="dib"> | ||||
|               <span class="icon iconfont"></span> | ||||
|                 <div class="name">cmdb-table</div> | ||||
|                 <div class="code-name">&#xe882;</div> | ||||
|               </li> | ||||
|            | ||||
|             <li class="dib"> | ||||
|               <span class="icon iconfont"></span> | ||||
|                 <div class="name">itsm-all</div> | ||||
|                 <div class="code-name">&#xe87f;</div> | ||||
|               </li> | ||||
|            | ||||
|             <li class="dib"> | ||||
|               <span class="icon iconfont"></span> | ||||
|                 <div class="name">itsm-reply</div> | ||||
|                 <div class="code-name">&#xe87e;</div> | ||||
|               </li> | ||||
|            | ||||
|             <li class="dib"> | ||||
|               <span class="icon iconfont"></span> | ||||
|                 <div class="name">itsm-information</div> | ||||
|                 <div class="code-name">&#xe880;</div> | ||||
|               </li> | ||||
|            | ||||
|             <li class="dib"> | ||||
|               <span class="icon iconfont"></span> | ||||
|                 <div class="name">itsm-contact</div> | ||||
|                 <div class="code-name">&#xe881;</div> | ||||
|               </li> | ||||
|            | ||||
|             <li class="dib"> | ||||
|               <span class="icon iconfont"></span> | ||||
|                 <div class="name">itsm-my-processed</div> | ||||
|                 <div class="code-name">&#xe87d;</div> | ||||
|               </li> | ||||
|            | ||||
|             <li class="dib"> | ||||
|               <span class="icon iconfont"></span> | ||||
|                 <div class="name">rule_7</div> | ||||
|                 <div class="code-name">&#xe87c;</div> | ||||
|               </li> | ||||
|            | ||||
|             <li class="dib"> | ||||
|               <span class="icon iconfont"></span> | ||||
|                 <div class="name">itsm-my-completed</div> | ||||
|                 <div class="code-name">&#xe879;</div> | ||||
|               </li> | ||||
|            | ||||
|             <li class="dib"> | ||||
|               <span class="icon iconfont"></span> | ||||
|                 <div class="name">itsm-my-plan</div> | ||||
|                 <div class="code-name">&#xe87b;</div> | ||||
|               </li> | ||||
|            | ||||
|             <li class="dib"> | ||||
|               <span class="icon iconfont"></span> | ||||
|                 <div class="name">rule_100</div> | ||||
| @@ -3876,9 +3954,9 @@ | ||||
| <pre><code class="language-css" | ||||
| >@font-face { | ||||
|   font-family: 'iconfont'; | ||||
|   src: url('iconfont.woff2?t=1688550067963') format('woff2'), | ||||
|        url('iconfont.woff?t=1688550067963') format('woff'), | ||||
|        url('iconfont.ttf?t=1688550067963') format('truetype'); | ||||
|   src: url('iconfont.woff2?t=1694508259411') format('woff2'), | ||||
|        url('iconfont.woff?t=1694508259411') format('woff'), | ||||
|        url('iconfont.ttf?t=1694508259411') format('truetype'); | ||||
| } | ||||
| </code></pre> | ||||
|           <h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3> | ||||
| @@ -3904,6 +3982,123 @@ | ||||
|       <div class="content font-class"> | ||||
|         <ul class="icon_lists dib-box"> | ||||
|            | ||||
|           <li class="dib"> | ||||
|             <span class="icon iconfont cmdb-bar"></span> | ||||
|             <div class="name"> | ||||
|               cmdb-histogram | ||||
|             </div> | ||||
|             <div class="code-name">.cmdb-bar | ||||
|             </div> | ||||
|           </li> | ||||
|            | ||||
|           <li class="dib"> | ||||
|             <span class="icon iconfont cmdb-count"></span> | ||||
|             <div class="name"> | ||||
|               cmdb-index | ||||
|             </div> | ||||
|             <div class="code-name">.cmdb-count | ||||
|             </div> | ||||
|           </li> | ||||
|            | ||||
|           <li class="dib"> | ||||
|             <span class="icon iconfont cmdb-pie"></span> | ||||
|             <div class="name"> | ||||
|               cmdb-piechart | ||||
|             </div> | ||||
|             <div class="code-name">.cmdb-pie | ||||
|             </div> | ||||
|           </li> | ||||
|            | ||||
|           <li class="dib"> | ||||
|             <span class="icon iconfont cmdb-line"></span> | ||||
|             <div class="name"> | ||||
|               cmdb-line | ||||
|             </div> | ||||
|             <div class="code-name">.cmdb-line | ||||
|             </div> | ||||
|           </li> | ||||
|            | ||||
|           <li class="dib"> | ||||
|             <span class="icon iconfont cmdb-table"></span> | ||||
|             <div class="name"> | ||||
|               cmdb-table | ||||
|             </div> | ||||
|             <div class="code-name">.cmdb-table | ||||
|             </div> | ||||
|           </li> | ||||
|            | ||||
|           <li class="dib"> | ||||
|             <span class="icon iconfont itsm-all"></span> | ||||
|             <div class="name"> | ||||
|               itsm-all | ||||
|             </div> | ||||
|             <div class="code-name">.itsm-all | ||||
|             </div> | ||||
|           </li> | ||||
|            | ||||
|           <li class="dib"> | ||||
|             <span class="icon iconfont itsm-reply"></span> | ||||
|             <div class="name"> | ||||
|               itsm-reply | ||||
|             </div> | ||||
|             <div class="code-name">.itsm-reply | ||||
|             </div> | ||||
|           </li> | ||||
|            | ||||
|           <li class="dib"> | ||||
|             <span class="icon iconfont itsm-information"></span> | ||||
|             <div class="name"> | ||||
|               itsm-information | ||||
|             </div> | ||||
|             <div class="code-name">.itsm-information | ||||
|             </div> | ||||
|           </li> | ||||
|            | ||||
|           <li class="dib"> | ||||
|             <span class="icon iconfont itsm-contact"></span> | ||||
|             <div class="name"> | ||||
|               itsm-contact | ||||
|             </div> | ||||
|             <div class="code-name">.itsm-contact | ||||
|             </div> | ||||
|           </li> | ||||
|            | ||||
|           <li class="dib"> | ||||
|             <span class="icon iconfont itsm-my-my_already_handle"></span> | ||||
|             <div class="name"> | ||||
|               itsm-my-processed | ||||
|             </div> | ||||
|             <div class="code-name">.itsm-my-my_already_handle | ||||
|             </div> | ||||
|           </li> | ||||
|            | ||||
|           <li class="dib"> | ||||
|             <span class="icon iconfont rule_7"></span> | ||||
|             <div class="name"> | ||||
|               rule_7 | ||||
|             </div> | ||||
|             <div class="code-name">.rule_7 | ||||
|             </div> | ||||
|           </li> | ||||
|            | ||||
|           <li class="dib"> | ||||
|             <span class="icon iconfont itsm-my-completed"></span> | ||||
|             <div class="name"> | ||||
|               itsm-my-completed | ||||
|             </div> | ||||
|             <div class="code-name">.itsm-my-completed | ||||
|             </div> | ||||
|           </li> | ||||
|            | ||||
|           <li class="dib"> | ||||
|             <span class="icon iconfont itsm-my-plan"></span> | ||||
|             <div class="name"> | ||||
|               itsm-my-plan | ||||
|             </div> | ||||
|             <div class="code-name">.itsm-my-plan | ||||
|             </div> | ||||
|           </li> | ||||
|            | ||||
|           <li class="dib"> | ||||
|             <span class="icon iconfont rule_100"></span> | ||||
|             <div class="name"> | ||||
| @@ -5759,11 +5954,11 @@ | ||||
|           </li> | ||||
|            | ||||
|           <li class="dib"> | ||||
|             <span class="icon iconfont itsm-node-strat"></span> | ||||
|             <span class="icon iconfont itsm-node-start"></span> | ||||
|             <div class="name"> | ||||
|               itsm-node-strat | ||||
|             </div> | ||||
|             <div class="code-name">.itsm-node-strat | ||||
|             <div class="code-name">.itsm-node-start | ||||
|             </div> | ||||
|           </li> | ||||
|            | ||||
| @@ -9637,6 +9832,110 @@ | ||||
|       <div class="content symbol"> | ||||
|           <ul class="icon_lists dib-box"> | ||||
|            | ||||
|             <li class="dib"> | ||||
|                 <svg class="icon svg-icon" aria-hidden="true"> | ||||
|                   <use xlink:href="#cmdb-bar"></use> | ||||
|                 </svg> | ||||
|                 <div class="name">cmdb-histogram</div> | ||||
|                 <div class="code-name">#cmdb-bar</div> | ||||
|             </li> | ||||
|            | ||||
|             <li class="dib"> | ||||
|                 <svg class="icon svg-icon" aria-hidden="true"> | ||||
|                   <use xlink:href="#cmdb-count"></use> | ||||
|                 </svg> | ||||
|                 <div class="name">cmdb-index</div> | ||||
|                 <div class="code-name">#cmdb-count</div> | ||||
|             </li> | ||||
|            | ||||
|             <li class="dib"> | ||||
|                 <svg class="icon svg-icon" aria-hidden="true"> | ||||
|                   <use xlink:href="#cmdb-pie"></use> | ||||
|                 </svg> | ||||
|                 <div class="name">cmdb-piechart</div> | ||||
|                 <div class="code-name">#cmdb-pie</div> | ||||
|             </li> | ||||
|            | ||||
|             <li class="dib"> | ||||
|                 <svg class="icon svg-icon" aria-hidden="true"> | ||||
|                   <use xlink:href="#cmdb-line"></use> | ||||
|                 </svg> | ||||
|                 <div class="name">cmdb-line</div> | ||||
|                 <div class="code-name">#cmdb-line</div> | ||||
|             </li> | ||||
|            | ||||
|             <li class="dib"> | ||||
|                 <svg class="icon svg-icon" aria-hidden="true"> | ||||
|                   <use xlink:href="#cmdb-table"></use> | ||||
|                 </svg> | ||||
|                 <div class="name">cmdb-table</div> | ||||
|                 <div class="code-name">#cmdb-table</div> | ||||
|             </li> | ||||
|            | ||||
|             <li class="dib"> | ||||
|                 <svg class="icon svg-icon" aria-hidden="true"> | ||||
|                   <use xlink:href="#itsm-all"></use> | ||||
|                 </svg> | ||||
|                 <div class="name">itsm-all</div> | ||||
|                 <div class="code-name">#itsm-all</div> | ||||
|             </li> | ||||
|            | ||||
|             <li class="dib"> | ||||
|                 <svg class="icon svg-icon" aria-hidden="true"> | ||||
|                   <use xlink:href="#itsm-reply"></use> | ||||
|                 </svg> | ||||
|                 <div class="name">itsm-reply</div> | ||||
|                 <div class="code-name">#itsm-reply</div> | ||||
|             </li> | ||||
|            | ||||
|             <li class="dib"> | ||||
|                 <svg class="icon svg-icon" aria-hidden="true"> | ||||
|                   <use xlink:href="#itsm-information"></use> | ||||
|                 </svg> | ||||
|                 <div class="name">itsm-information</div> | ||||
|                 <div class="code-name">#itsm-information</div> | ||||
|             </li> | ||||
|            | ||||
|             <li class="dib"> | ||||
|                 <svg class="icon svg-icon" aria-hidden="true"> | ||||
|                   <use xlink:href="#itsm-contact"></use> | ||||
|                 </svg> | ||||
|                 <div class="name">itsm-contact</div> | ||||
|                 <div class="code-name">#itsm-contact</div> | ||||
|             </li> | ||||
|            | ||||
|             <li class="dib"> | ||||
|                 <svg class="icon svg-icon" aria-hidden="true"> | ||||
|                   <use xlink:href="#itsm-my-my_already_handle"></use> | ||||
|                 </svg> | ||||
|                 <div class="name">itsm-my-processed</div> | ||||
|                 <div class="code-name">#itsm-my-my_already_handle</div> | ||||
|             </li> | ||||
|            | ||||
|             <li class="dib"> | ||||
|                 <svg class="icon svg-icon" aria-hidden="true"> | ||||
|                   <use xlink:href="#rule_7"></use> | ||||
|                 </svg> | ||||
|                 <div class="name">rule_7</div> | ||||
|                 <div class="code-name">#rule_7</div> | ||||
|             </li> | ||||
|            | ||||
|             <li class="dib"> | ||||
|                 <svg class="icon svg-icon" aria-hidden="true"> | ||||
|                   <use xlink:href="#itsm-my-completed"></use> | ||||
|                 </svg> | ||||
|                 <div class="name">itsm-my-completed</div> | ||||
|                 <div class="code-name">#itsm-my-completed</div> | ||||
|             </li> | ||||
|            | ||||
|             <li class="dib"> | ||||
|                 <svg class="icon svg-icon" aria-hidden="true"> | ||||
|                   <use xlink:href="#itsm-my-plan"></use> | ||||
|                 </svg> | ||||
|                 <div class="name">itsm-my-plan</div> | ||||
|                 <div class="code-name">#itsm-my-plan</div> | ||||
|             </li> | ||||
|            | ||||
|             <li class="dib"> | ||||
|                 <svg class="icon svg-icon" aria-hidden="true"> | ||||
|                   <use xlink:href="#rule_100"></use> | ||||
| @@ -11287,10 +11586,10 @@ | ||||
|            | ||||
|             <li class="dib"> | ||||
|                 <svg class="icon svg-icon" aria-hidden="true"> | ||||
|                   <use xlink:href="#itsm-node-strat"></use> | ||||
|                   <use xlink:href="#itsm-node-start"></use> | ||||
|                 </svg> | ||||
|                 <div class="name">itsm-node-strat</div> | ||||
|                 <div class="code-name">#itsm-node-strat</div> | ||||
|                 <div class="code-name">#itsm-node-start</div> | ||||
|             </li> | ||||
|            | ||||
|             <li class="dib"> | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| @font-face { | ||||
|   font-family: "iconfont"; /* Project id 3857903 */ | ||||
|   src: url('iconfont.woff2?t=1688550067963') format('woff2'), | ||||
|        url('iconfont.woff?t=1688550067963') format('woff'), | ||||
|        url('iconfont.ttf?t=1688550067963') format('truetype'); | ||||
|   src: url('iconfont.woff2?t=1694508259411') format('woff2'), | ||||
|        url('iconfont.woff?t=1694508259411') format('woff'), | ||||
|        url('iconfont.ttf?t=1694508259411') format('truetype'); | ||||
| } | ||||
|  | ||||
| .iconfont { | ||||
| @@ -13,6 +13,58 @@ | ||||
|   -moz-osx-font-smoothing: grayscale; | ||||
| } | ||||
|  | ||||
| .cmdb-bar:before { | ||||
|   content: "\e886"; | ||||
| } | ||||
|  | ||||
| .cmdb-count:before { | ||||
|   content: "\e883"; | ||||
| } | ||||
|  | ||||
| .cmdb-pie:before { | ||||
|   content: "\e884"; | ||||
| } | ||||
|  | ||||
| .cmdb-line:before { | ||||
|   content: "\e885"; | ||||
| } | ||||
|  | ||||
| .cmdb-table:before { | ||||
|   content: "\e882"; | ||||
| } | ||||
|  | ||||
| .itsm-all:before { | ||||
|   content: "\e87f"; | ||||
| } | ||||
|  | ||||
| .itsm-reply:before { | ||||
|   content: "\e87e"; | ||||
| } | ||||
|  | ||||
| .itsm-information:before { | ||||
|   content: "\e880"; | ||||
| } | ||||
|  | ||||
| .itsm-contact:before { | ||||
|   content: "\e881"; | ||||
| } | ||||
|  | ||||
| .itsm-my-my_already_handle:before { | ||||
|   content: "\e87d"; | ||||
| } | ||||
|  | ||||
| .rule_7:before { | ||||
|   content: "\e87c"; | ||||
| } | ||||
|  | ||||
| .itsm-my-completed:before { | ||||
|   content: "\e879"; | ||||
| } | ||||
|  | ||||
| .itsm-my-plan:before { | ||||
|   content: "\e87b"; | ||||
| } | ||||
|  | ||||
| .rule_100:before { | ||||
|   content: "\e87a"; | ||||
| } | ||||
| @@ -837,7 +889,7 @@ | ||||
|   content: "\e7ad"; | ||||
| } | ||||
|  | ||||
| .itsm-node-strat:before { | ||||
| .itsm-node-start:before { | ||||
|   content: "\e7ae"; | ||||
| } | ||||
|  | ||||
|   | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -5,6 +5,97 @@ | ||||
|   "css_prefix_text": "", | ||||
|   "description": "", | ||||
|   "glyphs": [ | ||||
|     { | ||||
|       "icon_id": "37334642", | ||||
|       "name": "cmdb-histogram", | ||||
|       "font_class": "cmdb-bar", | ||||
|       "unicode": "e886", | ||||
|       "unicode_decimal": 59526 | ||||
|     }, | ||||
|     { | ||||
|       "icon_id": "37334651", | ||||
|       "name": "cmdb-index", | ||||
|       "font_class": "cmdb-count", | ||||
|       "unicode": "e883", | ||||
|       "unicode_decimal": 59523 | ||||
|     }, | ||||
|     { | ||||
|       "icon_id": "37334650", | ||||
|       "name": "cmdb-piechart", | ||||
|       "font_class": "cmdb-pie", | ||||
|       "unicode": "e884", | ||||
|       "unicode_decimal": 59524 | ||||
|     }, | ||||
|     { | ||||
|       "icon_id": "37334648", | ||||
|       "name": "cmdb-line", | ||||
|       "font_class": "cmdb-line", | ||||
|       "unicode": "e885", | ||||
|       "unicode_decimal": 59525 | ||||
|     }, | ||||
|     { | ||||
|       "icon_id": "37334627", | ||||
|       "name": "cmdb-table", | ||||
|       "font_class": "cmdb-table", | ||||
|       "unicode": "e882", | ||||
|       "unicode_decimal": 59522 | ||||
|     }, | ||||
|     { | ||||
|       "icon_id": "37310392", | ||||
|       "name": "itsm-all", | ||||
|       "font_class": "itsm-all", | ||||
|       "unicode": "e87f", | ||||
|       "unicode_decimal": 59519 | ||||
|     }, | ||||
|     { | ||||
|       "icon_id": "36998696", | ||||
|       "name": "itsm-reply", | ||||
|       "font_class": "itsm-reply", | ||||
|       "unicode": "e87e", | ||||
|       "unicode_decimal": 59518 | ||||
|     }, | ||||
|     { | ||||
|       "icon_id": "36639018", | ||||
|       "name": "itsm-information", | ||||
|       "font_class": "itsm-information", | ||||
|       "unicode": "e880", | ||||
|       "unicode_decimal": 59520 | ||||
|     }, | ||||
|     { | ||||
|       "icon_id": "36639017", | ||||
|       "name": "itsm-contact", | ||||
|       "font_class": "itsm-contact", | ||||
|       "unicode": "e881", | ||||
|       "unicode_decimal": 59521 | ||||
|     }, | ||||
|     { | ||||
|       "icon_id": "36557425", | ||||
|       "name": "itsm-my-processed", | ||||
|       "font_class": "itsm-my-my_already_handle", | ||||
|       "unicode": "e87d", | ||||
|       "unicode_decimal": 59517 | ||||
|     }, | ||||
|     { | ||||
|       "icon_id": "36488174", | ||||
|       "name": "rule_7", | ||||
|       "font_class": "rule_7", | ||||
|       "unicode": "e87c", | ||||
|       "unicode_decimal": 59516 | ||||
|     }, | ||||
|     { | ||||
|       "icon_id": "36380087", | ||||
|       "name": "itsm-my-completed", | ||||
|       "font_class": "itsm-my-completed", | ||||
|       "unicode": "e879", | ||||
|       "unicode_decimal": 59513 | ||||
|     }, | ||||
|     { | ||||
|       "icon_id": "36380096", | ||||
|       "name": "itsm-my-plan", | ||||
|       "font_class": "itsm-my-plan", | ||||
|       "unicode": "e87b", | ||||
|       "unicode_decimal": 59515 | ||||
|     }, | ||||
|     { | ||||
|       "icon_id": "36304673", | ||||
|       "name": "rule_100", | ||||
| @@ -1450,7 +1541,7 @@ | ||||
|     { | ||||
|       "icon_id": "35024980", | ||||
|       "name": "itsm-node-strat", | ||||
|       "font_class": "itsm-node-strat", | ||||
|       "font_class": "itsm-node-start", | ||||
|       "unicode": "e7ae", | ||||
|       "unicode_decimal": 59310 | ||||
|     }, | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -68,7 +68,8 @@ export default { | ||||
|   }, | ||||
|  | ||||
|   methods: { | ||||
|     visibleChange(open) { | ||||
|     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] | ||||
| @@ -151,15 +152,20 @@ export default { | ||||
|         }) | ||||
|         this.ruleList = [...expArray] | ||||
|       } else if (open) { | ||||
|         this.ruleList = [ | ||||
|           { | ||||
|             id: uuidv4(), | ||||
|             type: 'and', | ||||
|             property: this.canSearchPreferenceAttrList[0].name, | ||||
|             exp: 'is', | ||||
|             value: null, | ||||
|           }, | ||||
|         ] | ||||
|         this.ruleList = isInitOne | ||||
|           ? [ | ||||
|               { | ||||
|                 id: uuidv4(), | ||||
|                 type: 'and', | ||||
|                 property: | ||||
|                   this.canSearchPreferenceAttrList && this.canSearchPreferenceAttrList.length | ||||
|                     ? this.canSearchPreferenceAttrList[0].name | ||||
|                     : undefined, | ||||
|                 exp: 'is', | ||||
|                 value: null, | ||||
|               }, | ||||
|             ] | ||||
|           : [] | ||||
|       } | ||||
|     }, | ||||
|     handleClear() { | ||||
|   | ||||
| @@ -77,6 +77,14 @@ export function getCITypeAttributesByTypeIds(params) { | ||||
|   }) | ||||
| } | ||||
|  | ||||
| export function getCITypeCommonAttributesByTypeIds(params) { | ||||
|   return axios({ | ||||
|     url: `/v0.1/ci_types/common_attributes`, | ||||
|     method: 'get', | ||||
|     params: params | ||||
|   }) | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * 删除属性 | ||||
|  * @param attrId | ||||
|   | ||||
| @@ -61,3 +61,10 @@ export function revokeTypeRelation(first_type_id, second_type_id, rid, data) { | ||||
|     data | ||||
|   }) | ||||
| } | ||||
|  | ||||
| export function getRecursive_level2children(type_id) { | ||||
|   return axios({ | ||||
|     url: `/v0.1/ci_type_relations/${type_id}/recursive_level2children`, | ||||
|     method: 'GET' | ||||
|   }) | ||||
| } | ||||
|   | ||||
| @@ -37,3 +37,11 @@ export function batchUpdateCustomDashboard(data) { | ||||
|         data | ||||
|     }) | ||||
| } | ||||
|  | ||||
| export function postCustomDashboardPreview(data) { | ||||
|     return axios({ | ||||
|         url: '/v0.1/custom_dashboard/preview', | ||||
|         method: 'post', | ||||
|         data | ||||
|     }) | ||||
| } | ||||
|   | ||||
| @@ -1,11 +1,55 @@ | ||||
| <template> | ||||
|   <div :style="{ width: '100%', height: 'calc(100% - 2.2vw)' }"> | ||||
|     <div v-if="category === 0" class="cmdb-dashboard-grid-item-chart"> | ||||
|   <div | ||||
|     :id="`cmdb-dashboard-${chartId}-${editable}-${isPreview}`" | ||||
|     :style="{ width: '100%', height: 'calc(100% - 2.2vw)' }" | ||||
|   > | ||||
|     <div | ||||
|       v-if="options.chartType === 'count'" | ||||
|       :style="{ color: options.fontColor || '#fff' }" | ||||
|       class="cmdb-dashboard-grid-item-chart" | ||||
|     > | ||||
|       <div class="cmdb-dashboard-grid-item-chart-icon" v-if="options.showIcon && ciType"> | ||||
|         <template v-if="ciType.icon"> | ||||
|           <img v-if="ciType.icon.split('$$')[2]" :src="`/api/common-setting/v1/file/${ciType.icon.split('$$')[3]}`" /> | ||||
|           <ops-icon | ||||
|             v-else | ||||
|             :style="{ | ||||
|               color: ciType.icon.split('$$')[1], | ||||
|             }" | ||||
|             :type="ciType.icon.split('$$')[0]" | ||||
|           /> | ||||
|         </template> | ||||
|         <span :style="{ color: '#2f54eb' }" v-else>{{ ciType.name[0].toUpperCase() }}</span> | ||||
|       </div> | ||||
|       <span :style="{ ...options.fontConfig }">{{ toThousands(data) }}</span> | ||||
|     </div> | ||||
|     <vxe-table | ||||
|       :max-height="tableHeight" | ||||
|       :data="tableData" | ||||
|       :stripe="!!options.ret" | ||||
|       size="mini" | ||||
|       class="ops-stripe-table" | ||||
|       v-if="options.chartType === 'table'" | ||||
|       :span-method="mergeRowMethod" | ||||
|       :border="!options.ret" | ||||
|       :show-header="!!options.ret" | ||||
|     > | ||||
|       <template v-if="options.ret"> | ||||
|         <vxe-column v-for="col in columns" :key="col" :title="col" :field="col"></vxe-column> | ||||
|       </template> | ||||
|       <template v-else> | ||||
|         <vxe-column | ||||
|           v-for="(key, index) in Array(keyLength)" | ||||
|           :key="`key${index}`" | ||||
|           :title="`key${index}`" | ||||
|           :field="`key${index}`" | ||||
|         ></vxe-column> | ||||
|         <vxe-column field="value" title="value"></vxe-column> | ||||
|       </template> | ||||
|     </vxe-table> | ||||
|     <div | ||||
|       :id="`cmdb-dashboard-${chartId}-${editable}`" | ||||
|       v-if="category === 1 || category === 2" | ||||
|       v-else-if="category === 1 || category === 2" | ||||
|       class="cmdb-dashboard-grid-item-chart" | ||||
|     ></div> | ||||
|   </div> | ||||
| @@ -15,17 +59,27 @@ | ||||
| import * as echarts from 'echarts' | ||||
| import { mixin } from '@/utils/mixin' | ||||
| import { toThousands } from '../../utils/helper' | ||||
| import { category_1_bar_options, category_1_pie_options, category_2_bar_options } from './chartOptions' | ||||
| import { | ||||
|   category_1_bar_options, | ||||
|   category_1_line_options, | ||||
|   category_1_pie_options, | ||||
|   category_2_bar_options, | ||||
|   category_2_pie_options, | ||||
| } from './chartOptions' | ||||
| export default { | ||||
|   name: 'Chart', | ||||
|   mixins: [mixin], | ||||
|   props: { | ||||
|     ci_types: { | ||||
|       type: Array, | ||||
|       default: () => [], | ||||
|     }, | ||||
|     chartId: { | ||||
|       type: Number, | ||||
|       default: 0, | ||||
|     }, | ||||
|     data: { | ||||
|       type: [Number, Object], | ||||
|       type: [Number, Object, Array], | ||||
|       default: 0, | ||||
|     }, | ||||
|     category: { | ||||
| @@ -40,20 +94,65 @@ export default { | ||||
|       type: Boolean, | ||||
|       default: false, | ||||
|     }, | ||||
|     type_id: { | ||||
|       type: [Number, Array], | ||||
|       default: null, | ||||
|     }, | ||||
|     isPreview: { | ||||
|       type: Boolean, | ||||
|       default: false, | ||||
|     }, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       chart: null, | ||||
|       columns: [], | ||||
|       tableHeight: '', | ||||
|       tableData: [], | ||||
|       keyLength: 0, | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     ciType() { | ||||
|       if (this.type_id || this.options?.type_ids) { | ||||
|         const _find = this.ci_types.find((item) => item.id === this.type_id || item.id === this.options?.type_ids[0]) | ||||
|         return _find || null | ||||
|       } | ||||
|       return null | ||||
|     }, | ||||
|   }, | ||||
|   watch: { | ||||
|     data: { | ||||
|       immediate: true, | ||||
|       deep: true, | ||||
|       handler(newValue, oldValue) { | ||||
|         if (this.category === 1 || this.category === 2) { | ||||
|           if (Object.prototype.toString.call(newValue) === '[object Object]') { | ||||
|             this.setChart() | ||||
|           if (this.options.chartType !== 'table' && Object.prototype.toString.call(newValue) === '[object Object]') { | ||||
|             if (this.isPreview) { | ||||
|               this.$nextTick(() => { | ||||
|                 this.setChart() | ||||
|               }) | ||||
|             } else { | ||||
|               this.setChart() | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|         if (this.options.chartType === 'table') { | ||||
|           this.$nextTick(() => { | ||||
|             const dom = document.getElementById(`cmdb-dashboard-${this.chartId}-${this.editable}-${this.isPreview}`) | ||||
|             this.tableHeight = dom.offsetHeight | ||||
|           }) | ||||
|           if (this.options.ret) { | ||||
|             const excludeKeys = ['_X_ROW_KEY', 'ci_type', 'ci_type_alias', 'unique', 'unique_alias', '_id', '_type'] | ||||
|             if (newValue && newValue.length) { | ||||
|               this.columns = Object.keys(newValue[0]).filter((keys) => !excludeKeys.includes(keys)) | ||||
|               this.tableData = newValue | ||||
|             } | ||||
|           } else { | ||||
|             const _data = [] | ||||
|             this.keyLength = this.options?.attr_ids?.length ?? 0 | ||||
|             this.formatTableData(_data, this.data, {}) | ||||
|             this.tableData = _data | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
| @@ -81,13 +180,19 @@ export default { | ||||
|         this.chart = echarts.init(document.getElementById(`cmdb-dashboard-${this.chartId}-${this.editable}`)) | ||||
|       } | ||||
|       if (this.category === 1 && this.options.chartType === 'bar') { | ||||
|         this.chart.setOption(category_1_bar_options(this.data), true) | ||||
|         this.chart.setOption(category_1_bar_options(this.data, this.options), true) | ||||
|       } | ||||
|       if (this.category === 1 && this.options.chartType === 'line') { | ||||
|         this.chart.setOption(category_1_line_options(this.data, this.options), true) | ||||
|       } | ||||
|       if (this.category === 1 && this.options.chartType === 'pie') { | ||||
|         this.chart.setOption(category_1_pie_options(this.data), true) | ||||
|         this.chart.setOption(category_1_pie_options(this.data, this.options), true) | ||||
|       } | ||||
|       if (this.category === 2) { | ||||
|         this.chart.setOption(category_2_bar_options(this.data), true) | ||||
|       if (this.category === 2 && ['bar', 'line'].includes(this.options.chartType)) { | ||||
|         this.chart.setOption(category_2_bar_options(this.data, this.options, this.options.chartType), true) | ||||
|       } | ||||
|       if (this.category === 2 && this.options.chartType === 'pie') { | ||||
|         this.chart.setOption(category_2_pie_options(this.data, this.options), true) | ||||
|       } | ||||
|     }, | ||||
|     resizeChart() { | ||||
| @@ -97,6 +202,34 @@ export default { | ||||
|         } | ||||
|       }) | ||||
|     }, | ||||
|     formatTableData(_data, data, obj) { | ||||
|       Object.keys(data).forEach((k) => { | ||||
|         if (typeof data[k] === 'number') { | ||||
|           _data.push({ ...obj, [`key${Object.keys(obj).length}`]: k, value: data[k] }) | ||||
|         } else { | ||||
|           this.formatTableData(_data, data[k], { ...obj, [`key${Object.keys(obj).length}`]: k }) | ||||
|         } | ||||
|       }) | ||||
|     }, | ||||
|     mergeRowMethod({ row, _rowIndex, column, visibleData }) { | ||||
|       const fields = ['key0', 'key1', 'key2'] | ||||
|       const cellValue = row[column.field] | ||||
|       if (cellValue && fields.includes(column.field)) { | ||||
|         const prevRow = visibleData[_rowIndex - 1] | ||||
|         let nextRow = visibleData[_rowIndex + 1] | ||||
|         if (prevRow && prevRow[column.field] === cellValue) { | ||||
|           return { rowspan: 0, colspan: 0 } | ||||
|         } else { | ||||
|           let countRowspan = 1 | ||||
|           while (nextRow && nextRow[column.field] === cellValue) { | ||||
|             nextRow = visibleData[++countRowspan + _rowIndex] | ||||
|           } | ||||
|           if (countRowspan > 1) { | ||||
|             return { rowspan: countRowspan, colspan: 1 } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|   }, | ||||
| } | ||||
| </script> | ||||
| @@ -106,14 +239,28 @@ export default { | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
|   position: relative; | ||||
|   padding: 10px; | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
|   align-items: center; | ||||
|   > span { | ||||
|     font-size: 50px; | ||||
|     font-weight: 700; | ||||
|     position: absolute; | ||||
|     top: 50%; | ||||
|     left: 50%; | ||||
|     transform: translate(-50%, -50%); | ||||
|   } | ||||
|   .cmdb-dashboard-grid-item-chart-icon { | ||||
|     > i { | ||||
|       font-size: 4vw; | ||||
|     } | ||||
|     > img { | ||||
|       width: 4vw; | ||||
|     } | ||||
|     > span { | ||||
|       display: inline-block; | ||||
|       width: 4vw; | ||||
|       height: 4vw; | ||||
|       font-size: 50px; | ||||
|       text-align: center; | ||||
|       line-height: 50px; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -1,65 +1,307 @@ | ||||
| <template> | ||||
|   <a-modal :title="`${type === 'add' ? '新增' : '编辑'}图表`" :visible="visible" @cancel="handleclose" @ok="handleok"> | ||||
|     <a-form-model ref="chartForm" :model="form" :rules="rules" :label-col="{ span: 6 }" :wrapper-col="{ span: 14 }"> | ||||
|       <a-form-model-item label="类型" prop="category"> | ||||
|         <a-select v-model="form.category" @change="changeDashboardCategory"> | ||||
|           <a-select-option v-for="cate in Object.keys(dashboardCategory)" :key="cate" :value="Number(cate)">{{ | ||||
|             dashboardCategory[cate].label | ||||
|           }}</a-select-option> | ||||
|         </a-select> | ||||
|       </a-form-model-item> | ||||
|       <a-form-model-item v-if="form.category !== 0" label="名称" prop="name"> | ||||
|         <a-input v-model="form.name" placeholder="请输入图表名称"></a-input> | ||||
|       </a-form-model-item> | ||||
|       <a-form-model-item label="模型" prop="type_id"> | ||||
|         <a-select | ||||
|           show-search | ||||
|           optionFilterProp="children" | ||||
|           @change="changeCIType" | ||||
|           v-model="form.type_id" | ||||
|           placeholder="请选择模型" | ||||
|         > | ||||
|           <a-select-option v-for="ci_type in ci_types" :key="ci_type.id" :value="ci_type.id">{{ | ||||
|             ci_type.alias || ci_type.name | ||||
|           }}</a-select-option> | ||||
|         </a-select> | ||||
|       </a-form-model-item> | ||||
|       <a-form-model-item v-if="form.category === 1" label="模型属性" prop="attr_id"> | ||||
|         <a-select show-search optionFilterProp="children" v-model="form.attr_id" placeholder="请选择模型属性"> | ||||
|           <a-select-option v-for="attr in attributes" :key="attr.id" :value="attr.id">{{ | ||||
|             attr.alias || attr.name | ||||
|           }}</a-select-option> | ||||
|         </a-select> | ||||
|       </a-form-model-item> | ||||
|       <a-form-model-item v-if="form.category === 1" label="图表类型" prop="chartType"> | ||||
|         <a-radio-group v-model="chartType"> | ||||
|           <a-radio value="bar"> | ||||
|             柱状图 | ||||
|           </a-radio> | ||||
|           <a-radio value="pie"> | ||||
|             饼图 | ||||
|           </a-radio> | ||||
|         </a-radio-group> | ||||
|       </a-form-model-item> | ||||
|       <a-form-model-item v-if="form.category === 2" label="关系层级" prop="level"> | ||||
|         <a-input v-model="form.level" placeholder="请输入关系层级"></a-input> | ||||
|       </a-form-model-item> | ||||
|       <a-form-model-item v-if="form.category === 0" label="字体"> | ||||
|         <FontConfig ref="fontConfig" /> | ||||
|       </a-form-model-item> | ||||
|     </a-form-model> | ||||
|   <a-modal | ||||
|     width="1100px" | ||||
|     :title="`${type === 'add' ? '新增' : '编辑'}图表`" | ||||
|     :visible="visible" | ||||
|     @cancel="handleclose" | ||||
|     @ok="handleok" | ||||
|     :bodyStyle="{ paddingTop: 0 }" | ||||
|   > | ||||
|     <div class="chart-wrapper"> | ||||
|       <div class="chart-left"> | ||||
|         <a-form-model ref="chartForm" :model="form" :rules="rules" :label-col="{ span: 4 }" :wrapper-col="{ span: 18 }"> | ||||
|           <a-form-model-item label="标题" prop="name"> | ||||
|             <a-input v-model="form.name" placeholder="请输入图表标题"></a-input> | ||||
|           </a-form-model-item> | ||||
|           <a-form-model-item label="类型" prop="category" v-if="chartType !== 'count' && chartType !== 'table'"> | ||||
|             <a-radio-group | ||||
|               @change=" | ||||
|                 () => { | ||||
|                   resetForm() | ||||
|                 } | ||||
|               " | ||||
|               :default-value="1" | ||||
|               v-model="form.category" | ||||
|             > | ||||
|               <a-radio-button :value="Number(key)" :key="key" v-for="key in Object.keys(dashboardCategory)"> | ||||
|                 {{ dashboardCategory[key].label }} | ||||
|               </a-radio-button> | ||||
|             </a-radio-group> | ||||
|           </a-form-model-item> | ||||
|           <a-form-model-item label="类型" prop="tableCategory" v-if="chartType === 'table'"> | ||||
|             <a-radio-group | ||||
|               @change=" | ||||
|                 () => { | ||||
|                   resetForm() | ||||
|                 } | ||||
|               " | ||||
|               :default-value="1" | ||||
|               v-model="form.tableCategory" | ||||
|             > | ||||
|               <a-radio-button :value="1"> | ||||
|                 计算指标 | ||||
|               </a-radio-button> | ||||
|               <a-radio-button :value="2"> | ||||
|                 资源数据 | ||||
|               </a-radio-button> | ||||
|             </a-radio-group> | ||||
|           </a-form-model-item> | ||||
|           <a-form-model-item | ||||
|             v-if="(chartType !== 'table' && form.category !== 2) || (chartType === 'table' && form.tableCategory === 1)" | ||||
|             label="模型" | ||||
|             prop="type_ids" | ||||
|           > | ||||
|             <a-select | ||||
|               show-search | ||||
|               optionFilterProp="children" | ||||
|               @change="changeCIType" | ||||
|               v-model="form.type_ids" | ||||
|               placeholder="请选择模型" | ||||
|               mode="multiple" | ||||
|             > | ||||
|               <a-select-option v-for="ci_type in ci_types" :key="ci_type.id" :value="ci_type.id">{{ | ||||
|                 ci_type.alias || ci_type.name | ||||
|               }}</a-select-option> | ||||
|             </a-select> | ||||
|           </a-form-model-item> | ||||
|           <a-form-model-item v-else label="模型" prop="type_id"> | ||||
|             <a-select | ||||
|               show-search | ||||
|               optionFilterProp="children" | ||||
|               @change="changeCIType" | ||||
|               v-model="form.type_id" | ||||
|               placeholder="请选择模型" | ||||
|             > | ||||
|               <a-select-option v-for="ci_type in ci_types" :key="ci_type.id" :value="ci_type.id">{{ | ||||
|                 ci_type.alias || ci_type.name | ||||
|               }}</a-select-option> | ||||
|             </a-select> | ||||
|           </a-form-model-item> | ||||
|           <a-form-model-item | ||||
|             label="维度" | ||||
|             prop="attr_ids" | ||||
|             v-if="(['bar', 'line', 'pie'].includes(chartType) && form.category === 1) || chartType === 'table'" | ||||
|           > | ||||
|             <a-select @change="changeAttr" v-model="form.attr_ids" placeholder="请选择维度" mode="multiple" show-search> | ||||
|               <a-select-option v-for="attr in commonAttributes" :key="attr.id" :value="attr.id">{{ | ||||
|                 attr.alias || attr.name | ||||
|               }}</a-select-option> | ||||
|             </a-select> | ||||
|           </a-form-model-item> | ||||
|           <a-form-model-item | ||||
|             prop="type_ids" | ||||
|             label="关系模型" | ||||
|             v-if="['bar', 'line', 'pie'].includes(chartType) && form.category === 2" | ||||
|           > | ||||
|             <a-select | ||||
|               show-search | ||||
|               optionFilterProp="children" | ||||
|               mode="multiple" | ||||
|               v-model="form.type_ids" | ||||
|               placeholder="请选择模型" | ||||
|             > | ||||
|               <a-select-opt-group | ||||
|                 v-for="(key, index) in Object.keys(level2children)" | ||||
|                 :key="key" | ||||
|                 :label="`层级${index + 1}`" | ||||
|               > | ||||
|                 <a-select-option | ||||
|                   @click="(e) => clickLevel2children(e, citype, index + 1)" | ||||
|                   v-for="citype in level2children[key]" | ||||
|                   :key="citype.id" | ||||
|                   :value="citype.id" | ||||
|                 > | ||||
|                   {{ citype.alias || citype.name }} | ||||
|                 </a-select-option> | ||||
|               </a-select-opt-group> | ||||
|             </a-select> | ||||
|           </a-form-model-item> | ||||
|           <div class="chart-left-preview"> | ||||
|             <span class="chart-left-preview-operation" @click="showPreview"><a-icon type="play-circle" /> 预览</span> | ||||
|             <template v-if="isShowPreview"> | ||||
|               <div v-if="chartType !== 'count'" class="cmdb-dashboard-grid-item-title"> | ||||
|                 <template v-if="form.showIcon && ciType"> | ||||
|                   <template v-if="ciType.icon"> | ||||
|                     <img | ||||
|                       v-if="ciType.icon.split('$$')[2]" | ||||
|                       :src="`/api/common-setting/v1/file/${ciType.icon.split('$$')[3]}`" | ||||
|                     /> | ||||
|                     <ops-icon | ||||
|                       v-else | ||||
|                       :style="{ | ||||
|                         color: ciType.icon.split('$$')[1], | ||||
|                       }" | ||||
|                       :type="ciType.icon.split('$$')[0]" | ||||
|                     /> | ||||
|                   </template> | ||||
|                   <span :style="{ color: '#2f54eb' }" v-else>{{ ciType.name[0].toUpperCase() }}</span> | ||||
|                 </template> | ||||
|                 <span :style="{ color: '#000' }"> {{ form.name }}</span> | ||||
|               </div> | ||||
|               <div | ||||
|                 class="chart-left-preview-box" | ||||
|                 :style="{ | ||||
|                   height: chartType === 'count' ? '120px' : '', | ||||
|                   marginTop: chartType === 'count' ? '80px' : '', | ||||
|                   background: | ||||
|                     chartType === 'count' | ||||
|                       ? Array.isArray(bgColor) | ||||
|                         ? `linear-gradient(to bottom, ${bgColor[0]} 0%, ${bgColor[1]} 100%)` | ||||
|                         : bgColor | ||||
|                       : '#fafafa', | ||||
|                 }" | ||||
|               > | ||||
|                 <div :style="{ color: fontColor }">{{ form.name }}</div> | ||||
|                 <Chart | ||||
|                   :ref="`chart_${item.id}`" | ||||
|                   :chartId="item.id" | ||||
|                   :data="previewData" | ||||
|                   :category="form.category" | ||||
|                   :options="{ | ||||
|                     ...item.options, | ||||
|                     name: form.name, | ||||
|                     fontColor: fontColor, | ||||
|                     bgColor: bgColor, | ||||
|                     chartType: chartType, | ||||
|                     showIcon: form.showIcon, | ||||
|                     barDirection: barDirection, | ||||
|                     barStack: barStack, | ||||
|                     chartColor: chartColor, | ||||
|                     type_ids: form.type_ids, | ||||
|                     attr_ids: form.attr_ids, | ||||
|                     isShadow: isShadow, | ||||
|                   }" | ||||
|                   :editable="false" | ||||
|                   :ci_types="ci_types" | ||||
|                   :type_id="form.type_id || form.type_ids" | ||||
|                   isPreview | ||||
|                 /> | ||||
|               </div> | ||||
|             </template> | ||||
|           </div> | ||||
|           <a-form-model-item label="是否显示icon" prop="showIcon" :label-col="{ span: 5 }" :wrapper-col="{ span: 18 }"> | ||||
|             <a-switch v-model="form.showIcon"></a-switch> | ||||
|           </a-form-model-item> | ||||
|         </a-form-model> | ||||
|       </div> | ||||
|  | ||||
|       <div class="chart-right"> | ||||
|         <h4>图表类型</h4> | ||||
|         <div class="chart-right-type"> | ||||
|           <div | ||||
|             :class="{ 'chart-right-type-box': true, 'chart-right-type-box-selected': chartType === t.value }" | ||||
|             v-for="t in chartTypeList" | ||||
|             :key="t.value" | ||||
|             @click="changeChartType(t)" | ||||
|           > | ||||
|             <ops-icon :type="`cmdb-${t.value}`" /> | ||||
|             <span>{{ t.label }}</span> | ||||
|           </div> | ||||
|         </div> | ||||
|         <h4>数据筛选</h4> | ||||
|         <FilterComp | ||||
|           ref="filterComp" | ||||
|           :isDropdown="false" | ||||
|           :canSearchPreferenceAttrList="attributes" | ||||
|           @setExpFromFilter="setExpFromFilter" | ||||
|           :expression="filterExp ? `q=${filterExp}` : ''" | ||||
|         /> | ||||
|         <h4>格式</h4> | ||||
|         <a-form-model :colon="false" :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }"> | ||||
|           <a-form-model-item label="字体颜色" v-if="chartType === 'count'"> | ||||
|             <ColorPicker | ||||
|               v-model="fontColor" | ||||
|               :colorList="[ | ||||
|                 '#1D2129', | ||||
|                 '#4E5969', | ||||
|                 '#103C93', | ||||
|                 '#86909C', | ||||
|                 '#ffffff', | ||||
|                 '#C9F2FF', | ||||
|                 '#FFEAC0', | ||||
|                 '#D6FFE6', | ||||
|                 '#F2DEFF', | ||||
|               ]" | ||||
|             /> | ||||
|           </a-form-model-item> | ||||
|           <a-form-model-item label="背景颜色" v-if="chartType === 'count'"> | ||||
|             <ColorPicker | ||||
|               v-model="bgColor" | ||||
|               :colorList="[ | ||||
|                 ['#6ABFFE', '#5375EB'], | ||||
|                 ['#C69EFF', '#A377F9'], | ||||
|                 ['#85EBC9', '#4AB8D8'], | ||||
|                 ['#FEB58B', '#DF6463'], | ||||
|                 '#ffffff', | ||||
|                 '#FFFBF0', | ||||
|                 '#FFF1EC', | ||||
|                 '#E5FFFE', | ||||
|                 '#E5E7FF', | ||||
|               ]" | ||||
|             /> | ||||
|           </a-form-model-item> | ||||
|           <a-form-model-item label="图表颜色" v-else-if="chartType !== 'table'"> | ||||
|             <ColorListPicker v-model="chartColor" /> | ||||
|           </a-form-model-item> | ||||
|           <a-form-model-item label="图表长度(%)"> | ||||
|             <a-radio-group class="chart-width" style="width:100%;" v-model="width"> | ||||
|               <a-radio-button :value="3"> | ||||
|                 25 | ||||
|               </a-radio-button> | ||||
|               <a-radio-button :value="6"> | ||||
|                 50 | ||||
|               </a-radio-button> | ||||
|               <a-radio-button :value="9"> | ||||
|                 75 | ||||
|               </a-radio-button> | ||||
|               <a-radio-button :value="12"> | ||||
|                 100 | ||||
|               </a-radio-button> | ||||
|             </a-radio-group> | ||||
|           </a-form-model-item> | ||||
|           <a-form-model-item label="柱状图类型" v-if="chartType === 'bar'"> | ||||
|             <a-radio-group v-model="barStack"> | ||||
|               <a-radio value="total"> | ||||
|                 堆积柱状图 | ||||
|               </a-radio> | ||||
|               <a-radio value=""> | ||||
|                 多系列柱状图 | ||||
|               </a-radio> | ||||
|             </a-radio-group> | ||||
|           </a-form-model-item> | ||||
|           <a-form-model-item label="方向" v-if="chartType === 'bar'"> | ||||
|             <a-radio-group v-model="barDirection"> | ||||
|               <a-radio value="x"> | ||||
|                 X轴 | ||||
|               </a-radio> | ||||
|               <a-radio value="y"> | ||||
|                 y轴 | ||||
|               </a-radio> | ||||
|             </a-radio-group> | ||||
|           </a-form-model-item> | ||||
|           <a-form-model-item label="下方阴影" v-if="chartType === 'line'"> | ||||
|             <a-switch v-model="isShadow" /> | ||||
|           </a-form-model-item> | ||||
|         </a-form-model> | ||||
|       </div> | ||||
|     </div> | ||||
|   </a-modal> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import Chart from './chart.vue' | ||||
| import { dashboardCategory } from './constant' | ||||
| import { postCustomDashboard, putCustomDashboard } from '../../api/customDashboard' | ||||
| import { getCITypeAttributesById } from '../../api/CITypeAttr' | ||||
| import { postCustomDashboard, putCustomDashboard, postCustomDashboardPreview } from '../../api/customDashboard' | ||||
| import { getCITypeAttributesByTypeIds, getCITypeCommonAttributesByTypeIds } from '../../api/CITypeAttr' | ||||
| import { getRecursive_level2children } from '../../api/CITypeRelation' | ||||
| import { getLastLayout } from '../../utils/helper' | ||||
| import FontConfig from './fontConfig.vue' | ||||
| import FilterComp from '@/components/CMDBFilterComp' | ||||
| import ColorPicker from './colorPicker.vue' | ||||
| import ColorListPicker from './colorListPicker.vue' | ||||
|  | ||||
| export default { | ||||
|   name: 'ChartForm', | ||||
|   components: { FontConfig }, | ||||
|   components: { Chart, FilterComp, ColorPicker, ColorListPicker }, | ||||
|   props: { | ||||
|     ci_types: { | ||||
|       type: Array, | ||||
| @@ -67,100 +309,226 @@ export default { | ||||
|     }, | ||||
|   }, | ||||
|   data() { | ||||
|     const chartTypeList = [ | ||||
|       { | ||||
|         value: 'count', | ||||
|         label: '指标', | ||||
|       }, | ||||
|       { | ||||
|         value: 'bar', | ||||
|         label: '柱状图', | ||||
|       }, | ||||
|       { | ||||
|         value: 'line', | ||||
|         label: '折线图', | ||||
|       }, | ||||
|       { | ||||
|         value: 'pie', | ||||
|         label: '饼状图', | ||||
|       }, | ||||
|       { | ||||
|         value: 'table', | ||||
|         label: '表格', | ||||
|       }, | ||||
|     ] | ||||
|     return { | ||||
|       dashboardCategory, | ||||
|       chartTypeList, | ||||
|       visible: false, | ||||
|       attributes: [], | ||||
|       type: 'add', | ||||
|       form: { | ||||
|         category: 0, | ||||
|         tableCategory: 1, | ||||
|         name: undefined, | ||||
|         type_id: undefined, | ||||
|         attr_id: undefined, | ||||
|         type_ids: undefined, | ||||
|         attr_ids: undefined, | ||||
|         level: undefined, | ||||
|         showIcon: false, | ||||
|       }, | ||||
|       rules: { | ||||
|         category: [{ required: true, trigger: 'change' }], | ||||
|         name: [{ required: true, message: '请输入图表名称' }], | ||||
|         type_id: [{ required: true, message: '请选择模型', trigger: 'change' }], | ||||
|         attr_id: [{ required: true, message: '请选择模型属性', trigger: 'change' }], | ||||
|         type_ids: [{ required: true, message: '请选择模型', trigger: 'change' }], | ||||
|         attr_ids: [{ required: true, message: '请选择模型属性', trigger: 'change' }], | ||||
|         level: [{ required: true, message: '请输入关系层级' }], | ||||
|         showIcon: [{ required: false }], | ||||
|       }, | ||||
|       item: {}, | ||||
|       chartType: 'bar', | ||||
|       chartType: 'count', // table,bar,line,pie,count | ||||
|       width: 3, | ||||
|       fontColor: '#ffffff', | ||||
|       bgColor: ['#6ABFFE', '#5375EB'], | ||||
|       chartColor: '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD', // 图表颜色 | ||||
|       isShowPreview: false, | ||||
|       filterExp: undefined, | ||||
|       previewData: null, | ||||
|       barStack: 'total', | ||||
|       barDirection: 'y', | ||||
|       commonAttributes: [], | ||||
|       level2children: {}, | ||||
|       isShadow: false, | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     ciType() { | ||||
|       if (this.form.type_id || this.form.type_ids) { | ||||
|         const _find = this.ci_types.find((item) => item.id === this.form.type_id || item.id === this.form.type_ids[0]) | ||||
|         return _find || null | ||||
|       } | ||||
|       return null | ||||
|     }, | ||||
|   }, | ||||
|   inject: ['layout'], | ||||
|   methods: { | ||||
|     open(type, item = {}) { | ||||
|     async open(type, item = {}) { | ||||
|       this.visible = true | ||||
|       this.type = type | ||||
|       this.item = item | ||||
|       const { category = 0, name, type_id, attr_id, level } = item | ||||
|       const chartType = (item.options || {}).chartType || 'bar' | ||||
|       const chartType = (item.options || {}).chartType || 'count' | ||||
|       const fontColor = (item.options || {}).fontColor || '#ffffff' | ||||
|       const bgColor = (item.options || {}).bgColor || ['#6ABFFE', '#5375EB'] | ||||
|       const width = (item.options || {}).w | ||||
|       const showIcon = (item.options || {}).showIcon | ||||
|       const type_ids = item?.options?.type_ids || [] | ||||
|       const attr_ids = item?.options?.attr_ids || [] | ||||
|       const ret = item?.options?.ret || '' | ||||
|       this.width = width | ||||
|       this.chartType = chartType | ||||
|       if (type_id && attr_id) { | ||||
|         getCITypeAttributesById(type_id).then((res) => { | ||||
|       this.filterExp = item?.options?.filter ?? '' | ||||
|       this.chartColor = item?.options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD' | ||||
|       this.isShadow = item?.options?.isShadow ?? false | ||||
|  | ||||
|       if (chartType === 'count') { | ||||
|         this.fontColor = fontColor | ||||
|         this.bgColor = bgColor | ||||
|       } | ||||
|       if (type_ids && type_ids.length) { | ||||
|         await getCITypeAttributesByTypeIds({ type_ids: type_ids.join(',') }).then((res) => { | ||||
|           this.attributes = res.attributes | ||||
|         }) | ||||
|         if ((['bar', 'line', 'pie'].includes(chartType) && category === 1) || chartType === 'table') { | ||||
|           this.barDirection = item?.options?.barDirection ?? 'y' | ||||
|           this.barStack = item?.options?.barStack ?? 'total' | ||||
|           await getCITypeCommonAttributesByTypeIds({ | ||||
|             type_ids: type_ids.join(','), | ||||
|           }).then((res) => { | ||||
|             this.commonAttributes = res.attributes | ||||
|           }) | ||||
|         } | ||||
|       } | ||||
|       if (type_id) { | ||||
|         getRecursive_level2children(type_id).then((res) => { | ||||
|           this.level2children = res | ||||
|         }) | ||||
|         await getCITypeCommonAttributesByTypeIds({ | ||||
|           type_ids: type_id, | ||||
|         }).then((res) => { | ||||
|           this.commonAttributes = res.attributes | ||||
|         }) | ||||
|       } | ||||
|       this.$nextTick(() => { | ||||
|         this.$refs.filterComp.visibleChange(true, false) | ||||
|       }) | ||||
|       const default_form = { | ||||
|         category: 0, | ||||
|         name: undefined, | ||||
|         type_id: undefined, | ||||
|         attr_id: undefined, | ||||
|         type_ids: undefined, | ||||
|         attr_ids: undefined, | ||||
|         level: undefined, | ||||
|         showIcon: false, | ||||
|         tableCategory: 1, | ||||
|       } | ||||
|       this.form = { | ||||
|         ...default_form, | ||||
|         category, | ||||
|         name, | ||||
|         type_id, | ||||
|         attr_id, | ||||
|         type_ids, | ||||
|         attr_ids, | ||||
|         level, | ||||
|       } | ||||
|       if (category === 0) { | ||||
|         this.$nextTick(() => { | ||||
|           this.$refs.fontConfig.setConfig((item.options || {}).fontConfig) | ||||
|         }) | ||||
|         showIcon, | ||||
|         tableCategory: ret === 'cis' ? 2 : 1, | ||||
|       } | ||||
|     }, | ||||
|     handleclose() { | ||||
|       this.attributes = [] | ||||
|       this.$refs.chartForm.clearValidate() | ||||
|       this.isShowPreview = false | ||||
|       this.visible = false | ||||
|     }, | ||||
|     changeCIType(value) { | ||||
|       getCITypeAttributesById(value).then((res) => { | ||||
|       this.form.attr_ids = [] | ||||
|       this.commonAttributes = [] | ||||
|       getCITypeAttributesByTypeIds({ type_ids: Array.isArray(value) ? value.join(',') : value }).then((res) => { | ||||
|         this.attributes = res.attributes | ||||
|         this.form = { | ||||
|           ...this.form, | ||||
|           attr_id: undefined, | ||||
|         } | ||||
|       }) | ||||
|       if (!Array.isArray(value)) { | ||||
|         getRecursive_level2children(value).then((res) => { | ||||
|           this.level2children = res | ||||
|         }) | ||||
|       } | ||||
|       if ((['bar', 'line', 'pie'].includes(this.chartType) && this.form.category === 1) || this.chartType === 'table') { | ||||
|         getCITypeCommonAttributesByTypeIds({ type_ids: Array.isArray(value) ? value.join(',') : value }).then((res) => { | ||||
|           this.commonAttributes = res.attributes | ||||
|         }) | ||||
|       } | ||||
|     }, | ||||
|     handleok() { | ||||
|       this.$refs.chartForm.validate(async (valid) => { | ||||
|         if (valid) { | ||||
|           const fontConfig = this.form.category === 0 ? this.$refs.fontConfig.getConfig() : undefined | ||||
|           const _find = this.ci_types.find((attr) => attr.id === this.form.type_id) | ||||
|           const name = this.form.name || (_find || {}).alias || (_find || {}).name | ||||
|           const name = this.form.name | ||||
|           const { chartType, fontColor, bgColor } = this | ||||
|           this.$refs.filterComp.handleSubmit() | ||||
|           if (this.item.id) { | ||||
|             await putCustomDashboard(this.item.id, { | ||||
|             const params = { | ||||
|               ...this.form, | ||||
|               options: { | ||||
|                 ...this.item.options, | ||||
|                 name, | ||||
|                 fontConfig, | ||||
|                 w: this.width, | ||||
|                 chartType: this.chartType, | ||||
|                 showIcon: this.form.showIcon, | ||||
|                 type_ids: this.form.type_ids, | ||||
|                 filter: this.filterExp, | ||||
|                 isShadow: this.isShadow, | ||||
|               }, | ||||
|             }) | ||||
|             } | ||||
|             if (chartType === 'count') { | ||||
|               params.options.fontColor = fontColor | ||||
|               params.options.bgColor = bgColor | ||||
|             } | ||||
|             if (['bar', 'line', 'pie'].includes(chartType)) { | ||||
|               if (this.form.category === 1) { | ||||
|                 params.options.attr_ids = this.form.attr_ids | ||||
|               } | ||||
|               params.options.chartColor = this.chartColor | ||||
|             } | ||||
|             if (chartType === 'bar') { | ||||
|               params.options.barDirection = this.barDirection | ||||
|               params.options.barStack = this.barStack | ||||
|             } | ||||
|             if (chartType === 'table') { | ||||
|               params.options.attr_ids = this.form.attr_ids | ||||
|               if (this.form.tableCategory === 2) { | ||||
|                 params.options.ret = 'cis' | ||||
|               } | ||||
|             } | ||||
|             delete params.showIcon | ||||
|             delete params.type_ids | ||||
|             delete params.attr_ids | ||||
|             delete params.tableCategory | ||||
|             await putCustomDashboard(this.item.id, params) | ||||
|           } else { | ||||
|             const { xLast, yLast, wLast } = getLastLayout(this.layout()) | ||||
|             const w = 3 | ||||
|             const w = this.width | ||||
|             const x = xLast + wLast + w > 12 ? 0 : xLast + wLast | ||||
|             const y = xLast + wLast + w > 12 ? yLast + 1 : yLast | ||||
|             await postCustomDashboard({ | ||||
|             const params = { | ||||
|               ...this.form, | ||||
|               options: { | ||||
|                 x, | ||||
| @@ -169,23 +537,216 @@ export default { | ||||
|                 h: this.form.category === 0 ? 3 : 5, | ||||
|                 name, | ||||
|                 chartType: this.chartType, | ||||
|                 fontConfig, | ||||
|                 showIcon: this.form.showIcon, | ||||
|                 type_ids: this.form.type_ids, | ||||
|                 filter: this.filterExp, | ||||
|                 isShadow: this.isShadow, | ||||
|               }, | ||||
|             }) | ||||
|             } | ||||
|             if (chartType === 'count') { | ||||
|               params.options.fontColor = fontColor | ||||
|               params.options.bgColor = bgColor | ||||
|             } | ||||
|             if (['bar', 'line', 'pie'].includes(chartType)) { | ||||
|               if (this.form.category === 1) { | ||||
|                 params.options.attr_ids = this.form.attr_ids | ||||
|               } | ||||
|               params.options.chartColor = this.chartColor | ||||
|             } | ||||
|             if (chartType === 'bar') { | ||||
|               params.options.barDirection = this.barDirection | ||||
|               params.options.barStack = this.barStack | ||||
|             } | ||||
|             if (chartType === 'table') { | ||||
|               params.options.attr_ids = this.form.attr_ids | ||||
|               if (this.form.tableCategory === 2) { | ||||
|                 params.options.ret = 'cis' | ||||
|               } | ||||
|             } | ||||
|             delete params.showIcon | ||||
|             delete params.type_ids | ||||
|             delete params.attr_ids | ||||
|             delete params.tableCategory | ||||
|             await postCustomDashboard(params) | ||||
|           } | ||||
|           this.handleclose() | ||||
|           this.$emit('refresh') | ||||
|         } | ||||
|       }) | ||||
|     }, | ||||
|     changeDashboardCategory(value) { | ||||
|       this.$refs.chartForm.clearValidate() | ||||
|       if (value === 1 && this.form.type_id) { | ||||
|         this.changeCIType(this.form.type_id) | ||||
|     // changeDashboardCategory(value) { | ||||
|     //   this.$refs.chartForm.clearValidate() | ||||
|     //   if (value === 1 && this.form.type_id) { | ||||
|     //     this.changeCIType(this.form.type_id) | ||||
|     //   } | ||||
|     // }, | ||||
|     changeChartType(t) { | ||||
|       this.chartType = t.value | ||||
|       this.isShowPreview = false | ||||
|       if (t.value === 'count') { | ||||
|         this.form.category = 0 | ||||
|       } else { | ||||
|         this.form.category = 1 | ||||
|       } | ||||
|       this.resetForm() | ||||
|     }, | ||||
|     showPreview() { | ||||
|       this.$refs.chartForm.validate(async (valid) => { | ||||
|         if (valid) { | ||||
|           this.isShowPreview = false | ||||
|           const name = this.form.name | ||||
|           const { chartType, fontColor, bgColor } = this | ||||
|           this.$refs.filterComp.handleSubmit() | ||||
|           const params = { | ||||
|             ...this.form, | ||||
|             options: { | ||||
|               name, | ||||
|               chartType, | ||||
|               showIcon: this.form.showIcon, | ||||
|               type_ids: this.form.type_ids, | ||||
|               filter: this.filterExp, | ||||
|               isShadow: this.isShadow, | ||||
|             }, | ||||
|           } | ||||
|           if (chartType === 'count') { | ||||
|             params.options.fontColor = fontColor | ||||
|             params.options.bgColor = bgColor | ||||
|           } | ||||
|           if (['bar', 'line', 'pie'].includes(chartType)) { | ||||
|             if (this.form.category === 1) { | ||||
|               params.options.attr_ids = this.form.attr_ids | ||||
|             } | ||||
|             params.options.chartColor = this.chartColor | ||||
|           } | ||||
|           if (chartType === 'bar') { | ||||
|             params.options.barDirection = this.barDirection | ||||
|             params.options.barStack = this.barStack | ||||
|           } | ||||
|           if (chartType === 'table') { | ||||
|             params.options.attr_ids = this.form.attr_ids | ||||
|             if (this.form.tableCategory === 2) { | ||||
|               params.options.ret = 'cis' | ||||
|             } | ||||
|           } | ||||
|           delete params.showIcon | ||||
|           delete params.type_ids | ||||
|           delete params.attr_ids | ||||
|           delete params.tableCategory | ||||
|           postCustomDashboardPreview(params).then((res) => { | ||||
|             this.isShowPreview = true | ||||
|             this.previewData = res.counter | ||||
|           }) | ||||
|         } | ||||
|       }) | ||||
|     }, | ||||
|     setExpFromFilter(filterExp) { | ||||
|       if (filterExp) { | ||||
|         this.filterExp = `${filterExp}` | ||||
|       } else { | ||||
|         this.filterExp = undefined | ||||
|       } | ||||
|     }, | ||||
|     resetForm() { | ||||
|       this.form.type_id = undefined | ||||
|       this.form.type_ids = [] | ||||
|       this.form.attr_ids = [] | ||||
|       this.$refs.chartForm.clearValidate() | ||||
|     }, | ||||
|     changeAttr(value) { | ||||
|       if (value && value.length) { | ||||
|         if (['line', 'pie'].includes(this.chartType)) { | ||||
|           this.form.attr_ids = [value[value.length - 1]] | ||||
|         } | ||||
|         if (['bar'].includes(this.chartType) && value.length > 2) { | ||||
|           this.form.attr_ids = value.slice(value.length - 2, value.length) | ||||
|         } | ||||
|         if (['table'].includes(this.chartType) && value.length > 3) { | ||||
|           this.form.attr_ids = value.slice(value.length - 3, value.length) | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     clickLevel2children(e, citype, level) { | ||||
|       if (this.form.level !== level) { | ||||
|         this.$nextTick(() => { | ||||
|           this.form.type_ids = [citype.id] | ||||
|         }) | ||||
|       } | ||||
|       this.form.level = level | ||||
|     }, | ||||
|   }, | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style></style> | ||||
| <style lang="less" scoped> | ||||
| .chart-wrapper { | ||||
|   display: flex; | ||||
|   .chart-left { | ||||
|     width: 50%; | ||||
|     .chart-left-preview { | ||||
|       border: 1px solid #e4e7ed; | ||||
|       border-radius: 2px; | ||||
|       height: 280px; | ||||
|       width: 92%; | ||||
|       position: relative; | ||||
|       padding: 12px; | ||||
|       .chart-left-preview-operation { | ||||
|         color: #86909c; | ||||
|         position: absolute; | ||||
|         top: 12px; | ||||
|         right: 12px; | ||||
|         cursor: pointer; | ||||
|       } | ||||
|       .chart-left-preview-box { | ||||
|         padding: 6px 12px; | ||||
|         height: 250px; | ||||
|         border-radius: 8px; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   .chart-right { | ||||
|     width: 50%; | ||||
|     h4 { | ||||
|       font-weight: 700; | ||||
|       color: #000; | ||||
|     } | ||||
|     .chart-right-type { | ||||
|       display: flex; | ||||
|       justify-content: space-between; | ||||
|       background-color: #f0f5ff; | ||||
|       padding: 6px 12px; | ||||
|       .chart-right-type-box { | ||||
|         cursor: pointer; | ||||
|         width: 70px; | ||||
|         height: 60px; | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         align-items: center; | ||||
|         justify-content: center; | ||||
|         > i { | ||||
|           font-size: 32px; | ||||
|         } | ||||
|         > span { | ||||
|           font-size: 12px; | ||||
|         } | ||||
|       } | ||||
|       .chart-right-type-box-selected { | ||||
|         background-color: #e5f1ff; | ||||
|       } | ||||
|     } | ||||
|     .chart-width { | ||||
|       width: 100%; | ||||
|       > label { | ||||
|         width: 25%; | ||||
|         text-align: center; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
| <style lang="less"> | ||||
| .chart-wrapper { | ||||
|   .ant-form-item { | ||||
|     margin-bottom: 0; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -1,23 +1,61 @@ | ||||
| export const colorList = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc'] | ||||
|  | ||||
| export const category_1_bar_options = (data) => { | ||||
| export const category_1_bar_options = (data, options) => { | ||||
|     // 计算一级分类 | ||||
|     const xData = Object.keys(data) | ||||
|     // 计算共有多少二级分类 | ||||
|     const secondCategory = {} | ||||
|     Object.keys(data).forEach(key => { | ||||
|         if (Object.prototype.toString.call(data[key]) === '[object Object]') { | ||||
|             Object.keys(data[key]).forEach(key1 => { | ||||
|                 secondCategory[key1] = Array.from({ length: xData.length }).fill(0) | ||||
|             }) | ||||
|         } else { | ||||
|             secondCategory['其他'] = Array.from({ length: xData.length }).fill(0) | ||||
|         } | ||||
|     }) | ||||
|     Object.keys(secondCategory).forEach(key => { | ||||
|         xData.forEach((x, idx) => { | ||||
|             if (data[x][key]) { | ||||
|                 secondCategory[key][idx] = data[x][key] | ||||
|             } | ||||
|             if (typeof data[x] === 'number') { | ||||
|                 secondCategory['其他'][idx] = data[x] | ||||
|             } | ||||
|         }) | ||||
|     }) | ||||
|     return { | ||||
|         color: options.chartColor.split(','), | ||||
|         grid: { | ||||
|             top: 15, | ||||
|             left: 'left', | ||||
|             right: 0, | ||||
|             bottom: 0, | ||||
|             right: 10, | ||||
|             bottom: 20, | ||||
|             containLabel: true, | ||||
|         }, | ||||
|         xAxis: { | ||||
|             type: 'category', | ||||
|             data: Object.keys(data) | ||||
|         legend: { | ||||
|             data: Object.keys(secondCategory), | ||||
|             bottom: 0, | ||||
|             type: 'scroll', | ||||
|         }, | ||||
|         yAxis: { | ||||
|         xAxis: options.barDirection === 'y' ? { | ||||
|             type: 'category', | ||||
|             axisTick: { show: false }, | ||||
|             data: xData | ||||
|         } | ||||
|             : { | ||||
|                 type: 'value', | ||||
|                 splitLine: { | ||||
|                     show: false | ||||
|                 } | ||||
|             }, | ||||
|         yAxis: options.barDirection === 'y' ? { | ||||
|             type: 'value', | ||||
|             splitLine: { | ||||
|                 show: false | ||||
|             } | ||||
|         } : { | ||||
|             type: 'category', | ||||
|             axisTick: { show: false }, | ||||
|             data: xData | ||||
|         }, | ||||
|         tooltip: { | ||||
|             trigger: 'axis', | ||||
| @@ -25,34 +63,76 @@ export const category_1_bar_options = (data) => { | ||||
|                 type: 'shadow' | ||||
|             } | ||||
|         }, | ||||
|         series: Object.keys(secondCategory).map(key => { | ||||
|             return { | ||||
|                 name: key, | ||||
|                 type: 'bar', | ||||
|                 stack: options?.barStack ?? 'total', | ||||
|                 barGap: 0, | ||||
|                 emphasis: { | ||||
|                     focus: 'series' | ||||
|                 }, | ||||
|                 data: secondCategory[key] | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| export const category_1_line_options = (data, options) => { | ||||
|     const xData = Object.keys(data) | ||||
|     return { | ||||
|         color: options.chartColor.split(','), | ||||
|         grid: { | ||||
|             top: 15, | ||||
|             left: 'left', | ||||
|             right: 10, | ||||
|             bottom: 20, | ||||
|             containLabel: true, | ||||
|         }, | ||||
|         tooltip: { | ||||
|             trigger: 'axis' | ||||
|         }, | ||||
|         xAxis: { | ||||
|             type: 'category', | ||||
|             data: xData | ||||
|         }, | ||||
|         yAxis: { | ||||
|             type: 'value' | ||||
|         }, | ||||
|         series: [ | ||||
|             { | ||||
|                 data: Object.keys(data).map((key, index) => { | ||||
|                     return { | ||||
|                         value: data[key], | ||||
|                         itemStyle: { color: colorList[0] } | ||||
|                 data: xData.map(item => data[item]), | ||||
|                 type: 'line', | ||||
|                 smooth: true, | ||||
|                 showSymbol: false, | ||||
|                 areaStyle: options?.isShadow ? { | ||||
|                     opacity: 0.5, | ||||
|                     color: { | ||||
|                         type: 'linear', | ||||
|                         x: 0, | ||||
|                         y: 0, | ||||
|                         x2: 0, | ||||
|                         y2: 1, | ||||
|                         colorStops: [{ | ||||
|                             offset: 0, color: options.chartColor.split(',')[0] // 0% 处的颜色 | ||||
|                         }, { | ||||
|                             offset: 1, color: '#ffffff' // 100% 处的颜色 | ||||
|                         }], | ||||
|                         global: false // 缺省为 false | ||||
|                     } | ||||
|                 }), | ||||
|                 type: 'bar', | ||||
|                 label: { | ||||
|                     show: true, | ||||
|                     position: 'top', | ||||
|                     fontSize: 10, | ||||
|                     formatter(data) { | ||||
|                         return `${data.value || ''}` | ||||
|                     } | ||||
|                 }, | ||||
|                 } : null | ||||
|             } | ||||
|         ] | ||||
|     } | ||||
| } | ||||
|  | ||||
| export const category_1_pie_options = (data) => { | ||||
| export const category_1_pie_options = (data, options) => { | ||||
|     return { | ||||
|         color: options.chartColor.split(','), | ||||
|         grid: { | ||||
|             top: 10, | ||||
|             left: 'left', | ||||
|             right: 0, | ||||
|             right: 10, | ||||
|             bottom: 0, | ||||
|             containLabel: true, | ||||
|         }, | ||||
| @@ -89,7 +169,7 @@ export const category_1_pie_options = (data) => { | ||||
|     } | ||||
| } | ||||
|  | ||||
| export const category_2_bar_options = (data) => { | ||||
| export const category_2_bar_options = (data, options, chartType) => { | ||||
|     const xAxisData = Object.keys(data.detail) | ||||
|     const _legend = [] | ||||
|     xAxisData.forEach(key => { | ||||
| @@ -97,10 +177,11 @@ export const category_2_bar_options = (data) => { | ||||
|     }) | ||||
|     const legend = [...new Set(_legend)] | ||||
|     return { | ||||
|         color: options.chartColor.split(','), | ||||
|         grid: { | ||||
|             top: 15, | ||||
|             left: 'left', | ||||
|             right: 0, | ||||
|             right: 10, | ||||
|             bottom: 20, | ||||
|             containLabel: true, | ||||
|         }, | ||||
| @@ -116,41 +197,110 @@ export const category_2_bar_options = (data) => { | ||||
|             type: 'scroll', | ||||
|             data: legend | ||||
|         }, | ||||
|         xAxis: [ | ||||
|             { | ||||
|                 type: 'category', | ||||
|                 axisTick: { show: false }, | ||||
|                 data: xAxisData | ||||
|             } | ||||
|         ], | ||||
|         yAxis: [ | ||||
|             { | ||||
|         xAxis: options.barDirection === 'y' || chartType === 'line' ? { | ||||
|             type: 'category', | ||||
|             axisTick: { show: false }, | ||||
|             data: xAxisData | ||||
|         } | ||||
|             : { | ||||
|                 type: 'value', | ||||
|                 splitLine: { | ||||
|                     show: false | ||||
|                 } | ||||
|             }, | ||||
|         yAxis: options.barDirection === 'y' || chartType === 'line' ? { | ||||
|             type: 'value', | ||||
|             splitLine: { | ||||
|                 show: false | ||||
|             } | ||||
|         ], | ||||
|         series: legend.map(le => { | ||||
|         } : { | ||||
|             type: 'category', | ||||
|             axisTick: { show: false }, | ||||
|             data: xAxisData | ||||
|         }, | ||||
|         series: legend.map((le, index) => { | ||||
|             return { | ||||
|                 name: le, | ||||
|                 type: 'bar', | ||||
|                 type: chartType, | ||||
|                 barGap: 0, | ||||
|                 emphasis: { | ||||
|                     focus: 'series' | ||||
|                 }, | ||||
|                 stack: chartType === 'line' ? '' : options?.barStack ?? 'total', | ||||
|                 data: xAxisData.map(x => { | ||||
|                     return data.detail[x][le] || 0 | ||||
|                 }), | ||||
|                 smooth: true, | ||||
|                 showSymbol: false, | ||||
|                 label: { | ||||
|                     show: true, | ||||
|                     position: 'top', | ||||
|                     fontSize: 10, | ||||
|                     formatter(data) { | ||||
|                         return `${data.value || ''}` | ||||
|                     } | ||||
|                     show: false, | ||||
|                 }, | ||||
|                 areaStyle: chartType === 'line' && options?.isShadow ? { | ||||
|                     opacity: 0.5, | ||||
|                     color: { | ||||
|                         type: 'linear', | ||||
|                         x: 0, | ||||
|                         y: 0, | ||||
|                         x2: 0, | ||||
|                         y2: 1, | ||||
|                         colorStops: [{ | ||||
|                             offset: 0, color: options.chartColor.split(',')[index % 8] // 0% 处的颜色 | ||||
|                         }, { | ||||
|                             offset: 1, color: '#ffffff' // 100% 处的颜色 | ||||
|                         }], | ||||
|                         global: false // 缺省为 false | ||||
|                     } | ||||
|                 } : null | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| export const category_2_pie_options = (data, options) => { | ||||
|     console.log(1111, options) | ||||
|     const _legend = [] | ||||
|     Object.keys(data.detail).forEach(key => { | ||||
|         Object.keys(data.detail[key]).forEach(key2 => { | ||||
|             _legend.push({ value: data.detail[key][key2], name: `${key}-${key2}` }) | ||||
|         }) | ||||
|     }) | ||||
|     return { | ||||
|         color: options.chartColor.split(','), | ||||
|         grid: { | ||||
|             top: 15, | ||||
|             left: 'left', | ||||
|             right: 10, | ||||
|             bottom: 20, | ||||
|             containLabel: true, | ||||
|         }, | ||||
|         tooltip: { | ||||
|             trigger: 'item' | ||||
|         }, | ||||
|         legend: { | ||||
|             orient: 'vertical', | ||||
|             left: 'left', | ||||
|             type: 'scroll', | ||||
|             formatter: function (name) { | ||||
|                 const _find = _legend.find(item => item.name === name) | ||||
|                 return `${name}:${_find.value}` | ||||
|             } | ||||
|         }, | ||||
|         series: [ | ||||
|             { | ||||
|                 type: 'pie', | ||||
|                 radius: '90%', | ||||
|                 data: _legend, | ||||
|                 label: { | ||||
|                     show: false, | ||||
|                 }, | ||||
|                 emphasis: { | ||||
|                     itemStyle: { | ||||
|                         shadowBlur: 10, | ||||
|                         shadowOffsetX: 0, | ||||
|                         shadowColor: 'rgba(0, 0, 0, 0.5)' | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         ] | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,54 @@ | ||||
| <template> | ||||
|   <a-select v-model="currenColor"> | ||||
|     <a-select-option v-for="i in list" :value="i" :key="i"> | ||||
|       <div> | ||||
|         <span :style="{ backgroundColor: color }" class="color-box" v-for="color in i.split(',')" :key="color"></span> | ||||
|       </div> | ||||
|     </a-select-option> | ||||
|   </a-select> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|   name: 'ColorListPicker', | ||||
|   model: { | ||||
|     prop: 'value', | ||||
|     event: 'change', | ||||
|   }, | ||||
|   props: { | ||||
|     value: { | ||||
|       type: [String, Array], | ||||
|       default: null, | ||||
|     }, | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       list: [ | ||||
|         '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD', | ||||
|         '#C1A9DC,#E2B5CD,#EE8EBC,#8483C3,#4D66BD,#213764,#D9B6E9,#DD88EB', | ||||
|         '#6FC4DF,#9FE8CE,#16B4BE,#86E6FB,#1871A3,#E1BF8D,#ED8D8D,#DD88EB', | ||||
|         '#F8B751,#FC9054,#FFE380,#DF963F,#AB5200,#EA9387,#FFBB7C,#D27467', | ||||
|       ], | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     currenColor: { | ||||
|       get() { | ||||
|         return this.value | ||||
|       }, | ||||
|       set(val) { | ||||
|         this.$emit('change', val) | ||||
|         return val | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="less" scoped> | ||||
| .color-box { | ||||
|   display: inline-block; | ||||
|   width: 40px; | ||||
|   height: 10px; | ||||
| } | ||||
| </style> | ||||
| @@ -0,0 +1,79 @@ | ||||
| <template> | ||||
|   <div class="color-picker"> | ||||
|     <div | ||||
|       :style="{ | ||||
|         background: Array.isArray(item) ? `linear-gradient(to bottom, ${item[0]} 0%, ${item[1]} 100%)` : item, | ||||
|       }" | ||||
|       :class="{ 'color-picker-box': true, 'color-picker-box-selected': isEqual(currenColor, item) }" | ||||
|       v-for="item in colorList" | ||||
|       :key="Array.isArray(item) ? item.join() : item" | ||||
|       @click="changeColor(item)" | ||||
|     ></div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import _ from 'lodash' | ||||
| export default { | ||||
|   name: 'ColorPicker', | ||||
|   model: { | ||||
|     prop: 'value', | ||||
|     event: 'change', | ||||
|   }, | ||||
|   props: { | ||||
|     value: { | ||||
|       type: [String, Array], | ||||
|       default: null, | ||||
|     }, | ||||
|     colorList: { | ||||
|       type: Array, | ||||
|       default: () => [], | ||||
|     }, | ||||
|   }, | ||||
|   computed: { | ||||
|     currenColor: { | ||||
|       get() { | ||||
|         return this.value | ||||
|       }, | ||||
|       set(val) { | ||||
|         this.$emit('change', val) | ||||
|         return val | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|     isEqual: _.isEqual, | ||||
|     changeColor(item) { | ||||
|       this.$emit('change', item) | ||||
|     }, | ||||
|   }, | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="less" scoped> | ||||
| .color-picker { | ||||
|   display: flex; | ||||
|   justify-content: space-between; | ||||
|   align-items: center; | ||||
|   margin-top: 10px; | ||||
|   .color-picker-box { | ||||
|     width: 19px; | ||||
|     height: 19px; | ||||
|     border: 1px solid #dae2e7; | ||||
|     border-radius: 1px; | ||||
|     cursor: pointer; | ||||
|   } | ||||
|   .color-picker-box-selected { | ||||
|     position: relative; | ||||
|     &:after { | ||||
|       content: ''; | ||||
|       position: absolute; | ||||
|       width: 24px; | ||||
|       height: 24px; | ||||
|       border: 1px solid #43bbff; | ||||
|       top: -3px; | ||||
|       left: -3px; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
| @@ -1,5 +1,4 @@ | ||||
| export const dashboardCategory = { | ||||
|     0: { label: 'CI数统计' }, | ||||
|     1: { label: '按属性值分类统计' }, | ||||
|     2: { label: '关系统计' } | ||||
|     1: { label: '默认' }, | ||||
|     2: { label: '关系' } | ||||
| } | ||||
|   | ||||
| @@ -11,8 +11,8 @@ | ||||
|     <template v-if="layout && layout.length"> | ||||
|       <div v-if="editable"> | ||||
|         <a-button | ||||
|           :style="{ marginLeft: '10px' }" | ||||
|           @click="openChartForm('add', {})" | ||||
|           :style="{ marginLeft: '22px', marginTop: '20px' }" | ||||
|           @click="openChartForm('add', { options: { w: 3 } })" | ||||
|           ghost | ||||
|           type="primary" | ||||
|           size="small" | ||||
| @@ -39,11 +39,44 @@ | ||||
|           :h="item.h" | ||||
|           :i="item.i" | ||||
|           :key="item.i" | ||||
|           :style="{ backgroundColor: '#fafafa' }" | ||||
|           :style="{ | ||||
|             background: | ||||
|               item.options.chartType === 'count' | ||||
|                 ? Array.isArray(item.options.bgColor) | ||||
|                   ? `linear-gradient(to bottom, ${item.options.bgColor[0]} 0%, ${item.options.bgColor[1]} 100%)` | ||||
|                   : item.options.bgColor | ||||
|                 : '#fafafa', | ||||
|           }" | ||||
|         > | ||||
|           <CardTitle>{{ item.options.name }}</CardTitle> | ||||
|           <div class="cmdb-dashboard-grid-item-title"> | ||||
|             <template v-if="item.options.chartType !== 'count' && item.options.showIcon && getCiType(item)"> | ||||
|               <template v-if="getCiType(item).icon"> | ||||
|                 <img | ||||
|                   v-if="getCiType(item).icon.split('$$')[2]" | ||||
|                   :src="`/api/common-setting/v1/file/${getCiType(item).icon.split('$$')[3]}`" | ||||
|                 /> | ||||
|                 <ops-icon | ||||
|                   v-else | ||||
|                   :style="{ | ||||
|                     color: getCiType(item).icon.split('$$')[1], | ||||
|                   }" | ||||
|                   :type="getCiType(item).icon.split('$$')[0]" | ||||
|                 /> | ||||
|               </template> | ||||
|               <span :style="{ color: '#2f54eb' }" v-else>{{ getCiType(item).name[0].toUpperCase() }}</span> | ||||
|             </template> | ||||
|             <span :style="{ color: item.options.chartType === 'count' ? item.options.fontColor : '#000' }">{{ | ||||
|               item.options.name | ||||
|             }}</span> | ||||
|           </div> | ||||
|           <a-dropdown v-if="editable"> | ||||
|             <a class="cmdb-dashboard-grid-item-operation"><a-icon type="menu"></a-icon></a> | ||||
|             <a | ||||
|               class="cmdb-dashboard-grid-item-operation" | ||||
|               :style="{ | ||||
|                 color: item.options.chartType === 'count' ? item.options.fontColor : '', | ||||
|               }" | ||||
|             ><a-icon type="menu"></a-icon | ||||
|             ></a> | ||||
|             <a-menu slot="overlay"> | ||||
|               <a-menu-item> | ||||
|                 <a @click="() => openChartForm('edit', item)"><a-icon style="margin-right:5px" type="edit" />编辑</a> | ||||
| @@ -53,13 +86,13 @@ | ||||
|               </a-menu-item> | ||||
|             </a-menu> | ||||
|           </a-dropdown> | ||||
|           <a | ||||
|           <!-- <a | ||||
|             v-if="editable && item.category === 1" | ||||
|             class="cmdb-dashboard-grid-item-chart-type" | ||||
|             @click="changeChartType(item)" | ||||
|           ><a-icon | ||||
|             :type="item.options.chartType === 'bar' ? 'bar-chart' : 'pie-chart'" | ||||
|           /></a> | ||||
|           /></a> --> | ||||
|           <Chart | ||||
|             :ref="`chart_${item.id}`" | ||||
|             :chartId="item.id" | ||||
| @@ -67,18 +100,26 @@ | ||||
|             :category="item.category" | ||||
|             :options="item.options" | ||||
|             :editable="editable" | ||||
|             :ci_types="ci_types" | ||||
|             :type_id="item.type_id" | ||||
|           /> | ||||
|         </GridItem> | ||||
|       </GridLayout> | ||||
|     </template> | ||||
|     <div v-else class="dashboard-empty"> | ||||
|       <a-empty :image="emptyImage" description=""></a-empty> | ||||
|       <a-button @click="openChartForm('add', {})" v-if="editable" size="small" type="primary" icon="plus"> | ||||
|       <a-button | ||||
|         @click="openChartForm('add', { options: { w: 3 } })" | ||||
|         v-if="editable" | ||||
|         size="small" | ||||
|         type="primary" | ||||
|         icon="plus" | ||||
|       > | ||||
|         定制仪表盘 | ||||
|       </a-button> | ||||
|       <span v-else>管理员暂未定制仪表盘</span> | ||||
|     </div> | ||||
|     <ChartForm ref="chartForm" @refresh="refresh" :ci_types="ci_types" /> | ||||
|     <ChartForm ref="chartForm" @refresh="refresh" :ci_types="ci_types" :totalData="totalData" /> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| @@ -127,12 +168,14 @@ export default { | ||||
|       }, | ||||
|     } | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.getLayout() | ||||
|   created() { | ||||
|     getCITypes().then((res) => { | ||||
|       this.ci_types = res.ci_types | ||||
|     }) | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.getLayout() | ||||
|   }, | ||||
|   methods: { | ||||
|     async getLayout() { | ||||
|       const res = await getCustomDashboard() | ||||
| @@ -196,6 +239,13 @@ export default { | ||||
|         }) | ||||
|       } | ||||
|     }, | ||||
|     getCiType(item) { | ||||
|       if (item.type_id || item.options?.type_ids) { | ||||
|         const _find = this.ci_types.find((type) => type.id === item.type_id || type.id === item.options?.type_ids[0]) | ||||
|         return _find || null | ||||
|       } | ||||
|       return null | ||||
|     }, | ||||
|   }, | ||||
| } | ||||
| </script> | ||||
| @@ -206,15 +256,18 @@ export default { | ||||
|   text-align: center; | ||||
| } | ||||
| .cmdb-dashboard-grid-item { | ||||
|   border-radius: 15px; | ||||
|   border-radius: 8px; | ||||
|   padding: 6px 12px; | ||||
|   .cmdb-dashboard-grid-item-title { | ||||
|     overflow: hidden; | ||||
|     text-overflow: ellipsis; | ||||
|     white-space: nowrap; | ||||
|     font-weight: 700; | ||||
|     padding-left: 6px; | ||||
|     color: #000000bd; | ||||
|     color: #000000; | ||||
|   } | ||||
|   .cmdb-dashboard-grid-item-operation { | ||||
|     position: absolute; | ||||
|     right: 6px; | ||||
|     right: 12px; | ||||
|     top: 6px; | ||||
|   } | ||||
|   .cmdb-dashboard-grid-item-chart-type { | ||||
| @@ -224,3 +277,26 @@ export default { | ||||
|   } | ||||
| } | ||||
| </style> | ||||
|  | ||||
| <style lang="less"> | ||||
| .cmdb-dashboard-grid-item-title { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   > i { | ||||
|     font-size: 16px; | ||||
|     margin-right: 5px; | ||||
|   } | ||||
|   > img { | ||||
|     width: 16px; | ||||
|     margin-right: 5px; | ||||
|   } | ||||
|   > span:not(:last-child) { | ||||
|     display: inline-block; | ||||
|     width: 16px; | ||||
|     height: 16px; | ||||
|     font-size: 16px; | ||||
|     text-align: center; | ||||
|     margin-right: 5px; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
|   | ||||
| @@ -30,7 +30,7 @@ services: | ||||
|           - redis | ||||
|  | ||||
|   cmdb-api: | ||||
|     image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.3.2 | ||||
|     image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.3.3 | ||||
| #    build: | ||||
| #      context: . | ||||
| #      target: cmdb-api | ||||
| @@ -61,7 +61,7 @@ services: | ||||
|           - cmdb-api | ||||
|  | ||||
|   cmdb-ui: | ||||
|     image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.3.2 | ||||
|     image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.3.3 | ||||
| #    build: | ||||
| #      context: . | ||||
| #      target: cmdb-ui | ||||
|   | ||||
		Reference in New Issue
	
	Block a user