Merge branch 'veops:master' into master

This commit is contained in:
thexqn 2024-08-23 14:56:12 +08:00 committed by GitHub
commit de51cb3e21
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
57 changed files with 2560 additions and 1192 deletions

View File

@ -167,24 +167,30 @@ class AttributeManager(object):
def get_attribute_by_name(self, name): def get_attribute_by_name(self, name):
attr = Attribute.get_by(name=name, first=True) attr = Attribute.get_by(name=name, first=True)
if attr.get("is_choice"): if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"], attr["choice_value"] = self.get_choice_values(attr["id"],
attr["choice_web_hook"], attr.get("choice_other")) attr["value_type"],
attr["choice_web_hook"],
attr.get("choice_other"))
return attr return attr
def get_attribute_by_alias(self, alias): def get_attribute_by_alias(self, alias):
attr = Attribute.get_by(alias=alias, first=True) attr = Attribute.get_by(alias=alias, first=True)
if attr.get("is_choice"): if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"], attr["choice_value"] = self.get_choice_values(attr["id"],
attr["choice_web_hook"], attr.get("choice_other")) attr["value_type"],
attr["choice_web_hook"],
attr.get("choice_other"))
return attr return attr
def get_attribute_by_id(self, _id): def get_attribute_by_id(self, _id):
attr = Attribute.get_by_id(_id).to_dict() attr = Attribute.get_by_id(_id).to_dict()
if attr.get("is_choice"): if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"], attr["choice_value"] = self.get_choice_values(attr["id"],
attr["choice_web_hook"], attr.get("choice_other")) attr["value_type"],
attr["choice_web_hook"],
attr.get("choice_other"))
return attr return attr

View File

@ -266,7 +266,7 @@ class CIManager(object):
value_table = TableMap(attr_name=id2name[attr_id]).table value_table = TableMap(attr_name=id2name[attr_id]).table
values = value_table.get_by(attr_id=attr_id, values = value_table.get_by(attr_id=attr_id,
value=ci_dict.get(id2name[attr_id]) or None, value=ci_dict.get(id2name[attr_id]),
only_query=True).join( only_query=True).join(
CI, CI.id == value_table.ci_id).filter(CI.type_id == type_id) CI, CI.id == value_table.ci_id).filter(CI.type_id == type_id)
_ci_ids = set([i.ci_id for i in values]) _ci_ids = set([i.ci_id for i in values])
@ -292,6 +292,53 @@ class CIManager(object):
return 1 return 1
@staticmethod
def _reference_to_ci_id(attr, payload):
def __unique_value2id(_type, _v):
value_table = TableMap(attr_name=_type.unique_id).table
ci = value_table.get_by(attr_id=attr.id, value=_v)
if ci is not None:
return ci.ci_id
return abort(400, ErrFormat.ci_reference_invalid.format(attr.alias, _v))
def __valid_reference_id_existed(_id, _type_id):
ci = CI.get_by_id(_id) or abort(404, ErrFormat.ci_reference_not_found.format(attr.alias, _id))
if ci.type_id != _type_id:
return abort(400, ErrFormat.ci_reference_invalid.format(attr.alias, _id))
if attr.name in payload:
k, reference_value = attr.name, payload[attr.name]
elif attr.alias in payload:
k, reference_value = attr.alias, payload[attr.alias]
else:
return
if not reference_value:
return
reference_type = None
if isinstance(reference_value, list):
for idx, v in enumerate(reference_value):
if isinstance(v, dict) and v.get('unique'):
if reference_type is None:
reference_type = CITypeCache.get(attr.reference_type_id)
if reference_type is not None:
reference_value[idx] = __unique_value2id(reference_type, v)
else:
__valid_reference_id_existed(v, attr.reference_type_id)
elif isinstance(reference_value, dict) and reference_value.get('unique'):
if reference_type is None:
reference_type = CITypeCache.get(attr.reference_type_id)
if reference_type is not None:
reference_value = __unique_value2id(reference_type, reference_value)
elif str(reference_value).isdigit():
reference_value = int(reference_value)
__valid_reference_id_existed(reference_value, attr.reference_type_id)
payload[k] = reference_value
@classmethod @classmethod
def add(cls, ci_type_name, def add(cls, ci_type_name,
exist_policy=ExistPolicy.REPLACE, exist_policy=ExistPolicy.REPLACE,
@ -392,6 +439,8 @@ class CIManager(object):
if attr.re_check and password_dict.get(attr.id): if attr.re_check and password_dict.get(attr.id):
value_manager.check_re(attr.re_check, attr.alias, password_dict[attr.id][0]) value_manager.check_re(attr.re_check, attr.alias, password_dict[attr.id][0])
elif attr.is_reference:
cls._reference_to_ci_id(attr, ci_dict)
cls._valid_unique_constraint(ci_type.id, ci_dict, ci and ci.id) cls._valid_unique_constraint(ci_type.id, ci_dict, ci and ci.id)
@ -443,9 +492,10 @@ class CIManager(object):
return ci.id return ci.id
def update(self, ci_id, _is_admin=False, ticket_id=None, __sync=False, **ci_dict): def update(self, ci_id, _is_admin=False, ticket_id=None, _sync=False, **ci_dict):
now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
ci = self.confirm_ci_existed(ci_id) ci = self.confirm_ci_existed(ci_id)
ci_type = ci.ci_type
attrs = CITypeAttributeManager.get_all_attributes(ci.type_id) attrs = CITypeAttributeManager.get_all_attributes(ci.type_id)
ci_type_attrs_name = {attr.name: attr for _, attr in attrs} ci_type_attrs_name = {attr.name: attr for _, attr in attrs}
@ -475,11 +525,13 @@ class CIManager(object):
if attr.re_check and password_dict.get(attr.id): if attr.re_check and password_dict.get(attr.id):
value_manager.check_re(attr.re_check, attr.alias, password_dict[attr.id][0]) value_manager.check_re(attr.re_check, attr.alias, password_dict[attr.id][0])
elif attr.is_reference:
self._reference_to_ci_id(attr, ci_dict)
limit_attrs = self._valid_ci_for_no_read(ci) if not _is_admin else {} limit_attrs = self._valid_ci_for_no_read(ci) if not _is_admin else {}
record_id = None record_id = None
with redis_lock.Lock(rd.r, ci.ci_type.name): with redis_lock.Lock(rd.r, ci_type.name):
db.session.commit() db.session.commit()
self._valid_unique_constraint(ci.type_id, ci_dict, ci_id) self._valid_unique_constraint(ci.type_id, ci_dict, ci_id)
@ -510,14 +562,14 @@ class CIManager(object):
record_id = self.save_password(ci.id, attr_id, password_dict[attr_id], record_id, ci.type_id) record_id = self.save_password(ci.id, attr_id, password_dict[attr_id], record_id, ci.type_id)
if record_id or has_dynamic: # has changed if record_id or has_dynamic: # has changed
if not __sync: if not _sync:
ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE) ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE)
else: else:
ci_cache(ci_id, OperateType.UPDATE, record_id) ci_cache(ci_id, OperateType.UPDATE, record_id)
ref_ci_dict = {k: v for k, v in ci_dict.items() if k.startswith("$") and "." in k} ref_ci_dict = {k: v for k, v in ci_dict.items() if k.startswith("$") and "." in k}
if ref_ci_dict: if ref_ci_dict:
if not __sync: if not _sync:
ci_relation_add.apply_async(args=(ref_ci_dict, ci.id), queue=CMDB_QUEUE) ci_relation_add.apply_async(args=(ref_ci_dict, ci.id), queue=CMDB_QUEUE)
else: else:
ci_relation_add(ref_ci_dict, ci.id) ci_relation_add(ref_ci_dict, ci.id)
@ -578,7 +630,7 @@ class CIManager(object):
if ci_dict: if ci_dict:
AttributeHistoryManger.add(None, ci_id, [(None, OperateType.DELETE, ci_dict, None)], ci.type_id) AttributeHistoryManger.add(None, ci_id, [(None, OperateType.DELETE, ci_dict, None)], ci.type_id)
ci_delete.apply_async(args=(ci_id,), queue=CMDB_QUEUE) ci_delete.apply_async(args=(ci_id, ci.type_id), queue=CMDB_QUEUE)
delete_id_filter.apply_async(args=(ci_id,), queue=CMDB_QUEUE) delete_id_filter.apply_async(args=(ci_id,), queue=CMDB_QUEUE)
return ci_id return ci_id
@ -773,7 +825,7 @@ class CIManager(object):
value_table = ValueTypeMap.table[ValueTypeEnum.PASSWORD] value_table = ValueTypeMap.table[ValueTypeEnum.PASSWORD]
if current_app.config.get('SECRETS_ENGINE') == 'inner': if current_app.config.get('SECRETS_ENGINE') == 'inner':
if value: if value:
encrypt_value, status = InnerCrypt().encrypt(value) encrypt_value, status = InnerCrypt().encrypt(str(value))
if not status: if not status:
current_app.logger.error('save password failed: {}'.format(encrypt_value)) current_app.logger.error('save password failed: {}'.format(encrypt_value))
return abort(400, ErrFormat.password_save_failed.format(encrypt_value)) return abort(400, ErrFormat.password_save_failed.format(encrypt_value))
@ -801,7 +853,7 @@ class CIManager(object):
vault = VaultClient(current_app.config.get('VAULT_URL'), current_app.config.get('VAULT_TOKEN')) vault = VaultClient(current_app.config.get('VAULT_URL'), current_app.config.get('VAULT_TOKEN'))
if value: if value:
try: try:
vault.update("/{}/{}".format(ci_id, attr_id), dict(v=value)) vault.update("/{}/{}".format(ci_id, attr_id), dict(v=str(value)))
except Exception as e: except Exception as e:
current_app.logger.error('save password to vault failed: {}'.format(e)) current_app.logger.error('save password to vault failed: {}'.format(e))
return abort(400, ErrFormat.password_save_failed.format('write vault failed')) return abort(400, ErrFormat.password_save_failed.format('write vault failed'))

View File

@ -145,7 +145,7 @@ class CITypeManager(object):
kwargs["alias"] = kwargs["name"] if not kwargs.get("alias") else kwargs["alias"] kwargs["alias"] = kwargs["name"] if not kwargs.get("alias") else kwargs["alias"]
cls._validate_unique(name=kwargs['name']) cls._validate_unique(name=kwargs['name'])
cls._validate_unique(alias=kwargs['alias']) # cls._validate_unique(alias=kwargs['alias'])
kwargs["unique_id"] = unique_key.id kwargs["unique_id"] = unique_key.id
kwargs['uid'] = current_user.uid kwargs['uid'] = current_user.uid
@ -184,7 +184,7 @@ class CITypeManager(object):
ci_type = cls.check_is_existed(type_id) ci_type = cls.check_is_existed(type_id)
cls._validate_unique(type_id=type_id, name=kwargs.get('name')) cls._validate_unique(type_id=type_id, name=kwargs.get('name'))
cls._validate_unique(type_id=type_id, alias=kwargs.get('alias') or kwargs.get('name')) # cls._validate_unique(type_id=type_id, alias=kwargs.get('alias') or kwargs.get('name'))
unique_key = kwargs.pop("unique_key", None) unique_key = kwargs.pop("unique_key", None)
unique_key = AttributeCache.get(unique_key) unique_key = AttributeCache.get(unique_key)
@ -234,6 +234,10 @@ class CITypeManager(object):
if CITypeInheritance.get_by(parent_id=type_id, first=True): if CITypeInheritance.get_by(parent_id=type_id, first=True):
return abort(400, ErrFormat.ci_type_inheritance_cannot_delete) return abort(400, ErrFormat.ci_type_inheritance_cannot_delete)
reference = Attribute.get_by(reference_type_id=type_id, first=True, to_dict=False)
if reference is not None:
return abort(400, ErrFormat.ci_type_referenced_cannot_delete.format(reference.alias))
relation_views = PreferenceRelationView.get_by(to_dict=False) relation_views = PreferenceRelationView.get_by(to_dict=False)
for rv in relation_views: for rv in relation_views:
for item in (rv.cr_ids or []): for item in (rv.cr_ids or []):
@ -1323,6 +1327,7 @@ class CITypeTemplateManager(object):
def _import_attributes(self, type2attributes): def _import_attributes(self, type2attributes):
attributes = [attr for type_id in type2attributes for attr in type2attributes[type_id]] attributes = [attr for type_id in type2attributes for attr in type2attributes[type_id]]
attrs = [] attrs = []
references = []
for i in copy.deepcopy(attributes): for i in copy.deepcopy(attributes):
if i.pop('inherited', None): if i.pop('inherited', None):
continue continue
@ -1337,6 +1342,10 @@ class CITypeTemplateManager(object):
if not choice_value: if not choice_value:
i['is_choice'] = False i['is_choice'] = False
if i.get('reference_type_id'):
references.append(copy.deepcopy(i))
i.pop('reference_type_id')
attrs.append((i, choice_value)) attrs.append((i, choice_value))
attr_id_map = self.__import(Attribute, [i[0] for i in copy.deepcopy(attrs)]) attr_id_map = self.__import(Attribute, [i[0] for i in copy.deepcopy(attrs)])
@ -1345,7 +1354,7 @@ class CITypeTemplateManager(object):
if choice_value and not i.get('choice_web_hook') and not i.get('choice_other'): if choice_value and not i.get('choice_web_hook') and not i.get('choice_other'):
AttributeManager.add_choice_values(attr_id_map.get(i['id'], i['id']), i['value_type'], choice_value) AttributeManager.add_choice_values(attr_id_map.get(i['id'], i['id']), i['value_type'], choice_value)
return attr_id_map return attr_id_map, references
def _import_ci_types(self, ci_types, attr_id_map): def _import_ci_types(self, ci_types, attr_id_map):
for i in ci_types: for i in ci_types:
@ -1359,6 +1368,11 @@ class CITypeTemplateManager(object):
return self.__import(CIType, ci_types) return self.__import(CIType, ci_types)
def _import_reference_attributes(self, attrs, type_id_map):
for attr in attrs:
attr['reference_type_id'] = type_id_map.get(attr['reference_type_id'], attr['reference_type_id'])
self.__import(Attribute, attrs)
def _import_ci_type_groups(self, ci_type_groups, type_id_map): def _import_ci_type_groups(self, ci_type_groups, type_id_map):
_ci_type_groups = copy.deepcopy(ci_type_groups) _ci_type_groups = copy.deepcopy(ci_type_groups)
for i in _ci_type_groups: for i in _ci_type_groups:
@ -1584,13 +1598,15 @@ class CITypeTemplateManager(object):
import time import time
s = time.time() s = time.time()
attr_id_map = self._import_attributes(tpt.get('type2attributes') or {}) attr_id_map, references = self._import_attributes(tpt.get('type2attributes') or {})
current_app.logger.info('import attributes cost: {}'.format(time.time() - s)) current_app.logger.info('import attributes cost: {}'.format(time.time() - s))
s = time.time() s = time.time()
ci_type_id_map = self._import_ci_types(tpt.get('ci_types') or [], attr_id_map) ci_type_id_map = self._import_ci_types(tpt.get('ci_types') or [], attr_id_map)
current_app.logger.info('import ci_types cost: {}'.format(time.time() - s)) current_app.logger.info('import ci_types cost: {}'.format(time.time() - s))
self._import_reference_attributes(references, ci_type_id_map)
s = time.time() s = time.time()
self._import_ci_type_groups(tpt.get('ci_type_groups') or [], ci_type_id_map) self._import_ci_type_groups(tpt.get('ci_type_groups') or [], ci_type_id_map)
current_app.logger.info('import ci_type_groups cost: {}'.format(time.time() - s)) current_app.logger.info('import ci_type_groups cost: {}'.format(time.time() - s))
@ -1675,6 +1691,16 @@ class CITypeTemplateManager(object):
type_ids.extend(extend_type_ids) type_ids.extend(extend_type_ids)
ci_types.extend(CITypeManager.get_ci_types(type_ids=extend_type_ids)) ci_types.extend(CITypeManager.get_ci_types(type_ids=extend_type_ids))
# handle reference type
references = Attribute.get_by(only_query=True).join(
CITypeAttribute, CITypeAttribute.attr_id == Attribute.id).filter(
CITypeAttribute.type_id.in_(type_ids)).filter(CITypeAttribute.deleted.is_(False)).filter(
Attribute.reference_type_id.isnot(None))
reference_type_ids = list(set([i.reference_type_id for i in references if i.reference_type_id]))
if reference_type_ids:
type_ids.extend(reference_type_ids)
ci_types.extend(CITypeManager.get_ci_types(type_ids=reference_type_ids))
tpt = dict( tpt = dict(
ci_types=ci_types, ci_types=ci_types,
relation_types=[i.to_dict() for i in RelationTypeManager.get_all()], relation_types=[i.to_dict() for i in RelationTypeManager.get_all()],
@ -1687,6 +1713,7 @@ class CITypeTemplateManager(object):
icons=dict() icons=dict()
) )
tpt['ci_type_groups'] = CITypeGroupManager.get(ci_types=tpt['ci_types'], type_ids=type_ids) tpt['ci_type_groups'] = CITypeGroupManager.get(ci_types=tpt['ci_types'], type_ids=type_ids)
tpt['ci_type_groups'] = [i for i in tpt['ci_type_groups'] if i.get('ci_types')]
def get_icon_value(icon): def get_icon_value(icon):
try: try:

View File

@ -14,6 +14,8 @@ class ValueTypeEnum(BaseEnum):
JSON = "6" JSON = "6"
PASSWORD = TEXT PASSWORD = TEXT
LINK = TEXT LINK = TEXT
BOOL = "7"
REFERENCE = INT
class ConstraintEnum(BaseEnum): class ConstraintEnum(BaseEnum):

View File

@ -44,6 +44,8 @@ class ErrFormat(CommonErrFormat):
unique_value_not_found = _l("The model's primary key {} does not exist!") # 模型的主键 {} 不存在! unique_value_not_found = _l("The model's primary key {} does not exist!") # 模型的主键 {} 不存在!
unique_key_required = _l("Primary key {} is missing") # 主键字段 {} 缺失 unique_key_required = _l("Primary key {} is missing") # 主键字段 {} 缺失
ci_is_already_existed = _l("CI already exists!") # CI 已经存在! ci_is_already_existed = _l("CI already exists!") # CI 已经存在!
ci_reference_not_found = _l("{}: CI reference {} does not exist!") # {}: CI引用 {} 不存在!
ci_reference_invalid = _l("{}: CI reference {} is illegal!") # {}, CI引用 {} 不合法!
relation_constraint = _l("Relationship constraint: {}, verification failed") # 关系约束: {}, 校验失败 relation_constraint = _l("Relationship constraint: {}, verification failed") # 关系约束: {}, 校验失败
# 多对多关系 限制: 模型 {} <-> {} 已经存在多对多关系! # 多对多关系 限制: 模型 {} <-> {} 已经存在多对多关系!
m2m_relation_constraint = _l( m2m_relation_constraint = _l(
@ -63,6 +65,8 @@ class ErrFormat(CommonErrFormat):
ci_exists_and_cannot_delete_inheritance = _l( ci_exists_and_cannot_delete_inheritance = _l(
"The inheritance cannot be deleted because the CI already exists") # 因为CI已经存在不能删除继承关系 "The inheritance cannot be deleted because the CI already exists") # 因为CI已经存在不能删除继承关系
ci_type_inheritance_cannot_delete = _l("The model is inherited and cannot be deleted") # 该模型被继承, 不能删除 ci_type_inheritance_cannot_delete = _l("The model is inherited and cannot be deleted") # 该模型被继承, 不能删除
ci_type_referenced_cannot_delete = _l(
"The model is referenced by attribute {} and cannot be deleted") # 该模型被属性 {} 引用, 不能删除
# 因为关系视图 {} 引用了该模型,不能删除模型 # 因为关系视图 {} 引用了该模型,不能删除模型
ci_relation_view_exists_and_cannot_delete_type = _l( ci_relation_view_exists_and_cannot_delete_type = _l(

View File

@ -451,6 +451,9 @@ class Search(object):
if field_type == ValueTypeEnum.DATE and len(v) == 10: if field_type == ValueTypeEnum.DATE and len(v) == 10:
v = "{} 00:00:00".format(v) v = "{} 00:00:00".format(v)
if field_type == ValueTypeEnum.BOOL and "*" not in str(v):
v = str(int(v in current_app.config.get('BOOL_TRUE')))
# in query # in query
if v.startswith("(") and v.endswith(")"): if v.startswith("(") and v.endswith(")"):
_query_sql = self._in_query_handler(attr, v, is_not) _query_sql = self._in_query_handler(attr, v, is_not)

View File

@ -7,6 +7,7 @@ import json
import re import re
import six import six
from flask import current_app
import api.models.cmdb as model import api.models.cmdb as model
from api.lib.cmdb.cache import AttributeCache from api.lib.cmdb.cache import AttributeCache
@ -64,6 +65,7 @@ class ValueTypeMap(object):
ValueTypeEnum.DATETIME: str2datetime, ValueTypeEnum.DATETIME: str2datetime,
ValueTypeEnum.DATE: str2date, ValueTypeEnum.DATE: str2date,
ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x, ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x,
ValueTypeEnum.BOOL: lambda x: x in current_app.config.get('BOOL_TRUE'),
} }
serialize = { serialize = {
@ -74,6 +76,7 @@ class ValueTypeMap(object):
ValueTypeEnum.DATE: lambda x: x.strftime("%Y-%m-%d") if not isinstance(x, six.string_types) else x, ValueTypeEnum.DATE: lambda x: x.strftime("%Y-%m-%d") if not isinstance(x, six.string_types) else x,
ValueTypeEnum.DATETIME: lambda x: x.strftime("%Y-%m-%d %H:%M:%S") if not isinstance(x, six.string_types) else x, ValueTypeEnum.DATETIME: lambda x: x.strftime("%Y-%m-%d %H:%M:%S") if not isinstance(x, six.string_types) else x,
ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x, ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x,
ValueTypeEnum.BOOL: lambda x: x in current_app.config.get('BOOL_TRUE'),
} }
serialize2 = { serialize2 = {
@ -84,6 +87,7 @@ class ValueTypeMap(object):
ValueTypeEnum.DATE: lambda x: (x.decode() if not isinstance(x, six.string_types) else x).split()[0], ValueTypeEnum.DATE: lambda x: (x.decode() if not isinstance(x, six.string_types) else x).split()[0],
ValueTypeEnum.DATETIME: lambda x: x.decode() if not isinstance(x, six.string_types) else x, ValueTypeEnum.DATETIME: lambda x: x.decode() if not isinstance(x, six.string_types) else x,
ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x, ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x,
ValueTypeEnum.BOOL: lambda x: x in current_app.config.get('BOOL_TRUE'),
} }
choice = { choice = {
@ -105,6 +109,7 @@ class ValueTypeMap(object):
'index_{0}'.format(ValueTypeEnum.TIME): model.CIIndexValueText, 'index_{0}'.format(ValueTypeEnum.TIME): model.CIIndexValueText,
'index_{0}'.format(ValueTypeEnum.FLOAT): model.CIIndexValueFloat, 'index_{0}'.format(ValueTypeEnum.FLOAT): model.CIIndexValueFloat,
'index_{0}'.format(ValueTypeEnum.JSON): model.CIValueJson, 'index_{0}'.format(ValueTypeEnum.JSON): model.CIValueJson,
'index_{0}'.format(ValueTypeEnum.BOOL): model.CIIndexValueInteger,
} }
table_name = { table_name = {
@ -117,6 +122,7 @@ class ValueTypeMap(object):
'index_{0}'.format(ValueTypeEnum.TIME): 'c_value_index_texts', 'index_{0}'.format(ValueTypeEnum.TIME): 'c_value_index_texts',
'index_{0}'.format(ValueTypeEnum.FLOAT): 'c_value_index_floats', 'index_{0}'.format(ValueTypeEnum.FLOAT): 'c_value_index_floats',
'index_{0}'.format(ValueTypeEnum.JSON): 'c_value_json', 'index_{0}'.format(ValueTypeEnum.JSON): 'c_value_json',
'index_{0}'.format(ValueTypeEnum.BOOL): 'c_value_index_integers',
} }
es_type = { es_type = {

View File

@ -128,14 +128,20 @@ class AttributeValueManager(object):
return abort(400, ErrFormat.attribute_value_invalid2.format(alias, value)) return abort(400, ErrFormat.attribute_value_invalid2.format(alias, value))
def _validate(self, attr, value, value_table, ci=None, type_id=None, ci_id=None, type_attr=None): def _validate(self, attr, value, value_table, ci=None, type_id=None, ci_id=None, type_attr=None):
ci = ci or {} if not attr.is_reference:
v = self._deserialize_value(attr.alias, attr.value_type, value) ci = ci or {}
v = self._deserialize_value(attr.alias, attr.value_type, value)
attr.is_choice and value and self._check_is_choice(attr, attr.value_type, v)
else:
v = value or None
attr.is_choice and value and self._check_is_choice(attr, attr.value_type, v)
attr.is_unique and self._check_is_unique( attr.is_unique and self._check_is_unique(
value_table, attr, ci and ci.id or ci_id, ci and ci.type_id or type_id, v) value_table, attr, ci and ci.id or ci_id, ci and ci.type_id or type_id, v)
self._check_is_required(ci and ci.type_id or type_id, attr, v, type_attr=type_attr) self._check_is_required(ci and ci.type_id or type_id, attr, v, type_attr=type_attr)
if attr.is_reference:
return v
if v == "" and attr.value_type not in (ValueTypeEnum.TEXT,): if v == "" and attr.value_type not in (ValueTypeEnum.TEXT,):
v = None v = None

View File

@ -58,7 +58,7 @@ def _request_messenger(subject, body, tos, sender, payload):
def notify_send(subject, body, methods, tos, payload=None): def notify_send(subject, body, methods, tos, payload=None):
payload = payload or {} payload = payload or {}
payload = {k: v or '' for k, v in payload.items()} payload = {k: '' if v is None else v for k, v in payload.items()}
subject = Template(subject).render(payload) subject = Template(subject).render(payload)
body = Template(body).render(payload) body = Template(body).render(payload)

View File

@ -105,6 +105,10 @@ class Attribute(Model):
is_password = db.Column(db.Boolean, default=False) is_password = db.Column(db.Boolean, default=False)
is_sortable = db.Column(db.Boolean, default=False) is_sortable = db.Column(db.Boolean, default=False)
is_dynamic = db.Column(db.Boolean, default=False) is_dynamic = db.Column(db.Boolean, default=False)
is_bool = db.Column(db.Boolean, default=False)
is_reference = db.Column(db.Boolean, default=False)
reference_type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'))
default = db.Column(db.JSON) # {"default": None} default = db.Column(db.JSON) # {"default": None}

View File

@ -20,10 +20,12 @@ 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 REDIS_PREFIX_CI_RELATION2
from api.lib.cmdb.const import RelationSourceEnum from api.lib.cmdb.const import RelationSourceEnum
from api.lib.cmdb.perms import CIFilterPermsCRUD from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.cmdb.utils import TableMap
from api.lib.decorator import flush_db from api.lib.decorator import flush_db
from api.lib.decorator import reconnect_db from api.lib.decorator import reconnect_db
from api.lib.perm.acl.cache import UserCache from api.lib.perm.acl.cache import UserCache
from api.lib.utils import handle_arg_list from api.lib.utils import handle_arg_list
from api.models.cmdb import Attribute
from api.models.cmdb import AutoDiscoveryCI from api.models.cmdb import AutoDiscoveryCI
from api.models.cmdb import AutoDiscoveryCIType from api.models.cmdb import AutoDiscoveryCIType
from api.models.cmdb import AutoDiscoveryCITypeRelation from api.models.cmdb import AutoDiscoveryCITypeRelation
@ -84,7 +86,7 @@ def batch_ci_cache(ci_ids, ): # only for attribute change index
@celery.task(name="cmdb.ci_delete", queue=CMDB_QUEUE) @celery.task(name="cmdb.ci_delete", queue=CMDB_QUEUE)
@reconnect_db @reconnect_db
def ci_delete(ci_id): def ci_delete(ci_id, type_id):
current_app.logger.info(ci_id) current_app.logger.info(ci_id)
if current_app.config.get("USE_ES"): if current_app.config.get("USE_ES"):
@ -99,6 +101,12 @@ def ci_delete(ci_id):
adt.update(updated_at=datetime.datetime.now()) adt.update(updated_at=datetime.datetime.now())
instance.delete() instance.delete()
for attr in Attribute.get_by(reference_type_id=type_id, to_dict=False):
table = TableMap(attr=attr).table
for i in getattr(table, 'get_by')(attr_id=attr.id, value=ci_id, to_dict=False):
i.delete()
ci_cache(i.ci_id, None, None)
current_app.logger.info("{0} delete..........".format(ci_id)) current_app.logger.info("{0} delete..........".format(ci_id))

View File

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PROJECT VERSION\n" "Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2024-06-20 19:12+0800\n" "POT-Creation-Date: 2024-08-20 13:47+0800\n"
"PO-Revision-Date: 2023-12-25 20:21+0800\n" "PO-Revision-Date: 2023-12-25 20:21+0800\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: zh\n" "Language: zh\n"
@ -16,7 +16,7 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.14.0\n" "Generated-By: Babel 2.16.0\n"
#: api/lib/resp_format.py:7 #: api/lib/resp_format.py:7
msgid "unauthorized" msgid "unauthorized"
@ -169,8 +169,8 @@ msgstr "目前只允许 属性创建人、管理员 删除属性!"
#: api/lib/cmdb/resp_format.py:37 #: api/lib/cmdb/resp_format.py:37
msgid "" msgid ""
"Attribute field names cannot be built-in fields: id, _id, ci_id, type, " "Attribute field names cannot be built-in fields: id, _id, ci_id, type, "
"_type, ci_type" "_type, ci_type, ticket_id"
msgstr "属性字段名不能是内置字段: id, _id, ci_id, type, _type, ci_type" msgstr "属性字段名不能是内置字段: id, _id, ci_id, type, _type, ci_type, ci_type, ticket_id"
#: api/lib/cmdb/resp_format.py:39 #: api/lib/cmdb/resp_format.py:39
msgid "Predefined value: Other model request parameters are illegal!" msgid "Predefined value: Other model request parameters are illegal!"
@ -197,286 +197,298 @@ msgid "CI already exists!"
msgstr "CI 已经存在!" msgstr "CI 已经存在!"
#: api/lib/cmdb/resp_format.py:47 #: api/lib/cmdb/resp_format.py:47
msgid "{}: CI reference {} does not exist!"
msgstr "{}: CI引用 {} 不存在!"
#: api/lib/cmdb/resp_format.py:48
msgid "{}: CI reference {} is illegal!"
msgstr "{}, CI引用 {} 不合法!"
#: api/lib/cmdb/resp_format.py:49
msgid "Relationship constraint: {}, verification failed" msgid "Relationship constraint: {}, verification failed"
msgstr "关系约束: {}, 校验失败" msgstr "关系约束: {}, 校验失败"
#: api/lib/cmdb/resp_format.py:49 #: api/lib/cmdb/resp_format.py:51
msgid "" msgid ""
"Many-to-many relationship constraint: Model {} <-> {} already has a many-" "Many-to-many relationship constraint: Model {} <-> {} already has a many-"
"to-many relationship!" "to-many relationship!"
msgstr "多对多关系 限制: 模型 {} <-> {} 已经存在多对多关系!" msgstr "多对多关系 限制: 模型 {} <-> {} 已经存在多对多关系!"
#: api/lib/cmdb/resp_format.py:52 #: api/lib/cmdb/resp_format.py:54
msgid "CI relationship: {} does not exist" msgid "CI relationship: {} does not exist"
msgstr "CI关系: {} 不存在" msgstr "CI关系: {} 不存在"
#: api/lib/cmdb/resp_format.py:55 #: api/lib/cmdb/resp_format.py:57
msgid "In search expressions, not supported before parentheses: or, not" msgid "In search expressions, not supported before parentheses: or, not"
msgstr "搜索表达式里小括号前不支持: 或、非" msgstr "搜索表达式里小括号前不支持: 或、非"
#: api/lib/cmdb/resp_format.py:57 #: api/lib/cmdb/resp_format.py:59
msgid "Model {} does not exist" msgid "Model {} does not exist"
msgstr "模型 {} 不存在" msgstr "模型 {} 不存在"
#: api/lib/cmdb/resp_format.py:58 #: api/lib/cmdb/resp_format.py:60
msgid "Model {} already exists" msgid "Model {} already exists"
msgstr "模型 {} 已经存在" msgstr "模型 {} 已经存在"
#: api/lib/cmdb/resp_format.py:59 #: api/lib/cmdb/resp_format.py:61
msgid "The primary key is undefined or has been deleted" msgid "The primary key is undefined or has been deleted"
msgstr "主键未定义或者已被删除" msgstr "主键未定义或者已被删除"
#: api/lib/cmdb/resp_format.py:60 #: api/lib/cmdb/resp_format.py:62
msgid "Only the creator can delete it!" msgid "Only the creator can delete it!"
msgstr "只有创建人才能删除它!" msgstr "只有创建人才能删除它!"
#: api/lib/cmdb/resp_format.py:61 #: api/lib/cmdb/resp_format.py:63
msgid "The model cannot be deleted because the CI already exists" msgid "The model cannot be deleted because the CI already exists"
msgstr "因为CI已经存在不能删除模型" msgstr "因为CI已经存在不能删除模型"
#: api/lib/cmdb/resp_format.py:63 #: api/lib/cmdb/resp_format.py:65
msgid "The inheritance cannot be deleted because the CI already exists" msgid "The inheritance cannot be deleted because the CI already exists"
msgstr "因为CI已经存在不能删除继承关系" msgstr "因为CI已经存在不能删除继承关系"
#: api/lib/cmdb/resp_format.py:65 #: api/lib/cmdb/resp_format.py:67
msgid "The model is inherited and cannot be deleted" msgid "The model is inherited and cannot be deleted"
msgstr "该模型被继承, 不能删除" msgstr "该模型被继承, 不能删除"
#: api/lib/cmdb/resp_format.py:68 #: api/lib/cmdb/resp_format.py:68
msgid "The model is referenced by attribute {} and cannot be deleted"
msgstr "该模型被属性 {} 引用, 不能删除"
#: api/lib/cmdb/resp_format.py:72
msgid "" msgid ""
"The model cannot be deleted because the model is referenced by the " "The model cannot be deleted because the model is referenced by the "
"relational view {}" "relational view {}"
msgstr "因为关系视图 {} 引用了该模型,不能删除模型" msgstr "因为关系视图 {} 引用了该模型,不能删除模型"
#: api/lib/cmdb/resp_format.py:70 #: api/lib/cmdb/resp_format.py:74
msgid "Model group {} does not exist" msgid "Model group {} does not exist"
msgstr "模型分组 {} 不存在" msgstr "模型分组 {} 不存在"
#: api/lib/cmdb/resp_format.py:71 #: api/lib/cmdb/resp_format.py:75
msgid "Model group {} already exists" msgid "Model group {} already exists"
msgstr "模型分组 {} 已经存在" msgstr "模型分组 {} 已经存在"
#: api/lib/cmdb/resp_format.py:72 #: api/lib/cmdb/resp_format.py:76
msgid "Model relationship {} does not exist" msgid "Model relationship {} does not exist"
msgstr "模型关系 {} 不存在" msgstr "模型关系 {} 不存在"
#: api/lib/cmdb/resp_format.py:73 #: api/lib/cmdb/resp_format.py:77
msgid "Attribute group {} already exists" msgid "Attribute group {} already exists"
msgstr "属性分组 {} 已存在" msgstr "属性分组 {} 已存在"
#: api/lib/cmdb/resp_format.py:74 #: api/lib/cmdb/resp_format.py:78
msgid "Attribute group {} does not exist" msgid "Attribute group {} does not exist"
msgstr "属性分组 {} 不存在" msgstr "属性分组 {} 不存在"
#: api/lib/cmdb/resp_format.py:76 #: api/lib/cmdb/resp_format.py:80
msgid "Attribute group <{0}> - attribute <{1}> does not exist" msgid "Attribute group <{0}> - attribute <{1}> does not exist"
msgstr "属性组<{0}> - 属性<{1}> 不存在" msgstr "属性组<{0}> - 属性<{1}> 不存在"
#: api/lib/cmdb/resp_format.py:77 #: api/lib/cmdb/resp_format.py:81
msgid "The unique constraint already exists!" msgid "The unique constraint already exists!"
msgstr "唯一约束已经存在!" msgstr "唯一约束已经存在!"
#: api/lib/cmdb/resp_format.py:79 #: api/lib/cmdb/resp_format.py:83
msgid "Uniquely constrained attributes cannot be JSON and multi-valued" msgid "Uniquely constrained attributes cannot be JSON and multi-valued"
msgstr "唯一约束的属性不能是 JSON 和 多值" msgstr "唯一约束的属性不能是 JSON 和 多值"
#: api/lib/cmdb/resp_format.py:80 #: api/lib/cmdb/resp_format.py:84
msgid "Duplicated trigger" msgid "Duplicated trigger"
msgstr "重复的触发器" msgstr "重复的触发器"
#: api/lib/cmdb/resp_format.py:81 #: api/lib/cmdb/resp_format.py:85
msgid "Trigger {} does not exist" msgid "Trigger {} does not exist"
msgstr "触发器 {} 不存在" msgstr "触发器 {} 不存在"
#: api/lib/cmdb/resp_format.py:82 #: api/lib/cmdb/resp_format.py:86
msgid "Duplicated reconciliation rule" msgid "Duplicated reconciliation rule"
msgstr "" msgstr ""
#: api/lib/cmdb/resp_format.py:83 #: api/lib/cmdb/resp_format.py:87
msgid "Reconciliation rule {} does not exist" msgid "Reconciliation rule {} does not exist"
msgstr "关系类型 {} 不存在" msgstr "关系类型 {} 不存在"
#: api/lib/cmdb/resp_format.py:85 #: api/lib/cmdb/resp_format.py:89
msgid "Operation record {} does not exist" msgid "Operation record {} does not exist"
msgstr "操作记录 {} 不存在" msgstr "操作记录 {} 不存在"
#: api/lib/cmdb/resp_format.py:86 #: api/lib/cmdb/resp_format.py:90
msgid "Unique identifier cannot be deleted" msgid "Unique identifier cannot be deleted"
msgstr "不能删除唯一标识" msgstr "不能删除唯一标识"
#: api/lib/cmdb/resp_format.py:87 #: api/lib/cmdb/resp_format.py:91
msgid "Cannot delete default sorted attributes" msgid "Cannot delete default sorted attributes"
msgstr "不能删除默认排序的属性" msgstr "不能删除默认排序的属性"
#: api/lib/cmdb/resp_format.py:89 #: api/lib/cmdb/resp_format.py:93
msgid "No node selected" msgid "No node selected"
msgstr "没有选择节点" msgstr "没有选择节点"
#: api/lib/cmdb/resp_format.py:90 #: api/lib/cmdb/resp_format.py:94
msgid "This search option does not exist!" msgid "This search option does not exist!"
msgstr "该搜索选项不存在!" msgstr "该搜索选项不存在!"
#: api/lib/cmdb/resp_format.py:91 #: api/lib/cmdb/resp_format.py:95
msgid "This search option has a duplicate name!" msgid "This search option has a duplicate name!"
msgstr "该搜索选项命名重复!" msgstr "该搜索选项命名重复!"
#: api/lib/cmdb/resp_format.py:93 #: api/lib/cmdb/resp_format.py:97
msgid "Relationship type {} already exists" msgid "Relationship type {} already exists"
msgstr "关系类型 {} 已经存在" msgstr "关系类型 {} 已经存在"
#: api/lib/cmdb/resp_format.py:94 #: api/lib/cmdb/resp_format.py:98
msgid "Relationship type {} does not exist" msgid "Relationship type {} does not exist"
msgstr "关系类型 {} 不存在" msgstr "关系类型 {} 不存在"
#: api/lib/cmdb/resp_format.py:96 #: api/lib/cmdb/resp_format.py:100
msgid "Invalid attribute value: {}" msgid "Invalid attribute value: {}"
msgstr "无效的属性值: {}" msgstr "无效的属性值: {}"
#: api/lib/cmdb/resp_format.py:97 #: api/lib/cmdb/resp_format.py:101
msgid "{} Invalid value: {}" msgid "{} Invalid value: {}"
msgstr "{} 无效的值: {}" msgstr "{} 无效的值: {}"
#: api/lib/cmdb/resp_format.py:98 #: api/lib/cmdb/resp_format.py:102
msgid "{} is not in the predefined values" msgid "{} is not in the predefined values"
msgstr "{} 不在预定义值里" msgstr "{} 不在预定义值里"
#: api/lib/cmdb/resp_format.py:100 #: api/lib/cmdb/resp_format.py:104
msgid "The value of attribute {} must be unique, {} already exists" msgid "The value of attribute {} must be unique, {} already exists"
msgstr "属性 {} 的值必须是唯一的, 当前值 {} 已存在" msgstr "属性 {} 的值必须是唯一的, 当前值 {} 已存在"
#: api/lib/cmdb/resp_format.py:101 #: api/lib/cmdb/resp_format.py:105
msgid "Attribute {} value must exist" msgid "Attribute {} value must exist"
msgstr "属性 {} 值必须存在" msgstr "属性 {} 值必须存在"
#: api/lib/cmdb/resp_format.py:102 #: api/lib/cmdb/resp_format.py:106
msgid "Out of range value, the maximum value is 2147483647" msgid "Out of range value, the maximum value is 2147483647"
msgstr "超过最大值限制, 最大值是2147483647" msgstr "超过最大值限制, 最大值是2147483647"
#: api/lib/cmdb/resp_format.py:104 #: api/lib/cmdb/resp_format.py:108
msgid "Unknown error when adding or modifying attribute value: {}" msgid "Unknown error when adding or modifying attribute value: {}"
msgstr "新增或者修改属性值未知错误: {}" msgstr "新增或者修改属性值未知错误: {}"
#: api/lib/cmdb/resp_format.py:106 #: api/lib/cmdb/resp_format.py:110
msgid "Duplicate custom name" msgid "Duplicate custom name"
msgstr "订制名重复" msgstr "订制名重复"
#: api/lib/cmdb/resp_format.py:108 #: api/lib/cmdb/resp_format.py:112
msgid "Number of models exceeds limit: {}" msgid "Number of models exceeds limit: {}"
msgstr "模型数超过限制: {}" msgstr "模型数超过限制: {}"
#: api/lib/cmdb/resp_format.py:109 #: api/lib/cmdb/resp_format.py:113
msgid "The number of CIs exceeds the limit: {}" msgid "The number of CIs exceeds the limit: {}"
msgstr "CI数超过限制: {}" msgstr "CI数超过限制: {}"
#: api/lib/cmdb/resp_format.py:111 #: api/lib/cmdb/resp_format.py:115
msgid "Auto-discovery rule: {} already exists!" msgid "Auto-discovery rule: {} already exists!"
msgstr "自动发现规则: {} 已经存在!" msgstr "自动发现规则: {} 已经存在!"
#: api/lib/cmdb/resp_format.py:112 #: api/lib/cmdb/resp_format.py:116
msgid "Auto-discovery rule: {} does not exist!" msgid "Auto-discovery rule: {} does not exist!"
msgstr "自动发现规则: {} 不存在!" msgstr "自动发现规则: {} 不存在!"
#: api/lib/cmdb/resp_format.py:114 #: api/lib/cmdb/resp_format.py:118
msgid "This auto-discovery rule is referenced by the model and cannot be deleted!" msgid "This auto-discovery rule is referenced by the model and cannot be deleted!"
msgstr "该自动发现规则被模型引用, 不能删除!" msgstr "该自动发现规则被模型引用, 不能删除!"
#: api/lib/cmdb/resp_format.py:116 #: api/lib/cmdb/resp_format.py:120
msgid "The application of auto-discovery rules cannot be defined repeatedly!" msgid "The application of auto-discovery rules cannot be defined repeatedly!"
msgstr "自动发现规则的应用不能重复定义!" msgstr "自动发现规则的应用不能重复定义!"
#: api/lib/cmdb/resp_format.py:117 #: api/lib/cmdb/resp_format.py:121
msgid "The auto-discovery you want to modify: {} does not exist!" msgid "The auto-discovery you want to modify: {} does not exist!"
msgstr "您要修改的自动发现: {} 不存在!" msgstr "您要修改的自动发现: {} 不存在!"
#: api/lib/cmdb/resp_format.py:118 #: api/lib/cmdb/resp_format.py:122
msgid "Attribute does not include unique identifier: {}" msgid "Attribute does not include unique identifier: {}"
msgstr "属性字段没有包括唯一标识: {}" msgstr "属性字段没有包括唯一标识: {}"
#: api/lib/cmdb/resp_format.py:119 #: api/lib/cmdb/resp_format.py:123
msgid "The auto-discovery instance does not exist!" msgid "The auto-discovery instance does not exist!"
msgstr "自动发现的实例不存在!" msgstr "自动发现的实例不存在!"
#: api/lib/cmdb/resp_format.py:120 #: api/lib/cmdb/resp_format.py:124
msgid "The model is not associated with this auto-discovery!" msgid "The model is not associated with this auto-discovery!"
msgstr "模型并未关联该自动发现!" msgstr "模型并未关联该自动发现!"
#: api/lib/cmdb/resp_format.py:121 #: api/lib/cmdb/resp_format.py:125
msgid "Only the creator can modify the Secret!" msgid "Only the creator can modify the Secret!"
msgstr "只有创建人才能修改Secret!" msgstr "只有创建人才能修改Secret!"
#: api/lib/cmdb/resp_format.py:123 #: api/lib/cmdb/resp_format.py:127
msgid "This rule already has auto-discovery instances and cannot be deleted!" msgid "This rule already has auto-discovery instances and cannot be deleted!"
msgstr "该规则已经有自动发现的实例, 不能被删除!" msgstr "该规则已经有自动发现的实例, 不能被删除!"
#: api/lib/cmdb/resp_format.py:125 #: api/lib/cmdb/resp_format.py:129
msgid "The default auto-discovery rule is already referenced by model {}!" msgid "The default auto-discovery rule is already referenced by model {}!"
msgstr "该默认的自动发现规则 已经被模型 {} 引用!" msgstr "该默认的自动发现规则 已经被模型 {} 引用!"
#: api/lib/cmdb/resp_format.py:127 #: api/lib/cmdb/resp_format.py:131
msgid "The unique_key method must return a non-empty string!" msgid "The unique_key method must return a non-empty string!"
msgstr "unique_key方法必须返回非空字符串!" msgstr "unique_key方法必须返回非空字符串!"
#: api/lib/cmdb/resp_format.py:128 #: api/lib/cmdb/resp_format.py:132
msgid "The attributes method must return a list" msgid "The attributes method must return a list"
msgstr "attributes方法必须返回的是list" msgstr "attributes方法必须返回的是list"
#: api/lib/cmdb/resp_format.py:130 #: api/lib/cmdb/resp_format.py:134
msgid "The list returned by the attributes method cannot be empty!" msgid "The list returned by the attributes method cannot be empty!"
msgstr "attributes方法返回的list不能为空!" msgstr "attributes方法返回的list不能为空!"
#: api/lib/cmdb/resp_format.py:132 #: api/lib/cmdb/resp_format.py:136
msgid "Only administrators can define execution targets as: all nodes!" msgid "Only administrators can define execution targets as: all nodes!"
msgstr "只有管理员才可以定义执行机器为: 所有节点!" msgstr "只有管理员才可以定义执行机器为: 所有节点!"
#: api/lib/cmdb/resp_format.py:133 #: api/lib/cmdb/resp_format.py:137
msgid "Execute targets permission check failed: {}" msgid "Execute targets permission check failed: {}"
msgstr "执行机器权限检查不通过: {}" msgstr "执行机器权限检查不通过: {}"
#: api/lib/cmdb/resp_format.py:135 #: api/lib/cmdb/resp_format.py:139
msgid "CI filter authorization must be named!" msgid "CI filter authorization must be named!"
msgstr "CI过滤授权 必须命名!" msgstr "CI过滤授权 必须命名!"
#: api/lib/cmdb/resp_format.py:136 #: api/lib/cmdb/resp_format.py:140
msgid "CI filter authorization is currently not supported or query" msgid "CI filter authorization is currently not supported or query"
msgstr "CI过滤授权 暂时不支持 或 查询" msgstr "CI过滤授权 暂时不支持 或 查询"
#: api/lib/cmdb/resp_format.py:139 #: api/lib/cmdb/resp_format.py:143
msgid "You do not have permission to operate attribute {}!" msgid "You do not have permission to operate attribute {}!"
msgstr "您没有属性 {} 的操作权限!" msgstr "您没有属性 {} 的操作权限!"
#: api/lib/cmdb/resp_format.py:140 #: api/lib/cmdb/resp_format.py:144
msgid "You do not have permission to operate this CI!" msgid "You do not have permission to operate this CI!"
msgstr "您没有该CI的操作权限!" msgstr "您没有该CI的操作权限!"
#: api/lib/cmdb/resp_format.py:142 #: api/lib/cmdb/resp_format.py:146
msgid "Failed to save password: {}" msgid "Failed to save password: {}"
msgstr "保存密码失败: {}" msgstr "保存密码失败: {}"
#: api/lib/cmdb/resp_format.py:143 #: api/lib/cmdb/resp_format.py:147
msgid "Failed to get password: {}" msgid "Failed to get password: {}"
msgstr "获取密码失败: {}" msgstr "获取密码失败: {}"
#: api/lib/cmdb/resp_format.py:145 #: api/lib/cmdb/resp_format.py:149
msgid "Scheduling time format error" msgid "Scheduling time format error"
msgstr "{}格式错误,应该为:%Y-%m-%d %H:%M:%S" msgstr "{}格式错误,应该为:%Y-%m-%d %H:%M:%S"
#: api/lib/cmdb/resp_format.py:146 #: api/lib/cmdb/resp_format.py:150
msgid "CMDB data reconciliation results" msgid "CMDB data reconciliation results"
msgstr "" msgstr ""
#: api/lib/cmdb/resp_format.py:147 #: api/lib/cmdb/resp_format.py:151
msgid "Number of {} illegal: {}" msgid "Number of {} illegal: {}"
msgstr "" msgstr ""
#: api/lib/cmdb/resp_format.py:149 #: api/lib/cmdb/resp_format.py:153
msgid "Topology view {} already exists" msgid "Topology view {} already exists"
msgstr "拓扑视图 {} 已经存在" msgstr "拓扑视图 {} 已经存在"
#: api/lib/cmdb/resp_format.py:150 #: api/lib/cmdb/resp_format.py:154
msgid "Topology group {} already exists" msgid "Topology group {} already exists"
msgstr "拓扑视图分组 {} 已经存在" msgstr "拓扑视图分组 {} 已经存在"
#: api/lib/cmdb/resp_format.py:152 #: api/lib/cmdb/resp_format.py:156
msgid "The group cannot be deleted because the topology view already exists" msgid "The group cannot be deleted because the topology view already exists"
msgstr "因为该分组下定义了拓扑视图,不能删除" msgstr "因为该分组下定义了拓扑视图,不能删除"

View File

@ -15,7 +15,7 @@ from api.lib.cmdb.const import ResourceTypeEnum, PermEnum
from api.lib.cmdb.const import RetKey from api.lib.cmdb.const import RetKey
from api.lib.cmdb.perms import has_perm_for_ci from api.lib.cmdb.perms import has_perm_for_ci
from api.lib.cmdb.search import SearchError from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search from api.lib.cmdb.search.ci import search as ci_search
from api.lib.decorator import args_required from api.lib.decorator import args_required
from api.lib.perm.acl.acl import has_perm_from_args from api.lib.perm.acl.acl import has_perm_from_args
from api.lib.utils import get_page from api.lib.utils import get_page
@ -160,7 +160,7 @@ class CISearchView(APIView):
use_id_filter = request.values.get("use_id_filter", False) in current_app.config.get('BOOL_TRUE') use_id_filter = request.values.get("use_id_filter", False) in current_app.config.get('BOOL_TRUE')
start = time.time() start = time.time()
s = search(query, fl, facet, page, ret_key, count, sort, excludes, use_id_filter=use_id_filter) s = ci_search(query, fl, facet, page, ret_key, count, sort, excludes, use_id_filter=use_id_filter)
try: try:
response, counter, total, page, numfound, facet = s.search() response, counter, total, page, numfound, facet = s.search()
except SearchError as e: except SearchError as e:

View File

@ -48,16 +48,21 @@ class CITypeView(APIView):
if request.url.endswith("icons"): if request.url.endswith("icons"):
return self.jsonify(CITypeManager().get_icons()) return self.jsonify(CITypeManager().get_icons())
q = request.args.get("type_name") q = request.values.get("type_name")
type_ids = handle_arg_list(request.values.get("type_ids"))
type_ids = type_ids or (type_id and [type_id])
if type_ids:
ci_types = []
for _type_id in type_ids:
ci_type = CITypeCache.get(_type_id)
if ci_type is None:
return abort(404, ErrFormat.ci_type_not_found)
if type_id is not None: ci_type = ci_type.to_dict()
ci_type = CITypeCache.get(type_id) ci_type['parent_ids'] = CITypeInheritanceManager.get_parents(_type_id)
if ci_type is None: ci_type['show_name'] = ci_type.get('show_id') and AttributeCache.get(ci_type['show_id']).name
return abort(404, ErrFormat.ci_type_not_found) ci_type['unique_name'] = ci_type['unique_id'] and AttributeCache.get(ci_type['unique_id']).name
ci_types.append(ci_type)
ci_type = ci_type.to_dict()
ci_type['parent_ids'] = CITypeInheritanceManager.get_parents(type_id)
ci_types = [ci_type]
elif type_name is not None: elif type_name is not None:
ci_type = 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_type['parent_ids'] = CITypeInheritanceManager.get_parents(ci_type['id'])

View File

@ -54,6 +54,12 @@
<div class="content unicode" style="display: block;"> <div class="content unicode" style="display: block;">
<ul class="icon_lists dib-box"> <ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont">&#xe997;</span>
<div class="name">duose-changwenben (1)</div>
<div class="code-name">&amp;#xe997;</div>
</li>
<li class="dib"> <li class="dib">
<span class="icon iconfont">&#xe995;</span> <span class="icon iconfont">&#xe995;</span>
<div class="name">duose-quote</div> <div class="name">duose-quote</div>
@ -5508,9 +5514,9 @@
<pre><code class="language-css" <pre><code class="language-css"
>@font-face { >@font-face {
font-family: 'iconfont'; font-family: 'iconfont';
src: url('iconfont.woff2?t=1723012344599') format('woff2'), src: url('iconfont.woff2?t=1724135954264') format('woff2'),
url('iconfont.woff?t=1723012344599') format('woff'), url('iconfont.woff?t=1724135954264') format('woff'),
url('iconfont.ttf?t=1723012344599') format('truetype'); url('iconfont.ttf?t=1724135954264') format('truetype');
} }
</code></pre> </code></pre>
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3> <h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@ -5536,6 +5542,15 @@
<div class="content font-class"> <div class="content font-class">
<ul class="icon_lists dib-box"> <ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont duose-changwenben1"></span>
<div class="name">
duose-changwenben (1)
</div>
<div class="code-name">.duose-changwenben1
</div>
</li>
<li class="dib"> <li class="dib">
<span class="icon iconfont duose-quote"></span> <span class="icon iconfont duose-quote"></span>
<div class="name"> <div class="name">
@ -13717,6 +13732,14 @@
<div class="content symbol"> <div class="content symbol">
<ul class="icon_lists dib-box"> <ul class="icon_lists dib-box">
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#duose-changwenben1"></use>
</svg>
<div class="name">duose-changwenben (1)</div>
<div class="code-name">#duose-changwenben1</div>
</li>
<li class="dib"> <li class="dib">
<svg class="icon svg-icon" aria-hidden="true"> <svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#duose-quote"></use> <use xlink:href="#duose-quote"></use>

View File

@ -1,8 +1,8 @@
@font-face { @font-face {
font-family: "iconfont"; /* Project id 3857903 */ font-family: "iconfont"; /* Project id 3857903 */
src: url('iconfont.woff2?t=1723012344599') format('woff2'), src: url('iconfont.woff2?t=1724135954264') format('woff2'),
url('iconfont.woff?t=1723012344599') format('woff'), url('iconfont.woff?t=1724135954264') format('woff'),
url('iconfont.ttf?t=1723012344599') format('truetype'); url('iconfont.ttf?t=1724135954264') format('truetype');
} }
.iconfont { .iconfont {
@ -13,6 +13,10 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.duose-changwenben1:before {
content: "\e997";
}
.duose-quote:before { .duose-quote:before {
content: "\e995"; content: "\e995";
} }

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,13 @@
"css_prefix_text": "", "css_prefix_text": "",
"description": "", "description": "",
"glyphs": [ "glyphs": [
{
"icon_id": "41437322",
"name": "duose-changwenben (1)",
"font_class": "duose-changwenben1",
"unicode": "e997",
"unicode_decimal": 59799
},
{ {
"icon_id": "41363381", "icon_id": "41363381",
"name": "duose-quote", "name": "duose-quote",

Binary file not shown.

18
cmdb-ui/src/api/cmdb.js Normal file
View File

@ -0,0 +1,18 @@
import { axios } from '@/utils/request'
export function searchCI(params, isShowMessage = true) {
return axios({
url: `/v0.1/ci/s`,
method: 'GET',
params: params,
isShowMessage
})
}
export function getCIType(CITypeName, parameter) {
return axios({
url: `/v0.1/ci_types/${CITypeName}`,
method: 'GET',
params: parameter
})
}

View File

@ -85,6 +85,29 @@
:disabled="disabled" :disabled="disabled"
> >
</treeselect> </treeselect>
<CIReferenceAttr
v-if="getAttr(item.property).is_reference && (item.exp === 'is' || item.exp === '~is')"
:style="{ width: '175px' }"
class="select-filter-component"
:referenceTypeId="getAttr(item.property).reference_type_id"
:disabled="disabled"
v-model="item.value"
/>
<a-select
v-else-if="getAttr(item.property).is_bool && (item.exp === 'is' || item.exp === '~is')"
v-model="item.value"
class="select-filter-component"
:style="{ width: '175px' }"
:disabled="disabled"
:placeholder="$t('placeholder2')"
>
<a-select-option key="1">
true
</a-select-option>
<a-select-option key="0">
false
</a-select-option>
</a-select>
<treeselect <treeselect
class="custom-treeselect" class="custom-treeselect"
:style="{ width: '175px', '--custom-height': '24px' }" :style="{ width: '175px', '--custom-height': '24px' }"
@ -92,7 +115,7 @@
:multiple="false" :multiple="false"
:clearable="false" :clearable="false"
searchable searchable
v-if="isChoiceByProperty(item.property) && (item.exp === 'is' || item.exp === '~is')" v-else-if="isChoiceByProperty(item.property) && (item.exp === 'is' || item.exp === '~is')"
:options="getChoiceValueByProperty(item.property)" :options="getChoiceValueByProperty(item.property)"
:placeholder="$t('placeholder2')" :placeholder="$t('placeholder2')"
:normalizer=" :normalizer="
@ -199,10 +222,11 @@ import _ from 'lodash'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { ruleTypeList, expList, advancedExpList, compareTypeList } from './constants' import { ruleTypeList, expList, advancedExpList, compareTypeList } from './constants'
import ValueTypeMapIcon from '../CMDBValueTypeMapIcon' import ValueTypeMapIcon from '../CMDBValueTypeMapIcon'
import CIReferenceAttr from '../ciReferenceAttr/index.vue'
export default { export default {
name: 'Expression', name: 'Expression',
components: { ValueTypeMapIcon }, components: { ValueTypeMapIcon, CIReferenceAttr },
model: { model: {
prop: 'value', prop: 'value',
event: 'change', event: 'change',
@ -255,7 +279,7 @@ export default {
getExpListByProperty(property) { getExpListByProperty(property) {
if (property) { if (property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property) const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find && ['0', '1', '3', '4', '5'].includes(_find.value_type)) { if (_find && (['0', '1', '3', '4', '5'].includes(_find.value_type) || _find.is_reference || _find.is_bool)) {
return [ return [
{ value: 'is', label: this.$t('cmdbFilterComp.is') }, { value: 'is', label: this.$t('cmdbFilterComp.is') },
{ value: '~is', label: this.$t('cmdbFilterComp.~is') }, { value: '~is', label: this.$t('cmdbFilterComp.~is') },
@ -315,6 +339,9 @@ export default {
} }
return [] return []
}, },
getAttr(property) {
return this.canSearchPreferenceAttrList.find((item) => item.name === property) || {}
},
handleChangeExp({ value }, item, index) { handleChangeExp({ value }, item, index) {
const _ruleList = _.cloneDeep(this.ruleList) const _ruleList = _.cloneDeep(this.ruleList)
if (value === 'range') { if (value === 'range') {
@ -343,4 +370,20 @@ export default {
} }
</script> </script>
<style></style> <style lang="less" scoped>
.select-filter-component {
height: 24px;
/deep/ .ant-select-selection {
height: 24px;
background: #f7f8fa;
line-height: 24px;
border: none;
.ant-select-selection__rendered {
height: 24px;
line-height: 24px;
}
}
}
</style>

View File

@ -15,18 +15,38 @@ export default {
}, },
methods: { methods: {
getPropertyIcon(attr) { getPropertyIcon(attr) {
switch (attr.value_type) { let valueType = attr.value_type
if (valueType === '2') {
if (attr.is_password) {
valueType = '7'
} else if (attr.is_link) {
valueType = '8'
} else if (!attr.is_index) {
valueType = '9'
}
}
if (
valueType === '7' &&
attr.is_bool
) {
valueType = '10'
}
if (
valueType === '0' &&
attr.is_reference
) {
valueType = '11'
}
switch (valueType) {
case '0': case '0':
return 'duose-shishu' return 'duose-shishu'
case '1': case '1':
return 'duose-fudianshu' return 'duose-fudianshu'
case '2': case '2':
if (attr.is_password) {
return 'duose-password'
}
if (attr.is_link) {
return 'duose-link'
}
return 'duose-wenben' return 'duose-wenben'
case '3': case '3':
return 'duose-datetime' return 'duose-datetime'
@ -40,6 +60,14 @@ export default {
return 'duose-password' return 'duose-password'
case '8': case '8':
return 'duose-link' return 'duose-link'
case '9':
return 'duose-changwenben1'
case '10':
return 'duose-boole'
case '11':
return 'duose-quote'
default:
return ''
} }
}, },
}, },

View File

@ -61,7 +61,13 @@
</template> </template>
</div> </div>
</div> </div>
<a-input ref="regInput" :placeholder="$t('regexSelect.placeholder')" :value="current.label" @change="changeLabel"> <a-input
ref="regInput"
:placeholder="$t('regexSelect.placeholder')"
:value="current.label"
:disabled="disabled"
@change="changeLabel"
>
</a-input> </a-input>
</a-popover> </a-popover>
</template> </template>
@ -88,6 +94,10 @@ export default {
type: Array, type: Array,
default: () => [], default: () => [],
}, },
disabled: {
type: Boolean,
default: false,
}
}, },
data() { data() {
return { return {

View File

@ -0,0 +1,178 @@
<template>
<div class="reference-attr-select-wrap">
<a-select
v-bind="$attrs"
v-model="selectCIIds"
optionFilterProp="title"
:mode="isList ? 'multiple' : 'default'"
showSearch
allowClear
:getPopupContainer="(trigger) => trigger.parentElement"
class="reference-attr-select"
:maxTagCount="2"
@dropdownVisibleChange="handleDropdownVisibleChange"
@search="handleSearch"
@change="handleChange"
>
<template v-if="!isInit">
<a-select-option
v-for="(item) in initSelectOption"
:key="item.key"
:title="item.title"
>
{{ item.title }}
</a-select-option>
</template>
<a-select-option
v-for="(item) in options"
:key="item.key"
:title="item.title"
>
{{ item.title }}
</a-select-option>
</a-select>
</div>
</template>
<script>
import _ from 'lodash'
import debounce from 'lodash/debounce'
import { searchCI, getCIType } from '@/api/cmdb'
export default {
name: 'CIReferenceAttr',
props: {
value: {
type: [Number, String, Array],
default: () => '',
},
isList: {
type: Boolean,
default: false,
},
referenceShowAttrName: {
type: String,
default: ''
},
referenceTypeId: {
type: [String, Number],
default: ''
},
initSelectOption: {
type: Array,
default: () => []
}
},
model: {
prop: 'value',
event: 'change',
},
data() {
return {
isInit: false,
options: [],
innerReferenceShowAttrName: ''
}
},
watch: {
referenceTypeId: {
immediate: true,
deep: true,
handler() {
this.isInit = false
}
}
},
computed: {
selectCIIds: {
get() {
if (this.isList) {
return this.value || []
} else {
return this.value ? Number(this.value) : ''
}
},
set(val) {
this.$emit('change', val ?? (this.isList ? [] : null))
return val
},
},
},
methods: {
async handleDropdownVisibleChange(open) {
if (!this.isInit && open && this.referenceTypeId) {
this.isInit = true
if (!this.referenceShowAttrName) {
const res = await getCIType(this.referenceTypeId)
const ciType = res?.ci_types?.[0]
this.innerReferenceShowAttrName = ciType?.show_name || ciType?.unique_name || ''
}
const attrName = this.referenceShowAttrName || this.innerReferenceShowAttrName || ''
if (!attrName) {
return
}
const res = await searchCI({
q: `_type:${this.referenceTypeId}`,
fl: attrName,
count: 25,
})
let options = res?.result?.map((item) => {
return {
key: item._id,
title: String(item?.[attrName] ?? '')
}
})
options = _.uniqBy([...this.initSelectOption, ...options], 'key')
this.options = options
}
},
handleSearch: debounce(async function(v) {
const attrName = this.referenceShowAttrName || this.innerReferenceShowAttrName || ''
if (!attrName || !this.referenceTypeId) {
return
}
const res = await searchCI({
q: `_type:${this.referenceTypeId}${v ? ',*' + v + '*' : ''}`,
fl: attrName,
count: v ? 100 : 25,
})
this.options = res?.result?.map((item) => {
return {
key: item._id,
title: String(item?.[attrName] ?? '')
}
})
}, 300),
handleChange(v) {
if (Array.isArray(v) ? !v.length : !v) {
this.handleSearch()
}
}
}
}
</script>
<style lang="less" scoped>
.reference-attr-select-wrap {
width: 100%;
.reference-attr-select {
width: 100%;
/deep/ .ant-select-dropdown {
z-index: 15;
}
}
}
</style>

View File

@ -8,6 +8,7 @@
resizable resizable
ref="xTable" ref="xTable"
size="small" size="small"
:data="data"
:loading="loading" :loading="loading"
:row-config="{ useKey: true, keyField: '_id' }" :row-config="{ useKey: true, keyField: '_id' }"
show-header-overflow show-header-overflow
@ -56,8 +57,20 @@
<span>{{ col.title }}</span> <span>{{ col.title }}</span>
</span> </span>
</template> </template>
<template v-if="col.is_choice || col.is_password" #edit="{ row }"> <template v-if="col.is_choice || col.is_password || col.is_bool || col.is_reference" #edit="{ row }">
<vxe-input v-if="col.is_password" v-model="passwordValue[col.field]" /> <CIReferenceAttr
v-if="col.is_reference"
:referenceTypeId="col.reference_type_id"
:isList="col.is_list"
:referenceShowAttrName="referenceShowAttrNameMap[col.reference_type_id] || ''"
:initSelectOption="getInitReferenceSelectOption(row[col.field], col)"
v-model="row[col.field]"
/>
<a-switch
v-else-if="col.is_bool"
v-model="row[col.field]"
/>
<vxe-input v-else-if="col.is_password" v-model="passwordValue[col.field]" />
<a-select <a-select
v-if="col.is_choice" v-if="col.is_choice"
v-model="row[col.field]" v-model="row[col.field]"
@ -100,10 +113,20 @@
</a-select> </a-select>
</template> </template>
<template <template
v-if="col.value_type === '6' || col.is_link || col.is_password || col.is_choice" v-if="col.value_type === '6' || col.is_link || col.is_password || col.is_choice || col.is_reference"
#default="{ row }" #default="{ row }"
> >
<span v-if="col.value_type === '6' && row[col.field]">{{ row[col.field] }}</span> <template v-if="col.is_reference" >
<a
v-for="(ciId) in (col.is_list ? row[col.field] : [row[col.field]])"
:key="ciId"
:href="`/cmdb/cidetail/${col.reference_type_id}/${ciId}`"
target="_blank"
>
{{ getReferenceAttrValue(ciId, col) }}
</a>
</template>
<span v-else-if="col.value_type === '6' && row[col.field]">{{ row[col.field] }}</span>
<template v-else-if="col.is_link && row[col.field]"> <template v-else-if="col.is_link && row[col.field]">
<a <a
v-for="(item, linkIndex) in (col.is_list ? row[col.field] : [row[col.field]])" v-for="(item, linkIndex) in (col.is_list ? row[col.field] : [row[col.field]])"
@ -187,16 +210,21 @@
</template> </template>
<script> <script>
import _ from 'lodash'
import { getCITypes } from '@/modules/cmdb/api/CIType'
import { searchCI } from '@/modules/cmdb/api/ci'
import JsonEditor from '../JsonEditor/jsonEditor.vue' import JsonEditor from '../JsonEditor/jsonEditor.vue'
import PasswordField from '../passwordField/index.vue' import PasswordField from '../passwordField/index.vue'
import { ops_move_icon as OpsMoveIcon } from '@/core/icons' import { ops_move_icon as OpsMoveIcon } from '@/core/icons'
import CIReferenceAttr from '@/components/ciReferenceAttr/index.vue'
export default { export default {
name: 'CITable', name: 'CITable',
components: { components: {
JsonEditor, JsonEditor,
PasswordField, PasswordField,
OpsMoveIcon OpsMoveIcon,
CIReferenceAttr
}, },
props: { props: {
// table ID // table ID
@ -237,6 +265,18 @@ export default {
showDelete: { showDelete: {
type: Boolean, type: Boolean,
default: true default: true
},
// 表格数据
data: {
type: Array,
default: () => []
}
},
data() {
return {
referenceShowAttrNameMap: {},
referenceCIIdMap: {},
} }
}, },
@ -245,6 +285,46 @@ export default {
const idx = this.columns.findIndex((item) => item.is_fixed) const idx = this.columns.findIndex((item) => item.is_fixed)
return idx > -1 return idx > -1
}, },
tableDataWatch() {
return {
data: this.data,
columns: this.columns
}
},
referenceCIIdWatch() {
const referenceTypeCol = this.columns?.filter((col) => col?.is_reference && col?.reference_type_id) || []
if (!this.data?.length || !referenceTypeCol?.length) {
return []
}
const ids = []
this.data.forEach((row) => {
referenceTypeCol.forEach((col) => {
if (row[col.field]) {
ids.push(...(Array.isArray(row[col.field]) ? row[col.field] : [row[col.field]]))
}
})
})
return _.uniq(ids)
}
},
watch: {
columns: {
immediate: true,
deep: true,
handler(newVal) {
this.handleReferenceShowAttrName(newVal)
}
},
referenceCIIdWatch: {
immediate: true,
deep: true,
handler() {
this.handleReferenceCIIdMap()
}
}
}, },
methods: { methods: {
@ -330,6 +410,101 @@ export default {
getRowSeq(row) { getRowSeq(row) {
return this.getVxetableRef().getRowSeq(row) return this.getVxetableRef().getRowSeq(row)
},
async handleReferenceShowAttrName(columns) {
const needRequiredCITypeIds = columns?.filter((col) => col?.is_reference && col?.reference_type_id).map((col) => col.reference_type_id) || []
if (!needRequiredCITypeIds.length) {
this.referenceShowAttrNameMap = {}
return
}
const res = await getCITypes({
type_ids: needRequiredCITypeIds.join(',')
})
const map = {}
res.ci_types.forEach((ciType) => {
map[ciType.id] = ciType?.show_name || ciType?.unique_name || ''
})
this.referenceShowAttrNameMap = map
},
async handleReferenceCIIdMap() {
const referenceTypeCol = this.columns.filter((col) => col?.is_reference && col?.reference_type_id) || []
if (!this.data?.length || !referenceTypeCol?.length) {
this.referenceCIIdMap = {}
return
}
const map = {}
this.data.forEach((row) => {
referenceTypeCol.forEach((col) => {
const ids = Array.isArray(row[col.field]) ? row[col.field] : row[col.field] ? [row[col.field]] : []
if (ids.length) {
if (!map?.[col.reference_type_id]) {
map[col.reference_type_id] = {}
}
ids.forEach((id) => {
map[col.reference_type_id][id] = {}
})
}
})
})
if (!Object.keys(map).length) {
this.referenceCIIdMap = {}
return
}
const allRes = await Promise.all(
Object.keys(map).map((key) => {
return searchCI({
q: `_type:${key},_id:(${Object.keys(map[key]).join(';')})`,
count: 9999
})
})
)
allRes.forEach((res) => {
res.result.forEach((item) => {
if (map?.[item._type]?.[item._id]) {
map[item._type][item._id] = item
}
})
})
this.referenceCIIdMap = map
},
getReferenceAttrValue(id, col) {
const ci = this?.referenceCIIdMap?.[col?.reference_type_id]?.[id]
if (!ci) {
return id
}
const attrName = this.referenceShowAttrNameMap?.[col.reference_type_id]
return ci?.[attrName] || id
},
getInitReferenceSelectOption(value, col) {
const ids = Array.isArray(value) ? value : value ? [value] : []
if (!ids.length) {
return []
}
const map = this?.referenceCIIdMap?.[col?.reference_type_id]
const attrName = this.referenceShowAttrNameMap?.[col?.reference_type_id]
const option = (Array.isArray(value) ? value : [value]).map((id) => {
return {
key: id,
title: map?.[id]?.[attrName] || id
}
})
return option
} }
} }
} }

View File

@ -113,6 +113,11 @@ export default {
this.editor.insertNode(node) this.editor.insertNode(node)
} }
}, },
destroy() {
const editor = this.editor
if (editor == null) return
editor.destroy()
}
}, },
} }
</script> </script>

View File

@ -190,6 +190,14 @@ const cmdb_en = {
confirmDeleteTrigger: 'Are you sure to delete this trigger?', confirmDeleteTrigger: 'Are you sure to delete this trigger?',
int: 'Integer', int: 'Integer',
float: 'Float', float: 'Float',
longText: 'Long Text',
shortText: 'Short Text',
shortTextTip: 'Text length <= 128',
referenceModel: 'Reference Model',
referenceModelTip: 'Please select reference model',
referenceModelTip1: 'For quick view of referenced model instances',
bool: 'Bool',
reference: 'Reference',
text: 'Text', text: 'Text',
datetime: 'DateTime', datetime: 'DateTime',
date: 'Date', date: 'Date',
@ -207,7 +215,7 @@ const cmdb_en = {
otherGroupTips: 'Non sortable within the other group', otherGroupTips: 'Non sortable within the other group',
filterTips: 'click to show {name}', filterTips: 'click to show {name}',
attributeAssociation: 'Attribute Association', attributeAssociation: 'Attribute Association',
attributeAssociationTip1: 'Automatically establish relationships through the attributes except password, json and multiple of two models', attributeAssociationTip1: 'Automatically establish relationships through attribute values (except password, json, multi-value, long text, boolean, reference) of two models',
attributeAssociationTip2: 'Double click to edit', attributeAssociationTip2: 'Double click to edit',
attributeAssociationTip3: 'Two Attributes must be selected', attributeAssociationTip3: 'Two Attributes must be selected',
attributeAssociationTip4: 'Please select a attribute from Source CIType', attributeAssociationTip4: 'Please select a attribute from Source CIType',
@ -283,6 +291,9 @@ const cmdb_en = {
rule: 'Rule', rule: 'Rule',
cascadeAttr: 'Cascade', cascadeAttr: 'Cascade',
cascadeAttrTip: 'Cascading attributes note the order', cascadeAttrTip: 'Cascading attributes note the order',
enumValue: 'Value',
label: 'Label',
valueInputTip: 'Please input value'
}, },
components: { components: {
unselectAttributes: 'Unselected', unselectAttributes: 'Unselected',
@ -324,7 +335,7 @@ const cmdb_en = {
pleaseSearch: 'Please search', pleaseSearch: 'Please search',
conditionFilter: 'Conditional filtering', conditionFilter: 'Conditional filtering',
attributeDesc: 'Attribute Description', 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', ciSearchTips: '1. JSON/password/link/longText/reference 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*', ciSearchTips2: 'For example: q=hostname:*0.0.0.0*',
subCIType: 'Subscription CIType', subCIType: 'Subscription CIType',
already: 'already', already: 'already',
@ -549,7 +560,7 @@ class AutoDiscovery(object):
""" """
Define attribute fields Define attribute fields
:return: Returns a list of attribute fields. The list items are (name, type, description). The name must be in English. :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 type: String Integer Float Date DateTime Time JSON Bool Reference
For example: For example:
return [ return [
("ci_type", "String", "CIType name"), ("ci_type", "String", "CIType name"),

View File

@ -190,6 +190,14 @@ const cmdb_zh = {
confirmDeleteTrigger: '确认删除该触发器吗?', confirmDeleteTrigger: '确认删除该触发器吗?',
int: '整数', int: '整数',
float: '浮点数', float: '浮点数',
longText: '长文本',
shortText: '短文本',
shortTextTip: '文本长度 <= 128',
referenceModel: '引用模型',
referenceModelTip: '请选择引用模型',
referenceModelTip1: '用于快捷查看引用模型实例',
bool: '布尔',
reference: '引用',
text: '文本', text: '文本',
datetime: '日期时间', datetime: '日期时间',
date: '日期', date: '日期',
@ -207,7 +215,7 @@ const cmdb_zh = {
otherGroupTips: '其他分组属性不可排序', otherGroupTips: '其他分组属性不可排序',
filterTips: '点击可仅查看{name}属性', filterTips: '点击可仅查看{name}属性',
attributeAssociation: '属性关联', attributeAssociation: '属性关联',
attributeAssociationTip1: '通过2个模型的属性值(除密码、json、多值)来自动建立关系', attributeAssociationTip1: '通过2个模型的属性值(除密码、json、多值、长文本、布尔、引用)来自动建立关系',
attributeAssociationTip2: '双击可编辑', attributeAssociationTip2: '双击可编辑',
attributeAssociationTip3: '属性关联必须选择两个属性', attributeAssociationTip3: '属性关联必须选择两个属性',
attributeAssociationTip4: '请选择原模型属性', attributeAssociationTip4: '请选择原模型属性',
@ -283,6 +291,9 @@ const cmdb_zh = {
rule: '规则', rule: '规则',
cascadeAttr: '级联', cascadeAttr: '级联',
cascadeAttrTip: '级联属性注意顺序', cascadeAttrTip: '级联属性注意顺序',
enumValue: '枚举值',
label: '标签',
valueInputTip: '请输入枚举值'
}, },
components: { components: {
unselectAttributes: '未选属性', unselectAttributes: '未选属性',
@ -324,7 +335,7 @@ const cmdb_zh = {
pleaseSearch: '请查找', pleaseSearch: '请查找',
conditionFilter: '条件过滤', conditionFilter: '条件过滤',
attributeDesc: '属性说明', attributeDesc: '属性说明',
ciSearchTips: '1. json、密码、链接属性不能搜索\n2. 搜索内容包括逗号, 则需转义\n3. 只搜索索引属性, 非索引属性使用条件过滤', ciSearchTips: '1. json、密码、链接、长文本、引用属性不能搜索\n2. 搜索内容包括逗号, 则需转义\n3. 只搜索索引属性, 非索引属性使用条件过滤',
ciSearchTips2: '例: q=hostname:*0.0.0.0*', ciSearchTips2: '例: q=hostname:*0.0.0.0*',
subCIType: '订阅模型', subCIType: '订阅模型',
already: '已', already: '已',
@ -548,7 +559,7 @@ class AutoDiscovery(object):
""" """
Define attribute fields Define attribute fields
:return: Returns a list of attribute fields. The list items are (name, type, description). The name must be in English. :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 type: String Integer Float Date DateTime Time JSON Bool Reference
For example: For example:
return [ return [
("ci_type", "String", "CIType name"), ("ci_type", "String", "CIType name"),

View File

@ -4,13 +4,16 @@ export const valueTypeMap = () => {
return { return {
'0': i18n.t('cmdb.ciType.int'), '0': i18n.t('cmdb.ciType.int'),
'1': i18n.t('cmdb.ciType.float'), '1': i18n.t('cmdb.ciType.float'),
'2': i18n.t('cmdb.ciType.text'), '2': i18n.t('cmdb.ciType.shortText'),
'3': i18n.t('cmdb.ciType.datetime'), '3': i18n.t('cmdb.ciType.datetime'),
'4': i18n.t('cmdb.ciType.date'), '4': i18n.t('cmdb.ciType.date'),
'5': i18n.t('cmdb.ciType.time'), '5': i18n.t('cmdb.ciType.time'),
'6': 'JSON', '6': 'JSON',
'7': i18n.t('cmdb.ciType.password'), '7': i18n.t('cmdb.ciType.password'),
'8': i18n.t('cmdb.ciType.link') '8': i18n.t('cmdb.ciType.link'),
'9': i18n.t('cmdb.ciType.longText'),
'10': i18n.t('cmdb.ciType.bool'),
'11': i18n.t('cmdb.ciType.reference'),
} }
} }

View File

@ -97,6 +97,9 @@ export function getCITableColumns(data, attrList, width = 1600, height) {
is_list: attr.is_list, is_list: attr.is_list,
is_choice: attr.is_choice, is_choice: attr.is_choice,
is_fixed: attr.is_fixed, is_fixed: attr.is_fixed,
is_bool: attr.is_bool,
is_reference: attr.is_reference,
reference_type_id: attr.reference_type_id
}) })
} }
@ -137,6 +140,10 @@ export const getPropertyStyle = (attr) => {
export const getPropertyIcon = (attr) => { export const getPropertyIcon = (attr) => {
switch (attr.value_type) { switch (attr.value_type) {
case '0': case '0':
if (attr.is_reference) {
return 'duose-quote'
}
return 'duose-shishu' return 'duose-shishu'
case '1': case '1':
return 'duose-fudianshu' return 'duose-fudianshu'
@ -147,6 +154,9 @@ export const getPropertyIcon = (attr) => {
if (attr.is_link) { if (attr.is_link) {
return 'duose-link' return 'duose-link'
} }
if (attr.is_index === false) {
return 'duose-changwenben1'
}
return 'duose-wenben' return 'duose-wenben'
case '3': case '3':
return 'duose-datetime' return 'duose-datetime'
@ -157,12 +167,52 @@ export const getPropertyIcon = (attr) => {
case '6': case '6':
return 'duose-json' return 'duose-json'
case '7': case '7':
if (attr.is_bool) {
return 'duose-boole'
}
return 'duose-password' return 'duose-password'
case '8': case '8':
return 'duose-link' return 'duose-link'
case '9':
return 'duose-changwenben1'
case '10':
return 'duose-boole'
case '11':
return 'duose-quote'
default:
return ''
} }
} }
export const getPropertyType = (attr) => {
if (attr.is_password) {
return '7'
}
if (attr.is_link) {
return '8'
}
switch (attr.value_type) {
case '0':
if (attr.is_reference) {
return '11'
}
return '0'
case '2':
if (!attr.is_index) {
return '9'
}
return '2'
case '7':
if (attr.is_bool) {
return '10'
}
return '7'
default:
return attr?.value_type ?? ''
}
}
export const getLastLayout = (data, x1 = 0, y1 = 0, w1 = 0) => { export const getLastLayout = (data, x1 = 0, y1 = 0, w1 = 0) => {
const _tempData = _.orderBy(data, ['y', 'x'], ['asc', 'asc']) const _tempData = _.orderBy(data, ['y', 'x'], ['asc', 'asc'])
if (!_tempData.length) { if (!_tempData.length) {

View File

@ -465,13 +465,12 @@ export default {
this.loadTip = this.$t('cmdb.ci.batchUpdateInProgress') + '...' this.loadTip = this.$t('cmdb.ci.batchUpdateInProgress') + '...'
const payload = {} const payload = {}
Object.keys(values).forEach((key) => { Object.keys(values).forEach((key) => {
if (values[key] || values[key] === 0) {
payload[key] = values[key]
}
// Field values support blanking // Field values support blanking
// There are currently field values that do not support blanking and will be returned by the backend. // There are currently field values that do not support blanking and will be returned by the backend.
if (values[key] === undefined || values[key] === null) { if (values[key] === undefined || values[key] === null) {
payload[key] = null payload[key] = null
} else {
payload[key] = values[key]
} }
}) })
this.$refs.create.visible = false this.$refs.create.visible = false

View File

@ -35,7 +35,7 @@
<a-select v-model="parentsForm[item.name].attr"> <a-select v-model="parentsForm[item.name].attr">
<a-select-option <a-select-option
:title="attr.alias || attr.name" :title="attr.alias || attr.name"
v-for="attr in item.attributes" v-for="attr in filterAttributes(item.attributes)"
:key="attr.name" :key="attr.name"
:value="attr.name" :value="attr.name"
> >
@ -87,11 +87,32 @@
</a-col> </a-col>
<a-col :span="showListOperation(list.name) ? 10 : 13"> <a-col :span="showListOperation(list.name) ? 10 : 13">
<a-form-item> <a-form-item>
<CIReferenceAttr
v-if="getAttr(list.name).is_reference"
:referenceTypeId="getAttr(list.name).reference_type_id"
:isList="getAttr(list.name).is_list"
v-decorator="[
list.name,
{
initialValue: getAttr(list.name).is_list ? [] : ''
}
]"
/>
<a-switch
v-else-if="getAttr(list.name).is_bool"
v-decorator="[
list.name,
{
valuePropName: 'checked',
initialValue: false
}
]"
/>
<a-select <a-select
:style="{ width: '100%' }" :style="{ width: '100%' }"
v-decorator="[list.name, { rules: getDecoratorRules(list) }]" v-decorator="[list.name, { rules: getDecoratorRules(list) }]"
:placeholder="$t('placeholder2')" :placeholder="$t('placeholder2')"
v-if="getFieldType(list.name).split('%%')[0] === 'select'" v-else-if="getFieldType(list.name).split('%%')[0] === 'select'"
:mode="getFieldType(list.name).split('%%')[1] === 'multiple' ? 'multiple' : 'default'" :mode="getFieldType(list.name).split('%%')[1] === 'multiple' ? 'multiple' : 'default'"
showSearch showSearch
allowClear allowClear
@ -114,18 +135,18 @@
<a-input-number <a-input-number
v-decorator="[list.name, { rules: getDecoratorRules(list) }]" v-decorator="[list.name, { rules: getDecoratorRules(list) }]"
style="width: 100%" style="width: 100%"
v-if="getFieldType(list.name) === 'input_number'" v-else-if="getFieldType(list.name) === 'input_number'"
/> />
<a-date-picker <a-date-picker
v-decorator="[list.name, { rules: getDecoratorRules(list) }]" v-decorator="[list.name, { rules: getDecoratorRules(list) }]"
style="width: 100%" style="width: 100%"
:format="getFieldType(list.name) == '4' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'" :format="getFieldType(list.name) == '4' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
:valueFormat="getFieldType(list.name) == '4' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'" :valueFormat="getFieldType(list.name) == '4' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
v-if="getFieldType(list.name) === '4' || getFieldType(list.name) === '3'" v-else-if="getFieldType(list.name) === '4' || getFieldType(list.name) === '3'"
:showTime="getFieldType(list.name) === '4' ? false : { format: 'HH:mm:ss' }" :showTime="getFieldType(list.name) === '4' ? false : { format: 'HH:mm:ss' }"
/> />
<a-input <a-input
v-if="getFieldType(list.name) === 'input'" v-else-if="getFieldType(list.name) === 'input'"
@focus="(e) => handleFocusInput(e, list)" @focus="(e) => handleFocusInput(e, list)"
v-decorator="[list.name, { rules: getDecoratorRules(list) }]" v-decorator="[list.name, { rules: getDecoratorRules(list) }]"
/> />
@ -156,6 +177,7 @@ import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue'
import { valueTypeMap } from '../../../utils/const' import { valueTypeMap } from '../../../utils/const'
import CreateInstanceFormByGroup from './createInstanceFormByGroup.vue' import CreateInstanceFormByGroup from './createInstanceFormByGroup.vue'
import { getCITypeParent, getCanEditByParentIdChildId } from '@/modules/cmdb/api/CITypeRelation' import { getCITypeParent, getCanEditByParentIdChildId } from '@/modules/cmdb/api/CITypeRelation'
import CIReferenceAttr from '@/components/ciReferenceAttr/index.vue'
export default { export default {
name: 'CreateInstanceForm', name: 'CreateInstanceForm',
@ -164,6 +186,7 @@ export default {
ElOption: Option, ElOption: Option,
JsonEditor, JsonEditor,
CreateInstanceFormByGroup, CreateInstanceFormByGroup,
CIReferenceAttr
}, },
props: { props: {
typeIdFromRelation: { typeIdFromRelation: {
@ -261,6 +284,11 @@ export default {
} }
Object.keys(values).forEach((k) => { Object.keys(values).forEach((k) => {
const _tempFind = this.attributeList.find((item) => item.name === k) const _tempFind = this.attributeList.find((item) => item.name === k)
if (_tempFind.is_reference) {
values[k] = values[k] ? values[k] : null
}
if ( if (
_tempFind.value_type === '3' && _tempFind.value_type === '3' &&
values[k] && values[k] &&
@ -309,6 +337,11 @@ export default {
Object.keys(values).forEach((k) => { Object.keys(values).forEach((k) => {
const _tempFind = this.attributeList.find((item) => item.name === k) const _tempFind = this.attributeList.find((item) => item.name === k)
if (_tempFind.is_reference) {
values[k] = values[k] ? values[k] : null
}
if ( if (
_tempFind.value_type === '3' && _tempFind.value_type === '3' &&
values[k] && values[k] &&
@ -426,6 +459,9 @@ export default {
} }
return 'input' return 'input'
}, },
getAttr(name) {
return this.attributeList.find((item) => item.name === name) ?? {}
},
getSelectFieldOptions(name) { getSelectFieldOptions(name) {
const _find = this.attributeList.find((item) => item.name === name) const _find = this.attributeList.find((item) => item.name === name)
if (_find) { if (_find) {
@ -487,7 +523,12 @@ export default {
} }
return rules return rules
} },
filterAttributes(attributes) {
return attributes.filter((attr) => {
return !attr.is_bool && !attr.is_reference
})
},
}, },
} }
</script> </script>
@ -498,7 +539,7 @@ export default {
} }
.ant-drawer-body { .ant-drawer-body {
overflow-y: auto; overflow-y: auto;
max-height: calc(100vh - 110px); height: calc(100vh - 110px);
} }
} }
</style> </style>

View File

@ -79,6 +79,8 @@
import XEUtils from 'xe-utils' import XEUtils from 'xe-utils'
import { getCITypeAttributesByName } from '@/modules/cmdb/api/CITypeAttr' import { getCITypeAttributesByName } from '@/modules/cmdb/api/CITypeAttr'
import { valueTypeMap } from '@/modules/cmdb/utils/const' import { valueTypeMap } from '@/modules/cmdb/utils/const'
import { getPropertyType } from '@/modules/cmdb/utils/helper'
export default { export default {
name: 'MetadataDrawer', name: 'MetadataDrawer',
data() { data() {
@ -187,12 +189,7 @@ export default {
this.loading = true this.loading = true
const { attributes = [] } = await getCITypeAttributesByName(this.typeId) const { attributes = [] } = await getCITypeAttributesByName(this.typeId)
this.tableData = attributes.map((attr) => { this.tableData = attributes.map((attr) => {
if (attr.is_password) { attr.value_type = getPropertyType(attr)
attr.value_type = '7'
}
if (attr.is_link) {
attr.value_type = '8'
}
return attr return attr
}) })
this.loading = false this.loading = false

View File

@ -1,9 +1,19 @@
<template> <template>
<span :id="`ci-detail-attr-${attr.name}`"> <span :id="`ci-detail-attr-${attr.name}`">
<span v-if="!isEdit || attr.value_type === '6'"> <span v-if="!isEdit || attr.value_type === '6'">
<template v-if="attr.is_reference" >
<a
v-for="(ciId) in (attr.is_list ? ci[attr.name] : [ci[attr.name]])"
:key="ciId"
:href="`/cmdb/cidetail/${attr.reference_type_id}/${ciId}`"
target="_blank"
>
{{ attr.referenceShowAttrNameMap ? attr.referenceShowAttrNameMap[ciId] || ciId : ciId }}
</a>
</template>
<PasswordField <PasswordField
:style="{ display: 'inline-block' }" :style="{ display: 'inline-block' }"
v-if="attr.is_password && ci[attr.name]" v-else-if="attr.is_password && ci[attr.name]"
:ci_id="ci._id" :ci_id="ci._id"
:attr_id="attr.id" :attr_id="attr.id"
></PasswordField> ></PasswordField>
@ -67,6 +77,29 @@
<template v-else> <template v-else>
<a-form :form="form"> <a-form :form="form">
<a-form-item label="" :colon="false"> <a-form-item label="" :colon="false">
<CIReferenceAttr
v-if="attr.is_reference"
:referenceTypeId="attr.reference_type_id"
:isList="attr.is_list"
:referenceShowAttrName="attr.showAttrName"
:initSelectOption="getInitReferenceSelectOption(attr)"
v-decorator="[
attr.name,
{
rules: [{ required: attr.is_required, message: $t('placeholder2') + `${attr.alias || attr.name}` }],
}
]"
/>
<a-switch
v-else-if="attr.is_bool"
v-decorator="[
attr.name,
{
rules: [{ required: attr.is_required }],
valuePropName: 'checked',
}
]"
/>
<a-select <a-select
:style="{ width: '100%' }" :style="{ width: '100%' }"
v-decorator="[ v-decorator="[
@ -76,7 +109,7 @@
}, },
]" ]"
:placeholder="$t('placeholder2')" :placeholder="$t('placeholder2')"
v-if="attr.is_choice" v-else-if="attr.is_choice"
:mode="attr.is_list ? 'multiple' : 'default'" :mode="attr.is_list ? 'multiple' : 'default'"
showSearch showSearch
allowClear allowClear
@ -157,10 +190,11 @@ import { updateCI } from '@/modules/cmdb/api/ci'
import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue' import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue'
import PasswordField from '../../../components/passwordField/index.vue' import PasswordField from '../../../components/passwordField/index.vue'
import { getAttrPassword } from '../../../api/CITypeAttr' import { getAttrPassword } from '../../../api/CITypeAttr'
import CIReferenceAttr from '@/components/ciReferenceAttr/index.vue'
export default { export default {
name: 'CiDetailAttrContent', name: 'CiDetailAttrContent',
components: { JsonEditor, PasswordField }, components: { JsonEditor, PasswordField, CIReferenceAttr },
props: { props: {
ci: { ci: {
type: Object, type: Object,
@ -209,7 +243,7 @@ export default {
} }
this.isEdit = true this.isEdit = true
this.$nextTick(async () => { this.$nextTick(async () => {
if (this.attr.is_list && !this.attr.is_choice) { if (this.attr.is_list && !this.attr.is_choice && !this.attr.is_reference) {
this.form.setFieldsValue({ this.form.setFieldsValue({
[`${this.attr.name}`]: Array.isArray(this.ci[this.attr.name]) [`${this.attr.name}`]: Array.isArray(this.ci[this.attr.name])
? this.ci[this.attr.name].join(',') ? this.ci[this.attr.name].join(',')
@ -237,6 +271,10 @@ export default {
.then(() => { .then(() => {
this.$message.success(this.$t('updateSuccess')) this.$message.success(this.$t('updateSuccess'))
this.$emit('updateCIByself', { [`${this.attr.name}`]: newData }, this.attr.name) this.$emit('updateCIByself', { [`${this.attr.name}`]: newData }, this.attr.name)
if (this.attr.is_reference) {
this.$emit('refreshReferenceAttr')
}
}) })
.catch(() => { .catch(() => {
this.$emit('refresh', this.attr.name) this.$emit('refresh', this.attr.name)
@ -283,6 +321,16 @@ export default {
getName(name) { getName(name) {
return name ?? '' return name ?? ''
}, },
getInitReferenceSelectOption(attr) {
const option = Object.keys(attr?.referenceShowAttrNameMap || {}).map((key) => {
return {
key: Number(key),
title: attr?.referenceShowAttrNameMap?.[key] ?? ''
}
})
return option
}
}, },
} }
</script> </script>

View File

@ -38,6 +38,16 @@
resizable resizable
class="ops-stripe-table" class="ops-stripe-table"
> >
<template #reference_default="{ row, column }">
<a
v-for="(id) in (column.params.attr.is_list ? row[column.field] : [row[column.field]])"
:key="id"
:href="`/cmdb/cidetail/${column.params.attr.reference_type_id}/${id}`"
target="_blank"
>
{{ id }}
</a>
</template>
<template #operation_default="{ row }"> <template #operation_default="{ row }">
<a-popconfirm <a-popconfirm
arrowPointAtCenter arrowPointAtCenter
@ -85,6 +95,16 @@
resizable resizable
class="ops-stripe-table" class="ops-stripe-table"
> >
<template #reference_default="{ row, column }">
<a
v-for="(id) in (column.params.attr.is_list ? row[column.field] : [row[column.field]])"
:key="id"
:href="`/cmdb/cidetail/${column.params.attr.reference_type_id}/${id}`"
target="_blank"
>
{{ id }}
</a>
</template>
<template #operation_default="{ row }"> <template #operation_default="{ row }">
<a-popconfirm <a-popconfirm
arrowPointAtCenter arrowPointAtCenter
@ -258,7 +278,22 @@ export default {
const columns = [] const columns = []
const jsonAttr = [] const jsonAttr = []
item.attributes.forEach((attr) => { item.attributes.forEach((attr) => {
columns.push({ key: 'p_' + attr.id, field: attr.name, title: attr.alias, minWidth: '100px' }) const column = {
key: 'p_' + attr.id,
field: attr.name,
title: attr.alias,
minWidth: '100px',
params: {
attr
},
}
if (attr.is_reference) {
column.slots = {
default: 'reference_default'
}
}
columns.push(column)
if (attr.value_type === '6') { if (attr.value_type === '6') {
jsonAttr.push(attr.name) jsonAttr.push(attr.name)
} }
@ -299,7 +334,22 @@ export default {
const columns = [] const columns = []
const jsonAttr = [] const jsonAttr = []
item.attributes.forEach((attr) => { item.attributes.forEach((attr) => {
columns.push({ key: 'c_' + attr.id, field: attr.name, title: attr.alias, minWidth: '100px' }) const column = {
key: 'c_' + attr.id,
field: attr.name,
title: attr.alias,
minWidth: '100px',
params: {
attr
},
}
if (attr.is_reference) {
column.slots = {
default: 'reference_default'
}
}
columns.push(column)
if (attr.value_type === '6') { if (attr.value_type === '6') {
jsonAttr.push(attr.name) jsonAttr.push(attr.name)
} }

View File

@ -20,7 +20,7 @@
:key="attr.name" :key="attr.name"
v-for="attr in group.attributes" v-for="attr in group.attributes"
> >
<ci-detail-attr-content :ci="ci" :attr="attr" @refresh="refresh" @updateCIByself="updateCIByself" /> <ci-detail-attr-content :ci="ci" :attr="attr" @refresh="refresh" @updateCIByself="updateCIByself" @refreshReferenceAttr="handleReferenceAttr" />
</el-descriptions-item> </el-descriptions-item>
</el-descriptions> </el-descriptions>
</div> </div>
@ -137,7 +137,7 @@ import _ from 'lodash'
import { Descriptions, DescriptionsItem } from 'element-ui' import { Descriptions, DescriptionsItem } from 'element-ui'
import { getCITypeGroupById, getCITypes } from '@/modules/cmdb/api/CIType' import { getCITypeGroupById, getCITypes } from '@/modules/cmdb/api/CIType'
import { getCIHistory, judgeItsmInstalled } from '@/modules/cmdb/api/history' import { getCIHistory, judgeItsmInstalled } from '@/modules/cmdb/api/history'
import { getCIById } from '@/modules/cmdb/api/ci' import { getCIById, searchCI } from '@/modules/cmdb/api/ci'
import CiDetailAttrContent from './ciDetailAttrContent.vue' import CiDetailAttrContent from './ciDetailAttrContent.vue'
import CiDetailRelation from './ciDetailRelation.vue' import CiDetailRelation from './ciDetailRelation.vue'
import TriggerTable from '../../operation_history/modules/triggerTable.vue' import TriggerTable from '../../operation_history/modules/triggerTable.vue'
@ -244,9 +244,78 @@ export default {
getCITypeGroupById(this.typeId, { need_other: 1 }) getCITypeGroupById(this.typeId, { need_other: 1 })
.then((res) => { .then((res) => {
this.attributeGroups = res this.attributeGroups = res
this.handleReferenceAttr()
}) })
.catch((e) => {}) .catch((e) => {})
}, },
async handleReferenceAttr() {
const map = {}
this.attributeGroups.forEach((group) => {
group.attributes.forEach((attr) => {
if (attr?.is_reference && attr?.reference_type_id && this.ci[attr.name]) {
const ids = Array.isArray(this.ci[attr.name]) ? this.ci[attr.name] : this.ci[attr.name] ? [this.ci[attr.name]] : []
if (ids.length) {
if (!map?.[attr.reference_type_id]) {
map[attr.reference_type_id] = {}
}
ids.forEach((id) => {
map[attr.reference_type_id][id] = {}
})
}
}
})
})
if (!Object.keys(map).length) {
return
}
const ciTypesRes = await getCITypes({
type_ids: Object.keys(map).join(',')
})
const showAttrNameMap = {}
ciTypesRes.ci_types.forEach((ciType) => {
showAttrNameMap[ciType.id] = ciType?.show_name || ciType?.unique_name || ''
})
const allRes = await Promise.all(
Object.keys(map).map((key) => {
return searchCI({
q: `_type:${key},_id:(${Object.keys(map[key]).join(';')})`,
count: 9999
})
})
)
const ciNameMap = {}
allRes.forEach((res) => {
res.result.forEach((item) => {
ciNameMap[item._id] = item
})
})
const newAttrGroups = _.cloneDeep(this.attributeGroups)
newAttrGroups.forEach((group) => {
group.attributes.forEach((attr) => {
if (attr?.is_reference && attr?.reference_type_id) {
attr.showAttrName = showAttrNameMap?.[attr?.reference_type_id] || ''
const referenceShowAttrNameMap = {}
const referenceCIIds = this.ci[attr.name];
(Array.isArray(referenceCIIds) ? referenceCIIds : referenceCIIds ? [referenceCIIds] : []).forEach((id) => {
referenceShowAttrNameMap[id] = ciNameMap?.[id]?.[attr.showAttrName] ?? id
})
attr.referenceShowAttrNameMap = referenceShowAttrNameMap
}
})
})
this.$set(this, 'attributeGroups', newAttrGroups)
},
async getCI() { async getCI() {
await getCIById(this.ciId) await getCIById(this.ciId)
.then((res) => { .then((res) => {

View File

@ -16,6 +16,29 @@
:key="attr.name + attr_idx" :key="attr.name + attr_idx"
> >
<a-form-item :label="attr.alias || attr.name" :colon="false"> <a-form-item :label="attr.alias || attr.name" :colon="false">
<CIReferenceAttr
v-if="attr.is_reference"
:referenceTypeId="attr.reference_type_id"
:isList="attr.is_list"
v-decorator="[
attr.name,
{
rules: [{ required: attr.is_required, message: $t('placeholder2') + `${attr.alias || attr.name}` }],
initialValue: attr.is_list ? [] : ''
}
]"
/>
<a-switch
v-else-if="attr.is_bool"
v-decorator="[
attr.name,
{
rules: [{ required: false }],
valuePropName: 'checked',
initialValue: attr.default ? Boolean(attr.default.default) : false
}
]"
/>
<a-select <a-select
:style="{ width: '100%' }" :style="{ width: '100%' }"
v-decorator="[ v-decorator="[
@ -33,7 +56,7 @@
}, },
]" ]"
:placeholder="$t('placeholder2')" :placeholder="$t('placeholder2')"
v-if="attr.is_choice" v-else-if="attr.is_choice"
:mode="attr.is_list ? 'multiple' : 'default'" :mode="attr.is_list ? 'multiple' : 'default'"
showSearch showSearch
allowClear allowClear
@ -133,10 +156,14 @@
import _ from 'lodash' import _ from 'lodash'
import moment from 'moment' import moment from 'moment'
import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue' import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue'
import CIReferenceAttr from '@/components/ciReferenceAttr/index.vue'
export default { export default {
name: 'CreateInstanceFormByGroup', name: 'CreateInstanceFormByGroup',
components: { JsonEditor }, components: {
JsonEditor,
CIReferenceAttr
},
props: { props: {
group: { group: {
type: Object, type: Object,
@ -146,6 +173,10 @@ export default {
type: Array, type: Array,
default: () => [], default: () => [],
}, },
ciTypeId: {
type: [Number, String],
default: ''
}
}, },
inject: ['getFieldType'], inject: ['getFieldType'],
data() { data() {

View File

@ -39,6 +39,7 @@
import { mapState } from 'vuex' import { mapState } from 'vuex'
import _ from 'lodash' import _ from 'lodash'
import { valueTypeMap } from '@/modules/cmdb/utils/const' import { valueTypeMap } from '@/modules/cmdb/utils/const'
import { getPropertyType } from '@/modules/cmdb/utils/helper'
export default { export default {
name: 'AllAttrDrawer', name: 'AllAttrDrawer',
@ -84,12 +85,7 @@ export default {
}) })
otherAttrData.forEach((attr) => { otherAttrData.forEach((attr) => {
if (attr.is_password) { attr.value_type = getPropertyType(attr)
attr.value_type = '7'
}
if (attr.is_link) {
attr.value_type = '8'
}
attr.groupId = -1 attr.groupId = -1
attr.groupName = this.$t('other') attr.groupName = this.$t('other')

View File

@ -21,9 +21,7 @@
<div :class="{ 'attribute-card-name': true, 'attribute-card-name-default-show': property.default_show }"> <div :class="{ 'attribute-card-name': true, 'attribute-card-name-default-show': property.default_show }">
{{ property.alias || property.name }} {{ property.alias || property.name }}
</div> </div>
<div v-if="property.is_password" class="attribute-card_value-type">{{ $t('cmdb.ciType.password') }}</div> <div class="attribute-card_value-type">{{ valueTypeMap[getPropertyType(property)] }}</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>
<div <div
class="attribute-card-trigger" class="attribute-card-trigger"
@ -74,7 +72,9 @@
!isUnique && !isUnique &&
!['6'].includes(property.value_type) && !['6'].includes(property.value_type) &&
!property.is_password && !property.is_password &&
!property.is_list !property.is_list &&
!property.is_reference &&
!property.is_bool
" "
:title="$t(isShowId ? 'cmdb.ciType.cancelSetAsShow' : 'cmdb.ciType.setAsShow')" :title="$t(isShowId ? 'cmdb.ciType.cancelSetAsShow' : 'cmdb.ciType.setAsShow')"
> >
@ -101,6 +101,8 @@ import ValueTypeIcon from '@/components/CMDBValueTypeMapIcon'
import { valueTypeMap } from '../../utils/const' import { valueTypeMap } from '../../utils/const'
import TriggerForm from './triggerForm.vue' import TriggerForm from './triggerForm.vue'
import { updateCIType } from '@/modules/cmdb/api/CIType' import { updateCIType } from '@/modules/cmdb/api/CIType'
import { getPropertyType } from '../../utils/helper'
export default { export default {
name: 'AttributeCard', name: 'AttributeCard',
inject: { inject: {
@ -191,6 +193,7 @@ export default {
}, },
}, },
methods: { methods: {
getPropertyType,
handleEdit() { handleEdit() {
this.$emit('edit') this.$emit('edit')
}, },

View File

@ -0,0 +1,78 @@
<template>
<a-form-item
:label="$t('cmdb.ciType.referenceModel')"
:extra="$t('cmdb.ciType.referenceModelTip1')"
:label-col="formItemLayout.labelCol"
:wrapper-col="formItemLayout.wrapperCol"
>
<a-select
allowClear
v-decorator="['reference_type_id', {
rules: [{ required: true, message: $t('cmdb.ciType.referenceModelTip') }],
initialValue: ''
}]"
showSearch
optionFilterProp="title"
@dropdownVisibleChange="handleDropdownVisibleChange"
>
<a-select-option
v-for="(item) in options"
:key="item.value"
:title="item.label"
>
{{ item.label }}
</a-select-option>
</a-select>
</a-form-item>
</template>
<script>
import { getCITypes } from '@/modules/cmdb/api/CIType'
export default {
name: 'ReferenceModelSelect',
props: {
form: {
type: Object,
required: true,
},
isLazyRequire: {
type: Boolean,
default: true
},
formItemLayout: {
type: Object,
default: () => {}
}
},
data() {
return {
isInit: false,
options: []
}
},
mounted() {
if (!this.isLazyRequire) {
this.getSelectOptions()
}
},
methods: {
handleDropdownVisibleChange(open) {
if (!this.isInit && open) {
this.getSelectOptions()
}
},
async getSelectOptions() {
this.isInit = true
const res = await getCITypes()
this.options = res.ci_types.map((ciType) => {
return {
value: ciType.id,
label: ciType?.alias || ciType?.name || ''
}
})
}
}
}
</script>

View File

@ -60,11 +60,17 @@
v-decorator="['value_type', { rules: [{ required: true }] }]" v-decorator="['value_type', { rules: [{ required: true }] }]"
@change="handleChangeValueType" @change="handleChangeValueType"
> >
<a-select-option :value="key" :key="key" v-for="(value, key) in valueTypeMap">{{ value }}</a-select-option> <a-select-option :value="key" :key="key" v-for="(value, key) in valueTypeMap">
<ops-icon :type="getPropertyIcon({ value_type: key })" />
<span class="value-type-text">{{ value }}</span>
</a-select-option>
</a-select> </a-select>
</a-form-item></a-col </a-form-item></a-col
> >
<a-col :span="currentValueType === '6' ? 24 : 12"> <a-col
v-if="currentValueType !== '11'"
:span="currentValueType === '6' ? 24 : 12"
>
<a-form-item <a-form-item
:label-col="{ span: currentValueType === '6' ? 4 : 8 }" :label-col="{ span: currentValueType === '6' ? 4 : 8 }"
:wrapper-col="{ span: currentValueType === '6' ? 18 : 12 }" :wrapper-col="{ span: currentValueType === '6' ? 18 : 12 }"
@ -77,6 +83,10 @@
v-decorator="['default_value', { rules: [{ required: false }] }]" v-decorator="['default_value', { rules: [{ required: false }] }]"
> >
</a-input> </a-input>
<a-switch
v-else-if="currentValueType === '10'"
v-decorator="['default_value', { rules: [{ required: false }], valuePropName: 'checked' }]"
/>
<a-select <a-select
v-decorator="['default_value', { rules: [{ required: false }] }]" v-decorator="['default_value', { rules: [{ required: false }] }]"
mode="tags" mode="tags"
@ -95,12 +105,7 @@
</a-input-number> </a-input-number>
<a-input <a-input
style="width: 100%" style="width: 100%"
v-else-if=" v-else-if="['2', '5', '7', '8', '9'].includes(currentValueType)"
currentValueType === '2' ||
currentValueType === '5' ||
currentValueType === '7' ||
currentValueType === '8'
"
v-decorator="['default_value', { rules: [{ required: false }] }]" v-decorator="['default_value', { rules: [{ required: false }] }]"
> >
</a-input> </a-input>
@ -157,7 +162,18 @@
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="currentValueType === '2' ? 6 : 0" v-if="currentValueType !== '6'"> <a-col
v-if="currentValueType === '11'"
:span="12"
>
<ReferenceModelSelect
:form="form"
:isLazyRequire="false"
:formItemLayout="formItemLayout"
/>
</a-col>
<!-- <a-col :span="currentValueType === '2' ? 6 : 0" v-if="currentValueType !== '6'">
<a-form-item <a-form-item
:hidden="currentValueType === '2' ? false : true" :hidden="currentValueType === '2' ? false : true"
:label-col="horizontalFormItemLayout.labelCol" :label-col="horizontalFormItemLayout.labelCol"
@ -189,10 +205,10 @@
v-decorator="['is_index', { rules: [], valuePropName: 'checked' }]" v-decorator="['is_index', { rules: [], valuePropName: 'checked' }]"
/> />
</a-form-item> </a-form-item>
</a-col> </a-col> -->
<a-col :span="6" v-if="currentValueType !== '6' && currentValueType !== '7'"> <a-col :span="6" v-if="currentValueType !== '6' && currentValueType !== '7'">
<a-form-item <a-form-item
:label-col="currentValueType === '2' ? { span: 8 } : horizontalFormItemLayout.labelCol" :label-col="horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol" :wrapper-col="horizontalFormItemLayout.wrapperCol"
:label="$t('cmdb.ciType.unique')" :label="$t('cmdb.ciType.unique')"
> >
@ -206,7 +222,7 @@
</a-col> </a-col>
<a-col :span="6"> <a-col :span="6">
<a-form-item <a-form-item
:label-col="['2', '6', '7'].findIndex(i => currentValueType === i) === -1 ? { span: 8 } : horizontalFormItemLayout.labelCol" :label-col="horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol" :wrapper-col="horizontalFormItemLayout.wrapperCol"
:label="$t('required')" :label="$t('required')"
> >
@ -219,7 +235,7 @@
</a-col> </a-col>
<a-col :span="6"> <a-col :span="6">
<a-form-item <a-form-item
:label-col="currentValueType === '2' ? { span: 12 } : horizontalFormItemLayout.labelCol" :label-col="horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol" :wrapper-col="horizontalFormItemLayout.wrapperCol"
> >
<template slot="label"> <template slot="label">
@ -228,8 +244,8 @@
>{{ $t('cmdb.ciType.defaultShow') }} >{{ $t('cmdb.ciType.defaultShow') }}
<a-tooltip :title="$t('cmdb.ciType.defaultShowTips')"> <a-tooltip :title="$t('cmdb.ciType.defaultShowTips')">
<a-icon <a-icon
style="position:absolute;top:2px;left:-17px;color:#2f54eb;" style="position:absolute;top:2px;left:-17px;color:#A5A9BC;"
type="question-circle" type="info-circle"
theme="filled" theme="filled"
@click=" @click="
(e) => { (e) => {
@ -250,7 +266,7 @@
</a-col> </a-col>
<a-col :span="6" v-if="currentValueType !== '6' && currentValueType !== '7'"> <a-col :span="6" v-if="currentValueType !== '6' && currentValueType !== '7'">
<a-form-item <a-form-item
:label-col="currentValueType === '2' ? horizontalFormItemLayout.labelCol : { span: 8 }" :label-col="horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol" :wrapper-col="horizontalFormItemLayout.wrapperCol"
:label="$t('cmdb.ciType.isSortable')" :label="$t('cmdb.ciType.isSortable')"
> >
@ -263,7 +279,7 @@
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="6" v-if="currentValueType !== '6' && currentValueType !== '7'"> <a-col :span="6" v-if="!['6', '7', '10'].includes(currentValueType)">
<a-form-item <a-form-item
:label-col="currentValueType === '2' ? { span: 8 } : horizontalFormItemLayout.labelCol" :label-col="currentValueType === '2' ? { span: 8 } : horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol" :wrapper-col="horizontalFormItemLayout.wrapperCol"
@ -274,8 +290,8 @@
>{{ $t('cmdb.ciType.list') }} >{{ $t('cmdb.ciType.list') }}
<a-tooltip :title="$t('cmdb.ciType.listTips')"> <a-tooltip :title="$t('cmdb.ciType.listTips')">
<a-icon <a-icon
style="position:absolute;top:3px;left:-17px;color:#2f54eb;" style="position:absolute;top:3px;left:-17px;color:#A5A9BC;"
type="question-circle" type="info-circle"
theme="filled" theme="filled"
@click=" @click="
(e) => { (e) => {
@ -297,7 +313,7 @@
</a-col> </a-col>
<a-col span="6"> <a-col span="6">
<a-form-item <a-form-item
:label-col="['2', '6', '7'].findIndex(i => currentValueType === i) === -1 ? { span: 12 } : horizontalFormItemLayout.labelCol" :label-col="horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol" :wrapper-col="horizontalFormItemLayout.wrapperCol"
> >
<template slot="label"> <template slot="label">
@ -306,8 +322,8 @@
>{{ $t('cmdb.ciType.isDynamic') }} >{{ $t('cmdb.ciType.isDynamic') }}
<a-tooltip :title="$t('cmdb.ciType.dynamicTips')"> <a-tooltip :title="$t('cmdb.ciType.dynamicTips')">
<a-icon <a-icon
style="position:absolute;top:3px;left:-17px;color:#2f54eb;" style="position:absolute;top:3px;left:-17px;color:#A5A9BC;"
type="question-circle" type="info-circle"
theme="filled" theme="filled"
@click=" @click="
(e) => { (e) => {
@ -328,17 +344,22 @@
</a-col> </a-col>
<a-divider style="font-size:14px;margin-top:6px;">{{ $t('cmdb.ciType.advancedSettings') }}</a-divider> <a-divider style="font-size:14px;margin-top:6px;">{{ $t('cmdb.ciType.advancedSettings') }}</a-divider>
<a-row> <a-row>
<a-col :span="24" v-if="!['6'].includes(currentValueType)"> <a-col :span="24">
<a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 12 }" :label="$t('cmdb.ciType.reg')"> <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()" /> <RegSelect
:isShowErrorMsg="false"
:limitedFormat="getLimitedFormat()"
:disabled="['6', '10', '11'].includes(currentValueType)"
v-model="re_check"
/>
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="24"> <a-col :span="24">
<a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }" :label="$t('cmdb.ciType.font')"> <a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }" :label="$t('cmdb.ciType.font')">
<FontArea ref="fontArea" /> <FontArea ref="fontArea" :fontColorDisabled="['8', '11'].includes(currentValueType)" />
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="24" v-if="!['6', '7'].includes(currentValueType)"> <a-col :span="24" v-if="!['6', '7', '10', '11'].includes(currentValueType)">
<a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }" :label="$t('cmdb.ciType.choiceValue')"> <a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }" :label="$t('cmdb.ciType.choiceValue')">
<PreValueArea <PreValueArea
v-if="drawerVisible" v-if="drawerVisible"
@ -349,7 +370,7 @@
/> />
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="24" v-if="!['6', '7'].includes(currentValueType)"> <a-col :span="24" v-if="!['6', '7', '10', '11'].includes(currentValueType)">
<a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }"> <a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
<template slot="label"> <template slot="label">
<span <span
@ -357,8 +378,8 @@
>{{ $t('cmdb.ciType.computedAttribute') }} >{{ $t('cmdb.ciType.computedAttribute') }}
<a-tooltip :title="$t('cmdb.ciType.computedAttributeTips')"> <a-tooltip :title="$t('cmdb.ciType.computedAttributeTips')">
<a-icon <a-icon
style="position:absolute;top:3px;left:-17px;color:#2f54eb;" style="position:absolute;top:3px;left:-17px;color:#A5A9BC;"
type="question-circle" type="info-circle"
theme="filled" theme="filled"
@click=" @click="
(e) => { (e) => {
@ -415,14 +436,16 @@ import {
calcComputedAttribute, calcComputedAttribute,
} from '@/modules/cmdb/api/CITypeAttr' } from '@/modules/cmdb/api/CITypeAttr'
import { valueTypeMap } from '../../utils/const' import { valueTypeMap } from '../../utils/const'
import { getPropertyType, getPropertyIcon } from '../../utils/helper'
import ComputedArea from './computedArea.vue' import ComputedArea from './computedArea.vue'
import PreValueArea from './preValueArea.vue' import PreValueArea from './preValueArea.vue'
import FontArea from './fontArea.vue' import FontArea from './fontArea.vue'
import RegSelect from '@/components/RegexSelect' import RegSelect from '@/components/RegexSelect'
import ReferenceModelSelect from './attributeEdit/referenceModelSelect.vue'
export default { export default {
name: 'AttributeEditForm', name: 'AttributeEditForm',
components: { ComputedArea, PreValueArea, vueJsonEditor, FontArea, RegSelect }, components: { ComputedArea, PreValueArea, vueJsonEditor, FontArea, RegSelect, ReferenceModelSelect },
props: { props: {
CITypeId: { CITypeId: {
type: Number, type: Number,
@ -467,7 +490,7 @@ export default {
return formLayout === 'horizontal' return formLayout === 'horizontal'
? { ? {
labelCol: { span: 8 }, labelCol: { span: 8 },
wrapperCol: { span: 12 }, wrapperCol: { span: 15 },
} }
: {} : {}
}, },
@ -484,6 +507,7 @@ export default {
}, },
mounted() {}, mounted() {},
methods: { methods: {
getPropertyIcon,
async handleCreate() { async handleCreate() {
try { try {
await canDefineComputed() await canDefineComputed()
@ -516,9 +540,7 @@ export default {
} }
} }
if (property === 'is_list') { if (property === 'is_list') {
this.form.setFieldsValue({ this.handleSwitchIsList(checked)
default_value: checked ? [] : '',
})
} }
if (checked && property === 'is_sortable') { if (checked && property === 'is_sortable') {
this.$message.warning(this.$t('cmdb.ciType.addAttributeTips1')) this.$message.warning(this.$t('cmdb.ciType.addAttributeTips1'))
@ -536,6 +558,26 @@ export default {
} }
}, },
handleSwitchIsList(checked) {
let defaultValue = checked ? [] : ''
switch (this.currentValueType) {
case '2':
case '9':
defaultValue = ''
break
case '10':
defaultValue = checked ? '' : false
break
default:
break
}
this.form.setFieldsValue({
default_value: defaultValue,
})
},
async handleEdit(record, attributes) { async handleEdit(record, attributes) {
try { try {
await canDefineComputed() await canDefineComputed()
@ -544,12 +586,7 @@ export default {
this.canDefineComputed = false this.canDefineComputed = false
} }
const _record = _.cloneDeep(record) const _record = _.cloneDeep(record)
if (_record.is_password) { _record.value_type = getPropertyType(_record)
_record.value_type = '7'
}
if (_record.is_link) {
_record.value_type = '8'
}
this.drawerTitle = this.$t('cmdb.ciType.editAttribute') this.drawerTitle = this.$t('cmdb.ciType.editAttribute')
this.drawerVisible = true this.drawerVisible = true
this.record = _record this.record = _record
@ -573,8 +610,13 @@ export default {
is_dynamic: _record.is_dynamic, is_dynamic: _record.is_dynamic,
}) })
} }
if (_record.value_type === '11') {
this.form.setFieldsValue({
reference_type_id: _record.reference_type_id
})
}
console.log(_record) console.log(_record)
if (!['6'].includes(_record.value_type) && _record.re_check) { if (!['6', '10', '11'].includes(_record.value_type) && _record.re_check) {
this.re_check = { this.re_check = {
value: _record.re_check, value: _record.re_check,
} }
@ -583,7 +625,11 @@ export default {
} }
if (_record.default) { if (_record.default) {
this.$nextTick(() => { this.$nextTick(() => {
if (_record.value_type === '0') { if (_record.value_type === '10') {
this.form.setFieldsValue({
default_value: Boolean(_record.default.default),
})
} else if (_record.value_type === '0') {
if (_record.is_list) { if (_record.is_list) {
this.$nextTick(() => { this.$nextTick(() => {
this.form.setFieldsValue({ this.form.setFieldsValue({
@ -639,7 +685,7 @@ export default {
}) })
} }
const _find = attributes.find((item) => item.id === _record.id) const _find = attributes.find((item) => item.id === _record.id)
if (!['6', '7'].includes(_record.value_type)) { if (!['6', '7', '10', '11'].includes(_record.value_type)) {
this.$refs.preValueArea.setData({ this.$refs.preValueArea.setData({
choice_value: (_find || {}).choice_value || [], choice_value: (_find || {}).choice_value || [],
choice_web_hook: _record.choice_web_hook, choice_web_hook: _record.choice_web_hook,
@ -672,7 +718,9 @@ export default {
delete values['default_show'] delete values['default_show']
delete values['is_required'] delete values['is_required']
const { default_value } = values const { default_value } = values
if (values.value_type === '0' && default_value) { if (values.value_type === '10') {
values.default = { default: values.is_list ? default_value : Boolean(default_value) }
} else if (values.value_type === '0' && default_value) {
if (values.is_list) { if (values.is_list) {
values.default = { default: default_value || null } values.default = { default: default_value || null }
} else { } else {
@ -706,23 +754,42 @@ export default {
values = { ...values, ...computedAreaData } values = { ...values, ...computedAreaData }
} else { } else {
// If it is a non-computed attribute, check to see if there is a predefined value // If it is a non-computed attribute, check to see if there is a predefined value
if (!['6', '7'].includes(values.value_type)) { if (!['6', '7', '10', '11'].includes(values.value_type)) {
const preValueAreaData = this.$refs.preValueArea.getData() const preValueAreaData = this.$refs.preValueArea.getData()
values = { ...values, ...preValueAreaData } values = { ...values, ...preValueAreaData }
} }
} }
const fontOptions = this.$refs.fontArea.getData() const fontOptions = this.$refs.fontArea.getData()
if (values.value_type === '7') {
values.value_type = '2' if (!['6', '10', '11'].includes(values.value_type)) {
values.is_password = true
}
if (values.value_type === '8') {
values.value_type = '2'
values.is_link = true
}
if (values.value_type !== '6') {
values.re_check = this.re_check?.value ?? null values.re_check = this.re_check?.value ?? null
} }
// 重置数据类型
switch (values.value_type) {
case '7':
values.value_type = '2'
values.is_password = true
break
case '8':
values.value_type = '2'
values.is_link = true
break
case '9':
values.value_type = '2'
break
case '10':
values.value_type = '7'
values.is_bool = true
break
case '11':
values.value_type = '0'
values.is_reference = true
break
default:
break
}
if (values.id) { if (values.id) {
await this.updateAttribute(values.id, { ...values, option: { fontOptions } }, isCalcComputed) await this.updateAttribute(values.id, { ...values, option: { fontOptions } }, isCalcComputed)
} else { } else {
@ -806,6 +873,9 @@ export default {
line-height: 22px; line-height: 22px;
color: #a5a9bc; color: #a5a9bc;
} }
.value-type-text {
margin-left: 4px;
}
</style> </style>
<style lang="less"> <style lang="less">
.attribute-edit-form { .attribute-edit-form {

View File

@ -17,21 +17,21 @@
<a-space style="margin-bottom: 10px"> <a-space style="margin-bottom: 10px">
<a-button @click="handleAddGroup" size="small" icon="plus">{{ $t('cmdb.ciType.group') }}</a-button> <a-button @click="handleAddGroup" size="small" icon="plus">{{ $t('cmdb.ciType.group') }}</a-button>
<a-button @click="handleOpenUniqueConstraint" size="small">{{ $t('cmdb.ciType.uniqueConstraint') }}</a-button> <a-button @click="handleOpenUniqueConstraint" size="small">{{ $t('cmdb.ciType.uniqueConstraint') }}</a-button>
<div> <div class="ci-types-attributes-flex">
<a-tooltip <a-tooltip
v-for="typeKey in Object.keys(valueTypeMap)" v-for="item in valueTypeMap"
:key="typeKey" :key="item.key"
:title="$t('cmdb.ciType.filterTips', { name: valueTypeMap[typeKey] })" :title="$t('cmdb.ciType.filterTips', { name: item.value })"
> >
<span <span
@click="handleFilterType(typeKey)" @click="handleFilterType(item.key)"
:class="{ :class="{
'ci-types-attributes-filter': true, 'ci-types-attributes-filter': true,
'ci-types-attributes-filter-selected': attrTypeFilter.includes(typeKey), 'ci-types-attributes-filter-selected': attrTypeFilter.includes(item.key),
}" }"
> >
<ops-icon :type="getPropertyIcon({ value_type: typeKey })" /> <ops-icon :type="getPropertyIcon({ value_type: item.key })" />
{{ valueTypeMap[typeKey] }} {{ item.value }}
</span> </span>
</a-tooltip> </a-tooltip>
</div> </div>
@ -201,7 +201,7 @@ import AttributeEditForm from './attributeEditForm.vue'
import NewCiTypeAttrModal from './newCiTypeAttrModal.vue' import NewCiTypeAttrModal from './newCiTypeAttrModal.vue'
import UniqueConstraint from './uniqueConstraint.vue' import UniqueConstraint from './uniqueConstraint.vue'
import { valueTypeMap } from '../../utils/const' import { valueTypeMap } from '../../utils/const'
import { getPropertyIcon } from '../../utils/helper' import { getPropertyIcon, getPropertyType } from '../../utils/helper'
export default { export default {
name: 'AttributesTable', name: 'AttributesTable',
@ -245,7 +245,12 @@ export default {
return this.$store.state.windowHeight return this.$store.state.windowHeight
}, },
valueTypeMap() { valueTypeMap() {
return valueTypeMap() const map = valueTypeMap()
const keys = ['0', '1', '2', '9', '3', '4', '5', '6', '7', '8', '10', '11']
return keys.map((key) => ({
key,
value: map[key]
}))
}, },
}, },
provide() { provide() {
@ -598,23 +603,8 @@ export default {
if (!attrTypeFilter.length) { if (!attrTypeFilter.length) {
return true return true
} else { } else {
if (attrTypeFilter.includes('7') && attr.is_password) { const valueType = getPropertyType(attr)
return true return attrTypeFilter.includes(valueType)
}
if (attrTypeFilter.includes('8') && attr.is_link) {
return true
}
if (
attrTypeFilter.includes(attr.value_type) &&
attr.value_type === '2' &&
(attr.is_password || attr.is_link)
) {
return false
}
if (attrTypeFilter.includes(attr.value_type)) {
return true
}
return false
} }
}) })
}, },
@ -638,6 +628,12 @@ export default {
.ci-types-attributes { .ci-types-attributes {
padding: 0 20px; padding: 0 20px;
overflow-y: auto; overflow-y: auto;
&-flex {
display: flex;
flex-wrap: wrap;
}
.ci-types-attributes-filter { .ci-types-attributes-filter {
color: @text-color_4; color: @text-color_4;
cursor: pointer; cursor: pointer;

View File

@ -46,16 +46,21 @@
v-decorator="['value_type', { rules: [{ required: true }], initialValue: '2' }]" v-decorator="['value_type', { rules: [{ required: true }], initialValue: '2' }]"
@change="handleChangeValueType" @change="handleChangeValueType"
> >
<a-select-option :value="key" :key="key" v-for="(value, key) in valueTypeMap"> <a-select-option :value="item.key" :key="item.key" v-for="(item) in valueTypeMap">
{{ value }} <ops-icon :type="getPropertyIcon({ value_type: item.key })" />
<span class="value-type-des" v-if="key === '3'">yyyy-mm-dd HH:MM:SS</span> <span class="value-type-text">{{ item.value }}</span>
<span class="value-type-des" v-if="key === '4'">yyyy-mm-dd</span> <span class="value-type-des" v-if="item.key === '2'">{{ $t('cmdb.ciType.shortTextTip') }}</span>
<span class="value-type-des" v-if="key === '5'">HH:MM:SS</span> <span class="value-type-des" v-if="item.key === '3'">yyyy-mm-dd HH:MM:SS</span>
<span class="value-type-des" v-if="item.key === '4'">yyyy-mm-dd</span>
<span class="value-type-des" v-if="item.key === '5'">HH:MM:SS</span>
</a-select-option> </a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="currentValueType === '6' ? 24 : 12"> <a-col
v-if="currentValueType !== '11'"
:span="currentValueType === '6' ? 24 : 12"
>
<a-form-item <a-form-item
:label-col="{ span: currentValueType === '6' ? 4 : 8 }" :label-col="{ span: currentValueType === '6' ? 4 : 8 }"
:wrapper-col="{ span: currentValueType === '6' ? 18 : 15 }" :wrapper-col="{ span: currentValueType === '6' ? 18 : 15 }"
@ -68,6 +73,10 @@
v-decorator="['default_value', { rules: [{ required: false }] }]" v-decorator="['default_value', { rules: [{ required: false }] }]"
> >
</a-input> </a-input>
<a-switch
v-else-if="currentValueType === '10'"
v-decorator="['default_value', { rules: [{ required: false }], valuePropName: 'checked' }]"
/>
<a-input-number <a-input-number
style="width: 100%" style="width: 100%"
v-else-if="currentValueType === '1'" v-else-if="currentValueType === '1'"
@ -86,12 +95,7 @@
</a-select> </a-select>
<a-input <a-input
style="width: 100%" style="width: 100%"
v-else-if=" v-else-if="['2', '5', '7', '8', '9'].includes(currentValueType)"
currentValueType === '2' ||
currentValueType === '5' ||
currentValueType === '7' ||
currentValueType === '8'
"
v-decorator="['default_value', { rules: [{ required: false }] }]" v-decorator="['default_value', { rules: [{ required: false }] }]"
> >
</a-input> </a-input>
@ -148,9 +152,19 @@
</template> </template>
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col
v-if="currentValueType === '11'"
:span="12"
>
<ReferenceModelSelect
:form="form"
:formItemLayout="formItemLayout"
/>
</a-col>
</a-row> </a-row>
<a-col :span="currentValueType === '2' ? 6 : 0" v-if="currentValueType !== '6'"> <!-- <a-col :span="currentValueType === '2' ? 6 : 0" v-if="currentValueType !== '6'">
<a-form-item <a-form-item
:hidden="currentValueType === '2' ? false : true" :hidden="currentValueType === '2' ? false : true"
:label-col="horizontalFormItemLayout.labelCol" :label-col="horizontalFormItemLayout.labelCol"
@ -182,10 +196,10 @@
v-decorator="['is_index', { rules: [], valuePropName: 'checked', initialValue: true }]" v-decorator="['is_index', { rules: [], valuePropName: 'checked', initialValue: true }]"
/> />
</a-form-item> </a-form-item>
</a-col> </a-col> -->
<a-col :span="6" v-if="currentValueType !== '6' && currentValueType !== '7'"> <a-col :span="6" v-if="currentValueType !== '6' && currentValueType !== '7'">
<a-form-item <a-form-item
:label-col="currentValueType === '2' ? { span: 8 } : horizontalFormItemLayout.labelCol" :label-col="horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol" :wrapper-col="horizontalFormItemLayout.wrapperCol"
:label="$t('cmdb.ciType.unique')" :label="$t('cmdb.ciType.unique')"
> >
@ -199,7 +213,7 @@
</a-col> </a-col>
<a-col :span="6"> <a-col :span="6">
<a-form-item <a-form-item
:label-col="['2', '6', '7'].findIndex(i => currentValueType === i) === -1 ? { span: 8 } : horizontalFormItemLayout.labelCol" :label-col="horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol" :wrapper-col="horizontalFormItemLayout.wrapperCol"
:label="$t('required')" :label="$t('required')"
> >
@ -212,7 +226,7 @@
</a-col> </a-col>
<a-col :span="6"> <a-col :span="6">
<a-form-item <a-form-item
:label-col="currentValueType === '2' ? { span: 12 } : horizontalFormItemLayout.labelCol" :label-col="horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol" :wrapper-col="horizontalFormItemLayout.wrapperCol"
> >
<template slot="label"> <template slot="label">
@ -221,8 +235,8 @@
>{{ $t('cmdb.ciType.defaultShow') }} >{{ $t('cmdb.ciType.defaultShow') }}
<a-tooltip :title="$t('cmdb.ciType.defaultShowTips')"> <a-tooltip :title="$t('cmdb.ciType.defaultShowTips')">
<a-icon <a-icon
style="position:absolute;top:2px;left:-17px;color:#2f54eb;" style="position:absolute;top:2px;left:-17px;color:#A5A9BC;"
type="question-circle" type="info-circle"
theme="filled" theme="filled"
@click=" @click="
(e) => { (e) => {
@ -243,7 +257,7 @@
</a-col> </a-col>
<a-col :span="6" v-if="currentValueType !== '6' && currentValueType !== '7'"> <a-col :span="6" v-if="currentValueType !== '6' && currentValueType !== '7'">
<a-form-item <a-form-item
:label-col="currentValueType === '2' ? horizontalFormItemLayout.labelCol : { span: 8 }" :label-col="horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol" :wrapper-col="horizontalFormItemLayout.wrapperCol"
:label="$t('cmdb.ciType.isSortable')" :label="$t('cmdb.ciType.isSortable')"
> >
@ -256,7 +270,7 @@
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="6" v-if="currentValueType !== '6' && currentValueType !== '7'"> <a-col :span="6" v-if="!['6', '7', '10'].includes(currentValueType)">
<a-form-item <a-form-item
:label-col="currentValueType === '2' ? { span: 8 } : horizontalFormItemLayout.labelCol" :label-col="currentValueType === '2' ? { span: 8 } : horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol" :wrapper-col="horizontalFormItemLayout.wrapperCol"
@ -264,11 +278,11 @@
<template slot="label"> <template slot="label">
<span <span
style="position:relative;white-space:pre;" style="position:relative;white-space:pre;"
>{{ $t('cmdb.ciType.list') }} >
<a-tooltip :title="$t('cmdb.ciType.listTips')"> <a-tooltip :title="$t('cmdb.ciType.listTips')">
<a-icon <a-icon
style="position:absolute;top:3px;left:-17px;color:#2f54eb;" style="position:absolute;top:3px;left:-17px;color:#A5A9BC;"
type="question-circle" type="info-circle"
theme="filled" theme="filled"
@click=" @click="
(e) => { (e) => {
@ -278,6 +292,7 @@
" "
/> />
</a-tooltip> </a-tooltip>
{{ $t('cmdb.ciType.list') }}
</span> </span>
</template> </template>
<a-switch <a-switch
@ -290,17 +305,17 @@
</a-col> </a-col>
<a-col span="6"> <a-col span="6">
<a-form-item <a-form-item
:label-col="['2', '6', '7'].findIndex(i => currentValueType === i) === -1 ? { span: 12 } : horizontalFormItemLayout.labelCol" :label-col="horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol" :wrapper-col="horizontalFormItemLayout.wrapperCol"
> >
<template slot="label"> <template slot="label">
<span <span
style="position:relative;white-space:pre;" style="position:relative;white-space:pre;"
>{{ $t('cmdb.ciType.isDynamic') }} >
<a-tooltip :title="$t('cmdb.ciType.dynamicTips')"> <a-tooltip :title="$t('cmdb.ciType.dynamicTips')">
<a-icon <a-icon
style="position:absolute;top:3px;left:-17px;color:#2f54eb;" style="position:absolute;top:3px;left:-17px;color:#A5A9BC;"
type="question-circle" type="info-circle"
theme="filled" theme="filled"
@click=" @click="
(e) => { (e) => {
@ -310,6 +325,7 @@
" "
/> />
</a-tooltip> </a-tooltip>
{{ $t('cmdb.ciType.isDynamic') }}
</span> </span>
</template> </template>
<a-switch <a-switch
@ -321,17 +337,22 @@
</a-col> </a-col>
<a-divider style="font-size:14px;margin-top:6px;">{{ $t('cmdb.ciType.advancedSettings') }}</a-divider> <a-divider style="font-size:14px;margin-top:6px;">{{ $t('cmdb.ciType.advancedSettings') }}</a-divider>
<a-row> <a-row>
<a-col :span="24" v-if="!['6'].includes(currentValueType)"> <a-col :span="24">
<a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 12 }" :label="$t('cmdb.ciType.reg')"> <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()" /> <RegSelect
v-model="re_check"
:isShowErrorMsg="false"
:limitedFormat="getLimitedFormat()"
:disabled="['6', '10', '11'].includes(currentValueType)"
/>
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="24"> <a-col :span="24">
<a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }" :label="$t('cmdb.ciType.font')"> <a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }" :label="$t('cmdb.ciType.font')">
<FontArea ref="fontArea" /> <FontArea ref="fontArea" :fontColorDisabled="['8', '11'].includes(currentValueType)" />
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="24" v-if="!['6', '7'].includes(currentValueType)"> <a-col :span="24" v-if="!['6', '7', '10', '11'].includes(currentValueType)">
<a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }" :label="$t('cmdb.ciType.choiceValue')"> <a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }" :label="$t('cmdb.ciType.choiceValue')">
<PreValueArea <PreValueArea
ref="preValueArea" ref="preValueArea"
@ -341,16 +362,16 @@
/> />
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="24" v-if="!['6', '7'].includes(currentValueType)"> <a-col :span="24" v-if="!['6', '7', '10', '11'].includes(currentValueType)">
<a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }"> <a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
<template slot="label"> <template slot="label">
<span <span
style="position:relative;white-space:pre;" 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 <a-icon
style="position:absolute;top:3px;left:-17px;color:#2f54eb;" style="position:absolute;top:3px;left:-17px;color:#A5A9BC;"
type="question-circle" type="info-circle"
theme="filled" theme="filled"
@click=" @click="
(e) => { (e) => {
@ -360,6 +381,7 @@
" "
/> />
</a-tooltip> </a-tooltip>
{{ $t('cmdb.ciType.computedAttribute') }}
</span> </span>
</template> </template>
<a-switch <a-switch
@ -392,6 +414,8 @@ import ComputedArea from './computedArea.vue'
import PreValueArea from './preValueArea.vue' import PreValueArea from './preValueArea.vue'
import FontArea from './fontArea.vue' import FontArea from './fontArea.vue'
import RegSelect from '@/components/RegexSelect' import RegSelect from '@/components/RegexSelect'
import { getPropertyIcon } from '../../utils/helper'
import ReferenceModelSelect from './attributeEdit/referenceModelSelect.vue'
export default { export default {
name: 'CreateNewAttribute', name: 'CreateNewAttribute',
@ -401,6 +425,7 @@ export default {
vueJsonEditor, vueJsonEditor,
FontArea, FontArea,
RegSelect, RegSelect,
ReferenceModelSelect,
}, },
props: { props: {
hasFooter: { hasFooter: {
@ -437,13 +462,19 @@ export default {
}, },
computed: { computed: {
valueTypeMap() { valueTypeMap() {
return valueTypeMap() const map = valueTypeMap()
const keys = ['0', '1', '2', '9', '3', '4', '5', '6', '7', '8', '10', '11']
return keys.map((key) => ({
key,
value: map[key]
}))
}, },
canDefineScript() { canDefineScript() {
return this.canDefineComputed return this.canDefineComputed
}, },
}, },
methods: { methods: {
getPropertyIcon,
handleSubmit(isCloseModal = true) { handleSubmit(isCloseModal = true) {
this.form.validateFields(async (err, values) => { this.form.validateFields(async (err, values) => {
if (!err) { if (!err) {
@ -456,7 +487,9 @@ export default {
const data = { is_required, default_show, is_dynamic } const data = { is_required, default_show, is_dynamic }
delete values.is_required delete values.is_required
delete values.default_show delete values.default_show
if (values.value_type === '0' && default_value) { if (values.value_type === '10') {
values.default = { default: values.is_list ? (default_value || null) : Boolean(default_value) }
} else if (values.value_type === '0' && default_value) {
if (values.is_list) { if (values.is_list) {
values.default = { default: default_value || null } values.default = { default: default_value || null }
} else { } else {
@ -489,39 +522,48 @@ export default {
values = { ...values, ...computedAreaData } values = { ...values, ...computedAreaData }
} else { } else {
// If it is a non-computed attribute, check to see if there is a predefined value // If it is a non-computed attribute, check to see if there is a predefined value
if (!['6', '7'].includes(values.value_type)) { if (!['6', '7', '10', '11'].includes(values.value_type)) {
const preValueAreaData = this.$refs.preValueArea.getData() const preValueAreaData = this.$refs.preValueArea.getData()
values = { ...values, ...preValueAreaData } values = { ...values, ...preValueAreaData }
} }
} }
const fontOptions = this.$refs.fontArea.getData() const fontOptions = this.$refs.fontArea.getData()
// is_index: except for text, the index is hidden, text index default is true // 索引
// 5 types in the box, is_index=true values.is_index = !['6', '7', '8', '9', '11'].includes(values.value_type)
// json, password, link is_index=false
if (['6', '7', '8'].includes(values.value_type)) { // 重置数据类型
values.is_index = false switch (values.value_type) {
} else if (values.value_type !== '2') { case '7':
values.is_index = true values.value_type = '2'
} values.is_password = true
if (values.value_type === '7') { break
values.value_type = '2' case '8':
values.is_password = true values.value_type = '2'
} values.is_link = true
if (values.value_type === '8') { break
values.value_type = '2' case '9':
values.is_link = true values.value_type = '2'
} break
if (values.value_type !== '6') { case '10':
values.re_check = this.re_check?.value ?? null values.value_type = '7'
values.is_bool = true
break
case '11':
values.value_type = '0'
values.is_reference = true
break
default:
break
} }
const { attr_id } = await createAttribute({ ...values, option: { fontOptions } }) const { attr_id } = await createAttribute({ ...values, option: { fontOptions } })
this.form.resetFields() this.form.resetFields()
this.currentValueType = '2' if (this?.$refs?.preValueArea) {
if (!['6'].includes(values.value_type) && !values.is_password) {
this.$refs.preValueArea.valueList = [] this.$refs.preValueArea.valueList = []
} }
this.currentValueType = '2'
this.$emit('done', attr_id, data, isCloseModal) this.$emit('done', attr_id, data, isCloseModal)
} else { } else {
throw new Error() throw new Error()
@ -540,11 +582,12 @@ export default {
} }
}, },
handleChangeValueType(value) { handleChangeValueType(value) {
this.currentValueType = value
this.$nextTick(() => { this.$nextTick(() => {
this.form.setFieldsValue({ this.currentValueType = value
default_value: this.form.getFieldValue('is_list') || value === '0' ? [] : null, if (['6', '10', '11'].includes(value)) {
}) this.re_check = {}
}
this.handleSwitchType({ valueType: value })
}) })
}, },
onChange(checked, property) { onChange(checked, property) {
@ -560,9 +603,7 @@ export default {
} }
} }
if (property === 'is_list') { if (property === 'is_list') {
this.form.setFieldsValue({ this.handleSwitchType({ checked })
default_value: checked ? [] : '',
})
} }
if (checked && property === 'is_sortable') { if (checked && property === 'is_sortable') {
this.$message.warning(this.$t('cmdb.ciType.addAttributeTips1')) this.$message.warning(this.$t('cmdb.ciType.addAttributeTips1'))
@ -579,6 +620,33 @@ export default {
}) })
} }
}, },
handleSwitchType({
checked,
valueType
}) {
checked = checked ?? this.form.getFieldValue('is_list')
valueType = valueType ?? this.currentValueType
let defaultValue = checked || valueType === '0' ? [] : ''
switch (valueType) {
case '2':
case '9':
defaultValue = ''
break
case '10':
defaultValue = checked ? '' : false
break
default:
break
}
this.form.setFieldsValue({
default_value: defaultValue,
})
},
onJsonChange(value) { onJsonChange(value) {
this.default_value_json_right = true this.default_value_json_right = true
}, },
@ -629,6 +697,9 @@ export default {
line-height: 22px; line-height: 22px;
color: #a5a9bc; color: #a5a9bc;
} }
.value-type-text {
margin: 0 4px;
}
</style> </style>
<style lang="less"> <style lang="less">
.create-new-attribute { .create-new-attribute {

View File

@ -21,7 +21,13 @@
<a-icon type="underline" /> <a-icon type="underline" />
</div> </div>
<div :style="{ width: '100px', marginLeft: '10px', display: 'inline-flex', alignItems: 'center' }"> <div :style="{ width: '100px', marginLeft: '10px', display: 'inline-flex', alignItems: 'center' }">
<a-icon type="font-colors" /><el-color-picker size="mini" v-model="fontOptions.color"> </el-color-picker> <a-icon type="font-colors" />
<el-color-picker
size="mini"
:disabled="fontColorDisabled"
v-model="fontOptions.color"
>
</el-color-picker>
</div> </div>
</div> </div>
</template> </template>
@ -30,6 +36,12 @@
import _ from 'lodash' import _ from 'lodash'
export default { export default {
name: 'FontArea', name: 'FontArea',
props: {
fontColorDisabled: {
type: Boolean,
default: false
}
},
data() { data() {
return { return {
fontOptions: { fontOptions: {
@ -57,7 +69,11 @@ export default {
if (flag) { if (flag) {
return undefined return undefined
} else { } else {
return this.fontOptions const fontOptions = _.cloneDeep(this.fontOptions)
if (this.fontColorDisabled) {
Reflect.deleteProperty(fontOptions, 'color')
}
return fontOptions
} }
}, },
setData({ fontOptions = {} }) { setData({ fontOptions = {} }) {

View File

@ -545,7 +545,7 @@ export default {
}, },
showIdSelectOptions() { showIdSelectOptions() {
const _showIdSelectOptions = this.currentTypeAttrs.filter( const _showIdSelectOptions = this.currentTypeAttrs.filter(
(item) => item.id !== this.unique_id && !['6'].includes(item.value_type) && !item.is_password && !item.is_list (item) => item.id !== this.unique_id && !['6'].includes(item.value_type) && !item.is_password && !item.is_list && !item.is_bool && !item.is_reference
) )
if (this.showIdFilterInput) { if (this.showIdFilterInput) {
return _showIdSelectOptions.filter( return _showIdSelectOptions.filter(
@ -898,6 +898,7 @@ export default {
this.loadCITypes() this.loadCITypes()
this.loading = false this.loading = false
this.drawerVisible = false this.drawerVisible = false
this.isInherit = false
}, 1000) }, 1000)
}, },
async updateCIType(CITypeId, data) { async updateCIType(CITypeId, data) {
@ -916,6 +917,7 @@ export default {
this.loadCITypes() this.loadCITypes()
this.loading = false this.loading = false
this.drawerVisible = false this.drawerVisible = false
this.isInherit = false
}, 1000) }, 1000)
}) })
}) })

View File

@ -47,9 +47,32 @@
> >
<ops-icon class="text-group-icon" type="veops-text" /> <ops-icon class="text-group-icon" type="veops-text" />
</div> </div>
<CIReferenceAttr
v-if="getAttr(rule.property).is_reference && (rule.exp === 'is' || rule.exp === '~is')"
class="select-filter"
:referenceTypeId="getAttr(rule.property).reference_type_id"
:value="rule.value"
:disabled="disabled"
@change="(value) => handleChange('value', value)"
/>
<a-select
v-else-if="getAttr(rule.property).is_bool && (rule.exp === 'is' || rule.exp === '~is')"
class="select-filter"
:disabled="disabled"
:placeholder="$t('placeholder2')"
:value="rule.value"
@change="(value) => handleChange('value', value)"
>
<a-select-option key="1">
true
</a-select-option>
<a-select-option key="0">
false
</a-select-option>
</a-select>
<div <div
class="input-group" class="input-group"
v-if="isChoiceByProperty(rule.property) && (rule.exp === 'is' || rule.exp === '~is')" v-else-if="isChoiceByProperty(rule.property) && (rule.exp === 'is' || rule.exp === '~is')"
> >
<treeselect <treeselect
class="custom-treeselect" class="custom-treeselect"
@ -148,9 +171,13 @@
<script> <script>
import { compareTypeList } from '../constants.js' import { compareTypeList } from '../constants.js'
import CIReferenceAttr from '@/components/ciReferenceAttr/index.vue'
export default { export default {
name: 'ValueControls', name: 'ValueControls',
components: {
CIReferenceAttr
},
props: { props: {
rule: { rule: {
type: Object, type: Object,
@ -215,7 +242,10 @@ export default {
...this.rule, ...this.rule,
[key]: value [key]: value
}) })
} },
getAttr(property) {
return this.attrList.find((item) => item.name === property) || {}
},
} }
} }
</script> </script>
@ -270,4 +300,21 @@ export default {
color: #FFFFFF; color: #FFFFFF;
} }
} }
.select-filter {
height: 36px;
width: 136px;
/deep/ .ant-select-selection {
height: 36px;
background: #f7f8fa;
line-height: 36px;
border: none;
.ant-select-selection__rendered {
height: 36px;
line-height: 36px;
}
}
}
</style> </style>

View File

@ -137,6 +137,7 @@ import {
getCITypeChildren, getCITypeChildren,
getCITypeParent getCITypeParent
} from '../../api/CITypeRelation.js' } from '../../api/CITypeRelation.js'
import { getCITypeAttributesById } from '../../api/CITypeAttr'
export default { export default {
name: 'RelationAutoDiscovery', name: 'RelationAutoDiscovery',
@ -169,7 +170,18 @@ export default {
methods: { methods: {
async getCITypeAttributes() { async getCITypeAttributes() {
const res = await getCITypeAttributes(this.CITypeId) const res = await getCITypeAttributes(this.CITypeId)
this.ciTypeADTAttributes = res.map((item) => { const attr = await getCITypeAttributesById(this.CITypeId)
const filterAttr = res.filter((name) => {
const currentAttr = attr?.attributes?.find((item) => item?.name === name)
if (!currentAttr) {
return true
}
return this.filterAttributes(name)
})
this.ciTypeADTAttributes = filterAttr.map((item) => {
return { return {
id: item, id: item,
value: item, value: item,
@ -239,7 +251,7 @@ export default {
const peer_type_id = item.peer_type_id const peer_type_id = item.peer_type_id
const attributes = this?.relationOptions?.find((option) => option?.value === peer_type_id)?.attributes const attributes = this?.relationOptions?.find((option) => option?.value === peer_type_id)?.attributes
item.attributes = attributes item.attributes = attributes.filter((attr) => this.filterAttributes(attr))
item.peer_attr_id = undefined item.peer_attr_id = undefined
}) })
}, },
@ -288,6 +300,15 @@ export default {
this.getCITypeRelations() this.getCITypeRelations()
} }
}, },
filterAttributes(attr) {
// filter password/json/is_list/longText/bool/reference
if (attr?.value_type === '2' && !attr?.is_index) {
return false
}
return !attr?.is_password && !attr?.is_list && attr?.value_type !== '6' && !attr?.is_bool && !attr?.is_reference
},
}, },
} }
</script> </script>

View File

@ -598,8 +598,14 @@ export default {
}) })
}, },
filterAttributes(attributes) { filterAttributes(attributes) {
// filter password/json/is_list // filter password/json/is_list/longText/bool/reference
return attributes.filter((attr) => !attr.is_password && !attr.is_list && attr.value_type !== '6') return attributes.filter((attr) => {
if (attr.value_type === '2' && !attr.is_index) {
return false
}
return !attr.is_password && !attr.is_list && attr.value_type !== '6' && !attr.is_bool && !attr.is_reference
})
}, },
addTableAttr() { addTableAttr() {
this.tableAttrList.push({ this.tableAttrList.push({

View File

@ -471,10 +471,15 @@ export default {
this.dateForm = _.cloneDeep(this.defaultDateForm) this.dateForm = _.cloneDeep(this.defaultDateForm)
this.notifies = _.cloneDeep(this.defaultNotify) this.notifies = _.cloneDeep(this.defaultNotify)
this.category = 1 this.category = 1
this.triggerAction = '1'
this.filterExp = '' this.filterExp = ''
this.selectedBot = undefined this.selectedBot = undefined
this.visible = false if (this.$refs.noticeContent) {
this.$refs.noticeContent.destroy()
}
this.$nextTick(() => {
this.visible = false
})
}, },
filterChange(value) { filterChange(value) {
this.filterValue = value this.filterValue = value

View File

@ -364,8 +364,14 @@ export default {
return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0 return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
}, },
filterAttributes(attributes) { filterAttributes(attributes) {
// filter password/json/is_list // filter password/json/is_list/longText/bool/reference
return attributes.filter((attr) => !attr.is_password && !attr.is_list && attr.value_type !== '6') return attributes.filter((attr) => {
if (attr.value_type === '2' && !attr.is_index) {
return false
}
return !attr.is_password && !attr.is_list && attr.value_type !== '6' && !attr.is_bool && !attr.is_reference
})
}, },
addModalAttr() { addModalAttr() {

View File

@ -306,8 +306,14 @@ export default {
return _find?.alias ?? _find?.name ?? id return _find?.alias ?? _find?.name ?? id
}, },
filterAttributes(attributes) { filterAttributes(attributes) {
// filter password/json/is_list // filter password/json/is_list/longText/bool/reference
return attributes.filter((attr) => !attr.is_password && !attr.is_list && attr.value_type !== '6') return attributes.filter((attr) => {
if (attr.value_type === '2' && !attr.is_index) {
return false
}
return !attr.is_password && !attr.is_list && attr.value_type !== '6' && !attr.is_bool && !attr.is_reference
})
}, },
addTableAttr() { addTableAttr() {

View File

@ -53,8 +53,18 @@
:width="col.width" :width="col.width"
:sortable="col.sortable" :sortable="col.sortable"
> >
<template #default="{row}" v-if="col.value_type === '6'"> <template v-if="col.is_reference" #default="{row}">
<span v-if="col.value_type === '6' && row[col.field]">{{ JSON.stringify(row[col.field]) }}</span> <a
v-for="(id) in (col.is_list ? row[col.field] : [row[col.field]])"
:key="id"
:href="`/cmdb/cidetail/${col.reference_type_id}/${id}`"
target="_blank"
>
{{ id }}
</a>
</template>
<template #default="{row}" v-else-if="col.value_type == '6'">
<span v-if="col.value_type == '6' && row[col.field]">{{ JSON.stringify(row[col.field]) }}</span>
</template> </template>
</vxe-table-column> </vxe-table-column>
</vxe-table> </vxe-table>

View File

@ -101,8 +101,18 @@
:minWidth="100" :minWidth="100"
:cell-type="col.value_type === '2' ? 'string' : 'auto'" :cell-type="col.value_type === '2' ? 'string' : 'auto'"
> >
<template v-if="col.value_type === '6' || col.is_link || col.is_password || col.is_choice" #default="{row}"> <template v-if="col.value_type === '6' || col.is_link || col.is_password || col.is_choice || col.is_reference" #default="{row}">
<span v-if="col.value_type === '6' && row[col.field]">{{ JSON.stringify(row[col.field]) }}</span> <template v-if="col.is_reference && row[col.field]" >
<a
v-for="(ciId) in (col.is_list ? row[col.field] : [row[col.field]])"
:key="ciId"
:href="`/cmdb/cidetail/${col.reference_type_id}/${ciId}`"
target="_blank"
>
{{ getReferenceAttrValue(ciId, col) }}
</a>
</template>
<span v-else-if="col.value_type === '6' && row[col.field]">{{ JSON.stringify(row[col.field]) }}</span>
<template v-else-if="col.is_link && row[col.field]"> <template v-else-if="col.is_link && row[col.field]">
<a <a
v-for="(item, linkIndex) in (col.is_list ? row[col.field] : [row[col.field]])" v-for="(item, linkIndex) in (col.is_list ? row[col.field] : [row[col.field]])"
@ -254,6 +264,8 @@ export default {
sortByTable: undefined, sortByTable: undefined,
loading: false, loading: false,
columnsGroup: [], columnsGroup: [],
referenceShowAttrNameMap: {},
referenceCIIdMap: {},
} }
}, },
computed: { computed: {
@ -433,12 +445,99 @@ export default {
await Promise.all(promises1).then(() => { await Promise.all(promises1).then(() => {
this.columnsGroup = [..._commonColumnsGroup, ..._columnsGroup] this.columnsGroup = [..._commonColumnsGroup, ..._columnsGroup]
this.instanceList = res['result'] this.instanceList = res['result']
this.handlePerference()
}) })
}) })
.finally(() => { .finally(() => {
this.loading = false this.loading = false
}) })
}, },
handlePerference() {
let needRequiredCIType = []
this.columnsGroup.forEach((group) => {
group.children.forEach((col) => {
if (col?.is_reference && col?.reference_type_id) {
needRequiredCIType.push(col)
}
})
})
needRequiredCIType = _.uniq(needRequiredCIType)
if (!needRequiredCIType.length) {
this.referenceShowAttrNameMap = {}
this.referenceCIIdMap = {}
return
}
this.handleReferenceShowAttrName(needRequiredCIType)
this.handleReferenceCIIdMap(needRequiredCIType)
},
async handleReferenceShowAttrName(needRequiredCIType) {
const res = await getCITypes({
type_ids: needRequiredCIType.map((col) => col.reference_type_id).join(',')
})
const map = {}
res.ci_types.forEach((ciType) => {
map[ciType.id] = ciType?.show_name || ciType?.unique_name || ''
})
this.referenceShowAttrNameMap = map
},
async handleReferenceCIIdMap(needRequiredCIType) {
const map = {}
this.instanceList.forEach((row) => {
needRequiredCIType.forEach((col) => {
const ids = Array.isArray(row[col.field]) ? row[col.field] : row[col.field] ? [row[col.field]] : []
if (ids.length) {
if (!map?.[col.reference_type_id]) {
map[col.reference_type_id] = {}
}
ids.forEach((id) => {
map[col.reference_type_id][id] = {}
})
}
})
})
if (!Object.keys(map).length) {
this.referenceCIIdMap = {}
return
}
const allRes = await Promise.all(
Object.keys(map).map((key) => {
return searchCI({
q: `_type:${key},_id:(${Object.keys(map[key]).join(';')})`,
count: 9999
})
})
)
allRes.forEach((res) => {
res.result.forEach((item) => {
if (map?.[item._type]?.[item._id]) {
map[item._type][item._id] = item
}
})
})
this.referenceCIIdMap = map
},
getReferenceAttrValue(id, col) {
const ci = this?.referenceCIIdMap?.[col?.reference_type_id]?.[id]
if (!ci) {
return id
}
const attrName = this.referenceShowAttrNameMap?.[col.reference_type_id]
return ci?.[attrName] || id
},
getColumns(data, attrList) { getColumns(data, attrList) {
const width = document.getElementById('resource_search').clientWidth - 50 const width = document.getElementById('resource_search').clientWidth - 50
return getCITableColumns(data, attrList, width).map((item) => { return getCITableColumns(data, attrList, width).map((item) => {

View File

@ -14,7 +14,7 @@ export default {
name: 'TreeViewsNode', name: 'TreeViewsNode',
props: { props: {
title: { title: {
type: [String, Number], type: [String, Number, Boolean],
default: '', default: '',
}, },
treeKey: { treeKey: {