Merge branch 'master' of github.com:veops/cmdb into dev_ui

This commit is contained in:
wang-liang0615 2023-09-25 14:43:34 +08:00
commit 249ba7ad5c
45 changed files with 2511 additions and 394 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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', ''))
cls.set(result) if res and flush:
result[custom['id']] = res
cls.set(result)
return res
@staticmethod @staticmethod
def summary_counter(type_id): def relation_counter(type_id, level, other_filer, type_ids):
return db.session.query(CI.id).filter(CI.deleted.is_(False)).filter(CI.type_id == type_id).count() from api.lib.cmdb.search.ci_relation.search import Search as RelSearch
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
@staticmethod query = "_type:{}".format(type_id)
def relation_counter(type_id, level): s = search(query, count=1000000)
try:
type_names, _, _, _, _, _ = s.search()
except SearchError as e:
current_app.logger.error(e)
return
uri = current_app.config.get('CMDB_API')
type_names = requests.get("{}/ci/s?q=_type:{}&count=10000".format(uri, type_id)).json().get('result')
type_id_names = [(str(i.get('_id')), i.get(i.get('unique'))) for i in type_names] 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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">&#xe886;</span>
<div class="name">cmdb-histogram</div>
<div class="code-name">&amp;#xe886;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe883;</span>
<div class="name">cmdb-index</div>
<div class="code-name">&amp;#xe883;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe884;</span>
<div class="name">cmdb-piechart</div>
<div class="code-name">&amp;#xe884;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe885;</span>
<div class="name">cmdb-line</div>
<div class="code-name">&amp;#xe885;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe882;</span>
<div class="name">cmdb-table</div>
<div class="code-name">&amp;#xe882;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe87f;</span>
<div class="name">itsm-all</div>
<div class="code-name">&amp;#xe87f;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe87e;</span>
<div class="name">itsm-reply</div>
<div class="code-name">&amp;#xe87e;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe880;</span>
<div class="name">itsm-information</div>
<div class="code-name">&amp;#xe880;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe881;</span>
<div class="name">itsm-contact</div>
<div class="code-name">&amp;#xe881;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe87d;</span>
<div class="name">itsm-my-processed</div>
<div class="code-name">&amp;#xe87d;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe87c;</span>
<div class="name">rule_7</div>
<div class="code-name">&amp;#xe87c;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe879;</span>
<div class="name">itsm-my-completed</div>
<div class="code-name">&amp;#xe879;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe87b;</span>
<div class="name">itsm-my-plan</div>
<div class="code-name">&amp;#xe87b;</div>
</li>
<li class="dib"> <li class="dib">
<span class="icon iconfont">&#xe87a;</span> <span class="icon iconfont">&#xe87a;</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">

View File

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

View File

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

View File

@ -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(), {
type: 'and', id: uuidv4(),
property: this.canSearchPreferenceAttrList[0].name, type: 'and',
exp: 'is', property:
value: null, this.canSearchPreferenceAttrList && this.canSearchPreferenceAttrList.length
}, ? this.canSearchPreferenceAttrList[0].name
] : undefined,
exp: 'is',
value: null,
},
]
: []
} }
}, },
handleClear() { handleClear() {

View File

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

View File

@ -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'
})
}

View File

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

View File

@ -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]') {
this.setChart() if (this.isPreview) {
this.$nextTick(() => {
this.setChart()
})
} else {
this.setChart()
}
}
}
if (this.options.chartType === 'table') {
this.$nextTick(() => {
const dom = document.getElementById(`cmdb-dashboard-${this.chartId}-${this.editable}-${this.isPreview}`)
this.tableHeight = dom.offsetHeight
})
if (this.options.ret) {
const excludeKeys = ['_X_ROW_KEY', 'ci_type', 'ci_type_alias', 'unique', 'unique_alias', '_id', '_type']
if (newValue && newValue.length) {
this.columns = Object.keys(newValue[0]).filter((keys) => !excludeKeys.includes(keys))
this.tableData = newValue
}
} else {
const _data = []
this.keyLength = this.options?.attr_ids?.length ?? 0
this.formatTableData(_data, this.data, {})
this.tableData = _data
} }
} }
}, },
@ -81,13 +180,19 @@ export default {
this.chart = echarts.init(document.getElementById(`cmdb-dashboard-${this.chartId}-${this.editable}`)) 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>

View File

@ -1,65 +1,307 @@
<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"
}}</a-select-option> :bodyStyle="{ paddingTop: 0 }"
</a-select> >
</a-form-model-item> <div class="chart-wrapper">
<a-form-model-item v-if="form.category !== 0" label="名称" prop="name"> <div class="chart-left">
<a-input v-model="form.name" placeholder="请输入图表名称"></a-input> <a-form-model ref="chartForm" :model="form" :rules="rules" :label-col="{ span: 4 }" :wrapper-col="{ span: 18 }">
</a-form-model-item> <a-form-model-item label="标题" prop="name">
<a-form-model-item label="模型" prop="type_id"> <a-input v-model="form.name" placeholder="请输入图表标题"></a-input>
<a-select </a-form-model-item>
show-search <a-form-model-item label="类型" prop="category" v-if="chartType !== 'count' && chartType !== 'table'">
optionFilterProp="children" <a-radio-group
@change="changeCIType" @change="
v-model="form.type_id" () => {
placeholder="请选择模型" resetForm()
> }
<a-select-option v-for="ci_type in ci_types" :key="ci_type.id" :value="ci_type.id">{{ "
ci_type.alias || ci_type.name :default-value="1"
}}</a-select-option> v-model="form.category"
</a-select> >
</a-form-model-item> <a-radio-button :value="Number(key)" :key="key" v-for="key in Object.keys(dashboardCategory)">
<a-form-model-item v-if="form.category === 1" label="模型属性" prop="attr_id"> {{ dashboardCategory[key].label }}
<a-select show-search optionFilterProp="children" v-model="form.attr_id" placeholder="请选择模型属性"> </a-radio-button>
<a-select-option v-for="attr in attributes" :key="attr.id" :value="attr.id">{{ </a-radio-group>
attr.alias || attr.name </a-form-model-item>
}}</a-select-option> <a-form-model-item label="类型" prop="tableCategory" v-if="chartType === 'table'">
</a-select> <a-radio-group
</a-form-model-item> @change="
<a-form-model-item v-if="form.category === 1" label="图表类型" prop="chartType"> () => {
<a-radio-group v-model="chartType"> resetForm()
<a-radio value="bar"> }
柱状图 "
</a-radio> :default-value="1"
<a-radio value="pie"> v-model="form.tableCategory"
饼图 >
</a-radio> <a-radio-button :value="1">
</a-radio-group> 计算指标
</a-form-model-item> </a-radio-button>
<a-form-model-item v-if="form.category === 2" label="关系层级" prop="level"> <a-radio-button :value="2">
<a-input v-model="form.level" placeholder="请输入关系层级"></a-input> 资源数据
</a-form-model-item> </a-radio-button>
<a-form-model-item v-if="form.category === 0" label="字体"> </a-radio-group>
<FontConfig ref="fontConfig" /> </a-form-model-item>
</a-form-model-item> <a-form-model-item
</a-form-model> v-if="(chartType !== 'table' && form.category !== 2) || (chartType === 'table' && form.tableCategory === 1)"
label="模型"
prop="type_ids"
>
<a-select
show-search
optionFilterProp="children"
@change="changeCIType"
v-model="form.type_ids"
placeholder="请选择模型"
mode="multiple"
>
<a-select-option v-for="ci_type in ci_types" :key="ci_type.id" :value="ci_type.id">{{
ci_type.alias || ci_type.name
}}</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item v-else label="模型" prop="type_id">
<a-select
show-search
optionFilterProp="children"
@change="changeCIType"
v-model="form.type_id"
placeholder="请选择模型"
>
<a-select-option v-for="ci_type in ci_types" :key="ci_type.id" :value="ci_type.id">{{
ci_type.alias || ci_type.name
}}</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item
label="维度"
prop="attr_ids"
v-if="(['bar', 'line', 'pie'].includes(chartType) && form.category === 1) || chartType === 'table'"
>
<a-select @change="changeAttr" v-model="form.attr_ids" placeholder="请选择维度" mode="multiple" show-search>
<a-select-option v-for="attr in commonAttributes" :key="attr.id" :value="attr.id">{{
attr.alias || attr.name
}}</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item
prop="type_ids"
label="关系模型"
v-if="['bar', 'line', 'pie'].includes(chartType) && form.category === 2"
>
<a-select
show-search
optionFilterProp="children"
mode="multiple"
v-model="form.type_ids"
placeholder="请选择模型"
>
<a-select-opt-group
v-for="(key, index) in Object.keys(level2children)"
:key="key"
:label="`层级${index + 1}`"
>
<a-select-option
@click="(e) => clickLevel2children(e, citype, index + 1)"
v-for="citype in level2children[key]"
:key="citype.id"
:value="citype.id"
>
{{ citype.alias || citype.name }}
</a-select-option>
</a-select-opt-group>
</a-select>
</a-form-model-item>
<div class="chart-left-preview">
<span class="chart-left-preview-operation" @click="showPreview"><a-icon type="play-circle" /> 预览</span>
<template v-if="isShowPreview">
<div v-if="chartType !== 'count'" class="cmdb-dashboard-grid-item-title">
<template v-if="form.showIcon && ciType">
<template v-if="ciType.icon">
<img
v-if="ciType.icon.split('$$')[2]"
:src="`/api/common-setting/v1/file/${ciType.icon.split('$$')[3]}`"
/>
<ops-icon
v-else
:style="{
color: ciType.icon.split('$$')[1],
}"
:type="ciType.icon.split('$$')[0]"
/>
</template>
<span :style="{ color: '#2f54eb' }" v-else>{{ ciType.name[0].toUpperCase() }}</span>
</template>
<span :style="{ color: '#000' }"> {{ form.name }}</span>
</div>
<div
class="chart-left-preview-box"
:style="{
height: chartType === 'count' ? '120px' : '',
marginTop: chartType === 'count' ? '80px' : '',
background:
chartType === 'count'
? Array.isArray(bgColor)
? `linear-gradient(to bottom, ${bgColor[0]} 0%, ${bgColor[1]} 100%)`
: bgColor
: '#fafafa',
}"
>
<div :style="{ color: fontColor }">{{ form.name }}</div>
<Chart
:ref="`chart_${item.id}`"
:chartId="item.id"
:data="previewData"
:category="form.category"
:options="{
...item.options,
name: form.name,
fontColor: fontColor,
bgColor: bgColor,
chartType: chartType,
showIcon: form.showIcon,
barDirection: barDirection,
barStack: barStack,
chartColor: chartColor,
type_ids: form.type_ids,
attr_ids: form.attr_ids,
isShadow: isShadow,
}"
:editable="false"
:ci_types="ci_types"
:type_id="form.type_id || form.type_ids"
isPreview
/>
</div>
</template>
</div>
<a-form-model-item label="是否显示icon" prop="showIcon" :label-col="{ span: 5 }" :wrapper-col="{ span: 18 }">
<a-switch v-model="form.showIcon"></a-switch>
</a-form-model-item>
</a-form-model>
</div>
<div class="chart-right">
<h4>图表类型</h4>
<div class="chart-right-type">
<div
:class="{ 'chart-right-type-box': true, 'chart-right-type-box-selected': chartType === t.value }"
v-for="t in chartTypeList"
:key="t.value"
@click="changeChartType(t)"
>
<ops-icon :type="`cmdb-${t.value}`" />
<span>{{ t.label }}</span>
</div>
</div>
<h4>数据筛选</h4>
<FilterComp
ref="filterComp"
:isDropdown="false"
:canSearchPreferenceAttrList="attributes"
@setExpFromFilter="setExpFromFilter"
:expression="filterExp ? `q=${filterExp}` : ''"
/>
<h4>格式</h4>
<a-form-model :colon="false" :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
<a-form-model-item label="字体颜色" v-if="chartType === 'count'">
<ColorPicker
v-model="fontColor"
:colorList="[
'#1D2129',
'#4E5969',
'#103C93',
'#86909C',
'#ffffff',
'#C9F2FF',
'#FFEAC0',
'#D6FFE6',
'#F2DEFF',
]"
/>
</a-form-model-item>
<a-form-model-item label="背景颜色" v-if="chartType === 'count'">
<ColorPicker
v-model="bgColor"
:colorList="[
['#6ABFFE', '#5375EB'],
['#C69EFF', '#A377F9'],
['#85EBC9', '#4AB8D8'],
['#FEB58B', '#DF6463'],
'#ffffff',
'#FFFBF0',
'#FFF1EC',
'#E5FFFE',
'#E5E7FF',
]"
/>
</a-form-model-item>
<a-form-model-item label="图表颜色" v-else-if="chartType !== 'table'">
<ColorListPicker v-model="chartColor" />
</a-form-model-item>
<a-form-model-item label="图表长度(%)">
<a-radio-group class="chart-width" style="width:100%;" v-model="width">
<a-radio-button :value="3">
25
</a-radio-button>
<a-radio-button :value="6">
50
</a-radio-button>
<a-radio-button :value="9">
75
</a-radio-button>
<a-radio-button :value="12">
100
</a-radio-button>
</a-radio-group>
</a-form-model-item>
<a-form-model-item label="柱状图类型" v-if="chartType === 'bar'">
<a-radio-group v-model="barStack">
<a-radio value="total">
堆积柱状图
</a-radio>
<a-radio value="">
多系列柱状图
</a-radio>
</a-radio-group>
</a-form-model-item>
<a-form-model-item label="方向" v-if="chartType === 'bar'">
<a-radio-group v-model="barDirection">
<a-radio value="x">
X轴
</a-radio>
<a-radio value="y">
y轴
</a-radio>
</a-radio-group>
</a-form-model-item>
<a-form-model-item label="下方阴影" v-if="chartType === 'line'">
<a-switch v-model="isShadow" />
</a-form-model-item>
</a-form-model>
</div>
</div>
</a-modal> </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>

View File

@ -1,23 +1,61 @@
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',
splitLine: {
show: false
}
},
yAxis: options.barDirection === 'y' ? {
type: 'value', type: 'value',
splitLine: { splitLine: {
show: false show: false
} }
} : {
type: 'category',
axisTick: { show: false },
data: xData
}, },
tooltip: { tooltip: {
trigger: 'axis', trigger: 'axis',
@ -25,34 +63,76 @@ export const category_1_bar_options = (data) => {
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)'
}
}
}
]
}
}

View File

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

View File

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

View File

@ -1,5 +1,4 @@
export const dashboardCategory = { export const dashboardCategory = {
0: { label: 'CI数统计' }, 1: { label: '默认' },
1: { label: '按属性值分类统计' }, 2: { label: '关系' }
2: { label: '关系统计' }
} }

View File

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

View File

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