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