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

View File

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

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'):
return abort(403, ErrFormat.role_required.format(RoleEnum.CONFIG))
@staticmethod
def calc_computed_attribute(attr_id):
@classmethod
def calc_computed_attribute(cls, attr_id):
"""
calculate computed attribute for all ci
:param attr_id:
:return:
"""
cls.can_create_computed_attribute()
from api.tasks.cmdb import calc_computed_attribute
calc_computed_attribute.apply_async(args=(attr_id, current_user.uid), queue=CMDB_QUEUE)

View File

@@ -2,14 +2,11 @@
from __future__ import unicode_literals
import requests
from flask import current_app
from api.extensions import cache
from api.extensions import db
from api.lib.cmdb.custom_dashboard import CustomDashboardManager
from api.models.cmdb import Attribute
from api.models.cmdb import CI
from api.models.cmdb import CIType
from api.models.cmdb import CITypeAttribute
from api.models.cmdb import RelationType
@@ -210,7 +207,6 @@ class CITypeAttributeCache(object):
@classmethod
def get(cls, type_id, attr_id):
attr = cache.get(cls.PREFIX_ID.format(type_id, attr_id))
attr = attr or cache.get(cls.PREFIX_ID.format(type_id, attr_id))
attr = attr or CITypeAttribute.get_by(type_id=type_id, attr_id=attr_id, first=True, to_dict=False)
@@ -251,53 +247,72 @@ class CMDBCounterCache(object):
result = {}
for custom in customs:
if custom['category'] == 0:
result[custom['id']] = cls.summary_counter(custom['type_id'])
res = cls.sum_counter(custom)
elif custom['category'] == 1:
result[custom['id']] = cls.attribute_counter(custom['type_id'], custom['attr_id'])
elif custom['category'] == 2:
result[custom['id']] = cls.relation_counter(custom['type_id'], custom['level'])
res = cls.attribute_counter(custom)
else:
res = cls.relation_counter(custom.get('type_id'),
custom.get('level'),
custom.get('options', {}).get('filter', ''),
custom.get('options', {}).get('type_ids', ''))
if res:
result[custom['id']] = res
cls.set(result)
return result
@classmethod
def update(cls, custom):
def update(cls, custom, flush=True):
result = cache.get(cls.KEY) or {}
if not result:
result = cls.reset()
if custom['category'] == 0:
result[custom['id']] = cls.summary_counter(custom['type_id'])
res = cls.sum_counter(custom)
elif custom['category'] == 1:
result[custom['id']] = cls.attribute_counter(custom['type_id'], custom['attr_id'])
elif custom['category'] == 2:
result[custom['id']] = cls.relation_counter(custom['type_id'], custom['level'])
res = cls.attribute_counter(custom)
else:
res = cls.relation_counter(custom.get('type_id'),
custom.get('level'),
custom.get('options', {}).get('filter', ''),
custom.get('options', {}).get('type_ids', ''))
cls.set(result)
if res and flush:
result[custom['id']] = res
cls.set(result)
return res
@staticmethod
def summary_counter(type_id):
return db.session.query(CI.id).filter(CI.deleted.is_(False)).filter(CI.type_id == type_id).count()
def relation_counter(type_id, level, other_filer, type_ids):
from api.lib.cmdb.search.ci_relation.search import Search as RelSearch
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
@staticmethod
def relation_counter(type_id, level):
query = "_type:{}".format(type_id)
s = search(query, count=1000000)
try:
type_names, _, _, _, _, _ = s.search()
except SearchError as e:
current_app.logger.error(e)
return
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]
url = "{}/ci_relations/statistics?root_ids={}&level={}".format(
uri, ','.join([i[0] for i in type_id_names]), level)
stats = requests.get(url).json()
s = RelSearch([i[0] for i in type_id_names], level, other_filer or '')
try:
stats = s.statistics(type_ids)
except SearchError as e:
current_app.logger.error(e)
return
id2name = dict(type_id_names)
type_ids = set()
for i in (stats.get('detail') or []):
for j in stats['detail'][i]:
type_ids.add(j)
for type_id in type_ids:
_type = CITypeCache.get(type_id)
id2name[type_id] = _type and _type.alias
@@ -317,9 +332,94 @@ class CMDBCounterCache(object):
return result
@staticmethod
def attribute_counter(type_id, attr_id):
uri = current_app.config.get('CMDB_API')
url = "{}/ci/s?q=_type:{}&fl={}&facet={}".format(uri, type_id, attr_id, attr_id)
res = requests.get(url).json()
if res.get('facet'):
return dict([i[:2] for i in list(res.get('facet').values())[0]])
def attribute_counter(custom):
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
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])))
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 datetime
import toposort
from flask import abort
from flask import current_app
from flask_login import current_user
from toposort import toposort_flatten
from api.extensions import db
from api.lib.cmdb.attribute import AttributeManager
@@ -114,7 +116,7 @@ class CITypeManager(object):
@kwargs_required("name")
def add(cls, **kwargs):
unique_key = kwargs.pop("unique_key", None)
unique_key = kwargs.pop("unique_key", None) or kwargs.pop("unique_id", None)
unique_key = AttributeCache.get(unique_key) or abort(404, ErrFormat.unique_key_not_define)
kwargs["alias"] = kwargs["name"] if not kwargs.get("alias") else kwargs["alias"]
@@ -276,10 +278,10 @@ class CITypeGroupManager(object):
def update(gid, name, type_ids):
"""
update part
:param gid:
:param name:
:param type_ids:
:return:
:param gid:
:param name:
:param type_ids:
:return:
"""
existed = CITypeGroup.get_by_id(gid) or abort(
404, ErrFormat.ci_type_group_not_found.format("id={}".format(gid)))
@@ -370,6 +372,16 @@ class CITypeAttributeManager(object):
return result
@staticmethod
def get_common_attributes(type_ids):
result = CITypeAttribute.get_by(__func_in___key_type_id=list(map(int, type_ids)), to_dict=False)
attr2types = {}
for i in result:
attr2types.setdefault(i.attr_id, []).append(i.type_id)
return [AttributeCache.get(attr_id).to_dict() for attr_id in attr2types
if len(attr2types[attr_id]) == len(type_ids)]
@staticmethod
def _check(type_id, attr_ids):
ci_type = CITypeManager.check_is_existed(type_id)
@@ -386,10 +398,10 @@ class CITypeAttributeManager(object):
def add(cls, type_id, attr_ids=None, **kwargs):
"""
add attributes to CIType
:param type_id:
:param type_id:
:param attr_ids: list
:param kwargs:
:return:
:param kwargs:
:return:
"""
attr_ids = list(set(attr_ids))
@@ -416,9 +428,9 @@ class CITypeAttributeManager(object):
def update(cls, type_id, attributes):
"""
update attributes to CIType
:param type_id:
:param type_id:
:param attributes: list
:return:
:return:
"""
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):
"""
delete attributes from CIType
:param type_id:
:param type_id:
:param attr_ids: list
:return:
:return:
"""
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]
@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
def get_parents(cls, child_id):
parents = CITypeRelation.get_by(child_id=child_id, to_dict=False)
@@ -586,6 +614,17 @@ class CITypeRelationManager(object):
p = CITypeManager.check_is_existed(parent)
c = CITypeManager.check_is_existed(child)
rels = {}
for i in CITypeRelation.get_by(to_dict=False):
rels.setdefault(i.child_id, set()).add(i.parent_id)
rels.setdefault(c.id, set()).add(p.id)
try:
toposort_flatten(rels)
except toposort.CircularDependencyError as e:
current_app.logger.warning(str(e))
return abort(400, ErrFormat.circular_dependency_error)
existed = cls._get(p.id, c.id)
if existed is not None:
existed.update(relation_type_id=relation_type_id,
@@ -823,6 +862,12 @@ class CITypeTemplateManager(object):
for added_id in set(id2obj_dicts.keys()) - set(existed_ids):
if cls == CIType:
CITypeManager.add(**id2obj_dicts[added_id])
elif cls == CITypeRelation:
CITypeRelationManager.add(id2obj_dicts[added_id].get('parent_id'),
id2obj_dicts[added_id].get('child_id'),
id2obj_dicts[added_id].get('relation_type_id'),
id2obj_dicts[added_id].get('constraint'),
)
else:
cls.create(flush=True, **id2obj_dicts[added_id])

View File

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

View File

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

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

View File

@@ -9,6 +9,8 @@ class CommonErrFormat(object):
not_found = "不存在"
circular_dependency_error = "存在循环依赖!"
unknown_search_error = "未知搜索错误"
invalid_json = "json格式似乎不正确了, 请仔细确认一下!"

View File

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

View File

@@ -1,4 +1,4 @@
# -*- coding:utf-8 -*-
# -*- coding:utf-8 -*-
import json
@@ -154,9 +154,15 @@ class EnableCITypeView(APIView):
class CITypeAttributeView(APIView):
url_prefix = ("/ci_types/<int:type_id>/attributes", "/ci_types/<string:type_name>/attributes")
url_prefix = ("/ci_types/<int:type_id>/attributes", "/ci_types/<string:type_name>/attributes",
"/ci_types/common_attributes")
def get(self, type_id=None, type_name=None):
if request.path.endswith("/common_attributes"):
type_ids = handle_arg_list(request.values.get('type_ids'))
return self.jsonify(attributes=CITypeAttributeManager.get_common_attributes(type_ids))
t = CITypeCache.get(type_id) or CITypeCache.get(type_name) or abort(404, ErrFormat.ci_type_not_found)
type_id = t.id
unique_id = t.unique_id
@@ -500,3 +506,4 @@ class CITypeFilterPermissionView(APIView):
@auth_with_app_token
def get(self, type_id):
return self.jsonify(CIFilterPermsCRUD().get(type_id))

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

Binary file not shown.

View File

@@ -68,7 +68,8 @@ export default {
},
methods: {
visibleChange(open) {
visibleChange(open, isInitOne = true) {
// isInitOne 初始化exp为空时ruleList是否默认给一条
// const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
const exp = this.expression.match(new RegExp(this.regQ, 'g'))
? this.expression.match(new RegExp(this.regQ, 'g'))[0]
@@ -151,15 +152,20 @@ export default {
})
this.ruleList = [...expArray]
} else if (open) {
this.ruleList = [
{
id: uuidv4(),
type: 'and',
property: this.canSearchPreferenceAttrList[0].name,
exp: 'is',
value: null,
},
]
this.ruleList = isInitOne
? [
{
id: uuidv4(),
type: 'and',
property:
this.canSearchPreferenceAttrList && this.canSearchPreferenceAttrList.length
? this.canSearchPreferenceAttrList[0].name
: undefined,
exp: 'is',
value: null,
},
]
: []
}
},
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
@@ -153,3 +161,10 @@ export function canDefineComputed() {
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
})
}
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
})
}
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 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-space>
</div>
@@ -59,7 +62,7 @@
</template>
<script>
import { deleteCITypeAttributesById, deleteAttributesById } from '@/modules/cmdb/api/CITypeAttr'
import { deleteCITypeAttributesById, deleteAttributesById, calcComputedAttribute } from '@/modules/cmdb/api/CITypeAttr'
import ValueTypeIcon from '@/components/CMDBValueTypeMapIcon'
import {
ops_default_show,
@@ -165,6 +168,18 @@ export default {
openTrigger() {
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>

View File

@@ -10,7 +10,7 @@
:headerStyle="{ borderBottom: 'none' }"
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-col :span="12">
<a-form-item :label-col="formItemLayout.labelCol" :wrapper-col="formItemLayout.wrapperCol" label="属性名(英文)">
@@ -343,7 +343,13 @@
name="is_password"
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-col>
</a-row>
@@ -353,7 +359,7 @@
</a-form-item>
<div class="custom-drawer-bottom-action">
<a-button @click="onClose">取消</a-button>
<a-button @click="handleSubmit" type="primary">确定</a-button>
<a-button @click="handleSubmit(false)" type="primary">确定</a-button>
</div>
</a-form>
</CustomDrawer>
@@ -366,6 +372,7 @@ import {
updateAttributeById,
updateCITypeAttributesById,
canDefineComputed,
calcComputedAttribute,
} from '@/modules/cmdb/api/CITypeAttr'
import { valueTypeMap } from '../../utils/const'
import ComputedArea from './computedArea.vue'
@@ -576,15 +583,14 @@ export default {
})
},
handleSubmit(e) {
e.preventDefault()
this.form.validateFields((err, values) => {
async handleSubmit(isCalcComputed = false) {
await this.form.validateFields(async (err, values) => {
if (!err) {
console.log('Received values of form: ', values)
if (this.record.is_required !== values.is_required || this.record.default_show !== values.default_show) {
console.log('changed is_required')
updateCITypeAttributesById(this.CITypeId, {
await updateCITypeAttributesById(this.CITypeId, {
attributes: [
{ 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()
if (values.id) {
this.updateAttribute(values.id, { ...values, option: { fontOptions } })
await this.updateAttribute(values.id, { ...values, option: { fontOptions } }, isCalcComputed)
} else {
// this.createAttribute(values)
}
}
})
},
updateAttribute(attrId, data) {
updateAttributeById(attrId, data).then((res) => {
this.$message.success(`更新成功`)
this.handleOk()
this.onClose()
})
async updateAttribute(attrId, data, isCalcComputed = false) {
await updateAttributeById(attrId, data)
if (isCalcComputed) {
await calcComputedAttribute(attrId)
}
this.$message.success(`更新成功`)
this.handleOk()
this.onClose()
},
handleOk() {
this.$emit('ok')
@@ -682,6 +690,9 @@ export default {
default_value: key,
})
},
async handleCalcComputed() {
await this.handleSubmit(true)
},
},
watch: {},
}

View File

@@ -8,6 +8,14 @@
<span style="font-size:12px;" slot="tab">代码</span>
<codemirror style="z-index: 9999" :options="cmOptions" v-model="compute_script"></codemirror>
</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>
</template>
@@ -25,6 +33,10 @@ export default {
type: Boolean,
default: true,
},
showCalcComputed: {
type: Boolean,
default: false,
}
},
data() {
return {
@@ -62,6 +74,16 @@ export default {
this.activeKey = 'expr'
}
},
handleCalcComputed() {
const that = this
this.$confirm({
title: '警告',
content: `确认触发将保存当前配置及触发所有CI的计算`,
onOk() {
that.$emit('handleCalcComputed')
},
})
},
},
}
</script>

View File

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

View File

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

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

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 = {
0: { label: 'CI数统计' },
1: { label: '按属性值分类统计' },
2: { label: '关系统计' }
1: { label: '默认' },
2: { label: '关系' }
}

View File

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

View File

@@ -30,7 +30,7 @@ services:
- redis
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:
# context: .
# target: cmdb-api
@@ -61,7 +61,7 @@ services:
- cmdb-api
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:
# context: .
# target: cmdb-ui