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