Compare commits

..

9 Commits
2.3.2 ... 2.3.3

Author SHA1 Message Date
pycook
62829c885b release v2.3.3 2023-09-15 17:57:39 +08:00
pycook
260aed6462 dashboard ui update 2023-09-15 17:36:10 +08:00
simontigers
3841999cca feat: init resource for backend (#176) 2023-09-15 15:30:30 +08:00
pycook
14c03ce5d2 enhance dashboard 2023-09-15 15:26:20 +08:00
pycook
f463ecd6e6 cmdb-api/api/lib/resp_format.py 2023-09-12 20:01:30 +08:00
pycook
adc0cfd5c5 Detect circular dependencies when adding CIType relationships 2023-09-12 20:00:56 +08:00
wang-liang0615
086481657e 计算属性 触发计算 (#174) 2023-09-11 19:16:05 +08:00
pycook
d2f84ae3dc fix upload template and add /api/v0.1/attributes/<int:attr_id>/calc_computed_attribute 2023-09-11 19:15:31 +08:00
pycook
0196c8a82c release 2.3.2 2023-09-07 13:44:51 +08:00
36 changed files with 2119 additions and 275 deletions

View File

@@ -9,6 +9,7 @@ import time
import click import click
from flask import current_app from flask import current_app
from flask.cli import with_appcontext from flask.cli import with_appcontext
from flask_login import login_user
import api.lib.cmdb.ci import api.lib.cmdb.ci
from api.extensions import db from api.extensions import db
@@ -24,6 +25,7 @@ from api.lib.cmdb.const import ValueTypeEnum
from api.lib.exception import AbortException from api.lib.exception import AbortException
from api.lib.perm.acl.acl import ACLManager from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.cache import AppCache from api.lib.perm.acl.cache import AppCache
from api.lib.perm.acl.cache import UserCache
from api.lib.perm.acl.resource import ResourceCRUD from api.lib.perm.acl.resource import ResourceCRUD
from api.lib.perm.acl.resource import ResourceTypeCRUD from api.lib.perm.acl.resource import ResourceTypeCRUD
from api.lib.perm.acl.role import RoleCRUD from api.lib.perm.acl.role import RoleCRUD
@@ -207,6 +209,8 @@ def cmdb_counter():
""" """
from api.lib.cmdb.cache import CMDBCounterCache from api.lib.cmdb.cache import CMDBCounterCache
current_app.test_request_context().push()
login_user(UserCache.get('worker'))
while True: while True:
try: try:
db.session.remove() db.session.remove()

View File

@@ -161,6 +161,55 @@ class InitDepartment(object):
info = f"update department acl_rid: {acl_rid}" info = f"update department acl_rid: {acl_rid}"
current_app.logger.info(info) current_app.logger.info(info)
def init_backend_resource(self):
acl = self.check_app('backend')
resources_types = acl.get_all_resources_types()
results = list(filter(lambda t: t['name'] == '操作权限', resources_types['groups']))
if len(results) == 0:
payload = dict(
app_id=acl.app_name,
name='操作权限',
description='',
perms=['read', 'grant', 'delete', 'update']
)
resource_type = acl.create_resources_type(payload)
else:
resource_type = results[0]
for name in ['公司信息']:
payload = dict(
type_id=resource_type['id'],
app_id=acl.app_name,
name=name,
)
try:
acl.create_resource(payload)
except Exception as e:
if '已经存在' in str(e):
pass
else:
raise Exception(e)
def check_app(self, app_name):
acl = ACLManager(app_name)
payload = dict(
name=app_name,
description=app_name
)
try:
app = acl.validate_app()
if app:
return acl
acl.create_app(payload)
except Exception as e:
current_app.logger.error(e)
if '不存在' in str(e):
acl.create_app(payload)
return acl
raise Exception(e)
@click.command() @click.command()
@with_appcontext @with_appcontext
@@ -177,5 +226,7 @@ def init_department():
""" """
Department initialization Department initialization
""" """
InitDepartment().init() cli = InitDepartment()
InitDepartment().create_acl_role_with_department() cli.init_wide_company()
cli.create_acl_role_with_department()
cli.init_backend_resource()

View File

@@ -163,13 +163,15 @@ class AttributeManager(object):
if RoleEnum.CONFIG not in session.get("acl", {}).get("parentRoles", []) and not is_app_admin('cmdb'): if RoleEnum.CONFIG not in session.get("acl", {}).get("parentRoles", []) and not is_app_admin('cmdb'):
return abort(403, ErrFormat.role_required.format(RoleEnum.CONFIG)) return abort(403, ErrFormat.role_required.format(RoleEnum.CONFIG))
@staticmethod @classmethod
def calc_computed_attribute(attr_id): def calc_computed_attribute(cls, attr_id):
""" """
calculate computed attribute for all ci calculate computed attribute for all ci
:param attr_id: :param attr_id:
:return: :return:
""" """
cls.can_create_computed_attribute()
from api.tasks.cmdb import calc_computed_attribute from api.tasks.cmdb import calc_computed_attribute
calc_computed_attribute.apply_async(args=(attr_id, current_user.uid), queue=CMDB_QUEUE) calc_computed_attribute.apply_async(args=(attr_id, current_user.uid), queue=CMDB_QUEUE)

View File

@@ -2,14 +2,11 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import requests
from flask import current_app from flask import current_app
from api.extensions import cache from api.extensions import cache
from api.extensions import db
from api.lib.cmdb.custom_dashboard import CustomDashboardManager from api.lib.cmdb.custom_dashboard import CustomDashboardManager
from api.models.cmdb import Attribute from api.models.cmdb import Attribute
from api.models.cmdb import CI
from api.models.cmdb import CIType from api.models.cmdb import CIType
from api.models.cmdb import CITypeAttribute from api.models.cmdb import CITypeAttribute
from api.models.cmdb import RelationType from api.models.cmdb import RelationType
@@ -210,7 +207,6 @@ class CITypeAttributeCache(object):
@classmethod @classmethod
def get(cls, type_id, attr_id): def get(cls, type_id, attr_id):
attr = cache.get(cls.PREFIX_ID.format(type_id, attr_id)) attr = cache.get(cls.PREFIX_ID.format(type_id, attr_id))
attr = attr or cache.get(cls.PREFIX_ID.format(type_id, attr_id)) attr = attr or cache.get(cls.PREFIX_ID.format(type_id, attr_id))
attr = attr or CITypeAttribute.get_by(type_id=type_id, attr_id=attr_id, first=True, to_dict=False) attr = attr or CITypeAttribute.get_by(type_id=type_id, attr_id=attr_id, first=True, to_dict=False)
@@ -251,53 +247,72 @@ class CMDBCounterCache(object):
result = {} result = {}
for custom in customs: for custom in customs:
if custom['category'] == 0: if custom['category'] == 0:
result[custom['id']] = cls.summary_counter(custom['type_id']) res = cls.sum_counter(custom)
elif custom['category'] == 1: elif custom['category'] == 1:
result[custom['id']] = cls.attribute_counter(custom['type_id'], custom['attr_id']) res = cls.attribute_counter(custom)
elif custom['category'] == 2: else:
result[custom['id']] = cls.relation_counter(custom['type_id'], custom['level']) res = cls.relation_counter(custom.get('type_id'),
custom.get('level'),
custom.get('options', {}).get('filter', ''),
custom.get('options', {}).get('type_ids', ''))
if res:
result[custom['id']] = res
cls.set(result) cls.set(result)
return result return result
@classmethod @classmethod
def update(cls, custom): def update(cls, custom, flush=True):
result = cache.get(cls.KEY) or {} result = cache.get(cls.KEY) or {}
if not result: if not result:
result = cls.reset() result = cls.reset()
if custom['category'] == 0: if custom['category'] == 0:
result[custom['id']] = cls.summary_counter(custom['type_id']) res = cls.sum_counter(custom)
elif custom['category'] == 1: elif custom['category'] == 1:
result[custom['id']] = cls.attribute_counter(custom['type_id'], custom['attr_id']) res = cls.attribute_counter(custom)
elif custom['category'] == 2: else:
result[custom['id']] = cls.relation_counter(custom['type_id'], custom['level']) res = cls.relation_counter(custom.get('type_id'),
custom.get('level'),
custom.get('options', {}).get('filter', ''),
custom.get('options', {}).get('type_ids', ''))
cls.set(result) if res and flush:
result[custom['id']] = res
cls.set(result)
return res
@staticmethod @staticmethod
def summary_counter(type_id): def relation_counter(type_id, level, other_filer, type_ids):
return db.session.query(CI.id).filter(CI.deleted.is_(False)).filter(CI.type_id == type_id).count() from api.lib.cmdb.search.ci_relation.search import Search as RelSearch
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
@staticmethod query = "_type:{}".format(type_id)
def relation_counter(type_id, level): s = search(query, count=1000000)
try:
type_names, _, _, _, _, _ = s.search()
except SearchError as e:
current_app.logger.error(e)
return
uri = current_app.config.get('CMDB_API')
type_names = requests.get("{}/ci/s?q=_type:{}&count=10000".format(uri, type_id)).json().get('result')
type_id_names = [(str(i.get('_id')), i.get(i.get('unique'))) for i in type_names] type_id_names = [(str(i.get('_id')), i.get(i.get('unique'))) for i in type_names]
url = "{}/ci_relations/statistics?root_ids={}&level={}".format( s = RelSearch([i[0] for i in type_id_names], level, other_filer or '')
uri, ','.join([i[0] for i in type_id_names]), level) try:
stats = requests.get(url).json() stats = s.statistics(type_ids)
except SearchError as e:
current_app.logger.error(e)
return
id2name = dict(type_id_names) id2name = dict(type_id_names)
type_ids = set() type_ids = set()
for i in (stats.get('detail') or []): for i in (stats.get('detail') or []):
for j in stats['detail'][i]: for j in stats['detail'][i]:
type_ids.add(j) type_ids.add(j)
for type_id in type_ids: for type_id in type_ids:
_type = CITypeCache.get(type_id) _type = CITypeCache.get(type_id)
id2name[type_id] = _type and _type.alias id2name[type_id] = _type and _type.alias
@@ -317,9 +332,94 @@ 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()
if res.get('facet'): custom.setdefault('options', {})
return dict([i[:2] for i in list(res.get('facet').values())[0]]) 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])))
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[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][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][i[0]] = i[1]
return result
@staticmethod
def sum_counter(custom):
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
custom.setdefault('options', {})
type_id = custom.get('type_id')
type_ids = custom['options'].get('type_ids') or (type_id and [type_id])
other_filter = custom['options'].get('filter') or ''
query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter)
s = search(query, count=1)
try:
_, _, _, _, numfound, _ = s.search()
except SearchError as e:
current_app.logger.error(e)
return
return numfound

View File

@@ -1,11 +1,13 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
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"]
@@ -276,10 +278,10 @@ class CITypeGroupManager(object):
def update(gid, name, type_ids): def update(gid, name, type_ids):
""" """
update part update part
:param gid: :param gid:
:param name: :param name:
:param type_ids: :param type_ids:
:return: :return:
""" """
existed = CITypeGroup.get_by_id(gid) or abort( existed = CITypeGroup.get_by_id(gid) or abort(
404, ErrFormat.ci_type_group_not_found.format("id={}".format(gid))) 404, ErrFormat.ci_type_group_not_found.format("id={}".format(gid)))
@@ -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)
@@ -386,10 +398,10 @@ class CITypeAttributeManager(object):
def add(cls, type_id, attr_ids=None, **kwargs): def add(cls, type_id, attr_ids=None, **kwargs):
""" """
add attributes to CIType add attributes to CIType
:param type_id: :param type_id:
:param attr_ids: list :param attr_ids: list
:param kwargs: :param kwargs:
:return: :return:
""" """
attr_ids = list(set(attr_ids)) attr_ids = list(set(attr_ids))
@@ -416,9 +428,9 @@ class CITypeAttributeManager(object):
def update(cls, type_id, attributes): def update(cls, type_id, attributes):
""" """
update attributes to CIType update attributes to CIType
:param type_id: :param type_id:
:param attributes: list :param attributes: list
:return: :return:
""" """
cls._check(type_id, [i.get('attr_id') for i in attributes]) cls._check(type_id, [i.get('attr_id') for i in attributes])
@@ -446,9 +458,9 @@ class CITypeAttributeManager(object):
def delete(cls, type_id, attr_ids=None): def delete(cls, type_id, attr_ids=None):
""" """
delete attributes from CIType delete attributes from CIType
:param type_id: :param type_id:
:param attr_ids: list :param attr_ids: list
:return: :return:
""" """
from api.tasks.cmdb import ci_cache from api.tasks.cmdb import ci_cache
@@ -564,6 +576,22 @@ 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)
result[level + 1] = [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 +614,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 +862,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])

View File

@@ -14,6 +14,14 @@ class CustomDashboardManager(object):
def get(): def get():
return sorted(CustomDashboard.get_by(to_dict=True), key=lambda x: (x["category"], x['order'])) return sorted(CustomDashboard.get_by(to_dict=True), key=lambda x: (x["category"], x['order']))
@staticmethod
def preview(**kwargs):
from api.lib.cmdb.cache import CMDBCounterCache
res = CMDBCounterCache.update(kwargs, flush=False)
return res
@staticmethod @staticmethod
def add(**kwargs): def add(**kwargs):
from api.lib.cmdb.cache import CMDBCounterCache from api.lib.cmdb.cache import CMDBCounterCache
@@ -23,9 +31,9 @@ class CustomDashboardManager(object):
new = CustomDashboard.create(**kwargs) new = CustomDashboard.create(**kwargs)
CMDBCounterCache.update(new.to_dict()) res = CMDBCounterCache.update(new.to_dict())
return new return new, res
@staticmethod @staticmethod
def update(_id, **kwargs): def update(_id, **kwargs):
@@ -35,9 +43,9 @@ class CustomDashboardManager(object):
new = existed.update(**kwargs) new = existed.update(**kwargs)
CMDBCounterCache.update(new.to_dict()) res = CMDBCounterCache.update(new.to_dict())
return new return new, res
@staticmethod @staticmethod
def batch_update(id2options): def batch_update(id2options):

View File

@@ -42,7 +42,7 @@ FACET_QUERY1 = """
FACET_QUERY = """ FACET_QUERY = """
SELECT {0}.value, SELECT {0}.value,
count({0}.ci_id) count(distinct({0}.ci_id))
FROM {0} FROM {0}
INNER JOIN ({1}) AS F ON F.ci_id={0}.ci_id INNER JOIN ({1}) AS F ON F.ci_id={0}.ci_id
WHERE {0}.attr_id={2:d} WHERE {0}.attr_id={2:d}

View File

@@ -6,6 +6,7 @@ from api.lib.common_setting.resp_format import ErrFormat
from api.lib.perm.acl.cache import RoleCache, AppCache from api.lib.perm.acl.cache import RoleCache, AppCache
from api.lib.perm.acl.role import RoleCRUD, RoleRelationCRUD from api.lib.perm.acl.role import RoleCRUD, RoleRelationCRUD
from api.lib.perm.acl.user import UserCRUD from api.lib.perm.acl.user import UserCRUD
from api.lib.perm.acl.resource import ResourceTypeCRUD, ResourceCRUD
class ACLManager(object): class ACLManager(object):
@@ -94,3 +95,22 @@ class ACLManager(object):
avatar=user_info.get('avatar')) avatar=user_info.get('avatar'))
return result return result
def validate_app(self):
return AppCache.get(self.app_name)
def get_all_resources_types(self, q=None, page=1, page_size=999999):
app_id = self.validate_app().id
numfound, res, id2perms = ResourceTypeCRUD.search(q, app_id, page, page_size)
return dict(
numfound=numfound,
groups=[i.to_dict() for i in res],
id2perms=id2perms
)
def create_resource(self, payload):
payload['app_id'] = self.validate_app().id
resource = ResourceCRUD.add(**payload)
return resource.to_dict()

View File

@@ -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格式似乎不正确了, 请仔细确认一下!"

View File

@@ -56,12 +56,7 @@ class AttributeView(APIView):
@args_required("name") @args_required("name")
@args_validate(AttributeManager.cls) @args_validate(AttributeManager.cls)
def post(self, attr_id=None): def post(self):
if request.url.endswith("/calc_computed_attribute"):
AttributeManager.calc_computed_attribute(attr_id)
return self.jsonify(attr_id=attr_id)
choice_value = handle_arg_list(request.values.get("choice_value")) choice_value = handle_arg_list(request.values.get("choice_value"))
params = request.values params = request.values
params["choice_value"] = choice_value params["choice_value"] = choice_value
@@ -74,6 +69,11 @@ class AttributeView(APIView):
@args_validate(AttributeManager.cls) @args_validate(AttributeManager.cls)
def put(self, attr_id): def put(self, attr_id):
if request.url.endswith("/calc_computed_attribute"):
AttributeManager.calc_computed_attribute(attr_id)
return self.jsonify(attr_id=attr_id)
choice_value = handle_arg_list(request.values.get("choice_value")) choice_value = handle_arg_list(request.values.get("choice_value"))
params = request.values params = request.values
params["choice_value"] = choice_value params["choice_value"] = choice_value

View File

@@ -1,4 +1,4 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
import json import json
@@ -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
@@ -500,3 +506,4 @@ class CITypeFilterPermissionView(APIView):
@auth_with_app_token @auth_with_app_token
def get(self, type_id): def get(self, type_id):
return self.jsonify(CIFilterPermsCRUD().get(type_id)) return self.jsonify(CIFilterPermsCRUD().get(type_id))

View File

@@ -19,9 +19,14 @@ from api.resource import APIView
class GetChildrenView(APIView): class GetChildrenView(APIView):
url_prefix = "/ci_type_relations/<int:parent_id>/children" url_prefix = ("/ci_type_relations/<int:parent_id>/children",
"/ci_type_relations/<int:parent_id>/recursive_level2children",
)
def get(self, parent_id): def get(self, parent_id):
if request.url.endswith("recursive_level2children"):
return self.jsonify(CITypeRelationManager.recursive_level2children(parent_id))
return self.jsonify(children=CITypeRelationManager.get_children(parent_id)) return self.jsonify(children=CITypeRelationManager.get_children(parent_id))

View File

@@ -13,7 +13,8 @@ from api.resource import APIView
class CustomDashboardApiView(APIView): class CustomDashboardApiView(APIView):
url_prefix = ("/custom_dashboard", "/custom_dashboard/<int:_id>", "/custom_dashboard/batch") url_prefix = ("/custom_dashboard", "/custom_dashboard/<int:_id>", "/custom_dashboard/batch",
"/custom_dashboard/preview")
def get(self): def get(self):
return self.jsonify(CustomDashboardManager.get()) return self.jsonify(CustomDashboardManager.get())
@@ -21,17 +22,26 @@ class CustomDashboardApiView(APIView):
@role_required(RoleEnum.CONFIG) @role_required(RoleEnum.CONFIG)
@args_validate(CustomDashboardManager.cls) @args_validate(CustomDashboardManager.cls)
def post(self): def post(self):
cm = CustomDashboardManager.add(**request.values) if request.url.endswith("/preview"):
return self.jsonify(counter=CustomDashboardManager.preview(**request.values))
return self.jsonify(cm.to_dict()) cm, counter = CustomDashboardManager.add(**request.values)
res = cm.to_dict()
res.update(counter=counter)
return self.jsonify(res)
@role_required(RoleEnum.CONFIG) @role_required(RoleEnum.CONFIG)
@args_validate(CustomDashboardManager.cls) @args_validate(CustomDashboardManager.cls)
def put(self, _id=None): def put(self, _id=None):
if _id is not None: if _id is not None:
cm = CustomDashboardManager.update(_id, **request.values) cm, counter = CustomDashboardManager.update(_id, **request.values)
return self.jsonify(cm.to_dict()) res = cm.to_dict()
res.update(counter=counter)
return self.jsonify(res)
CustomDashboardManager.batch_update(request.values.get("id2options")) CustomDashboardManager.batch_update(request.values.get("id2options"))

View File

@@ -94,5 +94,3 @@ ES_HOST = '127.0.0.1'
USE_ES = False USE_ES = False
BOOL_TRUE = ['true', 'TRUE', 'True', True, '1', 1, "Yes", "YES", "yes", 'Y', 'y'] BOOL_TRUE = ['true', 'TRUE', 'True', True, '1', 1, "Yes", "YES", "yes", 'Y', 'y']
CMDB_API = "http://127.0.0.1:5000/api/v0.1"

View File

@@ -54,6 +54,84 @@
<div class="content unicode" style="display: block;"> <div class="content unicode" style="display: block;">
<ul class="icon_lists dib-box"> <ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont">&#xe886;</span>
<div class="name">cmdb-histogram</div>
<div class="code-name">&amp;#xe886;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe883;</span>
<div class="name">cmdb-index</div>
<div class="code-name">&amp;#xe883;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe884;</span>
<div class="name">cmdb-piechart</div>
<div class="code-name">&amp;#xe884;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe885;</span>
<div class="name">cmdb-line</div>
<div class="code-name">&amp;#xe885;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe882;</span>
<div class="name">cmdb-table</div>
<div class="code-name">&amp;#xe882;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe87f;</span>
<div class="name">itsm-all</div>
<div class="code-name">&amp;#xe87f;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe87e;</span>
<div class="name">itsm-reply</div>
<div class="code-name">&amp;#xe87e;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe880;</span>
<div class="name">itsm-information</div>
<div class="code-name">&amp;#xe880;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe881;</span>
<div class="name">itsm-contact</div>
<div class="code-name">&amp;#xe881;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe87d;</span>
<div class="name">itsm-my-processed</div>
<div class="code-name">&amp;#xe87d;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe87c;</span>
<div class="name">rule_7</div>
<div class="code-name">&amp;#xe87c;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe879;</span>
<div class="name">itsm-my-completed</div>
<div class="code-name">&amp;#xe879;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe87b;</span>
<div class="name">itsm-my-plan</div>
<div class="code-name">&amp;#xe87b;</div>
</li>
<li class="dib"> <li class="dib">
<span class="icon iconfont">&#xe87a;</span> <span class="icon iconfont">&#xe87a;</span>
<div class="name">rule_100</div> <div class="name">rule_100</div>
@@ -3876,9 +3954,9 @@
<pre><code class="language-css" <pre><code class="language-css"
>@font-face { >@font-face {
font-family: 'iconfont'; font-family: 'iconfont';
src: url('iconfont.woff2?t=1688550067963') format('woff2'), src: url('iconfont.woff2?t=1694508259411') format('woff2'),
url('iconfont.woff?t=1688550067963') format('woff'), url('iconfont.woff?t=1694508259411') format('woff'),
url('iconfont.ttf?t=1688550067963') format('truetype'); url('iconfont.ttf?t=1694508259411') format('truetype');
} }
</code></pre> </code></pre>
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3> <h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@@ -3904,6 +3982,123 @@
<div class="content font-class"> <div class="content font-class">
<ul class="icon_lists dib-box"> <ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont cmdb-bar"></span>
<div class="name">
cmdb-histogram
</div>
<div class="code-name">.cmdb-bar
</div>
</li>
<li class="dib">
<span class="icon iconfont cmdb-count"></span>
<div class="name">
cmdb-index
</div>
<div class="code-name">.cmdb-count
</div>
</li>
<li class="dib">
<span class="icon iconfont cmdb-pie"></span>
<div class="name">
cmdb-piechart
</div>
<div class="code-name">.cmdb-pie
</div>
</li>
<li class="dib">
<span class="icon iconfont cmdb-line"></span>
<div class="name">
cmdb-line
</div>
<div class="code-name">.cmdb-line
</div>
</li>
<li class="dib">
<span class="icon iconfont cmdb-table"></span>
<div class="name">
cmdb-table
</div>
<div class="code-name">.cmdb-table
</div>
</li>
<li class="dib">
<span class="icon iconfont itsm-all"></span>
<div class="name">
itsm-all
</div>
<div class="code-name">.itsm-all
</div>
</li>
<li class="dib">
<span class="icon iconfont itsm-reply"></span>
<div class="name">
itsm-reply
</div>
<div class="code-name">.itsm-reply
</div>
</li>
<li class="dib">
<span class="icon iconfont itsm-information"></span>
<div class="name">
itsm-information
</div>
<div class="code-name">.itsm-information
</div>
</li>
<li class="dib">
<span class="icon iconfont itsm-contact"></span>
<div class="name">
itsm-contact
</div>
<div class="code-name">.itsm-contact
</div>
</li>
<li class="dib">
<span class="icon iconfont itsm-my-my_already_handle"></span>
<div class="name">
itsm-my-processed
</div>
<div class="code-name">.itsm-my-my_already_handle
</div>
</li>
<li class="dib">
<span class="icon iconfont rule_7"></span>
<div class="name">
rule_7
</div>
<div class="code-name">.rule_7
</div>
</li>
<li class="dib">
<span class="icon iconfont itsm-my-completed"></span>
<div class="name">
itsm-my-completed
</div>
<div class="code-name">.itsm-my-completed
</div>
</li>
<li class="dib">
<span class="icon iconfont itsm-my-plan"></span>
<div class="name">
itsm-my-plan
</div>
<div class="code-name">.itsm-my-plan
</div>
</li>
<li class="dib"> <li class="dib">
<span class="icon iconfont rule_100"></span> <span class="icon iconfont rule_100"></span>
<div class="name"> <div class="name">
@@ -5759,11 +5954,11 @@
</li> </li>
<li class="dib"> <li class="dib">
<span class="icon iconfont itsm-node-strat"></span> <span class="icon iconfont itsm-node-start"></span>
<div class="name"> <div class="name">
itsm-node-strat itsm-node-strat
</div> </div>
<div class="code-name">.itsm-node-strat <div class="code-name">.itsm-node-start
</div> </div>
</li> </li>
@@ -9637,6 +9832,110 @@
<div class="content symbol"> <div class="content symbol">
<ul class="icon_lists dib-box"> <ul class="icon_lists dib-box">
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#cmdb-bar"></use>
</svg>
<div class="name">cmdb-histogram</div>
<div class="code-name">#cmdb-bar</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#cmdb-count"></use>
</svg>
<div class="name">cmdb-index</div>
<div class="code-name">#cmdb-count</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#cmdb-pie"></use>
</svg>
<div class="name">cmdb-piechart</div>
<div class="code-name">#cmdb-pie</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#cmdb-line"></use>
</svg>
<div class="name">cmdb-line</div>
<div class="code-name">#cmdb-line</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#cmdb-table"></use>
</svg>
<div class="name">cmdb-table</div>
<div class="code-name">#cmdb-table</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-all"></use>
</svg>
<div class="name">itsm-all</div>
<div class="code-name">#itsm-all</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-reply"></use>
</svg>
<div class="name">itsm-reply</div>
<div class="code-name">#itsm-reply</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-information"></use>
</svg>
<div class="name">itsm-information</div>
<div class="code-name">#itsm-information</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-contact"></use>
</svg>
<div class="name">itsm-contact</div>
<div class="code-name">#itsm-contact</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-my-my_already_handle"></use>
</svg>
<div class="name">itsm-my-processed</div>
<div class="code-name">#itsm-my-my_already_handle</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#rule_7"></use>
</svg>
<div class="name">rule_7</div>
<div class="code-name">#rule_7</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-my-completed"></use>
</svg>
<div class="name">itsm-my-completed</div>
<div class="code-name">#itsm-my-completed</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-my-plan"></use>
</svg>
<div class="name">itsm-my-plan</div>
<div class="code-name">#itsm-my-plan</div>
</li>
<li class="dib"> <li class="dib">
<svg class="icon svg-icon" aria-hidden="true"> <svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#rule_100"></use> <use xlink:href="#rule_100"></use>
@@ -11287,10 +11586,10 @@
<li class="dib"> <li class="dib">
<svg class="icon svg-icon" aria-hidden="true"> <svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-node-strat"></use> <use xlink:href="#itsm-node-start"></use>
</svg> </svg>
<div class="name">itsm-node-strat</div> <div class="name">itsm-node-strat</div>
<div class="code-name">#itsm-node-strat</div> <div class="code-name">#itsm-node-start</div>
</li> </li>
<li class="dib"> <li class="dib">

View File

@@ -1,8 +1,8 @@
@font-face { @font-face {
font-family: "iconfont"; /* Project id 3857903 */ font-family: "iconfont"; /* Project id 3857903 */
src: url('iconfont.woff2?t=1688550067963') format('woff2'), src: url('iconfont.woff2?t=1694508259411') format('woff2'),
url('iconfont.woff?t=1688550067963') format('woff'), url('iconfont.woff?t=1694508259411') format('woff'),
url('iconfont.ttf?t=1688550067963') format('truetype'); url('iconfont.ttf?t=1694508259411') format('truetype');
} }
.iconfont { .iconfont {
@@ -13,6 +13,58 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.cmdb-bar:before {
content: "\e886";
}
.cmdb-count:before {
content: "\e883";
}
.cmdb-pie:before {
content: "\e884";
}
.cmdb-line:before {
content: "\e885";
}
.cmdb-table:before {
content: "\e882";
}
.itsm-all:before {
content: "\e87f";
}
.itsm-reply:before {
content: "\e87e";
}
.itsm-information:before {
content: "\e880";
}
.itsm-contact:before {
content: "\e881";
}
.itsm-my-my_already_handle:before {
content: "\e87d";
}
.rule_7:before {
content: "\e87c";
}
.itsm-my-completed:before {
content: "\e879";
}
.itsm-my-plan:before {
content: "\e87b";
}
.rule_100:before { .rule_100:before {
content: "\e87a"; content: "\e87a";
} }
@@ -837,7 +889,7 @@
content: "\e7ad"; content: "\e7ad";
} }
.itsm-node-strat:before { .itsm-node-start:before {
content: "\e7ae"; content: "\e7ae";
} }

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,97 @@
"css_prefix_text": "", "css_prefix_text": "",
"description": "", "description": "",
"glyphs": [ "glyphs": [
{
"icon_id": "37334642",
"name": "cmdb-histogram",
"font_class": "cmdb-bar",
"unicode": "e886",
"unicode_decimal": 59526
},
{
"icon_id": "37334651",
"name": "cmdb-index",
"font_class": "cmdb-count",
"unicode": "e883",
"unicode_decimal": 59523
},
{
"icon_id": "37334650",
"name": "cmdb-piechart",
"font_class": "cmdb-pie",
"unicode": "e884",
"unicode_decimal": 59524
},
{
"icon_id": "37334648",
"name": "cmdb-line",
"font_class": "cmdb-line",
"unicode": "e885",
"unicode_decimal": 59525
},
{
"icon_id": "37334627",
"name": "cmdb-table",
"font_class": "cmdb-table",
"unicode": "e882",
"unicode_decimal": 59522
},
{
"icon_id": "37310392",
"name": "itsm-all",
"font_class": "itsm-all",
"unicode": "e87f",
"unicode_decimal": 59519
},
{
"icon_id": "36998696",
"name": "itsm-reply",
"font_class": "itsm-reply",
"unicode": "e87e",
"unicode_decimal": 59518
},
{
"icon_id": "36639018",
"name": "itsm-information",
"font_class": "itsm-information",
"unicode": "e880",
"unicode_decimal": 59520
},
{
"icon_id": "36639017",
"name": "itsm-contact",
"font_class": "itsm-contact",
"unicode": "e881",
"unicode_decimal": 59521
},
{
"icon_id": "36557425",
"name": "itsm-my-processed",
"font_class": "itsm-my-my_already_handle",
"unicode": "e87d",
"unicode_decimal": 59517
},
{
"icon_id": "36488174",
"name": "rule_7",
"font_class": "rule_7",
"unicode": "e87c",
"unicode_decimal": 59516
},
{
"icon_id": "36380087",
"name": "itsm-my-completed",
"font_class": "itsm-my-completed",
"unicode": "e879",
"unicode_decimal": 59513
},
{
"icon_id": "36380096",
"name": "itsm-my-plan",
"font_class": "itsm-my-plan",
"unicode": "e87b",
"unicode_decimal": 59515
},
{ {
"icon_id": "36304673", "icon_id": "36304673",
"name": "rule_100", "name": "rule_100",
@@ -1450,7 +1541,7 @@
{ {
"icon_id": "35024980", "icon_id": "35024980",
"name": "itsm-node-strat", "name": "itsm-node-strat",
"font_class": "itsm-node-strat", "font_class": "itsm-node-start",
"unicode": "e7ae", "unicode": "e7ae",
"unicode_decimal": 59310 "unicode_decimal": 59310
}, },

Binary file not shown.

View File

@@ -68,7 +68,8 @@ export default {
}, },
methods: { methods: {
visibleChange(open) { visibleChange(open, isInitOne = true) {
// isInitOne 初始化exp为空时ruleList是否默认给一条
// const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g // const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
const exp = this.expression.match(new RegExp(this.regQ, 'g')) const exp = this.expression.match(new RegExp(this.regQ, 'g'))
? this.expression.match(new RegExp(this.regQ, 'g'))[0] ? this.expression.match(new RegExp(this.regQ, 'g'))[0]
@@ -151,15 +152,20 @@ export default {
}) })
this.ruleList = [...expArray] this.ruleList = [...expArray]
} else if (open) { } else if (open) {
this.ruleList = [ this.ruleList = isInitOne
{ ? [
id: uuidv4(), {
type: 'and', id: uuidv4(),
property: this.canSearchPreferenceAttrList[0].name, type: 'and',
exp: 'is', property:
value: null, this.canSearchPreferenceAttrList && this.canSearchPreferenceAttrList.length
}, ? this.canSearchPreferenceAttrList[0].name
] : undefined,
exp: 'is',
value: null,
},
]
: []
} }
}, },
handleClear() { handleClear() {

View File

@@ -77,6 +77,14 @@ export function getCITypeAttributesByTypeIds(params) {
}) })
} }
export function getCITypeCommonAttributesByTypeIds(params) {
return axios({
url: `/v0.1/ci_types/common_attributes`,
method: 'get',
params: params
})
}
/** /**
* 删除属性 * 删除属性
* @param attrId * @param attrId
@@ -153,3 +161,10 @@ export function canDefineComputed() {
method: 'HEAD', method: 'HEAD',
}) })
} }
export function calcComputedAttribute(attr_id) {
return axios({
url: `/v0.1/attributes/${attr_id}/calc_computed_attribute`,
method: 'PUT',
})
}

View File

@@ -61,3 +61,10 @@ export function revokeTypeRelation(first_type_id, second_type_id, rid, data) {
data data
}) })
} }
export function getRecursive_level2children(type_id) {
return axios({
url: `/v0.1/ci_type_relations/${type_id}/recursive_level2children`,
method: 'GET'
})
}

View File

@@ -37,3 +37,11 @@ export function batchUpdateCustomDashboard(data) {
data data
}) })
} }
export function postCustomDashboardPreview(data) {
return axios({
url: '/v0.1/custom_dashboard/preview',
method: 'post',
data
})
}

View File

@@ -51,6 +51,9 @@
<a-space class="attribute-card-operation"> <a-space class="attribute-card-operation">
<a v-if="!isStore"><a-icon type="edit" @click="handleEdit"/></a> <a v-if="!isStore"><a-icon type="edit" @click="handleEdit"/></a>
<a-tooltip title="所有CI触发计算">
<a v-if="!isStore && property.is_computed"><a-icon type="redo" @click="handleCalcComputed"/></a>
</a-tooltip>
<a style="color:red;"><a-icon type="delete" @click="handleDelete"/></a> <a style="color:red;"><a-icon type="delete" @click="handleDelete"/></a>
</a-space> </a-space>
</div> </div>
@@ -59,7 +62,7 @@
</template> </template>
<script> <script>
import { deleteCITypeAttributesById, deleteAttributesById } from '@/modules/cmdb/api/CITypeAttr' import { deleteCITypeAttributesById, deleteAttributesById, calcComputedAttribute } from '@/modules/cmdb/api/CITypeAttr'
import ValueTypeIcon from '@/components/CMDBValueTypeMapIcon' import ValueTypeIcon from '@/components/CMDBValueTypeMapIcon'
import { import {
ops_default_show, ops_default_show,
@@ -165,6 +168,18 @@ export default {
openTrigger() { openTrigger() {
this.$refs.triggerForm.open(this.property) this.$refs.triggerForm.open(this.property)
}, },
handleCalcComputed() {
const that = this
this.$confirm({
title: '警告',
content: `确认触发所有CI的计算`,
onOk() {
calcComputedAttribute(that.property.id).then(() => {
that.$message.success('触发成功!')
})
},
})
},
}, },
} }
</script> </script>

View File

@@ -10,7 +10,7 @@
:headerStyle="{ borderBottom: 'none' }" :headerStyle="{ borderBottom: 'none' }"
wrapClassName="attribute-edit-form" wrapClassName="attribute-edit-form"
> >
<a-form :form="form" :layout="formLayout" @submit="handleSubmit"> <a-form :form="form" :layout="formLayout">
<a-divider style="font-size:14px;margin-top:6px;">基础设置</a-divider> <a-divider style="font-size:14px;margin-top:6px;">基础设置</a-divider>
<a-col :span="12"> <a-col :span="12">
<a-form-item :label-col="formItemLayout.labelCol" :wrapper-col="formItemLayout.wrapperCol" label="属性名(英文)"> <a-form-item :label-col="formItemLayout.labelCol" :wrapper-col="formItemLayout.wrapperCol" label="属性名(英文)">
@@ -343,7 +343,13 @@
name="is_password" name="is_password"
v-decorator="['is_computed', { rules: [], valuePropName: 'checked' }]" v-decorator="['is_computed', { rules: [], valuePropName: 'checked' }]"
/> />
<ComputedArea ref="computedArea" v-show="isShowComputedArea" :canDefineComputed="canDefineComputed" /> <ComputedArea
showCalcComputed
ref="computedArea"
v-show="isShowComputedArea"
@handleCalcComputed="handleCalcComputed"
:canDefineComputed="canDefineComputed"
/>
</a-form-item> </a-form-item>
</a-col> </a-col>
</a-row> </a-row>
@@ -353,7 +359,7 @@
</a-form-item> </a-form-item>
<div class="custom-drawer-bottom-action"> <div class="custom-drawer-bottom-action">
<a-button @click="onClose">取消</a-button> <a-button @click="onClose">取消</a-button>
<a-button @click="handleSubmit" type="primary">确定</a-button> <a-button @click="handleSubmit(false)" type="primary">确定</a-button>
</div> </div>
</a-form> </a-form>
</CustomDrawer> </CustomDrawer>
@@ -366,6 +372,7 @@ import {
updateAttributeById, updateAttributeById,
updateCITypeAttributesById, updateCITypeAttributesById,
canDefineComputed, canDefineComputed,
calcComputedAttribute,
} from '@/modules/cmdb/api/CITypeAttr' } from '@/modules/cmdb/api/CITypeAttr'
import { valueTypeMap } from '../../utils/const' import { valueTypeMap } from '../../utils/const'
import ComputedArea from './computedArea.vue' import ComputedArea from './computedArea.vue'
@@ -576,15 +583,14 @@ export default {
}) })
}, },
handleSubmit(e) { async handleSubmit(isCalcComputed = false) {
e.preventDefault() await this.form.validateFields(async (err, values) => {
this.form.validateFields((err, values) => {
if (!err) { if (!err) {
console.log('Received values of form: ', values) console.log('Received values of form: ', values)
if (this.record.is_required !== values.is_required || this.record.default_show !== values.default_show) { if (this.record.is_required !== values.is_required || this.record.default_show !== values.default_show) {
console.log('changed is_required') console.log('changed is_required')
updateCITypeAttributesById(this.CITypeId, { await updateCITypeAttributesById(this.CITypeId, {
attributes: [ attributes: [
{ attr_id: this.record.id, is_required: values.is_required, default_show: values.default_show }, { attr_id: this.record.id, is_required: values.is_required, default_show: values.default_show },
], ],
@@ -630,19 +636,21 @@ export default {
const fontOptions = this.$refs.fontArea.getData() const fontOptions = this.$refs.fontArea.getData()
if (values.id) { if (values.id) {
this.updateAttribute(values.id, { ...values, option: { fontOptions } }) await this.updateAttribute(values.id, { ...values, option: { fontOptions } }, isCalcComputed)
} else { } else {
// this.createAttribute(values) // this.createAttribute(values)
} }
} }
}) })
}, },
updateAttribute(attrId, data) { async updateAttribute(attrId, data, isCalcComputed = false) {
updateAttributeById(attrId, data).then((res) => { await updateAttributeById(attrId, data)
this.$message.success(`更新成功`) if (isCalcComputed) {
this.handleOk() await calcComputedAttribute(attrId)
this.onClose() }
}) this.$message.success(`更新成功`)
this.handleOk()
this.onClose()
}, },
handleOk() { handleOk() {
this.$emit('ok') this.$emit('ok')
@@ -682,6 +690,9 @@ export default {
default_value: key, default_value: key,
}) })
}, },
async handleCalcComputed() {
await this.handleSubmit(true)
},
}, },
watch: {}, watch: {},
} }

View File

@@ -8,6 +8,14 @@
<span style="font-size:12px;" slot="tab">代码</span> <span style="font-size:12px;" slot="tab">代码</span>
<codemirror style="z-index: 9999" :options="cmOptions" v-model="compute_script"></codemirror> <codemirror style="z-index: 9999" :options="cmOptions" v-model="compute_script"></codemirror>
</a-tab-pane> </a-tab-pane>
<template slot="tabBarExtraContent" v-if="showCalcComputed">
<a-button type="primary" size="small" @click="handleCalcComputed">
应用
</a-button>
<a-tooltip title="所有CI触发计算">
<a-icon type="question-circle" style="margin-left:5px" />
</a-tooltip>
</template>
</a-tabs> </a-tabs>
</template> </template>
@@ -25,6 +33,10 @@ export default {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
showCalcComputed: {
type: Boolean,
default: false,
}
}, },
data() { data() {
return { return {
@@ -62,6 +74,16 @@ export default {
this.activeKey = 'expr' this.activeKey = 'expr'
} }
}, },
handleCalcComputed() {
const that = this
this.$confirm({
title: '警告',
content: `确认触发将保存当前配置及触发所有CI的计算`,
onOk() {
that.$emit('handleCalcComputed')
},
})
},
}, },
} }
</script> </script>

View File

@@ -1,11 +1,55 @@
<template> <template>
<div :style="{ width: '100%', height: 'calc(100% - 2.2vw)' }"> <div
<div v-if="category === 0" class="cmdb-dashboard-grid-item-chart"> :id="`cmdb-dashboard-${chartId}-${editable}-${isPreview}`"
:style="{ width: '100%', height: 'calc(100% - 2.2vw)' }"
>
<div
v-if="options.chartType === 'count'"
:style="{ color: options.fontColor || '#fff' }"
class="cmdb-dashboard-grid-item-chart"
>
<div class="cmdb-dashboard-grid-item-chart-icon" v-if="options.showIcon && ciType">
<template v-if="ciType.icon">
<img v-if="ciType.icon.split('$$')[2]" :src="`/api/common-setting/v1/file/${ciType.icon.split('$$')[3]}`" />
<ops-icon
v-else
:style="{
color: ciType.icon.split('$$')[1],
}"
:type="ciType.icon.split('$$')[0]"
/>
</template>
<span :style="{ color: '#2f54eb' }" v-else>{{ ciType.name[0].toUpperCase() }}</span>
</div>
<span :style="{ ...options.fontConfig }">{{ toThousands(data) }}</span> <span :style="{ ...options.fontConfig }">{{ toThousands(data) }}</span>
</div> </div>
<vxe-table
:max-height="tableHeight"
:data="tableData"
:stripe="!!options.ret"
size="mini"
class="ops-stripe-table"
v-if="options.chartType === 'table'"
:span-method="mergeRowMethod"
:border="!options.ret"
:show-header="!!options.ret"
>
<template v-if="options.ret">
<vxe-column v-for="col in columns" :key="col" :title="col" :field="col"></vxe-column>
</template>
<template v-else>
<vxe-column
v-for="(key, index) in Array(keyLength)"
:key="`key${index}`"
:title="`key${index}`"
:field="`key${index}`"
></vxe-column>
<vxe-column field="value" title="value"></vxe-column>
</template>
</vxe-table>
<div <div
:id="`cmdb-dashboard-${chartId}-${editable}`" :id="`cmdb-dashboard-${chartId}-${editable}`"
v-if="category === 1 || category === 2" v-else-if="category === 1 || category === 2"
class="cmdb-dashboard-grid-item-chart" class="cmdb-dashboard-grid-item-chart"
></div> ></div>
</div> </div>
@@ -15,17 +59,27 @@
import * as echarts from 'echarts' import * as echarts from 'echarts'
import { mixin } from '@/utils/mixin' import { mixin } from '@/utils/mixin'
import { toThousands } from '../../utils/helper' import { toThousands } from '../../utils/helper'
import { category_1_bar_options, category_1_pie_options, category_2_bar_options } from './chartOptions' import {
category_1_bar_options,
category_1_line_options,
category_1_pie_options,
category_2_bar_options,
category_2_pie_options,
} from './chartOptions'
export default { export default {
name: 'Chart', name: 'Chart',
mixins: [mixin], mixins: [mixin],
props: { props: {
ci_types: {
type: Array,
default: () => [],
},
chartId: { chartId: {
type: Number, type: Number,
default: 0, default: 0,
}, },
data: { data: {
type: [Number, Object], type: [Number, Object, Array],
default: 0, default: 0,
}, },
category: { category: {
@@ -40,20 +94,65 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
type_id: {
type: [Number, Array],
default: null,
},
isPreview: {
type: Boolean,
default: false,
},
}, },
data() { data() {
return { return {
chart: null, chart: null,
columns: [],
tableHeight: '',
tableData: [],
keyLength: 0,
} }
}, },
computed: {
ciType() {
if (this.type_id || this.options?.type_ids) {
const _find = this.ci_types.find((item) => item.id === this.type_id || item.id === this.options?.type_ids[0])
return _find || null
}
return null
},
},
watch: { watch: {
data: { data: {
immediate: true, immediate: true,
deep: true, deep: true,
handler(newValue, oldValue) { handler(newValue, oldValue) {
if (this.category === 1 || this.category === 2) { if (this.category === 1 || this.category === 2) {
if (Object.prototype.toString.call(newValue) === '[object Object]') { if (this.options.chartType !== 'table' && Object.prototype.toString.call(newValue) === '[object Object]') {
this.setChart() if (this.isPreview) {
this.$nextTick(() => {
this.setChart()
})
} else {
this.setChart()
}
}
}
if (this.options.chartType === 'table') {
this.$nextTick(() => {
const dom = document.getElementById(`cmdb-dashboard-${this.chartId}-${this.editable}-${this.isPreview}`)
this.tableHeight = dom.offsetHeight
})
if (this.options.ret) {
const excludeKeys = ['_X_ROW_KEY', 'ci_type', 'ci_type_alias', 'unique', 'unique_alias', '_id', '_type']
if (newValue && newValue.length) {
this.columns = Object.keys(newValue[0]).filter((keys) => !excludeKeys.includes(keys))
this.tableData = newValue
}
} else {
const _data = []
this.keyLength = this.options?.attr_ids?.length ?? 0
this.formatTableData(_data, this.data, {})
this.tableData = _data
} }
} }
}, },
@@ -81,13 +180,19 @@ export default {
this.chart = echarts.init(document.getElementById(`cmdb-dashboard-${this.chartId}-${this.editable}`)) this.chart = echarts.init(document.getElementById(`cmdb-dashboard-${this.chartId}-${this.editable}`))
} }
if (this.category === 1 && this.options.chartType === 'bar') { if (this.category === 1 && this.options.chartType === 'bar') {
this.chart.setOption(category_1_bar_options(this.data), true) this.chart.setOption(category_1_bar_options(this.data, this.options), true)
}
if (this.category === 1 && this.options.chartType === 'line') {
this.chart.setOption(category_1_line_options(this.data, this.options), true)
} }
if (this.category === 1 && this.options.chartType === 'pie') { if (this.category === 1 && this.options.chartType === 'pie') {
this.chart.setOption(category_1_pie_options(this.data), true) this.chart.setOption(category_1_pie_options(this.data, this.options), true)
} }
if (this.category === 2) { if (this.category === 2 && ['bar', 'line'].includes(this.options.chartType)) {
this.chart.setOption(category_2_bar_options(this.data), true) this.chart.setOption(category_2_bar_options(this.data, this.options, this.options.chartType), true)
}
if (this.category === 2 && this.options.chartType === 'pie') {
this.chart.setOption(category_2_pie_options(this.data, this.options), true)
} }
}, },
resizeChart() { resizeChart() {
@@ -97,6 +202,34 @@ export default {
} }
}) })
}, },
formatTableData(_data, data, obj) {
Object.keys(data).forEach((k) => {
if (typeof data[k] === 'number') {
_data.push({ ...obj, [`key${Object.keys(obj).length}`]: k, value: data[k] })
} else {
this.formatTableData(_data, data[k], { ...obj, [`key${Object.keys(obj).length}`]: k })
}
})
},
mergeRowMethod({ row, _rowIndex, column, visibleData }) {
const fields = ['key0', 'key1', 'key2']
const cellValue = row[column.field]
if (cellValue && fields.includes(column.field)) {
const prevRow = visibleData[_rowIndex - 1]
let nextRow = visibleData[_rowIndex + 1]
if (prevRow && prevRow[column.field] === cellValue) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow[column.field] === cellValue) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
},
}, },
} }
</script> </script>
@@ -106,14 +239,28 @@ export default {
width: 100%; width: 100%;
height: 100%; height: 100%;
position: relative; position: relative;
padding: 10px; display: flex;
justify-content: space-between;
align-items: center;
> span { > span {
font-size: 50px; font-size: 50px;
font-weight: 700; font-weight: 700;
position: absolute; }
top: 50%; .cmdb-dashboard-grid-item-chart-icon {
left: 50%; > i {
transform: translate(-50%, -50%); font-size: 4vw;
}
> img {
width: 4vw;
}
> span {
display: inline-block;
width: 4vw;
height: 4vw;
font-size: 50px;
text-align: center;
line-height: 50px;
}
} }
} }
</style> </style>

View File

@@ -1,65 +1,307 @@
<template> <template>
<a-modal :title="`${type === 'add' ? '新增' : '编辑'}图表`" :visible="visible" @cancel="handleclose" @ok="handleok"> <a-modal
<a-form-model ref="chartForm" :model="form" :rules="rules" :label-col="{ span: 6 }" :wrapper-col="{ span: 14 }"> width="1100px"
<a-form-model-item label="类型" prop="category"> :title="`${type === 'add' ? '新增' : '编辑'}图表`"
<a-select v-model="form.category" @change="changeDashboardCategory"> :visible="visible"
<a-select-option v-for="cate in Object.keys(dashboardCategory)" :key="cate" :value="Number(cate)">{{ @cancel="handleclose"
dashboardCategory[cate].label @ok="handleok"
}}</a-select-option> :bodyStyle="{ paddingTop: 0 }"
</a-select> >
</a-form-model-item> <div class="chart-wrapper">
<a-form-model-item v-if="form.category !== 0" label="名称" prop="name"> <div class="chart-left">
<a-input v-model="form.name" placeholder="请输入图表名称"></a-input> <a-form-model ref="chartForm" :model="form" :rules="rules" :label-col="{ span: 4 }" :wrapper-col="{ span: 18 }">
</a-form-model-item> <a-form-model-item label="标题" prop="name">
<a-form-model-item label="模型" prop="type_id"> <a-input v-model="form.name" placeholder="请输入图表标题"></a-input>
<a-select </a-form-model-item>
show-search <a-form-model-item label="类型" prop="category" v-if="chartType !== 'count' && chartType !== 'table'">
optionFilterProp="children" <a-radio-group
@change="changeCIType" @change="
v-model="form.type_id" () => {
placeholder="请选择模型" resetForm()
> }
<a-select-option v-for="ci_type in ci_types" :key="ci_type.id" :value="ci_type.id">{{ "
ci_type.alias || ci_type.name :default-value="1"
}}</a-select-option> v-model="form.category"
</a-select> >
</a-form-model-item> <a-radio-button :value="Number(key)" :key="key" v-for="key in Object.keys(dashboardCategory)">
<a-form-model-item v-if="form.category === 1" label="模型属性" prop="attr_id"> {{ dashboardCategory[key].label }}
<a-select show-search optionFilterProp="children" v-model="form.attr_id" placeholder="请选择模型属性"> </a-radio-button>
<a-select-option v-for="attr in attributes" :key="attr.id" :value="attr.id">{{ </a-radio-group>
attr.alias || attr.name </a-form-model-item>
}}</a-select-option> <a-form-model-item label="类型" prop="tableCategory" v-if="chartType === 'table'">
</a-select> <a-radio-group
</a-form-model-item> @change="
<a-form-model-item v-if="form.category === 1" label="图表类型" prop="chartType"> () => {
<a-radio-group v-model="chartType"> resetForm()
<a-radio value="bar"> }
柱状图 "
</a-radio> :default-value="1"
<a-radio value="pie"> v-model="form.tableCategory"
饼图 >
</a-radio> <a-radio-button :value="1">
</a-radio-group> 计算指标
</a-form-model-item> </a-radio-button>
<a-form-model-item v-if="form.category === 2" label="关系层级" prop="level"> <a-radio-button :value="2">
<a-input v-model="form.level" placeholder="请输入关系层级"></a-input> 资源数据
</a-form-model-item> </a-radio-button>
<a-form-model-item v-if="form.category === 0" label="字体"> </a-radio-group>
<FontConfig ref="fontConfig" /> </a-form-model-item>
</a-form-model-item> <a-form-model-item
</a-form-model> v-if="(chartType !== 'table' && form.category !== 2) || (chartType === 'table' && form.tableCategory === 1)"
label="模型"
prop="type_ids"
>
<a-select
show-search
optionFilterProp="children"
@change="changeCIType"
v-model="form.type_ids"
placeholder="请选择模型"
mode="multiple"
>
<a-select-option v-for="ci_type in ci_types" :key="ci_type.id" :value="ci_type.id">{{
ci_type.alias || ci_type.name
}}</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item v-else label="模型" prop="type_id">
<a-select
show-search
optionFilterProp="children"
@change="changeCIType"
v-model="form.type_id"
placeholder="请选择模型"
>
<a-select-option v-for="ci_type in ci_types" :key="ci_type.id" :value="ci_type.id">{{
ci_type.alias || ci_type.name
}}</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item
label="维度"
prop="attr_ids"
v-if="(['bar', 'line', 'pie'].includes(chartType) && form.category === 1) || chartType === 'table'"
>
<a-select @change="changeAttr" v-model="form.attr_ids" placeholder="请选择维度" mode="multiple" show-search>
<a-select-option v-for="attr in commonAttributes" :key="attr.id" :value="attr.id">{{
attr.alias || attr.name
}}</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item
prop="type_ids"
label="关系模型"
v-if="['bar', 'line', 'pie'].includes(chartType) && form.category === 2"
>
<a-select
show-search
optionFilterProp="children"
mode="multiple"
v-model="form.type_ids"
placeholder="请选择模型"
>
<a-select-opt-group
v-for="(key, index) in Object.keys(level2children)"
:key="key"
:label="`层级${index + 1}`"
>
<a-select-option
@click="(e) => clickLevel2children(e, citype, index + 1)"
v-for="citype in level2children[key]"
:key="citype.id"
:value="citype.id"
>
{{ citype.alias || citype.name }}
</a-select-option>
</a-select-opt-group>
</a-select>
</a-form-model-item>
<div class="chart-left-preview">
<span class="chart-left-preview-operation" @click="showPreview"><a-icon type="play-circle" /> 预览</span>
<template v-if="isShowPreview">
<div v-if="chartType !== 'count'" class="cmdb-dashboard-grid-item-title">
<template v-if="form.showIcon && ciType">
<template v-if="ciType.icon">
<img
v-if="ciType.icon.split('$$')[2]"
:src="`/api/common-setting/v1/file/${ciType.icon.split('$$')[3]}`"
/>
<ops-icon
v-else
:style="{
color: ciType.icon.split('$$')[1],
}"
:type="ciType.icon.split('$$')[0]"
/>
</template>
<span :style="{ color: '#2f54eb' }" v-else>{{ ciType.name[0].toUpperCase() }}</span>
</template>
<span :style="{ color: '#000' }"> {{ form.name }}</span>
</div>
<div
class="chart-left-preview-box"
:style="{
height: chartType === 'count' ? '120px' : '',
marginTop: chartType === 'count' ? '80px' : '',
background:
chartType === 'count'
? Array.isArray(bgColor)
? `linear-gradient(to bottom, ${bgColor[0]} 0%, ${bgColor[1]} 100%)`
: bgColor
: '#fafafa',
}"
>
<div :style="{ color: fontColor }">{{ form.name }}</div>
<Chart
:ref="`chart_${item.id}`"
:chartId="item.id"
:data="previewData"
:category="form.category"
:options="{
...item.options,
name: form.name,
fontColor: fontColor,
bgColor: bgColor,
chartType: chartType,
showIcon: form.showIcon,
barDirection: barDirection,
barStack: barStack,
chartColor: chartColor,
type_ids: form.type_ids,
attr_ids: form.attr_ids,
isShadow: isShadow,
}"
:editable="false"
:ci_types="ci_types"
:type_id="form.type_id || form.type_ids"
isPreview
/>
</div>
</template>
</div>
<a-form-model-item label="是否显示icon" prop="showIcon" :label-col="{ span: 5 }" :wrapper-col="{ span: 18 }">
<a-switch v-model="form.showIcon"></a-switch>
</a-form-model-item>
</a-form-model>
</div>
<div class="chart-right">
<h4>图表类型</h4>
<div class="chart-right-type">
<div
:class="{ 'chart-right-type-box': true, 'chart-right-type-box-selected': chartType === t.value }"
v-for="t in chartTypeList"
:key="t.value"
@click="changeChartType(t)"
>
<ops-icon :type="`cmdb-${t.value}`" />
<span>{{ t.label }}</span>
</div>
</div>
<h4>数据筛选</h4>
<FilterComp
ref="filterComp"
:isDropdown="false"
:canSearchPreferenceAttrList="attributes"
@setExpFromFilter="setExpFromFilter"
:expression="filterExp ? `q=${filterExp}` : ''"
/>
<h4>格式</h4>
<a-form-model :colon="false" :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
<a-form-model-item label="字体颜色" v-if="chartType === 'count'">
<ColorPicker
v-model="fontColor"
:colorList="[
'#1D2129',
'#4E5969',
'#103C93',
'#86909C',
'#ffffff',
'#C9F2FF',
'#FFEAC0',
'#D6FFE6',
'#F2DEFF',
]"
/>
</a-form-model-item>
<a-form-model-item label="背景颜色" v-if="chartType === 'count'">
<ColorPicker
v-model="bgColor"
:colorList="[
['#6ABFFE', '#5375EB'],
['#C69EFF', '#A377F9'],
['#85EBC9', '#4AB8D8'],
['#FEB58B', '#DF6463'],
'#ffffff',
'#FFFBF0',
'#FFF1EC',
'#E5FFFE',
'#E5E7FF',
]"
/>
</a-form-model-item>
<a-form-model-item label="图表颜色" v-else-if="chartType !== 'table'">
<ColorListPicker v-model="chartColor" />
</a-form-model-item>
<a-form-model-item label="图表长度(%)">
<a-radio-group class="chart-width" style="width:100%;" v-model="width">
<a-radio-button :value="3">
25
</a-radio-button>
<a-radio-button :value="6">
50
</a-radio-button>
<a-radio-button :value="9">
75
</a-radio-button>
<a-radio-button :value="12">
100
</a-radio-button>
</a-radio-group>
</a-form-model-item>
<a-form-model-item label="柱状图类型" v-if="chartType === 'bar'">
<a-radio-group v-model="barStack">
<a-radio value="total">
堆积柱状图
</a-radio>
<a-radio value="">
多系列柱状图
</a-radio>
</a-radio-group>
</a-form-model-item>
<a-form-model-item label="方向" v-if="chartType === 'bar'">
<a-radio-group v-model="barDirection">
<a-radio value="x">
X轴
</a-radio>
<a-radio value="y">
y轴
</a-radio>
</a-radio-group>
</a-form-model-item>
<a-form-model-item label="下方阴影" v-if="chartType === 'line'">
<a-switch v-model="isShadow" />
</a-form-model-item>
</a-form-model>
</div>
</div>
</a-modal> </a-modal>
</template> </template>
<script> <script>
import Chart from './chart.vue'
import { dashboardCategory } from './constant' import { dashboardCategory } from './constant'
import { postCustomDashboard, putCustomDashboard } from '../../api/customDashboard' import { postCustomDashboard, putCustomDashboard, postCustomDashboardPreview } from '../../api/customDashboard'
import { getCITypeAttributesById } from '../../api/CITypeAttr' import { getCITypeAttributesByTypeIds, getCITypeCommonAttributesByTypeIds } from '../../api/CITypeAttr'
import { getRecursive_level2children } from '../../api/CITypeRelation'
import { getLastLayout } from '../../utils/helper' import { getLastLayout } from '../../utils/helper'
import FontConfig from './fontConfig.vue' import FilterComp from '@/components/CMDBFilterComp'
import ColorPicker from './colorPicker.vue'
import ColorListPicker from './colorListPicker.vue'
export default { export default {
name: 'ChartForm', name: 'ChartForm',
components: { FontConfig }, components: { Chart, FilterComp, ColorPicker, ColorListPicker },
props: { props: {
ci_types: { ci_types: {
type: Array, type: Array,
@@ -67,100 +309,226 @@ export default {
}, },
}, },
data() { data() {
const chartTypeList = [
{
value: 'count',
label: '指标',
},
{
value: 'bar',
label: '柱状图',
},
{
value: 'line',
label: '折线图',
},
{
value: 'pie',
label: '饼状图',
},
{
value: 'table',
label: '表格',
},
]
return { return {
dashboardCategory, dashboardCategory,
chartTypeList,
visible: false, visible: false,
attributes: [], attributes: [],
type: 'add', type: 'add',
form: { form: {
category: 0, category: 0,
tableCategory: 1,
name: undefined, name: undefined,
type_id: undefined, type_id: undefined,
attr_id: undefined, type_ids: undefined,
attr_ids: undefined,
level: undefined, level: undefined,
showIcon: false,
}, },
rules: { rules: {
category: [{ required: true, trigger: 'change' }], category: [{ required: true, trigger: 'change' }],
name: [{ required: true, message: '请输入图表名称' }], name: [{ required: true, message: '请输入图表名称' }],
type_id: [{ required: true, message: '请选择模型', trigger: 'change' }], type_id: [{ required: true, message: '请选择模型', trigger: 'change' }],
attr_id: [{ required: true, message: '请选择模型属性', trigger: 'change' }], type_ids: [{ required: true, message: '请选择模型', trigger: 'change' }],
attr_ids: [{ required: true, message: '请选择模型属性', trigger: 'change' }],
level: [{ required: true, message: '请输入关系层级' }], level: [{ required: true, message: '请输入关系层级' }],
showIcon: [{ required: false }],
}, },
item: {}, item: {},
chartType: 'bar', chartType: 'count', // table,bar,line,pie,count
width: 3,
fontColor: '#ffffff',
bgColor: ['#6ABFFE', '#5375EB'],
chartColor: '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD', // 图表颜色
isShowPreview: false,
filterExp: undefined,
previewData: null,
barStack: 'total',
barDirection: 'y',
commonAttributes: [],
level2children: {},
isShadow: false,
} }
}, },
computed: {
ciType() {
if (this.form.type_id || this.form.type_ids) {
const _find = this.ci_types.find((item) => item.id === this.form.type_id || item.id === this.form.type_ids[0])
return _find || null
}
return null
},
},
inject: ['layout'], inject: ['layout'],
methods: { methods: {
open(type, item = {}) { async open(type, item = {}) {
this.visible = true this.visible = true
this.type = type this.type = type
this.item = item this.item = item
const { category = 0, name, type_id, attr_id, level } = item const { category = 0, name, type_id, attr_id, level } = item
const chartType = (item.options || {}).chartType || 'bar' const chartType = (item.options || {}).chartType || 'count'
const fontColor = (item.options || {}).fontColor || '#ffffff'
const bgColor = (item.options || {}).bgColor || ['#6ABFFE', '#5375EB']
const width = (item.options || {}).w
const showIcon = (item.options || {}).showIcon
const type_ids = item?.options?.type_ids || []
const attr_ids = item?.options?.attr_ids || []
const ret = item?.options?.ret || ''
this.width = width
this.chartType = chartType this.chartType = chartType
if (type_id && attr_id) { this.filterExp = item?.options?.filter ?? ''
getCITypeAttributesById(type_id).then((res) => { this.chartColor = item?.options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD'
this.isShadow = item?.options?.isShadow ?? false
if (chartType === 'count') {
this.fontColor = fontColor
this.bgColor = bgColor
}
if (type_ids && type_ids.length) {
await getCITypeAttributesByTypeIds({ type_ids: type_ids.join(',') }).then((res) => {
this.attributes = res.attributes this.attributes = res.attributes
}) })
if ((['bar', 'line', 'pie'].includes(chartType) && category === 1) || chartType === 'table') {
this.barDirection = item?.options?.barDirection ?? 'y'
this.barStack = item?.options?.barStack ?? 'total'
await getCITypeCommonAttributesByTypeIds({
type_ids: type_ids.join(','),
}).then((res) => {
this.commonAttributes = res.attributes
})
}
} }
if (type_id) {
getRecursive_level2children(type_id).then((res) => {
this.level2children = res
})
await getCITypeCommonAttributesByTypeIds({
type_ids: type_id,
}).then((res) => {
this.commonAttributes = res.attributes
})
}
this.$nextTick(() => {
this.$refs.filterComp.visibleChange(true, false)
})
const default_form = { const default_form = {
category: 0, category: 0,
name: undefined, name: undefined,
type_id: undefined, type_id: undefined,
attr_id: undefined, type_ids: undefined,
attr_ids: undefined,
level: undefined, level: undefined,
showIcon: false,
tableCategory: 1,
} }
this.form = { this.form = {
...default_form, ...default_form,
category, category,
name, name,
type_id, type_id,
attr_id, type_ids,
attr_ids,
level, level,
} showIcon,
if (category === 0) { tableCategory: ret === 'cis' ? 2 : 1,
this.$nextTick(() => {
this.$refs.fontConfig.setConfig((item.options || {}).fontConfig)
})
} }
}, },
handleclose() { handleclose() {
this.attributes = [] this.attributes = []
this.$refs.chartForm.clearValidate() this.$refs.chartForm.clearValidate()
this.isShowPreview = false
this.visible = false this.visible = false
}, },
changeCIType(value) { changeCIType(value) {
getCITypeAttributesById(value).then((res) => { this.form.attr_ids = []
this.commonAttributes = []
getCITypeAttributesByTypeIds({ type_ids: Array.isArray(value) ? value.join(',') : value }).then((res) => {
this.attributes = res.attributes this.attributes = res.attributes
this.form = {
...this.form,
attr_id: undefined,
}
}) })
if (!Array.isArray(value)) {
getRecursive_level2children(value).then((res) => {
this.level2children = res
})
}
if ((['bar', 'line', 'pie'].includes(this.chartType) && this.form.category === 1) || this.chartType === 'table') {
getCITypeCommonAttributesByTypeIds({ type_ids: Array.isArray(value) ? value.join(',') : value }).then((res) => {
this.commonAttributes = res.attributes
})
}
}, },
handleok() { handleok() {
this.$refs.chartForm.validate(async (valid) => { this.$refs.chartForm.validate(async (valid) => {
if (valid) { if (valid) {
const fontConfig = this.form.category === 0 ? this.$refs.fontConfig.getConfig() : undefined const name = this.form.name
const _find = this.ci_types.find((attr) => attr.id === this.form.type_id) const { chartType, fontColor, bgColor } = this
const name = this.form.name || (_find || {}).alias || (_find || {}).name this.$refs.filterComp.handleSubmit()
if (this.item.id) { if (this.item.id) {
await putCustomDashboard(this.item.id, { const params = {
...this.form, ...this.form,
options: { options: {
...this.item.options, ...this.item.options,
name, name,
fontConfig, w: this.width,
chartType: this.chartType, chartType: this.chartType,
showIcon: this.form.showIcon,
type_ids: this.form.type_ids,
filter: this.filterExp,
isShadow: this.isShadow,
}, },
}) }
if (chartType === 'count') {
params.options.fontColor = fontColor
params.options.bgColor = bgColor
}
if (['bar', 'line', 'pie'].includes(chartType)) {
if (this.form.category === 1) {
params.options.attr_ids = this.form.attr_ids
}
params.options.chartColor = this.chartColor
}
if (chartType === 'bar') {
params.options.barDirection = this.barDirection
params.options.barStack = this.barStack
}
if (chartType === 'table') {
params.options.attr_ids = this.form.attr_ids
if (this.form.tableCategory === 2) {
params.options.ret = 'cis'
}
}
delete params.showIcon
delete params.type_ids
delete params.attr_ids
delete params.tableCategory
await putCustomDashboard(this.item.id, params)
} else { } else {
const { xLast, yLast, wLast } = getLastLayout(this.layout()) const { xLast, yLast, wLast } = getLastLayout(this.layout())
const w = 3 const w = this.width
const x = xLast + wLast + w > 12 ? 0 : xLast + wLast const x = xLast + wLast + w > 12 ? 0 : xLast + wLast
const y = xLast + wLast + w > 12 ? yLast + 1 : yLast const y = xLast + wLast + w > 12 ? yLast + 1 : yLast
await postCustomDashboard({ const params = {
...this.form, ...this.form,
options: { options: {
x, x,
@@ -169,23 +537,216 @@ export default {
h: this.form.category === 0 ? 3 : 5, h: this.form.category === 0 ? 3 : 5,
name, name,
chartType: this.chartType, chartType: this.chartType,
fontConfig, showIcon: this.form.showIcon,
type_ids: this.form.type_ids,
filter: this.filterExp,
isShadow: this.isShadow,
}, },
}) }
if (chartType === 'count') {
params.options.fontColor = fontColor
params.options.bgColor = bgColor
}
if (['bar', 'line', 'pie'].includes(chartType)) {
if (this.form.category === 1) {
params.options.attr_ids = this.form.attr_ids
}
params.options.chartColor = this.chartColor
}
if (chartType === 'bar') {
params.options.barDirection = this.barDirection
params.options.barStack = this.barStack
}
if (chartType === 'table') {
params.options.attr_ids = this.form.attr_ids
if (this.form.tableCategory === 2) {
params.options.ret = 'cis'
}
}
delete params.showIcon
delete params.type_ids
delete params.attr_ids
delete params.tableCategory
await postCustomDashboard(params)
} }
this.handleclose() this.handleclose()
this.$emit('refresh') this.$emit('refresh')
} }
}) })
}, },
changeDashboardCategory(value) { // changeDashboardCategory(value) {
this.$refs.chartForm.clearValidate() // this.$refs.chartForm.clearValidate()
if (value === 1 && this.form.type_id) { // if (value === 1 && this.form.type_id) {
this.changeCIType(this.form.type_id) // this.changeCIType(this.form.type_id)
// }
// },
changeChartType(t) {
this.chartType = t.value
this.isShowPreview = false
if (t.value === 'count') {
this.form.category = 0
} else {
this.form.category = 1
} }
this.resetForm()
},
showPreview() {
this.$refs.chartForm.validate(async (valid) => {
if (valid) {
this.isShowPreview = false
const name = this.form.name
const { chartType, fontColor, bgColor } = this
this.$refs.filterComp.handleSubmit()
const params = {
...this.form,
options: {
name,
chartType,
showIcon: this.form.showIcon,
type_ids: this.form.type_ids,
filter: this.filterExp,
isShadow: this.isShadow,
},
}
if (chartType === 'count') {
params.options.fontColor = fontColor
params.options.bgColor = bgColor
}
if (['bar', 'line', 'pie'].includes(chartType)) {
if (this.form.category === 1) {
params.options.attr_ids = this.form.attr_ids
}
params.options.chartColor = this.chartColor
}
if (chartType === 'bar') {
params.options.barDirection = this.barDirection
params.options.barStack = this.barStack
}
if (chartType === 'table') {
params.options.attr_ids = this.form.attr_ids
if (this.form.tableCategory === 2) {
params.options.ret = 'cis'
}
}
delete params.showIcon
delete params.type_ids
delete params.attr_ids
delete params.tableCategory
postCustomDashboardPreview(params).then((res) => {
this.isShowPreview = true
this.previewData = res.counter
})
}
})
},
setExpFromFilter(filterExp) {
if (filterExp) {
this.filterExp = `${filterExp}`
} else {
this.filterExp = undefined
}
},
resetForm() {
this.form.type_id = undefined
this.form.type_ids = []
this.form.attr_ids = []
this.$refs.chartForm.clearValidate()
},
changeAttr(value) {
if (value && value.length) {
if (['line', 'pie'].includes(this.chartType)) {
this.form.attr_ids = [value[value.length - 1]]
}
if (['bar'].includes(this.chartType) && value.length > 2) {
this.form.attr_ids = value.slice(value.length - 2, value.length)
}
if (['table'].includes(this.chartType) && value.length > 3) {
this.form.attr_ids = value.slice(value.length - 3, value.length)
}
}
},
clickLevel2children(e, citype, level) {
if (this.form.level !== level) {
this.$nextTick(() => {
this.form.type_ids = [citype.id]
})
}
this.form.level = level
}, },
}, },
} }
</script> </script>
<style></style> <style lang="less" scoped>
.chart-wrapper {
display: flex;
.chart-left {
width: 50%;
.chart-left-preview {
border: 1px solid #e4e7ed;
border-radius: 2px;
height: 280px;
width: 92%;
position: relative;
padding: 12px;
.chart-left-preview-operation {
color: #86909c;
position: absolute;
top: 12px;
right: 12px;
cursor: pointer;
}
.chart-left-preview-box {
padding: 6px 12px;
height: 250px;
border-radius: 8px;
}
}
}
.chart-right {
width: 50%;
h4 {
font-weight: 700;
color: #000;
}
.chart-right-type {
display: flex;
justify-content: space-between;
background-color: #f0f5ff;
padding: 6px 12px;
.chart-right-type-box {
cursor: pointer;
width: 70px;
height: 60px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
> i {
font-size: 32px;
}
> span {
font-size: 12px;
}
}
.chart-right-type-box-selected {
background-color: #e5f1ff;
}
}
.chart-width {
width: 100%;
> label {
width: 25%;
text-align: center;
}
}
}
}
</style>
<style lang="less">
.chart-wrapper {
.ant-form-item {
margin-bottom: 0;
}
}
</style>

View File

@@ -1,23 +1,61 @@
export const colorList = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc'] export const category_1_bar_options = (data, options) => {
// 计算一级分类
export const category_1_bar_options = (data) => { const xData = Object.keys(data)
// 计算共有多少二级分类
const secondCategory = {}
Object.keys(data).forEach(key => {
if (Object.prototype.toString.call(data[key]) === '[object Object]') {
Object.keys(data[key]).forEach(key1 => {
secondCategory[key1] = Array.from({ length: xData.length }).fill(0)
})
} else {
secondCategory['其他'] = Array.from({ length: xData.length }).fill(0)
}
})
Object.keys(secondCategory).forEach(key => {
xData.forEach((x, idx) => {
if (data[x][key]) {
secondCategory[key][idx] = data[x][key]
}
if (typeof data[x] === 'number') {
secondCategory['其他'][idx] = data[x]
}
})
})
return { return {
color: options.chartColor.split(','),
grid: { grid: {
top: 15, top: 15,
left: 'left', left: 'left',
right: 0, right: 10,
bottom: 0, bottom: 20,
containLabel: true, containLabel: true,
}, },
xAxis: { legend: {
type: 'category', data: Object.keys(secondCategory),
data: Object.keys(data) bottom: 0,
type: 'scroll',
}, },
yAxis: { xAxis: options.barDirection === 'y' ? {
type: 'category',
axisTick: { show: false },
data: xData
}
: {
type: 'value',
splitLine: {
show: false
}
},
yAxis: options.barDirection === 'y' ? {
type: 'value', type: 'value',
splitLine: { splitLine: {
show: false show: false
} }
} : {
type: 'category',
axisTick: { show: false },
data: xData
}, },
tooltip: { tooltip: {
trigger: 'axis', trigger: 'axis',
@@ -25,34 +63,76 @@ export const category_1_bar_options = (data) => {
type: 'shadow' type: 'shadow'
} }
}, },
series: Object.keys(secondCategory).map(key => {
return {
name: key,
type: 'bar',
stack: options?.barStack ?? 'total',
barGap: 0,
emphasis: {
focus: 'series'
},
data: secondCategory[key]
}
})
}
}
export const category_1_line_options = (data, options) => {
const xData = Object.keys(data)
return {
color: options.chartColor.split(','),
grid: {
top: 15,
left: 'left',
right: 10,
bottom: 20,
containLabel: true,
},
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: xData
},
yAxis: {
type: 'value'
},
series: [ series: [
{ {
data: Object.keys(data).map((key, index) => { data: xData.map(item => data[item]),
return { type: 'line',
value: data[key], smooth: true,
itemStyle: { color: colorList[0] } showSymbol: false,
areaStyle: options?.isShadow ? {
opacity: 0.5,
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [{
offset: 0, color: options.chartColor.split(',')[0] // 0% 处的颜色
}, {
offset: 1, color: '#ffffff' // 100% 处的颜色
}],
global: false // 缺省为 false
} }
}), } : null
type: 'bar',
label: {
show: true,
position: 'top',
fontSize: 10,
formatter(data) {
return `${data.value || ''}`
}
},
} }
] ]
} }
} }
export const category_1_pie_options = (data) => { export const category_1_pie_options = (data, options) => {
return { return {
color: options.chartColor.split(','),
grid: { grid: {
top: 10, top: 10,
left: 'left', left: 'left',
right: 0, right: 10,
bottom: 0, bottom: 0,
containLabel: true, containLabel: true,
}, },
@@ -89,7 +169,7 @@ export const category_1_pie_options = (data) => {
} }
} }
export const category_2_bar_options = (data) => { export const category_2_bar_options = (data, options, chartType) => {
const xAxisData = Object.keys(data.detail) const xAxisData = Object.keys(data.detail)
const _legend = [] const _legend = []
xAxisData.forEach(key => { xAxisData.forEach(key => {
@@ -97,10 +177,11 @@ export const category_2_bar_options = (data) => {
}) })
const legend = [...new Set(_legend)] const legend = [...new Set(_legend)]
return { return {
color: options.chartColor.split(','),
grid: { grid: {
top: 15, top: 15,
left: 'left', left: 'left',
right: 0, right: 10,
bottom: 20, bottom: 20,
containLabel: true, containLabel: true,
}, },
@@ -116,41 +197,110 @@ export const category_2_bar_options = (data) => {
type: 'scroll', type: 'scroll',
data: legend data: legend
}, },
xAxis: [ xAxis: options.barDirection === 'y' || chartType === 'line' ? {
{ type: 'category',
type: 'category', axisTick: { show: false },
axisTick: { show: false }, data: xAxisData
data: xAxisData }
} : {
],
yAxis: [
{
type: 'value', type: 'value',
splitLine: { splitLine: {
show: false show: false
} }
},
yAxis: options.barDirection === 'y' || chartType === 'line' ? {
type: 'value',
splitLine: {
show: false
} }
], } : {
series: legend.map(le => { type: 'category',
axisTick: { show: false },
data: xAxisData
},
series: legend.map((le, index) => {
return { return {
name: le, name: le,
type: 'bar', type: chartType,
barGap: 0, barGap: 0,
emphasis: { emphasis: {
focus: 'series' focus: 'series'
}, },
stack: chartType === 'line' ? '' : options?.barStack ?? 'total',
data: xAxisData.map(x => { data: xAxisData.map(x => {
return data.detail[x][le] || 0 return data.detail[x][le] || 0
}), }),
smooth: true,
showSymbol: false,
label: { label: {
show: true, show: false,
position: 'top',
fontSize: 10,
formatter(data) {
return `${data.value || ''}`
}
}, },
areaStyle: chartType === 'line' && options?.isShadow ? {
opacity: 0.5,
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [{
offset: 0, color: options.chartColor.split(',')[index % 8] // 0% 处的颜色
}, {
offset: 1, color: '#ffffff' // 100% 处的颜色
}],
global: false // 缺省为 false
}
} : null
} }
}) })
} }
} }
export const category_2_pie_options = (data, options) => {
console.log(1111, options)
const _legend = []
Object.keys(data.detail).forEach(key => {
Object.keys(data.detail[key]).forEach(key2 => {
_legend.push({ value: data.detail[key][key2], name: `${key}-${key2}` })
})
})
return {
color: options.chartColor.split(','),
grid: {
top: 15,
left: 'left',
right: 10,
bottom: 20,
containLabel: true,
},
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left',
type: 'scroll',
formatter: function (name) {
const _find = _legend.find(item => item.name === name)
return `${name}${_find.value}`
}
},
series: [
{
type: 'pie',
radius: '90%',
data: _legend,
label: {
show: false,
},
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
}
}

View File

@@ -0,0 +1,54 @@
<template>
<a-select v-model="currenColor">
<a-select-option v-for="i in list" :value="i" :key="i">
<div>
<span :style="{ backgroundColor: color }" class="color-box" v-for="color in i.split(',')" :key="color"></span>
</div>
</a-select-option>
</a-select>
</template>
<script>
export default {
name: 'ColorListPicker',
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: [String, Array],
default: null,
},
},
data() {
return {
list: [
'#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD',
'#C1A9DC,#E2B5CD,#EE8EBC,#8483C3,#4D66BD,#213764,#D9B6E9,#DD88EB',
'#6FC4DF,#9FE8CE,#16B4BE,#86E6FB,#1871A3,#E1BF8D,#ED8D8D,#DD88EB',
'#F8B751,#FC9054,#FFE380,#DF963F,#AB5200,#EA9387,#FFBB7C,#D27467',
],
}
},
computed: {
currenColor: {
get() {
return this.value
},
set(val) {
this.$emit('change', val)
return val
},
},
},
}
</script>
<style lang="less" scoped>
.color-box {
display: inline-block;
width: 40px;
height: 10px;
}
</style>

View File

@@ -0,0 +1,79 @@
<template>
<div class="color-picker">
<div
:style="{
background: Array.isArray(item) ? `linear-gradient(to bottom, ${item[0]} 0%, ${item[1]} 100%)` : item,
}"
:class="{ 'color-picker-box': true, 'color-picker-box-selected': isEqual(currenColor, item) }"
v-for="item in colorList"
:key="Array.isArray(item) ? item.join() : item"
@click="changeColor(item)"
></div>
</div>
</template>
<script>
import _ from 'lodash'
export default {
name: 'ColorPicker',
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: [String, Array],
default: null,
},
colorList: {
type: Array,
default: () => [],
},
},
computed: {
currenColor: {
get() {
return this.value
},
set(val) {
this.$emit('change', val)
return val
},
},
},
methods: {
isEqual: _.isEqual,
changeColor(item) {
this.$emit('change', item)
},
},
}
</script>
<style lang="less" scoped>
.color-picker {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
.color-picker-box {
width: 19px;
height: 19px;
border: 1px solid #dae2e7;
border-radius: 1px;
cursor: pointer;
}
.color-picker-box-selected {
position: relative;
&:after {
content: '';
position: absolute;
width: 24px;
height: 24px;
border: 1px solid #43bbff;
top: -3px;
left: -3px;
}
}
}
</style>

View File

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

View File

@@ -11,8 +11,8 @@
<template v-if="layout && layout.length"> <template v-if="layout && layout.length">
<div v-if="editable"> <div v-if="editable">
<a-button <a-button
:style="{ marginLeft: '10px' }" :style="{ marginLeft: '22px', marginTop: '20px' }"
@click="openChartForm('add', {})" @click="openChartForm('add', { options: { w: 3 } })"
ghost ghost
type="primary" type="primary"
size="small" size="small"
@@ -39,11 +39,44 @@
:h="item.h" :h="item.h"
:i="item.i" :i="item.i"
:key="item.i" :key="item.i"
:style="{ backgroundColor: '#fafafa' }" :style="{
background:
item.options.chartType === 'count'
? Array.isArray(item.options.bgColor)
? `linear-gradient(to bottom, ${item.options.bgColor[0]} 0%, ${item.options.bgColor[1]} 100%)`
: item.options.bgColor
: '#fafafa',
}"
> >
<CardTitle>{{ item.options.name }}</CardTitle> <div class="cmdb-dashboard-grid-item-title">
<template v-if="item.options.chartType !== 'count' && item.options.showIcon && getCiType(item)">
<template v-if="getCiType(item).icon">
<img
v-if="getCiType(item).icon.split('$$')[2]"
:src="`/api/common-setting/v1/file/${getCiType(item).icon.split('$$')[3]}`"
/>
<ops-icon
v-else
:style="{
color: getCiType(item).icon.split('$$')[1],
}"
:type="getCiType(item).icon.split('$$')[0]"
/>
</template>
<span :style="{ color: '#2f54eb' }" v-else>{{ getCiType(item).name[0].toUpperCase() }}</span>
</template>
<span :style="{ color: item.options.chartType === 'count' ? item.options.fontColor : '#000' }">{{
item.options.name
}}</span>
</div>
<a-dropdown v-if="editable"> <a-dropdown v-if="editable">
<a class="cmdb-dashboard-grid-item-operation"><a-icon type="menu"></a-icon></a> <a
class="cmdb-dashboard-grid-item-operation"
:style="{
color: item.options.chartType === 'count' ? item.options.fontColor : '',
}"
><a-icon type="menu"></a-icon
></a>
<a-menu slot="overlay"> <a-menu slot="overlay">
<a-menu-item> <a-menu-item>
<a @click="() => openChartForm('edit', item)"><a-icon style="margin-right:5px" type="edit" />编辑</a> <a @click="() => openChartForm('edit', item)"><a-icon style="margin-right:5px" type="edit" />编辑</a>
@@ -53,13 +86,13 @@
</a-menu-item> </a-menu-item>
</a-menu> </a-menu>
</a-dropdown> </a-dropdown>
<a <!-- <a
v-if="editable && item.category === 1" v-if="editable && item.category === 1"
class="cmdb-dashboard-grid-item-chart-type" class="cmdb-dashboard-grid-item-chart-type"
@click="changeChartType(item)" @click="changeChartType(item)"
><a-icon ><a-icon
:type="item.options.chartType === 'bar' ? 'bar-chart' : 'pie-chart'" :type="item.options.chartType === 'bar' ? 'bar-chart' : 'pie-chart'"
/></a> /></a> -->
<Chart <Chart
:ref="`chart_${item.id}`" :ref="`chart_${item.id}`"
:chartId="item.id" :chartId="item.id"
@@ -67,18 +100,26 @@
:category="item.category" :category="item.category"
:options="item.options" :options="item.options"
:editable="editable" :editable="editable"
:ci_types="ci_types"
:type_id="item.type_id"
/> />
</GridItem> </GridItem>
</GridLayout> </GridLayout>
</template> </template>
<div v-else class="dashboard-empty"> <div v-else class="dashboard-empty">
<a-empty :image="emptyImage" description=""></a-empty> <a-empty :image="emptyImage" description=""></a-empty>
<a-button @click="openChartForm('add', {})" v-if="editable" size="small" type="primary" icon="plus"> <a-button
@click="openChartForm('add', { options: { w: 3 } })"
v-if="editable"
size="small"
type="primary"
icon="plus"
>
定制仪表盘 定制仪表盘
</a-button> </a-button>
<span v-else>管理员暂未定制仪表盘</span> <span v-else>管理员暂未定制仪表盘</span>
</div> </div>
<ChartForm ref="chartForm" @refresh="refresh" :ci_types="ci_types" /> <ChartForm ref="chartForm" @refresh="refresh" :ci_types="ci_types" :totalData="totalData" />
</div> </div>
</template> </template>
@@ -127,12 +168,14 @@ export default {
}, },
} }
}, },
mounted() { created() {
this.getLayout()
getCITypes().then((res) => { getCITypes().then((res) => {
this.ci_types = res.ci_types this.ci_types = res.ci_types
}) })
}, },
mounted() {
this.getLayout()
},
methods: { methods: {
async getLayout() { async getLayout() {
const res = await getCustomDashboard() const res = await getCustomDashboard()
@@ -196,6 +239,13 @@ export default {
}) })
} }
}, },
getCiType(item) {
if (item.type_id || item.options?.type_ids) {
const _find = this.ci_types.find((type) => type.id === item.type_id || type.id === item.options?.type_ids[0])
return _find || null
}
return null
},
}, },
} }
</script> </script>
@@ -206,15 +256,18 @@ export default {
text-align: center; text-align: center;
} }
.cmdb-dashboard-grid-item { .cmdb-dashboard-grid-item {
border-radius: 15px; border-radius: 8px;
padding: 6px 12px;
.cmdb-dashboard-grid-item-title { .cmdb-dashboard-grid-item-title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 700; font-weight: 700;
padding-left: 6px; color: #000000;
color: #000000bd;
} }
.cmdb-dashboard-grid-item-operation { .cmdb-dashboard-grid-item-operation {
position: absolute; position: absolute;
right: 6px; right: 12px;
top: 6px; top: 6px;
} }
.cmdb-dashboard-grid-item-chart-type { .cmdb-dashboard-grid-item-chart-type {
@@ -224,3 +277,26 @@ export default {
} }
} }
</style> </style>
<style lang="less">
.cmdb-dashboard-grid-item-title {
display: flex;
align-items: center;
> i {
font-size: 16px;
margin-right: 5px;
}
> img {
width: 16px;
margin-right: 5px;
}
> span:not(:last-child) {
display: inline-block;
width: 16px;
height: 16px;
font-size: 16px;
text-align: center;
margin-right: 5px;
}
}
</style>

View File

@@ -30,7 +30,7 @@ services:
- redis - redis
cmdb-api: cmdb-api:
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.3.1 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.1 image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.3.3
# build: # build:
# context: . # context: .
# target: cmdb-ui # target: cmdb-ui