Compare commits

...

27 Commits

Author SHA1 Message Date
pycook
65ef58dea9 feat: update docker-compose 2024-05-30 13:18:28 +08:00
pycook
0a2e7aa99f feat: put the mysql password in .env 2024-05-30 13:08:18 +08:00
pycook
8875e75883 fix(acl): add relation 2024-05-30 09:33:30 +08:00
pycook
2f03639c57 chore: release v2.4.5 2024-05-29 13:32:40 +08:00
pycook
49bc5d94a9 fix(api): topology view read permission 2024-05-29 11:34:02 +08:00
pycook
39354e1293 feat(ui): update components CMDBExprDrawer 2024-05-28 20:16:39 +08:00
pycook
d3714f3ecf feat(ui): relation-graph upgrade to 2.1.42 2024-05-28 20:11:53 +08:00
pycook
729a616282 feat(ui): topology view (#525) 2024-05-28 20:03:10 +08:00
fxiang21
2d3a290aa3 fix: cmdb-inner-secrets-init bug 2024-05-28 19:57:16 +08:00
pycook
9e885a5b12 feat(api): i18n update 2024-05-28 18:08:15 +08:00
simontigers
f5822d7cba Merge pull request #524 from simontigers/common_cmdb_app_perm
fix: cmdb app perms
2024-05-28 17:55:08 +08:00
hu.sima
21ea553e74 fix: cmdb app perms 2024-05-28 17:54:51 +08:00
pycook
e63038d1b6 feat(api): topology view (#523)
* feat(api): topology views crud

* feat(api): topology view

* feat(api): topology view api done

* feat(api): topology view is done
2024-05-28 17:50:09 +08:00
simontigers
d56806f511 Merge pull request #521 from simontigers/common_cmdb_app_perm
feat: CMDB add TopologyView resource
2024-05-28 16:21:25 +08:00
simontigers
7ac7fdc08e feat: CMDB add TopologyView resource 2024-05-28 16:20:56 +08:00
pycook
ba11707146 feat(ui): resource views router 2024-05-21 17:58:06 +08:00
pycook
d49dc8a067 fix(api): hot loading is blocked in development mode 2024-05-21 13:14:40 +08:00
pycook
6bfb34fe2a Dev UI 240520 (#517)
* feat(ui): Model configuration supports search models

* fix(ui): Jump to the first subscription by default
2024-05-20 14:10:33 +08:00
thexqn
2c7ed8c32d chore: Update local.md with instructions for setting up MySQL and Redis services (#515) 2024-05-20 13:39:05 +08:00
pycook
5b275af54e fix(api): exception when calling webhook (#516) 2024-05-20 13:22:54 +08:00
pycook
dde7ec6246 feat(ui): Resources and Preference support grouping 2024-05-19 21:55:36 +08:00
pycook
9181817e96 feat(api): my preference support grouping (#513) 2024-05-18 22:55:01 +08:00
pycook
46b54bb7f2 fix(ui): some bugs (#512) 2024-05-17 12:07:56 +08:00
pycook
fe63310c4e Dev api 240517 (#511)
* fix(api): list values delete

* fix(acl): role rebuild cache
2024-05-17 11:20:53 +08:00
pycook
27c733aa2c docs: update sql 2024-05-16 20:59:30 +08:00
pycook
2a8e9e684e fix(ui): issue#490 2024-05-02 21:28:06 +08:00
pycook
095190a785 fix(api): unique constraint (#505) 2024-05-02 21:22:40 +08:00
58 changed files with 4518 additions and 811 deletions

6
.env Normal file
View File

@@ -0,0 +1,6 @@
MYSQL_ROOT_PASSWORD='123456'
MYSQL_HOST='mysql'
MYSQL_PORT=3306
MYSQL_USER='cmdb'
MYSQL_DATABASE='cmdb'
MYSQL_PASSWORD='123456'

View File

@@ -36,7 +36,7 @@ Flask-Caching = ">=1.0.0"
environs = "==4.2.0"
marshmallow = "==2.20.2"
# async tasks
celery = ">=5.3.1"
celery = "==5.3.1"
celery_once = "==3.0.1"
more-itertools = "==5.0.0"
kombu = ">=5.3.1"

View File

@@ -128,7 +128,7 @@ def cmdb_init_acl():
perms = [PermEnum.READ]
elif resource_type == ResourceTypeEnum.CI_TYPE_RELATION:
perms = [PermEnum.ADD, PermEnum.DELETE, PermEnum.GRANT]
elif resource_type == ResourceTypeEnum.RELATION_VIEW:
elif resource_type in (ResourceTypeEnum.RELATION_VIEW, ResourceTypeEnum.TOPOLOGY_VIEW):
perms = [PermEnum.READ, PermEnum.UPDATE, PermEnum.DELETE, PermEnum.GRANT]
ResourceTypeCRUD.add(app_id, resource_type, '', perms)
@@ -326,7 +326,7 @@ def cmdb_inner_secrets_init(address):
"""
init inner secrets for password feature
"""
res, ok = KeyManage(backend=InnerKVManger).init()
res, ok = KeyManage(backend=InnerKVManger()).init()
if not ok:
if res.get("status") == "failed":
KeyManage.print_response(res)

View File

@@ -264,9 +264,11 @@ class CIManager(object):
for attr_id in constraint.attr_ids:
value_table = TableMap(attr_name=id2name[attr_id]).table
_ci_ids = set([i.ci_id for i in value_table.get_by(attr_id=attr_id,
to_dict=False,
value=ci_dict.get(id2name[attr_id]) or None)])
values = value_table.get_by(attr_id=attr_id,
value=ci_dict.get(id2name[attr_id]) or None,
only_query=True).join(
CI, CI.id == value_table.ci_id).filter(CI.type_id == type_id)
_ci_ids = set([i.ci_id for i in values])
if ci_ids is None:
ci_ids = _ci_ids
else:
@@ -437,7 +439,8 @@ class CIManager(object):
return ci.id
def update(self, ci_id, _is_admin=False, ticket_id=None, **ci_dict):
def update(self, ci_id, _is_admin=False, ticket_id=None, __sync=False, **ci_dict):
current_app.logger.info((ci_id, ci_dict, __sync))
now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
ci = self.confirm_ci_existed(ci_id)
@@ -501,11 +504,17 @@ class CIManager(object):
record_id = self.save_password(ci.id, attr_id, password_dict[attr_id], record_id, ci.type_id)
if record_id: # has change
ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE)
if not __sync:
ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE)
else:
ci_cache((ci_id, OperateType.UPDATE, record_id))
ref_ci_dict = {k: v for k, v in ci_dict.items() if k.startswith("$") and "." in k}
if ref_ci_dict:
ci_relation_add.apply_async(args=(ref_ci_dict, ci.id), queue=CMDB_QUEUE)
if not __sync:
ci_relation_add.apply_async(args=(ref_ci_dict, ci.id), queue=CMDB_QUEUE)
else:
ci_relation_add((ref_ci_dict, ci.id))
@staticmethod
def update_unique_value(ci_id, unique_name, unique_value):
@@ -829,6 +838,12 @@ class CIManager(object):
return data.get('v')
def baseline(self, ci_ids, before_date):
"""
return CI changes
:param ci_ids:
:param before_date:
:return:
"""
ci_list = self.get_cis_by_ids(ci_ids, ret_key=RetKey.ALIAS)
if not ci_list:
return dict()
@@ -912,6 +927,44 @@ class CIManager(object):
return result
def baseline_cis(self, ci_ids, before_date, fl=None):
"""
return CI changes
:param ci_ids:
:param before_date:
:param fl:
:return:
"""
ci_list = self.get_cis_by_ids(ci_ids, fields=fl)
if not ci_list:
return []
id2ci = {ci['_id']: ci for ci in ci_list}
changed = AttributeHistoryManger.get_records_for_attributes(
before_date, None, None, 1, 100000, None, None, ci_ids=ci_ids, more=True)[1]
for records in changed:
for change in records[1]:
if change['is_computed'] or change['is_password']:
continue
if change.get('default') and change['default'].get('default') == AttributeDefaultValueEnum.UPDATED_AT:
continue
if change['is_list']:
old, new, value_type, operate_type, ci_id, attr_name = (
change['old'], change['new'], change['value_type'], change['operate_type'],
change['ci_id'], change['attr_name'])
old = ValueTypeMap.deserialize[value_type](old) if old else old
new = ValueTypeMap.deserialize[value_type](new) if new else new
if operate_type == OperateType.ADD and new in (id2ci[ci_id][attr_name] or []):
id2ci[ci_id][attr_name].remove(new)
elif operate_type == OperateType.DELETE and old not in id2ci[ci_id][attr_name]:
id2ci[ci_id][attr_name].append(old)
else:
id2ci[change['ci_id']][change['attr_name']] = change['old']
return list(id2ci.values())
def rollback(self, ci_id, before_date):
baseline_ci = self.baseline([ci_id], before_date)
@@ -1037,6 +1090,18 @@ class CIRelationManager(object):
return ci_ids, level2ids
@classmethod
def get_parent_ids(cls, ci_ids):
cis = db.session.query(CIRelation.first_ci_id, CIRelation.second_ci_id, CI.type_id).join(
CI, CI.id == CIRelation.first_ci_id).filter(
CIRelation.second_ci_id.in_(ci_ids)).filter(CIRelation.deleted.is_(False))
result = {}
for ci in cis:
result.setdefault(ci.second_ci_id, []).append((ci.first_ci_id, ci.type_id))
return result
@staticmethod
def _check_constraint(first_ci_id, first_type_id, second_ci_id, second_type_id, type_relation):
db.session.commit()
@@ -1087,7 +1152,7 @@ class CIRelationManager(object):
relation_type_id or abort(404, ErrFormat.relation_not_found.format("{} -> {}".format(
first_ci.ci_type.name, second_ci.ci_type.name)))
if current_app.config.get('USE_ACL') and valid:
if current_app.config.get('USE_ACL') and valid and current_user.username != 'worker':
resource_name = CITypeRelationManager.acl_resource_name(first_ci.ci_type.name,
second_ci.ci_type.name)
if not ACLManager().has_permission(
@@ -1121,7 +1186,7 @@ class CIRelationManager(object):
def delete(cr_id):
cr = CIRelation.get_by_id(cr_id) or abort(404, ErrFormat.relation_not_found.format("id={}".format(cr_id)))
if current_app.config.get('USE_ACL'):
if current_app.config.get('USE_ACL') and current_user.username != 'worker':
resource_name = CITypeRelationManager.acl_resource_name(cr.first_ci.ci_type.name, cr.second_ci.ci_type.name)
if not ACLManager().has_permission(
resource_name,
@@ -1155,6 +1220,21 @@ class CIRelationManager(object):
return cr
@classmethod
def delete_3(cls, first_ci_id, second_ci_id):
cr = CIRelation.get_by(first_ci_id=first_ci_id,
second_ci_id=second_ci_id,
to_dict=False,
first=True)
if cr is not None:
ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id, cr.ancestor_ids), queue=CMDB_QUEUE)
delete_id_filter.apply_async(args=(second_ci_id,), queue=CMDB_QUEUE)
cls.delete(cr.id)
return cr
@classmethod
def batch_update(cls, ci_ids, parents, children, ancestor_ids=None):
"""

View File

@@ -5,7 +5,6 @@ import copy
import toposort
from flask import abort
from flask import current_app
from flask import session
from flask_login import current_user
from toposort import toposort_flatten
from werkzeug.exceptions import BadRequest
@@ -28,9 +27,11 @@ from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.cmdb.relation_type import RelationTypeManager
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.value import AttributeValueManager
from api.lib.common_setting.role_perm_base import CMDBApp
from api.lib.decorator import kwargs_required
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import is_app_admin
from api.lib.perm.acl.acl import validate_permission
from api.models.cmdb import Attribute
from api.models.cmdb import AutoDiscoveryCI
from api.models.cmdb import AutoDiscoveryCIType
@@ -48,6 +49,7 @@ from api.models.cmdb import CITypeTrigger
from api.models.cmdb import CITypeUniqueConstraint
from api.models.cmdb import CustomDashboard
from api.models.cmdb import PreferenceCITypeOrder
from api.models.cmdb import TopologyView
from api.models.cmdb import PreferenceRelationView
from api.models.cmdb import PreferenceSearchOption
from api.models.cmdb import PreferenceShowAttributes
@@ -130,7 +132,9 @@ class CITypeManager(object):
def add(cls, **kwargs):
if current_app.config.get('USE_ACL') and not is_app_admin('cmdb'):
if ErrFormat.ci_type_config not in {i['name'] for i in ACLManager().get_resources(ResourceTypeEnum.PAGE)}:
return abort(403, ErrFormat.no_permission2)
app_cli = CMDBApp()
validate_permission(app_cli.op.Model_Configuration, app_cli.resource_type_name,
app_cli.op.create_CIType, app_cli.app_name)
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)
@@ -260,6 +264,9 @@ class CITypeManager(object):
except Exception:
pass
for item in TopologyView.get_by(central_node_type=type_id, to_dict=False):
item.delete(commit=False)
db.session.commit()
ci_type.soft_delete()
@@ -822,6 +829,70 @@ class CITypeRelationManager(object):
return [cls._wrap_relation_type_dict(parent.parent_id, parent) for parent in parents]
@staticmethod
def get_relations_by_type_id(type_id):
nodes, edges = [], []
node_ids, edge_tuples = set(), set()
ci_type = CITypeCache.get(type_id)
if ci_type is None:
return nodes, edges
nodes.append(ci_type.to_dict())
node_ids.add(ci_type.id)
def _find(_id, lv):
lv += 1
for i in CITypeRelation.get_by(parent_id=_id, to_dict=False):
if i.child_id not in node_ids:
node_ids.add(i.child_id)
node = i.child.to_dict()
node.setdefault('level', []).append(lv)
nodes.append(node)
edges.append(dict(from_id=i.parent_id, to_id=i.child_id, text=i.relation_type.name, reverse=False))
edge_tuples.add((i.parent_id, i.child_id))
_find(i.child_id, lv)
continue
elif (i.parent_id, i.child_id) not in edge_tuples:
edges.append(dict(from_id=i.parent_id, to_id=i.child_id, text=i.relation_type.name, reverse=False))
edge_tuples.add((i.parent_id, i.child_id))
_find(i.child_id, lv)
for _node in nodes:
if _node['id'] == i.child_id and lv not in _node['level']:
_node['level'].append(lv)
def _reverse_find(_id, lv):
lv -= 1
for i in CITypeRelation.get_by(child_id=_id, to_dict=False):
if i.parent_id not in node_ids:
node_ids.add(i.parent_id)
node = i.parent.to_dict()
node.setdefault('level', []).append(lv)
nodes.append(node)
edges.append(dict(from_id=i.parent_id, to_id=i.child_id, text=i.relation_type.name, reverse=True))
edge_tuples.add((i.parent_id, i.child_id))
_reverse_find(i.parent_id, lv)
continue
elif (i.parent_id, i.child_id) not in edge_tuples:
edges.append(dict(from_id=i.parent_id, to_id=i.child_id, text=i.relation_type.name, reverse=True))
edge_tuples.add((i.parent_id, i.child_id))
_reverse_find(i.parent_id, lv)
for _node in nodes:
if _node['id'] == i.child_id and lv not in _node['level']:
_node['level'].append(lv)
level = 0
_reverse_find(ci_type.id, level)
level = 0
_find(ci_type.id, level)
return nodes, edges
@staticmethod
def _get(parent_id, child_id):
return CITypeRelation.get_by(parent_id=parent_id,

View File

@@ -73,6 +73,7 @@ class ResourceTypeEnum(BaseEnum):
RELATION_VIEW = "RelationView" # read/update/delete/grant
CI_FILTER = "CIFilter" # read
PAGE = "page" # read
TOPOLOGY_VIEW = "TopologyView" # read/update/delete/grant
class PermEnum(BaseEnum):

View File

@@ -25,6 +25,8 @@ from api.lib.cmdb.resp_format import ErrFormat
from api.lib.exception import AbortException
from api.lib.perm.acl.acl import ACLManager
from api.models.cmdb import CITypeAttribute
from api.models.cmdb import CITypeGroup
from api.models.cmdb import CITypeGroupItem
from api.models.cmdb import CITypeRelation
from api.models.cmdb import PreferenceCITypeOrder
from api.models.cmdb import PreferenceRelationView
@@ -43,22 +45,46 @@ class PreferenceManager(object):
def get_types(instance=False, tree=False):
ci_type_order = sorted(PreferenceCITypeOrder.get_by(uid=current_user.uid, to_dict=False), key=lambda x: x.order)
type2group = {}
for i in db.session.query(CITypeGroupItem, CITypeGroup).join(
CITypeGroup, CITypeGroup.id == CITypeGroupItem.group_id).filter(
CITypeGroup.deleted.is_(False)).filter(CITypeGroupItem.deleted.is_(False)):
type2group[i.CITypeGroupItem.type_id] = i.CITypeGroup.to_dict()
types = db.session.query(PreferenceShowAttributes.type_id).filter(
PreferenceShowAttributes.uid == current_user.uid).filter(
PreferenceShowAttributes.deleted.is_(False)).group_by(
PreferenceShowAttributes.type_id).all() if instance else []
types = sorted(types, key=lambda x: {i.type_id: idx for idx, i in enumerate(
ci_type_order) if not i.is_tree}.get(x.type_id, 1))
group_types = []
other_types = []
group2idx = {}
type_ids = set()
for ci_type in types:
type_id = ci_type.type_id
type_ids.add(type_id)
type_dict = CITypeCache.get(type_id).to_dict()
if type_id not in type2group:
other_types.append(type_dict)
else:
group = type2group[type_id]
if group['id'] not in group2idx:
group_types.append(type2group[type_id])
group2idx[group['id']] = len(group_types) - 1
group_types[group2idx[group['id']]].setdefault('ci_types', []).append(type_dict)
if other_types:
group_types.append(dict(ci_types=other_types))
tree_types = PreferenceTreeView.get_by(uid=current_user.uid, to_dict=False) if tree else []
tree_types = sorted(tree_types, key=lambda x: {i.type_id: idx for idx, i in enumerate(
ci_type_order) if i.is_tree}.get(x.type_id, 1))
type_ids = [i.type_id for i in types + tree_types]
if types and tree_types:
type_ids = set(type_ids)
tree_types = [CITypeCache.get(_type.type_id).to_dict() for _type in tree_types]
for _type in tree_types:
type_ids.add(_type['id'])
return [CITypeCache.get(type_id).to_dict() for type_id in type_ids]
return dict(group_types=group_types, tree_types=tree_types, type_ids=list(type_ids))
@staticmethod
def get_types2(instance=False, tree=False):

View File

@@ -140,3 +140,12 @@ class ErrFormat(CommonErrFormat):
password_save_failed = _l("Failed to save password: {}") # 保存密码失败: {}
password_load_failed = _l("Failed to get password: {}") # 获取密码失败: {}
cron_time_format_invalid = _l("Scheduling time format error") # 调度时间格式错误
reconciliation_title = _l("CMDB data reconciliation results") # CMDB数据合规检查结果
reconciliation_body = _l("Number of {} illegal: {}") # "{} 不合规数: {}"
topology_exists = _l("Topology view {} already exists") # 拓扑视图 {} 已经存在
topology_group_exists = _l("Topology group {} already exists") # 拓扑视图分组 {} 已经存在
# 因为该分组下定义了拓扑视图,不能删除
topo_view_exists_cannot_delete_group = _l("The group cannot be deleted because the topology view already exists")

View File

@@ -17,10 +17,12 @@ def search(query=None,
count=1,
sort=None,
excludes=None,
use_id_filter=False):
use_id_filter=False,
use_ci_filter=True):
if current_app.config.get("USE_ES"):
s = SearchFromES(query, fl, facet, page, ret_key, count, sort)
else:
s = SearchFromDB(query, fl, facet, page, ret_key, count, sort, excludes=excludes, use_id_filter=use_id_filter)
s = SearchFromDB(query, fl, facet, page, ret_key, count, sort, excludes=excludes,
use_id_filter=use_id_filter, use_ci_filter=use_ci_filter)
return s

View File

@@ -47,7 +47,8 @@ class Search(object):
excludes=None,
parent_node_perm_passed=False,
use_id_filter=False,
use_ci_filter=True):
use_ci_filter=True,
only_ids=False):
self.orig_query = query
self.fl = fl or []
self.excludes = excludes or []
@@ -64,10 +65,12 @@ class Search(object):
self.parent_node_perm_passed = parent_node_perm_passed
self.use_id_filter = use_id_filter
self.use_ci_filter = use_ci_filter
self.only_ids = only_ids
self.valid_type_names = []
self.type2filter_perms = dict()
self.is_app_admin = is_app_admin('cmdb') or current_user.username == "worker"
self.is_app_admin = self.is_app_admin or (not self.use_ci_filter and not self.use_id_filter)
@staticmethod
def _operator_proc(key):
@@ -590,6 +593,8 @@ class Search(object):
def search(self):
numfound, ci_ids = self._query_build_raw()
ci_ids = list(map(str, ci_ids))
if self.only_ids:
return ci_ids
_fl = self._fl_build()

View File

@@ -69,7 +69,7 @@ class Search(object):
if _l < int(level) and c == ConstraintEnum.Many2Many:
self.has_m2m = True
self.type2filter_perms = None
self.type2filter_perms = {}
self.is_app_admin = is_app_admin('cmdb') or current_user.username == "worker"
@@ -79,7 +79,7 @@ class Search(object):
key = []
_tmp = []
for level in range(1, sorted(self.level)[-1] + 1):
if len(self.descendant_ids) >= level and self.type2filter_perms.get(self.descendant_ids[level - 1]):
if len(self.descendant_ids or []) >= level and self.type2filter_perms.get(self.descendant_ids[level - 1]):
id_filter_limit, _ = self._get_ci_filter(self.type2filter_perms[self.descendant_ids[level - 1]])
else:
id_filter_limit = {}
@@ -151,9 +151,9 @@ class Search(object):
return True
def search(self):
use_ci_filter = len(self.descendant_ids) == self.level[0] - 1
parent_node_perm_passed = self._has_read_perm_from_parent_nodes()
def search(self, only_ids=False):
use_ci_filter = len(self.descendant_ids or []) == self.level[0] - 1
parent_node_perm_passed = not self.is_app_admin and self._has_read_perm_from_parent_nodes()
ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id
cis = [CI.get_by_id(_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(_id))) for _id in ids]
@@ -197,7 +197,8 @@ class Search(object):
sort=self.sort,
ci_ids=merge_ids,
parent_node_perm_passed=parent_node_perm_passed,
use_ci_filter=use_ci_filter).search()
use_ci_filter=use_ci_filter,
only_ids=only_ids).search()
def _get_ci_filter(self, filter_perms, ci_filters=None):
ci_filters = ci_filters or []

View File

@@ -0,0 +1,251 @@
# -*- coding:utf-8 -*-
import json
from flask import abort
from flask import current_app
from flask_login import current_user
from werkzeug.exceptions import BadRequest
from api.extensions import rd
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.ci_type import CITypeRelationManager
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import is_app_admin
from api.models.cmdb import TopologyView
from api.models.cmdb import TopologyViewGroup
class TopologyViewManager(object):
group_cls = TopologyViewGroup
cls = TopologyView
@classmethod
def get_name_by_id(cls, _id):
res = cls.cls.get_by_id(_id)
return res and res.name
def get_view_by_id(self, _id):
res = self.cls.get_by_id(_id)
return res and res.to_dict() or {}
@classmethod
def add_group(cls, name, order):
if order is None:
cur_max_order = cls.group_cls.get_by(only_query=True).order_by(cls.group_cls.order.desc()).first()
cur_max_order = cur_max_order and cur_max_order.order or 0
order = cur_max_order + 1
cls.group_cls.get_by(name=name, first=True, to_dict=False) and abort(
400, ErrFormat.topology_group_exists.format(name))
return cls.group_cls.create(name=name, order=order)
def update_group(self, group_id, name, view_ids):
existed = self.group_cls.get_by_id(group_id) or abort(404, ErrFormat.not_found)
if name is not None and name != existed.name:
existed.update(name=name)
for idx, view_id in enumerate(view_ids):
view = self.cls.get_by_id(view_id)
if view is not None:
view.update(group_id=group_id, order=idx)
return existed.to_dict()
@classmethod
def delete_group(cls, _id):
existed = cls.group_cls.get_by_id(_id) or abort(404, ErrFormat.not_found)
if cls.cls.get_by(group_id=_id, first=True):
return abort(400, ErrFormat.topo_view_exists_cannot_delete_group)
existed.soft_delete()
@classmethod
def group_order(cls, group_ids):
for idx, group_id in enumerate(group_ids):
group = cls.group_cls.get_by_id(group_id)
group.update(order=idx + 1)
@classmethod
def add(cls, name, group_id, option, order=None, **kwargs):
cls.cls.get_by(name=name, first=True) and abort(400, ErrFormat.topology_exists.format(name))
if order is None:
cur_max_order = cls.cls.get_by(group_id=group_id, only_query=True).order_by(
cls.cls.order.desc()).first()
cur_max_order = cur_max_order and cur_max_order.order or 0
order = cur_max_order + 1
inst = cls.cls.create(name=name, group_id=group_id, option=option, order=order, **kwargs).to_dict()
if current_app.config.get('USE_ACL'):
try:
ACLManager().add_resource(name, ResourceTypeEnum.TOPOLOGY_VIEW)
except BadRequest:
pass
ACLManager().grant_resource_to_role(name,
current_user.username,
ResourceTypeEnum.TOPOLOGY_VIEW)
return inst
@classmethod
def update(cls, _id, **kwargs):
existed = cls.cls.get_by_id(_id) or abort(404, ErrFormat.not_found)
existed_name = existed.name
inst = existed.update(filter_none=False, **kwargs).to_dict()
if current_app.config.get('USE_ACL') and existed_name != kwargs.get('name') and kwargs.get('name'):
try:
ACLManager().update_resource(existed_name, kwargs['name'], ResourceTypeEnum.TOPOLOGY_VIEW)
except BadRequest:
pass
return inst
@classmethod
def delete(cls, _id):
existed = cls.cls.get_by_id(_id) or abort(404, ErrFormat.not_found)
existed.soft_delete()
if current_app.config.get("USE_ACL"):
ACLManager().del_resource(existed.name, ResourceTypeEnum.TOPOLOGY_VIEW)
@classmethod
def group_inner_order(cls, _ids):
for idx, _id in enumerate(_ids):
topology = cls.cls.get_by_id(_id)
topology.update(order=idx + 1)
@classmethod
def get_all(cls):
resources = None
if current_app.config.get('USE_ACL') and not is_app_admin('cmdb'):
resources = set([i.get('name') for i in ACLManager().get_resources(ResourceTypeEnum.TOPOLOGY_VIEW)])
groups = cls.group_cls.get_by(to_dict=True)
groups = sorted(groups, key=lambda x: x['order'])
group2pos = {group['id']: idx for idx, group in enumerate(groups)}
topo_views = sorted(cls.cls.get_by(to_dict=True), key=lambda x: x['order'])
other_group = dict(views=[])
for view in topo_views:
if resources is not None and view['name'] not in resources:
continue
if view['group_id']:
groups[group2pos[view['group_id']]].setdefault('views', []).append(view)
else:
other_group['views'].append(view)
if other_group['views']:
groups.append(other_group)
return groups
@staticmethod
def relation_from_ci_type(type_id):
nodes, edges = CITypeRelationManager.get_relations_by_type_id(type_id)
return dict(nodes=nodes, edges=edges)
def topology_view(self, view_id=None, preview=None):
if view_id is not None:
view = self.cls.get_by_id(view_id) or abort(404, ErrFormat.not_found)
central_node_type, central_node_instances, path = (view.central_node_type,
view.central_node_instances, view.path)
else:
central_node_type = preview.get('central_node_type')
central_node_instances = preview.get('central_node_instances')
path = preview.get('path')
nodes, links = [], []
_type = CITypeCache.get(central_node_type)
if not _type:
return dict(nodes=nodes, links=links)
root_ids = []
show_key = AttributeCache.get(_type.show_id or _type.unique_id)
q = (central_node_instances[2:] if central_node_instances.startswith('q=') else
central_node_instances)
s = search(q, fl=['_id', show_key.name], use_id_filter=False, use_ci_filter=False, count=1000000)
try:
response, _, _, _, _, _ = s.search()
except SearchError as e:
current_app.logger.info(e)
return dict(nodes=nodes, links=links)
for i in response:
root_ids.append(i['_id'])
nodes.append(dict(id=str(i['_id']), name=i[show_key.name], type_id=central_node_type))
if not root_ids:
return dict(nodes=nodes, links=links)
prefix = REDIS_PREFIX_CI_RELATION
key = list(map(str, root_ids))
id2node = {}
type2meta = {}
for level in sorted([i for i in path.keys() if int(i) > 0]):
type_ids = {int(i) for i in path[level]}
res = [json.loads(x).items() for x in [i or '{}' for i in rd.get(key, prefix) or []]]
new_key = []
for idx, from_id in enumerate(key):
for to_id, type_id in res[idx]:
if type_id in type_ids:
links.append({'from': from_id, 'to': to_id})
id2node[to_id] = {'id': to_id, 'type_id': type_id}
new_key.append(to_id)
if type_id not in type2meta:
type2meta[type_id] = CITypeCache.get(type_id).icon
key = new_key
ci_ids = list(map(int, root_ids))
for level in sorted([i for i in path.keys() if int(i) < 0]):
type_ids = {int(i) for i in path[level]}
res = CIRelationManager.get_parent_ids(ci_ids)
_ci_ids = []
for to_id in res:
for from_id, type_id in res[to_id]:
if type_id in type_ids:
from_id, to_id = str(from_id), str(to_id)
links.append({'from': from_id, 'to': to_id})
id2node[from_id] = {'id': str(from_id), 'type_id': type_id}
_ci_ids.append(from_id)
if type_id not in type2meta:
type2meta[type_id] = CITypeCache.get(type_id).icon
ci_ids = _ci_ids
fl = set()
type_ids = {t for lv in path if lv != '0' for t in path[lv]}
type2show = {}
for type_id in type_ids:
ci_type = CITypeCache.get(type_id)
if ci_type:
attr = AttributeCache.get(ci_type.show_id or ci_type.unique_id)
if attr:
fl.add(attr.name)
type2show[type_id] = attr.name
if id2node:
s = search("_id:({})".format(';'.join(id2node.keys())), fl=list(fl),
use_id_filter=False, use_ci_filter=False, count=1000000)
try:
response, _, _, _, _, _ = s.search()
except SearchError:
return dict(nodes=nodes, links=links)
for i in response:
id2node[str(i['_id'])]['name'] = i[type2show[str(i['_type'])]]
nodes.extend(id2node.values())
return dict(nodes=nodes, links=links, type2meta=type2meta)

View File

@@ -274,7 +274,8 @@ class AttributeValueManager(object):
if attr.is_list:
existed_attrs = value_table.get_by(attr_id=attr.id, ci_id=ci.id, to_dict=False)
existed_values = [i.value for i in existed_attrs]
existed_values = [(ValueTypeMap.serialize[attr.value_type](i.value) if
i.value or i.value == 0 else i.value) for i in existed_attrs]
# Comparison array starts from which position changes
min_len = min(len(value), len(existed_values))
@@ -283,17 +284,15 @@ class AttributeValueManager(object):
if value[index] != existed_values[index]:
break
index += 1
added = value[index:]
deleted = existed_values[index:]
# Delete first and then add to ensure id sorting
for v in deleted:
existed_attr = existed_attrs[existed_values.index(v)]
for idx in range(index, len(existed_attrs)):
existed_attr = existed_attrs[idx]
existed_attr.delete(flush=False, commit=False)
changed.append((ci.id, attr.id, OperateType.DELETE, v, None, ci.type_id))
for v in added:
value_table.create(ci_id=ci.id, attr_id=attr.id, value=v, flush=False, commit=False)
changed.append((ci.id, attr.id, OperateType.ADD, None, v, ci.type_id))
changed.append((ci.id, attr.id, OperateType.DELETE, existed_values[idx], None, ci.type_id))
for idx in range(index, len(value)):
value_table.create(ci_id=ci.id, attr_id=attr.id, value=value[idx], flush=False, commit=False)
changed.append((ci.id, attr.id, OperateType.ADD, None, value[idx], ci.type_id))
else:
existed_attr = value_table.get_by(attr_id=attr.id, ci_id=ci.id, first=True, to_dict=False)
existed_value = existed_attr and existed_attr.value

View File

@@ -48,7 +48,12 @@ class CMDBApp(BaseApp):
{"page": "Model_Relationships", "page_cn": "模型关系", "perms": ["read"]},
{"page": "Operation_Audit", "page_cn": "操作审计", "perms": ["read"]},
{"page": "Relationship_Types", "page_cn": "关系类型", "perms": ["read"]},
{"page": "Auto_Discovery", "page_cn": "自动发现", "perms": ["read"]}]
{"page": "Auto_Discovery", "page_cn": "自动发现", "perms": ["read"]},
{"page": "TopologyView", "page_cn": "拓扑视图",
"perms": ["read", "create_topology_group", "update_topology_group", "delete_topology_group",
"create_topology_view"],
},
]
def __init__(self):
super().__init__()

View File

@@ -153,11 +153,11 @@ class ACLManager(object):
if resource:
return ResourceCRUD.delete(resource.id, rebuild=rebuild)
def has_permission(self, resource_name, resource_type, perm, resource_id=None):
def has_permission(self, resource_name, resource_type, perm, resource_id=None, rid=None):
if is_app_admin(self.app_id):
return True
role = self._get_role(current_user.username)
role = self._get_role(current_user.username) if rid is None else RoleCache.get(rid)
role or abort(404, ErrFormat.role_not_found.format(current_user.username))

View File

@@ -5,6 +5,7 @@ import msgpack
import redis_lock
from api.extensions import cache
from api.extensions import db
from api.extensions import rd
from api.lib.decorator import flush_db
from api.models.acl import App
@@ -157,9 +158,10 @@ class RoleRelationCache(object):
PREFIX_RESOURCES2 = "RoleRelationResources2::id::{0}::AppId::{1}"
@classmethod
def get_parent_ids(cls, rid, app_id):
def get_parent_ids(cls, rid, app_id, force=False):
parent_ids = cache.get(cls.PREFIX_PARENT.format(rid, app_id))
if not parent_ids:
if not parent_ids or force:
db.session.commit()
from api.lib.perm.acl.role import RoleRelationCRUD
parent_ids = RoleRelationCRUD.get_parent_ids(rid, app_id)
cache.set(cls.PREFIX_PARENT.format(rid, app_id), parent_ids, timeout=0)
@@ -167,9 +169,10 @@ class RoleRelationCache(object):
return parent_ids
@classmethod
def get_child_ids(cls, rid, app_id):
def get_child_ids(cls, rid, app_id, force=False):
child_ids = cache.get(cls.PREFIX_CHILDREN.format(rid, app_id))
if not child_ids:
if not child_ids or force:
db.session.commit()
from api.lib.perm.acl.role import RoleRelationCRUD
child_ids = RoleRelationCRUD.get_child_ids(rid, app_id)
cache.set(cls.PREFIX_CHILDREN.format(rid, app_id), child_ids, timeout=0)
@@ -177,14 +180,16 @@ class RoleRelationCache(object):
return child_ids
@classmethod
def get_resources(cls, rid, app_id):
def get_resources(cls, rid, app_id, force=False):
"""
:param rid:
:param app_id:
:param force:
:return: {id2perms: {resource_id: [perm,]}, group2perms: {group_id: [perm, ]}}
"""
resources = cache.get(cls.PREFIX_RESOURCES.format(rid, app_id))
if not resources:
if not resources or force:
db.session.commit()
from api.lib.perm.acl.role import RoleCRUD
resources = RoleCRUD.get_resources(rid, app_id)
if resources['id2perms'] or resources['group2perms']:
@@ -193,9 +198,10 @@ class RoleRelationCache(object):
return resources or {}
@classmethod
def get_resources2(cls, rid, app_id):
def get_resources2(cls, rid, app_id, force=False):
r_g = cache.get(cls.PREFIX_RESOURCES2.format(rid, app_id))
if not r_g:
if not r_g or force:
db.session.commit()
res = cls.get_resources(rid, app_id)
id2perms = res['id2perms']
group2perms = res['group2perms']
@@ -232,20 +238,20 @@ class RoleRelationCache(object):
for _app_id in app_ids:
cls.clean(rid, _app_id)
cls.get_parent_ids(rid, _app_id)
cls.get_child_ids(rid, _app_id)
resources = cls.get_resources(rid, _app_id)
cls.get_parent_ids(rid, _app_id, force=True)
cls.get_child_ids(rid, _app_id, force=True)
resources = cls.get_resources(rid, _app_id, force=True)
if resources.get('id2perms') or resources.get('group2perms'):
HasResourceRoleCache.add(rid, _app_id)
else:
HasResourceRoleCache.remove(rid, _app_id)
cls.get_resources2(rid, _app_id)
cls.get_resources2(rid, _app_id, force=True)
@classmethod
@flush_db
def rebuild2(cls, rid, app_id):
cache.delete(cls.PREFIX_RESOURCES2.format(rid, app_id))
cls.get_resources2(rid, app_id)
cls.get_resources2(rid, app_id, force=True)
@classmethod
def clean(cls, rid, app_id):

View File

@@ -315,9 +315,12 @@ class ResourceCRUD(object):
return resource
@staticmethod
def delete(_id, rebuild=True):
def delete(_id, rebuild=True, app_id=None):
resource = Resource.get_by_id(_id) or abort(404, ErrFormat.resource_not_found.format("id={}".format(_id)))
if app_id is not None and resource.app_id != app_id:
return abort(404, ErrFormat.resource_not_found.format("id={}".format(_id)))
origin = resource.to_dict()
resource.soft_delete()

View File

@@ -154,19 +154,19 @@ class RoleRelationCRUD(object):
if existed:
continue
RoleRelationCache.clean(parent_id, app_id)
RoleRelationCache.clean(child_id, app_id)
if parent_id in cls.recursive_child_ids(child_id, app_id):
return abort(400, ErrFormat.inheritance_dead_loop)
result.append(RoleRelation.create(parent_id=parent_id, child_id=child_id, app_id=app_id).to_dict())
RoleRelationCache.clean(parent_id, app_id)
RoleRelationCache.clean(child_id, app_id)
if app_id is None:
for app in AppCRUD.get_all():
if app.name != "acl":
RoleRelationCache.clean(child_id, app.id)
result.append(RoleRelation.create(parent_id=parent_id, child_id=child_id, app_id=app_id).to_dict())
AuditCRUD.add_role_log(app_id, AuditOperateType.role_relation_add,
AuditScope.role_relation, role.id, {}, {},
{'child_ids': list(child_ids), 'parent_ids': [parent_id], }
@@ -379,16 +379,16 @@ class RoleCRUD(object):
resource_type_id = resource_type and resource_type.id
result = dict(resources=dict(), groups=dict())
s = time.time()
# s = time.time()
parent_ids = RoleRelationCRUD.recursive_parent_ids(rid, app_id)
current_app.logger.info('parent ids {0}: {1}'.format(parent_ids, time.time() - s))
# current_app.logger.info('parent ids {0}: {1}'.format(parent_ids, time.time() - s))
for parent_id in parent_ids:
_resources, _groups = cls._extend_resources(parent_id, resource_type_id, app_id)
current_app.logger.info('middle1: {0}'.format(time.time() - s))
# current_app.logger.info('middle1: {0}'.format(time.time() - s))
_merge(result['resources'], _resources)
current_app.logger.info('middle2: {0}'.format(time.time() - s))
current_app.logger.info(len(_groups))
# current_app.logger.info('middle2: {0}'.format(time.time() - s))
# current_app.logger.info(len(_groups))
if not group_flat:
_merge(result['groups'], _groups)
else:
@@ -399,7 +399,7 @@ class RoleCRUD(object):
item.setdefault('permissions', [])
item['permissions'] = list(set(item['permissions'] + _groups[rg_id]['permissions']))
result['resources'][item['id']] = item
current_app.logger.info('End: {0}'.format(time.time() - s))
# current_app.logger.info('End: {0}'.format(time.time() - s))
result['resources'] = list(result['resources'].values())
result['groups'] = list(result['groups'].values())

View File

@@ -3,8 +3,8 @@ import os
import secrets
import sys
import threading
from base64 import b64decode, b64encode
from Cryptodome.Protocol.SecretSharing import Shamir
from colorama import Back, Fore, Style, init as colorama_init
from cryptography.hazmat.backends import default_backend
@@ -30,6 +30,7 @@ seal_status = True
secrets_encrypt_key = ""
secrets_root_key = ""
def string_to_bytes(value):
if not value:
return ""
@@ -78,7 +79,7 @@ class KeyManage:
(len(sys.argv) > 1 and sys.argv[1] in ("run", "cmdb-password-data-migrate"))):
self.backend = backend
threading.Thread(target=self.watch_root_key, args=(app,)).start()
threading.Thread(target=self.watch_root_key, args=(app,), daemon=True).start()
self.trigger = app.config.get("INNER_TRIGGER_TOKEN")
if not self.trigger:
@@ -412,7 +413,7 @@ class KeyManage:
class InnerCrypt:
def __init__(self):
self.encrypt_key = b64decode(secrets_encrypt_key)
#self.encrypt_key = b64decode(secrets_encrypt_key, "".encode("utf-8"))
# self.encrypt_key = b64decode(secrets_encrypt_key, "".encode("utf-8"))
def encrypt(self, plaintext):
"""
@@ -490,4 +491,4 @@ if __name__ == "__main__":
t_ciphertext, status1 = c.encrypt(t_plaintext)
print("Ciphertext:", t_ciphertext)
decrypted_plaintext, status2 = c.decrypt(t_ciphertext)
print("Decrypted plaintext:", decrypted_plaintext)
print("Decrypted plaintext:", decrypted_plaintext)

View File

@@ -88,11 +88,11 @@ def webhook_request(webhook, payload):
params = webhook.get('parameters') or None
if isinstance(params, dict):
params = json.loads(Template(json.dumps(params)).render(payload))
params = json.loads(Template(json.dumps(params)).render(payload).encode('utf-8'))
headers = json.loads(Template(json.dumps(webhook.get('headers') or {})).render(payload))
data = Template(json.dumps(webhook.get('body', ''))).render(payload)
data = Template(json.dumps(webhook.get('body', ''))).render(payload).encode('utf-8')
auth = _wrap_auth(**webhook.get('authorization', {}))
if (webhook.get('authorization', {}).get("type") or '').lower() == 'oauth2.0':

View File

@@ -209,6 +209,26 @@ class CITriggerHistory(Model):
webhook = db.Column(db.Text)
class TopologyViewGroup(Model):
__tablename__ = 'c_topology_view_groups'
name = db.Column(db.String(64), index=True)
order = db.Column(db.Integer, default=0)
class TopologyView(Model):
__tablename__ = 'c_topology_views'
name = db.Column(db.String(64), index=True)
group_id = db.Column(db.Integer, db.ForeignKey('c_topology_view_groups.id'))
category = db.Column(db.String(32))
central_node_type = db.Column(db.Integer)
central_node_instances = db.Column(db.Text)
path = db.Column(db.JSON)
order = db.Column(db.Integer, default=0)
option = db.Column(db.JSON)
class CITypeUniqueConstraint(Model):
__tablename__ = "c_c_t_u_c"

View File

@@ -113,18 +113,17 @@ def ci_delete_trigger(trigger, operate_type, ci_dict):
@reconnect_db
def ci_relation_cache(parent_id, child_id, ancestor_ids):
with redis_lock.Lock(rd.r, "CIRelation_{}".format(parent_id)):
if ancestor_ids is None:
children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0]
children = json.loads(children) if children is not None else {}
children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0]
children = json.loads(children) if children is not None else {}
cr = CIRelation.get_by(first_ci_id=parent_id, second_ci_id=child_id, ancestor_ids=ancestor_ids,
first=True, to_dict=False)
if str(child_id) not in children:
children[str(child_id)] = cr.second_ci.type_id
cr = CIRelation.get_by(first_ci_id=parent_id, second_ci_id=child_id, ancestor_ids=ancestor_ids,
first=True, to_dict=False)
if str(child_id) not in children:
children[str(child_id)] = cr.second_ci.type_id
rd.create_or_update({parent_id: json.dumps(children)}, REDIS_PREFIX_CI_RELATION)
rd.create_or_update({parent_id: json.dumps(children)}, REDIS_PREFIX_CI_RELATION)
else:
if ancestor_ids is not None:
key = "{},{}".format(ancestor_ids, parent_id)
grandson = rd.get([key], REDIS_PREFIX_CI_RELATION2)[0]
grandson = json.loads(grandson) if grandson is not None else {}
@@ -191,16 +190,15 @@ def ci_relation_add(parent_dict, child_id, uid):
@reconnect_db
def ci_relation_delete(parent_id, child_id, ancestor_ids):
with redis_lock.Lock(rd.r, "CIRelation_{}".format(parent_id)):
if ancestor_ids is None:
children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0]
children = json.loads(children) if children is not None else {}
children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0]
children = json.loads(children) if children is not None else {}
if str(child_id) in children:
children.pop(str(child_id))
if str(child_id) in children:
children.pop(str(child_id))
rd.create_or_update({parent_id: json.dumps(children)}, REDIS_PREFIX_CI_RELATION)
rd.create_or_update({parent_id: json.dumps(children)}, REDIS_PREFIX_CI_RELATION)
else:
if ancestor_ids is not None:
key = "{},{}".format(ancestor_ids, parent_id)
grandson = rd.get([key], REDIS_PREFIX_CI_RELATION2)[0]
grandson = json.loads(grandson) if grandson is not None else {}

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2024-03-29 10:42+0800\n"
"POT-Creation-Date: 2024-05-28 18:05+0800\n"
"PO-Revision-Date: 2023-12-25 20:21+0800\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: zh\n"
@@ -284,166 +284,198 @@ msgstr "重复的触发器"
msgid "Trigger {} does not exist"
msgstr "触发器 {} 不存在"
#: api/lib/cmdb/resp_format.py:81
msgid "Duplicated reconciliation rule"
msgstr ""
#: api/lib/cmdb/resp_format.py:82
msgid "Reconciliation rule {} does not exist"
msgstr "关系类型 {} 不存在"
#: api/lib/cmdb/resp_format.py:84
msgid "Operation record {} does not exist"
msgstr "操作记录 {} 不存在"
#: api/lib/cmdb/resp_format.py:83
#: api/lib/cmdb/resp_format.py:85
msgid "Unique identifier cannot be deleted"
msgstr "不能删除唯一标识"
#: api/lib/cmdb/resp_format.py:84
#: api/lib/cmdb/resp_format.py:86
msgid "Cannot delete default sorted attributes"
msgstr "不能删除默认排序的属性"
#: api/lib/cmdb/resp_format.py:86
#: api/lib/cmdb/resp_format.py:88
msgid "No node selected"
msgstr "没有选择节点"
#: api/lib/cmdb/resp_format.py:87
#: api/lib/cmdb/resp_format.py:89
msgid "This search option does not exist!"
msgstr "该搜索选项不存在!"
#: api/lib/cmdb/resp_format.py:88
#: api/lib/cmdb/resp_format.py:90
msgid "This search option has a duplicate name!"
msgstr "该搜索选项命名重复!"
#: api/lib/cmdb/resp_format.py:90
#: api/lib/cmdb/resp_format.py:92
msgid "Relationship type {} already exists"
msgstr "关系类型 {} 已经存在"
#: api/lib/cmdb/resp_format.py:91
#: api/lib/cmdb/resp_format.py:93
msgid "Relationship type {} does not exist"
msgstr "关系类型 {} 不存在"
#: api/lib/cmdb/resp_format.py:93
#: api/lib/cmdb/resp_format.py:95
msgid "Invalid attribute value: {}"
msgstr "无效的属性值: {}"
#: api/lib/cmdb/resp_format.py:94
#: api/lib/cmdb/resp_format.py:96
msgid "{} Invalid value: {}"
msgstr "{} 无效的值: {}"
#: api/lib/cmdb/resp_format.py:95
#: api/lib/cmdb/resp_format.py:97
msgid "{} is not in the predefined values"
msgstr "{} 不在预定义值里"
#: api/lib/cmdb/resp_format.py:97
#: api/lib/cmdb/resp_format.py:99
msgid "The value of attribute {} must be unique, {} already exists"
msgstr "属性 {} 的值必须是唯一的, 当前值 {} 已存在"
#: api/lib/cmdb/resp_format.py:98
#: api/lib/cmdb/resp_format.py:100
msgid "Attribute {} value must exist"
msgstr "属性 {} 值必须存在"
#: api/lib/cmdb/resp_format.py:99
#: api/lib/cmdb/resp_format.py:101
msgid "Out of range value, the maximum value is 2147483647"
msgstr "超过最大值限制, 最大值是2147483647"
#: api/lib/cmdb/resp_format.py:101
#: api/lib/cmdb/resp_format.py:103
msgid "Unknown error when adding or modifying attribute value: {}"
msgstr "新增或者修改属性值未知错误: {}"
#: api/lib/cmdb/resp_format.py:103
#: api/lib/cmdb/resp_format.py:105
msgid "Duplicate custom name"
msgstr "订制名重复"
#: api/lib/cmdb/resp_format.py:105
#: api/lib/cmdb/resp_format.py:107
msgid "Number of models exceeds limit: {}"
msgstr "模型数超过限制: {}"
#: api/lib/cmdb/resp_format.py:106
#: api/lib/cmdb/resp_format.py:108
msgid "The number of CIs exceeds the limit: {}"
msgstr "CI数超过限制: {}"
#: api/lib/cmdb/resp_format.py:108
#: api/lib/cmdb/resp_format.py:110
msgid "Auto-discovery rule: {} already exists!"
msgstr "自动发现规则: {} 已经存在!"
#: api/lib/cmdb/resp_format.py:109
#: api/lib/cmdb/resp_format.py:111
msgid "Auto-discovery rule: {} does not exist!"
msgstr "自动发现规则: {} 不存在!"
#: api/lib/cmdb/resp_format.py:111
#: api/lib/cmdb/resp_format.py:113
msgid "This auto-discovery rule is referenced by the model and cannot be deleted!"
msgstr "该自动发现规则被模型引用, 不能删除!"
#: api/lib/cmdb/resp_format.py:113
#: api/lib/cmdb/resp_format.py:115
msgid "The application of auto-discovery rules cannot be defined repeatedly!"
msgstr "自动发现规则的应用不能重复定义!"
#: api/lib/cmdb/resp_format.py:114
#: api/lib/cmdb/resp_format.py:116
msgid "The auto-discovery you want to modify: {} does not exist!"
msgstr "您要修改的自动发现: {} 不存在!"
#: api/lib/cmdb/resp_format.py:115
#: api/lib/cmdb/resp_format.py:117
msgid "Attribute does not include unique identifier: {}"
msgstr "属性字段没有包括唯一标识: {}"
#: api/lib/cmdb/resp_format.py:116
#: api/lib/cmdb/resp_format.py:118
msgid "The auto-discovery instance does not exist!"
msgstr "自动发现的实例不存在!"
#: api/lib/cmdb/resp_format.py:117
#: api/lib/cmdb/resp_format.py:119
msgid "The model is not associated with this auto-discovery!"
msgstr "模型并未关联该自动发现!"
#: api/lib/cmdb/resp_format.py:118
#: api/lib/cmdb/resp_format.py:120
msgid "Only the creator can modify the Secret!"
msgstr "只有创建人才能修改Secret!"
#: api/lib/cmdb/resp_format.py:120
#: api/lib/cmdb/resp_format.py:122
msgid "This rule already has auto-discovery instances and cannot be deleted!"
msgstr "该规则已经有自动发现的实例, 不能被删除!"
#: api/lib/cmdb/resp_format.py:122
#: api/lib/cmdb/resp_format.py:124
msgid "The default auto-discovery rule is already referenced by model {}!"
msgstr "该默认的自动发现规则 已经被模型 {} 引用!"
#: api/lib/cmdb/resp_format.py:124
#: api/lib/cmdb/resp_format.py:126
msgid "The unique_key method must return a non-empty string!"
msgstr "unique_key方法必须返回非空字符串!"
#: api/lib/cmdb/resp_format.py:125
#: api/lib/cmdb/resp_format.py:127
msgid "The attributes method must return a list"
msgstr "attributes方法必须返回的是list"
#: api/lib/cmdb/resp_format.py:127
#: api/lib/cmdb/resp_format.py:129
msgid "The list returned by the attributes method cannot be empty!"
msgstr "attributes方法返回的list不能为空!"
#: api/lib/cmdb/resp_format.py:129
#: api/lib/cmdb/resp_format.py:131
msgid "Only administrators can define execution targets as: all nodes!"
msgstr "只有管理员才可以定义执行机器为: 所有节点!"
#: api/lib/cmdb/resp_format.py:130
#: api/lib/cmdb/resp_format.py:132
msgid "Execute targets permission check failed: {}"
msgstr "执行机器权限检查不通过: {}"
#: api/lib/cmdb/resp_format.py:132
#: api/lib/cmdb/resp_format.py:134
msgid "CI filter authorization must be named!"
msgstr "CI过滤授权 必须命名!"
#: api/lib/cmdb/resp_format.py:133
#: api/lib/cmdb/resp_format.py:135
msgid "CI filter authorization is currently not supported or query"
msgstr "CI过滤授权 暂时不支持 或 查询"
#: api/lib/cmdb/resp_format.py:136
#: api/lib/cmdb/resp_format.py:138
msgid "You do not have permission to operate attribute {}!"
msgstr "您没有属性 {} 的操作权限!"
#: api/lib/cmdb/resp_format.py:137
#: api/lib/cmdb/resp_format.py:139
msgid "You do not have permission to operate this CI!"
msgstr "您没有该CI的操作权限!"
#: api/lib/cmdb/resp_format.py:139
#: api/lib/cmdb/resp_format.py:141
msgid "Failed to save password: {}"
msgstr "保存密码失败: {}"
#: api/lib/cmdb/resp_format.py:140
#: api/lib/cmdb/resp_format.py:142
msgid "Failed to get password: {}"
msgstr "获取密码失败: {}"
#: api/lib/cmdb/resp_format.py:144
msgid "Scheduling time format error"
msgstr "{}格式错误,应该为:%Y-%m-%d %H:%M:%S"
#: api/lib/cmdb/resp_format.py:145
msgid "CMDB data reconciliation results"
msgstr ""
#: api/lib/cmdb/resp_format.py:146
msgid "Number of {} illegal: {}"
msgstr ""
#: api/lib/cmdb/resp_format.py:148
msgid "Topology view {} already exists"
msgstr "拓扑视图 {} 已经存在"
#: api/lib/cmdb/resp_format.py:149
msgid "Topology group {} already exists"
msgstr "拓扑视图分组 {} 已经存在"
#: api/lib/cmdb/resp_format.py:151
msgid "The group cannot be deleted because the topology view already exists"
msgstr "因为该分组下定义了拓扑视图,不能删除"
#: api/lib/common_setting/resp_format.py:8
msgid "Company info already existed"
msgstr "公司信息已存在,无法创建!"
@@ -700,6 +732,10 @@ msgstr "LDAP测试用户名必填"
msgid "Company wide"
msgstr "全公司"
#: api/lib/common_setting/resp_format.py:84
msgid "No permission to access resource {}, perm {} "
msgstr "您没有资源: {} 的 {} 权限"
#: api/lib/perm/acl/resp_format.py:9
msgid "login successful"
msgstr "登录成功"

View File

@@ -0,0 +1,178 @@
# -*- coding:utf-8 -*-
from flask import abort
from flask import request
from api.lib.cmdb.const import PermEnum, ResourceTypeEnum
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.topology import TopologyViewManager
from api.lib.common_setting.decorator import perms_role_required
from api.lib.common_setting.role_perm_base import CMDBApp
from api.lib.decorator import args_required
from api.lib.decorator import args_validate
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import has_perm_from_args
from api.lib.perm.acl.acl import is_app_admin
from api.resource import APIView
app_cli = CMDBApp()
class TopologyGroupView(APIView):
url_prefix = ('/topology_views/groups', '/topology_views/groups/<int:group_id>')
@args_required('name')
@args_validate(TopologyViewManager.group_cls)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.TopologyView,
app_cli.op.create_topology_group, app_cli.admin_name)
def post(self):
name = request.values.get('name')
order = request.values.get('order')
group = TopologyViewManager.add_group(name, order)
return self.jsonify(group.to_dict())
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.TopologyView,
app_cli.op.update_topology_group, app_cli.admin_name)
def put(self, group_id):
name = request.values.get('name')
view_ids = request.values.get('view_ids')
group = TopologyViewManager().update_group(group_id, name, view_ids)
return self.jsonify(**group)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.TopologyView,
app_cli.op.delete_topology_group, app_cli.admin_name)
def delete(self, group_id):
TopologyViewManager.delete_group(group_id)
return self.jsonify(group_id=group_id)
class TopologyGroupOrderView(APIView):
url_prefix = ('/topology_views/groups/order',)
@args_required('group_ids')
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.TopologyView,
app_cli.op.update_topology_group, app_cli.admin_name)
def post(self):
group_ids = request.values.get('group_ids')
TopologyViewManager.group_order(group_ids)
return self.jsonify(group_ids=group_ids)
def put(self):
return self.post()
class TopologyView(APIView):
url_prefix = ('/topology_views', '/topology_views/relations/ci_types/<int:type_id>', '/topology_views/<int:_id>')
def get(self, type_id=None, _id=None):
if type_id is not None:
return self.jsonify(TopologyViewManager.relation_from_ci_type(type_id))
if _id is not None:
return self.jsonify(TopologyViewManager().get_view_by_id(_id))
return self.jsonify(TopologyViewManager.get_all())
@args_required('name', 'central_node_type', 'central_node_instances', 'path', 'group_id')
@args_validate(TopologyViewManager.cls)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.TopologyView,
app_cli.op.create_topology_view, app_cli.admin_name)
def post(self):
name = request.values.pop('name')
group_id = request.values.pop('group_id', None)
option = request.values.pop('option', None)
order = request.values.pop('order', None)
topo_view = TopologyViewManager.add(name, group_id, option, order, **request.values)
return self.jsonify(topo_view)
@args_validate(TopologyViewManager.cls)
@has_perm_from_args("_id", ResourceTypeEnum.TOPOLOGY_VIEW, PermEnum.UPDATE, TopologyViewManager.get_name_by_id)
def put(self, _id):
topo_view = TopologyViewManager.update(_id, **request.values)
return self.jsonify(topo_view)
@has_perm_from_args("_id", ResourceTypeEnum.TOPOLOGY_VIEW, PermEnum.DELETE, TopologyViewManager.get_name_by_id)
def delete(self, _id):
TopologyViewManager.delete(_id)
return self.jsonify(code=200)
class TopologyOrderView(APIView):
url_prefix = ('/topology_views/order',)
@args_required('view_ids')
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.TopologyView,
app_cli.op.create_topology_view, app_cli.admin_name)
def post(self):
view_ids = request.values.get('view_ids')
TopologyViewManager.group_inner_order(view_ids)
return self.jsonify(view_ids=view_ids)
def put(self):
return self.post()
class TopologyViewPreview(APIView):
url_prefix = ('/topology_views/preview', '/topology_views/<int:_id>/view')
def get(self, _id=None):
if _id is not None:
acl = ACLManager('cmdb')
resource_name = TopologyViewManager.get_name_by_id(_id)
if (not acl.has_permission(resource_name, ResourceTypeEnum.TOPOLOGY_VIEW, PermEnum.READ) and
not is_app_admin('cmdb')):
return abort(403, ErrFormat.no_permission.format(resource_name, PermEnum.READ))
return self.jsonify(TopologyViewManager().topology_view(view_id=_id))
else:
return self.jsonify(TopologyViewManager().topology_view(preview=request.values))
def post(self, _id=None):
return self.get(_id)
class TopologyViewGrantView(APIView):
url_prefix = "/topology_views/<int:view_id>/roles/<int:rid>/grant"
def post(self, view_id, rid):
perms = request.values.pop('perms', None)
view_name = TopologyViewManager.get_name_by_id(view_id) or abort(404, ErrFormat.not_found)
acl = ACLManager('cmdb')
if not acl.has_permission(view_name, ResourceTypeEnum.TOPOLOGY_VIEW,
PermEnum.GRANT) and not is_app_admin('cmdb'):
return abort(403, ErrFormat.no_permission.format(view_name, PermEnum.GRANT))
acl.grant_resource_to_role_by_rid(view_name, rid, ResourceTypeEnum.TOPOLOGY_VIEW, perms, rebuild=True)
return self.jsonify(code=200)
class TopologyViewRevokeView(APIView):
url_prefix = "/topology_views/<int:view_id>/roles/<int:rid>/revoke"
@args_required('perms')
def post(self, view_id, rid):
perms = request.values.pop('perms', None)
view_name = TopologyViewManager.get_name_by_id(view_id) or abort(404, ErrFormat.not_found)
acl = ACLManager('cmdb')
if not acl.has_permission(view_name, ResourceTypeEnum.TOPOLOGY_VIEW,
PermEnum.GRANT) and not is_app_admin('cmdb'):
return abort(403, ErrFormat.no_permission.format(view_name, PermEnum.GRANT))
acl.revoke_resource_from_role_by_rid(view_name, rid, ResourceTypeEnum.TOPOLOGY_VIEW, perms, rebuild=True)
return self.jsonify(code=200)

View File

@@ -1,7 +1,7 @@
-i https://mirrors.aliyun.com/pypi/simple
alembic==1.7.7
bs4==0.0.1
celery>=5.3.1
celery==5.3.1
celery-once==3.0.1
click==8.1.3
elasticsearch==7.17.9
@@ -53,4 +53,4 @@ shamir~=17.12.0
pycryptodomex>=3.19.0
colorama>=0.4.6
lz4>=4.3.2
python-magic==0.4.27
python-magic==0.4.27

View File

@@ -20,10 +20,16 @@ DEBUG_TB_INTERCEPT_REDIRECTS = False
ERROR_CODES = [400, 401, 403, 404, 405, 500, 502]
MYSQL_USER = env.str('MYSQL_USER', default='cmdb')
MYSQL_PASSWORD = env.str('MYSQL_PASSWORD', default='123456')
MYSQL_HOST = env.str('MYSQL_HOST', default='127.0.0.1')
MYSQL_PORT = env.int('MYSQL_PORT', default=3306)
MYSQL_DATABASE = env.str('MYSQL_DATABASE', default='cmdb')
# # database
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://{user}:{password}@127.0.0.1:3306/{db}?charset=utf8'
SQLALCHEMY_DATABASE_URI = f'mysql+pymysql://{MYSQL_USER}:{MYSQL_PASSWORD}@' \
f'{MYSQL_HOST}:{MYSQL_PORT}/{MYSQL_DATABASE}?charset=utf8'
SQLALCHEMY_BINDS = {
'user': 'mysql+pymysql://{user}:{password}@127.0.0.1:3306/{db}?charset=utf8'
'user': SQLALCHEMY_DATABASE_URI
}
SQLALCHEMY_ECHO = False
SQLALCHEMY_TRACK_MODIFICATIONS = False

View File

@@ -39,7 +39,7 @@
"md5": "^2.2.1",
"moment": "^2.24.0",
"nprogress": "^0.2.0",
"relation-graph": "^1.1.0",
"relation-graph": "^2.1.42",
"snabbdom": "^3.5.1",
"sortablejs": "1.9.0",
"style-resources-loader": "^1.5.0",

View File

@@ -54,6 +54,252 @@
<div class="content unicode" style="display: block;">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont">&#xe92b;</span>
<div class="name">ops-topology_view</div>
<div class="code-name">&amp;#xe92b;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe92a;</span>
<div class="name">monitor-host_analysis</div>
<div class="code-name">&amp;#xe92a;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe929;</span>
<div class="name">monitor-add2</div>
<div class="code-name">&amp;#xe929;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe928;</span>
<div class="name">monitor-native</div>
<div class="code-name">&amp;#xe928;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe927;</span>
<div class="name">veops-filter2</div>
<div class="code-name">&amp;#xe927;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe601;</span>
<div class="name">ops-cmdb-data_companies-selected</div>
<div class="code-name">&amp;#xe601;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe926;</span>
<div class="name">ops-cmdb-data_companies</div>
<div class="code-name">&amp;#xe926;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe921;</span>
<div class="name">monitor-threshold_value</div>
<div class="code-name">&amp;#xe921;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe922;</span>
<div class="name">monitor-disposition</div>
<div class="code-name">&amp;#xe922;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe923;</span>
<div class="name">monitor-automatic_discovery</div>
<div class="code-name">&amp;#xe923;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe924;</span>
<div class="name">monitor-grouping_list</div>
<div class="code-name">&amp;#xe924;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe925;</span>
<div class="name">monitor-node_list</div>
<div class="code-name">&amp;#xe925;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe920;</span>
<div class="name">monitor-general_view</div>
<div class="code-name">&amp;#xe920;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe91b;</span>
<div class="name">monitor-network_topology</div>
<div class="code-name">&amp;#xe91b;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe91c;</span>
<div class="name">monitor-node_management</div>
<div class="code-name">&amp;#xe91c;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe91d;</span>
<div class="name">monitor-alarm_policy</div>
<div class="code-name">&amp;#xe91d;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe91e;</span>
<div class="name">monitor-alarm</div>
<div class="code-name">&amp;#xe91e;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe91f;</span>
<div class="name">monitor-healing</div>
<div class="code-name">&amp;#xe91f;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe8d4;</span>
<div class="name">monitor-data_acquisition</div>
<div class="code-name">&amp;#xe8d4;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe91a;</span>
<div class="name">monitor-analysis</div>
<div class="code-name">&amp;#xe91a;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe89b;</span>
<div class="name">monitor-index</div>
<div class="code-name">&amp;#xe89b;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe867;</span>
<div class="name">monitor-user_defined</div>
<div class="code-name">&amp;#xe867;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe861;</span>
<div class="name">monitor-database</div>
<div class="code-name">&amp;#xe861;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe865;</span>
<div class="name">monitor-common</div>
<div class="code-name">&amp;#xe865;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe866;</span>
<div class="name">veops-edit</div>
<div class="code-name">&amp;#xe866;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe863;</span>
<div class="name">veops-empower</div>
<div class="code-name">&amp;#xe863;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe864;</span>
<div class="name">veops-share</div>
<div class="code-name">&amp;#xe864;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe862;</span>
<div class="name">veops-export</div>
<div class="code-name">&amp;#xe862;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe860;</span>
<div class="name">veops-import</div>
<div class="code-name">&amp;#xe860;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe807;</span>
<div class="name">monitor-ip (1)</div>
<div class="code-name">&amp;#xe807;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe803;</span>
<div class="name">monitor-director</div>
<div class="code-name">&amp;#xe803;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe804;</span>
<div class="name">monitor-host</div>
<div class="code-name">&amp;#xe804;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe802;</span>
<div class="name">cmdb-log</div>
<div class="code-name">&amp;#xe802;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe7ff;</span>
<div class="name">monitor-add</div>
<div class="code-name">&amp;#xe7ff;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe7fc;</span>
<div class="name">monitor-down</div>
<div class="code-name">&amp;#xe7fc;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe7fd;</span>
<div class="name">monitor-up</div>
<div class="code-name">&amp;#xe7fd;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe7f9;</span>
<div class="name">itsm-unfold</div>
<div class="code-name">&amp;#xe7f9;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe7f8;</span>
<div class="name">itsm-shrink</div>
<div class="code-name">&amp;#xe7f8;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe7a1;</span>
<div class="name">monitor-data_comaparison2</div>
<div class="code-name">&amp;#xe7a1;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe7f7;</span>
<div class="name">monitor-data_comaparison1</div>
<div class="code-name">&amp;#xe7f7;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe7a0;</span>
<div class="name">monitor-online</div>
<div class="code-name">&amp;#xe7a0;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe919;</span>
<div class="name">ops-setting-application-selected</div>
@@ -4620,9 +4866,9 @@
<pre><code class="language-css"
>@font-face {
font-family: 'iconfont';
src: url('iconfont.woff2?t=1713840593232') format('woff2'),
url('iconfont.woff?t=1713840593232') format('woff'),
url('iconfont.ttf?t=1713840593232') format('truetype');
src: url('iconfont.woff2?t=1716896994700') format('woff2'),
url('iconfont.woff?t=1716896994700') format('woff'),
url('iconfont.ttf?t=1716896994700') format('truetype');
}
</code></pre>
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@@ -4648,6 +4894,375 @@
<div class="content font-class">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont ops-topology_view"></span>
<div class="name">
ops-topology_view
</div>
<div class="code-name">.ops-topology_view
</div>
</li>
<li class="dib">
<span class="icon iconfont monitor-host_analysis"></span>
<div class="name">
monitor-host_analysis
</div>
<div class="code-name">.monitor-host_analysis
</div>
</li>
<li class="dib">
<span class="icon iconfont a-Group427319324"></span>
<div class="name">
monitor-add2
</div>
<div class="code-name">.a-Group427319324
</div>
</li>
<li class="dib">
<span class="icon iconfont monitor-native"></span>
<div class="name">
monitor-native
</div>
<div class="code-name">.monitor-native
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-filter2"></span>
<div class="name">
veops-filter2
</div>
<div class="code-name">.veops-filter2
</div>
</li>
<li class="dib">
<span class="icon iconfont ops-cmdb-data_companies-selected"></span>
<div class="name">
ops-cmdb-data_companies-selected
</div>
<div class="code-name">.ops-cmdb-data_companies-selected
</div>
</li>
<li class="dib">
<span class="icon iconfont ops-cmdb-data_companies"></span>
<div class="name">
ops-cmdb-data_companies
</div>
<div class="code-name">.ops-cmdb-data_companies
</div>
</li>
<li class="dib">
<span class="icon iconfont monitor-threshold_value"></span>
<div class="name">
monitor-threshold_value
</div>
<div class="code-name">.monitor-threshold_value
</div>
</li>
<li class="dib">
<span class="icon iconfont monitor-disposition"></span>
<div class="name">
monitor-disposition
</div>
<div class="code-name">.monitor-disposition
</div>
</li>
<li class="dib">
<span class="icon iconfont monitor-automatic_discovery"></span>
<div class="name">
monitor-automatic_discovery
</div>
<div class="code-name">.monitor-automatic_discovery
</div>
</li>
<li class="dib">
<span class="icon iconfont monitor-grouping_list"></span>
<div class="name">
monitor-grouping_list
</div>
<div class="code-name">.monitor-grouping_list
</div>
</li>
<li class="dib">
<span class="icon iconfont monitor-node_list"></span>
<div class="name">
monitor-node_list
</div>
<div class="code-name">.monitor-node_list
</div>
</li>
<li class="dib">
<span class="icon iconfont monitor-general_view"></span>
<div class="name">
monitor-general_view
</div>
<div class="code-name">.monitor-general_view
</div>
</li>
<li class="dib">
<span class="icon iconfont monitor-network_topology"></span>
<div class="name">
monitor-network_topology
</div>
<div class="code-name">.monitor-network_topology
</div>
</li>
<li class="dib">
<span class="icon iconfont monitor-node_management"></span>
<div class="name">
monitor-node_management
</div>
<div class="code-name">.monitor-node_management
</div>
</li>
<li class="dib">
<span class="icon iconfont monitor-alarm_policy"></span>
<div class="name">
monitor-alarm_policy
</div>
<div class="code-name">.monitor-alarm_policy
</div>
</li>
<li class="dib">
<span class="icon iconfont monitor-alarm"></span>
<div class="name">
monitor-alarm
</div>
<div class="code-name">.monitor-alarm
</div>
</li>
<li class="dib">
<span class="icon iconfont monitor-healing"></span>
<div class="name">
monitor-healing
</div>
<div class="code-name">.monitor-healing
</div>
</li>
<li class="dib">
<span class="icon iconfont monitor-data_acquisition"></span>
<div class="name">
monitor-data_acquisition
</div>
<div class="code-name">.monitor-data_acquisition
</div>
</li>
<li class="dib">
<span class="icon iconfont monitor-analysis"></span>
<div class="name">
monitor-analysis
</div>
<div class="code-name">.monitor-analysis
</div>
</li>
<li class="dib">
<span class="icon iconfont monitor-index"></span>
<div class="name">
monitor-index
</div>
<div class="code-name">.monitor-index
</div>
</li>
<li class="dib">
<span class="icon iconfont monitor-user_defined"></span>
<div class="name">
monitor-user_defined
</div>
<div class="code-name">.monitor-user_defined
</div>
</li>
<li class="dib">
<span class="icon iconfont monitor-database"></span>
<div class="name">
monitor-database
</div>
<div class="code-name">.monitor-database
</div>
</li>
<li class="dib">
<span class="icon iconfont monitor-common"></span>
<div class="name">
monitor-common
</div>
<div class="code-name">.monitor-common
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-edit"></span>
<div class="name">
veops-edit
</div>
<div class="code-name">.veops-edit
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-empower"></span>
<div class="name">
veops-empower
</div>
<div class="code-name">.veops-empower
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-share"></span>
<div class="name">
veops-share
</div>
<div class="code-name">.veops-share
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-export"></span>
<div class="name">
veops-export
</div>
<div class="code-name">.veops-export
</div>
</li>
<li class="dib">
<span class="icon iconfont a-veops-import1"></span>
<div class="name">
veops-import
</div>
<div class="code-name">.a-veops-import1
</div>
</li>
<li class="dib">
<span class="icon iconfont monitor-ip"></span>
<div class="name">
monitor-ip (1)
</div>
<div class="code-name">.monitor-ip
</div>
</li>
<li class="dib">
<span class="icon iconfont monitor-director"></span>
<div class="name">
monitor-director
</div>
<div class="code-name">.monitor-director
</div>
</li>
<li class="dib">
<span class="icon iconfont monitor-host"></span>
<div class="name">
monitor-host
</div>
<div class="code-name">.monitor-host
</div>
</li>
<li class="dib">
<span class="icon iconfont a-cmdb-log1"></span>
<div class="name">
cmdb-log
</div>
<div class="code-name">.a-cmdb-log1
</div>
</li>
<li class="dib">
<span class="icon iconfont monitor-add"></span>
<div class="name">
monitor-add
</div>
<div class="code-name">.monitor-add
</div>
</li>
<li class="dib">
<span class="icon iconfont monitor-down"></span>
<div class="name">
monitor-down
</div>
<div class="code-name">.monitor-down
</div>
</li>
<li class="dib">
<span class="icon iconfont monitor-up"></span>
<div class="name">
monitor-up
</div>
<div class="code-name">.monitor-up
</div>
</li>
<li class="dib">
<span class="icon iconfont itsm-unfold"></span>
<div class="name">
itsm-unfold
</div>
<div class="code-name">.itsm-unfold
</div>
</li>
<li class="dib">
<span class="icon iconfont itsm-stretch"></span>
<div class="name">
itsm-shrink
</div>
<div class="code-name">.itsm-stretch
</div>
</li>
<li class="dib">
<span class="icon iconfont monitor-data_comaparison2"></span>
<div class="name">
monitor-data_comaparison2
</div>
<div class="code-name">.monitor-data_comaparison2
</div>
</li>
<li class="dib">
<span class="icon iconfont monitor-data_comaparison1"></span>
<div class="name">
monitor-data_comaparison1
</div>
<div class="code-name">.monitor-data_comaparison1
</div>
</li>
<li class="dib">
<span class="icon iconfont a-monitor-online1"></span>
<div class="name">
monitor-online
</div>
<div class="code-name">.a-monitor-online1
</div>
</li>
<li class="dib">
<span class="icon iconfont ops-setting-application-selected"></span>
<div class="name">
@@ -11497,6 +12112,334 @@
<div class="content symbol">
<ul class="icon_lists dib-box">
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ops-topology_view"></use>
</svg>
<div class="name">ops-topology_view</div>
<div class="code-name">#ops-topology_view</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#monitor-host_analysis"></use>
</svg>
<div class="name">monitor-host_analysis</div>
<div class="code-name">#monitor-host_analysis</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#a-Group427319324"></use>
</svg>
<div class="name">monitor-add2</div>
<div class="code-name">#a-Group427319324</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#monitor-native"></use>
</svg>
<div class="name">monitor-native</div>
<div class="code-name">#monitor-native</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-filter2"></use>
</svg>
<div class="name">veops-filter2</div>
<div class="code-name">#veops-filter2</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ops-cmdb-data_companies-selected"></use>
</svg>
<div class="name">ops-cmdb-data_companies-selected</div>
<div class="code-name">#ops-cmdb-data_companies-selected</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ops-cmdb-data_companies"></use>
</svg>
<div class="name">ops-cmdb-data_companies</div>
<div class="code-name">#ops-cmdb-data_companies</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#monitor-threshold_value"></use>
</svg>
<div class="name">monitor-threshold_value</div>
<div class="code-name">#monitor-threshold_value</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#monitor-disposition"></use>
</svg>
<div class="name">monitor-disposition</div>
<div class="code-name">#monitor-disposition</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#monitor-automatic_discovery"></use>
</svg>
<div class="name">monitor-automatic_discovery</div>
<div class="code-name">#monitor-automatic_discovery</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#monitor-grouping_list"></use>
</svg>
<div class="name">monitor-grouping_list</div>
<div class="code-name">#monitor-grouping_list</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#monitor-node_list"></use>
</svg>
<div class="name">monitor-node_list</div>
<div class="code-name">#monitor-node_list</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#monitor-general_view"></use>
</svg>
<div class="name">monitor-general_view</div>
<div class="code-name">#monitor-general_view</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#monitor-network_topology"></use>
</svg>
<div class="name">monitor-network_topology</div>
<div class="code-name">#monitor-network_topology</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#monitor-node_management"></use>
</svg>
<div class="name">monitor-node_management</div>
<div class="code-name">#monitor-node_management</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#monitor-alarm_policy"></use>
</svg>
<div class="name">monitor-alarm_policy</div>
<div class="code-name">#monitor-alarm_policy</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#monitor-alarm"></use>
</svg>
<div class="name">monitor-alarm</div>
<div class="code-name">#monitor-alarm</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#monitor-healing"></use>
</svg>
<div class="name">monitor-healing</div>
<div class="code-name">#monitor-healing</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#monitor-data_acquisition"></use>
</svg>
<div class="name">monitor-data_acquisition</div>
<div class="code-name">#monitor-data_acquisition</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#monitor-analysis"></use>
</svg>
<div class="name">monitor-analysis</div>
<div class="code-name">#monitor-analysis</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#monitor-index"></use>
</svg>
<div class="name">monitor-index</div>
<div class="code-name">#monitor-index</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#monitor-user_defined"></use>
</svg>
<div class="name">monitor-user_defined</div>
<div class="code-name">#monitor-user_defined</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#monitor-database"></use>
</svg>
<div class="name">monitor-database</div>
<div class="code-name">#monitor-database</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#monitor-common"></use>
</svg>
<div class="name">monitor-common</div>
<div class="code-name">#monitor-common</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-edit"></use>
</svg>
<div class="name">veops-edit</div>
<div class="code-name">#veops-edit</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-empower"></use>
</svg>
<div class="name">veops-empower</div>
<div class="code-name">#veops-empower</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-share"></use>
</svg>
<div class="name">veops-share</div>
<div class="code-name">#veops-share</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-export"></use>
</svg>
<div class="name">veops-export</div>
<div class="code-name">#veops-export</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#a-veops-import1"></use>
</svg>
<div class="name">veops-import</div>
<div class="code-name">#a-veops-import1</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#monitor-ip"></use>
</svg>
<div class="name">monitor-ip (1)</div>
<div class="code-name">#monitor-ip</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#monitor-director"></use>
</svg>
<div class="name">monitor-director</div>
<div class="code-name">#monitor-director</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#monitor-host"></use>
</svg>
<div class="name">monitor-host</div>
<div class="code-name">#monitor-host</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#a-cmdb-log1"></use>
</svg>
<div class="name">cmdb-log</div>
<div class="code-name">#a-cmdb-log1</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#monitor-add"></use>
</svg>
<div class="name">monitor-add</div>
<div class="code-name">#monitor-add</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#monitor-down"></use>
</svg>
<div class="name">monitor-down</div>
<div class="code-name">#monitor-down</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#monitor-up"></use>
</svg>
<div class="name">monitor-up</div>
<div class="code-name">#monitor-up</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-unfold"></use>
</svg>
<div class="name">itsm-unfold</div>
<div class="code-name">#itsm-unfold</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-stretch"></use>
</svg>
<div class="name">itsm-shrink</div>
<div class="code-name">#itsm-stretch</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#monitor-data_comaparison2"></use>
</svg>
<div class="name">monitor-data_comaparison2</div>
<div class="code-name">#monitor-data_comaparison2</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#monitor-data_comaparison1"></use>
</svg>
<div class="name">monitor-data_comaparison1</div>
<div class="code-name">#monitor-data_comaparison1</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#a-monitor-online1"></use>
</svg>
<div class="name">monitor-online</div>
<div class="code-name">#a-monitor-online1</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ops-setting-application-selected"></use>

View File

@@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 3857903 */
src: url('iconfont.woff2?t=1713840593232') format('woff2'),
url('iconfont.woff?t=1713840593232') format('woff'),
url('iconfont.ttf?t=1713840593232') format('truetype');
src: url('iconfont.woff2?t=1716896994700') format('woff2'),
url('iconfont.woff?t=1716896994700') format('woff'),
url('iconfont.ttf?t=1716896994700') format('truetype');
}
.iconfont {
@@ -13,6 +13,170 @@
-moz-osx-font-smoothing: grayscale;
}
.ops-topology_view:before {
content: "\e92b";
}
.monitor-host_analysis:before {
content: "\e92a";
}
.a-Group427319324:before {
content: "\e929";
}
.monitor-native:before {
content: "\e928";
}
.veops-filter2:before {
content: "\e927";
}
.ops-cmdb-data_companies-selected:before {
content: "\e601";
}
.ops-cmdb-data_companies:before {
content: "\e926";
}
.monitor-threshold_value:before {
content: "\e921";
}
.monitor-disposition:before {
content: "\e922";
}
.monitor-automatic_discovery:before {
content: "\e923";
}
.monitor-grouping_list:before {
content: "\e924";
}
.monitor-node_list:before {
content: "\e925";
}
.monitor-general_view:before {
content: "\e920";
}
.monitor-network_topology:before {
content: "\e91b";
}
.monitor-node_management:before {
content: "\e91c";
}
.monitor-alarm_policy:before {
content: "\e91d";
}
.monitor-alarm:before {
content: "\e91e";
}
.monitor-healing:before {
content: "\e91f";
}
.monitor-data_acquisition:before {
content: "\e8d4";
}
.monitor-analysis:before {
content: "\e91a";
}
.monitor-index:before {
content: "\e89b";
}
.monitor-user_defined:before {
content: "\e867";
}
.monitor-database:before {
content: "\e861";
}
.monitor-common:before {
content: "\e865";
}
.veops-edit:before {
content: "\e866";
}
.veops-empower:before {
content: "\e863";
}
.veops-share:before {
content: "\e864";
}
.veops-export:before {
content: "\e862";
}
.a-veops-import1:before {
content: "\e860";
}
.monitor-ip:before {
content: "\e807";
}
.monitor-director:before {
content: "\e803";
}
.monitor-host:before {
content: "\e804";
}
.a-cmdb-log1:before {
content: "\e802";
}
.monitor-add:before {
content: "\e7ff";
}
.monitor-down:before {
content: "\e7fc";
}
.monitor-up:before {
content: "\e7fd";
}
.itsm-unfold:before {
content: "\e7f9";
}
.itsm-stretch:before {
content: "\e7f8";
}
.monitor-data_comaparison2:before {
content: "\e7a1";
}
.monitor-data_comaparison1:before {
content: "\e7f7";
}
.a-monitor-online1:before {
content: "\e7a0";
}
.ops-setting-application-selected:before {
content: "\e919";
}

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,293 @@
"css_prefix_text": "",
"description": "",
"glyphs": [
{
"icon_id": "40499246",
"name": "ops-topology_view",
"font_class": "ops-topology_view",
"unicode": "e92b",
"unicode_decimal": 59691
},
{
"icon_id": "40411336",
"name": "monitor-host_analysis",
"font_class": "monitor-host_analysis",
"unicode": "e92a",
"unicode_decimal": 59690
},
{
"icon_id": "40372105",
"name": "monitor-add2",
"font_class": "a-Group427319324",
"unicode": "e929",
"unicode_decimal": 59689
},
{
"icon_id": "40368097",
"name": "monitor-native",
"font_class": "monitor-native",
"unicode": "e928",
"unicode_decimal": 59688
},
{
"icon_id": "40357355",
"name": "veops-filter2",
"font_class": "veops-filter2",
"unicode": "e927",
"unicode_decimal": 59687
},
{
"icon_id": "40356229",
"name": "ops-cmdb-data_companies-selected",
"font_class": "ops-cmdb-data_companies-selected",
"unicode": "e601",
"unicode_decimal": 58881
},
{
"icon_id": "40343814",
"name": "ops-cmdb-data_companies",
"font_class": "ops-cmdb-data_companies",
"unicode": "e926",
"unicode_decimal": 59686
},
{
"icon_id": "40326916",
"name": "monitor-threshold_value",
"font_class": "monitor-threshold_value",
"unicode": "e921",
"unicode_decimal": 59681
},
{
"icon_id": "40326913",
"name": "monitor-disposition",
"font_class": "monitor-disposition",
"unicode": "e922",
"unicode_decimal": 59682
},
{
"icon_id": "40326911",
"name": "monitor-automatic_discovery",
"font_class": "monitor-automatic_discovery",
"unicode": "e923",
"unicode_decimal": 59683
},
{
"icon_id": "40326907",
"name": "monitor-grouping_list",
"font_class": "monitor-grouping_list",
"unicode": "e924",
"unicode_decimal": 59684
},
{
"icon_id": "40326906",
"name": "monitor-node_list",
"font_class": "monitor-node_list",
"unicode": "e925",
"unicode_decimal": 59685
},
{
"icon_id": "40326667",
"name": "monitor-general_view",
"font_class": "monitor-general_view",
"unicode": "e920",
"unicode_decimal": 59680
},
{
"icon_id": "40326683",
"name": "monitor-network_topology",
"font_class": "monitor-network_topology",
"unicode": "e91b",
"unicode_decimal": 59675
},
{
"icon_id": "40326682",
"name": "monitor-node_management",
"font_class": "monitor-node_management",
"unicode": "e91c",
"unicode_decimal": 59676
},
{
"icon_id": "40326681",
"name": "monitor-alarm_policy",
"font_class": "monitor-alarm_policy",
"unicode": "e91d",
"unicode_decimal": 59677
},
{
"icon_id": "40326680",
"name": "monitor-alarm",
"font_class": "monitor-alarm",
"unicode": "e91e",
"unicode_decimal": 59678
},
{
"icon_id": "40326679",
"name": "monitor-healing",
"font_class": "monitor-healing",
"unicode": "e91f",
"unicode_decimal": 59679
},
{
"icon_id": "40326685",
"name": "monitor-data_acquisition",
"font_class": "monitor-data_acquisition",
"unicode": "e8d4",
"unicode_decimal": 59604
},
{
"icon_id": "40326684",
"name": "monitor-analysis",
"font_class": "monitor-analysis",
"unicode": "e91a",
"unicode_decimal": 59674
},
{
"icon_id": "40308359",
"name": "monitor-index",
"font_class": "monitor-index",
"unicode": "e89b",
"unicode_decimal": 59547
},
{
"icon_id": "40307829",
"name": "monitor-user_defined",
"font_class": "monitor-user_defined",
"unicode": "e867",
"unicode_decimal": 59495
},
{
"icon_id": "40307835",
"name": "monitor-database",
"font_class": "monitor-database",
"unicode": "e861",
"unicode_decimal": 59489
},
{
"icon_id": "40307833",
"name": "monitor-common",
"font_class": "monitor-common",
"unicode": "e865",
"unicode_decimal": 59493
},
{
"icon_id": "40307035",
"name": "veops-edit",
"font_class": "veops-edit",
"unicode": "e866",
"unicode_decimal": 59494
},
{
"icon_id": "40307027",
"name": "veops-empower",
"font_class": "veops-empower",
"unicode": "e863",
"unicode_decimal": 59491
},
{
"icon_id": "40307026",
"name": "veops-share",
"font_class": "veops-share",
"unicode": "e864",
"unicode_decimal": 59492
},
{
"icon_id": "40306997",
"name": "veops-export",
"font_class": "veops-export",
"unicode": "e862",
"unicode_decimal": 59490
},
{
"icon_id": "40306881",
"name": "veops-import",
"font_class": "a-veops-import1",
"unicode": "e860",
"unicode_decimal": 59488
},
{
"icon_id": "40262335",
"name": "monitor-ip (1)",
"font_class": "monitor-ip",
"unicode": "e807",
"unicode_decimal": 59399
},
{
"icon_id": "40262337",
"name": "monitor-director",
"font_class": "monitor-director",
"unicode": "e803",
"unicode_decimal": 59395
},
{
"icon_id": "40262336",
"name": "monitor-host",
"font_class": "monitor-host",
"unicode": "e804",
"unicode_decimal": 59396
},
{
"icon_id": "40262216",
"name": "cmdb-log",
"font_class": "a-cmdb-log1",
"unicode": "e802",
"unicode_decimal": 59394
},
{
"icon_id": "40261712",
"name": "monitor-add",
"font_class": "monitor-add",
"unicode": "e7ff",
"unicode_decimal": 59391
},
{
"icon_id": "40248186",
"name": "monitor-down",
"font_class": "monitor-down",
"unicode": "e7fc",
"unicode_decimal": 59388
},
{
"icon_id": "40248184",
"name": "monitor-up",
"font_class": "monitor-up",
"unicode": "e7fd",
"unicode_decimal": 59389
},
{
"icon_id": "40235502",
"name": "itsm-unfold",
"font_class": "itsm-unfold",
"unicode": "e7f9",
"unicode_decimal": 59385
},
{
"icon_id": "40235453",
"name": "itsm-shrink",
"font_class": "itsm-stretch",
"unicode": "e7f8",
"unicode_decimal": 59384
},
{
"icon_id": "40217123",
"name": "monitor-data_comaparison2",
"font_class": "monitor-data_comaparison2",
"unicode": "e7a1",
"unicode_decimal": 59297
},
{
"icon_id": "40217122",
"name": "monitor-data_comaparison1",
"font_class": "monitor-data_comaparison1",
"unicode": "e7f7",
"unicode_decimal": 59383
},
{
"icon_id": "40176936",
"name": "monitor-online",
"font_class": "a-monitor-online1",
"unicode": "e7a0",
"unicode_decimal": 59296
},
{
"icon_id": "40043662",
"name": "ops-setting-application-selected",

Binary file not shown.

View File

@@ -9,7 +9,7 @@
:bodyStyle="{ padding: '24px 12px' }"
:placement="placement"
>
<ResourceSearch :fromCronJob="true" @copySuccess="copySuccess" />
<ResourceSearch ref="resourceSearch" :fromCronJob="true" :type="type" :typeId="typeId" @copySuccess="copySuccess" />
</CustomDrawer>
</template>
@@ -23,6 +23,14 @@ export default {
type: String,
default: 'right',
},
type: {
type: String,
default: 'resourceSearch'
},
typeId: {
type: Number,
default: null
}
},
data() {
return {

View File

@@ -182,8 +182,8 @@ export default {
<tag {...{ props, attrs }}>
{this.renderIcon({ icon: menu.meta.icon, customIcon: menu.meta.customIcon, name: menu.meta.name, typeId: menu.meta.typeId, routeName: menu.name, selectedIcon: menu.meta.selectedIcon, })}
<span>
<span class={this.renderI18n(menu.meta.title).length > 10 ? 'scroll' : ''}>{this.renderI18n(menu.meta.title)}</span>
{isShowDot &&
<span style={menu.meta.style} class={this.renderI18n(menu.meta.title).length > 10 ? 'scroll' : ''}>{this.renderI18n(menu.meta.title)}</span>
{isShowDot && !menu.meta.disabled &&
<a-popover
overlayClassName="custom-menu-extra-submenu"
placement="rightTop"

View File

@@ -81,7 +81,7 @@ export default {
if (route.name === 'cmdb') {
const preference = await getPreference()
const lastTypeId = window.localStorage.getItem('ops_ci_typeid') || undefined
if (lastTypeId && preference.some((item) => item.id === Number(lastTypeId))) {
if (lastTypeId && preference.type_ids.some((item) => item === Number(lastTypeId))) {
this.$router.push(`/cmdb/instances/types/${lastTypeId}`)
} else {
this.$router.push('/cmdb/dashboard')

View File

@@ -2,11 +2,12 @@ import { axios } from '@/utils/request'
const urlPrefix = '/v0.1'
export function searchCI(params) {
export function searchCI(params, isShowMessage = true) {
return axios({
url: urlPrefix + `/ci/s`,
method: 'GET',
params: params
params: params,
isShowMessage
})
}

View File

@@ -0,0 +1,110 @@
import { axios } from '@/utils/request'
const urlPrefix = '/v0.1'
export function getTopoGroups() {
return axios({
url: `${urlPrefix}/topology_views`,
method: 'GET',
})
}
export function getTopoView(_id) {
return axios({
url: `${urlPrefix}/topology_views/${_id}`,
method: 'GET',
})
}
export function postTopoGroup(data) {
return axios({
url: `${urlPrefix}/topology_views/groups`,
method: 'POST',
data: data
})
}
export function putTopoGroupByGId(gid, data) {
return axios({
url: `${urlPrefix}/topology_views/groups/${gid}`,
method: 'PUT',
data: data
})
}
export function putTopoGroupsOrder(data) {
return axios({
url: `${urlPrefix}/topology_views/groups/order`,
method: 'PUT',
data: data
})
}
export function deleteTopoGroup(gid, data) {
return axios({
url: `${urlPrefix}/topology_views/groups/${gid}`,
method: 'DELETE',
data: data
})
}
export function addTopoView(data) {
return axios({
url: `${urlPrefix}/topology_views`,
method: 'POST',
data: data
})
}
export function updateTopoView(_id, data) {
return axios({
url: `${urlPrefix}/topology_views/${_id}`,
method: 'PUT',
data: data
})
}
export function deleteTopoView(_id) {
return axios({
url: `${urlPrefix}/topology_views/${_id}`,
method: 'DELETE',
})
}
export function getRelationsByTypeId(_id) {
return axios({
url: `${urlPrefix}/topology_views/relations/ci_types/${_id}`,
method: 'GET',
})
}
export function previewTopoView(params) {
return axios({
url: `${urlPrefix}/topology_views/preview`,
method: 'POST',
data: params,
})
}
export function showTopoView(_id) {
return axios({
url: `${urlPrefix}/topology_views/${_id}/view`,
method: 'GET',
})
}
export function grantTopologyView(viewId, rid, data) {
return axios({
url: `/v0.1/topology_views/${viewId}/roles/${rid}/grant`,
method: 'POST',
data: data
})
}
export function revokeTopologyView(viewId, rid, data) {
return axios({
url: `/v0.1/topology_views/${viewId}/roles/${rid}/revoke`,
method: 'POST',
data: data
})
}

View File

@@ -57,6 +57,20 @@
:addedRids="addedRids"
/>
</template>
<template v-if="cmdbGrantType.includes('TopologyView')">
<div class="cmdb-grant-title">{{ resourceTypeName }}{{ $t('cmdb.components.perm') }}</div>
<TopologyViewGrant
:resourceTypeName="resourceTypeName"
:tableData="tableData"
:viewId="CITypeId"
grantType="TopologyView"
@grantDepart="grantDepart"
@grantRole="grantRole"
@getTableData="getTableData"
ref="grantTopologyView"
:addedRids="addedRids"
/>
</template>
<GrantModal ref="grantModal" @handleOk="handleOk" />
<ReadGrantModal ref="readGrantModal" :CITypeId="CITypeId" @updateTableDataRead="updateTableDataRead" />
@@ -72,11 +86,12 @@ import { getResourcePerms } from '@/modules/acl/api/permission'
import GrantModal from './grantModal.vue'
import ReadGrantModal from './readGrantModal'
import RelationViewGrant from './relationViewGrant.vue'
import TopologyViewGrant from './topologyViewGrant.vue'
import { getCITypeGroupById, ciTypeFilterPermissions } from '../../api/CIType'
export default {
name: 'GrantComp',
components: { CiTypeGrant, TypeRelationGrant, RelationViewGrant, GrantModal, ReadGrantModal },
components: { CiTypeGrant, TypeRelationGrant, RelationViewGrant, TopologyViewGrant, GrantModal, ReadGrantModal },
props: {
CITypeId: {
type: Number,
@@ -291,6 +306,20 @@ export default {
})
)
}
if (grantType === 'TopologyView') {
this.tableData.unshift(
...rids.map(({ rid, name }) => {
return {
rid,
name,
read: false,
update: false,
delete: false,
grant: false,
}
})
)
}
this.addedRids = rids
this.$nextTick(() => {
setTimeout(() => {

View File

@@ -0,0 +1,102 @@
<template>
<div class="topology-view-grant">
<vxe-table
ref="xTable"
size="mini"
stripe
class="ops-stripe-table"
:data="tableData"
:max-height="`${tableHeight}px`"
:scroll-y="{enabled: true}"
:row-style="(params) => getCurrentRowStyle(params, addedRids)"
>
<vxe-column field="name"></vxe-column>
<vxe-column v-for="col in columns" :key="col" :field="col" :title="permMap[col]">
<template #default="{row}">
<a-checkbox @change="(e) => handleChange(e, col, row)" v-model="row[col]"></a-checkbox>
</template>
</vxe-column>
</vxe-table>
<a-space>
<span class="grant-button" @click="grantDepart">{{ $t('cmdb.components.grantUser') }}</span>
<span class="grant-button" @click="grantRole">{{ $t('cmdb.components.grantRole') }}</span>
</a-space>
</div>
</template>
<script>
import { permMap } from './constants.js'
import { grantTopologyView, revokeTopologyView } from '@/modules/cmdb/api/topology.js'
import { getCurrentRowStyle } from './utils'
export default {
name: 'TopologyViewGrant',
inject: ['loading', 'isModal'],
props: {
viewId: {
type: Number,
default: null,
},
resourceTypeName: {
type: String,
default: '',
},
tableData: {
type: Array,
default: () => [],
},
grantType: {
type: String,
default: 'relation_view',
},
addedRids: {
type: Array,
default: () => [],
},
},
data() {
return {
columns: ['read', 'update', 'delete', 'grant'],
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
tableHeight() {
if (this.isModal) {
return (this.windowHeight - 104) / 2
}
return (this.windowHeight - 104) / 2 - 116
},
permMap() {
return permMap()
}
},
methods: {
getCurrentRowStyle,
grantDepart() {
this.$emit('grantDepart', this.grantType)
},
grantRole() {
this.$emit('grantRole', this.grantType)
},
handleChange(e, col, row) {
if (e.target.checked) {
grantTopologyView(this.viewId, row.rid, { perms: [col], name: this.resourceTypeName }).catch(() => {
this.$emit('getTableData')
})
} else {
revokeTopologyView(this.viewId, row.rid, { perms: [col], name: this.resourceTypeName }).catch(() => {
this.$emit('getTableData')
})
}
},
},
}
</script>
<style lang="less" scoped>
.topology-view-grant {
padding: 10px 0;
}
</style>

View File

@@ -49,6 +49,8 @@
import _ from 'lodash'
import '@wangeditor/editor/dist/css/style.css'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { i18nChangeLanguage } from '@wangeditor/editor'
export default {
name: 'NoticeContent',
components: { Editor, Toolbar },
@@ -76,6 +78,10 @@ export default {
if (editor == null) return
editor.destroy() // When the component is destroyed, destroy the editor in time
},
beforeCreate() {
const locale = this.$i18n.locale === 'zh' ? 'zh-CN' : 'en'
i18nChangeLanguage(locale)
},
methods: {
onCreated(editor) {
this.editor = Object.seal(editor) // Be sure to use Object.seal(), otherwise an error will be reported

View File

@@ -198,6 +198,9 @@ export default {
if (this.type === 'resourceSearch') {
this.getCITypeGroups()
}
if (this.typeId) {
this.currenCiType = [this.typeId]
}
},
methods: {
// toggleAdvanced() {

View File

@@ -4,6 +4,8 @@ const cmdb_en = {
configTable: 'Config Table',
menu: {
views: 'Views',
topologyView: 'Topology Views',
resources: 'Resources',
config: 'Configuration',
backend: 'Management',
ciTable: 'Resource Views',
@@ -69,9 +71,9 @@ const cmdb_en = {
adAutoInLib: 'Save as CI auto',
adInterval: 'Collection frequency',
byInterval: 'by interval',
allNodes: 'All nodes',
specifyNodes: 'Specify Node',
specifyNodesTips: 'Please fill in the specify node!',
allNodes: 'All Instances',
specifyNodes: 'Specify Instance',
specifyNodesTips: 'Please fill in the specify instance!',
username: 'Username',
password: 'Password',
link: 'Link',
@@ -144,7 +146,7 @@ const cmdb_en = {
triggerDataChange: 'Data changes',
triggerDate: 'Date attribute',
triggerEnable: 'Turn on',
descInput: 'Please enter remarks',
descInput: 'Please enter descriptions',
triggerCondition: 'Triggering conditions',
addInstance: 'Add new instance',
deleteInstance: 'Delete instance',
@@ -562,6 +564,30 @@ if __name__ == "__main__":
tree: {
tips1: 'Please go to Preference page first to complete your subscription!',
subSettings: 'Settings',
}
},
topo: {
addTopoView: 'Add Topology View',
editTopoView: 'Edit Topology View',
addTopoViewInGroup: 'Define topology view under grouping',
groupRequired: 'Please select a group first or create a group',
viewName: 'Title',
viewNamePlaceholder: 'Please enter a title for the topology view',
inputNameTips: 'Title is required',
centralNodeType: 'Central Node Model',
filterInstances: 'Central Node Instances',
typeRequired: 'Central Node Model is required',
instancesRequired: 'instances are required',
path: 'Path',
aggregationCount: 'Aggregation Count',
aggreationCountTip: 'When the number of child nodes > the number of aggregations, paging display',
preview: 'Preivew',
noData: 'No data',
edit: 'Edit',
delete: 'Delete',
searchPlaceholder: 'Search topology view',
confirmDeleteView: 'Are you sure you want to delete this view ?',
noInstancePerm: 'You do not have read permissions for this instance',
noPreferenceAttributes: 'This instance has no subscription attributes or no default displayed attributes',
},
}
export default cmdb_en

View File

@@ -4,8 +4,10 @@ const cmdb_zh = {
configTable: '配置表格',
menu: {
views: '视图',
topologyView: '拓扑视图',
resources: '资源',
config: '配置',
backend: '管理',
backend: '管理',
ciTable: '资源数据',
ciTree: '资源层级',
ciSearch: '资源搜索',
@@ -69,9 +71,9 @@ const cmdb_zh = {
adAutoInLib: '自动入库',
adInterval: '采集频率',
byInterval: '按间隔',
allNodes: '所有节点',
specifyNodes: '指定节点',
specifyNodesTips: '请填写指定节点',
allNodes: '所有实例',
specifyNodes: '指定实例',
specifyNodesTips: '请填写指定实例',
username: '用户名',
password: '密码',
link: '链接',
@@ -144,7 +146,7 @@ const cmdb_zh = {
triggerDataChange: '数据变更',
triggerDate: '日期属性',
triggerEnable: '开启',
descInput: '请输入备注',
descInput: '请输入描述',
triggerCondition: '触发条件',
addInstance: '新增实例',
deleteInstance: '删除实例',
@@ -562,6 +564,30 @@ if __name__ == "__main__":
tree: {
tips1: '请先到 我的订阅 页面完成订阅!',
subSettings: '订阅设置',
}
},
topo: {
addTopoView: '新增拓扑视图',
editTopoView: '编辑拓扑视图',
addTopoViewInGroup: '在分组下定义拓扑视图',
groupRequired: '请先选择分组或者创建分组',
viewName: '标题',
viewNamePlaceholder: '请输入拓扑视图的标题',
inputNameTips: '必须输入标题',
centralNodeType: '中心节点模型',
filterInstances: '中心节点实例',
typeRequired: '必须要选择中心节点模型',
instancesRequired: '实例必须要选择',
path: '路径选择',
aggregationCount: '聚合数',
aggreationCountTip: '当子节点数 > 聚合数 则进行分页展示',
preview: '预览',
noData: '没有数据',
edit: '编辑',
delete: '删除',
searchPlaceholder: '搜索拓扑视图',
confirmDeleteView: '您确定要删除该视图吗?',
noInstancePerm: '您没有该实例的查看权限',
noPreferenceAttributes: '该实例没有订阅属性或者没有默认展示的属性',
},
}
export default cmdb_zh

View File

@@ -16,10 +16,16 @@ const genCmdbRoutes = async () => {
meta: { title: 'dashboard', icon: 'ops-cmdb-dashboard', selectedIcon: 'ops-cmdb-dashboard', keepAlive: false },
component: () => import('../views/dashboard/index_v2.vue')
},
{
path: '/cmdb/topoviews',
name: 'cmdb_topology_views',
meta: { title: 'cmdb.menu.topologyView', appName: 'cmdb', icon: 'ops-topology_view', selectedIcon: 'ops-topology_view', keepAlive: false },
component: () => import('../views/topology_view/index.vue')
},
{
path: '/cmdb/disabled1',
name: 'cmdb_disabled1',
meta: { title: 'cmdb.menu.views', disabled: true },
meta: { title: 'cmdb.menu.resources', disabled: true },
},
{
path: '/cmdb/resourceviews',
@@ -143,21 +149,30 @@ const genCmdbRoutes = async () => {
}
// Dynamically add subscription items and business relationships
const [preference, relation] = await Promise.all([getPreference(), getRelationView()])
preference.forEach(item => {
routes.children[2].children.push({
path: `/cmdb/instances/types/${item.id}`,
component: () => import(`../views/ci/index`),
name: `cmdb_${item.id}`,
meta: { title: item.alias, keepAlive: false, typeId: item.id, name: item.name, customIcon: item.icon },
// hideChildrenInMenu: true // Force display of MenuItem instead of SubMenu
const resourceViewsIndex = routes.children.findIndex(item => item.name === 'cmdb_resource_views')
preference.group_types.forEach(group => {
if (preference.group_types.length > 1) {
routes.children[resourceViewsIndex].children.push({
path: `/cmdb/instances/types/group${group.id}`,
name: `cmdb_instances_group_${group.id}`,
meta: { title: group.name || 'other', disabled: true, style: 'margin-left: 12px' },
})
}
group.ci_types.forEach(item => {
routes.children[resourceViewsIndex].children.push({
path: `/cmdb/instances/types/${item.id}`,
component: () => import(`../views/ci/index`),
name: `cmdb_${item.id}`,
meta: { title: item.alias, keepAlive: false, typeId: item.id, name: item.name, customIcon: item.icon },
// hideChildrenInMenu: true // Force display of MenuItem instead of SubMenu
})
})
})
const lastTypeId = window.localStorage.getItem('ops_ci_typeid') || undefined
if (lastTypeId && preference.some(item => item.id === Number(lastTypeId))) {
if (lastTypeId && preference.type_ids.some(item => item === Number(lastTypeId))) {
routes.redirect = `/cmdb/instances/types/${lastTypeId}`
} else if (routes.children[2]?.children?.length > 0) {
routes.redirect = routes.children[2].children.find(item => !item.hidden)?.path
} else if (routes.children[resourceViewsIndex]?.children?.length > 0) {
routes.redirect = routes.children[resourceViewsIndex].children.find(item => !item.hidden && !item.meta.disabled)?.path
} else {
routes.redirect = '/cmdb/dashboard'
}
@@ -169,7 +184,7 @@ const genCmdbRoutes = async () => {
meta: { title: item[0], icon: 'ops-cmdb-relation', selectedIcon: 'ops-cmdb-relation', keepAlive: false, name: item[0] },
}
})
routes.children.splice(2, 0, ...relationViews)
routes.children.splice(resourceViewsIndex, 0, ...relationViews)
return routes
}

View File

@@ -107,6 +107,7 @@
v-decorator="[list.name, { rules: [{ required: false }] }]"
style="width: 100%"
:format="getFieldType(list.name) == '4' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
:valueFormat="getFieldType(list.name) == '4' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
v-if="getFieldType(list.name) === '4' || getFieldType(list.name) === '3'"
:showTime="getFieldType(list.name) === '4' ? false : { format: 'HH:mm:ss' }"
/>
@@ -370,7 +371,7 @@ export default {
return 'select%%multiple'
}
return 'select'
} else if (_find.value_type === '0' || _find.value_type === '1') {
} else if ((_find.value_type === '0' || _find.value_type === '1') && !_find.is_list) {
return 'input_number'
} else if (_find.value_type === '4' || _find.value_type === '3') {
return _find.value_type

View File

@@ -182,7 +182,7 @@ export default {
const edges = []
this.parentCITypes.forEach((parent) => {
const _findCiType = ci_types_list.find((item) => item.id === parent.id)
if (this.firstCIs[parent.name]) {
if (this.firstCIs[parent.name] && _findCiType) {
const unique_id = _findCiType.show_id || _findCiType.unique_id
const _findUnique = parent.attributes.find((attr) => attr.id === unique_id)
const unique_name = _findUnique?.name
@@ -225,7 +225,7 @@ export default {
})
this.childCITypes.forEach((child) => {
const _findCiType = ci_types_list.find((item) => item.id === child.id)
if (this.secondCIs[child.name]) {
if (this.secondCIs[child.name] && _findCiType) {
const unique_id = _findCiType.show_id || _findCiType.unique_id
const _findUnique = child.attributes.find((attr) => attr.id === unique_id)
const unique_name = _findUnique?.name

View File

@@ -112,7 +112,7 @@
<a-tab-pane key="tab_5">
<span slot="tab"><ops-icon type="itsm-association" />{{ $t('cmdb.ci.relITSM') }}</span>
<div :style="{ padding: '24px', height: '100%' }">
<related-itsm-table :ci_id="ci._id" :ciHistory="ciHistory" :itsmInstalled="itsmInstalled" />
<RelatedItsmTable ref="relatedITSMTable" :ci_id="ci._id" :ciHistory="ciHistory" :itsmInstalled="itsmInstalled" :attrList="attrList" />
</div>
</a-tab-pane>
</a-tabs>
@@ -140,8 +140,6 @@ import CiDetailRelation from './ciDetailRelation.vue'
import TriggerTable from '../../operation_history/modules/triggerTable.vue'
import RelatedItsmTable from './ciDetailRelatedItsmTable.vue'
import CiRollbackForm from './ciRollbackForm.vue'
import { sleep } from '@/utils/util'
export default {
name: 'CiDetailTab',
components: {

View File

@@ -63,7 +63,7 @@
</a-tooltip>
<a-tooltip v-if="index !== CITypeGroups.length - 1">
<template slot="title">{{ $t('cmdb.ciType.up') }}</template>
<template slot="title">{{ $t('cmdb.ciType.down') }}</template>
<a
><a-icon
type="arrow-down"

View File

@@ -1,5 +1,5 @@
<template>
<div class="ci-types-wrap" :style="{ height: `${windowHeight - 66}px` }">
<div class="ci-types-wrap" :style="{ height: `${windowHeight - 96}px` }">
<div v-if="!CITypeGroups.length" class="ci-types-empty">
<a-empty :image="emptyImage" description=""></a-empty>
<a-button icon="plus" size="small" type="primary" @click="handleClickAddGroup">{{
@@ -15,6 +15,13 @@
:triggerLength="18"
>
<template #one>
<a-input
:placeholder="$t('cmdb.preference.searchPlaceholder')"
class="cmdb-ci-types-left-input"
@pressEnter="handleSearch"
>
<a-icon slot="prefix" type="search" />
</a-input>
<div class="ci-types-left">
<div class="ci-types-left-title">
<a-button
@@ -62,8 +69,8 @@
</a-dropdown>
</a-space>
</div>
<draggable class="ci-types-left-content" :list="CITypeGroups" @end="handleChangeGroups" filter=".undraggable">
<div v-for="g in CITypeGroups" :key="g.id || g.name">
<draggable class="ci-types-left-content" :list="computedCITypeGroups" @end="handleChangeGroups" filter=".undraggable">
<div v-for="g in computedCITypeGroups" :key="g.id || g.name">
<div
:class="
`${currentGId === g.id && !currentCId ? 'selected' : ''} ci-types-left-group ${
@@ -74,6 +81,7 @@
>
<div>
<OpsMoveIcon
v-if="g.id !== -1"
style="width: 17px; height: 17px; display: none; position: absolute; left: -3px; top: 10px"
/>
<span style="font-weight: 700">{{ g.name || $t('other') }}</span>
@@ -103,6 +111,7 @@
@start="start(g)"
@end="end(g)"
@add="add(g)"
filter=".undraggable"
>
<div
v-for="ci in g.ci_types"
@@ -110,8 +119,9 @@
:class="`${currentCId === ci.id ? 'selected' : ''} ci-types-left-detail`"
@click="handleClickCIType(g.id, ci.id, ci.name)"
>
<div>
<div :class="`${ g.id === -1 ? 'undraggable' : '' }`">
<OpsMoveIcon
v-if="g.id !== -1"
style="width: 17px; height: 17px; display: none; position: absolute; left: -1px; top: 8px"
/>
<span class="ci-types-left-detail-icon">
@@ -464,6 +474,8 @@ export default {
isInherit: false,
unique_id: null,
searchValue: '',
}
},
computed: {
@@ -536,6 +548,16 @@ export default {
}
return _showIdSelectOptions
},
computedCITypeGroups() {
if (this.searchValue) {
const ciTypes = _.cloneDeep(this.CITypeGroups)
ciTypes.forEach((item) => {
item.ci_types = item.ci_types.filter((_item) => _item.alias.toLowerCase().includes(this.searchValue.toLowerCase()))
})
return ciTypes
}
return this.CITypeGroups
},
},
provide() {
return {
@@ -565,6 +587,9 @@ export default {
this.allTreeDepAndEmp = res
})
},
handleSearch(e) {
this.searchValue = e.target.value
},
async loadCITypes(isResetCurrentId = false) {
const groups = await getCITypeGroupsConfig({ need_other: true })
let alreadyReset = false
@@ -664,7 +689,7 @@ export default {
content: that.$t('cmdb.ciType.confirmDeleteGroup', { groupName: `${g.name}` }),
onOk() {
deleteCITypeGroup(g.id).then((res) => {
that.$message.info(that.$t('deleteSuccess'))
that.$message.success(that.$t('deleteSuccess'))
that.loadCITypes(true)
})
},
@@ -1021,6 +1046,14 @@ export default {
top: 40%;
transform: translate(-50%, -50%);
}
/deep/.cmdb-ci-types-left-input {
input {
background-color: transparent;
}
.ant-input:focus {
box-shadow: none;
}
}
.ci-types-left {
width: 100%;
overflow: auto;

View File

@@ -32,62 +32,77 @@
}"
/></span>
</div>
<div class="cmdb-preference-group" v-for="(group, index) in myPreferences" :key="group.name">
<div class="cmdb-preference-group" v-for="(subType, index) in myPreferences" :key="subType.name">
<div class="cmdb-preference-group-title">
<span> <ops-icon :style="{ marginRight: '10px' }" :type="group.icon" />{{ group.name }} </span>
<span> <ops-icon :style="{ marginRight: '10px' }" :type="subType.icon" />{{ subType.name }}</span>
</div>
<draggable
v-model="group.ci_types"
:animation="300"
@change="
(e) => {
orderChange(e, group)
}
"
>
<div class="cmdb-preference-group-content" v-for="ciType in group.ci_types" :key="ciType.id">
<OpsMoveIcon class="cmdb-preference-move-icon" />
<draggable class="ci-types-left-content" :list="subType.groups" @end="handleChangeGroups" filter=".undraggable">
<div v-for="group in subType.groups" :key="group.id || group.name">
<div
:class="{
'cmdb-preference-avatar': true,
'cmdb-preference-avatar-noicon': !ciType.icon,
}"
:style="{ width: '30px', height: '30px', marginRight: '10px' }"
:class="
`${group.id === undefined ? 'undraggable' : ''}`
"
>
<template v-if="ciType.icon">
<img
v-if="ciType.icon.split('$$')[2]"
:src="`/api/common-setting/v1/file/${ciType.icon.split('$$')[3]}`"
:style="{ maxHeight: '30px', maxWidth: '30px' }"
/>
<ops-icon
v-else
:style="{
color: ciType.icon.split('$$')[1],
fontSize: '14px',
}"
:type="ciType.icon.split('$$')[0]"
/>
</template>
<span v-else :style="{ fontSize: '20px' }">{{ ciType.name[0].toUpperCase() }}</span>
<div v-if="index === 0 && subType.groups.length > 1" class="cmdb-preference-group-content">
<OpsMoveIcon class="cmdb-preference-move-icon" v-if="group.name"/>
<span style="font-weight: 500; color: #a5a9bc"><ellipsis :length="25" tooltip>{{ group.name || $t('other') }}</ellipsis></span>
<span :style="{ color: '#c3cdd7' }">({{ group.ci_types.length }})</span>
</div>
</div>
<span class="cmdb-preference-group-content-title">{{ ciType.alias || ciType.name }}</span>
<span class="cmdb-preference-group-content-action">
<a-tooltip :title="$t('cmdb.preference.cancelSub')">
<span
@click="unsubscribe(ciType, group.type)"
><ops-icon type="cmdb-preference-cancel-subscribe" />
<draggable
v-model="group.ci_types"
:animation="300"
@change="
(e) => {
orderChange(e, group, index === 1)
}
"
>
<div class="cmdb-preference-group-content" v-for="ciType in group.ci_types" :key="ciType.id">
<OpsMoveIcon class="cmdb-preference-move-icon" />
<div
:class="{
'cmdb-preference-avatar': true,
'cmdb-preference-avatar-noicon': !ciType.icon,
}"
:style="{ width: '30px', height: '30px', marginRight: '10px' }"
>
<template v-if="ciType.icon">
<img
v-if="ciType.icon.split('$$')[2]"
:src="`/api/common-setting/v1/file/${ciType.icon.split('$$')[3]}`"
:style="{ maxHeight: '30px', maxWidth: '30px' }"
/>
<ops-icon
v-else
:style="{
color: ciType.icon.split('$$')[1],
fontSize: '14px',
}"
:type="ciType.icon.split('$$')[0]"
/>
</template>
<span v-else :style="{ fontSize: '20px' }">{{ ciType.name[0].toUpperCase() }}</span>
</div>
<span class="cmdb-preference-group-content-title">{{ ciType.alias || ciType.name }}</span>
<span class="cmdb-preference-group-content-action">
<a-tooltip :title="$t('cmdb.preference.cancelSub')">
<span
@click="unsubscribe(ciType, group.type)"
><ops-icon type="cmdb-preference-cancel-subscribe" />
</span>
</a-tooltip>
<a-divider type="vertical" :style="{ margin: '0 3px' }" />
<a-tooltip :title="$t('cmdb.preference.editSub')">
<span
@click="openSubscribeSetting(ciType, `${index + 1}`)"
><ops-icon
type="cmdb-preference-subscribe"
/></span>
</a-tooltip>
</span>
</a-tooltip>
<a-divider type="vertical" :style="{ margin: '0 3px' }" />
<a-tooltip :title="$t('cmdb.preference.editSub')">
<span
@click="openSubscribeSetting(ciType, `${index + 1}`)"
><ops-icon
type="cmdb-preference-subscribe"
/></span>
</a-tooltip>
</span>
</div>
</draggable>
</div>
</draggable>
</div>
@@ -197,6 +212,7 @@ import store from '@/store'
import { mapState } from 'vuex'
import moment from 'moment'
import draggable from 'vuedraggable'
import { Ellipsis } from '@/components'
import { getCITypeGroups } from '../../api/ciTypeGroup'
import {
getPreference,
@@ -212,7 +228,7 @@ import { ops_move_icon as OpsMoveIcon } from '@/core/icons'
export default {
name: 'Preference',
components: { CollapseTransition, SubscribeSetting, draggable, OpsMoveIcon },
components: { CollapseTransition, SubscribeSetting, draggable, OpsMoveIcon, Ellipsis },
data() {
return {
citypeData: [],
@@ -261,7 +277,7 @@ export default {
ciTypeGroup.forEach((group) => {
if (group.ci_types && group.ci_types.length) {
group.ci_types.forEach((type) => {
const idx = pref.findIndex((p) => p.id === type.id)
const idx = pref.type_ids.findIndex((p) => p === type.id)
if (idx > -1) {
type.is_subscribed = true
}
@@ -283,19 +299,18 @@ export default {
const _myPreferences = [
{
name: this.$t('cmdb.menu.ciTable'),
ci_types: self.instance.map((item) => {
const _find = pref.find((ci) => ci.id === item)
return _find
}),
groups: pref.group_types,
icon: 'cmdb-ci',
type: 'ci',
},
{
name: this.$t('cmdb.menu.ciTree'),
ci_types: self.tree.map((item) => {
const _find = pref.find((ci) => ci.id === item)
return _find
}),
groups: [
{
ci_types: pref.tree_types,
name: null,
}
],
icon: 'cmdb-tree',
type: 'tree',
},
@@ -377,10 +392,41 @@ export default {
this.expandKeys.push(group.id)
}
},
orderChange(e, group) {
preferenceCitypeOrder({ type_ids: group.ci_types.map((type) => type.id), is_tree: group.type !== 'ci' })
async handleChangeGroups() {
const typeIds = []
this.myPreferences[0].groups.forEach(groupTypes => {
groupTypes.ci_types.forEach(ciType => {
typeIds.push(ciType.id)
})
})
preferenceCitypeOrder({ type_ids: typeIds, is_tree: false })
.then(() => {
if (group.type === 'ci') {
this.resetRoute()
})
.catch(() => {
this.getCITypes(false)
})
},
orderChange(e, group, isTree) {
let typeIds = []
if (!isTree) {
this.myPreferences[0].groups.forEach(groupTypes => {
if (group.id === groupTypes.id) {
group.ci_types.forEach(ciType => {
typeIds.push(ciType.id)
})
} else {
groupTypes.ci_types.forEach(ciType => {
typeIds.push(ciType.id)
})
}
})
} else {
typeIds = group.ci_types.map(item => item.id)
}
preferenceCitypeOrder({ type_ids: typeIds, is_tree: isTree })
.then(() => {
if (!isTree) {
this.resetRoute()
}
})
@@ -442,6 +488,23 @@ export default {
margin-bottom: 20px;
}
.cmdb-preference-group {
.ci-types-left-content {
max-height: calc(100% - 45px);
overflow: hidden;
&:hover {
overflow: auto;
}
.undraggable{
.cmdb-preference-group-content {
cursor: default;
margin-left: 20px;
&:hover {
background: transparent;
box-shadow: none;
}
}
}
}
.cmdb-preference-group-title {
text-align: center;
margin-bottom: 5px;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +1,19 @@
version: '3.5'
version: '2.19'
services:
cmdb-db:
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-db:2.3
container_name: cmdb-db
env_file:
- .env
environment:
TZ: Asia/Shanghai
MYSQL_ROOT_PASSWORD: '123456'
MYSQL_DATABASE: 'cmdb'
MYSQL_USER: 'cmdb'
MYSQL_PASSWORD: '123456'
volumes:
- db-data:/var/lib/mysql
- ./docs/mysqld.cnf:/etc/mysql/conf.d/mysqld.cnf
- ./docs/cmdb.sql:/docker-entrypoint-initdb.d/cmdb.sql
healthcheck:
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"]
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost", "-p$MYSQL_ROOT_PASSWORD"]
interval: 10s
timeout: 5s
retries: 5
@@ -45,11 +43,10 @@ services:
- redis
cmdb-api:
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.4.4
# build:
# context: .
# target: cmdb-api
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.4.5
container_name: cmdb-api
env_file:
- .env
environment:
TZ: Asia/Shanghai
WAIT_HOSTS: cmdb-db:3306, cmdb-cache:6379
@@ -77,17 +74,13 @@ services:
flask init-import-user-from-acl
flask init-department
flask cmdb-counter > counter.log 2>&1
networks:
new:
aliases:
- cmdb-api
cmdb-ui:
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.4.4
# build:
# context: .
# target: cmdb-ui
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.4.5
container_name: cmdb-ui
depends_on:
- cmdb-api

View File

@@ -492,7 +492,7 @@ CREATE TABLE `acl_role_relations` (
LOCK TABLES `acl_role_relations` WRITE;
/*!40000 ALTER TABLE `acl_role_relations` DISABLE KEYS */;
INSERT INTO `acl_role_relations` VALUES (NULL,0,NULL,NULL,1,1,2,NULL);
INSERT INTO `acl_role_relations` VALUES (NULL,0,NULL,NULL,1,1,2,NULL),(NULL,0,NULL,NULL,2,3,2,2);
/*!40000 ALTER TABLE `acl_role_relations` ENABLE KEYS */;
UNLOCK TABLES;

View File

@@ -5,14 +5,31 @@
## Install
- 启动 mysql 服务, redis 服务
> mysql一定要设置sql_mode, root进入mysql执行:
>
> `set global sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';`
>
> `set session sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';`
- 启动 mysql 服务, redis 服务,此处以 docker 为例
```bash
mkdir ~/cmdb_db # 用于持久化存储mysql数据
docker run -d -p 3306:3306 --name mysql-cmdb -e MYSQL_ROOT_PASSWORD=Root_321 -v ~/cmdb_db:/var/lib/mysql mysql
docker run -d --name redis -p 6379:6379 redis
```
- mysql需要先设置sql_mode, 进入容器,使用root账号,进入mysql执行:
```bash
docker exec -it mysql-cmdb bash
mysql -uroot -pRoot_321
```
```sql
`set global sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';`
`set session sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';`
```
- 创建数据库 cmdb
```sql
create database cmdb;
```
- 拉取代码
```bash
@@ -26,7 +43,8 @@ cp cmdb-api/settings.example.py cmdb-api/settings.py
- 安装库
- 后端: `cd cmdb-api && pipenv run pipenv install && cd ..`
- 前端: `cd cmdb-ui && yarn install && cd ..`
- 可以将 docs/cmdb.sql 导入到数据库里,登录用户和密码分别是:demo/123456
- node推荐使用14.x版本,推荐使用nvm进行nodejs版本管理`nvm install 14 && nvm use 14`
- 可以将 docs/cmdb.sql 导入到数据库里,登录用户和密码分别是:demo/123456
- 创建数据库表: 进入**cmdb-api**目录执行 `pipenv run flask db-setup && pipenv run flask common-check-new-columns && pipenv run flask cmdb-init-cache`
- 启动服务