mirror of
				https://github.com/veops/cmdb.git
				synced 2025-10-31 19:39:24 +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" | bs4 = ">=0.0.1" | ||||||
| toposort = ">=1.5" | toposort = ">=1.5" | ||||||
| requests = ">=2.22.0" | requests = ">=2.22.0" | ||||||
|  | requests_oauthlib = "==1.3.1" | ||||||
| PyJWT = "==2.4.0" | PyJWT = "==2.4.0" | ||||||
| elasticsearch = "==7.17.9" | elasticsearch = "==7.17.9" | ||||||
| future = "==0.18.3" | future = "==0.18.3" | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ import time | |||||||
| import click | import click | ||||||
| from flask import current_app | from flask import current_app | ||||||
| from flask.cli import with_appcontext | from flask.cli import with_appcontext | ||||||
|  | from flask_login import login_user | ||||||
|  |  | ||||||
| import api.lib.cmdb.ci | import api.lib.cmdb.ci | ||||||
| from api.extensions import db | 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.exception import AbortException | ||||||
| from api.lib.perm.acl.acl import ACLManager | from api.lib.perm.acl.acl import ACLManager | ||||||
| from api.lib.perm.acl.cache import AppCache | 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 ResourceCRUD | ||||||
| from api.lib.perm.acl.resource import ResourceTypeCRUD | from api.lib.perm.acl.resource import ResourceTypeCRUD | ||||||
| from api.lib.perm.acl.role import RoleCRUD | from api.lib.perm.acl.role import RoleCRUD | ||||||
| @@ -207,6 +209,8 @@ def cmdb_counter(): | |||||||
|     """ |     """ | ||||||
|     from api.lib.cmdb.cache import CMDBCounterCache |     from api.lib.cmdb.cache import CMDBCounterCache | ||||||
|  |  | ||||||
|  |     current_app.test_request_context().push() | ||||||
|  |     login_user(UserCache.get('worker')) | ||||||
|     while True: |     while True: | ||||||
|         try: |         try: | ||||||
|             db.session.remove() |             db.session.remove() | ||||||
|   | |||||||
| @@ -161,6 +161,55 @@ class InitDepartment(object): | |||||||
|             info = f"update department acl_rid: {acl_rid}" |             info = f"update department acl_rid: {acl_rid}" | ||||||
|             current_app.logger.info(info) |             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() | @click.command() | ||||||
| @with_appcontext | @with_appcontext | ||||||
| @@ -177,5 +226,7 @@ def init_department(): | |||||||
|     """ |     """ | ||||||
|     Department initialization |     Department initialization | ||||||
|     """ |     """ | ||||||
|     InitDepartment().init() |     cli = InitDepartment() | ||||||
|     InitDepartment().create_acl_role_with_department() |     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'): |         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)) |             return abort(403, ErrFormat.role_required.format(RoleEnum.CONFIG)) | ||||||
|  |  | ||||||
|     @staticmethod |     @classmethod | ||||||
|     def calc_computed_attribute(attr_id): |     def calc_computed_attribute(cls, attr_id): | ||||||
|         """ |         """ | ||||||
|         calculate computed attribute for all ci |         calculate computed attribute for all ci | ||||||
|         :param attr_id: |         :param attr_id: | ||||||
|         :return: |         :return: | ||||||
|         """ |         """ | ||||||
|  |         cls.can_create_computed_attribute() | ||||||
|  |  | ||||||
|         from api.tasks.cmdb import calc_computed_attribute |         from api.tasks.cmdb import calc_computed_attribute | ||||||
|  |  | ||||||
|         calc_computed_attribute.apply_async(args=(attr_id, current_user.uid), queue=CMDB_QUEUE) |         calc_computed_attribute.apply_async(args=(attr_id, current_user.uid), queue=CMDB_QUEUE) | ||||||
|   | |||||||
| @@ -2,14 +2,11 @@ | |||||||
|  |  | ||||||
| from __future__ import unicode_literals | from __future__ import unicode_literals | ||||||
|  |  | ||||||
| import requests |  | ||||||
| from flask import current_app | from flask import current_app | ||||||
|  |  | ||||||
| from api.extensions import cache | from api.extensions import cache | ||||||
| from api.extensions import db |  | ||||||
| from api.lib.cmdb.custom_dashboard import CustomDashboardManager | from api.lib.cmdb.custom_dashboard import CustomDashboardManager | ||||||
| from api.models.cmdb import Attribute | from api.models.cmdb import Attribute | ||||||
| from api.models.cmdb import CI |  | ||||||
| from api.models.cmdb import CIType | from api.models.cmdb import CIType | ||||||
| from api.models.cmdb import CITypeAttribute | from api.models.cmdb import CITypeAttribute | ||||||
| from api.models.cmdb import RelationType | from api.models.cmdb import RelationType | ||||||
| @@ -210,7 +207,6 @@ class CITypeAttributeCache(object): | |||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def get(cls, type_id, attr_id): |     def get(cls, type_id, attr_id): | ||||||
|  |  | ||||||
|         attr = cache.get(cls.PREFIX_ID.format(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 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) |         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 = {} |         result = {} | ||||||
|         for custom in customs: |         for custom in customs: | ||||||
|             if custom['category'] == 0: |             if custom['category'] == 0: | ||||||
|                 result[custom['id']] = cls.summary_counter(custom['type_id']) |                 res = cls.sum_counter(custom) | ||||||
|             elif custom['category'] == 1: |             elif custom['category'] == 1: | ||||||
|                 result[custom['id']] = cls.attribute_counter(custom['type_id'], custom['attr_id']) |                 res = cls.attribute_counter(custom) | ||||||
|             elif custom['category'] == 2: |             else: | ||||||
|                 result[custom['id']] = cls.relation_counter(custom['type_id'], custom['level']) |                 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) |         cls.set(result) | ||||||
|  |  | ||||||
|         return result |         return result | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def update(cls, custom): |     def update(cls, custom, flush=True): | ||||||
|         result = cache.get(cls.KEY) or {} |         result = cache.get(cls.KEY) or {} | ||||||
|         if not result: |         if not result: | ||||||
|             result = cls.reset() |             result = cls.reset() | ||||||
|  |  | ||||||
|         if custom['category'] == 0: |         if custom['category'] == 0: | ||||||
|             result[custom['id']] = cls.summary_counter(custom['type_id']) |             res = cls.sum_counter(custom) | ||||||
|         elif custom['category'] == 1: |         elif custom['category'] == 1: | ||||||
|             result[custom['id']] = cls.attribute_counter(custom['type_id'], custom['attr_id']) |             res = cls.attribute_counter(custom) | ||||||
|         elif custom['category'] == 2: |         else: | ||||||
|             result[custom['id']] = cls.relation_counter(custom['type_id'], custom['level']) |             res = cls.relation_counter(custom.get('type_id'), | ||||||
|  |                                        custom.get('level'), | ||||||
|  |                                        custom.get('options', {}).get('filter', ''), | ||||||
|  |                                        custom.get('options', {}).get('type_ids', '')) | ||||||
|  |  | ||||||
|  |         if res and flush: | ||||||
|  |             result[custom['id']] = res | ||||||
|             cls.set(result) |             cls.set(result) | ||||||
|  |  | ||||||
|     @staticmethod |         return res | ||||||
|     def summary_counter(type_id): |  | ||||||
|         return db.session.query(CI.id).filter(CI.deleted.is_(False)).filter(CI.type_id == type_id).count() |  | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def relation_counter(type_id, level): |     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 | ||||||
|  |  | ||||||
|         uri = current_app.config.get('CMDB_API') |         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 | ||||||
|  |  | ||||||
|         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] |         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( |         s = RelSearch([i[0] for i in type_id_names], level, other_filer or '') | ||||||
|             uri, ','.join([i[0] for i in type_id_names]), level) |         try: | ||||||
|         stats = requests.get(url).json() |             stats = s.statistics(type_ids) | ||||||
|  |         except SearchError as e: | ||||||
|  |             current_app.logger.error(e) | ||||||
|  |             return | ||||||
|  |  | ||||||
|         id2name = dict(type_id_names) |         id2name = dict(type_id_names) | ||||||
|         type_ids = set() |         type_ids = set() | ||||||
|         for i in (stats.get('detail') or []): |         for i in (stats.get('detail') or []): | ||||||
|             for j in stats['detail'][i]: |             for j in stats['detail'][i]: | ||||||
|                 type_ids.add(j) |                 type_ids.add(j) | ||||||
|  |  | ||||||
|         for type_id in type_ids: |         for type_id in type_ids: | ||||||
|             _type = CITypeCache.get(type_id) |             _type = CITypeCache.get(type_id) | ||||||
|             id2name[type_id] = _type and _type.alias |             id2name[type_id] = _type and _type.alias | ||||||
| @@ -317,9 +332,100 @@ class CMDBCounterCache(object): | |||||||
|         return result |         return result | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def attribute_counter(type_id, attr_id): |     def attribute_counter(custom): | ||||||
|         uri = current_app.config.get('CMDB_API') |         from api.lib.cmdb.search import SearchError | ||||||
|         url = "{}/ci/s?q=_type:{}&fl={}&facet={}".format(uri, type_id, attr_id, attr_id) |         from api.lib.cmdb.search.ci import search | ||||||
|         res = requests.get(url).json() |         from api.lib.cmdb.utils import ValueTypeMap | ||||||
|         if res.get('facet'): |  | ||||||
|             return dict([i[:2] for i in list(res.get('facet').values())[0]]) |         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 copy | ||||||
| import datetime | import datetime | ||||||
| import json | import json | ||||||
|  | import threading | ||||||
|  |  | ||||||
| from flask import abort | from flask import abort | ||||||
| from flask import current_app | 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 ConstraintEnum | ||||||
| from api.lib.cmdb.const import ExistPolicy | from api.lib.cmdb.const import ExistPolicy | ||||||
| from api.lib.cmdb.const import OperateType | 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 REDIS_PREFIX_CI | ||||||
|  | from api.lib.cmdb.const import ResourceTypeEnum | ||||||
| from api.lib.cmdb.const import RetKey | from api.lib.cmdb.const import RetKey | ||||||
| from api.lib.cmdb.history import AttributeHistoryManger | from api.lib.cmdb.history import AttributeHistoryManger | ||||||
| from api.lib.cmdb.history import CIRelationHistoryManager | 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.perms import CIFilterPermsCRUD | ||||||
| from api.lib.cmdb.resp_format import ErrFormat | from api.lib.cmdb.resp_format import ErrFormat | ||||||
| from api.lib.cmdb.utils import TableMap | from api.lib.cmdb.utils import TableMap | ||||||
| from api.lib.cmdb.utils import ValueTypeMap | from api.lib.cmdb.utils import ValueTypeMap | ||||||
| from api.lib.cmdb.value import AttributeValueManager | from api.lib.cmdb.value import AttributeValueManager | ||||||
| from api.lib.decorator import kwargs_required | 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 ACLManager | ||||||
| from api.lib.perm.acl.acl import is_app_admin | from api.lib.perm.acl.acl import is_app_admin | ||||||
| from api.lib.perm.acl.acl import validate_permission | from api.lib.perm.acl.acl import validate_permission | ||||||
| from api.lib.utils import Lock | from api.lib.utils import Lock | ||||||
| from api.lib.utils import handle_arg_list | from api.lib.utils import handle_arg_list | ||||||
|  | from api.lib.webhook import webhook_request | ||||||
|  | from api.models.cmdb import AttributeHistory | ||||||
| from api.models.cmdb import AutoDiscoveryCI | from api.models.cmdb import AutoDiscoveryCI | ||||||
| from api.models.cmdb import CI | from api.models.cmdb import CI | ||||||
| from api.models.cmdb import CIRelation | from api.models.cmdb import CIRelation | ||||||
| from api.models.cmdb import CITypeAttribute | from api.models.cmdb import CITypeAttribute | ||||||
| from api.models.cmdb import CITypeRelation | 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_cache | ||||||
| from api.tasks.cmdb import ci_delete | from api.tasks.cmdb import ci_delete | ||||||
| from api.tasks.cmdb import ci_relation_add | 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, |             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) |                                                       ci_type_attrs_name, ci_type_attrs_alias, ci_attr2type_attr) | ||||||
|  |  | ||||||
|  |             operate_type = OperateType.UPDATE if ci is not None else OperateType.ADD | ||||||
|             try: |             try: | ||||||
|                 ci = ci or CI.create(type_id=ci_type.id, is_auto_discovery=is_auto_discovery) |                 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: |             except BadRequest as e: | ||||||
|                 if existed is None: |                 if existed is None: | ||||||
|                     cls.delete(ci.id) |                     cls.delete(ci.id) | ||||||
|                 raise e |                 raise e | ||||||
|  |  | ||||||
|         if record_id:  # has change |         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 |         if ref_ci_dict:  # add relations | ||||||
|             ci_relation_add.apply_async(args=(ref_ci_dict, ci.id, current_user.uid), queue=CMDB_QUEUE) |             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)) |                         return abort(403, ErrFormat.ci_filter_perm_attr_no_permission.format(k)) | ||||||
|  |  | ||||||
|             try: |             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: |             except BadRequest as e: | ||||||
|                 raise e |                 raise e | ||||||
|  |  | ||||||
|         if record_id:  # has change |         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} |         ref_ci_dict = {k: v for k, v in ci_dict.items() if k.startswith("$") and "." in k} | ||||||
|         if ref_ci_dict: |         if ref_ci_dict: | ||||||
| @@ -442,9 +450,10 @@ class CIManager(object): | |||||||
|     def update_unique_value(ci_id, unique_name, unique_value): |     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))) |         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 |     @classmethod | ||||||
|     def delete(cls, ci_id): |     def delete(cls, ci_id): | ||||||
| @@ -477,9 +486,9 @@ class CIManager(object): | |||||||
|  |  | ||||||
|         db.session.commit() |         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 |         return ci_id | ||||||
|  |  | ||||||
| @@ -896,3 +905,128 @@ class CIRelationManager(object): | |||||||
|             for parent_id in parents: |             for parent_id in parents: | ||||||
|                 for ci_id in ci_ids: |                 for ci_id in ci_ids: | ||||||
|                     cls.delete_2(parent_id, ci_id) |                     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 copy | ||||||
| import datetime | import datetime | ||||||
|  |  | ||||||
|  | import toposort | ||||||
| from flask import abort | from flask import abort | ||||||
| from flask import current_app | from flask import current_app | ||||||
| from flask_login import current_user | from flask_login import current_user | ||||||
|  | from toposort import toposort_flatten | ||||||
|  |  | ||||||
| from api.extensions import db | from api.extensions import db | ||||||
| from api.lib.cmdb.attribute import AttributeManager | from api.lib.cmdb.attribute import AttributeManager | ||||||
| @@ -114,7 +116,7 @@ class CITypeManager(object): | |||||||
|     @kwargs_required("name") |     @kwargs_required("name") | ||||||
|     def add(cls, **kwargs): |     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) |         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"] |         kwargs["alias"] = kwargs["name"] if not kwargs.get("alias") else kwargs["alias"] | ||||||
| @@ -370,6 +372,16 @@ class CITypeAttributeManager(object): | |||||||
|  |  | ||||||
|         return result |         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 |     @staticmethod | ||||||
|     def _check(type_id, attr_ids): |     def _check(type_id, attr_ids): | ||||||
|         ci_type = CITypeManager.check_is_existed(type_id) |         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] |         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 |     @classmethod | ||||||
|     def get_parents(cls, child_id): |     def get_parents(cls, child_id): | ||||||
|         parents = CITypeRelation.get_by(child_id=child_id, to_dict=False) |         parents = CITypeRelation.get_by(child_id=child_id, to_dict=False) | ||||||
| @@ -586,6 +615,17 @@ class CITypeRelationManager(object): | |||||||
|         p = CITypeManager.check_is_existed(parent) |         p = CITypeManager.check_is_existed(parent) | ||||||
|         c = CITypeManager.check_is_existed(child) |         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) |         existed = cls._get(p.id, c.id) | ||||||
|         if existed is not None: |         if existed is not None: | ||||||
|             existed.update(relation_type_id=relation_type_id, |             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): |         for added_id in set(id2obj_dicts.keys()) - set(existed_ids): | ||||||
|             if cls == CIType: |             if cls == CIType: | ||||||
|                 CITypeManager.add(**id2obj_dicts[added_id]) |                 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: |             else: | ||||||
|                 cls.create(flush=True, **id2obj_dicts[added_id]) |                 cls.create(flush=True, **id2obj_dicts[added_id]) | ||||||
|  |  | ||||||
| @@ -1120,16 +1166,18 @@ class CITypeUniqueConstraintManager(object): | |||||||
|  |  | ||||||
| class CITypeTriggerManager(object): | class CITypeTriggerManager(object): | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def get(type_id): |     def get(type_id, to_dict=True): | ||||||
|         return CITypeTrigger.get_by(type_id=type_id, to_dict=True) |         return CITypeTrigger.get_by(type_id=type_id, to_dict=to_dict) | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def add(type_id, attr_id, notify): |     def add(type_id, attr_id, option): | ||||||
|         CITypeTrigger.get_by(type_id=type_id, attr_id=attr_id) and abort(400, ErrFormat.ci_type_trigger_duplicate) |         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, |         CITypeHistoryManager.add(CITypeOperateType.ADD_TRIGGER, | ||||||
|                                  type_id, |                                  type_id, | ||||||
| @@ -1139,12 +1187,12 @@ class CITypeTriggerManager(object): | |||||||
|         return trigger.to_dict() |         return trigger.to_dict() | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def update(_id, notify): |     def update(_id, option): | ||||||
|         existed = (CITypeTrigger.get_by_id(_id) or |         existed = (CITypeTrigger.get_by_id(_id) or | ||||||
|                    abort(404, ErrFormat.ci_type_trigger_not_found.format("id={}".format(_id)))) |                    abort(404, ErrFormat.ci_type_trigger_not_found.format("id={}".format(_id)))) | ||||||
|  |  | ||||||
|         existed2 = existed.to_dict() |         existed2 = existed.to_dict() | ||||||
|         new = existed.update(notify=notify) |         new = existed.update(option=option) | ||||||
|  |  | ||||||
|         CITypeHistoryManager.add(CITypeOperateType.UPDATE_TRIGGER, |         CITypeHistoryManager.add(CITypeOperateType.UPDATE_TRIGGER, | ||||||
|                                  existed.type_id, |                                  existed.type_id, | ||||||
| @@ -1164,35 +1212,3 @@ class CITypeTriggerManager(object): | |||||||
|                                  existed.type_id, |                                  existed.type_id, | ||||||
|                                  trigger_id=_id, |                                  trigger_id=_id, | ||||||
|                                  change=existed.to_dict()) |                                  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(): |     def get(): | ||||||
|         return sorted(CustomDashboard.get_by(to_dict=True), key=lambda x: (x["category"], x['order'])) |         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 |     @staticmethod | ||||||
|     def add(**kwargs): |     def add(**kwargs): | ||||||
|         from api.lib.cmdb.cache import CMDBCounterCache |         from api.lib.cmdb.cache import CMDBCounterCache | ||||||
| @@ -23,9 +31,9 @@ class CustomDashboardManager(object): | |||||||
|  |  | ||||||
|         new = CustomDashboard.create(**kwargs) |         new = CustomDashboard.create(**kwargs) | ||||||
|  |  | ||||||
|         CMDBCounterCache.update(new.to_dict()) |         res = CMDBCounterCache.update(new.to_dict()) | ||||||
|  |  | ||||||
|         return new |         return new, res | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def update(_id, **kwargs): |     def update(_id, **kwargs): | ||||||
| @@ -35,9 +43,9 @@ class CustomDashboardManager(object): | |||||||
|  |  | ||||||
|         new = existed.update(**kwargs) |         new = existed.update(**kwargs) | ||||||
|  |  | ||||||
|         CMDBCounterCache.update(new.to_dict()) |         res = CMDBCounterCache.update(new.to_dict()) | ||||||
|  |  | ||||||
|         return new |         return new, res | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def batch_update(id2options): |     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 Attribute | ||||||
| from api.models.cmdb import AttributeHistory | from api.models.cmdb import AttributeHistory | ||||||
| from api.models.cmdb import CIRelationHistory | from api.models.cmdb import CIRelationHistory | ||||||
|  | from api.models.cmdb import CITriggerHistory | ||||||
| from api.models.cmdb import CITypeHistory | from api.models.cmdb import CITypeHistory | ||||||
| from api.models.cmdb import CITypeTrigger | from api.models.cmdb import CITypeTrigger | ||||||
| from api.models.cmdb import CITypeUniqueConstraint | from api.models.cmdb import CITypeUniqueConstraint | ||||||
| @@ -286,3 +287,67 @@ class CITypeHistoryManager(object): | |||||||
|                            change=change) |                            change=change) | ||||||
|  |  | ||||||
|             CITypeHistory.create(**payload) |             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 = """ | FACET_QUERY = """ | ||||||
|     SELECT {0}.value, |     SELECT {0}.value, | ||||||
|            count({0}.ci_id) |            count(distinct({0}.ci_id)) | ||||||
|     FROM {0} |     FROM {0} | ||||||
|     INNER JOIN ({1}) AS F ON F.ci_id={0}.ci_id |     INNER JOIN ({1}) AS F ON F.ci_id={0}.ci_id | ||||||
|     WHERE {0}.attr_id={2:d} |     WHERE {0}.attr_id={2:d} | ||||||
|   | |||||||
| @@ -141,6 +141,10 @@ class Search(object): | |||||||
|     @staticmethod |     @staticmethod | ||||||
|     def _in_query_handler(attr, v, is_not): |     def _in_query_handler(attr, v, is_not): | ||||||
|         new_v = v[1:-1].split(";") |         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 |         table_name = TableMap(attr=attr).table_name | ||||||
|         in_query = " OR {0}.value ".format(table_name).join(['{0} "{1}"'.format( |         in_query = " OR {0}.value ".format(table_name).join(['{0} "{1}"'.format( | ||||||
|             "NOT LIKE" if is_not else "LIKE", |             "NOT LIKE" if is_not else "LIKE", | ||||||
| @@ -151,6 +155,11 @@ class Search(object): | |||||||
|     @staticmethod |     @staticmethod | ||||||
|     def _range_query_handler(attr, v, is_not): |     def _range_query_handler(attr, v, is_not): | ||||||
|         start, end = [x.strip() for x in v[1:-1].split("_TO_")] |         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 |         table_name = TableMap(attr=attr).table_name | ||||||
|         range_query = "{0} '{1}' AND '{2}'".format( |         range_query = "{0} '{1}' AND '{2}'".format( | ||||||
|             "NOT BETWEEN" if is_not else "BETWEEN", |             "NOT BETWEEN" if is_not else "BETWEEN", | ||||||
| @@ -162,8 +171,14 @@ class Search(object): | |||||||
|     def _comparison_query_handler(attr, v): |     def _comparison_query_handler(attr, v): | ||||||
|         table_name = TableMap(attr=attr).table_name |         table_name = TableMap(attr=attr).table_name | ||||||
|         if v.startswith(">=") or v.startswith("<="): |         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("*", "%")) |             comparison_query = "{0} '{1}'".format(v[:2], v[2:].replace("*", "%")) | ||||||
|         else: |         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("*", "%")) |             comparison_query = "{0} '{1}'".format(v[0], v[1:].replace("*", "%")) | ||||||
|         _query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, comparison_query) |         _query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, comparison_query) | ||||||
|         return _query_sql |         return _query_sql | ||||||
| @@ -295,7 +310,7 @@ class Search(object): | |||||||
|  |  | ||||||
|         start = time.time() |         start = time.time() | ||||||
|         execute = db.session.execute |         execute = db.session.execute | ||||||
|         current_app.logger.debug(v_query_sql) |         # current_app.logger.debug(v_query_sql) | ||||||
|         res = execute(v_query_sql).fetchall() |         res = execute(v_query_sql).fetchall() | ||||||
|         end_time = time.time() |         end_time = time.time() | ||||||
|         current_app.logger.debug("query ci ids time is: {0}".format(end_time - start)) |         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 |             is_not = True if operator == "|~" else False | ||||||
|  |  | ||||||
|  |             if field_type == ValueTypeEnum.DATE and len(v) == 10: | ||||||
|  |                 v = "{} 00:00:00".format(v) | ||||||
|  |  | ||||||
|             # in query |             # in query | ||||||
|             if v.startswith("(") and v.endswith(")"): |             if v.startswith("(") and v.endswith(")"): | ||||||
|                 _query_sql = self._in_query_handler(attr, v, is_not) |                 _query_sql = self._in_query_handler(attr, v, is_not) | ||||||
| @@ -506,7 +524,7 @@ class Search(object): | |||||||
|             if k: |             if k: | ||||||
|                 table_name = TableMap(attr=attr).table_name |                 table_name = TableMap(attr=attr).table_name | ||||||
|                 query_sql = FACET_QUERY.format(table_name, self.query_sql, attr.id) |                 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() |                 result = db.session.execute(query_sql).fetchall() | ||||||
|                 facet[k] = result |                 facet[k] = result | ||||||
|  |  | ||||||
|   | |||||||
| @@ -18,7 +18,6 @@ from api.extensions import db | |||||||
| from api.lib.cmdb.attribute import AttributeManager | from api.lib.cmdb.attribute import AttributeManager | ||||||
| from api.lib.cmdb.cache import AttributeCache | from api.lib.cmdb.cache import AttributeCache | ||||||
| from api.lib.cmdb.cache import CITypeAttributeCache | 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 OperateType | ||||||
| from api.lib.cmdb.const import ValueTypeEnum | from api.lib.cmdb.const import ValueTypeEnum | ||||||
| from api.lib.cmdb.history import AttributeHistoryManger | from api.lib.cmdb.history import AttributeHistoryManger | ||||||
| @@ -140,6 +139,7 @@ class AttributeValueManager(object): | |||||||
|         try: |         try: | ||||||
|             db.session.commit() |             db.session.commit() | ||||||
|         except Exception as e: |         except Exception as e: | ||||||
|  |             db.session.rollback() | ||||||
|             current_app.logger.error("write change failed: {}".format(str(e))) |             current_app.logger.error("write change failed: {}".format(str(e))) | ||||||
|  |  | ||||||
|         return record_id |         return record_id | ||||||
| @@ -235,7 +235,7 @@ class AttributeValueManager(object): | |||||||
|  |  | ||||||
|         return key2attr |         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 |         add or update attribute value, then write history | ||||||
|         :param ci: instance object |         :param ci: instance object | ||||||
| @@ -288,66 +288,6 @@ class AttributeValueManager(object): | |||||||
|  |  | ||||||
|         return self._write_change2(changed) |         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 |     @staticmethod | ||||||
|     def delete_attr_value(attr_id, ci_id): |     def delete_attr_value(attr_id, ci_id): | ||||||
|         attr = AttributeCache.get(attr_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.cache import RoleCache, AppCache | ||||||
| from api.lib.perm.acl.role import RoleCRUD, RoleRelationCRUD | from api.lib.perm.acl.role import RoleCRUD, RoleRelationCRUD | ||||||
| from api.lib.perm.acl.user import UserCRUD | from api.lib.perm.acl.user import UserCRUD | ||||||
|  | from api.lib.perm.acl.resource import ResourceTypeCRUD, ResourceCRUD | ||||||
|  |  | ||||||
|  |  | ||||||
| class ACLManager(object): | class ACLManager(object): | ||||||
| @@ -94,3 +95,22 @@ class ACLManager(object): | |||||||
|                       avatar=user_info.get('avatar')) |                       avatar=user_info.get('avatar')) | ||||||
|  |  | ||||||
|         return result |         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 = "不存在" |     not_found = "不存在" | ||||||
|  |  | ||||||
|  |     circular_dependency_error = "存在循环依赖!" | ||||||
|  |  | ||||||
|     unknown_search_error = "未知搜索错误" |     unknown_search_error = "未知搜索错误" | ||||||
|  |  | ||||||
|     invalid_json = "json格式似乎不正确了, 请仔细确认一下!" |     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): | class CITypeTrigger(Model): | ||||||
|     # __tablename__ = "c_ci_type_triggers" |  | ||||||
|     __tablename__ = "c_c_t_t" |     __tablename__ = "c_c_t_t" | ||||||
|  |  | ||||||
|     type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'), nullable=False) |     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) |     attr_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id")) | ||||||
|     notify = db.Column(db.JSON)  # {subject: x, body: x, wx_to: [], mail_to: [], before_days: 0, notify_at: 08:00} |     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): | class CITypeUniqueConstraint(Model): | ||||||
|     # __tablename__ = "c_ci_type_unique_constraints" |  | ||||||
|     __tablename__ = "c_c_t_u_c" |     __tablename__ = "c_c_t_u_c" | ||||||
|  |  | ||||||
|     type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'), nullable=False) |     type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'), nullable=False) | ||||||
| @@ -363,7 +373,6 @@ class CITypeHistory(Model): | |||||||
|  |  | ||||||
| # preference | # preference | ||||||
| class PreferenceShowAttributes(Model): | class PreferenceShowAttributes(Model): | ||||||
|     # __tablename__ = "c_preference_show_attributes" |  | ||||||
|     __tablename__ = "c_psa" |     __tablename__ = "c_psa" | ||||||
|  |  | ||||||
|     uid = db.Column(db.Integer, index=True, nullable=False) |     uid = db.Column(db.Integer, index=True, nullable=False) | ||||||
| @@ -377,7 +386,6 @@ class PreferenceShowAttributes(Model): | |||||||
|  |  | ||||||
|  |  | ||||||
| class PreferenceTreeView(Model): | class PreferenceTreeView(Model): | ||||||
|     # __tablename__ = "c_preference_tree_views" |  | ||||||
|     __tablename__ = "c_ptv" |     __tablename__ = "c_ptv" | ||||||
|  |  | ||||||
|     uid = db.Column(db.Integer, index=True, nullable=False) |     uid = db.Column(db.Integer, index=True, nullable=False) | ||||||
| @@ -386,7 +394,6 @@ class PreferenceTreeView(Model): | |||||||
|  |  | ||||||
|  |  | ||||||
| class PreferenceRelationView(Model): | class PreferenceRelationView(Model): | ||||||
|     # __tablename__ = "c_preference_relation_views" |  | ||||||
|     __tablename__ = "c_prv" |     __tablename__ = "c_prv" | ||||||
|  |  | ||||||
|     uid = db.Column(db.Integer, index=True, nullable=False) |     uid = db.Column(db.Integer, index=True, nullable=False) | ||||||
|   | |||||||
| @@ -56,12 +56,7 @@ class AttributeView(APIView): | |||||||
|  |  | ||||||
|     @args_required("name") |     @args_required("name") | ||||||
|     @args_validate(AttributeManager.cls) |     @args_validate(AttributeManager.cls) | ||||||
|     def post(self, attr_id=None): |     def post(self): | ||||||
|         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")) |         choice_value = handle_arg_list(request.values.get("choice_value")) | ||||||
|         params = request.values |         params = request.values | ||||||
|         params["choice_value"] = choice_value |         params["choice_value"] = choice_value | ||||||
| @@ -74,6 +69,11 @@ class AttributeView(APIView): | |||||||
|  |  | ||||||
|     @args_validate(AttributeManager.cls) |     @args_validate(AttributeManager.cls) | ||||||
|     def put(self, attr_id): |     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")) |         choice_value = handle_arg_list(request.values.get("choice_value")) | ||||||
|         params = request.values |         params = request.values | ||||||
|         params["choice_value"] = choice_value |         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) |     @has_perm_from_args("ci_id", ResourceTypeEnum.CI, PermEnum.UPDATE, CIManager.get_type_name) | ||||||
|     def put(self, ci_id): |     def put(self, ci_id): | ||||||
|         params = request.values |         params = request.values | ||||||
|         unique_name = params.keys()[0] |         unique_name = list(params.keys())[0] | ||||||
|         unique_value = params.values()[0] |         unique_value = list(params.values())[0] | ||||||
|  |  | ||||||
|         CIManager.update_unique_value(ci_id, unique_name, unique_value) |         CIManager.update_unique_value(ci_id, unique_name, unique_value) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -154,9 +154,15 @@ class EnableCITypeView(APIView): | |||||||
|  |  | ||||||
|  |  | ||||||
| class CITypeAttributeView(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): |     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) |         t = CITypeCache.get(type_id) or CITypeCache.get(type_name) or abort(404, ErrFormat.ci_type_not_found) | ||||||
|         type_id = t.id |         type_id = t.id | ||||||
|         unique_id = t.unique_id |         unique_id = t.unique_id | ||||||
| @@ -413,22 +419,21 @@ class CITypeTriggerView(APIView): | |||||||
|         return self.jsonify(CITypeTriggerManager.get(type_id)) |         return self.jsonify(CITypeTriggerManager.get(type_id)) | ||||||
|  |  | ||||||
|     @has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id) |     @has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id) | ||||||
|     @args_required("attr_id") |     @args_required("option") | ||||||
|     @args_required("notify") |  | ||||||
|     def post(self, type_id): |     def post(self, type_id): | ||||||
|         attr_id = request.values.get('attr_id') |         attr_id = request.values.get('attr_id') or None | ||||||
|         notify = request.values.get('notify') |         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) |     @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): |     def put(self, type_id, _id): | ||||||
|         assert type_id is not None |         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) |     @has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id) | ||||||
|     def delete(self, type_id, _id): |     def delete(self, type_id, _id): | ||||||
| @@ -500,3 +505,4 @@ class CITypeFilterPermissionView(APIView): | |||||||
|     @auth_with_app_token |     @auth_with_app_token | ||||||
|     def get(self, type_id): |     def get(self, type_id): | ||||||
|         return self.jsonify(CIFilterPermsCRUD().get(type_id)) |         return self.jsonify(CIFilterPermsCRUD().get(type_id)) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -19,9 +19,14 @@ from api.resource import APIView | |||||||
|  |  | ||||||
|  |  | ||||||
| class GetChildrenView(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): |     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)) |         return self.jsonify(children=CITypeRelationManager.get_children(parent_id)) | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -13,7 +13,8 @@ from api.resource import APIView | |||||||
|  |  | ||||||
|  |  | ||||||
| class CustomDashboardApiView(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): |     def get(self): | ||||||
|         return self.jsonify(CustomDashboardManager.get()) |         return self.jsonify(CustomDashboardManager.get()) | ||||||
| @@ -21,17 +22,26 @@ class CustomDashboardApiView(APIView): | |||||||
|     @role_required(RoleEnum.CONFIG) |     @role_required(RoleEnum.CONFIG) | ||||||
|     @args_validate(CustomDashboardManager.cls) |     @args_validate(CustomDashboardManager.cls) | ||||||
|     def post(self): |     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) |     @role_required(RoleEnum.CONFIG) | ||||||
|     @args_validate(CustomDashboardManager.cls) |     @args_validate(CustomDashboardManager.cls) | ||||||
|     def put(self, _id=None): |     def put(self, _id=None): | ||||||
|         if _id is not 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")) |         CustomDashboardManager.batch_update(request.values.get("id2options")) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,15 +5,18 @@ import datetime | |||||||
|  |  | ||||||
| from flask import abort | from flask import abort | ||||||
| from flask import request | from flask import request | ||||||
|  | from flask import session | ||||||
|  |  | ||||||
| from api.lib.cmdb.ci import CIManager | from api.lib.cmdb.ci import CIManager | ||||||
| from api.lib.cmdb.const import PermEnum | from api.lib.cmdb.const import PermEnum | ||||||
| from api.lib.cmdb.const import ResourceTypeEnum | from api.lib.cmdb.const import ResourceTypeEnum | ||||||
| from api.lib.cmdb.const import RoleEnum | from api.lib.cmdb.const import RoleEnum | ||||||
| from api.lib.cmdb.history import AttributeHistoryManger | 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.history import CITypeHistoryManager | ||||||
| from api.lib.cmdb.resp_format import ErrFormat | 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 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.perm.acl.acl import role_required | ||||||
| from api.lib.utils import get_page | from api.lib.utils import get_page | ||||||
| from api.lib.utils import get_page_size | from api.lib.utils import get_page_size | ||||||
| @@ -76,6 +79,39 @@ class CIHistoryView(APIView): | |||||||
|         return self.jsonify(result) |         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): | class CITypeHistoryView(APIView): | ||||||
|     url_prefix = "/history/ci_types" |     url_prefix = "/history/ci_types" | ||||||
|  |  | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ from api.resource import APIView | |||||||
| prefix = '/file' | prefix = '/file' | ||||||
|  |  | ||||||
| ALLOWED_EXTENSIONS = { | 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 | PyYAML==6.0 | ||||||
| redis==4.6.0 | redis==4.6.0 | ||||||
| requests==2.31.0 | requests==2.31.0 | ||||||
|  | requests_oauthlib==1.3.1 | ||||||
| six==1.12.0 | six==1.12.0 | ||||||
| SQLAlchemy==1.4.49 | SQLAlchemy==1.4.49 | ||||||
| supervisor==4.0.3 | supervisor==4.0.3 | ||||||
|   | |||||||
| @@ -94,5 +94,3 @@ ES_HOST = '127.0.0.1' | |||||||
| USE_ES = False | USE_ES = False | ||||||
|  |  | ||||||
| BOOL_TRUE = ['true', 'TRUE', 'True', True, '1', 1, "Yes", "YES", "yes", 'Y', 'y'] | 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;"> |       <div class="content unicode" style="display: block;"> | ||||||
|           <ul class="icon_lists dib-box"> |           <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"> |             <li class="dib"> | ||||||
|               <span class="icon iconfont"></span> |               <span class="icon iconfont"></span> | ||||||
|                 <div class="name">rule_100</div> |                 <div class="name">rule_100</div> | ||||||
| @@ -3876,9 +3954,9 @@ | |||||||
| <pre><code class="language-css" | <pre><code class="language-css" | ||||||
| >@font-face { | >@font-face { | ||||||
|   font-family: 'iconfont'; |   font-family: 'iconfont'; | ||||||
|   src: url('iconfont.woff2?t=1688550067963') format('woff2'), |   src: url('iconfont.woff2?t=1694508259411') format('woff2'), | ||||||
|        url('iconfont.woff?t=1688550067963') format('woff'), |        url('iconfont.woff?t=1694508259411') format('woff'), | ||||||
|        url('iconfont.ttf?t=1688550067963') format('truetype'); |        url('iconfont.ttf?t=1694508259411') format('truetype'); | ||||||
| } | } | ||||||
| </code></pre> | </code></pre> | ||||||
|           <h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3> |           <h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3> | ||||||
| @@ -3904,6 +3982,123 @@ | |||||||
|       <div class="content font-class"> |       <div class="content font-class"> | ||||||
|         <ul class="icon_lists dib-box"> |         <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"> |           <li class="dib"> | ||||||
|             <span class="icon iconfont rule_100"></span> |             <span class="icon iconfont rule_100"></span> | ||||||
|             <div class="name"> |             <div class="name"> | ||||||
| @@ -5759,11 +5954,11 @@ | |||||||
|           </li> |           </li> | ||||||
|            |            | ||||||
|           <li class="dib"> |           <li class="dib"> | ||||||
|             <span class="icon iconfont itsm-node-strat"></span> |             <span class="icon iconfont itsm-node-start"></span> | ||||||
|             <div class="name"> |             <div class="name"> | ||||||
|               itsm-node-strat |               itsm-node-strat | ||||||
|             </div> |             </div> | ||||||
|             <div class="code-name">.itsm-node-strat |             <div class="code-name">.itsm-node-start | ||||||
|             </div> |             </div> | ||||||
|           </li> |           </li> | ||||||
|            |            | ||||||
| @@ -9637,6 +9832,110 @@ | |||||||
|       <div class="content symbol"> |       <div class="content symbol"> | ||||||
|           <ul class="icon_lists dib-box"> |           <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"> |             <li class="dib"> | ||||||
|                 <svg class="icon svg-icon" aria-hidden="true"> |                 <svg class="icon svg-icon" aria-hidden="true"> | ||||||
|                   <use xlink:href="#rule_100"></use> |                   <use xlink:href="#rule_100"></use> | ||||||
| @@ -11287,10 +11586,10 @@ | |||||||
|            |            | ||||||
|             <li class="dib"> |             <li class="dib"> | ||||||
|                 <svg class="icon svg-icon" aria-hidden="true"> |                 <svg class="icon svg-icon" aria-hidden="true"> | ||||||
|                   <use xlink:href="#itsm-node-strat"></use> |                   <use xlink:href="#itsm-node-start"></use> | ||||||
|                 </svg> |                 </svg> | ||||||
|                 <div class="name">itsm-node-strat</div> |                 <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> | ||||||
|            |            | ||||||
|             <li class="dib"> |             <li class="dib"> | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| @font-face { | @font-face { | ||||||
|   font-family: "iconfont"; /* Project id 3857903 */ |   font-family: "iconfont"; /* Project id 3857903 */ | ||||||
|   src: url('iconfont.woff2?t=1688550067963') format('woff2'), |   src: url('iconfont.woff2?t=1694508259411') format('woff2'), | ||||||
|        url('iconfont.woff?t=1688550067963') format('woff'), |        url('iconfont.woff?t=1694508259411') format('woff'), | ||||||
|        url('iconfont.ttf?t=1688550067963') format('truetype'); |        url('iconfont.ttf?t=1694508259411') format('truetype'); | ||||||
| } | } | ||||||
|  |  | ||||||
| .iconfont { | .iconfont { | ||||||
| @@ -13,6 +13,58 @@ | |||||||
|   -moz-osx-font-smoothing: grayscale; |   -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 { | .rule_100:before { | ||||||
|   content: "\e87a"; |   content: "\e87a"; | ||||||
| } | } | ||||||
| @@ -837,7 +889,7 @@ | |||||||
|   content: "\e7ad"; |   content: "\e7ad"; | ||||||
| } | } | ||||||
|  |  | ||||||
| .itsm-node-strat:before { | .itsm-node-start:before { | ||||||
|   content: "\e7ae"; |   content: "\e7ae"; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -5,6 +5,97 @@ | |||||||
|   "css_prefix_text": "", |   "css_prefix_text": "", | ||||||
|   "description": "", |   "description": "", | ||||||
|   "glyphs": [ |   "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", |       "icon_id": "36304673", | ||||||
|       "name": "rule_100", |       "name": "rule_100", | ||||||
| @@ -1450,7 +1541,7 @@ | |||||||
|     { |     { | ||||||
|       "icon_id": "35024980", |       "icon_id": "35024980", | ||||||
|       "name": "itsm-node-strat", |       "name": "itsm-node-strat", | ||||||
|       "font_class": "itsm-node-strat", |       "font_class": "itsm-node-start", | ||||||
|       "unicode": "e7ae", |       "unicode": "e7ae", | ||||||
|       "unicode_decimal": 59310 |       "unicode_decimal": 59310 | ||||||
|     }, |     }, | ||||||
|   | |||||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -68,7 +68,8 @@ export default { | |||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   methods: { |   methods: { | ||||||
|     visibleChange(open) { |     visibleChange(open, isInitOne = true) { | ||||||
|  |       // isInitOne  初始化exp为空时,ruleList是否默认给一条 | ||||||
|       //   const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g |       //   const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g | ||||||
|       const exp = this.expression.match(new RegExp(this.regQ, 'g')) |       const exp = this.expression.match(new RegExp(this.regQ, 'g')) | ||||||
|         ? this.expression.match(new RegExp(this.regQ, 'g'))[0] |         ? this.expression.match(new RegExp(this.regQ, 'g'))[0] | ||||||
| @@ -151,15 +152,20 @@ export default { | |||||||
|         }) |         }) | ||||||
|         this.ruleList = [...expArray] |         this.ruleList = [...expArray] | ||||||
|       } else if (open) { |       } else if (open) { | ||||||
|         this.ruleList = [ |         this.ruleList = isInitOne | ||||||
|  |           ? [ | ||||||
|               { |               { | ||||||
|                 id: uuidv4(), |                 id: uuidv4(), | ||||||
|                 type: 'and', |                 type: 'and', | ||||||
|             property: this.canSearchPreferenceAttrList[0].name, |                 property: | ||||||
|  |                   this.canSearchPreferenceAttrList && this.canSearchPreferenceAttrList.length | ||||||
|  |                     ? this.canSearchPreferenceAttrList[0].name | ||||||
|  |                     : undefined, | ||||||
|                 exp: 'is', |                 exp: 'is', | ||||||
|                 value: null, |                 value: null, | ||||||
|               }, |               }, | ||||||
|             ] |             ] | ||||||
|  |           : [] | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     handleClear() { |     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 |  * @param attrId | ||||||
|   | |||||||
| @@ -61,3 +61,10 @@ export function revokeTypeRelation(first_type_id, second_type_id, rid, data) { | |||||||
|     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 |         data | ||||||
|     }) |     }) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export function postCustomDashboardPreview(data) { | ||||||
|  |     return axios({ | ||||||
|  |         url: '/v0.1/custom_dashboard/preview', | ||||||
|  |         method: 'post', | ||||||
|  |         data | ||||||
|  |     }) | ||||||
|  | } | ||||||
|   | |||||||
| @@ -1,11 +1,55 @@ | |||||||
| <template> | <template> | ||||||
|   <div :style="{ width: '100%', height: 'calc(100% - 2.2vw)' }"> |   <div | ||||||
|     <div v-if="category === 0" class="cmdb-dashboard-grid-item-chart"> |     :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> |       <span :style="{ ...options.fontConfig }">{{ toThousands(data) }}</span> | ||||||
|     </div> |     </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 |     <div | ||||||
|       :id="`cmdb-dashboard-${chartId}-${editable}`" |       :id="`cmdb-dashboard-${chartId}-${editable}`" | ||||||
|       v-if="category === 1 || category === 2" |       v-else-if="category === 1 || category === 2" | ||||||
|       class="cmdb-dashboard-grid-item-chart" |       class="cmdb-dashboard-grid-item-chart" | ||||||
|     ></div> |     ></div> | ||||||
|   </div> |   </div> | ||||||
| @@ -15,17 +59,27 @@ | |||||||
| import * as echarts from 'echarts' | import * as echarts from 'echarts' | ||||||
| import { mixin } from '@/utils/mixin' | import { mixin } from '@/utils/mixin' | ||||||
| import { toThousands } from '../../utils/helper' | 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 { | export default { | ||||||
|   name: 'Chart', |   name: 'Chart', | ||||||
|   mixins: [mixin], |   mixins: [mixin], | ||||||
|   props: { |   props: { | ||||||
|  |     ci_types: { | ||||||
|  |       type: Array, | ||||||
|  |       default: () => [], | ||||||
|  |     }, | ||||||
|     chartId: { |     chartId: { | ||||||
|       type: Number, |       type: Number, | ||||||
|       default: 0, |       default: 0, | ||||||
|     }, |     }, | ||||||
|     data: { |     data: { | ||||||
|       type: [Number, Object], |       type: [Number, Object, Array], | ||||||
|       default: 0, |       default: 0, | ||||||
|     }, |     }, | ||||||
|     category: { |     category: { | ||||||
| @@ -40,20 +94,65 @@ export default { | |||||||
|       type: Boolean, |       type: Boolean, | ||||||
|       default: false, |       default: false, | ||||||
|     }, |     }, | ||||||
|  |     type_id: { | ||||||
|  |       type: [Number, Array], | ||||||
|  |       default: null, | ||||||
|  |     }, | ||||||
|  |     isPreview: { | ||||||
|  |       type: Boolean, | ||||||
|  |       default: false, | ||||||
|  |     }, | ||||||
|   }, |   }, | ||||||
|   data() { |   data() { | ||||||
|     return { |     return { | ||||||
|       chart: null, |       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: { |   watch: { | ||||||
|     data: { |     data: { | ||||||
|       immediate: true, |       immediate: true, | ||||||
|       deep: true, |       deep: true, | ||||||
|       handler(newValue, oldValue) { |       handler(newValue, oldValue) { | ||||||
|         if (this.category === 1 || this.category === 2) { |         if (this.category === 1 || this.category === 2) { | ||||||
|           if (Object.prototype.toString.call(newValue) === '[object Object]') { |           if (this.options.chartType !== 'table' && Object.prototype.toString.call(newValue) === '[object Object]') { | ||||||
|  |             if (this.isPreview) { | ||||||
|  |               this.$nextTick(() => { | ||||||
|                 this.setChart() |                 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}`)) |         this.chart = echarts.init(document.getElementById(`cmdb-dashboard-${this.chartId}-${this.editable}`)) | ||||||
|       } |       } | ||||||
|       if (this.category === 1 && this.options.chartType === 'bar') { |       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') { |       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) { |       if (this.category === 2 && ['bar', 'line'].includes(this.options.chartType)) { | ||||||
|         this.chart.setOption(category_2_bar_options(this.data), true) |         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() { |     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> | </script> | ||||||
| @@ -106,14 +239,28 @@ export default { | |||||||
|   width: 100%; |   width: 100%; | ||||||
|   height: 100%; |   height: 100%; | ||||||
|   position: relative; |   position: relative; | ||||||
|   padding: 10px; |   display: flex; | ||||||
|  |   justify-content: space-between; | ||||||
|  |   align-items: center; | ||||||
|   > span { |   > span { | ||||||
|     font-size: 50px; |     font-size: 50px; | ||||||
|     font-weight: 700; |     font-weight: 700; | ||||||
|     position: absolute; |   } | ||||||
|     top: 50%; |   .cmdb-dashboard-grid-item-chart-icon { | ||||||
|     left: 50%; |     > i { | ||||||
|     transform: translate(-50%, -50%); |       font-size: 4vw; | ||||||
|  |     } | ||||||
|  |     > img { | ||||||
|  |       width: 4vw; | ||||||
|  |     } | ||||||
|  |     > span { | ||||||
|  |       display: inline-block; | ||||||
|  |       width: 4vw; | ||||||
|  |       height: 4vw; | ||||||
|  |       font-size: 50px; | ||||||
|  |       text-align: center; | ||||||
|  |       line-height: 50px; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -1,17 +1,70 @@ | |||||||
| <template> | <template> | ||||||
|   <a-modal :title="`${type === 'add' ? '新增' : '编辑'}图表`" :visible="visible" @cancel="handleclose" @ok="handleok"> |   <a-modal | ||||||
|     <a-form-model ref="chartForm" :model="form" :rules="rules" :label-col="{ span: 6 }" :wrapper-col="{ span: 14 }"> |     width="1100px" | ||||||
|       <a-form-model-item label="类型" prop="category"> |     :title="`${type === 'add' ? '新增' : '编辑'}图表`" | ||||||
|         <a-select v-model="form.category" @change="changeDashboardCategory"> |     :visible="visible" | ||||||
|           <a-select-option v-for="cate in Object.keys(dashboardCategory)" :key="cate" :value="Number(cate)">{{ |     @cancel="handleclose" | ||||||
|             dashboardCategory[cate].label |     @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-option> | ||||||
|             </a-select> |             </a-select> | ||||||
|           </a-form-model-item> |           </a-form-model-item> | ||||||
|       <a-form-model-item v-if="form.category !== 0" label="名称" prop="name"> |           <a-form-model-item v-else label="模型" prop="type_id"> | ||||||
|         <a-input v-model="form.name" placeholder="请输入图表名称"></a-input> |  | ||||||
|       </a-form-model-item> |  | ||||||
|       <a-form-model-item label="模型" prop="type_id"> |  | ||||||
|             <a-select |             <a-select | ||||||
|               show-search |               show-search | ||||||
|               optionFilterProp="children" |               optionFilterProp="children" | ||||||
| @@ -24,42 +77,231 @@ | |||||||
|               }}</a-select-option> |               }}</a-select-option> | ||||||
|             </a-select> |             </a-select> | ||||||
|           </a-form-model-item> |           </a-form-model-item> | ||||||
|       <a-form-model-item v-if="form.category === 1" label="模型属性" prop="attr_id"> |           <a-form-model-item | ||||||
|         <a-select show-search optionFilterProp="children" v-model="form.attr_id" placeholder="请选择模型属性"> |             label="维度" | ||||||
|           <a-select-option v-for="attr in attributes" :key="attr.id" :value="attr.id">{{ |             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 |                 attr.alias || attr.name | ||||||
|               }}</a-select-option> |               }}</a-select-option> | ||||||
|             </a-select> |             </a-select> | ||||||
|           </a-form-model-item> |           </a-form-model-item> | ||||||
|       <a-form-model-item v-if="form.category === 1" label="图表类型" prop="chartType"> |           <a-form-model-item | ||||||
|         <a-radio-group v-model="chartType"> |             prop="type_ids" | ||||||
|           <a-radio value="bar"> |             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> | ||||||
|           <a-radio value="pie"> |               <a-radio value=""> | ||||||
|             饼图 |                 多系列柱状图 | ||||||
|               </a-radio> |               </a-radio> | ||||||
|             </a-radio-group> |             </a-radio-group> | ||||||
|           </a-form-model-item> |           </a-form-model-item> | ||||||
|       <a-form-model-item v-if="form.category === 2" label="关系层级" prop="level"> |           <a-form-model-item label="方向" v-if="chartType === 'bar'"> | ||||||
|         <a-input v-model="form.level" placeholder="请输入关系层级"></a-input> |             <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> | ||||||
|       <a-form-model-item v-if="form.category === 0" label="字体"> |           <a-form-model-item label="下方阴影" v-if="chartType === 'line'"> | ||||||
|         <FontConfig ref="fontConfig" /> |             <a-switch v-model="isShadow" /> | ||||||
|           </a-form-model-item> |           </a-form-model-item> | ||||||
|         </a-form-model> |         </a-form-model> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|   </a-modal> |   </a-modal> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
|  | import Chart from './chart.vue' | ||||||
| import { dashboardCategory } from './constant' | import { dashboardCategory } from './constant' | ||||||
| import { postCustomDashboard, putCustomDashboard } from '../../api/customDashboard' | import { postCustomDashboard, putCustomDashboard, postCustomDashboardPreview } from '../../api/customDashboard' | ||||||
| import { getCITypeAttributesById } from '../../api/CITypeAttr' | import { getCITypeAttributesByTypeIds, getCITypeCommonAttributesByTypeIds } from '../../api/CITypeAttr' | ||||||
|  | import { getRecursive_level2children } from '../../api/CITypeRelation' | ||||||
| import { getLastLayout } from '../../utils/helper' | 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 { | export default { | ||||||
|   name: 'ChartForm', |   name: 'ChartForm', | ||||||
|   components: { FontConfig }, |   components: { Chart, FilterComp, ColorPicker, ColorListPicker }, | ||||||
|   props: { |   props: { | ||||||
|     ci_types: { |     ci_types: { | ||||||
|       type: Array, |       type: Array, | ||||||
| @@ -67,100 +309,226 @@ export default { | |||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|   data() { |   data() { | ||||||
|  |     const chartTypeList = [ | ||||||
|  |       { | ||||||
|  |         value: 'count', | ||||||
|  |         label: '指标', | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         value: 'bar', | ||||||
|  |         label: '柱状图', | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         value: 'line', | ||||||
|  |         label: '折线图', | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         value: 'pie', | ||||||
|  |         label: '饼状图', | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         value: 'table', | ||||||
|  |         label: '表格', | ||||||
|  |       }, | ||||||
|  |     ] | ||||||
|     return { |     return { | ||||||
|       dashboardCategory, |       dashboardCategory, | ||||||
|  |       chartTypeList, | ||||||
|       visible: false, |       visible: false, | ||||||
|       attributes: [], |       attributes: [], | ||||||
|       type: 'add', |       type: 'add', | ||||||
|       form: { |       form: { | ||||||
|         category: 0, |         category: 0, | ||||||
|  |         tableCategory: 1, | ||||||
|         name: undefined, |         name: undefined, | ||||||
|         type_id: undefined, |         type_id: undefined, | ||||||
|         attr_id: undefined, |         type_ids: undefined, | ||||||
|  |         attr_ids: undefined, | ||||||
|         level: undefined, |         level: undefined, | ||||||
|  |         showIcon: false, | ||||||
|       }, |       }, | ||||||
|       rules: { |       rules: { | ||||||
|         category: [{ required: true, trigger: 'change' }], |         category: [{ required: true, trigger: 'change' }], | ||||||
|         name: [{ required: true, message: '请输入图表名称' }], |         name: [{ required: true, message: '请输入图表名称' }], | ||||||
|         type_id: [{ required: true, message: '请选择模型', trigger: 'change' }], |         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: '请输入关系层级' }], |         level: [{ required: true, message: '请输入关系层级' }], | ||||||
|  |         showIcon: [{ required: false }], | ||||||
|       }, |       }, | ||||||
|       item: {}, |       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'], |   inject: ['layout'], | ||||||
|   methods: { |   methods: { | ||||||
|     open(type, item = {}) { |     async open(type, item = {}) { | ||||||
|       this.visible = true |       this.visible = true | ||||||
|       this.type = type |       this.type = type | ||||||
|       this.item = item |       this.item = item | ||||||
|       const { category = 0, name, type_id, attr_id, level } = 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 |       this.chartType = chartType | ||||||
|       if (type_id && attr_id) { |       this.filterExp = item?.options?.filter ?? '' | ||||||
|         getCITypeAttributesById(type_id).then((res) => { |       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 |           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 = { |       const default_form = { | ||||||
|         category: 0, |         category: 0, | ||||||
|         name: undefined, |         name: undefined, | ||||||
|         type_id: undefined, |         type_id: undefined, | ||||||
|         attr_id: undefined, |         type_ids: undefined, | ||||||
|  |         attr_ids: undefined, | ||||||
|         level: undefined, |         level: undefined, | ||||||
|  |         showIcon: false, | ||||||
|  |         tableCategory: 1, | ||||||
|       } |       } | ||||||
|       this.form = { |       this.form = { | ||||||
|         ...default_form, |         ...default_form, | ||||||
|         category, |         category, | ||||||
|         name, |         name, | ||||||
|         type_id, |         type_id, | ||||||
|         attr_id, |         type_ids, | ||||||
|  |         attr_ids, | ||||||
|         level, |         level, | ||||||
|       } |         showIcon, | ||||||
|       if (category === 0) { |         tableCategory: ret === 'cis' ? 2 : 1, | ||||||
|         this.$nextTick(() => { |  | ||||||
|           this.$refs.fontConfig.setConfig((item.options || {}).fontConfig) |  | ||||||
|         }) |  | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     handleclose() { |     handleclose() { | ||||||
|       this.attributes = [] |       this.attributes = [] | ||||||
|       this.$refs.chartForm.clearValidate() |       this.$refs.chartForm.clearValidate() | ||||||
|  |       this.isShowPreview = false | ||||||
|       this.visible = false |       this.visible = false | ||||||
|     }, |     }, | ||||||
|     changeCIType(value) { |     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.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() { |     handleok() { | ||||||
|       this.$refs.chartForm.validate(async (valid) => { |       this.$refs.chartForm.validate(async (valid) => { | ||||||
|         if (valid) { |         if (valid) { | ||||||
|           const fontConfig = this.form.category === 0 ? this.$refs.fontConfig.getConfig() : undefined |           const name = this.form.name | ||||||
|           const _find = this.ci_types.find((attr) => attr.id === this.form.type_id) |           const { chartType, fontColor, bgColor } = this | ||||||
|           const name = this.form.name || (_find || {}).alias || (_find || {}).name |           this.$refs.filterComp.handleSubmit() | ||||||
|           if (this.item.id) { |           if (this.item.id) { | ||||||
|             await putCustomDashboard(this.item.id, { |             const params = { | ||||||
|               ...this.form, |               ...this.form, | ||||||
|               options: { |               options: { | ||||||
|                 ...this.item.options, |                 ...this.item.options, | ||||||
|                 name, |                 name, | ||||||
|                 fontConfig, |                 w: this.width, | ||||||
|                 chartType: this.chartType, |                 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 { |           } else { | ||||||
|             const { xLast, yLast, wLast } = getLastLayout(this.layout()) |             const { xLast, yLast, wLast } = getLastLayout(this.layout()) | ||||||
|             const w = 3 |             const w = this.width | ||||||
|             const x = xLast + wLast + w > 12 ? 0 : xLast + wLast |             const x = xLast + wLast + w > 12 ? 0 : xLast + wLast | ||||||
|             const y = xLast + wLast + w > 12 ? yLast + 1 : yLast |             const y = xLast + wLast + w > 12 ? yLast + 1 : yLast | ||||||
|             await postCustomDashboard({ |             const params = { | ||||||
|               ...this.form, |               ...this.form, | ||||||
|               options: { |               options: { | ||||||
|                 x, |                 x, | ||||||
| @@ -169,23 +537,216 @@ export default { | |||||||
|                 h: this.form.category === 0 ? 3 : 5, |                 h: this.form.category === 0 ? 3 : 5, | ||||||
|                 name, |                 name, | ||||||
|                 chartType: this.chartType, |                 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.handleclose() | ||||||
|           this.$emit('refresh') |           this.$emit('refresh') | ||||||
|         } |         } | ||||||
|       }) |       }) | ||||||
|     }, |     }, | ||||||
|     changeDashboardCategory(value) { |     // changeDashboardCategory(value) { | ||||||
|       this.$refs.chartForm.clearValidate() |     //   this.$refs.chartForm.clearValidate() | ||||||
|       if (value === 1 && this.form.type_id) { |     //   if (value === 1 && this.form.type_id) { | ||||||
|         this.changeCIType(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> | </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,58 +1,138 @@ | |||||||
| export const colorList = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc'] | export const category_1_bar_options = (data, options) => { | ||||||
|  |     // 计算一级分类 | ||||||
| export const category_1_bar_options = (data) => { |     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 { |     return { | ||||||
|  |         color: options.chartColor.split(','), | ||||||
|         grid: { |         grid: { | ||||||
|             top: 15, |             top: 15, | ||||||
|             left: 'left', |             left: 'left', | ||||||
|             right: 0, |             right: 10, | ||||||
|             bottom: 0, |             bottom: 20, | ||||||
|             containLabel: true, |             containLabel: true, | ||||||
|         }, |         }, | ||||||
|         xAxis: { |         legend: { | ||||||
|             type: 'category', |             data: Object.keys(secondCategory), | ||||||
|             data: Object.keys(data) |             bottom: 0, | ||||||
|  |             type: 'scroll', | ||||||
|         }, |         }, | ||||||
|         yAxis: { |         xAxis: options.barDirection === 'y' ? { | ||||||
|  |             type: 'category', | ||||||
|  |             axisTick: { show: false }, | ||||||
|  |             data: xData | ||||||
|  |         } | ||||||
|  |             : { | ||||||
|                 type: 'value', |                 type: 'value', | ||||||
|                 splitLine: { |                 splitLine: { | ||||||
|                     show: false |                     show: false | ||||||
|                 } |                 } | ||||||
|             }, |             }, | ||||||
|  |         yAxis: options.barDirection === 'y' ? { | ||||||
|  |             type: 'value', | ||||||
|  |             splitLine: { | ||||||
|  |                 show: false | ||||||
|  |             } | ||||||
|  |         } : { | ||||||
|  |             type: 'category', | ||||||
|  |             axisTick: { show: false }, | ||||||
|  |             data: xData | ||||||
|  |         }, | ||||||
|         tooltip: { |         tooltip: { | ||||||
|             trigger: 'axis', |             trigger: 'axis', | ||||||
|             axisPointer: { |             axisPointer: { | ||||||
|                 type: 'shadow' |                 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: [ |         series: [ | ||||||
|             { |             { | ||||||
|                 data: Object.keys(data).map((key, index) => { |                 data: xData.map(item => data[item]), | ||||||
|                     return { |                 type: 'line', | ||||||
|                         value: data[key], |                 smooth: true, | ||||||
|                         itemStyle: { color: colorList[0] } |                 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 | ||||||
|                     } |                     } | ||||||
|                 }), |                 } : null | ||||||
|                 type: 'bar', |  | ||||||
|                 label: { |  | ||||||
|                     show: true, |  | ||||||
|                     position: 'top', |  | ||||||
|                     fontSize: 10, |  | ||||||
|                     formatter(data) { |  | ||||||
|                         return `${data.value || ''}` |  | ||||||
|                     } |  | ||||||
|                 }, |  | ||||||
|             } |             } | ||||||
|         ] |         ] | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| export const category_1_pie_options = (data) => { | export const category_1_pie_options = (data, options) => { | ||||||
|     return { |     return { | ||||||
|  |         color: options.chartColor.split(','), | ||||||
|         grid: { |         grid: { | ||||||
|             top: 10, |             top: 10, | ||||||
|             left: 'left', |             left: 'left', | ||||||
|             right: 0, |             right: 10, | ||||||
|             bottom: 0, |             bottom: 0, | ||||||
|             containLabel: true, |             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 xAxisData = Object.keys(data.detail) | ||||||
|     const _legend = [] |     const _legend = [] | ||||||
|     xAxisData.forEach(key => { |     xAxisData.forEach(key => { | ||||||
| @@ -97,10 +177,11 @@ export const category_2_bar_options = (data) => { | |||||||
|     }) |     }) | ||||||
|     const legend = [...new Set(_legend)] |     const legend = [...new Set(_legend)] | ||||||
|     return { |     return { | ||||||
|  |         color: options.chartColor.split(','), | ||||||
|         grid: { |         grid: { | ||||||
|             top: 15, |             top: 15, | ||||||
|             left: 'left', |             left: 'left', | ||||||
|             right: 0, |             right: 10, | ||||||
|             bottom: 20, |             bottom: 20, | ||||||
|             containLabel: true, |             containLabel: true, | ||||||
|         }, |         }, | ||||||
| @@ -116,41 +197,110 @@ export const category_2_bar_options = (data) => { | |||||||
|             type: 'scroll', |             type: 'scroll', | ||||||
|             data: legend |             data: legend | ||||||
|         }, |         }, | ||||||
|         xAxis: [ |         xAxis: options.barDirection === 'y' || chartType === 'line' ? { | ||||||
|             { |  | ||||||
|             type: 'category', |             type: 'category', | ||||||
|             axisTick: { show: false }, |             axisTick: { show: false }, | ||||||
|             data: xAxisData |             data: xAxisData | ||||||
|         } |         } | ||||||
|         ], |             : { | ||||||
|         yAxis: [ |  | ||||||
|             { |  | ||||||
|                 type: 'value', |                 type: 'value', | ||||||
|                 splitLine: { |                 splitLine: { | ||||||
|                     show: false |                     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 { |             return { | ||||||
|                 name: le, |                 name: le, | ||||||
|                 type: 'bar', |                 type: chartType, | ||||||
|                 barGap: 0, |                 barGap: 0, | ||||||
|                 emphasis: { |                 emphasis: { | ||||||
|                     focus: 'series' |                     focus: 'series' | ||||||
|                 }, |                 }, | ||||||
|  |                 stack: chartType === 'line' ? '' : options?.barStack ?? 'total', | ||||||
|                 data: xAxisData.map(x => { |                 data: xAxisData.map(x => { | ||||||
|                     return data.detail[x][le] || 0 |                     return data.detail[x][le] || 0 | ||||||
|                 }), |                 }), | ||||||
|  |                 smooth: true, | ||||||
|  |                 showSymbol: false, | ||||||
|                 label: { |                 label: { | ||||||
|                     show: true, |                     show: false, | ||||||
|                     position: 'top', |  | ||||||
|                     fontSize: 10, |  | ||||||
|                     formatter(data) { |  | ||||||
|                         return `${data.value || ''}` |  | ||||||
|                     } |  | ||||||
|                 }, |                 }, | ||||||
|  |                 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 = { | export const dashboardCategory = { | ||||||
|     0: { label: 'CI数统计' }, |     1: { label: '默认' }, | ||||||
|     1: { label: '按属性值分类统计' }, |     2: { label: '关系' } | ||||||
|     2: { label: '关系统计' } |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -11,8 +11,8 @@ | |||||||
|     <template v-if="layout && layout.length"> |     <template v-if="layout && layout.length"> | ||||||
|       <div v-if="editable"> |       <div v-if="editable"> | ||||||
|         <a-button |         <a-button | ||||||
|           :style="{ marginLeft: '10px' }" |           :style="{ marginLeft: '22px', marginTop: '20px' }" | ||||||
|           @click="openChartForm('add', {})" |           @click="openChartForm('add', { options: { w: 3 } })" | ||||||
|           ghost |           ghost | ||||||
|           type="primary" |           type="primary" | ||||||
|           size="small" |           size="small" | ||||||
| @@ -39,11 +39,44 @@ | |||||||
|           :h="item.h" |           :h="item.h" | ||||||
|           :i="item.i" |           :i="item.i" | ||||||
|           :key="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-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 slot="overlay"> | ||||||
|               <a-menu-item> |               <a-menu-item> | ||||||
|                 <a @click="() => openChartForm('edit', item)"><a-icon style="margin-right:5px" type="edit" />编辑</a> |                 <a @click="() => openChartForm('edit', item)"><a-icon style="margin-right:5px" type="edit" />编辑</a> | ||||||
| @@ -53,13 +86,13 @@ | |||||||
|               </a-menu-item> |               </a-menu-item> | ||||||
|             </a-menu> |             </a-menu> | ||||||
|           </a-dropdown> |           </a-dropdown> | ||||||
|           <a |           <!-- <a | ||||||
|             v-if="editable && item.category === 1" |             v-if="editable && item.category === 1" | ||||||
|             class="cmdb-dashboard-grid-item-chart-type" |             class="cmdb-dashboard-grid-item-chart-type" | ||||||
|             @click="changeChartType(item)" |             @click="changeChartType(item)" | ||||||
|           ><a-icon |           ><a-icon | ||||||
|             :type="item.options.chartType === 'bar' ? 'bar-chart' : 'pie-chart'" |             :type="item.options.chartType === 'bar' ? 'bar-chart' : 'pie-chart'" | ||||||
|           /></a> |           /></a> --> | ||||||
|           <Chart |           <Chart | ||||||
|             :ref="`chart_${item.id}`" |             :ref="`chart_${item.id}`" | ||||||
|             :chartId="item.id" |             :chartId="item.id" | ||||||
| @@ -67,18 +100,26 @@ | |||||||
|             :category="item.category" |             :category="item.category" | ||||||
|             :options="item.options" |             :options="item.options" | ||||||
|             :editable="editable" |             :editable="editable" | ||||||
|  |             :ci_types="ci_types" | ||||||
|  |             :type_id="item.type_id" | ||||||
|           /> |           /> | ||||||
|         </GridItem> |         </GridItem> | ||||||
|       </GridLayout> |       </GridLayout> | ||||||
|     </template> |     </template> | ||||||
|     <div v-else class="dashboard-empty"> |     <div v-else class="dashboard-empty"> | ||||||
|       <a-empty :image="emptyImage" description=""></a-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> |       </a-button> | ||||||
|       <span v-else>管理员暂未定制仪表盘</span> |       <span v-else>管理员暂未定制仪表盘</span> | ||||||
|     </div> |     </div> | ||||||
|     <ChartForm ref="chartForm" @refresh="refresh" :ci_types="ci_types" /> |     <ChartForm ref="chartForm" @refresh="refresh" :ci_types="ci_types" :totalData="totalData" /> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| @@ -127,12 +168,14 @@ export default { | |||||||
|       }, |       }, | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   mounted() { |   created() { | ||||||
|     this.getLayout() |  | ||||||
|     getCITypes().then((res) => { |     getCITypes().then((res) => { | ||||||
|       this.ci_types = res.ci_types |       this.ci_types = res.ci_types | ||||||
|     }) |     }) | ||||||
|   }, |   }, | ||||||
|  |   mounted() { | ||||||
|  |     this.getLayout() | ||||||
|  |   }, | ||||||
|   methods: { |   methods: { | ||||||
|     async getLayout() { |     async getLayout() { | ||||||
|       const res = await getCustomDashboard() |       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> | </script> | ||||||
| @@ -206,15 +256,18 @@ export default { | |||||||
|   text-align: center; |   text-align: center; | ||||||
| } | } | ||||||
| .cmdb-dashboard-grid-item { | .cmdb-dashboard-grid-item { | ||||||
|   border-radius: 15px; |   border-radius: 8px; | ||||||
|  |   padding: 6px 12px; | ||||||
|   .cmdb-dashboard-grid-item-title { |   .cmdb-dashboard-grid-item-title { | ||||||
|  |     overflow: hidden; | ||||||
|  |     text-overflow: ellipsis; | ||||||
|  |     white-space: nowrap; | ||||||
|     font-weight: 700; |     font-weight: 700; | ||||||
|     padding-left: 6px; |     color: #000000; | ||||||
|     color: #000000bd; |  | ||||||
|   } |   } | ||||||
|   .cmdb-dashboard-grid-item-operation { |   .cmdb-dashboard-grid-item-operation { | ||||||
|     position: absolute; |     position: absolute; | ||||||
|     right: 6px; |     right: 12px; | ||||||
|     top: 6px; |     top: 6px; | ||||||
|   } |   } | ||||||
|   .cmdb-dashboard-grid-item-chart-type { |   .cmdb-dashboard-grid-item-chart-type { | ||||||
| @@ -224,3 +277,26 @@ export default { | |||||||
|   } |   } | ||||||
| } | } | ||||||
| </style> | </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 |           - redis | ||||||
|  |  | ||||||
|   cmdb-api: |   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: | #    build: | ||||||
| #      context: . | #      context: . | ||||||
| #      target: cmdb-api | #      target: cmdb-api | ||||||
| @@ -61,7 +61,7 @@ services: | |||||||
|           - cmdb-api |           - cmdb-api | ||||||
|  |  | ||||||
|   cmdb-ui: |   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: | #    build: | ||||||
| #      context: . | #      context: . | ||||||
| #      target: cmdb-ui | #      target: cmdb-ui | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user