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,16 +278,16 @@ 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:
return int(max_v.value) + 1
return 1
return 1
@classmethod
def add(cls, ci_type_name,
@@ -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 = 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))
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:
return abort(403, ErrFormat.ci_filter_perm_attr_no_permission.format(k))
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:
return abort(403, ErrFormat.ci_filter_perm_attr_no_permission.format(k))
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)
result = list()
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
if not has_config_perm:
attr_dict.pop('choice_web_hook', None)
attr_dict.pop('choice_other', None)
parent_ids = CITypeInheritanceManager.base(type_id)
result.append(attr_dict)
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))
from_group = CITypeAttributeGroup.get_by_id(_from)
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)))
to_group = CITypeAttributeGroup.get_by_id(_to)
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,34 +163,67 @@ 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)
obj = self.cls.get_by(type_id=kwargs.get('type_id'),
rid=kwargs.get('rid'),
first=True, to_dict=False)
if obj is not None:
obj = obj.update(filter_none=False, **kwargs)
if not obj.attr_filter and not obj.ci_filter:
if current_app.config.get('USE_ACL'):
ACLManager().del_resource(str(obj.id), ResourceTypeEnum.CI_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']
obj.soft_delete()
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 and not obj.id_filter:
if current_app.config.get('USE_ACL'):
ACLManager().del_resource(str(obj.id), ResourceTypeEnum.CI_FILTER, rebuild=False)
obj.soft_delete()
if not is_recursive and request_id_filter:
self._revoke_children(obj.rid, request_id_filter, rebuild=False)
else:
if not kwargs.get('ci_filter') and not kwargs.get('attr_filter'):
return
obj = self.cls.create(**kwargs)
else:
if not kwargs.get('ci_filter') and not kwargs.get('attr_filter') and not kwargs.get('id_filter'):
return
if current_app.config.get('USE_ACL'):
try:
ACLManager().add_resource(obj.id, ResourceTypeEnum.CI_FILTER)
except:
pass
ACLManager().grant_resource_to_role_by_rid(obj.id,
kwargs.get('rid'),
ResourceTypeEnum.CI_FILTER)
if request_id_filter:
kwargs['id_filter'] = request_id_filter
return obj
obj = self.cls.create(**kwargs)
if current_app.config.get('USE_ACL'): # new resource
try:
ACLManager().add_resource(obj.id, ResourceTypeEnum.CI_FILTER)
except:
pass
ACLManager().grant_resource_to_role_by_rid(obj.id,
kwargs.get('rid'),
ResourceTypeEnum.CI_FILTER)
return obj
def _can_update(self, **kwargs):
pass
@@ -138,19 +232,84 @@ class CIFilterPermsCRUD(DBMixin):
pass
def delete(self, **kwargs):
obj = self.cls.get_by(type_id=kwargs.get('type_id'),
rid=kwargs.get('rid'),
first=True, to_dict=False)
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:
resource = None
if current_app.config.get('USE_ACL'):
resource = ACLManager().del_resource(str(obj.id), ResourceTypeEnum.CI_FILTER)
obj.soft_delete()
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']
if obj is not None:
resource = None
if current_app.config.get('USE_ACL'):
resource = ACLManager().del_resource(str(obj.id), ResourceTypeEnum.CI_FILTER)
if obj is not None:
obj.soft_delete()
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['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['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
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 = 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]
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,21 +49,20 @@ 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 != old_department.acl_rid:
old_department.update(
acl_rid=old_d_rid_in_acl
)
d_acl_rid = old_department.acl_rid if old_d_rid_in_acl == old_department.acl_rid else old_d_rid_in_acl
payload = {
'app_id': 'acl',
'parent_id': d_acl_rid,
}
try:
acl.remove_user_from_role(employee_acl_rid, payload)
except Exception as e:
result.append(ErrFormat.acl_remove_user_from_role_failed.format(str(e)))
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
)
d_acl_rid = old_department.acl_rid if old_d_rid_in_acl == old_department.acl_rid else old_d_rid_in_acl
payload = {
'app_id': 'acl',
'parent_id': d_acl_rid,
}
try:
acl.remove_user_from_role(employee_acl_rid, payload)
except Exception as e:
result.append(ErrFormat.acl_remove_user_from_role_failed.format(str(e)))
payload = {
'app_id': '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))
acl.grant_resource_to_role_by_rid(type_name, rid, ResourceTypeEnum.CI_TYPE, perms, rebuild=False)
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

@@ -1,41 +1,41 @@
import i18n from '@/lang'
export const ruleTypeList = () => {
return [
{ value: 'and', label: i18n.t('cmdbFilterComp.and') },
{ value: 'or', label: i18n.t('cmdbFilterComp.or') },
// { value: 'not', label: '非' },
]
}
export const expList = () => {
return [
{ value: 'is', label: i18n.t('cmdbFilterComp.is') },
{ value: '~is', label: i18n.t('cmdbFilterComp.~is') },
{ value: 'contain', label: i18n.t('cmdbFilterComp.contain') },
{ value: '~contain', label: i18n.t('cmdbFilterComp.~contain') },
{ value: 'start_with', label: i18n.t('cmdbFilterComp.start_with') },
{ value: '~start_with', label: i18n.t('cmdbFilterComp.~start_with') },
{ value: 'end_with', label: i18n.t('cmdbFilterComp.end_with') },
{ value: '~end_with', label: i18n.t('cmdbFilterComp.~end_with') },
{ value: '~value', label: i18n.t('cmdbFilterComp.~value') }, // 为空的定义有点绕
{ value: 'value', label: i18n.t('cmdbFilterComp.value') },
]
}
export const advancedExpList = () => {
return [
{ value: 'in', label: i18n.t('cmdbFilterComp.in') },
{ value: '~in', label: i18n.t('cmdbFilterComp.~in') },
{ value: 'range', label: i18n.t('cmdbFilterComp.range') },
{ value: '~range', label: i18n.t('cmdbFilterComp.~range') },
{ value: 'compare', label: i18n.t('cmdbFilterComp.compare') },
]
}
export const compareTypeList = [
{ value: '1', label: '>' },
{ value: '2', label: '>=' },
{ value: '3', label: '<' },
{ value: '4', label: '<=' },
]
import i18n from '@/lang'
export const ruleTypeList = () => {
return [
{ value: 'and', label: i18n.t('cmdbFilterComp.and') },
{ value: 'or', label: i18n.t('cmdbFilterComp.or') },
// { value: 'not', label: '非' },
]
}
export const expList = () => {
return [
{ value: 'is', label: i18n.t('cmdbFilterComp.is') },
{ value: '~is', label: i18n.t('cmdbFilterComp.~is') },
{ value: 'contain', label: i18n.t('cmdbFilterComp.contain') },
{ value: '~contain', label: i18n.t('cmdbFilterComp.~contain') },
{ value: 'start_with', label: i18n.t('cmdbFilterComp.start_with') },
{ value: '~start_with', label: i18n.t('cmdbFilterComp.~start_with') },
{ value: 'end_with', label: i18n.t('cmdbFilterComp.end_with') },
{ value: '~end_with', label: i18n.t('cmdbFilterComp.~end_with') },
{ value: '~value', label: i18n.t('cmdbFilterComp.~value') }, // 为空的定义有点绕
{ value: 'value', label: i18n.t('cmdbFilterComp.value') },
]
}
export const advancedExpList = () => {
return [
{ value: 'in', label: i18n.t('cmdbFilterComp.in') },
{ value: '~in', label: i18n.t('cmdbFilterComp.~in') },
{ value: 'range', label: i18n.t('cmdbFilterComp.range') },
{ value: '~range', label: i18n.t('cmdbFilterComp.~range') },
{ value: 'compare', label: i18n.t('cmdbFilterComp.compare') },
]
}
export const compareTypeList = [
{ value: '1', label: '>' },
{ value: '2', label: '>=' },
{ value: '3', label: '<' },
{ value: '4', label: '<=' },
]

View File

@@ -1,332 +1,346 @@
<template>
<div>
<a-space :style="{ display: 'flex', marginBottom: '10px' }" v-for="(item, index) in ruleList" :key="item.id">
<div :style="{ width: '70px', height: '24px', position: 'relative' }">
<treeselect
v-if="index"
class="custom-treeselect"
:style="{ width: '70px', '--custom-height': '24px', position: 'absolute', top: '-17px', left: 0 }"
v-model="item.type"
:multiple="false"
:clearable="false"
searchable
:options="ruleTypeList"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
>
</treeselect>
</div>
<treeselect
class="custom-treeselect"
:style="{ width: '130px', '--custom-height': '24px' }"
v-model="item.property"
:multiple="false"
:clearable="false"
searchable
:options="canSearchPreferenceAttrList"
:normalizer="
(node) => {
return {
id: node.name,
label: node.alias || node.name,
children: node.children,
}
}
"
appendToBody
:zIndex="1050"
>
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
<ValueTypeMapIcon :attr="node.raw" />
{{ node.label }}
</div>
<div
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
slot="value-label"
slot-scope="{ node }"
>
<ValueTypeMapIcon :attr="node.raw" /> {{ node.label }}
</div>
</treeselect>
<treeselect
class="custom-treeselect"
:style="{ width: '100px', '--custom-height': '24px' }"
v-model="item.exp"
:multiple="false"
:clearable="false"
searchable
:options="[...getExpListByProperty(item.property), ...advancedExpList]"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
@select="(value) => handleChangeExp(value, item, index)"
appendToBody
:zIndex="1050"
>
</treeselect>
<treeselect
class="custom-treeselect"
:style="{ width: '175px', '--custom-height': '24px' }"
v-model="item.value"
:multiple="false"
:clearable="false"
searchable
v-if="isChoiceByProperty(item.property) && (item.exp === 'is' || item.exp === '~is')"
:options="getChoiceValueByProperty(item.property)"
:placeholder="$t('placeholder2')"
:normalizer="
(node) => {
return {
id: node[0],
label: node[0],
children: node.children,
}
}
"
appendToBody
:zIndex="1050"
>
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
{{ node.label }}
</div>
</treeselect>
<a-input-group
size="small"
compact
v-else-if="item.exp === 'range' || item.exp === '~range'"
:style="{ width: '175px' }"
>
<a-input
class="ops-input"
size="small"
v-model="item.min"
:style="{ width: '78px' }"
:placeholder="$t('min')"
/>
~
<a-input
class="ops-input"
size="small"
v-model="item.max"
:style="{ width: '78px' }"
:placeholder="$t('max')"
/>
</a-input-group>
<a-input-group size="small" compact v-else-if="item.exp === 'compare'" :style="{ width: '175px' }">
<treeselect
class="custom-treeselect"
:style="{ width: '60px', '--custom-height': '24px' }"
v-model="item.compareType"
:multiple="false"
:clearable="false"
searchable
:options="compareTypeList"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
appendToBody
:zIndex="1050"
>
</treeselect>
<a-input class="ops-input" v-model="item.value" size="small" style="width: 113px" />
</a-input-group>
<a-input
v-else-if="item.exp !== 'value' && item.exp !== '~value'"
size="small"
v-model="item.value"
:placeholder="item.exp === 'in' || item.exp === '~in' ? $t('cmdbFilterComp.split', { separator: ';' }) : ''"
class="ops-input"
:style="{ width: '175px' }"
></a-input>
<div v-else :style="{ width: '175px' }"></div>
<a-tooltip :title="$t('copy')">
<a class="operation" @click="handleCopyRule(item)"><ops-icon type="icon-xianxing-copy"/></a>
</a-tooltip>
<a-tooltip :title="$t('delete')">
<a class="operation" @click="handleDeleteRule(item)"><ops-icon type="icon-xianxing-delete"/></a>
</a-tooltip>
<a-tooltip :title="$t('cmdbFilterComp.addHere')" v-if="needAddHere">
<a class="operation" @click="handleAddRuleAt(item)"><a-icon type="plus-circle"/></a>
</a-tooltip>
</a-space>
<div class="table-filter-add">
<a @click="handleAddRule">+ {{ $t('new') }}</a>
</div>
</div>
</template>
<script>
import _ from 'lodash'
import { v4 as uuidv4 } from 'uuid'
import { ruleTypeList, expList, advancedExpList, compareTypeList } from './constants'
import ValueTypeMapIcon from '../CMDBValueTypeMapIcon'
export default {
name: 'Expression',
components: { ValueTypeMapIcon },
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: Array,
default: () => [],
},
canSearchPreferenceAttrList: {
type: Array,
required: true,
default: () => [],
},
needAddHere: {
type: Boolean,
default: false,
},
},
data() {
return {
compareTypeList,
}
},
computed: {
ruleList: {
get() {
return this.value
},
set(val) {
this.$emit('change', val)
return val
},
},
ruleTypeList() {
return ruleTypeList()
},
expList() {
return expList()
},
advancedExpList() {
return advancedExpList()
},
},
methods: {
getExpListByProperty(property) {
if (property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find && ['0', '1', '3', '4', '5'].includes(_find.value_type)) {
return [
{ value: 'is', label: this.$t('cmdbFilterComp.is') },
{ value: '~is', label: this.$t('cmdbFilterComp.~is') },
{ value: '~value', label: this.$t('cmdbFilterComp.~value') }, // 为空的定义有点绕
{ value: 'value', label: this.$t('cmdbFilterComp.value') },
]
}
return this.expList
}
return this.expList
},
isChoiceByProperty(property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find) {
return _find.is_choice
}
return false
},
handleAddRule() {
this.ruleList.push({
id: uuidv4(),
type: 'and',
property: this.canSearchPreferenceAttrList[0]?.name,
exp: 'is',
value: null,
})
this.$emit('change', this.ruleList)
},
handleCopyRule(item) {
this.ruleList.push({ ...item, id: uuidv4() })
this.$emit('change', this.ruleList)
},
handleDeleteRule(item) {
const idx = this.ruleList.findIndex((r) => r.id === item.id)
if (idx > -1) {
this.ruleList.splice(idx, 1)
}
this.$emit('change', this.ruleList)
},
handleAddRuleAt(item) {
const idx = this.ruleList.findIndex((r) => r.id === item.id)
if (idx > -1) {
this.ruleList.splice(idx, 0, {
id: uuidv4(),
type: 'and',
property: this.canSearchPreferenceAttrList[0]?.name,
exp: 'is',
value: null,
})
}
this.$emit('change', this.ruleList)
},
getChoiceValueByProperty(property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find) {
return _find.choice_value
}
return []
},
handleChangeExp({ value }, item, index) {
const _ruleList = _.cloneDeep(this.ruleList)
if (value === 'range') {
_ruleList[index] = {
..._ruleList[index],
min: '',
max: '',
exp: value,
}
} else if (value === 'compare') {
_ruleList[index] = {
..._ruleList[index],
compareType: '1',
exp: value,
}
} else {
_ruleList[index] = {
..._ruleList[index],
exp: value,
}
}
this.ruleList = _ruleList
this.$emit('change', this.ruleList)
},
},
}
</script>
<style></style>
<template>
<div>
<a-space :style="{ display: 'flex', marginBottom: '10px' }" v-for="(item, index) in ruleList" :key="item.id">
<div :style="{ width: '70px', height: '24px', position: 'relative' }">
<treeselect
v-if="index"
class="custom-treeselect"
:style="{ width: '70px', '--custom-height': '24px', position: 'absolute', top: '-17px', left: 0 }"
v-model="item.type"
:multiple="false"
:clearable="false"
searchable
:options="ruleTypeList"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
:disabled="disabled"
>
</treeselect>
</div>
<treeselect
class="custom-treeselect"
:style="{ width: '130px', '--custom-height': '24px' }"
v-model="item.property"
:multiple="false"
:clearable="false"
searchable
:options="canSearchPreferenceAttrList"
:normalizer="
(node) => {
return {
id: node.name,
label: node.alias || node.name,
children: node.children,
}
}
"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
<ValueTypeMapIcon :attr="node.raw" />
{{ node.label }}
</div>
<div
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
slot="value-label"
slot-scope="{ node }"
>
<ValueTypeMapIcon :attr="node.raw" /> {{ node.label }}
</div>
</treeselect>
<treeselect
class="custom-treeselect"
:style="{ width: '100px', '--custom-height': '24px' }"
v-model="item.exp"
:multiple="false"
:clearable="false"
searchable
:options="[...getExpListByProperty(item.property), ...advancedExpList]"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
@select="(value) => handleChangeExp(value, item, index)"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
</treeselect>
<treeselect
class="custom-treeselect"
:style="{ width: '175px', '--custom-height': '24px' }"
v-model="item.value"
:multiple="false"
:clearable="false"
searchable
v-if="isChoiceByProperty(item.property) && (item.exp === 'is' || item.exp === '~is')"
:options="getChoiceValueByProperty(item.property)"
:placeholder="$t('placeholder2')"
:normalizer="
(node) => {
return {
id: node[0],
label: node[0],
children: node.children,
}
}
"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
{{ node.label }}
</div>
</treeselect>
<a-input-group
size="small"
compact
v-else-if="item.exp === 'range' || item.exp === '~range'"
:style="{ width: '175px' }"
>
<a-input
class="ops-input"
size="small"
v-model="item.min"
:style="{ width: '78px' }"
:placeholder="$t('min')"
:disabled="disabled"
/>
~
<a-input
class="ops-input"
size="small"
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' }">
<treeselect
class="custom-treeselect"
:style="{ width: '60px', '--custom-height': '24px' }"
v-model="item.compareType"
:multiple="false"
:clearable="false"
searchable
:options="compareTypeList"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
</treeselect>
<a-input class="ops-input" v-model="item.value" size="small" style="width: 113px" />
</a-input-group>
<a-input
v-else-if="item.exp !== 'value' && item.exp !== '~value'"
size="small"
v-model="item.value"
: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>
<a-tooltip :title="$t('delete')">
<a class="operation" @click="handleDeleteRule(item)"><ops-icon type="icon-xianxing-delete"/></a>
</a-tooltip>
<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" v-if="!disabled">
<a @click="handleAddRule">+ {{ $t('new') }}</a>
</div>
</div>
</template>
<script>
import _ from 'lodash'
import { v4 as uuidv4 } from 'uuid'
import { ruleTypeList, expList, advancedExpList, compareTypeList } from './constants'
import ValueTypeMapIcon from '../CMDBValueTypeMapIcon'
export default {
name: 'Expression',
components: { ValueTypeMapIcon },
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: Array,
default: () => [],
},
canSearchPreferenceAttrList: {
type: Array,
required: true,
default: () => [],
},
needAddHere: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
},
data() {
return {
compareTypeList,
}
},
computed: {
ruleList: {
get() {
return this.value
},
set(val) {
this.$emit('change', val)
return val
},
},
ruleTypeList() {
return ruleTypeList()
},
expList() {
return expList()
},
advancedExpList() {
return advancedExpList()
},
},
methods: {
getExpListByProperty(property) {
if (property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find && ['0', '1', '3', '4', '5'].includes(_find.value_type)) {
return [
{ value: 'is', label: this.$t('cmdbFilterComp.is') },
{ value: '~is', label: this.$t('cmdbFilterComp.~is') },
{ value: '~value', label: this.$t('cmdbFilterComp.~value') }, // 为空的定义有点绕
{ value: 'value', label: this.$t('cmdbFilterComp.value') },
]
}
return this.expList
}
return this.expList
},
isChoiceByProperty(property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find) {
return _find.is_choice
}
return false
},
handleAddRule() {
this.ruleList.push({
id: uuidv4(),
type: 'and',
property: this.canSearchPreferenceAttrList[0]?.name,
exp: 'is',
value: null,
})
this.$emit('change', this.ruleList)
},
handleCopyRule(item) {
this.ruleList.push({ ...item, id: uuidv4() })
this.$emit('change', this.ruleList)
},
handleDeleteRule(item) {
const idx = this.ruleList.findIndex((r) => r.id === item.id)
if (idx > -1) {
this.ruleList.splice(idx, 1)
}
this.$emit('change', this.ruleList)
},
handleAddRuleAt(item) {
const idx = this.ruleList.findIndex((r) => r.id === item.id)
if (idx > -1) {
this.ruleList.splice(idx, 0, {
id: uuidv4(),
type: 'and',
property: this.canSearchPreferenceAttrList[0]?.name,
exp: 'is',
value: null,
})
}
this.$emit('change', this.ruleList)
},
getChoiceValueByProperty(property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find) {
return _find.choice_value
}
return []
},
handleChangeExp({ value }, item, index) {
const _ruleList = _.cloneDeep(this.ruleList)
if (value === 'range') {
_ruleList[index] = {
..._ruleList[index],
min: '',
max: '',
exp: value,
}
} else if (value === 'compare') {
_ruleList[index] = {
..._ruleList[index],
compareType: '1',
exp: value,
}
} else {
_ruleList[index] = {
..._ruleList[index],
exp: value,
}
}
this.ruleList = _ruleList
this.$emit('change', this.ruleList)
},
},
}
</script>
<style></style>

View File

@@ -1,296 +1,302 @@
<template>
<div>
<a-popover
v-if="isDropdown"
v-model="visible"
trigger="click"
:placement="placement"
overlayClassName="table-filter"
@visibleChange="visibleChange"
>
<slot name="popover_item">
<a-button type="primary" ghost>{{ $t('cmdbFilterComp.conditionFilter') }}<a-icon type="filter"/></a-button>
</slot>
<template slot="content">
<Expression
:needAddHere="needAddHere"
v-model="ruleList"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)"
/>
<a-divider :style="{ margin: '10px 0' }" />
<div style="width:554px">
<a-space :style="{ display: 'flex', justifyContent: 'flex-end' }">
<a-button type="primary" size="small" @click="handleSubmit">{{ $t('confirm') }}</a-button>
<a-button size="small" @click="handleClear">{{ $t('clear') }}</a-button>
</a-space>
</div>
</template>
</a-popover>
<Expression
:needAddHere="needAddHere"
v-else
v-model="ruleList"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)"
/>
</div>
</template>
<script>
import { v4 as uuidv4 } from 'uuid'
import Expression from './expression.vue'
import { advancedExpList, compareTypeList } from './constants'
export default {
name: 'FilterComp',
components: { Expression },
props: {
canSearchPreferenceAttrList: {
type: Array,
required: true,
default: () => [],
},
expression: {
type: String,
default: '',
},
regQ: {
type: String,
default: '(?<=q=).+(?=&)|(?<=q=).+$',
},
placement: {
type: String,
default: 'bottomRight',
},
isDropdown: {
type: Boolean,
default: true,
},
needAddHere: {
type: Boolean,
default: false,
},
},
data() {
return {
advancedExpList,
compareTypeList,
visible: false,
ruleList: [],
filterExp: '',
}
},
methods: {
visibleChange(open, isInitOne = true) {
// isInitOne 初始化exp为空时ruleList是否默认给一条
// const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
const exp = this.expression.match(new RegExp(this.regQ, 'g'))
? this.expression.match(new RegExp(this.regQ, 'g'))[0]
: null
if (open && exp) {
const expArray = exp.split(',').map((item) => {
let has_not = ''
const key = item.split(':')[0]
const val = item
.split(':')
.slice(1)
.join(':')
let type, property, exp, value, min, max, compareType
if (key.includes('-')) {
type = 'or'
if (key.includes('~')) {
property = key.substring(2)
has_not = '~'
} else {
property = key.substring(1)
}
} else {
type = 'and'
if (key.includes('~')) {
property = key.substring(1)
has_not = '~'
} else {
property = key
}
}
const in_reg = /(?<=\().+(?=\))/g
const range_reg = /(?<=\[).+(?=\])/g
const compare_reg = /(?<=>=|<=|>(?!=)|<(?!=)).+/
if (val === '*') {
exp = has_not + 'value'
value = ''
} else if (in_reg.test(val)) {
exp = has_not + 'in'
value = val.match(in_reg)[0]
} else if (range_reg.test(val)) {
exp = has_not + 'range'
value = val.match(range_reg)[0]
min = value.split('_TO_')[0]
max = value.split('_TO_')[1]
} else if (compare_reg.test(val)) {
exp = has_not + 'compare'
value = val.match(compare_reg)[0]
const _compareType = val.substring(0, val.match(compare_reg)['index'])
const idx = compareTypeList.findIndex((item) => item.label === _compareType)
compareType = compareTypeList[idx].value
} else if (!val.includes('*')) {
exp = has_not + 'is'
value = val
} else {
const resList = [
['contain', /(?<=\*).*(?=\*)/g],
['end_with', /(?<=\*).+/g],
['start_with', /.+(?=\*)/g],
]
for (let i = 0; i < 3; i++) {
const reg = resList[i]
if (reg[1].test(val)) {
exp = has_not + reg[0]
value = val.match(reg[1])[0]
break
}
}
}
return {
id: uuidv4(),
type,
property,
exp,
value,
min,
max,
compareType,
}
})
this.ruleList = [...expArray]
} else if (open) {
const _canSearchPreferenceAttrList = this.canSearchPreferenceAttrList.filter((attr) => !attr.is_password)
this.ruleList = isInitOne
? [
{
id: uuidv4(),
type: 'and',
property:
_canSearchPreferenceAttrList && _canSearchPreferenceAttrList.length
? _canSearchPreferenceAttrList[0].name
: undefined,
exp: 'is',
value: null,
},
]
: []
}
},
handleClear() {
this.ruleList = [
{
id: uuidv4(),
type: 'and',
property: this.canSearchPreferenceAttrList[0].name,
exp: 'is',
value: null,
},
]
this.filterExp = ''
this.visible = false
this.$emit('setExpFromFilter', this.filterExp)
},
handleSubmit() {
if (this.ruleList && this.ruleList.length) {
this.ruleList[0].type = 'and' // 增删后以防万一第一个不是and
this.filterExp = ''
const expList = this.ruleList.map((rule) => {
let singleRuleExp = ''
let _exp = rule.exp
if (rule.type === 'or') {
singleRuleExp += '-'
}
if (rule.exp.includes('~')) {
singleRuleExp += '~'
_exp = rule.exp.split('~')[1]
}
singleRuleExp += `${rule.property}:`
if (_exp === 'is') {
singleRuleExp += `${rule.value ?? ''}`
}
if (_exp === 'contain') {
singleRuleExp += `*${rule.value ?? ''}*`
}
if (_exp === 'start_with') {
singleRuleExp += `${rule.value ?? ''}*`
}
if (_exp === 'end_with') {
singleRuleExp += `*${rule.value ?? ''}`
}
if (_exp === 'value') {
singleRuleExp += `*`
}
if (_exp === 'in') {
singleRuleExp += `(${rule.value ?? ''})`
}
if (_exp === 'range') {
singleRuleExp += `[${rule.min}_TO_${rule.max}]`
}
if (_exp === 'compare') {
const idx = compareTypeList.findIndex((item) => item.value === rule.compareType)
singleRuleExp += `${compareTypeList[idx].label}${rule.value ?? ''}`
}
return singleRuleExp
})
this.filterExp = expList.join(',')
this.$emit('setExpFromFilter', this.filterExp)
} else {
this.$emit('setExpFromFilter', '')
}
this.visible = false
},
},
}
</script>
<style lang="less" scoped>
.table-filter {
.table-filter-add {
margin-top: 10px;
& > a {
padding: 2px 8px;
&:hover {
background-color: #f0faff;
border-radius: 5px;
}
}
}
.table-filter-extra-icon {
padding: 0px 2px;
&:hover {
display: inline-block;
border-radius: 5px;
background-color: #f0faff;
}
}
}
</style>
<style lang="less">
.table-filter-extra-operation {
.ant-popover-inner-content {
padding: 3px 4px;
.operation {
cursor: pointer;
width: 90px;
height: 30px;
line-height: 30px;
padding: 3px 4px;
border-radius: 5px;
transition: all 0.3s;
&:hover {
background-color: #f0faff;
}
> .anticon {
margin-right: 10px;
}
}
}
}
</style>
<template>
<div>
<a-popover
v-if="isDropdown"
v-model="visible"
trigger="click"
:placement="placement"
overlayClassName="table-filter"
@visibleChange="visibleChange"
>
<slot name="popover_item">
<a-button type="primary" ghost>{{ $t('cmdbFilterComp.conditionFilter') }}<a-icon type="filter"/></a-button>
</slot>
<template slot="content">
<Expression
:needAddHere="needAddHere"
v-model="ruleList"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)"
:disabled="disabled"
/>
<a-divider :style="{ margin: '10px 0' }" />
<div style="width:554px">
<a-space :style="{ display: 'flex', justifyContent: 'flex-end' }">
<a-button type="primary" size="small" @click="handleSubmit">{{ $t('confirm') }}</a-button>
<a-button size="small" @click="handleClear">{{ $t('clear') }}</a-button>
</a-space>
</div>
</template>
</a-popover>
<Expression
:needAddHere="needAddHere"
v-else
v-model="ruleList"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)"
:disabled="disabled"
/>
</div>
</template>
<script>
import { v4 as uuidv4 } from 'uuid'
import Expression from './expression.vue'
import { advancedExpList, compareTypeList } from './constants'
export default {
name: 'FilterComp',
components: { Expression },
props: {
canSearchPreferenceAttrList: {
type: Array,
required: true,
default: () => [],
},
expression: {
type: String,
default: '',
},
regQ: {
type: String,
default: '(?<=q=).+(?=&)|(?<=q=).+$',
},
placement: {
type: String,
default: 'bottomRight',
},
isDropdown: {
type: Boolean,
default: true,
},
needAddHere: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
},
data() {
return {
advancedExpList,
compareTypeList,
visible: false,
ruleList: [],
filterExp: '',
}
},
methods: {
visibleChange(open, isInitOne = true) {
// isInitOne 初始化exp为空时ruleList是否默认给一条
// const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
const exp = this.expression.match(new RegExp(this.regQ, 'g'))
? this.expression.match(new RegExp(this.regQ, 'g'))[0]
: null
if (open && exp) {
const expArray = exp.split(',').map((item) => {
let has_not = ''
const key = item.split(':')[0]
const val = item
.split(':')
.slice(1)
.join(':')
let type, property, exp, value, min, max, compareType
if (key.includes('-')) {
type = 'or'
if (key.includes('~')) {
property = key.substring(2)
has_not = '~'
} else {
property = key.substring(1)
}
} else {
type = 'and'
if (key.includes('~')) {
property = key.substring(1)
has_not = '~'
} else {
property = key
}
}
const in_reg = /(?<=\().+(?=\))/g
const range_reg = /(?<=\[).+(?=\])/g
const compare_reg = /(?<=>=|<=|>(?!=)|<(?!=)).+/
if (val === '*') {
exp = has_not + 'value'
value = ''
} else if (in_reg.test(val)) {
exp = has_not + 'in'
value = val.match(in_reg)[0]
} else if (range_reg.test(val)) {
exp = has_not + 'range'
value = val.match(range_reg)[0]
min = value.split('_TO_')[0]
max = value.split('_TO_')[1]
} else if (compare_reg.test(val)) {
exp = has_not + 'compare'
value = val.match(compare_reg)[0]
const _compareType = val.substring(0, val.match(compare_reg)['index'])
const idx = compareTypeList.findIndex((item) => item.label === _compareType)
compareType = compareTypeList[idx].value
} else if (!val.includes('*')) {
exp = has_not + 'is'
value = val
} else {
const resList = [
['contain', /(?<=\*).*(?=\*)/g],
['end_with', /(?<=\*).+/g],
['start_with', /.+(?=\*)/g],
]
for (let i = 0; i < 3; i++) {
const reg = resList[i]
if (reg[1].test(val)) {
exp = has_not + reg[0]
value = val.match(reg[1])[0]
break
}
}
}
return {
id: uuidv4(),
type,
property,
exp,
value,
min,
max,
compareType,
}
})
this.ruleList = [...expArray]
} else if (open) {
const _canSearchPreferenceAttrList = this.canSearchPreferenceAttrList.filter((attr) => !attr.is_password)
this.ruleList = isInitOne
? [
{
id: uuidv4(),
type: 'and',
property:
_canSearchPreferenceAttrList && _canSearchPreferenceAttrList.length
? _canSearchPreferenceAttrList[0].name
: undefined,
exp: 'is',
value: null,
},
]
: []
}
},
handleClear() {
this.ruleList = [
{
id: uuidv4(),
type: 'and',
property: this.canSearchPreferenceAttrList[0].name,
exp: 'is',
value: null,
},
]
this.filterExp = ''
this.visible = false
this.$emit('setExpFromFilter', this.filterExp)
},
handleSubmit() {
if (this.ruleList && this.ruleList.length) {
this.ruleList[0].type = 'and' // 增删后以防万一第一个不是and
this.filterExp = ''
const expList = this.ruleList.map((rule) => {
let singleRuleExp = ''
let _exp = rule.exp
if (rule.type === 'or') {
singleRuleExp += '-'
}
if (rule.exp.includes('~')) {
singleRuleExp += '~'
_exp = rule.exp.split('~')[1]
}
singleRuleExp += `${rule.property}:`
if (_exp === 'is') {
singleRuleExp += `${rule.value ?? ''}`
}
if (_exp === 'contain') {
singleRuleExp += `*${rule.value ?? ''}*`
}
if (_exp === 'start_with') {
singleRuleExp += `${rule.value ?? ''}*`
}
if (_exp === 'end_with') {
singleRuleExp += `*${rule.value ?? ''}`
}
if (_exp === 'value') {
singleRuleExp += `*`
}
if (_exp === 'in') {
singleRuleExp += `(${rule.value ?? ''})`
}
if (_exp === 'range') {
singleRuleExp += `[${rule.min}_TO_${rule.max}]`
}
if (_exp === 'compare') {
const idx = compareTypeList.findIndex((item) => item.value === rule.compareType)
singleRuleExp += `${compareTypeList[idx].label}${rule.value ?? ''}`
}
return singleRuleExp
})
this.filterExp = expList.join(',')
this.$emit('setExpFromFilter', this.filterExp)
} else {
this.$emit('setExpFromFilter', '')
}
this.visible = false
},
},
}
</script>
<style lang="less" scoped>
.table-filter {
.table-filter-add {
margin-top: 10px;
& > a {
padding: 2px 8px;
&:hover {
background-color: #f0faff;
border-radius: 5px;
}
}
}
.table-filter-extra-icon {
padding: 0px 2px;
&:hover {
display: inline-block;
border-radius: 5px;
background-color: #f0faff;
}
}
}
</style>
<style lang="less">
.table-filter-extra-operation {
.ant-popover-inner-content {
padding: 3px 4px;
.operation {
cursor: pointer;
width: 90px;
height: 30px;
line-height: 30px;
padding: 3px 4px;
border-radius: 5px;
transition: all 0.3s;
&:hover {
background-color: #f0faff;
}
> .anticon {
margin-right: 10px;
}
}
}
}
</style>

View File

@@ -1,45 +1,45 @@
<template>
<div>
<a-row class="row" type="flex" justify="end">
<a-col>
<a-space align="end">
<a-button
class="left-button"
size="small"
:disabled="prevIsDisabled"
@click="prevPage"
><a-icon
type="left"
/></a-button>
<a-button class="page-button" size="small">{{ currentPage }}</a-button>
<a-button
class="right-button"
size="small"
:disabled="nextIsDisabled"
@click="nextPage"
><a-icon
type="right"
/></a-button>
<a-dropdown
class="dropdown"
size="small"
placement="topCenter"
:trigger="['click']"
:disabled="dropdownIsDisabled"
>
<a-menu slot="overlay">
<a-menu-item v-for="(size, index) in pageSizes" :key="index" @click="handleItemClick(size)">
{{ `${size}${$t('itemsPerPage')}` }}
</a-menu-item>
</a-menu>
<a-button size="small">{{ `${pageSize}${$t('itemsPerPage')}` }}<a-icon type="down" /> </a-button>
</a-dropdown>
</a-space>
</a-col>
</a-row>
</div>
</template>
<div>
<a-row class="row" type="flex" justify="end">
<a-col>
<a-space align="end">
<a-button
class="left-button"
size="small"
:disabled="prevIsDisabled"
@click="prevPage"
><a-icon
type="left"
/></a-button>
<a-button class="page-button" size="small">{{ currentPage }}</a-button>
<a-button
class="right-button"
size="small"
:disabled="nextIsDisabled"
@click="nextPage"
><a-icon
type="right"
/></a-button>
<a-dropdown
class="dropdown"
size="small"
placement="topCenter"
:trigger="['click']"
:disabled="dropdownIsDisabled"
>
<a-menu slot="overlay">
<a-menu-item v-for="(size, index) in pageSizes" :key="index" @click="handleItemClick(size)">
{{ `${size}${$t('itemsPerPage')}` }}
</a-menu-item>
</a-menu>
<a-button size="small">{{ `${pageSize}${$t('itemsPerPage')}` }}<a-icon type="down" /> </a-button>
</a-dropdown>
</a-space>
</a-col>
</a-row>
</div>
</template>
<script>
export default {
name: 'Pager',
@@ -117,7 +117,7 @@
},
}
</script>
<style lang="less" scoped>
.row {
margin-top: 5px;
@@ -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

@@ -1,179 +1,183 @@
<template>
<div ref="splitPane" class="split-pane" :class="direction + ' ' + appName" :style="{ flexDirection: direction }">
<div class="pane pane-one" ref="one" :style="lengthType + ':' + paneLengthValue1">
<slot name="one"></slot>
</div>
<div class="spliter-wrap">
<a-button
v-show="collapsable"
:icon="isExpanded ? 'left' : 'right'"
class="collapse-btn"
@click="handleExpand"
></a-button>
<div
class="pane-trigger"
@mousedown="handleMouseDown"
:style="{ backgroundColor: triggerColor, width: `${triggerLength}px` }"
></div>
</div>
<div class="pane pane-two" ref="two" :style="lengthType + ':' + paneLengthValue2">
<slot name="two"></slot>
</div>
</div>
</template>
<script>
export default {
name: 'SplitPane',
props: {
direction: {
type: String,
default: 'row',
},
min: {
type: Number,
default: 10,
},
max: {
type: Number,
default: 90,
},
paneLengthPixel: {
type: Number,
default: 220,
},
triggerLength: {
type: Number,
default: 8,
},
appName: {
type: String,
default: 'viewer',
},
collapsable: {
type: Boolean,
default: false,
},
triggerColor: {
type: String,
default: '#f0f2f5',
},
},
data() {
return {
triggerLeftOffset: 0, // 鼠标距滑动器左()侧偏移量
isExpanded: localStorage.getItem(`${this.appName}-isExpanded`)
? JSON.parse(localStorage.getItem(`${this.appName}-isExpanded`))
: false,
parentContainer: null,
}
},
computed: {
lengthType() {
return this.direction === 'row' ? 'width' : 'height'
},
minLengthType() {
return this.direction === 'row' ? 'minWidth' : 'minHeight'
},
paneLengthValue1() {
return `calc(${this.paneLengthPercent}% - ${this.triggerLength / 2 + 'px'})`
},
paneLengthValue2() {
const rest = 100 - this.paneLengthPercent
return `calc(${rest}% - ${this.triggerLength / 2 + 'px'})`
},
paneLengthPercent() {
const clientRectWidth = this.parentContainer
? this.parentContainer.clientWidth
: document.documentElement.getBoundingClientRect().width
return (this.paneLengthPixel / clientRectWidth) * 100
},
},
watch: {
isExpanded(newValue) {
if (newValue) {
document.querySelector(`.${this.appName} .pane-two`).style.display = 'none'
} else {
document.querySelector(`.${this.appName} .pane-two`).style.display = ''
}
},
},
mounted() {
this.parentContainer = document.querySelector(`.${this.appName}`)
if (this.isExpanded) {
document.querySelector(`.${this.appName} .pane-two`).style.display = 'none'
} else {
document.querySelector(`.${this.appName} .pane-two`).style.display = ''
}
},
methods: {
// 按下滑动器
handleMouseDown(e) {
document.addEventListener('mousemove', this.handleMouseMove)
document.addEventListener('mouseup', this.handleMouseUp)
if (this.direction === 'row') {
this.triggerLeftOffset = e.pageX - e.srcElement.getBoundingClientRect().left
} else {
this.triggerLeftOffset = e.pageY - e.srcElement.getBoundingClientRect().top
}
},
// 按下滑动器后移动鼠标
handleMouseMove(e) {
this.isExpanded = false
this.$emit('expand', this.isExpanded)
const clientRect = this.$refs.splitPane.getBoundingClientRect()
let paneLengthPixel = 0
if (this.direction === 'row') {
const offset = e.pageX - clientRect.left - this.triggerLeftOffset + this.triggerLength / 2
paneLengthPixel = offset
} else {
const offset = e.pageY - clientRect.top - this.triggerLeftOffset + this.triggerLength / 2
paneLengthPixel = offset
}
if (paneLengthPixel < this.min) {
paneLengthPixel = this.min
}
if (paneLengthPixel > this.max) {
paneLengthPixel = this.max
}
this.$emit('update:paneLengthPixel', paneLengthPixel)
localStorage.setItem(`${this.appName}-paneLengthPixel`, paneLengthPixel)
},
// 松开滑动器
handleMouseUp() {
document.removeEventListener('mousemove', this.handleMouseMove)
},
handleExpand() {
this.isExpanded = !this.isExpanded
this.$emit('expand', this.isExpanded)
localStorage.setItem(`${this.appName}-isExpanded`, this.isExpanded)
},
},
}
</script>
<style scoped lang="less">
@import './index.less';
</style>
<template>
<div ref="splitPane" class="split-pane" :class="direction + ' ' + appName" :style="{ flexDirection: direction }">
<div class="pane pane-one" ref="one" :style="lengthType + ':' + paneLengthValue1">
<slot name="one"></slot>
</div>
<div class="spliter-wrap">
<a-button
v-show="collapsable"
:icon="isExpanded ? 'left' : 'right'"
class="collapse-btn"
@click="handleExpand"
></a-button>
<div
class="pane-trigger"
@mousedown="handleMouseDown"
:style="{ backgroundColor: triggerColor, width: `${triggerLength}px` }"
></div>
</div>
<div class="pane pane-two" ref="two" :style="lengthType + ':' + paneLengthValue2">
<slot name="two"></slot>
</div>
</div>
</template>
<script>
export default {
name: 'SplitPane',
props: {
direction: {
type: String,
default: 'row',
},
min: {
type: Number,
default: 10,
},
max: {
type: Number,
default: 90,
},
paneLengthPixel: {
type: Number,
default: 220,
},
triggerLength: {
type: Number,
default: 8,
},
appName: {
type: String,
default: 'viewer',
},
collapsable: {
type: Boolean,
default: false,
},
triggerColor: {
type: String,
default: '#f0f2f5',
},
},
data() {
return {
triggerLeftOffset: 0, // 鼠标距滑动器左()侧偏移量
isExpanded: localStorage.getItem(`${this.appName}-isExpanded`)
? JSON.parse(localStorage.getItem(`${this.appName}-isExpanded`))
: false,
parentContainer: null,
}
},
computed: {
lengthType() {
return this.direction === 'row' ? 'width' : 'height'
},
minLengthType() {
return this.direction === 'row' ? 'minWidth' : 'minHeight'
},
paneLengthValue1() {
return `calc(${this.paneLengthPercent}% - ${this.triggerLength / 2 + 'px'})`
},
paneLengthValue2() {
const rest = 100 - this.paneLengthPercent
return `calc(${rest}% - ${this.triggerLength / 2 + 'px'})`
},
paneLengthPercent() {
const clientRectWidth = this.parentContainer
? this.parentContainer.clientWidth
: document.documentElement.getBoundingClientRect().width
return (this.paneLengthPixel / clientRectWidth) * 100
},
},
watch: {
isExpanded(newValue) {
if (newValue) {
document.querySelector(`.${this.appName} .pane-two`).style.display = 'none'
} else {
document.querySelector(`.${this.appName} .pane-two`).style.display = ''
}
},
},
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'
} else {
document.querySelector(`.${this.appName} .pane-two`).style.display = ''
}
},
methods: {
// 按下滑动器
handleMouseDown(e) {
document.addEventListener('mousemove', this.handleMouseMove)
document.addEventListener('mouseup', this.handleMouseUp)
if (this.direction === 'row') {
this.triggerLeftOffset = e.pageX - e.srcElement.getBoundingClientRect().left
} else {
this.triggerLeftOffset = e.pageY - e.srcElement.getBoundingClientRect().top
}
},
// 按下滑动器后移动鼠标
handleMouseMove(e) {
this.isExpanded = false
this.$emit('expand', this.isExpanded)
const clientRect = this.$refs.splitPane.getBoundingClientRect()
let paneLengthPixel = 0
if (this.direction === 'row') {
const offset = e.pageX - clientRect.left - this.triggerLeftOffset + this.triggerLength / 2
paneLengthPixel = offset
} else {
const offset = e.pageY - clientRect.top - this.triggerLeftOffset + this.triggerLength / 2
paneLengthPixel = offset
}
if (paneLengthPixel < this.min) {
paneLengthPixel = this.min
}
if (paneLengthPixel > this.max) {
paneLengthPixel = this.max
}
this.$emit('update:paneLengthPixel', paneLengthPixel)
localStorage.setItem(`${this.appName}-paneLengthPixel`, paneLengthPixel)
},
// 松开滑动器
handleMouseUp() {
document.removeEventListener('mousemove', this.handleMouseMove)
},
handleExpand() {
this.isExpanded = !this.isExpanded
this.$emit('expand', this.isExpanded)
localStorage.setItem(`${this.appName}-isExpanded`, this.isExpanded)
},
},
}
</script>
<style scoped lang="less">
@import './index.less';
</style>

View File

@@ -1,2 +1,2 @@
import SplitPane from './SplitPane'
export default SplitPane
import SplitPane from './SplitPane'
export default SplitPane

View File

@@ -1,48 +1,48 @@
.split-pane {
height: 100%;
display: flex;
}
.split-pane .pane-two {
flex: 1;
}
.split-pane .pane-trigger {
user-select: none;
}
.split-pane.row .pane-one {
width: 20%;
height: 100%;
// overflow-y: auto;
}
.split-pane.column .pane {
width: 100%;
}
.split-pane.row .pane-trigger {
width: 8px;
height: 100%;
cursor: e-resize;
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAPCAYAAADDNm69AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAAeSURBVBhXY/4PBMzMzA379u1rANFMDGhgGAswMAAAn6EH6K9ktYAAAAAASUVORK5CYII=')
1px 50% no-repeat #f0f2f5;
}
.split-pane .collapse-btn {
width: 25px;
height: 70px;
position: absolute;
right: 8px;
top: calc(50% - 35px);
background-color: #f0f2f5;
border-color: transparent;
border-radius: 8px 0px 0px 8px;
.anticon {
color: #7cb0fe;
}
}
.split-pane .spliter-wrap {
position: relative;
}
.split-pane {
height: 100%;
display: flex;
}
.split-pane .pane-two {
flex: 1;
}
.split-pane .pane-trigger {
user-select: none;
}
.split-pane.row .pane-one {
width: 20%;
height: 100%;
// overflow-y: auto;
}
.split-pane.column .pane {
width: 100%;
}
.split-pane.row .pane-trigger {
width: 8px;
height: 100%;
cursor: e-resize;
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAPCAYAAADDNm69AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAAeSURBVBhXY/4PBMzMzA379u1rANFMDGhgGAswMAAAn6EH6K9ktYAAAAAASUVORK5CYII=')
1px 50% no-repeat #f0f2f5;
}
.split-pane .collapse-btn {
width: 25px;
height: 70px;
position: absolute;
right: 8px;
top: calc(50% - 35px);
background-color: #f0f2f5;
border-color: transparent;
border-radius: 8px 0px 0px 8px;
.anticon {
color: #7cb0fe;
}
}
.split-pane .spliter-wrap {
position: relative;
}

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

@@ -1,4 +1,4 @@
<template>
<template>
<CustomDrawer
width="800px"
placement="left"

View File

@@ -1,4 +1,4 @@
<template>
<template>
<CustomDrawer
@close="handleClose"
width="500"

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

@@ -77,7 +77,7 @@
<!-- 2 -->
<vxe-table-column field="name" :title="$t('acl.resourceName')" :min-widh="150" fixed="left" show-overflow>
<template #title="{row}">
<template #title="{ row }">
{{ row.isGroup ? $t('acl.groupName') : $t('acl.resourceName') }}
</template>
</vxe-table-column>
@@ -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,8 +101,9 @@
:min-widh="200"
fixed="right"
align="center"
show-overflow>
<template #default="{row}">
show-overflow
>
<template #default="{ row }">
<span v-show="isGroup">
<a @click="handleDisplayMember(row)">{{ $t('acl.member') }}</a>
<a-divider type="vertical" />
@@ -117,27 +120,36 @@
</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%">
<a-icon style="font-size:50px; margin-bottom: 20px; color: orange" type="info-circle" />
<div v-else style="text-align: center; margin-top: 20%">
<a-icon style="font-size: 50px; margin-bottom: 20px; color: orange" type="info-circle" />
<h3>{{ $t('acl.addTypeTips') }}</h3>
</div>
<resourceForm ref="resourceForm" @fresh="handleOk"> </resourceForm>
@@ -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

@@ -42,13 +42,13 @@
<!-- 2 -->
<vxe-table-column field="is_app_admin" :title="$t('admin')" :min-width="100" align="center">
<template #default="{row}">
<template #default="{ row }">
<a-icon type="check" v-if="row.is_app_admin" />
</template>
</vxe-table-column>
<vxe-table-column field="id" :title="$t('acl.inheritedFrom')" :min-width="150">
<template #default="{row}">
<template #default="{ row }">
<a-tag color="cyan" v-for="role in id2parents[row.id]" :key="role.id">{{ role.name }}</a-tag>
</template>
</vxe-table-column>
@@ -69,13 +69,13 @@
}
"
>
<template #default="{row}">
<template #default="{ row }">
{{ row.uid ? $t('no') : $t('yes') }}
</template>
</vxe-table-column>
<vxe-table-column field="action" :title="$t('operation')" :width="120" fixed="right">
<template #default="{row}">
<template #default="{ row }">
<a-space>
<a-tooltip :title="$t('acl.resourceList')">
<a
@@ -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

@@ -1,150 +1,150 @@
<template>
<div class="ci-type-grant">
<vxe-table
ref="xTable"
size="mini"
stripe
class="ops-stripe-table"
:data="filterTableData"
:max-height="`${tableHeight}px`"
:row-style="(params) => getCurrentRowStyle(params, addedRids)"
>
<vxe-column field="name"></vxe-column>
<vxe-column v-for="col in columns" :key="col" :field="col" :title="permMap[col]">
<template #default="{row}">
<ReadCheckbox
v-if="['read'].includes(col.split('_')[0])"
:value="row[col.split('_')[0]]"
:valueKey="col"
:rid="row.rid"
@openReadGrantModal="() => openReadGrantModal(col, row)"
/>
<a-checkbox v-else-if="col === 'grant'" :checked="row[col]" @click="clickGrant(col, row)"></a-checkbox>
<a-checkbox @change="(e) => handleChange(e, col, row)" v-else v-model="row[col]"></a-checkbox>
</template>
</vxe-column>
<template #empty>
<div v-if="loading()" style="height: 200px; line-height: 200px;color:#2F54EB">
<a-icon type="loading" /> {{ $t('loading') }}
</div>
<div v-else>
<img :style="{ width: '100px' }" :src="require('@/assets/data_empty.png')" />
<div>{{ $t('noData') }}</div>
</div>
</template>
</vxe-table>
<a-space>
<span class="grant-button" @click="grantDepart">{{ $t('cmdb.components.grantUser') }}</span>
<span class="grant-button" @click="grantRole">{{ $t('cmdb.components.grantRole') }}</span>
</a-space>
</div>
</template>
<script>
import _ from 'lodash'
import { permMap } from './constants.js'
import { grantCiType, revokeCiType } from '../../api/CIType'
import ReadCheckbox from './readCheckbox.vue'
import { getCurrentRowStyle } from './utils'
export default {
name: 'CiTypeGrant',
components: { ReadCheckbox },
inject: ['loading', 'isModal'],
props: {
CITypeId: {
type: Number,
default: null,
},
tableData: {
type: Array,
default: () => [],
},
grantType: {
type: String,
default: 'ci_type',
},
addedRids: {
type: Array,
default: () => [],
},
},
computed: {
filterTableData() {
const _tableData = this.tableData.filter((data) => {
const _intersection = _.intersection(
Object.keys(data),
this.columns.map((col) => col.split('_')[0])
)
return _intersection && _intersection.length
})
return _.uniqBy(_tableData, (item) => item.rid)
},
columns() {
if (this.grantType === 'ci_type') {
return ['config', 'grant']
}
return ['read_attr', 'read_ci', 'create', 'update', 'delete']
},
windowHeight() {
return this.$store.state.windowHeight
},
tableHeight() {
if (this.isModal) {
return (this.windowHeight - 104) / 2
}
return (this.windowHeight - 104) / 2 - 116
},
permMap() {
return permMap()
}
},
methods: {
getCurrentRowStyle,
async handleChange(e, col, row) {
if (e.target.checked) {
await grantCiType(this.CITypeId, row.rid, { perms: [col] }).catch(() => {
this.$emit('getTableData')
})
} else {
await revokeCiType(this.CITypeId, row.rid, { perms: [col] }).catch(() => {
this.$emit('getTableData')
})
}
},
grantDepart() {
this.$emit('grantDepart', this.grantType)
},
grantRole() {
this.$emit('grantRole', this.grantType)
},
openReadGrantModal(col, row) {
this.$emit('openReadGrantModal', col, row)
},
clickGrant(col, row, rowIndex) {
if (!row[col]) {
this.handleChange({ target: { checked: true } }, col, row)
const _idx = this.tableData.findIndex((item) => item.rid === row.rid)
this.$set(this.tableData, _idx, { ...this.tableData[_idx], grant: true })
} else {
const that = this
this.$confirm({
title: that.$t('warning'),
content: that.$t('cmdb.components.confirmRevoke', { name: `${row.name}` }),
onOk() {
that.handleChange({ target: { checked: false } }, col, row)
const _idx = that.tableData.findIndex((item) => item.rid === row.rid)
that.$set(that.tableData, _idx, { ...that.tableData[_idx], grant: false })
},
})
}
},
},
}
</script>
<style lang="less" scoped>
.ci-type-grant {
padding: 10px 0;
}
</style>
<template>
<div class="ci-type-grant">
<vxe-table
ref="xTable"
size="mini"
stripe
class="ops-stripe-table"
:data="filterTableData"
:max-height="`${tableHeight}px`"
:row-style="(params) => getCurrentRowStyle(params, addedRids)"
>
<vxe-column field="name"></vxe-column>
<vxe-column v-for="col in columns" :key="col" :field="col" :title="permMap[col]">
<template #default="{row}">
<ReadCheckbox
v-if="['read'].includes(col.split('_')[0])"
:value="row[col.split('_')[0]]"
:valueKey="col"
:rid="row.rid"
@openReadGrantModal="() => openReadGrantModal(col, row)"
/>
<a-checkbox v-else-if="col === 'grant'" :checked="row[col]" @click="clickGrant(col, row)"></a-checkbox>
<a-checkbox @change="(e) => handleChange(e, col, row)" v-else v-model="row[col]"></a-checkbox>
</template>
</vxe-column>
<template #empty>
<div v-if="loading()" style="height: 200px; line-height: 200px;color:#2F54EB">
<a-icon type="loading" /> {{ $t('loading') }}
</div>
<div v-else>
<img :style="{ width: '100px' }" :src="require('@/assets/data_empty.png')" />
<div>{{ $t('noData') }}</div>
</div>
</template>
</vxe-table>
<a-space>
<span class="grant-button" @click="grantDepart">{{ $t('cmdb.components.grantUser') }}</span>
<span class="grant-button" @click="grantRole">{{ $t('cmdb.components.grantRole') }}</span>
</a-space>
</div>
</template>
<script>
import _ from 'lodash'
import { permMap } from './constants.js'
import { grantCiType, revokeCiType } from '../../api/CIType'
import ReadCheckbox from './readCheckbox.vue'
import { getCurrentRowStyle } from './utils'
export default {
name: 'CiTypeGrant',
components: { ReadCheckbox },
inject: ['loading', 'isModal'],
props: {
CITypeId: {
type: Number,
default: null,
},
tableData: {
type: Array,
default: () => [],
},
grantType: {
type: String,
default: 'ci_type',
},
addedRids: {
type: Array,
default: () => [],
},
},
computed: {
filterTableData() {
const _tableData = this.tableData.filter((data) => {
const _intersection = _.intersection(
Object.keys(data),
this.columns.map((col) => col.split('_')[0])
)
return _intersection && _intersection.length
})
return _.uniqBy(_tableData, (item) => item.rid)
},
columns() {
if (this.grantType === 'ci_type') {
return ['config', 'grant']
}
return ['read_attr', 'read_ci', 'create', 'update', 'delete']
},
windowHeight() {
return this.$store.state.windowHeight
},
tableHeight() {
if (this.isModal) {
return (this.windowHeight - 104) / 2
}
return (this.windowHeight - 104) / 2 - 116
},
permMap() {
return permMap()
}
},
methods: {
getCurrentRowStyle,
async handleChange(e, col, row) {
if (e.target.checked) {
await grantCiType(this.CITypeId, row.rid, { perms: [col] }).catch(() => {
this.$emit('getTableData')
})
} else {
await revokeCiType(this.CITypeId, row.rid, { perms: [col] }).catch(() => {
this.$emit('getTableData')
})
}
},
grantDepart() {
this.$emit('grantDepart', this.grantType)
},
grantRole() {
this.$emit('grantRole', this.grantType)
},
openReadGrantModal(col, row) {
this.$emit('openReadGrantModal', col, row)
},
clickGrant(col, row, rowIndex) {
if (!row[col]) {
this.handleChange({ target: { checked: true } }, col, row)
const _idx = this.tableData.findIndex((item) => item.rid === row.rid)
this.$set(this.tableData, _idx, { ...this.tableData[_idx], grant: true })
} else {
const that = this
this.$confirm({
title: that.$t('warning'),
content: that.$t('cmdb.components.confirmRevoke', { name: `${row.name}` }),
onOk() {
that.handleChange({ target: { checked: false } }, col, row)
const _idx = that.tableData.findIndex((item) => item.rid === row.rid)
that.$set(that.tableData, _idx, { ...that.tableData[_idx], grant: false })
},
})
}
},
},
}
</script>
<style lang="less" scoped>
.ci-type-grant {
padding: 10px 0;
}
</style>

View File

@@ -1,15 +1,15 @@
import i18n from '@/lang'
export const permMap = () => {
return {
read: i18n.t('view'),
add: i18n.t('new'),
create: i18n.t('new'),
update: i18n.t('update'),
delete: i18n.t('delete'),
config: i18n.t('cmdb.components.config'),
grant: i18n.t('grant'),
'read_attr': i18n.t('cmdb.components.readAttribute'),
'read_ci': i18n.t('cmdb.components.readCI')
}
}
import i18n from '@/lang'
export const permMap = () => {
return {
read: i18n.t('view'),
add: i18n.t('new'),
create: i18n.t('new'),
update: i18n.t('update'),
delete: i18n.t('delete'),
config: i18n.t('cmdb.components.config'),
grant: i18n.t('grant'),
'read_attr': i18n.t('cmdb.components.readAttribute'),
'read_ci': i18n.t('cmdb.components.readCI')
}
}

View File

@@ -1,343 +1,343 @@
<template>
<div class="cmdb-grant" :style="{ maxHeight: `${windowHeight - 104}px` }">
<template v-if="cmdbGrantType.includes('ci_type')">
<div class="cmdb-grant-title">{{ $t('cmdb.components.ciTypeGrant') }}</div>
<CiTypeGrant
:CITypeId="CITypeId"
:tableData="tableData"
grantType="ci_type"
@grantDepart="grantDepart"
@grantRole="grantRole"
@getTableData="getTableData"
ref="grant_ci_type"
:addedRids="addedRids"
/>
</template>
<template
v-if="
cmdbGrantType.includes('ci_type,ci') || (cmdbGrantType.includes('ci') && !cmdbGrantType.includes('ci_type'))
"
>
<div class="cmdb-grant-title">{{ $t('cmdb.components.ciGrant') }}</div>
<CiTypeGrant
:CITypeId="CITypeId"
:tableData="tableData"
grantType="ci"
@grantDepart="grantDepart"
@grantRole="grantRole"
@getTableData="getTableData"
@openReadGrantModal="openReadGrantModal"
ref="grant_ci"
:addedRids="addedRids"
/>
</template>
<template v-if="cmdbGrantType.includes('type_relation')">
<div class="cmdb-grant-title">{{ $t('cmdb.components.relationGrant') }}</div>
<TypeRelationGrant
:typeRelationIds="typeRelationIds"
:tableData="tableData"
grantType="type_relation"
@grantDepart="grantDepart"
@grantRole="grantRole"
@getTableData="getTableData"
ref="grant_type_relation"
:addedRids="addedRids"
/>
</template>
<template v-if="cmdbGrantType.includes('relation_view')">
<div class="cmdb-grant-title">{{ resourceTypeName }}{{ $t('cmdb.components.perm') }}</div>
<RelationViewGrant
:resourceTypeName="resourceTypeName"
:tableData="tableData"
grantType="relation_view"
@grantDepart="grantDepart"
@grantRole="grantRole"
@getTableData="getTableData"
ref="grant_relation_view"
:addedRids="addedRids"
/>
</template>
<GrantModal ref="grantModal" @handleOk="handleOk" />
<ReadGrantModal ref="readGrantModal" :CITypeId="CITypeId" @updateTableDataRead="updateTableDataRead" />
</div>
</template>
<script>
import { mapState } from 'vuex'
import CiTypeGrant from './ciTypeGrant.vue'
import TypeRelationGrant from './typeRelationGrant.vue'
import { searchResource } from '@/modules/acl/api/resource'
import { getResourcePerms } from '@/modules/acl/api/permission'
import GrantModal from './grantModal.vue'
import ReadGrantModal from './readGrantModal'
import RelationViewGrant from './relationViewGrant.vue'
import { getCITypeGroupById, ciTypeFilterPermissions } from '../../api/CIType'
export default {
name: 'GrantComp',
components: { CiTypeGrant, TypeRelationGrant, RelationViewGrant, GrantModal, ReadGrantModal },
props: {
CITypeId: {
type: Number,
default: null,
},
resourceTypeName: {
type: String,
default: '',
},
resourceType: {
type: String,
default: 'CIType',
},
app_id: {
type: String,
default: 'cmdb',
},
cmdbGrantType: {
type: String,
default: 'ci_type,ci',
},
typeRelationIds: {
type: Array,
default: null,
},
isModal: {
type: Boolean,
default: false,
},
},
inject: ['resource_type'],
data() {
return {
tableData: [],
grantType: '',
resource_id: null,
attrGroup: [],
filerPerimissions: {},
loading: false,
addedRids: [], // added rid this time
}
},
computed: {
...mapState({
allEmployees: (state) => state.user.allEmployees,
allDepartments: (state) => state.user.allDepartments,
}),
child_resource_type() {
return this.resource_type()
},
windowHeight() {
return this.$store.state.windowHeight
},
},
provide() {
return {
attrGroup: () => {
return this.attrGroup
},
filerPerimissions: () => {
return this.filerPerimissions
},
loading: () => {
return this.loading
},
isModal: this.isModal,
}
},
watch: {
resourceTypeName: {
immediate: true,
handler() {
this.init()
},
},
CITypeId: {
immediate: true,
handler() {
if (this.CITypeId && this.cmdbGrantType.includes('ci')) {
this.getFilterPermissions()
this.getAttrGroup()
}
},
},
},
mounted() {},
methods: {
getAttrGroup() {
getCITypeGroupById(this.CITypeId, { need_other: true }).then((res) => {
this.attrGroup = res
})
},
getFilterPermissions() {
ciTypeFilterPermissions(this.CITypeId).then((res) => {
this.filerPerimissions = res
})
},
async init() {
const _find = this.child_resource_type.groups.find((item) => item.name === this.resourceType)
const resource_type_id = _find?.id ?? 0
const res = await searchResource({
app_id: this.app_id,
resource_type_id,
page_size: 9999,
})
const _tempFind = res.resources.find((item) => item.name === this.resourceTypeName)
console.log(this.resourceTypeName)
this.resource_id = _tempFind?.id || 0
this.getTableData()
},
async getTableData() {
this.loading = true
const _tableData = await getResourcePerms(this.resource_id, { need_users: 0 })
const perms = []
for (const key in _tableData) {
const obj = {}
obj.name = key
_tableData[key].perms.forEach((perm) => {
obj[`${perm.name}`] = true
obj.rid = perm?.rid ?? null
})
perms.push(obj)
}
this.tableData = perms
this.loading = false
},
// Grant the department in common-setting and get the roleid from it
grantDepart(grantType) {
this.$refs.grantModal.open('depart')
this.grantType = grantType
},
// Grant the oldest role permissions
grantRole(grantType) {
this.$refs.grantModal.open('role')
this.grantType = grantType
},
handleOk(params, type) {
const { grantType } = this
let rids
if (type === 'depart') {
rids = [
...params.department.map((rid) => {
const _find = this.allDepartments.find((dep) => dep.acl_rid === rid)
return { rid, name: _find?.department_name ?? rid }
}),
...params.user.map((rid) => {
const _find = this.allEmployees.find((dep) => dep.acl_rid === rid)
return { rid, name: _find?.nickname ?? rid }
}),
]
}
if (type === 'role') {
rids = [
...params.map((role) => {
return { rid: role.id, name: role.name }
}),
]
}
if (grantType === 'ci_type') {
this.tableData.unshift(
...rids.map(({ rid, name }) => {
const _find = this.tableData.find((item) => item.rid === rid)
return {
rid,
name,
conifg: false,
grant: false,
..._find,
}
})
)
}
if (grantType === 'ci') {
this.tableData.unshift(
...rids.map(({ rid, name }) => {
const _find = this.tableData.find((item) => item.rid === rid)
return {
rid,
name,
read_attr: false,
read_ci: false,
create: false,
update: false,
delete: false,
..._find,
}
})
)
}
if (grantType === 'type_relation') {
this.tableData.unshift(
...rids.map(({ rid, name }) => {
return {
rid,
name,
create: false,
grant: false,
delete: false,
}
})
)
}
if (grantType === 'relation_view') {
this.tableData.unshift(
...rids.map(({ rid, name }) => {
return {
rid,
name,
read: false,
grant: false,
}
})
)
}
this.addedRids = rids
this.$nextTick(() => {
setTimeout(() => {
this.$refs[`grant_${grantType}`].$refs.xTable.elemStore['main-body-wrapper'].scrollTo(0, 0)
}, 300)
})
},
openReadGrantModal(col, row) {
this.$refs.readGrantModal.open(col, row)
},
updateTableDataRead(row, hasRead) {
const _idx = this.tableData.findIndex((item) => item.rid === row.rid)
this.$set(this.tableData, _idx, { ...this.tableData[_idx], read: hasRead })
this.getFilterPermissions()
},
},
}
</script>
<style lang="less" scoped>
@import '~@/style/static.less';
.cmdb-grant {
position: relative;
padding: 24px 24px 0 24px;
overflow: auto;
.cmdb-grant-title {
border-left: 4px solid #custom_colors[color_1];
padding-left: 10px;
}
}
</style>
<style lang="less">
@import '~@/style/static.less';
.cmdb-grant {
.grant-button {
padding: 6px 8px;
color: #custom_colors[color_1];
background-color: #custom_colors[color_2];
border-radius: 2px;
cursor: pointer;
margin: 15px 0;
display: inline-block;
transition: all 0.3s;
&:hover {
box-shadow: 2px 3px 4px #custom_colors[color_2];
}
}
}
</style>
<template>
<div class="cmdb-grant" :style="{ maxHeight: `${windowHeight - 104}px` }">
<template v-if="cmdbGrantType.includes('ci_type')">
<div class="cmdb-grant-title">{{ $t('cmdb.components.ciTypeGrant') }}</div>
<CiTypeGrant
:CITypeId="CITypeId"
:tableData="tableData"
grantType="ci_type"
@grantDepart="grantDepart"
@grantRole="grantRole"
@getTableData="getTableData"
ref="grant_ci_type"
:addedRids="addedRids"
/>
</template>
<template
v-if="
cmdbGrantType.includes('ci_type,ci') || (cmdbGrantType.includes('ci') && !cmdbGrantType.includes('ci_type'))
"
>
<div class="cmdb-grant-title">{{ $t('cmdb.components.ciGrant') }}</div>
<CiTypeGrant
:CITypeId="CITypeId"
:tableData="tableData"
grantType="ci"
@grantDepart="grantDepart"
@grantRole="grantRole"
@getTableData="getTableData"
@openReadGrantModal="openReadGrantModal"
ref="grant_ci"
:addedRids="addedRids"
/>
</template>
<template v-if="cmdbGrantType.includes('type_relation')">
<div class="cmdb-grant-title">{{ $t('cmdb.components.relationGrant') }}</div>
<TypeRelationGrant
:typeRelationIds="typeRelationIds"
:tableData="tableData"
grantType="type_relation"
@grantDepart="grantDepart"
@grantRole="grantRole"
@getTableData="getTableData"
ref="grant_type_relation"
:addedRids="addedRids"
/>
</template>
<template v-if="cmdbGrantType.includes('relation_view')">
<div class="cmdb-grant-title">{{ resourceTypeName }}{{ $t('cmdb.components.perm') }}</div>
<RelationViewGrant
:resourceTypeName="resourceTypeName"
:tableData="tableData"
grantType="relation_view"
@grantDepart="grantDepart"
@grantRole="grantRole"
@getTableData="getTableData"
ref="grant_relation_view"
:addedRids="addedRids"
/>
</template>
<GrantModal ref="grantModal" @handleOk="handleOk" />
<ReadGrantModal ref="readGrantModal" :CITypeId="CITypeId" @updateTableDataRead="updateTableDataRead" />
</div>
</template>
<script>
import { mapState } from 'vuex'
import CiTypeGrant from './ciTypeGrant.vue'
import TypeRelationGrant from './typeRelationGrant.vue'
import { searchResource } from '@/modules/acl/api/resource'
import { getResourcePerms } from '@/modules/acl/api/permission'
import GrantModal from './grantModal.vue'
import ReadGrantModal from './readGrantModal'
import RelationViewGrant from './relationViewGrant.vue'
import { getCITypeGroupById, ciTypeFilterPermissions } from '../../api/CIType'
export default {
name: 'GrantComp',
components: { CiTypeGrant, TypeRelationGrant, RelationViewGrant, GrantModal, ReadGrantModal },
props: {
CITypeId: {
type: Number,
default: null,
},
resourceTypeName: {
type: String,
default: '',
},
resourceType: {
type: String,
default: 'CIType',
},
app_id: {
type: String,
default: 'cmdb',
},
cmdbGrantType: {
type: String,
default: 'ci_type,ci',
},
typeRelationIds: {
type: Array,
default: null,
},
isModal: {
type: Boolean,
default: false,
},
},
inject: ['resource_type'],
data() {
return {
tableData: [],
grantType: '',
resource_id: null,
attrGroup: [],
filerPerimissions: {},
loading: false,
addedRids: [], // added rid this time
}
},
computed: {
...mapState({
allEmployees: (state) => state.user.allEmployees,
allDepartments: (state) => state.user.allDepartments,
}),
child_resource_type() {
return this.resource_type()
},
windowHeight() {
return this.$store.state.windowHeight
},
},
provide() {
return {
attrGroup: () => {
return this.attrGroup
},
filerPerimissions: () => {
return this.filerPerimissions
},
loading: () => {
return this.loading
},
isModal: this.isModal,
}
},
watch: {
resourceTypeName: {
immediate: true,
handler() {
this.init()
},
},
CITypeId: {
immediate: true,
handler() {
if (this.CITypeId && this.cmdbGrantType.includes('ci')) {
this.getFilterPermissions()
this.getAttrGroup()
}
},
},
},
mounted() {},
methods: {
getAttrGroup() {
getCITypeGroupById(this.CITypeId, { need_other: true }).then((res) => {
this.attrGroup = res
})
},
getFilterPermissions() {
ciTypeFilterPermissions(this.CITypeId).then((res) => {
this.filerPerimissions = res
})
},
async init() {
const _find = this.child_resource_type.groups.find((item) => item.name === this.resourceType)
const resource_type_id = _find?.id ?? 0
const res = await searchResource({
app_id: this.app_id,
resource_type_id,
page_size: 9999,
})
const _tempFind = res.resources.find((item) => item.name === this.resourceTypeName)
console.log(this.resourceTypeName)
this.resource_id = _tempFind?.id || 0
this.getTableData()
},
async getTableData() {
this.loading = true
const _tableData = await getResourcePerms(this.resource_id, { need_users: 0 })
const perms = []
for (const key in _tableData) {
const obj = {}
obj.name = key
_tableData[key].perms.forEach((perm) => {
obj[`${perm.name}`] = true
obj.rid = perm?.rid ?? null
})
perms.push(obj)
}
this.tableData = perms
this.loading = false
},
// Grant the department in common-setting and get the roleid from it
grantDepart(grantType) {
this.$refs.grantModal.open('depart')
this.grantType = grantType
},
// Grant the oldest role permissions
grantRole(grantType) {
this.$refs.grantModal.open('role')
this.grantType = grantType
},
handleOk(params, type) {
const { grantType } = this
let rids
if (type === 'depart') {
rids = [
...params.department.map((rid) => {
const _find = this.allDepartments.find((dep) => dep.acl_rid === rid)
return { rid, name: _find?.department_name ?? rid }
}),
...params.user.map((rid) => {
const _find = this.allEmployees.find((dep) => dep.acl_rid === rid)
return { rid, name: _find?.nickname ?? rid }
}),
]
}
if (type === 'role') {
rids = [
...params.map((role) => {
return { rid: role.id, name: role.name }
}),
]
}
if (grantType === 'ci_type') {
this.tableData.unshift(
...rids.map(({ rid, name }) => {
const _find = this.tableData.find((item) => item.rid === rid)
return {
rid,
name,
conifg: false,
grant: false,
..._find,
}
})
)
}
if (grantType === 'ci') {
this.tableData.unshift(
...rids.map(({ rid, name }) => {
const _find = this.tableData.find((item) => item.rid === rid)
return {
rid,
name,
read_attr: false,
read_ci: false,
create: false,
update: false,
delete: false,
..._find,
}
})
)
}
if (grantType === 'type_relation') {
this.tableData.unshift(
...rids.map(({ rid, name }) => {
return {
rid,
name,
create: false,
grant: false,
delete: false,
}
})
)
}
if (grantType === 'relation_view') {
this.tableData.unshift(
...rids.map(({ rid, name }) => {
return {
rid,
name,
read: false,
grant: false,
}
})
)
}
this.addedRids = rids
this.$nextTick(() => {
setTimeout(() => {
this.$refs[`grant_${grantType}`].$refs.xTable.elemStore['main-body-wrapper'].scrollTo(0, 0)
}, 300)
})
},
openReadGrantModal(col, row) {
this.$refs.readGrantModal.open(col, row)
},
updateTableDataRead(row, hasRead) {
const _idx = this.tableData.findIndex((item) => item.rid === row.rid)
this.$set(this.tableData, _idx, { ...this.tableData[_idx], read: hasRead })
this.getFilterPermissions()
},
},
}
</script>
<style lang="less" scoped>
@import '~@/style/static.less';
.cmdb-grant {
position: relative;
padding: 24px 24px 0 24px;
overflow: auto;
.cmdb-grant-title {
border-left: 4px solid #custom_colors[color_1];
padding-left: 10px;
}
}
</style>
<style lang="less">
@import '~@/style/static.less';
.cmdb-grant {
.grant-button {
padding: 6px 8px;
color: #custom_colors[color_1];
background-color: #custom_colors[color_2];
border-radius: 2px;
cursor: pointer;
margin: 15px 0;
display: inline-block;
transition: all 0.3s;
&:hover {
box-shadow: 2px 3px 4px #custom_colors[color_2];
}
}
}
</style>

View File

@@ -1,57 +1,67 @@
<template>
<a-modal :title="title" :visible="visible" @ok="handleOk" @cancel="handleCancel" destroyOnClose>
<EmployeeTransfer
:isDisabledAllCompany="true"
v-if="type === 'depart'"
uniqueKey="acl_rid"
ref="employeeTransfer"
:height="350"
/>
<RoleTransfer app_id="cmdb" :height="350" ref="roleTransfer" v-if="type === 'role'" />
</a-modal>
</template>
<script>
import EmployeeTransfer from '@/components/EmployeeTransfer'
import RoleTransfer from '@/components/RoleTransfer'
export default {
name: 'GrantModal',
components: { EmployeeTransfer, RoleTransfer },
data() {
return {
visible: false,
type: 'depart',
}
},
computed: {
title() {
if (this.type === 'depart') {
return this.$t('cmdb.components.grantUser')
}
return this.$t('cmdb.components.grantRole')
},
},
methods: {
open(type) {
this.visible = true
this.type = type
},
handleOk() {
let params
if (this.type === 'depart') {
params = this.$refs.employeeTransfer.getValues()
}
if (this.type === 'role') {
params = this.$refs.roleTransfer.getValues()
}
this.handleCancel()
this.$emit('handleOk', params, this.type)
},
handleCancel() {
this.visible = false
},
},
}
</script>
<style></style>
<template>
<a-modal :title="title" :visible="visible" @ok="handleOk" @cancel="handleCancel" destroyOnClose>
<EmployeeTransfer
:isDisabledAllCompany="true"
v-if="type === 'depart'"
uniqueKey="acl_rid"
ref="employeeTransfer"
:height="350"
/>
<RoleTransfer app_id="cmdb" :height="350" ref="roleTransfer" v-if="type === 'role'" />
</a-modal>
</template>
<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,
type: 'depart',
}
},
computed: {
title() {
if (this.customTitle) {
return this.customTitle
}
if (this.type === 'depart') {
return this.$t('cmdb.components.grantUser')
}
return this.$t('cmdb.components.grantRole')
},
},
methods: {
open(type) {
this.visible = true
this.type = type
},
handleOk() {
let params
if (this.type === 'depart') {
params = this.$refs.employeeTransfer.getValues()
}
if (this.type === 'role') {
params = this.$refs.roleTransfer.getValues()
}
this.handleCancel()
this.$emit('handleOk', params, this.type)
},
handleCancel() {
this.visible = false
},
},
}
</script>
<style></style>

View File

@@ -1,57 +1,57 @@
<template>
<a-modal width="800px" :visible="visible" @ok="handleOk" @cancel="handleCancel" :bodyStyle="{ padding: 0 }">
<GrantComp
:resourceType="resourceType"
:app_id="app_id"
:cmdbGrantType="cmdbGrantType"
:resourceTypeName="resourceTypeName"
:typeRelationIds="typeRelationIds"
:CITypeId="CITypeId"
:isModal="true"
/>
</a-modal>
</template>
<script>
import GrantComp from './grantComp.vue'
export default {
name: 'CMDBGrant',
components: { GrantComp },
props: {
resourceType: {
type: String,
default: 'CIType',
},
app_id: {
type: String,
default: '',
},
},
data() {
return {
visible: false,
resourceTypeName: '',
typeRelationIds: [],
cmdbGrantType: '',
CITypeId: null,
}
},
methods: {
open({ name, typeRelationIds = [], cmdbGrantType, CITypeId }) {
this.visible = true
this.resourceTypeName = name
this.typeRelationIds = typeRelationIds
this.cmdbGrantType = cmdbGrantType
this.CITypeId = CITypeId
},
handleOk() {
this.handleCancel()
},
handleCancel() {
this.visible = false
},
},
}
</script>
<style></style>
<template>
<a-modal width="800px" :visible="visible" @ok="handleOk" @cancel="handleCancel" :bodyStyle="{ padding: 0 }">
<GrantComp
:resourceType="resourceType"
:app_id="app_id"
:cmdbGrantType="cmdbGrantType"
:resourceTypeName="resourceTypeName"
:typeRelationIds="typeRelationIds"
:CITypeId="CITypeId"
:isModal="true"
/>
</a-modal>
</template>
<script>
import GrantComp from './grantComp.vue'
export default {
name: 'CMDBGrant',
components: { GrantComp },
props: {
resourceType: {
type: String,
default: 'CIType',
},
app_id: {
type: String,
default: '',
},
},
data() {
return {
visible: false,
resourceTypeName: '',
typeRelationIds: [],
cmdbGrantType: '',
CITypeId: null,
}
},
methods: {
open({ name, typeRelationIds = [], cmdbGrantType, CITypeId }) {
this.visible = true
this.resourceTypeName = name
this.typeRelationIds = typeRelationIds
this.cmdbGrantType = cmdbGrantType
this.CITypeId = CITypeId
},
handleOk() {
this.handleCancel()
},
handleCancel() {
this.visible = false
},
},
}
</script>
<style></style>

View File

@@ -1,89 +1,89 @@
<template>
<div :class="{ 'read-checkbox': true, 'ant-checkbox-wrapper': isHalfChecked }" @click="openReadGrantModal">
<a-tooltip
v-if="value && isHalfChecked"
:title="valueKey === 'read_ci' ? filerPerimissions[this.rid].name || '' : ''"
>
<div v-if="value && isHalfChecked" :class="{ 'read-checkbox-half-checked': true, 'ant-checkbox': true }"></div>
</a-tooltip>
<a-checkbox v-else :checked="value" />
</div>
</template>
<script>
export default {
name: 'ReadCheckbox',
inject: {
provide_filerPerimissions: {
from: 'filerPerimissions',
},
},
props: {
value: {
type: Boolean,
default: false,
},
valueKey: {
type: String,
default: 'read_attr',
},
rid: {
type: Number,
default: 0,
},
},
computed: {
filerPerimissions() {
return this.provide_filerPerimissions()
},
filterKey() {
if (this.valueKey === 'read_attr') {
return 'attr_filter'
}
return 'ci_filter'
},
isHalfChecked() {
if (this.filerPerimissions[this.rid]) {
const _tempValue = this.filerPerimissions[this.rid][this.filterKey]
return !!(_tempValue && _tempValue.length)
}
return false
},
},
methods: {
openReadGrantModal() {
this.$emit('openReadGrantModal')
},
},
}
</script>
<style lang="less" scoped>
@import '~@/style/static.less';
.read-checkbox {
.read-checkbox-half-checked {
width: 16px;
height: 16px;
border: 1px solid #d9d9d9;
border-radius: 2px;
cursor: pointer;
margin: 0;
padding: 0;
position: relative;
overflow: hidden;
&::after {
content: '';
position: absolute;
width: 0;
height: 0;
// background-color: #custom_colors[color_1];
border-radius: 2px;
border: 14px solid transparent;
border-left-color: #custom_colors[color_1];
transform: rotate(225deg);
top: -16px;
left: -17px;
}
}
}
</style>
<template>
<div :class="{ 'read-checkbox': true, 'ant-checkbox-wrapper': isHalfChecked }" @click="openReadGrantModal">
<a-tooltip
v-if="value && isHalfChecked"
:title="valueKey === 'read_ci' ? filerPerimissions[this.rid].name || '' : ''"
>
<div v-if="value && isHalfChecked" :class="{ 'read-checkbox-half-checked': true, 'ant-checkbox': true }"></div>
</a-tooltip>
<a-checkbox v-else :checked="value" />
</div>
</template>
<script>
export default {
name: 'ReadCheckbox',
inject: {
provide_filerPerimissions: {
from: 'filerPerimissions',
},
},
props: {
value: {
type: Boolean,
default: false,
},
valueKey: {
type: String,
default: 'read_attr',
},
rid: {
type: Number,
default: 0,
},
},
computed: {
filerPerimissions() {
return this.provide_filerPerimissions()
},
filterKey() {
if (this.valueKey === 'read_attr') {
return 'attr_filter'
}
return 'ci_filter'
},
isHalfChecked() {
if (this.filerPerimissions[this.rid]) {
const _tempValue = this.filerPerimissions[this.rid][this.filterKey]
return !!(_tempValue && _tempValue.length)
}
return false
},
},
methods: {
openReadGrantModal() {
this.$emit('openReadGrantModal')
},
},
}
</script>
<style lang="less" scoped>
@import '~@/style/static.less';
.read-checkbox {
.read-checkbox-half-checked {
width: 16px;
height: 16px;
border: 1px solid #d9d9d9;
border-radius: 2px;
cursor: pointer;
margin: 0;
padding: 0;
position: relative;
overflow: hidden;
&::after {
content: '';
position: absolute;
width: 0;
height: 0;
// background-color: #custom_colors[color_1];
border-radius: 2px;
border: 14px solid transparent;
border-left-color: #custom_colors[color_1];
transform: rotate(225deg);
top: -16px;
left: -17px;
}
}
}
</style>

View File

@@ -1,205 +1,212 @@
<template>
<a-modal :width="680" :title="title" :visible="visible" @ok="handleOk" @cancel="handleCancel">
<CustomRadio
:radioList="[
{ value: 1, label: $t('cmdb.components.all') },
{ value: 2, label: $t('cmdb.components.customize'), layout: 'vertical' },
{ value: 3, label: $t('cmdb.components.none') },
]"
v-model="radioValue"
>
<template slot="extra_2" v-if="radioValue === 2">
<treeselect
v-if="colType === 'read_attr'"
v-model="selectedAttr"
:multiple="true"
:clearable="true"
searchable
:options="attrGroup"
:placeholder="$t('cmdb.ciType.selectAttributes')"
value-consists-of="LEAF_PRIORITY"
:limit="10"
:limitText="(count) => `+ ${count}`"
:normalizer="
(node) => {
return {
id: node.name || -1,
label: node.alias || node.name || $t('other'),
title: node.alias || node.name || $t('other'),
children: node.attributes,
}
}
"
appendToBody
zIndex="1050"
>
</treeselect>
<a-form-model
:model="form"
:rules="rules"
v-if="colType === 'read_ci'"
:labelCol="{ span: 2 }"
:wrapperCol="{ span: 10 }"
ref="form"
>
<a-form-model-item :label="$t('name')" prop="name">
<a-input v-model="form.name" />
</a-form-model-item>
<FilterComp
ref="filterComp"
:isDropdown="false"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList"
@setExpFromFilter="setExpFromFilter"
:expression="expression"
/>
</a-form-model>
</template>
</CustomRadio>
</a-modal>
</template>
<script>
import { grantCiType, revokeCiType } from '../../api/CIType'
import { getCITypeAttributesByTypeIds } from '../../api/CITypeAttr'
import FilterComp from '@/components/CMDBFilterComp'
export default {
name: 'ReadGrantModal',
components: { FilterComp },
props: {
CITypeId: {
type: Number,
default: null,
},
},
inject: {
provide_attrGroup: {
from: 'attrGroup',
},
provide_filerPerimissions: {
from: 'filerPerimissions',
},
},
data() {
return {
visible: false,
colType: '',
row: {},
radioValue: 1,
radioStyle: {
display: 'block',
height: '30px',
lineHeight: '30px',
},
selectedAttr: [],
ruleList: [],
canSearchPreferenceAttrList: [],
expression: '',
form: {
name: '',
},
rules: {
name: [{ required: true, message: this.$t('cmdb.components.customizeFilterName') }],
},
}
},
computed: {
title() {
if (this.colType === 'read_attr') {
return this.$t('cmdb.components.attributeGrant')
}
return this.$t('cmdb.components.ciGrant')
},
attrGroup() {
return this.provide_attrGroup()
},
filerPerimissions() {
return this.provide_filerPerimissions()
},
filterKey() {
if (this.colType === 'read_attr') {
return 'attr_filter'
}
return 'ci_filter'
},
},
methods: {
async open(colType, row) {
this.visible = true
this.colType = colType
this.row = row
if (this.colType === 'read_ci') {
await getCITypeAttributesByTypeIds({ type_ids: this.CITypeId }).then((res) => {
this.canSearchPreferenceAttrList = res.attributes.filter((item) => item.value_type !== '6')
})
}
if (this.filerPerimissions[row.rid]) {
const _tempValue = this.filerPerimissions[row.rid][this.filterKey]
if (_tempValue && _tempValue.length) {
this.radioValue = 2
if (this.colType === 'read_attr') {
this.selectedAttr = _tempValue
} else {
this.expression = `q=${_tempValue}`
this.form = {
name: this.filerPerimissions[row.rid].name || '',
}
this.$nextTick(() => {
this.$refs.filterComp.visibleChange(true)
})
}
}
} else {
this.form = {
name: '',
}
}
},
async handleOk() {
if (this.radioValue === 1) {
await grantCiType(this.CITypeId, this.row.rid, {
perms: ['read'],
attr_filter: this.colType === 'read_attr' ? [] : undefined,
ci_filter: this.colType === 'read_ci' ? '' : undefined,
})
} else if (this.radioValue === 2) {
if (this.colType === 'read_ci') {
this.$refs.filterComp.handleSubmit()
}
await grantCiType(this.CITypeId, this.row.rid, {
perms: ['read'],
attr_filter: this.colType === 'read_attr' ? this.selectedAttr : undefined,
ci_filter: this.colType === 'read_ci' ? this.expression.slice(2) : undefined,
name: this.colType === 'read_ci' ? this.form.name : undefined,
})
} else {
const _tempValue = this.filerPerimissions?.[this.row.rid]?.[this.filterKey]
await revokeCiType(this.CITypeId, this.row.rid, {
perms: ['read'],
attr_filter: this.colType === 'read_attr' ? _tempValue : undefined,
ci_filter: this.colType === 'read_ci' ? _tempValue : undefined,
})
}
this.$emit('updateTableDataRead', this.row, this.radioValue === 1 || this.radioValue === 2)
this.handleCancel()
},
handleCancel() {
this.radioValue = 1
this.selectedAttr = []
if (this.$refs.form) {
this.$refs.form.resetFields()
}
this.visible = false
},
setExpFromFilter(filterExp) {
let expression = ''
if (filterExp) {
expression = `q=${filterExp}`
}
this.expression = expression
},
},
}
</script>
<style></style>
<template>
<a-modal :width="680" :title="title" :visible="visible" @ok="handleOk" @cancel="handleCancel">
<CustomRadio
:radioList="[
{ value: 1, label: $t('cmdb.components.all') },
{ value: 2, label: $t('cmdb.components.customize'), layout: 'vertical' },
{ value: 3, label: $t('cmdb.components.none') },
]"
:value="radioValue"
@change="changeRadioValue"
>
<template slot="extra_2" v-if="radioValue === 2">
<treeselect
v-if="colType === 'read_attr'"
v-model="selectedAttr"
:multiple="true"
:clearable="true"
searchable
:options="attrGroup"
:placeholder="$t('cmdb.ciType.selectAttributes')"
value-consists-of="LEAF_PRIORITY"
:limit="10"
:limitText="(count) => `+ ${count}`"
:normalizer="
(node) => {
return {
id: node.name || -1,
label: node.alias || node.name || $t('other'),
title: node.alias || node.name || $t('other'),
children: node.attributes,
}
}
"
appendToBody
zIndex="1050"
>
</treeselect>
<a-form-model
:model="form"
:rules="rules"
v-if="colType === 'read_ci'"
:labelCol="{ span: 2 }"
:wrapperCol="{ span: 10 }"
ref="form"
>
<a-form-model-item :label="$t('name')" prop="name">
<a-input v-model="form.name" />
</a-form-model-item>
<FilterComp
ref="filterComp"
:isDropdown="false"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList"
@setExpFromFilter="setExpFromFilter"
:expression="expression"
/>
</a-form-model>
</template>
</CustomRadio>
</a-modal>
</template>
<script>
import { grantCiType, revokeCiType } from '../../api/CIType'
import { getCITypeAttributesByTypeIds } from '../../api/CITypeAttr'
import FilterComp from '@/components/CMDBFilterComp'
export default {
name: 'ReadGrantModal',
components: { FilterComp },
props: {
CITypeId: {
type: Number,
default: null,
},
},
inject: {
provide_attrGroup: {
from: 'attrGroup',
},
provide_filerPerimissions: {
from: 'filerPerimissions',
},
},
data() {
return {
visible: false,
colType: '',
row: {},
radioValue: 1,
radioStyle: {
display: 'block',
height: '30px',
lineHeight: '30px',
},
selectedAttr: [],
ruleList: [],
canSearchPreferenceAttrList: [],
expression: '',
form: {
name: '',
},
rules: {
name: [{ required: true, message: this.$t('cmdb.components.customizeFilterName') }],
},
}
},
computed: {
title() {
if (this.colType === 'read_attr') {
return this.$t('cmdb.components.attributeGrant')
}
return this.$t('cmdb.components.ciGrant')
},
attrGroup() {
return this.provide_attrGroup()
},
filerPerimissions() {
return this.provide_filerPerimissions()
},
filterKey() {
if (this.colType === 'read_attr') {
return 'attr_filter'
}
return 'ci_filter'
},
},
methods: {
async open(colType, row) {
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')
})
}
if (this.filerPerimissions[row.rid]) {
const _tempValue = this.filerPerimissions[row.rid][this.filterKey]
if (_tempValue && _tempValue.length) {
this.radioValue = 2
if (this.colType === 'read_attr') {
this.selectedAttr = _tempValue
} else {
this.expression = `q=${_tempValue}`
this.form = {
name: this.filerPerimissions[row.rid].name || '',
}
this.$nextTick(() => {
this.$refs.filterComp.visibleChange(true)
})
}
}
}
},
async handleOk() {
if (this.radioValue === 1) {
await grantCiType(this.CITypeId, this.row.rid, {
perms: ['read'],
attr_filter: this.colType === 'read_attr' ? [] : undefined,
ci_filter: this.colType === 'read_ci' ? '' : undefined,
})
} else if (this.radioValue === 2) {
if (this.colType === 'read_ci') {
this.$refs.filterComp.handleSubmit()
}
await grantCiType(this.CITypeId, this.row.rid, {
perms: ['read'],
attr_filter: this.colType === 'read_attr' ? this.selectedAttr : undefined,
ci_filter: this.colType === 'read_ci' ? this.expression.slice(2) : undefined,
name: this.colType === 'read_ci' ? this.form.name : undefined,
})
} else {
const _tempValue = this.filerPerimissions?.[this.row.rid]?.[this.filterKey]
await revokeCiType(this.CITypeId, this.row.rid, {
perms: ['read'],
attr_filter: this.colType === 'read_attr' ? _tempValue : undefined,
ci_filter: this.colType === 'read_ci' ? _tempValue : undefined,
})
}
this.$emit('updateTableDataRead', this.row, this.radioValue === 1 || this.radioValue === 2)
this.handleCancel()
},
handleCancel() {
this.radioValue = 1
this.selectedAttr = []
if (this.$refs.form) {
this.$refs.form.resetFields()
}
this.visible = false
},
setExpFromFilter(filterExp) {
let expression = ''
if (filterExp) {
expression = `q=${filterExp}`
}
this.expression = expression
},
changeRadioValue(value) {
if (this.id_filter) {
this.$message.warning(this.$t('cmdb.serviceTree.grantedByServiceTreeTips'))
} else {
this.radioValue = value
}
},
},
}
</script>
<style></style>

View File

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

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

@@ -1,100 +1,100 @@
<template>
<div class="ci-relation-grant">
<vxe-table
ref="xTable"
size="mini"
stripe
class="ops-stripe-table"
:data="tableData"
:max-height="`${tableHeight}px`"
:row-style="(params) => getCurrentRowStyle(params, addedRids)"
>
<vxe-column field="name"></vxe-column>
<vxe-column v-for="col in columns" :key="col" :field="col" :title="permMap[col]">
<template #default="{row}">
<a-checkbox @change="(e) => handleChange(e, col, row)" v-model="row[col]"></a-checkbox>
</template>
</vxe-column>
</vxe-table>
<a-space>
<span class="grant-button" @click="grantDepart">{{ $t('cmdb.components.grantUser') }}</span>
<span class="grant-button" @click="grantRole">{{ $t('cmdb.components.grantRole') }}</span>
</a-space>
</div>
</template>
<script>
import { permMap } from './constants.js'
import { grantTypeRelation, revokeTypeRelation } from '../../api/CITypeRelation.js'
import { getCurrentRowStyle } from './utils'
export default {
name: 'TypeRelationGrant',
inject: ['loading', 'isModal'],
props: {
tableData: {
type: Array,
default: () => [],
},
grantType: {
type: String,
default: 'type_relation',
},
typeRelationIds: {
type: Array,
default: null,
},
addedRids: {
type: Array,
default: () => [],
},
},
data() {
return {
columns: ['create', 'grant', 'delete'],
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
tableHeight() {
if (this.isModal) {
return (this.windowHeight - 104) / 2
}
return (this.windowHeight - 104) / 2 - 116
},
permMap() {
return permMap()
}
},
methods: {
getCurrentRowStyle,
grantDepart() {
this.$emit('grantDepart', this.grantType)
},
grantRole() {
this.$emit('grantRole', this.grantType)
},
handleChange(e, col, row) {
const first = this.typeRelationIds[0]
const second = this.typeRelationIds[1]
if (e.target.checked) {
grantTypeRelation(first, second, row.rid, { perms: [col] }).catch(() => {
this.$emit('getTableData')
})
} else {
revokeTypeRelation(first, second, row.rid, { perms: [col] }).catch(() => {
this.$emit('getTableData')
})
}
},
},
}
</script>
<style lang="less" scoped>
.ci-relation-grant {
padding: 10px 0;
}
</style>
<template>
<div class="ci-relation-grant">
<vxe-table
ref="xTable"
size="mini"
stripe
class="ops-stripe-table"
:data="tableData"
:max-height="`${tableHeight}px`"
:row-style="(params) => getCurrentRowStyle(params, addedRids)"
>
<vxe-column field="name"></vxe-column>
<vxe-column v-for="col in columns" :key="col" :field="col" :title="permMap[col]">
<template #default="{row}">
<a-checkbox @change="(e) => handleChange(e, col, row)" v-model="row[col]"></a-checkbox>
</template>
</vxe-column>
</vxe-table>
<a-space>
<span class="grant-button" @click="grantDepart">{{ $t('cmdb.components.grantUser') }}</span>
<span class="grant-button" @click="grantRole">{{ $t('cmdb.components.grantRole') }}</span>
</a-space>
</div>
</template>
<script>
import { permMap } from './constants.js'
import { grantTypeRelation, revokeTypeRelation } from '../../api/CITypeRelation.js'
import { getCurrentRowStyle } from './utils'
export default {
name: 'TypeRelationGrant',
inject: ['loading', 'isModal'],
props: {
tableData: {
type: Array,
default: () => [],
},
grantType: {
type: String,
default: 'type_relation',
},
typeRelationIds: {
type: Array,
default: null,
},
addedRids: {
type: Array,
default: () => [],
},
},
data() {
return {
columns: ['create', 'grant', 'delete'],
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
tableHeight() {
if (this.isModal) {
return (this.windowHeight - 104) / 2
}
return (this.windowHeight - 104) / 2 - 116
},
permMap() {
return permMap()
}
},
methods: {
getCurrentRowStyle,
grantDepart() {
this.$emit('grantDepart', this.grantType)
},
grantRole() {
this.$emit('grantRole', this.grantType)
},
handleChange(e, col, row) {
const first = this.typeRelationIds[0]
const second = this.typeRelationIds[1]
if (e.target.checked) {
grantTypeRelation(first, second, row.rid, { perms: [col] }).catch(() => {
this.$emit('getTableData')
})
} else {
revokeTypeRelation(first, second, row.rid, { perms: [col] }).catch(() => {
this.$emit('getTableData')
})
}
},
},
}
</script>
<style lang="less" scoped>
.ci-relation-grant {
padding: 10px 0;
}
</style>

View File

@@ -1,4 +1,4 @@
export const getCurrentRowStyle = ({ row }, addedRids) => {
const idx = addedRids.findIndex(item => item.rid === row.rid)
return idx > -1 ? 'background-color:#E0E7FF!important' : ''
}
export const getCurrentRowStyle = ({ row }, addedRids) => {
const idx = addedRids.findIndex(item => item.rid === row.rid)
return idx > -1 ? 'background-color:#E0E7FF!important' : ''
}

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

@@ -1,294 +1,305 @@
<template>
<div>
<div id="search-form-bar" class="search-form-bar">
<div :style="{ display: 'inline-flex', alignItems: 'center' }">
<a-space>
<treeselect
v-if="type === 'resourceSearch'"
class="custom-treeselect"
:style="{ width: '250px', marginRight: '10px', '--custom-height': '32px' }"
v-model="currenCiType"
:multiple="true"
:clearable="true"
searchable
:options="ciTypeGroup"
:limit="1"
:limitText="(count) => `+ ${count}`"
value-consists-of="LEAF_PRIORITY"
:placeholder="$t('cmdb.ciType.ciType')"
@close="closeCiTypeGroup"
@open="openCiTypeGroup"
@input="inputCiTypeGroup"
:normalizer="
(node) => {
return {
id: node.id || -1,
label: node.alias || node.name || $t('other'),
title: node.alias || node.name || $t('other'),
children: node.ci_types,
}
}
"
>
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
{{ node.label }}
</div>
</treeselect>
<a-input
v-model="fuzzySearch"
:style="{ display: 'inline-block', width: '244px' }"
:placeholder="$t('cmdb.components.pleaseSearch')"
@pressEnter="emitRefresh"
class="ops-input ops-input-radius"
>
<a-icon
type="search"
slot="suffix"
:style="{ color: fuzzySearch ? '#2f54eb' : '', cursor: 'pointer' }"
@click="emitRefresh"
/>
<a-tooltip slot="prefix" placement="bottom" :overlayStyle="{ maxWidth: '550px' }">
<template slot="title">
{{ $t('cmdb.components.ciSearchTips') }}
</template>
<a><a-icon type="question-circle"/></a>
</a-tooltip>
</a-input>
<FilterComp
ref="filterComp"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList"
@setExpFromFilter="setExpFromFilter"
:expression="expression"
placement="bottomLeft"
>
<div slot="popover_item" class="search-form-bar-filter">
<a-icon class="search-form-bar-filter-icon" type="filter" />
{{ $t('cmdb.components.conditionFilter') }}
<a-icon class="search-form-bar-filter-icon" type="down" />
</div>
</FilterComp>
<a-input
v-if="isShowExpression"
v-model="expression"
v-show="!selectedRowKeys.length"
@focus="
() => {
isFocusExpression = true
}
"
@blur="
() => {
isFocusExpression = false
}
"
class="ci-searchform-expression"
:style="{ width }"
:placeholder="placeholder"
@keyup.enter="emitRefresh"
>
<a-icon slot="suffix" type="copy" @click="handleCopyExpression" />
</a-input>
<slot></slot>
</a-space>
</div>
<a-space>
<a-button @click="reset" size="small">{{ $t('reset') }}</a-button>
<a-tooltip :title="$t('cmdb.components.attributeDesc')" v-if="type === 'relationView'">
<a
@click="
() => {
$refs.metadataDrawer.open(typeId)
}
"
><a-icon
v-if="type === 'relationView'"
type="question-circle"
/></a>
</a-tooltip>
</a-space>
</div>
<MetadataDrawer ref="metadataDrawer" />
</div>
</template>
<script>
import _ from 'lodash'
import Treeselect from '@riophae/vue-treeselect'
import MetadataDrawer from '../../views/ci/modules/MetadataDrawer.vue'
import FilterComp from '@/components/CMDBFilterComp'
import { getCITypeGroups } from '../../api/ciTypeGroup'
export default {
name: 'SearchForm',
components: { MetadataDrawer, FilterComp, Treeselect },
props: {
preferenceAttrList: {
type: Array,
required: true,
},
isShowExpression: {
type: Boolean,
default: true,
},
typeId: {
type: Number,
default: null,
},
type: {
type: String,
default: '',
},
selectedRowKeys: {
type: Array,
default: () => [],
}
},
data() {
return {
// Advanced Search Expand/Close
advanced: false,
queryParam: {},
isFocusExpression: false,
expression: '',
fuzzySearch: '',
currenCiType: [],
ciTypeGroup: [],
lastCiType: [],
}
},
computed: {
placeholder() {
return this.isFocusExpression ? this.$t('cmdb.components.ciSearchTips2') : this.$t('cmdb.ciType.expr')
},
width() {
return '200px'
},
canSearchPreferenceAttrList() {
return this.preferenceAttrList.filter((item) => item.value_type !== '6')
},
},
watch: {
'$route.path': function(newValue, oldValue) {
this.queryParam = {}
this.expression = ''
this.fuzzySearch = ''
},
},
inject: ['setPreferenceSearchCurrent'],
mounted() {
if (this.type === 'resourceSearch') {
this.getCITypeGroups()
}
},
methods: {
getCITypeGroups() {
getCITypeGroups({ need_other: true }).then((res) => {
this.ciTypeGroup = res
.filter((item) => item.ci_types && item.ci_types.length)
.map((item) => {
item.id = `parent_${item.id || -1}`
return { ..._.cloneDeep(item) }
})
})
},
reset() {
this.queryParam = {}
this.expression = ''
this.fuzzySearch = ''
this.currenCiType = []
this.emitRefresh()
},
setExpFromFilter(filterExp) {
const regSort = /(?<=sort=).+/g
const expSort = this.expression.match(regSort) ? this.expression.match(regSort)[0] : undefined
let expression = ''
if (filterExp) {
expression = `q=${filterExp}`
}
if (expSort) {
expression += `&sort=${expSort}`
}
this.expression = expression
this.emitRefresh()
},
handleSubmit() {
this.$refs.filterComp.handleSubmit()
},
openCiTypeGroup() {
this.lastCiType = _.cloneDeep(this.currenCiType)
},
closeCiTypeGroup(value) {
if (!_.isEqual(value, this.lastCiType)) {
this.$emit('updateAllAttributesList', value)
}
},
inputCiTypeGroup(value) {
console.log(value)
if (!value || !value.length) {
this.$emit('updateAllAttributesList', value)
}
},
emitRefresh() {
this.setPreferenceSearchCurrent(null)
this.$nextTick(() => {
this.$emit('refresh', true)
})
},
handleCopyExpression() {
this.$emit('copyExpression')
},
},
}
</script>
<style lang="less">
@import '../../views/index.less';
.ci-searchform-expression {
> input {
border-bottom: 2px solid #d9d9d9;
border-top: none;
border-left: none;
border-right: none;
&:hover,
&:focus {
border-bottom: 2px solid #2f54eb;
}
&:focus {
box-shadow: 0 2px 2px -2px #1f78d133;
}
}
.ant-input-suffix {
color: #2f54eb;
cursor: pointer;
}
}
.cmdb-search-form {
.ant-form-item-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
</style>
<style lang="less" scoped>
@import '~@/style/static.less';
.search-form-bar {
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
.search-form-bar-filter {
.ops_display_wrapper();
.search-form-bar-filter-icon {
color: #custom_colors[color_1];
font-size: 12px;
}
}
}
</style>
<template>
<div>
<div id="search-form-bar" class="search-form-bar">
<div :style="{ display: 'inline-flex', alignItems: 'center' }">
<a-space>
<treeselect
v-if="type === 'resourceSearch'"
class="custom-treeselect"
:style="{ width: '250px', marginRight: '10px', '--custom-height': '32px' }"
v-model="currenCiType"
:multiple="true"
:clearable="true"
searchable
:options="ciTypeGroup"
:limit="1"
:limitText="(count) => `+ ${count}`"
value-consists-of="LEAF_PRIORITY"
:placeholder="$t('cmdb.ciType.ciType')"
@close="closeCiTypeGroup"
@open="openCiTypeGroup"
@input="inputCiTypeGroup"
:normalizer="
(node) => {
return {
id: node.id || -1,
label: node.alias || node.name || $t('other'),
title: node.alias || node.name || $t('other'),
children: node.ci_types,
}
}
"
>
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
{{ node.label }}
</div>
</treeselect>
<a-input
v-model="fuzzySearch"
:style="{ display: 'inline-block', width: '244px' }"
:placeholder="$t('cmdb.components.pleaseSearch')"
@pressEnter="emitRefresh"
class="ops-input ops-input-radius"
>
<a-icon
type="search"
slot="suffix"
:style="{ color: fuzzySearch ? '#2f54eb' : '', cursor: 'pointer' }"
@click="emitRefresh"
/>
<a-tooltip slot="prefix" placement="bottom" :overlayStyle="{ maxWidth: '550px', whiteSpace: 'pre-line' }">
<template slot="title">
{{ $t('cmdb.components.ciSearchTips') }}
</template>
<a><a-icon type="question-circle"/></a>
</a-tooltip>
</a-input>
<FilterComp
ref="filterComp"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList"
@setExpFromFilter="setExpFromFilter"
:expression="expression"
placement="bottomLeft"
>
<div slot="popover_item" class="search-form-bar-filter">
<a-icon class="search-form-bar-filter-icon" type="filter" />
{{ $t('cmdb.components.conditionFilter') }}
<a-icon class="search-form-bar-filter-icon" type="down" />
</div>
</FilterComp>
<a-input
v-if="isShowExpression"
v-model="expression"
v-show="!selectedRowKeys.length"
@focus="
() => {
isFocusExpression = true
}
"
@blur="
() => {
isFocusExpression = false
}
"
class="ci-searchform-expression"
:style="{ width }"
:placeholder="placeholder"
@keyup.enter="emitRefresh"
>
<a-icon slot="suffix" type="copy" @click="handleCopyExpression" />
</a-input>
<slot></slot>
</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
@click="
() => {
$refs.metadataDrawer.open(typeId)
}
"
><a-icon
v-if="type === 'relationView'"
type="question-circle"
/></a>
</a-tooltip>
</a-space>
</div>
<MetadataDrawer ref="metadataDrawer" />
</div>
</template>
<script>
import _ from 'lodash'
import Treeselect from '@riophae/vue-treeselect'
import MetadataDrawer from '../../views/ci/modules/MetadataDrawer.vue'
import FilterComp from '@/components/CMDBFilterComp'
import { getCITypeGroups } from '../../api/ciTypeGroup'
export default {
name: 'SearchForm',
components: { MetadataDrawer, FilterComp, Treeselect },
props: {
preferenceAttrList: {
type: Array,
required: true,
},
isShowExpression: {
type: Boolean,
default: true,
},
typeId: {
type: Number,
default: null,
},
type: {
type: String,
default: '',
},
selectedRowKeys: {
type: Array,
default: () => [],
},
},
data() {
return {
// Advanced Search Expand/Close
advanced: false,
queryParam: {},
isFocusExpression: false,
expression: '',
fuzzySearch: '',
currenCiType: [],
ciTypeGroup: [],
lastCiType: [],
}
},
computed: {
placeholder() {
return this.isFocusExpression ? this.$t('cmdb.components.ciSearchTips2') : this.$t('cmdb.ciType.expr')
},
width() {
return '200px'
},
canSearchPreferenceAttrList() {
return this.preferenceAttrList.filter((item) => item.value_type !== '6')
},
},
watch: {
'$route.path': function(newValue, oldValue) {
this.queryParam = {}
this.expression = ''
this.fuzzySearch = ''
},
},
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
.filter((item) => item.ci_types && item.ci_types.length)
.map((item) => {
item.id = `parent_${item.id || -1}`
return { ..._.cloneDeep(item) }
})
})
},
reset() {
this.queryParam = {}
this.expression = ''
this.fuzzySearch = ''
this.currenCiType = []
this.emitRefresh()
},
setExpFromFilter(filterExp) {
const regSort = /(?<=sort=).+/g
const expSort = this.expression.match(regSort) ? this.expression.match(regSort)[0] : undefined
let expression = ''
if (filterExp) {
expression = `q=${filterExp}`
}
if (expSort) {
expression += `&sort=${expSort}`
}
this.expression = expression
this.emitRefresh()
},
handleSubmit() {
this.$refs.filterComp.handleSubmit()
},
openCiTypeGroup() {
this.lastCiType = _.cloneDeep(this.currenCiType)
},
closeCiTypeGroup(value) {
if (!_.isEqual(value, this.lastCiType)) {
this.$emit('updateAllAttributesList', value)
}
},
inputCiTypeGroup(value) {
console.log(value)
if (!value || !value.length) {
this.$emit('updateAllAttributesList', value)
}
},
emitRefresh() {
if (this.setPreferenceSearchCurrent) {
this.setPreferenceSearchCurrent(null)
}
this.$nextTick(() => {
this.$emit('refresh', true)
})
},
handleCopyExpression() {
this.$emit('copyExpression')
},
},
}
</script>
<style lang="less">
@import '../../views/index.less';
.ci-searchform-expression {
> input {
border-bottom: 2px solid #d9d9d9;
border-top: none;
border-left: none;
border-right: none;
&:hover,
&:focus {
border-bottom: 2px solid #2f54eb;
}
&:focus {
box-shadow: 0 2px 2px -2px #1f78d133;
}
}
.ant-input-suffix {
color: #2f54eb;
cursor: pointer;
}
}
.cmdb-search-form {
.ant-form-item-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
</style>
<style lang="less" scoped>
@import '~@/style/static.less';
.search-form-bar {
margin-bottom: 10px;
display: flex;
justify-content: space-between;
align-items: center;
.search-form-bar-filter {
.ops_display_wrapper();
.search-form-bar-filter-icon {
color: #custom_colors[color_1];
font-size: 12px;
}
}
}
</style>

View File

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

View File

@@ -1,483 +1,503 @@
const cmdb_en = {
relation: 'Relation',
attribute: 'Attributes',
menu: {
views: 'Views',
config: 'Configuration',
backend: 'Management',
ciTable: 'Resource Views',
ciTree: 'Tree Views',
ciSearch: 'Search',
adCIs: 'AutoDiscovery Pool',
preference: 'Preference',
batchUpload: 'Batch Import',
citypeManage: 'Modeling',
backendManage: 'Backend',
customDashboard: 'Custom Dashboard',
serviceTreeDefine: 'Service Tree',
citypeRelation: 'CIType Relation',
operationHistory: 'Operation Audit',
relationType: 'Relation Type',
ad: 'AutoDiscovery',
},
ciType: {
ciType: 'CIType',
attributes: 'Attributes',
relation: 'Relation',
trigger: 'Triggers',
attributeAD: 'Attributes AutoDiscovery',
relationAD: 'Relation AutoDiscovery',
grant: 'Grant',
addGroup: 'New Group',
editGroup: 'Edit Group',
group: 'Group',
attributeLibray: 'Attribute Library',
addCITypeInGroup: 'Add a new CIType to the group',
addCIType: 'Add CIType',
editGroupName: 'Edit group name',
deleteGroup: 'Delete this group',
CITypeName: 'Name(English)',
English: 'English',
inputAttributeName: 'Please enter the attribute name',
attributeNameTips: 'It cannot start with a number, it can be English numbers and underscores (_)',
editCIType: 'Edit CIType',
defaultSort: 'Default sort',
selectDefaultOrderAttr: 'Select default sorting attributes',
asec: 'Forward order',
desc: 'Reverse order',
uniqueKey: 'Uniquely Identifies',
uniqueKeySelect: 'Please select a unique identifier',
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}]?',
confirmDeleteCIType: 'Are you sure you want to delete model [{typeName}]?',
uploading: 'Uploading',
uploadFailed: 'Upload failed, please try again later',
addPlugin: 'New plugin',
deletePlugin: 'Delete plugin',
confirmDeleteADT: 'Do you confirm to delete [{pluginName}]',
attributeMap: 'Attribute mapping',
autoDiscovery: 'AutoDiscovery',
node: 'Node',
adExecConfig: 'Execute configuration',
adExecTarget: 'Execute targets',
oneagentIdTips: 'Please enter the hexadecimal OneAgent ID starting with 0x',
selectFromCMDBTips: 'Select from CMDB ',
adAutoInLib: 'Save as CI auto',
adInterval: 'Collection frequency',
byInterval: 'by interval',
allNodes: 'All nodes',
specifyNodes: 'Specify Node',
specifyNodesTips: 'Please fill in the specify node!',
username: 'Username',
password: 'Password',
link: 'Link',
list: 'List',
listTips: 'The value of the field is one or more, and the type of the value returned by the interface is list.',
computeForAllCITips: 'All CI trigger computes',
confirmcomputeForAllCITips: 'Confirm triggering computes for all CIs?',
isUnique: 'Is it unique',
unique: 'Unique',
isChoice: 'Choiced',
defaultShow: 'Default Display',
defaultShowTips: 'The CI instance table displays this field by default',
isSortable: 'Sortable',
isIndex: 'Indexed',
index: 'Index',
indexTips: 'Fields can be used for retrieval to speed up queries',
confirmDelete: 'Confirm to delete [{name}]?',
confirmDelete2: 'Confirm to delete?',
computeSuccess: 'Triggered successfully!',
basicConfig: 'Basic Settings',
AttributeName: 'Name(English)',
DataType: 'Data Type',
defaultValue: 'Default value',
autoIncID: 'Auto-increment ID',
customTime: 'Custom time',
advancedSettings: 'Advanced Settings',
font: 'Font',
color: 'Color',
choiceValue: 'Predefined value',
computedAttribute: 'Computed Attribute',
computedAttributeTips: 'The value of this attribute is calculated through an expression constructed from other attributes of the CIType or by executing a piece of code. The reference method of the attribute is: {{ attribute name }}',
addAttribute: 'New attribute',
existedAttributes: 'Already have attributes',
editAttribute: 'Edit attribute',
addAttributeTips1: 'If sorting is selected, it must also be selected!',
uniqueConstraint: 'Unique Constraint',
up: 'Move up',
down: 'Move down',
selectAttribute: 'Select Attribute',
groupExisted: 'Group name already exists',
attributeSortedTips: 'Attributes in other groups cannot be sorted. If you need to sort, please drag them to a custom group first!',
buildinAttribute: 'built-in attributes',
expr: 'Expression',
code: 'Code',
apply: 'apply',
continueAdd: 'Keep adding',
filter: 'Filter',
choiceOther: 'Other CIType Attributes',
choiceWebhookTips: 'The returned results are filtered by fields, and the hierarchical nesting is separated by ##, such as k1##k2. The web request returns {k1: [{k2: 1}, {k2: 2}]}, and the parsing result is [1, 2 ]',
selectCIType: 'Please select a CMDB CIType',
selectCITypeAttributes: 'Please select CIType attributes',
selectAttributes: 'Please select attributes',
choiceScriptDemo: 'class ChoiceValue(object):\n @staticmethod\n def values():\n """\n Execution entry, returns predefined value\n :return: Returns a list, the type of the value is the same as the type of the attribute\n For example:\n return ["online", "offline"]\n """\n return []',
valueExisted: 'The current value already exists!',
addRelation: 'Add Relation',
sourceCIType: 'Source CIType',
sourceCITypeTips: 'Please select Source CIType',
dstCIType: 'Target CIType',
dstCITypeTips: 'Please select target CIType',
relationType: 'Relation Type',
relationTypeTips: 'Please select relation type',
isParent: 'is parent',
relationConstraint: 'Constraints',
relationConstraintTips: 'please select a relationship constraint',
one2Many: 'One to Many',
one2One: 'One to One',
many2Many: 'Many to Many',
basicInfo: 'Basic Information',
nameInputTips: 'Please enter name',
triggerDataChange: 'Data changes',
triggerDate: 'Date attribute',
triggerEnable: 'Turn on',
descInput: 'Please enter remarks',
triggerCondition: 'Triggering conditions',
addInstance: 'Add new instance',
deleteInstance: 'Delete instance',
changeInstance: 'Instance changes',
selectMutipleAttributes: 'Please select attributes (multiple selections)',
selectSingleAttribute: 'Please select an attribute (single choice)',
beforeDays: 'ahead of time',
days: 'Days',
notifyAt: 'Send time',
notify: 'Notify',
triggerAction: 'Trigger action',
receivers: 'Recipients',
emailTips: 'Please enter your email address, separate multiple email addresses with ;',
customEmail: 'Custom recipients',
notifySubject: 'Notification title',
notifySubjectTips: 'Please enter notification title',
notifyContent: 'Content',
notifyMethod: 'Notify methods',
botSelect: 'Please select a robot',
refAttributeTips: 'The title and content can reference the attribute value of the CIType. The reference method is: {{ attr_name }}',
webhookRefAttributeTips: 'Request parameters can reference the attribute value of the model. The reference method is: {{ attr_name }}',
newTrigger: 'Add trigger',
editTriggerTitle: 'Edit trigger {name}',
newTriggerTitle: 'Add trigger {name}',
confirmDeleteTrigger: 'Are you sure to delete this trigger?',
int: 'Integer',
float: 'Float',
text: 'Text',
datetime: 'DateTime',
date: 'Date',
time: 'Time',
json: 'JSON',
event: 'Event'
},
components: {
unselectAttributes: 'Unselected',
selectAttributes: 'Selected',
downloadCI: 'Export data',
filename: 'Filename',
filenameInputTips: 'Please enter filename',
saveType: 'Save type',
saveTypeTips: 'Please select save type',
xlsx: 'Excel workbook (*.xlsx)',
csv: 'CSV (comma separated) (*.csv)',
html: 'Web page (*.html)',
xml: 'XML data (*.xml)',
txt: 'Text file (tab delimited) (*.txt)',
grantUser: 'Grant User/Department',
grantRole: 'Grant Role',
confirmRevoke: 'Confirm to delete the [Authorization] permission of [{name}]?',
readAttribute: 'View Attributes',
readCI: 'View CIs',
config: 'Configuration',
ciTypeGrant: 'Grant CIType',
ciGrant: 'Grant CI',
attributeGrant: 'Grant Attribute',
relationGrant: 'Grant Relation',
perm: 'Permissions',
all: 'All',
customize: 'Customize',
none: 'None',
customizeFilterName: 'Please enter a custom filter name',
colorPickerError: 'Initialization color format error, use #fff or rgb format',
example: 'Example value',
aliyun: 'aliyun',
tencentcloud: 'Tencent Cloud',
huaweicloud: 'Huawei Cloud',
beforeChange: 'Before change',
afterChange: 'After change',
noticeContentTips: 'Please enter notification content',
saveQuery: 'Save Filters',
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',
ciSearchTips2: 'For example: q=hostname:*0.0.0.0*',
subCIType: 'Subscription CIType',
already: 'already',
not: 'not',
sub: 'subscription',
selectBelow: 'Please select below',
subSuccess: 'Subscription successful',
selectMethods: 'Please select a method',
noAuthRequest: 'No certification requested yet',
noParamRequest: 'No parameter certification yet',
requestParam: 'Request parameters',
param: 'Parameter{param}',
value: 'Value{value}',
clear: 'Clear',
},
batch: {
downloadFailed: 'Download failed',
unselectCIType: 'No CIType selected yet',
pleaseUploadFile: 'Please upload files',
batchUploadCanceled: 'Batch upload canceled',
selectCITypeTips: 'Please select CIType',
downloadTemplate: 'Download Template',
drawTips: 'Click or drag files here to upload!',
supportFileTypes: 'Supported file types: xls, xlsx',
uploadResult: 'Upload results',
total: 'total',
successItems: 'items, succeeded',
failedItems: 'items, failed',
items: 'items',
errorTips: 'Error message',
requestFailedTips: 'An error occurred with the request, please try again later',
requestSuccessTips: 'Upload completed',
},
preference: {
mySub: 'My Subscription',
sub: 'Subscribe',
cancelSub: 'Unsubscribe',
editSub: 'Edit subscription',
peopleSub: ' people subscribed',
noSub: 'No subscribed',
cancelSubSuccess: 'Unsubscribe successfully',
confirmcancelSub: 'Are you sure to cancel your subscription?',
confirmcancelSub2: 'Are you sure you want to unsubscribe {name}?',
of: 'of',
hoursAgo: 'hours ago',
daysAgo: 'days ago',
monthsAgo: 'month ago',
yearsAgo: 'years ago',
just: 'just now',
},
custom_dashboard: {
charts: 'Chart',
newChart: 'Add Chart',
editChart: 'Edit Chart',
title: 'Title',
titleTips: 'Please enter a chart title',
calcIndicators: 'Counter',
dimensions: 'Dimensions',
selectDimensions: 'Please select a dimension',
quantity: 'Quantity',
childCIType: 'Relational CIType',
level: 'Level',
levelTips: 'Please enter the relationship level',
preview: 'Preview',
showIcon: 'Display icon',
chartType: 'Chart Type',
dataFilter: 'Data Filtering',
format: 'Formats',
fontColor: 'Font Color',
backgroundColor: 'Background',
chartColor: 'Chart Color',
chartLength: 'Length',
barType: 'Bar Type',
stackedBar: 'Stacked Bar',
multipleSeriesBar: 'Multiple Series Bar ',
axis: 'Axis',
direction: 'Direction',
lowerShadow: 'Lower Shadow',
count: 'Indicator',
bar: 'Bar',
line: 'Line',
pie: 'Pie',
table: 'Table',
default: 'default',
relation: 'Relation',
noCustomDashboard: 'The administrator has not customized the dashboard yet',
},
preference_relation: {
newServiceTree: 'Add ServiceTree',
serviceTreeName: 'Name',
public: 'Public',
saveLayout: 'Save Layout',
childNodesNotFound: 'There are no child nodes and no business relationship can be formed. Please select again!',
tips1: 'Cannot form a view with the currently selected node, please select again!',
tips2: 'Please enter the new serviceTree name!',
tips3: 'Please select at least two nodes!',
},
history: {
ciChange: 'CI',
relationChange: 'Relation',
ciTypeChange: 'CIType',
triggerHistory: 'Triggers',
opreateTime: 'Operate Time',
user: 'User',
userTips: 'Enter filter username',
filter: 'Search',
filterOperate: 'fitler operation',
attribute: 'Attribute',
old: 'Old',
new: 'New',
noUpdate: 'No update',
itemsPerPage: '/page',
triggerName: 'Name',
event: 'Event',
action: 'Actoin',
status: 'Status',
done: 'Done',
undone: 'Undone',
triggerTime: 'Trigger Time',
totalItems: '{total} records in total',
pleaseSelect: 'Please select',
startTime: 'Start Time',
endTime: 'End Time',
deleteCIType: 'Delete CIType',
addCIType: 'Add CIType',
updateCIType: 'Update CIType',
addAttribute: 'Add Attribute',
updateAttribute: 'Update Attribute',
deleteAttribute: 'Delete Attribute',
addTrigger: 'Add Trigger',
updateTrigger: 'Update Trigger',
deleteTrigger: 'Delete Trigger',
addUniqueConstraint: 'Add Unique Constraint',
updateUniqueConstraint: 'Update Unique Constraint',
deleteUniqueConstraint: 'Delete Unique Constraint',
addRelation: 'Add Relation',
deleteRelation: 'Delete Relation',
noModifications: 'No Modifications',
attr: 'attribute',
attrId: 'attribute id',
changeDescription: 'attribute id: {attr_id}, {before_days} day(s) in advance, Subject: {subject}\nContent: {body}\nNotify At: {notify_at}'
},
relation_type: {
addRelationType: 'New',
nameTips: 'Please enter a type name',
},
ad: {
upload: 'Import',
download: 'Export',
accpet: 'Accept',
accpetBy: 'Accept By',
acceptTime: 'Accept Time',
confirmAccept: 'Confirm Accept?',
accpetSuccess: 'Accept successfully',
isAccpet: 'Is accept',
deleteADC: 'Confirm to delete this data?',
batchDelete: 'Confirm to delete this data?',
agent: 'Built-in & Plug-ins',
snmp: 'Network Devices',
http: 'Public Clouds',
rule: 'AutoDiscovery Rules',
timeout: 'Timeout error',
mode: 'Mode',
collectSettings: 'Collection Settings',
updateFields: 'Update Field',
pluginScript: `# -*- coding:utf-8 -*-
import json
class AutoDiscovery(object):
@property
def unique_key(self):
"""
:return: Returns the name of a unique attribute
"""
return
@staticmethod
def attributes():
"""
Define attribute fields
:return: Returns a list of attribute fields. The list items are (name, type, description). The name must be in English.
type: String Integer Float Date DateTime Time JSON
For example:
return [
("ci_type", "String", "CIType name"),
("private_ip", "String", "Internal IP, multiple values separated by commas")
]
"""
return []
@staticmethod
def run():
"""
Execution entry, returns collected attribute values
:return:
Returns a list, the list item is a dictionary, the dictionary key is the attribute name, and the value is the attribute value
For example:
return [dict(ci_type="server", private_ip="192.168.1.1")]
"""
return []
if __name__ == "__main__":
result = AutoDiscovery().run()
if isinstance(result, list):
print("AutoDiscovery::Result::{}".format(json.dumps(result)))
else:
print("ERROR: The collection return must be a list")
`,
server: 'Server',
vserver: 'VServer',
nic: 'NIC',
disk: 'harddisk',
},
ci: {
attributeDesc: 'Attribute Description',
selectRows: 'Select: {rows} items',
addRelation: 'Add Relation',
all: 'All',
batchUpdate: 'Batch Update',
batchUpdateConfirm: 'Are you sure you want to make batch updates?',
batchUpdateInProgress: 'Currently being updated in batches',
batchUpdateInProgress2: 'Updating in batches, {total} in total, {successNum} successful, {errorNum} failed',
batchDeleting: 'Deleting...',
batchDeleting2: 'Deleting {total} items in total, {successNum} items successful, {errorNum} items failed',
copyFailed: 'Copy failed',
noLevel: 'No hierarchical relationship!',
batchAddRelation: 'Batch Add Relation',
history: 'History',
topo: 'Topology',
table: 'Table',
m2mTips: 'The current CIType relationship is many-to-many, please go to the SerivceTree(relation view) to add or delete',
confirmDeleteRelation: 'Confirm to delete the relationship?',
tips1: 'Use commas to separate multiple values',
tips2: 'The field can be modified as needed. When the value is empty, the field will be left empty.',
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',
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',
},
serviceTree: {
deleteNode: 'Delete Node',
tips1: 'For example: q=os_version:centos&sort=os_version',
tips2: 'Expression search',
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?',
},
tree: {
tips1: 'Please go to Preference page first to complete your subscription!',
subSettings: 'Settings',
}
}
export default cmdb_en
const cmdb_en = {
relation: 'Relation',
attribute: 'Attributes',
menu: {
views: 'Views',
config: 'Configuration',
backend: 'Management',
ciTable: 'Resource Views',
ciTree: 'Tree Views',
ciSearch: 'Search',
adCIs: 'AutoDiscovery Pool',
preference: 'Preference',
batchUpload: 'Batch Import',
citypeManage: 'Modeling',
backendManage: 'Backend',
customDashboard: 'Custom Dashboard',
serviceTreeDefine: 'Service Tree',
citypeRelation: 'CIType Relation',
operationHistory: 'Operation Audit',
relationType: 'Relation Type',
ad: 'AutoDiscovery',
cidetail: 'CI Detail'
},
ciType: {
ciType: 'CIType',
attributes: 'Attributes',
relation: 'Relation',
trigger: 'Triggers',
attributeAD: 'Attributes AutoDiscovery',
relationAD: 'Relation AutoDiscovery',
grant: 'Grant',
addGroup: 'New Group',
editGroup: 'Edit Group',
group: 'Group',
attributeLibray: 'Attribute Library',
addCITypeInGroup: 'Add a new CIType to the group',
addCIType: 'Add CIType',
editGroupName: 'Edit group name',
deleteGroup: 'Delete this group',
CITypeName: 'Name(English)',
English: 'English',
inputAttributeName: 'Please enter the attribute name',
attributeNameTips: 'It cannot start with a number, it can be English numbers and underscores (_)',
editCIType: 'Edit CIType',
defaultSort: 'Default sort',
selectDefaultOrderAttr: 'Select default sorting attributes',
asec: 'Forward order',
desc: 'Reverse order',
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}]?',
confirmDeleteCIType: 'Are you sure you want to delete model [{typeName}]?',
uploading: 'Uploading',
uploadFailed: 'Upload failed, please try again later',
addPlugin: 'New plugin',
deletePlugin: 'Delete plugin',
confirmDeleteADT: 'Do you confirm to delete [{pluginName}]',
attributeMap: 'Attribute mapping',
autoDiscovery: 'AutoDiscovery',
node: 'Node',
adExecConfig: 'Execute configuration',
adExecTarget: 'Execute targets',
oneagentIdTips: 'Please enter the hexadecimal OneAgent ID starting with 0x',
selectFromCMDBTips: 'Select from CMDB ',
adAutoInLib: 'Save as CI auto',
adInterval: 'Collection frequency',
byInterval: 'by interval',
allNodes: 'All nodes',
specifyNodes: 'Specify Node',
specifyNodesTips: 'Please fill in the specify node!',
username: 'Username',
password: 'Password',
link: 'Link',
list: 'List',
listTips: 'The value of the field is one or more, and the type of the value returned by the interface is list.',
computeForAllCITips: 'All CI trigger computes',
confirmcomputeForAllCITips: 'Confirm triggering computes for all CIs?',
isUnique: 'Is it unique',
unique: 'Unique',
isChoice: 'Choiced',
defaultShow: 'Default Display',
defaultShowTips: 'The CI instance table displays this field by default',
isSortable: 'Sortable',
isIndex: 'Indexed',
index: 'Index',
indexTips: 'Fields can be used for retrieval to speed up queries',
confirmDelete: 'Confirm to delete [{name}]?',
confirmDelete2: 'Confirm to delete?',
computeSuccess: 'Triggered successfully!',
basicConfig: 'Basic Settings',
AttributeName: 'Name(English)',
DataType: 'Data Type',
defaultValue: 'Default value',
autoIncID: 'Auto-increment ID',
customTime: 'Custom time',
advancedSettings: 'Advanced Settings',
font: 'Font',
color: 'Color',
choiceValue: 'Predefined value',
computedAttribute: 'Computed Attribute',
computedAttributeTips: 'The value of this attribute is calculated through an expression constructed from other attributes of the CIType or by executing a piece of code. The reference method of the attribute is: {{ attribute name }}',
addAttribute: 'New attribute',
existedAttributes: 'Already have attributes',
editAttribute: 'Edit attribute',
addAttributeTips1: 'If sorting is selected, it must also be selected!',
uniqueConstraint: 'Unique Constraint',
up: 'Move up',
down: 'Move down',
selectAttribute: 'Select Attribute',
groupExisted: 'Group name already exists',
attributeSortedTips: 'Attributes in other groups cannot be sorted. If you need to sort, please drag them to a custom group first!',
buildinAttribute: 'built-in attributes',
expr: 'Expression',
code: 'Code',
apply: 'apply',
continueAdd: 'Keep adding',
filter: 'Filter',
choiceOther: 'Other CIType Attributes',
choiceWebhookTips: 'The returned results are filtered by fields, and the hierarchical nesting is separated by ##, such as k1##k2. The web request returns {k1: [{k2: 1}, {k2: 2}]}, and the parsing result is [1, 2 ]',
selectCIType: 'Please select a CMDB CIType',
selectCITypeAttributes: 'Please select CIType attributes',
selectAttributes: 'Please select attributes',
choiceScriptDemo: 'class ChoiceValue(object):\n @staticmethod\n def values():\n """\n Execution entry, returns predefined value\n :return: Returns a list, the type of the value is the same as the type of the attribute\n For example:\n return ["online", "offline"]\n """\n return []',
valueExisted: 'The current value already exists!',
addRelation: 'Add Relation',
sourceCIType: 'Source CIType',
sourceCITypeTips: 'Please select Source CIType',
dstCIType: 'Target CIType',
dstCITypeTips: 'Please select target CIType',
relationType: 'Relation Type',
relationTypeTips: 'Please select relation type',
isParent: 'is parent',
relationConstraint: 'Constraints',
relationConstraintTips: 'please select a relationship constraint',
one2Many: 'One to Many',
one2One: 'One to One',
many2Many: 'Many to Many',
basicInfo: 'Basic Information',
nameInputTips: 'Please enter name',
triggerDataChange: 'Data changes',
triggerDate: 'Date attribute',
triggerEnable: 'Turn on',
descInput: 'Please enter remarks',
triggerCondition: 'Triggering conditions',
addInstance: 'Add new instance',
deleteInstance: 'Delete instance',
changeInstance: 'Instance changes',
selectMutipleAttributes: 'Please select attributes (multiple selections)',
selectSingleAttribute: 'Please select an attribute (single choice)',
beforeDays: 'ahead of time',
days: 'Days',
notifyAt: 'Send time',
notify: 'Notify',
triggerAction: 'Trigger action',
receivers: 'Recipients',
emailTips: 'Please enter your email address, separate multiple email addresses with ;',
customEmail: 'Custom recipients',
notifySubject: 'Notification title',
notifySubjectTips: 'Please enter notification title',
notifyContent: 'Content',
notifyMethod: 'Notify methods',
botSelect: 'Please select a robot',
refAttributeTips: 'The title and content can reference the attribute value of the CIType. The reference method is: {{ attr_name }}',
webhookRefAttributeTips: 'Request parameters can reference the attribute value of the model. The reference method is: {{ attr_name }}',
newTrigger: 'Add trigger',
editTriggerTitle: 'Edit trigger {name}',
newTriggerTitle: 'Add trigger {name}',
confirmDeleteTrigger: 'Are you sure to delete this trigger?',
int: 'Integer',
float: 'Float',
text: 'Text',
datetime: 'DateTime',
date: 'Date',
time: 'Time',
json: 'JSON',
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',
selectAttributes: 'Selected',
downloadCI: 'Export data',
filename: 'Filename',
filenameInputTips: 'Please enter filename',
saveType: 'Save type',
saveTypeTips: 'Please select save type',
xlsx: 'Excel workbook (*.xlsx)',
csv: 'CSV (comma separated) (*.csv)',
html: 'Web page (*.html)',
xml: 'XML data (*.xml)',
txt: 'Text file (tab delimited) (*.txt)',
grantUser: 'Grant User/Department',
grantRole: 'Grant Role',
confirmRevoke: 'Confirm to delete the [Authorization] permission of [{name}]?',
readAttribute: 'View Attributes',
readCI: 'View CIs',
config: 'Configuration',
ciTypeGrant: 'Grant CIType',
ciGrant: 'Grant CI',
attributeGrant: 'Grant Attribute',
relationGrant: 'Grant Relation',
perm: 'Permissions',
all: 'All',
customize: 'Customize',
none: 'None',
customizeFilterName: 'Please enter a custom filter name',
colorPickerError: 'Initialization color format error, use #fff or rgb format',
example: 'Example value',
aliyun: 'aliyun',
tencentcloud: 'Tencent Cloud',
huaweicloud: 'Huawei Cloud',
beforeChange: 'Before change',
afterChange: 'After change',
noticeContentTips: 'Please enter notification content',
saveQuery: 'Save Filters',
pleaseSearch: 'Please search',
conditionFilter: 'Conditional filtering',
attributeDesc: 'Attribute Description',
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',
not: 'not',
sub: 'subscription',
selectBelow: 'Please select below',
subSuccess: 'Subscription successful',
selectMethods: 'Please select a method',
noAuthRequest: 'No certification requested yet',
noParamRequest: 'No parameter certification yet',
requestParam: 'Request parameters',
param: 'Parameter{param}',
value: 'Value{value}',
clear: 'Clear',
},
batch: {
downloadFailed: 'Download failed',
unselectCIType: 'No CIType selected yet',
pleaseUploadFile: 'Please upload files',
batchUploadCanceled: 'Batch upload canceled',
selectCITypeTips: 'Please select CIType',
downloadTemplate: 'Download Template',
drawTips: 'Click or drag files here to upload!',
supportFileTypes: 'Supported file types: xls, xlsx',
uploadResult: 'Upload results',
total: 'total',
successItems: 'items, succeeded',
failedItems: 'items, failed',
items: 'items',
errorTips: 'Error message',
requestFailedTips: 'An error occurred with the request, please try again later',
requestSuccessTips: 'Upload completed',
},
preference: {
mySub: 'My Subscription',
sub: 'Subscribe',
cancelSub: 'Unsubscribe',
editSub: 'Edit subscription',
peopleSub: ' people subscribed',
noSub: 'No subscribed',
cancelSubSuccess: 'Unsubscribe successfully',
confirmcancelSub: 'Are you sure to cancel your subscription?',
confirmcancelSub2: 'Are you sure you want to unsubscribe {name}?',
of: 'of',
hoursAgo: 'hours ago',
daysAgo: 'days ago',
monthsAgo: 'month ago',
yearsAgo: 'years ago',
just: 'just now',
},
custom_dashboard: {
charts: 'Chart',
newChart: 'Add Chart',
editChart: 'Edit Chart',
title: 'Title',
titleTips: 'Please enter a chart title',
calcIndicators: 'Counter',
dimensions: 'Dimensions',
selectDimensions: 'Please select a dimension',
quantity: 'Quantity',
childCIType: 'Relational CIType',
level: 'Level',
levelTips: 'Please enter the relationship level',
preview: 'Preview',
showIcon: 'Display icon',
chartType: 'Chart Type',
dataFilter: 'Data Filtering',
format: 'Formats',
fontColor: 'Font Color',
backgroundColor: 'Background',
chartColor: 'Chart Color',
chartLength: 'Length',
barType: 'Bar Type',
stackedBar: 'Stacked Bar',
multipleSeriesBar: 'Multiple Series Bar ',
axis: 'Axis',
direction: 'Direction',
lowerShadow: 'Lower Shadow',
count: 'Indicator',
bar: 'Bar',
line: 'Line',
pie: 'Pie',
table: 'Table',
default: 'default',
relation: 'Relation',
noCustomDashboard: 'The administrator has not customized the dashboard yet',
},
preference_relation: {
newServiceTree: 'Add ServiceTree',
serviceTreeName: 'Name',
public: 'Public',
saveLayout: 'Save Layout',
childNodesNotFound: 'There are no child nodes and no business relationship can be formed. Please select again!',
tips1: 'Cannot form a view with the currently selected node, please select again!',
tips2: 'Please enter the new serviceTree name!',
tips3: 'Please select at least two nodes!',
},
history: {
ciChange: 'CI',
relationChange: 'Relation',
ciTypeChange: 'CIType',
triggerHistory: 'Triggers',
opreateTime: 'Operate Time',
user: 'User',
userTips: 'Enter filter username',
filter: 'Search',
filterOperate: 'fitler operation',
attribute: 'Attribute',
old: 'Old',
new: 'New',
noUpdate: 'No update',
itemsPerPage: '/page',
triggerName: 'Name',
event: 'Event',
action: 'Actoin',
status: 'Status',
done: 'Done',
undone: 'Undone',
triggerTime: 'Trigger Time',
totalItems: '{total} records in total',
pleaseSelect: 'Please select',
startTime: 'Start Time',
endTime: 'End Time',
deleteCIType: 'Delete CIType',
addCIType: 'Add CIType',
updateCIType: 'Update CIType',
addAttribute: 'Add Attribute',
updateAttribute: 'Update Attribute',
deleteAttribute: 'Delete Attribute',
addTrigger: 'Add Trigger',
updateTrigger: 'Update Trigger',
deleteTrigger: 'Delete Trigger',
addUniqueConstraint: 'Add Unique Constraint',
updateUniqueConstraint: 'Update Unique Constraint',
deleteUniqueConstraint: 'Delete Unique Constraint',
addRelation: 'Add Relation',
deleteRelation: 'Delete Relation',
noModifications: 'No Modifications',
attr: 'attribute',
attrId: 'attribute id',
changeDescription: 'attribute id: {attr_id}, {before_days} day(s) in advance, Subject: {subject}\nContent: {body}\nNotify At: {notify_at}'
},
relation_type: {
addRelationType: 'New',
nameTips: 'Please enter a type name',
},
ad: {
upload: 'Import',
download: 'Export',
accept: 'Accept',
acceptBy: 'Accept By',
acceptTime: 'Accept Time',
confirmAccept: 'Confirm Accept?',
acceptSuccess: 'Accept successfully',
isAccept: 'Is accept',
deleteADC: 'Confirm to delete this data?',
batchDelete: 'Confirm to delete this data?',
agent: 'Built-in & Plug-ins',
snmp: 'Network Devices',
http: 'Public Clouds',
rule: 'AutoDiscovery Rules',
timeout: 'Timeout error',
mode: 'Mode',
collectSettings: 'Collection Settings',
updateFields: 'Update Field',
pluginScript: `# -*- coding:utf-8 -*-
import json
class AutoDiscovery(object):
@property
def unique_key(self):
"""
:return: Returns the name of a unique attribute
"""
return
@staticmethod
def attributes():
"""
Define attribute fields
:return: Returns a list of attribute fields. The list items are (name, type, description). The name must be in English.
type: String Integer Float Date DateTime Time JSON
For example:
return [
("ci_type", "String", "CIType name"),
("private_ip", "String", "Internal IP, multiple values separated by commas")
]
"""
return []
@staticmethod
def run():
"""
Execution entry, returns collected attribute values
:return:
Returns a list, the list item is a dictionary, the dictionary key is the attribute name, and the value is the attribute value
For example:
return [dict(ci_type="server", private_ip="192.168.1.1")]
"""
return []
if __name__ == "__main__":
result = AutoDiscovery().run()
if isinstance(result, list):
print("AutoDiscovery::Result::{}".format(json.dumps(result)))
else:
print("ERROR: The collection return must be a list")
`,
server: 'Server',
vserver: 'VServer',
nic: 'NIC',
disk: 'harddisk',
},
ci: {
attributeDesc: 'Attribute Description',
selectRows: 'Select: {rows} items',
addRelation: 'Add Relation',
all: 'All',
batchUpdate: 'Batch Update',
batchUpdateConfirm: 'Are you sure you want to make batch updates?',
batchUpdateInProgress: 'Currently being updated in batches',
batchUpdateInProgress2: 'Updating in batches, {total} in total, {successNum} successful, {errorNum} failed',
batchDeleting: 'Deleting...',
batchDeleting2: 'Deleting {total} items in total, {successNum} items successful, {errorNum} items failed',
copyFailed: 'Copy failed',
noLevel: 'No hierarchical relationship!',
batchAddRelation: 'Batch Add Relation',
history: 'History',
topo: 'Topology',
table: 'Table',
m2mTips: 'The current CIType relationship is many-to-many, please go to the SerivceTree(relation view) to add or delete',
confirmDeleteRelation: 'Confirm to delete the relationship?',
tips1: 'Use commas to separate multiple values',
tips2: 'The field can be modified as needed. When the value is empty, the field will be left empty.',
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/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',
tips1: 'For example: q=os_version:centos&sort=os_version',
tips2: 'Expression search',
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!',
subSettings: 'Settings',
}
}
export default cmdb_en

View File

@@ -1,482 +1,502 @@
const cmdb_zh = {
relation: '关系',
attribute: '属性',
menu: {
views: '视图',
config: '配置',
backend: '管理端',
ciTable: '资源数据',
ciTree: '资源层级',
ciSearch: '资源搜索',
adCIs: '自动发现池',
preference: '我的订阅',
batchUpload: '批量导入',
citypeManage: '模型配置',
backendManage: '后台管理',
customDashboard: '定制仪表盘',
serviceTreeDefine: '服务树定义',
citypeRelation: '模型关系',
operationHistory: '操作审计',
relationType: '关系类型',
ad: '自动发现',
},
ciType: {
ciType: '模型',
attributes: '模型属性',
relation: '模型关联',
trigger: '触发器',
attributeAD: '属性自动发现',
relationAD: '关系自动发现',
grant: '权限配置',
addGroup: '新增分组',
editGroup: '修改分组',
group: '分组',
attributeLibray: '属性库',
addCITypeInGroup: '在该组中新增CI模型',
addCIType: '新增CI模型',
editGroupName: '编辑组名称',
deleteGroup: '删除该组',
CITypeName: '模型名(英文)',
English: '英文',
inputAttributeName: '请输入属性名',
attributeNameTips: '不能以数字开头,可以是英文 数字以及下划线 (_)',
editCIType: '编辑模型',
defaultSort: '默认排序',
selectDefaultOrderAttr: '选择默认排序属性',
asec: '正序',
desc: '序',
uniqueKey: '唯一标识',
uniqueKeySelect: '请选择唯一标识',
notfound: '找不到想要的?',
cannotDeleteGroupTips: '该分组下有数据, 不能删除!',
confirmDeleteGroup: '确定要删除分组 【{groupName}】 吗?',
confirmDeleteCIType: '确定要删除模型 【{typeName}】 吗?',
uploading: '正在导入中',
uploadFailed: '导入失败,请稍后重试',
addPlugin: '新建plugin',
deletePlugin: '删除plugin',
confirmDeleteADT: '确认删除 【{pluginName}】',
attributeMap: '字段映射',
autoDiscovery: '自动发现',
node: '节点',
adExecConfig: '执行配置',
adExecTarget: '执行机器',
oneagentIdTips: '请输入以0x开头的16进制OneAgent ID',
selectFromCMDBTips: '从CMDB中选择 ',
adAutoInLib: '自动入库',
adInterval: '采集频率',
byInterval: '按间隔',
allNodes: '所有节点',
specifyNodes: '指定节点',
specifyNodesTips: '请填写指定节点',
username: '用户名',
password: '密码',
link: '链接',
list: '多值',
listTips: '字段的值是1个或者多个接口返回的值的类型是list',
computeForAllCITips: '所有CI触发计算',
confirmcomputeForAllCITips: '确认触发所有CI的计算',
isUnique: '是否唯一',
unique: '唯一',
isChoice: '是否选择',
defaultShow: '默认显示',
defaultShowTips: 'CI实例表格默认展示该字段',
isSortable: '可排序',
isIndex: '是否索引',
index: '索引',
indexTips: '字段可被用于检索,加速查询',
confirmDelete: '确认删除【{name}】?',
confirmDelete2: '确认删除?',
computeSuccess: '触发成功!',
basicConfig: '基础设置',
AttributeName: '属性名(英文)',
DataType: '数据类型',
defaultValue: '默认值',
autoIncID: '自增ID',
customTime: '自定义时间',
advancedSettings: '高级设置',
font: '字体',
color: '颜色',
choiceValue: '预定义值',
computedAttribute: '计算属性',
computedAttributeTips: '该属性的值是通过模型的其它属性构建的表达式或者执行一段代码的方式计算而来,属性的引用方法为: {{ 属性名 }}',
addAttribute: '新增属性',
existedAttributes: '已有属性',
editAttribute: '编辑属性',
addAttributeTips1: '选中排序,则必须也要选中!',
uniqueConstraint: '唯一校验',
up: '上移',
down: '下移',
selectAttribute: '添加属性',
groupExisted: '分组名称已存在',
attributeSortedTips: '其他分组中的属性不能进行排序,如需排序请先拖至自定义的分组!',
buildinAttribute: '内置字段',
expr: '表达式',
code: '代码',
apply: '应用',
continueAdd: '继续添加',
filter: '过滤',
choiceOther: '其他模型属性',
choiceWebhookTips: '返回的结果按字段来过滤,层级嵌套用##分隔比如k1##k2web请求返回{k1: [{k2: 1}, {k2: 2}]}, 解析结果为[1, 2]',
selectCIType: '请选择CMDB模型',
selectCITypeAttributes: '请选择模型属性',
selectAttributes: '请选择属性',
choiceScriptDemo: 'class ChoiceValue(object):\n @staticmethod\n def values():\n """\n 执行入口, 返回预定义值\n :return: 返回一个列表, 值的类型同属性的类型\n 例如:\n return ["在线", "下线"]\n """\n return []',
valueExisted: '当前值已存在!',
addRelation: '新增关系',
sourceCIType: '源模型',
sourceCITypeTips: '请选择源模型',
dstCIType: '目标模型',
dstCITypeTips: '请选择目标模型',
relationType: '关联类型',
relationTypeTips: '请选择关联类型',
isParent: '',
relationConstraint: '关系约束',
relationConstraintTips: '请选择关系约束',
one2Many: '一对多',
one2One: '一对一',
many2Many: '对多',
basicInfo: '基本信息',
nameInputTips: '请输入名称',
triggerDataChange: '数据变更',
triggerDate: '日期属性',
triggerEnable: '开启',
descInput: '请输入备注',
triggerCondition: '触发条件',
addInstance: '新增实例',
deleteInstance: '删除实例',
changeInstance: '实例变更',
selectMutipleAttributes: '请选择属性(多选)',
selectSingleAttribute: '请选择属性(单选)',
beforeDays: '提前',
days: '天',
notifyAt: '发送时间',
notify: '通知',
triggerAction: '触发动作',
receivers: '收件人',
emailTips: '请输入邮箱,多个邮箱用;分隔',
customEmail: '自定义收件人',
notifySubject: '通知标题',
notifySubjectTips: '请输入通知标题',
notifyContent: '内容',
notifyMethod: '通知方式',
botSelect: '请选择机器人',
refAttributeTips: '标题、内容可以引用该模型的属性值,引用方法为: {{ attr_name }}',
webhookRefAttributeTips: '请求参数可以引用该模型的属性值,引用方法为: {{ attr_name }}',
newTrigger: '新增触发器',
editTriggerTitle: '编辑触发器 {name}',
newTriggerTitle: '新增触发器 {name}',
confirmDeleteTrigger: '确认删除该触发器吗?',
int: '整数',
float: '浮点数',
text: '文本',
datetime: '日期时间',
date: '日期',
time: '时间',
json: 'JSON',
event: '事件'
},
components: {
unselectAttributes: '未选属性',
selectAttributes: '已选属性',
downloadCI: '导出数据',
filename: '文件名',
filenameInputTips: '请输入文件名',
saveType: '保存类型',
saveTypeTips: '请选择保存类型',
xlsx: 'Excel工作簿(*.xlsx)',
csv: 'CSV(逗号分隔)(*.csv)',
html: '网页(*.html)',
xml: 'XML数据(*.xml)',
txt: '文本文件(制表符分隔)(*.txt)',
grantUser: '授权用户/部门',
grantRole: '授权角色',
confirmRevoke: '确认删除 【{name}】 的 【授权】 权限?',
readAttribute: '查看字段',
readCI: '查看实例',
config: '配置',
ciTypeGrant: '模型权限',
ciGrant: '实例权限',
attributeGrant: '字段权限',
relationGrant: '关系权限',
perm: '权限',
all: '全部',
customize: '自定义',
none: '',
customizeFilterName: '请输入自定义筛选条件名',
colorPickerError: '初始化颜色格式错误,使用#fff或rgb格式',
example: '示例值',
aliyun: '阿里云',
tencentcloud: '腾讯云',
huaweicloud: '华为云',
beforeChange: '变更前',
afterChange: '变更后',
noticeContentTips: '请输入通知内容',
saveQuery: '保存筛选条件',
pleaseSearch: '请查找',
conditionFilter: '条件过滤',
attributeDesc: '属性说明',
ciSearchTips: '1. json属性不能搜索<br />2. 搜索内容包括逗号, 则需转义 ,<br />3. 只搜索索引属性, 非索引属性使用条件过滤',
ciSearchTips2: '例: q=hostname:*0.0.0.0*',
subCIType: '订阅模型',
already: '已',
not: '',
sub: '订阅',
selectBelow: '请在下方进行选择',
subSuccess: '订阅成功',
selectMethods: '请选择方式',
noAuthRequest: '暂无请求认证',
noParamRequest: '暂无参数认证',
requestParam: '请求参数',
param: '参数{param}',
value: '值{value}',
clear: '清空',
},
batch: {
downloadFailed: '失败下载',
unselectCIType: '尚未选择模板类型',
pleaseUploadFile: '请上传文件',
batchUploadCanceled: '批量上传已取消',
selectCITypeTips: '请选择模板类型',
downloadTemplate: '下载模板',
drawTips: '点击或拖拽文件至此上传!',
supportFileTypes: '支持文件类型xlsxlsx',
uploadResult: '上传结果',
total: '共',
successItems: '条,已成功',
failedItems: '条,失败',
items: '',
errorTips: '错误信息',
requestFailedTips: '请求出现错误,请稍后再试',
requestSuccessTips: '批量上传已完成',
},
preference: {
mySub: '我的订阅',
sub: '订阅',
cancelSub: '取消订阅',
editSub: '编辑订阅',
peopleSub: '位同事已订阅',
noSub: '暂无同事订阅',
cancelSubSuccess: '取消订阅成功',
confirmcancelSub: '确认取消订阅',
confirmcancelSub2: '确认取消订阅 {name} 吗?',
of: '',
hoursAgo: '小时前',
daysAgo: '天前',
monthsAgo: '月前',
yearsAgo: '年前',
just: '刚刚',
},
custom_dashboard: {
charts: '图表',
newChart: '新增图表',
editChart: '编辑图表',
title: '标题',
titleTips: '请输入图表标题',
calcIndicators: '计算指标',
dimensions: '维度',
selectDimensions: '请选择维度',
quantity: '数量',
childCIType: '关系模型',
level: '层级',
levelTips: '请输入关系层级',
preview: '预览',
showIcon: '是否显示icon',
chartType: '图表类型',
dataFilter: '数据筛选',
format: '格式',
fontColor: '字体颜色',
backgroundColor: '背景颜色',
chartColor: '图表颜色',
chartLength: '图表长度',
barType: '柱状图类型',
stackedBar: '堆积柱状图',
multipleSeriesBar: '多系列柱状图',
axis: '',
direction: '方向',
lowerShadow: '下方阴影',
count: '指标',
bar: '柱状图',
line: '折线图',
pie: '状图',
table: '表格',
default: '默认',
relation: '关系',
noCustomDashboard: '管理员暂未定制仪表盘',
},
preference_relation: {
newServiceTree: '新增服务树',
serviceTreeName: '服务树名',
public: '公开',
saveLayout: '保存布局',
childNodesNotFound: '不存在子节点,不能形成业务关系,请重新选择!',
tips1: '不能与当前选中节点形成视图,请重新选择!',
tips2: '请输入新增服务树名!',
tips3: '请选择至少两个节点!',
},
history: {
ciChange: 'CI变更',
relationChange: '关系变更',
ciTypeChange: '模型变更',
triggerHistory: '触发历史',
opreateTime: '操作时间',
user: '用户',
userTips: '输入筛选用户名',
filter: '筛选',
filterOperate: '筛选操作',
attribute: '属性',
old: '',
new: '',
noUpdate: '没有修改',
itemsPerPage: '/页',
triggerName: '触发器名称',
event: '事件',
action: '作',
status: '状态',
done: '已完成',
undone: '未完成',
triggerTime: '触发时间',
totalItems: '共 {total} 条记录',
pleaseSelect: '请选择',
startTime: '开始时间',
endTime: '结束时间',
deleteCIType: '删除模型',
addCIType: '新增模型',
updateCIType: '修改模型',
addAttribute: '新增属性',
updateAttribute: '修改属性',
deleteAttribute: '删除属性',
addTrigger: '新增触发器',
updateTrigger: '修改触发器',
deleteTrigger: '删除触发器',
addUniqueConstraint: '新增联合唯一',
updateUniqueConstraint: '修改联合唯一',
deleteUniqueConstraint: '删除联合唯一',
addRelation: '新增关系',
deleteRelation: '删除关系',
noModifications: '没有修改',
attr: '属性名',
attrId: '属性ID',
changeDescription: '属性ID{attr_id},提前:{before_days}天,主题:{subject}\n内容{body}\n通知时间{notify_at}'
},
relation_type: {
addRelationType: '新增关系类型',
nameTips: '请输入类型名',
},
ad: {
upload: '规则导入',
download: '规则导出',
accpet: '入库',
accpetBy: '入库人',
acceptTime: '入库时间',
confirmAccept: '确认入库?',
accpetSuccess: '入库成功',
isAccpet: '是否入库',
deleteADC: '确认删除该条数据?',
batchDelete: '确认删除这些数据?',
agent: '内置 & 插件',
snmp: '网络设备',
http: '公有云资源',
rule: '自动发现规则',
timeout: '超时错误',
mode: '模式',
collectSettings: '采集设置',
updateFields: '更新字段',
pluginScript: `# -*- coding:utf-8 -*-
import json
class AutoDiscovery(object):
@property
def unique_key(self):
"""
:return: 返回唯一属性的名字
"""
return
@staticmethod
def attributes():
"""
定义属性字段
:return: 返回属性字段列表, 列表项是(名称, 类型, 描述), 名称必须是英文
类型: String Integer Float Date DateTime Time JSON
例如:
return [
("ci_type", "String", "模型名称"),
("private_ip", "String", "内网IP, 多值逗号分隔")
]
"""
return []
@staticmethod
def run():
"""
执行入口, 返回采集的属性值
:return: 返回一个列表, 列表项是字典, 字典key是属性名称, value是属性值
例如:
return [dict(ci_type="server", private_ip="192.168.1.1")]
"""
return []
if __name__ == "__main__":
result = AutoDiscovery().run()
if isinstance(result, list):
print("AutoDiscovery::Result::{}".format(json.dumps(result)))
else:
print("ERROR: 采集返回必须是列表")
`,
server: '物理机',
vserver: '虚拟机',
nic: '网卡',
disk: '硬盘',
},
ci: {
attributeDesc: '属性说明',
selectRows: '选取:{rows} 项',
addRelation: '添加关系',
all: '全部',
batchUpdate: '批量修改',
batchUpdateConfirm: '确认要批量修改吗?',
batchUpdateInProgress: '正在批量修改',
batchUpdateInProgress2: '正在批量修改,共{total}个,成功{successNum}个,失败{errorNum}个',
batchDeleting: '正在删除...',
batchDeleting2: '正在删除,共{total}个,成功{successNum}个,失败{errorNum}个',
copyFailed: '复制失败!',
noLevel: '无层级关系!',
batchAddRelation: '批量添加关系',
history: '操作历史',
topo: '拓扑',
table: '表格',
m2mTips: '当前模型关系为多对多,请前往关系视图进行增删操作',
confirmDeleteRelation: '确认删除关系?',
tips1: '多个值使用,分割',
tips2: '可根据需要修改字段,当值为 空 时,则该字段 置空',
tips3: '请选择需要修改的字段',
tips4: '必须至少选择一个字段',
tips5: '搜索 名称 | 别名',
tips6: '加快检索, 可以全文搜索, 无需使用条件过滤\n\n json目前不支持建索引 \n\n文本字符长度超过190不能建索引',
tips7: '表现形式是下拉框, 值必须在预定义值里',
tips8: '多值, 比如内网IP',
tips9: '仅针对前端',
tips10: '模型的其他属性通过表达式的方式计算出来\n\n一个代码片段计算返回的值',
newUpdateField: '新增修改字段',
attributeSettings: '字段设置',
},
serviceTree: {
deleteNode: '删除节点',
tips1: 'q=os_version:centos&sort=os_version',
tips2: '表达式搜索',
alert1: '管理员 还未配置业务关系, 或者你无权限访问!',
copyFailed: '复制失败',
deleteRelationConfirm: '确认将选中的 {name} 从当前关系中删除?',
},
tree: {
tips1: '请先到 我的订阅 页面完成订阅!',
subSettings: '订阅设置',
}
}
export default cmdb_zh
const cmdb_zh = {
relation: '关系',
attribute: '属性',
menu: {
views: '视图',
config: '配置',
backend: '管理端',
ciTable: '资源数据',
ciTree: '资源层级',
ciSearch: '资源搜索',
adCIs: '自动发现池',
preference: '我的订阅',
batchUpload: '批量导入',
citypeManage: '模型配置',
backendManage: '后台管理',
customDashboard: '定制仪表盘',
serviceTreeDefine: '服务树定义',
citypeRelation: '模型关系',
operationHistory: '操作审计',
relationType: '关系类型',
ad: '自动发现',
cidetail: 'CI 详情'
},
ciType: {
ciType: '模型',
attributes: '模型属性',
relation: '模型关联',
trigger: '触发器',
attributeAD: '属性自动发现',
relationAD: '关系自动发现',
grant: '权限配置',
addGroup: '新增分组',
editGroup: '修改分组',
group: '分组',
attributeLibray: '属性库',
addCITypeInGroup: '在该组中新增CI模型',
addCIType: '新增CI模型',
editGroupName: '编辑组名称',
deleteGroup: '删除该组',
CITypeName: '模型名(英文)',
English: '英文',
inputAttributeName: '请输入属性名',
attributeNameTips: '不能以数字开头,可以是英文 数字以及下划线 (_)',
editCIType: '编辑模型',
defaultSort: '默认排序',
selectDefaultOrderAttr: '选择默认排序属性',
asec: '序',
desc: '倒序',
uniqueKey: '唯一标识',
uniqueKeySelect: '请选择唯一标识',
uniqueKeyTips: 'json、密码、计算属性、预定义值属性不能作为唯一标识',
notfound: '找不到想要的?',
cannotDeleteGroupTips: '该分组下有数据, 不能删除!',
confirmDeleteGroup: '确定要删除分组 【{groupName}】 吗?',
confirmDeleteCIType: '确定要删除模型 【{typeName}】 吗?',
uploading: '正在导入中',
uploadFailed: '导入失败,请稍后重试',
addPlugin: '新建plugin',
deletePlugin: '删除plugin',
confirmDeleteADT: '确认删除 【{pluginName}】',
attributeMap: '字段映射',
autoDiscovery: '自动发现',
node: '节点',
adExecConfig: '执行配置',
adExecTarget: '执行机器',
oneagentIdTips: '请输入以0x开头的16进制OneAgent ID',
selectFromCMDBTips: '从CMDB中选择 ',
adAutoInLib: '自动入库',
adInterval: '采集频率',
byInterval: '按间隔',
allNodes: '所有节点',
specifyNodes: '指定节点',
specifyNodesTips: '请填写指定节点!',
username: '用户名',
password: '密码',
link: '链接',
list: '多值',
listTips: '字段的值是1个或者多个接口返回的值的类型是list',
computeForAllCITips: '所有CI触发计算',
confirmcomputeForAllCITips: '确认触发所有CI的计算',
isUnique: '是否唯一',
unique: '唯一',
isChoice: '是否选择',
defaultShow: '默认显示',
defaultShowTips: 'CI实例表格默认展示该字段',
isSortable: '可排序',
isIndex: '是否索引',
index: '索引',
indexTips: '字段可被用于检索,加速查询',
confirmDelete: '确认删除【{name}】?',
confirmDelete2: '确认删除?',
computeSuccess: '触发成功!',
basicConfig: '基础设置',
AttributeName: '属性名(英文)',
DataType: '数据类型',
defaultValue: '默认值',
autoIncID: '自增ID',
customTime: '自定义时间',
advancedSettings: '高级设置',
font: '字体',
color: '颜色',
choiceValue: '预定义值',
computedAttribute: '计算属性',
computedAttributeTips: '该属性的值是通过模型的其它属性构建的表达式或者执行一段代码的方式计算而来,属性的引用方法为: {{ 属性名 }}',
addAttribute: '新增属性',
existedAttributes: '已有属性',
editAttribute: '编辑属性',
addAttributeTips1: '选中排序,则必须也要选中!',
uniqueConstraint: '唯一校验',
up: '上移',
down: '下移',
selectAttribute: '添加属性',
groupExisted: '分组名称已存在',
attributeSortedTips: '其他分组中的属性不能进行排序,如需排序请先拖至自定义的分组!',
buildinAttribute: '内置字段',
expr: '表达式',
code: '代码',
apply: '应用',
continueAdd: '继续添加',
filter: '过滤',
choiceOther: '其他模型属性',
choiceWebhookTips: '返回的结果按字段来过滤,层级嵌套用##分隔比如k1##k2web请求返回{k1: [{k2: 1}, {k2: 2}]}, 解析结果为[1, 2]',
selectCIType: '请选择CMDB模型',
selectCITypeAttributes: '请选择模型属性',
selectAttributes: '请选择属性',
choiceScriptDemo: 'class ChoiceValue(object):\n @staticmethod\n def values():\n """\n 执行入口, 返回预定义值\n :return: 返回一个列表, 值的类型同属性的类型\n 例如:\n return ["在线", "下线"]\n """\n return []',
valueExisted: '当前值已存在!',
addRelation: '新增关系',
sourceCIType: '模型',
sourceCITypeTips: '请选择模型',
dstCIType: '目标模型名',
dstCITypeTips: '请选择目标模型',
relationType: '关联类型',
relationTypeTips: '请选择关联类型',
isParent: '被',
relationConstraint: '关系约束',
relationConstraintTips: '请选择关系约束',
one2Many: '对多',
one2One: '一对一',
many2Many: '多对多',
basicInfo: '基本信息',
nameInputTips: '请输入名称',
triggerDataChange: '数据变更',
triggerDate: '日期属性',
triggerEnable: '开启',
descInput: '请输入备注',
triggerCondition: '触发条件',
addInstance: '新增实例',
deleteInstance: '删除实例',
changeInstance: '实例变更',
selectMutipleAttributes: '请选择属性(多选)',
selectSingleAttribute: '请选择属性(单选)',
beforeDays: '提前',
days: '',
notifyAt: '发送时间',
notify: '通知',
triggerAction: '触发动作',
receivers: '收件人',
emailTips: '请输入邮箱,多个邮箱用;分隔',
customEmail: '自定义收件人',
notifySubject: '通知标题',
notifySubjectTips: '请输入通知标题',
notifyContent: '内容',
notifyMethod: '通知方式',
botSelect: '请选择机器人',
refAttributeTips: '标题、内容可以引用该模型的属性值,引用方法为: {{ attr_name }}',
webhookRefAttributeTips: '请求参数可以引用该模型的属性值,引用方法为: {{ attr_name }}',
newTrigger: '新增触发器',
editTriggerTitle: '编辑触发器 {name}',
newTriggerTitle: '新增触发器 {name}',
confirmDeleteTrigger: '确认删除该触发器吗?',
int: '整数',
float: '浮点数',
text: '文本',
datetime: '日期时间',
date: '日期',
time: '时间',
json: 'JSON',
event: '事件',
reg: '正则校验',
isInherit: '是否继承',
inheritType: '继承模型',
inheritTypePlaceholder: '请选择继承模型(多选)',
inheritFrom: '属性继承自{name}',
groupInheritFrom: '请至{name}进行修改'
},
components: {
unselectAttributes: '未选属性',
selectAttributes: '已选属性',
downloadCI: '导出数据',
filename: '文件名',
filenameInputTips: '请输入文件名',
saveType: '保存类型',
saveTypeTips: '请选择保存类型',
xlsx: 'Excel工作簿(*.xlsx)',
csv: 'CSV(逗号分隔)(*.csv)',
html: '网页(*.html)',
xml: 'XML数据(*.xml)',
txt: '文本文件(制表符分隔)(*.txt)',
grantUser: '授权用户/部门',
grantRole: '授权角色',
confirmRevoke: '确认删除 【{name}】 的 【授权】 权限',
readAttribute: '查看字段',
readCI: '查看实例',
config: '配置',
ciTypeGrant: '模型权限',
ciGrant: '实例权限',
attributeGrant: '字段权限',
relationGrant: '关系权限',
perm: '权限',
all: '全部',
customize: '自定义',
none: '',
customizeFilterName: '请输入自定义筛选条件名',
colorPickerError: '初始化颜色格式错误,使用#fff或rgb格式',
example: '示例值',
aliyun: '阿里云',
tencentcloud: '腾讯云',
huaweicloud: '华为云',
beforeChange: '变更前',
afterChange: '变更后',
noticeContentTips: '请输入通知内容',
saveQuery: '保存筛选条件',
pleaseSearch: '请查找',
conditionFilter: '条件过滤',
attributeDesc: '属性说明',
ciSearchTips: '1. json、密码、链接属性不能搜索\n2. 搜索内容包括逗号, 则需转义\n3. 只搜索索引属性, 非索引属性使用条件过滤',
ciSearchTips2: '例: q=hostname:*0.0.0.0*',
subCIType: '订阅模型',
already: '',
not: '',
sub: '订阅',
selectBelow: '请在下方进行选择',
subSuccess: '订阅成功',
selectMethods: '请选择方式',
noAuthRequest: '暂无请求认证',
noParamRequest: '暂无参数认证',
requestParam: '请求参数',
param: '参数{param}',
value: '值{value}',
clear: '清空',
},
batch: {
downloadFailed: '失败下载',
unselectCIType: '尚未选择模板类型',
pleaseUploadFile: '请上传文件',
batchUploadCanceled: '批量上传已取消',
selectCITypeTips: '请选择模板类型',
downloadTemplate: '下载模板',
drawTips: '点击或拖拽文件至此上传!',
supportFileTypes: '支持文件类型xlsxlsx',
uploadResult: '上传结果',
total: '共',
successItems: '条,已成功',
failedItems: '条,失败',
items: '',
errorTips: '错误信息',
requestFailedTips: '请求出现错误,请稍后再试',
requestSuccessTips: '批量上传已完成',
},
preference: {
mySub: '我的订阅',
sub: '订阅',
cancelSub: '取消订阅',
editSub: '编辑订阅',
peopleSub: '位同事已订阅',
noSub: '暂无同事订阅',
cancelSubSuccess: '取消订阅成功',
confirmcancelSub: '确认取消订阅',
confirmcancelSub2: '确认取消订阅 {name} 吗?',
of: '',
hoursAgo: '小时前',
daysAgo: '天前',
monthsAgo: '月前',
yearsAgo: '年前',
just: '刚刚',
},
custom_dashboard: {
charts: '图表',
newChart: '新增图表',
editChart: '编辑图表',
title: '标题',
titleTips: '请输入图表标题',
calcIndicators: '计算指标',
dimensions: '维度',
selectDimensions: '请选择维度',
quantity: '数量',
childCIType: '关系模型',
level: '层级',
levelTips: '请输入关系层级',
preview: '预览',
showIcon: '是否显示icon',
chartType: '图表类型',
dataFilter: '数据筛选',
format: '格式',
fontColor: '字体颜色',
backgroundColor: '背景颜色',
chartColor: '图表颜色',
chartLength: '图表长度',
barType: '柱状图类型',
stackedBar: '堆积柱状图',
multipleSeriesBar: '多系列柱状图',
axis: '',
direction: '方向',
lowerShadow: '下方阴影',
count: '指标',
bar: '柱状图',
line: '折线图',
pie: '饼状图',
table: '表格',
default: '默认',
relation: '关系',
noCustomDashboard: '管理员暂未定制仪表盘',
},
preference_relation: {
newServiceTree: '新增服务树',
serviceTreeName: '服务树名',
public: '公开',
saveLayout: '保存布局',
childNodesNotFound: '不存在子节点,不能形成业务关系,请重新选择!',
tips1: '不能与当前选中节点形成视图,请重新选择!',
tips2: '请输入新增服务树名!',
tips3: '请选择至少两个节点!',
},
history: {
ciChange: 'CI变更',
relationChange: '关系变更',
ciTypeChange: '模型变更',
triggerHistory: '触发历史',
opreateTime: '操作时间',
user: '用户',
userTips: '输入筛选用户名',
filter: '筛选',
filterOperate: '筛选操作',
attribute: '属性',
old: '',
new: '',
noUpdate: '没有修改',
itemsPerPage: '/页',
triggerName: '触发器名称',
event: '事件',
action: '动作',
status: '状态',
done: '已完成',
undone: '未完成',
triggerTime: '触发时间',
totalItems: '共 {total} 条记录',
pleaseSelect: '请选择',
startTime: '开始时间',
endTime: '结束时间',
deleteCIType: '删除模型',
addCIType: '新增模型',
updateCIType: '修改模型',
addAttribute: '新增属性',
updateAttribute: '修改属性',
deleteAttribute: '删除属性',
addTrigger: '新增触发器',
updateTrigger: '修改触发器',
deleteTrigger: '删除触发器',
addUniqueConstraint: '新增联合唯一',
updateUniqueConstraint: '修改联合唯一',
deleteUniqueConstraint: '删除联合唯一',
addRelation: '新增关系',
deleteRelation: '删除关系',
noModifications: '没有修改',
attr: '属性名',
attrId: '属性ID',
changeDescription: '属性ID{attr_id},提前:{before_days}天,主题:{subject}\n内容{body}\n通知时间{notify_at}'
},
relation_type: {
addRelationType: '新增关系类型',
nameTips: '请输入类型名',
},
ad: {
upload: '规则导入',
download: '规则导出',
accept: '入库',
acceptBy: '入库人',
acceptTime: '入库时间',
confirmAccept: '确认入库?',
acceptSuccess: '入库成功',
isAccept: '是否入库',
deleteADC: '确认删除该条数据?',
batchDelete: '确认删除这些数据?',
agent: '内置 & 插件',
snmp: '网络设备',
http: '公有云资源',
rule: '自动发现规则',
timeout: '超时错误',
mode: '模式',
collectSettings: '采集设置',
updateFields: '更新字段',
pluginScript: `# -*- coding:utf-8 -*-
import json
class AutoDiscovery(object):
@property
def unique_key(self):
"""
:return: 返回唯一属性的名字
"""
return
@staticmethod
def attributes():
"""
定义属性字段
:return: 返回属性字段列表, 列表项是(名称, 类型, 描述), 名称必须是英文
类型: String Integer Float Date DateTime Time JSON
例如:
return [
("ci_type", "String", "模型名称"),
("private_ip", "String", "内网IP, 多值逗号分隔")
]
"""
return []
@staticmethod
def run():
"""
执行入口, 返回采集的属性值
:return: 返回一个列表, 列表项是字典, 字典key是属性名称, value是属性值
例如:
return [dict(ci_type="server", private_ip="192.168.1.1")]
"""
return []
if __name__ == "__main__":
result = AutoDiscovery().run()
if isinstance(result, list):
print("AutoDiscovery::Result::{}".format(json.dumps(result)))
else:
print("ERROR: 采集返回必须是列表")
`,
server: '物理机',
vserver: '虚拟机',
nic: '网卡',
disk: '硬盘',
},
ci: {
attributeDesc: '属性说明',
selectRows: '选取:{rows} 项',
addRelation: '添加关系',
all: '全部',
batchUpdate: '批量修改',
batchUpdateConfirm: '确认要批量修改吗?',
batchUpdateInProgress: '正在批量修改',
batchUpdateInProgress2: '正在批量修改,共{total}个,成功{successNum}个,失败{errorNum}个',
batchDeleting: '正在删除...',
batchDeleting2: '正在删除,共{total}个,成功{successNum}个,失败{errorNum}个',
copyFailed: '复制失败!',
noLevel: '无层级关系!',
batchAddRelation: '批量添加关系',
history: '操作历史',
topo: '拓扑',
table: '表格',
m2mTips: '当前模型关系为多对多,请前往关系视图进行增删操作',
confirmDeleteRelation: '确认删除关系?',
tips1: '多个值使用,分割',
tips2: '可根据需要修改字段,当值为 空 时,则该字段 置空',
tips3: '请选择需要修改字段',
tips4: '必须至少选择一个字段',
tips5: '搜索 名称 | 别名',
tips6: '加快检索, 可以全文搜索, 无需使用条件过滤\n\n json、链接、密码目前不支持建索引 \n\n文本字符长度超过190不能建索引',
tips7: '表现形式是下拉框, 值必须在预定义值里',
tips8: '多值, 比如内网IP',
tips9: '仅针对前端',
tips10: '模型的其他属性通过表达式的方式计算出来\n\n一个代码片段计算返回的值',
newUpdateField: '新增修改字段',
attributeSettings: '字段设置',
share: '分享',
noPermission: '暂无权限'
},
serviceTree: {
deleteNode: '删除节点',
tips1: '例q=os_version:centos&sort=os_version',
tips2: '表达式搜索',
alert1: '管理员 还未配置业务关系, 或者你无权限访问!',
copyFailed: '复制失败',
deleteRelationConfirm: '确认将选中的 {name} 从当前关系中删除?',
batch: '批量操作',
grantTitle: '授权(查看权限)',
userPlaceholder: '请选择用户',
rolePlaceholder: '请选择角色',
grantedByServiceTree: '服务树授权:',
grantedByServiceTreeTips: '请先在服务树里删掉节点授权',
peopleHasRead: '当前有查看权限的人员:',
authorizationPolicy: '实例授权策略:',
idAuthorizationPolicy: '按节点授权的:',
view: '查看权限'
},
tree: {
tips1: '请先到 我的订阅 页面完成订阅!',
subSettings: '订阅设置',
}
}
export default cmdb_zh

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

@@ -178,4 +178,14 @@ export const downloadExcel = (data, fileName = `${moment().format('YYYY-MM-DD HH
// ws['!rows'] = [{ 'hpt': 80 }]
// 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

@@ -1,388 +1,430 @@
<template>
<CustomDrawer
:title="title + CIType.alias"
width="800"
@close="handleClose"
:maskClosable="false"
:visible="visible"
wrapClassName="create-instance-form"
:bodyStyle="{ paddingTop: 0 }"
:headerStyle="{ borderBottom: 'none' }"
>
<div class="custom-drawer-bottom-action">
<a-button @click="handleClose">{{ $t('cancel') }}</a-button>
<a-button type="primary" @click="createInstance">{{ $t('submit') }}</a-button>
</div>
<template v-if="action === 'create'">
<template v-for="group in attributesByGroup">
<CreateInstanceFormByGroup
:ref="`createInstanceFormByGroup_${group.id}`"
:key="group.id || group.name"
:group="group"
@handleFocusInput="handleFocusInput"
:attributeList="attributeList"
/>
</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-form>
<a-row :gutter="24" align="top" type="flex">
<a-col :span="12" v-for="item in parentsType" :key="item.id">
<a-form-item :label="item.alias || item.name" :colon="false">
<a-input-group compact style="width: 100%">
<a-select v-model="parentsForm[item.name].attr">
<a-select-option
:title="attr.alias || attr.name"
v-for="attr in item.attributes"
:key="attr.name"
:value="attr.name"
>
{{ 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-group>
</a-form-item>
</a-col>
</a-row>
</a-form>
</template>
</template>
<template v-if="action === 'update'">
<a-form :form="form">
<p>{{ $t('cmdb.ci.tips2') }}</p>
<a-row :gutter="24" v-for="list in batchUpdateLists" :key="list.name">
<a-col :span="11">
<a-form-item>
<el-select showSearch size="small" filterable v-model="list.name" :placeholder="$t('cmdb.ci.tips3')">
<el-option
v-for="attr in attributeList"
:key="attr.name"
:value="attr.name"
:disabled="batchUpdateLists.findIndex((item) => item.name === attr.name) > -1"
:label="attr.alias || attr.name"
>
</el-option>
</el-select>
</a-form-item>
</a-col>
<a-col :span="11">
<a-form-item>
<a-select
:style="{ width: '100%' }"
v-decorator="[list.name, { rules: [{ required: false }] }]"
:placeholder="$t('placeholder2')"
v-if="getFieldType(list.name).split('%%')[0] === 'select'"
:mode="getFieldType(list.name).split('%%')[1] === 'multiple' ? 'multiple' : 'default'"
showSearch
allowClear
>
<a-select-option
:value="choice[0]"
:key="'New_' + choice + choice_idx"
v-for="(choice, choice_idx) in getSelectFieldOptions(list.name)"
>
<span :style="choice[1] ? choice[1].style || {} : {}">
<ops-icon
:style="{ color: choice[1].icon.color }"
v-if="choice[1] && choice[1].icon && choice[1].icon.name"
:type="choice[1].icon.name"
/>
{{ choice[0] }}
</span>
</a-select-option>
</a-select>
<a-input-number
v-decorator="[list.name, { rules: [{ required: false }] }]"
style="width: 100%"
v-if="getFieldType(list.name) === 'input_number'"
/>
<a-date-picker
v-decorator="[list.name, { rules: [{ required: false }] }]"
style="width: 100%"
:format="getFieldType(list.name) == 'date' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
v-if="getFieldType(list.name) === 'date' || getFieldType(list.name) === 'datetime'"
:showTime="getFieldType(list.name) === 'date' ? false : { format: 'HH:mm:ss' }"
/>
<a-input
v-if="getFieldType(list.name) === 'input'"
@focus="(e) => handleFocusInput(e, list)"
v-decorator="[list.name, { rules: [{ required: false }] }]"
/>
</a-form-item>
</a-col>
<a-col :span="2">
<a-form-item>
<a :style="{ color: 'red', marginTop: '2px' }" @click="handleDelete(list.name)">
<a-icon type="delete" />
</a>
</a-form-item>
</a-col>
</a-row>
<a-button type="primary" ghost icon="plus" @click="handleAdd">{{ $t('cmdb.ci.newUpdateField') }}</a-button>
</a-form>
</template>
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
</CustomDrawer>
</template>
<script>
import _ from 'lodash'
import moment from 'moment'
import { Select, Option } from 'element-ui'
import { getCIType, getCITypeGroupById } from '@/modules/cmdb/api/CIType'
import { addCI } from '@/modules/cmdb/api/ci'
import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue'
import { valueTypeMap } from '../../../utils/const'
import CreateInstanceFormByGroup from './createInstanceFormByGroup.vue'
import { getCITypeParent, getCanEditByParentIdChildId } from '@/modules/cmdb/api/CITypeRelation'
export default {
name: 'CreateInstanceForm',
components: {
ElSelect: Select,
ElOption: Option,
JsonEditor,
CreateInstanceFormByGroup,
},
props: {
typeIdFromRelation: {
type: Number,
default: 0,
},
},
data() {
return {
action: '',
form: this.$form.createForm(this),
visible: false,
attributeList: [],
CIType: {},
batchUpdateLists: [],
editAttr: null,
attributesByGroup: [],
parentsType: [],
parentsForm: {},
canEdit: {},
}
},
computed: {
title() {
return this.action === 'create' ? this.$t('create') + ' ' : this.$t('cmdb.ci.batchUpdate') + ' '
},
typeId() {
if (this.typeIdFromRelation) {
return this.typeIdFromRelation
}
return this.$router.currentRoute.meta.typeId
},
valueTypeMap() {
return valueTypeMap()
},
},
provide() {
return {
getFieldType: this.getFieldType,
}
},
inject: ['attrList'],
methods: {
moment,
async getCIType() {
await getCIType(this.typeId).then((res) => {
this.CIType = res.ci_types[0]
})
},
async getAttributeList() {
const _attrList = this.attrList()
this.attributeList = _attrList.sort((x, y) => y.is_required - x.is_required)
await getCITypeGroupById(this.typeId).then((res1) => {
const _attributesByGroup = res1.map((g) => {
g.attributes = g.attributes.filter((attr) => !attr.is_computed)
return g
})
const attrHasGroupIds = []
res1.forEach((g) => {
const id = g.attributes.map((attr) => attr.id)
attrHasGroupIds.push(...id)
})
const otherGroupAttr = this.attributeList.filter(
(attr) => !attrHasGroupIds.includes(attr.id) && !attr.is_computed
)
if (otherGroupAttr.length) {
_attributesByGroup.push({ id: -1, name: this.$t('other'), attributes: otherGroupAttr })
}
this.attributesByGroup = _attributesByGroup
})
},
createInstance() {
const _this = this
if (_this.action === 'update') {
this.form.validateFields((err, values) => {
if (err) {
return
}
Object.keys(values).forEach((k) => {
const _tempFind = this.attributeList.find((item) => item.name === k)
if (
_tempFind.value_type === '3' &&
values[k] &&
Object.prototype.toString.call(values[k]) === '[object Object]'
) {
values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
}
if (
_tempFind.value_type === '4' &&
values[k] &&
Object.prototype.toString.call(values[k]) === '[object Object]'
) {
values[k] = values[k].format('YYYY-MM-DD')
}
if (_tempFind.value_type === '6') {
values[k] = values[k] ? JSON.parse(values[k]) : undefined
}
})
_this.$emit('submit', values)
})
} else {
let values = {}
for (let i = 0; i < this.attributesByGroup.length; i++) {
const data = this.$refs[`createInstanceFormByGroup_${this.attributesByGroup[i].id}`][0].getData()
if (data === 'error') {
return
}
values = { ...values, ...data }
}
Object.keys(values).forEach((k) => {
const _tempFind = this.attributeList.find((item) => item.name === k)
if (
_tempFind.value_type === '3' &&
values[k] &&
Object.prototype.toString.call(values[k]) === '[object Object]'
) {
values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
}
if (
_tempFind.value_type === '4' &&
values[k] &&
Object.prototype.toString.call(values[k]) === '[object Object]'
) {
values[k] = values[k].format('YYYY-MM-DD')
}
if (_tempFind.value_type === '6') {
values[k] = values[k] ? JSON.parse(values[k]) : undefined
}
})
values.ci_type = _this.typeId
console.log(this.parentsForm)
Object.keys(this.parentsForm).forEach((type) => {
if (this.parentsForm[type].value) {
values[`$${type}.${this.parentsForm[type].attr}`] = this.parentsForm[type].value
}
})
addCI(values).then((res) => {
_this.$message.success(this.$t('addSuccess'))
_this.visible = false
_this.$emit('reload', { ci_id: res.ci_id })
})
}
},
handleClose() {
this.visible = false
},
handleOpen(visible, action) {
this.visible = visible
this.action = action
this.$nextTick(() => {
this.form.resetFields()
Promise.all([this.getCIType(), this.getAttributeList()]).then(() => {
this.batchUpdateLists = [{ name: this.attributeList[0].name }]
})
if (action === 'create') {
getCITypeParent(this.typeId).then(async (res) => {
for (let i = 0; i < res.parents.length; i++) {
await getCanEditByParentIdChildId(res.parents[i].id, this.typeId).then((p_res) => {
this.canEdit = {
..._.cloneDeep(this.canEdit),
[res.parents[i].id]: p_res.result,
}
})
}
this.parentsType = res.parents.filter((parent) => this.canEdit[parent.id])
const _parentsForm = {}
res.parents.forEach((item) => {
const _find = item.attributes.find((attr) => attr.id === item.unique_id)
_parentsForm[item.name] = { attr: _find.name, value: '' }
})
this.parentsForm = _parentsForm
})
}
})
},
getFieldType(name) {
const _find = this.attributeList.find((item) => item.name === name)
if (_find) {
if (_find.is_choice) {
if (_find.is_list) {
return 'select%%multiple'
}
return 'select'
} else if (_find.value_type === '0' || _find.value_type === '1') {
return 'input_number'
} else if (_find.value_type === '4' || _find.value_type === '3') {
return this.valueTypeMap[_find.value_type]
} else {
return 'input'
}
}
return 'input'
},
getSelectFieldOptions(name) {
const _find = this.attributeList.find((item) => item.name === name)
if (_find) {
return _find.choice_value
}
return []
},
handleAdd() {
this.batchUpdateLists.push({ name: undefined })
},
handleDelete(name) {
const _idx = this.batchUpdateLists.findIndex((item) => item.name === name)
if (_idx > -1) {
this.batchUpdateLists.splice(_idx, 1)
}
},
handleFocusInput(e, attr) {
console.log(attr)
const _tempFind = this.attributeList.find((item) => item.name === attr.name)
if (_tempFind.value_type === '6') {
this.editAttr = attr
e.srcElement.blur()
const jsonData = this.form.getFieldValue(attr.name)
this.$refs.jsonEditor.open(null, null, jsonData ? JSON.parse(jsonData) : {})
} else {
this.editAttr = null
}
},
jsonEditorOk(jsonData) {
this.form.setFieldsValue({ [this.editAttr.name]: JSON.stringify(jsonData) })
},
},
}
</script>
<style lang="less">
.create-instance-form {
.ant-form-item {
margin-bottom: 5px;
}
.ant-drawer-body {
overflow-y: auto;
max-height: calc(100vh - 110px);
}
}
</style>
<template>
<CustomDrawer
:title="title + CIType.alias"
width="800"
@close="handleClose"
:maskClosable="false"
:visible="visible"
wrapClassName="create-instance-form"
:bodyStyle="{ paddingTop: 0 }"
:headerStyle="{ borderBottom: 'none' }"
>
<div class="custom-drawer-bottom-action">
<a-button @click="handleClose">{{ $t('cancel') }}</a-button>
<a-button type="primary" @click="createInstance">{{ $t('submit') }}</a-button>
</div>
<template v-if="action === 'create'">
<template v-for="group in attributesByGroup">
<CreateInstanceFormByGroup
:ref="`createInstanceFormByGroup_${group.id}`"
:key="group.id || group.name"
:group="group"
@handleFocusInput="handleFocusInput"
:attributeList="attributeList"
/>
</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-form>
<a-row :gutter="24" align="top" type="flex">
<a-col :span="12" v-for="item in parentsType" :key="item.id">
<a-form-item :label="item.alias || item.name" :colon="false">
<a-input-group compact style="width: 100%">
<a-select v-model="parentsForm[item.name].attr">
<a-select-option
:title="attr.alias || attr.name"
v-for="attr in item.attributes"
:key="attr.name"
:value="attr.name"
>
{{ 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-group>
</a-form-item>
</a-col>
</a-row>
</a-form>
</template>
</template>
<template v-if="action === 'update'">
<a-form :form="form">
<p>{{ $t('cmdb.ci.tips2') }}</p>
<a-row :gutter="24" v-for="list in batchUpdateLists" :key="list.name">
<a-col :span="11">
<a-form-item>
<el-select showSearch size="small" filterable v-model="list.name" :placeholder="$t('cmdb.ci.tips3')">
<el-option
v-for="attr in attributeList"
:key="attr.name"
:value="attr.name"
:disabled="batchUpdateLists.findIndex((item) => item.name === attr.name) > -1"
:label="attr.alias || attr.name"
>
</el-option>
</el-select>
</a-form-item>
</a-col>
<a-col :span="11">
<a-form-item>
<a-select
:style="{ width: '100%' }"
v-decorator="[list.name, { rules: [{ required: false }] }]"
:placeholder="$t('placeholder2')"
v-if="getFieldType(list.name).split('%%')[0] === 'select'"
:mode="getFieldType(list.name).split('%%')[1] === 'multiple' ? 'multiple' : 'default'"
showSearch
allowClear
>
<a-select-option
:value="choice[0]"
:key="'New_' + choice + choice_idx"
v-for="(choice, choice_idx) in getSelectFieldOptions(list.name)"
>
<span :style="choice[1] ? choice[1].style || {} : {}">
<ops-icon
:style="{ color: choice[1].icon.color }"
v-if="choice[1] && choice[1].icon && choice[1].icon.name"
:type="choice[1].icon.name"
/>
{{ choice[0] }}
</span>
</a-select-option>
</a-select>
<a-input-number
v-decorator="[list.name, { rules: [{ required: false }] }]"
style="width: 100%"
v-if="getFieldType(list.name) === 'input_number'"
/>
<a-date-picker
v-decorator="[list.name, { rules: [{ required: false }] }]"
style="width: 100%"
:format="getFieldType(list.name) == 'date' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
v-if="getFieldType(list.name) === 'date' || getFieldType(list.name) === 'datetime'"
:showTime="getFieldType(list.name) === 'date' ? false : { format: 'HH:mm:ss' }"
/>
<a-input
v-if="getFieldType(list.name) === 'input'"
@focus="(e) => handleFocusInput(e, list)"
v-decorator="[list.name, { rules: [{ required: false }] }]"
/>
</a-form-item>
</a-col>
<a-col :span="2">
<a-form-item>
<a :style="{ color: 'red', marginTop: '2px' }" @click="handleDelete(list.name)">
<a-icon type="delete" />
</a>
</a-form-item>
</a-col>
</a-row>
<a-button type="primary" ghost icon="plus" @click="handleAdd">{{ $t('cmdb.ci.newUpdateField') }}</a-button>
</a-form>
</template>
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
</CustomDrawer>
</template>
<script>
import _ from 'lodash'
import moment from 'moment'
import { Select, Option } from 'element-ui'
import { getCIType, getCITypeGroupById } from '@/modules/cmdb/api/CIType'
import { addCI } from '@/modules/cmdb/api/ci'
import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue'
import { valueTypeMap } from '../../../utils/const'
import CreateInstanceFormByGroup from './createInstanceFormByGroup.vue'
import { getCITypeParent, getCanEditByParentIdChildId } from '@/modules/cmdb/api/CITypeRelation'
export default {
name: 'CreateInstanceForm',
components: {
ElSelect: Select,
ElOption: Option,
JsonEditor,
CreateInstanceFormByGroup,
},
props: {
typeIdFromRelation: {
type: Number,
default: 0,
},
},
data() {
return {
action: '',
form: this.$form.createForm(this),
visible: false,
attributeList: [],
CIType: {},
batchUpdateLists: [],
editAttr: null,
attributesByGroup: [],
parentsType: [],
parentsForm: {},
canEdit: {},
}
},
computed: {
title() {
return this.action === 'create' ? this.$t('create') + ' ' : this.$t('cmdb.ci.batchUpdate') + ' '
},
typeId() {
if (this.typeIdFromRelation) {
return this.typeIdFromRelation
}
return this.$router.currentRoute.meta.typeId
},
valueTypeMap() {
return valueTypeMap()
},
},
provide() {
return {
getFieldType: this.getFieldType,
}
},
inject: ['attrList'],
methods: {
moment,
async getCIType() {
await getCIType(this.typeId).then((res) => {
this.CIType = res.ci_types[0]
})
},
async getAttributeList() {
const _attrList = this.attrList()
this.attributeList = _attrList.sort((x, y) => y.is_required - x.is_required)
await getCITypeGroupById(this.typeId).then((res1) => {
const _attributesByGroup = res1.map((g) => {
g.attributes = g.attributes.filter((attr) => !attr.is_computed)
return g
})
const attrHasGroupIds = []
res1.forEach((g) => {
const id = g.attributes.map((attr) => attr.id)
attrHasGroupIds.push(...id)
})
const otherGroupAttr = this.attributeList.filter(
(attr) => !attrHasGroupIds.includes(attr.id) && !attr.is_computed
)
if (otherGroupAttr.length) {
_attributesByGroup.push({ id: -1, name: this.$t('other'), attributes: otherGroupAttr })
}
console.log(otherGroupAttr, _attributesByGroup)
this.attributesByGroup = _attributesByGroup
})
},
createInstance() {
const _this = this
if (_this.action === 'update') {
this.form.validateFields((err, values) => {
if (err) {
return
}
Object.keys(values).forEach((k) => {
const _tempFind = this.attributeList.find((item) => item.name === k)
if (
_tempFind.value_type === '3' &&
values[k] &&
Object.prototype.toString.call(values[k]) === '[object Object]'
) {
values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
}
if (
_tempFind.value_type === '4' &&
values[k] &&
Object.prototype.toString.call(values[k]) === '[object Object]'
) {
values[k] = values[k].format('YYYY-MM-DD')
}
if (_tempFind.value_type === '6') {
values[k] = values[k] ? JSON.parse(values[k]) : undefined
}
})
_this.$emit('submit', values)
})
} else {
let values = {}
for (let i = 0; i < this.attributesByGroup.length; i++) {
const data = this.$refs[`createInstanceFormByGroup_${this.attributesByGroup[i].id}`][0].getData()
if (data === 'error') {
return
}
values = { ...values, ...data }
}
Object.keys(values).forEach((k) => {
const _tempFind = this.attributeList.find((item) => item.name === k)
if (
_tempFind.value_type === '3' &&
values[k] &&
Object.prototype.toString.call(values[k]) === '[object Object]'
) {
values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
}
if (
_tempFind.value_type === '4' &&
values[k] &&
Object.prototype.toString.call(values[k]) === '[object Object]'
) {
values[k] = values[k].format('YYYY-MM-DD')
}
if (_tempFind.value_type === '6') {
values[k] = values[k] ? JSON.parse(values[k]) : undefined
}
})
values.ci_type = _this.typeId
console.log(this.parentsForm)
Object.keys(this.parentsForm).forEach((type) => {
if (this.parentsForm[type].value) {
values[`$${type}.${this.parentsForm[type].attr}`] = this.parentsForm[type].value
}
})
addCI(values).then((res) => {
_this.$message.success(this.$t('addSuccess'))
_this.visible = false
_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
},
handleOpen(visible, action) {
this.visible = visible
this.action = action
this.$nextTick(() => {
this.form.resetFields()
Promise.all([this.getCIType(), this.getAttributeList()]).then(() => {
this.batchUpdateLists = [{ name: this.attributeList[0].name }]
})
if (action === 'create') {
getCITypeParent(this.typeId).then(async (res) => {
for (let i = 0; i < res.parents.length; i++) {
await getCanEditByParentIdChildId(res.parents[i].id, this.typeId).then((p_res) => {
this.canEdit = {
..._.cloneDeep(this.canEdit),
[res.parents[i].id]: p_res.result,
}
})
}
this.parentsType = res.parents.filter((parent) => this.canEdit[parent.id])
const _parentsForm = {}
res.parents.forEach((item) => {
const _find = item.attributes.find((attr) => attr.id === item.unique_id)
_parentsForm[item.name] = { attr: _find.name, value: '' }
})
this.parentsForm = _parentsForm
})
}
})
},
getFieldType(name) {
const _find = this.attributeList.find((item) => item.name === name)
if (_find) {
if (_find.is_choice) {
if (_find.is_list) {
return 'select%%multiple'
}
return 'select'
} else if (_find.value_type === '0' || _find.value_type === '1') {
return 'input_number'
} else if (_find.value_type === '4' || _find.value_type === '3') {
return this.valueTypeMap[_find.value_type]
} else {
return 'input'
}
}
return 'input'
},
getSelectFieldOptions(name) {
const _find = this.attributeList.find((item) => item.name === name)
if (_find) {
return _find.choice_value
}
return []
},
handleAdd() {
this.batchUpdateLists.push({ name: undefined })
},
handleDelete(name) {
const _idx = this.batchUpdateLists.findIndex((item) => item.name === name)
if (_idx > -1) {
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)
if (_tempFind.value_type === '6') {
this.editAttr = attr
e.srcElement.blur()
const jsonData = this.form.getFieldValue(attr.name)
this.$refs.jsonEditor.open(null, null, jsonData ? JSON.parse(jsonData) : {})
} else {
this.editAttr = null
}
},
jsonEditorOk(jsonData) {
this.form.setFieldsValue({ [this.editAttr.name]: JSON.stringify(jsonData) })
},
},
}
</script>
<style lang="less">
.create-instance-form {
.ant-form-item {
margin-bottom: 5px;
}
.ant-drawer-body {
overflow-y: auto;
max-height: calc(100vh - 110px);
}
}
</style>

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,344 +1,390 @@
<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">
<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">
<el-descriptions
:title="group.name || $t('other')"
:key="group.name"
v-for="group in attributeGroups"
border
:column="3"
>
<el-descriptions-item
:label="`${attr.alias || attr.name}`"
:key="attr.name"
v-for="attr in group.attributes"
>
<CiDetailAttrContent :ci="ci" :attr="attr" @refresh="refresh" @updateCIByself="updateCIByself" />
</el-descriptions-item>
</el-descriptions>
</div>
</a-tab-pane>
<a-tab-pane key="tab_2">
<span slot="tab"><a-icon type="branches" />{{ $t('cmdb.relation') }}</span>
<div :style="{ padding: '24px' }">
<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)' }">
<vxe-table
ref="xTable"
:data="ciHistory"
size="small"
:max-height="`${windowHeight - 94}px`"
:span-method="mergeRowMethod"
border
:scroll-y="{ enabled: false }"
class="ops-stripe-table"
>
<vxe-table-column sortable field="created_at" :title="$t('created_at')"></vxe-table-column>
<vxe-table-column
field="username"
:title="$t('user')"
:filters="[]"
:filter-method="filterUsernameMethod"
></vxe-table-column>
<vxe-table-column
field="operate_type"
:filters="[
{ value: 0, label: $t('new') },
{ value: 1, label: $t('delete') },
{ value: 3, label: $t('update') },
]"
:filter-method="filterOperateMethod"
:title="$t('operation')"
>
<template #default="{ row }">
{{ operateTypeMap[row.operate_type] }}
</template>
</vxe-table-column>
<vxe-table-column
field="attr_alias"
:title="$t('cmdb.attribute')"
:filters="[]"
:filter-method="filterAttrMethod"
></vxe-table-column>
<vxe-table-column field="old" :title="$t('cmdb.history.old')"></vxe-table-column>
<vxe-table-column field="new" :title="$t('cmdb.history.new')"></vxe-table-column>
</vxe-table>
</div>
</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)' }">
<TriggerTable :ci_id="ci._id" />
</div>
</a-tab-pane>
</a-tabs>
</CustomDrawer>
</template>
<script>
import _ from 'lodash'
import { Descriptions, DescriptionsItem } from 'element-ui'
import { getCITypeGroupById, getCITypes } from '@/modules/cmdb/api/CIType'
import { getCIHistory } from '@/modules/cmdb/api/history'
import { 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 {
components: {
ElDescriptions: Descriptions,
ElDescriptionsItem: DescriptionsItem,
CiDetailAttrContent,
CiDetailRelation,
TriggerTable,
},
props: {
typeId: {
type: Number,
required: true,
},
treeViewsLevels: {
type: Array,
default: () => [],
},
},
data() {
return {
visible: false,
ci: {},
attributeGroups: [],
activeTabKey: 'tab_1',
rowSpanMap: {},
ciHistory: [],
ciId: null,
ci_types: [],
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
operateTypeMap() {
return {
0: this.$t('new'),
1: this.$t('delete'),
2: this.$t('update'),
}
},
},
provide() {
return {
ci_types: () => {
return this.ci_types
},
}
},
inject: ['reload', 'handleSearch', 'attrList'],
methods: {
create(ciId, activeTabKey = 'tab_1', ciDetailRelationKey = '1') {
this.visible = true
this.activeTabKey = activeTabKey
if (activeTabKey === 'tab_2') {
this.$nextTick(() => {
this.$refs.ciDetailRelation.activeKey = ciDetailRelationKey
})
}
this.ciId = ciId
this.getAttributes()
this.getCI()
this.getCIHistory()
getCITypes().then((res) => {
this.ci_types = res.ci_types
})
},
getAttributes() {
getCITypeGroupById(this.typeId, { need_other: 1 })
.then((res) => {
this.attributeGroups = res
})
.catch((e) => {})
},
getCI() {
getCIById(this.ciId)
.then((res) => {
// this.ci = res.ci
this.ci = res.result[0]
})
.catch((e) => {})
},
getCIHistory() {
getCIHistory(this.ciId)
.then((res) => {
this.ciHistory = res
const rowSpanMap = {}
let startIndex = 0
let startCount = 1
res.forEach((item, index) => {
if (index === 0) {
return
}
if (res[index].record_id === res[startIndex].record_id) {
startCount += 1
rowSpanMap[index] = 0
if (index === res.length - 1) {
rowSpanMap[startIndex] = startCount
}
} else {
rowSpanMap[startIndex] = startCount
startIndex = index
startCount = 1
if (index === res.length - 1) {
rowSpanMap[index] = 1
}
}
})
this.rowSpanMap = rowSpanMap
})
.catch((e) => {
console.log(e)
})
},
changeTab(key) {
this.activeTabKey = key
if (key === 'tab_3') {
this.$nextTick(() => {
const $table = this.$refs.xTable
if ($table) {
const usernameColumn = $table.getColumnByField('username')
const attrColumn = $table.getColumnByField('attr_alias')
if (usernameColumn) {
const usernameList = [...new Set(this.ciHistory.map((item) => item.username))]
$table.setFilter(
usernameColumn,
usernameList.map((item) => {
return {
value: item,
label: item,
}
})
)
}
if (attrColumn) {
$table.setFilter(
attrColumn,
this.attrList().map((attr) => {
return { value: attr.alias || attr.name, label: attr.alias || attr.name }
})
)
}
}
})
}
},
filterUsernameMethod({ value, row, column }) {
return row.username === value
},
filterOperateMethod({ value, row, column }) {
return Number(row.operate_type) === Number(value)
},
filterAttrMethod({ value, row, column }) {
return row.attr_alias === value
},
refresh(editAttrName) {
this.getCI()
const _find = this.treeViewsLevels.find((level) => level.name === editAttrName)
// 修改的字段为树形视图订阅的字段 则全部reload
setTimeout(() => {
if (_find) {
this.reload()
} else {
this.handleSearch()
}
}, 500)
},
mergeRowMethod({ row, _rowIndex, column, visibleData }) {
const fields = ['created_at', 'username']
const cellValue1 = row['created_at']
const cellValue2 = row['username']
if (cellValue1 && cellValue2 && fields.includes(column.property)) {
const prevRow = visibleData[_rowIndex - 1]
let nextRow = visibleData[_rowIndex + 1]
if (prevRow && prevRow['created_at'] === cellValue1 && prevRow['username'] === cellValue2) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow['created_at'] === cellValue1 && nextRow['username'] === cellValue2) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
},
updateCIByself(params, editAttrName) {
const _ci = { ..._.cloneDeep(this.ci), ...params }
this.ci = _ci
const _find = this.treeViewsLevels.find((level) => level.name === editAttrName)
// 修改的字段为树形视图订阅的字段 则全部reload
setTimeout(() => {
if (_find) {
this.reload()
} else {
this.handleSearch()
}
}, 500)
},
},
}
</script>
<style lang="less" scoped></style>
<style lang="less">
.ci-detail {
.ant-tabs-bar {
margin: 0;
}
.ci-detail-attr {
.el-descriptions-item__content {
cursor: default;
&:hover a {
opacity: 1 !important;
}
}
.el-descriptions:first-child > .el-descriptions__header {
margin-top: 0;
}
.el-descriptions__header {
margin-bottom: 5px;
margin-top: 20px;
}
.ant-form-item {
margin-bottom: 0;
}
.ant-form-item-control {
line-height: 19px;
}
}
}
</style>
<template>
<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 class="ci-detail-attr">
<el-descriptions
:title="group.name || $t('other')"
:key="group.name"
v-for="group in attributeGroups"
border
:column="3"
>
<el-descriptions-item
:label="`${attr.alias || attr.name}`"
:key="attr.name"
v-for="attr in group.attributes"
>
<CiDetailAttrContent :ci="ci" :attr="attr" @refresh="refresh" @updateCIByself="updateCIByself" />
</el-descriptions-item>
</el-descriptions>
</div>
</a-tab-pane>
<a-tab-pane key="tab_2">
<span slot="tab"><a-icon type="branches" />{{ $t('cmdb.relation') }}</span>
<div :style="{ height: '100%', padding: '24px', overflow: 'auto' }">
<CiDetailRelation ref="ciDetailRelation" :ciId="ciId" :typeId="typeId" :ci="ci" />
</div>
</a-tab-pane>
<a-tab-pane key="tab_3">
<span slot="tab"><a-icon type="clock-circle" />{{ $t('cmdb.ci.history') }}</span>
<div :style="{ padding: '24px', height: '100%' }">
<vxe-table
ref="xTable"
:data="ciHistory"
size="small"
height="auto"
:span-method="mergeRowMethod"
border
:scroll-y="{ enabled: false }"
class="ops-stripe-table"
>
<vxe-table-column sortable field="created_at" :title="$t('created_at')"></vxe-table-column>
<vxe-table-column
field="username"
:title="$t('user')"
:filters="[]"
:filter-method="filterUsernameMethod"
></vxe-table-column>
<vxe-table-column
field="operate_type"
:filters="[
{ value: 0, label: $t('new') },
{ value: 1, label: $t('delete') },
{ value: 3, label: $t('update') },
]"
:filter-method="filterOperateMethod"
:title="$t('operation')"
>
<template #default="{ row }">
{{ operateTypeMap[row.operate_type] }}
</template>
</vxe-table-column>
<vxe-table-column
field="attr_alias"
:title="$t('cmdb.attribute')"
:filters="[]"
:filter-method="filterAttrMethod"
></vxe-table-column>
<vxe-table-column field="old" :title="$t('cmdb.history.old')"></vxe-table-column>
<vxe-table-column field="new" :title="$t('cmdb.history.new')"></vxe-table-column>
</vxe-table>
</div>
</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: '100%' }">
<TriggerTable :ci_id="ci._id" />
</div>
</a-tab-pane>
</a-tabs>
<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>
import _ from 'lodash'
import { Descriptions, DescriptionsItem } from 'element-ui'
import { getCITypeGroupById, getCITypes } from '@/modules/cmdb/api/CIType'
import { getCIHistory } from '@/modules/cmdb/api/history'
import { 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,
CiDetailAttrContent,
CiDetailRelation,
TriggerTable,
},
props: {
typeId: {
type: Number,
required: true,
},
treeViewsLevels: {
type: Array,
default: () => [],
},
},
data() {
return {
ci: {},
attributeGroups: [],
activeTabKey: 'tab_1',
rowSpanMap: {},
ciHistory: [],
ciId: null,
ci_types: [],
hasPermission: true,
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
operateTypeMap() {
return {
0: this.$t('new'),
1: this.$t('delete'),
2: this.$t('update'),
}
},
},
provide() {
return {
ci_types: () => {
return this.ci_types
},
}
},
inject: {
reload: {
from: 'reload',
default: null,
},
handleSearch: {
from: 'handleSearch',
default: null,
},
attrList: {
from: 'attrList',
default: () => [],
},
},
methods: {
async create(ciId, activeTabKey = 'tab_1', ciDetailRelationKey = '1') {
this.activeTabKey = activeTabKey
if (activeTabKey === 'tab_2') {
this.$nextTick(() => {
this.$refs.ciDetailRelation.activeKey = ciDetailRelationKey
})
}
this.ciId = ciId
await this.getCI()
if (this.hasPermission) {
this.getAttributes()
this.getCIHistory()
getCITypes().then((res) => {
this.ci_types = res.ci_types
})
}
},
getAttributes() {
getCITypeGroupById(this.typeId, { need_other: 1 })
.then((res) => {
this.attributeGroups = res
})
.catch((e) => {})
},
async getCI() {
await getCIById(this.ciId)
.then((res) => {
if (res.result.length) {
this.ci = res.result[0]
} else {
this.hasPermission = false
}
})
.catch((e) => {})
},
getCIHistory() {
getCIHistory(this.ciId)
.then((res) => {
this.ciHistory = res
const rowSpanMap = {}
let startIndex = 0
let startCount = 1
res.forEach((item, index) => {
if (index === 0) {
return
}
if (res[index].record_id === res[startIndex].record_id) {
startCount += 1
rowSpanMap[index] = 0
if (index === res.length - 1) {
rowSpanMap[startIndex] = startCount
}
} else {
rowSpanMap[startIndex] = startCount
startIndex = index
startCount = 1
if (index === res.length - 1) {
rowSpanMap[index] = 1
}
}
})
this.rowSpanMap = rowSpanMap
})
.catch((e) => {
console.log(e)
})
},
changeTab(key) {
this.activeTabKey = key
if (key === 'tab_3') {
this.$nextTick(() => {
const $table = this.$refs.xTable
if ($table) {
const usernameColumn = $table.getColumnByField('username')
const attrColumn = $table.getColumnByField('attr_alias')
if (usernameColumn) {
const usernameList = [...new Set(this.ciHistory.map((item) => item.username))]
$table.setFilter(
usernameColumn,
usernameList.map((item) => {
return {
value: item,
label: item,
}
})
)
}
if (attrColumn) {
$table.setFilter(
attrColumn,
this.attrList().map((attr) => {
return { value: attr.alias || attr.name, label: attr.alias || attr.name }
})
)
}
}
})
}
},
filterUsernameMethod({ value, row, column }) {
return row.username === value
},
filterOperateMethod({ value, row, column }) {
return Number(row.operate_type) === Number(value)
},
filterAttrMethod({ value, row, column }) {
return row.attr_alias === value
},
refresh(editAttrName) {
this.getCI()
const _find = this.treeViewsLevels.find((level) => level.name === editAttrName)
// 修改的字段为树形视图订阅的字段 则全部reload
setTimeout(() => {
if (_find) {
if (this.reload) {
this.reload()
}
} else {
if (this.handleSearch) {
this.handleSearch()
}
}
}, 500)
},
mergeRowMethod({ row, _rowIndex, column, visibleData }) {
const fields = ['created_at', 'username']
const cellValue1 = row['created_at']
const cellValue2 = row['username']
if (cellValue1 && cellValue2 && fields.includes(column.property)) {
const prevRow = visibleData[_rowIndex - 1]
let nextRow = visibleData[_rowIndex + 1]
if (prevRow && prevRow['created_at'] === cellValue1 && prevRow['username'] === cellValue2) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow['created_at'] === cellValue1 && nextRow['username'] === cellValue2) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
},
updateCIByself(params, editAttrName) {
const _ci = { ..._.cloneDeep(this.ci), ...params }
this.ci = _ci
const _find = this.treeViewsLevels.find((level) => level.name === editAttrName)
// 修改的字段为树形视图订阅的字段 则全部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">
.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 {
opacity: 1 !important;
}
}
.el-descriptions:first-child > .el-descriptions__header {
margin-top: 0;
}
.el-descriptions__header {
margin-bottom: 5px;
margin-top: 20px;
}
.ant-form-item {
margin-bottom: 0;
}
.ant-form-item-control {
line-height: 19px;
}
}
}
</style>

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,24 +1,30 @@
<template>
<div class="attribute-card">
<div class="attribute-card-content">
<div class="attribute-card-value-type-icon handle" :style="{ ...getPropertyStyle(property) }">
<ValueTypeIcon :attr="property" />
</div>
<div :class="{ 'attribute-card-content-inner': true, 'attribute-card-name-required': property.is_required }">
<div :class="{ 'attribute-card-name': true, 'attribute-card-name-default-show': property.default_show }">
{{ property.alias || property.name }}
<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': true, handle: !inherited }"
:style="{ ...getPropertyStyle(property) }"
>
<ValueTypeIcon :attr="property" />
</div>
<div :class="{ 'attribute-card-content-inner': true, 'attribute-card-name-required': property.is_required }">
<div :class="{ 'attribute-card-name': true, 'attribute-card-name-default-show': property.default_show }">
{{ property.alias || property.name }}
</div>
<div v-if="property.is_password" class="attribute-card_value-type">{{ $t('cmdb.ciType.password') }}</div>
<div v-else-if="property.is_link" class="attribute-card_value-type">{{ $t('cmdb.ciType.link') }}</div>
<div v-else class="attribute-card_value-type">{{ valueTypeMap[property.value_type] }}</div>
</div>
<div
class="attribute-card-trigger"
v-if="(property.value_type === '3' || property.value_type === '4') && !isStore"
>
<a @click="openTrigger"><ops-icon type="ops-trigger"/></a>
</div>
<div v-if="property.is_password" class="attribute-card_value-type">{{ $t('cmdb.ciType.password') }}</div>
<div v-else-if="property.is_link" class="attribute-card_value-type">{{ $t('cmdb.ciType.link') }}</div>
<div v-else class="attribute-card_value-type">{{ valueTypeMap[property.value_type] }}</div>
</div>
<div
class="attribute-card-trigger"
v-if="(property.value_type === '3' || property.value_type === '4') && !isStore"
>
<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') {
this.form.setFieldsValue({
default_value: _record.default.default ? [_record.default.default] : [],
})
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) {
values.default = { default: default_value[0] || null }
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>

File diff suppressed because it is too large Load Diff

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

@@ -5,7 +5,7 @@
<span class="cmdb-preference-left-card-title">{{ $t('cmdb.preference.mySub') }}</span>
<span
class="cmdb-preference-left-card-content"
><ops-icon type="cmdb-ci" :style="{ marginRight: '5px' }" />{{ $t('cmdb.menu.ciTable') }}:
><ops-icon type="cmdb-ci" :style="{ marginRight: '5px' }"/>{{ $t('cmdb.menu.ciTable') }}:
<a-badge
showZero
:count="self.instance.length"
@@ -16,11 +16,10 @@
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') }}:
><ops-icon type="cmdb-tree" :style="{ marginRight: '5px' }"/>{{ $t('cmdb.menu.ciTree') }}:
<a-badge
showZero
:count="self.tree.length"
@@ -31,57 +30,67 @@
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>
<div class="cmdb-preference-group-content" v-for="ciType in group.ci_types" :key="ciType.id">
<div
:class="{
'cmdb-preference-avatar': true,
'cmdb-preference-avatar-noicon': !ciType.icon,
'cmdb-preference-avatar-noicon-is_subscribed': !ciType.icon && ciType.is_subscribed,
}"
:style="{ width: '30px', height: '30px', marginRight: '10px' }"
>
<template v-if="ciType.icon">
<img
v-if="ciType.icon.split('$$')[2]"
:src="`/api/common-setting/v1/file/${ciType.icon.split('$$')[3]}`"
:style="{ maxHeight: '30px', maxWidth: '30px' }"
/>
<ops-icon
v-else
:style="{
color: ciType.icon.split('$$')[1],
fontSize: '14px',
}"
:type="ciType.icon.split('$$')[0]"
/>
</template>
<span v-else :style="{ fontSize: '20px' }">{{ ciType.name[0].toUpperCase() }}</span>
<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,
'cmdb-preference-avatar-noicon': !ciType.icon,
'cmdb-preference-avatar-noicon-is_subscribed': !ciType.icon && ciType.is_subscribed,
}"
:style="{ width: '30px', height: '30px', marginRight: '10px' }"
>
<template v-if="ciType.icon">
<img
v-if="ciType.icon.split('$$')[2]"
:src="`/api/common-setting/v1/file/${ciType.icon.split('$$')[3]}`"
:style="{ maxHeight: '30px', maxWidth: '30px' }"
/>
<ops-icon
v-else
:style="{
color: ciType.icon.split('$$')[1],
fontSize: '14px',
}"
:type="ciType.icon.split('$$')[0]"
/>
</template>
<span v-else :style="{ fontSize: '20px' }">{{ ciType.name[0].toUpperCase() }}</span>
</div>
<span class="cmdb-preference-group-content-title">{{ ciType.alias || ciType.name }}</span>
<span class="cmdb-preference-group-content-action">
<a-tooltip :title="$t('cmdb.preference.cancelSub')">
<span
@click="unsubscribe(ciType, group.type)"
><ops-icon type="cmdb-preference-cancel-subscribe" />
</span>
</a-tooltip>
<a-divider type="vertical" :style="{ margin: '0 3px' }" />
<a-tooltip :title="$t('cmdb.preference.editSub')">
<span
@click="openSubscribeSetting(ciType, `${index + 1}`)"
><ops-icon
type="cmdb-preference-subscribe"
/></span>
</a-tooltip>
</span>
</div>
<span class="cmdb-preference-group-content-title">{{ ciType.alias || ciType.name }}</span>
<span class="cmdb-preference-group-content-action">
<a-tooltip :title="$t('cmdb.preference.cancelSub')">
<span
@click="unsubscribe(ciType, group.type)"
><ops-icon type="cmdb-preference-cancel-subscribe" />
</span>
</a-tooltip>
<a-divider type="vertical" :style="{ margin: '0 3px' }" />
<a-tooltip :title="$t('cmdb.preference.editSub')">
<span
@click="openSubscribeSetting(ciType, `${index + 1}`)"
><ops-icon
type="cmdb-preference-subscribe"
/></span>
</a-tooltip>
</span>
</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
>{{ 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-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 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;

File diff suppressed because it is too large Load Diff

View File

@@ -1,224 +1,250 @@
<template>
<a-modal
v-model="visible"
width="90%"
:closable="false"
:centered="true"
:maskClosable="false"
:destroyOnClose="true"
@cancel="handleClose"
@ok="handleOk"
>
<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> -->
<vxe-table
ref="xTable"
row-id="_id"
:data="tableData"
:height="tableHeight"
highlight-hover-row
:checkbox-config="{ reserve: true }"
@checkbox-change="onSelectChange"
@checkbox-all="onSelectChange"
show-overflow="tooltip"
show-header-overflow="tooltip"
:scroll-y="{ enabled: true, gt: 50 }"
:scroll-x="{ enabled: true, gt: 0 }"
class="ops-stripe-table"
>
<vxe-column align="center" type="checkbox" width="60" fixed="left"></vxe-column>
<vxe-table-column
v-for="col in columns"
:key="col.field"
:title="col.title"
:field="col.field"
:width="col.width"
:sortable="col.sortable"
>
<template #default="{row}" v-if="col.value_type === '6'">
<span v-if="col.value_type === '6' && row[col.field]">{{ JSON.stringify(row[col.field]) }}</span>
</template>
</vxe-table-column>
</vxe-table>
<a-pagination
v-model="currentPage"
size="small"
:total="totalNumber"
show-quick-jumper
:page-size="50"
:show-total="
(total, range) =>
$t('pagination.total', {
range0: range[0],
range1: range[1],
total,
})
"
:style="{ textAlign: 'right', marginTop: '10px' }"
@change="handleChangePage"
/>
</a-spin>
</div>
</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'
export default {
name: 'AddTableModal',
components: { SearchForm },
data() {
return {
visible: false,
currentPage: 1,
totalNumber: 0,
tableData: [],
columns: [],
ciObj: {},
ciId: null,
addTypeId: null,
loading: false,
expression: '',
isFocusExpression: false,
type: 'children',
preferenceAttrList: [],
ancestor_ids: undefined,
}
},
computed: {
tableHeight() {
return this.$store.state.windowHeight - 250
},
placeholder() {
return this.isFocusExpression ? this.$t('cmdb.serviceTreetips1') : this.$t('cmdb.serviceTreetips2')
},
width() {
return this.isFocusExpression ? '500px' : '100px'
},
},
watch: {},
methods: {
async openModal(ciObj, ciId, addTypeId, type, ancestor_ids = undefined) {
console.log(ciObj, ciId, addTypeId, type)
this.visible = true
this.ciObj = ciObj
this.ciId = ciId
this.addTypeId = addTypeId
this.type = type
this.ancestor_ids = ancestor_ids
await getSubscribeAttributes(addTypeId).then((res) => {
this.preferenceAttrList = res.attributes // 已经订阅的全部列
})
this.getTableData(true)
},
async getTableData(isInit) {
if (this.addTypeId) {
await this.fetchData(isInit)
}
},
async fetchData(isInit) {
this.loading = true
// if (isInit) {
// const subscribed = await getSubscribeAttributes(this.addTypeId)
// this.preferenceAttrList = subscribed.attributes // 已经订阅的全部列
// }
let sort, fuzzySearch, expression, exp
if (!isInit) {
fuzzySearch = this.$refs['searchForm'].fuzzySearch
expression = this.$refs['searchForm'].expression || ''
const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
exp = expression.match(regQ) ? expression.match(regQ)[0] : null
}
await searchCI({
q: `_type:${this.addTypeId}${exp ? `,${exp}` : ''}${fuzzySearch ? `,*${fuzzySearch}*` : ''}`,
count: 50,
page: this.currentPage,
sort,
})
.then((res) => {
this.tableData = res.result
this.totalNumber = res.numfound
this.columns = this.getColumns(res.result, this.preferenceAttrList)
this.$nextTick(() => {
const _table = this.$refs.xTable
if (_table) {
_table.refreshColumn()
}
this.loading = false
})
})
.catch(() => {
this.loading = false
})
},
getColumns(data, attrList) {
const modalDom = document.getElementById('add-table-modal')
if (modalDom) {
const width = modalDom.clientWidth - 50
return getCITableColumns(data, attrList, width)
}
return []
},
onSelectChange() {},
handleClose() {
this.$refs.xTable.clearCheckboxRow()
this.currentPage = 1
this.expression = ''
this.isFocusExpression = false
this.visible = false
},
async handleOk() {
const selectRecordsCurrent = this.$refs.xTable.getCheckboxRecords()
const selectRecordsReserved = this.$refs.xTable.getCheckboxReserveRecords()
const ciIds = [...selectRecordsCurrent, ...selectRecordsReserved].map((record) => record._id)
if (ciIds.length) {
if (this.type === 'children') {
await batchUpdateCIRelationChildren(ciIds, [this.ciId], this.ancestor_ids)
} else {
await batchUpdateCIRelationParents(ciIds, [this.ciId])
}
setTimeout(() => {
this.$message.success(this.$t('addSuccess'))
this.handleClose()
this.$emit('reload')
}, 500)
}
},
handleSearch() {
this.currentPage = 1
this.fetchData()
},
handleChangePage(page, pageSize) {
this.currentPage = page
this.fetchData()
},
},
}
</script>
<style lang="less" scoped></style>
<template>
<a-modal
v-model="visible"
width="90%"
:closable="false"
:centered="true"
:maskClosable="false"
:destroyOnClose="true"
@cancel="handleClose"
@ok="handleOk"
>
<div :style="{ width: '100%' }" id="add-table-modal">
<a-spin :spinning="loading">
<SearchForm
ref="searchForm"
:typeId="addTypeId"
:preferenceAttrList="preferenceAttrList"
@refresh="handleSearch"
>
<a-button
@click="
() => {
$refs.createInstanceForm.handleOpen(true, 'create')
}
"
slot="extraContent"
type="primary"
size="small"
>新增</a-button
>
</SearchForm>
<vxe-table
ref="xTable"
row-id="_id"
:data="tableData"
:height="tableHeight"
highlight-hover-row
:checkbox-config="{ reserve: true }"
@checkbox-change="onSelectChange"
@checkbox-all="onSelectChange"
show-overflow="tooltip"
show-header-overflow="tooltip"
:scroll-y="{ enabled: true, gt: 50 }"
:scroll-x="{ enabled: true, gt: 0 }"
class="ops-stripe-table"
>
<vxe-column align="center" type="checkbox" width="60" fixed="left"></vxe-column>
<vxe-table-column
v-for="col in columns"
:key="col.field"
:title="col.title"
:field="col.field"
:width="col.width"
:sortable="col.sortable"
>
<template #default="{row}" v-if="col.value_type === '6'">
<span v-if="col.value_type === '6' && row[col.field]">{{ JSON.stringify(row[col.field]) }}</span>
</template>
</vxe-table-column>
</vxe-table>
<a-pagination
v-model="currentPage"
size="small"
:total="totalNumber"
show-quick-jumper
:page-size="50"
:show-total="
(total, range) =>
$t('pagination.total', {
range0: range[0],
range1: range[1],
total,
})
"
:style="{ textAlign: 'right', marginTop: '10px' }"
@change="handleChangePage"
/>
</a-spin>
</div>
<CreateInstanceForm
ref="createInstanceForm"
:typeIdFromRelation="addTypeId"
@reload="
() => {
currentPage = 1
getTableData(true)
}
"
/>
</a-modal>
</template>
<script>
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, CreateInstanceForm },
data() {
return {
visible: false,
currentPage: 1,
totalNumber: 0,
tableData: [],
columns: [],
ciObj: {},
ciId: null,
addTypeId: null,
loading: false,
expression: '',
isFocusExpression: false,
type: 'children',
preferenceAttrList: [],
ancestor_ids: undefined,
attrList1: [],
}
},
computed: {
tableHeight() {
return this.$store.state.windowHeight - 250
},
placeholder() {
return this.isFocusExpression ? this.$t('cmdb.serviceTreetips1') : this.$t('cmdb.serviceTreetips2')
},
width() {
return this.isFocusExpression ? '500px' : '100px'
},
},
provide() {
return {
attrList: () => {
return this.attrList
},
}
},
watch: {},
methods: {
async openModal(ciObj, ciId, addTypeId, type, ancestor_ids = undefined) {
console.log(ciObj, ciId, addTypeId, type)
this.visible = true
this.ciObj = ciObj
this.ciId = ciId
this.addTypeId = addTypeId
this.type = type
this.ancestor_ids = ancestor_ids
await getSubscribeAttributes(addTypeId).then((res) => {
this.preferenceAttrList = res.attributes // 已经订阅的全部列
})
getCITypeAttributesById(addTypeId).then((res) => {
this.attrList = res.attributes
})
this.getTableData(true)
},
async getTableData(isInit) {
if (this.addTypeId) {
await this.fetchData(isInit)
}
},
async fetchData(isInit) {
this.loading = true
// if (isInit) {
// const subscribed = await getSubscribeAttributes(this.addTypeId)
// this.preferenceAttrList = subscribed.attributes // 已经订阅的全部列
// }
let sort, fuzzySearch, expression, exp
if (!isInit) {
fuzzySearch = this.$refs['searchForm'].fuzzySearch
expression = this.$refs['searchForm'].expression || ''
const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
exp = expression.match(regQ) ? expression.match(regQ)[0] : null
}
await searchCI({
q: `_type:${this.addTypeId}${exp ? `,${exp}` : ''}${fuzzySearch ? `,*${fuzzySearch}*` : ''}`,
count: 50,
page: this.currentPage,
sort,
})
.then((res) => {
this.tableData = res.result
this.totalNumber = res.numfound
this.columns = this.getColumns(res.result, this.preferenceAttrList)
this.$nextTick(() => {
const _table = this.$refs.xTable
if (_table) {
_table.refreshColumn()
}
this.loading = false
})
})
.catch(() => {
this.loading = false
})
},
getColumns(data, attrList) {
const modalDom = document.getElementById('add-table-modal')
if (modalDom) {
const width = modalDom.clientWidth - 50
return getCITableColumns(data, attrList, width)
}
return []
},
onSelectChange() {},
handleClose() {
this.$refs.xTable.clearCheckboxRow()
this.currentPage = 1
this.expression = ''
this.isFocusExpression = false
this.visible = false
},
async handleOk() {
const selectRecordsCurrent = this.$refs.xTable.getCheckboxRecords()
const selectRecordsReserved = this.$refs.xTable.getCheckboxReserveRecords()
const ciIds = [...selectRecordsCurrent, ...selectRecordsReserved].map((record) => record._id)
if (ciIds.length) {
if (this.type === 'children') {
await batchUpdateCIRelationChildren(ciIds, [this.ciId], this.ancestor_ids)
} else {
await batchUpdateCIRelationParents(ciIds, [this.ciId])
}
setTimeout(() => {
this.$message.success(this.$t('addSuccess'))
this.handleClose()
this.$emit('reload')
}, 500)
} else {
this.handleClose()
this.$emit('reload')
}
},
handleSearch() {
this.currentPage = 1
this.fetchData()
},
handleChangePage(page, pageSize) {
this.currentPage = page
this.fetchData()
},
},
}
</script>
<style lang="less" scoped></style>

View File

@@ -1,166 +1,253 @@
<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',
}"
@click="clickNode"
>
<span
:style="{
display: 'flex',
overflow: 'hidden',
width: '100%',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
alignItems: 'center',
}"
>
<template v-if="icon">
<img
v-if="icon.split('$$')[2]"
:src="`/api/common-setting/v1/file/${icon.split('$$')[3]}`"
:style="{ maxHeight: '14px', maxWidth: '14px' }"
/>
<ops-icon
v-else
:style="{
color: icon.split('$$')[1],
fontSize: '14px',
}"
:type="icon.split('$$')[0]"
/>
</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>
<a-icon :style="{ fontSize: '10px' }" v-if="childLength && !isLeaf" :type="switchIcon"></a-icon>
</div>
</a-dropdown>
</template>
<script>
export default {
name: 'ContextMenu',
props: {
title: {
type: String,
default: '',
},
treeKey: {
type: String,
default: '',
},
levels: {
type: Array,
default: () => [],
},
currentViews: {
type: Object,
default: () => {},
},
id2type: {
type: Object,
default: () => {},
},
isLeaf: {
type: Boolean,
default: () => false,
},
ciTypes: {
type: Array,
default: () => [],
},
},
data() {
return {
switchIcon: 'down',
}
},
computed: {
childLength() {
const reg = /(?<=\()\S+(?=\))/g
return Number(this.title.match(reg)[0])
},
splitTreeKey() {
return this.treeKey.split('@^@')
},
_tempTree() {
return this.splitTreeKey[this.splitTreeKey.length - 1].split('%')
},
_typeIdIdx() {
return this.levels.findIndex((level) => level[0] === Number(this._tempTree[1])) // 当前节点在levels中的index
},
showDelete() {
if (this._typeIdIdx === 0) {
// 如果是第一层节点则不能删除
return false
}
return true
},
menuList() {
let _menuList = []
if (this._typeIdIdx > -1 && this._typeIdIdx < this.levels.length - 1) {
// 不是叶子节点
const id = Number(this.levels[this._typeIdIdx + 1])
_menuList = [
{
id,
alias: this.id2type[id].alias || this.id2type[id].name,
},
]
} else {
// 叶子节点
_menuList = this.currentViews.node2show_types[this._tempTree[1]].map((item) => {
return { id: item.id, alias: item.alias || item.name }
})
}
return _menuList
},
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
},
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 || ''
},
},
methods: {
onContextMenuClick(treeKey, menuKey) {
this.$emit('onContextMenuClick', treeKey, menuKey)
},
clickNode() {
this.$emit('onNodeClick', this.treeKey)
this.switchIcon = this.switchIcon === 'down' ? 'up' : 'down'
},
},
}
</script>
<style></style>
<template>
<div
:class="{
'relation-views-node': true,
'relation-views-node-checkbox': showCheckbox,
}"
@click="clickNode"
>
<span>
<a-checkbox @click.stop="clickCheckbox" class="relation-views-node-checkbox" v-if="showCheckbox" />
<template v-if="icon">
<img
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-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 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>
</template>
<script>
export default {
name: 'ContextMenu',
props: {
title: {
type: String,
default: '',
},
treeKey: {
type: String,
default: '',
},
levels: {
type: Array,
default: () => [],
},
currentViews: {
type: Object,
default: () => {},
},
id2type: {
type: Object,
default: () => {},
},
isLeaf: {
type: Boolean,
default: () => false,
},
ciTypeIcons: {
type: Object,
default: () => {},
},
showBatchLevel: {
type: Number,
default: null,
},
batchTreeKey: {
type: Array,
default: () => [],
},
},
data() {
return {
switchIcon: 'down',
}
},
computed: {
childLength() {
const reg = /(?<=\()\S+(?=\))/g
return Number(this.title.match(reg)[0])
},
splitTreeKey() {
return this.treeKey.split('@^@')
},
_tempTree() {
return this.splitTreeKey[this.splitTreeKey.length - 1].split('%')
},
_typeIdIdx() {
return this.levels.findIndex((level) => level[0] === Number(this._tempTree[1])) // 当前节点在levels中的index
},
showDelete() {
if (this._typeIdIdx === 0) {
// 如果是第一层节点则不能删除
return false
}
return true
},
menuList() {
let _menuList = []
if (this._typeIdIdx > -1 && this._typeIdIdx < this.levels.length - 1) {
// 不是叶子节点
const id = Number(this.levels[this._typeIdIdx + 1])
_menuList = [
{
id,
alias: this.id2type[id].alias || this.id2type[id].name,
},
]
} else {
// 叶子节点
_menuList = this.currentViews.node2show_types[this._tempTree[1]].map((item) => {
return { id: item.id, alias: item.alias || item.name }
})
}
return _menuList
},
icon() {
const _split = this.treeKey.split('@^@')
const currentNodeTypeId = _split[_split.length - 1].split('%')[1]
return this.ciTypeIcons[Number(currentNodeTypeId)] ?? null
},
showCheckbox() {
return this.showBatchLevel === this.treeKey.split('@^@').filter((item) => !!item).length - 1
},
},
methods: {
onContextMenuClick(treeKey, menuKey) {
this.$emit('onContextMenuClick', treeKey, menuKey)
},
clickNode() {
this.$emit('onNodeClick', this.treeKey)
this.switchIcon = this.switchIcon === 'down' ? 'up' : 'down'
},
clickCheckbox() {
this.$emit('clickCheckbox', this.treeKey)
},
},
}
</script>
<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>

File diff suppressed because it is too large Load Diff

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