Compare commits

..

18 Commits

Author SHA1 Message Date
pycook
c96c4ffd54 fix(api): unique constraint 2024-05-02 21:22:11 +08:00
pycook
ef25c94b5d fix(api): permissions for CIType group editing 2024-04-29 15:18:47 +08:00
pycook
06ae1bcf13 docs: update build_api_key 2024-04-29 15:11:12 +08:00
pycook
9ead4e7d8d chore: release v2.4.4 2024-04-29 14:44:33 +08:00
pycook
994a28dd25 feat(ui): baseline rollback (#502) 2024-04-29 10:10:07 +08:00
simontigers
74b587e46c Merge pull request #501 from simontigers/common_decorator_perms
fix: role base app perm
2024-04-29 09:27:36 +08:00
hu.sima
091cd882bd fix: role base app perm 2024-04-29 09:26:23 +08:00
simontigers
73093db467 Merge pull request #500 from simontigers/common_decorator_perms
fix(api): decorator_perms_role_required
2024-04-28 19:43:22 +08:00
hu.sima
66e268ce68 fix(api): decorator_perms_role_required 2024-04-28 19:41:50 +08:00
simontigers
a41d1a5e97 Merge pull request #499 from simontigers/common_decorator_perms
feat(api): role perm
2024-04-28 19:22:43 +08:00
hu.sima
b4b728fe28 feat(api): role perm 2024-04-28 19:22:10 +08:00
pycook
d16462d8b7 feat(api): ci baseline rollback (#498) 2024-04-28 19:19:14 +08:00
kdyq007
de7d98c0b4 feat(api): Add sorting function to ci list attribute (#495)
Co-authored-by: sherlock <sherlock@gmail.com>
2024-04-27 09:20:24 +08:00
pycook
51332c7236 feat(ui): CI change logs related itsm 2024-04-24 20:09:59 +08:00
dagongren
bf1076fe4a feat:update cs && update style (#488) 2024-04-23 12:20:27 +08:00
dagongren
3454a98cfb fix(cmdb-ui):service tree search (#487) 2024-04-19 13:32:12 +08:00
dagongren
506dcbb40e fix(cmdb-ui):fix service tree change table page (#486) 2024-04-19 11:46:51 +08:00
dagongren
5ac4517187 style (#482) 2024-04-18 10:49:39 +08:00
87 changed files with 1559 additions and 2195 deletions

View File

@@ -263,10 +263,11 @@ class CIManager(object):
ci_ids = None
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:
@@ -441,10 +442,13 @@ class CIManager(object):
now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
ci = self.confirm_ci_existed(ci_id)
raw_dict = copy.deepcopy(ci_dict)
attrs = CITypeAttributeManager.get_all_attributes(ci.type_id)
ci_type_attrs_name = {attr.name: attr for _, attr in attrs}
ci_type_attrs_alias2name = {attr.alias: attr.name for _, attr in attrs}
ci_dict = {ci_type_attrs_alias2name[k] if k in ci_type_attrs_alias2name else k: v for k, v in ci_dict.items()}
raw_dict = copy.deepcopy(ci_dict)
ci_attr2type_attr = {type_attr.attr_id: type_attr for type_attr, _ in attrs}
for _, attr in attrs:
if attr.default and attr.default.get('default') == AttributeDefaultValueEnum.UPDATED_AT:
@@ -825,6 +829,105 @@ class CIManager(object):
return data.get('v')
def baseline(self, ci_ids, before_date):
ci_list = self.get_cis_by_ids(ci_ids, ret_key=RetKey.ALIAS)
if not ci_list:
return dict()
ci2changed = dict()
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
ci2changed.setdefault(change['ci_id'], {})
item = (change['old'],
change['new'],
change.get('is_list'),
change.get('value_type'),
change['operate_type'])
if change.get('is_list'):
ci2changed[change['ci_id']].setdefault(change.get('attr_alias'), []).append(item)
else:
ci2changed[change['ci_id']].update({change.get('attr_alias'): item})
type2show_name = {}
result = []
for ci in ci_list:
list_attr2item = {}
for alias_name, v in (ci2changed.get(ci['_id']) or {}).items():
if not alias_name:
continue
if alias_name == ci.get('unique_alias'):
continue
if ci.get('_type') not in type2show_name:
ci_type = CITypeCache.get(ci.get('_type'))
show_id = ci_type.show_id or ci_type.unique_id
type2show_name[ci['_type']] = AttributeCache.get(show_id).alias
if isinstance(v, list):
for old, new, is_list, value_type, operate_type in v:
if alias_name not in list_attr2item:
list_attr2item[alias_name] = dict(instance=ci.get(type2show_name[ci['_type']]),
attr_name=alias_name,
value_type=value_type,
is_list=is_list,
ci_type=ci.get('ci_type'),
unique_alias=ci.get('unique_alias'),
unique_value=ci.get(ci['unique_alias']),
cur=copy.deepcopy(ci.get(alias_name)),
to=ci.get(alias_name) or [])
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:
list_attr2item[alias_name]['to'].remove(new)
elif operate_type == OperateType.DELETE and old not in list_attr2item[alias_name]['to']:
list_attr2item[alias_name]['to'].append(old)
continue
old, value_type = v[0], v[3]
old = ValueTypeMap.deserialize[value_type](old) if old else old
if isinstance(old, (datetime.datetime, datetime.date)):
old = str(old)
if ci.get(alias_name) != old:
item = dict(instance=ci.get(type2show_name[ci['_type']]),
attr_name=alias_name,
value_type=value_type,
ci_type=ci.get('ci_type'),
unique_alias=ci.get('unique_alias'),
unique_value=ci.get(ci['unique_alias']),
cur=ci.get(alias_name),
to=old)
result.append(item)
for alias_name, item in list_attr2item.items():
if sorted(item['cur'] or []) != sorted(item['to'] or []):
result.append(item)
return result
def rollback(self, ci_id, before_date):
baseline_ci = self.baseline([ci_id], before_date)
payload = dict()
for item in baseline_ci:
payload[item.get('attr_name')] = item.get('to')
if payload:
payload['ci_type'] = baseline_ci[0]['ci_type']
payload[baseline_ci[0]['unique_alias']] = baseline_ci[0]['unique_value']
self.update(ci_id, **payload)
return payload
class CIRelationManager(object):
"""

View File

@@ -253,6 +253,13 @@ class CITypeManager(object):
for item in CITypeInheritance.get_by(child_id=type_id, to_dict=False):
item.delete(commit=False)
try:
from api.models.cmdb import CITypeReconciliation
for item in CITypeReconciliation.get_by(type_id=type_id, to_dict=False):
item.delete(commit=False)
except Exception:
pass
db.session.commit()
ci_type.soft_delete()
@@ -414,9 +421,6 @@ class CITypeGroupManager(object):
existed = CITypeGroup.get_by_id(gid) or abort(
404, ErrFormat.ci_type_group_not_found.format("id={}".format(gid)))
if name is not None and name != existed.name:
if RoleEnum.CONFIG not in session.get("acl", {}).get("parentRoles", []) and not is_app_admin("cmdb"):
return abort(403, ErrFormat.role_required.format(RoleEnum.CONFIG))
existed.update(name=name)
max_order = max([i.order or 0 for i in CITypeGroupItem.get_by(group_id=gid, to_dict=False)] or [0])

View File

@@ -55,6 +55,9 @@ class CITypeOperateType(BaseEnum):
DELETE_UNIQUE_CONSTRAINT = "11" # 删除联合唯一
ADD_RELATION = "12" # 新增关系
DELETE_RELATION = "13" # 删除关系
ADD_RECONCILIATION = "14" # 删除关系
UPDATE_RECONCILIATION = "15" # 删除关系
DELETE_RECONCILIATION = "16" # 删除关系
class RetKey(BaseEnum):
@@ -98,6 +101,12 @@ class AttributeDefaultValueEnum(BaseEnum):
AUTO_INC_ID = "$auto_inc_id"
class ExecuteStatusEnum(BaseEnum):
COMPLETED = '0'
FAILED = '1'
RUNNING = '2'
CMDB_QUEUE = "one_cmdb_async"
REDIS_PREFIX_CI = "ONE_CMDB"
REDIS_PREFIX_CI_RELATION = "CMDB_CI_RELATION"

View File

@@ -26,7 +26,7 @@ from api.models.cmdb import OperationRecord
class AttributeHistoryManger(object):
@staticmethod
def get_records_for_attributes(start, end, username, page, page_size, operate_type, type_id,
ci_id=None, attr_id=None):
ci_id=None, attr_id=None, ci_ids=None, more=False):
records = db.session.query(OperationRecord, AttributeHistory).join(
AttributeHistory, OperationRecord.id == AttributeHistory.record_id)
@@ -48,6 +48,9 @@ class AttributeHistoryManger(object):
if ci_id is not None:
records = records.filter(AttributeHistory.ci_id == ci_id)
if ci_ids and isinstance(ci_ids, list):
records = records.filter(AttributeHistory.ci_id.in_(ci_ids))
if attr_id is not None:
records = records.filter(AttributeHistory.attr_id == attr_id)
@@ -62,6 +65,12 @@ class AttributeHistoryManger(object):
if attr_hist['attr']:
attr_hist['attr_name'] = attr_hist['attr'].name
attr_hist['attr_alias'] = attr_hist['attr'].alias
if more:
attr_hist['is_list'] = attr_hist['attr'].is_list
attr_hist['is_computed'] = attr_hist['attr'].is_computed
attr_hist['is_password'] = attr_hist['attr'].is_password
attr_hist['default'] = attr_hist['attr'].default
attr_hist['value_type'] = attr_hist['attr'].value_type
attr_hist.pop("attr")
if record_id not in res:
@@ -161,6 +170,7 @@ class AttributeHistoryManger(object):
record = i.OperationRecord
item = dict(attr_name=attr.name,
attr_alias=attr.alias,
value_type=attr.value_type,
operate_type=hist.operate_type,
username=user and user.nickname,
old=hist.old,
@@ -271,7 +281,7 @@ class CITypeHistoryManager(object):
return numfound, result
@staticmethod
def add(operate_type, type_id, attr_id=None, trigger_id=None, unique_constraint_id=None, change=None):
def add(operate_type, type_id, attr_id=None, trigger_id=None, unique_constraint_id=None, change=None, rc_id=None):
if type_id is None and attr_id is not None:
from api.models.cmdb import CITypeAttribute
type_ids = [i.type_id for i in CITypeAttribute.get_by(attr_id=attr_id, to_dict=False)]
@@ -284,6 +294,7 @@ class CITypeHistoryManager(object):
uid=current_user.uid,
attr_id=attr_id,
trigger_id=trigger_id,
rc_id=rc_id,
unique_constraint_id=unique_constraint_id,
change=change)

View File

@@ -78,6 +78,8 @@ class ErrFormat(CommonErrFormat):
unique_constraint_invalid = _l("Uniquely constrained attributes cannot be JSON and multi-valued")
ci_type_trigger_duplicate = _l("Duplicated trigger") # 重复的触发器
ci_type_trigger_not_found = _l("Trigger {} does not exist") # 触发器 {} 不存在
ci_type_reconciliation_duplicate = _l("Duplicated reconciliation rule") # 重复的校验规则
ci_type_reconciliation_not_found = _l("Reconciliation rule {} does not exist") # 规则 {} 不存在
record_not_found = _l("Operation record {} does not exist") # 操作记录 {} 不存在
cannot_delete_unique = _l("Unique identifier cannot be deleted") # 不能删除唯一标识

View File

@@ -275,25 +275,36 @@ 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]
added = set(value) - set(existed_values)
deleted = set(existed_values) - set(value)
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))
# Comparison array starts from which position changes
min_len = min(len(value), len(existed_values))
index = 0
while index < min_len:
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)]
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))
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
existed_value = (ValueTypeMap.serialize[attr.value_type](existed_value) if
existed_value or existed_value == 0 else existed_value)
if existed_value is None and value is not None:
value_table.create(ci_id=ci.id, attr_id=attr.id, value=value, flush=False, commit=False)
changed.append((ci.id, attr.id, OperateType.ADD, None, value, ci.type_id))
else:
if existed_value != value:
if existed_value != value and existed_attr:
if value is None:
existed_attr.delete(flush=False, commit=False)
else:

View File

@@ -10,6 +10,11 @@ from api.lib.perm.acl.role import RoleCRUD, RoleRelationCRUD
from api.lib.perm.acl.user import UserCRUD
def validate_app(app_id):
app = AppCache.get(app_id)
return app.id if app else None
class ACLManager(object):
def __init__(self, app_name='acl', uid=None):
self.log = current_app.logger
@@ -133,7 +138,8 @@ class ACLManager(object):
numfound, res = ResourceCRUD.search(q, u, self.validate_app().id, rt_id, page, page_size)
return res
def grant_resource(self, rid, resource_id, perms):
@staticmethod
def grant_resource(rid, resource_id, perms):
PermissionCRUD.grant(rid, perms, resource_id=resource_id, group_id=None)
@staticmethod
@@ -141,3 +147,7 @@ class ACLManager(object):
rt = AppCRUD.add(**payload)
return rt.to_dict()
def role_has_perms(self, rid, resource_name, resource_type_name, perm):
app_id = validate_app(self.app_name)
return RoleCRUD.has_permission(rid, resource_name, resource_type_name, app_id, perm)

View File

@@ -0,0 +1,37 @@
import functools
from flask import abort, session
from api.lib.common_setting.acl import ACLManager
from api.lib.common_setting.resp_format import ErrFormat
def perms_role_required(app_name, resource_type_name, resource_name, perm, role_name=None):
def decorator_perms_role_required(func):
@functools.wraps(func)
def wrapper_required(*args, **kwargs):
acl = ACLManager(app_name)
has_perms = False
try:
has_perms = acl.role_has_perms(session["acl"]['rid'], resource_name, resource_type_name, perm)
except Exception as e:
# resource_type not exist, continue check role
if role_name:
if role_name not in session.get("acl", {}).get("parentRoles", []):
abort(403, ErrFormat.role_required.format(role_name))
return func(*args, **kwargs)
else:
abort(403, ErrFormat.resource_no_permission.format(resource_name, perm))
if not has_perms:
if role_name:
if role_name not in session.get("acl", {}).get("parentRoles", []):
abort(403, ErrFormat.role_required.format(role_name))
else:
abort(403, ErrFormat.resource_no_permission.format(resource_name, perm))
return func(*args, **kwargs)
return wrapper_required
return decorator_perms_role_required

View File

@@ -80,3 +80,5 @@ class ErrFormat(CommonErrFormat):
ldap_test_username_required = _l("LDAP test username required") # LDAP测试用户名必填
company_wide = _l("Company wide") # 全公司
resource_no_permission = _l("No permission to access resource {}, perm {} ") # 没有权限访问 {} 资源的 {} 权限"

View File

@@ -0,0 +1,59 @@
class OperationPermission(object):
def __init__(self, resource_perms):
for _r in resource_perms:
setattr(self, _r['page'], _r['page'])
for _p in _r['perms']:
setattr(self, _p, _p)
class BaseApp(object):
resource_type_name = 'OperationPermission'
all_resource_perms = []
def __init__(self):
self.admin_name = None
self.roles = []
self.app_name = 'acl'
self.require_create_resource_type = self.resource_type_name
self.extra_create_resource_type_list = []
self.op = None
@staticmethod
def format_role(role_name, role_type, acl_rid, resource_perms, description=''):
return dict(
role_name=role_name,
role_type=role_type,
acl_rid=acl_rid,
description=description,
resource_perms=resource_perms,
)
class CMDBApp(BaseApp):
all_resource_perms = [
{"page": "Big_Screen", "page_cn": "大屏", "perms": ["read"]},
{"page": "Dashboard", "page_cn": "仪表盘", "perms": ["read"]},
{"page": "Resource_Search", "page_cn": "资源搜索", "perms": ["read"]},
{"page": "Auto_Discovery_Pool", "page_cn": "自动发现池", "perms": ["read"]},
{"page": "My_Subscriptions", "page_cn": "我的订阅", "perms": ["read"]},
{"page": "Bulk_Import", "page_cn": "批量导入", "perms": ["read"]},
{"page": "Model_Configuration", "page_cn": "模型配置",
"perms": ["read", "create_CIType", "create_CIType_group", "update_CIType_group",
"delete_CIType_group", "download_CIType"]},
{"page": "Backend_Management", "page_cn": "后台管理", "perms": ["read"]},
{"page": "Customized_Dashboard", "page_cn": "定制仪表盘", "perms": ["read"]},
{"page": "Service_Tree_Definition", "page_cn": "服务树定义", "perms": ["read"]},
{"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"]}]
def __init__(self):
super().__init__()
self.admin_name = 'cmdb_admin'
self.app_name = 'cmdb'
self.op = OperationPermission(self.all_resource_perms)

View File

@@ -224,16 +224,22 @@ class RoleRelationCache(object):
@classmethod
@flush_db
def rebuild(cls, rid, app_id):
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)
if resources.get('id2perms') or resources.get('group2perms'):
HasResourceRoleCache.add(rid, app_id)
if app_id is None:
app_ids = [None] + [i.id for i in App.get_by(to_dict=False)]
else:
HasResourceRoleCache.remove(rid, app_id)
cls.get_resources2(rid, app_id)
app_ids = [app_id]
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)
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)
@classmethod
@flush_db

View File

@@ -274,12 +274,14 @@ class PermissionCRUD(object):
perm2resource.setdefault(_perm, []).append(resource_id)
for _perm in perm2resource:
perm = PermissionCache.get(_perm, resource_type_id)
existeds = RolePermission.get_by(rid=rid,
app_id=app_id,
perm_id=perm.id,
__func_in___key_resource_id=perm2resource[_perm],
to_dict=False)
for existed in existeds:
if perm is None:
continue
exists = RolePermission.get_by(rid=rid,
app_id=app_id,
perm_id=perm.id,
__func_in___key_resource_id=perm2resource[_perm],
to_dict=False)
for existed in exists:
existed.deleted = True
existed.deleted_at = datetime.datetime.now()
db.session.add(existed)

View File

@@ -2,7 +2,6 @@
from flask import abort
from flask import current_app
from api.extensions import db
from api.lib.perm.acl.audit import AuditCRUD
@@ -127,11 +126,18 @@ class ResourceTypeCRUD(object):
existed_ids = [i.id for i in existed]
current_ids = []
rebuild_rids = set()
for i in existed:
if i.name not in perms:
i.soft_delete()
i.soft_delete(commit=False)
for rp in RolePermission.get_by(perm_id=i.id, to_dict=False):
rp.soft_delete(commit=False)
rebuild_rids.add((rp.app_id, rp.rid))
else:
current_ids.append(i.id)
db.session.commit()
for _app_id, _rid in rebuild_rids:
role_rebuild.apply_async(args=(_rid, _app_id), queue=ACL_QUEUE)
for i in perms:
if i not in existed_names:

View File

@@ -3,12 +3,14 @@
import time
import redis_lock
import six
from flask import abort
from flask import current_app
from sqlalchemy import or_
from api.extensions import db
from api.extensions import rd
from api.lib.perm.acl.app import AppCRUD
from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType, AuditScope
from api.lib.perm.acl.cache import AppCache
@@ -62,7 +64,9 @@ class RoleRelationCRUD(object):
id2parents = {}
for i in res:
id2parents.setdefault(rid2uid.get(i.child_id, i.child_id), []).append(RoleCache.get(i.parent_id).to_dict())
parent = RoleCache.get(i.parent_id)
if parent:
id2parents.setdefault(rid2uid.get(i.child_id, i.child_id), []).append(parent.to_dict())
return id2parents
@@ -141,24 +145,27 @@ class RoleRelationCRUD(object):
@classmethod
def add(cls, role, parent_id, child_ids, app_id):
result = []
for child_id in child_ids:
existed = RoleRelation.get_by(parent_id=parent_id, child_id=child_id, app_id=app_id)
if existed:
continue
with redis_lock.Lock(rd.r, "ROLE_RELATION_ADD"):
db.session.commit()
RoleRelationCache.clean(parent_id, app_id)
RoleRelationCache.clean(child_id, app_id)
result = []
for child_id in child_ids:
existed = RoleRelation.get_by(parent_id=parent_id, child_id=child_id, app_id=app_id)
if existed:
continue
if parent_id in cls.recursive_child_ids(child_id, app_id):
return abort(400, ErrFormat.inheritance_dead_loop)
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)
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())
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, {}, {},

View File

@@ -432,6 +432,7 @@ class CITypeHistory(Model):
attr_id = db.Column(db.Integer)
trigger_id = db.Column(db.Integer)
rc_id = db.Column(db.Integer)
unique_constraint_id = db.Column(db.Integer)
uid = db.Column(db.Integer, index=True)

View File

@@ -3,12 +3,13 @@
import json
import re
from celery_once import QueueOnce
import redis_lock
from flask import current_app
from werkzeug.exceptions import BadRequest
from werkzeug.exceptions import NotFound
from api.extensions import celery
from api.extensions import rd
from api.lib.decorator import flush_db
from api.lib.decorator import reconnect_db
from api.lib.perm.acl.audit import AuditCRUD
@@ -25,14 +26,14 @@ from api.models.acl import Role
from api.models.acl import Trigger
@celery.task(name="acl.role_rebuild",
queue=ACL_QUEUE,)
@celery.task(name="acl.role_rebuild", queue=ACL_QUEUE, )
@flush_db
@reconnect_db
def role_rebuild(rids, app_id):
rids = rids if isinstance(rids, list) else [rids]
for rid in rids:
RoleRelationCache.rebuild(rid, app_id)
with redis_lock.Lock(rd.r, "ROLE_REBUILD_{}_{}".format(rid, app_id)):
RoleRelationCache.rebuild(rid, app_id)
current_app.logger.info("Role {0} App {1} rebuild..........".format(rids, app_id))

View File

@@ -16,6 +16,7 @@ from api.lib.cmdb.const import RetKey
from api.lib.cmdb.perms import has_perm_for_ci
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
from api.lib.decorator import args_required
from api.lib.perm.acl.acl import has_perm_from_args
from api.lib.utils import get_page
from api.lib.utils import get_page_size
@@ -254,3 +255,23 @@ class CIPasswordView(APIView):
def post(self, ci_id, attr_id):
return self.get(ci_id, attr_id)
class CIBaselineView(APIView):
url_prefix = ("/ci/baseline", "/ci/<int:ci_id>/baseline/rollback")
@args_required("before_date")
def get(self):
ci_ids = handle_arg_list(request.values.get('ci_ids'))
before_date = request.values.get('before_date')
return self.jsonify(CIManager().baseline(list(map(int, ci_ids)), before_date))
@args_required("before_date")
def post(self, ci_id):
if 'rollback' in request.url:
before_date = request.values.get('before_date')
return self.jsonify(**CIManager().rollback(ci_id, before_date))
return self.get(ci_id)

View File

@@ -7,7 +7,6 @@ from io import BytesIO
from flask import abort
from flask import current_app
from flask import request
from flask import session
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeCache
@@ -19,10 +18,12 @@ from api.lib.cmdb.ci_type import CITypeManager
from api.lib.cmdb.ci_type import CITypeTemplateManager
from api.lib.cmdb.ci_type import CITypeTriggerManager
from api.lib.cmdb.ci_type import CITypeUniqueConstraintManager
from api.lib.cmdb.const import PermEnum, ResourceTypeEnum, RoleEnum
from api.lib.cmdb.const import PermEnum, ResourceTypeEnum
from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.cmdb.preference import PreferenceManager
from api.lib.cmdb.resp_format import ErrFormat
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
@@ -36,6 +37,8 @@ from api.lib.perm.auth import auth_with_app_token
from api.lib.utils import handle_arg_list
from api.resource import APIView
app_cli = CMDBApp()
class CITypeView(APIView):
url_prefix = ("/ci_types", "/ci_types/<int:type_id>", "/ci_types/<string:type_name>",
@@ -116,7 +119,6 @@ class CITypeInheritanceView(APIView):
class CITypeGroupView(APIView):
url_prefix = ("/ci_types/groups",
"/ci_types/groups/config",
"/ci_types/groups/order",
"/ci_types/groups/<int:gid>")
def get(self):
@@ -125,7 +127,8 @@ class CITypeGroupView(APIView):
return self.jsonify(CITypeGroupManager.get(need_other, config_required))
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Model_Configuration,
app_cli.op.create_CIType_group, app_cli.admin_name)
@args_required("name")
@args_validate(CITypeGroupManager.cls)
def post(self):
@@ -136,15 +139,6 @@ class CITypeGroupView(APIView):
@args_validate(CITypeGroupManager.cls)
def put(self, gid=None):
if "/order" in request.url:
if RoleEnum.CONFIG not in session.get("acl", {}).get("parentRoles", []) and not is_app_admin("cmdb"):
return abort(403, ErrFormat.role_required.format(RoleEnum.CONFIG))
group_ids = request.values.get('group_ids')
CITypeGroupManager.order(group_ids)
return self.jsonify(group_ids=group_ids)
name = request.values.get('name') or abort(400, ErrFormat.argument_value_required.format("name"))
type_ids = request.values.get('type_ids')
@@ -152,7 +146,8 @@ class CITypeGroupView(APIView):
return self.jsonify(gid=gid)
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Model_Configuration,
app_cli.op.delete_CIType_group, app_cli.admin_name)
def delete(self, gid):
type_ids = request.values.get("type_ids")
CITypeGroupManager.delete(gid, type_ids)
@@ -160,6 +155,18 @@ class CITypeGroupView(APIView):
return self.jsonify(gid=gid)
class CITypeGroupOrderView(APIView):
url_prefix = "/ci_types/groups/order"
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Model_Configuration,
app_cli.op.update_CIType_group, app_cli.admin_name)
def put(self):
group_ids = request.values.get('group_ids')
CITypeGroupManager.order(group_ids)
return self.jsonify(group_ids=group_ids)
class CITypeQueryView(APIView):
url_prefix = "/ci_types/query"
@@ -352,14 +359,16 @@ class CITypeAttributeGroupView(APIView):
class CITypeTemplateView(APIView):
url_prefix = ("/ci_types/template/import", "/ci_types/template/export", "/ci_types/<int:type_id>/template/export")
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Model_Configuration,
app_cli.op.download_CIType, app_cli.admin_name)
def get(self, type_id=None): # export
if type_id is not None:
return self.jsonify(dict(ci_type_template=CITypeTemplateManager.export_template_by_type(type_id)))
return self.jsonify(dict(ci_type_template=CITypeTemplateManager.export_template()))
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Model_Configuration,
app_cli.op.download_CIType, app_cli.admin_name)
def post(self): # import
tpt = request.values.get('ci_type_template') or {}
@@ -379,7 +388,8 @@ class CITypeCanDefineComputed(APIView):
class CITypeTemplateFileView(APIView):
url_prefix = ("/ci_types/template/import/file", "/ci_types/template/export/file")
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Model_Configuration,
app_cli.op.download_CIType, app_cli.admin_name)
def get(self): # export
tpt_json = CITypeTemplateManager.export_template()
tpt_json = dict(ci_type_template=tpt_json)
@@ -394,7 +404,8 @@ class CITypeTemplateFileView(APIView):
mimetype='application/json',
max_age=0)
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Model_Configuration,
app_cli.op.download_CIType, app_cli.admin_name)
def post(self): # import
f = request.files.get('file')

View File

@@ -11,6 +11,8 @@ from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.preference import PreferenceManager
from api.lib.cmdb.resp_format import ErrFormat
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.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import has_perm_from_args
@@ -18,6 +20,8 @@ from api.lib.perm.acl.acl import is_app_admin
from api.lib.perm.acl.acl import role_required
from api.resource import APIView
app_cli = CMDBApp()
class GetChildrenView(APIView):
url_prefix = ("/ci_type_relations/<int:parent_id>/children",
@@ -41,7 +45,8 @@ class GetParentsView(APIView):
class CITypeRelationView(APIView):
url_prefix = ("/ci_type_relations", "/ci_type_relations/<int:parent_id>/<int:child_id>")
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Service_Tree_Definition,
app_cli.op.read, app_cli.admin_name)
def get(self):
res, type2attributes = CITypeRelationManager.get()
@@ -69,7 +74,8 @@ class CITypeRelationView(APIView):
class CITypeRelationDelete2View(APIView):
url_prefix = "/ci_type_relations/<int:ctr_id>"
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Model_Relationships,
app_cli.op.read, app_cli.admin_name)
def delete(self, ctr_id):
CITypeRelationManager.delete(ctr_id)

View File

@@ -3,14 +3,16 @@
from flask import request
from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.custom_dashboard import CustomDashboardManager
from api.lib.cmdb.custom_dashboard import SystemConfigManager
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 role_required
from api.resource import APIView
app_cli = CMDBApp()
class CustomDashboardApiView(APIView):
url_prefix = ("/custom_dashboard", "/custom_dashboard/<int:_id>", "/custom_dashboard/batch",
@@ -19,7 +21,8 @@ class CustomDashboardApiView(APIView):
def get(self):
return self.jsonify(CustomDashboardManager.get())
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Customized_Dashboard,
app_cli.op.read, app_cli.admin_name)
@args_validate(CustomDashboardManager.cls)
def post(self):
if request.url.endswith("/preview"):
@@ -32,7 +35,8 @@ class CustomDashboardApiView(APIView):
return self.jsonify(res)
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Customized_Dashboard,
app_cli.op.read, app_cli.admin_name)
@args_validate(CustomDashboardManager.cls)
def put(self, _id=None):
if _id is not None:
@@ -47,7 +51,8 @@ class CustomDashboardApiView(APIView):
return self.jsonify(id2options=request.values.get('id2options'))
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Customized_Dashboard,
app_cli.op.read, app_cli.admin_name)
def delete(self, _id):
CustomDashboardManager.delete(_id)
@@ -57,12 +62,14 @@ class CustomDashboardApiView(APIView):
class SystemConfigApiView(APIView):
url_prefix = ("/system_config",)
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Service_Tree_Definition,
app_cli.op.read, app_cli.admin_name)
@args_required("name", value_required=True)
def get(self):
return self.jsonify(SystemConfigManager.get(request.values['name']))
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Service_Tree_Definition,
app_cli.op.read, app_cli.admin_name)
@args_validate(SystemConfigManager.cls)
@args_required("name", value_required=True)
@args_required("option", value_required=True)
@@ -74,7 +81,8 @@ class SystemConfigApiView(APIView):
def put(self, _id=None):
return self.post()
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Service_Tree_Definition,
app_cli.op.read, app_cli.admin_name)
@args_required("name")
def delete(self):
CustomDashboardManager.delete(request.values['name'])

View File

@@ -5,28 +5,29 @@ import datetime
from flask import abort
from flask import request
from flask import session
from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.history import AttributeHistoryManger
from api.lib.cmdb.history import CITriggerHistoryManager
from api.lib.cmdb.history import CITypeHistoryManager
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.common_setting.decorator import perms_role_required
from api.lib.common_setting.role_perm_base import CMDBApp
from api.lib.perm.acl.acl import has_perm_from_args
from api.lib.perm.acl.acl import is_app_admin
from api.lib.perm.acl.acl import role_required
from api.lib.utils import get_page
from api.lib.utils import get_page_size
from api.resource import APIView
app_cli = CMDBApp()
class RecordView(APIView):
url_prefix = ("/history/records/attribute", "/history/records/relation")
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Operation_Audit,
app_cli.op.read, app_cli.admin_name)
def get(self):
page = get_page(request.values.get("page", 1))
page_size = get_page_size(request.values.get("page_size"))
@@ -80,18 +81,21 @@ class CIHistoryView(APIView):
class CITriggerHistoryView(APIView):
url_prefix = ("/history/ci_triggers/<int:ci_id>", "/history/ci_triggers")
url_prefix = ("/history/ci_triggers/<int:ci_id>",)
@has_perm_from_args("ci_id", ResourceTypeEnum.CI, PermEnum.READ, CIManager.get_type_name)
def get(self, ci_id=None):
if ci_id is not None:
result = CITriggerHistoryManager.get_by_ci_id(ci_id)
def get(self, ci_id):
result = CITriggerHistoryManager.get_by_ci_id(ci_id)
return self.jsonify(result)
return self.jsonify(result)
if RoleEnum.CONFIG not in session.get("acl", {}).get("parentRoles", []) and not is_app_admin("cmdb"):
return abort(403, ErrFormat.role_required.format(RoleEnum.CONFIG))
class CIsTriggerHistoryView(APIView):
url_prefix = ("/history/ci_triggers",)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Operation_Audit,
app_cli.op.read, app_cli.admin_name)
def get(self):
type_id = request.values.get("type_id")
trigger_id = request.values.get("trigger_id")
operate_type = request.values.get("operate_type")
@@ -115,7 +119,8 @@ class CITriggerHistoryView(APIView):
class CITypeHistoryView(APIView):
url_prefix = "/history/ci_types"
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Operation_Audit,
app_cli.op.read, app_cli.admin_name)
def get(self):
type_id = request.values.get("type_id")
username = request.values.get("username")

View File

@@ -8,20 +8,22 @@ from flask import request
from api.lib.cmdb.ci_type import CITypeManager
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.cmdb.preference import PreferenceManager
from api.lib.cmdb.resp_format import ErrFormat
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.lib.perm.acl.acl import role_required
from api.lib.perm.acl.acl import validate_permission
from api.lib.utils import handle_arg_list
from api.resource import APIView
app_cli = CMDBApp()
class PreferenceShowCITypesView(APIView):
url_prefix = ("/preference/ci_types", "/preference/ci_types2")
@@ -104,7 +106,8 @@ class PreferenceRelationApiView(APIView):
return self.jsonify(views=views, id2type=id2type, name2id=name2id)
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Service_Tree_Definition,
app_cli.op.read, app_cli.admin_name)
@args_required("name")
@args_required("cr_ids")
@args_validate(PreferenceManager.pref_rel_cls)
@@ -118,14 +121,16 @@ class PreferenceRelationApiView(APIView):
return self.jsonify(views=views, id2type=id2type, name2id=name2id)
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Service_Tree_Definition,
app_cli.op.read, app_cli.admin_name)
@args_required("name")
def put(self, _id):
views, id2type, name2id = PreferenceManager.create_or_update_relation_view(_id=_id, **request.values)
return self.jsonify(views=views, id2type=id2type, name2id=name2id)
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Service_Tree_Definition,
app_cli.op.read, app_cli.admin_name)
@args_required("name")
def delete(self):
name = request.values.get("name")

View File

@@ -4,14 +4,16 @@
from flask import abort
from flask import request
from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.relation_type import RelationTypeManager
from api.lib.cmdb.resp_format import ErrFormat
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 role_required
from api.resource import APIView
app_cli = CMDBApp()
class RelationTypeView(APIView):
url_prefix = ("/relation_types", "/relation_types/<int:rel_id>")
@@ -19,7 +21,8 @@ class RelationTypeView(APIView):
def get(self):
return self.jsonify([i.to_dict() for i in RelationTypeManager.get_all()])
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Relationship_Types,
app_cli.op.read, app_cli.admin_name)
@args_required("name")
@args_validate(RelationTypeManager.cls)
def post(self):
@@ -28,7 +31,8 @@ class RelationTypeView(APIView):
return self.jsonify(rel.to_dict())
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Relationship_Types,
app_cli.op.read, app_cli.admin_name)
@args_required("name")
@args_validate(RelationTypeManager.cls)
def put(self, rel_id):
@@ -37,7 +41,8 @@ class RelationTypeView(APIView):
return self.jsonify(rel.to_dict())
@role_required(RoleEnum.CONFIG)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Relationship_Types,
app_cli.op.read, app_cli.admin_name)
def delete(self, rel_id):
RelationTypeManager.delete(rel_id)

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 3857903 */
src: url('iconfont.woff2?t=1713335725699') format('woff2'),
url('iconfont.woff?t=1713335725699') format('woff'),
url('iconfont.ttf?t=1713335725699') format('truetype');
src: url('iconfont.woff2?t=1713840593232') format('woff2'),
url('iconfont.woff?t=1713840593232') format('woff'),
url('iconfont.ttf?t=1713840593232') format('truetype');
}
.iconfont {
@@ -13,6 +13,30 @@
-moz-osx-font-smoothing: grayscale;
}
.ops-setting-application-selected:before {
content: "\e919";
}
.ops-setting-application:before {
content: "\e918";
}
.ops-setting-basic:before {
content: "\e889";
}
.ops-setting-basic-selected:before {
content: "\e917";
}
.ops-setting-security:before {
content: "\e915";
}
.ops-setting-theme:before {
content: "\e916";
}
.veops-show:before {
content: "\e914";
}
@@ -21,7 +45,7 @@
content: "\e913";
}
.a-itsm-workload1:before {
.itsm-workload:before {
content: "\e912";
}
@@ -269,10 +293,6 @@
content: "\e8d5";
}
.ops-setting-auth-selected:before {
content: "\e8d4";
}
.itsm-knowledge2:before {
content: "\e8d2";
}
@@ -501,10 +521,6 @@
content: "\e89c";
}
.ops-setting-duty-selected:before {
content: "\e89b";
}
.datainsight-sequential:before {
content: "\e899";
}
@@ -669,38 +685,6 @@
content: "\e870";
}
.ops-itsm-ticketsetting-selected:before {
content: "\e860";
}
.ops-itsm-reports-selected:before {
content: "\e861";
}
.ops-itsm-servicecatalog-selected:before {
content: "\e862";
}
.ops-itsm-ticketmanage-selected:before {
content: "\e863";
}
.ops-itsm-knowledge-selected:before {
content: "\e864";
}
.ops-itsm-workstation-selected:before {
content: "\e865";
}
.ops-itsm-servicedesk-selected:before {
content: "\e866";
}
.ops-itsm-planticket-selected:before {
content: "\e867";
}
.ops-itsm-servicecatalog:before {
content: "\e868";
}
@@ -1073,26 +1057,10 @@
content: "\e816";
}
.ops-cmdb-batch-selected:before {
content: "\e803";
}
.ops-cmdb-batch:before {
content: "\e80a";
}
.ops-cmdb-adc-selected:before {
content: "\e7f7";
}
.ops-cmdb-resource-selected:before {
content: "\e7f8";
}
.ops-cmdb-preference-selected:before {
content: "\e7f9";
}
.ops-cmdb-preference:before {
content: "\e7fa";
}
@@ -1101,22 +1069,10 @@
content: "\e7fb";
}
.ops-cmdb-tree-selected:before {
content: "\e7fc";
}
.ops-cmdb-relation-selected:before {
content: "\e7fd";
}
.ops-cmdb-adc:before {
content: "\e7fe";
}
.ops-cmdb-search-selected:before {
content: "\e7ff";
}
.ops-cmdb-relation:before {
content: "\e800";
}
@@ -1125,14 +1081,6 @@
content: "\e801";
}
.ops-cmdb-citype-selected:before {
content: "\e802";
}
.ops-cmdb-dashboard-selected:before {
content: "\e804";
}
.ops-cmdb-citype:before {
content: "\e805";
}
@@ -1141,10 +1089,6 @@
content: "\e806";
}
.ops-cmdb-screen-selected:before {
content: "\e807";
}
.ops-cmdb-resource:before {
content: "\e808";
}
@@ -1493,14 +1437,6 @@
content: "\e7a6";
}
.ops-setting-role-selected:before {
content: "\e7a0";
}
.ops-setting-group-selected:before {
content: "\e7a1";
}
.ops-setting-role:before {
content: "\e7a2";
}
@@ -1941,18 +1877,10 @@
content: "\e738";
}
.ops-setting-notice-email-selected-copy:before {
content: "\e889";
}
.ops-setting-notice:before {
content: "\e72f";
}
.ops-setting-notice-selected:before {
content: "\e730";
}
.ops-setting-notice-email-selected:before {
content: "\e731";
}
@@ -1977,10 +1905,6 @@
content: "\e736";
}
.ops-setting-companyStructure-selected:before {
content: "\e72b";
}
.ops-setting-companyStructure:before {
content: "\e72c";
}
@@ -1989,10 +1913,6 @@
content: "\e72d";
}
.ops-setting-companyInfo-selected:before {
content: "\e72e";
}
.ops-email:before {
content: "\e61a";
}
@@ -3033,14 +2953,6 @@
content: "\e600";
}
.ops-dag-dashboard-selected:before {
content: "\e601";
}
.ops-dag-applet-selected:before {
content: "\e602";
}
.ops-dag-applet:before {
content: "\e603";
}
@@ -3049,62 +2961,26 @@
content: "\e604";
}
.ops-dag-terminal-selected:before {
content: "\e605";
}
.ops-dag-cron:before {
content: "\e606";
}
.ops-dag-cron-selected:before {
content: "\e608";
}
.ops-dag-history:before {
content: "\e609";
}
.ops-dag-history-selected:before {
content: "\e60a";
}
.ops-dag-dags-selected:before {
content: "\e60c";
}
.ops-dag-dagreview:before {
content: "\e60d";
}
.ops-dag-dagreview-selected:before {
content: "\e60e";
}
.ops-dag-panel:before {
content: "\e60f";
}
.ops-dag-panel-selected:before {
content: "\e615";
}
.ops-dag-variables:before {
content: "\e616";
}
.ops-dag-variables-selected:before {
content: "\e618";
}
.ops-dag-appletadmin:before {
content: "\e65c";
}
.ops-dag-appletadmin-selected:before {
content: "\e65d";
}
.ops-dag-dags:before {
content: "\e60b";
}

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,48 @@
"css_prefix_text": "",
"description": "",
"glyphs": [
{
"icon_id": "40043662",
"name": "ops-setting-application-selected",
"font_class": "ops-setting-application-selected",
"unicode": "e919",
"unicode_decimal": 59673
},
{
"icon_id": "40043685",
"name": "ops-setting-application",
"font_class": "ops-setting-application",
"unicode": "e918",
"unicode_decimal": 59672
},
{
"icon_id": "40043049",
"name": "ops-setting-basic",
"font_class": "ops-setting-basic",
"unicode": "e889",
"unicode_decimal": 59529
},
{
"icon_id": "40043047",
"name": "ops-setting-basic-selected",
"font_class": "ops-setting-basic-selected",
"unicode": "e917",
"unicode_decimal": 59671
},
{
"icon_id": "40038753",
"name": "ops-setting-security",
"font_class": "ops-setting-security",
"unicode": "e915",
"unicode_decimal": 59669
},
{
"icon_id": "40038752",
"name": "ops-setting-theme",
"font_class": "ops-setting-theme",
"unicode": "e916",
"unicode_decimal": 59670
},
{
"icon_id": "39948814",
"name": "veops-show",
@@ -22,7 +64,7 @@
{
"icon_id": "39926833",
"name": "itsm-workload (1)",
"font_class": "a-itsm-workload1",
"font_class": "itsm-workload",
"unicode": "e912",
"unicode_decimal": 59666
},
@@ -453,13 +495,6 @@
"unicode": "e8d5",
"unicode_decimal": 59605
},
{
"icon_id": "38547389",
"name": "setting-authentication-selected",
"font_class": "ops-setting-auth-selected",
"unicode": "e8d4",
"unicode_decimal": 59604
},
{
"icon_id": "38533133",
"name": "itsm-knowledge (2)",
@@ -859,13 +894,6 @@
"unicode": "e89c",
"unicode_decimal": 59548
},
{
"icon_id": "37940033",
"name": "ops-setting-duty-selected",
"font_class": "ops-setting-duty-selected",
"unicode": "e89b",
"unicode_decimal": 59547
},
{
"icon_id": "37841524",
"name": "datainsight-sequential",
@@ -1153,62 +1181,6 @@
"unicode": "e870",
"unicode_decimal": 59504
},
{
"icon_id": "35984161",
"name": "ops-itsm-ticketsetting-selected",
"font_class": "ops-itsm-ticketsetting-selected",
"unicode": "e860",
"unicode_decimal": 59488
},
{
"icon_id": "35984162",
"name": "ops-itsm-reports-selected",
"font_class": "ops-itsm-reports-selected",
"unicode": "e861",
"unicode_decimal": 59489
},
{
"icon_id": "35984163",
"name": "ops-itsm-servicecatalog-selected",
"font_class": "ops-itsm-servicecatalog-selected",
"unicode": "e862",
"unicode_decimal": 59490
},
{
"icon_id": "35984164",
"name": "ops-itsm-ticketmanage-selected",
"font_class": "ops-itsm-ticketmanage-selected",
"unicode": "e863",
"unicode_decimal": 59491
},
{
"icon_id": "35984165",
"name": "ops-itsm-knowledge-selected",
"font_class": "ops-itsm-knowledge-selected",
"unicode": "e864",
"unicode_decimal": 59492
},
{
"icon_id": "35984166",
"name": "ops-itsm-workstation-selected",
"font_class": "ops-itsm-workstation-selected",
"unicode": "e865",
"unicode_decimal": 59493
},
{
"icon_id": "35984167",
"name": "ops-itsm-servicedesk-selected",
"font_class": "ops-itsm-servicedesk-selected",
"unicode": "e866",
"unicode_decimal": 59494
},
{
"icon_id": "35984168",
"name": "ops-itsm-planticket-selected",
"font_class": "ops-itsm-planticket-selected",
"unicode": "e867",
"unicode_decimal": 59495
},
{
"icon_id": "35984169",
"name": "ops-itsm-servicecatalog",
@@ -1860,13 +1832,6 @@
"unicode": "e816",
"unicode_decimal": 59414
},
{
"icon_id": "35400645",
"name": "ops-cmdb-batch-selected",
"font_class": "ops-cmdb-batch-selected",
"unicode": "e803",
"unicode_decimal": 59395
},
{
"icon_id": "35400646",
"name": "ops-cmdb-batch",
@@ -1874,27 +1839,6 @@
"unicode": "e80a",
"unicode_decimal": 59402
},
{
"icon_id": "35395300",
"name": "ops-cmdb-adc-selected",
"font_class": "ops-cmdb-adc-selected",
"unicode": "e7f7",
"unicode_decimal": 59383
},
{
"icon_id": "35395301",
"name": "ops-cmdb-resource-selected",
"font_class": "ops-cmdb-resource-selected",
"unicode": "e7f8",
"unicode_decimal": 59384
},
{
"icon_id": "35395302",
"name": "ops-cmdb-preference-selected",
"font_class": "ops-cmdb-preference-selected",
"unicode": "e7f9",
"unicode_decimal": 59385
},
{
"icon_id": "35395303",
"name": "ops-cmdb-preference",
@@ -1909,20 +1853,6 @@
"unicode": "e7fb",
"unicode_decimal": 59387
},
{
"icon_id": "35395305",
"name": "ops-cmdb-tree-selected",
"font_class": "ops-cmdb-tree-selected",
"unicode": "e7fc",
"unicode_decimal": 59388
},
{
"icon_id": "35395306",
"name": "ops-cmdb-relation-selected",
"font_class": "ops-cmdb-relation-selected",
"unicode": "e7fd",
"unicode_decimal": 59389
},
{
"icon_id": "35395307",
"name": "ops-cmdb-adc",
@@ -1930,13 +1860,6 @@
"unicode": "e7fe",
"unicode_decimal": 59390
},
{
"icon_id": "35395308",
"name": "ops-cmdb-search-selected",
"font_class": "ops-cmdb-search-selected",
"unicode": "e7ff",
"unicode_decimal": 59391
},
{
"icon_id": "35395309",
"name": "ops-cmdb-relation",
@@ -1951,20 +1874,6 @@
"unicode": "e801",
"unicode_decimal": 59393
},
{
"icon_id": "35395311",
"name": "ops-cmdb-citype-selected",
"font_class": "ops-cmdb-citype-selected",
"unicode": "e802",
"unicode_decimal": 59394
},
{
"icon_id": "35395313",
"name": "ops-cmdb-dashboard-selected",
"font_class": "ops-cmdb-dashboard-selected",
"unicode": "e804",
"unicode_decimal": 59396
},
{
"icon_id": "35395314",
"name": "ops-cmdb-citype",
@@ -1979,13 +1888,6 @@
"unicode": "e806",
"unicode_decimal": 59398
},
{
"icon_id": "35395316",
"name": "ops-cmdb-screen-selected",
"font_class": "ops-cmdb-screen-selected",
"unicode": "e807",
"unicode_decimal": 59399
},
{
"icon_id": "35395317",
"name": "ops-cmdb-resource",
@@ -2595,20 +2497,6 @@
"unicode": "e7a6",
"unicode_decimal": 59302
},
{
"icon_id": "34792792",
"name": "ops-setting-role-selected",
"font_class": "ops-setting-role-selected",
"unicode": "e7a0",
"unicode_decimal": 59296
},
{
"icon_id": "34792793",
"name": "ops-setting-group-selected",
"font_class": "ops-setting-group-selected",
"unicode": "e7a1",
"unicode_decimal": 59297
},
{
"icon_id": "34792794",
"name": "ops-setting-role",
@@ -3379,13 +3267,6 @@
"unicode": "e738",
"unicode_decimal": 59192
},
{
"icon_id": "37575490",
"name": "ops-setting-notice-email-selected",
"font_class": "ops-setting-notice-email-selected-copy",
"unicode": "e889",
"unicode_decimal": 59529
},
{
"icon_id": "34108346",
"name": "ops-setting-notice",
@@ -3393,13 +3274,6 @@
"unicode": "e72f",
"unicode_decimal": 59183
},
{
"icon_id": "34108348",
"name": "ops-setting-notice-selected",
"font_class": "ops-setting-notice-selected",
"unicode": "e730",
"unicode_decimal": 59184
},
{
"icon_id": "34108504",
"name": "ops-setting-notice-email-selected",
@@ -3442,13 +3316,6 @@
"unicode": "e736",
"unicode_decimal": 59190
},
{
"icon_id": "34108244",
"name": "ops-setting-companyStructure-selected",
"font_class": "ops-setting-companyStructure-selected",
"unicode": "e72b",
"unicode_decimal": 59179
},
{
"icon_id": "34108296",
"name": "ops-setting-companyStructure",
@@ -3463,13 +3330,6 @@
"unicode": "e72d",
"unicode_decimal": 59181
},
{
"icon_id": "34108330",
"name": "ops-setting-companyInfo-selected",
"font_class": "ops-setting-companyInfo-selected",
"unicode": "e72e",
"unicode_decimal": 59182
},
{
"icon_id": "34099810",
"name": "ops-email",
@@ -5290,20 +5150,6 @@
"unicode": "e600",
"unicode_decimal": 58880
},
{
"icon_id": "33053294",
"name": "ops-dag-dashboard-selected",
"font_class": "ops-dag-dashboard-selected",
"unicode": "e601",
"unicode_decimal": 58881
},
{
"icon_id": "33053330",
"name": "ops-dag-applet-selected",
"font_class": "ops-dag-applet-selected",
"unicode": "e602",
"unicode_decimal": 58882
},
{
"icon_id": "33053531",
"name": "ops-dag-applet",
@@ -5318,13 +5164,6 @@
"unicode": "e604",
"unicode_decimal": 58884
},
{
"icon_id": "33053589",
"name": "ops-dag-terminal-selected",
"font_class": "ops-dag-terminal-selected",
"unicode": "e605",
"unicode_decimal": 58885
},
{
"icon_id": "33053591",
"name": "ops-dag-cron",
@@ -5332,13 +5171,6 @@
"unicode": "e606",
"unicode_decimal": 58886
},
{
"icon_id": "33053609",
"name": "ops-dag-cron-selected",
"font_class": "ops-dag-cron-selected",
"unicode": "e608",
"unicode_decimal": 58888
},
{
"icon_id": "33053615",
"name": "ops-dag-history",
@@ -5346,20 +5178,6 @@
"unicode": "e609",
"unicode_decimal": 58889
},
{
"icon_id": "33053617",
"name": "ops-dag-history-selected",
"font_class": "ops-dag-history-selected",
"unicode": "e60a",
"unicode_decimal": 58890
},
{
"icon_id": "33053681",
"name": "ops-dag-dags-selected",
"font_class": "ops-dag-dags-selected",
"unicode": "e60c",
"unicode_decimal": 58892
},
{
"icon_id": "33053682",
"name": "ops-dag-dagreview",
@@ -5367,13 +5185,6 @@
"unicode": "e60d",
"unicode_decimal": 58893
},
{
"icon_id": "33053684",
"name": "ops-dag-dagreview-selected",
"font_class": "ops-dag-dagreview-selected",
"unicode": "e60e",
"unicode_decimal": 58894
},
{
"icon_id": "33053691",
"name": "ops-dag-panel",
@@ -5381,13 +5192,6 @@
"unicode": "e60f",
"unicode_decimal": 58895
},
{
"icon_id": "33053692",
"name": "ops-dag-panel-selected",
"font_class": "ops-dag-panel-selected",
"unicode": "e615",
"unicode_decimal": 58901
},
{
"icon_id": "33053707",
"name": "ops-dag-variables",
@@ -5395,27 +5199,6 @@
"unicode": "e616",
"unicode_decimal": 58902
},
{
"icon_id": "33053715",
"name": "ops-dag-variables-selected",
"font_class": "ops-dag-variables-selected",
"unicode": "e618",
"unicode_decimal": 58904
},
{
"icon_id": "33053718",
"name": "ops-dag-appletadmin",
"font_class": "ops-dag-appletadmin",
"unicode": "e65c",
"unicode_decimal": 58972
},
{
"icon_id": "33053720",
"name": "ops-dag-appletadmin-selected",
"font_class": "ops-dag-appletadmin-selected",
"unicode": "e65d",
"unicode_decimal": 58973
},
{
"icon_id": "33055163",
"name": "ops-dag-dags",

Binary file not shown.

View File

@@ -73,7 +73,7 @@ export default {
.custom-drawer-close {
position: absolute;
cursor: pointer;
background: #custom_colors[color_1];
background: @primary-color;
color: white;
text-align: center;
transition: all 0.3s;

View File

@@ -298,14 +298,14 @@ export default {
width: 20px;
height: 20px;
border-radius: 2px;
background-color: #custom_colors[color_2];
color: #custom_colors[color_1];
background-color: @primary-color_5;
color: @primary-color;
display: inline-flex;
justify-content: center;
align-items: center;
cursor: pointer;
&:hover {
background-color: #custom_colors[color_1];
background-color: @primary-color;
color: #fff;
}
}

View File

@@ -161,7 +161,7 @@ export default {
cursor: pointer;
&-selected,
&:hover {
color: #custom_colors[color_1];
color: @primary-color;
}
}
}
@@ -179,7 +179,7 @@ export default {
font-weight: 400;
font-size: 14px;
color: #000000;
border-left: 2px solid #custom_colors[color_1];
border-left: 2px solid @primary-color;
padding-left: 6px;
margin-left: -6px;
}

View File

@@ -185,14 +185,14 @@ export default {
width: 20px;
height: 20px;
border-radius: 2px;
background-color: #custom_colors[color_2];
color: #custom_colors[color_1];
background-color: @primary-color_5;
color: @primary-color;
display: inline-flex;
justify-content: center;
align-items: center;
cursor: pointer;
&:hover {
background-color: #custom_colors[color_1];
background-color: @primary-color;
color: #fff;
}
}

View File

@@ -93,7 +93,7 @@ export default {
background-color: transparent;
}
.sidebar-list-item.sidebar-list-item-selected::before {
background-color: #custom_colors[color_1];
background-color: @primary-color;
}
.sidebar-list-item-dotline {
padding-left: 20px;

View File

@@ -97,8 +97,8 @@ export default {
<style lang="less">
.color {
color: #custom_colors[color_1];
background-color: #custom_colors[color_2];
color: @primary-color;
background-color: @primary-color_5;
}
.custom-user {
.custom-user-item {
@@ -117,7 +117,7 @@ export default {
.locale {
cursor: pointer;
&:hover {
color: #custom_colors[color_1];
color: @primary-color;
}
}
</style>

View File

@@ -54,3 +54,36 @@ export function getCiTriggersByCiId(ci_id, params) {
params
})
}
export function getCiRelatedTickets(params) {
return axios({
url: `/itsm/v1/process_ticket/get_tickets_by`,
method: 'POST',
data: params,
isShowMessage: false
})
}
export function judgeItsmInstalled() {
return axios({
url: `/itsm/v1/process_ticket/itsm_existed`,
method: 'GET',
isShowMessage: false
})
}
export function getCIsBaseline(params) {
return axios({
url: `/v0.1/ci/baseline`,
method: 'GET',
params
})
}
export function CIBaselineRollback(ciId, params) {
return axios({
url: `/v0.1/ci/${ciId}/baseline/rollback`,
method: 'POST',
data: params
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -230,7 +230,7 @@ export default {
background-color: #f9fbff;
border-bottom: none;
.ant-transfer-list-header-title {
color: #custom_colors[color_1];
color: @primary-color;
font-weight: 400;
font-size: 14px;
}
@@ -258,7 +258,7 @@ export default {
cursor: pointer;
font-size: 12px;
background-color: #fff;
color: #custom_colors[color_1];
color: @primary-color;
border-radius: 4px;
width: 12px;
}
@@ -271,7 +271,7 @@ export default {
font-size: 12px;
color: #cacdd9;
&:hover {
color: #custom_colors[color_1];
color: @primary-color;
}
}
.move-icon {
@@ -291,8 +291,8 @@ export default {
}
}
.ant-transfer-list-content-item-selected {
background-color: #custom_colors[color_2];
border-color: #custom_colors[color_1];
background-color: @primary-color_5;
border-color: @primary-color;
}
}
}

View File

@@ -1,5 +1,5 @@
<template>
<div class="cmdb-grant" :style="{ maxHeight: `${windowHeight - 130}px` }">
<div class="cmdb-grant" :style="{ }">
<template v-if="cmdbGrantType.includes('ci_type')">
<div class="cmdb-grant-title">{{ $t('cmdb.components.ciTypeGrant') }}</div>
<CiTypeGrant

View File

@@ -75,10 +75,10 @@ export default {
position: absolute;
width: 0;
height: 0;
// background-color: #custom_colors[color_1];
// background-color: @primary-color;
border-radius: 2px;
border: 14px solid transparent;
border-left-color: #custom_colors[color_1];
border-left-color: @primary-color;
transform: rotate(225deg);
top: -16px;
left: -17px;

View File

@@ -175,8 +175,8 @@ export default {
text-overflow: ellipsis;
white-space: nowrap;
&:hover {
background-color: #custom_colors[color_2];
color: #custom_colors[color_1];
background-color: @primary-color_5;
color: @primary-color;
}
}
}
@@ -188,7 +188,7 @@ export default {
.notice-content {
.w-e-bar {
background-color: #custom_colors[color_2];
background-color: @primary-color_5;
}
.w-e-text-placeholder {
line-height: 1.5;

View File

@@ -253,7 +253,7 @@ export default {
}
.cmdb-subscribe-drawer-tree-header {
border-radius: 4px;
background-color: #custom_colors[color_2];
background-color: @primary-color_5;
color: rgba(0, 0, 0, 0.4);
padding: 8px 12px;
margin-bottom: 12px;
@@ -264,7 +264,7 @@ export default {
> span {
display: inline-block;
background-color: #fff;
border-left: 2px solid #custom_colors[color_1];
border-left: 2px solid @primary-color;
padding: 3px 12px;
position: relative;
white-space: nowrap;
@@ -272,7 +272,7 @@ export default {
cursor: pointer;
font-size: 12px;
&:hover {
color: #custom_colors[color_1];
color: @primary-color;
}
}
}
@@ -316,7 +316,7 @@ export default {
<style lang="less">
.cmdb-subscribe-drawer {
.ant-tabs-bar {
background-color: #custom_colors[color_2];
background-color: @primary-color_5;
border-bottom: none;
}
}

View File

@@ -39,7 +39,7 @@
>
<img slot="image" :src="require('@/assets/data_empty.png')" />
<span slot="description"> {{ $t('cmdb.components.noParamRequest') }} </span>
<a-button @click="add" type="primary" size="small" icon="plus" class="ops-button-primary">
<a-button @click="add" type="primary" size="small" icon="plus">
{{ $t('add') }}
</a-button>
</a-empty>

View File

@@ -402,7 +402,15 @@ const cmdb_en = {
noModifications: 'No Modifications',
attr: 'attribute',
attrId: 'attribute id',
changeDescription: 'attribute id: {attr_id}, {before_days} day(s) in advance, Subject: {subject}\nContent: {body}\nNotify At: {notify_at}'
changeDescription: 'attribute id: {attr_id}, {before_days} day(s) in advance, Subject: {subject}\nContent: {body}\nNotify At: {notify_at}',
ticketStartTime: 'Start Time',
ticketCreator: 'Creator',
ticketTitle: 'Title',
ticketFinishTime: 'Finish Time',
ticketNodeName: 'Node Name',
itsmUninstalled: 'Please use it in combination with VE ITSM',
applyItsm: 'Free Apply ITSM',
ticketId: 'Ticket ID',
},
relation_type: {
addRelationType: 'New',
@@ -494,7 +502,8 @@ if __name__ == "__main__":
copyFailed: 'Copy failed',
noLevel: 'No hierarchical relationship!',
batchAddRelation: 'Batch Add Relation',
history: 'History',
history: 'Change Logs',
relITSM: 'Related Tickets',
topo: 'Topology',
table: 'Table',
m2mTips: 'The current CIType relationship is many-to-many, please go to the SerivceTree(relation view) to add or delete',
@@ -512,7 +521,21 @@ if __name__ == "__main__":
newUpdateField: 'Add a Attribute',
attributeSettings: 'Attribute Settings',
share: 'Share',
noPermission: 'No Permission'
noPermission: 'No Permission',
rollback: 'Rollback',
rollbackHeader: 'Instance Rollback',
rollbackTo: 'Rollback to',
rollbackToTips: 'Please select rollback time',
baselineDiff: 'Difference from baseline',
instance: 'Instance',
rollbackBefore: 'Current value',
rollbackAfter: 'After rollback',
noDiff: 'CI data has not changed after {baseline}',
rollbackConfirm: 'Are you sure you want to rollback?',
rollbackSuccess: 'Rollback successfully',
rollbackingTips: 'Rollbacking',
batchRollbacking: 'Deleting {total} items in total, {successNum} items successful, {errorNum} items failed',
baselineTips: 'Changes at this point in time will also be rollbacked, Unique ID and password attributes do not support',
},
serviceTree: {
remove: 'Remove',

View File

@@ -402,7 +402,15 @@ const cmdb_zh = {
noModifications: '没有修改',
attr: '属性名',
attrId: '属性ID',
changeDescription: '属性ID{attr_id},提前:{before_days}天,主题:{subject}\n内容{body}\n通知时间{notify_at}'
changeDescription: '属性ID{attr_id},提前:{before_days}天,主题:{subject}\n内容{body}\n通知时间{notify_at}',
ticketStartTime: '工单发起时间',
ticketCreator: '发起人',
ticketTitle: '工单名称',
ticketFinishTime: '节点完成时间',
ticketNodeName: '节点名称',
itsmUninstalled: '请结合维易ITSM使用',
applyItsm: '免费申请',
ticketId: '工单ID',
},
relation_type: {
addRelationType: '新增关系类型',
@@ -494,7 +502,8 @@ if __name__ == "__main__":
copyFailed: '复制失败!',
noLevel: '无层级关系!',
batchAddRelation: '批量添加关系',
history: '操作历史',
history: '变更记录',
relITSM: '关联工单',
topo: '拓扑',
table: '表格',
m2mTips: '当前模型关系为多对多,请前往关系视图进行增删操作',
@@ -512,7 +521,21 @@ if __name__ == "__main__":
newUpdateField: '新增修改字段',
attributeSettings: '字段设置',
share: '分享',
noPermission: '暂无权限'
noPermission: '暂无权限',
rollback: '回滚',
rollbackHeader: '实例回滚',
rollbackTo: '回滚至: ',
rollbackToTips: '请选择回滚时间点',
baselineDiff: '基线对比结果',
instance: '实例',
rollbackBefore: '当前值',
rollbackAfter: '回滚后',
noDiff: '在【{baseline}】后数据没有发生变化',
rollbackConfirm: '确认要回滚吗 ',
rollbackSuccess: '回滚成功',
rollbackingTips: '正在批量回滚中',
batchRollbacking: '正在回滚,共{total}个,成功{successNum}个,失败{errorNum}个',
baselineTips: '该时间点的变更也会被回滚, 唯一标识、密码属性不支持回滚',
},
serviceTree: {
remove: '移除',

View File

@@ -13,7 +13,7 @@ const genCmdbRoutes = async () => {
{
path: '/cmdb/dashboard',
name: 'cmdb_dashboard',
meta: { title: 'dashboard', icon: 'ops-cmdb-dashboard', selectedIcon: 'ops-cmdb-dashboard-selected', keepAlive: false },
meta: { title: 'dashboard', icon: 'ops-cmdb-dashboard', selectedIcon: 'ops-cmdb-dashboard', keepAlive: false },
component: () => import('../views/dashboard/index_v2.vue')
},
{
@@ -25,7 +25,7 @@ const genCmdbRoutes = async () => {
path: '/cmdb/resourceviews',
name: 'cmdb_resource_views',
component: RouteView,
meta: { title: 'cmdb.menu.ciTable', icon: 'ops-cmdb-resource', selectedIcon: 'ops-cmdb-resource-selected', keepAlive: true },
meta: { title: 'cmdb.menu.ciTable', icon: 'ops-cmdb-resource', selectedIcon: 'ops-cmdb-resource', keepAlive: true },
hideChildrenInMenu: false,
children: []
},
@@ -33,7 +33,7 @@ const genCmdbRoutes = async () => {
path: '/cmdb/tree_views',
component: () => import('../views/tree_views'),
name: 'cmdb_tree_views',
meta: { title: 'cmdb.menu.ciTree', icon: 'ops-cmdb-tree', selectedIcon: 'ops-cmdb-tree-selected', keepAlive: false },
meta: { title: 'cmdb.menu.ciTree', icon: 'ops-cmdb-tree', selectedIcon: 'ops-cmdb-tree', keepAlive: false },
hideChildrenInMenu: true,
children: [
{
@@ -47,13 +47,13 @@ const genCmdbRoutes = async () => {
{
path: '/cmdb/resourcesearch',
name: 'cmdb_resource_search',
meta: { title: 'cmdb.menu.ciSearch', icon: 'ops-cmdb-search', selectedIcon: 'ops-cmdb-search-selected', keepAlive: false },
meta: { title: 'cmdb.menu.ciSearch', icon: 'ops-cmdb-search', selectedIcon: 'ops-cmdb-search', keepAlive: false },
component: () => import('../views/resource_search/index.vue')
},
{
path: '/cmdb/adc',
name: 'cmdb_auto_discovery_ci',
meta: { title: 'cmdb.menu.adCIs', icon: 'ops-cmdb-adc', selectedIcon: 'ops-cmdb-adc-selected', keepAlive: false },
meta: { title: 'cmdb.menu.adCIs', icon: 'ops-cmdb-adc', selectedIcon: 'ops-cmdb-adc', keepAlive: false },
component: () => import('../views/discoveryCI/index.vue')
},
{
@@ -72,19 +72,19 @@ const genCmdbRoutes = async () => {
path: '/cmdb/preference',
component: () => import('../views/preference/index'),
name: 'cmdb_preference',
meta: { title: 'cmdb.menu.preference', icon: 'ops-cmdb-preference', selectedIcon: 'ops-cmdb-preference-selected', keepAlive: false }
meta: { title: 'cmdb.menu.preference', icon: 'ops-cmdb-preference', selectedIcon: 'ops-cmdb-preference', keepAlive: false }
},
{
path: '/cmdb/batch',
component: () => import('../views/batch'),
name: 'cmdb_batch',
meta: { 'title': 'cmdb.menu.batchUpload', icon: 'ops-cmdb-batch', selectedIcon: 'ops-cmdb-batch-selected', keepAlive: false }
meta: { 'title': 'cmdb.menu.batchUpload', icon: 'ops-cmdb-batch', selectedIcon: 'ops-cmdb-batch', keepAlive: false }
},
{
path: '/cmdb/ci_types',
name: 'ci_type',
component: () => import('../views/ci_types/index'),
meta: { title: 'cmdb.menu.citypeManage', icon: 'ops-cmdb-citype', selectedIcon: 'ops-cmdb-citype-selected', keepAlive: false, permission: ['cmdb_admin', 'admin'] }
meta: { title: 'cmdb.menu.citypeManage', icon: 'ops-cmdb-citype', selectedIcon: 'ops-cmdb-citype', keepAlive: false, permission: ['cmdb_admin', 'admin'] }
},
{
path: '/cmdb/disabled3',
@@ -166,7 +166,7 @@ const genCmdbRoutes = async () => {
path: `/cmdb/relationviews/${item[1]}`,
name: `cmdb_relation_views_${item[1]}`,
component: () => import('../views/relation_views/index'),
meta: { title: item[0], icon: 'ops-cmdb-relation', selectedIcon: 'ops-cmdb-relation-selected', keepAlive: false, name: item[0] },
meta: { title: item[0], icon: 'ops-cmdb-relation', selectedIcon: 'ops-cmdb-relation', keepAlive: false, name: item[0] },
}
})
routes.children.splice(2, 0, ...relationViews)

View File

@@ -2,7 +2,7 @@
<div>
<div class="ci-detail-header">{{ this.type.alias }}</div>
<div class="ci-detail-page">
<CiDetailTab ref="ciDetailTab" :typeId="typeId" />
<ci-detail-tab ref="ciDetailTab" :typeId="typeId" :attributeHistoryTableHeight="windowHeight - 250" />
</div>
</div>
</template>
@@ -23,6 +23,11 @@ export default {
attributes: {},
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
},
provide() {
return {
attrList: () => {
@@ -55,7 +60,6 @@ export default {
</script>
<style lang="less" scoped>
.ci-detail-header {
border-left: 3px solid @primary-color;
padding-left: 10px;

View File

@@ -68,6 +68,8 @@
<span @click="openBatchDownload">{{ $t('download') }}</span>
<a-divider type="vertical" />
<span @click="batchDelete">{{ $t('delete') }}</span>
<a-divider type="vertical" />
<span @click="batchRollback">{{ $t('cmdb.ci.rollback') }}</span>
<span>{{ $t('cmdb.ci.selectRows', { rows: selectedRowKeys.length }) }}</span>
</div>
</SearchForm>
@@ -293,6 +295,7 @@
<create-instance-form ref="create" @reload="reloadData" @submit="batchUpdate" />
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
<BatchDownload ref="batchDownload" @batchDownload="batchDownload" />
<ci-rollback-form ref="ciRollbackForm" @batchRollbackAsync="batchRollbackAsync($event)" :ciIds="selectedRowKeys" />
<MetadataDrawer ref="metadataDrawer" />
<CMDBGrant ref="cmdbGrant" resourceTypeName="CIType" app_id="cmdb" />
</a-spin>
@@ -325,6 +328,8 @@ import MetadataDrawer from './modules/MetadataDrawer.vue'
import CMDBGrant from '../../components/cmdbGrant'
import { ops_move_icon as OpsMoveIcon } from '@/core/icons'
import { getAttrPassword } from '../../api/CITypeAttr'
import CiRollbackForm from './modules/ciRollbackForm.vue'
import { CIBaselineRollback } from '../../api/history'
export default {
name: 'InstanceList',
@@ -340,6 +345,7 @@ export default {
MetadataDrawer,
CMDBGrant,
OpsMoveIcon,
CiRollbackForm,
},
computed: {
windowHeight() {
@@ -429,6 +435,12 @@ export default {
// window.onkeypress = (e) => {
// this.handleKeyPress(e)
// }
this.$nextTick(() => {
const loadingNode = document.getElementsByClassName('ant-drawer-mask')
if (loadingNode?.style) {
loadingNode.style.zIndex = 8
}
})
setTimeout(() => {
this.columnDrop()
}, 1000)
@@ -661,7 +673,7 @@ export default {
message: this.$t('warning'),
description: errorMsg,
duration: 0,
style: { whiteSpace: 'break-spaces' },
style: { whiteSpace: 'break-spaces', overflow: 'auto', maxHeight: this.windowHeight - 80 + 'px' },
})
errorNum += 1
})
@@ -744,6 +756,55 @@ export default {
},
})
},
batchRollback() {
this.$nextTick(() => {
this.$refs.ciRollbackForm.onOpen(true)
})
},
async batchRollbackAsync(params) {
const mask = document.querySelector('.ant-drawer-mask')
const oldValue = mask.style.zIndex
mask.style.zIndex = 2
let successNum = 0
let errorNum = 0
this.loading = true
this.loadTip = this.$t('cmdb.ci.rollbackingTips')
const floor = Math.ceil(this.selectedRowKeys.length / 6)
for (let i = 0; i < floor; i++) {
const itemList = this.selectedRowKeys.slice(6 * i, 6 * i + 6)
const promises = itemList.map((x) => CIBaselineRollback(x, params))
await Promise.allSettled(promises)
.then((res) => {
res.forEach((r) => {
if (r.status === 'fulfilled') {
successNum += 1
} else {
errorNum += 1
}
})
})
.finally(() => {
this.loadTip = this.$t('cmdb.ci.batchRollbacking', {
total: this.selectedRowKeys.length,
successNum: successNum,
errorNum: errorNum,
})
})
}
this.loading = false
this.loadTip = ''
mask.style.zIndex = oldValue
this.selectedRowKeys = []
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
this.$nextTick(() => {
if (this.currentPage === 1) {
this.loadTableData()
} else {
this.currentPage = 1
}
})
},
async refreshAfterEditAttrs() {
await this.loadPreferenceAttrList()
await this.loadTableData()

View File

@@ -1,12 +1,13 @@
<template>
<CustomDrawer
width="80%"
width="90%"
placement="left"
@close="
() => {
visible = false
}
"
style="transform: translateX(0px)!important"
:visible="visible"
:hasTitle="false"
:hasFooter="false"

View File

@@ -0,0 +1,225 @@
<template>
<div :style="{ height: '100%' }" v-if="itsmInstalled">
<vxe-table
ref="xTable"
show-overflow
show-header-overflow
resizable
border
size="small"
class="ops-unstripe-table"
:span-method="mergeRowMethod"
:data="tableData"
v-bind="ci_id ? { height: 'auto' } : { height: `${windowHeight - 225}px` }"
>
<template #empty>
<a-empty :image-style="{ height: '100px' }" :style="{ paddingTop: '10%' }">
<img slot="image" :src="require('@/assets/data_empty.png')" />
<span slot="description"> {{ $t('noData') }} </span>
</a-empty>
</template>
<vxe-column field="ticket.ticket_id" min-width="80" :title="$t('cmdb.history.ticketId')"> </vxe-column>
<vxe-column field="ticket.created_at" width="160" :title="$t('cmdb.history.ticketStartTime')"> </vxe-column>
<vxe-column field="ticket.creator_name" min-width="80" :title="$t('cmdb.history.ticketCreator')"> </vxe-column>
<vxe-column field="ticket.title" min-width="150" :title="$t('cmdb.history.ticketTitle')">
<template slot-scope="{ row }">
<a target="_blank" :href="row.ticket.url">{{ row.ticket.title }}</a>
</template>
</vxe-column>
<vxe-column field="ticket.node_finish_time" width="160" :title="$t('cmdb.history.ticketFinishTime')">
</vxe-column>
<vxe-column field="ticket.node_name" min-width="100" :title="$t('cmdb.history.ticketNodeName')"> </vxe-column>
<vxe-table-column
field="operate_type"
min-width="100"
:filters="[
{ value: 0, label: $t('new') },
{ value: 1, label: $t('delete') },
{ value: 2, label: $t('update') },
]"
:filter-method="filterOperateMethod"
:title="$t('operation')"
>
<template #default="{ row }">
{{ operateTypeMap[row.operate_type] }}
</template>
</vxe-table-column>
<vxe-table-column
field="attr_alias"
min-width="100"
:title="$t('cmdb.attribute')"
:filters="[]"
:filter-method="filterAttrMethod"
>
</vxe-table-column>
<vxe-table-column field="old" min-width="100" :title="$t('cmdb.history.old')"></vxe-table-column>
<vxe-table-column field="new" min-width="100" :title="$t('cmdb.history.new')"></vxe-table-column>
</vxe-table>
<div :style="{ textAlign: 'right' }" v-if="!ci_id">
<a-pagination
size="small"
show-size-changer
show-quick-jumper
:page-size-options="pageSizeOptions"
:current="tablePage.currentPage"
:total="tablePage.totalResult"
:show-total="(total, range) => $t('cmdb.history.totalItems', { total: total })"
:page-size="tablePage.pageSize"
:default-current="1"
@change="pageOrSizeChange"
@showSizeChange="pageOrSizeChange"
>
</a-pagination>
</div>
</div>
<a-empty
v-else
:image-style="{
height: '200px',
}"
:style="{ paddingTop: '10%' }"
>
<img slot="image" :src="require('@/modules/cmdb/assets/itsm_uninstalled.png')" />
<span slot="description"> {{ $t('cmdb.history.itsmUninstalled') }} </span>
<a-button href="https://veops.cn/apply" target="_blank" type="primary">
{{ $t('cmdb.history.applyItsm') }}
</a-button>
</a-empty>
</template>
<script>
import { getCiRelatedTickets } from '../../../api/history'
export default {
name: 'RelatedItsmTable',
props: {
ci_id: {
type: Number,
default: null,
},
ciHistory: {
type: Array,
default: () => [],
},
itsmInstalled: {
type: Boolean,
default: true,
},
attrList: {
type: Array,
default: () => [],
}
},
data() {
return {
tableData: [],
tablePage: {
currentPage: 1,
pageSize: 50,
totalResult: 0,
},
pageSizeOptions: ['50', '100', '200'],
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
operateTypeMap() {
return {
0: this.$t('new'),
1: this.$t('delete'),
2: this.$t('update'),
}
},
},
mounted() {
this.updateTableData()
},
methods: {
updateTableData(currentPage = 1, pageSize = this.tablePage.pageSize) {
const params = { page: currentPage, page_size: pageSize, next_todo_ids: [] }
if (this.ci_id) {
const tableData = []
this.ciHistory.forEach((item) => {
if (item.ticket_id) {
params.next_todo_ids.push(item.ticket_id)
tableData.push(item)
}
})
if (params.next_todo_ids.length) {
getCiRelatedTickets(params)
.then((res) => {
const ticketId2Detail = {}
res.forEach((item) => {
ticketId2Detail[item.next_todo_id] = item
})
this.tableData = tableData.map((item) => {
return {
...item,
ticket: ticketId2Detail[item.ticket_id],
}
})
this.updateAttrFilter()
})
.catch((e) => {})
}
} else {
}
},
updateAttrFilter() {
this.$nextTick(() => {
const $table = this.$refs.xTable
if ($table) {
const attrColumn = $table.getColumnByField('attr_alias')
if (attrColumn) {
$table.setFilter(
attrColumn,
this.attrList().map((attr) => {
return { value: attr.alias || attr.name, label: attr.alias || attr.name }
})
)
}
}
})
},
mergeRowMethod({ row, _rowIndex, column, visibleData }) {
const fields = [
'ticket.ticket_id',
'ticket.created_at',
'ticket.creator_name',
'ticket.title',
'ticket.node_finish_time',
'ticket.node_name',
]
const cellValue1 = row.ticket.ticket_id
const cellValue2 = row.ticket.node_name
if (cellValue1 && cellValue2 && fields.includes(column.property)) {
const prevRow = visibleData[_rowIndex - 1]
let nextRow = visibleData[_rowIndex + 1]
if (prevRow && prevRow.ticket.ticket_id === cellValue1 && prevRow.ticket.node_name === cellValue2) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow.ticket.ticket_id === cellValue1 && nextRow.ticket.node_name === cellValue2) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
},
pageOrSizeChange(currentPage, pageSize) {
this.updateTableData(currentPage, pageSize)
},
filterOperateMethod({ value, row, column }) {
return Number(row.operate_type) === Number(value)
},
filterAttrMethod({ value, row, column }) {
return row.attr_alias === value
},
},
}
</script>
<style></style>

View File

@@ -20,7 +20,7 @@
:key="attr.name"
v-for="attr in group.attributes"
>
<CiDetailAttrContent :ci="ci" :attr="attr" @refresh="refresh" @updateCIByself="updateCIByself" />
<ci-detail-attr-content :ci="ci" :attr="attr" @refresh="refresh" @updateCIByself="updateCIByself" />
</el-descriptions-item>
</el-descriptions>
</div>
@@ -28,22 +28,39 @@
<a-tab-pane key="tab_2">
<span slot="tab"><a-icon type="branches" />{{ $t('cmdb.relation') }}</span>
<div :style="{ height: '100%', padding: '24px', overflow: 'auto' }">
<CiDetailRelation ref="ciDetailRelation" :ciId="ciId" :typeId="typeId" :ci="ci" />
<ci-detail-relation ref="ciDetailRelation" :ciId="ciId" :typeId="typeId" :ci="ci" />
</div>
</a-tab-pane>
<a-tab-pane key="tab_3">
<span slot="tab"><a-icon type="clock-circle" />{{ $t('cmdb.ci.history') }}</span>
<div :style="{ padding: '24px', height: '100%' }">
<a-space :style="{ 'margin-bottom': '10px', display: 'flex' }">
<a-button type="primary" class="ops-button-ghost" ghost @click="handleRollbackCI()">
<ops-icon type="shishizhuangtai" />{{ $t('cmdb.ci.rollback') }}
</a-button>
</a-space>
<ci-rollback-form ref="ciRollbackForm" :ciIds="[ciId]" @getCIHistory="getCIHistory" />
<vxe-table
ref="xTable"
show-overflow
show-header-overflow
:data="ciHistory"
size="small"
height="auto"
:height="tableHeight"
highlight-hover-row
:span-method="mergeRowMethod"
:scroll-y="{ enabled: false, gt: 20 }"
:scroll-x="{ enabled: false, gt: 0 }"
border
:scroll-y="{ enabled: false }"
class="ops-stripe-table"
resizable
class="ops-unstripe-table"
>
<template #empty>
<a-empty :image-style="{ height: '100px' }" :style="{ paddingTop: '10%' }">
<img slot="image" :src="require('@/assets/data_empty.png')" />
<span slot="description"> {{ $t('noData') }} </span>
</a-empty>
</template>
<vxe-table-column sortable field="created_at" :title="$t('created_at')"></vxe-table-column>
<vxe-table-column
field="username"
@@ -56,7 +73,7 @@
:filters="[
{ value: 0, label: $t('new') },
{ value: 1, label: $t('delete') },
{ value: 3, label: $t('update') },
{ value: 2, label: $t('update') },
]"
:filter-method="filterOperateMethod"
:title="$t('operation')"
@@ -71,8 +88,18 @@
:filters="[]"
:filter-method="filterAttrMethod"
></vxe-table-column>
<vxe-table-column field="old" :title="$t('cmdb.history.old')"></vxe-table-column>
<vxe-table-column field="new" :title="$t('cmdb.history.new')"></vxe-table-column>
<vxe-table-column field="old" :title="$t('cmdb.history.old')">
<template #default="{ row }">
<span v-if="row.value_type === '6'">{{ JSON.parse(row.old) }}</span>
<span v-else>{{ row.old }}</span>
</template>
</vxe-table-column>
<vxe-table-column field="new" :title="$t('cmdb.history.new')">
<template #default="{ row }">
<span v-if="row.value_type === '6'">{{ JSON.parse(row.new) }}</span>
<span v-else>{{ row.new }}</span>
</template>
</vxe-table-column>
</vxe-table>
</div>
</a-tab-pane>
@@ -82,6 +109,12 @@
<TriggerTable :ci_id="ci._id" />
</div>
</a-tab-pane>
<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" />
</div>
</a-tab-pane>
</a-tabs>
<a-empty
v-else
@@ -100,11 +133,15 @@
import _ from 'lodash'
import { Descriptions, DescriptionsItem } from 'element-ui'
import { getCITypeGroupById, getCITypes } from '@/modules/cmdb/api/CIType'
import { getCIHistory } from '@/modules/cmdb/api/history'
import { getCIHistory, judgeItsmInstalled } from '@/modules/cmdb/api/history'
import { getCIById } from '@/modules/cmdb/api/ci'
import CiDetailAttrContent from './ciDetailAttrContent.vue'
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: {
@@ -113,6 +150,8 @@ export default {
CiDetailAttrContent,
CiDetailRelation,
TriggerTable,
RelatedItsmTable,
CiRollbackForm,
},
props: {
typeId: {
@@ -123,10 +162,15 @@ export default {
type: Array,
default: () => [],
},
attributeHistoryTableHeight: {
type: Number,
default: null
}
},
data() {
return {
ci: {},
item: [],
attributeGroups: [],
activeTabKey: 'tab_1',
rowSpanMap: {},
@@ -134,6 +178,8 @@ export default {
ciId: null,
ci_types: [],
hasPermission: true,
itsmInstalled: true,
tableHeight: this.attributeHistoryTableHeight || (this.$store.state.windowHeight - 120),
}
},
computed: {
@@ -179,6 +225,7 @@ export default {
}
this.ciId = ciId
await this.getCI()
await this.judgeItsmInstalled()
if (this.hasPermission) {
this.getAttributes()
this.getCIHistory()
@@ -203,7 +250,16 @@ export default {
this.hasPermission = false
}
})
.catch((e) => {})
.catch((e) => {
if (e.response.status === 404) {
this.itsmInstalled = false
}
})
},
async judgeItsmInstalled() {
await judgeItsmInstalled().catch((e) => {
this.itsmInstalled = false
})
},
getCIHistory() {
@@ -343,6 +399,11 @@ export default {
this.$message.error(this.$t('cmdb.ci.copyFailed'))
})
},
handleRollbackCI() {
this.$nextTick(() => {
this.$refs.ciRollbackForm.onOpen()
})
},
},
}
</script>

View File

@@ -0,0 +1,165 @@
<template>
<CustomDrawer
:closable="true"
:title="drawerTitle"
:visible="drawerVisible"
@close="onClose"
placement="right"
width="800"
:bodyStyle="{ paddingTop: 0 }"
>
<div class="custom-drawer-bottom-action">
<a-button @click="onClose">{{ $t('cancel') }}</a-button>
<a-button type="primary" @click="handleSubmit" :loading="loading" :disabled="!hasDiff">{{
$t('submit')
}}</a-button>
</div>
<a-form :form="form" :style="{ paddingTop: '20px' }">
<a-form-item :label="$t('cmdb.ci.rollbackTo')" required :help="$t('cmdb.ci.baselineTips')">
<a-date-picker
:style="{ width: '278px' }"
format="YYYY-MM-DD HH:mm:ss"
valueFormat="YYYY-MM-DD HH:mm:ss"
@ok="getBaselineDiff"
:show-time="{ format: 'HH:mm:ss' }"
:placeholder="$t('cmdb.ci.rollbackToTips')"
v-decorator="['before_date', { rules: [{ required: true, message: $t('cmdb.ci.rollbackToTips') }] }]"
/>
</a-form-item>
<span :style="{ fontWeight: 'bold' }">{{ $t('cmdb.ci.baselineDiff') }}</span>
<vxe-table
ref="xTable"
show-overflow
show-header-overflow
resizable
border
size="small"
:span-method="mergeRowMethod"
:data="tableData"
:scroll-y="{ enabled: false, gt: 20 }"
:scroll-x="{ enabled: false, gt: 0 }"
class="ops-unstripe-table"
>
<template #empty>
<a-empty :image-style="{ height: '100px' }" :style="{ paddingTop: '10%' }">
<img slot="image" :src="require('@/assets/data_empty.png')" />
<span slot="description"> {{ dataLoad }} </span>
</a-empty>
</template>
<vxe-column field="instance" min-width="80" :title="$t('cmdb.ci.instance')"> </vxe-column>
<vxe-column field="attr_name" min-width="80" :title="$t('cmdb.attribute')"> </vxe-column>
<vxe-column field="cur" min-width="80" :title="$t('cmdb.ci.rollbackBefore')">
<template #default="{ row }">
<span v-if="row.value_type === '6'">{{ JSON.stringify(row.cur) }}</span>
<span v-else>{{ row.cur }}</span>
</template>
</vxe-column>
<vxe-column field="to" min-width="80" :title="$t('cmdb.ci.rollbackAfter')">
<template #default="{ row }">
<span v-if="row.value_type === '6'">{{ JSON.stringify(row.to) }}</span>
<span v-else>{{ row.to }}</span>
</template>
</vxe-column>
</vxe-table>
</a-form>
</CustomDrawer>
</template>
<script>
import { getCIsBaseline, CIBaselineRollback } from '../../../api/history'
export default {
name: 'CiRollbackForm',
props: {
ciIds: {
type: Array,
default: () => [],
},
},
data() {
return {
form: this.$form.createForm(this),
drawerTitle: this.$t('cmdb.ci.rollbackHeader'),
drawerVisible: false,
formLayout: 'horizontal',
tableData: [],
dataLoad: this.$t('noData'),
loading: false,
hasDiff: false,
batched: false,
}
},
methods: {
onClose() {
this.drawerVisible = false
this.form.resetFields()
this.tableData = []
this.dataLoad = this.$t('noData')
},
onOpen(batched = false) {
this.drawerTitle = this.$t('cmdb.ci.rollbackHeader')
this.drawerVisible = true
this.batched = batched
},
handleSubmit() {
this.form.validateFields((err, values) => {
if (!err) {
const that = this
this.$confirm({
title: that.$t('warning'),
content: that.$t('cmdb.ci.rollbackConfirm'),
onOk() {
if (that.batched) {
that.$emit('batchRollbackAsync', values)
} else {
that.rollbackCI(values)
}
},
})
}
})
},
rollbackCI(params) {
CIBaselineRollback(this.ciIds[0], params).then((res) => {
this.$message.success(this.$t('cmdb.ci.rollbackSuccess'))
this.form.resetFields()
this.$emit('getCIHistory')
})
},
getBaselineDiff(value) {
this.dataLoad = 'loading...'
this.loading = true
this.hasDiff = false
getCIsBaseline({ ci_ids: this.ciIds.join(','), before_date: value }).then((res) => {
this.tableData = res
this.loading = false
if (!res.length) {
this.dataLoad = this.$t('cmdb.ci.noDiff', { baseline: value })
} else {
this.hasDiff = true
}
})
},
mergeRowMethod({ row, _rowIndex, column, visibleData }) {
const fields = ['instance']
const cellValue1 = row.instance
if (cellValue1 && fields.includes(column.property)) {
const prevRow = visibleData[_rowIndex - 1]
let nextRow = visibleData[_rowIndex + 1]
if (prevRow && prevRow.instance === cellValue1) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow.instance === cellValue1) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
},
},
}
</script>

View File

@@ -50,7 +50,6 @@
type="primary"
size="small"
icon="plus"
class="ops-button-primary"
>
{{ $t('add') }}
</a-button>
@@ -214,7 +213,7 @@ export default {
line-height: 32px;
padding-left: 10px;
margin-bottom: 20px;
border-left: 4px solid #custom_colors[color_1];
border-left: 4px solid @primary-color;
font-size: 16px;
color: rgba(0, 0, 0, 0.75);
}

View File

@@ -171,7 +171,7 @@ export default {
margin-right: 60px;
.ant-input-group.ant-input-group-compact > *:first-child,
.ant-input-group.ant-input-group-compact > .ant-select:first-child > .ant-select-selection {
background-color: #custom_colors[color_1];
background-color: @primary-color;
color: #fff;
border: none;
}

View File

@@ -223,10 +223,10 @@
<a-form-item :label="$t('cmdb.ciType.isInherit')">
<a-radio-group v-model="isInherit">
<a-radio :value="true">
{{ $t('yes') }}
</a-radio>
<a-radio :value="false">
{{ $t('no') }}
</a-radio>
</a-radio-group>
</a-form-item>

View File

@@ -6,7 +6,6 @@
@click="handleCreate"
type="primary"
size="small"
class="ops-button-primary"
icon="plus"
>{{ $t('cmdb.ciType.addRelation') }}</a-button
>
@@ -18,7 +17,6 @@
show-header-overflow
highlight-hover-row
keep-source
:height="windowHeight - 190"
class="ops-stripe-table"
:row-class-name="rowClass"
:edit-config="{ trigger: 'dblclick', mode: 'cell', showIcon: false }"
@@ -38,9 +36,9 @@
<template #default="{row}">
<span v-if="row.isParent && constraintMap[row.constraint]">{{
constraintMap[row.constraint]
.split('')
.split(' ')
.reverse()
.join('')
.join(' ')
}}</span>
<span v-else>{{ constraintMap[row.constraint] }}</span>
</template>

View File

@@ -157,7 +157,6 @@
"
type="primary"
size="small"
class="ops-button-primary"
>{{ `${showCustomEmail ? $t('delete') : $t('add')}` }}{{ $t('cmdb.ciType.customEmail') }}</a-button
>
</div>

View File

@@ -5,7 +5,6 @@
type="primary"
@click="handleAddTrigger"
size="small"
class="ops-button-primary"
icon="plus"
>{{ $t('cmdb.ciType.newTrigger') }}</a-button
>

View File

@@ -309,7 +309,4 @@ export default {
margin-right: 5px;
}
}
.ops-button-primary:hover {
background-color: #2f54eb !important;
}
</style>

View File

@@ -153,7 +153,7 @@ export default {
line-height: 32px;
padding-left: 10px;
margin-bottom: 20px;
border-left: 4px solid #custom_colors[color_1];
border-left: 4px solid @primary-color;
justify-content: space-between;
> div {
font-weight: bold;

View File

@@ -5,10 +5,16 @@
show-header-overflow
stripe
size="small"
class="ops-stripe-table"
class="ops-unstripe-table"
:data="tableData"
v-bind="ci_id ? { height: 'auto' } : { height: `${windowHeight - 225}px` }"
>
<template #empty>
<a-empty :image-style="{ height: '100px' }" :style="{ paddingTop: '10%' }">
<img slot="image" :src="require('@/assets/data_empty.png')" />
<span slot="description"> {{ $t('noData') }} </span>
</a-empty>
</template>
<vxe-column field="trigger_name" :title="$t('cmdb.history.triggerName')"> </vxe-column>
<vxe-column field="type" :title="$t('type')">
<template #default="{ row }">

View File

@@ -494,7 +494,7 @@ export default {
.cmdb-preference-group-content-action {
margin-left: auto;
font-size: 12px;
color: #custom_colors[color_1];
color: @primary-color;
cursor: pointer;
display: none;
}
@@ -592,7 +592,7 @@ export default {
.cmdb-preference-footor-unsubscribed {
text-align: center;
> span {
color: #custom_colors[color_1];
color: @primary-color;
cursor: pointer;
font-size: 12px;
}
@@ -605,7 +605,7 @@ export default {
color: rgba(0, 0, 0, 0.45);
}
> span:nth-child(2) {
color: #custom_colors[color_1];
color: @primary-color;
cursor: pointer;
}
}

View File

@@ -9,7 +9,7 @@
:triggerLength="18"
>
<template #one>
<div class="relation-views-left" :style="{ height: `${windowHeight - 115}px` }">
<div class="relation-views-left" :style="{ height: `${windowHeight - 64}px` }">
<div class="relation-views-left-header" :title="$route.meta.name">{{ $route.meta.name }}</div>
<a-input
:placeholder="$t('cmdb.serviceTree.searchTips')"
@@ -607,7 +607,7 @@ export default {
this.reload()
},
pageNo: function(newPage, oldPage) {
this.loadData({ params: { pageNo: newPage }, refreshType: undefined, sortByTable: this.sortByTable })
this.loadData({ parameter: { pageNo: newPage }, refreshType: undefined, sortByTable: this.sortByTable })
},
},
@@ -1193,7 +1193,7 @@ export default {
that.$refs.xTable.clearCheckboxRow()
that.$refs.xTable.clearCheckboxReserve()
that.selectedRowKeys = []
that.loadData({ params: {}, refreshType: 'refreshNumber' })
that.loadData({ parameter: {}, refreshType: 'refreshNumber' })
})
},
})
@@ -1241,7 +1241,7 @@ export default {
this.sortByTable = sortByTable
this.$nextTick(() => {
if (this.pageNo === 1) {
this.loadData({ params: {}, refreshType: undefined, sortByTable })
this.loadData({ parameter: {}, refreshType: undefined, sortByTable })
} else {
this.pageNo = 1
}
@@ -1400,7 +1400,7 @@ export default {
onOk() {
deleteCI(record.ci_id || record._id).then((res) => {
that.$message.success(that.$t('deleteSuccess'))
that.loadData({ params: {}, refreshType: 'refreshNumber' })
that.loadData({ parameter: {}, refreshType: 'refreshNumber' })
})
},
})
@@ -1420,7 +1420,7 @@ export default {
}
addCIRelationView(first_ci_id, ci_id, { ancestor_ids }).then((res) => {
setTimeout(() => {
this.loadData({ params: {}, refreshType: 'refreshNumber' })
this.loadData({ parameter: {}, refreshType: 'refreshNumber' })
}, 500)
})
},
@@ -1458,7 +1458,7 @@ export default {
.finally(() => {
that.loading = false
setTimeout(() => {
that.loadData({ params: {} })
that.loadData({ parameter: {} })
}, 800)
})
},
@@ -1521,7 +1521,7 @@ export default {
that.selectedRowKeys = []
that.$refs.xTable.clearCheckboxRow()
that.$refs.xTable.clearCheckboxReserve()
that.loadData({ params: {}, refreshType: 'refreshNumber' })
that.loadData({ parameter: {}, refreshType: 'refreshNumber' })
})
},
})
@@ -1539,7 +1539,7 @@ export default {
this.$refs.xTable.refreshColumn()
},
relationViewRefreshNumber() {
this.loadData({ params: {}, refreshType: 'refreshNumber' })
this.loadData({ parameter: {}, refreshType: 'refreshNumber' })
},
onShowSizeChange(current, pageSize) {
this.pageSize = pageSize
@@ -1762,12 +1762,29 @@ export default {
}
if (node.children) {
node.children = node.children.filter((child) => {
if (predicateCiIds.some((id) => child.key.includes(String(id)))) {
if (
predicateCiIds.some(
(id) =>
child.key
.split('@^@')
.map((item) => Number(item.split('%')[0]))
.indexOf(id) > -1
)
) {
return true
}
return filterTree(child, predicate)
})
if (node.children.length && !predicateCiIds.some((id) => node.key.includes(String(id)))) {
if (
node.children.length &&
!predicateCiIds.some(
(id) =>
node.key
.split('@^@')
.map((item) => Number(item.split('%')[0]))
.indexOf(id) > -1
)
) {
_expandedKeys.push(node.key)
}
return node.children.length > 0

View File

@@ -57,47 +57,37 @@ export const generatorDynamicRouter = async () => {
{
path: '/setting/companyinfo',
name: 'company_info',
meta: { title: 'cs.menu.companyInfo', appName: 'backend', icon: 'ops-setting-companyInfo', selectedIcon: 'ops-setting-companyInfo-selected', permission: ['公司信息', 'backend_admin'] },
meta: { title: 'cs.menu.companyInfo', appName: 'backend', icon: 'ops-setting-companyInfo', selectedIcon: 'ops-setting-companyInfo', permission: ['公司信息', 'backend_admin'] },
component: () => import(/* webpackChunkName: "setting" */ '@/views/setting/companyInfo/index')
},
{
path: '/setting/companystructure',
name: 'company_structure',
meta: { title: 'cs.menu.companyStructure', appName: 'backend', icon: 'ops-setting-companyStructure', selectedIcon: 'ops-setting-companyStructure-selected', permission: ['公司架构', 'backend_admin'] },
meta: { title: 'cs.menu.companyStructure', appName: 'backend', icon: 'ops-setting-companyStructure', selectedIcon: 'ops-setting-companyStructure', permission: ['公司架构', 'backend_admin'] },
component: () => import(/* webpackChunkName: "setting" */ '@/views/setting/companyStructure/index')
},
{
path: '/setting/notice',
name: 'notice',
component: RouteView,
meta: { title: 'cs.menu.notice', appName: 'backend', icon: 'ops-setting-notice', selectedIcon: 'ops-setting-notice-selected', permission: ['通知设置', 'backend_admin'] },
meta: { title: 'cs.menu.notice', appName: 'backend', icon: 'ops-setting-notice', selectedIcon: 'ops-setting-notice', permission: ['通知设置', 'backend_admin'] },
redirect: '/setting/notice/email',
children: [{
path: '/setting/notice/basic',
name: 'notice_basic',
meta: { title: 'cs.menu.basic', icon: 'ops-setting-basic', selectedIcon: 'ops-setting-basic-selected' },
component: () => import(/* webpackChunkName: "setting" */ '@/views/setting/notice/basic')
}, {
path: '/setting/notice/email',
name: 'notice_email',
meta: { title: 'cs.menu.email', icon: 'ops-setting-notice-email', selectedIcon: 'ops-setting-notice-email-selected' },
component: () => import(/* webpackChunkName: "setting" */ '@/views/setting/notice/email/index')
}, {
path: '/setting/notice/wx',
name: 'notice_wx',
meta: { title: 'cs.menu.wx', icon: 'ops-setting-notice-wx', selectedIcon: 'ops-setting-notice-wx-selected' },
component: () => import(/* webpackChunkName: "setting" */ '@/views/setting/notice/wx')
}, {
path: '/setting/notice/dingding',
name: 'notice_dingding',
meta: { title: 'cs.menu.dingding', icon: 'ops-setting-notice-dingding', selectedIcon: 'ops-setting-notice-dingding-selected' },
component: () => import(/* webpackChunkName: "setting" */ '@/views/setting/notice/dingding')
}, {
path: '/setting/notice/feishu',
name: 'notice_feishu',
meta: { title: 'cs.menu.feishu', icon: 'ops-setting-notice-feishu', selectedIcon: 'ops-setting-notice-feishu-selected' },
component: () => import(/* webpackChunkName: "setting" */ '@/views/setting/notice/feishu')
}]
},
{
path: '/setting/auth',
name: 'company_auth',
meta: { title: 'cs.menu.auth', appName: 'backend', icon: 'ops-setting-auth', selectedIcon: 'ops-setting-auth-selected', permission: ['acl_admin'] },
meta: { title: 'cs.menu.auth', appName: 'backend', icon: 'ops-setting-auth', selectedIcon: 'ops-setting-auth', permission: ['acl_admin'] },
component: () => import(/* webpackChunkName: "setting" */ '@/views/setting/auth/index')
},
]

View File

@@ -1152,13 +1152,6 @@ body {
}
}
// button
.ops-button-primary {
background-color: @primary-color_4;
border-color: @primary-color_4;
color: @primary-color;
box-shadow: none;
}
// button
.ops-button-ghost.ant-btn-background-ghost.ant-btn-primary {
background-color: @primary-color_5!important;

View File

@@ -46,11 +46,6 @@
@layout-sidebar-selected-font-color: @primary-color;
@layout-sidebar-disabled-font-color: @text-color_4;
#custom_colors() {
color_1: #2f54eb; //primary color
color_2: #f0f5ff; //light background color
color_3: #d2e2ff;
}
.ops_display_wrapper(@backgroundColor:@primary-color_5) {
cursor: pointer;

View File

@@ -309,10 +309,10 @@ export default {
.notice-center-left:hover,
.notice-center-left-select {
background-color: #f0f5ff;
border-color: #custom_colors[color_1];
border-color: @primary-color;
> span:nth-child(2) {
background-color: #fff;
color: #custom_colors[color_1];
color: @primary-color;
}
}
.notice-center-header {
@@ -329,7 +329,7 @@ export default {
}
> .notice-center-header-app:hover,
.notice-center-header-app-selected {
background-color: #custom_colors[color_1];
background-color: @primary-color;
color: #fff;
}
.notice-center-categories {
@@ -342,7 +342,7 @@ export default {
> span:hover,
.notice-center-categories-selected {
color: #fff;
background-color: #custom_colors[color_1];
background-color: @primary-color;
}
}
}

View File

@@ -1,37 +1,34 @@
<template>
<a-tabs type="card" class="ops-tab" v-model="activeKey" @change="changeActiveKey">
<a-tab-pane v-for="item in authList" :key="item.value">
<span slot="tab">
{{ item.label }}
<a-icon
v-if="enable_list.find((en) => en.auth_type === item.value)"
type="check-circle"
theme="filled"
style="color:#2f54eb"
/>
</span>
<div class="setting-auth">
<components :ref="item.value" :is="item.value === 'OIDC' ? 'OAUTH2' : item.value" :data_type="item.value" />
<a-row>
<a-col :offset="item.value === 'AuthCommonConfig' ? 5 : 3">
<a-space>
<a-button :loading="loading" type="primary" @click="handleSave">{{ $t('save') }}</a-button>
<template v-if="item.value === 'LDAP'">
<a-button :loading="loading" ghost type="primary" @click="handleTest('connect')">{{
$t('cs.auth.testConnect')
}}</a-button>
<a-button :loading="loading" ghost type="primary" @click="handleTest('login')">{{
$t('cs.auth.testLogin')
}}</a-button>
</template>
<a-button :loading="loading" @click="handleReset">{{ $t('reset') }}</a-button>
</a-space>
</a-col>
</a-row>
</div>
<LoginModal v-if="item.value === 'LDAP'" ref="loginModal" @handleOK="(values) => handleTest('login', values)" />
</a-tab-pane>
</a-tabs>
<div class="ops-setting-auth">
<a-tabs class="ops-tab" v-model="activeKey" @change="changeActiveKey">
<a-tab-pane v-for="item in authList" :key="item.value">
<span slot="tab">
{{ item.label }}
<a-icon v-if="enable_list.find((en) => en.auth_type === item.value)" type="check-circle" theme="filled" />
</span>
<div class="setting-auth">
<components :ref="item.value" :is="item.value === 'OIDC' ? 'OAUTH2' : item.value" :data_type="item.value" />
<a-row>
<a-col :offset="item.value === 'AuthCommonConfig' ? 5 : 3">
<a-space>
<a-button :loading="loading" type="primary" @click="handleSave">{{ $t('save') }}</a-button>
<template v-if="item.value === 'LDAP'">
<a-button :loading="loading" ghost type="primary" @click="handleTest('connect')">{{
$t('cs.auth.testConnect')
}}</a-button>
<a-button :loading="loading" ghost type="primary" @click="handleTest('login')">{{
$t('cs.auth.testLogin')
}}</a-button>
</template>
<a-button :loading="loading" @click="handleReset">{{ $t('reset') }}</a-button>
</a-space>
</a-col>
</a-row>
</div>
<LoginModal v-if="item.value === 'LDAP'" ref="loginModal" @handleOK="(values) => handleTest('login', values)" />
</a-tab-pane>
</a-tabs>
</div>
</template>
<script>
@@ -148,12 +145,20 @@ export default {
</script>
<style lang="less" scoped>
.ops-setting-auth {
padding: 20px;
padding-top: 0;
background-color: #fff;
border-radius: @border-radius-box;
overflow: auto;
margin-bottom: -24px;
height: calc(100vh - 64px);
}
.setting-auth {
background-color: #fff;
height: calc(100vh - 128px);
height: calc(100vh - 150px);
overflow: auto;
border-radius: 0 5px 5px 5px;
padding-top: 24px;
}
</style>
@@ -161,10 +166,10 @@ export default {
.setting-auth {
.jsoneditor-outer {
height: var(--custom-height) !important;
border: 1px solid #2f54eb;
border: 1px solid @primary-color;
}
div.jsoneditor-menu {
background-color: #2f54eb;
background-color: @primary-color;
}
.jsoneditor-modes {
display: none;

View File

@@ -1,5 +1,5 @@
<template>
<div class="ops-setting-companyinfo" :style="{ height: `${windowHeight - 64}px` }">
<div class="ops-setting-companyinfo">
<a-form-model ref="infoData" :model="infoData" :label-col="labelCol" :wrapper-col="wrapperCol" :rules="rule">
<SpanTitle>{{ $t('cs.companyInfo.spanCompany') }}</SpanTitle>
<a-form-model-item :label="$t('cs.companyInfo.name')" prop="name">
@@ -35,9 +35,6 @@
<a-input v-model="infoData.email" :disabled="!isEditable" />
</a-form-model-item>
<SpanTitle>{{ $t('cs.companyInfo.spanLogo') }}</SpanTitle>
<a-form-model-item :label="$t('cs.companyInfo.messenger')" prop="messenger">
<a-input v-model="infoData.messenger" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item :label="$t('cs.companyInfo.domainName')" prop="domainName">
<a-input v-model="infoData.domainName" :disabled="!isEditable" />
</a-form-model-item>
@@ -51,7 +48,6 @@
<script>
import { getCompanyInfo, postCompanyInfo, putCompanyInfo } from '@/api/company'
import { mapState } from 'vuex'
import SpanTitle from '../components/spanTitle.vue'
import { mixinPermissions } from '@/utils/mixin'
@@ -74,7 +70,6 @@ export default {
phone: '',
faxCode: '',
email: '',
messenger: '',
domainName: '',
},
getId: -1,
@@ -90,9 +85,6 @@ export default {
}
},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
}),
isEditable() {
return this.hasDetailPermission('backend', '公司信息', ['update'])
},
@@ -157,7 +149,6 @@ export default {
phone: '',
faxCode: '',
email: '',
messenger: '',
domainName: '',
}
},
@@ -167,56 +158,15 @@ export default {
<style lang="less">
.ops-setting-companyinfo {
padding-top: 15px;
padding: 20px;
background-color: #fff;
border-radius: @border-radius-box;
overflow: auto;
margin-bottom: -24px;
.ops-setting-companyinfo-upload-show {
position: relative;
width: 290px;
height: 100px;
max-height: 100px;
img {
width: 100%;
height: 100%;
}
height: calc(100vh - 64px);
.delete-icon {
display: none;
}
}
.ant-upload:hover .delete-icon {
display: block;
position: absolute;
top: 5px;
right: 5px;
color: rgb(247, 85, 85);
}
.ant-form-item {
margin-bottom: 10px;
}
.ant-form-item label {
padding-right: 10px;
}
.avatar-uploader > .ant-upload {
// max-width: 100px;
max-height: 100px;
}
// .ant-upload.ant-upload-select-picture-card {
// width: 100%;
// > .ant-upload {
// padding: 0px;
.ant-upload-picture-card-wrapper {
height: 100px;
.ant-upload.ant-upload-select-picture-card {
width: 100%;
height: 100%;
margin: 0;
> .ant-upload {
padding: 0px;
}
}
}
}
</style>

View File

@@ -173,9 +173,9 @@ li {
color: rgba(0, 0, 0, 0.7);
font-size: 14px;
.ops-setting-companystructure-sidebar-group-tree-info:hover {
color: #custom_colors[color_1];
color: @primary-color;
> .ops-setting-companystructure-sidebar-group-tree-info::before {
background-color: #custom_colors[color_1];
background-color: @primary-color;
}
}
// .ops-setting-companystructure-sidebar-group-tree-info:first-child::before {
@@ -313,7 +313,7 @@ li {
}
}
> .ops-setting-companystructure-sidebar-group-tree-info::before {
background-color: #custom_colors[color_1];
background-color: @primary-color;
}
}
}

View File

@@ -6,7 +6,6 @@
:max="500"
:paneLengthPixel.sync="paneLengthPixel"
appName="setting-structure"
triggerColor="#F0F5FF"
:triggerLength="18"
>
<template #one>
@@ -396,11 +395,6 @@ export default {
title: this.$t('cs.companyStructure.employee'),
icon: 'user',
},
{
id: 1,
title: this.$t('cs.companyStructure.departmentName'),
icon: 'apartment',
},
]
}
},
@@ -884,7 +878,7 @@ export default {
&:hover {
background-color: #e1efff;
.ops-setting-structure-sidebar-group-header-title {
color: #custom_colors[color_1];
color: @primary-color;
}
}
.ops-setting-structure-sidebar-group-header-avatar {
@@ -925,9 +919,9 @@ export default {
font-size: 14px;
width: 100%;
&:hover {
color: #custom_colors[color_1];
color: @primary-color;
&::before {
background-color: #custom_colors[color_1];
background-color: @primary-color;
}
}
&::before {
@@ -1004,19 +998,19 @@ export default {
}
}
.item-selected {
color: #custom_colors[color_1];
color: @primary-color;
&::before {
background-color: #custom_colors[color_1];
background-color: @primary-color;
}
}
}
.group-selected {
background-color: #e1efff;
.ops-setting-structure-sidebar-group-header-avatar {
background-color: #custom_colors[color_1];
background-color: @primary-color;
}
.ops-setting-structure-sidebar-group-header-title {
color: #custom_colors[color_1];
color: @primary-color;
}
}
}
@@ -1042,7 +1036,7 @@ export default {
display: inline-block;
.ops_display_wrapper(#fff);
.screening-box-scene-icon {
color: #custom_colors[color_1];
color: @primary-color;
font-size: 12px;
}
.history-scene-item {
@@ -1054,7 +1048,7 @@ export default {
.search-form-bar-filter {
.ops_display_wrapper();
.search-form-bar-filter-icon {
color: #custom_colors[color_1];
color: @primary-color;
font-size: 12px;
}

View File

@@ -68,7 +68,7 @@ name: 'SearchForm',
background-color: rgb(240, 245, 255);
.ops_display_wrapper();
.search-form-bar-filter-icon {
color: #custom_colors[color_1];
color: @primary-color;
font-size: 12px;
}
.search-form-bar-filter-icon_selected{

View File

@@ -23,10 +23,9 @@ export default {
height: 28px;
margin-bottom: 12px;
line-height: 28px;
padding-left: 24px;
border-radius: 0px 20px 20px 0px;
padding-left: 12px;
font-weight: 700;
color: #0637bf;
background-color: #e0e9ff;
color: #000;
border-left: 3px solid @primary-color;
}
</style>

View File

@@ -1,20 +1,25 @@
const cs_en = {
menu: {
person: 'My Profile',
companyInfo: 'Company Info',
companyStructure: 'Company Structure',
notice: 'Notification Settings',
email: 'Email Settings',
wx: 'WeChat Work',
dingding: 'DingTalk',
feishu: 'Feishu',
auth: 'Auth Settings',
role: 'Role Management',
sys: 'System Role',
technician: 'Technician Role',
user: 'User Role',
group: 'User Group',
duty: 'Duty Management',
menu: {
person: 'My Profile',
companyInfo: 'Company Info',
companyStructure: 'Company Structure',
notice: 'Notification Settings',
email: 'Email Settings',
wx: 'WeChat Work',
dingding: 'DingTalk',
feishu: 'Feishu',
auth: 'Auth Settings',
authority: 'Authority Management',
sys: 'System Role',
technician: 'Technician Role',
user: 'User Role',
group: 'User Group',
duty: 'Duty Management',
role: 'Role Management',
app: 'APP Authority',
basic: 'Basic Settings',
theme: 'Theme Settings',
security: 'Security Settings'
},
companyInfo: {
spanCompany: 'Description',
@@ -37,11 +42,6 @@ const cs_en = {
emailValidate: 'Please enter a valid email address',
messenger: 'Messenger Address',
domainName: 'Deployment Domain',
logo: 'Company Logo',
upload: 'Upload',
editCompanyLogo: 'Edit Company Logo',
editCompanyLogoSmall: 'Edit Company Logo Thumbnail',
imageSizeLimit2MB: 'Image size does not exceed 2MB',
checkInputCorrect: 'Please check if the input content is correct',
},
companyStructure: {

View File

@@ -9,12 +9,17 @@ const cs_zh = {
dingding: '钉钉',
feishu: '飞书',
auth: '认证设置',
role: '角色管理',
authority: '权限管理',
sys: '系统角色',
technician: '技术员角色',
user: '用户角色',
group: '用户分组',
duty: '值班管理',
role: '角色管理',
app: '应用权限',
basic: '基础设置',
theme: '主题配置',
security: '安全配置'
},
companyInfo: {
spanCompany: '公司描述',
@@ -37,11 +42,6 @@ const cs_zh = {
emailValidate: '请输入正确的邮箱地址',
messenger: 'Messenger地址',
domainName: '部署域名',
logo: '公司logo',
upload: '上传',
editCompanyLogo: '编辑公司logo',
editCompanyLogoSmall: '编辑公司logo缩略图',
imageSizeLimit2MB: '图片大小不超过2MB',
checkInputCorrect: '请检查输入内容是否正确',
},
companyStructure: {

View File

@@ -0,0 +1,80 @@
<template>
<div class="ops-setting-notice-basic">
<a-form-model ref="infoData" :model="infoData" :label-col="labelCol" :wrapper-col="wrapperCol">
<a-form-model-item :label="$t('cs.companyInfo.messenger')" prop="messenger">
<a-input v-model="infoData.messenger" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item :wrapper-col="{ span: 14, offset: 3 }" v-if="isEditable">
<a-button type="primary" @click="onSubmit"> {{ $t('save') }}</a-button>
<a-button ghost type="primary" style="margin-left: 28px" @click="resetForm"> {{ $t('reset') }}</a-button>
</a-form-model-item>
</a-form-model>
</div>
</template>
<script>
import { getCompanyInfo, postCompanyInfo, putCompanyInfo } from '@/api/company'
import { mixinPermissions } from '@/utils/mixin'
export default {
name: 'CompanyInfo',
mixins: [mixinPermissions],
data() {
return {
labelCol: { span: 3 },
wrapperCol: { span: 10 },
infoData: {
messenger: '',
},
getId: -1,
}
},
async mounted() {
const res = await getCompanyInfo()
if (!res.id) {
this.getId = -1
} else {
this.infoData = res.info
this.getId = res.id
}
},
computed: {
isEditable() {
return this.hasDetailPermission('backend', '公司信息', ['update'])
},
},
methods: {
async onSubmit() {
this.$refs.infoData.validate(async (valid) => {
if (valid) {
if (this.getId === -1) {
await postCompanyInfo(this.infoData)
} else {
await putCompanyInfo(this.getId, this.infoData)
}
this.$message.success(this.$t('saveSuccess'))
} else {
this.$message.warning(this.$t('cs.companyInfo.checkInputCorrect'))
return false
}
})
},
resetForm() {
this.infoData = {
messenger: '',
}
},
},
}
</script>
<style lang="less">
.ops-setting-notice-basic {
padding-top: 20px;
background-color: #fff;
border-radius: @border-radius-box;
overflow: auto;
margin-bottom: -24px;
height: calc(100vh - 64px);
}
</style>

View File

@@ -1,106 +0,0 @@
<template>
<div>
<vxe-table
ref="xTable"
:data="tableData"
size="mini"
stripe
class="ops-stripe-table"
show-overflow
:edit-config="{ showIcon: false, trigger: 'manual', mode: 'row' }"
>
<vxe-column v-for="col in columns" :key="col.field" :field="col.field" :title="col.title" :edit-render="{}">
<template #header> <span v-if="col.required" :style="{ color: 'red' }">* </span>{{ col.title }} </template>
<template #edit="{ row }">
<vxe-input v-model="row[col.field]" type="text"></vxe-input>
</template>
</vxe-column>
<vxe-column :title="$t('operation')" width="80" v-if="!disabled">
<template #default="{ row }">
<template v-if="$refs.xTable.isActiveByRow(row)">
<a @click="saveRowEvent(row)"><a-icon type="save"/></a>
</template>
<a-space v-else>
<a @click="editRowEvent(row)"><ops-icon type="icon-xianxing-edit"/></a>
<a style="color:red" @click="deleteRowEvent(row)"><ops-icon type="icon-xianxing-delete"/></a>
</a-space>
</template>
</vxe-column>
</vxe-table>
<div :style="{ color: '#f5222d' }" v-if="errorFlag">{{ $t('cs.notice.robotConfigErrorTips') }}</div>
<a-button v-if="!disabled" icon="plus-circle" class="ops-button-primary" type="primary" @click="insertEvent">{{
$t('add')
}}</a-button>
</div>
</template>
<script>
export default {
name: 'Bot',
props: {
columns: {
type: Array,
default: () => [],
},
disabled: {
type: Boolean,
default: false,
},
},
data() {
return {
tableData: [],
errorFlag: false,
}
},
methods: {
async insertEvent() {
const $table = this.$refs.xTable
const record = {
name: '',
url: '',
}
const { row: newRow } = await $table.insertAt(record, -1)
await $table.setActiveRow(newRow)
},
saveRowEvent(row) {
const $table = this.$refs.xTable
$table.clearActived()
},
editRowEvent(row) {
const $table = this.$refs.xTable
$table.setActiveRow(row)
},
deleteRowEvent(row) {
const $table = this.$refs.xTable
$table.remove(row)
},
getData(callback) {
const $table = this.$refs.xTable
const { fullData: _tableData } = $table.getTableData()
const requiredObj = {}
this.columns.forEach((col) => {
if (col.required) {
requiredObj[col.field] = true
}
})
let flag = true
_tableData.forEach((td) => {
Object.keys(requiredObj).forEach((key) => {
if (requiredObj[key]) {
flag = !!(flag && td[`${key}`])
}
})
})
this.errorFlag = !flag
callback(flag, _tableData)
},
setData(value) {
this.tableData = value
this.errorFlag = false
},
},
}
</script>
<style></style>

View File

@@ -1,151 +0,0 @@
<template>
<div class="notice-dingding-wrapper" :style="{ height: `${windowHeight - 64}px` }">
<a-form-model ref="dingdingForm" :model="dingdingData" :label-col="labelCol" :wrapper-col="wrapperCol">
<SpanTitle>{{ $t('cs.duty.basicSetting') }}</SpanTitle>
<a-form-model-item :label="$t('cs.notice.appKey')">
<a-input v-model="dingdingData.appKey" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item :label="$t('cs.notice.appSecret')">
<a-input v-model="dingdingData.appSecret" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item :label="$t('cs.notice.robotCode')">
<a-input v-model="dingdingData.robotCode" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item :label="$t('cs.notice.robot')">
<Bot
ref="bot"
:disabled="!isEditable"
:columns="[
{
field: 'name',
title: $t('cs.notice.title'),
required: true,
},
{
field: 'url',
title: $t('cs.notice.webhookAddress'),
required: true,
},
{
field: 'token',
title: 'token',
required: false,
},
]"
/>
</a-form-model-item>
<!-- <a-form-model-item :label="测试邮件设置">
<a-button type="primary" ghost>测试回收箱</a-button>
<br />
<span
class="notice-dingding-wrapper-tips"
><ops-icon type="icon-shidi-quxiao" :style="{ color: '#D81E06' }" /> 邮件接收失败</span
>
<br />
<span>邮箱服务器未配置请配置一个邮箱服务器 | <a>故障诊断</a></span>
</a-form-model-item> -->
<a-row v-if="isEditable">
<a-col :span="16" :offset="3">
<a-form-model-item :label-col="labelCol" :wrapper-col="wrapperCol">
<a-button type="primary" @click="onSubmit"> {{ $t('save') }} </a-button>
<a-button ghost type="primary" style="margin-left: 28px;" @click="resetForm"> {{ $t('reset') }} </a-button>
</a-form-model-item>
</a-col>
</a-row>
</a-form-model>
</div>
</template>
<script>
import { mapState } from 'vuex'
import SpanTitle from '../components/spanTitle.vue'
import { getNoticeConfigByPlatform, postNoticeConfigByPlatform, putNoticeConfigByPlatform } from '@/api/noticeSetting'
import { mixinPermissions } from '@/utils/mixin'
import Bot from './bot.vue'
export default {
name: 'NoticeDingding',
components: { SpanTitle, Bot },
mixins: [mixinPermissions],
data() {
return {
labelCol: { lg: 3, md: 5, sm: 8 },
wrapperCol: { lg: 15, md: 19, sm: 16 },
id: null,
dingdingData: {
appKey: '',
appSecret: '',
robotCode: '',
},
}
},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
}),
isEditable() {
return this.hasDetailPermission('backend', '通知设置', ['update'])
},
},
mounted() {
this.getData()
},
methods: {
getData() {
getNoticeConfigByPlatform({ platform: 'dingdingApp' }).then((res) => {
this.id = res?.id ?? null
if (this.id) {
this.dingdingData = res.info
this.$refs.bot.setData(res?.info?.bot)
}
})
},
onSubmit() {
this.$refs.dingdingForm.validate(async (valid) => {
if (valid) {
this.$refs.bot.getData(async (flag, bot) => {
if (flag) {
if (this.id) {
await putNoticeConfigByPlatform(this.id, { info: { ...this.dingdingData, bot, label: this.$t('cs.person.dingdingApp') } })
} else {
await postNoticeConfigByPlatform({
platform: 'dingdingApp',
info: { ...this.dingdingData, bot, label: this.$t('cs.person.dingdingApp') },
})
}
this.$message.success(this.$t('saveSuccess'))
this.getData()
}
})
}
})
},
resetForm() {
this.dingdingData = {
appKey: '',
appSecret: '',
robotCode: '',
}
},
},
}
</script>
<style lang="less" scoped>
.notice-dingding-wrapper {
background-color: #fff;
padding-top: 15px;
overflow: auto;
margin-bottom: -24px;
border-radius: @border-radius-box;
.notice-dingding-wrapper-tips {
display: inline-block;
background-color: #ffdfdf;
border-radius: 4px;
padding: 0 12px;
width: 300px;
color: #000000;
margin-top: 8px;
}
}
</style>

View File

@@ -1,6 +1,5 @@
.notice-email-wrapper {
background-color: #fff;
padding-top: 24px;
overflow: auto;
.notice-email-error-tips {
display: inline-block;

View File

@@ -1,6 +1,6 @@
<template>
<div :style="{ marginBottom: '-24px' }">
<a-tabs :activeKey="activeKey" @change="changeTab" class="ops-tab" type="card">
<div class="ops-setting-notice-mail">
<a-tabs :activeKey="activeKey" @change="changeTab" class="ops-tab">
<!-- <a-tab-pane key="1" tab="接收服务器">
<Receive />
</a-tab-pane> -->
@@ -30,4 +30,14 @@ export default {
}
</script>
<style></style>
<style lang="less" scoped>
.ops-setting-notice-mail {
padding: 20px;
padding-top: 0;
background-color: #fff;
border-radius: @border-radius-box;
overflow: auto;
margin-bottom: -24px;
height: calc(100vh - 64px);
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<div class="notice-email-wrapper" :style="{ height: `${windowHeight - 104}px` }">
<div class="notice-email-wrapper">
<a-form-model ref="sendForm" :model="settingData" :label-col="labelCol" :rules="rules" :wrapper-col="wrapperCol">
<SpanTitle>{{ $t('cs.duty.basicSetting') }}</SpanTitle>
<a-form-model-item :label="$t('cs.notice.isEncrypted')">

View File

@@ -1,131 +0,0 @@
<template>
<div class="notice-feishu-wrapper" :style="{ height: `${windowHeight - 64}px` }">
<a-form-model ref="feishuForm" :model="feishuData" :label-col="labelCol" :wrapper-col="wrapperCol">
<SpanTitle>{{ $t('cs.duty.basicSetting') }}</SpanTitle>
<a-form-model-item :label="$t('cs.notice.appKey')">
<a-input v-model="feishuData.id" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item :label="$t('cs.notice.appSecret')">
<a-input v-model="feishuData.password" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item :label="$t('cs.notice.robot')">
<Bot
ref="bot"
:disabled="!isEditable"
:columns="[
{
field: 'name',
title: $t('cs.notice.title'),
required: true,
},
{
field: 'url',
title: $t('cs.notice.webhookAddress'),
required: true,
},
]"
/>
</a-form-model-item>
<a-row v-if="isEditable">
<a-col :span="16" :offset="3">
<a-form-model-item :label-col="labelCol" :wrapper-col="wrapperCol">
<a-button type="primary" @click="onSubmit"> {{ $t('save') }} </a-button>
<a-button ghost type="primary" style="margin-left: 28px;" @click="resetForm"> {{ $t('reset') }} </a-button>
</a-form-model-item>
</a-col>
</a-row>
</a-form-model>
</div>
</template>
<script>
import { mapState } from 'vuex'
import SpanTitle from '../components/spanTitle.vue'
import { getNoticeConfigByPlatform, postNoticeConfigByPlatform, putNoticeConfigByPlatform } from '@/api/noticeSetting'
import { mixinPermissions } from '@/utils/mixin'
import Bot from './bot.vue'
export default {
name: 'NoticeFeishu',
components: { SpanTitle, Bot },
mixins: [mixinPermissions],
data() {
return {
labelCol: { lg: 3, md: 5, sm: 8 },
wrapperCol: { lg: 15, md: 19, sm: 16 },
id: null,
feishuData: {
id: '',
password: '',
},
}
},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
}),
isEditable() {
return this.hasDetailPermission('backend', '通知设置', ['update'])
},
},
mounted() {
this.getData()
},
methods: {
getData() {
getNoticeConfigByPlatform({ platform: 'feishuApp' }).then((res) => {
this.id = res?.id ?? null
if (this.id) {
this.feishuData = res.info
this.$refs.bot.setData(res?.info?.bot)
}
})
},
onSubmit() {
this.$refs.feishuForm.validate(async (valid) => {
if (valid) {
this.$refs.bot.getData(async (flag, bot) => {
if (flag) {
if (this.id) {
await putNoticeConfigByPlatform(this.id, { info: { ...this.feishuData, bot, label: this.$t('cs.person.feishuApp') } })
} else {
await postNoticeConfigByPlatform({
platform: 'feishuApp',
info: { ...this.feishuData, bot, label: this.$t('cs.person.feishuApp') },
})
}
this.$message.success(this.$t('saveSuccess'))
this.getData()
}
})
}
})
},
resetForm() {
this.feishuData = {
id: '',
password: '',
}
},
},
}
</script>
<style lang="less" scoped>
.notice-feishu-wrapper {
background-color: #fff;
padding-top: 15px;
overflow: auto;
margin-bottom: -24px;
border-radius: @border-radius-box;
.notice-feishu-wrapper-tips {
display: inline-block;
background-color: #ffdfdf;
border-radius: 4px;
padding: 0 12px;
width: 300px;
color: #000000;
margin-top: 8px;
}
}
</style>

View File

@@ -1,152 +0,0 @@
<template>
<div class="notice-wx-wrapper" :style="{ height: `${windowHeight - 64}px` }">
<a-form-model ref="wxForm" :model="wxData" :label-col="labelCol" :wrapper-col="wrapperCol">
<SpanTitle>{{ $t('cs.duty.basicSetting') }}</SpanTitle>
<a-form-model-item :label="$t('cs.notice.corpid')">
<a-input v-model="wxData.corpid" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item :label="$t('cs.notice.agentid')">
<a-input v-model="wxData.agentid" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item :label="$t('cs.notice.corpsecret')">
<a-input-password v-model="wxData.corpsecret" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item label="ITSM AppId">
<a-input v-model="wxData.itsm_app_id" :disabled="!isEditable" />
</a-form-model-item>
<a-form-model-item :label="$t('cs.notice.robot')">
<Bot
ref="bot"
:disabled="!isEditable"
:columns="[
{
field: 'name',
title: $t('cs.notice.title'),
required: true,
},
{
field: 'url',
title: $t('cs.notice.webhookAddress'),
required: true,
},
]"
/>
</a-form-model-item>
<!-- <a-form-model-item :label="测试邮件设置">
<a-button type="primary" ghost>测试回收箱</a-button>
<br />
<span
class="notice-wx-wrapper-tips"
><ops-icon type="icon-shidi-quxiao" :style="{ color: '#D81E06' }" /> 邮件接收失败</span
>
<br />
<span>邮箱服务器未配置请配置一个邮箱服务器 | <a>故障诊断</a></span>
</a-form-model-item> -->
<a-row v-if="isEditable">
<a-col :span="16" :offset="3">
<a-form-model-item :label-col="labelCol" :wrapper-col="wrapperCol">
<a-button type="primary" @click="onSubmit"> {{ $t('save') }} </a-button>
<a-button ghost type="primary" style="margin-left: 28px;" @click="resetForm"> {{ $t('reset') }} </a-button>
</a-form-model-item>
</a-col>
</a-row>
</a-form-model>
</div>
</template>
<script>
import { mapState } from 'vuex'
import SpanTitle from '../components/spanTitle.vue'
import { getNoticeConfigByPlatform, postNoticeConfigByPlatform, putNoticeConfigByPlatform } from '@/api/noticeSetting'
import { mixinPermissions } from '@/utils/mixin'
import Bot from './bot.vue'
export default {
name: 'NoticeWx',
mixins: [mixinPermissions],
components: { SpanTitle, Bot },
data() {
return {
labelCol: { lg: 3, md: 5, sm: 8 },
wrapperCol: { lg: 15, md: 19, sm: 16 },
id: null,
wxData: {
corpid: '',
agentid: '',
corpsecret: '',
itsm_app_id: '',
},
}
},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
}),
isEditable() {
return this.hasDetailPermission('backend', '通知设置', ['update'])
},
},
mounted() {
this.getData()
},
methods: {
getData() {
getNoticeConfigByPlatform({ platform: 'wechatApp' }).then((res) => {
this.id = res?.id ?? null
if (this.id) {
this.wxData = res.info
this.$refs.bot.setData(res?.info?.bot)
}
})
},
onSubmit() {
this.$refs.wxForm.validate(async (valid) => {
if (valid) {
this.$refs.bot.getData(async (flag, bot) => {
if (flag) {
if (this.id) {
await putNoticeConfigByPlatform(this.id, {
info: { ...this.wxData, bot, label: this.$t('cs.person.wechatApp') },
})
} else {
await postNoticeConfigByPlatform({
platform: 'wechatApp',
info: { ...this.wxData, bot, label: this.$t('cs.person.wechatApp') },
})
}
this.$message.success(this.$t('saveSuccess'))
this.getData()
}
})
}
})
},
resetForm() {
this.wxData = {
corpid: '',
agentid: '',
corpsecret: '',
itsm_app_id: '',
}
},
},
}
</script>
<style lang="less" scoped>
.notice-wx-wrapper {
background-color: #fff;
padding-top: 15px;
overflow: auto;
margin-bottom: -24px;
border-radius: @border-radius-box;
.notice-wx-wrapper-tips {
display: inline-block;
background-color: #ffdfdf;
border-radius: 4px;
padding: 0 12px;
width: 300px;
color: #000000;
margin-top: 8px;
}
}
</style>

View File

@@ -372,7 +372,7 @@ export default {
margin-bottom: 5px;
&:hover {
.ops_popover_item_selected();
border-color: #custom_colors[color_1];
border-color: @primary-color;
}
> i {
margin-right: 10px;
@@ -380,7 +380,7 @@ export default {
}
.setting-person-left-item-selected {
.ops_popover_item_selected();
border-color: #custom_colors[color_1];
border-color: @primary-color;
}
}
.setting-person-right {
@@ -390,7 +390,7 @@ export default {
border-radius: @border-radius-box;
padding: 24px 48px;
.setting-person-right-disabled {
background-color: #custom_colors[color_2];
background-color: @primary-color_5;
border-radius: 4px;
height: 30px;
line-height: 30px;

View File

@@ -45,7 +45,7 @@ services:
- redis
cmdb-api:
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.4.3
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.4.4
# build:
# context: .
# target: cmdb-api
@@ -84,7 +84,7 @@ services:
- cmdb-api
cmdb-ui:
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.4.3
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.4.4
# build:
# context: .
# target: cmdb-ui

View File

@@ -342,8 +342,8 @@ key = "Your API key"
secret = "Your API secret"
def build_api_key(path, params):
values = "".join([str(params[k]) for k in sorted(params.keys())
if params[k] is not None and not k.startswith('_')]) if params.keys() else ""
values = "".join([str(params[k]) for k in sorted((params or {}).keys())
if k not in ("_key", "_secret") and not isinstance(params[k], (dict, list))])
_secret = "".join([path, secret, values]).encode("utf-8")
params["_secret"] = hashlib.sha1(_secret).hexdigest()
params["_key"] = key
@@ -365,8 +365,8 @@ SECRET = "Your API secret"
def build_api_key(path, params):
values = "".join([str(params[k]) for k in sorted(params.keys())
if params[k] is not None and not k.startswith('_')]) if params.keys() else ""
values = "".join([str(params[k]) for k in sorted((params or {}).keys())
if k not in ("_key", "_secret") and not isinstance(params[k], (dict, list))])
_secret = "".join([path, SECRET, values]).encode("utf-8")
params["_secret"] = hashlib.sha1(_secret).hexdigest()
params["_key"] = KEY
@@ -395,8 +395,8 @@ SECRET = "Your API secret"
def build_api_key(path, params):
values = "".join([str(params[k]) for k in sorted(params.keys())
if params[k] is not None and not k.startswith('_')]) if params.keys() else ""
values = "".join([str(params[k]) for k in sorted((params or {}).keys())
if k not in ("_key", "_secret") and not isinstance(params[k], (dict, list))])
_secret = "".join([path, SECRET, values]).encode("utf-8")
params["_secret"] = hashlib.sha1(_secret).hexdigest()
params["_key"] = KEY