Compare commits

...

43 Commits

Author SHA1 Message Date
pycook
4117cf87ec Merge branch 'master' of github.com:veops/cmdb 2024-03-20 11:56:49 +08:00
pycook
9e0fe0b818 fix: custom dashboard 2024-03-20 11:56:39 +08:00
dagongren
2a8f1ab9a4 style:update global.less (#426) 2024-03-19 10:01:38 +08:00
pycook
c0fe99b8c7 release: 2.3.13 2024-03-18 20:35:51 +08:00
dagongren
42feb4b862 feat(cmdb-ui):service tree grant (#425) 2024-03-18 19:59:16 +08:00
pycook
482d34993b Dev api 0308 (#424)
* feat(api): grant by node in relation view

* fix(api): When removing attributes, remove the unique constraint

* feat(api): grant by service tree
2024-03-18 19:57:25 +08:00
simontigers
7ff309b8b8 fix(api): edit employee depart with rid=0 (#420) 2024-03-12 17:46:50 +08:00
rustrover
98eb47d44f fix: some typos (#415)
Signed-off-by: gcmutator <329964069@qq.com>
Co-authored-by: gcmutator <329964069@qq.com>
2024-03-11 15:04:38 +08:00
pycook
9ab0f624ef fix(api): remove ACL resources when deleting CIType (#414) 2024-03-08 16:31:03 +08:00
pycook
3f3eda8b3c fix(api): issule #412, unique value restrictions (#413) 2024-03-05 16:21:27 +08:00
pycook
f788adc8cf feat(api): multi-id search (#411)
_id:(id1;id2)
2024-03-04 15:15:34 +08:00
simontigers
693ae4ff05 fix: deploy init common (#407) 2024-03-01 17:21:32 +08:00
pycook
a1a9d99eb4 release: v2.3.12 2024-03-01 17:04:38 +08:00
dagongren
e045e0fb43 fix(cmdb-ui):to lowercase (#406) 2024-03-01 13:52:30 +08:00
pycook
09376dbd2b feat(api): CIType inheritance (#405) 2024-03-01 13:51:13 +08:00
dagongren
7fda5a1e7b feat(cmdb-ui):ci type inherit (#404) 2024-03-01 13:39:20 +08:00
dagongren
113b84763f feat:ci detail share (#403) 2024-02-27 16:13:28 +08:00
dagongren
190170acad fix(cmdb-ui):triggers webhook headers (#402) 2024-02-26 13:46:40 +08:00
pycook
513d2af4b8 feat(api): Remove many-to-many restrictions (#401) 2024-02-26 10:17:53 +08:00
pycook
4588bd8996 fix(api): db-setup commands (#399)
fix(api): db-setup commands
2024-02-23 11:05:11 +08:00
dagongren
082da5fade fix(cmdb-ui):resource search common attrs (#397) 2024-02-22 16:19:12 +08:00
pycook
013b116eb5 feat(acl): login channel add ssh options (#396) 2024-02-21 18:10:44 +08:00
simontigers
208d29165b fix: grant common perm after create new employee (#394) 2024-02-04 13:48:02 +08:00
dagongren
d510330cde fix(cmdb-ui):fix multiple default value (#395) 2024-02-04 11:49:44 +08:00
wang-liang0615
ea4f0fc2a5 fix(ui):login email-》username (#393) 2024-01-31 15:52:27 +08:00
pycook
9bcdaacdc4 docs: update init sql
docs: update init sql
2024-01-26 13:57:36 +08:00
pycook
5045581ddf feat(api): Auto-increment id can be used as primary key (#391) 2024-01-26 13:12:17 +08:00
simontigers
232913172c fix: change common_setting task queue (#390) 2024-01-25 17:39:52 +08:00
pycook
157e1809ed release: 2.3.11 2024-01-13 15:06:51 +08:00
pycook
ab8ccf7d1b ui: lint 2024-01-12 18:21:47 +08:00
wang-liang0615
ae1f0f6b4f lint regexSelect (#382) 2024-01-12 18:09:03 +08:00
wang-liang0615
ff47e0ade6 feat:citype regex check & pref:edit is_list (#380) 2024-01-12 17:09:44 +08:00
pycook
0dd272fb04 feat(api): password supports regular check 2024-01-12 16:56:10 +08:00
pycook
6bc5c1516d feat(api): Attributes support regular check (#379)
feat(api): Attributes support regular check
2024-01-12 13:05:37 +08:00
wang-liang0615
4a4b9e6ef0 feat(cmdb-ui):preference citype order (#378) 2024-01-12 11:14:53 +08:00
pycook
6e3f9478b3 Dev api 240111 (#377)
* feat(api): My subscription supports CIType sorting

* feat(api): db change
2024-01-11 18:01:37 +08:00
pycook
691051c254 perf(api): /api/v0.1/ci/adc/statistics
perf(api): /api/v0.1/ci/adc/statistics
2024-01-11 10:10:01 +08:00
pycook
8f066e95a6 fix(api): grant by attr (#373) 2024-01-10 16:52:27 +08:00
pycook
75bca39bf6 fix(api): commands add-user 2024-01-10 11:56:39 +08:00
pycook
3360e4d0fe feat(db): set variable sql_mode
feat(db): set variable sql_mode
2024-01-10 10:28:15 +08:00
wang-liang0615
521fcd0ba2 fix(ui):some fix (#370)
* pref(ui):some bugfix & some style

* fix(ui):some fix
2024-01-10 09:46:02 +08:00
wang-liang0615
fc113425cb pref(ui):some bugfix & some style (#369) 2024-01-09 14:48:13 +08:00
pycook
81a76a9632 fix(api): cmdb-init-acl commands (#368) 2024-01-09 10:04:53 +08:00
112 changed files with 11044 additions and 8751 deletions

View File

@@ -5,8 +5,8 @@ name = "pypi"
[packages]
# Flask
Flask = "==2.3.2"
Werkzeug = ">=2.3.6"
Flask = "==2.2.5"
Werkzeug = "==2.2.3"
click = ">=5.0"
# Api
Flask-RESTful = "==0.3.10"
@@ -15,6 +15,7 @@ Flask-SQLAlchemy = "==2.5.0"
SQLAlchemy = "==1.4.49"
PyMySQL = "==1.1.0"
redis = "==4.6.0"
python-redis-lock = "==4.0.0"
# Migrations
Flask-Migrate = "==2.5.2"
# Deployment

View File

@@ -50,7 +50,7 @@ def add_user():
if is_admin:
app = AppCache.get('acl') or App.create(name='acl')
acl_admin = RoleCache.get('acl_admin') or RoleCRUD.add_role('acl_admin', app.id, True)
acl_admin = RoleCache.get_by_name(app.id, 'acl_admin') or RoleCRUD.add_role('acl_admin', app.id, True)
rid = RoleCache.get_by_name(None, username).id
RoleRelationCRUD.add(acl_admin, acl_admin.id, [rid], app.id)

View File

@@ -115,6 +115,8 @@ def cmdb_init_acl():
_app = AppCache.get('cmdb') or App.create(name='cmdb')
app_id = _app.id
current_app.test_request_context().push()
# 1. add resource type
for resource_type in ResourceTypeEnum.all():
try:
@@ -183,11 +185,21 @@ def cmdb_counter():
UserCRUD.add(username='worker', password=uuid.uuid4().hex, email='worker@xxx.com')
login_user(UserCache.get('worker'))
i = 0
while True:
try:
db.session.remove()
CMDBCounterCache.reset()
if i % 5 == 0:
CMDBCounterCache.flush_adc_counter()
i = 0
CMDBCounterCache.flush_sub_counter()
i += 1
except:
import traceback
print(traceback.format_exc())

View File

@@ -4,7 +4,7 @@ from flask.cli import with_appcontext
from werkzeug.datastructures import MultiDict
from api.lib.common_setting.acl import ACLManager
from api.lib.common_setting.employee import EmployeeAddForm
from api.lib.common_setting.employee import EmployeeAddForm, GrantEmployeeACLPerm
from api.lib.common_setting.resp_format import ErrFormat
from api.models.common_setting import Employee, Department
@@ -158,50 +158,11 @@ class InitDepartment(object):
def init_backend_resource(self):
acl = self.check_app('backend')
resources_types = acl.get_all_resources_types()
perms = ['read', 'grant', 'delete', 'update']
acl_rid = self.get_admin_user_rid()
results = list(filter(lambda t: t['name'] == '操作权限', resources_types['groups']))
if len(results) == 0:
payload = dict(
app_id=acl.app_name,
name='操作权限',
description='',
perms=perms
)
resource_type = acl.create_resources_type(payload)
else:
resource_type = results[0]
resource_type_id = resource_type['id']
existed_perms = resources_types.get('id2perms', {}).get(resource_type_id, [])
existed_perms = [p['name'] for p in existed_perms]
new_perms = []
for perm in perms:
if perm not in existed_perms:
new_perms.append(perm)
if len(new_perms) > 0:
resource_type['perms'] = existed_perms + new_perms
acl.update_resources_type(resource_type_id, resource_type)
resource_list = acl.get_resource_by_type(None, None, resource_type['id'])
for name in ['公司信息', '公司架构', '通知设置']:
target = list(filter(lambda r: r['name'] == name, resource_list))
if len(target) == 0:
payload = dict(
type_id=resource_type['id'],
app_id=acl.app_name,
name=name,
)
resource = acl.create_resource(payload)
else:
resource = target[0]
if acl_rid > 0:
acl.grant_resource(acl_rid, resource['id'], perms)
if acl_rid == 0:
return
GrantEmployeeACLPerm(acl).grant_by_rid(acl_rid, True)
@staticmethod
def check_app(app_name):

View File

@@ -89,6 +89,19 @@ def db_setup():
"""
db.create_all()
try:
db.session.execute("set global sql_mode='STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,"
"ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'")
db.session.commit()
except:
pass
try:
db.session.execute("set global tidb_enable_noop_functions='ON'")
db.session.commit()
except:
pass
@click.group()
def translate():

View File

@@ -330,8 +330,8 @@ class AutoDiscoveryCICRUD(DBMixin):
@staticmethod
def get_attributes_by_type_id(type_id):
from api.lib.cmdb.cache import CITypeAttributesCache
attributes = [i[1] for i in CITypeAttributesCache.get2(type_id) or []]
from api.lib.cmdb.ci_type import CITypeAttributeManager
attributes = [i[1] for i in CITypeAttributeManager.get_all_attributes(type_id) or []]
attr_names = set()
adts = AutoDiscoveryCITypeCRUD.get_by_type_id(type_id)

View File

@@ -5,10 +5,14 @@ from __future__ import unicode_literals
from flask import current_app
from api.extensions import cache
from api.extensions import db
from api.lib.cmdb.custom_dashboard import CustomDashboardManager
from api.models.cmdb import Attribute
from api.models.cmdb import CI
from api.models.cmdb import CIType
from api.models.cmdb import CITypeAttribute
from api.models.cmdb import PreferenceShowAttributes
from api.models.cmdb import PreferenceTreeView
from api.models.cmdb import RelationType
@@ -226,7 +230,9 @@ class CITypeAttributeCache(object):
class CMDBCounterCache(object):
KEY = 'CMDB::Counter'
KEY = 'CMDB::Counter::dashboard'
KEY2 = 'CMDB::Counter::adc'
KEY3 = 'CMDB::Counter::sub'
@classmethod
def get(cls):
@@ -303,7 +309,7 @@ class CMDBCounterCache(object):
s = RelSearch([i[0] for i in type_id_names], level, other_filer or '')
try:
stats = s.statistics(type_ids)
stats = s.statistics(type_ids, need_filter=False)
except SearchError as e:
current_app.logger.error(e)
return
@@ -429,3 +435,47 @@ class CMDBCounterCache(object):
return
return numfound
@classmethod
def flush_adc_counter(cls):
res = db.session.query(CI.type_id, CI.is_auto_discovery)
result = dict()
for i in res:
result.setdefault(i.type_id, dict(total=0, auto_discovery=0))
result[i.type_id]['total'] += 1
if i.is_auto_discovery:
result[i.type_id]['auto_discovery'] += 1
cache.set(cls.KEY2, result, timeout=0)
return result
@classmethod
def get_adc_counter(cls):
return cache.get(cls.KEY2) or cls.flush_adc_counter()
@classmethod
def flush_sub_counter(cls):
result = dict(type_id2users=dict())
types = db.session.query(PreferenceShowAttributes.type_id,
PreferenceShowAttributes.uid, PreferenceShowAttributes.created_at).filter(
PreferenceShowAttributes.deleted.is_(False)).group_by(
PreferenceShowAttributes.uid, PreferenceShowAttributes.type_id)
for i in types:
result['type_id2users'].setdefault(i.type_id, []).append(i.uid)
types = PreferenceTreeView.get_by(to_dict=False)
for i in types:
result['type_id2users'].setdefault(i.type_id, [])
if i.uid not in result['type_id2users'][i.type_id]:
result['type_id2users'][i.type_id].append(i.uid)
cache.set(cls.KEY3, result, timeout=0)
return result
@classmethod
def get_sub_counter(cls):
return cache.get(cls.KEY3) or cls.flush_sub_counter()

View File

@@ -6,6 +6,7 @@ import datetime
import json
import threading
import redis_lock
from flask import abort
from flask import current_app
from flask_login import current_user
@@ -14,8 +15,8 @@ from werkzeug.exceptions import BadRequest
from api.extensions import db
from api.extensions import rd
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeAttributesCache
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.cache import CMDBCounterCache
from api.lib.cmdb.ci_type import CITypeAttributeManager
from api.lib.cmdb.ci_type import CITypeManager
from api.lib.cmdb.ci_type import CITypeRelationManager
@@ -45,14 +46,12 @@ from api.lib.perm.acl.acl import is_app_admin
from api.lib.perm.acl.acl import validate_permission
from api.lib.secrets.inner import InnerCrypt
from api.lib.secrets.vault import VaultClient
from api.lib.utils import Lock
from api.lib.utils import handle_arg_list
from api.lib.webhook import webhook_request
from api.models.cmdb import AttributeHistory
from api.models.cmdb import AutoDiscoveryCI
from api.models.cmdb import CI
from api.models.cmdb import CIRelation
from api.models.cmdb import CITypeAttribute
from api.models.cmdb import CITypeRelation
from api.models.cmdb import CITypeTrigger
from api.tasks.cmdb import ci_cache
@@ -61,8 +60,8 @@ from api.tasks.cmdb import ci_delete_trigger
from api.tasks.cmdb import ci_relation_add
from api.tasks.cmdb import ci_relation_cache
from api.tasks.cmdb import ci_relation_delete
from api.tasks.cmdb import delete_id_filter
PRIVILEGED_USERS = {"worker", "cmdb_agent", "agent"}
PASSWORD_DEFAULT_SHOW = "******"
@@ -218,15 +217,7 @@ class CIManager(object):
@classmethod
def get_ad_statistics(cls):
res = CI.get_by(to_dict=False)
result = dict()
for i in res:
result.setdefault(i.type_id, dict(total=0, auto_discovery=0))
result[i.type_id]['total'] += 1
if i.is_auto_discovery:
result[i.type_id]['auto_discovery'] += 1
return result
return CMDBCounterCache.get_adc_counter()
@staticmethod
def ci_is_exist(unique_key, unique_value, type_id):
@@ -287,10 +278,10 @@ class CIManager(object):
@staticmethod
def _auto_inc_id(attr):
db.session.remove()
db.session.commit()
value_table = TableMap(attr_name=attr.name).table
with Lock("auto_inc_id_{}".format(attr.name), need_lock=True):
with redis_lock.Lock(rd.r, "auto_inc_id_{}".format(attr.name)):
max_v = value_table.get_by(attr_id=attr.id, only_query=True).order_by(
getattr(value_table, 'value').desc()).first()
if max_v is not None:
@@ -317,14 +308,18 @@ class CIManager(object):
"""
now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
ci_type = CITypeManager.check_is_existed(ci_type_name)
raw_dict = copy.deepcopy(ci_dict)
unique_key = AttributeCache.get(ci_type.unique_id) or abort(
400, ErrFormat.unique_value_not_found.format("unique_id={}".format(ci_type.unique_id)))
unique_value = None
if not (unique_key.default and unique_key.default.get('default') == AttributeDefaultValueEnum.AUTO_INC_ID and
not ci_dict.get(unique_key.name)): # primary key is not auto inc id
unique_value = ci_dict.get(unique_key.name) or ci_dict.get(unique_key.alias) or ci_dict.get(unique_key.id)
unique_value = unique_value or abort(400, ErrFormat.unique_key_required.format(unique_key.name))
attrs = CITypeAttributesCache.get2(ci_type_name)
attrs = CITypeAttributeManager.get_all_attributes(ci_type.id)
ci_type_attrs_name = {attr.name: attr for _, attr in attrs}
ci_type_attrs_alias = {attr.alias: attr for _, attr in attrs}
ci_attr2type_attr = {type_attr.attr_id: type_attr for type_attr, _ in attrs}
@@ -332,8 +327,15 @@ class CIManager(object):
ci = None
record_id = None
password_dict = {}
need_lock = current_user.username not in current_app.config.get('PRIVILEGED_USERS', PRIVILEGED_USERS)
with Lock(ci_type_name, need_lock=need_lock):
with redis_lock.Lock(rd.r, ci_type.name):
db.session.commit()
if (unique_key.default and unique_key.default.get('default') == AttributeDefaultValueEnum.AUTO_INC_ID and
not ci_dict.get(unique_key.name)):
ci_dict[unique_key.name] = cls._auto_inc_id(unique_key)
current_app.logger.info(ci_dict[unique_key.name])
unique_value = ci_dict[unique_key.name]
existed = cls.ci_is_exist(unique_key, unique_value, ci_type.id)
if existed is not None:
if exist_policy == ExistPolicy.REJECT:
@@ -354,7 +356,8 @@ class CIManager(object):
if attr.default.get('default') and attr.default.get('default') in (
AttributeDefaultValueEnum.CREATED_AT, AttributeDefaultValueEnum.UPDATED_AT):
ci_dict[attr.name] = now
elif attr.default.get('default') == AttributeDefaultValueEnum.AUTO_INC_ID:
elif (attr.default.get('default') == AttributeDefaultValueEnum.AUTO_INC_ID and
not ci_dict.get(attr.name)):
ci_dict[attr.name] = cls._auto_inc_id(attr)
elif ((attr.name not in ci_dict and attr.alias not in ci_dict) or (
ci_dict.get(attr.name) is None and ci_dict.get(attr.alias) is None)):
@@ -368,6 +371,8 @@ class CIManager(object):
if attr.default and attr.default.get('default') == AttributeDefaultValueEnum.UPDATED_AT:
ci_dict[attr.name] = now
value_manager = AttributeValueManager()
computed_attrs = []
for _, attr in attrs:
if attr.is_computed:
@@ -378,7 +383,8 @@ class CIManager(object):
elif attr.alias in ci_dict:
password_dict[attr.id] = ci_dict.pop(attr.alias)
value_manager = AttributeValueManager()
if attr.re_check and password_dict.get(attr.id):
value_manager.check_re(attr.re_check, password_dict[attr.id])
if computed_attrs:
value_manager.handle_ci_compute_attributes(ci_dict, computed_attrs, ci)
@@ -386,7 +392,7 @@ class CIManager(object):
cls._valid_unique_constraint(ci_type.id, ci_dict, ci and ci.id)
ref_ci_dict = dict()
for k in ci_dict:
for k in copy.deepcopy(ci_dict):
if k.startswith("$") and "." in k:
ref_ci_dict[k] = ci_dict[k]
continue
@@ -398,7 +404,10 @@ class CIManager(object):
_attr_name = ((ci_type_attrs_name.get(k) and ci_type_attrs_name[k].name) or
(ci_type_attrs_alias.get(k) and ci_type_attrs_alias[k].name))
if limit_attrs and _attr_name not in limit_attrs:
if k in raw_dict:
return abort(403, ErrFormat.ci_filter_perm_attr_no_permission.format(k))
else:
ci_dict.pop(k)
ci_dict = {k: v for k, v in ci_dict.items() if k in ci_type_attrs_name or k in ci_type_attrs_alias}
@@ -430,13 +439,17 @@ class CIManager(object):
now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
ci = self.confirm_ci_existed(ci_id)
attrs = CITypeAttributesCache.get2(ci.type_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_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:
ci_dict[attr.name] = now
value_manager = AttributeValueManager()
password_dict = dict()
computed_attrs = list()
for _, attr in attrs:
@@ -448,7 +461,8 @@ class CIManager(object):
elif attr.alias in ci_dict:
password_dict[attr.id] = ci_dict.pop(attr.alias)
value_manager = AttributeValueManager()
if attr.re_check and password_dict.get(attr.id):
value_manager.check_re(attr.re_check, password_dict[attr.id])
if computed_attrs:
value_manager.handle_ci_compute_attributes(ci_dict, computed_attrs, ci)
@@ -456,17 +470,21 @@ class CIManager(object):
limit_attrs = self._valid_ci_for_no_read(ci) if not _is_admin else {}
record_id = None
need_lock = current_user.username not in current_app.config.get('PRIVILEGED_USERS', PRIVILEGED_USERS)
with Lock(ci.ci_type.name, need_lock=need_lock):
with redis_lock.Lock(rd.r, ci.ci_type.name):
db.session.commit()
self._valid_unique_constraint(ci.type_id, ci_dict, ci_id)
ci_dict = {k: v for k, v in ci_dict.items() if k in ci_type_attrs_name}
key2attr = value_manager.valid_attr_value(ci_dict, ci.type_id, ci.id, ci_type_attrs_name,
ci_attr2type_attr=ci_attr2type_attr)
if limit_attrs:
for k in ci_dict:
for k in copy.deepcopy(ci_dict):
if k not in limit_attrs:
if k in raw_dict:
return abort(403, ErrFormat.ci_filter_perm_attr_no_permission.format(k))
else:
ci_dict.pop(k)
try:
record_id = value_manager.create_or_update_attr_value(ci, ci_dict, key2attr)
@@ -514,8 +532,7 @@ class CIManager(object):
ci_delete_trigger.apply_async(args=(trigger, OperateType.DELETE, ci_dict), queue=CMDB_QUEUE)
attrs = CITypeAttribute.get_by(type_id=ci.type_id, to_dict=False)
attrs = [AttributeCache.get(attr.attr_id) for attr in attrs]
attrs = [i for _, i in CITypeAttributeManager.get_all_attributes(type_id=ci.type_id)]
for attr in attrs:
value_table = TableMap(attr=attr).table
for item in value_table.get_by(ci_id=ci_id, to_dict=False):
@@ -542,6 +559,7 @@ class CIManager(object):
AttributeHistoryManger.add(None, ci_id, [(None, OperateType.DELETE, ci_dict, None)], ci.type_id)
ci_delete.apply_async(args=(ci_id,), queue=CMDB_QUEUE)
delete_id_filter.apply_async(args=(ci_id,), queue=CMDB_QUEUE)
return ci_id
@@ -848,6 +866,20 @@ class CIRelationManager(object):
return numfound, len(ci_ids), result
@staticmethod
def recursive_children(ci_id):
result = []
def _get_children(_id):
children = CIRelation.get_by(first_ci_id=_id, to_dict=False)
result.extend([i.second_ci_id for i in children])
for child in children:
_get_children(child.second_ci_id)
_get_children(ci_id)
return result
@staticmethod
def _sort_handler(sort_by, query_sql):
@@ -903,7 +935,7 @@ class CIRelationManager(object):
@staticmethod
def _check_constraint(first_ci_id, first_type_id, second_ci_id, second_type_id, type_relation):
db.session.remove()
db.session.commit()
if type_relation.constraint == ConstraintEnum.Many2Many:
return
@@ -963,7 +995,7 @@ class CIRelationManager(object):
else:
type_relation = CITypeRelation.get_by_id(relation_type_id)
with Lock("ci_relation_add_{}_{}".format(first_ci.type_id, second_ci.type_id), need_lock=True):
with redis_lock.Lock(rd.r, "ci_relation_add_{}_{}".format(first_ci.type_id, second_ci.type_id)):
cls._check_constraint(first_ci_id, first_ci.type_id, second_ci_id, second_ci.type_id, type_relation)
@@ -999,6 +1031,7 @@ class CIRelationManager(object):
his_manager.add(cr, operate_type=OperateType.DELETE)
ci_relation_delete.apply_async(args=(cr.first_ci_id, cr.second_ci_id, cr.ancestor_ids), queue=CMDB_QUEUE)
delete_id_filter.apply_async(args=(cr.second_ci_id,), queue=CMDB_QUEUE)
return cr_id
@@ -1010,9 +1043,13 @@ class CIRelationManager(object):
to_dict=False,
first=True)
ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id, ancestor_ids), queue=CMDB_QUEUE)
if cr is not None:
cls.delete(cr.id)
return cr and cls.delete(cr.id)
ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id, ancestor_ids), queue=CMDB_QUEUE)
delete_id_filter.apply_async(args=(second_ci_id,), queue=CMDB_QUEUE)
return cr
@classmethod
def batch_update(cls, ci_ids, parents, children, ancestor_ids=None):
@@ -1053,7 +1090,7 @@ class CIRelationManager(object):
class CITriggerManager(object):
@staticmethod
def get(type_id):
db.session.remove()
db.session.commit()
return CITypeTrigger.get_by(type_id=type_id, to_dict=True)
@staticmethod

View File

@@ -42,10 +42,12 @@ from api.models.cmdb import CITypeAttributeGroup
from api.models.cmdb import CITypeAttributeGroupItem
from api.models.cmdb import CITypeGroup
from api.models.cmdb import CITypeGroupItem
from api.models.cmdb import CITypeInheritance
from api.models.cmdb import CITypeRelation
from api.models.cmdb import CITypeTrigger
from api.models.cmdb import CITypeUniqueConstraint
from api.models.cmdb import CustomDashboard
from api.models.cmdb import PreferenceCITypeOrder
from api.models.cmdb import PreferenceRelationView
from api.models.cmdb import PreferenceSearchOption
from api.models.cmdb import PreferenceShowAttributes
@@ -74,17 +76,23 @@ class CITypeManager(object):
return CIType.get_by_id(ci_type.id)
def get_icons(self):
return {i.id: i.icon or i.name for i in db.session.query(
self.cls.id, self.cls.icon, self.cls.name).filter(self.cls.deleted.is_(False))}
@staticmethod
def get_ci_types(type_name=None):
def get_ci_types(type_name=None, like=True):
resources = None
if current_app.config.get('USE_ACL') and not is_app_admin('cmdb'):
resources = set([i.get('name') for i in ACLManager().get_resources(ResourceTypeEnum.CI_TYPE)])
ci_types = CIType.get_by() if type_name is None else CIType.get_by_like(name=type_name)
ci_types = CIType.get_by() if type_name is None else (
CIType.get_by_like(name=type_name) if like else CIType.get_by(name=type_name))
res = list()
for type_dict in ci_types:
attr = AttributeCache.get(type_dict["unique_id"])
type_dict["unique_key"] = 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)
@@ -131,8 +139,13 @@ class CITypeManager(object):
kwargs["unique_id"] = unique_key.id
kwargs['uid'] = current_user.uid
parent_ids = kwargs.pop('parent_ids', None)
ci_type = CIType.create(**kwargs)
CITypeInheritanceManager.add(parent_ids, ci_type.id)
CITypeAttributeManager.add(ci_type.id, [unique_key.id], is_required=True)
CITypeCache.clean(ci_type.name)
@@ -214,21 +227,29 @@ class CITypeManager(object):
if item.get('parent_id') == type_id or item.get('child_id') == type_id:
return abort(400, ErrFormat.ci_relation_view_exists_and_cannot_delete_type.format(rv.name))
for item in CITypeRelation.get_by(parent_id=type_id, to_dict=False):
item.soft_delete(commit=False)
for item in (CITypeRelation.get_by(parent_id=type_id, to_dict=False) +
CITypeRelation.get_by(child_id=type_id, to_dict=False)):
if current_app.config.get('USE_ACL'):
resource_name = CITypeRelationManager.acl_resource_name(item.parent.name, item.child.name)
ACLManager().del_resource(resource_name, ResourceTypeEnum.CI_TYPE_RELATION)
for item in CITypeRelation.get_by(child_id=type_id, to_dict=False):
item.soft_delete(commit=False)
for table in [PreferenceTreeView, PreferenceShowAttributes, PreferenceSearchOption, CustomDashboard,
CITypeGroupItem, CITypeAttributeGroup, CITypeAttribute, CITypeUniqueConstraint, CITypeTrigger,
AutoDiscoveryCIType, CIFilterPerms]:
AutoDiscoveryCIType, CIFilterPerms, PreferenceCITypeOrder]:
for item in table.get_by(type_id=type_id, to_dict=False):
item.soft_delete(commit=False)
for item in AutoDiscoveryCI.get_by(type_id=type_id, to_dict=False):
item.delete(commit=False)
for item in CITypeInheritance.get_by(parent_id=type_id, to_dict=False):
item.delete(commit=False)
for item in CITypeInheritance.get_by(child_id=type_id, to_dict=False):
item.delete(commit=False)
db.session.commit()
ci_type.soft_delete()
@@ -241,6 +262,100 @@ class CITypeManager(object):
ACLManager().del_resource(ci_type.name, ResourceTypeEnum.CI)
class CITypeInheritanceManager(object):
cls = CITypeInheritance
@classmethod
def get_parents(cls, type_id):
return [i.parent_id for i in cls.cls.get_by(child_id=type_id, to_dict=False)]
@classmethod
def recursive_children(cls, type_id):
result = []
def _get_child(_id):
children = [i.child_id for i in cls.cls.get_by(parent_id=_id, to_dict=False)]
result.extend(children)
for child_id in children:
_get_child(child_id)
_get_child(type_id)
return result
@classmethod
def base(cls, type_id):
result = []
q = []
def _get_parents(_type_id):
parents = [i.parent_id for i in cls.cls.get_by(child_id=_type_id, to_dict=False)]
for i in parents[::-1]:
q.append(i)
try:
out = q.pop(0)
except IndexError:
return
result.append(out)
_get_parents(out)
_get_parents(type_id)
return result[::-1]
@classmethod
def add(cls, parent_ids, child_id):
rels = {}
for i in cls.cls.get_by(to_dict=False):
rels.setdefault(i.child_id, set()).add(i.parent_id)
try:
toposort_flatten(rels)
except toposort.CircularDependencyError as e:
current_app.logger.warning(str(e))
return abort(400, ErrFormat.circular_dependency_error)
for parent_id in parent_ids or []:
if parent_id == child_id:
return abort(400, ErrFormat.circular_dependency_error)
existed = cls.cls.get_by(parent_id=parent_id, child_id=child_id, first=True, to_dict=False)
if existed is None:
rels.setdefault(child_id, set()).add(parent_id)
try:
toposort_flatten(rels)
except toposort.CircularDependencyError as e:
current_app.logger.warning(str(e))
return abort(400, ErrFormat.circular_dependency_error)
cls.cls.create(parent_id=parent_id, child_id=child_id, commit=False)
db.session.commit()
@classmethod
def delete(cls, parent_id, child_id):
existed = cls.cls.get_by(parent_id=parent_id, child_id=child_id, first=True, to_dict=False)
if existed is not None:
children = cls.recursive_children(child_id) + [child_id]
for _id in children:
if CI.get_by(type_id=_id, to_dict=False, first=True) is not None:
return abort(400, ErrFormat.ci_exists_and_cannot_delete_inheritance)
attr_ids = set([i.id for _, i in CITypeAttributeManager.get_all_attributes(parent_id)])
for _id in children:
for attr_id in attr_ids:
for i in PreferenceShowAttributes.get_by(type_id=_id, attr_id=attr_id, to_dict=False):
i.soft_delete(commit=False)
db.session.commit()
existed.soft_delete()
class CITypeGroupManager(object):
cls = CITypeGroup
@@ -261,6 +376,7 @@ class CITypeGroupManager(object):
ci_type = CITypeCache.get(t['type_id']).to_dict()
if resources is None or (ci_type and ci_type['name'] in resources):
ci_type['permissions'] = resources[ci_type['name']] if resources is not None else None
ci_type['inherited'] = True if CITypeInheritanceManager.get_parents(ci_type['id']) else False
group.setdefault("ci_types", []).append(ci_type)
group_types.add(t["type_id"])
@@ -270,6 +386,7 @@ class CITypeGroupManager(object):
for ci_type in ci_types:
if ci_type["id"] not in group_types and (resources is None or ci_type['name'] in resources):
ci_type['permissions'] = resources.get(ci_type['name']) if resources is not None else None
ci_type['inherited'] = True if CITypeInheritanceManager.get_parents(ci_type['id']) else False
other_types['ci_types'].append(ci_type)
groups.append(other_types)
@@ -361,40 +478,62 @@ class CITypeAttributeManager(object):
return attr.name
@staticmethod
def get_attr_names_by_type_id(type_id):
return [AttributeCache.get(attr.attr_id).name for attr in CITypeAttributesCache.get(type_id)]
def get_all_attributes(type_id):
parent_ids = CITypeInheritanceManager.base(type_id)
result = []
for _type_id in parent_ids + [type_id]:
result.extend(CITypeAttributesCache.get2(_type_id))
return result
@classmethod
def get_attr_names_by_type_id(cls, type_id):
return [attr.name for _, attr in cls.get_all_attributes(type_id)]
@staticmethod
def get_attributes_by_type_id(type_id, choice_web_hook_parse=True, choice_other_parse=True):
has_config_perm = ACLManager('cmdb').has_permission(
CITypeManager.get_name_by_id(type_id), ResourceTypeEnum.CI, PermEnum.CONFIG)
attrs = CITypeAttributesCache.get(type_id)
parent_ids = CITypeInheritanceManager.base(type_id)
result = list()
id2pos = dict()
type2name = {i: CITypeCache.get(i) for i in parent_ids}
for _type_id in parent_ids + [type_id]:
attrs = CITypeAttributesCache.get(_type_id)
for attr in sorted(attrs, key=lambda x: (x.order, x.id)):
attr_dict = AttributeManager().get_attribute(attr.attr_id, choice_web_hook_parse, choice_other_parse)
attr_dict["is_required"] = attr.is_required
attr_dict["order"] = attr.order
attr_dict["default_show"] = attr.default_show
attr_dict["inherited"] = False if _type_id == type_id else True
attr_dict["inherited_from"] = type2name.get(_type_id) and type2name[_type_id].alias
if not has_config_perm:
attr_dict.pop('choice_web_hook', None)
attr_dict.pop('choice_other', None)
if attr_dict['id'] not in id2pos:
id2pos[attr_dict['id']] = len(result)
result.append(attr_dict)
else:
result[id2pos[attr_dict['id']]] = attr_dict
return result
@staticmethod
def get_common_attributes(type_ids):
@classmethod
def get_common_attributes(cls, type_ids):
has_config_perm = False
for type_id in type_ids:
has_config_perm |= ACLManager('cmdb').has_permission(
CITypeManager.get_name_by_id(type_id), ResourceTypeEnum.CI, PermEnum.CONFIG)
result = CITypeAttribute.get_by(__func_in___key_type_id=list(map(int, type_ids)), to_dict=False)
result = {type_id: [i for _, i in cls.get_all_attributes(type_id)] for type_id in type_ids}
attr2types = {}
for i in result:
attr2types.setdefault(i.attr_id, []).append(i.type_id)
for type_id in result:
for i in result[type_id]:
attr2types.setdefault(i.id, []).append(type_id)
attrs = []
for attr_id in attr2types:
@@ -511,10 +650,30 @@ class CITypeAttributeManager(object):
existed.soft_delete()
for ci in CI.get_by(type_id=type_id, to_dict=False):
AttributeValueManager.delete_attr_value(attr_id, ci.id)
AttributeValueManager.delete_attr_value(attr_id, ci.id, commit=False)
ci_cache.apply_async(args=(ci.id, None, None), queue=CMDB_QUEUE)
for item in PreferenceShowAttributes.get_by(type_id=type_id, attr_id=attr_id, to_dict=False):
item.soft_delete(commit=False)
child_ids = CITypeInheritanceManager.recursive_children(type_id)
for _type_id in [type_id] + child_ids:
for item in CITypeUniqueConstraint.get_by(type_id=_type_id, to_dict=False):
if attr_id in item.attr_ids:
attr_ids = copy.deepcopy(item.attr_ids)
attr_ids.remove(attr_id)
if attr_ids:
item.update(attr_ids=attr_ids, commit=False)
else:
item.soft_delete(commit=False)
item = CITypeTrigger.get_by(type_id=_type_id, attr_id=attr_id, to_dict=False, first=True)
item and item.soft_delete(commit=False)
db.session.commit()
CITypeAttributeCache.clean(type_id, attr_id)
CITypeHistoryManager.add(CITypeOperateType.DELETE_ATTRIBUTE, type_id, attr_id=attr.id,
@@ -528,8 +687,18 @@ class CITypeAttributeManager(object):
attr_id = _from.get('attr_id')
from_group_id = _from.get('group_id')
to_group_id = _to.get('group_id')
from_group_name = _from.get('group_name')
to_group_name = _to.get('group_name')
order = _to.get('order')
if from_group_name:
from_group = CITypeAttributeGroup.get_by(type_id=type_id, name=from_group_name, first=True, to_dict=False)
from_group_id = from_group and from_group.id
if to_group_name:
to_group = CITypeAttributeGroup.get_by(type_id=type_id, name=to_group_name, first=True, to_dict=False)
to_group_id = to_group and to_group.id
if from_group_id != to_group_id:
if from_group_id is not None:
CITypeAttributeGroupManager.delete_item(from_group_id, attr_id)
@@ -655,15 +824,15 @@ class CITypeRelationManager(object):
current_app.logger.warning(str(e))
return abort(400, ErrFormat.circular_dependency_error)
if constraint == ConstraintEnum.Many2Many:
other_c = CITypeRelation.get_by(parent_id=p.id, constraint=ConstraintEnum.Many2Many,
to_dict=False, first=True)
other_p = CITypeRelation.get_by(child_id=c.id, constraint=ConstraintEnum.Many2Many,
to_dict=False, first=True)
if other_c and other_c.child_id != c.id:
return abort(400, ErrFormat.m2m_relation_constraint.format(p.name, other_c.child.name))
if other_p and other_p.parent_id != p.id:
return abort(400, ErrFormat.m2m_relation_constraint.format(other_p.parent.name, c.name))
# if constraint == ConstraintEnum.Many2Many:
# other_c = CITypeRelation.get_by(parent_id=p.id, constraint=ConstraintEnum.Many2Many,
# to_dict=False, first=True)
# other_p = CITypeRelation.get_by(child_id=c.id, constraint=ConstraintEnum.Many2Many,
# to_dict=False, first=True)
# if other_c and other_c.child_id != c.id:
# return abort(400, ErrFormat.m2m_relation_constraint.format(p.name, other_c.child.name))
# if other_p and other_p.parent_id != p.id:
# return abort(400, ErrFormat.m2m_relation_constraint.format(other_p.parent.name, c.name))
existed = cls._get(p.id, c.id)
if existed is not None:
@@ -738,25 +907,66 @@ class CITypeAttributeGroupManager(object):
@staticmethod
def get_by_type_id(type_id, need_other=False):
groups = CITypeAttributeGroup.get_by(type_id=type_id)
groups = sorted(groups, key=lambda x: x["order"] or 0)
grouped = list()
parent_ids = CITypeInheritanceManager.base(type_id)
groups = []
id2type = {i: CITypeCache.get(i).alias for i in parent_ids}
for _type_id in parent_ids + [type_id]:
_groups = CITypeAttributeGroup.get_by(type_id=_type_id)
_groups = sorted(_groups, key=lambda x: x["order"] or 0)
for i in _groups:
if type_id != _type_id:
i['inherited'] = True
i['inherited_from'] = id2type[_type_id]
else:
i['inherited'] = False
groups.extend(_groups)
grouped = set()
attributes = CITypeAttributeManager.get_attributes_by_type_id(type_id)
id2attr = {i.get('id'): i for i in attributes}
group2pos = dict()
attr2pos = dict()
result = []
for group in groups:
items = CITypeAttributeGroupItem.get_by(group_id=group["id"], to_dict=False)
items = sorted(items, key=lambda x: x.order or 0)
group["attributes"] = [id2attr.get(i.attr_id) for i in items if i.attr_id in id2attr]
grouped.extend([i.attr_id for i in items])
if group['name'] not in group2pos:
group_pos = len(result)
group['attributes'] = []
result.append(group)
group2pos[group['name']] = group_pos
else:
group_pos = group2pos[group['name']]
attr = None
for i in items:
if i.attr_id in id2attr:
attr = id2attr[i.attr_id]
attr['inherited'] = group['inherited']
attr['inherited_from'] = group.get('inherited_from')
result[group_pos]['attributes'].append(attr)
if i.attr_id in attr2pos:
result[attr2pos[i.attr_id][0]]['attributes'].remove(attr2pos[i.attr_id][1])
attr2pos[i.attr_id] = [group_pos, attr]
group.pop('inherited_from', None)
grouped |= set([i.attr_id for i in items])
if need_other:
grouped = set(grouped)
other_attributes = [attr for attr in attributes if attr["id"] not in grouped]
groups.append(dict(attributes=other_attributes))
result.append(dict(attributes=other_attributes))
return groups
return result
@staticmethod
def create_or_update(type_id, name, attr_order, group_order=0, is_update=False):
@@ -890,10 +1100,16 @@ class CITypeAttributeGroupManager(object):
@classmethod
def transfer(cls, type_id, _from, _to):
current_app.logger.info("CIType[{0}] {1} -> {2}".format(type_id, _from, _to))
if isinstance(_from, int):
from_group = CITypeAttributeGroup.get_by_id(_from)
else:
from_group = CITypeAttributeGroup.get_by(name=_from, first=True, to_dict=False)
from_group or abort(404, ErrFormat.ci_type_attribute_group_not_found.format("id={}".format(_from)))
if isinstance(_to, int):
to_group = CITypeAttributeGroup.get_by_id(_to)
else:
to_group = CITypeAttributeGroup.get_by(name=_to, first=True, to_dict=False)
to_group or abort(404, ErrFormat.ci_type_attribute_group_not_found.format("id={}".format(_to)))
from_order, to_order = from_group.order, to_group.order
@@ -1274,7 +1490,7 @@ class CITypeTemplateManager(object):
from api.lib.common_setting.upload_file import CommonFileCRUD
tpt = dict(
ci_types=CITypeManager.get_ci_types(type_name=ci_type.name),
ci_types=CITypeManager.get_ci_types(type_name=ci_type.name, like=False),
ci_type_auto_discovery_rules=list(),
type2attributes=dict(),
type2attribute_group=dict(),

View File

@@ -1,12 +1,15 @@
# -*- coding:utf-8 -*-
import copy
import functools
import redis_lock
from flask import abort
from flask import current_app
from flask import request
from flask_login import current_user
from api.extensions import db
from api.extensions import rd
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.mixin import DBMixin
@@ -40,6 +43,11 @@ class CIFilterPermsCRUD(DBMixin):
result[i['rid']]['ci_filter'] = ""
result[i['rid']]['ci_filter'] += (i['ci_filter'] or "")
if i['id_filter']:
if not result[i['rid']]['id_filter']:
result[i['rid']]['id_filter'] = {}
result[i['rid']]['id_filter'].update(i['id_filter'] or {})
return result
def get_by_ids(self, _ids, type_id=None):
@@ -70,6 +78,11 @@ class CIFilterPermsCRUD(DBMixin):
result[i['type_id']]['ci_filter'] = ""
result[i['type_id']]['ci_filter'] += (i['ci_filter'] or "")
if i['id_filter']:
if not result[i['type_id']]['id_filter']:
result[i['type_id']]['id_filter'] = {}
result[i['type_id']]['id_filter'].update(i['id_filter'] or {})
return result
@classmethod
@@ -82,6 +95,54 @@ class CIFilterPermsCRUD(DBMixin):
type2filter_perms = cls().get_by_ids(list(map(int, [i['name'] for i in res2])), type_id=type_id)
return type2filter_perms.get(type_id, {}).get('attr_filter') or []
def _revoke_children(self, rid, id_filter, rebuild=True):
items = self.cls.get_by(rid=rid, ci_filter=None, attr_filter=None, to_dict=False)
for item in items:
changed, item_id_filter = False, copy.deepcopy(item.id_filter)
for prefix in id_filter:
for k, v in copy.deepcopy((item.id_filter or {})).items():
if k.startswith(prefix) and k != prefix:
item_id_filter.pop(k)
changed = True
if not item_id_filter and current_app.config.get('USE_ACL'):
item.soft_delete(commit=False)
ACLManager().del_resource(str(item.id), ResourceTypeEnum.CI_FILTER, rebuild=rebuild)
elif changed:
item.update(id_filter=item_id_filter, commit=False)
db.session.commit()
def _revoke_parent(self, rid, parent_path, rebuild=True):
parent_path = [i for i in parent_path.split(',') if i] or []
revoke_nodes = [','.join(parent_path[:i]) for i in range(len(parent_path), 0, -1)]
for node_path in revoke_nodes:
delete_item, can_deleted = None, True
items = self.cls.get_by(rid=rid, ci_filter=None, attr_filter=None, to_dict=False)
for item in items:
if node_path in item.id_filter:
delete_item = item
if any(filter(lambda x: x.startswith(node_path) and x != node_path, item.id_filter.keys())):
can_deleted = False
break
if can_deleted and delete_item:
id_filter = copy.deepcopy(delete_item.id_filter)
id_filter.pop(node_path)
delete_item = delete_item.update(id_filter=id_filter, filter_none=False)
if current_app.config.get('USE_ACL') and not id_filter:
ACLManager().del_resource(str(delete_item.id), ResourceTypeEnum.CI_FILTER, rebuild=False)
delete_item.soft_delete()
items.remove(delete_item)
if rebuild:
from api.tasks.acl import role_rebuild
from api.lib.perm.acl.const import ACL_QUEUE
from api.lib.perm.acl.cache import AppCache
role_rebuild.apply_async(args=(rid, AppCache.get('cmdb').id), queue=ACL_QUEUE)
def _can_add(self, **kwargs):
ci_filter = kwargs.get('ci_filter')
attr_filter = kwargs.get('attr_filter') or ""
@@ -102,25 +163,58 @@ class CIFilterPermsCRUD(DBMixin):
def add(self, **kwargs):
kwargs = self._can_add(**kwargs) or kwargs
with redis_lock.Lock(rd.r, 'CMDB_FILTER_{}_{}'.format(kwargs['type_id'], kwargs['rid'])):
request_id_filter = {}
if kwargs.get('id_filter'):
obj = self.cls.get_by(type_id=kwargs.get('type_id'),
rid=kwargs.get('rid'),
ci_filter=None,
attr_filter=None,
first=True, to_dict=False)
for _id, v in (kwargs.get('id_filter') or {}).items():
key = ",".join(([v['parent_path']] if v.get('parent_path') else []) + [str(_id)])
request_id_filter[key] = v['name']
else:
obj = self.cls.get_by(type_id=kwargs.get('type_id'),
rid=kwargs.get('rid'),
id_filter=None,
first=True, to_dict=False)
is_recursive = kwargs.pop('is_recursive', 0)
if obj is not None:
if obj.id_filter and isinstance(kwargs.get('id_filter'), dict):
obj_id_filter = copy.deepcopy(obj.id_filter)
for k, v in request_id_filter.items():
obj_id_filter[k] = v
kwargs['id_filter'] = obj_id_filter
obj = obj.update(filter_none=False, **kwargs)
if not obj.attr_filter and not obj.ci_filter:
if not obj.attr_filter and not obj.ci_filter and not obj.id_filter:
if current_app.config.get('USE_ACL'):
ACLManager().del_resource(str(obj.id), ResourceTypeEnum.CI_FILTER)
ACLManager().del_resource(str(obj.id), ResourceTypeEnum.CI_FILTER, rebuild=False)
obj.soft_delete()
else:
if not kwargs.get('ci_filter') and not kwargs.get('attr_filter'):
if not is_recursive and request_id_filter:
self._revoke_children(obj.rid, request_id_filter, rebuild=False)
return
else:
if not kwargs.get('ci_filter') and not kwargs.get('attr_filter') and not kwargs.get('id_filter'):
return
if request_id_filter:
kwargs['id_filter'] = request_id_filter
obj = self.cls.create(**kwargs)
if current_app.config.get('USE_ACL'):
if current_app.config.get('USE_ACL'): # new resource
try:
ACLManager().add_resource(obj.id, ResourceTypeEnum.CI_FILTER)
except:
@@ -138,8 +232,10 @@ class CIFilterPermsCRUD(DBMixin):
pass
def delete(self, **kwargs):
with redis_lock.Lock(rd.r, 'CMDB_FILTER_{}_{}'.format(kwargs['type_id'], kwargs['rid'])):
obj = self.cls.get_by(type_id=kwargs.get('type_id'),
rid=kwargs.get('rid'),
id_filter=None,
first=True, to_dict=False)
if obj is not None:
@@ -151,6 +247,69 @@ class CIFilterPermsCRUD(DBMixin):
return resource
def delete2(self, **kwargs):
with redis_lock.Lock(rd.r, 'CMDB_FILTER_{}_{}'.format(kwargs['type_id'], kwargs['rid'])):
obj = self.cls.get_by(type_id=kwargs.get('type_id'),
rid=kwargs.get('rid'),
ci_filter=None,
attr_filter=None,
first=True, to_dict=False)
request_id_filter = {}
for _id, v in (kwargs.get('id_filter') or {}).items():
key = ",".join([v['parent_path']] if v.get('parent_path') else [] + [str(_id)])
request_id_filter[key] = v['name']
resource = None
if obj is not None:
id_filter = {}
for k, v in copy.deepcopy(obj.id_filter or {}).items(): # important
if k not in request_id_filter:
id_filter[k] = v
if not id_filter and current_app.config.get('USE_ACL'):
resource = ACLManager().del_resource(str(obj.id), ResourceTypeEnum.CI_FILTER, rebuild=False)
obj.soft_delete()
db.session.commit()
else:
obj.update(id_filter=id_filter)
self._revoke_children(kwargs.get('rid'), request_id_filter, rebuild=False)
self._revoke_parent(kwargs.get('rid'), kwargs.get('parent_path'))
return resource
def delete_id_filter_by_ci_id(self, ci_id):
items = self.cls.get_by(ci_filter=None, attr_filter=None, to_dict=False)
rebuild_roles = set()
for item in items:
id_filter = copy.deepcopy(item.id_filter)
changed = False
for node_path in item.id_filter:
if str(ci_id) in node_path:
id_filter.pop(node_path)
changed = True
if changed:
rebuild_roles.add(item.rid)
if not id_filter:
item.soft_delete(commit=False)
else:
item.update(id_filter=id_filter, commit=False)
db.session.commit()
if rebuild_roles:
from api.tasks.acl import role_rebuild
from api.lib.perm.acl.const import ACL_QUEUE
from api.lib.perm.acl.cache import AppCache
for rid in rebuild_roles:
role_rebuild.apply_async(args=(rid, AppCache.get('cmdb').id), queue=ACL_QUEUE)
def has_perm_for_ci(arg_name, resource_type, perm, callback=None, app=None):
def decorator_has_perm(func):

View File

@@ -14,6 +14,8 @@ from api.lib.cmdb.attribute import AttributeManager
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeAttributesCache
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.cache import CMDBCounterCache
from api.lib.cmdb.ci_type import CITypeAttributeManager
from api.lib.cmdb.const import ConstraintEnum
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
@@ -24,6 +26,7 @@ from api.lib.exception import AbortException
from api.lib.perm.acl.acl import ACLManager
from api.models.cmdb import CITypeAttribute
from api.models.cmdb import CITypeRelation
from api.models.cmdb import PreferenceCITypeOrder
from api.models.cmdb import PreferenceRelationView
from api.models.cmdb import PreferenceSearchOption
from api.models.cmdb import PreferenceShowAttributes
@@ -38,13 +41,22 @@ class PreferenceManager(object):
@staticmethod
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)
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))
tree_types = PreferenceTreeView.get_by(uid=current_user.uid, to_dict=False) if tree else []
type_ids = set([i.type_id for i in types + tree_types])
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)
return [CITypeCache.get(type_id).to_dict() for type_id in type_ids]
@@ -59,32 +71,36 @@ class PreferenceManager(object):
:param tree:
:return:
"""
result = dict(self=dict(instance=[], tree=[], type_id2subs_time=dict()),
type_id2users=dict())
result = dict(self=dict(instance=[], tree=[], type_id2subs_time=dict()))
result.update(CMDBCounterCache.get_sub_counter())
ci_type_order = sorted(PreferenceCITypeOrder.get_by(uid=current_user.uid, to_dict=False), key=lambda x: x.order)
if instance:
types = db.session.query(PreferenceShowAttributes.type_id,
PreferenceShowAttributes.uid, PreferenceShowAttributes.created_at).filter(
PreferenceShowAttributes.deleted.is_(False)).group_by(
PreferenceShowAttributes.deleted.is_(False)).filter(
PreferenceShowAttributes.uid == current_user.uid).group_by(
PreferenceShowAttributes.uid, PreferenceShowAttributes.type_id)
for i in types:
if i.uid == current_user.uid:
result['self']['instance'].append(i.type_id)
if str(i.created_at) > str(result['self']['type_id2subs_time'].get(i.type_id, "")):
result['self']['type_id2subs_time'][i.type_id] = i.created_at
result['type_id2users'].setdefault(i.type_id, []).append(i.uid)
instance_order = [i.type_id for i in ci_type_order if not i.is_tree]
if len(instance_order) == len(result['self']['instance']):
result['self']['instance'] = instance_order
if tree:
types = PreferenceTreeView.get_by(to_dict=False)
types = PreferenceTreeView.get_by(uid=current_user.uid, to_dict=False)
for i in types:
if i.uid == current_user.uid:
result['self']['tree'].append(i.type_id)
if str(i.created_at) > str(result['self']['type_id2subs_time'].get(i.type_id, "")):
result['self']['type_id2subs_time'][i.type_id] = i.created_at
result['type_id2users'].setdefault(i.type_id, [])
if i.uid not in result['type_id2users'][i.type_id]:
result['type_id2users'][i.type_id].append(i.uid)
tree_order = [i.type_id for i in ci_type_order if i.is_tree]
if len(tree_order) == len(result['self']['tree']):
result['self']['tree'] = tree_order
return result
@@ -98,8 +114,8 @@ class PreferenceManager(object):
CITypeAttribute, CITypeAttribute.attr_id == PreferenceShowAttributes.attr_id).filter(
PreferenceShowAttributes.uid == current_user.uid).filter(
PreferenceShowAttributes.type_id == type_id).filter(
PreferenceShowAttributes.deleted.is_(False)).filter(CITypeAttribute.deleted.is_(False)).filter(
CITypeAttribute.type_id == type_id).all()
PreferenceShowAttributes.deleted.is_(False)).filter(CITypeAttribute.deleted.is_(False)).group_by(
CITypeAttribute.attr_id).all()
result = []
for i in sorted(attrs, key=lambda x: x.PreferenceShowAttributes.order):
@@ -109,17 +125,16 @@ class PreferenceManager(object):
is_subscribed = True
if not attrs:
attrs = db.session.query(CITypeAttribute).filter(
CITypeAttribute.type_id == type_id).filter(
CITypeAttribute.deleted.is_(False)).filter(
CITypeAttribute.default_show.is_(True)).order_by(CITypeAttribute.order)
result = [i.attr.to_dict() for i in attrs]
result = CITypeAttributeManager.get_attributes_by_type_id(type_id,
choice_web_hook_parse=False,
choice_other_parse=False)
result = [i for i in result if i['default_show']]
is_subscribed = False
for i in result:
if i["is_choice"]:
i.update(dict(choice_value=AttributeManager.get_choice_values(
i["id"], i["value_type"], i["choice_web_hook"], i.get("choice_other"))))
i["id"], i["value_type"], i.get("choice_web_hook"), i.get("choice_other"))))
return is_subscribed, result
@@ -151,9 +166,22 @@ class PreferenceManager(object):
if i.attr_id not in attr_dict:
i.soft_delete()
if not existed_all and attr_order:
cls.add_ci_type_order_item(type_id, is_tree=False)
elif not PreferenceShowAttributes.get_by(type_id=type_id, uid=current_user.uid, to_dict=False):
cls.delete_ci_type_order_item(type_id, is_tree=False)
@staticmethod
def get_tree_view():
ci_type_order = sorted(PreferenceCITypeOrder.get_by(uid=current_user.uid, is_tree=True, to_dict=False),
key=lambda x: x.order)
res = PreferenceTreeView.get_by(uid=current_user.uid, to_dict=True)
if ci_type_order:
res = sorted(res, key=lambda x: {ii.type_id: idx for idx, ii in enumerate(
ci_type_order)}.get(x['type_id'], 1))
for item in res:
if item["levels"]:
ci_type = CITypeCache.get(item['type_id']).to_dict()
@@ -172,8 +200,8 @@ class PreferenceManager(object):
return res
@staticmethod
def create_or_update_tree_view(type_id, levels):
@classmethod
def create_or_update_tree_view(cls, type_id, levels):
attrs = CITypeAttributesCache.get(type_id)
for idx, i in enumerate(levels):
for attr in attrs:
@@ -185,9 +213,12 @@ class PreferenceManager(object):
if existed is not None:
if not levels:
existed.soft_delete()
cls.delete_ci_type_order_item(type_id, is_tree=True)
return existed
return existed.update(levels=levels)
elif levels:
cls.add_ci_type_order_item(type_id, is_tree=True)
return PreferenceTreeView.create(levels=levels, type_id=type_id, uid=current_user.uid)
@staticmethod
@@ -356,6 +387,9 @@ class PreferenceManager(object):
for i in PreferenceTreeView.get_by(type_id=type_id, uid=uid, to_dict=False):
i.soft_delete()
for i in PreferenceCITypeOrder.get_by(type_id=type_id, uid=uid, to_dict=False):
i.soft_delete()
@staticmethod
def can_edit_relation(parent_id, child_id):
views = PreferenceRelationView.get_by(to_dict=False)
@@ -381,3 +415,36 @@ class PreferenceManager(object):
return False
return True
@staticmethod
def add_ci_type_order_item(type_id, is_tree=False):
max_order = PreferenceCITypeOrder.get_by(
uid=current_user.uid, is_tree=is_tree, only_query=True).order_by(PreferenceCITypeOrder.order.desc()).first()
order = (max_order and max_order.order + 1) or 1
PreferenceCITypeOrder.create(type_id=type_id, is_tree=is_tree, uid=current_user.uid, order=order)
@staticmethod
def delete_ci_type_order_item(type_id, is_tree=False):
existed = PreferenceCITypeOrder.get_by(uid=current_user.uid, type_id=type_id, is_tree=is_tree,
first=True, to_dict=False)
existed and existed.soft_delete()
@staticmethod
def upsert_ci_type_order(type_ids, is_tree=False):
for idx, type_id in enumerate(type_ids):
order = idx + 1
existed = PreferenceCITypeOrder.get_by(uid=current_user.uid, type_id=type_id, is_tree=is_tree,
to_dict=False, first=True)
if existed is not None:
existed.update(order=order, flush=True)
else:
PreferenceCITypeOrder.create(uid=current_user.uid, type_id=type_id, is_tree=is_tree, order=order,
flush=True)
try:
db.session.commit()
except Exception as e:
db.session.rollback()
current_app.logger.error("upsert citype order failed: {}".format(e))
return abort(400, ErrFormat.unknown_error)

View File

@@ -60,6 +60,8 @@ class ErrFormat(CommonErrFormat):
only_owner_can_delete = _l("Only the creator can delete it!") # 只有创建人才能删除它!
ci_exists_and_cannot_delete_type = _l(
"The model cannot be deleted because the CI already exists") # 因为CI已经存在不能删除模型
ci_exists_and_cannot_delete_inheritance = _l(
"The inheritance cannot be deleted because the CI already exists") # 因为CI已经存在不能删除继承关系
# 因为关系视图 {} 引用了该模型,不能删除模型
ci_relation_view_exists_and_cannot_delete_type = _l(

View File

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

View File

@@ -62,7 +62,7 @@ QUERY_CI_BY_ATTR_NAME = """
QUERY_CI_BY_ID = """
SELECT c_cis.id as ci_id
FROM c_cis
WHERE c_cis.id={}
WHERE c_cis.id {}
"""
QUERY_CI_BY_TYPE = """

View File

@@ -44,7 +44,10 @@ class Search(object):
count=1,
sort=None,
ci_ids=None,
excludes=None):
excludes=None,
parent_node_perm_passed=False,
use_id_filter=False,
use_ci_filter=True):
self.orig_query = query
self.fl = fl or []
self.excludes = excludes or []
@@ -54,12 +57,17 @@ class Search(object):
self.count = count
self.sort = sort
self.ci_ids = ci_ids or []
self.raw_ci_ids = copy.deepcopy(self.ci_ids)
self.query_sql = ""
self.type_id_list = []
self.only_type_query = False
self.parent_node_perm_passed = parent_node_perm_passed
self.use_id_filter = use_id_filter
self.use_ci_filter = use_ci_filter
self.valid_type_names = []
self.type2filter_perms = dict()
self.is_app_admin = is_app_admin('cmdb') or current_user.username == "worker"
@staticmethod
def _operator_proc(key):
@@ -106,7 +114,7 @@ class Search(object):
self.type_id_list.append(str(ci_type.id))
if ci_type.id in self.type2filter_perms:
ci_filter = self.type2filter_perms[ci_type.id].get('ci_filter')
if ci_filter:
if ci_filter and self.use_ci_filter and not self.use_id_filter:
sub = []
ci_filter = Template(ci_filter).render(user=current_user)
for i in ci_filter.split(','):
@@ -122,6 +130,14 @@ class Search(object):
self.fl = set(self.type2filter_perms[ci_type.id]['attr_filter'])
else:
self.fl = set(self.fl) & set(self.type2filter_perms[ci_type.id]['attr_filter'])
if self.type2filter_perms[ci_type.id].get('id_filter') and self.use_id_filter:
if not self.raw_ci_ids:
self.ci_ids = list(self.type2filter_perms[ci_type.id]['id_filter'].keys())
if self.use_id_filter and not self.ci_ids and not self.is_app_admin:
self.raw_ci_ids = [0]
else:
raise SearchError(ErrFormat.no_permission.format(ci_type.alias, PermEnum.READ))
else:
@@ -138,7 +154,10 @@ class Search(object):
@staticmethod
def _id_query_handler(v):
return QUERY_CI_BY_ID.format(v)
if ";" in v:
return QUERY_CI_BY_ID.format("in {}".format(v.replace(';', ',')))
else:
return QUERY_CI_BY_ID.format("= {}".format(v))
@staticmethod
def _in_query_handler(attr, v, is_not):
@@ -152,6 +171,7 @@ class Search(object):
"NOT LIKE" if is_not else "LIKE",
_v.replace("*", "%")) for _v in new_v])
_query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, in_query)
return _query_sql
@staticmethod
@@ -167,6 +187,7 @@ class Search(object):
"NOT BETWEEN" if is_not else "BETWEEN",
start.replace("*", "%"), end.replace("*", "%"))
_query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, range_query)
return _query_sql
@staticmethod
@@ -183,6 +204,7 @@ class Search(object):
comparison_query = "{0} '{1}'".format(v[0], v[1:].replace("*", "%"))
_query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, comparison_query)
return _query_sql
@staticmethod
@@ -194,6 +216,7 @@ class Search(object):
elif field.startswith("-"):
field = field[1:]
sort_type = "DESC"
return field, sort_type
def __sort_by_id(self, sort_type, query_sql):
@@ -322,6 +345,11 @@ class Search(object):
return numfound, res
def __get_type2filter_perms(self):
res2 = ACLManager('cmdb').get_resources(ResourceTypeEnum.CI_FILTER)
if res2:
self.type2filter_perms = CIFilterPermsCRUD().get_by_ids(list(map(int, [i['name'] for i in res2])))
def __get_types_has_read(self):
"""
:return: _type:(type1;type2)
@@ -331,14 +359,23 @@ class Search(object):
self.valid_type_names = {i['name'] for i in res if PermEnum.READ in i['permissions']}
res2 = acl.get_resources(ResourceTypeEnum.CI_FILTER)
if res2:
self.type2filter_perms = CIFilterPermsCRUD().get_by_ids(list(map(int, [i['name'] for i in res2])))
self.__get_type2filter_perms()
for type_id in self.type2filter_perms:
ci_type = CITypeCache.get(type_id)
if ci_type:
if self.type2filter_perms[type_id].get('id_filter'):
if self.use_id_filter:
self.valid_type_names.add(ci_type.name)
elif self.type2filter_perms[type_id].get('ci_filter'):
if self.use_ci_filter:
self.valid_type_names.add(ci_type.name)
else:
self.valid_type_names.add(ci_type.name)
return "_type:({})".format(";".join(self.valid_type_names))
def __confirm_type_first(self, queries):
has_type = False
result = []
@@ -371,8 +408,10 @@ class Search(object):
else:
result.append(q)
_is_app_admin = is_app_admin('cmdb') or current_user.username == "worker"
if result and not has_type and not _is_app_admin:
if self.parent_node_perm_passed:
self.__get_type2filter_perms()
self.valid_type_names = "ALL"
elif result and not has_type and not self.is_app_admin:
type_q = self.__get_types_has_read()
if id_query:
ci = CIManager.get_by_id(id_query)
@@ -381,13 +420,11 @@ class Search(object):
result.insert(0, "_type:{}".format(ci.type_id))
else:
result.insert(0, type_q)
elif _is_app_admin:
elif self.is_app_admin:
self.valid_type_names = "ALL"
else:
self.__get_types_has_read()
current_app.logger.warning(result)
return result
def __query_by_attr(self, q, queries, alias):
@@ -479,7 +516,7 @@ class Search(object):
def _filter_ids(self, query_sql):
if self.ci_ids:
return "SELECT * FROM ({0}) AS IN_QUERY WHERE IN_QUERY.ci_id IN ({1})".format(
query_sql, ",".join(list(map(str, self.ci_ids))))
query_sql, ",".join(list(set(map(str, self.ci_ids)))))
return query_sql
@@ -511,6 +548,9 @@ class Search(object):
s = time.time()
if query_sql:
query_sql = self._filter_ids(query_sql)
if self.raw_ci_ids and not self.ci_ids:
return 0, []
self.query_sql = query_sql
# current_app.logger.debug(query_sql)
numfound, res = self._execute_sql(query_sql)
@@ -569,3 +609,8 @@ class Search(object):
total = len(response)
return response, counter, total, self.page, numfound, facet
def get_ci_ids(self):
_, ci_ids = self._query_build_raw()
return ci_ids

View File

@@ -1,9 +1,11 @@
# -*- coding:utf-8 -*-
import json
import sys
from collections import Counter
from flask import abort
from flask import current_app
from flask_login import current_user
from api.extensions import rd
from api.lib.cmdb.ci import CIRelationManager
@@ -11,11 +13,14 @@ from api.lib.cmdb.ci_type import CITypeRelationManager
from api.lib.cmdb.const import ConstraintEnum
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2
from api.lib.cmdb.const import ResourceTypeEnum
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.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import is_app_admin
from api.models.cmdb import CI
from api.models.cmdb import CIRelation
class Search(object):
@@ -29,7 +34,9 @@ class Search(object):
sort=None,
reverse=False,
ancestor_ids=None,
has_m2m=None):
descendant_ids=None,
has_m2m=None,
root_parent_path=None):
self.orig_query = query
self.fl = fl
self.facet_field = facet_field
@@ -46,6 +53,8 @@ class Search(object):
level[0] if isinstance(level, list) and level else level)
self.ancestor_ids = ancestor_ids
self.descendant_ids = descendant_ids
self.root_parent_path = root_parent_path
self.has_m2m = has_m2m or False
if not self.has_m2m:
if self.ancestor_ids:
@@ -56,27 +65,23 @@ class Search(object):
if _l < int(level) and c == ConstraintEnum.Many2Many:
self.has_m2m = True
self.type2filter_perms = None
self.is_app_admin = is_app_admin('cmdb') or current_user.username == "worker"
def _get_ids(self, ids):
if self.level[-1] == 1 and len(ids) == 1:
if self.ancestor_ids is None:
return [i.second_ci_id for i in CIRelation.get_by(first_ci_id=ids[0], to_dict=False)]
else:
seconds = {i.second_ci_id for i in CIRelation.get_by(first_ci_id=ids[0],
ancestor_ids=self.ancestor_ids,
to_dict=False)}
return list(seconds)
merge_ids = []
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]):
id_filter_limit, _ = self._get_ci_filter(self.type2filter_perms[self.descendant_ids[level - 1]])
else:
id_filter_limit = {}
if not self.has_m2m:
_tmp = map(lambda x: json.loads(x).keys(),
filter(lambda x: x is not None, rd.get(ids, REDIS_PREFIX_CI_RELATION) or []))
ids = [j for i in _tmp for j in i]
key, prefix = ids, REDIS_PREFIX_CI_RELATION
key, prefix = list(map(str, ids)), REDIS_PREFIX_CI_RELATION
else:
if not self.ancestor_ids:
@@ -92,12 +97,16 @@ class Search(object):
key = list(set(["{},{}".format(i, j) for idx, i in enumerate(key) for j in _tmp[idx]]))
prefix = REDIS_PREFIX_CI_RELATION2
_tmp = list(map(lambda x: json.loads(x).keys() if x else [], rd.get(key, prefix) or []))
ids = [j for i in _tmp for j in i]
if not key:
if not key or id_filter_limit is None:
return []
res = [json.loads(x).items() for x in [i or '{}' for i in rd.get(key, prefix) or []]]
_tmp = [[i[0] 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)]
ids = [j for i in _tmp for j in i]
if level in self.level:
merge_ids.extend(ids)
@@ -120,7 +129,28 @@ class Search(object):
return merge_ids
def _has_read_perm_from_parent_nodes(self):
self.root_parent_path = list(map(str, self.root_parent_path))
if str(self.root_id).isdigit() and str(self.root_id) not in self.root_parent_path:
self.root_parent_path.append(str(self.root_id))
self.root_parent_path = set(self.root_parent_path)
if self.is_app_admin:
self.type2filter_perms = {}
return True
res = ACLManager().get_resources(ResourceTypeEnum.CI_FILTER) or {}
self.type2filter_perms = CIFilterPermsCRUD().get_by_ids(list(map(int, [i['name'] for i in res]))) or {}
for _, filters in self.type2filter_perms.items():
if set((filters.get('id_filter') or {}).keys()) & self.root_parent_path:
return True
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()
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]
@@ -161,42 +191,105 @@ class Search(object):
page=self.page,
count=self.count,
sort=self.sort,
ci_ids=merge_ids).search()
ci_ids=merge_ids,
parent_node_perm_passed=parent_node_perm_passed,
use_ci_filter=use_ci_filter).search()
def statistics(self, type_ids):
def _get_ci_filter(self, filter_perms, ci_filters=None):
ci_filters = ci_filters or []
if ci_filters:
result = {}
for item in ci_filters:
res = SearchFromDB('_type:{},{}'.format(item['type_id'], item['ci_filter']),
count=sys.maxsize, parent_node_perm_passed=True).get_ci_ids()
if res:
result[item['type_id']] = set(res)
return {}, result if result else None
result = dict()
if filter_perms.get('id_filter'):
for k in filter_perms['id_filter']:
node_path = k.split(',')
if len(node_path) == 1:
result[int(node_path[0])] = 1
elif not self.has_m2m:
result.setdefault(node_path[-2], set()).add(int(node_path[-1]))
else:
result.setdefault(','.join(node_path[:-1]), set()).add(int(node_path[-1]))
if result:
return result, None
else:
return None, None
return {}, None
def statistics(self, type_ids, need_filter=True):
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
_tmp = []
level2ids = {}
for lv in range(1, self.level + 1):
level2ids[lv] = []
if need_filter:
id_filter_limit, ci_filter_limit = None, None
if len(self.descendant_ids or []) >= lv and type2filter_perms.get(self.descendant_ids[lv - 1]):
id_filter_limit, _ = self._get_ci_filter(type2filter_perms[self.descendant_ids[lv - 1]])
elif type_ids and self.level == lv:
ci_filters = [type2filter_perms[type_id] for type_id in type_ids if type_id in type2filter_perms]
if ci_filters:
id_filter_limit, ci_filter_limit = self._get_ci_filter({}, ci_filters=ci_filters)
else:
id_filter_limit = {}
else:
id_filter_limit = {}
else:
id_filter_limit, ci_filter_limit = {}, {}
if lv == 1:
if not self.has_m2m:
key, prefix = ids, REDIS_PREFIX_CI_RELATION
else:
if not self.ancestor_ids:
key, prefix = ids, REDIS_PREFIX_CI_RELATION
key, prefix = [str(i) for i in ids], REDIS_PREFIX_CI_RELATION
else:
key = ["{},{}".format(self.ancestor_ids, _id) for _id in ids]
if not self.ancestor_ids:
key, prefix = [str(i) for i in ids], REDIS_PREFIX_CI_RELATION
else:
prefix = REDIS_PREFIX_CI_RELATION2
level2ids[lv] = [[i] for i in key]
if not key:
_tmp = []
if not key or id_filter_limit is None:
_tmp = [[]] * len(ids)
continue
res = [json.loads(x).items() for x in [i or '{}' for i in rd.get(key, prefix) or []]]
_tmp = []
if type_ids and lv == self.level:
_tmp = list(map(lambda x: [i for i in x if i[1] in type_ids],
(map(lambda x: list(json.loads(x).items()),
[i or '{}' for i in rd.get(key, prefix) or []]))))
_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)]
else:
_tmp = list(map(lambda x: list(json.loads(x).items()),
[i or '{}' for i in rd.get(key, prefix) or []]))
_tmp = [[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)]
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]
else:
for idx, item in enumerate(_tmp):
if item:
if not self.has_m2m:
@@ -208,15 +301,22 @@ class Search(object):
level2ids[lv].append(key)
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 = map(lambda x: [(_id, type_id) for _id, type_id in json.loads(x).items()
if type_id in type_ids],
filter(lambda x: x is not None,
rd.get(key, prefix) or []))
__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)]
else:
__tmp = map(lambda x: list(json.loads(x).items()),
filter(lambda x: x is not None,
rd.get(key, prefix) or []))
__tmp = [[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)]
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]
else:
__tmp = []

View File

@@ -5,10 +5,10 @@ from __future__ import unicode_literals
import copy
import imp
import os
import tempfile
import jinja2
import os
import re
import tempfile
from flask import abort
from flask import current_app
from jinja2schema import infer
@@ -117,6 +117,11 @@ class AttributeValueManager(object):
if type_attr and type_attr.is_required and not value and value != 0:
return abort(400, ErrFormat.attribute_value_required.format(attr.alias))
@staticmethod
def check_re(expr, value):
if not re.compile(expr).match(str(value)):
return abort(400, ErrFormat.attribute_value_invalid.format(value))
def _validate(self, attr, value, value_table, ci=None, type_id=None, ci_id=None, type_attr=None):
ci = ci or {}
v = self._deserialize_value(attr.value_type, value)
@@ -130,6 +135,9 @@ class AttributeValueManager(object):
if v == "" and attr.value_type not in (ValueTypeEnum.TEXT,):
v = None
if attr.re_check and value:
self.check_re(attr.re_check, value)
return v
@staticmethod
@@ -294,9 +302,9 @@ class AttributeValueManager(object):
return self.write_change2(changed)
@staticmethod
def delete_attr_value(attr_id, ci_id):
def delete_attr_value(attr_id, ci_id, commit=True):
attr = AttributeCache.get(attr_id)
if attr is not None:
value_table = TableMap(attr=attr).table
for item in value_table.get_by(attr_id=attr.id, ci_id=ci_id, to_dict=False):
item.delete()
item.delete(commit=commit)

View File

@@ -16,7 +16,7 @@ from wtforms import validators
from api.extensions import db
from api.lib.common_setting.acl import ACLManager
from api.lib.common_setting.const import OperatorType
from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.perm.acl.const import ACL_QUEUE
from api.lib.common_setting.resp_format import ErrFormat
from api.models.common_setting import Employee, Department
@@ -141,7 +141,7 @@ class EmployeeCRUD(object):
def add(**kwargs):
try:
res = CreateEmployee().create_single(**kwargs)
refresh_employee_acl_info.apply_async(args=(), queue=CMDB_QUEUE)
refresh_employee_acl_info.apply_async(args=(res.employee_id,), queue=ACL_QUEUE)
return res
except Exception as e:
abort(400, str(e))
@@ -171,7 +171,7 @@ class EmployeeCRUD(object):
if len(e_list) > 0:
edit_employee_department_in_acl.apply_async(
args=(e_list, new_department_id, current_user.uid),
queue=CMDB_QUEUE
queue=ACL_QUEUE
)
return existed
@@ -577,7 +577,6 @@ class EmployeeCRUD(object):
@staticmethod
def import_employee(employee_list):
res = CreateEmployee().batch_create(employee_list)
refresh_employee_acl_info.apply_async(args=(), queue=CMDB_QUEUE)
return res
@staticmethod
@@ -788,9 +787,11 @@ class CreateEmployee(object):
if existed:
return existed
return Employee.create(
res = Employee.create(
**kwargs
)
refresh_employee_acl_info.apply_async(args=(res.employee_id,), queue=ACL_QUEUE)
return res
@staticmethod
def get_department_by_name(d_name):
@@ -897,3 +898,75 @@ class EmployeeUpdateByUidForm(Form):
avatar = StringField(validators=[])
sex = StringField(validators=[])
mobile = StringField(validators=[])
class GrantEmployeeACLPerm(object):
"""
Grant ACL Permission After Create New Employee
"""
def __init__(self, acl=None):
self.perms_by_create_resources_type = ['read', 'grant', 'delete', 'update']
self.perms_by_common_grant = ['read']
self.resource_name_list = ['公司信息', '公司架构', '通知设置']
self.acl = acl if acl else self.check_app('backend')
self.resources_types = self.acl.get_all_resources_types()
self.resources_type = self.get_resources_type()
self.resource_list = self.acl.get_resource_by_type(None, None, self.resources_type['id'])
@staticmethod
def check_app(app_name):
acl = ACLManager(app_name)
payload = dict(
name=app_name,
description=app_name
)
app = acl.validate_app()
if not app:
acl.create_app(payload)
return acl
def get_resources_type(self):
results = list(filter(lambda t: t['name'] == '操作权限', self.resources_types['groups']))
if len(results) == 0:
payload = dict(
app_id=self.acl.app_name,
name='操作权限',
description='',
perms=self.perms_by_create_resources_type
)
resource_type = self.acl.create_resources_type(payload)
else:
resource_type = results[0]
resource_type_id = resource_type['id']
existed_perms = self.resources_types.get('id2perms', {}).get(resource_type_id, [])
existed_perms = [p['name'] for p in existed_perms]
new_perms = []
for perm in self.perms_by_create_resources_type:
if perm not in existed_perms:
new_perms.append(perm)
if len(new_perms) > 0:
resource_type['perms'] = existed_perms + new_perms
self.acl.update_resources_type(resource_type_id, resource_type)
return resource_type
def grant(self, rid_list):
[self.grant_by_rid(rid) for rid in rid_list if rid > 0]
def grant_by_rid(self, rid, is_admin=False):
for name in self.resource_name_list:
resource = list(filter(lambda r: r['name'] == name, self.resource_list))
if len(resource) == 0:
payload = dict(
type_id=self.resources_type['id'],
app_id=self.acl.app_name,
name=name,
)
resource = self.acl.create_resource(payload)
else:
resource = resource[0]
perms = self.perms_by_create_resources_type if is_admin else self.perms_by_common_grant
self.acl.grant_resource(rid, resource['id'], perms)

View File

@@ -389,6 +389,7 @@ class AuditCRUD(object):
logout_at=logout_at,
ip=request.headers.get('X-Real-IP') or request.remote_addr,
browser=request.headers.get('User-Agent'),
channel=request.values.get('channel', 'web'),
)
if logout_at is None:

View File

@@ -2,10 +2,11 @@
import msgpack
import redis_lock
from api.extensions import cache
from api.extensions import rd
from api.lib.decorator import flush_db
from api.lib.utils import Lock
from api.models.acl import App
from api.models.acl import Permission
from api.models.acl import Resource
@@ -136,14 +137,14 @@ class HasResourceRoleCache(object):
@classmethod
def add(cls, rid, app_id):
with Lock('HasResourceRoleCache'):
with redis_lock.Lock(rd.r, 'HasResourceRoleCache'):
c = cls.get(app_id)
c[rid] = 1
cache.set(cls.PREFIX_KEY.format(app_id), c, timeout=0)
@classmethod
def remove(cls, rid, app_id):
with Lock('HasResourceRoleCache'):
with redis_lock.Lock(rd.r, 'HasResourceRoleCache'):
c = cls.get(app_id)
c.pop(rid, None)
cache.set(cls.PREFIX_KEY.format(app_id), c, timeout=0)

View File

@@ -194,7 +194,7 @@ def validate(ticket):
def _parse_tag(string, tag):
"""
Used for parsing xml. Search string for the first occurence of
Used for parsing xml. Search string for the first occurrence of
<tag>.....</tag> and return text (stripped of leading and tailing
whitespace) between tags. Return "" if tag not found.
"""

View File

@@ -1,8 +1,6 @@
# -*- coding:utf-8 -*-
import base64
import sys
import time
from typing import Set
import elasticsearch
@@ -213,52 +211,6 @@ class ESHandler(object):
return 0, [], {}
class Lock(object):
def __init__(self, name, timeout=10, app=None, need_lock=True):
self.lock_key = name
self.need_lock = need_lock
self.timeout = timeout
if not app:
app = current_app
self.app = app
try:
self.redis = redis.Redis(host=self.app.config.get('CACHE_REDIS_HOST'),
port=self.app.config.get('CACHE_REDIS_PORT'),
password=self.app.config.get('CACHE_REDIS_PASSWORD'))
except:
self.app.logger.error("cannot connect redis")
raise Exception("cannot connect redis")
def lock(self, timeout=None):
if not timeout:
timeout = self.timeout
retry = 0
while retry < 100:
timestamp = time.time() + timeout + 1
_lock = self.redis.setnx(self.lock_key, timestamp)
if _lock == 1 or (
time.time() > float(self.redis.get(self.lock_key) or sys.maxsize) and
time.time() > float(self.redis.getset(self.lock_key, timestamp) or sys.maxsize)):
break
else:
retry += 1
time.sleep(0.6)
if retry >= 100:
raise Exception("get lock failed...")
def release(self):
if time.time() < float(self.redis.get(self.lock_key)):
self.redis.delete(self.lock_key)
def __enter__(self):
if self.need_lock:
self.lock()
def __exit__(self, exc_type, exc_val, exc_tb):
if self.need_lock:
self.release()
class AESCrypto(object):
BLOCK_SIZE = 16 # Bytes
pad = lambda s: s + ((AESCrypto.BLOCK_SIZE - len(s) % AESCrypto.BLOCK_SIZE) *

View File

@@ -356,7 +356,7 @@ class AuditLoginLog(Model2):
__tablename__ = "acl_audit_login_logs"
username = db.Column(db.String(64), index=True)
channel = db.Column(db.Enum('web', 'api'), default="web")
channel = db.Column(db.Enum('web', 'api', 'ssh'), default="web")
ip = db.Column(db.String(15))
browser = db.Column(db.String(256))
description = db.Column(db.String(128))

View File

@@ -57,6 +57,16 @@ class CIType(Model):
uid = db.Column(db.Integer, index=True)
class CITypeInheritance(Model):
__tablename__ = "c_ci_type_inheritance"
parent_id = db.Column(db.Integer, db.ForeignKey("c_ci_types.id"), nullable=False)
child_id = db.Column(db.Integer, db.ForeignKey("c_ci_types.id"), nullable=False)
parent = db.relationship("CIType", primaryjoin="CIType.id==CITypeInheritance.parent_id")
child = db.relationship("CIType", primaryjoin="CIType.id==CITypeInheritance.child_id")
class CITypeRelation(Model):
__tablename__ = "c_ci_type_relations"
@@ -94,6 +104,8 @@ class Attribute(Model):
_choice_web_hook = db.Column('choice_web_hook', db.JSON)
choice_other = db.Column(db.JSON)
re_check = db.Column(db.Text)
uid = db.Column(db.Integer, index=True)
option = db.Column(db.JSON)
@@ -464,6 +476,15 @@ class PreferenceSearchOption(Model):
option = db.Column(db.JSON)
class PreferenceCITypeOrder(Model):
__tablename__ = "c_pcto"
uid = db.Column(db.Integer, index=True, nullable=False)
type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'))
order = db.Column(db.SmallInteger, default=0)
is_tree = db.Column(db.Boolean, default=False) # True is tree view, False is resource view
# custom
class CustomDashboard(Model):
__tablename__ = "c_c_d"
@@ -548,6 +569,7 @@ class CIFilterPerms(Model):
type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'))
ci_filter = db.Column(db.Text)
attr_filter = db.Column(db.Text)
id_filter = db.Column(db.JSON) # {node_path: unique_value}
rid = db.Column(db.Integer, index=True)

View File

@@ -4,6 +4,7 @@
import json
import time
import redis_lock
from flask import current_app
from flask_login import login_user
@@ -17,10 +18,10 @@ from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.cmdb.const import REDIS_PREFIX_CI
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2
from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.decorator import flush_db
from api.lib.decorator import reconnect_db
from api.lib.perm.acl.cache import UserCache
from api.lib.utils import Lock
from api.lib.utils import handle_arg_list
from api.models.cmdb import CI
from api.models.cmdb import CIRelation
@@ -83,6 +84,13 @@ def ci_delete(ci_id):
current_app.logger.info("{0} delete..........".format(ci_id))
@celery.task(name="cmdb.delete_id_filter", queue=CMDB_QUEUE)
@reconnect_db
def delete_id_filter(ci_id):
CIFilterPermsCRUD().delete_id_filter_by_ci_id(ci_id)
@celery.task(name="cmdb.ci_delete_trigger", queue=CMDB_QUEUE)
@reconnect_db
def ci_delete_trigger(trigger, operate_type, ci_dict):
@@ -99,7 +107,7 @@ def ci_delete_trigger(trigger, operate_type, ci_dict):
@flush_db
@reconnect_db
def ci_relation_cache(parent_id, child_id, ancestor_ids):
with Lock("CIRelation_{}".format(parent_id)):
with redis_lock.Lock(rd.r, "CIRelation_{}".format(parent_id)):
if ancestor_ids is None:
children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0]
children = json.loads(children) if children is not None else {}
@@ -177,7 +185,7 @@ def ci_relation_add(parent_dict, child_id, uid):
@celery.task(name="cmdb.ci_relation_delete", queue=CMDB_QUEUE)
@reconnect_db
def ci_relation_delete(parent_id, child_id, ancestor_ids):
with Lock("CIRelation_{}".format(parent_id)):
with redis_lock.Lock(rd.r, "CIRelation_{}".format(parent_id)):
if ancestor_ids is None:
children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0]
children = json.loads(children) if children is not None else {}

View File

@@ -3,14 +3,14 @@ from flask import current_app
from api.extensions import celery
from api.lib.common_setting.acl import ACLManager
from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.perm.acl.const import ACL_QUEUE
from api.lib.common_setting.resp_format import ErrFormat
from api.models.common_setting import Department, Employee
from api.lib.decorator import flush_db
from api.lib.decorator import reconnect_db
@celery.task(name="common_setting.edit_employee_department_in_acl", queue=CMDB_QUEUE)
@celery.task(name="common_setting.edit_employee_department_in_acl", queue=ACL_QUEUE)
@flush_db
@reconnect_db
def edit_employee_department_in_acl(e_list, new_d_id, op_uid):
@@ -49,8 +49,7 @@ def edit_employee_department_in_acl(e_list, new_d_id, op_uid):
continue
old_d_rid_in_acl = role_map.get(old_department.department_name, 0)
if old_d_rid_in_acl == 0:
return
if old_d_rid_in_acl > 0:
if old_d_rid_in_acl != old_department.acl_rid:
old_department.update(
acl_rid=old_d_rid_in_acl
@@ -77,10 +76,10 @@ def edit_employee_department_in_acl(e_list, new_d_id, op_uid):
return result
@celery.task(name="common_setting.refresh_employee_acl_info", queue=CMDB_QUEUE)
@celery.task(name="common_setting.refresh_employee_acl_info", queue=ACL_QUEUE)
@flush_db
@reconnect_db
def refresh_employee_acl_info():
def refresh_employee_acl_info(current_employee_id=None):
acl = ACLManager('acl')
role_map = {role['name']: role for role in acl.get_all_roles()}
@@ -90,8 +89,12 @@ def refresh_employee_acl_info():
query = Employee.query.filter(*criterion).order_by(
Employee.created_at.desc()
)
current_employee_rid = 0
for em in query.all():
if current_employee_id and em.employee_id == current_employee_id:
current_employee_rid = em.acl_rid if em.acl_rid else 0
if em.acl_uid and em.acl_rid:
continue
role = role_map.get(em.username, None)
@@ -105,6 +108,9 @@ def refresh_employee_acl_info():
if not em.acl_rid:
params['acl_rid'] = role.get('id', 0)
if current_employee_id and em.employee_id == current_employee_id:
current_employee_rid = params['acl_rid'] if params.get('acl_rid', 0) else 0
try:
em.update(**params)
current_app.logger.info(
@@ -113,3 +119,12 @@ def refresh_employee_acl_info():
except Exception as e:
current_app.logger.error(str(e))
continue
if current_employee_rid and current_employee_rid > 0:
try:
from api.lib.common_setting.employee import GrantEmployeeACLPerm
GrantEmployeeACLPerm().grant_by_rid(current_employee_rid, False)
current_app.logger.info(f"GrantEmployeeACLPerm success, current_employee_rid: {current_employee_rid}")
except Exception as e:
current_app.logger.error(str(e))

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2024-01-03 11:39+0800\n"
"POT-Creation-Date: 2024-03-01 13:49+0800\n"
"PO-Revision-Date: 2023-12-25 20:21+0800\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: zh\n"
@@ -234,205 +234,209 @@ msgstr "只有创建人才能删除它!"
msgid "The model cannot be deleted because the CI already exists"
msgstr "因为CI已经存在不能删除模型"
#: api/lib/cmdb/resp_format.py:65
#: api/lib/cmdb/resp_format.py:63
msgid "The inheritance cannot be deleted because the CI already exists"
msgstr "因为CI已经存在不能删除继承关系"
#: api/lib/cmdb/resp_format.py:67
msgid ""
"The model cannot be deleted because the model is referenced by the "
"relational view {}"
msgstr "因为关系视图 {} 引用了该模型,不能删除模型"
#: api/lib/cmdb/resp_format.py:67
#: api/lib/cmdb/resp_format.py:69
msgid "Model group {} does not exist"
msgstr "模型分组 {} 不存在"
#: api/lib/cmdb/resp_format.py:68
#: api/lib/cmdb/resp_format.py:70
msgid "Model group {} already exists"
msgstr "模型分组 {} 已经存在"
#: api/lib/cmdb/resp_format.py:69
#: api/lib/cmdb/resp_format.py:71
msgid "Model relationship {} does not exist"
msgstr "模型关系 {} 不存在"
#: api/lib/cmdb/resp_format.py:70
#: api/lib/cmdb/resp_format.py:72
msgid "Attribute group {} already exists"
msgstr "属性分组 {} 已存在"
#: api/lib/cmdb/resp_format.py:71
#: api/lib/cmdb/resp_format.py:73
msgid "Attribute group {} does not exist"
msgstr "属性分组 {} 不存在"
#: api/lib/cmdb/resp_format.py:73
#: api/lib/cmdb/resp_format.py:75
msgid "Attribute group <{0}> - attribute <{1}> does not exist"
msgstr "属性组<{0}> - 属性<{1}> 不存在"
#: api/lib/cmdb/resp_format.py:74
#: api/lib/cmdb/resp_format.py:76
msgid "The unique constraint already exists!"
msgstr "唯一约束已经存在!"
#: api/lib/cmdb/resp_format.py:76
#: api/lib/cmdb/resp_format.py:78
msgid "Uniquely constrained attributes cannot be JSON and multi-valued"
msgstr "唯一约束的属性不能是 JSON 和 多值"
#: api/lib/cmdb/resp_format.py:77
#: api/lib/cmdb/resp_format.py:79
msgid "Duplicated trigger"
msgstr "重复的触发器"
#: api/lib/cmdb/resp_format.py:78
#: api/lib/cmdb/resp_format.py:80
msgid "Trigger {} does not exist"
msgstr "触发器 {} 不存在"
#: api/lib/cmdb/resp_format.py:80
#: api/lib/cmdb/resp_format.py:82
msgid "Operation record {} does not exist"
msgstr "操作记录 {} 不存在"
#: api/lib/cmdb/resp_format.py:81
#: api/lib/cmdb/resp_format.py:83
msgid "Unique identifier cannot be deleted"
msgstr "不能删除唯一标识"
#: api/lib/cmdb/resp_format.py:82
#: api/lib/cmdb/resp_format.py:84
msgid "Cannot delete default sorted attributes"
msgstr "不能删除默认排序的属性"
#: api/lib/cmdb/resp_format.py:84
#: api/lib/cmdb/resp_format.py:86
msgid "No node selected"
msgstr "没有选择节点"
#: api/lib/cmdb/resp_format.py:85
#: api/lib/cmdb/resp_format.py:87
msgid "This search option does not exist!"
msgstr "该搜索选项不存在!"
#: api/lib/cmdb/resp_format.py:86
#: api/lib/cmdb/resp_format.py:88
msgid "This search option has a duplicate name!"
msgstr "该搜索选项命名重复!"
#: api/lib/cmdb/resp_format.py:88
#: api/lib/cmdb/resp_format.py:90
msgid "Relationship type {} already exists"
msgstr "关系类型 {} 已经存在"
#: api/lib/cmdb/resp_format.py:89
#: api/lib/cmdb/resp_format.py:91
msgid "Relationship type {} does not exist"
msgstr "关系类型 {} 不存在"
#: api/lib/cmdb/resp_format.py:91
#: api/lib/cmdb/resp_format.py:93
msgid "Invalid attribute value: {}"
msgstr "无效的属性值: {}"
#: api/lib/cmdb/resp_format.py:92
#: api/lib/cmdb/resp_format.py:94
msgid "{} Invalid value: {}"
msgstr "无效的值: {}"
#: api/lib/cmdb/resp_format.py:93
#: api/lib/cmdb/resp_format.py:95
msgid "{} is not in the predefined values"
msgstr "{} 不在预定义值里"
#: api/lib/cmdb/resp_format.py:95
#: api/lib/cmdb/resp_format.py:97
msgid "The value of attribute {} must be unique, {} already exists"
msgstr "属性 {} 的值必须是唯一的, 当前值 {} 已存在"
#: api/lib/cmdb/resp_format.py:96
#: api/lib/cmdb/resp_format.py:98
msgid "Attribute {} value must exist"
msgstr "属性 {} 值必须存在"
#: api/lib/cmdb/resp_format.py:99
#: api/lib/cmdb/resp_format.py:101
msgid "Unknown error when adding or modifying attribute value: {}"
msgstr "新增或者修改属性值未知错误: {}"
#: api/lib/cmdb/resp_format.py:101
#: api/lib/cmdb/resp_format.py:103
msgid "Duplicate custom name"
msgstr "订制名重复"
#: api/lib/cmdb/resp_format.py:103
#: api/lib/cmdb/resp_format.py:105
msgid "Number of models exceeds limit: {}"
msgstr "模型数超过限制: {}"
#: api/lib/cmdb/resp_format.py:104
#: api/lib/cmdb/resp_format.py:106
msgid "The number of CIs exceeds the limit: {}"
msgstr "CI数超过限制: {}"
#: api/lib/cmdb/resp_format.py:106
#: api/lib/cmdb/resp_format.py:108
msgid "Auto-discovery rule: {} already exists!"
msgstr "自动发现规则: {} 已经存在!"
#: api/lib/cmdb/resp_format.py:107
#: api/lib/cmdb/resp_format.py:109
msgid "Auto-discovery rule: {} does not exist!"
msgstr "自动发现规则: {} 不存在!"
#: api/lib/cmdb/resp_format.py:109
#: api/lib/cmdb/resp_format.py:111
msgid "This auto-discovery rule is referenced by the model and cannot be deleted!"
msgstr "该自动发现规则被模型引用, 不能删除!"
#: api/lib/cmdb/resp_format.py:111
#: api/lib/cmdb/resp_format.py:113
msgid "The application of auto-discovery rules cannot be defined repeatedly!"
msgstr "自动发现规则的应用不能重复定义!"
#: api/lib/cmdb/resp_format.py:112
#: api/lib/cmdb/resp_format.py:114
msgid "The auto-discovery you want to modify: {} does not exist!"
msgstr "您要修改的自动发现: {} 不存在!"
#: api/lib/cmdb/resp_format.py:113
#: api/lib/cmdb/resp_format.py:115
msgid "Attribute does not include unique identifier: {}"
msgstr "属性字段没有包括唯一标识: {}"
#: api/lib/cmdb/resp_format.py:114
#: api/lib/cmdb/resp_format.py:116
msgid "The auto-discovery instance does not exist!"
msgstr "自动发现的实例不存在!"
#: api/lib/cmdb/resp_format.py:115
#: api/lib/cmdb/resp_format.py:117
msgid "The model is not associated with this auto-discovery!"
msgstr "模型并未关联该自动发现!"
#: api/lib/cmdb/resp_format.py:116
#: api/lib/cmdb/resp_format.py:118
msgid "Only the creator can modify the Secret!"
msgstr "只有创建人才能修改Secret!"
#: api/lib/cmdb/resp_format.py:118
#: api/lib/cmdb/resp_format.py:120
msgid "This rule already has auto-discovery instances and cannot be deleted!"
msgstr "该规则已经有自动发现的实例, 不能被删除!"
#: api/lib/cmdb/resp_format.py:120
#: api/lib/cmdb/resp_format.py:122
msgid "The default auto-discovery rule is already referenced by model {}!"
msgstr "该默认的自动发现规则 已经被模型 {} 引用!"
#: api/lib/cmdb/resp_format.py:122
#: api/lib/cmdb/resp_format.py:124
msgid "The unique_key method must return a non-empty string!"
msgstr "unique_key方法必须返回非空字符串!"
#: api/lib/cmdb/resp_format.py:123
#: api/lib/cmdb/resp_format.py:125
msgid "The attributes method must return a list"
msgstr "attributes方法必须返回的是list"
#: api/lib/cmdb/resp_format.py:125
#: api/lib/cmdb/resp_format.py:127
msgid "The list returned by the attributes method cannot be empty!"
msgstr "attributes方法返回的list不能为空!"
#: api/lib/cmdb/resp_format.py:127
#: api/lib/cmdb/resp_format.py:129
msgid "Only administrators can define execution targets as: all nodes!"
msgstr "只有管理员才可以定义执行机器为: 所有节点!"
#: api/lib/cmdb/resp_format.py:128
#: api/lib/cmdb/resp_format.py:130
msgid "Execute targets permission check failed: {}"
msgstr "执行机器权限检查不通过: {}"
#: api/lib/cmdb/resp_format.py:130
#: api/lib/cmdb/resp_format.py:132
msgid "CI filter authorization must be named!"
msgstr "CI过滤授权 必须命名!"
#: api/lib/cmdb/resp_format.py:131
#: api/lib/cmdb/resp_format.py:133
msgid "CI filter authorization is currently not supported or query"
msgstr "CI过滤授权 暂时不支持 或 查询"
#: api/lib/cmdb/resp_format.py:134
#: api/lib/cmdb/resp_format.py:136
msgid "You do not have permission to operate attribute {}!"
msgstr "您没有属性 {} 的操作权限!"
#: api/lib/cmdb/resp_format.py:135
#: api/lib/cmdb/resp_format.py:137
msgid "You do not have permission to operate this CI!"
msgstr "您没有该CI的操作权限!"
#: api/lib/cmdb/resp_format.py:137
#: api/lib/cmdb/resp_format.py:139
msgid "Failed to save password: {}"
msgstr "保存密码失败: {}"
#: api/lib/cmdb/resp_format.py:138
#: api/lib/cmdb/resp_format.py:140
msgid "Failed to get password: {}"
msgstr "获取密码失败: {}"

View File

@@ -11,8 +11,7 @@ from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.const import ExistPolicy
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import ResourceTypeEnum, PermEnum
from api.lib.cmdb.const import RetKey
from api.lib.cmdb.perms import has_perm_for_ci
from api.lib.cmdb.search import SearchError
@@ -152,9 +151,10 @@ class CISearchView(APIView):
ret_key = RetKey.NAME
facet = handle_arg_list(request.values.get("facet", ""))
sort = request.values.get("sort")
use_id_filter = request.values.get("use_id_filter", False) in current_app.config.get('BOOL_TRUE')
start = time.time()
s = search(query, fl, facet, page, ret_key, count, sort, excludes)
s = search(query, fl, facet, page, ret_key, count, sort, excludes, use_id_filter=use_id_filter)
try:
response, counter, total, page, numfound, facet = s.search()
except SearchError as e:

View File

@@ -13,7 +13,6 @@ from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci_relation.search import Search
from api.lib.decorator import args_required
from api.lib.perm.auth import auth_abandoned
from api.lib.utils import get_page
from api.lib.utils import get_page_size
from api.lib.utils import handle_arg_list
@@ -36,6 +35,8 @@ class CIRelationSearchView(APIView):
root_id = request.values.get('root_id')
ancestor_ids = request.values.get('ancestor_ids') or None # only for many to many
root_parent_path = handle_arg_list(request.values.get('root_parent_path') or '')
descendant_ids = list(map(int, handle_arg_list(request.values.get('descendant_ids', []))))
level = list(map(int, handle_arg_list(request.values.get('level', '1'))))
query = request.values.get('q', "")
@@ -47,7 +48,8 @@ class CIRelationSearchView(APIView):
start = time.time()
s = Search(root_id, level, query, fl, facet, page, count, sort, reverse,
ancestor_ids=ancestor_ids, has_m2m=has_m2m)
ancestor_ids=ancestor_ids, has_m2m=has_m2m, root_parent_path=root_parent_path,
descendant_ids=descendant_ids)
try:
response, counter, total, page, numfound, facet = s.search()
except SearchError as e:
@@ -65,16 +67,16 @@ class CIRelationSearchView(APIView):
class CIRelationStatisticsView(APIView):
url_prefix = "/ci_relations/statistics"
@auth_abandoned
def get(self):
root_ids = list(map(int, handle_arg_list(request.values.get('root_ids'))))
level = request.values.get('level', 1)
type_ids = set(map(int, handle_arg_list(request.values.get('type_ids', []))))
ancestor_ids = request.values.get('ancestor_ids') or None # only for many to many
descendant_ids = list(map(int, handle_arg_list(request.values.get('descendant_ids', []))))
has_m2m = request.values.get("has_m2m") in current_app.config.get('BOOL_TRUE')
start = time.time()
s = Search(root_ids, level, ancestor_ids=ancestor_ids, has_m2m=has_m2m)
s = Search(root_ids, level, ancestor_ids=ancestor_ids, descendant_ids=descendant_ids, has_m2m=has_m2m)
try:
result = s.statistics(type_ids)
except SearchError as e:

View File

@@ -14,6 +14,7 @@ from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci_type import CITypeAttributeGroupManager
from api.lib.cmdb.ci_type import CITypeAttributeManager
from api.lib.cmdb.ci_type import CITypeGroupManager
from api.lib.cmdb.ci_type import CITypeInheritanceManager
from api.lib.cmdb.ci_type import CITypeManager
from api.lib.cmdb.ci_type import CITypeTemplateManager
from api.lib.cmdb.ci_type import CITypeTriggerManager
@@ -37,15 +38,23 @@ from api.resource import APIView
class CITypeView(APIView):
url_prefix = ("/ci_types", "/ci_types/<int:type_id>", "/ci_types/<string:type_name>")
url_prefix = ("/ci_types", "/ci_types/<int:type_id>", "/ci_types/<string:type_name>",
"/ci_types/icons")
def get(self, type_id=None, type_name=None):
if request.url.endswith("icons"):
return self.jsonify(CITypeManager().get_icons())
q = request.args.get("type_name")
if type_id is not None:
ci_types = [CITypeCache.get(type_id).to_dict()]
ci_type = CITypeCache.get(type_id).to_dict()
ci_type['parent_ids'] = CITypeInheritanceManager.get_parents(type_id)
ci_types = [ci_type]
elif type_name is not None:
ci_types = [CITypeCache.get(type_name).to_dict()]
ci_type = CITypeCache.get(type_name).to_dict()
ci_type['parent_ids'] = CITypeInheritanceManager.get_parents(ci_type['id'])
ci_types = [ci_type]
else:
ci_types = CITypeManager().get_ci_types(q)
count = len(ci_types)
@@ -53,7 +62,7 @@ class CITypeView(APIView):
return self.jsonify(numfound=count, ci_types=ci_types)
@args_required("name")
@args_validate(CITypeManager.cls)
@args_validate(CITypeManager.cls, exclude_args=['parent_ids'])
def post(self):
params = request.values
@@ -84,6 +93,26 @@ class CITypeView(APIView):
return self.jsonify(type_id=type_id)
class CITypeInheritanceView(APIView):
url_prefix = ("/ci_types/inheritance",)
@args_required("parent_ids")
@args_required("child_id")
@has_perm_from_args("child_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id)
def post(self):
CITypeInheritanceManager.add(request.values['parent_ids'], request.values['child_id'])
return self.jsonify(**request.values)
@args_required("parent_id")
@args_required("child_id")
@has_perm_from_args("child_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id)
def delete(self):
CITypeInheritanceManager.delete(request.values['parent_id'], request.values['child_id'])
return self.jsonify(**request.values)
class CITypeGroupView(APIView):
url_prefix = ("/ci_types/groups",
"/ci_types/groups/config",
@@ -248,8 +277,8 @@ class CITypeAttributeTransferView(APIView):
@args_required('from')
@args_required('to')
def post(self, type_id):
_from = request.values.get('from') # {'attr_id': xx, 'group_id': xx}
_to = request.values.get('to') # {'group_id': xx, 'order': xxx}
_from = request.values.get('from') # {'attr_id': xx, 'group_id': xx, 'group_name': xx}
_to = request.values.get('to') # {'group_id': xx, 'group_name': xx, 'order': xxx}
CITypeAttributeManager.transfer(type_id, _from, _to)
@@ -262,8 +291,8 @@ class CITypeAttributeGroupTransferView(APIView):
@args_required('from')
@args_required('to')
def post(self, type_id):
_from = request.values.get('from') # group_id
_to = request.values.get('to') # group_id
_from = request.values.get('from') # group_id or group_name
_to = request.values.get('to') # group_id or group_name
CITypeAttributeGroupManager.transfer(type_id, _from, _to)
@@ -296,7 +325,7 @@ class CITypeAttributeGroupView(APIView):
attr_order = list(zip(attrs, orders))
group = CITypeAttributeGroupManager.create_or_update(type_id, name, attr_order, order)
current_app.logger.warning(group.id)
return self.jsonify(group_id=group.id)
@has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id)
@@ -310,11 +339,13 @@ class CITypeAttributeGroupView(APIView):
attr_order = list(zip(attrs, orders))
CITypeAttributeGroupManager.update(group_id, name, attr_order, order)
return self.jsonify(group_id=group_id)
@has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id)
def delete(self, group_id):
CITypeAttributeGroupManager.delete(group_id)
return self.jsonify(group_id=group_id)
@@ -463,18 +494,19 @@ class CITypeGrantView(APIView):
if not acl.has_permission(type_name, ResourceTypeEnum.CI_TYPE, PermEnum.GRANT) and not is_app_admin('cmdb'):
return abort(403, ErrFormat.no_permission.format(type_name, PermEnum.GRANT))
if perms and not request.values.get('id_filter'):
acl.grant_resource_to_role_by_rid(type_name, rid, ResourceTypeEnum.CI_TYPE, perms, rebuild=False)
if request.values.get('ci_filter') or request.values.get('attr_filter'):
CIFilterPermsCRUD().add(type_id=type_id, rid=rid, **request.values)
else:
new_resource = None
if 'ci_filter' in request.values or 'attr_filter' in request.values or 'id_filter' in request.values:
new_resource = CIFilterPermsCRUD().add(type_id=type_id, rid=rid, **request.values)
if not new_resource:
from api.tasks.acl import role_rebuild
from api.lib.perm.acl.const import ACL_QUEUE
app_id = AppCache.get('cmdb').id
current_app.logger.info((rid, app_id))
role_rebuild.apply_async(args=(rid, app_id), queue=ACL_QUEUE)
current_app.logger.info('done')
return self.jsonify(code=200)
@@ -495,10 +527,18 @@ class CITypeRevokeView(APIView):
if not acl.has_permission(type_name, ResourceTypeEnum.CI_TYPE, PermEnum.GRANT) and not is_app_admin('cmdb'):
return abort(403, ErrFormat.no_permission.format(type_name, PermEnum.GRANT))
acl.revoke_resource_from_role_by_rid(type_name, rid, ResourceTypeEnum.CI_TYPE, perms, rebuild=False)
app_id = AppCache.get('cmdb').id
resource = None
if request.values.get('id_filter'):
CIFilterPermsCRUD().delete2(
type_id=type_id, rid=rid, id_filter=request.values['id_filter'],
parent_path=request.values.get('parent_path'))
return self.jsonify(type_id=type_id, rid=rid)
acl.revoke_resource_from_role_by_rid(type_name, rid, ResourceTypeEnum.CI_TYPE, perms, rebuild=False)
if PermEnum.READ in perms or not perms:
resource = CIFilterPermsCRUD().delete(type_id=type_id, rid=rid)

View File

@@ -2,6 +2,7 @@
from flask import abort
from flask import current_app
from flask import request
from api.lib.cmdb.ci_type import CITypeManager
@@ -187,3 +188,15 @@ class PreferenceRelationRevokeView(APIView):
acl.revoke_resource_from_role_by_rid(name, rid, ResourceTypeEnum.RELATION_VIEW, perms)
return self.jsonify(code=200)
class PreferenceCITypeOrderView(APIView):
url_prefix = ("/preference/ci_types/order",)
def post(self):
type_ids = request.values.get("type_ids")
is_tree = request.values.get("is_tree") in current_app.config.get('BOOL_TRUE')
PreferenceManager.upsert_ci_type_order(type_ids, is_tree)
return self.jsonify(type_ids=type_ids, is_tree=is_tree)

View File

@@ -8,7 +8,7 @@ elasticsearch==7.17.9
email-validator==1.3.1
environs==4.2.0
flasgger==0.9.5
Flask==2.3.2
Flask==2.2.5
Flask-Bcrypt==1.0.1
flask-babel==4.0.0
Flask-Caching==2.0.2
@@ -37,6 +37,7 @@ PyMySQL==1.1.0
ldap3==2.9.1
PyYAML==6.0.1
redis==4.6.0
python-redis-lock==4.0.0
requests==2.31.0
requests_oauthlib==1.3.1
markdownify==0.11.6
@@ -46,7 +47,7 @@ supervisor==4.0.3
timeout-decorator==0.5.0
toposort==1.10
treelib==1.6.1
Werkzeug>=2.3.6
Werkzeug==2.2.3
WTForms==3.0.0
shamir~=17.12.0
pycryptodomex>=3.19.0

View File

@@ -13,7 +13,7 @@ const getAntdSerials = (color) => {
const themePluginOption = {
fileName: 'css/theme-colors-[contenthash:8].css',
matchColors: getAntdSerials('#1890ff'), // 主色系列
matchColors: getAntdSerials('#2f54eb'), // 主色系列
// 改变样式选择器,解决样式覆盖问题
changeSelector (selector) {
switch (selector) {

View File

@@ -20,6 +20,7 @@
}
}
"
:disabled="disabled"
>
</treeselect>
</div>
@@ -42,6 +43,7 @@
"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
<div
:title="node.label"
@@ -80,6 +82,7 @@
@select="(value) => handleChangeExp(value, item, index)"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
</treeselect>
<treeselect
@@ -103,6 +106,7 @@
"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
<div
:title="node.label"
@@ -125,6 +129,7 @@
v-model="item.min"
:style="{ width: '78px' }"
:placeholder="$t('min')"
:disabled="disabled"
/>
~
<a-input
@@ -133,6 +138,7 @@
v-model="item.max"
:style="{ width: '78px' }"
:placeholder="$t('max')"
:disabled="disabled"
/>
</a-input-group>
<a-input-group size="small" compact v-else-if="item.exp === 'compare'" :style="{ width: '175px' }">
@@ -155,6 +161,7 @@
"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
</treeselect>
<a-input class="ops-input" v-model="item.value" size="small" style="width: 113px" />
@@ -166,8 +173,10 @@
:placeholder="item.exp === 'in' || item.exp === '~in' ? $t('cmdbFilterComp.split', { separator: ';' }) : ''"
class="ops-input"
:style="{ width: '175px' }"
:disabled="disabled"
></a-input>
<div v-else :style="{ width: '175px' }"></div>
<template v-if="!disabled">
<a-tooltip :title="$t('copy')">
<a class="operation" @click="handleCopyRule(item)"><ops-icon type="icon-xianxing-copy"/></a>
</a-tooltip>
@@ -177,8 +186,9 @@
<a-tooltip :title="$t('cmdbFilterComp.addHere')" v-if="needAddHere">
<a class="operation" @click="handleAddRuleAt(item)"><a-icon type="plus-circle"/></a>
</a-tooltip>
</template>
</a-space>
<div class="table-filter-add">
<div class="table-filter-add" v-if="!disabled">
<a @click="handleAddRule">+ {{ $t('new') }}</a>
</div>
</div>
@@ -211,6 +221,10 @@ export default {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
},
data() {
return {

View File

@@ -16,6 +16,7 @@
:needAddHere="needAddHere"
v-model="ruleList"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)"
:disabled="disabled"
/>
<a-divider :style="{ margin: '10px 0' }" />
<div style="width:554px">
@@ -31,6 +32,7 @@
v-else
v-model="ruleList"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)"
:disabled="disabled"
/>
</div>
</template>
@@ -69,6 +71,10 @@ export default {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
},
data() {
return {

View File

@@ -135,4 +135,3 @@
}
}
</style>

View File

@@ -0,0 +1,19 @@
/* eslint-disable no-useless-escape */
import i18n from '@/lang'
export const regList = () => {
return [
{ id: 'letter', label: i18n.t('regexSelect.letter'), value: '^[A-Za-z]+$', message: '请输入字母' },
{ id: 'number', label: i18n.t('regexSelect.number'), value: '^-?(?!0\\d+)\\d+(\\.\\d+)?$', message: '请输入数字' },
{ id: 'letterAndNumber', label: i18n.t('regexSelect.letterAndNumber'), value: '^[A-Za-z0-9.]+$', message: '请输入字母和数字' },
{ id: 'phone', label: i18n.t('regexSelect.phone'), value: '^1[3-9]\\d{9}$', message: '请输入正确手机号码' },
{ id: 'landline', label: i18n.t('regexSelect.landline'), value: '^(?:(?:\\d{3}-)?\\d{8}|^(?:\\d{4}-)?\\d{7,8})(?:-\\d+)?$', message: '请输入正确座机' },
{ id: 'zipCode', label: i18n.t('regexSelect.zipCode'), value: '^(0[1-7]|1[0-356]|2[0-7]|3[0-6]|4[0-7]|5[1-7]|6[1-7]|7[0-5]|8[013-6])\\d{4}$', message: '请输入正确邮政编码' },
{ id: 'IDCard', label: i18n.t('regexSelect.IDCard'), value: '(^[1-9]\\d{5}(18|19|([23]\\d))\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$)|(^[1-9]\\d{5}\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}$)', message: '请输入正确身份证号' },
{ id: 'ip', label: i18n.t('regexSelect.ip'), value: '^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$', message: '请输入正确IP地址' },
{ id: 'email', label: i18n.t('regexSelect.email'), value: '^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\.\\w+([-.]\\w+)*$', message: '请输入正确邮箱' },
{ id: 'link', label: i18n.t('regexSelect.link'), value: '^(https:\/\/www\.|http:\/\/www\.|https:\/\/|http:\/\/)?[a-zA-Z0-9]{2,}(\.[a-zA-Z0-9]{2,})(\.[a-zA-Z0-9]{2,})?$', message: '请输入链接' },
{ id: 'monetaryAmount', label: i18n.t('regexSelect.monetaryAmount'), value: '^-?\\d+(,\\d{3})*(\.\\d{1,2})?$', message: '请输入货币金额' },
{ id: 'custom', label: i18n.t('regexSelect.custom'), value: '', message: '' }
]
}

View File

@@ -0,0 +1,2 @@
import RegexSelect from './regexSelect.vue'
export default RegexSelect

View File

@@ -0,0 +1,208 @@
<template>
<a-popover
trigger="click"
placement="bottom"
ref="regexSelect"
overlayClassName="regex-select-wrapper"
:overlayStyle="{ '--overlay-width': `${width}px` }"
@visibleChange="visibleChange"
>
<div class="regex-select" slot="content">
<div class="regex-select-left">
<div class="regex-select-left-header">{{ $t('regexSelect.limitedFormat') }}</div>
<div
@click="
() => {
current = reg
testInput = ''
showMessage = false
}
"
:class="{
'regex-select-left-reg': true,
'regex-select-left-reg-selected': current && current.label === reg.label,
}"
v-for="(reg, index) in regList"
:key="reg.label"
>
<a-divider :style="{ margin: '2px -12px', width: 'calc(100% + 24px)' }" v-if="index === regList.length - 1" />
{{ reg.label }}
</div>
</div>
<div class="regex-select-right">
<template v-if="current">
<div class="regex-select-right-header">{{ $t('regexSelect.regExp') }}</div>
<div
v-if="current.label !== $t('regexSelect.custom')"
:style="{ color: '#000', fontSize: '12px', margin: '12px 0' }"
>
{{ current.value }}
</div>
<a-input
:style="{ margin: '12px 0' }"
size="small"
v-else
v-model="current.value"
@change="
() => {
this.$emit('change', current)
}
"
/>
<template v-if="isShowErrorMsg">
<div class="regex-select-right-header">{{ $t('regexSelect.errMsg') }}</div>
<a-input :style="{ margin: '12px 0' }" size="small" v-model="current.message" />
</template>
<div class="regex-select-right-header">{{ $t('regexSelect.test') }}</div>
<a-input v-model="testInput" :style="{ margin: '12px 0 4px' }" size="small" @change="validate" />
<span :style="{ color: 'red', fontSize: '12px' }" v-if="showMessage">{{
locale === 'zh' ? current.message || '错误' : $t('regexSelect.error')
}}</span>
</template>
</div>
</div>
<a-input ref="regInput" :placeholder="$t('regexSelect.placeholder')" :value="current.label" @change="changeLabel">
</a-input>
</a-popover>
</template>
<script>
import { mapState } from 'vuex'
import { regList } from './constants'
export default {
name: 'RegexSelect',
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: Object,
default: () => {},
},
isShowErrorMsg: {
type: Boolean,
default: true,
},
limitedFormat: {
type: Array,
default: () => [],
},
},
data() {
return {
showMessage: false,
width: 370,
testInput: '',
}
},
computed: {
...mapState(['locale']),
regList() {
if (this.limitedFormat.length) {
return regList().filter((item) => this.limitedFormat.includes(item.id))
}
return regList()
},
current: {
get() {
if (this.value?.value && !this.value?.label) {
const _find = this.regList.find((reg) => reg.value === this.value?.value)
return { ...this.value, label: _find?.label ?? this.$t('regexSelect.custom') }
}
return this.value ?? {}
},
set(val) {
this.showMessage = false
this.$emit('change', val)
return val
},
},
},
mounted() {
this.$nextTick(() => {
const regInput = this.$refs.regInput.$refs.input
this.width = regInput.offsetWidth || 370
})
},
methods: {
validate(e) {
const reg = RegExp(this.current.value, 'g')
this.showMessage = !reg.test(e.target.value)
},
changeLabel(e) {
this.current = {}
},
visibleChange(visible) {
if (visible) {
this.$nextTick(() => {
this.testInput = ''
this.showMessage = false
})
}
},
},
}
</script>
<style lang="less" scoped>
@import '~@/style/static.less';
.regex-select {
width: 100%;
height: 300px;
display: flex;
.regex-select-left {
width: 40%;
height: 100%;
border: 1px solid #cacdd9;
border-radius: 4px;
padding: 12px;
&-reg {
padding-left: 2px;
cursor: pointer;
&-selected,
&:hover {
color: #custom_colors[color_1];
}
}
}
&-right {
flex: 1;
height: 100%;
border: 1px solid #cacdd9;
border-radius: 4px;
margin-left: 8px;
padding: 12px;
}
&-left,
&-right {
&-header {
font-weight: 400;
font-size: 14px;
color: #000000;
border-left: 2px solid #custom_colors[color_1];
padding-left: 6px;
margin-left: -6px;
}
}
}
</style>
<style lang="less">
.regex-select-wrapper {
.ant-popover-arrow {
display: none;
}
.ant-popover-inner-content {
padding: 0;
min-width: 370px;
width: var(--overlay-width);
}
}
.regex-select-wrapper.ant-popover-placement-bottom .ant-popover-content {
margin-top: -8px;
}
.regex-select-wrapper.ant-popover-placement-top .ant-popover-content {
margin-bottom: -8px;
}
</style>

View File

@@ -113,6 +113,10 @@ export default {
},
mounted() {
const paneLengthPixel = localStorage.getItem(`${this.appName}-paneLengthPixel`)
if (paneLengthPixel) {
this.$emit('update:paneLengthPixel', Number(paneLengthPixel))
}
this.parentContainer = document.querySelector(`.${this.appName}`)
if (this.isExpanded) {
document.querySelector(`.${this.appName} .pane-two`).style.display = 'none'

View File

@@ -119,7 +119,8 @@ export default {
border-radius: 4px;
color: @layout-header-font-color;
height: @layout-header-height;
line-height: @layout-header-height;
display: inline-flex;
align-items: center;
&:hover {
background: linear-gradient(0deg, rgba(0, 80, 201, 0.2) 0%, rgba(174, 207, 255, 0.06) 86.76%);
color: @layout-header-font-selected-color;

View File

@@ -14,7 +14,7 @@
*/
export default {
primaryColor: '#1890ff', // primary color of ant design
primaryColor: '#2f54eb', // primary color of ant design
navTheme: 'dark', // theme for nav menu
layout: 'sidemenu', // nav menu position: sidemenu or topmenu
contentWidth: 'Fixed', // layout of content: Fluid or Fixed, only works when layout is topmenu

View File

@@ -25,6 +25,7 @@ export default {
deleting: 'Deleting',
deletingTip: 'Deleting, total of {total}, {successNum} succeeded, {errorNum} failed',
grant: 'Grant',
revoke: 'Revoke',
login_at: 'Login At',
logout_at: 'Logout At',
createSuccess: 'Create Success',
@@ -145,6 +146,26 @@ export default {
sizeLimit: 'The image size cannot exceed 2MB!',
nodata: 'There are currently no custom icons available. Click here to upload'
},
regexSelect: {
limitedFormat: 'Limited Format',
regExp: 'RegExp',
errMsg: 'Error Message',
test: 'Test',
placeholder: 'Please Select RegExp',
error: 'Error',
letter: 'letter',
number: 'number',
letterAndNumber: 'letter&number',
phone: 'phone',
landline: 'landline',
zipCode: 'zip code',
IDCard: 'ID card',
ip: 'IP',
email: 'email',
link: 'link',
monetaryAmount: 'monetary amount',
custom: 'custom',
},
cmdb: cmdb_en,
cs: cs_en,
acl: acl_en,

View File

@@ -25,6 +25,7 @@ export default {
deleting: '正在删除',
deletingTip: '正在删除,共{total}个,成功{successNum}个,失败{errorNum}个',
grant: '授权',
revoke: '回收',
login_at: '登录时间',
logout_at: '登出时间',
createSuccess: '创建成功',
@@ -145,6 +146,26 @@ export default {
sizeLimit: '图片大小不可超过2MB',
nodata: '暂无自定义图标,点击此处上传'
},
regexSelect: {
limitedFormat: '限定格式',
regExp: '正则表达式',
errMsg: '错误时提示',
test: '测试',
placeholder: '请选择正则表达式',
error: '错误',
letter: '字母',
number: '数字',
letterAndNumber: '字母和数字',
phone: '手机号码',
landline: '座机',
zipCode: '邮政编码',
IDCard: '身份证号',
ip: 'IP地址',
email: '邮箱',
link: '链接',
monetaryAmount: '货币金额',
custom: '自定义',
},
cmdb: cmdb_zh,
cs: cs_zh,
acl: acl_zh,

View File

@@ -57,7 +57,7 @@
</a-tag>
</template>
</vxe-column>
<vxe-column field="operate" :title="$t('batchOperate')">
<vxe-column field="operate" :title="$t('acl.batchOperate')">
<template #default="{row}">
<a-button size="small" type="danger" @click="handleClearAll(row)">
{{ $t('clear') }}

View File

@@ -57,17 +57,20 @@
</template>
</vxe-table-column>
</ops-table>
<vxe-pager
<a-pagination
size="small"
:layouts="['Total', 'PrevPage', 'JumpNumber', 'NextPage', 'Sizes']"
:current-page.sync="tablePage.currentPage"
:page-size.sync="tablePage.pageSize"
show-size-changer
show-quick-jumper
:current="tablePage.currentPage"
:total="tablePage.total"
:page-sizes="pageSizeOptions"
@page-change="handlePageChange"
:style="{ marginTop: '10px' }"
>
</vxe-pager>
:show-total="(total, range) => `当前展示 ${range[0]}-${range[1]} 条数据, 共 ${total} 条`"
:page-size="tablePage.pageSize"
:default-current="1"
:page-size-options="pageSizeOptions"
@change="pageOrSizeChange"
@showSizeChange="pageOrSizeChange"
:style="{ marginTop: '10px', textAlign: 'right' }"
/>
</a-spin>
<resourceTypeForm ref="resourceTypeForm" :handleOk="handleOk"> </resourceTypeForm>
@@ -89,7 +92,7 @@ export default {
loading: false,
groups: [],
id2perms: {},
pageSizeOptions: [10, 25, 50, 100],
pageSizeOptions: ['20', '50', '100', '200'],
tablePage: {
total: 0,
currentPage: 1,
@@ -176,7 +179,7 @@ export default {
this.handleOk()
})
},
handlePageChange({ currentPage, pageSize }) {
pageOrSizeChange(currentPage, pageSize) {
this.tablePage.currentPage = currentPage
this.tablePage.pageSize = pageSize
this.searchData()

View File

@@ -86,10 +86,12 @@
<vxe-table-column field="user" :title="$t('acl.creator')" :min-widh="100"> </vxe-table-column>
<!-- 4 -->
<vxe-table-column field="created_at" :title="$t('created_at')" :min-widh="220" align="center"> </vxe-table-column>
<vxe-table-column field="created_at" :title="$t('created_at')" :min-widh="220" align="center">
</vxe-table-column>
<!-- 5 -->
<vxe-table-column field="updated_at" :title="$t('updated_at')" :min-widh="220" fixed="center"> </vxe-table-column>
<vxe-table-column field="updated_at" :title="$t('updated_at')" :min-widh="220" fixed="center">
</vxe-table-column>
<!-- 6 -->
@@ -99,7 +101,8 @@
:min-widh="200"
fixed="right"
align="center"
show-overflow>
show-overflow
>
<template #default="{ row }">
<span v-show="isGroup">
<a @click="handleDisplayMember(row)">{{ $t('acl.member') }}</a>
@@ -117,23 +120,32 @@
</a>
</a-tooltip>
<a-divider type="vertical" />
<a-popconfirm :title="$t('confirmDelete')" @confirm="handleDelete(row)" @cancel="cancel" :okText="$t('yes')" :cancelText="$t('no')">
<a-popconfirm
:title="$t('confirmDelete')"
@confirm="handleDelete(row)"
@cancel="cancel"
:okText="$t('yes')"
:cancelText="$t('no')"
>
<a style="color: red"><a-icon type="delete"/></a>
</a-popconfirm>
</template>
</vxe-table-column>
</ops-table>
<vxe-pager
<a-pagination
size="small"
:layouts="['Total', 'PrevPage', 'JumpNumber', 'NextPage', 'Sizes']"
:current-page.sync="tablePage.currentPage"
:page-size.sync="tablePage.pageSize"
show-size-changer
show-quick-jumper
:current="tablePage.currentPage"
:total="tablePage.total"
:page-sizes="pageSizeOptions"
@page-change="handlePageChange"
:style="{ marginTop: '10px' }"
>
</vxe-pager>
:show-total="(total, range) => `当前展示 ${range[0]}-${range[1]} 条数据, 共 ${total} 条`"
:page-size="tablePage.pageSize"
:default-current="1"
:page-size-options="pageSizeOptions"
@change="pageOrSizeChange"
@showSizeChange="pageOrSizeChange"
:style="{ marginTop: '10px', textAlign: 'right' }"
/>
</a-spin>
</div>
<div v-else style="text-align: center; margin-top: 20%">
@@ -191,7 +203,7 @@ export default {
isGroup: false,
allResourceTypes: [],
currentType: { id: 0 },
pageSizeOptions: [10, 25, 50, 100],
pageSizeOptions: ['20', '50', '100', '200'],
searchName: '',
selectedRows: [],
}
@@ -291,7 +303,7 @@ export default {
}
},
cancel() {},
handlePageChange({ currentPage, pageSize }) {
pageOrSizeChange(currentPage, pageSize) {
this.tablePage.currentPage = currentPage
this.tablePage.pageSize = pageSize
this.searchData()
@@ -302,7 +314,6 @@ export default {
.getVxetableRef()
.getCheckboxRecords()
.concat(this.$refs.xTable.getVxetableRef().getCheckboxReserveRecords())
console.log(this.selectedRows)
},
onSelectRangeEnd({ records }) {
this.selectedRows = records

View File

@@ -104,17 +104,20 @@
</template>
</vxe-table-column>
</ops-table>
<vxe-pager
<a-pagination
size="small"
:layouts="['Total', 'PrevPage', 'JumpNumber', 'NextPage', 'Sizes']"
:current-page.sync="tablePage.currentPage"
:page-size.sync="tablePage.pageSize"
show-size-changer
show-quick-jumper
:current="tablePage.currentPage"
:total="tablePage.total"
:page-sizes="pageSizeOptions"
@page-change="handlePageChange"
:style="{ marginTop: '10px' }"
>
</vxe-pager>
:show-total="(total, range) => `当前展示 ${range[0]}-${range[1]} 条数据, 共 ${total} 条`"
:page-size="tablePage.pageSize"
:default-current="1"
:page-size-options="pageSizeOptions"
@change="pageOrSizeChange"
@showSizeChange="pageOrSizeChange"
:style="{ marginTop: '10px', textAlign: 'right' }"
/>
</a-spin>
<roleForm ref="roleForm" :allRoles="allRoles" :id2parents="id2parents" :handleOk="handleOk"></roleForm>
@@ -149,7 +152,7 @@ export default {
tableData: [],
allRoles: [],
id2parents: {},
pageSizeOptions: [10, 25, 50, 100],
pageSizeOptions: ['20', '50', '100', '200'],
searchName: '',
filterTableValue: { user_role: 1, user_only: 0 },
}
@@ -254,7 +257,7 @@ export default {
cancel(e) {
return false
},
handlePageChange({ currentPage, pageSize }) {
pageOrSizeChange(currentPage, pageSize) {
this.tablePage.currentPage = currentPage
this.tablePage.pageSize = pageSize
this.loadData()

View File

@@ -205,3 +205,28 @@ export function ciTypeFilterPermissions(type_id) {
method: 'get',
})
}
// parent_ids, child_id
export function postCiTypeInheritance(data) {
return axios({
url: `/v0.1/ci_types/inheritance`,
method: 'post',
data
})
}
// parent_id, child_id
export function deleteCiTypeInheritance(data) {
return axios({
url: `/v0.1/ci_types/inheritance`,
method: 'delete',
data
})
}
export function getCITypeIcons() {
return axios({
url: '/v0.1/ci_types/icons',
method: 'GET',
})
}

View File

@@ -114,3 +114,12 @@ export function revokeRelationView(rid, data) {
data: data
})
}
// preference citype order
export function preferenceCitypeOrder(data) {
return axios({
url: `/v0.1/preference/ci_types/order`,
method: 'POST',
data: data
})
}

View File

@@ -14,9 +14,16 @@
<script>
import EmployeeTransfer from '@/components/EmployeeTransfer'
import RoleTransfer from '@/components/RoleTransfer'
export default {
name: 'GrantModal',
components: { EmployeeTransfer, RoleTransfer },
props: {
customTitle: {
type: String,
default: '',
},
},
data() {
return {
visible: false,
@@ -25,6 +32,9 @@ export default {
},
computed: {
title() {
if (this.customTitle) {
return this.customTitle
}
if (this.type === 'depart') {
return this.$t('cmdb.components.grantUser')
}

View File

@@ -6,7 +6,8 @@
{ value: 2, label: $t('cmdb.components.customize'), layout: 'vertical' },
{ value: 3, label: $t('cmdb.components.none') },
]"
v-model="radioValue"
:value="radioValue"
@change="changeRadioValue"
>
<template slot="extra_2" v-if="radioValue === 2">
<treeselect
@@ -128,6 +129,9 @@ export default {
this.visible = true
this.colType = colType
this.row = row
this.form = {
name: '',
}
if (this.colType === 'read_ci') {
await getCITypeAttributesByTypeIds({ type_ids: this.CITypeId }).then((res) => {
this.canSearchPreferenceAttrList = res.attributes.filter((item) => item.value_type !== '6')
@@ -149,10 +153,6 @@ export default {
})
}
}
} else {
this.form = {
name: '',
}
}
},
async handleOk() {
@@ -198,6 +198,13 @@ export default {
}
this.expression = expression
},
changeRadioValue(value) {
if (this.id_filter) {
this.$message.warning(this.$t('cmdb.serviceTree.grantedByServiceTreeTips'))
} else {
this.radioValue = value
}
},
},
}
</script>

View File

@@ -0,0 +1,122 @@
<template>
<a-modal :visible="visible" @cancel="handleCancel" @ok="handleOK" :title="$t('revoke')">
<a-form-model :model="form" :label-col="{ span: 4 }" :wrapper-col="{ span: 16 }">
<a-form-model-item :label="$t('user')">
<EmployeeTreeSelect
class="custom-treeselect custom-treeselect-bgcAndBorder"
:style="{
'--custom-height': '32px',
lineHeight: '32px',
'--custom-bg-color': '#fff',
'--custom-border': '1px solid #d9d9d9',
'--custom-multiple-lineHeight': '18px',
}"
:multiple="true"
v-model="form.users"
:placeholder="$t('cmdb.serviceTree.userPlaceholder')"
:idType="2"
departmentKey="acl_rid"
employeeKey="acl_rid"
/>
</a-form-model-item>
<a-form-model-item :label="$t('role')">
<treeselect
v-model="form.roles"
:multiple="true"
:options="filterAllRoles"
class="custom-treeselect custom-treeselect-bgcAndBorder"
:style="{
'--custom-height': '32px',
lineHeight: '32px',
'--custom-bg-color': '#fff',
'--custom-border': '1px solid #d9d9d9',
'--custom-multiple-lineHeight': '18px',
}"
:limit="10"
:limitText="(count) => `+ ${count}`"
:normalizer="
(node) => {
return {
id: node.id,
label: node.name,
}
}
"
appendToBody
zIndex="1050"
:placeholder="$t('cmdb.serviceTree.rolePlaceholder')"
@search-change="searchRole"
/>
</a-form-model-item>
</a-form-model>
</a-modal>
</template>
<script>
import EmployeeTreeSelect from '@/views/setting/components/employeeTreeSelect.vue'
import { getAllDepAndEmployee } from '@/api/company'
import { searchRole } from '@/modules/acl/api/role'
export default {
name: 'RevokeModal',
components: { EmployeeTreeSelect },
data() {
return {
visible: false,
form: {
users: undefined,
roles: undefined,
},
allTreeDepAndEmp: [],
allRoles: [],
filterAllRoles: [],
}
},
provide() {
return {
provide_allTreeDepAndEmp: () => {
return this.allTreeDepAndEmp
},
}
},
mounted() {
this.getAllDepAndEmployee()
this.loadRoles()
},
methods: {
async loadRoles() {
const res = await searchRole({ page_size: 9999, app_id: 'cmdb', is_all: true })
this.allRoles = res.roles
this.filterAllRoles = this.allRoles.slice(0, 100)
},
getAllDepAndEmployee() {
getAllDepAndEmployee({ block: 0 }).then((res) => {
this.allTreeDepAndEmp = res
})
},
open() {
this.visible = true
this.$nextTick(() => {
this.form = {
users: undefined,
roles: undefined,
}
})
},
handleCancel() {
this.visible = false
},
searchRole(searchQuery) {
this.filterAllRoles = this.allRoles
.filter((item) => item.name.toLowerCase().includes(searchQuery.toLowerCase()))
.slice(0, 100)
},
handleOK() {
this.$emit('handleRevoke', this.form)
this.handleCancel()
},
},
}
</script>
<style></style>

View File

@@ -0,0 +1,148 @@
<template>
<treeselect
:disabled="disabled"
ref="cmdb_type_select"
:disable-branch-nodes="true"
class="custom-treeselect custom-treeselect-bgcAndBorder"
:style="{
'--custom-height': '30px',
lineHeight: '30px',
'--custom-bg-color': '#fff',
'--custom-border': '1px solid #d9d9d9',
}"
v-model="currenCiType"
:multiple="multiple"
:clearable="true"
searchable
:options="ciTypeGroup"
value-consists-of="LEAF_PRIORITY"
:placeholder="placeholder || `${$t(`placeholder2`)}`"
:load-options="loadOptions"
@select="
(node, instanceId) => {
$emit('select', node, instanceId)
}
"
@deselect="
(node, instanceId) => {
$emit('deselect', node, instanceId)
}
"
:normalizer="
(node) => {
return {
id: node.id || -1,
label: node.alias || node.name || '其他',
title: node.alias || node.name || '其他',
}
}
"
>
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
{{ node.label }}
</div>
<div slot="value-label" slot-scope="{ node }">{{ getTreeSelectLabel(node) }}</div>
</treeselect>
</template>
<script>
import _ from 'lodash'
import { getCITypeGroupsConfig } from '@/modules/cmdb/api/ciTypeGroup'
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
import { getTreeSelectLabel } from '../../utils/helper'
export default {
name: 'CMDBTypeSelect',
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: [String, Number, Array],
default: null,
},
selectType: {
type: String,
default: 'attributes',
},
attrIdkey: {
type: String,
default: 'id',
},
disabled: {
type: Boolean,
default: false,
},
multiple: {
type: Boolean,
default: false,
},
placeholder: {
type: String,
default: '',
},
},
data() {
return {
ciTypeGroup: [],
childrenOptions: [],
}
},
computed: {
currenCiType: {
get() {
return this.value
},
set(val) {
this.$emit('change', val)
return val
},
},
},
async mounted() {
if (this.value) {
const typeId = this.value.split('-')[0]
await getCITypeAttributesById(this.value.split('-')[0]).then((res) => {
this.childrenOptions = res.attributes.map((item) => ({ ...item, id: `${typeId}-${item[this.attrIdkey]}` }))
})
}
this.getCITypeGroups()
},
methods: {
getTreeSelectLabel,
getCITypeGroups() {
getCITypeGroupsConfig({ need_other: true }).then((res) => {
this.ciTypeGroup = res
.filter((item) => item.ci_types && item.ci_types.length)
.map((item) => {
item.id = `type_${item.id || -1}`
item.children = item.ci_types.map((type) => {
const obj = { ...type }
if (this.selectType === 'attributes') {
obj.children = this.value && type.id === Number(this.value.split('-')[0]) ? this.childrenOptions : null
}
return obj
})
return { ..._.cloneDeep(item) }
})
})
},
loadOptions({ action, parentNode, callback }) {
getCITypeAttributesById(parentNode.id).then((res) => {
parentNode.children = res.attributes.map((item) => ({
...item,
id: `${parentNode.id}-${item[this.attrIdkey]}`,
}))
callback()
})
},
},
}
</script>
<style></style>

View File

@@ -0,0 +1,2 @@
import CMDBTypeSelect from './cmdbTypeSelect.vue'
export default CMDBTypeSelect

View File

@@ -52,7 +52,7 @@
:style="{ color: fuzzySearch ? '#2f54eb' : '', cursor: 'pointer' }"
@click="emitRefresh"
/>
<a-tooltip slot="prefix" placement="bottom" :overlayStyle="{ maxWidth: '550px' }">
<a-tooltip slot="prefix" placement="bottom" :overlayStyle="{ maxWidth: '550px', whiteSpace: 'pre-line' }">
<template slot="title">
{{ $t('cmdb.components.ciSearchTips') }}
</template>
@@ -97,6 +97,7 @@
</a-space>
</div>
<a-space>
<slot name="extraContent"></slot>
<a-button @click="reset" size="small">{{ $t('reset') }}</a-button>
<a-tooltip :title="$t('cmdb.components.attributeDesc')" v-if="type === 'relationView'">
<a
@@ -145,7 +146,7 @@ export default {
selectedRowKeys: {
type: Array,
default: () => [],
}
},
},
data() {
return {
@@ -179,13 +180,21 @@ export default {
this.fuzzySearch = ''
},
},
inject: ['setPreferenceSearchCurrent'],
inject: {
setPreferenceSearchCurrent: {
from: 'setPreferenceSearchCurrent',
default: null,
},
},
mounted() {
if (this.type === 'resourceSearch') {
this.getCITypeGroups()
}
},
methods: {
// toggleAdvanced() {
// this.advanced = !this.advanced
// },
getCITypeGroups() {
getCITypeGroups({ need_other: true }).then((res) => {
this.ciTypeGroup = res
@@ -234,7 +243,9 @@ export default {
}
},
emitRefresh() {
if (this.setPreferenceSearchCurrent) {
this.setPreferenceSearchCurrent(null)
}
this.$nextTick(() => {
this.$emit('refresh', true)
})

View File

@@ -88,7 +88,9 @@ export default {
} catch {}
const headers = {}
this.$refs.Header.headers.forEach((item) => {
if (item.key) {
headers[item.key] = item.value
}
})
let authorization = {}
const type = this.$refs.Authorization.authorizationType

View File

@@ -19,6 +19,7 @@ const cmdb_en = {
operationHistory: 'Operation Audit',
relationType: 'Relation Type',
ad: 'AutoDiscovery',
cidetail: 'CI Detail'
},
ciType: {
ciType: 'CIType',
@@ -45,8 +46,9 @@ const cmdb_en = {
selectDefaultOrderAttr: 'Select default sorting attributes',
asec: 'Forward order',
desc: 'Reverse order',
uniqueKey: 'Uniquely Identifies',
uniqueKey: 'Unique Identifies',
uniqueKeySelect: 'Please select a unique identifier',
uniqueKeyTips: 'json/password/computed/choice can not be unique identifies',
notfound: 'Can\'t find what you want?',
cannotDeleteGroupTips: 'There is data under this group and cannot be deleted!',
confirmDeleteGroup: 'Are you sure you want to delete group [{groupName}]?',
@@ -174,7 +176,13 @@ const cmdb_en = {
date: 'Date',
time: 'Time',
json: 'JSON',
event: 'Event'
event: 'Event',
reg: 'Regex',
isInherit: 'Inherit',
inheritType: 'Inherit Type',
inheritTypePlaceholder: 'Please select inherit types',
inheritFrom: 'inherit from {name}',
groupInheritFrom: 'Please go to the {name} for modification'
},
components: {
unselectAttributes: 'Unselected',
@@ -216,7 +224,7 @@ const cmdb_en = {
pleaseSearch: 'Please search',
conditionFilter: 'Conditional filtering',
attributeDesc: 'Attribute Description',
ciSearchTips: '1. JSON attributes cannot be searched<br />2. If the search content includes commas, they need to be escaped,<br />3. Only index attributes are searched, non-index attributes use conditional filtering',
ciSearchTips: '1. JSON/password/link attributes cannot be searched\n2. If the search content includes commas, they need to be escaped\n3. Only index attributes are searched, non-index attributes use conditional filtering',
ciSearchTips2: 'For example: q=hostname:*0.0.0.0*',
subCIType: 'Subscription CIType',
already: 'already',
@@ -366,12 +374,12 @@ const cmdb_en = {
ad: {
upload: 'Import',
download: 'Export',
accpet: 'Accept',
accpetBy: 'Accept By',
accept: 'Accept',
acceptBy: 'Accept By',
acceptTime: 'Accept Time',
confirmAccept: 'Confirm Accept?',
accpetSuccess: 'Accept successfully',
isAccpet: 'Is accept',
acceptSuccess: 'Accept successfully',
isAccept: 'Is accept',
deleteADC: 'Confirm to delete this data?',
batchDelete: 'Confirm to delete this data?',
agent: 'Built-in & Plug-ins',
@@ -459,13 +467,15 @@ const cmdb_en = {
tips3: 'Please select the fields that need to be modified',
tips4: 'At least one field must be selected',
tips5: 'Search name | alias',
tips6: 'Speed up retrieval, full-text search possible, no need to use conditional filtering\n\n json currently does not support indexing \n\nText characters longer than 190 cannot be indexed',
tips6: 'Speed up retrieval, full-text search possible, no need to use conditional filtering\n\n json/link/password currently does not support indexing \n\nText characters longer than 190 cannot be indexed',
tips7: 'The form of expression is a drop-down box, and the value must be in the predefined value',
tips8: 'Multiple values, such as intranet IP',
tips9: 'For front-end only',
tips10: 'Other attributes of the CIType are computed using expressions\n\nA code snippet computes the returned value.',
newUpdateField: 'Add a Attribute',
attributeSettings: 'Attribute Settings',
share: 'Share',
noPermission: 'No Permission'
},
serviceTree: {
deleteNode: 'Delete Node',
@@ -474,6 +484,16 @@ const cmdb_en = {
alert1: 'The administrator has not configured the ServiceTree(relation view), or you do not have permission to access it!',
copyFailed: 'Copy failed',
deleteRelationConfirm: 'Confirm to remove selected {name} from current relationship?',
batch: 'Batch',
grantTitle: 'Grant(read)',
userPlaceholder: 'Please select users',
rolePlaceholder: 'Please select roles',
grantedByServiceTree: 'Granted By Service Tree:',
grantedByServiceTreeTips: 'Please delete id_filter in Servive Tree',
peopleHasRead: 'Personnel authorized to read:',
authorizationPolicy: 'CI Authorization Policy:',
idAuthorizationPolicy: 'Authorized by node:',
view: 'View permissions'
},
tree: {
tips1: 'Please go to Preference page first to complete your subscription!',

View File

@@ -19,6 +19,7 @@ const cmdb_zh = {
operationHistory: '操作审计',
relationType: '关系类型',
ad: '自动发现',
cidetail: 'CI 详情'
},
ciType: {
ciType: '模型',
@@ -47,6 +48,7 @@ const cmdb_zh = {
desc: '倒序',
uniqueKey: '唯一标识',
uniqueKeySelect: '请选择唯一标识',
uniqueKeyTips: 'json、密码、计算属性、预定义值属性不能作为唯一标识',
notfound: '找不到想要的?',
cannotDeleteGroupTips: '该分组下有数据, 不能删除!',
confirmDeleteGroup: '确定要删除分组 【{groupName}】 吗?',
@@ -174,7 +176,13 @@ const cmdb_zh = {
date: '日期',
time: '时间',
json: 'JSON',
event: '事件'
event: '事件',
reg: '正则校验',
isInherit: '是否继承',
inheritType: '继承模型',
inheritTypePlaceholder: '请选择继承模型(多选)',
inheritFrom: '属性继承自{name}',
groupInheritFrom: '请至{name}进行修改'
},
components: {
unselectAttributes: '未选属性',
@@ -216,7 +224,7 @@ const cmdb_zh = {
pleaseSearch: '请查找',
conditionFilter: '条件过滤',
attributeDesc: '属性说明',
ciSearchTips: '1. json属性不能搜索<br />2. 搜索内容包括逗号, 则需转义 ,<br />3. 只搜索索引属性, 非索引属性使用条件过滤',
ciSearchTips: '1. json、密码、链接属性不能搜索\n2. 搜索内容包括逗号, 则需转义\n3. 只搜索索引属性, 非索引属性使用条件过滤',
ciSearchTips2: '例: q=hostname:*0.0.0.0*',
subCIType: '订阅模型',
already: '已',
@@ -366,12 +374,12 @@ const cmdb_zh = {
ad: {
upload: '规则导入',
download: '规则导出',
accpet: '入库',
accpetBy: '入库人',
accept: '入库',
acceptBy: '入库人',
acceptTime: '入库时间',
confirmAccept: '确认入库?',
accpetSuccess: '入库成功',
isAccpet: '是否入库',
acceptSuccess: '入库成功',
isAccept: '是否入库',
deleteADC: '确认删除该条数据?',
batchDelete: '确认删除这些数据?',
agent: '内置 & 插件',
@@ -458,13 +466,15 @@ const cmdb_zh = {
tips3: '请选择需要修改的字段',
tips4: '必须至少选择一个字段',
tips5: '搜索 名称 | 别名',
tips6: '加快检索, 可以全文搜索, 无需使用条件过滤\n\n json目前不支持建索引 \n\n文本字符长度超过190不能建索引',
tips6: '加快检索, 可以全文搜索, 无需使用条件过滤\n\n json、链接、密码目前不支持建索引 \n\n文本字符长度超过190不能建索引',
tips7: '表现形式是下拉框, 值必须在预定义值里',
tips8: '多值, 比如内网IP',
tips9: '仅针对前端',
tips10: '模型的其他属性通过表达式的方式计算出来\n\n一个代码片段计算返回的值',
newUpdateField: '新增修改字段',
attributeSettings: '字段设置',
share: '分享',
noPermission: '暂无权限'
},
serviceTree: {
deleteNode: '删除节点',
@@ -473,6 +483,16 @@ const cmdb_zh = {
alert1: '管理员 还未配置业务关系, 或者你无权限访问!',
copyFailed: '复制失败',
deleteRelationConfirm: '确认将选中的 {name} 从当前关系中删除?',
batch: '批量操作',
grantTitle: '授权(查看权限)',
userPlaceholder: '请选择用户',
rolePlaceholder: '请选择角色',
grantedByServiceTree: '服务树授权:',
grantedByServiceTreeTips: '请先在服务树里删掉节点授权',
peopleHasRead: '当前有查看权限的人员:',
authorizationPolicy: '实例授权策略:',
idAuthorizationPolicy: '按节点授权的:',
view: '查看权限'
},
tree: {
tips1: '请先到 我的订阅 页面完成订阅!',

View File

@@ -56,6 +56,13 @@ const genCmdbRoutes = async () => {
meta: { title: 'cmdb.menu.adCIs', icon: 'ops-cmdb-adc', selectedIcon: 'ops-cmdb-adc-selected', keepAlive: false },
component: () => import('../views/discoveryCI/index.vue')
},
{
path: `/cmdb/cidetail/:typeId/:ciId`,
name: 'cmdb_ci_detail',
hidden: true,
meta: { title: 'cmdb.menu.cidetail', keepAlive: false },
component: () => import('../views/ci/ciDetailPage.vue')
},
{
path: '/cmdb/disabled2',
name: 'cmdb_disabled2',
@@ -138,7 +145,7 @@ const genCmdbRoutes = async () => {
const [preference, relation] = await Promise.all([getPreference(), getRelationView()])
preference.forEach(item => {
routes.children[2].children.unshift({
routes.children[2].children.push({
path: `/cmdb/instances/types/${item.id}`,
component: () => import(`../views/ci/index`),
name: `cmdb_${item.id}`,

View File

@@ -179,3 +179,13 @@ export const downloadExcel = (data, fileName = `${moment().format('YYYY-MM-DD HH
// STEP 4: Write Excel file to browser #导出
XLSXS.writeFile(wb, fileName + '.xlsx')
}
export const getAllParentNodesLabel = (node, label) => {
if (node.parentNode) {
return getAllParentNodesLabel(node.parentNode, `${node.parentNode.label}-${label}`)
}
return label
}
export const getTreeSelectLabel = (node) => {
return `${getAllParentNodesLabel(node, node.label)}`
}

View File

@@ -0,0 +1,71 @@
<template>
<div>
<div class="ci-detail-header">{{ this.type.alias }}</div>
<div class="ci-detail-page">
<CiDetailTab ref="ciDetailTab" :typeId="typeId" />
</div>
</div>
</template>
<script>
import CiDetailTab from './modules/ciDetailTab.vue'
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
import { getCIType } from '@/modules/cmdb/api/CIType'
export default {
name: 'CiDetailPage',
components: { CiDetailTab },
data() {
return {
typeId: Number(this.$route.params.typeId),
type: {},
attrList: [],
attributes: {},
}
},
provide() {
return {
attrList: () => {
return this.attrList
},
attributes: () => {
return this.attributes
},
}
},
mounted() {
const { ciId = undefined } = this.$route.params
if (ciId) {
this.$refs.ciDetailTab.create(Number(ciId))
}
getCIType(this.typeId).then((res) => {
this.type = res.ci_types[0]
})
this.getAttributeList()
},
methods: {
async getAttributeList() {
await getCITypeAttributesById(this.typeId).then((res) => {
this.attrList = res.attributes
this.attributes = res
})
},
},
}
</script>
<style lang="less" scoped>
@import '~@/style/static.less';
.ci-detail-header {
border-left: 3px solid @primary-color;
padding-left: 10px;
font-size: 16px;
font-weight: 700;
margin-bottom: 10px;
}
.ci-detail-page {
background-color: #fff;
height: calc(100vh - 122px);
}
</style>

View File

@@ -57,7 +57,7 @@
<span>{{ $t('cmdb.ci.selectRows', { rows: selectedRowKeys.length }) }}</span>
</div>
</SearchForm>
<CiDetail ref="detail" :typeId="typeId" />
<CiDetailDrawer ref="detail" :typeId="typeId" />
<ops-table
:id="`cmdb-ci-${typeId}`"
border
@@ -101,7 +101,6 @@
:cell-type="col.value_type === '2' ? 'string' : 'auto'"
:fixed="col.is_fixed ? 'left' : ''"
>
<!-- <template #edit="{row}"><a-input v-model="row[col.field]"></a-input></template> -->
<template #header>
<span class="vxe-handle">
<OpsMoveIcon
@@ -110,7 +109,7 @@
<span>{{ col.title }}</span>
</span>
</template>
<template v-if="col.is_choice || col.is_password || col.is_list" #edit="{ row }">
<template v-if="col.is_choice || col.is_password" #edit="{ row }">
<vxe-input v-if="col.is_password" v-model="passwordValue[col.field]" />
<a-select
:getPopupContainer="(trigger) => trigger.parentElement"
@@ -147,18 +146,6 @@
</span>
</a-select-option>
</a-select>
<a-select
:getPopupContainer="(trigger) => trigger.parentElement"
:style="{ width: '100%', height: '32px' }"
v-model="row[col.field]"
:placeholder="$t('placeholder2')"
v-else-if="col.is_list"
:showArrow="false"
mode="tags"
class="ci-table-edit-select"
allowClear
>
</a-select>
</template>
<template
v-if="col.value_type === '6' || col.is_link || col.is_password || col.is_choice"
@@ -293,8 +280,6 @@
</a-pagination>
</div>
<create-instance-form ref="create" @reload="reloadData" @submit="batchUpdate" />
<!-- <EditAttrsDrawer ref="editAttrsDrawer" @refresh="refreshAfterEditAttrs" /> -->
<!-- <batch-update-relation :typeId="typeId" ref="batchUpdateRelation" @submit="batchUpdateRelation" /> -->
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
<BatchDownload ref="batchDownload" @batchDownload="batchDownload" />
<MetadataDrawer ref="metadataDrawer" />
@@ -312,7 +297,7 @@ import router, { resetRouter } from '@/router'
import SearchForm from '../../components/searchForm/SearchForm.vue'
import CreateInstanceForm from './modules/CreateInstanceForm'
import CiDetail from './modules/CiDetail'
import CiDetailDrawer from './modules/ciDetailDrawer.vue'
import EditAttrsPopover from './modules/editAttrsPopover'
import JsonEditor from '../../components/JsonEditor/jsonEditor.vue'
import { searchCI, updateCI, deleteCI } from '@/modules/cmdb/api/ci'
@@ -335,7 +320,7 @@ export default {
components: {
SearchForm,
CreateInstanceForm,
CiDetail,
CiDetailDrawer,
JsonEditor,
PasswordField,
EditAttrsPopover,
@@ -471,11 +456,6 @@ export default {
const regSort = /(?<=sort=).+/g
const exp = expression.match(regQ) ? expression.match(regQ)[0] : null
// if (exp) {
// exp = exp.replace(/(\:)/g, '$1*')
// exp = exp.replace(/(\,)/g, '*$1')
// }
// If the sorting is done by clicking on the table, the table will prevail.
let sort
if (sortByTable) {
sort = sortByTable
@@ -484,7 +464,6 @@ export default {
}
const res = await searchCI({
q: `_type:${this.typeId}${exp ? `,${exp}` : ''}${fuzzySearch ? `,*${fuzzySearch}*` : ''}`,
// q: `${this.mergeQ(queryParams)}${exp ? `,${exp}` : ''}${fuzzySearch ? `,*${fuzzySearch}*` : ''}`,
count: this.pageSize,
page: this.currentPage,
sort,
@@ -533,55 +512,17 @@ export default {
this.$refs['xTable'].getVxetableRef().setCheckboxRow(rows, true)
}
},
// mergeQ(params) {
// let q = `_type:${this.typeId}`
// Object.keys(params).forEach((key) => {
// if (!['pageNo', 'pageSize', 'sortField', 'sortOrder'].includes(key) && params[key] + '' !== '') {
// if (typeof params[key] === 'object' && params[key] && params[key].length > 1) {
// q += `,${key}:(${params[key].join(';')})`
// } else if (params[key]) {
// q += `,${key}:*${params[key]}*`
// }
// }
// })
// return q
// },
async loadPreferenceAttrList() {
const subscribed = await getSubscribeAttributes(this.typeId)
this.preferenceAttrList = subscribed.attributes // All columns that have been subscribed
},
onSelectChange() {
// const current = records.map((i) => i.ci_id || i._id)
// const cached = new Set(this.selectedRowKeys)
// if (checked) {
// current.forEach((i) => {
// cached.add(i)
// })
// } else {
// if (row) {
// cached.delete(row.ci_id || row._id)
// } else {
// this.instanceList.map((row) => {
// cached.delete(row.ci_id || row._id)
// })
// }
// }
const xTable = this.$refs.xTable.getVxetableRef()
const records = [...xTable.getCheckboxRecords(), ...xTable.getCheckboxReserveRecords()]
this.selectedRowKeys = records.map((i) => i.ci_id || i._id)
},
onSelectRangeEnd({ records }) {
// const current = records.map((i) => i.ci_id || i._id)
// const cached = new Set(this.selectedRowKeys)
// current.forEach((i) => {
// cached.add(i)
// })
this.selectedRowKeys = records.map((i) => i.ci_id || i._id)
// this.setSelectRows()
},
reloadData() {
this.loadTableData()
@@ -623,7 +564,7 @@ export default {
})
.catch((err) => {
console.log(err)
$table.revertData(row)
this.loadTableData()
})
}
this.columns.forEach((col) => {
@@ -694,12 +635,22 @@ export default {
}
})
this.$refs.create.visible = false
const key = 'updatable'
let errorMsg = ''
for (let i = 0; i < this.selectedRowKeys.length; i++) {
await updateCI(this.selectedRowKeys[i], payload, false)
.then(() => {
successNum += 1
})
.catch(() => {
.catch((error) => {
errorMsg = errorMsg + '\n' + `${this.selectedRowKeys[i]}:${error.response?.data?.message ?? ''}`
this.$notification.warning({
key,
message: this.$t('warning'),
description: errorMsg,
duration: 0,
style: { whiteSpace: 'break-spaces' },
})
errorNum += 1
})
.finally(() => {

View File

@@ -24,7 +24,9 @@
/>
</template>
<template v-if="parentsType && parentsType.length">
<a-divider style="font-size:14px;margin:14px 0;font-weight:700;">{{ $t('cmdb.menu.citypeRelation') }}</a-divider>
<a-divider style="font-size:14px;margin:14px 0;font-weight:700;">{{
$t('cmdb.menu.citypeRelation')
}}</a-divider>
<a-form>
<a-row :gutter="24" align="top" type="flex">
<a-col :span="12" v-for="item in parentsType" :key="item.id">
@@ -40,7 +42,11 @@
{{ attr.alias || attr.name }}
</a-select-option>
</a-select>
<a-input :placeholder="$t('cmdb.ci.tips1')" v-model="parentsForm[item.name].value" style="width: 50%" />
<a-input
:placeholder="$t('cmdb.ci.tips1')"
v-model="parentsForm[item.name].value"
style="width: 50%"
/>
</a-input-group>
</a-form-item>
</a-col>
@@ -214,6 +220,7 @@ export default {
if (otherGroupAttr.length) {
_attributesByGroup.push({ id: -1, name: this.$t('other'), attributes: otherGroupAttr })
}
console.log(otherGroupAttr, _attributesByGroup)
this.attributesByGroup = _attributesByGroup
})
},
@@ -290,6 +297,38 @@ export default {
_this.$emit('reload', { ci_id: res.ci_id })
})
}
// this.form.validateFields((err, values) => {
// if (err) {
// _this.$message.error('字段填写不符合要求!')
// return
// }
// Object.keys(values).forEach((k) => {
// if (Object.prototype.toString.call(values[k]) === '[object Object]' && values[k]) {
// values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
// }
// const _tempFind = this.attributeList.find((item) => item.name === k)
// if (_tempFind.value_type === '6') {
// values[k] = values[k] ? JSON.parse(values[k]) : undefined
// }
// })
// if (_this.action === 'update') {
// _this.$emit('submit', values)
// return
// }
// values.ci_type = _this.typeId
// console.log(values)
// this.attributesByGroup.forEach((group) => {
// this.$refs[`createInstanceFormByGroup_${group.id}`][0].getData()
// })
// console.log(1111)
// // addCI(values).then((res) => {
// // _this.$message.success('新增成功!')
// // _this.visible = false
// // _this.$emit('reload')
// // })
// })
},
handleClose() {
this.visible = false
@@ -357,6 +396,9 @@ export default {
this.batchUpdateLists.splice(_idx, 1)
}
},
// filterOption(input, option) {
// return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
// },
handleFocusInput(e, attr) {
console.log(attr)
const _tempFind = this.attributeList.find((item) => item.name === attr.name)

View File

@@ -60,7 +60,7 @@
</span>
</template>
<template v-else-if="attr.is_list">
<span> {{ ci[attr.name].join(',') }}</span>
<span> {{ ci[attr.name] && Array.isArray(ci[attr.name]) ? ci[attr.name].join(',') : ci[attr.name] }}</span>
</template>
<template v-else>{{ getName(ci[attr.name]) }}</template>
</span>
@@ -105,23 +105,6 @@
</span>
</a-select-option>
</a-select>
<a-select
:style="{ width: '100%' }"
v-decorator="[
attr.name,
{
rules: [{ required: attr.is_required }],
},
]"
:placeholder="$t('placeholder2')"
v-else-if="attr.is_list"
mode="tags"
showSearch
allowClear
size="small"
:getPopupContainer="(trigger) => trigger.parentElement"
>
</a-select>
<a-input-number
size="small"
v-decorator="[
@@ -131,7 +114,7 @@
},
]"
style="width: 100%"
v-else-if="attr.value_type === '0' || attr.value_type === '1'"
v-else-if="(attr.value_type === '0' || attr.value_type === '1') && !attr.is_list"
/>
<a-date-picker
size="small"
@@ -144,22 +127,9 @@
style="width: 100%"
:format="attr.value_type === '4' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
:valueFormat="attr.value_type === '4' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
v-else-if="attr.value_type === '4' || attr.value_type === '3'"
v-else-if="(attr.value_type === '4' || attr.value_type === '3') && !attr.is_list"
:showTime="attr.value_type === '4' ? false : { format: 'HH:mm:ss' }"
/>
<!-- <a-input
size="small"
@focus="(e) => handleFocusInput(e, attr)"
v-decorator="[
attr.name,
{
validateTrigger: ['submit'],
rules: [{ required: attr.is_required }],
},
]"
style="width: 100%"
v-else-if="attr.value_type === '6'"
/> -->
<a-input
size="small"
v-decorator="[
@@ -241,7 +211,9 @@ export default {
this.$nextTick(async () => {
if (this.attr.is_list && !this.attr.is_choice) {
this.form.setFieldsValue({
[`${this.attr.name}`]: this.ci[this.attr.name] || null,
[`${this.attr.name}`]: Array.isArray(this.ci[this.attr.name])
? this.ci[this.attr.name].join(',')
: this.ci[this.attr.name],
})
return
}

View File

@@ -0,0 +1,50 @@
<template>
<CustomDrawer
width="80%"
placement="left"
@close="
() => {
visible = false
}
"
:visible="visible"
:hasTitle="false"
:hasFooter="false"
:bodyStyle="{ padding: 0, height: '100vh' }"
destroyOnClose
>
<CiDetailTab ref="ciDetailTab" :typeId="typeId" :treeViewsLevels="treeViewsLevels" />
</CustomDrawer>
</template>
<script>
import CiDetailTab from './ciDetailTab.vue'
export default {
name: 'CiDetailDrawer',
components: { CiDetailTab },
props: {
typeId: {
type: Number,
required: false,
default: null,
},
treeViewsLevels: {
type: Array,
default: () => [],
},
},
data() {
return {
visible: false,
}
},
methods: {
create(ciId, activeTabKey = 'tab_1', ciDetailRelationKey = '1') {
this.visible = true
this.$nextTick(() => {
this.$refs.ciDetailTab.create(ciId, activeTabKey, ciDetailRelationKey)
})
},
},
}
</script>

View File

@@ -39,7 +39,11 @@
class="ops-stripe-table"
>
<template #operation_default="{ row }">
<a-popconfirm arrowPointAtCenter :title="$t('cmdb.ci.confirmDeleteRelation')" @confirm="deleteRelation(row._id, ciId)">
<a-popconfirm
arrowPointAtCenter
:title="$t('cmdb.ci.confirmDeleteRelation')"
@confirm="deleteRelation(row._id, ciId)"
>
<a
:disabled="!canEdit[parent.id]"
:style="{
@@ -82,7 +86,11 @@
class="ops-stripe-table"
>
<template #operation_default="{ row }">
<a-popconfirm arrowPointAtCenter :title="$t('cmdb.ci.confirmDeleteRelation')" @confirm="deleteRelation(ciId, row._id)">
<a-popconfirm
arrowPointAtCenter
:title="$t('cmdb.ci.confirmDeleteRelation')"
@confirm="deleteRelation(ciId, row._id)"
>
<a
:disabled="!canEdit[child.id]"
:style="{
@@ -416,6 +424,7 @@ export default {
<style lang="less" scoped>
.ci-detail-relation {
height: 100%;
.ci-detail-relation-table-title {
font-size: 16px;
font-weight: 700;

View File

@@ -2,7 +2,7 @@
<div
id="ci-detail-relation-topo"
class="ci-detail-relation-topo"
:style="{ width: '100%', marginTop: '20px', height: 'calc(100vh - 136px)' }"
:style="{ width: '100%', marginTop: '20px', height: 'calc(100% - 44px)' }"
></div>
</template>

View File

@@ -1,23 +1,13 @@
<template>
<CustomDrawer
width="80%"
placement="left"
@close="
() => {
visible = false
}
"
:visible="visible"
:hasTitle="false"
:hasFooter="false"
:bodyStyle="{ padding: 0, height: '100vh' }"
wrapClassName="ci-detail"
destroyOnClose
>
<a-tabs v-model="activeTabKey" @change="changeTab">
<div :style="{ height: '100%' }">
<a-tabs v-if="hasPermission" class="ci-detail-tab" v-model="activeTabKey" @change="changeTab">
<a @click="shareCi" slot="tabBarExtraContent" :style="{ marginRight: '24px' }">
<a-icon type="share-alt" />
{{ $t('cmdb.ci.share') }}
</a>
<a-tab-pane key="tab_1">
<span slot="tab"><a-icon type="book" />{{ $t('cmdb.attribute') }}</span>
<div :style="{ maxHeight: `${windowHeight - 44}px`, overflow: 'auto', padding: '24px' }" class="ci-detail-attr">
<div class="ci-detail-attr">
<el-descriptions
:title="group.name || $t('other')"
:key="group.name"
@@ -37,18 +27,18 @@
</a-tab-pane>
<a-tab-pane key="tab_2">
<span slot="tab"><a-icon type="branches" />{{ $t('cmdb.relation') }}</span>
<div :style="{ padding: '24px' }">
<div :style="{ height: '100%', padding: '24px', overflow: 'auto' }">
<CiDetailRelation 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: 'calc(100vh - 44px)' }">
<div :style="{ padding: '24px', height: '100%' }">
<vxe-table
ref="xTable"
:data="ciHistory"
size="small"
:max-height="`${windowHeight - 94}px`"
height="auto"
:span-method="mergeRowMethod"
border
:scroll-y="{ enabled: false }"
@@ -88,12 +78,22 @@
</a-tab-pane>
<a-tab-pane key="tab_4">
<span slot="tab"><ops-icon type="itsm_auto_trigger" />{{ $t('cmdb.history.triggerHistory') }}</span>
<div :style="{ padding: '24px', height: 'calc(100vh - 44px)' }">
<div :style="{ padding: '24px', height: '100%' }">
<TriggerTable :ci_id="ci._id" />
</div>
</a-tab-pane>
</a-tabs>
</CustomDrawer>
<a-empty
v-else
:image-style="{
height: '100px',
}"
:style="{ paddingTop: '20%' }"
>
<img slot="image" :src="require('@/assets/data_empty.png')" />
<span slot="description"> {{ $t('cmdb.ci.noPermission') }} </span>
</a-empty>
</div>
</template>
<script>
@@ -105,8 +105,8 @@ 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'
export default {
name: 'CiDetailTab',
components: {
ElDescriptions: Descriptions,
ElDescriptionsItem: DescriptionsItem,
@@ -126,7 +126,6 @@ export default {
},
data() {
return {
visible: false,
ci: {},
attributeGroups: [],
activeTabKey: 'tab_1',
@@ -134,13 +133,13 @@ export default {
ciHistory: [],
ciId: null,
ci_types: [],
hasPermission: true,
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
operateTypeMap() {
return {
0: this.$t('new'),
@@ -156,10 +155,22 @@ export default {
},
}
},
inject: ['reload', 'handleSearch', 'attrList'],
inject: {
reload: {
from: 'reload',
default: null,
},
handleSearch: {
from: 'handleSearch',
default: null,
},
attrList: {
from: 'attrList',
default: () => [],
},
},
methods: {
create(ciId, activeTabKey = 'tab_1', ciDetailRelationKey = '1') {
this.visible = true
async create(ciId, activeTabKey = 'tab_1', ciDetailRelationKey = '1') {
this.activeTabKey = activeTabKey
if (activeTabKey === 'tab_2') {
this.$nextTick(() => {
@@ -167,12 +178,14 @@ export default {
})
}
this.ciId = ciId
await this.getCI()
if (this.hasPermission) {
this.getAttributes()
this.getCI()
this.getCIHistory()
getCITypes().then((res) => {
this.ci_types = res.ci_types
})
}
},
getAttributes() {
getCITypeGroupById(this.typeId, { need_other: 1 })
@@ -181,11 +194,14 @@ export default {
})
.catch((e) => {})
},
getCI() {
getCIById(this.ciId)
async getCI() {
await getCIById(this.ciId)
.then((res) => {
// this.ci = res.ci
if (res.result.length) {
this.ci = res.result[0]
} else {
this.hasPermission = false
}
})
.catch((e) => {})
},
@@ -270,10 +286,14 @@ export default {
// 修改的字段为树形视图订阅的字段 则全部reload
setTimeout(() => {
if (_find) {
if (this.reload) {
this.reload()
}
} else {
if (this.handleSearch) {
this.handleSearch()
}
}
}, 500)
},
mergeRowMethod({ row, _rowIndex, column, visibleData }) {
@@ -303,23 +323,49 @@ export default {
// 修改的字段为树形视图订阅的字段 则全部reload
setTimeout(() => {
if (_find) {
if (this.reload) {
this.reload()
}
} else {
if (this.handleSearch) {
this.handleSearch()
}
}
}, 500)
},
shareCi() {
const text = `${document.location.host}/cmdb/cidetail/${this.typeId}/${this.ciId}`
this.$copyText(text)
.then(() => {
this.$message.success(this.$t('copySuccess'))
})
.catch(() => {
this.$message.error(this.$t('cmdb.ci.copyFailed'))
})
},
},
}
</script>
<style lang="less" scoped></style>
<style lang="less">
.ci-detail {
.ci-detail-tab {
height: 100%;
.ant-tabs-content {
height: calc(100% - 45px);
.ant-tabs-tabpane {
height: 100%;
}
}
.ant-tabs-bar {
margin: 0;
}
.ant-tabs-extra-content {
line-height: 44px;
}
.ci-detail-attr {
height: 100%;
overflow: auto;
padding: 24px;
.el-descriptions-item__content {
cursor: default;
&:hover a {

View File

@@ -22,7 +22,14 @@
attr.name,
{
rules: [{ required: attr.is_required, message: $t('placeholder2') + `${attr.alias || attr.name}` }],
initialValue: attr.default && attr.default.default ? attr.default.default : attr.is_list ? [] : null,
initialValue:
attr.default && attr.default.default
? attr.is_list
? Array.isArray(attr.default.default)
? attr.default.default
: attr.default.default.split(',')
: attr.default.default
: null,
},
]"
:placeholder="$t('placeholder2')"
@@ -53,19 +60,18 @@
</span>
</a-select-option>
</a-select>
<a-select
<a-input
v-else-if="attr.is_list"
mode="tags"
:style="{ width: '100%' }"
v-decorator="[
attr.name,
{
rules: [{ required: attr.is_required, message: $t('placeholder2') + `${attr.alias || attr.name}` }],
initialValue: attr.default && attr.default.default ? attr.default.default : attr.is_list ? [] : null,
initialValue: attr.default && attr.default.default ? attr.default.default : '',
},
]"
>
</a-select>
</a-input>
<a-input-number
v-decorator="[
attr.name,

View File

@@ -1,103 +0,0 @@
<template>
<CustomDrawer
:visible="visible"
width="600"
@close="
() => {
visible = false
}
"
:title="$t('cmdb.ci.attributeSettings')"
>
<CustomTransfer
ref="customTransfer"
:dataSource="attrList"
:showSearch="true"
:listStyle="{
width: '230px',
height: '500px',
}"
:titles="[$t('cmdb.components.unselectAttributes'), $t('cmdb.components.selectAttributes')]"
:render="item => item.title"
:targetKeys="selectedAttrList"
@change="handleChange"
@selectChange="selectChange"
>
<span slot="notFoundContent">{{ $t('noData') }}</span>
</CustomTransfer>
<div class="custom-drawer-bottom-action">
<a-button @click="handleSubmit" type="primary">{{ $t('confirm') }}</a-button>
</div>
</CustomDrawer>
</template>
<script>
import { subscribeCIType, getSubscribeAttributes } from '@/modules/cmdb/api/preference'
import { getCITypeAttributesByName } from '@/modules/cmdb/api/CITypeAttr'
export default {
name: 'EditAttrsDrawer',
data() {
return {
attrList: [],
typeId: null,
visible: false,
selectedAttrList: [],
}
},
methods: {
open(typeId) {
this.typeId = typeId
this.getAttrs()
},
getAttrs() {
getCITypeAttributesByName(this.typeId).then(res => {
const attributes = res.attributes
getSubscribeAttributes(this.typeId).then(_res => {
const attrList = []
const selectedAttrList = []
const subAttributes = _res.attributes
this.instanceSubscribed = _res.is_subscribed
subAttributes.forEach(item => {
selectedAttrList.push(item.id.toString())
})
attributes.forEach(item => {
const data = {
key: item.id.toString(),
title: item.alias || item.name,
}
attrList.push(data)
})
this.attrList = attrList
this.selectedAttrList = selectedAttrList
this.visible = true
})
})
},
handleChange(targetKeys, direction, moveKeys) {
this.selectedAttrList = targetKeys
},
handleSubmit() {
const that = this
if (this.selectedAttrList.length) {
subscribeCIType(this.typeId, this.selectedAttrList).then(res => {
this.$message.success(this.$t('cmdb.components.subSuccess'))
this.visible = false
this.$emit('refresh')
})
} else {
this.$confirm({
title: that.$t('warning'),
content: that.$t('cmdb.ci.tips4'),
})
}
},
selectChange(sourceSelectedKeys, targetSelectedKeys) {
this.$refs.customTransfer.dbClick(sourceSelectedKeys, targetSelectedKeys, 'title', 'key')
},
},
}
</script>
<style></style>

View File

@@ -1 +0,0 @@
editAttrsDrawer 这个文件似乎也没用了

View File

@@ -1,7 +1,11 @@
<template>
<div class="attribute-card">
<div :class="{ 'attribute-card': true, 'attribute-card-inherited': inherited }">
<a-tooltip :title="inherited ? $t('cmdb.ciType.inheritFrom', { name: property.inherited_from }) : ''">
<div class="attribute-card-content">
<div class="attribute-card-value-type-icon handle" :style="{ ...getPropertyStyle(property) }">
<div
:class="{ 'attribute-card-value-type-icon': true, handle: !inherited }"
:style="{ ...getPropertyStyle(property) }"
>
<ValueTypeIcon :attr="property" />
</div>
<div :class="{ 'attribute-card-content-inner': true, 'attribute-card-name-required': property.is_required }">
@@ -19,6 +23,8 @@
<a @click="openTrigger"><ops-icon type="ops-trigger"/></a>
</div>
</div>
</a-tooltip>
<div class="attribute-card-footer">
<a-popover
trigger="click"
@@ -51,7 +57,7 @@
</a-space>
</a-popover>
<a-space class="attribute-card-operation">
<a-space class="attribute-card-operation" v-if="!inherited">
<a v-if="!isStore"><a-icon type="edit" @click="handleEdit"/></a>
<a-tooltip :title="$t('cmdb.ciType.computeForAllCITips')">
<a v-if="!isStore && property.is_computed"><a-icon type="redo" @click="handleCalcComputed"/></a>
@@ -140,6 +146,9 @@ export default {
},
]
},
inherited() {
return this.property.inherited || false
},
},
methods: {
getPropertyStyle,
@@ -211,13 +220,15 @@ export default {
width: 32px;
height: 32px;
font-size: 12px;
cursor: move;
background: #ffffff !important;
box-shadow: 0px 1px 2px rgba(47, 84, 235, 0.2);
border-radius: 2px;
text-align: center;
line-height: 32px;
}
.handle {
cursor: move;
}
.attribute-card-content-inner {
padding-left: 12px;
font-weight: 400;
@@ -269,6 +280,12 @@ export default {
}
}
}
.attribute-card-inherited {
background: #f3f4f7;
.attribute-card-footer {
background: #eaedf3;
}
}
</style>
<style lang="less">
.attribute-card-footer-popover {

View File

@@ -13,7 +13,11 @@
<a-form :form="form" :layout="formLayout">
<a-divider style="font-size:14px;margin-top:6px;">{{ $t('cmdb.ciType.basicConfig') }}</a-divider>
<a-col :span="12">
<a-form-item :label-col="formItemLayout.labelCol" :wrapper-col="formItemLayout.wrapperCol" :label="$t('cmdb.ciType.AttributeName')">
<a-form-item
:label-col="formItemLayout.labelCol"
:wrapper-col="formItemLayout.wrapperCol"
:label="$t('cmdb.ciType.AttributeName')"
>
<a-input
:disabled="true"
name="name"
@@ -35,12 +39,20 @@
</a-col>
<a-col
:span="12"
><a-form-item :label-col="formItemLayout.labelCol" :wrapper-col="formItemLayout.wrapperCol" :label="$t('alias')">
><a-form-item
:label-col="formItemLayout.labelCol"
:wrapper-col="formItemLayout.wrapperCol"
:label="$t('alias')"
>
<a-input name="alias" v-decorator="['alias', { rules: [] }]" /> </a-form-item
></a-col>
<a-col
:span="12"
><a-form-item :label-col="formItemLayout.labelCol" :wrapper-col="formItemLayout.wrapperCol" :label="$t('cmdb.ciType.DataType')">
><a-form-item
:label-col="formItemLayout.labelCol"
:wrapper-col="formItemLayout.wrapperCol"
:label="$t('cmdb.ciType.DataType')"
>
<a-select
:disabled="true"
name="value_type"
@@ -59,13 +71,12 @@
:label="$t('cmdb.ciType.defaultValue')"
>
<template>
<a-select
<a-input
v-if="form.getFieldValue('is_list')"
mode="tags"
:style="{ width: '100%' }"
v-decorator="['default_value', { rules: [{ required: false }] }]"
>
</a-select>
</a-input>
<a-select
v-decorator="['default_value', { rules: [{ required: false }] }]"
mode="tags"
@@ -160,7 +171,11 @@
</a-form-item>
</a-col>
<a-col :span="6" v-if="currentValueType !== '6' && currentValueType !== '7'">
<a-form-item :label-col="{ span: 8 }" :wrapper-col="horizontalFormItemLayout.wrapperCol" :label="$t('cmdb.ciType.unique')">
<a-form-item
:label-col="{ span: 8 }"
:wrapper-col="horizontalFormItemLayout.wrapperCol"
:label="$t('cmdb.ciType.unique')"
>
<a-switch
:disabled="isShowComputedArea"
@change="onChange"
@@ -282,6 +297,11 @@
</a-col>
<a-divider style="font-size:14px;margin-top:6px;">{{ $t('cmdb.ciType.advancedSettings') }}</a-divider>
<a-row>
<a-col :span="24" v-if="!['6'].includes(currentValueType)">
<a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 12 }" :label="$t('cmdb.ciType.reg')">
<RegSelect :isShowErrorMsg="false" v-model="re_check" :limitedFormat="getLimitedFormat()" />
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }" :label="$t('cmdb.ciType.font')">
<FontArea ref="fontArea" />
@@ -303,11 +323,7 @@
<span
style="position:relative;white-space:pre;"
>{{ $t('cmdb.ciType.computedAttribute') }}
<a-tooltip
:title="
$t('cmdb.ciType.computedAttributeTips')
"
>
<a-tooltip :title="$t('cmdb.ciType.computedAttributeTips')">
<a-icon
style="position:absolute;top:3px;left:-17px;color:#2f54eb;"
type="question-circle"
@@ -355,6 +371,8 @@ import _ from 'lodash'
import moment from 'moment'
import vueJsonEditor from 'vue-json-editor'
import {
// createAttribute,
// createCITypeAttributes,
updateAttributeById,
updateCITypeAttributesById,
canDefineComputed,
@@ -364,10 +382,11 @@ import { valueTypeMap } from '../../utils/const'
import ComputedArea from './computedArea.vue'
import PreValueArea from './preValueArea.vue'
import FontArea from './fontArea.vue'
import RegSelect from '@/components/RegexSelect'
export default {
name: 'AttributeEditForm',
components: { ComputedArea, PreValueArea, vueJsonEditor, FontArea },
components: { ComputedArea, PreValueArea, vueJsonEditor, FontArea, RegSelect },
props: {
CITypeId: {
type: Number,
@@ -395,6 +414,7 @@ export default {
isShowComputedArea: false,
defaultForDatetime: '',
re_check: {},
}
},
@@ -517,15 +537,30 @@ export default {
})
}
console.log(_record)
if (!['6'].includes(_record.value_type) && _record.re_check) {
this.re_check = {
value: _record.re_check,
}
} else {
this.re_check = {}
}
if (_record.default) {
this.$nextTick(() => {
if (_record.value_type === '0') {
if (_record.is_list) {
this.$nextTick(() => {
this.form.setFieldsValue({
default_value: _record.default.default ? _record.default.default : '',
})
})
} else {
this.form.setFieldsValue({
default_value: _record.default.default ? [_record.default.default] : [],
})
}
} else if (_record.value_type === '6') {
this.default_value_json = _record?.default?.default || null
} else if (_record.value_type === '3' || _record.value_type === '4') {
} else if ((_record.value_type === '3' || _record.value_type === '4') && !_record.is_list) {
if (_record?.default?.default === '$created_at' || _record?.default?.default === '$updated_at') {
this.defaultForDatetime = _record.default.default
this.form.setFieldsValue({
@@ -584,6 +619,9 @@ export default {
await this.form.validateFields(async (err, values) => {
if (!err) {
console.log('Received values of form: ', values)
// if (values.choice_value) {
// values.choice_value = values.choice_value.split('\n')
// }
if (this.record.is_required !== values.is_required || this.record.default_show !== values.default_show) {
console.log('changed is_required')
@@ -598,7 +636,11 @@ export default {
delete values['is_required']
const { default_value } = values
if (values.value_type === '0' && default_value) {
if (values.is_list) {
values.default = { default: default_value || null }
} else {
values.default = { default: default_value[0] || null }
}
} else if (values.value_type === '6') {
if (this.default_value_json_right) {
values.default = { default: this.default_value_json }
@@ -606,13 +648,13 @@ export default {
values.default = { default: null }
}
} else if (default_value || default_value === 0) {
if (values.value_type === '3') {
if (values.value_type === '3' && !values.is_list) {
if (default_value === '$created_at' || default_value === '$updated_at') {
values.default = { default: default_value }
} else {
values.default = { default: moment(default_value).format('YYYY-MM-DD HH:mm:ss') }
}
} else if (values.value_type === '4') {
} else if (values.value_type === '4' && !values.is_list) {
values.default = { default: moment(default_value).format('YYYY-MM-DD') }
} else {
values.default = { default: default_value }
@@ -641,6 +683,9 @@ export default {
values.value_type = '2'
values.is_link = true
}
if (values.value_type !== '6') {
values.re_check = this.re_check?.value ?? null
}
if (values.id) {
await this.updateAttribute(values.id, { ...values, option: { fontOptions } }, isCalcComputed)
} else {
@@ -698,6 +743,21 @@ export default {
async handleCalcComputed() {
await this.handleSubmit(true)
},
getLimitedFormat() {
if (['0'].includes(this.currentValueType)) {
return ['number', 'phone', 'landline', 'zipCode', 'IDCard', 'monetaryAmount', 'custom']
}
if (['1'].includes(this.currentValueType)) {
return ['number', 'monetaryAmount', 'custom']
}
if (['3', '4', '5'].includes(this.currentValueType)) {
return ['custom']
}
if (this.currentValueType === '8') {
return ['link', 'custom']
}
return []
},
},
watch: {},
}

View File

@@ -1,6 +1,11 @@
<template>
<div>
<a-modal v-model="addGroupModal" :title="$t('cmdb.ciType.addGroup')" @cancel="handleCancelCreateGroup" @ok="handleCreateGroup">
<a-modal
v-model="addGroupModal"
:title="$t('cmdb.ciType.addGroup')"
@cancel="handleCancelCreateGroup"
@ok="handleCreateGroup"
>
<span>
<a-form-item :label="$t('name')" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
<a-input type="text" v-model.trim="newGroupName" />
@@ -9,21 +14,12 @@
</a-modal>
<div class="ci-types-attributes" :style="{ maxHeight: `${windowHeight - 104}px` }">
<a-space style="margin-bottom: 10px">
<a-button
type="primary"
@click="handleAddGroup"
size="small"
class="ops-button-primary"
icon="plus"
>{{ $t('cmdb.ciType.group') }}</a-button
>
<a-button
type="primary"
@click="handleOpenUniqueConstraint"
size="small"
class="ops-button-primary"
>{{ $t('cmdb.ciType.uniqueConstraint') }}</a-button
>
<a-button type="primary" @click="handleAddGroup" size="small" class="ops-button-primary" icon="plus">{{
$t('cmdb.ciType.group')
}}</a-button>
<a-button type="primary" @click="handleOpenUniqueConstraint" size="small" class="ops-button-primary">{{
$t('cmdb.ciType.uniqueConstraint')
}}</a-button>
</a-space>
<div :key="CITypeGroup.id" v-for="(CITypeGroup, index) in CITypeGroups">
<div>
@@ -33,7 +29,11 @@
>
<span style="font-weight:700">{{ CITypeGroup.name }}</span>
<span style="color: #c3cdd7;margin:0 5px;">({{ CITypeGroup.attributes.length }})</span>
<a @click="handleEditGroupName(index, CITypeGroup)">
<a v-if="!CITypeGroup.inherited" @click="handleEditGroupName(index, CITypeGroup)">
<a-icon type="edit" />
</a>
<a v-else :style="{ cursor: 'not-allowed', color: 'gray' }">
<a-icon type="edit" />
</a>
</div>
@@ -71,7 +71,13 @@
</a-tooltip>
<a-tooltip>
<template slot="title">{{ $t('cmdb.ciType.deleteGroup') }}</template>
<a style="color:red;"><a-icon type="delete" @click="handleDeleteGroup(CITypeGroup)"/></a>
<a
:style="{ color: CITypeGroup.inherited ? 'gray' : 'red' }"
:disabled="CITypeGroup.inherited"
><a-icon
type="delete"
@click="handleDeleteGroup(CITypeGroup)"
/></a>
</a-tooltip>
</a-space>
</div>
@@ -82,7 +88,7 @@
@start="drag = true"
@change="
(e) => {
handleChange(e, CITypeGroup.id)
handleChange(e, CITypeGroup.name)
}
"
:filter="'.filter-empty'"
@@ -124,7 +130,7 @@
@start="drag = true"
@change="
(e) => {
handleChange(e, -1)
handleChange(e, null)
}
"
:animation="300"
@@ -144,18 +150,18 @@
</draggable>
</div>
</div>
<attribute-edit-form
<AttributeEditForm
ref="attributeEditForm"
:CITypeId="CITypeId"
:CITypeName="CITypeName"
@ok="handleOk"
></attribute-edit-form>
<new-ci-type-attr-modal
></AttributeEditForm>
<NewCiTypeAttrModal
ref="newCiTypeAttrModal"
:CITypeId="CITypeId"
:linked-ids="linkedIds"
@ok="handleOk"
></new-ci-type-attr-modal>
></NewCiTypeAttrModal>
<UniqueConstraint ref="uniqueConstraint" :CITypeId="CITypeId" />
</div>
</template>
@@ -347,8 +353,8 @@ export default {
},
handleMoveGroup(beforeIndex, afterIndex) {
const fromGroupId = this.CITypeGroups[beforeIndex].id
const toGroupId = this.CITypeGroups[afterIndex].id
const fromGroupId = this.CITypeGroups[beforeIndex].name
const toGroupId = this.CITypeGroups[afterIndex].name
transferCITypeGroupIndex(this.CITypeId, { from: fromGroupId, to: toGroupId }).then((res) => {
this.$message.success(this.$t('operateSuccess'))
const beforeGroup = this.CITypeGroups[beforeIndex]
@@ -414,14 +420,14 @@ export default {
})
},
handleChange(e, group) {
console.log('changess')
console.log('changess', group)
if (e.hasOwnProperty('moved') && e.moved.oldIndex !== e.moved.newIndex) {
if (group === -1) {
this.$message.error(this.$t('cmdb.ciType.attributeSortedTips'))
} else {
transferCITypeAttrIndex(this.CITypeId, {
from: { attr_id: e.moved.element.id, group_id: group > -1 ? group : null },
to: { order: e.moved.newIndex, group_id: group > -1 ? group : null },
from: { attr_id: e.moved.element.id, group_name: group },
to: { order: e.moved.newIndex, group_name: group },
})
.then((res) => this.$message.success(this.$t('updateSuccess')))
.catch(() => {
@@ -431,14 +437,14 @@ export default {
}
if (e.hasOwnProperty('added')) {
this.addRemoveGroupFlag = { to: { group_id: group > -1 ? group : null, order: e.added.newIndex }, inited: true }
this.addRemoveGroupFlag = { to: { group_name: group, order: e.added.newIndex }, inited: true }
}
if (e.hasOwnProperty('removed')) {
this.$nextTick(() => {
transferCITypeAttrIndex(this.CITypeId, {
from: { attr_id: e.removed.element.id, group_id: group > -1 ? group : null },
to: { group_id: this.addRemoveGroupFlag.to.group_id, order: this.addRemoveGroupFlag.to.order },
from: { attr_id: e.removed.element.id, group_name: group },
to: { group_name: this.addRemoveGroupFlag.to.group_name, order: this.addRemoveGroupFlag.to.order },
})
.then((res) => this.$message.success(this.$t('saveSuccess')))
.catch(() => {

View File

@@ -21,7 +21,10 @@
message: $t('cmdb.ciType.attributeNameTips'),
pattern: RegExp('^(?!\\d)[a-zA-Z_0-9]+$'),
},
{ message: $t('cmdb.ciType.buildinAttribute'), pattern: RegExp('^(?!(id|_id|ci_id|type|_type|ci_type)$).*$') },
{
message: $t('cmdb.ciType.buildinAttribute'),
pattern: RegExp('^(?!(id|_id|ci_id|type|_type|ci_type)$).*$'),
},
],
},
]"
@@ -59,13 +62,12 @@
:label="$t('cmdb.ciType.defaultValue')"
>
<template>
<a-select
<a-input
v-if="form.getFieldValue('is_list')"
mode="tags"
:style="{ width: '100%' }"
v-decorator="['default_value', { rules: [{ required: false }] }]"
>
</a-select>
</a-input>
<a-input-number
style="width: 100%"
v-else-if="currentValueType === '1'"
@@ -162,7 +164,11 @@
</a-form-item>
</a-col>
<a-col :span="6" v-if="currentValueType !== '6' && currentValueType !== '7'">
<a-form-item :label-col="{ span: 8 }" :wrapper-col="horizontalFormItemLayout.wrapperCol" :label="$t('cmdb.ciType.unique')">
<a-form-item
:label-col="{ span: 8 }"
:wrapper-col="horizontalFormItemLayout.wrapperCol"
:label="$t('cmdb.ciType.unique')"
>
<a-switch
:disabled="isShowComputedArea"
@change="onChange"
@@ -280,6 +286,11 @@
</a-col>
<a-divider style="font-size:14px;margin-top:6px;">{{ $t('cmdb.ciType.advancedSettings') }}</a-divider>
<a-row>
<a-col :span="24" v-if="!['6'].includes(currentValueType)">
<a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 12 }" :label="$t('cmdb.ciType.reg')">
<RegSelect :isShowErrorMsg="false" v-model="re_check" :limitedFormat="getLimitedFormat()" />
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }" :label="$t('cmdb.ciType.font')">
<FontArea ref="fontArea" />
@@ -296,11 +307,7 @@
<span
style="position:relative;white-space:pre;"
>{{ $t('cmdb.ciType.computedAttribute') }}
<a-tooltip
:title="
$t('cmdb.ciType.computedAttributeTips')
"
>
<a-tooltip :title="$t('cmdb.ciType.computedAttributeTips')">
<a-icon
style="position:absolute;top:3px;left:-17px;color:#2f54eb;"
type="question-circle"
@@ -340,6 +347,7 @@ import { valueTypeMap } from '../../utils/const'
import ComputedArea from './computedArea.vue'
import PreValueArea from './preValueArea.vue'
import FontArea from './fontArea.vue'
import RegSelect from '@/components/RegexSelect'
export default {
name: 'CreateNewAttribute',
@@ -348,6 +356,7 @@ export default {
PreValueArea,
vueJsonEditor,
FontArea,
RegSelect,
},
props: {
hasFooter: {
@@ -374,6 +383,8 @@ export default {
isShowComputedArea: false,
defaultForDatetime: '',
re_check: {},
}
},
computed: {
@@ -397,8 +408,12 @@ export default {
const data = { is_required, default_show }
delete values.is_required
delete values.default_show
if (values.value_type === '0' && default_value && default_value.length) {
values.default = { default: default_value[0] }
if (values.value_type === '0' && default_value) {
if (values.is_list) {
values.default = { default: default_value || null }
} else {
values.default = { default: default_value[0] || null }
}
} else if (values.value_type === '6') {
if (this.default_value_json_right) {
values.default = { default: this.default_value_json }
@@ -406,13 +421,13 @@ export default {
values.default = { default: null }
}
} else if (default_value || default_value === 0) {
if (values.value_type === '3') {
if (values.value_type === '3' && !values.is_list) {
if (default_value === '$created_at' || default_value === '$updated_at') {
values.default = { default: default_value }
} else {
values.default = { default: moment(default_value).format('YYYY-MM-DD HH:mm:ss') }
}
} else if (values.value_type === '4') {
} else if (values.value_type === '4' && !values.is_list) {
values.default = { default: moment(default_value).format('YYYY-MM-DD') }
} else {
values.default = { default: default_value }
@@ -449,6 +464,9 @@ export default {
values.value_type = '2'
values.is_link = true
}
if (values.value_type !== '6') {
values.re_check = this.re_check?.value ?? null
}
const { attr_id } = await createAttribute({ ...values, option: { fontOptions } })
this.form.resetFields()
@@ -539,6 +557,21 @@ export default {
default_value: key,
})
},
getLimitedFormat() {
if (['0'].includes(this.currentValueType)) {
return ['number', 'phone', 'landline', 'zipCode', 'IDCard', 'monetaryAmount', 'custom']
}
if (['1'].includes(this.currentValueType)) {
return ['number', 'monetaryAmount', 'custom']
}
if (['3', '4', '5'].includes(this.currentValueType)) {
return ['custom']
}
if (this.currentValueType === '8') {
return ['link', 'custom']
}
return []
},
},
}
</script>

View File

@@ -53,7 +53,10 @@
</a-menu-item>
<a-menu-item key="1">
<a-space>
<a href="/api/v0.1/ci_types/template/export/file"><a-icon type="download" /> {{ $t('download') }}</a>
<a
href="/api/v0.1/ci_types/template/export/file"
><a-icon type="download" /> {{ $t('download') }}</a
>
</a-space>
</a-menu-item>
</a-menu>
@@ -63,9 +66,11 @@
<draggable class="ci-types-left-content" :list="CITypeGroups" @end="handleChangeGroups" filter=".undraggable">
<div v-for="g in CITypeGroups" :key="g.id || g.name">
<div
:class="`${currentGId === g.id && !currentCId ? 'selected' : ''} ci-types-left-group ${
:class="
`${currentGId === g.id && !currentCId ? 'selected' : ''} ci-types-left-group ${
g.id === -1 ? 'undraggable' : ''
}`"
}`
"
@click="handleClickGroup(g.id)"
>
<div>
@@ -134,7 +139,9 @@
<a><a-icon type="edit" @click="(e) => handleEdit(e, ci)"/></a>
<a
v-if="permissions.includes('admin') || permissions.includes('cmdb_admin')"
@click="(e) => handleDownloadCiType(e, ci)">
:disabled="ci.inherited"
@click="(e) => handleDownloadCiType(e, ci)"
>
<a-icon type="download" />
</a>
<a style="color: red" @click="(e) => handleDelete(e, ci)"><a-icon type="delete"/></a>
@@ -170,6 +177,7 @@
placement="right"
width="900px"
:destroyOnClose="true"
:bodyStyle="{ height: 'calc(100vh - 108px)' }"
>
<a-form
:form="form"
@@ -198,6 +206,35 @@
<a-form-item :label="$t('alias')">
<a-input name="alias" v-decorator="['alias', { rules: [] }]" />
</a-form-item>
<a-form-item :label="$t('cmdb.ciType.isInherit')">
<a-radio-group v-model="isInherit">
<a-radio :value="true">
</a-radio>
<a-radio :value="false">
</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item v-if="isInherit" :label="$t('cmdb.ciType.inheritType')">
<CMDBTypeSelect
multiple
:placeholder="$t('cmdb.ciType.inheritTypePlaceholder')"
v-decorator="[
'parent_ids',
{ rules: [{ required: true, message: $t('cmdb.ciType.inheritTypePlaceholder') }] },
]"
selectType="ci_type"
:class="{
'custom-treeselect': true,
}"
:style="{
'--custom-height': '32px',
lineHeight: '32px',
'--custom-multiple-lineHeight': '14px',
}"
/>
</a-form-item>
<a-form-item :label="$t('icon')">
<IconArea class="ci_types-icon-area" ref="iconArea" />
</a-form-item>
@@ -233,7 +270,17 @@
</div>
</el-select>
</a-form-item>
<a-form-item :label="$t('cmdb.ciType.uniqueKey')">
<a-form-item>
<template slot="label">
<a-tooltip :title="$t('cmdb.ciType.uniqueKeyTips')">
<a-icon
style="position:absolute;top:3px;left:-17px;color:#2f54eb;"
type="question-circle"
theme="filled"
/>
</a-tooltip>
<span>{{ $t('cmdb.ciType.uniqueKey') }}</span>
</template>
<el-select
size="small"
filterable
@@ -242,6 +289,11 @@
:filter-method="filterOption"
v-decorator="['unique_key', { rules: [{ required: true, message: $t('cmdb.ciType.uniqueKeySelect') }] }]"
:placeholder="$t('placeholder2')"
@visible-change="
() => {
filterInput = ''
}
"
>
<el-option
:key="item.id"
@@ -285,7 +337,14 @@ import router, { resetRouter } from '@/router'
import store from '@/store'
import draggable from 'vuedraggable'
import { Select, Option } from 'element-ui'
import { createCIType, updateCIType, deleteCIType } from '@/modules/cmdb/api/CIType'
import {
createCIType,
updateCIType,
deleteCIType,
getCIType,
postCiTypeInheritance,
deleteCiTypeInheritance,
} from '@/modules/cmdb/api/CIType'
import {
getCITypeGroupsConfig,
postCITypeGroup,
@@ -305,6 +364,7 @@ import CMDBGrant from '../../components/cmdbGrant'
import { ops_move_icon as OpsMoveIcon } from '@/core/icons'
import AttributeStore from './attributeStore.vue'
import { getAllDepAndEmployee } from '@/api/company'
import CMDBTypeSelect from '../../components/cmdbTypeSelect'
export default {
name: 'CITypes',
@@ -319,6 +379,7 @@ export default {
SplitPane,
OpsMoveIcon,
AttributeStore,
CMDBTypeSelect,
},
inject: ['reload'],
data() {
@@ -357,6 +418,9 @@ export default {
default_order_asc: '1',
allTreeDepAndEmp: [],
editCiType: null,
isInherit: false,
}
},
computed: {
@@ -549,8 +613,10 @@ export default {
})
},
onClose() {
this.filterInput = ''
this.form.resetFields()
this.drawerVisible = false
this.isInherit = false
},
handleCreateNewAttrDone() {
this.getAttributes()
@@ -571,6 +637,22 @@ export default {
const icon =
_icon && _icon.name ? `${_icon.name}$$${_icon.color || ''}$$${_icon.id || ''}$$${_icon.url || ''}` : ''
if (values.id) {
const { parent_ids: oldP = [] } = this.editCiType
const { parent_ids: newP = [] } = values
const { remove, add } = this.compareArrays(newP, oldP)
if (add && add.length) {
await postCiTypeInheritance({ parent_ids: add, child_id: values.id }).catch(() => {
this.loading = false
})
}
if (remove && remove.length) {
for (let i = 0; i < remove.length; i++) {
await deleteCiTypeInheritance({ parent_id: remove[i], child_id: values.id }).catch(() => {
this.loading = false
})
}
}
delete values.parent_ids
await this.updateCIType(values.id, {
...values,
icon,
@@ -581,6 +663,23 @@ export default {
}
})
},
compareArrays(newArr, oldArr) {
const remove = []
const add = []
for (let i = 0; i < oldArr.length; i++) {
const item = oldArr[i]
if (newArr.indexOf(item) === -1) {
remove.push(item)
}
}
for (let i = 0; i < newArr.length; i++) {
const item = newArr[i]
if (oldArr.indexOf(item) === -1) {
add.push(item)
}
}
return { remove, add }
},
start(g) {
console.log('start', g)
this.startId = g.id
@@ -755,13 +854,26 @@ export default {
router.addRoutes(store.getters.appRoutes)
})
},
handleEdit(e, record) {
async handleEdit(e, record) {
e.preventDefault()
e.stopPropagation()
this.drawerTitle = this.$t('cmdb.ciType.editCIType')
this.drawerVisible = true
getCITypeAttributesById(record.id).then((res) => {
await getCITypeAttributesById(record.id).then((res) => {
this.orderSelectionOptions = res.attributes.filter((item) => item.is_required)
})
await getCIType(record.id).then((res) => {
const ci_type = res.ci_types[0]
this.editCiType = ci_type ?? null
if (ci_type.parent_ids && ci_type.parent_ids.length) {
this.isInherit = true
this.$nextTick(() => {
this.form.setFieldsValue({
parent_ids: ci_type.parent_ids,
})
})
}
})
this.$nextTick(() => {
this.default_order_asc = record.default_order_attr && record.default_order_attr.startsWith('-') ? '2' : '1'
@@ -775,6 +887,7 @@ export default {
? record.default_order_attr.slice(1)
: record.default_order_attr,
})
this.$refs.iconArea.setIcon(
record.icon
? {
@@ -786,7 +899,6 @@ export default {
: {}
)
})
})
},
handleCreatNewAttr() {
this.newAttrAreaVisible = !this.newAttrAreaVisible

View File

@@ -20,7 +20,9 @@
"
>{{ $t('cancel') }}</a-button
>
<a-button :loading="confirmLoading" @click="handleSubmit(false)" type="primary">{{ $t('cmdb.ciType.continueAdd') }}</a-button>
<a-button :loading="confirmLoading" @click="handleSubmit(false)" type="primary">{{
$t('cmdb.ciType.continueAdd')
}}</a-button>
<a-button :loading="confirmLoading" type="primary" @click="handleSubmit">{{ $t('confirm') }}</a-button>
</template>
<a-tabs v-model="activeKey">
@@ -47,7 +49,7 @@
<script>
import _ from 'lodash'
import { searchAttributes, createCITypeAttributes, updateCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
import { updateCITypeGroupById, getCITypeGroupById } from '@/modules/cmdb/api/CIType'
import { createCITypeGroupById, getCITypeGroupById } from '@/modules/cmdb/api/CIType'
import CreateNewAttribute from './ceateNewAttribute.vue'
import { valueTypeMap } from '../../utils/const'
import AttributesTransfer from '../../components/attributesTransfer'
@@ -102,11 +104,11 @@ export default {
if (this.currentGroup) {
await this.updateCurrentGroup()
const { id, name, order, attributes } = this.currentGroup
const attrIds = attributes.map((i) => i.id)
const attrIds = attributes.filter((i) => !i.inherited).map((i) => i.id)
this.targetKeys.forEach((key) => {
attrIds.push(Number(key))
})
await updateCITypeGroupById(id, { name, order, attributes: [...new Set(attrIds)] })
await createCITypeGroupById(this.CITypeId, { name, order, attributes: [...new Set(attrIds)] })
}
this.confirmLoading = false
this.handleClose(isCloseModal)
@@ -140,9 +142,9 @@ export default {
if (this.currentGroup) {
await this.updateCurrentGroup()
const { id, name, order, attributes } = this.currentGroup
const attrIds = attributes.map((i) => i.id)
const attrIds = attributes.filter((i) => !i.inherited).map((i) => i.id)
attrIds.push(newAttrId)
await updateCITypeGroupById(id, { name, order, attributes: attrIds })
await createCITypeGroupById(this.CITypeId, { name, order, attributes: attrIds })
}
this.confirmLoading = false
this.loadTotalAttrs()

View File

@@ -79,7 +79,7 @@
<vxe-column
align="center"
field="is_accept"
:title="$t('cmdb.ad.isAccpet')"
:title="$t('cmdb.ad.isAccept')"
v-bind="columns.length ? { width: '100px' } : { minWidth: '100px' }"
:filters="[
{ label: $t('yes'), value: true },
@@ -92,7 +92,7 @@
</vxe-column>
<vxe-column
field="accept_by"
:title="$t('cmdb.ad.accpetBy')"
:title="$t('cmdb.ad.acceptBy')"
v-bind="columns.length ? { width: '80px' } : { minWidth: '80px' }"
:filters="[]"
></vxe-column>
@@ -186,8 +186,8 @@ export default {
this.clickSidebar(Number(_currentType))
return
}
if (res && res.length) {
this.clickSidebar(res[0].id)
if (res && res.length && res[0].ci_types && res[0].ci_types.length) {
this.clickSidebar(res[0].ci_types[0].id)
}
})
},
@@ -246,7 +246,7 @@ export default {
content: that.$t('cmdb.ad.confirmAccept'),
onOk() {
updateADCAccept(row.id).then(() => {
that.$message.success(that.$t('cmdb.ad.accpetSuccess'))
that.$message.success(that.$t('cmdb.ad.acceptSuccess'))
that.getAdc(false)
})
},

View File

@@ -1,5 +1,5 @@
<template>
<div>
<div :style="{ height: '100%' }">
<vxe-table
show-overflow
show-header-overflow
@@ -7,7 +7,7 @@
size="small"
class="ops-stripe-table"
:data="tableData"
v-bind="ci_id ? { maxHeight: `${windowHeight - 94}px` } : { height: `${windowHeight - 225}px` }"
v-bind="ci_id ? { height: 'auto' } : { height: `${windowHeight - 225}px` }"
>
<vxe-column field="trigger_name" :title="$t('cmdb.history.triggerName')"> </vxe-column>
<vxe-column field="type" :title="$t('type')">

View File

@@ -16,8 +16,7 @@
height: '23px',
fontSize: '14px',
}"
/></span
>
/></span>
<span
class="cmdb-preference-left-card-content"
><ops-icon type="cmdb-tree" :style="{ marginRight: '5px' }"/>{{ $t('cmdb.menu.ciTree') }}:
@@ -31,14 +30,23 @@
height: '23px',
fontSize: '14px',
}"
/></span
>
/></span>
</div>
<div class="cmdb-preference-group" v-for="(group, index) in myPreferences" :key="group.name">
<div class="cmdb-preference-group-title">
<span> <ops-icon :style="{ marginRight: '10px' }" :type="group.icon" />{{ group.name }} </span>
</div>
<draggable
v-model="group.ci_types"
:animation="300"
@change="
(e) => {
orderChange(e, group)
}
"
>
<div class="cmdb-preference-group-content" v-for="ciType in group.ci_types" :key="ciType.id">
<OpsMoveIcon class="cmdb-preference-move-icon" />
<div
:class="{
'cmdb-preference-avatar': true,
@@ -82,6 +90,7 @@
</a-tooltip>
</span>
</div>
</draggable>
</div>
</div>
<div class="cmdb-preference-right">
@@ -124,19 +133,12 @@
>
</div>
<div class="cmdb-preference-colleague">
<template v-if="type_id2users[item.id] && type_id2users[item.id].length">
<span
v-if="type_id2users[item.id] && type_id2users[item.id].length"
>{{ type_id2users[item.id].length > 99 ? '99+' : type_id2users[item.id].length
}}{{ $t('cmdb.preference.peopleSub') }}</span
>
<span class="cmdb-preference-colleague-name">
<span v-for="uid in type_id2users[item.id].slice(0, 4)" :key="uid">
{{ getNameByUid(uid) }}
</span>
<span class="cmdb-preference-colleague-ellipsis" v-if="type_id2users[item.id].length > 4">...</span>
</span>
</template>
<span v-else :style="{ marginLeft: 'auto' }">{{ $t('cmdb.preference.noSub') }}</span>
<span v-else>{{ $t('cmdb.preference.noSub') }}</span>
</div>
<div class="cmdb-preference-progress">
<div class="cmdb-preference-progress-info">
@@ -190,15 +192,23 @@ import router, { resetRouter } from '@/router'
import store from '@/store'
import { mapState } from 'vuex'
import moment from 'moment'
import draggable from 'vuedraggable'
import { getCITypeGroups } from '../../api/ciTypeGroup'
import { getPreference, getPreference2, subscribeCIType, subscribeTreeView } from '@/modules/cmdb/api/preference'
import {
getPreference,
getPreference2,
subscribeCIType,
subscribeTreeView,
preferenceCitypeOrder,
} from '@/modules/cmdb/api/preference'
import CollapseTransition from '@/components/CollapseTransition'
import SubscribeSetting from '../../components/subscribeSetting/subscribeSetting'
import { getCIAdcStatistics } from '../../api/ci'
import { ops_move_icon as OpsMoveIcon } from '@/core/icons'
export default {
name: 'Preference',
components: { CollapseTransition, SubscribeSetting },
components: { CollapseTransition, SubscribeSetting, draggable, OpsMoveIcon },
data() {
return {
citypeData: [],
@@ -214,7 +224,6 @@ export default {
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
allUsers: (state) => state.user.allUsers,
}),
},
mounted() {
@@ -277,10 +286,6 @@ export default {
}, 300)
}
},
getNameByUid(uid) {
const _find = this.allUsers.find((item) => item.uid === uid)
return _find?.username[0].toUpperCase() || 'A'
},
getsubscribedDays(item) {
const subscribedTime = this.self.type_id2subs_time[item.id]
moment.duration(moment().diff(moment(subscribedTime)))
@@ -351,6 +356,17 @@ export default {
this.expandKeys.push(group.id)
}
},
orderChange(e, group) {
preferenceCitypeOrder({ type_ids: group.ci_types.map((type) => type.id), is_tree: group.type !== 'ci' })
.then(() => {
if (group.type === 'ci') {
this.resetRoute()
}
})
.catch(() => {
this.getCITypes(false)
})
},
},
}
</script>
@@ -426,7 +442,7 @@ export default {
align-items: center;
height: 45px;
padding: 0 8px;
cursor: default;
cursor: move;
justify-content: flex-start;
&:hover {
background: #ffffff;
@@ -437,6 +453,15 @@ export default {
white-space: nowrap;
margin-left: auto;
}
.cmdb-preference-move-icon {
visibility: visible;
}
}
.cmdb-preference-move-icon {
width: 14px;
height: 20px;
cursor: move;
visibility: hidden;
}
.cmdb-preference-group-content-title {
flex: 1;

View File

@@ -2,14 +2,55 @@
<div :style="{ marginBottom: '-24px', overflow: 'hidden' }">
<div v-if="relationViews.name2id && relationViews.name2id.length" class="relation-views-wrapper">
<div class="cmdb-views-header">
<span class="cmdb-views-header-title">{{ $route.meta.name }}</span>
<span
class="cmdb-views-header-title"
>{{ $route.meta.name }}
<div
class="ops-list-batch-action"
:style="{ backgroundColor: '#c0ceeb' }"
v-if="showBatchLevel !== null && batchTreeKey && batchTreeKey.length"
>
<span
@click="
() => {
$refs.grantModal.open('depart')
}
"
>{{ $t('grant') }}</span
>
<a-divider type="vertical" />
<span
@click="
() => {
$refs.revokeModal.open()
}
"
>{{ $t('revoke') }}</span
>
<template v-if="showBatchLevel > 0">
<a-divider type="vertical" />
<span @click="batchDeleteCIRelationFromTree">{{ $t('delete') }}</span>
</template>
<a-divider type="vertical" />
<span
@click="
() => {
showBatchLevel = null
batchTreeKey = []
}
"
>{{ $t('cancel') }}</span
>
<span>{{ $t('selectRows', { rows: batchTreeKey.length }) }}</span>
</div>
</span>
<a-button size="small" icon="user-add" type="primary" ghost @click="handlePerm">{{ $t('grant') }}</a-button>
</div>
<SplitPane
:min="200"
:max="500"
:paneLengthPixel.sync="paneLengthPixel"
appName="cmdb-relation-views"
:appName="`cmdb-relation-views-${viewId}`"
triggerColor="#F0F5FF"
:triggerLength="18"
>
@@ -24,7 +65,6 @@
@drop="onDrop"
:expandedKeys="expandedKeys"
>
<a-icon slot="switcherIcon" type="down" />
<template #title="{ key: treeKey, title, isLeaf }">
<ContextMenu
:title="title"
@@ -35,7 +75,10 @@
:id2type="relationViews.id2type"
@onContextMenuClick="onContextMenuClick"
@onNodeClick="onNodeClick"
:ciTypes="ciTypes"
:ciTypeIcons="ciTypeIcons"
:showBatchLevel="showBatchLevel"
:batchTreeKey="batchTreeKey"
@clickCheckbox="clickCheckbox"
/>
</template>
</a-tree>
@@ -58,13 +101,9 @@
/>
<div class="relation-views-right-bar">
<a-space>
<a-button
v-if="isLeaf"
type="primary"
size="small"
@click="$refs.create.handleOpen(true, 'create')"
>{{ $t('create') }}</a-button
>
<a-button v-if="isLeaf" type="primary" size="small" @click="$refs.create.handleOpen(true, 'create')">{{
$t('create')
}}</a-button>
<div class="ops-list-batch-action" v-if="isLeaf && isShowBatchIcon">
<template v-if="selectedRowKeys.length">
@@ -135,7 +174,7 @@
{{ col.title }}</span
>
</template>
<template v-if="col.is_choice || col.is_password || col.is_list" #edit="{ row }">
<template v-if="col.is_choice || col.is_password" #edit="{ row }">
<vxe-input v-if="col.is_password" v-model="passwordValue[col.field]" />
<a-select
:getPopupContainer="(trigger) => trigger.parentElement"
@@ -172,18 +211,6 @@
</span>
</a-select-option>
</a-select>
<a-select
:getPopupContainer="(trigger) => trigger.parentElement"
:style="{ width: '100%', height: '32px' }"
v-model="row[col.field]"
:placeholder="$t('placeholder2')"
v-else-if="col.is_list"
:showArrow="false"
mode="tags"
class="ci-table-edit-select"
allowClear
>
</a-select>
</template>
<template
v-if="col.value_type === '6' || col.is_link || col.is_password || col.is_choice"
@@ -329,10 +356,9 @@
v-else-if="relationViews.name2id && !relationViews.name2id.length"
></a-alert>
<AddTableModal ref="addTableModal" @reload="reload" />
<!-- <GrantDrawer ref="grantDrawer" resourceTypeName="RelationView" app_id="cmdb" /> -->
<CMDBGrant ref="cmdbGrant" resourceType="RelationView" app_id="cmdb" />
<ci-detail ref="detail" :typeId="Number(currentTypeId[0])" />
<GrantModal ref="grantModal" @handleOk="onRelationViewGrant" :customTitle="$t('cmdb.serviceTree.grantTitle')" />
<CiDetailDrawer ref="detail" :typeId="Number(currentTypeId[0])" />
<create-instance-form
ref="create"
:typeIdFromRelation="Number(currentTypeId[0])"
@@ -341,11 +367,12 @@
/>
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
<BatchDownload ref="batchDownload" @batchDownload="batchDownload" />
<ReadPermissionsModal ref="readPermissionsModal" />
<RevokeModal ref="revokeModal" @handleRevoke="handleRevoke" />
</div>
</template>
<script>
/* eslint-disable no-useless-escape */
import _ from 'lodash'
import { Tree } from 'element-ui'
import Sortable from 'sortablejs'
@@ -364,21 +391,24 @@ import {
} from '@/modules/cmdb/api/CIRelation'
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
import { searchCI2, updateCI, deleteCI, searchCI } from '@/modules/cmdb/api/ci'
import { getCITypes } from '../../api/CIType'
import { searchCI2, updateCI, deleteCI } from '@/modules/cmdb/api/ci'
import { getCITypeIcons, grantCiType, revokeCiType } from '../../api/CIType'
import { roleHasPermissionToGrant } from '@/modules/acl/api/permission'
import { searchResourceType } from '@/modules/acl/api/resource'
import SplitPane from '@/components/SplitPane'
import EditAttrsPopover from '../ci/modules/editAttrsPopover.vue'
import CiDetail from '../ci/modules/CiDetail'
import CiDetailDrawer from '../ci/modules/ciDetailDrawer.vue'
import CreateInstanceForm from '../ci/modules/CreateInstanceForm'
import JsonEditor from '../../components/JsonEditor/jsonEditor.vue'
import BatchDownload from '../../components/batchDownload/batchDownload.vue'
import PasswordField from '../../components/passwordField/index.vue'
import PreferenceSearch from '../../components/preferenceSearch/preferenceSearch.vue'
import CMDBGrant from '../../components/cmdbGrant'
import GrantModal from '../../components/cmdbGrant/grantModal.vue'
import { ops_move_icon as OpsMoveIcon } from '@/core/icons'
import { getAttrPassword } from '../../api/CITypeAttr'
import ReadPermissionsModal from './modules/ReadPermissionsModal.vue'
import RevokeModal from '../../components/cmdbGrant/revokeModal.vue'
export default {
name: 'RelationViews',
@@ -386,25 +416,27 @@ export default {
SearchForm,
AddTableModal,
ContextMenu,
// GrantDrawer,
CMDBGrant,
GrantModal,
SplitPane,
ElTree: Tree,
EditAttrsPopover,
CiDetail,
CiDetailDrawer,
CreateInstanceForm,
JsonEditor,
BatchDownload,
PasswordField,
PreferenceSearch,
OpsMoveIcon,
ReadPermissionsModal,
RevokeModal,
},
data() {
return {
treeData: [],
triggerSelect: false,
treeNode: null,
ciTypes: [],
ciTypeIcons: {},
relationViews: {},
levels: [],
showTypeIds: [],
@@ -446,6 +478,10 @@ export default {
passwordValue: {},
lastEditCiId: null,
isContinueCloseEdit: true,
contextMenuKey: null,
showBatchLevel: null,
batchTreeKey: [],
}
},
@@ -468,6 +504,21 @@ export default {
isShowBatchIcon() {
return !!this.selectedRowKeys.length
},
topo_flatten() {
return this.relationViews?.views[this.$route.meta.name]?.topo_flatten ?? []
},
descendant_ids() {
return this.topo_flatten.slice(this.treeKeys.length).join(',')
},
descendant_ids_for_statistics() {
return this.topo_flatten.slice(this.treeKeys.length + 1).join(',')
},
root_parent_path() {
return this.treeKeys
.slice(0, this.treeKeys.length)
.map((item) => item.split('%')[0])
.join(',')
},
},
provide() {
return {
@@ -521,8 +572,8 @@ export default {
})
},
getCITypesList() {
getCITypes().then((res) => {
this.ciTypes = res.ci_types
getCITypeIcons().then((res) => {
this.ciTypeIcons = res
})
},
refreshTable() {
@@ -588,33 +639,38 @@ export default {
q = q.slice(1)
}
if (this.treeKeys.length === 0) {
await this.judgeCITypes(q)
// await this.judgeCITypes(q)
if (!refreshType) {
this.loadRoot()
await this.loadRoot()
}
const fuzzySearch = (this.$refs['search'] || {}).fuzzySearch || ''
if (fuzzySearch) {
q = `q=_type:${this.currentTypeId[0]},*${fuzzySearch}*,` + q
} else {
q = `q=_type:${this.currentTypeId[0]},` + q
}
if (this.currentTypeId[0]) {
const res = await searchCI2(q)
this.pageNo = res.page
this.numfound = res.numfound
res.result.forEach((item, index) => (item.key = item._id))
const jsonAttrList = this.preferenceAttrList.filter((attr) => attr.value_type === '6')
console.log(jsonAttrList)
this.instanceList = res['result'].map((item) => {
jsonAttrList.forEach(
(jsonAttr) => (item[jsonAttr.name] = item[jsonAttr.name] ? JSON.stringify(item[jsonAttr.name]) : '')
)
return { ..._.cloneDeep(item) }
})
this.initialInstanceList = _.cloneDeep(this.instanceList)
this.calcColumns()
}
// const fuzzySearch = (this.$refs['search'] || {}).fuzzySearch || ''
// if (fuzzySearch) {
// q = `q=_type:${this.currentTypeId[0]},*${fuzzySearch}*,` + q
// } else {
// q = `q=_type:${this.currentTypeId[0]},` + q
// }
// if (this.currentTypeId[0] && this.treeData && this.treeData.length) {
// // default select first node
// this.onNodeClick(this.treeData[0].key)
// const res = await searchCI2(q)
// const root_id = this.treeData.map((item) => item.id).join(',')
// q += `&root_id=${root_id}`
// this.pageNo = res.page
// this.numfound = res.numfound
// res.result.forEach((item, index) => (item.key = item._id))
// const jsonAttrList = this.preferenceAttrList.filter((attr) => attr.value_type === '6')
// console.log(jsonAttrList)
// this.instanceList = res['result'].map((item) => {
// jsonAttrList.forEach(
// (jsonAttr) => (item[jsonAttr.name] = item[jsonAttr.name] ? JSON.stringify(item[jsonAttr.name]) : '')
// )
// return { ..._.cloneDeep(item) }
// })
// this.initialInstanceList = _.cloneDeep(this.instanceList)
// this.calcColumns()
// }
} else {
q += `&root_id=${this.treeKeys[this.treeKeys.length - 1].split('%')[0]}`
@@ -650,10 +706,10 @@ export default {
level = [1]
}
q += `&level=${level.join(',')}`
await this.judgeCITypes(q)
if (!refreshType) {
this.loadNoRoot(this.treeKeys[this.treeKeys.length - 1], level)
}
await this.judgeCITypes(q)
const fuzzySearch = (this.$refs['search'] || {}).fuzzySearch || ''
if (fuzzySearch) {
q = `q=_type:${this.currentTypeId[0]},*${fuzzySearch}*,` + q
@@ -661,8 +717,12 @@ export default {
q = `q=_type:${this.currentTypeId[0]},` + q
}
if (Object.values(this.level2constraint).includes('2')) {
q = q + `&&has_m2m=1`
q = q + `&has_m2m=1`
}
if (this.root_parent_path) {
q = q + `&root_parent_path=${this.root_parent_path}`
}
q = q + `&descendant_ids=${this.descendant_ids}`
if (this.currentTypeId[0]) {
const res = await searchCIRelation(q)
@@ -682,7 +742,6 @@ export default {
this.calcColumns()
}
if (refreshType === 'refreshNumber') {
const promises = this.treeKeys.map((key, index) => {
let ancestor_ids
@@ -700,8 +759,9 @@ export default {
ancestor_ids,
root_ids: key.split('%')[0],
level: this.treeKeys.length - index,
type_ids: this.showTypes.map((type) => type.id).join(','),
type_ids: this.leaf2showTypes[this.leaf[0]].join(','),
has_m2m: Number(Object.values(this.level2constraint).includes('2')),
descendant_ids: this.descendant_ids_for_statistics,
}).then((res) => {
let result
const getTreeItem = (data, id) => {
@@ -757,22 +817,25 @@ export default {
const promises = _showTypeIds.map((typeId) => {
let _q = (`q=_type:${typeId},` + q).replace(/count=\d*/, 'count=1')
if (Object.values(this.level2constraint).includes('2')) {
_q = _q + `&&has_m2m=1`
_q = _q + `&has_m2m=1`
}
console.log(_q)
if (this.treeKeys.length === 0) {
return searchCI2(_q).then((res) => {
if (res.numfound !== 0) {
showTypeIds.push(typeId)
if (this.root_parent_path) {
_q = _q + `&root_parent_path=${this.root_parent_path}`
}
})
} else {
// if (this.treeKeys.length === 0) {
// return searchCI2(_q).then((res) => {
// if (res.numfound !== 0) {
// showTypeIds.push(typeId)
// }
// })
// } else {
_q = _q + `&descendant_ids=${this.descendant_ids}`
return searchCIRelation(_q).then((res) => {
if (res.numfound !== 0) {
showTypeIds.push(typeId)
}
})
}
// }
})
await Promise.all(promises).then(async () => {
if (showTypeIds.length && showTypeIds.sort().join(',') !== this.showTypeIds.sort().join(',')) {
@@ -796,7 +859,7 @@ export default {
},
async loadRoot() {
searchCI2(`q=_type:(${this.levels[0].join(';')})&count=10000`).then(async (res) => {
await searchCI2(`q=_type:(${this.levels[0].join(';')})&count=10000&use_id_filter=1`).then(async (res) => {
const facet = []
const ciIds = []
res.result.forEach((item) => {
@@ -813,8 +876,9 @@ export default {
return statisticsCIRelation({
root_ids: ciIds.join(','),
level: level,
type_ids: this.showTypes.map((type) => type.id).join(','),
type_ids: this.leaf2showTypes[this.leaf[0]].join(','),
has_m2m: Number(Object.values(this.level2constraint).includes('2')),
descendant_ids: this.descendant_ids_for_statistics,
}).then((num) => {
facet.forEach((item, idx) => {
item[1] += num[ciIds[idx] + '']
@@ -822,16 +886,17 @@ export default {
})
})
await Promise.all(promises)
this.wrapTreeData(facet, 'loadRoot')
this.wrapTreeData(facet)
// default select first node
this.onNodeClick(this.treeData[0].key)
})
},
async loadNoRoot(rootIdAndTypeId, level) {
const rootId = rootIdAndTypeId.split('%')[0]
const typeId = Number(rootIdAndTypeId.split('%')[1])
const topo_flatten = this.relationViews?.views[this.$route.meta.name]?.topo_flatten ?? []
const index = topo_flatten.findIndex((id) => id === typeId)
const _type = topo_flatten[index + 1]
const index = this.topo_flatten.findIndex((id) => id === typeId)
const _type = this.topo_flatten[index + 1]
if (_type) {
let q = `q=_type:${_type}&root_id=${rootId}&level=1&count=10000`
if (
@@ -845,8 +910,12 @@ export default {
.join(',')}`
}
if (Object.values(this.level2constraint).includes('2')) {
q = q + `&&has_m2m=1`
q = q + `&has_m2m=1`
}
if (this.root_parent_path) {
q = q + `&root_parent_path=${this.root_parent_path}`
}
q = q + `&descendant_ids=${this.descendant_ids}`
searchCIRelation(q).then(async (res) => {
const facet = []
const ciIds = []
@@ -868,8 +937,9 @@ export default {
ancestor_ids,
root_ids: ciIds.join(','),
level: _level - 1,
type_ids: this.showTypes.map((type) => type.id).join(','),
type_ids: this.leaf2showTypes[this.leaf[0]].join(','),
has_m2m: Number(Object.values(this.level2constraint).includes('2')),
descendant_ids: this.descendant_ids_for_statistics,
}).then((num) => {
facet.forEach((item, idx) => {
item[1] += num[ciIds[idx] + '']
@@ -878,7 +948,7 @@ export default {
}
})
await Promise.all(promises)
this.wrapTreeData(facet, 'loadNoRoot')
this.wrapTreeData(facet)
})
}
},
@@ -933,6 +1003,7 @@ export default {
}
this.treeKeys = treeNode.eventKey.split('@^@').filter((item) => item !== '')
this.treeNode = treeNode
// this.refreshTable()
resolve()
})
},
@@ -995,8 +1066,7 @@ export default {
this.$refs.xTable.refreshColumn()
})
},
onContextMenuClick(treeKey, menuKey) {
if (treeKey) {
calculateParamsFromTreeKey(treeKey, menuKey) {
const splitTreeKey = treeKey.split('@^@')
const _tempTree = splitTreeKey[splitTreeKey.length - 1].split('%')
const firstCIObj = JSON.parse(_tempTree[2])
@@ -1012,17 +1082,24 @@ export default {
.slice(0, menuKey === 'delete' ? treeKey.split('@^@').length - 2 : treeKey.split('@^@').length - 1)
ancestor_ids = ancestor.map((item) => item.split('%')[0]).join(',')
}
return { splitTreeKey, firstCIObj, firstCIId, _tempTree, ancestor_ids }
},
onContextMenuClick(treeKey, menuKey) {
if (treeKey) {
if (!['batchGrant', 'batchRevoke', 'batchDelete', 'batchCancel'].includes(menuKey)) {
this.contextMenuKey = treeKey
}
const { splitTreeKey, firstCIObj, firstCIId, _tempTree, ancestor_ids } = this.calculateParamsFromTreeKey(
treeKey,
menuKey
)
if (menuKey === 'delete') {
const _tempTreeParent = splitTreeKey[splitTreeKey.length - 2].split('%')
const that = this
this.$confirm({
title: that.$t('warning'),
content: (h) => (
<div>
{that.$t('confirmDelete2', { name: Object.values(firstCIObj)[0] })}
</div>
),
content: (h) => <div>{that.$t('confirmDelete2', { name: Object.values(firstCIObj)[0] })}</div>,
onOk() {
deleteCIRelationView(_tempTreeParent[0], _tempTree[0], { ancestor_ids }).then((res) => {
that.$message.success(that.$t('deleteSuccess'))
@@ -1032,6 +1109,24 @@ export default {
})
},
})
} else if (menuKey === 'grant') {
this.$refs.grantModal.open('depart')
} else if (menuKey === 'revoke') {
this.$refs.revokeModal.open()
} else if (menuKey === 'view') {
this.$refs.readPermissionsModal.open(treeKey)
} else if (menuKey === 'batch') {
this.showBatchLevel = splitTreeKey.filter((item) => !!item).length - 1
this.batchTreeKey = []
} else if (menuKey === 'batchGrant') {
this.$refs.grantModal.open('depart')
} else if (menuKey === 'batchRevoke') {
this.$refs.revokeModal.open()
} else if (menuKey === 'batchDelete') {
this.batchDeleteCIRelationFromTree()
} else if (menuKey === 'batchCancel') {
this.showBatchLevel = null
this.batchTreeKey = []
} else {
const childTypeId = menuKey
this.$refs.addTableModal.openModal(firstCIObj, firstCIId, childTypeId, 'children', ancestor_ids)
@@ -1048,7 +1143,7 @@ export default {
title: that.$t('warning'),
content: (h) => (
<div>
{that.$t('cmdb.serviceTreedeleteRelationConfirm', { name: currentShowType.alias || currentShowType.name })}
{that.$t('cmdb.serviceTree.deleteRelationConfirm', { name: currentShowType.alias || currentShowType.name })}
</div>
),
onOk() {
@@ -1086,8 +1181,10 @@ export default {
const _splitTargetKey = targetKey.split('@^@').filter((item) => item !== '')
if (_splitDragKey.length - 1 === _splitTargetKey.length) {
const dragId = _splitDragKey[_splitDragKey.length - 1].split('%')[0]
// const targetObj = JSON.parse(_splitTargetKey[_splitTargetKey.length - 1].split('%')[2])
const targetId = _splitTargetKey[_splitTargetKey.length - 1].split('%')[0]
console.log(_splitDragKey)
// TODO 拖拽这里不造咋弄 等等再说吧
batchUpdateCIRelationChildren([dragId], [targetId]).then((res) => {
this.reload()
})
@@ -1458,6 +1555,138 @@ export default {
this.$message.error(this.$t('cmdb.serviceTreecopyFailed'))
})
},
async onRelationViewGrant({ department, user }, type) {
const result = []
if (this.showBatchLevel !== null && this.batchTreeKey && this.batchTreeKey.length) {
for (let i = 0; i < this.batchTreeKey.length; i++) {
await this.relationViewGrant({ department, user }, this.batchTreeKey[i], (_result) => {
result.push(..._result)
})
}
this.showBatchLevel = null
this.batchTreeKey = []
} else {
await this.relationViewGrant({ department, user }, this.contextMenuKey, (_result) => {
result.push(..._result)
})
}
if (result.every((r) => r.status === 'fulfilled')) {
this.$message.success(this.$t('operateSuccess'))
}
},
async relationViewGrant({ department, user }, nodeKey, callback) {
const needGrantNodes = nodeKey
.split('@^@')
.filter((item) => !!item)
.reverse()
console.log(needGrantNodes)
const needGrantRids = [...department, ...user]
const floor = Math.ceil(needGrantRids.length / 6)
const result = []
for (let i = 0; i < needGrantNodes.length; i++) {
const grantNode = needGrantNodes[i]
const _grantNode = grantNode.split('%')
const ciId = _grantNode[0]
const typeId = _grantNode[1]
const uniqueValue = Object.entries(JSON.parse(_grantNode[2]))[0][1]
const parent_path = needGrantNodes
.slice(i + 1)
.map((item) => {
return Number(item.split('%')[0])
})
.reverse()
.join(',')
for (let j = 0; j < floor; j++) {
const itemList = needGrantRids.slice(6 * j, 6 * j + 6)
const promises = itemList.map((rid) =>
grantCiType(typeId, rid, {
id_filter: { [ciId]: { name: uniqueValue, parent_path } },
is_recursive: Number(i > 0),
})
)
const _result = await Promise.allSettled(promises)
result.push(..._result)
}
}
callback(result)
},
clickCheckbox(treeKey) {
const _idx = this.batchTreeKey.findIndex((item) => item === treeKey)
if (_idx > -1) {
this.batchTreeKey.splice(_idx, 1)
} else {
this.batchTreeKey.push(treeKey)
}
},
batchDeleteCIRelationFromTree() {
const that = this
this.$confirm({
title: that.$t('warning'),
content: (h) => <div>{that.$t('confirmDelete')}</div>,
async onOk() {
for (let i = 0; i < that.batchTreeKey.length; i++) {
const { splitTreeKey, firstCIObj, firstCIId, _tempTree, ancestor_ids } = that.calculateParamsFromTreeKey(
that.batchTreeKey[i],
'delete'
)
const _tempTreeParent = splitTreeKey[splitTreeKey.length - 2].split('%')
await deleteCIRelationView(_tempTreeParent[0], _tempTree[0], { ancestor_ids }).then((res) => {})
}
that.$message.success(that.$t('deleteSuccess'))
that.showBatchLevel = null
that.batchTreeKey = []
setTimeout(() => {
that.reload()
}, 500)
},
})
},
async handleSingleRevoke({ users = [], roles = [] }, treeKey, callback) {
const rids = [...users.map((item) => Number(item.split('-')[1])), ...roles]
const treeKeyPath = treeKey.split('@^@').filter((item) => !!item)
const _treeKey = treeKeyPath.pop(-1).split('%')
const id_filter = {}
const typeId = _treeKey[1]
const ciId = _treeKey[0]
const uniqueValue = Object.entries(JSON.parse(_treeKey[2]))[0][1]
const parent_path = treeKeyPath
.map((item) => {
return Number(item.split('%')[0])
})
.join(',')
id_filter[ciId] = { name: uniqueValue, parent_path }
const floor = Math.ceil(rids.length / 6)
const result = []
for (let j = 0; j < floor; j++) {
const itemList = rids.slice(6 * j, 6 * j + 6)
const promises = itemList.map((rid) => revokeCiType(typeId, rid, { id_filter, perms: ['read'], parent_path }))
const _result = await Promise.allSettled(promises)
result.push(..._result)
}
callback(result)
},
async handleRevoke({ users = [], roles = [] }) {
const result = []
if (this.showBatchLevel !== null && this.batchTreeKey && this.batchTreeKey.length) {
for (let i = 0; i < this.batchTreeKey.length; i++) {
const treeKey = this.batchTreeKey[i]
await this.handleSingleRevoke({ users, roles }, treeKey, (_result) => {
result.push(..._result)
})
}
} else {
await this.handleSingleRevoke({ users, roles }, this.contextMenuKey, (_result) => {
result.push(..._result)
})
}
if (result.every((r) => r.status === 'fulfilled')) {
this.$message.success(this.$t('operateSuccess'))
}
this.showBatchLevel = null
this.batchTreeKey = []
},
},
}
</script>

View File

@@ -11,24 +11,24 @@
>
<div :style="{ width: '100%' }" id="add-table-modal">
<a-spin :spinning="loading">
<!-- <a-input
v-model="expression"
class="ci-searchform-expression"
:style="{ width, marginBottom: '10px' }"
:placeholder="placeholder"
@focus="
() => {
isFocusExpression = true
}
"
/> -->
<SearchForm
ref="searchForm"
:typeId="addTypeId"
:preferenceAttrList="preferenceAttrList"
@refresh="handleSearch"
/>
<!-- <a @click="handleSearch"><a-icon type="search"/></a> -->
>
<a-button
@click="
() => {
$refs.createInstanceForm.handleOpen(true, 'create')
}
"
slot="extraContent"
type="primary"
size="small"
>新增</a-button
>
</SearchForm>
<vxe-table
ref="xTable"
row-id="_id"
@@ -77,19 +77,31 @@
/>
</a-spin>
</div>
<CreateInstanceForm
ref="createInstanceForm"
:typeIdFromRelation="addTypeId"
@reload="
() => {
currentPage = 1
getTableData(true)
}
"
/>
</a-modal>
</template>
<script>
/* eslint-disable no-useless-escape */
import { searchCI } from '@/modules/cmdb/api/ci'
import { getSubscribeAttributes } from '@/modules/cmdb/api/preference'
import { batchUpdateCIRelationChildren, batchUpdateCIRelationParents } from '@/modules/cmdb/api/CIRelation'
import { getCITableColumns } from '../../../utils/helper'
import SearchForm from '../../../components/searchForm/SearchForm.vue'
import CreateInstanceForm from '../../ci/modules/CreateInstanceForm.vue'
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
export default {
name: 'AddTableModal',
components: { SearchForm },
components: { SearchForm, CreateInstanceForm },
data() {
return {
visible: false,
@@ -106,6 +118,7 @@ export default {
type: 'children',
preferenceAttrList: [],
ancestor_ids: undefined,
attrList1: [],
}
},
computed: {
@@ -119,6 +132,13 @@ export default {
return this.isFocusExpression ? '500px' : '100px'
},
},
provide() {
return {
attrList: () => {
return this.attrList
},
}
},
watch: {},
methods: {
async openModal(ciObj, ciId, addTypeId, type, ancestor_ids = undefined) {
@@ -132,6 +152,9 @@ export default {
await getSubscribeAttributes(addTypeId).then((res) => {
this.preferenceAttrList = res.attributes // 已经订阅的全部列
})
getCITypeAttributesById(addTypeId).then((res) => {
this.attrList = res.attributes
})
this.getTableData(true)
},
async getTableData(isInit) {
@@ -207,6 +230,9 @@ export default {
this.handleClose()
this.$emit('reload')
}, 500)
} else {
this.handleClose()
this.$emit('reload')
}
},
handleSearch() {

View File

@@ -1,63 +1,81 @@
<template>
<a-dropdown :trigger="['contextmenu']">
<a-menu slot="overlay" @click="({ key: menuKey }) => this.onContextMenuClick(this.treeKey, menuKey)">
<a-menu-item v-for="item in menuList" :key="item.id">{{ $t('new') }} {{ item.alias }}</a-menu-item>
<a-menu-item v-if="showDelete" key="delete">{{ $t('cmdb.serviceTree.deleteNode') }}</a-menu-item>
</a-menu>
<div
:style="{
width: '100%',
display: 'inline-flex',
justifyContent: 'space-between',
alignItems: 'center',
:class="{
'relation-views-node': true,
'relation-views-node-checkbox': showCheckbox,
}"
@click="clickNode"
>
<span
:style="{
display: 'flex',
overflow: 'hidden',
width: '100%',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
alignItems: 'center',
}"
>
<span>
<a-checkbox @click.stop="clickCheckbox" class="relation-views-node-checkbox" v-if="showCheckbox" />
<template v-if="icon">
<img
v-if="icon.split('$$')[2]"
v-if="icon.includes('$$') && icon.split('$$')[2]"
:src="`/api/common-setting/v1/file/${icon.split('$$')[3]}`"
:style="{ maxHeight: '14px', maxWidth: '14px' }"
/>
<ops-icon
v-else
v-else-if="icon.includes('$$') && icon.split('$$')[0]"
:style="{
color: icon.split('$$')[1],
fontSize: '14px',
}"
:type="icon.split('$$')[0]"
/>
<span class="relation-views-node-icon" v-else>{{ icon ? icon[0].toUpperCase() : 'i' }}</span>
</template>
<span
:style="{
display: 'inline-block',
width: '16px',
height: '16px',
borderRadius: '50%',
backgroundColor: '#d3d3d3',
color: '#fff',
textAlign: 'center',
lineHeight: '16px',
fontSize: '12px',
}"
v-else
>{{ ciTypeName ? ciTypeName[0].toUpperCase() : 'i' }}</span
>
<span :style="{ marginLeft: '5px' }">{{ this.title }}</span>
<span class="relation-views-node-title">{{ this.title }}</span>
</span>
<a-dropdown>
<a-menu slot="overlay" @click="({ key: menuKey }) => this.onContextMenuClick(this.treeKey, menuKey)">
<template v-if="showBatchLevel === null">
<a-menu-item
v-for="item in menuList"
:key="item.id"
><a-icon type="plus-circle" />{{ $t('new') }} {{ item.alias }}</a-menu-item
>
<a-menu-item
v-if="showDelete"
key="delete"
><ops-icon type="icon-xianxing-delete" />{{ $t('cmdb.serviceTree.deleteNode') }}</a-menu-item
>
<a-menu-divider />
<a-menu-item key="grant"><a-icon type="user-add" />{{ $t('grant') }}</a-menu-item>
<a-menu-item key="revoke"><a-icon type="user-delete" />{{ $t('revoke') }}</a-menu-item>
<a-menu-item key="view"><a-icon type="eye" />{{ $t('cmdb.serviceTree.view') }}</a-menu-item>
<a-menu-divider />
<a-menu-item
key="batch"
><ops-icon type="icon-xianxing-copy" />{{ $t('cmdb.serviceTree.batch') }}</a-menu-item
>
</template>
<template v-else>
<a-menu-item
:disabled="!batchTreeKey || !batchTreeKey.length"
key="batchGrant"
><a-icon type="user-add" />{{ $t('grant') }}</a-menu-item
>
<a-menu-item
:disabled="!batchTreeKey || !batchTreeKey.length"
key="batchRevoke"
><a-icon type="user-delete" />{{ $t('revoke') }}</a-menu-item
>
<a-menu-divider />
<template v-if="showBatchLevel > 0">
<a-menu-item
:disabled="!batchTreeKey || !batchTreeKey.length"
key="batchDelete"
><ops-icon type="icon-xianxing-delete" />{{ $t('delete') }}</a-menu-item
>
<a-menu-divider />
</template>
<a-menu-item key="batchCancel"><a-icon type="close-circle" />{{ $t('cancel') }}</a-menu-item>
</template>
</a-menu>
<a-icon class="relation-views-node-operation" type="ellipsis" />
</a-dropdown>
<a-icon :style="{ fontSize: '10px' }" v-if="childLength && !isLeaf" :type="switchIcon"></a-icon>
</div>
</a-dropdown>
</template>
<script>
@@ -88,7 +106,15 @@ export default {
type: Boolean,
default: () => false,
},
ciTypes: {
ciTypeIcons: {
type: Object,
default: () => {},
},
showBatchLevel: {
type: Number,
default: null,
},
batchTreeKey: {
type: Array,
default: () => [],
},
@@ -141,14 +167,10 @@ export default {
icon() {
const _split = this.treeKey.split('@^@')
const currentNodeTypeId = _split[_split.length - 1].split('%')[1]
const _find = this.ciTypes.find((type) => type.id === Number(currentNodeTypeId))
return _find?.icon || null
return this.ciTypeIcons[Number(currentNodeTypeId)] ?? null
},
ciTypeName() {
const _split = this.treeKey.split('@^@')
const currentNodeTypeId = _split[_split.length - 1].split('%')[1]
const _find = this.ciTypes.find((type) => type.id === Number(currentNodeTypeId))
return _find?.name || ''
showCheckbox() {
return this.showBatchLevel === this.treeKey.split('@^@').filter((item) => !!item).length - 1
},
},
methods: {
@@ -159,8 +181,73 @@ export default {
this.$emit('onNodeClick', this.treeKey)
this.switchIcon = this.switchIcon === 'down' ? 'up' : 'down'
},
clickCheckbox() {
this.$emit('clickCheckbox', this.treeKey)
},
},
}
</script>
<style></style>
<style lang="less" scoped>
.relation-views-node {
width: 100%;
display: inline-flex;
justify-content: space-between;
align-items: center;
> span {
display: flex;
overflow: hidden;
align-items: center;
width: 100%;
.relation-views-node-icon {
display: inline-block;
width: 16px;
height: 16px;
border-radius: 50%;
background-color: #d3d3d3;
color: #fff;
text-align: center;
line-height: 16px;
font-size: 12px;
}
.relation-views-node-title {
padding-left: 5px;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
width: calc(100% - 16px);
}
}
.relation-views-node-operation {
display: none;
margin-right: 5px;
}
}
.relation-views-node-checkbox,
.relation-views-node-moveright {
> span {
.relation-views-node-checkbox {
margin-right: 10px;
}
.relation-views-node-title {
width: calc(100% - 42px);
}
}
}
</style>
<style lang="less">
.relation-views-left .ant-tree-node-content-wrapper:hover {
.relation-views-node-operation {
display: inline-block;
}
}
.relation-views-left {
ul:has(.relation-views-node-checkbox) > li > ul {
margin-left: 26px;
}
ul:has(.relation-views-node-checkbox) {
margin-left: 0 !important;
}
}
</style>

View File

@@ -0,0 +1,105 @@
<template>
<a-modal
width="600px"
:bodyStyle="{
paddingTop: 0,
}"
:visible="visible"
:footer="null"
@cancel="handleCancel"
:title="$t('view')"
>
<div>
<template v-if="readCIIdFilterPermissions && readCIIdFilterPermissions.length">
<p>
<strong>{{ $t('cmdb.serviceTree.idAuthorizationPolicy') }}</strong>
<a
@click="
() => {
showAllReadCIIdFilterPermissions = !showAllReadCIIdFilterPermissions
}
"
v-if="readCIIdFilterPermissions.length > 10"
><a-icon
:type="showAllReadCIIdFilterPermissions ? 'caret-down' : 'caret-up'"
/></a>
</p>
<a-tag
v-for="item in showAllReadCIIdFilterPermissions
? readCIIdFilterPermissions
: readCIIdFilterPermissions.slice(0, 10)"
:key="item.name"
color="blue"
:style="{ marginBottom: '5px' }"
>{{ item.name }}</a-tag
>
<a-tag
:style="{ marginBottom: '5px' }"
v-if="readCIIdFilterPermissions.length > 10 && !showAllReadCIIdFilterPermissions"
>+{{ readCIIdFilterPermissions.length - 10 }}</a-tag
>
</template>
<a-empty v-else>
<img slot="image" :src="require('@/assets/data_empty.png')" />
<span slot="description"> {{ $t('noData') }} </span>
</a-empty>
</div>
</a-modal>
</template>
<script>
import { ciTypeFilterPermissions, getCIType } from '../../../api/CIType'
import FilterComp from '@/components/CMDBFilterComp'
import { searchRole } from '@/modules/acl/api/role'
export default {
name: 'ReadPermissionsModal',
components: { FilterComp },
data() {
return {
visible: false,
filerPerimissions: {},
readCIIdFilterPermissions: [],
canSearchPreferenceAttrList: [],
showAllReadCIIdFilterPermissions: false,
allRoles: [],
}
},
mounted() {
this.loadRoles()
},
methods: {
async loadRoles() {
const res = await searchRole({ page_size: 9999, app_id: 'cmdb', is_all: true })
this.allRoles = res.roles
},
async open(treeKey) {
this.visible = true
const _splitTreeKey = treeKey.split('@^@').filter((item) => !!item)
const _treeKey = _splitTreeKey.slice(_splitTreeKey.length - 1, _splitTreeKey.length)[0].split('%')
const typeId = _treeKey[1]
const _treeKeyPath = _splitTreeKey.map((item) => item.split('%')[0]).join(',')
await ciTypeFilterPermissions(typeId).then((res) => {
this.filerPerimissions = res
})
const readCIIdFilterPermissions = []
Object.entries(this.filerPerimissions).forEach(([k, v]) => {
const { id_filter } = v
if (id_filter && Object.keys(id_filter).includes(_treeKeyPath)) {
const _find = this.allRoles.find((item) => item.id === Number(k))
readCIIdFilterPermissions.push({ name: _find?.name ?? k, rid: k })
}
})
this.readCIIdFilterPermissions = readCIIdFilterPermissions
console.log(readCIIdFilterPermissions)
},
handleCancel() {
this.showAllReadCIIdFilterPermissions = false
this.visible = false
},
},
}
</script>
<style></style>

View File

@@ -206,7 +206,7 @@
import _ from 'lodash'
import SearchForm from '../../components/searchForm/SearchForm.vue'
import { searchCI } from '../../api/ci'
import { searchAttributes, getCITypeAttributesByTypeIds } from '../../api/CITypeAttr'
import { searchAttributes, getCITypeAttributesByTypeIds, getCITypeAttributesById } from '../../api/CITypeAttr'
import { getCITypes } from '../../api/CIType'
import { getSubscribeAttributes } from '../../api/preference'
import { getCITableColumns } from '../../utils/helper'
@@ -284,6 +284,10 @@ export default {
const regSort = /(?<=sort=).+/g
const exp = expression.match(regQ) ? expression.match(regQ)[0] : null
// if (exp) {
// exp = exp.replace(/(\:)/g, '$1*')
// exp = exp.replace(/(\,)/g, '*$1')
// }
// 如果是表格点击的排序 以表格为准
let sort
if (sortByTable) {
@@ -314,44 +318,47 @@ export default {
this.columnsGroup = []
this.instanceList = []
this.totalNumber = res['numfound']
const oldData = res.result
function allKeys(data) {
const keys = {}
const ignoreAttr = ['_id', '_type', 'ci_type', 'ci_type_alias', 'unique', 'unique_alias']
data.forEach((item) => {
Object.keys(item).forEach((key) => {
if (!ignoreAttr.includes(key)) {
keys[key] = ''
if (!res['numfound']) {
return
}
const { attributes: resAllAttributes } = await getCITypeAttributesByTypeIds({
type_ids: Object.keys(res.counter).join(','),
})
const _columnsGroup = Object.keys(res.counter).map((key) => {
const _find = this.ciTypes.find((item) => item.name === key)
return {
id: `parent-${_find.id}`,
value: key,
label: _find?.alias || _find?.name,
isCiType: true,
}
})
const ciTypeAttribute = {}
const promises = _columnsGroup.map((item) => {
return getCITypeAttributesById(item.id.split('-')[1]).then((res) => {
ciTypeAttribute[item.label] = res.attributes
})
})
await Promise.all(promises)
const outputKeys = {}
resAllAttributes.forEach((attr) => {
outputKeys[attr.name] = ''
})
return keys
}
function tidy(data) {
const outputKeys = allKeys(data)
const common = {}
data.forEach((item) => {
const tmp = {}
Object.keys(outputKeys).forEach((j) => {
if (j in item) {
tmp[j] = item[j]
// 提取common
{
const key = item['ci_type_alias']
if (j in common) {
common[j][[key]] = ''
Object.keys(outputKeys).forEach((key) => {
Object.entries(ciTypeAttribute).forEach(([type, attrs]) => {
if (attrs.find((a) => a.name === key)) {
if (key in common) {
common[key][type] = ''
} else {
common[j] = { [key]: '' }
common[key] = { [type]: '' }
}
}
} else {
tmp[j] = null
}
})
})
const commonObject = {}
const commonKeys = []
// 整理common
@@ -366,10 +373,7 @@ export default {
}
}
})
return { commonObject, commonKeys }
}
const { commonObject, commonKeys } = tidy(oldData)
const _commonColumnsGroup = Object.keys(commonObject).map((key) => {
return {
id: `parent-${key}`,
@@ -385,24 +389,14 @@ export default {
}
})
const _columnsGroup = Object.keys(res.counter).map((key) => {
const _find = this.ciTypes.find((item) => item.name === key)
return {
id: `parent-${_find.id}`,
value: key,
label: _find?.alias || _find?.name,
isCiType: true,
}
})
const promises = _columnsGroup.map((item) => {
const promises1 = _columnsGroup.map((item) => {
return getSubscribeAttributes(item.id.split('-')[1]).then((res1) => {
item.children = this.getColumns(res.result, res1.attributes).filter(
(col) => !commonKeys.includes(col.field)
)
})
})
await Promise.all(promises).then(() => {
await Promise.all(promises1).then(() => {
this.columnsGroup = [..._commonColumnsGroup, ..._columnsGroup]
this.instanceList = res['result']
})

View File

@@ -32,32 +32,24 @@
>
<template #one>
<div class="tree-views-left" :style="{ height: `${windowHeight - 115}px` }">
<a-collapse
:activeKey="current"
accordion
@change="handleChangeCi"
:bordered="false"
:destroyInactivePanel="true"
>
<a-collapse-panel
v-for="ciType in subscribeTreeViewCiTypes"
:key="String(ciType.type_id)"
:showArrow="false"
:style="{
borderRadius: '4px',
marginBottom: '5px',
border: 0,
overflow: 'hidden',
width: '100%',
}"
<draggable
v-model="subscribeTreeViewCiTypes"
:animation="300"
@change="
(e) => {
orderChange(e, subscribeTreeViewCiTypes)
}
"
>
<div v-for="ciType in subscribeTreeViewCiTypes" :key="ciType.type_id">
<div
slot="header"
@click="handleChangeCi(ciType.type_id)"
:class="{
'custom-header': true,
'custom-header-selected': Number(ciType.type_id) === Number(typeId),
'custom-header-selected': Number(ciType.type_id) === Number(typeId) && !treeKeys.length,
}"
>
<OpsMoveIcon class="move-icon" />
<span class="tree-views-left-header-icon">
<template v-if="ciType.icon">
<img
@@ -95,6 +87,7 @@
:tree-data="treeData"
:load-data="onLoadData"
:expandedKeys="expandedKeys"
v-if="Number(ciType.type_id) === Number(typeId)"
>
<a-icon slot="switcherIcon" type="down" />
<template #title="{ key: treeKey, title, isLeaf }">
@@ -107,8 +100,8 @@
/>
</template>
</a-tree>
</a-collapse-panel>
</a-collapse>
</div>
</draggable>
</div>
</template>
<template #two>
@@ -189,7 +182,7 @@
{{ col.title }}</span
>
</template>
<template v-if="col.is_choice || col.is_password || col.is_list" #edit="{ row }">
<template v-if="col.is_choice || col.is_password" #edit="{ row }">
<vxe-input v-if="col.is_password" v-model="passwordValue[col.field]" />
<a-select
:getPopupContainer="(trigger) => trigger.parentElement"
@@ -226,18 +219,6 @@
</span>
</a-select-option>
</a-select>
<a-select
:getPopupContainer="(trigger) => trigger.parentElement"
:style="{ width: '100%', height: '32px' }"
v-model="row[col.field]"
:placeholder="$t('placeholder2')"
v-else-if="col.is_list"
:showArrow="false"
mode="tags"
class="ci-table-edit-select"
allowClear
>
</a-select>
</template>
<template
v-if="col.value_type === '6' || col.is_link || col.is_password || col.is_choice"
@@ -390,7 +371,7 @@
}
"
/>
<ci-detail ref="detail" :typeId="Number(typeId)" :treeViewsLevels="treeViewsLevels" />
<CiDetailDrawer ref="detail" :typeId="Number(typeId)" :treeViewsLevels="treeViewsLevels" />
<create-instance-form
ref="create"
:typeIdFromRelation="Number(typeId)"
@@ -407,7 +388,13 @@
/* eslint-disable no-useless-escape */
import _ from 'lodash'
import Sortable from 'sortablejs'
import { getSubscribeTreeView, getSubscribeAttributes, subscribeTreeView } from '@/modules/cmdb/api/preference'
import draggable from 'vuedraggable'
import {
getSubscribeTreeView,
getSubscribeAttributes,
subscribeTreeView,
preferenceCitypeOrder,
} from '@/modules/cmdb/api/preference'
import { searchCI, updateCI, deleteCI } from '@/modules/cmdb/api/ci'
import { getCITypes } from '@/modules/cmdb/api/CIType'
import { getCITableColumns } from '../../utils/helper'
@@ -417,7 +404,7 @@ import PasswordField from '../../components/passwordField/index.vue'
import SplitPane from '@/components/SplitPane'
import TreeViewsNode from './modules/treeViewsNode.vue'
import EditAttrsPopover from '../ci/modules/editAttrsPopover.vue'
import CiDetail from '../ci/modules/CiDetail'
import CiDetailDrawer from '../ci/modules/ciDetailDrawer.vue'
import CreateInstanceForm from '../ci/modules/CreateInstanceForm'
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
import JsonEditor from '../../components/JsonEditor/jsonEditor.vue'
@@ -437,13 +424,14 @@ export default {
SplitPane,
TreeViewsNode,
EditAttrsPopover,
CiDetail,
CiDetailDrawer,
CreateInstanceForm,
JsonEditor,
BatchDownload,
PreferenceSearch,
MetadataDrawer,
OpsMoveIcon,
draggable,
},
data() {
return {
@@ -463,7 +451,6 @@ export default {
pageSize: 50,
currentPage: 1,
totalNumber: 0,
current: '', // 当前页面的type_id
currentAttrList: [],
trigger: false,
newLoad: true,
@@ -571,7 +558,6 @@ export default {
this.subscribeTreeViewCiTypes = res
if (this.subscribeTreeViewCiTypes.length) {
this.typeId = this.$route.params.typeId || this.subscribeTreeViewCiTypes[0].type_id
this.current = `${this.typeId}`
this.selectedRowKeys = []
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
@@ -600,7 +586,6 @@ export default {
async loadCurrentView() {
if (this.subscribeTreeViewCiTypes.length) {
this.typeId = this.$route.params.typeId || this.subscribeTreeViewCiTypes[0].type_id
this.current = String(this.typeId)
this.selectedRowKeys = []
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
@@ -746,9 +731,14 @@ export default {
name: 'cmdb_tree_views_item',
params: { typeId: Number(value) },
})
this.typeId = Number(value)
} else {
this.typeId = null
this.$nextTick(() => {
this.typeId = Number(value)
this.newLoad = true
this.initPage()
})
}
this.isSetDataNodes = []
},
@@ -1136,12 +1126,22 @@ export default {
}
})
this.$refs.create.visible = false
const key = 'updatable'
let errorMsg = ''
for (let i = 0; i < this.selectedRowKeys.length; i++) {
await updateCI(this.selectedRowKeys[i], payload, false)
.then(() => {
successNum += 1
})
.catch(() => {
.catch((error) => {
errorMsg = errorMsg + '\n' + `${this.selectedRowKeys[i]}:${error.response?.data?.message ?? ''}`
this.$notification.warning({
key,
message: this.$t('warning'),
description: errorMsg,
duration: 0,
style: { whiteSpace: 'break-spaces' },
})
errorNum += 1
})
.finally(() => {
@@ -1209,6 +1209,13 @@ export default {
this.$message.error(this.$t('cmdb.ci.copyFailed'))
})
},
orderChange(e, subscribeTreeViewCiTypes) {
preferenceCitypeOrder({ type_ids: subscribeTreeViewCiTypes.map((type) => type.type_id), is_tree: true }).catch(
() => {
this.getTreeViews()
}
)
},
},
}
</script>
@@ -1230,21 +1237,6 @@ export default {
&:hover {
overflow: auto;
}
.ant-collapse-borderless {
background-color: #fff;
}
.ant-collapse-item:has(.custom-header-selected):not(:has(.ant-tree-treenode-selected)) > .ant-collapse-header,
.ant-collapse-item-active:not(:has(.ant-tree-treenode-selected)) > .ant-collapse-header {
background-color: #d6e4ff;
}
.ant-collapse-header {
padding: 8px 12px 4px;
&:hover {
background-color: #f0f5ff;
}
&:hover > .custom-header > .actions {
display: inherit;
}
.custom-header {
width: 100%;
display: inline-flex;
@@ -1252,6 +1244,25 @@ export default {
flex-wrap: nowrap;
justify-content: flex-start;
align-items: center;
padding: 8px 0 8px 12px;
cursor: move;
border-radius: 2px;
position: relative;
&:hover {
background-color: #f0f5ff;
> .actions,
> .move-icon {
display: inherit;
}
}
.move-icon {
width: 14px;
height: 20px;
cursor: move;
position: absolute;
display: none;
left: 0;
}
.tree-views-left-header-icon {
display: inline-flex;
align-items: center;
@@ -1274,6 +1285,7 @@ export default {
.actions {
display: none;
margin-left: auto;
cursor: pointer;
}
.action {
display: inline-block;
@@ -1285,10 +1297,8 @@ export default {
}
}
}
}
.ant-collapse > .ant-collapse-item > .ant-collapse-header {
white-space: nowrap;
.custom-header-selected {
background-color: #d3e3fd !important;
}
.ant-tree li {
padding: 2px 0;

View File

@@ -12,13 +12,13 @@
}
::-webkit-scrollbar-thumb {
background-color: rgba(47, 122, 235, 0.2);
background-color: @scrollbar-color;
background-clip: padding-box;
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background-color: rgba(47, 122, 235, 0.2);
background-color: @scrollbar-color;
}
@import '~ant-design-vue/dist/antd.less';
@@ -32,10 +32,25 @@ body {
&.userLayout {
overflow: auto;
}
.text-color-1 {
color: @text-color_1;
}
.text-color-2 {
color: @text-color_2;
}
.text-color-3 {
color: @text-color_3;
}
.text-color-4 {
color: @text-color_4;
}
.border-radius-box {
border-radius: @border-radius-box;
}
}
.ant-layout {
background-color: #f0f5ff;
background-color: #f7f8fa;
}
.layout.ant-layout {
@@ -99,12 +114,8 @@ body {
height: @layout-header-icon-height;
line-height: @layout-header-icon-height;
display: inline-flex;
align-items: flex-end;
align-items: center;
margin-right: 10px;
&:hover {
background: linear-gradient(0deg, rgba(0, 80, 201, 0.2) 0%, rgba(174, 207, 255, 0.06) 86.76%);
color: @layout-header-font-selected-color;
}
}
.topmenu {
@@ -192,12 +203,6 @@ body {
color: @layout-header-font-color;
align-items: center;
border-radius: 4px;
&:hover {
background: linear-gradient(0deg, rgba(0, 80, 201, 0.2) 0%, rgba(174, 207, 255, 0.06) 86.76%);
color: @layout-header-font-selected-color;
}
.avatar {
margin-right: 5px;
color: @layout-header-font-color;
@@ -349,8 +354,6 @@ body {
}
&.light {
background-color: #225686;
.header-index-wide {
.header-index-left {
.logo {
@@ -364,19 +367,8 @@ body {
}
// 内容区
// .layout-content {
// margin: 24px 24px 0px;
// height: 100%;
// height: 64px;
// padding: 0 12px 0 0;
// }
.ant-layout-content {
padding: 0 24px;
// background: @layout-background-color-light;
//按钮样式
.ant-btn {
// border-radius: 2px;
}
}
// footer
@@ -425,7 +417,6 @@ body {
min-height: 100vh;
.ant-layout-sider-children {
background: #225686; //浅色系左边菜单栏 深色系需删除
overflow-y: hidden;
> .ant-menu {
height: calc(100vh - 40px);
@@ -517,12 +508,7 @@ body {
transition: none;
}
.ops-side-bar.ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected {
// background-image: url('../assets/sidebar_selected.png') !important;
// background-repeat: no-repeat !important;
background: #3e4a71;
// background-size: 228px 38px;
// background-position-x: -10px;
// background-position-y: center;
background: @layout-sidebar-selected-color;
transition: none;
}
.ops-side-bar.ant-menu .ant-menu-submenu .ant-menu-item.ant-menu-item-selected {
@@ -531,39 +517,29 @@ body {
@keyframes wordsLoop {
0% {
// transform: translateX(100%);
// -webkit-transform: translateX(100%);
margin-left: 0;
}
100% {
// transform: translateX(-100%);
// -webkit-transform: translateX(-100%);
margin-left: -300%;
}
}
.ops-side-bar.ant-menu-light {
border-right-color: transparent;
// background: @layout-background-light-color;
// background: url('../assets/sidebar_background.png');
background: #1d264c;
// background-position-x: center;
// background-position-y: center;
background: @layout-sidebar-color;
background-repeat: no-repeat !important;
background-size: cover;
.ant-menu-inline.ant-menu-sub {
background-color: #000c37;
background-color: @layout-sidebar-sub-color;
}
.ant-menu-submenu-content .ant-menu-item,
.ant-menu-item {
// margin: 0;
> a {
display: inline-flex;
align-items: center;
color: rgba(255, 255, 255, 0.8);
color: @layout-sidebar-font-color;
}
&:hover {
// background: #0000000a;
.scroll {
animation: 5s wordsLoop linear infinite normal;
}
@@ -578,7 +554,7 @@ body {
white-space: nowrap;
}
a:hover {
color: #fff;
color: @layout-sidebar-font-color;
font-weight: 600;
}
&:hover .custom-menu-extra-ellipsis {
@@ -614,7 +590,7 @@ body {
.ant-menu-item-selected {
a,
a:hover {
color: #fff;
color: @layout-sidebar-font-color;
font-weight: 600;
}
}
@@ -624,20 +600,20 @@ body {
}
.ant-menu-submenu {
color: rgba(255, 255, 255, 0.8);
color: @layout-sidebar-font-color;
}
.ant-menu-submenu-title:hover {
color: #fff;
background: #0000000a;
color: @layout-sidebar-font-color;
background: @layout-sidebar-selected-color;
font-weight: 600;
.ant-menu-submenu-arrow::before,
.ant-menu-submenu-arrow::after {
background: #fff;
background-image: linear-gradient(to right, #2e2e2e, #2e2e2e);
background: @layout-sidebar-arrow-color;
}
}
.ant-menu-submenu-selected {
> .ant-menu-submenu-title {
color: #fff;
color: @layout-sidebar-font-color;
font-weight: 800;
}
}
@@ -650,7 +626,7 @@ body {
padding-left: 0 !important;
> a {
padding-left: 10px;
color: rgba(255, 255, 255, 0.6) !important;
color: @layout-sidebar-disabled-font-color !important;
font-size: 12px;
}
&:hover {
@@ -659,8 +635,11 @@ body {
}
.ant-menu-submenu-arrow::after,
.ant-menu-submenu-arrow::before {
background: rgba(255, 255, 255, 0.6);
background-image: linear-gradient(to right, rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0.6)) !important;
background: @layout-sidebar-arrow-color;
}
.ant-menu-item.ant-menu-item-active:hover {
background: @layout-sidebar-selected-color;
}
}
// 侧边栏折叠时
@@ -668,7 +647,7 @@ body {
.ant-menu-submenu.ant-menu-submenu-placement-rightTop {
> .ant-menu {
// background: url('../assets/sidebar_background.png');
background: #1d264c;
background: @layout-sidebar-color;
background-position-x: center;
background-position-y: center;
background-repeat: no-repeat !important;
@@ -830,14 +809,21 @@ body {
.el-input__inner {
border-radius: 2px !important;
}
.el-input__inner:hover {
border-color: #4596de !important;
.el-input__inner:hover,
.el-select .el-input.is-focus .el-input__inner,
.el-select .el-input__inner:focus,
.el-button.is-plain:focus,
.el-button.is-plain:hover,
.el-input.is-active .el-input__inner,
.el-input__inner:focus {
border-color: @primary-color !important;
}
.el-select .el-input.is-focus .el-input__inner {
border-color: #4596de !important;
}
.el-select-dropdown__item.selected {
color: #4596de !important;
.el-button--text,
.el-select-dropdown__item.selected,
.el-button.is-plain:focus,
.el-button.is-plain:hover {
color: @primary-color !important;
}
.ant-tabs-nav .ant-tabs-tab {
@@ -866,7 +852,7 @@ body {
.ant-layout-sider {
box-shadow: none;
.ant-layout-sider-children {
background: #custom_colors[color_2];
background: @primary-color_5;
.ant-menu {
display: none;
}
@@ -897,7 +883,7 @@ body {
}
}
.custom-vue-treeselect__control(@bgColor:#f0f5ff,@border:none) {
.custom-vue-treeselect__control(@bgColor:@primary-color_5,@border:none) {
background-color: @bgColor;
border: @border;
}
@@ -916,6 +902,9 @@ body {
border: none;
box-shadow: 0px 4px 6px rgba(78, 94, 160, 0.25) !important;
}
.vue-treeselect__limit-tip-text {
margin: 0;
}
}
// 自定义背景颜色和border
@@ -937,30 +926,30 @@ body {
}
.vue-treeselect__option--highlight,
.vue-treeselect__option--selected {
color: #custom_colors[color_1];
background-color: #f0f5ff !important;
color: @primary-color;
background-color: @primary-color_5 !important;
}
.vue-treeselect__checkbox--checked,
.vue-treeselect__checkbox--indeterminate {
border-color: #2f54eb !important;
background: #2f54eb !important;
border-color: @primary-color !important;
background: @primary-color !important;
}
.vue-treeselect__label-container:hover {
.vue-treeselect__checkbox--checked,
.vue-treeselect__checkbox--indeterminate {
border-color: #2f54eb !important;
background: #2f54eb !important;
border-color: @primary-color !important;
background: @primary-color !important;
}
}
.vue-treeselect__multi-value-item {
background: #f0f5ff !important;
color: #2f54eb !important;
background: @primary-color_5 !important;
color: @primary-color !important;
}
.vue-treeselect__value-remove {
color: #2f54eb !important;
color: @primary-color !important;
}
.vue-treeselect__label-container:hover .vue-treeselect__checkbox--unchecked {
border-color: #2f54eb !important;
border-color: @primary-color !important;
}
//表格样式
@@ -970,7 +959,7 @@ body {
border: none !important;
}
.vxe-table--header-wrapper {
background-color: #f0f5ff !important;
background-color: @primary-color_5 !important;
}
.vxe-header--row .vxe-header--column:hover {
background: #2f54eb1f !important;
@@ -994,7 +983,7 @@ body {
border: none !important;
}
.vxe-table--header-wrapper {
background-color: #f0f5ff !important;
background-color: @primary-color_5 !important;
}
// .vxe-table--header-wrapper.body--wrapper {
// border-radius: 8px !important;
@@ -1028,12 +1017,12 @@ body {
.ops-input {
.ant-input,
.ant-time-picker-input {
background-color: #custom_colors[color_2];
background-color: @primary-color_5;
border: none;
}
}
.ops-input.ant-input {
background-color: #custom_colors[color_2];
background-color: @primary-color_5;
border: none;
}
.ops-input.ant-input[disabled] {
@@ -1088,14 +1077,40 @@ body {
.vxe-table--render-default .vxe-cell--checkbox:not(.is--disabled):hover .vxe-checkbox--icon,
.is--filter-active .vxe-cell--filter .vxe-filter--btn,
.vxe-table .vxe-sort--asc-btn.sort--active,
.vxe-table .vxe-sort--desc-btn.sort--active {
color: #2f54eb !important;
.vxe-table .vxe-sort--desc-btn.sort--active,
.vxe-select-option.is--selected,
.vxe-loading > .vxe-loading--chunk,
.vxe-loading > .vxe-loading--warpper,
.vxe-pager .vxe-pager--jump-next:not(.is--disabled).is--active,
.vxe-pager .vxe-pager--jump-next:not(.is--disabled):focus,
.vxe-pager .vxe-pager--jump-prev:not(.is--disabled).is--active,
.vxe-pager .vxe-pager--jump-prev:not(.is--disabled):focus,
.vxe-pager .vxe-pager--next-btn:not(.is--disabled).is--active,
.vxe-pager .vxe-pager--next-btn:not(.is--disabled):focus,
.vxe-pager .vxe-pager--num-btn:not(.is--disabled).is--active,
.vxe-pager .vxe-pager--num-btn:not(.is--disabled):focus,
.vxe-pager .vxe-pager--prev-btn:not(.is--disabled).is--active,
.vxe-pager .vxe-pager--prev-btn:not(.is--disabled):focus,
.vxe-button.type--text:not(.is--disabled):hover,
.vxe-table--filter-footer > button:hover {
color: @primary-color !important;
}
.vxe-cell .vxe-default-input:focus,
.vxe-cell .vxe-default-select:focus,
.vxe-cell .vxe-default-textarea:focus,
.vxe-table--filter-wrapper .vxe-default-input:focus,
.vxe-table--filter-wrapper .vxe-default-select:focus,
.vxe-table--filter-wrapper .vxe-default-textarea:focus,
.vxe-select.is--active:not(.is--filter) > .vxe-input .vxe-input--inner,
.vxe-input:not(.is--disabled).is--active .vxe-input--inner {
border-color: @primary-color !important;
}
//批量操作
.ops-list-batch-action {
display: inline-block;
background-color: #custom_colors[color_2];
background-color: @primary-color_5;
font-size: 12px;
color: rgba(0, 0, 0, 0.55);
> span {
@@ -1103,11 +1118,11 @@ body {
padding: 4px 8px;
cursor: pointer;
&:hover {
color: #custom_colors[color_1];
color: @primary-color;
}
}
> span:last-child {
color: rgba(47, 84, 235, 0.55);
color: @primary-color;
cursor: default;
}
}
@@ -1116,27 +1131,28 @@ body {
.ops-tab.ant-tabs.ant-tabs-card {
.ant-tabs-card-bar {
margin: 0;
border-bottom: none;
.ant-tabs-nav-container {
background-color: #custom_colors[color_2];
.ant-tabs-tab {
border: none;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
background: rgba(255, 255, 255, 0.5);
background: @primary-color_6;
margin-right: 5px;
}
}
.ant-tabs-tab-active {
background: #fff !important;
background: #fff;
}
}
}
}
//button
.ops-button-primary {
background-color: #custom_colors[color_2];
border-color: #custom_colors[color_2];
color: #custom_colors[color_1];
background-color: @primary-color_4;
border-color: @primary-color_4;
color: @primary-color;
box-shadow: none;
}
//select
@@ -1158,8 +1174,8 @@ body {
}
}
.ant-select-selection {
background-color: #custom_colors[color_2];
border-color: #custom_colors[color_2];
background-color: @primary-color_5;
border-color: @primary-color_5;
}
}
@@ -1167,8 +1183,8 @@ body {
.ops-dropdown {
.ant-dropdown-menu-item:hover,
.ant-dropdown-menu-submenu-title:hover {
background-color: #custom_colors[color_2];
color: #custom_colors[color_1];
background-color: @primary-color_5;
color: @primary-color;
}
}
@@ -1180,7 +1196,7 @@ body {
border-bottom: none;
.ant-modal-title {
padding-left: 10px;
border-left: 4px solid #custom_colors[color_1];
border-left: 4px solid @primary-color;
}
}
.ant-modal-footer {
@@ -1193,7 +1209,7 @@ body {
width: 276px;
}
.ant-tooltip-inner {
background-color: #custom_colors[color_3];
background-color: @primary-color_3;
border-radius: '4px';
display: flex;
align-items: flex-start;
@@ -1208,7 +1224,7 @@ body {
.ant-tooltip-arrow::before {
width: 7px;
height: 7px;
background-color: #custom_colors[color_3];
background-color: @primary-color_3;
}
}
@@ -1218,7 +1234,7 @@ body {
.el-tabs__header {
border-bottom: none;
background-color: #f0f5ff;
background-color: @primary-color_5;
border-radius: 8px 8px 0px 0px;
}
@@ -1229,7 +1245,7 @@ body {
.el-tabs__header .el-tabs__item.is-active {
background-color: white;
color: #custom_colors[color_1];
color: @primary-color;
}
.el-tabs__header .el-tabs__item:first-child.is-active {
border-top-left-radius: 8px;
@@ -1240,12 +1256,12 @@ body {
}
.el-radio__input.is-checked .el-radio__inner {
background-color: #custom_colors[color_1];
border-color: #custom_colors[color_1];
background-color: @primary-color;
border-color: @primary-color;
}
.el-radio__input.is-checked + .el-radio__label {
color: #custom_colors[color_1];
color: @primary-color;
}
.el-tab-pane {
@@ -1283,14 +1299,14 @@ body {
// a-drop-down
.ant-dropdown-menu-item-active {
color: #custom_colors[color_1];
color: @primary-color;
}
.ant-tag {
&.ops-perm-tag {
border: none;
background-color: #custom_colors[color_2];
color: #custom_colors[color_1];
background-color: @primary-color_5;
color: @primary-color;
}
}
@@ -1303,6 +1319,15 @@ body {
}
}
// json editor
.jsoneditor-vue {
div.jsoneditor {
border: none;
}
div.jsoneditor-menu {
border-bottom-color: @primary-color;
}
}
// .ant-menu.ant-menu-light {
// &.ops-menu {
// background-color: white;

View File

@@ -1,7 +1,22 @@
@border-radius-base: 2px; // 组件/浮层圆角
@primary-color: #2f54eb; // 全局主色
@border-radius-box: 4px; // big box radius
@primary-color: #2f54eb; // 全局主色 六大品牌色
@primary-color_2: #7f97fa;
@primary-color_3: #d3e3fd;
@primary-color_4: #e1efff;
@primary-color_5: #f0f5ff;
@primary-color_6: #f9fbff;
@text-color_1: #1d2119;
@text-color_2: #4e5969;
@text-color_3: #86909c;
@text-color_4: #a5a9bc;
@border-color: #e4e7ed;
@scrollbar-color: rgba(47, 122, 235, 0.2);
// @layout-header-background: #1a3652;
@layout-header-background: #fff;
@layout-header-height: 40px;
@layout-header-icon-height: 34px;
@@ -15,13 +30,20 @@
@layout-background-light-color: #fafafa;
@layout-background-light-color-light: #f0f0f0;
@layout-sidebar-color: #1d264c; //bg
@layout-sidebar-sub-color: #000c37; //bg
@layout-sidebar-selected-color: #3e4a71; //selected bg
@layout-sidebar-arrow-color: rgba(255, 255, 255, 0.6);
@layout-sidebar-font-color: #fff;
@layout-sidebar-disabled-font-color: rgba(255, 255, 255, 0.6);
#custom_colors() {
color_1: #2f54eb;
color_2: #f0f5ff;
color_3: #D2E2FF;
color_1: #2f54eb; //primary color
color_2: #f0f5ff; //light background color
color_3: #d2e2ff;
}
.ops_display_wrapper(@backgroundColor:#f0f5ff) {
.ops_display_wrapper(@backgroundColor:@primary-color_5) {
cursor: pointer;
padding: 5px 8px;
background-color: @backgroundColor;
@@ -35,10 +57,10 @@
cursor: pointer;
padding: 5px 10px;
&:hover {
background-color: #custom_colors[color_2];
background-color: @primary-color_5;
}
}
.ops_popover_item_selected() {
background-color: #custom_colors[color_2];
color: #custom_colors[color_1];
background-color: @primary-color_5;
color: @primary-color;
}

View File

@@ -76,7 +76,8 @@ const AppDeviceEnquire = {
const mixinPermissions = {
computed: {
...mapState({
detailPermissions: state => state.user.detailPermissions
detailPermissions: state => state.user.detailPermissions,
roles: state => state.user.roles
})
},
methods: {
@@ -85,7 +86,7 @@ const mixinPermissions = {
hasDetailPermission(appName, resourceName, perms = []) {
const appNamePer = this.detailPermissions[`${appName}`]
const _findResourcePermissions = appNamePer.find(item => item.name === resourceName)
return _findResourcePermissions.permissions.some(item => perms.includes(item))
return this.roles?.permissions.includes('acl_admin') || this.roles?.permissions.includes('backend_admin') || _findResourcePermissions?.permissions.some(item => perms.includes(item))
}
}
}

View File

@@ -60,34 +60,6 @@
<vxe-column field="mobile" :title="$t('cs.companyStructure.mobile')" min-width="80"></vxe-column>
<vxe-column field="position_name" :title="$t('cs.companyStructure.positionName')" min-width="80"></vxe-column>
<vxe-column field="department_name" :title="$t('cs.companyStructure.departmentName')" min-width="80"></vxe-column>
<vxe-column field="current_company" v-if="useDFC" :title="$t('cs.companyStructure.currentCompany')" min-width="120"></vxe-column>
<vxe-column field="dfc_entry_date" v-if="useDFC" :title="$t('cs.companyStructure.dfcEntryDate')" min-width="120"></vxe-column>
<vxe-column field="entry_date" :title="$t('cs.companyStructure.entryDate')" min-width="120"></vxe-column>
<vxe-column field="is_internship" :title="$t('cs.companyStructure.isInternship')" min-width="120"></vxe-column>
<vxe-column field="leave_date" :title="$t('cs.companyStructure.leaveDate')" min-width="120"></vxe-column>
<vxe-column field="id_card" :title="$t('cs.companyStructure.idCard')" min-width="120"></vxe-column>
<vxe-column field="nation" :title="$t('cs.companyStructure.nation')" min-width="80"></vxe-column>
<vxe-column field="id_place" :title="$t('cs.companyStructure.idPlace')" min-width="80"></vxe-column>
<vxe-column field="party" :title="$t('cs.companyStructure.party')" min-width="80"></vxe-column>
<vxe-column field="household_registration_type" :title="$t('cs.companyStructure.householdRegistrationType')" min-width="80"></vxe-column>
<vxe-column field="hometown" :title="$t('cs.companyStructure.homewtown')" min-width="80"></vxe-column>
<vxe-column field="marry" :title="$t('cs.companyStructure.marry')" min-width="80"></vxe-column>
<vxe-column field="max_degree" :title="$t('cs.companyStructure.maxDegree')" min-width="80"></vxe-column>
<vxe-column field="emergency_person" :title="$t('cs.companyStructure.emergencyPerson')" min-width="120"></vxe-column>
<vxe-column field="emergency_phone" :title="$t('cs.companyStructure.emergencyPhone')" min-width="120"></vxe-column>
<vxe-column field="bank_card_number" :title="$t('cs.companyStructure.bankCardNumber')" min-width="120"></vxe-column>
<vxe-column field="bank_card_name" :title="$t('cs.companyStructure.bankCardName')" min-width="80"></vxe-column>
<vxe-column field="opening_bank" :title="$t('cs.companyStructure.openingBank')" min-width="80"></vxe-column>
<vxe-column field="account_opening_location" :title="$t('cs.companyStructure.accountOpeningLocation')" min-width="120"></vxe-column>
<vxe-column field="school" :title="$t('cs.companyStructure.school')" min-width="80"></vxe-column>
<vxe-column field="major" :title="$t('cs.companyStructure.major')" min-width="80"></vxe-column>
<vxe-column field="education" :title="$t('cs.companyStructure.education')" min-width="80"></vxe-column>
<vxe-column field="graduation_year" :title="$t('cs.companyStructure.graduationYear')" min-width="120"></vxe-column>
<vxe-column field="birth_date" :title="$t('cs.companyStructure.birthDate')" min-width="120"></vxe-column>
<vxe-column field="birth_place" :title="$t('cs.companyStructure.birthPlace')" min-width="120"></vxe-column>
<vxe-column field="nationality_region" :title="$t('cs.companyStructure.nationalityRegion')" min-width="120"></vxe-column>
<vxe-column field="first_entry_date" :title="$t('cs.companyStructure.firstEntryDate')" min-width="120"></vxe-column>
<vxe-column field="estimated_departure_date" :title="$t('cs.companyStructure.estimatedDepartureDate')" min-width="120"></vxe-column>
<vxe-column v-if="has_error" field="err" :title="$t('cs.companyStructure.importFailedReason')" min-width="120" fixed="right">
<template #default="{ row }">
<span :style="{ color: '#D81E06' }">{{ row.err }}</span>
@@ -303,98 +275,6 @@ export default {
v: '部门',
t: 's',
},
{
v: '目前所属主体',
t: 's',
},
{
v: '初始入职日期',
t: 's',
},
{
v: '目前主体入职日期',
t: 's',
},
{
v: '正式/实习生',
t: 's',
},
{
v: '离职日期',
t: 's',
},
{
v: '身份证号码',
t: 's',
},
{
v: '民族',
t: 's',
},
{
v: '籍贯',
t: 's',
},
{
v: '组织关系',
t: 's',
},
{
v: '户籍类型',
t: 's',
},
{
v: '户口所在地',
t: 's',
},
{
v: '婚姻情况',
t: 's',
},
{
v: '最高学历',
t: 's',
},
{
v: '紧急联系人',
t: 's',
},
{
v: '紧急联系电话',
t: 's',
},
{
v: '卡号',
t: 's',
},
{
v: '银行',
t: 's',
},
{
v: '开户行',
t: 's',
},
{
v: '开户地',
t: 's',
},
{
v: '学校',
t: 's',
},
{
v: '专业',
t: 's',
},
{
v: '学历',
t: 's',
},
{
v: '毕业年份',
t: 's',
},
],
]
data[1] = data[1].filter((item) => item['v'] !== '目前所属主体')

View File

@@ -372,7 +372,7 @@ export default {
{ label: this.$t('cs.companyStructure.mobile'), value: 'mobile' },
{ label: this.$t('cs.companyStructure.departmentName'), value: 'department_name' },
{ label: this.$t('cs.companyStructure.positionName'), value: 'position_name' },
{ label: this.$t('cs.companyStructure.departmentDirector'), value: 'direct_supervisor_id' },
{ label: this.$t('cs.companyStructure.supervisor'), value: 'direct_supervisor_id' },
]
},
sceneList () {

View File

@@ -10,11 +10,12 @@
:noOptionsText="$t('cs.components.empty')"
:class="className ? className : 'ops-setting-treeselect'"
value-consists-of="LEAF_PRIORITY"
:limit="20"
:limit="limit"
:limitText="(count) => `+ ${count}`"
v-bind="$attrs"
appendToBody
:zIndex="1050"
:flat="flat"
>
</treeselect>
</template>
@@ -60,6 +61,14 @@ export default {
type: String,
default: 'employee_id',
},
limit: {
type: Number,
default: 20,
},
flat: {
type: Boolean,
default: false,
},
},
data() {
return {}

View File

@@ -139,7 +139,7 @@ export default {
console.log('login form', values)
const loginParams = { ...values }
delete loginParams.username
loginParams[!state.loginType ? 'email' : 'username'] = values.username
loginParams.username = values.username
loginParams.password = appConfig.useEncryption ? md5(values.password) : values.password
localStorage.setItem('ops_auth_type', '')
Login({ userInfo: loginParams })

View File

@@ -18,7 +18,7 @@ module.exports = {
// TODO 需要增加根据环境不开启主题需求
new ThemeColorReplacer({
fileName: 'css/theme-colors-[contenthash:8].css',
matchColors: getAntdSerials('#1890ff'), // 主色系列
matchColors: getAntdSerials('#2f54eb'), // 主色系列
// 改变样式选择器,解决样式覆盖问题
changeSelector(selector) {
switch (selector) {
@@ -83,11 +83,9 @@ module.exports = {
less: {
modifyVars: {
/* less 变量覆盖,用于自定义 ant design 主题 */
/*
'primary-color': '#F5222D',
'link-color': '#F5222D',
'border-radius-base': '4px',
*/
'primary-color': '#2f54eb',
// 'link-color': '#F5222D',
// 'border-radius-base': '4px',
},
javascriptEnabled: true,
},

View File

@@ -33,7 +33,7 @@ services:
- redis
cmdb-api:
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.3.10
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.3.13
# build:
# context: .
# target: cmdb-api
@@ -57,6 +57,8 @@ services:
nohup flask cmdb-trigger > trigger.log 2>&1 &
flask cmdb-init-cache
flask cmdb-init-acl
flask init-import-user-from-acl
flask init-department
flask cmdb-counter > counter.log 2>&1
depends_on:
@@ -68,7 +70,7 @@ services:
- cmdb-api
cmdb-ui:
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.3.10
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.3.13
# build:
# context: .
# target: cmdb-ui

File diff suppressed because one or more lines are too long

View File

@@ -13,7 +13,7 @@
**set database in config file cmdb-api/settings.py**
- In cmdb directory,start in order as follows:
- enviroment: `make env`
- environment: `make env`
- start API: `make api`
- start UI: `make ui`
- start worker: `make worker`