Compare commits

...

36 Commits

Author SHA1 Message Date
pycook
07eb902583 feat(api): my preference support grouping 2024-05-18 22:54:07 +08:00
pycook
46b54bb7f2 fix(ui): some bugs (#512) 2024-05-17 12:07:56 +08:00
pycook
fe63310c4e Dev api 240517 (#511)
* fix(api): list values delete

* fix(acl): role rebuild cache
2024-05-17 11:20:53 +08:00
pycook
27c733aa2c docs: update sql 2024-05-16 20:59:30 +08:00
pycook
2a8e9e684e fix(ui): issue#490 2024-05-02 21:28:06 +08:00
pycook
095190a785 fix(api): unique constraint (#505) 2024-05-02 21:22:40 +08:00
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
pycook
761e98884b chore: add volumes cmdb_cache-data in docker-compose 2024-04-18 10:02:57 +08:00
pycook
073654624e fix(api): commands cmdb-init-cache 2024-04-17 21:37:18 +08:00
dagongren
df54244ff1 fix(cmdb-ui):service tree key (#480) 2024-04-17 20:42:16 +08:00
pycook
27e9919198 chore: release v2.4.3 2024-04-17 19:35:35 +08:00
dagongren
dc8b1a5de2 feat(cmdb-ui):citype show attr && service tree search (#479) 2024-04-17 17:59:21 +08:00
pycook
d8a7728f1d feat(api): custom attribute display (#478) 2024-04-17 17:50:46 +08:00
simontigers
82881965fb Merge pull request #474 from simontigers/common_check_new_columns
Common check new columns
2024-04-16 15:35:15 +08:00
hu.sima
1bb62022f1 fix(api): check new column support enum change 2024-04-16 15:34:03 +08:00
hu.sima
ed445a8d82 fix(api): secrets_shares Import ERROR 2024-04-16 15:33:36 +08:00
pycook
3626b1a97e feat(api): service tree search by keywords (#471) 2024-04-15 20:04:56 +08:00
loveiwei
32529fba9b fix: support sealing and unsealing secret in multiple process(more than one workers started by gunicorn) (#469)
* fix: 解决在麒麟系统上使用docker安装时使用celery -D启动 celery 可能出现的问题

* fix: 解决在麒麟系统上使用docker安装时使用celery -D启动 celery 可能出现的问题

* fix: NoneType happend while unsealing the secret funtion, cancel the address check while unseal and seal

* fix: unseal secret function

* fix: remove depens_on in docker-compose

* fix: support sealing and unsealing secret in multiple process(more than one workers started by gunicorn)
2024-04-15 18:08:47 +08:00
dagongren
a042b4fe39 fix(cmdb-ui):ci detail relation repeatly ciid (#468) 2024-04-15 13:50:50 +08:00
dagongren
a0631414dc style: global static.less (#467) 2024-04-12 15:18:52 +08:00
149 changed files with 2926 additions and 2976 deletions

View File

@@ -87,7 +87,7 @@ docker compose up -d
- 第一步: 先安装 Docker 环境, 以及Docker Compose (v2)
- 第二步: 直接使用项目根目录下的install.sh 文件进行 `安装`、`启动`、`暂停`、`查状态`、`删除`、`卸载`
```shell
curl -so install.sh https://raw.githubusercontent.com/veops/cmdb/master/install.sh
curl -so install.sh https://raw.githubusercontent.com/veops/cmdb/deploy_on_kylin_docker/install.sh
sh install.sh install
```

View File

@@ -55,9 +55,12 @@ def cmdb_init_cache():
for cr in ci_relations:
relations.setdefault(cr.first_ci_id, {}).update({cr.second_ci_id: cr.second_ci.type_id})
if cr.ancestor_ids:
relations2.setdefault(cr.ancestor_ids, {}).update({cr.second_ci_id: cr.second_ci.type_id})
relations2.setdefault('{},{}'.format(cr.ancestor_ids, cr.first_ci_id), {}).update(
{cr.second_ci_id: cr.second_ci.type_id})
for i in relations:
relations[i] = json.dumps(relations[i])
for i in relations2:
relations2[i] = json.dumps(relations2[i])
if relations:
rd.create_or_update(relations, REDIS_PREFIX_CI_RELATION)
if relations2:
@@ -357,13 +360,13 @@ def cmdb_inner_secrets_unseal(address):
"""
unseal the secrets feature
"""
if not valid_address(address):
return
# if not valid_address(address):
# return
address = "{}/api/v0.1/secrets/unseal".format(address.strip("/"))
for i in range(global_key_threshold):
token = click.prompt(f'Enter unseal token {i + 1}', hide_input=True, confirmation_prompt=False)
assert token is not None
resp = requests.post(address, headers={"Unseal-Token": token})
resp = requests.post(address, headers={"Unseal-Token": token}, timeout=5)
if resp.status_code == 200:
KeyManage.print_response(resp.json())
if resp.json().get("status") in ["success", "skip"]:

View File

@@ -6,6 +6,7 @@ from werkzeug.datastructures import MultiDict
from api.lib.common_setting.acl import ACLManager
from api.lib.common_setting.employee import EmployeeAddForm, GrantEmployeeACLPerm
from api.lib.common_setting.resp_format import ErrFormat
from api.lib.common_setting.utils import CheckNewColumn
from api.models.common_setting import Employee, Department
@@ -209,57 +210,7 @@ def common_check_new_columns():
"""
add new columns to tables
"""
from api.extensions import db
from sqlalchemy import inspect, text
def get_model_by_table_name(_table_name):
registry = getattr(db.Model, 'registry', None)
class_registry = getattr(registry, '_class_registry', None)
for _model in class_registry.values():
if hasattr(_model, '__tablename__') and _model.__tablename__ == _table_name:
return _model
return None
def add_new_column(target_table_name, new_column):
column_type = new_column.type.compile(engine.dialect)
default_value = new_column.default.arg if new_column.default else None
sql = "ALTER TABLE " + target_table_name + " ADD COLUMN " + f"`{new_column.name}`" + " " + column_type
if new_column.comment:
sql += f" comment '{new_column.comment}'"
if column_type == 'JSON':
pass
elif default_value:
if column_type.startswith('VAR') or column_type.startswith('Text'):
if default_value is None or len(default_value) == 0:
pass
else:
sql += f" DEFAULT {default_value}"
sql = text(sql)
db.session.execute(sql)
engine = db.get_engine()
inspector = inspect(engine)
table_names = inspector.get_table_names()
for table_name in table_names:
existed_columns = inspector.get_columns(table_name)
existed_column_name_list = [c['name'] for c in existed_columns]
model = get_model_by_table_name(table_name)
if model is None:
continue
model_columns = getattr(getattr(getattr(model, '__table__'), 'columns'), '_all_columns')
for column in model_columns:
if column.name not in existed_column_name_list:
try:
add_new_column(table_name, column)
current_app.logger.info(f"add new column [{column.name}] in table [{table_name}] success.")
except Exception as e:
current_app.logger.error(f"add new column [{column.name}] in table [{table_name}] err:")
current_app.logger.error(e)
CheckNewColumn().run()
@click.command()

View File

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

View File

@@ -5,7 +5,6 @@ import copy
import toposort
from flask import abort
from flask import current_app
from flask import session
from flask_login import current_user
from toposort import toposort_flatten
from werkzeug.exceptions import BadRequest
@@ -28,9 +27,11 @@ from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.cmdb.relation_type import RelationTypeManager
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.value import AttributeValueManager
from api.lib.common_setting.role_perm_base import CMDBApp
from api.lib.decorator import kwargs_required
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import is_app_admin
from api.lib.perm.acl.acl import validate_permission
from api.models.cmdb import Attribute
from api.models.cmdb import AutoDiscoveryCI
from api.models.cmdb import AutoDiscoveryCIType
@@ -92,6 +93,9 @@ class CITypeManager(object):
for type_dict in ci_types:
attr = AttributeCache.get(type_dict["unique_id"])
type_dict["unique_key"] = attr and attr.name
if type_dict.get('show_id'):
attr = AttributeCache.get(type_dict["show_id"])
type_dict["show_name"] = attr and attr.name
type_dict['parent_ids'] = CITypeInheritanceManager.get_parents(type_dict['id'])
if resources is None or type_dict['name'] in resources:
res.append(type_dict)
@@ -127,7 +131,9 @@ class CITypeManager(object):
def add(cls, **kwargs):
if current_app.config.get('USE_ACL') and not is_app_admin('cmdb'):
if ErrFormat.ci_type_config not in {i['name'] for i in ACLManager().get_resources(ResourceTypeEnum.PAGE)}:
return abort(403, ErrFormat.no_permission2)
app_cli = CMDBApp()
validate_permission(app_cli.op.Model_Configuration, app_cli.resource_type_name,
app_cli.op.create_CIType, app_cli.app_name)
unique_key = kwargs.pop("unique_key", None) or kwargs.pop("unique_id", None)
unique_key = AttributeCache.get(unique_key) or abort(404, ErrFormat.unique_key_not_define)
@@ -192,7 +198,7 @@ class CITypeManager(object):
CITypeAttributeManager.update(type_id, [attr])
ci_type2 = ci_type.to_dict()
new = ci_type.update(**kwargs)
new = ci_type.update(**kwargs, filter_none=False)
CITypeCache.clean(type_id)
@@ -250,6 +256,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()
@@ -411,9 +424,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])
@@ -680,6 +690,9 @@ class CITypeAttributeManager(object):
CITypeAttributeCache.clean(type_id, attr_id)
if ci_type.show_id == attr_id:
ci_type.update(show_id=None, filter_none=False)
CITypeHistoryManager.add(CITypeOperateType.DELETE_ATTRIBUTE, type_id, attr_id=attr.id,
change=attr and attr.to_dict())
@@ -1227,7 +1240,10 @@ class CITypeTemplateManager(object):
def _import_ci_types(self, ci_types, attr_id_map):
for i in ci_types:
i.pop("unique_key", None)
i.pop("show_name", None)
i['unique_id'] = attr_id_map.get(i['unique_id'], i['unique_id'])
if i.get('show_id'):
i['show_id'] = attr_id_map.get(i['show_id'], i['show_id'])
i['uid'] = current_user.uid
return self.__import(CIType, ci_types)

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

@@ -31,7 +31,8 @@ from api.models.cmdb import PreferenceRelationView
from api.models.cmdb import PreferenceSearchOption
from api.models.cmdb import PreferenceShowAttributes
from api.models.cmdb import PreferenceTreeView
from api.models.cmdb import CITypeGroup
from api.models.cmdb import CITypeGroupItem
class PreferenceManager(object):
pref_attr_cls = PreferenceShowAttributes
@@ -43,22 +44,47 @@ class PreferenceManager(object):
def get_types(instance=False, tree=False):
ci_type_order = sorted(PreferenceCITypeOrder.get_by(uid=current_user.uid, to_dict=False), key=lambda x: x.order)
type2group = {}
for i in db.session.query(CITypeGroupItem, CITypeGroup).join(
CITypeGroup, CITypeGroup.id == CITypeGroupItem.group_id).filter(
CITypeGroup.deleted.is_(False)).filter(CITypeGroupItem.deleted.is_(False)):
type2group[i.CITypeGroupItem.type_id] = i.CITypeGroup.to_dict()
types = db.session.query(PreferenceShowAttributes.type_id).filter(
PreferenceShowAttributes.uid == current_user.uid).filter(
PreferenceShowAttributes.deleted.is_(False)).group_by(
PreferenceShowAttributes.type_id).all() if instance else []
types = sorted(types, key=lambda x: {i.type_id: idx for idx, i in enumerate(
ci_type_order) if not i.is_tree}.get(x.type_id, 1))
group_types = []
other_types = []
group2idx = {}
type_ids = set()
for ci_type in types:
type_id = ci_type.type_id
type_ids.add(type_id)
type_dict = CITypeCache.get(type_id).to_dict()
if type_id not in type2group:
other_types.append(type_dict)
else:
group = type2group[type_id]
if group['id'] not in group2idx:
group_types.append(type2group[type_id])
group2idx[group['id']] = len(group_types) - 1
group_types[group2idx[group['id']]].setdefault('ci_types', []).append(type_dict)
if other_types:
group_types.append(dict(ci_types=other_types))
tree_types = PreferenceTreeView.get_by(uid=current_user.uid, to_dict=False) if tree else []
tree_types = sorted(tree_types, key=lambda x: {i.type_id: idx for idx, i in enumerate(
ci_type_order) if i.is_tree}.get(x.type_id, 1))
type_ids = [i.type_id for i in types + tree_types]
if types and tree_types:
type_ids = set(type_ids)
tree_types = [CITypeCache.get(_type.type_id).to_dict() for _type in tree_types]
for _type in tree_types:
type_ids.add(_type['id'])
return dict(group_types=group_types, tree_types=tree_types, type_ids=list(type_ids))
return [CITypeCache.get(type_id).to_dict() for type_id in type_ids]
@staticmethod
def get_types2(instance=False, tree=False):
@@ -297,6 +323,10 @@ class PreferenceManager(object):
for type_id in id2type:
id2type[type_id] = CITypeCache.get(type_id).to_dict()
id2type[type_id]['unique_name'] = AttributeCache.get(id2type[type_id]['unique_id']).name
if id2type[type_id]['show_id']:
show_attr = AttributeCache.get(id2type[type_id]['show_id'])
id2type[type_id]['show_name'] = show_attr and show_attr.name
return result, id2type, sorted(name2id, key=lambda x: x[1])

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") # 不能删除唯一标识
@@ -138,3 +140,7 @@ class ErrFormat(CommonErrFormat):
password_save_failed = _l("Failed to save password: {}") # 保存密码失败: {}
password_load_failed = _l("Failed to get password: {}") # 获取密码失败: {}
cron_time_format_invalid = _l("Scheduling time format error") # 调度时间格式错误
reconciliation_title = _l("CMDB data reconciliation results") # CMDB数据合规检查结果
reconciliation_body = _l("Number of {} illegal: {}") # "{} 不合规数: {}"

View File

@@ -47,7 +47,8 @@ class Search(object):
excludes=None,
parent_node_perm_passed=False,
use_id_filter=False,
use_ci_filter=True):
use_ci_filter=True,
only_ids=False):
self.orig_query = query
self.fl = fl or []
self.excludes = excludes or []
@@ -64,6 +65,7 @@ class Search(object):
self.parent_node_perm_passed = parent_node_perm_passed
self.use_id_filter = use_id_filter
self.use_ci_filter = use_ci_filter
self.only_ids = only_ids
self.valid_type_names = []
self.type2filter_perms = dict()
@@ -590,6 +592,8 @@ class Search(object):
def search(self):
numfound, ci_ids = self._query_build_raw()
ci_ids = list(map(str, ci_ids))
if self.only_ids:
return ci_ids
_fl = self._fl_build()

View File

@@ -8,6 +8,8 @@ from flask import current_app
from flask_login import current_user
from api.extensions import rd
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.ci_type import CITypeRelationManager
from api.lib.cmdb.const import ConstraintEnum
@@ -18,6 +20,8 @@ from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB
from api.lib.cmdb.search.ci.es.search import Search as SearchFromES
from api.lib.cmdb.utils import TableMap
from api.lib.cmdb.utils import ValueTypeMap
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import is_app_admin
from api.models.cmdb import CI
@@ -65,7 +69,7 @@ class Search(object):
if _l < int(level) and c == ConstraintEnum.Many2Many:
self.has_m2m = True
self.type2filter_perms = None
self.type2filter_perms = {}
self.is_app_admin = is_app_admin('cmdb') or current_user.username == "worker"
@@ -75,7 +79,7 @@ class Search(object):
key = []
_tmp = []
for level in range(1, sorted(self.level)[-1] + 1):
if len(self.descendant_ids) >= level and self.type2filter_perms.get(self.descendant_ids[level - 1]):
if len(self.descendant_ids or []) >= level and self.type2filter_perms.get(self.descendant_ids[level - 1]):
id_filter_limit, _ = self._get_ci_filter(self.type2filter_perms[self.descendant_ids[level - 1]])
else:
id_filter_limit = {}
@@ -147,9 +151,9 @@ class Search(object):
return True
def search(self):
use_ci_filter = len(self.descendant_ids) == self.level[0] - 1
parent_node_perm_passed = self._has_read_perm_from_parent_nodes()
def search(self, only_ids=False):
use_ci_filter = len(self.descendant_ids or []) == self.level[0] - 1
parent_node_perm_passed = not self.is_app_admin and self._has_read_perm_from_parent_nodes()
ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id
cis = [CI.get_by_id(_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(_id))) for _id in ids]
@@ -193,7 +197,8 @@ class Search(object):
sort=self.sort,
ci_ids=merge_ids,
parent_node_perm_passed=parent_node_perm_passed,
use_ci_filter=use_ci_filter).search()
use_ci_filter=use_ci_filter,
only_ids=only_ids).search()
def _get_ci_filter(self, filter_perms, ci_filters=None):
ci_filters = ci_filters or []
@@ -236,7 +241,7 @@ class Search(object):
type2filter_perms = CIFilterPermsCRUD().get_by_ids(list(map(int, [i['name'] for i in res2])))
ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id
_tmp = []
_tmp, tmp_res = [], []
level2ids = {}
for lv in range(1, self.level + 1):
level2ids[lv] = []
@@ -303,25 +308,26 @@ class Search(object):
if key:
res = [json.loads(x).items() for x in [i or '{}' for i in rd.get(key, prefix) or []]]
if type_ids and lv == self.level:
__tmp = [[i for i in x if i[1] in type_ids and
(not id_filter_limit or (
key[idx] not in id_filter_limit or
int(i[0]) in id_filter_limit[key[idx]]) or
int(i[0]) in id_filter_limit)] for idx, x in enumerate(res)]
tmp_res = [[i for i in x if i[1] in type_ids and
(not id_filter_limit or (
key[idx] not in id_filter_limit or
int(i[0]) in id_filter_limit[key[idx]]) or
int(i[0]) in id_filter_limit)] for idx, x in enumerate(res)]
else:
__tmp = [[i for i in x if (not id_filter_limit or (
tmp_res = [[i for i in x if (not id_filter_limit or (
key[idx] not in id_filter_limit or
int(i[0]) in id_filter_limit[key[idx]]) or
int(i[0]) in id_filter_limit)] for idx, x in enumerate(res)]
int(i[0]) in id_filter_limit)] for idx, x in
enumerate(res)]
if ci_filter_limit:
__tmp = [[j for j in i if j[1] not in ci_filter_limit or
int(j[0]) in ci_filter_limit[j[1]]] for i in __tmp]
tmp_res = [[j for j in i if j[1] not in ci_filter_limit or
int(j[0]) in ci_filter_limit[j[1]]] for i in tmp_res]
else:
__tmp = []
tmp_res = []
if __tmp:
_tmp[idx] = [j for i in __tmp for j in i]
if tmp_res:
_tmp[idx] = [j for i in tmp_res for j in i]
else:
_tmp[idx] = []
level2ids[lv].append([])
@@ -332,3 +338,84 @@ class Search(object):
detail={str(_id): dict(Counter([i[1] for i in _tmp[idx]]).items()) for idx, _id in enumerate(ids)})
return result
def search_full(self, type_ids):
def _get_id2name(_type_id):
ci_type = CITypeCache.get(_type_id)
attr = AttributeCache.get(ci_type.unique_id)
value_table = TableMap(attr=attr).table
serializer = ValueTypeMap.serialize[attr.value_type]
unique_value = {i.ci_id: serializer(i.value) for i in value_table.get_by(attr_id=attr.id, to_dict=False)}
attr = AttributeCache.get(ci_type.show_id)
if attr:
value_table = TableMap(attr=attr).table
serializer = ValueTypeMap.serialize[attr.value_type]
show_value = {i.ci_id: serializer(i.value) for i in value_table.get_by(attr_id=attr.id, to_dict=False)}
else:
show_value = unique_value
return show_value, unique_value
self.level = int(self.level)
acl = ACLManager('cmdb')
type2filter_perms = dict()
if not self.is_app_admin:
res2 = acl.get_resources(ResourceTypeEnum.CI_FILTER)
if res2:
type2filter_perms = CIFilterPermsCRUD().get_by_ids(list(map(int, [i['name'] for i in res2])))
ids = [self.root_id] if not isinstance(self.root_id, list) else self.root_id
level_ids = [str(i) for i in ids]
result = []
id2children = {}
id2name = _get_id2name(type_ids[0])
for i in level_ids:
item = dict(id=int(i),
type_id=type_ids[0],
isLeaf=False,
title=id2name[0].get(int(i)),
uniqueValue=id2name[1].get(int(i)),
children=[])
result.append(item)
id2children[str(i)] = item['children']
for lv in range(1, self.level):
if len(type_ids or []) >= lv and type2filter_perms.get(type_ids[lv]):
id_filter_limit, _ = self._get_ci_filter(type2filter_perms[type_ids[lv]])
else:
id_filter_limit = {}
if self.has_m2m and lv != 1:
key, prefix = [i for i in level_ids], REDIS_PREFIX_CI_RELATION2
else:
key, prefix = [i.split(',')[-1] for i in level_ids], REDIS_PREFIX_CI_RELATION
res = [json.loads(x).items() for x in [i or '{}' for i in rd.get(key, prefix) or []]]
res = [[i for i in x if (not id_filter_limit or (key[idx] not in id_filter_limit or
int(i[0]) in id_filter_limit[key[idx]]) or
int(i[0]) in id_filter_limit)] for idx, x in enumerate(res)]
_level_ids = []
type_id = type_ids[lv]
id2name = _get_id2name(type_id)
for idx, node_path in enumerate(level_ids):
for child_id, _ in (res[idx] or []):
item = dict(id=int(child_id),
type_id=type_id,
isLeaf=True if lv == self.level - 1 else False,
title=id2name[0].get(int(child_id)),
uniqueValue=id2name[1].get(int(child_id)),
children=[])
id2children[node_path].append(item)
_node_path = "{},{}".format(node_path, child_id)
_level_ids.append(_node_path)
id2children[_node_path] = item['children']
level_ids = _level_ids
return result

View File

@@ -274,26 +274,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))
existed_values = [(ValueTypeMap.serialize[attr.value_type](i.value) if
i.value or i.value == 0 else i.value) for i in existed_attrs]
for v in deleted:
existed_attr = existed_attrs[existed_values.index(v)]
# 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
# Delete first and then add to ensure id sorting
for idx in range(index, len(existed_attrs)):
existed_attr = existed_attrs[idx]
existed_attr.delete(flush=False, commit=False)
changed.append((ci.id, attr.id, OperateType.DELETE, v, None, ci.type_id))
changed.append((ci.id, attr.id, OperateType.DELETE, existed_values[idx], None, ci.type_id))
for idx in range(index, len(value)):
value_table.create(ci_id=ci.id, attr_id=attr.id, value=value[idx], flush=False, commit=False)
changed.append((ci.id, attr.id, OperateType.ADD, None, value[idx], ci.type_id))
else:
existed_attr = value_table.get_by(attr_id=attr.id, ci_id=ci.id, first=True, to_dict=False)
existed_value = existed_attr and existed_attr.value
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

@@ -1,5 +1,10 @@
# -*- coding:utf-8 -*-
from datetime import datetime
from flask import current_app
from sqlalchemy import inspect, text
from sqlalchemy.dialects.mysql import ENUM
from api.extensions import db
def get_cur_time_str(split_flag='-'):
@@ -23,3 +28,115 @@ class BaseEnum(object):
if not attr.startswith("_") and not callable(getattr(cls, attr))
}
return cls._ALL_
class CheckNewColumn(object):
def __init__(self):
self.engine = db.get_engine()
self.inspector = inspect(self.engine)
self.table_names = self.inspector.get_table_names()
@staticmethod
def get_model_by_table_name(_table_name):
registry = getattr(db.Model, 'registry', None)
class_registry = getattr(registry, '_class_registry', None)
for _model in class_registry.values():
if hasattr(_model, '__tablename__') and _model.__tablename__ == _table_name:
return _model
return None
def run(self):
for table_name in self.table_names:
self.check_by_table(table_name)
def check_by_table(self, table_name):
existed_columns = self.inspector.get_columns(table_name)
enum_columns = []
existed_column_name_list = []
for c in existed_columns:
if isinstance(c['type'], ENUM):
enum_columns.append(c['name'])
existed_column_name_list.append(c['name'])
model = self.get_model_by_table_name(table_name)
if model is None:
return
model_columns = getattr(getattr(getattr(model, '__table__'), 'columns'), '_all_columns')
for column in model_columns:
if column.name not in existed_column_name_list:
add_res = self.add_new_column(table_name, column)
if not add_res:
continue
current_app.logger.info(f"add new column [{column.name}] in table [{table_name}] success.")
if column.name in enum_columns:
enum_columns.remove(column.name)
self.add_new_index(table_name, column)
if len(enum_columns) > 0:
self.check_enum_column(enum_columns, existed_columns, model_columns, table_name)
def add_new_column(self, target_table_name, new_column):
try:
column_type = new_column.type.compile(self.engine.dialect)
default_value = new_column.default.arg if new_column.default else None
sql = "ALTER TABLE " + target_table_name + " ADD COLUMN " + f"`{new_column.name}`" + " " + column_type
if new_column.comment:
sql += f" comment '{new_column.comment}'"
if column_type == 'JSON':
pass
elif default_value:
if column_type.startswith('VAR') or column_type.startswith('Text'):
if default_value is None or len(default_value) == 0:
pass
else:
sql += f" DEFAULT {default_value}"
sql = text(sql)
db.session.execute(sql)
return True
except Exception as e:
err = f"add_new_column [{new_column.name}] to table [{target_table_name}] err: {e}"
current_app.logger.error(err)
return False
@staticmethod
def add_new_index(target_table_name, new_column):
try:
if new_column.index:
index_name = f"{target_table_name}_{new_column.name}"
sql = "CREATE INDEX " + f"{index_name}" + " ON " + target_table_name + " (" + new_column.name + ")"
db.session.execute(sql)
current_app.logger.info(f"add new index [{index_name}] in table [{target_table_name}] success.")
return True
except Exception as e:
err = f"add_new_index [{new_column.name}] to table [{target_table_name}] err: {e}"
current_app.logger.error(err)
return False
@staticmethod
def check_enum_column(enum_columns, existed_columns, model_columns, table_name):
for column_name in enum_columns:
try:
enum_column = list(filter(lambda x: x['name'] == column_name, existed_columns))[0]
old_enum_value = enum_column.get('type', {}).enums
target_column = list(filter(lambda x: x.name == column_name, model_columns))[0]
new_enum_value = target_column.type.enums
if set(old_enum_value) == set(new_enum_value):
continue
enum_values_str = ','.join(["'{}'".format(value) for value in new_enum_value])
sql = f"ALTER TABLE {table_name} MODIFY COLUMN" + f"`{column_name}`" + f" enum({enum_values_str})"
db.session.execute(sql)
current_app.logger.info(
f"modify column [{column_name}] ENUM: {new_enum_value} in table [{table_name}] success.")
except Exception as e:
current_app.logger.error(
f"modify column ENUM [{column_name}] in table [{table_name}] err: {e}")

View File

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

View File

@@ -3,6 +3,7 @@
import msgpack
import redis_lock
from flask import current_app
from api.extensions import cache
from api.extensions import rd
@@ -157,9 +158,9 @@ class RoleRelationCache(object):
PREFIX_RESOURCES2 = "RoleRelationResources2::id::{0}::AppId::{1}"
@classmethod
def get_parent_ids(cls, rid, app_id):
def get_parent_ids(cls, rid, app_id, force=False):
parent_ids = cache.get(cls.PREFIX_PARENT.format(rid, app_id))
if not parent_ids:
if not parent_ids or force:
from api.lib.perm.acl.role import RoleRelationCRUD
parent_ids = RoleRelationCRUD.get_parent_ids(rid, app_id)
cache.set(cls.PREFIX_PARENT.format(rid, app_id), parent_ids, timeout=0)
@@ -167,9 +168,9 @@ class RoleRelationCache(object):
return parent_ids
@classmethod
def get_child_ids(cls, rid, app_id):
def get_child_ids(cls, rid, app_id, force=False):
child_ids = cache.get(cls.PREFIX_CHILDREN.format(rid, app_id))
if not child_ids:
if not child_ids or force:
from api.lib.perm.acl.role import RoleRelationCRUD
child_ids = RoleRelationCRUD.get_child_ids(rid, app_id)
cache.set(cls.PREFIX_CHILDREN.format(rid, app_id), child_ids, timeout=0)
@@ -177,14 +178,15 @@ class RoleRelationCache(object):
return child_ids
@classmethod
def get_resources(cls, rid, app_id):
def get_resources(cls, rid, app_id, force=False):
"""
:param rid:
:param app_id:
:param force:
:return: {id2perms: {resource_id: [perm,]}, group2perms: {group_id: [perm, ]}}
"""
resources = cache.get(cls.PREFIX_RESOURCES.format(rid, app_id))
if not resources:
if not resources or force:
from api.lib.perm.acl.role import RoleCRUD
resources = RoleCRUD.get_resources(rid, app_id)
if resources['id2perms'] or resources['group2perms']:
@@ -193,9 +195,9 @@ class RoleRelationCache(object):
return resources or {}
@classmethod
def get_resources2(cls, rid, app_id):
def get_resources2(cls, rid, app_id, force=False):
r_g = cache.get(cls.PREFIX_RESOURCES2.format(rid, app_id))
if not r_g:
if not r_g or force:
res = cls.get_resources(rid, app_id)
id2perms = res['id2perms']
group2perms = res['group2perms']
@@ -224,22 +226,28 @@ 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, force=True)
cls.get_child_ids(rid, _app_id, force=True)
resources = cls.get_resources(rid, _app_id, force=True)
if resources.get('id2perms') or resources.get('group2perms'):
HasResourceRoleCache.add(rid, _app_id)
else:
HasResourceRoleCache.remove(rid, _app_id)
cls.get_resources2(rid, _app_id, force=True)
@classmethod
@flush_db
def rebuild2(cls, rid, app_id):
cache.delete(cls.PREFIX_RESOURCES2.format(rid, app_id))
cls.get_resources2(rid, app_id)
cls.get_resources2(rid, app_id, force=True)
@classmethod
def clean(cls, rid, app_id):

View File

@@ -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, {}, {},
@@ -372,16 +379,16 @@ class RoleCRUD(object):
resource_type_id = resource_type and resource_type.id
result = dict(resources=dict(), groups=dict())
s = time.time()
# s = time.time()
parent_ids = RoleRelationCRUD.recursive_parent_ids(rid, app_id)
current_app.logger.info('parent ids {0}: {1}'.format(parent_ids, time.time() - s))
# current_app.logger.info('parent ids {0}: {1}'.format(parent_ids, time.time() - s))
for parent_id in parent_ids:
_resources, _groups = cls._extend_resources(parent_id, resource_type_id, app_id)
current_app.logger.info('middle1: {0}'.format(time.time() - s))
# current_app.logger.info('middle1: {0}'.format(time.time() - s))
_merge(result['resources'], _resources)
current_app.logger.info('middle2: {0}'.format(time.time() - s))
current_app.logger.info(len(_groups))
# current_app.logger.info('middle2: {0}'.format(time.time() - s))
# current_app.logger.info(len(_groups))
if not group_flat:
_merge(result['groups'], _groups)
else:
@@ -392,7 +399,7 @@ class RoleCRUD(object):
item.setdefault('permissions', [])
item['permissions'] = list(set(item['permissions'] + _groups[rg_id]['permissions']))
result['resources'][item['id']] = item
current_app.logger.info('End: {0}'.format(time.time() - s))
# current_app.logger.info('End: {0}'.format(time.time() - s))
result['resources'] = list(result['resources'].values())
result['groups'] = list(result['groups'].values())

View File

@@ -1,19 +1,15 @@
import json
import os
import secrets
import sys
from base64 import b64decode, b64encode
import threading
from base64 import b64decode, b64encode
from Cryptodome.Protocol.SecretSharing import Shamir
from colorama import Back
from colorama import Fore
from colorama import Style
from colorama import init as colorama_init
from colorama import Back, Fore, Style, init as colorama_init
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.primitives.ciphers import Cipher
from cryptography.hazmat.primitives.ciphers import algorithms
from cryptography.hazmat.primitives.ciphers import modes
from cryptography.hazmat.primitives import hashes, padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from flask import current_app
@@ -27,11 +23,16 @@ backend_encrypt_key_name = "encrypt_key"
backend_root_key_salt_name = "root_key_salt"
backend_encrypt_key_salt_name = "encrypt_key_salt"
backend_seal_key = "seal_status"
success = "success"
seal_status = True
secrets_encrypt_key = ""
secrets_root_key = ""
def string_to_bytes(value):
if not value:
return ""
if isinstance(value, bytes):
return value
if sys.version_info.major == 2:
@@ -44,6 +45,8 @@ def string_to_bytes(value):
class Backend:
def __init__(self, backend=None):
self.backend = backend
# cache is a redis object
self.cache = backend.cache
def get(self, key):
return self.backend.get(key)
@@ -54,23 +57,33 @@ class Backend:
def update(self, key, value):
return self.backend.update(key, value)
def get_shares(self, key):
return self.backend.get_shares(key)
def set_shares(self, key, value):
return self.backend.set_shares(key, value)
class KeyManage:
def __init__(self, trigger=None, backend=None):
self.trigger = trigger
self.backend = backend
self.share_key = "cmdb::secret::secrets_share"
if backend:
self.backend = Backend(backend)
def init_app(self, app, backend=None):
if (sys.argv[0].endswith("gunicorn") or
(len(sys.argv) > 1 and sys.argv[1] in ("run", "cmdb-password-data-migrate"))):
self.backend = backend
threading.Thread(target=self.watch_root_key, args=(app,)).start()
self.trigger = app.config.get("INNER_TRIGGER_TOKEN")
if not self.trigger:
return
self.backend = backend
resp = self.auto_unseal()
self.print_response(resp)
@@ -124,6 +137,8 @@ class KeyManage:
return new_shares
def is_valid_root_key(self, root_key):
if not root_key:
return False
root_key_hash, ok = self.hash_root_key(root_key)
if not ok:
return root_key_hash, ok
@@ -135,35 +150,42 @@ class KeyManage:
else:
return "", True
def auth_root_secret(self, root_key):
msg, ok = self.is_valid_root_key(root_key)
if not ok:
return {
"message": msg,
"status": "failed"
}
def auth_root_secret(self, root_key, app):
with app.app_context():
msg, ok = self.is_valid_root_key(root_key)
if not ok:
return {
"message": msg,
"status": "failed"
}
encrypt_key_aes = self.backend.get(backend_encrypt_key_name)
if not encrypt_key_aes:
return {
"message": "encrypt key is empty",
"status": "failed"
}
encrypt_key_aes = self.backend.get(backend_encrypt_key_name)
if not encrypt_key_aes:
return {
"message": "encrypt key is empty",
"status": "failed"
}
secrets_encrypt_key, ok = InnerCrypt.aes_decrypt(string_to_bytes(root_key), encrypt_key_aes)
if ok:
msg, ok = self.backend.update(backend_seal_key, "open")
secret_encrypt_key, ok = InnerCrypt.aes_decrypt(string_to_bytes(root_key), encrypt_key_aes)
if ok:
current_app.config["secrets_encrypt_key"] = secrets_encrypt_key
current_app.config["secrets_root_key"] = root_key
current_app.config["secrets_shares"] = []
return {"message": success, "status": success}
return {"message": msg, "status": "failed"}
else:
return {
"message": secrets_encrypt_key,
"status": "failed"
}
msg, ok = self.backend.update(backend_seal_key, "open")
if ok:
global secrets_encrypt_key, secrets_root_key
secrets_encrypt_key = secret_encrypt_key
secrets_root_key = root_key
self.backend.cache.set(self.share_key, json.dumps([]))
return {"message": success, "status": success}
return {"message": msg, "status": "failed"}
else:
return {
"message": secret_encrypt_key,
"status": "failed"
}
def parse_shares(self, shares, app):
if len(shares) >= global_key_threshold:
recovered_secret = Shamir.combine(shares[:global_key_threshold], False)
return self.auth_root_secret(b64encode(recovered_secret), app)
def unseal(self, key):
if not self.is_seal():
@@ -175,14 +197,12 @@ class KeyManage:
try:
t = [i for i in b64decode(key)]
v = (int("".join([chr(i) for i in t[-2:]])), bytes(t[:-2]))
shares = current_app.config.get("secrets_shares", [])
shares = self.backend.get_shares(self.share_key)
if v not in shares:
shares.append(v)
current_app.config["secrets_shares"] = shares
self.set_shares(shares)
if len(shares) >= global_key_threshold:
recovered_secret = Shamir.combine(shares[:global_key_threshold], False)
return self.auth_root_secret(b64encode(recovered_secret))
return self.parse_shares(shares, current_app)
else:
return {
"message": "waiting for inputting other unseal key {0}/{1}".format(len(shares),
@@ -242,8 +262,11 @@ class KeyManage:
msg, ok = self.backend.add(backend_seal_key, "open")
if not ok:
return {"message": msg, "status": "failed"}, False
current_app.config["secrets_root_key"] = root_key
current_app.config["secrets_encrypt_key"] = encrypt_key
global secrets_encrypt_key, secrets_root_key
secrets_encrypt_key = encrypt_key
secrets_root_key = root_key
self.print_token(shares, root_token=root_key)
return {"message": "OK",
@@ -266,7 +289,7 @@ class KeyManage:
}
# TODO
elif len(self.trigger.strip()) == 24:
res = self.auth_root_secret(self.trigger.encode())
res = self.auth_root_secret(self.trigger.encode(), current_app)
if res.get("status") == success:
return {
"message": success,
@@ -298,22 +321,31 @@ class KeyManage:
"message": msg,
"status": "failed",
}
current_app.config["secrets_root_key"] = ''
current_app.config["secrets_encrypt_key"] = ''
self.clear()
self.backend.cache.publish(self.share_key, "clear")
return {
"message": success,
"status": success
}
@staticmethod
def clear():
global secrets_encrypt_key, secrets_root_key
secrets_encrypt_key = ''
secrets_root_key = ''
def is_seal(self):
"""
If there is no initialization or the root key is inconsistent, it is considered to be in a sealed state.
If there is no initialization or the root key is inconsistent, it is considered to be in a sealed state..
:return:
"""
secrets_root_key = current_app.config.get("secrets_root_key")
# secrets_root_key = current_app.config.get("secrets_root_key")
if not secrets_root_key:
return True
msg, ok = self.is_valid_root_key(secrets_root_key)
if not ok:
return true
return True
status = self.backend.get(backend_seal_key)
return status == "block"
@@ -349,22 +381,53 @@ class KeyManage:
}
print(status_colors.get(status, Fore.GREEN), message, Style.RESET_ALL)
def set_shares(self, values):
new_value = list()
for v in values:
new_value.append((v[0], b64encode(v[1]).decode("utf-8")))
self.backend.cache.publish(self.share_key, json.dumps(new_value))
self.backend.cache.set(self.share_key, json.dumps(new_value))
def watch_root_key(self, app):
pubsub = self.backend.cache.pubsub()
pubsub.subscribe(self.share_key)
new_value = set()
for message in pubsub.listen():
if message["type"] == "message":
if message["data"] == b"clear":
self.clear()
continue
try:
value = json.loads(message["data"].decode("utf-8"))
for v in value:
new_value.add((v[0], b64decode(v[1])))
except Exception as e:
return []
if len(new_value) >= global_key_threshold:
self.parse_shares(list(new_value), app)
new_value = set()
class InnerCrypt:
def __init__(self):
secrets_encrypt_key = current_app.config.get("secrets_encrypt_key", "")
self.encrypt_key = b64decode(secrets_encrypt_key.encode("utf-8"))
self.encrypt_key = b64decode(secrets_encrypt_key)
#self.encrypt_key = b64decode(secrets_encrypt_key, "".encode("utf-8"))
def encrypt(self, plaintext):
"""
encrypt method contain aes currently
"""
if not self.encrypt_key:
return ValueError("secret is disabled, please seal firstly"), False
return self.aes_encrypt(self.encrypt_key, plaintext)
def decrypt(self, ciphertext):
"""
decrypt method contain aes currently
"""
if not self.encrypt_key:
return ValueError("secret is disabled, please seal firstly"), False
return self.aes_decrypt(self.encrypt_key, ciphertext)
@classmethod
@@ -381,6 +444,7 @@ class InnerCrypt:
return b64encode(iv + ciphertext).decode("utf-8"), True
except Exception as e:
return str(e), False
@classmethod
@@ -426,4 +490,4 @@ if __name__ == "__main__":
t_ciphertext, status1 = c.encrypt(t_plaintext)
print("Ciphertext:", t_ciphertext)
decrypted_plaintext, status2 = c.decrypt(t_ciphertext)
print("Decrypted plaintext:", decrypted_plaintext)
print("Decrypted plaintext:", decrypted_plaintext)

View File

@@ -1,8 +1,13 @@
import base64
import json
from api.models.cmdb import InnerKV
from api.extensions import rd
class InnerKVManger(object):
def __init__(self):
self.cache = rd.r
pass
@classmethod
@@ -33,3 +38,26 @@ class InnerKVManger(object):
return "success", True
return "update failed", True
@classmethod
def get_shares(cls, key):
new_value = list()
v = rd.get_str(key)
if not v:
return new_value
try:
value = json.loads(v.decode("utf-8"))
for v in value:
new_value.append((v[0], base64.b64decode(v[1])))
except Exception as e:
return []
return new_value
@classmethod
def set_shares(cls, key, value):
new_value = list()
for v in value:
new_value.append((v[0], base64.b64encode(v[1]).decode("utf-8")))
rd.set_str(key, json.dumps(new_value))

View File

@@ -117,6 +117,23 @@ class RedisHandler(object):
except Exception as e:
current_app.logger.error("delete redis key error, {0}".format(str(e)))
def set_str(self, key, value, expired=None):
try:
if expired:
self.r.setex(key, expired, value)
else:
self.r.set(key, value)
except Exception as e:
current_app.logger.error("set redis error, {0}".format(str(e)))
def get_str(self, key):
try:
value = self.r.get(key)
except Exception as e:
current_app.logger.error("get redis error, {0}".format(str(e)))
return
return value
class ESHandler(object):
def __init__(self, flask_app=None):

View File

@@ -46,13 +46,17 @@ class CIType(Model):
name = db.Column(db.String(32), nullable=False)
alias = db.Column(db.String(32), nullable=False)
unique_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id"), nullable=False)
show_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id"))
enabled = db.Column(db.Boolean, default=True, nullable=False)
is_attached = db.Column(db.Boolean, default=False, nullable=False)
icon = db.Column(db.Text)
order = db.Column(db.SmallInteger, default=0, nullable=False)
default_order_attr = db.Column(db.String(33))
unique_key = db.relationship("Attribute", backref="c_ci_types.unique_id")
unique_key = db.relationship("Attribute", backref="c_ci_types.unique_id",
primaryjoin="Attribute.id==CIType.unique_id", foreign_keys=[unique_id])
show_key = db.relationship("Attribute", backref="c_ci_types.show_id",
primaryjoin="Attribute.id==CIType.show_id", foreign_keys=[show_id])
uid = db.Column(db.Integer, index=True)
@@ -428,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

@@ -30,6 +30,7 @@ class CIRelationSearchView(APIView):
level: default is 1
facet: statistic
"""
page = get_page(request.values.get("page", 1))
count = get_page_size(request.values.get("count") or request.values.get("page_size"))
@@ -86,6 +87,26 @@ class CIRelationStatisticsView(APIView):
return self.jsonify(result)
class CIRelationSearchFullView(APIView):
url_prefix = "/ci_relations/search/full"
def get(self):
root_ids = list(map(int, handle_arg_list(request.values.get('root_ids'))))
level = request.values.get('level', 1)
type_ids = list(map(int, handle_arg_list(request.values.get('type_ids', []))))
has_m2m = request.values.get("has_m2m") in current_app.config.get('BOOL_TRUE')
start = time.time()
s = Search(root_ids, level, has_m2m=has_m2m)
try:
result = s.search_full(type_ids)
except SearchError as e:
return abort(400, str(e))
current_app.logger.debug("search time is :{0}".format(time.time() - start))
return self.jsonify(result)
class GetSecondCIsView(APIView):
url_prefix = "/ci_relations/<int:first_ci_id>/second_cis"

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)

View File

@@ -42,9 +42,11 @@
"relation-graph": "^1.1.0",
"snabbdom": "^3.5.1",
"sortablejs": "1.9.0",
"style-resources-loader": "^1.5.0",
"viser-vue": "^2.4.8",
"vue": "2.6.11",
"vue-clipboard2": "^0.3.3",
"vue-cli-plugin-style-resources-loader": "^0.1.5",
"vue-codemirror": "^4.0.6",
"vue-cropper": "^0.6.2",
"vue-grid-layout": "2.3.12",

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=1711963254221') format('woff2'),
url('iconfont.woff?t=1711963254221') format('woff'),
url('iconfont.ttf?t=1711963254221') 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,42 @@
-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";
}
.itsm-duration:before {
content: "\e913";
}
.itsm-workload:before {
content: "\e912";
}
.caise-VPC:before {
content: "\e910";
}
@@ -257,10 +293,6 @@
content: "\e8d5";
}
.ops-setting-auth-selected:before {
content: "\e8d4";
}
.itsm-knowledge2:before {
content: "\e8d2";
}
@@ -489,10 +521,6 @@
content: "\e89c";
}
.ops-setting-duty-selected:before {
content: "\e89b";
}
.datainsight-sequential:before {
content: "\e899";
}
@@ -657,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";
}
@@ -1061,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";
}
@@ -1089,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";
}
@@ -1113,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";
}
@@ -1129,10 +1089,6 @@
content: "\e806";
}
.ops-cmdb-screen-selected:before {
content: "\e807";
}
.ops-cmdb-resource:before {
content: "\e808";
}
@@ -1481,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";
}
@@ -1929,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";
}
@@ -1965,10 +1905,6 @@
content: "\e736";
}
.ops-setting-companyStructure-selected:before {
content: "\e72b";
}
.ops-setting-companyStructure:before {
content: "\e72c";
}
@@ -1977,10 +1913,6 @@
content: "\e72d";
}
.ops-setting-companyInfo-selected:before {
content: "\e72e";
}
.ops-email:before {
content: "\e61a";
}
@@ -3021,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";
}
@@ -3037,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,69 @@
"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",
"font_class": "veops-show",
"unicode": "e914",
"unicode_decimal": 59668
},
{
"icon_id": "39926816",
"name": "itsm-duration",
"font_class": "itsm-duration",
"unicode": "e913",
"unicode_decimal": 59667
},
{
"icon_id": "39926833",
"name": "itsm-workload (1)",
"font_class": "itsm-workload",
"unicode": "e912",
"unicode_decimal": 59666
},
{
"icon_id": "39782649",
"name": "VPC",
@@ -432,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)",
@@ -838,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",
@@ -1132,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",
@@ -1839,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",
@@ -1853,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",
@@ -1888,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",
@@ -1909,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",
@@ -1930,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",
@@ -1958,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",
@@ -2574,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",
@@ -3358,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",
@@ -3372,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",
@@ -3421,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",
@@ -3442,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",
@@ -5269,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",
@@ -5297,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",
@@ -5311,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",
@@ -5325,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",
@@ -5346,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",
@@ -5360,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",
@@ -5374,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

@@ -1,14 +0,0 @@
<svg width="1em" height="1em" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 0H2.5V1.25H1.25V2.5H0V1C0 0.447715 0.447715 0 1 0ZM0 7.5V9C0 9.55229 0.447715 10 1 10H2.5V8.75H1.25V7.5H0ZM8.75 7.5V8.75H7.5V10H9C9.55229 10 10 9.55228 10 9V7.5H8.75ZM10 2.5V1C10 0.447715 9.55228 0 9 0H7.5V1.25H8.75V2.5H10Z" fill="url(#paint0_linear_124_16807)"/>
<rect x="2.5" y="3.125" width="5" height="3.75" fill="url(#paint1_linear_124_16807)"/>
<defs>
<linearGradient id="paint0_linear_124_16807" x1="5" y1="0" x2="5" y2="10" gradientUnits="userSpaceOnUse">
<stop stop-color="#4F84FF"/>
<stop offset="1" stop-color="#85CBFF"/>
</linearGradient>
<linearGradient id="paint1_linear_124_16807" x1="5" y1="3.125" x2="5" y2="6.875" gradientUnits="userSpaceOnUse">
<stop stop-color="#4F84FF"/>
<stop offset="1" stop-color="#85CBFF"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 916 B

View File

@@ -1,14 +0,0 @@
<svg width="1em" height="1em" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="2.5" y="2.5" width="5" height="5" rx="0.5" fill="url(#paint0_linear_124_16808)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1 0C0.447715 0 0 0.447715 0 1V9C0 9.55229 0.447715 10 1 10H9C9.55229 10 10 9.55228 10 9V1C10 0.447715 9.55228 0 9 0H1ZM8.75 1.25H1.25V8.75H8.75V1.25Z" fill="url(#paint1_linear_124_16808)"/>
<defs>
<linearGradient id="paint0_linear_124_16808" x1="5" y1="2.5" x2="5" y2="7.5" gradientUnits="userSpaceOnUse">
<stop stop-color="#5187FF"/>
<stop offset="1" stop-color="#84C9FF"/>
</linearGradient>
<linearGradient id="paint1_linear_124_16808" x1="5" y1="0" x2="5" y2="10" gradientUnits="userSpaceOnUse">
<stop stop-color="#5187FF"/>
<stop offset="1" stop-color="#84C9FF"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 840 B

View File

@@ -1,9 +0,0 @@
<svg width="1em" height="1em" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.56845 8.8409C3.06335 8.78963 1.86719 8.05799 2.06279 6.48243C2.1538 5.75105 2.64549 5.3214 3.34457 5.16041C3.67173 5.08909 4.00806 5.06954 4.34128 5.10247C4.40203 5.10811 4.44843 5.11401 4.47689 5.11837L4.51586 5.12631C4.64379 5.15574 4.77263 5.18104 4.90218 5.20219C5.26786 5.2651 5.63914 5.28941 6.0099 5.27474C6.8046 5.23219 7.21015 4.97429 7.23092 4.41672C7.25424 3.79429 6.76332 3.29619 5.86659 2.91832C5.52815 2.77793 5.17843 2.66645 4.82117 2.58506C4.70325 2.55755 4.58482 2.53328 4.46587 2.51226C4.30323 2.94847 3.9867 3.31016 3.57591 3.5292C3.16512 3.74824 2.68841 3.80952 2.23557 3.70149C1.90324 3.61651 1.60053 3.44214 1.36029 3.1973C1.12004 2.95245 0.951447 2.64649 0.872793 2.3126C0.794138 1.97872 0.808429 1.62967 0.914116 1.30333C1.0198 0.976995 1.21285 0.685836 1.4723 0.461451C1.73176 0.237065 2.04771 0.0880244 2.38588 0.0305017C2.72404 -0.0270211 3.07151 0.00917138 3.39056 0.135152C3.70961 0.261132 3.98807 0.472088 4.19571 0.745127C4.40335 1.01817 4.53225 1.34286 4.56841 1.68397C4.6812 1.70269 4.83374 1.73217 5.01524 1.77421C5.42003 1.86601 5.81625 1.99216 6.1996 2.15131C7.38191 2.64966 8.1156 3.39463 8.07638 4.4462C8.03639 5.53187 7.23425 6.04253 6.0563 6.10533C5.62418 6.12373 5.19132 6.09614 4.76503 6.02304C4.61925 5.99997 4.47398 5.9716 4.32923 5.93793C4.30731 5.93532 4.28534 5.9331 4.26335 5.93127C4.02033 5.90687 3.77501 5.92018 3.53606 5.97075C3.15153 6.05893 2.94311 6.24146 2.90056 6.58267C2.78725 7.49504 3.47915 7.94443 5.42694 8.00416C5.44492 7.65558 5.5586 7.3187 5.75548 7.03049C5.95237 6.74229 6.22485 6.51389 6.54303 6.37039C6.8612 6.22689 7.21277 6.17383 7.55912 6.21703C7.90548 6.26023 8.23323 6.39802 8.50641 6.61528C8.77959 6.83254 8.98763 7.12086 9.10769 7.4486C9.22775 7.77634 9.25519 8.13082 9.187 8.47314C9.11881 8.81545 8.95763 9.13235 8.72114 9.38907C8.48465 9.64578 8.18201 9.83237 7.84643 9.92836C7.39921 10.0556 6.92094 10.0153 6.50129 9.81515C6.08164 9.61495 5.74941 9.26855 5.56691 8.8409H5.56845Z" fill="url(#paint0_linear_124_16804)"/>
<defs>
<linearGradient id="paint0_linear_124_16804" x1="5.02318" y1="0.00390625" x2="5.02318" y2="10.0013" gradientUnits="userSpaceOnUse">
<stop stop-color="#497DFF"/>
<stop offset="1" stop-color="#8CD5FF"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -1,9 +0,0 @@
<svg width="1em" height="1em" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.01211 4.50621L2.7769 5.74077C2.712 5.80565 2.66051 5.88268 2.62538 5.96747C2.59025 6.05225 2.57217 6.14313 2.57217 6.2349C2.57217 6.32668 2.59025 6.41755 2.62538 6.50234C2.66051 6.58712 2.712 6.66416 2.7769 6.72904L3.27085 7.223C3.33573 7.28791 3.41276 7.3394 3.49754 7.37453C3.58232 7.40966 3.67319 7.42774 3.76496 7.42774C3.85674 7.42774 3.94761 7.40966 4.03239 7.37453C4.11717 7.3394 4.1942 7.28791 4.25908 7.223L5.49394 5.98775C5.6237 6.1175 5.72663 6.27155 5.79686 6.44109C5.86708 6.61063 5.90323 6.79234 5.90323 6.97585C5.90323 7.15935 5.86708 7.34106 5.79686 7.5106C5.72663 7.68014 5.6237 7.83419 5.49394 7.96394L3.76479 9.69316C3.56827 9.88963 3.30176 10 3.02387 10C2.74599 10 2.47948 9.88963 2.28296 9.69316L0.306832 7.71696C0.110368 7.52043 0 7.25391 0 6.97602C0 6.69813 0.110368 6.43161 0.306832 6.23508L2.03599 4.50586C2.16574 4.3761 2.31978 4.27317 2.48931 4.20294C2.65884 4.13271 2.84055 4.09657 3.02405 4.09657C3.20755 4.09657 3.38925 4.13271 3.55879 4.20294C3.72832 4.27317 3.88236 4.3761 4.01211 4.50586V4.50621ZM5.98789 5.49414L7.2231 4.25923C7.288 4.19435 7.33949 4.11732 7.37462 4.03253C7.40975 3.94775 7.42783 3.85687 7.42783 3.7651C7.42783 3.67332 7.40975 3.58245 7.37462 3.49766C7.33949 3.41288 7.288 3.33584 7.2231 3.27096L6.72915 2.777C6.66428 2.71209 6.58724 2.6606 6.50246 2.62547C6.41768 2.59034 6.32681 2.57226 6.23504 2.57226C6.14326 2.57226 6.05239 2.59034 5.96761 2.62547C5.88283 2.6606 5.8058 2.71209 5.74092 2.777L4.50606 4.01225C4.3763 3.8825 4.27337 3.72845 4.20314 3.55891C4.13292 3.38937 4.09677 3.20766 4.09677 3.02415C4.09677 2.84065 4.13292 2.65894 4.20314 2.4894C4.27337 2.31986 4.3763 2.16581 4.50606 2.03606L6.23521 0.306843C6.43173 0.110371 6.69824 0 6.97613 0C7.25401 0 7.52052 0.110371 7.71704 0.306843L9.69317 2.28304C9.88963 2.47957 10 2.74609 10 3.02398C10 3.30187 9.88963 3.56839 9.69317 3.76492L7.96401 5.49414C7.83426 5.6239 7.68022 5.72683 7.51069 5.79706C7.34116 5.86729 7.15945 5.90343 6.97595 5.90343C6.79245 5.90343 6.61075 5.86729 6.44121 5.79706C6.27168 5.72683 6.11764 5.6239 5.98789 5.49414ZM3.51817 5.9881L5.98789 3.51829C6.05339 3.45274 6.14225 3.4159 6.23491 3.41586C6.32758 3.41583 6.41646 3.45261 6.48201 3.51812C6.54755 3.58362 6.5844 3.67248 6.58443 3.76515C6.58446 3.85782 6.54768 3.9467 6.48218 4.01225L4.01211 6.48206C3.94661 6.54761 3.85775 6.58445 3.76509 6.58449C3.67242 6.58452 3.58354 6.54774 3.51799 6.48223C3.45245 6.41673 3.4156 6.32787 3.41557 6.2352C3.41554 6.14253 3.45232 6.05365 3.51782 5.9881H3.51817Z" fill="url(#paint0_linear_124_16775)"/>
<defs>
<linearGradient id="paint0_linear_124_16775" x1="5" y1="0" x2="5" y2="10" gradientUnits="userSpaceOnUse">
<stop stop-color="#5A85FF"/>
<stop offset="1" stop-color="#8DD8FF"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -1,9 +0,0 @@
<svg width="1em" height="1em" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.31822 4.16667H2.54549V2.5C2.54549 1.11458 3.63981 0 5.00003 0C6.36026 0 7.45458 1.11458 7.45458 2.5V4.16667H8.68185C8.90685 4.16667 9.09094 4.35417 9.09094 4.58333V9.58333C9.09094 9.8125 8.90685 10 8.68185 10H1.31822C1.09322 10 0.909124 9.8125 0.909124 9.58333V4.58333C0.909124 4.35417 1.09322 4.16667 1.31822 4.16667ZM5.00003 7.91667C5.45003 7.91667 5.81822 7.54167 5.81822 7.08333C5.81822 6.625 5.45003 6.25 5.00003 6.25C4.55003 6.25 4.18185 6.625 4.18185 7.08333C4.18185 7.54167 4.55003 7.91667 5.00003 7.91667ZM3.36367 4.16667H6.6364V2.5C6.6364 1.58333 5.90003 0.833333 5.00003 0.833333C4.10003 0.833333 3.36367 1.58333 3.36367 2.5V4.16667Z" fill="url(#paint0_linear_124_16805)"/>
<defs>
<linearGradient id="paint0_linear_124_16805" x1="5.00003" y1="0" x2="5.00003" y2="10" gradientUnits="userSpaceOnUse">
<stop stop-color="#4D82FF"/>
<stop offset="1" stop-color="#88CFFF"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1022 B

View File

@@ -1,9 +0,0 @@
<svg width="1em" height="1em" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.91242 9.46382C3.91242 9.57428 3.82288 9.66382 3.71242 9.66382H2.35075C2.2403 9.66382 2.15075 9.57428 2.15075 9.46382V3.55962C2.15075 3.44916 2.06121 3.35962 1.95075 3.35962H0.539905C0.354312 3.35962 0.268806 3.12879 0.40961 3.00788L3.58212 0.283626C3.71182 0.172253 3.91242 0.264405 3.91242 0.43536V9.46382ZM6.08758 0.567715C6.08758 0.457258 6.17712 0.367716 6.28758 0.367716H7.64925C7.7597 0.367716 7.84925 0.457259 7.84925 0.567716V6.4411C7.84925 6.55156 7.93879 6.6411 8.04925 6.6411H9.46001C9.64561 6.6411 9.73111 6.87195 9.59029 6.99285L6.41786 9.71645C6.28816 9.8278 6.08758 9.73565 6.08758 9.5647V0.567715Z" fill="url(#paint0_linear_124_16806)"/>
<defs>
<linearGradient id="paint0_linear_124_16806" x1="5" y1="0" x2="5" y2="10" gradientUnits="userSpaceOnUse">
<stop stop-color="#5A85FF"/>
<stop offset="1" stop-color="#8DD8FF"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 979 B

View File

@@ -1,9 +0,0 @@
<svg width="1em" height="1em" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.51961 6.8937V10H4.48V6.8937H1.76732C1.65823 6.8937 1.56223 6.85372 1.48369 6.77237C1.40522 6.69504 1.3621 6.5915 1.36369 6.48421C1.36369 5.95891 1.52769 5.48566 1.85895 5.06411C2.18986 4.64428 2.56258 4.43334 2.97893 4.43334V1.64277C2.75966 1.64277 2.57167 1.56142 2.41022 1.39873C2.25355 1.24349 2.16738 1.0362 2.17022 0.821384C2.17022 0.598718 2.25022 0.407762 2.41022 0.244037C2.56912 0.0827244 2.7593 0 2.97893 0H7.01959C7.23885 0 7.42685 0.0813456 7.5883 0.244037C7.74721 0.406728 7.82866 0.598718 7.82866 0.821384C7.82866 1.04405 7.74866 1.23501 7.58867 1.39873C7.4283 1.5628 7.23885 1.64277 7.01959 1.64277V4.43196C7.43594 4.43196 7.81012 4.64291 8.13956 5.06273C8.46631 5.47151 8.64098 5.97137 8.63628 6.48421C8.63628 6.59486 8.59701 6.6924 8.51665 6.77237C8.43665 6.85234 8.34211 6.8937 8.23302 6.8937H5.51998H5.51961Z" fill="url(#paint0_linear_124_16803)"/>
<defs>
<linearGradient id="paint0_linear_124_16803" x1="5.00001" y1="0" x2="5.00001" y2="10" gradientUnits="userSpaceOnUse">
<stop stop-color="#5A85FF"/>
<stop offset="1" stop-color="#8DD8FF"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -69,12 +69,11 @@ export default {
</script>
<style lang="less">
@import '~@/style/static.less';
.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

@@ -230,7 +230,6 @@ export default {
</script>
<style lang="less">
@import '~@/style/static.less';
.employee-transfer {
width: 100%;
.vue-treeselect__multi-value-item-container {
@@ -263,7 +262,6 @@ export default {
</style>
<style lang="less" scoped>
@import '~@/style/static.less';
.employee-transfer {
display: flex;
justify-content: space-between;
@@ -300,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

@@ -146,7 +146,6 @@ export default {
</script>
<style lang="less" scoped>
@import '~@/style/static.less';
.regex-select {
width: 100%;
height: 300px;
@@ -162,7 +161,7 @@ export default {
cursor: pointer;
&-selected,
&:hover {
color: #custom_colors[color_1];
color: @primary-color;
}
}
}
@@ -180,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

@@ -126,7 +126,6 @@ export default {
</script>
<style lang="less" scoped>
@import '~@/style/static.less';
.role-transfer {
display: flex;
justify-content: space-between;
@@ -186,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

@@ -60,7 +60,6 @@ export default {
</script>
<style lang="less" scoped>
@import '~@/style/static.less';
.sidebar-list-item {
.ops_popover_item();
margin: 2px 0;
@@ -94,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

@@ -52,7 +52,6 @@ export default {
</script>
<style lang="less" scoped>
@import '~@/style/static.less';
.two-column-layout {
margin-bottom: -24px;

View File

@@ -4,34 +4,22 @@
@click="jumpTo"
v-if="showTitle && !collapsed"
style="width: 100%; height: 100%; cursor: pointer"
:src="file_name ? `/api/common-setting/v1/file/${file_name}` : require('@/assets/logo_VECMDB.png')"
:src="require('@/assets/logo_VECMDB.png')"
/>
<img
@click="jumpTo"
v-else
style="width: 32px; height: 32px; margin-left: 24px; cursor: pointer"
:src="small_file_name ? `/api/common-setting/v1/file/${small_file_name}` : require('@/assets/logo.png')"
:src="require('@/assets/logo.png')"
/>
<!-- <logo-svg/> -->
<!-- <img v-if="showTitle" style="width:92px;height: 32px" src="@/assets/OneOps.png" /> -->
<!-- <h1 v-if="showTitle">{{ title }}</h1> -->
</div>
</template>
<script>
// import LogoSvg from '@/assets/logo.svg?inline'
import { mapState } from 'vuex'
export default {
name: 'Logo',
components: {
// LogoSvg,
},
computed: {
...mapState({
file_name: (state) => state.logo.file_name,
small_file_name: (state) => state.logo.small_file_name,
}),
},
components: {},
computed: {},
props: {
title: {
type: String,

View File

@@ -96,10 +96,9 @@ export default {
</script>
<style lang="less">
@import '~@/style/static.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 {
@@ -118,7 +117,7 @@ export default {
.locale {
cursor: pointer;
&:hover {
color: #custom_colors[color_1];
color: @primary-color;
}
}
</style>

View File

@@ -9,25 +9,11 @@
import gridSvg from '@/assets/icons/grid.svg?inline'
import top_agent from '@/assets/icons/top_agent.svg?inline'
import top_acl from '@/assets/icons/top_acl.svg?inline'
import ops_default_show from '@/assets/icons/ops-default_show.svg?inline'
import ops_is_choice from '@/assets/icons/ops-is_choice.svg?inline'
import ops_is_index from '@/assets/icons/ops-is_index.svg?inline'
import ops_is_link from '@/assets/icons/ops-is_link.svg?inline'
import ops_is_password from '@/assets/icons/ops-is_password.svg?inline'
import ops_is_sortable from '@/assets/icons/ops-is_sortable.svg?inline'
import ops_is_unique from '@/assets/icons/ops-is_unique.svg?inline'
import ops_move_icon from '@/assets/icons/ops-move-icon.svg?inline'
export {
gridSvg,
top_agent,
top_acl,
ops_default_show,
ops_is_choice,
ops_is_index,
ops_is_link,
ops_is_password,
ops_is_sortable,
ops_is_unique,
ops_move_icon
}

View File

@@ -0,0 +1,14 @@
import './highlight.less'
const highlight = (el, binding) => {
if (binding.value.value) {
let testValue = `${binding.value.value}`
if (['(', ')', '$'].includes(testValue)) {
testValue = `\\${testValue}`
}
const regex = new RegExp(`(${testValue})`, 'gi')
el.innerHTML = el.innerText.replace(regex, `<span class='${binding.value.class ?? 'ops-text-highlight'}'>$1</span>`)
}
}
export default highlight

View File

@@ -0,0 +1,5 @@
@import '~@/style/static.less';
.ops-text-highlight {
background-color: @primary-color_3;
}

View File

@@ -0,0 +1,12 @@
import hightlight from './highlight'
const install = function (Vue) {
Vue.directive('hightlight', hightlight)
}
if (window.Vue) {
window.hightlight = hightlight
Vue.use(install); // eslint-disable-line
}
hightlight.install = install
export default hightlight

View File

@@ -0,0 +1,13 @@
import waves from './waves'
const install = function (Vue) {
Vue.directive('waves', waves)
}
if (window.Vue) {
window.waves = waves
Vue.use(install); // eslint-disable-line
}
waves.install = install
export default waves

View File

@@ -0,0 +1,26 @@
.waves-ripple {
position: absolute;
border-radius: 100%;
background-color: rgba(0, 0, 0, 0.15);
background-clip: padding-box;
pointer-events: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-transform: scale(0);
-ms-transform: scale(0);
transform: scale(0);
opacity: 1;
}
.waves-ripple.z-active {
opacity: 0;
-webkit-transform: scale(2);
-ms-transform: scale(2);
transform: scale(2);
-webkit-transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;
transition: opacity 1.2s ease-out, transform 0.6s ease-out;
transition: opacity 1.2s ease-out, transform 0.6s ease-out, -webkit-transform 0.6s ease-out;
}

View File

@@ -0,0 +1,72 @@
import './waves.css'
const context = '@@wavesContext'
function handleClick(el, binding) {
function handle(e) {
const customOpts = Object.assign({}, binding.value)
const opts = Object.assign({
ele: el, // 波纹作用元素
type: 'hit', // hit 点击位置扩散 center中心点扩展
color: 'rgba(0, 0, 0, 0.15)' // 波纹颜色
},
customOpts
)
const target = opts.ele
if (target) {
target.style.position = 'relative'
target.style.overflow = 'hidden'
const rect = target.getBoundingClientRect()
let ripple = target.querySelector('.waves-ripple')
if (!ripple) {
ripple = document.createElement('span')
ripple.className = 'waves-ripple'
ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px'
target.appendChild(ripple)
} else {
ripple.className = 'waves-ripple'
}
switch (opts.type) {
case 'center':
ripple.style.top = rect.height / 2 - ripple.offsetHeight / 2 + 'px'
ripple.style.left = rect.width / 2 - ripple.offsetWidth / 2 + 'px'
break
default:
ripple.style.top =
(e.pageY - rect.top - ripple.offsetHeight / 2 - document.documentElement.scrollTop ||
document.body.scrollTop) + 'px'
ripple.style.left =
(e.pageX - rect.left - ripple.offsetWidth / 2 - document.documentElement.scrollLeft ||
document.body.scrollLeft) + 'px'
}
ripple.style.backgroundColor = opts.color
ripple.className = 'waves-ripple z-active'
return false
}
}
if (!el[context]) {
el[context] = {
removeHandle: handle
}
} else {
el[context].removeHandle = handle
}
return handle
}
export default {
bind(el, binding) {
el.addEventListener('click', handleClick(el, binding), false)
},
update(el, binding) {
el.removeEventListener('click', el[context].removeHandle, false)
el.addEventListener('click', handleClick(el, binding), false)
},
unbind(el) {
el.removeEventListener('click', el[context].removeHandle, false)
el[context] = null
delete el[context]
}
}

View File

@@ -31,7 +31,6 @@ router.beforeEach(async (to, from, next) => {
store.dispatch("loadAllUsers")
store.dispatch("loadAllEmployees")
store.dispatch("loadAllDepartments")
store.dispatch("getCompanyInfo")
store.dispatch('GenerateRoutes', { roles }).then(() => {
router.addRoutes(store.getters.appRoutes)
const redirect = decodeURIComponent(from.query.redirect || to.path)

View File

@@ -233,7 +233,6 @@ export default {
</script>
<style lang="less" scoped>
@import '~@/style/static.less';
.acl-history {
border-radius: @border-radius-box;

View File

@@ -32,7 +32,6 @@ export default {
</script>
<style lang="less" scoped>
@import '~@/style/static.less';
.acl-operation-history {
border-radius: @border-radius-box;

View File

@@ -189,7 +189,6 @@ export default {
</script>
<style lang="less" scoped>
@import '~@/style/static.less';
.acl-resource-types {
border-radius: @border-radius-box;

View File

@@ -352,7 +352,6 @@ export default {
</script>
<style lang="less" scoped>
@import '~@/style/static.less';
.acl-resources {
border-radius: @border-radius-box;

View File

@@ -285,7 +285,6 @@ export default {
</script>
<style lang="less">
@import '~@/style/static.less';
.acl-roles {
border-radius: @border-radius-box;

View File

@@ -88,7 +88,6 @@ export default {
</script>
<style lang="less">
@import '~@/style/static.less';
.acl-secret-key {
background-color: #fff;

View File

@@ -320,7 +320,6 @@ export default {
}
</script>
<style lang="less" scoped>
@import '~@/style/static.less';
.acl-trigger {
border-radius: @border-radius-box;

View File

@@ -188,7 +188,6 @@ export default {
</script>
<style lang="less" scoped>
@import '~@/style/static.less';
.acl-users {
border-radius: @border-radius-box;

View File

@@ -73,3 +73,11 @@ export function deleteCIRelationView(firstCiId, secondCiId, data) {
data
})
}
export function searchCIRelationFull(params) {
return axios({
url: `/v0.1/ci_relations/search/full`,
method: 'GET',
params,
})
}

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

@@ -221,7 +221,6 @@ export default {
</script>
<style lang="less">
@import '~@/style/static.less';
.cmdb-transfer {
.ant-transfer-list {
@@ -231,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;
}
@@ -259,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;
}
@@ -272,7 +271,7 @@ export default {
font-size: 12px;
color: #cacdd9;
&:hover {
color: #custom_colors[color_1];
color: @primary-color;
}
}
.move-icon {
@@ -292,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
@@ -311,7 +311,6 @@ export default {
</script>
<style lang="less" scoped>
@import '~@/style/static.less';
.cmdb-grant {
position: relative;
padding: 0 20px;
@@ -324,7 +323,6 @@ export default {
</style>
<style lang="less">
@import '~@/style/static.less';
.cmdb-grant {
.grant-button {

View File

@@ -59,7 +59,6 @@ export default {
</script>
<style lang="less" scoped>
@import '~@/style/static.less';
.read-checkbox {
.read-checkbox-half-checked {
width: 16px;
@@ -76,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

@@ -49,6 +49,8 @@
import _ from 'lodash'
import '@wangeditor/editor/dist/css/style.css'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { i18nChangeLanguage } from '@wangeditor/editor'
export default {
name: 'NoticeContent',
components: { Editor, Toolbar },
@@ -76,6 +78,10 @@ export default {
if (editor == null) return
editor.destroy() // When the component is destroyed, destroy the editor in time
},
beforeCreate() {
const locale = this.$i18n.locale === 'zh' ? 'zh-CN' : 'en'
i18nChangeLanguage(locale)
},
methods: {
onCreated(editor) {
this.editor = Object.seal(editor) // Be sure to use Object.seal(), otherwise an error will be reported
@@ -112,7 +118,6 @@ export default {
</script>
<style lang="less" scoped>
@import '~@/style/static.less';
.notice-content {
width: 100%;
& &-main {
@@ -176,8 +181,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;
}
}
}
@@ -186,11 +191,10 @@ export default {
</style>
<style lang="less">
@import '~@/style/static.less';
.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

@@ -95,12 +95,12 @@
isFocusExpression = false
}
"
class="ci-searchform-expression"
:class="{ 'ci-searchform-expression': true, 'ci-searchform-expression-has-value': expression }"
:style="{ width }"
:placeholder="placeholder"
@keyup.enter="emitRefresh"
>
<ops-icon slot="suffix" type="veops-copy" @click="handleCopyExpression" />
<a-icon slot="suffix" type="check-circle" @click="handleCopyExpression" />
</a-input>
<slot></slot>
</a-space>
@@ -198,6 +198,9 @@ export default {
if (this.type === 'resourceSearch') {
this.getCITypeGroups()
}
if (this.typeId) {
this.currenCiType = [this.typeId]
}
},
methods: {
// toggleAdvanced() {
@@ -264,7 +267,6 @@ export default {
}
</script>
<style lang="less">
@import '~@/style/static.less';
@import '../../views/index.less';
.ci-searchform-expression {
> input {
@@ -285,6 +287,9 @@ export default {
cursor: pointer;
}
}
.ci-searchform-expression-has-value .ant-input-suffix {
color: @func-color_3;
}
.cmdb-search-form {
.ant-form-item-label {
overflow: hidden;
@@ -295,8 +300,6 @@ export default {
</style>
<style lang="less" scoped>
@import '~@/style/static.less';
.search-form-bar {
margin-bottom: 20px;
display: flex;

View File

@@ -244,7 +244,6 @@ export default {
</script>
<style lang="less" scoped>
@import '~@/style/static.less';
.cmdb-subscribe-drawer {
.cmdb-subscribe-drawer-container {
@@ -254,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;
@@ -265,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;
@@ -273,7 +272,7 @@ export default {
cursor: pointer;
font-size: 12px;
&:hover {
color: #custom_colors[color_1];
color: @primary-color;
}
}
}
@@ -315,10 +314,9 @@ export default {
</style>
<style lang="less">
@import '~@/style/static.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

@@ -4,6 +4,7 @@ const cmdb_en = {
configTable: 'Config Table',
menu: {
views: 'Views',
resources: 'Resources',
config: 'Configuration',
backend: 'Management',
ciTable: 'Resource Views',
@@ -69,9 +70,9 @@ const cmdb_en = {
adAutoInLib: 'Save as CI auto',
adInterval: 'Collection frequency',
byInterval: 'by interval',
allNodes: 'All nodes',
specifyNodes: 'Specify Node',
specifyNodesTips: 'Please fill in the specify node!',
allNodes: 'All Instances',
specifyNodes: 'Specify Instance',
specifyNodesTips: 'Please fill in the specify instance!',
username: 'Username',
password: 'Password',
link: 'Link',
@@ -144,7 +145,7 @@ const cmdb_en = {
triggerDataChange: 'Data changes',
triggerDate: 'Date attribute',
triggerEnable: 'Turn on',
descInput: 'Please enter remarks',
descInput: 'Please enter descriptions',
triggerCondition: 'Triggering conditions',
addInstance: 'Add new instance',
deleteInstance: 'Delete instance',
@@ -194,7 +195,10 @@ const cmdb_en = {
attributeAssociationTip3: 'Two Attributes must be selected',
attributeAssociationTip4: 'Please select a attribute from Source CIType',
attributeAssociationTip5: 'Please select a attribute from Target CIType',
show: 'show attribute',
setAsShow: 'Set as show attribute',
cancelSetAsShow: 'Cancel show attribute',
showTips: 'The names of nodes in the service tree and topology view'
},
components: {
unselectAttributes: 'Unselected',
@@ -399,7 +403,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',
@@ -491,7 +503,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',
@@ -509,7 +522,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',
@@ -530,7 +557,8 @@ if __name__ == "__main__":
peopleHasRead: 'Personnel authorized to read:',
authorizationPolicy: 'CI Authorization Policy:',
idAuthorizationPolicy: 'Authorized by node:',
view: 'View permissions'
view: 'View permissions',
searchTips: 'Search in service tree'
},
tree: {
tips1: 'Please go to Preference page first to complete your subscription!',

View File

@@ -4,8 +4,9 @@ const cmdb_zh = {
configTable: '配置表格',
menu: {
views: '视图',
resources: '资源',
config: '配置',
backend: '管理',
backend: '管理',
ciTable: '资源数据',
ciTree: '资源层级',
ciSearch: '资源搜索',
@@ -69,9 +70,9 @@ const cmdb_zh = {
adAutoInLib: '自动入库',
adInterval: '采集频率',
byInterval: '按间隔',
allNodes: '所有节点',
specifyNodes: '指定节点',
specifyNodesTips: '请填写指定节点',
allNodes: '所有实例',
specifyNodes: '指定实例',
specifyNodesTips: '请填写指定实例',
username: '用户名',
password: '密码',
link: '链接',
@@ -144,7 +145,7 @@ const cmdb_zh = {
triggerDataChange: '数据变更',
triggerDate: '日期属性',
triggerEnable: '开启',
descInput: '请输入备注',
descInput: '请输入描述',
triggerCondition: '触发条件',
addInstance: '新增实例',
deleteInstance: '删除实例',
@@ -194,6 +195,10 @@ const cmdb_zh = {
attributeAssociationTip3: '属性关联必须选择两个属性',
attributeAssociationTip4: '请选择原模型属性',
attributeAssociationTip5: '请选择目标模型属性',
show: '展示属性',
setAsShow: '设置为展示属性',
cancelSetAsShow: '取消设置为展示属性',
showTips: '服务树和拓扑视图里节点的名称'
},
components: {
unselectAttributes: '未选属性',
@@ -398,7 +403,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: '新增关系类型',
@@ -490,7 +503,8 @@ if __name__ == "__main__":
copyFailed: '复制失败!',
noLevel: '无层级关系!',
batchAddRelation: '批量添加关系',
history: '操作历史',
history: '变更记录',
relITSM: '关联工单',
topo: '拓扑',
table: '表格',
m2mTips: '当前模型关系为多对多,请前往关系视图进行增删操作',
@@ -508,7 +522,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: '移除',
@@ -529,7 +557,8 @@ if __name__ == "__main__":
peopleHasRead: '当前有查看权限的人员:',
authorizationPolicy: '实例授权策略:',
idAuthorizationPolicy: '按节点授权的:',
view: '查看权限'
view: '查看权限',
searchTips: '在服务树中筛选'
},
tree: {
tips1: '请先到 我的订阅 页面完成订阅!',

View File

@@ -13,19 +13,19 @@ 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')
},
{
path: '/cmdb/disabled1',
name: 'cmdb_disabled1',
meta: { title: 'cmdb.menu.views', disabled: true },
meta: { title: 'cmdb.menu.resources', disabled: true },
},
{
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

@@ -147,7 +147,6 @@ export default {
}
</script>
<style lang="less">
@import '~@/style/static.less';
@import '../index.less';
.cmdb-batch-upload-label {
color: @text-color_1;
@@ -159,7 +158,6 @@ export default {
}
</style>
<style lang="less" scoped>
@import '~@/style/static.less';
.cmdb-batch-upload {
margin-bottom: -24px;

View File

@@ -113,7 +113,6 @@ export default {
}
</script>
<style lang="less" scoped>
@import '~@/style/static.less';
.cmdb-batch-upload-table {
height: 200px;

View File

@@ -83,7 +83,6 @@ export default {
</script>
<style lang="less">
@import '~@/style/static.less';
.cmdb-batch-upload-dragger {
height: auto;
@@ -112,7 +111,6 @@ export default {
}
</style>
<style lang="less" scoped>
@import '~@/style/static.less';
.cmdb-batch-upload-dragger {
position: relative;

View File

@@ -101,7 +101,6 @@ export default {
}
</script>
<style lang="less" scoped>
@import '~@/style/static.less';
.cmdb-batch-upload-result {
.cmdb-batch-upload-result-content {

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,8 +60,6 @@ export default {
</script>
<style lang="less" scoped>
@import '~@/style/static.less';
.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()
@@ -992,7 +1053,6 @@ export default {
</style>
<style lang="less" scoped>
@import '~@/style/static.less';
.cmdb-ci {
background-color: #fff;
padding: 20px;

View File

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

View File

@@ -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

@@ -150,11 +150,11 @@ export default {
computed: {
topoData() {
const ci_types_list = this.ci_types()
const unique_id = this.attributes().unique_id
const unique_name = this.attributes().unique
const _findCiType = ci_types_list.find((item) => item.id === this.typeId)
const unique_id = _findCiType.show_id || this.attributes().unique_id
const unique_name = _findCiType.show_name || this.attributes().unique
const _findUnique = this.attrList().find((attr) => attr.id === unique_id)
const unique_alias = _findUnique?.alias || _findUnique?.name || ''
const _findCiType = ci_types_list.find((item) => item.id === this.typeId)
const nodes = {
isRoot: true,
id: `Root_${this.typeId}`,
@@ -182,7 +182,11 @@ export default {
const edges = []
this.parentCITypes.forEach((parent) => {
const _findCiType = ci_types_list.find((item) => item.id === parent.id)
if (this.firstCIs[parent.name]) {
if (this.firstCIs[parent.name] && _findCiType) {
const unique_id = _findCiType.show_id || _findCiType.unique_id
const _findUnique = parent.attributes.find((attr) => attr.id === unique_id)
const unique_name = _findUnique?.name
const unique_alias = _findUnique?.alias || _findUnique?.name || ''
this.firstCIs[parent.name].forEach((parentCi) => {
nodes.children.push({
id: `${parentCi._id}`,
@@ -190,9 +194,9 @@ export default {
title: parent.alias || parent.name,
name: parent.name,
side: 'left',
unique_alias: parentCi.unique_alias,
unique_name: parentCi.unique,
unique_value: parentCi[parentCi.unique],
unique_alias,
unique_name,
unique_value: parentCi[unique_name],
children: [],
icon: _findCiType?.icon || '',
endpoints: [
@@ -221,7 +225,11 @@ export default {
})
this.childCITypes.forEach((child) => {
const _findCiType = ci_types_list.find((item) => item.id === child.id)
if (this.secondCIs[child.name]) {
if (this.secondCIs[child.name] && _findCiType) {
const unique_id = _findCiType.show_id || _findCiType.unique_id
const _findUnique = child.attributes.find((attr) => attr.id === unique_id)
const unique_name = _findUnique?.name
const unique_alias = _findUnique?.alias || _findUnique?.name || ''
this.secondCIs[child.name].forEach((childCi) => {
nodes.children.push({
id: `${childCi._id}`,
@@ -229,9 +237,9 @@ export default {
title: child.alias || child.name,
name: child.name,
side: 'right',
unique_alias: childCi.unique_alias,
unique_name: childCi.unique,
unique_value: childCi[childCi.unique],
unique_alias,
unique_name,
unique_value: childCi[unique_name],
children: [],
icon: _findCiType?.icon || '',
endpoints: [
@@ -260,6 +268,24 @@ export default {
})
return { nodes, edges }
},
exsited_ci() {
const _exsited_ci = [this.typeId]
this.parentCITypes.forEach((parent) => {
if (this.firstCIs[parent.name]) {
this.firstCIs[parent.name].forEach((parentCi) => {
_exsited_ci.push(parentCi._id)
})
}
})
this.childCITypes.forEach((child) => {
if (this.secondCIs[child.name]) {
this.secondCIs[child.name].forEach((childCi) => {
_exsited_ci.push(childCi._id)
})
}
})
return _exsited_ci
},
},
inject: {
attrList: { from: 'attrList' },
@@ -278,6 +304,7 @@ export default {
await Promise.all([this.getParentCITypes(), this.getChildCITypes()])
Promise.all([this.getFirstCIs(), this.getSecondCIs()]).then(() => {
if (isFirst && this.$refs.ciDetailRelationTopo) {
this.$refs.ciDetailRelationTopo.exsited_ci = this.exsited_ci
this.$refs.ciDetailRelationTopo.setTopoData(this.topoData)
}
})
@@ -314,7 +341,6 @@ export default {
secondCIs[item.ci_type] = [item]
}
})
console.log(_.cloneDeep(secondCIs))
this.secondCIs = secondCIs
})
.catch((e) => {})
@@ -414,6 +440,7 @@ export default {
handleChangeActiveKey(e) {
if (e.target.value === '1') {
this.$nextTick(() => {
this.$refs.ciDetailRelationTopo.exsited_ci = this.exsited_ci
this.$refs.ciDetailRelationTopo.setTopoData(this.topoData)
})
}

View File

@@ -15,7 +15,7 @@
position: absolute;
background: #fff;
border: 1px solid #d9d9d9;
border-radius: 10px;
border-radius: 2px;
padding: 4px 8px;
width: 100px;
text-align: center;
@@ -74,13 +74,11 @@
}
.root {
width: 100px;
background: #2f54eb;
border: none;
border-radius: 5px;
font-weight: 500;
border-color: @primary-color;
font-weight: 700;
padding: 4px 8px;
.title {
color: #fff;
color: @primary-color;
}
}
}

View File

@@ -10,6 +10,7 @@
import _ from 'lodash'
import { TreeCanvas } from 'butterfly-dag'
import { searchCIRelation } from '@/modules/cmdb/api/CIRelation'
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
import Node from './node.js'
import 'butterfly-dag/dist/index.css'
@@ -87,7 +88,7 @@ export default {
this.canvas.focusCenterWithAnimate()
})
},
redrawData(res, sourceNode, side) {
async redrawData(res, sourceNode, side) {
const newNodes = []
const newEdges = []
if (!res.result.length) {
@@ -95,18 +96,24 @@ export default {
return
}
const ci_types_list = this.ci_types()
res.result.forEach((r) => {
for (let i = 0; i < res.result.length; i++) {
const r = res.result[i]
if (!this.exsited_ci.includes(r._id)) {
const _findCiType = ci_types_list.find((item) => item.id === r._type)
const { attributes } = await getCITypeAttributesById(_findCiType.id)
const unique_id = _findCiType.show_id || _findCiType.unique_id
const _findUnique = attributes.find((attr) => attr.id === unique_id)
const unique_name = _findUnique?.name
const unique_alias = _findUnique?.alias || _findUnique?.name || ''
newNodes.push({
id: `${r._id}`,
Class: Node,
title: r.ci_type_alias || r.ci_type,
name: r.ci_type,
side: side,
unique_alias: r.unique_alias,
unique_name: r.unique,
unique_value: r[r.unique],
unique_alias,
unique_name,
unique_value: r[unique_name],
children: [],
icon: _findCiType?.icon || '',
endpoints: [
@@ -131,7 +138,8 @@ export default {
targetNode: side === 'right' ? `${r._id}` : sourceNode,
type: 'endpoint',
})
})
}
const { nodes, edges } = this.canvas.getDataMap()
// 删除原节点和边
this.canvas.removeNodes(nodes.map((node) => node.id))

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%' }">
<RelatedItsmTable ref="relatedITSMTable" :ci_id="ci._id" :ciHistory="ciHistory" :itsmInstalled="itsmInstalled" :attrList="attrList" />
</div>
</a-tab-pane>
</a-tabs>
<a-empty
v-else
@@ -100,11 +133,13 @@
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'
export default {
name: 'CiDetailTab',
components: {
@@ -113,6 +148,8 @@ export default {
CiDetailAttrContent,
CiDetailRelation,
TriggerTable,
RelatedItsmTable,
CiRollbackForm,
},
props: {
typeId: {
@@ -123,10 +160,15 @@ export default {
type: Array,
default: () => [],
},
attributeHistoryTableHeight: {
type: Number,
default: null
}
},
data() {
return {
ci: {},
item: [],
attributeGroups: [],
activeTabKey: 'tab_1',
rowSpanMap: {},
@@ -134,6 +176,8 @@ export default {
ciId: null,
ci_types: [],
hasPermission: true,
itsmInstalled: true,
tableHeight: this.attributeHistoryTableHeight || (this.$store.state.windowHeight - 120),
}
},
computed: {
@@ -179,6 +223,7 @@ export default {
}
this.ciId = ciId
await this.getCI()
await this.judgeItsmInstalled()
if (this.hasPermission) {
this.getAttributes()
this.getCIHistory()
@@ -203,7 +248,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 +397,11 @@ export default {
this.$message.error(this.$t('cmdb.ci.copyFailed'))
})
},
handleRollbackCI() {
this.$nextTick(() => {
this.$refs.ciRollbackForm.onOpen()
})
},
},
}
</script>

Some files were not shown because too many files have changed in this diff Show More