Compare commits

...

20 Commits

Author SHA1 Message Date
pycook
3aac012ee9 chore: release v2.4.10 2024-07-31 16:42:26 +08:00
Leo Song
78d762cacc Merge pull request #588 from veops/dev_ui_240731
feat(ui): update ci type
2024-07-31 16:01:17 +08:00
songlh
c668ba7d3f feat(ui): update ci type 2024-07-31 16:00:40 +08:00
pycook
542a876ead fix(api): delete item for multi-value attributes 2024-07-30 20:05:21 +08:00
pycook
68b7497bba Merge pull request #587 from thexqn/master
修复在用了计算属性的情况下,批量上传功能可能出现的错误.
2024-07-30 09:18:30 +08:00
thexqn
dfbf3d462d 修复在用了计算属性的情况下,批量上传功能可能出现的错误. 2024-07-30 01:17:53 +08:00
pycook
692708fcba feat(api): Multi-valued attribute values ​​support adding and deleting 2024-07-29 19:55:07 +08:00
pycook
330b64edb3 chore: release v2.4.9 2024-07-26 17:03:00 +08:00
Leo Song
63a3074cb7 Merge pull request #585 from veops/fix_ui_240726
fix: discovery card eye btn
2024-07-26 16:50:43 +08:00
songlh
31b8cf49dc fix: discovery card eye btn 2024-07-26 16:49:28 +08:00
Leo Song
b01c335456 Merge pull request #584 from veops/dev_ui_240726
feat(ui): update auto discovery
2024-07-26 10:41:19 +08:00
songlh
002fef09e2 feat(ui): update auto discovery 2024-07-26 10:40:37 +08:00
pycook
175778a162 perf(api): auto discovery (#582) 2024-07-25 17:45:26 +08:00
Leo Song
5050a1bef5 Merge pull request #581 from veops/dev_ui_240722
feat: add accounts config
2024-07-22 17:39:18 +08:00
songlh
46a6cf67d6 feat: add accounts config 2024-07-22 17:38:48 +08:00
Leo Song
4e857c2775 Merge pull request #580 from veops/dev_ui_240716
feat: add history export
2024-07-16 13:46:40 +08:00
songlh
835df1bdeb feat: add history export 2024-07-16 13:45:31 +08:00
ivonGwy
579339d13c change pic 2024-07-15 16:23:34 +08:00
ivonGwy
629967ce82 change pic 2024-07-15 16:22:20 +08:00
pycook
3a00bfd236 chore: update docker compose 2024-07-11 14:29:34 +08:00
73 changed files with 5295 additions and 1822 deletions

View File

@@ -113,4 +113,7 @@ docker compose up -d
_**欢迎关注公众号(维易科技OneOps),关注后可加入微信群,进行产品和技术交流。**_
![公众号: 维易科技OneOps](docs/images/wechat.png)
<p align="center">
<img src="docs/images/wechat.png" alt="公众号: 维易科技OneOps" />
</p>

View File

@@ -229,7 +229,7 @@ class AttributeManager(object):
is_choice = True if choice_value or kwargs.get('choice_web_hook') or kwargs.get('choice_other') else False
name = kwargs.pop("name")
if name in BUILTIN_KEYWORDS:
if name in BUILTIN_KEYWORDS or kwargs.get('alias') in BUILTIN_KEYWORDS:
return abort(400, ErrFormat.attribute_name_cannot_be_builtin)
while kwargs.get('choice_other'):

View File

@@ -18,12 +18,10 @@ from api.lib.cmdb.cache import AutoDiscoveryMappingCache
from api.lib.cmdb.cache import CITypeAttributeCache
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.ci_type import CITypeGroupManager
from api.lib.cmdb.const import AutoDiscoveryType
from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import RelationSourceEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.custom_dashboard import SystemConfigManager
from api.lib.cmdb.resp_format import ErrFormat
@@ -35,6 +33,7 @@ from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import is_app_admin
from api.lib.perm.acl.acl import validate_permission
from api.lib.utils import AESCrypto
from api.models.cmdb import AutoDiscoveryAccount
from api.models.cmdb import AutoDiscoveryCI
from api.models.cmdb import AutoDiscoveryCIType
from api.models.cmdb import AutoDiscoveryCITypeRelation
@@ -42,6 +41,7 @@ from api.models.cmdb import AutoDiscoveryCounter
from api.models.cmdb import AutoDiscoveryExecHistory
from api.models.cmdb import AutoDiscoveryRule
from api.models.cmdb import AutoDiscoveryRuleSyncHistory
from api.tasks.cmdb import build_relations_for_ad_accept
from api.tasks.cmdb import write_ad_rule_sync_history
PWD = os.path.abspath(os.path.dirname(__file__))
@@ -226,14 +226,14 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
adr = AutoDiscoveryRuleCRUD.get_by_id(adt.adr_id)
if not adr:
continue
if adr.type == "http":
if adr.type == AutoDiscoveryType.HTTP:
for i in DEFAULT_INNER:
if adr.name == i['name']:
attrs = AutoDiscoveryHTTPManager.get_attributes(
i['en'], (adt.extra_option or {}).get('category')) or []
result.extend([i.get('name') for i in attrs])
break
elif adr.type == "snmp":
elif adr.type == AutoDiscoveryType.SNMP:
attributes = AutoDiscoverySNMPManager.get_attributes()
result.extend([i.get('name') for i in (attributes or [])])
else:
@@ -243,6 +243,14 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
@classmethod
def get(cls, ci_id, oneagent_id, oneagent_name, last_update_at=None):
"""
OneAgent sync rules
:param ci_id:
:param oneagent_id:
:param oneagent_name:
:param last_update_at:
:return:
"""
result = []
rules = cls.cls.get_by(to_dict=True)
@@ -250,17 +258,14 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
if not rule['enabled']:
continue
if isinstance(rule.get("extra_option"), dict) and rule['extra_option'].get('secret'):
if not (current_user.username in PRIVILEGED_USERS or current_user.uid == rule['uid']):
rule['extra_option'].pop('secret', None)
else:
rule['extra_option']['secret'] = AESCrypto.decrypt(rule['extra_option']['secret'])
if isinstance(rule.get("extra_option"), dict):
decrypt_account(rule['extra_option'], rule['uid'])
if isinstance(rule.get("extra_option"), dict) and rule['extra_option'].get('password'):
if not (current_user.username in PRIVILEGED_USERS or current_user.uid == rule['uid']):
if rule['extra_option'].get('_reference'):
rule['extra_option'].pop('password', None)
else:
rule['extra_option']['password'] = AESCrypto.decrypt(rule['extra_option']['password'])
rule['extra_option'].pop('secret', None)
rule['extra_option'].update(
AutoDiscoveryAccountCRUD().get_config_by_id(rule['extra_option']['_reference']))
if oneagent_id and rule['agent_id'] == oneagent_id:
result.append(rule)
@@ -364,7 +369,7 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
if kwargs.get('adr_id'):
adr = AutoDiscoveryRule.get_by_id(kwargs['adr_id']) or abort(
404, ErrFormat.adr_not_found.format("id={}".format(kwargs['adr_id'])))
if adr.type == "http":
if adr.type == AutoDiscoveryType.HTTP:
kwargs.setdefault('extra_option', dict())
en_name = None
for i in DEFAULT_INNER:
@@ -379,13 +384,16 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
kwargs["extra_option"]["provider"] = en_name
break
if adr.type == AutoDiscoveryType.COMPONENTS and kwargs.get('extra_option'):
for i in DEFAULT_INNER:
if i['name'] == adr.name:
kwargs['extra_option']['collect_key'] = i['option'].get('collect_key')
break
if kwargs.get('is_plugin') and kwargs.get('plugin_script'):
kwargs = check_plugin_script(**kwargs)
if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('secret'):
kwargs['extra_option']['secret'] = AESCrypto.encrypt(kwargs['extra_option']['secret'])
if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('password'):
kwargs['extra_option']['password'] = AESCrypto.encrypt(kwargs['extra_option']['password'])
encrypt_account(kwargs.get('extra_option'))
ci_type = CITypeCache.get(kwargs['type_id'])
unique = AttributeCache.get(ci_type.unique_id)
@@ -403,7 +411,7 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
adr = AutoDiscoveryRule.get_by_id(existed.adr_id) or abort(
404, ErrFormat.adr_not_found.format("id={}".format(existed.adr_id)))
if adr.type == "http":
if adr.type == AutoDiscoveryType.HTTP:
kwargs.setdefault('extra_option', dict())
en_name = None
for i in DEFAULT_INNER:
@@ -418,6 +426,12 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
kwargs["extra_option"]["provider"] = en_name
break
if adr.type == AutoDiscoveryType.COMPONENTS and kwargs.get('extra_option'):
for i in DEFAULT_INNER:
if i['name'] == adr.name:
kwargs['extra_option']['collect_key'] = i['option'].get('collect_key')
break
if 'attributes' in kwargs:
self.__valid_exec_target(kwargs.get('agent_id'), kwargs.get('query_expr'))
@@ -441,13 +455,10 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
if kwargs.get('is_plugin') and kwargs.get('plugin_script'):
kwargs = check_plugin_script(**kwargs)
if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('secret'):
kwargs['extra_option']['secret'] = AESCrypto.encrypt(kwargs['extra_option']['secret'])
if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('password'):
kwargs['extra_option']['password'] = AESCrypto.encrypt(kwargs['extra_option']['password'])
encrypt_account(kwargs.get('extra_option'))
inst = self._can_update(_id=_id, **kwargs)
if len(kwargs) == 1 and 'enabled' in kwargs: # enable or disable
if len(kwargs) == 1 and 'enabled' in kwargs: # enable or disable
pass
elif inst.agent_id != kwargs.get('agent_id') or inst.query_expr != kwargs.get('query_expr'):
for item in AutoDiscoveryRuleSyncHistory.get_by(adt_id=inst.id, to_dict=False):
@@ -688,9 +699,11 @@ class AutoDiscoveryCICRUD(DBMixin):
adt = AutoDiscoveryCITypeCRUD.get_by_id(adc.adt_id) or abort(404, ErrFormat.adt_not_found)
ci_id = None
if adt.attributes:
ci_dict = {adt.attributes[k]: None if not v and isinstance(v, (list, dict)) else v
for k, v in adc.instance.items() if k in adt.attributes}
ad_key2attr = adt.attributes or {}
if ad_key2attr:
ci_dict = {ad_key2attr[k]: None if not v and isinstance(v, (list, dict)) else v
for k, v in adc.instance.items() if k in ad_key2attr}
extra_option = adt.extra_option or {}
mapping, path_mapping = AutoDiscoveryHTTPManager.get_predefined_value_mapping(
extra_option.get('provider'), extra_option.get('category'))
@@ -703,37 +716,7 @@ class AutoDiscoveryCICRUD(DBMixin):
AutoDiscoveryExecHistoryCRUD().add(type_id=adt.type_id,
stdout="accept resource: {}".format(adc.unique_value))
relation_ads = AutoDiscoveryCITypeRelation.get_by(ad_type_id=adt.type_id, to_dict=False)
for r_adt in relation_ads:
ad_key = r_adt.ad_key
if not adc.instance.get(ad_key):
continue
ad_key_values = [adc.instance.get(ad_key)] if not isinstance(
adc.instance.get(ad_key), list) else adc.instance.get(ad_key)
for ad_key_value in ad_key_values:
query = "_type:{},{}:{}".format(r_adt.peer_type_id, r_adt.peer_attr_id, ad_key_value)
s = ci_search(query, use_ci_filter=False, count=1000000)
try:
response, _, _, _, _, _ = s.search()
except SearchError as e:
current_app.logger.warning(e)
return abort(400, str(e))
for relation_ci in response:
relation_ci_id = relation_ci['_id']
try:
CIRelationManager.add(ci_id, relation_ci_id,
valid=False,
source=RelationSourceEnum.AUTO_DISCOVERY)
except:
try:
CIRelationManager.add(relation_ci_id, ci_id,
valid=False,
source=RelationSourceEnum.AUTO_DISCOVERY)
except:
pass
build_relations_for_ad_accept.apply_async(args=(adc.to_dict(), ci_id, ad_key2attr), queue=CMDB_QUEUE)
adc.update(is_accept=True,
accept_by=nickname or current_user.nickname,
@@ -893,3 +876,121 @@ class AutoDiscoveryCounterCRUD(DBMixin):
def _can_delete(self, **kwargs):
pass
def encrypt_account(config):
if isinstance(config, dict):
if config.get('secret'):
config['secret'] = AESCrypto.encrypt(config['secret'])
if config.get('password'):
config['password'] = AESCrypto.encrypt(config['password'])
def decrypt_account(config, uid):
if isinstance(config, dict):
if config.get('password'):
if not (current_user.username in PRIVILEGED_USERS or current_user.uid == uid):
config.pop('password', None)
else:
try:
config['password'] = AESCrypto.decrypt(config['password'])
except Exception as e:
current_app.logger.error('decrypt account failed: {}'.format(e))
if config.get('secret'):
if not (current_user.username in PRIVILEGED_USERS or current_user.uid == uid):
config.pop('secret', None)
else:
try:
config['secret'] = AESCrypto.decrypt(config['secret'])
except Exception as e:
current_app.logger.error('decrypt account failed: {}'.format(e))
class AutoDiscoveryAccountCRUD(DBMixin):
cls = AutoDiscoveryAccount
def get(self, adr_id):
res = self.cls.get_by(adr_id=adr_id, to_dict=True)
for i in res:
decrypt_account(i.get('config'), i['uid'])
return res
def get_config_by_id(self, _id):
res = self.cls.get_by_id(_id)
if not res:
return {}
config = res.to_dict().get('config') or {}
decrypt_account(config, res.uid)
return config
def _can_add(self, **kwargs):
encrypt_account(kwargs.get('config'))
kwargs['uid'] = current_user.uid
return kwargs
def upsert(self, adr_id, accounts):
existed_all = self.cls.get_by(adr_id=adr_id, to_dict=False)
account_names = {i['name'] for i in accounts}
name_changed = dict()
for account in accounts:
existed = None
if account.get('id'):
existed = self.cls.get_by_id(account.get('id'))
if existed is None:
continue
account.pop('id')
name_changed[existed.name] = account.get('name')
else:
account = self._can_add(**account)
if existed is not None:
if current_user.uid == existed.uid:
config = copy.deepcopy(existed.config) or {}
config.update(account.get('config') or {})
account['config'] = config
existed.update(**account)
else:
self.cls.create(adr_id=adr_id, **account)
for item in existed_all:
if name_changed.get(item.name, item.name) not in account_names:
if current_user.uid == item.uid:
item.soft_delete()
def _can_update(self, **kwargs):
existed = self.cls.get_by_id(kwargs['_id']) or abort(404, ErrFormat.not_found)
if isinstance(kwargs.get('config'), dict) and kwargs['config'].get('secret'):
if current_user.uid != existed.uid:
return abort(403, ErrFormat.adt_secret_no_permission)
if isinstance(kwargs.get('config'), dict) and kwargs['config'].get('password'):
if current_user.uid != existed.uid:
return abort(403, ErrFormat.adt_secret_no_permission)
return existed
def update(self, _id, **kwargs):
if kwargs.get('is_plugin') and kwargs.get('plugin_script'):
kwargs = check_plugin_script(**kwargs)
encrypt_account(kwargs.get('config'))
inst = self._can_update(_id=_id, **kwargs)
obj = inst.update(_id=_id, filter_none=False, **kwargs)
return obj
def _can_delete(self, **kwargs):
pass

View File

@@ -19,10 +19,21 @@ DEFAULT_INNER = [
dict(name="KVM", en="kvm", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'ops-KVM'}, "category": "private_cloud", "en": "kvm"}),
dict(name="Nginx", en="nginx", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-nginx'}, "en": "nginx"}),
option={'icon': {'name': 'caise-nginx'}, "en": "nginx", "collect_key": "nginx"}),
dict(name="Apache", en="apache", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-apache'}, "en": "apache", "collect_key": "apache"}),
dict(name="Tomcat", en="tomcat", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-tomcat'}, "en": "tomcat", "collect_key": "tomcat"}),
dict(name="MySQL", en="mysql", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-mySQL'}, "en": "mysql", "collect_key": "mysql"}),
dict(name="MSSQL", en="mssql", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-SQLServer'}, "en": "mssql", "collect_key": "sqlserver"}),
dict(name="Oracle", en="oracle", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-oracle'}, "en": "oracle", "collect_key": "oracle"}),
dict(name="Redis", en="redis", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-redis'}, "en": "redis"}),
option={'icon': {'name': 'caise-redis'}, "en": "redis", "collect_key": "redis"}),
dict(name="交换机", type=AutoDiscoveryType.SNMP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-jiaohuanji'}}),
@@ -294,7 +305,7 @@ CLOUD_MAP = {
"category": "其他",
"items": ["资源池", "数据中心", "文件夹"],
"map": {
"资源池": "templates/vsphere_datastore.json",
"资源池": "templates/vsphere_pool.json",
"数据中心": "templates/vsphere_datacenter.json",
"文件夹": "templates/vsphere_folder.json",
},

View File

@@ -319,8 +319,8 @@ class CIManager(object):
400, ErrFormat.unique_value_not_found.format("unique_id={}".format(ci_type.unique_id)))
unique_value = None
if not (unique_key.default and unique_key.default.get('default') == AttributeDefaultValueEnum.AUTO_INC_ID and
not ci_dict.get(unique_key.name)): # primary key is not auto inc id
# primary key is not auto inc id
if not (unique_key.default and unique_key.default.get('default') == AttributeDefaultValueEnum.AUTO_INC_ID):
unique_value = ci_dict.get(unique_key.name) or ci_dict.get(unique_key.alias) or ci_dict.get(unique_key.id)
unique_value = unique_value or abort(400, ErrFormat.unique_key_required.format(unique_key.name))
@@ -328,7 +328,8 @@ class CIManager(object):
ci_type_attrs_name = {attr.name: attr for _, attr in attrs}
ci_type_attrs_alias = {attr.alias: attr for _, attr in attrs}
ci_attr2type_attr = {type_attr.attr_id: type_attr for type_attr, _ in attrs}
ci_type_attrs_name_alias = {**ci_type_attrs_name, **ci_type_attrs_alias}
ci = None
record_id = None
password_dict = {}
@@ -412,7 +413,7 @@ class CIManager(object):
else:
ci_dict.pop(k)
ci_dict = {k: v for k, v in ci_dict.items() if k in ci_type_attrs_name or k in ci_type_attrs_alias}
ci_dict = {ci_type_attrs_name_alias[k].name: v for k, v in ci_dict.items() if k in ci_type_attrs_name_alias}
key2attr = value_manager.valid_attr_value(ci_dict, ci_type.id, ci and ci.id,
ci_type_attrs_name, ci_type_attrs_alias, ci_attr2type_attr)

View File

@@ -1244,17 +1244,16 @@ class CITypeAttributeGroupManager(object):
if isinstance(_from, int):
from_group = CITypeAttributeGroup.get_by_id(_from)
else:
from_group = CITypeAttributeGroup.get_by(name=_from, first=True, to_dict=False)
from_group = CITypeAttributeGroup.get_by(name=_from, type_id=type_id, first=True, to_dict=False)
from_group or abort(404, ErrFormat.ci_type_attribute_group_not_found.format("id={}".format(_from)))
if isinstance(_to, int):
to_group = CITypeAttributeGroup.get_by_id(_to)
else:
to_group = CITypeAttributeGroup.get_by(name=_to, first=True, to_dict=False)
to_group = CITypeAttributeGroup.get_by(name=_to, type_id=type_id, first=True, to_dict=False)
to_group or abort(404, ErrFormat.ci_type_attribute_group_not_found.format("id={}".format(_to)))
from_order, to_order = from_group.order, to_group.order
from_group.update(order=to_order)
to_group.update(order=from_order)
@@ -1541,6 +1540,9 @@ class CITypeTemplateManager(object):
if ((i.extra_option or {}).get('alias') or None) == (
(rule.get('extra_option') or {}).get('alias') or None):
existed = True
rule.pop('extra_option', None)
rule.pop('enabled', None)
rule.pop('cron', None)
AutoDiscoveryCITypeCRUD().update(i.id, **rule)
break
@@ -1698,6 +1700,9 @@ class CITypeTemplateManager(object):
for r in ad_rules:
r = r.to_dict()
if r.get('extra_option') and '_reference' in r['extra_option']:
r['extra_option'].pop('_reference')
r['type_name'] = type_id2name.get(r.pop('type_id'))
if r.get('adr_id'):
adr = AutoDiscoveryRuleCRUD.get_by_id(r.pop('adr_id'))

View File

@@ -118,7 +118,7 @@ REDIS_PREFIX_CI = "ONE_CMDB"
REDIS_PREFIX_CI_RELATION = "CMDB_CI_RELATION"
REDIS_PREFIX_CI_RELATION2 = "CMDB_CI_RELATION2"
BUILTIN_KEYWORDS = {'id', '_id', 'ci_id', 'type', '_type', 'ci_type'}
BUILTIN_KEYWORDS = {'id', '_id', 'ci_id', 'type', '_type', 'ci_type', 'ticket_id'}
L_TYPE = None
L_CI = None

View File

@@ -35,7 +35,7 @@ class ErrFormat(CommonErrFormat):
"Only creators and administrators are allowed to delete attributes!") # 目前只允许 属性创建人、管理员 删除属性!
# 属性字段名不能是内置字段: id, _id, ci_id, type, _type, ci_type
attribute_name_cannot_be_builtin = _l(
"Attribute field names cannot be built-in fields: id, _id, ci_id, type, _type, ci_type")
"Attribute field names cannot be built-in fields: id, _id, ci_id, type, _type, ci_type, ticket_id")
attribute_choice_other_invalid = _l(
"Predefined value: Other model request parameters are illegal!") # 预定义值: 其他模型请求参数不合法!

View File

@@ -235,10 +235,19 @@ class AttributeValueManager(object):
try:
if attr.is_list:
if isinstance(value, dict):
if value.get('op') == "delete":
value['v'] = [ValueTypeMap.serialize[attr.value_type](
self._deserialize_value(attr.alias, attr.value_type, i))
for i in handle_arg_list(value['v'])]
continue
_value = value.get('v') or []
else:
_value = value
value_list = [self._validate(attr, i, value_table, ci=None, type_id=type_id, ci_id=ci_id,
type_attr=ci_attr2type_attr.get(attr.id))
for i in handle_arg_list(value)]
ci_dict[key] = value_list
for i in handle_arg_list(_value)]
ci_dict[key] = value_list if not isinstance(value, dict) else dict(op=value.get('op'), v=value_list)
if not value_list:
self._check_is_required(type_id, attr, '')
@@ -278,28 +287,47 @@ class AttributeValueManager(object):
existed_values = [(ValueTypeMap.serialize[attr.value_type](i.value) if
i.value or i.value == 0 else i.value) for i in existed_attrs]
# Comparison array starts from which position changes
min_len = min(len(value), len(existed_values))
index = 0
while index < min_len:
if value[index] != existed_values[index]:
break
index += 1
if isinstance(value, dict):
if value.get('op') == "add":
for v in (value.get('v') or []):
if v not in existed_values:
value_table.create(ci_id=ci.id, attr_id=attr.id, value=v, flush=False, commit=False)
if not attr.is_dynamic:
changed.append((ci.id, attr.id, OperateType.ADD, None, v, ci.type_id))
else:
has_dynamic = True
# Delete first and then add to ensure id sorting
for idx in range(index, len(existed_attrs)):
existed_attr = existed_attrs[idx]
existed_attr.delete(flush=False, commit=False)
if not attr.is_dynamic:
changed.append((ci.id, attr.id, OperateType.DELETE, existed_values[idx], None, ci.type_id))
else:
has_dynamic = True
for idx in range(index, len(value)):
value_table.create(ci_id=ci.id, attr_id=attr.id, value=value[idx], flush=False, commit=False)
if not attr.is_dynamic:
changed.append((ci.id, attr.id, OperateType.ADD, None, value[idx], ci.type_id))
else:
has_dynamic = True
elif value.get('op') == "delete":
for v in (value.get('v') or []):
if v in existed_values:
existed_attrs[existed_values.index(v)].delete(flush=False, commit=False)
if not attr.is_dynamic:
changed.append((ci.id, attr.id, OperateType.DELETE, v, None, ci.type_id))
else:
has_dynamic = True
else:
# Comparison array starts from which position changes
min_len = min(len(value), len(existed_values))
index = 0
while index < min_len:
if value[index] != existed_values[index]:
break
index += 1
# Delete first and then add to ensure id sorting
for idx in range(index, len(existed_attrs)):
existed_attr = existed_attrs[idx]
existed_attr.delete(flush=False, commit=False)
if not attr.is_dynamic:
changed.append((ci.id, attr.id, OperateType.DELETE, existed_values[idx], None, ci.type_id))
else:
has_dynamic = True
for idx in range(index, len(value)):
value_table.create(ci_id=ci.id, attr_id=attr.id, value=value[idx], flush=False, commit=False)
if not attr.is_dynamic:
changed.append((ci.id, attr.id, OperateType.ADD, None, value[idx], ci.type_id))
else:
has_dynamic = True
else:
existed_attr = value_table.get_by(attr_id=attr.id, ci_id=ci.id, first=True, to_dict=False)
existed_value = existed_attr and existed_attr.value

View File

@@ -1,21 +1,19 @@
# -*- coding:utf-8 -*-
from flask import abort
from sqlalchemy import func
from api.extensions import db
from api.lib.utils import get_page
from api.lib.utils import get_page_size
__author__ = 'pycook'
class DBMixin(object):
cls = None
@classmethod
def search(cls, page, page_size, fl=None, only_query=False, reverse=False, count_query=False, **kwargs):
def search(cls, page, page_size, fl=None, only_query=False, reverse=False, count_query=False,
last_size=None, **kwargs):
page = get_page(page)
page_size = get_page_size(page_size)
if fl is None:
@@ -47,14 +45,15 @@ class DBMixin(object):
return _query, query
numfound = query.count()
return numfound, [i.to_dict() if fl is None else getattr(i, '_asdict')()
for i in query.offset((page - 1) * page_size).limit(page_size)]
def _must_be_required(self, _id):
existed = self.cls.get_by_id(_id)
existed or abort(404, "Factor [{}] does not exist".format(_id))
return existed
if not last_size:
return numfound, [i.to_dict() if fl is None else getattr(i, '_asdict')()
for i in query.offset((page - 1) * page_size).limit(page_size)]
else:
offset = numfound - last_size
if offset < 0:
offset = 0
return numfound, [i.to_dict() if fl is None else getattr(i, '_asdict')()
for i in query.offset(offset).limit(last_size)]
def _can_add(self, **kwargs):
raise NotImplementedError

View File

@@ -636,6 +636,15 @@ class AutoDiscoveryCounter(Model2):
last_week_count = db.Column(db.Integer, default=0)
class AutoDiscoveryAccount(Model):
__tablename__ = "c_ad_accounts"
uid = db.Column(db.Integer, index=True)
name = db.Column(db.String(64))
adr_id = db.Column(db.Integer, db.ForeignKey('c_ad_rules.id'))
config = db.Column(db.JSON)
class CIFilterPerms(Model):
__tablename__ = "c_ci_filter_perms"

View File

@@ -1,9 +1,8 @@
# -*- coding:utf-8 -*-
import json
import datetime
import json
import redis_lock
from flask import current_app
from flask_login import login_user
@@ -13,21 +12,24 @@ from api.extensions import celery
from api.extensions import db
from api.extensions import es
from api.extensions import rd
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeAttributesCache
from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.cmdb.const import REDIS_PREFIX_CI
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2
from api.lib.cmdb.const import RelationSourceEnum
from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.decorator import flush_db
from api.lib.decorator import reconnect_db
from api.lib.perm.acl.cache import UserCache
from api.lib.utils import handle_arg_list
from api.models.cmdb import AutoDiscoveryCI
from api.models.cmdb import AutoDiscoveryCIType
from api.models.cmdb import AutoDiscoveryCITypeRelation
from api.models.cmdb import CI
from api.models.cmdb import CIRelation
from api.models.cmdb import CITypeAttribute
from api.models.cmdb import AutoDiscoveryCI
from api.models.cmdb import AutoDiscoveryCIType
@celery.task(name="cmdb.ci_cache", queue=CMDB_QUEUE)
@@ -277,3 +279,75 @@ def write_ad_rule_sync_history(rules, oneagent_id, oneagent_name, sync_at):
except Exception as e:
current_app.logger.error("write auto discovery rule sync history failed: {}".format(e))
db.session.rollback()
@celery.task(name="cmdb.build_relations_for_ad_accept", queue=CMDB_QUEUE)
@reconnect_db
def build_relations_for_ad_accept(adc, ci_id, ad_key2attr):
from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search as ci_search
current_app.test_request_context().push()
login_user(UserCache.get('worker'))
relation_ads = AutoDiscoveryCITypeRelation.get_by(ad_type_id=adc['type_id'], to_dict=False)
for r_adt in relation_ads:
ad_key = r_adt.ad_key
if not adc['instance'].get(ad_key):
continue
ad_key_values = [adc['instance'].get(ad_key)] if not isinstance(
adc['instance'].get(ad_key), list) else adc['instance'].get(ad_key)
for ad_key_value in ad_key_values:
query = "_type:{},{}:{}".format(r_adt.peer_type_id, r_adt.peer_attr_id, ad_key_value)
s = ci_search(query, use_ci_filter=False, count=1000000)
try:
response, _, _, _, _, _ = s.search()
except SearchError as e:
current_app.logger.error("build_relations_for_ad_accept failed: {}".format(e))
return
for relation_ci in response:
relation_ci_id = relation_ci['_id']
try:
CIRelationManager.add(ci_id, relation_ci_id,
valid=False,
source=RelationSourceEnum.AUTO_DISCOVERY)
except:
try:
CIRelationManager.add(relation_ci_id, ci_id,
valid=False,
source=RelationSourceEnum.AUTO_DISCOVERY)
except:
pass
# build relations in reverse
relation_ads = AutoDiscoveryCITypeRelation.get_by(peer_type_id=adc['type_id'], to_dict=False)
attr2ad_key = {v: k for k, v in ad_key2attr.items()}
for r_adt in relation_ads:
attr = AttributeCache.get(r_adt.peer_attr_id)
ad_key = attr2ad_key.get(attr and attr.name)
if not ad_key:
continue
ad_value = adc['instance'].get(ad_key)
peer_ad_key = r_adt.ad_key
peer_instances = AutoDiscoveryCI.get_by(type_id=r_adt.ad_type_id, to_dict=False)
for peer_instance in peer_instances:
peer_ad_values = peer_instance.instance.get(peer_ad_key)
peer_ad_values = [peer_ad_values] if not isinstance(peer_ad_values, list) else peer_ad_values
if ad_value in peer_ad_values and peer_instance.ci_id:
try:
CIRelationManager.add(peer_instance.ci_id, ci_id,
valid=False,
source=RelationSourceEnum.AUTO_DISCOVERY)
except:
try:
CIRelationManager.add(ci_id, peer_instance.ci_id,
valid=False,
source=RelationSourceEnum.AUTO_DISCOVERY)
except:
pass

View File

@@ -8,6 +8,7 @@ from flask import request
from flask_login import current_user
from io import BytesIO
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryAccountCRUD
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCICRUD
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCITypeCRUD
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCITypeRelationCRUD
@@ -20,6 +21,7 @@ from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryRuleSyncHist
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoverySNMPManager
from api.lib.cmdb.auto_discovery.const import DEFAULT_INNER
from api.lib.cmdb.auto_discovery.const import PRIVILEGED_USERS
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.resp_format import ErrFormat
@@ -272,14 +274,16 @@ class AutoDiscoveryRuleSyncView(APIView):
oneagent_id = request.values.get('oneagent_id')
last_update_at = request.values.get('last_update_at')
query = "oneagent_id:{}".format(oneagent_id)
s = ci_search(query)
try:
response, _, _, _, _, _ = s.search()
except SearchError as e:
import traceback
current_app.logger.error(traceback.format_exc())
return abort(400, str(e))
response = []
if AttributeCache.get('oneagent_id'):
query = "oneagent_id:{}".format(oneagent_id)
s = ci_search(query)
try:
response, _, _, _, _, _ = s.search()
except SearchError as e:
import traceback
current_app.logger.error(traceback.format_exc())
return abort(400, str(e))
for res in response:
if res.get('{}_name'.format(res['ci_type'])) == oneagent_name or oneagent_name == res.get('oneagent_name'):
@@ -328,8 +332,12 @@ class AutoDiscoveryExecHistoryView(APIView):
def get(self):
page = get_page(request.values.pop('page', 1))
page_size = get_page_size(request.values.pop('page_size', None))
last_size = request.values.pop('last_size', None)
if last_size and last_size.isdigit():
last_size = int(last_size)
numfound, res = AutoDiscoveryExecHistoryCRUD.search(page=page,
page_size=page_size,
last_size=last_size,
**request.values)
return self.jsonify(page=page,
@@ -355,3 +363,31 @@ class AutoDiscoveryCounterView(APIView):
type_id = request.values.get('type_id')
return self.jsonify(AutoDiscoveryCounterCRUD().get(type_id))
class AutoDiscoveryAccountView(APIView):
url_prefix = ("/adr/accounts", "/adr/accounts/<int:account_id>")
@args_required('adr_id')
def get(self):
adr_id = request.values.get('adr_id')
return self.jsonify(AutoDiscoveryAccountCRUD().get(adr_id))
@args_required('adr_id')
@args_required('accounts', value_required=False)
def post(self):
AutoDiscoveryAccountCRUD().upsert(**request.values)
return self.jsonify(code=200)
@args_required('config')
def put(self, account_id):
res = AutoDiscoveryAccountCRUD().update(account_id, **request.values)
return self.jsonify(res.to_dict())
def delete(self, account_id):
AutoDiscoveryAccountCRUD().delete(account_id)
return self.jsonify(account_id=account_id)

View File

@@ -3,3 +3,4 @@ VUE_APP_PREVIEW=false
VUE_APP_API_BASE_URL=http://127.0.0.1:5000/api
VUE_APP_BUILD_PACKAGES="ticket,calendar,acl"
VUE_APP_IS_OUTER=true
VUE_APP_IS_OPEN_SOURCE=true

View File

@@ -54,6 +54,84 @@
<div class="content unicode" style="display: block;">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont">&#xe96f;</span>
<div class="name">caise-数据中心</div>
<div class="code-name">&amp;#xe96f;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe970;</span>
<div class="name">caise-文件夹</div>
<div class="code-name">&amp;#xe970;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe971;</span>
<div class="name">caise-资源池</div>
<div class="code-name">&amp;#xe971;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe972;</span>
<div class="name">caise-网络</div>
<div class="code-name">&amp;#xe972;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe973;</span>
<div class="name">caise-分布式交换机</div>
<div class="code-name">&amp;#xe973;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe974;</span>
<div class="name">caise-标准式交换机</div>
<div class="code-name">&amp;#xe974;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe975;</span>
<div class="name">caise-主机集群</div>
<div class="code-name">&amp;#xe975;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe976;</span>
<div class="name">caise-数据存储集群</div>
<div class="code-name">&amp;#xe976;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe977;</span>
<div class="name">caise-数据存储</div>
<div class="code-name">&amp;#xe977;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe96e;</span>
<div class="name">veops-account</div>
<div class="code-name">&amp;#xe96e;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe96d;</span>
<div class="name">veops-collect</div>
<div class="code-name">&amp;#xe96d;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe96c;</span>
<div class="name">veops-collected</div>
<div class="code-name">&amp;#xe96c;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe96b;</span>
<div class="name">veops-text</div>
<div class="code-name">&amp;#xe96b;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe96a;</span>
<div class="name">veops-markdown</div>
@@ -5244,9 +5322,9 @@
<pre><code class="language-css"
>@font-face {
font-family: 'iconfont';
src: url('iconfont.woff2?t=1719487341033') format('woff2'),
url('iconfont.woff?t=1719487341033') format('woff'),
url('iconfont.ttf?t=1719487341033') format('truetype');
src: url('iconfont.woff2?t=1721959219377') format('woff2'),
url('iconfont.woff?t=1721959219377') format('woff'),
url('iconfont.ttf?t=1721959219377') format('truetype');
}
</code></pre>
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@@ -5272,6 +5350,123 @@
<div class="content font-class">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont caise-data_center"></span>
<div class="name">
caise-数据中心
</div>
<div class="code-name">.caise-data_center
</div>
</li>
<li class="dib">
<span class="icon iconfont caise-folder"></span>
<div class="name">
caise-文件夹
</div>
<div class="code-name">.caise-folder
</div>
</li>
<li class="dib">
<span class="icon iconfont caise-resource_pool"></span>
<div class="name">
caise-资源池
</div>
<div class="code-name">.caise-resource_pool
</div>
</li>
<li class="dib">
<span class="icon iconfont caise-network"></span>
<div class="name">
caise-网络
</div>
<div class="code-name">.caise-network
</div>
</li>
<li class="dib">
<span class="icon iconfont caise-distributed_switch"></span>
<div class="name">
caise-分布式交换机
</div>
<div class="code-name">.caise-distributed_switch
</div>
</li>
<li class="dib">
<span class="icon iconfont caise-standard_switch"></span>
<div class="name">
caise-标准式交换机
</div>
<div class="code-name">.caise-standard_switch
</div>
</li>
<li class="dib">
<span class="icon iconfont caise-host_cluster"></span>
<div class="name">
caise-主机集群
</div>
<div class="code-name">.caise-host_cluster
</div>
</li>
<li class="dib">
<span class="icon iconfont caise-storage_cluster"></span>
<div class="name">
caise-数据存储集群
</div>
<div class="code-name">.caise-storage_cluster
</div>
</li>
<li class="dib">
<span class="icon iconfont caise-data_storage"></span>
<div class="name">
caise-数据存储
</div>
<div class="code-name">.caise-data_storage
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-account"></span>
<div class="name">
veops-account
</div>
<div class="code-name">.veops-account
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-collect"></span>
<div class="name">
veops-collect
</div>
<div class="code-name">.veops-collect
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-collected"></span>
<div class="name">
veops-collected
</div>
<div class="code-name">.veops-collected
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-text"></span>
<div class="name">
veops-text
</div>
<div class="code-name">.veops-text
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-markdown"></span>
<div class="name">
@@ -13057,6 +13252,110 @@
<div class="content symbol">
<ul class="icon_lists dib-box">
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#caise-data_center"></use>
</svg>
<div class="name">caise-数据中心</div>
<div class="code-name">#caise-data_center</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#caise-folder"></use>
</svg>
<div class="name">caise-文件夹</div>
<div class="code-name">#caise-folder</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#caise-resource_pool"></use>
</svg>
<div class="name">caise-资源池</div>
<div class="code-name">#caise-resource_pool</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#caise-network"></use>
</svg>
<div class="name">caise-网络</div>
<div class="code-name">#caise-network</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#caise-distributed_switch"></use>
</svg>
<div class="name">caise-分布式交换机</div>
<div class="code-name">#caise-distributed_switch</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#caise-standard_switch"></use>
</svg>
<div class="name">caise-标准式交换机</div>
<div class="code-name">#caise-standard_switch</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#caise-host_cluster"></use>
</svg>
<div class="name">caise-主机集群</div>
<div class="code-name">#caise-host_cluster</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#caise-storage_cluster"></use>
</svg>
<div class="name">caise-数据存储集群</div>
<div class="code-name">#caise-storage_cluster</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#caise-data_storage"></use>
</svg>
<div class="name">caise-数据存储</div>
<div class="code-name">#caise-data_storage</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-account"></use>
</svg>
<div class="name">veops-account</div>
<div class="code-name">#veops-account</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-collect"></use>
</svg>
<div class="name">veops-collect</div>
<div class="code-name">#veops-collect</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-collected"></use>
</svg>
<div class="name">veops-collected</div>
<div class="code-name">#veops-collected</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-text"></use>
</svg>
<div class="name">veops-text</div>
<div class="code-name">#veops-text</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-markdown"></use>

View File

@@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 3857903 */
src: url('iconfont.woff2?t=1719487341033') format('woff2'),
url('iconfont.woff?t=1719487341033') format('woff'),
url('iconfont.ttf?t=1719487341033') format('truetype');
src: url('iconfont.woff2?t=1721959219377') format('woff2'),
url('iconfont.woff?t=1721959219377') format('woff'),
url('iconfont.ttf?t=1721959219377') format('truetype');
}
.iconfont {
@@ -13,6 +13,58 @@
-moz-osx-font-smoothing: grayscale;
}
.caise-data_center:before {
content: "\e96f";
}
.caise-folder:before {
content: "\e970";
}
.caise-resource_pool:before {
content: "\e971";
}
.caise-network:before {
content: "\e972";
}
.caise-distributed_switch:before {
content: "\e973";
}
.caise-standard_switch:before {
content: "\e974";
}
.caise-host_cluster:before {
content: "\e975";
}
.caise-storage_cluster:before {
content: "\e976";
}
.caise-data_storage:before {
content: "\e977";
}
.veops-account:before {
content: "\e96e";
}
.veops-collect:before {
content: "\e96d";
}
.veops-collected:before {
content: "\e96c";
}
.veops-text:before {
content: "\e96b";
}
.veops-markdown:before {
content: "\e96a";
}

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,97 @@
"css_prefix_text": "",
"description": "",
"glyphs": [
{
"icon_id": "41143117",
"name": "caise-数据中心",
"font_class": "caise-data_center",
"unicode": "e96f",
"unicode_decimal": 59759
},
{
"icon_id": "41143118",
"name": "caise-文件夹",
"font_class": "caise-folder",
"unicode": "e970",
"unicode_decimal": 59760
},
{
"icon_id": "41143119",
"name": "caise-资源池",
"font_class": "caise-resource_pool",
"unicode": "e971",
"unicode_decimal": 59761
},
{
"icon_id": "41143120",
"name": "caise-网络",
"font_class": "caise-network",
"unicode": "e972",
"unicode_decimal": 59762
},
{
"icon_id": "41143121",
"name": "caise-分布式交换机",
"font_class": "caise-distributed_switch",
"unicode": "e973",
"unicode_decimal": 59763
},
{
"icon_id": "41143122",
"name": "caise-标准式交换机",
"font_class": "caise-standard_switch",
"unicode": "e974",
"unicode_decimal": 59764
},
{
"icon_id": "41143128",
"name": "caise-主机集群",
"font_class": "caise-host_cluster",
"unicode": "e975",
"unicode_decimal": 59765
},
{
"icon_id": "41143129",
"name": "caise-数据存储集群",
"font_class": "caise-storage_cluster",
"unicode": "e976",
"unicode_decimal": 59766
},
{
"icon_id": "41143143",
"name": "caise-数据存储",
"font_class": "caise-data_storage",
"unicode": "e977",
"unicode_decimal": 59767
},
{
"icon_id": "41141857",
"name": "veops-account",
"font_class": "veops-account",
"unicode": "e96e",
"unicode_decimal": 59758
},
{
"icon_id": "41128804",
"name": "veops-collect",
"font_class": "veops-collect",
"unicode": "e96d",
"unicode_decimal": 59757
},
{
"icon_id": "41128781",
"name": "veops-collected",
"font_class": "veops-collected",
"unicode": "e96c",
"unicode_decimal": 59756
},
{
"icon_id": "41106846",
"name": "veops-text",
"font_class": "veops-text",
"unicode": "e96b",
"unicode_decimal": 59755
},
{
"icon_id": "40896913",
"name": "veops-markdown",

Binary file not shown.

View File

@@ -905,6 +905,33 @@ export const multicolorIconList = [
value: 'caise-application',
label: '应用',
list: [{
value: 'caise-data_center',
label: '数据中心'
}, {
value: 'caise-folder',
label: '文件夹'
}, {
value: 'caise-resource_pool',
label: '资源池'
}, {
value: 'caise-network',
label: '网络'
}, {
value: 'caise-distributed_switch',
label: '分布式交换机'
}, {
value: 'caise-standard_switch',
label: '标准式交换机'
}, {
value: 'caise-host_cluster',
label: '主机集群'
}, {
value: 'caise-storage_cluster',
label: '数据存储集群'
}, {
value: 'caise-data_storage',
label: '数据存储'
}, {
value: 'caise-yilianjie',
label: '已连接'
}, {

View File

@@ -69,6 +69,8 @@ Vue.prototype.$httpError = function (err, describe) {
window.$message = Vue.prototype.$message
Vue.prototype.isOpenSource = process.env.VUE_APP_IS_OPEN_SOURCE === 'true'
Vue.use(Antd)
Vue.use(Viser)

View File

@@ -1,232 +1,257 @@
import { axios } from '@/utils/request'
/**
* 获取 所有的 ci_types
* @param parameter
* @returns {AxiosPromise}
*/
export function getCITypes(parameter) {
return axios({
url: '/v0.1/ci_types',
method: 'GET',
params: parameter
})
}
/**
* 获取 某个 ci_types
* @param CITypeName
* @param parameter
* @returns {AxiosPromise}
*/
export function getCIType(CITypeName, parameter) {
return axios({
url: `/v0.1/ci_types/${CITypeName}`,
method: 'GET',
params: parameter
})
}
/**
* 创建 ci_type
* @param data
* @returns {AxiosPromise}
*/
export function createCIType(data) {
return axios({
url: '/v0.1/ci_types',
method: 'POST',
data: data
})
}
/**
* 更新 ci_type
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function updateCIType(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}`,
method: 'PUT',
data: data
})
}
/**
* 删除 ci_type
* @param CITypeId
* @returns {AxiosPromise}
*/
export function deleteCIType(CITypeId) {
return axios({
url: `/v0.1/ci_types/${CITypeId}`,
method: 'DELETE'
})
}
/**
* 获取 某个 ci_type 的分组
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function getCITypeGroupById(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attribute_groups`,
method: 'GET',
params: data
})
}
/**
* 保存 某个 ci_type 的分组
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function createCITypeGroupById(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attribute_groups`,
method: 'POST',
data: data
})
}
/**
* 修改 某个 ci_type 的分组
* @param groupId
* @param data
* @returns {AxiosPromise}
*/
export function updateCITypeGroupById(groupId, data) {
return axios({
url: `/v0.1/ci_types/attribute_groups/${groupId}`,
method: 'PUT',
data: data
})
}
/**
* 删除 某个 ci_type 的分组
* @param groupId
* @param data
* @returns {AxiosPromise}
*/
export function deleteCITypeGroupById(groupId, data) {
return axios({
url: `/v0.1/ci_types/attribute_groups/${groupId}`,
method: 'delete',
data: data
})
}
export function getUniqueConstraintList(type_id) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint`,
method: 'get',
})
}
export function addUniqueConstraint(type_id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint`,
method: 'post',
data: data
})
}
export function updateUniqueConstraint(type_id, id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint/${id}`,
method: 'put',
data: data
})
}
export function deleteUniqueConstraint(type_id, id) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint/${id}`,
method: 'delete',
})
}
export function getTriggerList(type_id) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers`,
method: 'get',
})
}
export function addTrigger(type_id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers`,
method: 'post',
data: data
})
}
export function updateTrigger(type_id, id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers/${id}`,
method: 'put',
data: data
})
}
export function deleteTrigger(type_id, id) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers/${id}`,
method: 'delete',
})
}
// CMDB的模型和实例的授权接口
export function grantCiType(type_id, rid, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/roles/${rid}/grant`,
method: 'post',
data
})
}
// CMDB的模型和实例的删除授权接口
export function revokeCiType(type_id, rid, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/roles/${rid}/revoke`,
method: 'post',
data
})
}
// CMDB的模型和实例的过滤的权限
export function ciTypeFilterPermissions(type_id) {
return axios({
url: `/v0.1/ci_types/${type_id}/filters/permissions`,
method: 'get',
})
}
// parent_ids, child_id
export function postCiTypeInheritance(data) {
return axios({
url: `/v0.1/ci_types/inheritance`,
method: 'post',
data
})
}
// parent_id, child_id
export function deleteCiTypeInheritance(data) {
return axios({
url: `/v0.1/ci_types/inheritance`,
method: 'delete',
data
})
}
export function getCITypeIcons() {
return axios({
url: '/v0.1/ci_types/icons',
method: 'GET',
})
}
import { axios } from '@/utils/request'
/**
* 获取 所有的 ci_types
* @param parameter
* @returns {AxiosPromise}
*/
export function getCITypes(parameter) {
return axios({
url: '/v0.1/ci_types',
method: 'GET',
params: parameter
})
}
/**
* 获取 某个 ci_types
* @param CITypeName
* @param parameter
* @returns {AxiosPromise}
*/
export function getCIType(CITypeName, parameter) {
return axios({
url: `/v0.1/ci_types/${CITypeName}`,
method: 'GET',
params: parameter
})
}
/**
* 创建 ci_type
* @param data
* @returns {AxiosPromise}
*/
export function createCIType(data) {
return axios({
url: '/v0.1/ci_types',
method: 'POST',
data: data
})
}
/**
* 更新 ci_type
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function updateCIType(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}`,
method: 'PUT',
data: data
})
}
/**
* 删除 ci_type
* @param CITypeId
* @returns {AxiosPromise}
*/
export function deleteCIType(CITypeId) {
return axios({
url: `/v0.1/ci_types/${CITypeId}`,
method: 'DELETE'
})
}
/**
* 获取 某个 ci_type 的分组
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function getCITypeGroupById(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attribute_groups`,
method: 'GET',
params: data
})
}
/**
* 保存 某个 ci_type 的分组
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function createCITypeGroupById(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attribute_groups`,
method: 'POST',
data: data
})
}
/**
* 修改 某个 ci_type 的分组
* @param groupId
* @param data
* @returns {AxiosPromise}
*/
export function updateCITypeGroupById(groupId, data) {
return axios({
url: `/v0.1/ci_types/attribute_groups/${groupId}`,
method: 'PUT',
data: data
})
}
/**
* 删除 某个 ci_type 的分组
* @param groupId
* @param data
* @returns {AxiosPromise}
*/
export function deleteCITypeGroupById(groupId, data) {
return axios({
url: `/v0.1/ci_types/attribute_groups/${groupId}`,
method: 'delete',
data: data
})
}
/**
* 获取级联属性配置
* @param {*} typeId
* @returns
*/
export function getCITypeCascadeAttributes(typeId) {
return axios({
url: `/v0.1/cascade_attributes/ci_types/${typeId}`,
method: 'get'
})
}
/**
* 获取级联属性数据
* @param {*} typeId
* @returns
*/
export function postCITypeCascadeAttributesValues(attrId, data) {
return axios({
url: `/v0.1/cascade_attributes/${attrId}/values`,
method: 'post',
data
})
}
export function getUniqueConstraintList(type_id) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint`,
method: 'get',
})
}
export function addUniqueConstraint(type_id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint`,
method: 'post',
data: data
})
}
export function updateUniqueConstraint(type_id, id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint/${id}`,
method: 'put',
data: data
})
}
export function deleteUniqueConstraint(type_id, id) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint/${id}`,
method: 'delete',
})
}
export function getTriggerList(type_id) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers`,
method: 'get',
})
}
export function addTrigger(type_id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers`,
method: 'post',
data: data
})
}
export function updateTrigger(type_id, id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers/${id}`,
method: 'put',
data: data
})
}
export function deleteTrigger(type_id, id) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers/${id}`,
method: 'delete',
})
}
// CMDB的模型和实例的授权接口
export function grantCiType(type_id, rid, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/roles/${rid}/grant`,
method: 'post',
data
})
}
// CMDB的模型和实例的删除授权接口
export function revokeCiType(type_id, rid, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/roles/${rid}/revoke`,
method: 'post',
data
})
}
// CMDB的模型和实例的过滤的权限
export function ciTypeFilterPermissions(type_id) {
return axios({
url: `/v0.1/ci_types/${type_id}/filters/permissions`,
method: 'get',
})
}
// parent_ids, child_id
export function postCiTypeInheritance(data) {
return axios({
url: `/v0.1/ci_types/inheritance`,
method: 'post',
data
})
}
// parent_id, child_id
export function deleteCiTypeInheritance(data) {
return axios({
url: `/v0.1/ci_types/inheritance`,
method: 'delete',
data
})
}
export function getCITypeIcons() {
return axios({
url: '/v0.1/ci_types/icons',
method: 'GET',
})
}

View File

@@ -62,6 +62,22 @@ export function getHttpAttrMapping(name, resource) {
})
}
export function getHTTPAccounts(params) {
return axios({
url: `/v0.1/adr/accounts`,
method: 'GET',
params
})
}
export function postHTTPAccounts(data) {
return axios({
url: `/v0.1/adr/accounts`,
method: 'POST',
data
})
}
export function getCITypeDiscovery(type_id) {
return axios({
url: `/v0.1/adt/ci_types/${type_id}`,

View File

@@ -1,89 +1,92 @@
import { axios } from '@/utils/request'
export function getCIHistory(ciId) {
return axios({
url: `/v0.1/history/ci/${ciId}`,
method: 'GET'
})
}
export function getCIHistoryTable(params) {
return axios({
url: `/v0.1/history/records/attribute`,
method: 'GET',
params: params
})
}
export function getRelationTable(params) {
return axios({
url: `/v0.1/history/records/relation`,
method: 'GET',
params: params
})
}
export function getCITypesTable(params) {
return axios({
url: `/v0.1/history/ci_types`,
method: 'GET',
params: params
})
}
export function getUsers(params) {
return axios({
url: `/v1/acl/users/employee`,
method: 'GET',
params: params
})
}
export function getCiTriggers(params) {
return axios({
url: `/v0.1/history/ci_triggers`,
method: 'GET',
params: params
})
}
export function getCiTriggersByCiId(ci_id, params) {
return axios({
url: `/v0.1/history/ci_triggers/${ci_id}`,
method: 'GET',
params
})
}
export function getCiRelatedTickets(params) {
return axios({
url: `/itsm/v1/process_ticket/get_tickets_by`,
method: 'POST',
data: params,
isShowMessage: false
})
}
export function judgeItsmInstalled() {
return axios({
url: `/itsm/v1/process_ticket/itsm_existed`,
method: 'GET',
isShowMessage: false
})
}
export function getCIsBaseline(params) {
return axios({
url: `/v0.1/ci/baseline`,
method: 'GET',
params
})
}
export function CIBaselineRollback(ciId, params) {
return axios({
url: `/v0.1/ci/${ciId}/baseline/rollback`,
method: 'POST',
data: params
})
}
import { axios } from '@/utils/request'
export function getCIHistory(ciId) {
return axios({
url: `/v0.1/history/ci/${ciId}`,
method: 'GET'
})
}
export function getCIHistoryTable(params) {
return axios({
url: `/v0.1/history/records/attribute`,
method: 'GET',
params: params,
timeout: 30 * 1000
})
}
export function getRelationTable(params) {
return axios({
url: `/v0.1/history/records/relation`,
method: 'GET',
params: params,
timeout: 30 * 1000
})
}
export function getCITypesTable(params) {
return axios({
url: `/v0.1/history/ci_types`,
method: 'GET',
params: params,
timeout: 30 * 1000
})
}
export function getUsers(params) {
return axios({
url: `/v1/acl/users/employee`,
method: 'GET',
params: params
})
}
export function getCiTriggers(params) {
return axios({
url: `/v0.1/history/ci_triggers`,
method: 'GET',
params: params
})
}
export function getCiTriggersByCiId(ci_id, params) {
return axios({
url: `/v0.1/history/ci_triggers/${ci_id}`,
method: 'GET',
params
})
}
export function getCiRelatedTickets(params) {
return axios({
url: `/itsm/v1/process_ticket/get_tickets_by`,
method: 'POST',
data: params,
isShowMessage: false
})
}
export function judgeItsmInstalled() {
return axios({
url: `/itsm/v1/process_ticket/itsm_existed`,
method: 'GET',
isShowMessage: false
})
}
export function getCIsBaseline(params) {
return axios({
url: `/v0.1/ci/baseline`,
method: 'GET',
params
})
}
export function CIBaselineRollback(ciId, params) {
return axios({
url: `/v0.1/ci/${ciId}/baseline/rollback`,
method: 'POST',
data: params
})
}

View File

@@ -21,15 +21,15 @@
>
*
</span>
<vxe-select
filterable
clearable
<a-select
v-model="row.attr"
type="text"
:options="ciTypeAttributes"
transfer
:placeholder="$t('cmdb.ciType.attrMapTableAttrPlaceholder')"
></vxe-select>
showSearch
allowClear
:options="ciTypeAttributes"
style="width: 100%; height: 28px; line-height: 28px;"
class="attr-map-table-left-select"
></a-select>
</div>
</template>
</vxe-column>
@@ -49,7 +49,7 @@
>
<vxe-column field="name" :title="$t('name')"></vxe-column>
<vxe-column field="type" :title="$t('type')"></vxe-column>
<vxe-column v-if="ruleType !== 'agent'" field="example" :title="$t('cmdb.components.example')">
<vxe-column v-if="ruleType !== DISCOVERY_CATEGORY_TYPE.AGENT" field="example" :title="$t('cmdb.components.example')">
<template #default="{row}">
<span v-if="row.type === 'json'">{{ JSON.stringify(row.example) }}</span>
<span v-else>{{ row.example }}</span>
@@ -72,6 +72,8 @@
</template>
<script>
import { DISCOVERY_CATEGORY_TYPE } from '@/modules/cmdb/views/discovery/constants.js'
export default {
name: 'AttrMapTable',
props: {
@@ -93,7 +95,9 @@ export default {
}
},
data() {
return {}
return {
DISCOVERY_CATEGORY_TYPE
}
},
methods: {
getTableData() {
@@ -123,6 +127,18 @@ export default {
&-left {
width: 30%;
&-select {
/deep/ .ant-select-selection {
height: 28px;
line-height: 28px;
.ant-select-selection__rendered {
height: 28px;
line-height: 28px;
}
}
}
}
&-right {

View File

@@ -29,7 +29,7 @@
class="category-side-children-item-corporate"
v-if="ruleType === 'private_cloud' || (ruleType === 'http' && (categoryIndex !== 0 || itemIndex !== 0))"
>
{{ $t('cmdb.enterpriseVersionFlag') }}
</span>
</div>
</div>
@@ -67,7 +67,7 @@
class="corporate-flag"
v-if="ruleType === 'private_cloud' || (ruleType === 'http' && (categoryIndex !== 0 || itemIndex !== 0))"
>
<span class="corporate-flag-text"></span>
<span class="corporate-flag-text">{{ $t('cmdb.enterpriseVersionFlag') }}</span>
</div>
</div>
</div>

View File

@@ -31,6 +31,7 @@
<script>
import _ from 'lodash'
import { getHttpCategories, getHttpAttributes, getSnmpAttributes, getHttpAttrMapping } from '../../api/discovery'
import { DISCOVERY_CATEGORY_TYPE } from '@/modules/cmdb/views/discovery/constants.js'
import AttrMapTable from '@/modules/cmdb/components/attrMapTable/index.vue'
import ADPreviewTable from './adPreviewTable.vue'
import HttpADCategory from './httpADCategory.vue'
@@ -101,7 +102,7 @@ export default {
}
},
isCloud() {
return ['http', 'private_cloud'].includes(this.ruleType)
return [DISCOVERY_CATEGORY_TYPE.HTTP, DISCOVERY_CATEGORY_TYPE.PRIVATE_CLOUD].includes(this.ruleType)
}
},
watch: {
@@ -119,7 +120,7 @@ export default {
this.currentCate = ''
this.$nextTick(() => {
const { ruleType, ruleName } = newVal
if (['snmp', 'components'].includes(ruleType) && ruleName) {
if ([DISCOVERY_CATEGORY_TYPE.SNMP, DISCOVERY_CATEGORY_TYPE.COMPONENT].includes(ruleType) && ruleName) {
getSnmpAttributes(ruleType, ruleName).then((res) => {
if (this.isEdit) {
this.formatTableData(res)

View File

@@ -1,53 +1,27 @@
<template>
<div class="node-setting-wrap">
<a-row v-for="(node) in nodes" :key="node.id">
<a-col :span="6">
<a-form-item :label="$t('cmdb.ciType.nodeSettingIp')">
<a-input
allowClear
v-decorator="[
`node_ip_${node.id}`,
{
rules: [
{ required: false, message: $t('cmdb.ciType.nodeSettingIpTip') },
{
pattern:
'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$',
message: $t('cmdb.ciType.nodeSettingIpTip1'),
trigger: 'blur',
},
],
},
]"
:placeholder="$t('cmdb.ciType.nodeSettingIpTip')"
/>
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item :label="$t('cmdb.ciType.nodeSettingCommunity')">
<a-input
allowClear
v-decorator="[
`node_community_${node.id}`,
{
rules: [{ required: false, message: $t('cmdb.ciType.nodeSettingCommunityTip') }],
},
]"
:placeholder="$t('cmdb.ciType.nodeSettingCommunityTip')"
/>
</a-form-item>
</a-col>
<a-col :span="5">
<a-form-item :label="$t('cmdb.ciType.nodeSettingVersion')">
<ops-table
:data="nodes"
size="mini"
show-header-overflow
:row-config="{ height: 42 }"
border
:min-height="78"
>
<vxe-column width="170" :title="$t('cmdb.ciType.nodeSettingIp')">
<template #default="{ row }">
<a-input v-model="row.ip"></a-input>
</template>
</vxe-column>
<vxe-column width="170" :title="$t('cmdb.ciType.nodeSettingCommunity')">
<template #default="{ row }">
<a-input v-model="row.community"></a-input>
</template>
</vxe-column>
<vxe-column width="170" :title="$t('cmdb.ciType.nodeSettingVersion')">
<template #default="{ row }">
<a-select
v-decorator="[
`node_version_${node.id}`,
{
rules: [{ required: false, message: $t('cmdb.ciType.nodeSettingVersionTip') }],
},
]"
v-model="row.version"
:placeholder="$t('cmdb.ciType.nodeSettingVersionTip')"
allowClear
class="node-setting-select"
@@ -58,26 +32,25 @@
<a-select-option value="2c">
v2c
</a-select-option>
<a-select-option value="3">
v3
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="3">
<div class="action">
<a @click="() => copyNode(node.id)">
<a-icon type="copy" />
</a>
<a @click="() => removeNode(node.id, 1)">
<a-icon type="minus-circle" />
</a>
<a @click="addNode">
<a-icon type="plus-circle" />
</a>
</div>
</a-col>
</a-row>
</template>
</vxe-column>
<vxe-column wdith="170">
<template #default="{ row }">
<div class="action">
<a @click="() => copyNode(row.id)">
<a-icon type="copy" />
</a>
<a @click="() => removeNode(row.id, 1)">
<a-icon type="minus-circle" />
</a>
<a @click="addNode">
<a-icon type="plus-circle" />
</a>
</div>
</template>
</vxe-column>
</ops-table>
</div>
</template>
@@ -110,17 +83,10 @@ export default {
const newNode = {
id: uuidv4(),
ip: '',
community: '',
community: 'public',
version: '',
}
this.nodes.push(newNode)
this.$nextTick(() => {
this.form.setFieldsValue({
[`node_ip_${newNode.id}`]: newNode.ip,
[`node_community_${newNode.id}`]: newNode.community,
[`node_version_${newNode.id}`]: newNode.version,
})
})
},
removeNode(removeId, minLength) {
if (this.nodes.length <= minLength) {
@@ -133,45 +99,20 @@ export default {
}
},
copyNode(id) {
const newNode = {
id: uuidv4(),
ip: this.form.getFieldValue(`node_ip_${id}`),
community: this.form.getFieldValue(`node_community_${id}`),
version: this.form.getFieldValue(`node_version_${id}`),
}
this.nodes.push(newNode)
this.$nextTick(() => {
this.form.setFieldsValue({
[`node_ip_${newNode.id}`]: newNode.ip,
[`node_community_${newNode.id}`]: newNode.community,
[`node_version_${newNode.id}`]: newNode.version,
})
})
},
getInfoValuesFromForm(values) {
return this.nodes.map((item) => {
return {
id: item.id,
ip: values[`node_ip_${item.id}`],
community: values[`node_community_${item.id}`],
version: values[`node_version_${item.id}`],
const copyNode = this.nodes.find((item) => item.id === id)
if (copyNode) {
const newNode = {
...copyNode,
id: uuidv4(),
}
})
},
setNodeField() {
if (this.nodes && this.nodes.length) {
this.nodes.forEach((item) => {
this.form.setFieldsValue({
[`node_ip_${item.id}`]: item.ip,
[`node_community_${item.id}`]: item.community,
[`node_version_${item.id}`]: item.version,
})
})
this.nodes.push(newNode)
}
},
getNodeValue() {
const values = this.form.getFieldsValue()
return this.getInfoValuesFromForm(values)
const nodes = this.nodes.map((node) => {
return _.pick(node, ['ip', 'community', 'version'])
})
return nodes
},
},
}
@@ -180,10 +121,9 @@ export default {
<style lang="less" scoped>
.node-setting-wrap {
margin-left: 17px;
width: 600px;
.ant-row {
// display: flex;
/deep/ .ant-input-clear-icon {
color: rgba(0,0,0,.25);

View File

@@ -2,6 +2,8 @@ const cmdb_en = {
relation: 'Relation',
attribute: 'Attributes',
configTable: 'Config Table',
enterpriseVersionFlag: 'Pro',
enterpriseVersionTip: 'Enterprise version only',
menu: {
views: 'Views',
topologyView: 'Topology Views',
@@ -236,7 +238,7 @@ const cmdb_en = {
checkModalColumn4: 'Last checkup time',
testModalTitle: 'Automated discovery testing',
attrMapTableAttrPlaceholder: 'Please edit the name',
nodeSettingIp: 'IP Addresses',
nodeSettingIp: 'Network device IP address',
nodeSettingIpTip: 'Please enter the ip address',
nodeSettingIpTip1: 'ip address format error',
nodeSettingCommunity: 'Community',
@@ -264,6 +266,22 @@ const cmdb_en = {
resourceSearchTip3: 'Note 2: If you do not need to filter, please click the grey button to copy and paste directly to configure for all nodes',
enable: 'Enable',
enableTip: 'Confirm switching on?',
portScanConfig: 'Port Scan Config',
portScanLabel1: 'CIDR',
portScanLabel2: 'Port Range',
portScanLabel3: 'AgentID',
viewAllAttr: 'View All Prop',
attrGroup: 'Attr Group',
attrName: 'Attr Name',
attrAlias: 'Attr Alias',
attrCode: 'Attr Code',
computedAttrTip1: 'Reference attributes follow jinja2 syntax',
computedAttrTip2: `Multi-valued attributes (lists) are rendered with [ ] included by default, if you want to remove it, the reference method is: """{{ attr_name | join(',') }}""" where commas are separators`,
example: 'Example',
attrFilterTip: `The third column of values allows you to select attributes of this model to cascade attributes`,
rule: 'Rule',
cascadeAttr: 'Cascade',
cascadeAttrTip: 'Cascading attributes note the order',
},
components: {
unselectAttributes: 'Unselected',
@@ -313,6 +331,7 @@ const cmdb_en = {
sub: 'subscription',
selectBelow: 'Please select below',
subSuccess: 'Subscription successful',
subFailed: 'Subscription failed, please try again later',
selectMethods: 'Please select a method',
noAuthRequest: 'No certification requested yet',
noParamRequest: 'No parameter certification yet',
@@ -368,6 +387,8 @@ const cmdb_en = {
yearsAgo: 'years ago',
just: 'just now',
searchPlaceholder: 'Please search CIType',
subCITable: 'Data',
subCITree: 'Tree',
},
custom_dashboard: {
charts: 'Chart',
@@ -571,7 +592,13 @@ if __name__ == "__main__":
discoveryCardResoureTip: 'Number of resource types automatically discovered',
addPlugin: 'Add plugin',
pluginSearchTip: 'Please search the rules',
innerFlag: 'Inner'
innerFlag: 'Inner',
defaultName: 'Default Name',
deleteTip: 'Cannot be deleted again.',
tabCustom: 'Custom',
tabConfig: 'Configured',
addConfig: 'Add Config',
configErrTip: 'Please select config'
},
ci: {
attributeDesc: 'Attribute Description',
@@ -621,6 +648,7 @@ if __name__ == "__main__":
rollbackingTips: 'Rollbacking',
batchRollbacking: 'Deleting {total} items in total, {successNum} items successful, {errorNum} items failed',
baselineTips: 'Changes at this point in time will also be rollbacked, Unique ID, password and dynamic attributes do not support',
cover: 'Cover',
},
serviceTree: {
remove: 'Remove',

View File

@@ -2,6 +2,8 @@ const cmdb_zh = {
relation: '关系',
attribute: '属性',
configTable: '配置表格',
enterpriseVersionFlag: '企',
enterpriseVersionTip: '仅限企业版',
menu: {
views: '视图',
topologyView: '拓扑视图',
@@ -236,7 +238,7 @@ const cmdb_zh = {
checkModalColumn4: '最近检查时间',
testModalTitle: '自动发现测试',
attrMapTableAttrPlaceholder: '请编辑名称',
nodeSettingIp: 'ip地址',
nodeSettingIp: '网络设备IP地址',
nodeSettingIpTip: '请输入 ip 地址',
nodeSettingIpTip1: 'ip地址格式错误',
nodeSettingCommunity: 'Community',
@@ -264,6 +266,22 @@ const cmdb_zh = {
resourceSearchTip3: '注2如不需要筛选请直接点击灰色按钮进行复制粘贴即可配置为所有节点',
enable: '开启',
enableTip: '确定切换开启状态吗',
portScanConfig: '端口扫描配置',
portScanLabel1: 'CIDR',
portScanLabel2: '端口范围',
portScanLabel3: 'AgentID',
viewAllAttr: '查看所有属性',
attrGroup: '属性分组',
attrName: '属性名称',
attrAlias: '属性别名',
attrCode: '属性代码',
computedAttrTip1: '引用属性遵循jinja2语法',
computedAttrTip2: `多值属性(列表)默认呈现包括[ ], 如果要去掉, 引用方法为: """{{ attr_name | join(',') }}""" 其中逗号为分隔符`,
example: '例如',
attrFilterTip: '第三列值可选择本模型的属性,来实现级联属性的功能',
rule: '规则',
cascadeAttr: '级联',
cascadeAttrTip: '级联属性注意顺序',
},
components: {
unselectAttributes: '未选属性',
@@ -313,6 +331,7 @@ const cmdb_zh = {
sub: '订阅',
selectBelow: '请在下方进行选择',
subSuccess: '订阅成功',
subFailed: '订阅失败,请稍后再试',
selectMethods: '请选择方式',
noAuthRequest: '暂无请求认证',
noParamRequest: '暂无参数认证',
@@ -367,6 +386,8 @@ const cmdb_zh = {
yearsAgo: '年前',
just: '刚刚',
searchPlaceholder: '请搜索模型',
subCITable: '数据订阅',
subCITree: '层级订阅',
},
custom_dashboard: {
charts: '图表',
@@ -570,7 +591,13 @@ if __name__ == "__main__":
discoveryCardResoureTip: '自动发现的资源类型数',
addPlugin: '新增插件',
pluginSearchTip: '请搜索规则',
innerFlag: '内置'
innerFlag: '内置',
defaultName: '默认名称',
deleteTip: '不可再删除',
tabCustom: '自定义',
tabConfig: '已有配置',
addConfig: '添加配置',
configErrTip: '请选择配置'
},
ci: {
attributeDesc: '查看属性配置',
@@ -620,6 +647,7 @@ if __name__ == "__main__":
rollbackingTips: '正在批量回滚中',
batchRollbacking: '正在回滚,共{total}个,成功{successNum}个,失败{errorNum}个',
baselineTips: '该时间点的变更也会被回滚, 唯一标识、密码属性、动态属性不支持回滚',
cover: '覆盖',
},
serviceTree: {
remove: '移除',

View File

@@ -140,6 +140,7 @@
:mode="col.is_list ? 'multiple' : 'default'"
class="ci-table-edit-select"
allowClear
showSearch
>
<a-select-option
:value="choice[0]"
@@ -161,7 +162,7 @@
:type="choice[1].icon.name"
/>
</template>
{{ choice[0] }}
<span>{{ choice[0] }}</span>
</span>
</a-select-option>
</a-select>
@@ -199,6 +200,7 @@
borderRadius: '4px',
padding: '1px 5px',
margin: '2px',
verticalAlign: 'bottom',
...getChoiceValueStyle(col, value),
display: 'inline-flex',
alignItems: 'center',
@@ -210,7 +212,7 @@
:style="{ maxHeight: '13px', maxWidth: '13px', marginRight: '5px' }"
/>
<ops-icon
v-else
v-else-if="getChoiceValueIcon(col, value).name"
:style="{ color: getChoiceValueIcon(col, value).color, marginRight: '5px' }"
:type="getChoiceValueIcon(col, value).name"
/>{{ value }}
@@ -222,6 +224,7 @@
borderRadius: '4px',
padding: '1px 5px',
margin: '2px 0',
verticalAlign: 'bottom',
...getChoiceValueStyle(col, row[col.field]),
display: 'inline-flex',
alignItems: 'center',
@@ -233,7 +236,7 @@
:style="{ maxHeight: '13px', maxWidth: '13px', marginRight: '5px' }"
/>
<ops-icon
v-else
v-else-if="getChoiceValueIcon(col, row[col.field]).name"
:style="{ color: getChoiceValueIcon(col, row[col.field]).color, marginRight: '5px' }"
:type="getChoiceValueIcon(col, row[col.field]).name"
/>

View File

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

View File

@@ -105,7 +105,7 @@ export default {
default: true,
},
attrList: {
type: Array,
type: Function,
default: () => [],
}
},

View File

@@ -132,6 +132,10 @@ export default {
type: Object,
default: () => {},
},
initQueryLoading: {
type: Boolean,
default: false,
}
},
data() {
return {
@@ -145,129 +149,13 @@ export default {
firstCIJsonAttr: {},
secondCIJsonAttr: {},
canEdit: {},
topoData: {
nodes: {},
edges: []
}
}
},
computed: {
topoData() {
const ci_types_list = this.ci_types()
const _findCiType = ci_types_list.find((item) => item.id === this.typeId)
const unique_id = _findCiType.show_id || this.attributes().unique_id
const unique_name = _findCiType.show_name || this.attributes().unique
const _findUnique = this.attrList().find((attr) => attr.id === unique_id)
const unique_alias = _findUnique?.alias || _findUnique?.name || ''
const nodes = {
isRoot: true,
id: `Root_${this.typeId}`,
title: _findCiType.alias || _findCiType.name, // 中文名
name: _findCiType.name, // 英文名
Class: Node,
unique_alias,
unique_name,
unique_value: this.ci[unique_name],
icon: _findCiType?.icon || '',
endpoints: [
{
id: 'left',
orientation: [-1, 0],
pos: [0, 0.5],
},
{
id: 'right',
orientation: [1, 0],
pos: [0, 0.5],
},
],
children: [],
}
const edges = []
this.parentCITypes.forEach((parent) => {
const _findCiType = ci_types_list.find((item) => item.id === parent.id)
if (this.firstCIs[parent.name] && _findCiType) {
const unique_id = _findCiType.show_id || _findCiType.unique_id
const _findUnique = parent.attributes.find((attr) => attr.id === unique_id)
const unique_name = _findUnique?.name
const unique_alias = _findUnique?.alias || _findUnique?.name || ''
this.firstCIs[parent.name].forEach((parentCi) => {
nodes.children.push({
id: `${parentCi._id}`,
Class: Node,
title: parent.alias || parent.name,
name: parent.name,
side: 'left',
unique_alias,
unique_name,
unique_value: parentCi[unique_name],
children: [],
icon: _findCiType?.icon || '',
endpoints: [
{
id: 'left',
orientation: [-1, 0],
pos: [0, 0.5],
},
{
id: 'right',
orientation: [1, 0],
pos: [0, 0.5],
},
],
})
edges.push({
id: `${parentCi._id}_Root`,
source: 'right',
target: 'left',
sourceNode: `${parentCi._id}`,
targetNode: `Root_${this.typeId}`,
type: 'endpoint',
})
})
}
})
this.childCITypes.forEach((child) => {
const _findCiType = ci_types_list.find((item) => item.id === child.id)
if (this.secondCIs[child.name] && _findCiType) {
const unique_id = _findCiType.show_id || _findCiType.unique_id
const _findUnique = child.attributes.find((attr) => attr.id === unique_id)
const unique_name = _findUnique?.name
const unique_alias = _findUnique?.alias || _findUnique?.name || ''
this.secondCIs[child.name].forEach((childCi) => {
nodes.children.push({
id: `${childCi._id}`,
Class: Node,
title: child.alias || child.name,
name: child.name,
side: 'right',
unique_alias,
unique_name,
unique_value: childCi[unique_name],
children: [],
icon: _findCiType?.icon || '',
endpoints: [
{
id: 'left',
orientation: [-1, 0],
pos: [0, 0.5],
},
{
id: 'right',
orientation: [1, 0],
pos: [0, 0.5],
},
],
})
edges.push({
id: `Root_${childCi._id}`,
source: 'right',
target: 'left',
sourceNode: `Root_${this.typeId}`,
targetNode: `${childCi._id}`,
type: 'endpoint',
})
})
}
})
return { nodes, edges }
},
exsited_ci() {
const _exsited_ci = [this.ciId]
this.parentCITypes.forEach((parent) => {
@@ -297,20 +185,28 @@ export default {
},
},
mounted() {
this.init(true)
if (!this.initQueryLoading) {
this.init(true)
}
},
methods: {
async init(isFirst) {
await Promise.all([this.getParentCITypes(), this.getChildCITypes()])
Promise.all([this.getFirstCIs(), this.getSecondCIs()]).then(() => {
if (isFirst && this.$refs.ciDetailRelationTopo) {
const ci_types_list = this.ci_types()
this.handleTopoData()
if (
isFirst &&
this.$refs.ciDetailRelationTopo &&
ci_types_list.length
) {
this.$refs.ciDetailRelationTopo.exsited_ci = this.exsited_ci
this.$refs.ciDetailRelationTopo.setTopoData(this.topoData)
}
})
},
async getFirstCIs() {
await searchCIRelation(`root_id=${Number(this.ciId)}&&level=1&&reverse=1&&count=10000`)
await searchCIRelation(`root_id=${Number(this.ciId)}&level=1&reverse=1&count=10000`)
.then((res) => {
const firstCIs = {}
res.result.forEach((item) => {
@@ -328,7 +224,7 @@ export default {
.catch((e) => {})
},
async getSecondCIs() {
await searchCIRelation(`root_id=${Number(this.ciId)}&&level=1&&reverse=0&&count=10000`)
await searchCIRelation(`root_id=${Number(this.ciId)}&level=1&reverse=0&count=10000`)
.then((res) => {
const secondCIs = {}
res.result.forEach((item) => {
@@ -445,6 +341,137 @@ export default {
})
}
},
handleTopoData() {
const ci_types_list = this.ci_types()
if (!ci_types_list?.length) {
this.$set(this, 'topoData', {
nodes: {},
edges: []
})
return
}
const _findCiType = ci_types_list.find((item) => item.id === this.typeId)
const unique_id = _findCiType.show_id || this.attributes().unique_id
const unique_name = _findCiType.show_name || this.attributes().unique
const _findUnique = this.attrList().find((attr) => attr.id === unique_id)
const unique_alias = _findUnique?.alias || _findUnique?.name || ''
const nodes = {
isRoot: true,
id: `Root_${this.typeId}`,
title: _findCiType.alias || _findCiType.name, // 中文名
name: _findCiType.name, // 英文名
Class: Node,
unique_alias,
unique_name,
unique_value: this.ci[unique_name],
icon: _findCiType?.icon || '',
endpoints: [
{
id: 'left',
orientation: [-1, 0],
pos: [0, 0.5],
},
{
id: 'right',
orientation: [1, 0],
pos: [0, 0.5],
},
],
children: [],
}
const edges = []
this.parentCITypes.forEach((parent) => {
const _findCiType = ci_types_list.find((item) => item.id === parent.id)
if (this.firstCIs[parent.name] && _findCiType) {
const unique_id = _findCiType.show_id || _findCiType.unique_id
const _findUnique = parent.attributes.find((attr) => attr.id === unique_id)
const unique_name = _findUnique?.name
const unique_alias = _findUnique?.alias || _findUnique?.name || ''
this.firstCIs[parent.name].forEach((parentCi) => {
nodes.children.push({
id: `${parentCi._id}`,
Class: Node,
title: parent.alias || parent.name,
name: parent.name,
side: 'left',
unique_alias,
unique_name,
unique_value: parentCi[unique_name],
children: [],
icon: _findCiType?.icon || '',
endpoints: [
{
id: 'left',
orientation: [-1, 0],
pos: [0, 0.5],
},
{
id: 'right',
orientation: [1, 0],
pos: [0, 0.5],
},
],
})
edges.push({
id: `${parentCi._id}_Root`,
source: 'right',
target: 'left',
sourceNode: `${parentCi._id}`,
targetNode: `Root_${this.typeId}`,
type: 'endpoint',
})
})
}
})
this.childCITypes.forEach((child) => {
const _findCiType = ci_types_list.find((item) => item.id === child.id)
if (this.secondCIs[child.name] && _findCiType) {
const unique_id = _findCiType.show_id || _findCiType.unique_id
const _findUnique = child.attributes.find((attr) => attr.id === unique_id)
const unique_name = _findUnique?.name
const unique_alias = _findUnique?.alias || _findUnique?.name || ''
this.secondCIs[child.name].forEach((childCi) => {
nodes.children.push({
id: `${childCi._id}`,
Class: Node,
title: child.alias || child.name,
name: child.name,
side: 'right',
unique_alias,
unique_name,
unique_value: childCi[unique_name],
children: [],
icon: _findCiType?.icon || '',
endpoints: [
{
id: 'left',
orientation: [-1, 0],
pos: [0, 0.5],
},
{
id: 'right',
orientation: [1, 0],
pos: [0, 0.5],
},
],
})
edges.push({
id: `Root_${childCi._id}`,
source: 'right',
target: 'left',
sourceNode: `Root_${this.typeId}`,
targetNode: `${childCi._id}`,
type: 'endpoint',
})
})
}
})
this.$set(this, 'topoData', {
nodes,
edges
})
}
},
}
</script>

View File

@@ -17,7 +17,7 @@
border: 1px solid #d9d9d9;
border-radius: 2px;
padding: 4px 8px;
width: 100px;
width: auto;
text-align: center;
.title {
font-size: 16px;
@@ -73,7 +73,7 @@
}
}
.root {
width: 100px;
width: auto;
border-color: @primary-color;
font-weight: 700;
padding: 4px 8px;

View File

@@ -29,6 +29,10 @@ export default {
methods: {
init() {
const root = document.getElementById('ci-detail-relation-topo')
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
context.font = '16px'
this.canvas = new TreeCanvas({
root: root,
disLinkable: false, // 可删除连线
@@ -54,7 +58,15 @@ export default {
return 10
},
getWidth(d) {
return 40
const metrics = context.measureText(d?.title || '')
const width = metrics.width
/**
* width 文字宽度
* 20 icon 宽度
* 4 盒子内边距
* 40 节点间距
*/
return width + 20 + 4 + 40
},
getHGap(d) {
return 80
@@ -69,22 +81,27 @@ export default {
this.canvas.on('events', ({ type, data }) => {
const sourceNode = data?.id || null
if (type === 'custom:clickLeft') {
searchCIRelation(`root_id=${Number(sourceNode)}&&level=1&&reverse=1&&count=10000`).then((res) => {
searchCIRelation(`root_id=${Number(sourceNode)}&level=1&reverse=1&count=10000`).then((res) => {
this.redrawData(res, sourceNode, 'left')
})
}
if (type === 'custom:clickRight') {
searchCIRelation(`root_id=${Number(sourceNode)}&&level=1&&reverse=0&&count=10000`).then((res) => {
searchCIRelation(`root_id=${Number(sourceNode)}&level=1&reverse=0&count=10000`).then((res) => {
this.redrawData(res, sourceNode, 'right')
})
}
})
},
setTopoData(data) {
const root = document.getElementById('ci-detail-relation-topo')
if (root && root?.innerHTML) {
root.innerHTML = ''
}
this.canvas = null
this.init()
this.topoData = _.cloneDeep(data)
this.canvas.draw(data, {}, () => {
this.canvas.redraw(data, {}, () => {
this.canvas.focusCenterWithAnimate()
})
},

View File

@@ -1,449 +1,468 @@
<template>
<div :style="{ height: '100%' }">
<a-tabs v-if="hasPermission" class="ci-detail-tab" v-model="activeTabKey" @change="changeTab">
<a @click="shareCi" slot="tabBarExtraContent" :style="{ marginRight: '24px' }">
<a-icon type="share-alt" />
{{ $t('cmdb.ci.share') }}
</a>
<a-tab-pane key="tab_1">
<span slot="tab"><a-icon type="book" />{{ $t('cmdb.attribute') }}</span>
<div class="ci-detail-attr">
<el-descriptions
:title="group.name || $t('other')"
:key="group.name"
v-for="group in attributeGroups"
border
:column="3"
>
<el-descriptions-item
:label="`${attr.alias || attr.name}`"
:key="attr.name"
v-for="attr in group.attributes"
>
<ci-detail-attr-content :ci="ci" :attr="attr" @refresh="refresh" @updateCIByself="updateCIByself" />
</el-descriptions-item>
</el-descriptions>
</div>
</a-tab-pane>
<a-tab-pane key="tab_2">
<span slot="tab"><a-icon type="branches" />{{ $t('cmdb.relation') }}</span>
<div :style="{ height: '100%', padding: '24px', overflow: 'auto' }">
<ci-detail-relation ref="ciDetailRelation" :ciId="ciId" :typeId="typeId" :ci="ci" />
</div>
</a-tab-pane>
<a-tab-pane key="tab_3">
<span slot="tab"><a-icon type="clock-circle" />{{ $t('cmdb.ci.history') }}</span>
<div :style="{ padding: '24px', height: '100%' }">
<a-space :style="{ 'margin-bottom': '10px', display: 'flex' }">
<a-button type="primary" class="ops-button-ghost" ghost @click="handleRollbackCI()">
<ops-icon type="shishizhuangtai" />{{ $t('cmdb.ci.rollback') }}
</a-button>
</a-space>
<ci-rollback-form ref="ciRollbackForm" :ciIds="[ciId]" @getCIHistory="getCIHistory" />
<vxe-table
ref="xTable"
show-overflow
show-header-overflow
:data="ciHistory"
size="small"
:height="tableHeight"
highlight-hover-row
:span-method="mergeRowMethod"
:scroll-y="{ enabled: false, gt: 20 }"
:scroll-x="{ enabled: false, gt: 0 }"
border
resizable
class="ops-unstripe-table"
>
<template #empty>
<a-empty :image-style="{ height: '100px' }" :style="{ paddingTop: '10%' }">
<img slot="image" :src="require('@/assets/data_empty.png')" />
<span slot="description"> {{ $t('noData') }} </span>
</a-empty>
</template>
<vxe-table-column sortable field="created_at" :title="$t('created_at')"></vxe-table-column>
<vxe-table-column
field="username"
:title="$t('user')"
:filters="[]"
:filter-method="filterUsernameMethod"
></vxe-table-column>
<vxe-table-column
field="operate_type"
:filters="[
{ value: 0, label: $t('new') },
{ value: 1, label: $t('delete') },
{ value: 2, label: $t('update') },
]"
:filter-method="filterOperateMethod"
:title="$t('operation')"
>
<template #default="{ row }">
{{ operateTypeMap[row.operate_type] }}
</template>
</vxe-table-column>
<vxe-table-column
field="attr_alias"
:title="$t('cmdb.attribute')"
:filters="[]"
:filter-method="filterAttrMethod"
></vxe-table-column>
<vxe-table-column field="old" :title="$t('cmdb.history.old')">
<template #default="{ row }">
<span v-if="row.value_type === '6'">{{ JSON.parse(row.old) }}</span>
<span v-else>{{ row.old }}</span>
</template>
</vxe-table-column>
<vxe-table-column field="new" :title="$t('cmdb.history.new')">
<template #default="{ row }">
<span v-if="row.value_type === '6'">{{ JSON.parse(row.new) }}</span>
<span v-else>{{ row.new }}</span>
</template>
</vxe-table-column>
</vxe-table>
</div>
</a-tab-pane>
<a-tab-pane key="tab_4">
<span slot="tab"><ops-icon type="itsm_auto_trigger" />{{ $t('cmdb.history.triggerHistory') }}</span>
<div :style="{ padding: '24px', height: '100%' }">
<TriggerTable :ci_id="ci._id" />
</div>
</a-tab-pane>
<a-tab-pane key="tab_5">
<span slot="tab"><ops-icon type="itsm-association" />{{ $t('cmdb.ci.relITSM') }}</span>
<div :style="{ padding: '24px', height: '100%' }">
<RelatedItsmTable ref="relatedITSMTable" :ci_id="ci._id" :ciHistory="ciHistory" :itsmInstalled="itsmInstalled" :attrList="attrList" />
</div>
</a-tab-pane>
</a-tabs>
<a-empty
v-else
:image-style="{
height: '100px',
}"
:style="{ paddingTop: '20%' }"
>
<img slot="image" :src="require('@/assets/data_empty.png')" />
<span slot="description"> {{ $t('cmdb.ci.noPermission') }} </span>
</a-empty>
</div>
</template>
<script>
import _ from 'lodash'
import { Descriptions, DescriptionsItem } from 'element-ui'
import { getCITypeGroupById, getCITypes } from '@/modules/cmdb/api/CIType'
import { getCIHistory, judgeItsmInstalled } from '@/modules/cmdb/api/history'
import { getCIById } from '@/modules/cmdb/api/ci'
import CiDetailAttrContent from './ciDetailAttrContent.vue'
import CiDetailRelation from './ciDetailRelation.vue'
import TriggerTable from '../../operation_history/modules/triggerTable.vue'
import RelatedItsmTable from './ciDetailRelatedItsmTable.vue'
import CiRollbackForm from './ciRollbackForm.vue'
export default {
name: 'CiDetailTab',
components: {
ElDescriptions: Descriptions,
ElDescriptionsItem: DescriptionsItem,
CiDetailAttrContent,
CiDetailRelation,
TriggerTable,
RelatedItsmTable,
CiRollbackForm,
},
props: {
typeId: {
type: Number,
required: true,
},
treeViewsLevels: {
type: Array,
default: () => [],
},
attributeHistoryTableHeight: {
type: Number,
default: null
}
},
data() {
return {
ci: {},
item: [],
attributeGroups: [],
activeTabKey: 'tab_1',
rowSpanMap: {},
ciHistory: [],
ciId: null,
ci_types: [],
hasPermission: true,
itsmInstalled: true,
tableHeight: this.attributeHistoryTableHeight || (this.$store.state.windowHeight - 120),
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
operateTypeMap() {
return {
0: this.$t('new'),
1: this.$t('delete'),
2: this.$t('update'),
}
},
},
provide() {
return {
ci_types: () => {
return this.ci_types
},
}
},
inject: {
reload: {
from: 'reload',
default: null,
},
handleSearch: {
from: 'handleSearch',
default: null,
},
attrList: {
from: 'attrList',
default: () => [],
},
},
methods: {
async create(ciId, activeTabKey = 'tab_1', ciDetailRelationKey = '1') {
this.activeTabKey = activeTabKey
if (activeTabKey === 'tab_2') {
this.$nextTick(() => {
this.$refs.ciDetailRelation.activeKey = ciDetailRelationKey
})
}
this.ciId = ciId
await this.getCI()
await this.judgeItsmInstalled()
if (this.hasPermission) {
this.getAttributes()
this.getCIHistory()
getCITypes().then((res) => {
this.ci_types = res.ci_types
})
}
},
getAttributes() {
getCITypeGroupById(this.typeId, { need_other: 1 })
.then((res) => {
this.attributeGroups = res
})
.catch((e) => {})
},
async getCI() {
await getCIById(this.ciId)
.then((res) => {
if (res.result.length) {
this.ci = res.result[0]
} else {
this.hasPermission = false
}
})
.catch((e) => {
if (e.response.status === 404) {
this.itsmInstalled = false
}
})
},
async judgeItsmInstalled() {
await judgeItsmInstalled().catch((e) => {
this.itsmInstalled = false
})
},
getCIHistory() {
getCIHistory(this.ciId)
.then((res) => {
this.ciHistory = res
const rowSpanMap = {}
let startIndex = 0
let startCount = 1
res.forEach((item, index) => {
if (index === 0) {
return
}
if (res[index].record_id === res[startIndex].record_id) {
startCount += 1
rowSpanMap[index] = 0
if (index === res.length - 1) {
rowSpanMap[startIndex] = startCount
}
} else {
rowSpanMap[startIndex] = startCount
startIndex = index
startCount = 1
if (index === res.length - 1) {
rowSpanMap[index] = 1
}
}
})
this.rowSpanMap = rowSpanMap
})
.catch((e) => {
console.log(e)
})
},
changeTab(key) {
this.activeTabKey = key
if (key === 'tab_3') {
this.$nextTick(() => {
const $table = this.$refs.xTable
if ($table) {
const usernameColumn = $table.getColumnByField('username')
const attrColumn = $table.getColumnByField('attr_alias')
if (usernameColumn) {
const usernameList = [...new Set(this.ciHistory.map((item) => item.username))]
$table.setFilter(
usernameColumn,
usernameList.map((item) => {
return {
value: item,
label: item,
}
})
)
}
if (attrColumn) {
$table.setFilter(
attrColumn,
this.attrList().map((attr) => {
return { value: attr.alias || attr.name, label: attr.alias || attr.name }
})
)
}
}
})
}
},
filterUsernameMethod({ value, row, column }) {
return row.username === value
},
filterOperateMethod({ value, row, column }) {
return Number(row.operate_type) === Number(value)
},
filterAttrMethod({ value, row, column }) {
return row.attr_alias === value
},
refresh(editAttrName) {
this.getCI()
const _find = this.treeViewsLevels.find((level) => level.name === editAttrName)
// 修改的字段为树形视图订阅的字段 则全部reload
setTimeout(() => {
if (_find) {
if (this.reload) {
this.reload()
}
} else {
if (this.handleSearch) {
this.handleSearch()
}
}
}, 500)
},
mergeRowMethod({ row, _rowIndex, column, visibleData }) {
const fields = ['created_at', 'username']
const cellValue1 = row['created_at']
const cellValue2 = row['username']
if (cellValue1 && cellValue2 && fields.includes(column.property)) {
const prevRow = visibleData[_rowIndex - 1]
let nextRow = visibleData[_rowIndex + 1]
if (prevRow && prevRow['created_at'] === cellValue1 && prevRow['username'] === cellValue2) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow['created_at'] === cellValue1 && nextRow['username'] === cellValue2) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
},
updateCIByself(params, editAttrName) {
const _ci = { ..._.cloneDeep(this.ci), ...params }
this.ci = _ci
const _find = this.treeViewsLevels.find((level) => level.name === editAttrName)
// 修改的字段为树形视图订阅的字段 则全部reload
setTimeout(() => {
if (_find) {
if (this.reload) {
this.reload()
}
} else {
if (this.handleSearch) {
this.handleSearch()
}
}
}, 500)
},
shareCi() {
const text = `${document.location.host}/cmdb/cidetail/${this.typeId}/${this.ciId}`
this.$copyText(text)
.then(() => {
this.$message.success(this.$t('copySuccess'))
})
.catch(() => {
this.$message.error(this.$t('cmdb.ci.copyFailed'))
})
},
handleRollbackCI() {
this.$nextTick(() => {
this.$refs.ciRollbackForm.onOpen()
})
},
},
}
</script>
<style lang="less">
.ci-detail-tab {
height: 100%;
.ant-tabs-content {
height: calc(100% - 45px);
.ant-tabs-tabpane {
height: 100%;
}
}
.ant-tabs-bar {
margin: 0;
}
.ant-tabs-extra-content {
line-height: 44px;
}
.ci-detail-attr {
height: 100%;
overflow: auto;
padding: 24px;
.el-descriptions-item__content {
cursor: default;
&:hover a {
opacity: 1 !important;
}
}
.el-descriptions:first-child > .el-descriptions__header {
margin-top: 0;
}
.el-descriptions__header {
margin-bottom: 5px;
margin-top: 20px;
}
.ant-form-item {
margin-bottom: 0;
}
.ant-form-item-control {
line-height: 19px;
}
}
}
</style>
<template>
<div :style="{ height: '100%' }">
<a-tabs v-if="hasPermission" class="ci-detail-tab" v-model="activeTabKey" @change="changeTab">
<a @click="shareCi" slot="tabBarExtraContent" :style="{ marginRight: '24px' }">
<a-icon type="share-alt" />
{{ $t('cmdb.ci.share') }}
</a>
<a-tab-pane key="tab_1">
<span slot="tab"><a-icon type="book" />{{ $t('cmdb.attribute') }}</span>
<div class="ci-detail-attr">
<el-descriptions
:title="group.name || $t('other')"
:key="group.name"
v-for="group in attributeGroups"
border
:column="3"
>
<el-descriptions-item
:label="`${attr.alias || attr.name}`"
:key="attr.name"
v-for="attr in group.attributes"
>
<ci-detail-attr-content :ci="ci" :attr="attr" @refresh="refresh" @updateCIByself="updateCIByself" />
</el-descriptions-item>
</el-descriptions>
</div>
</a-tab-pane>
<a-tab-pane key="tab_2">
<span slot="tab"><a-icon type="branches" />{{ $t('cmdb.relation') }}</span>
<div :style="{ height: '100%', padding: '24px', overflow: 'auto' }">
<ci-detail-relation ref="ciDetailRelation" :ciId="ciId" :typeId="typeId" :ci="ci" :initQueryLoading="initQueryLoading" />
</div>
</a-tab-pane>
<a-tab-pane key="tab_3">
<span slot="tab"><a-icon type="clock-circle" />{{ $t('cmdb.ci.history') }}</span>
<div :style="{ padding: '24px', height: '100%' }">
<a-space :style="{ 'margin-bottom': '10px', display: 'flex' }">
<a-button type="primary" class="ops-button-ghost" ghost @click="handleRollbackCI()">
<ops-icon type="shishizhuangtai" />{{ $t('cmdb.ci.rollback') }}
</a-button>
<a-button type="primary" class="ops-button-ghost" ghost @click="handleExport">
<ops-icon type="veops-export" />{{ $t('export') }}
</a-button>
</a-space>
<ci-rollback-form ref="ciRollbackForm" :ciIds="[ciId]" @getCIHistory="getCIHistory" />
<vxe-table
ref="xTable"
show-overflow
show-header-overflow
:data="ciHistory"
size="small"
:height="tableHeight"
highlight-hover-row
:span-method="mergeRowMethod"
:scroll-y="{ enabled: false, gt: 20 }"
:scroll-x="{ enabled: false, gt: 0 }"
border
resizable
class="ops-unstripe-table"
>
<template #empty>
<a-empty :image-style="{ height: '100px' }" :style="{ paddingTop: '10%' }">
<img slot="image" :src="require('@/assets/data_empty.png')" />
<span slot="description"> {{ $t('noData') }} </span>
</a-empty>
</template>
<vxe-table-column sortable field="created_at" :title="$t('created_at')"></vxe-table-column>
<vxe-table-column
field="username"
:title="$t('user')"
:filters="[]"
:filter-method="filterUsernameMethod"
></vxe-table-column>
<vxe-table-column
field="operate_type"
:filters="[
{ value: 0, label: $t('new') },
{ value: 1, label: $t('delete') },
{ value: 2, label: $t('update') },
]"
:filter-method="filterOperateMethod"
:title="$t('operation')"
>
<template #default="{ row }">
{{ operateTypeMap[row.operate_type] }}
</template>
</vxe-table-column>
<vxe-table-column
field="attr_alias"
:title="$t('cmdb.attribute')"
:filters="[]"
:filter-method="filterAttrMethod"
></vxe-table-column>
<vxe-table-column :cell-type="'string'" field="old" :title="$t('cmdb.history.old')">
<template #default="{ row }">
<span v-if="row.value_type === '6'">{{ JSON.parse(row.old) }}</span>
<span v-else>{{ row.old }}</span>
</template>
</vxe-table-column>
<vxe-table-column :cell-type="'string'" field="new" :title="$t('cmdb.history.new')">
<template #default="{ row }">
<span v-if="row.value_type === '6'">{{ JSON.parse(row.new) }}</span>
<span v-else>{{ row.new }}</span>
</template>
</vxe-table-column>
</vxe-table>
</div>
</a-tab-pane>
<a-tab-pane key="tab_4">
<span slot="tab"><ops-icon type="itsm_auto_trigger" />{{ $t('cmdb.history.triggerHistory') }}</span>
<div :style="{ padding: '24px', height: '100%' }">
<TriggerTable :ci_id="ci._id" />
</div>
</a-tab-pane>
<a-tab-pane key="tab_5">
<span slot="tab"><ops-icon type="itsm-association" />{{ $t('cmdb.ci.relITSM') }}</span>
<div :style="{ padding: '24px', height: '100%' }">
<RelatedItsmTable ref="relatedITSMTable" :ci_id="ci._id" :ciHistory="ciHistory" :itsmInstalled="itsmInstalled" :attrList="attrList" />
</div>
</a-tab-pane>
</a-tabs>
<a-empty
v-else
:image-style="{
height: '100px',
}"
:style="{ paddingTop: '20%' }"
>
<img slot="image" :src="require('@/assets/data_empty.png')" />
<span slot="description"> {{ $t('cmdb.ci.noPermission') }} </span>
</a-empty>
</div>
</template>
<script>
import _ from 'lodash'
import { Descriptions, DescriptionsItem } from 'element-ui'
import { getCITypeGroupById, getCITypes } from '@/modules/cmdb/api/CIType'
import { getCIHistory, judgeItsmInstalled } from '@/modules/cmdb/api/history'
import { getCIById } from '@/modules/cmdb/api/ci'
import CiDetailAttrContent from './ciDetailAttrContent.vue'
import CiDetailRelation from './ciDetailRelation.vue'
import TriggerTable from '../../operation_history/modules/triggerTable.vue'
import RelatedItsmTable from './ciDetailRelatedItsmTable.vue'
import CiRollbackForm from './ciRollbackForm.vue'
export default {
name: 'CiDetailTab',
components: {
ElDescriptions: Descriptions,
ElDescriptionsItem: DescriptionsItem,
CiDetailAttrContent,
CiDetailRelation,
TriggerTable,
RelatedItsmTable,
CiRollbackForm,
},
props: {
typeId: {
type: Number,
required: true,
},
treeViewsLevels: {
type: Array,
default: () => [],
},
attributeHistoryTableHeight: {
type: Number,
default: null
}
},
data() {
return {
ci: {},
item: [],
attributeGroups: [],
activeTabKey: 'tab_1',
rowSpanMap: {},
ciHistory: [],
ciId: null,
ci_types: [],
hasPermission: true,
itsmInstalled: true,
tableHeight: this.attributeHistoryTableHeight || (this.$store.state.windowHeight - 120),
initQueryLoading: true,
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
operateTypeMap() {
return {
0: this.$t('new'),
1: this.$t('delete'),
2: this.$t('update'),
}
},
},
provide() {
return {
ci_types: () => {
return this.ci_types
},
}
},
inject: {
reload: {
from: 'reload',
default: null,
},
handleSearch: {
from: 'handleSearch',
default: null,
},
attrList: {
from: 'attrList',
default: () => [],
},
},
methods: {
async create(ciId, activeTabKey = 'tab_1', ciDetailRelationKey = '1') {
this.initQueryLoading = true
this.activeTabKey = activeTabKey
if (activeTabKey === 'tab_2') {
this.$nextTick(() => {
this.$refs.ciDetailRelation.activeKey = ciDetailRelationKey
})
}
this.ciId = ciId
await this.getCI()
await this.judgeItsmInstalled()
if (this.hasPermission) {
this.getAttributes()
this.getCIHistory()
const ciTypeRes = await getCITypes()
this.ci_types = ciTypeRes.ci_types
if (this.activeTabKey === 'tab_2') {
this.$refs.ciDetailRelation.init(true)
}
}
this.initQueryLoading = false
},
getAttributes() {
getCITypeGroupById(this.typeId, { need_other: 1 })
.then((res) => {
this.attributeGroups = res
})
.catch((e) => {})
},
async getCI() {
await getCIById(this.ciId)
.then((res) => {
if (res.result.length) {
this.ci = res.result[0]
} else {
this.hasPermission = false
}
})
.catch((e) => {
if (e.response.status === 404) {
this.itsmInstalled = false
}
})
},
async judgeItsmInstalled() {
await judgeItsmInstalled().catch((e) => {
this.itsmInstalled = false
})
},
getCIHistory() {
getCIHistory(this.ciId)
.then((res) => {
this.ciHistory = res
const rowSpanMap = {}
let startIndex = 0
let startCount = 1
res.forEach((item, index) => {
if (index === 0) {
return
}
if (res[index].record_id === res[startIndex].record_id) {
startCount += 1
rowSpanMap[index] = 0
if (index === res.length - 1) {
rowSpanMap[startIndex] = startCount
}
} else {
rowSpanMap[startIndex] = startCount
startIndex = index
startCount = 1
if (index === res.length - 1) {
rowSpanMap[index] = 1
}
}
})
this.rowSpanMap = rowSpanMap
})
.catch((e) => {
console.log(e)
})
},
changeTab(key) {
this.activeTabKey = key
if (key === 'tab_3') {
this.$nextTick(() => {
const $table = this.$refs.xTable
if ($table) {
const usernameColumn = $table.getColumnByField('username')
const attrColumn = $table.getColumnByField('attr_alias')
if (usernameColumn) {
const usernameList = [...new Set(this.ciHistory.map((item) => item.username))]
$table.setFilter(
usernameColumn,
usernameList.map((item) => {
return {
value: item,
label: item,
}
})
)
}
if (attrColumn) {
$table.setFilter(
attrColumn,
this.attrList().map((attr) => {
return { value: attr.alias || attr.name, label: attr.alias || attr.name }
})
)
}
}
})
}
},
filterUsernameMethod({ value, row, column }) {
return row.username === value
},
filterOperateMethod({ value, row, column }) {
return Number(row.operate_type) === Number(value)
},
filterAttrMethod({ value, row, column }) {
return row.attr_alias === value
},
refresh(editAttrName) {
this.getCI()
const _find = this.treeViewsLevels.find((level) => level.name === editAttrName)
// 修改的字段为树形视图订阅的字段 则全部reload
setTimeout(() => {
if (_find) {
if (this.reload) {
this.reload()
}
} else {
if (this.handleSearch) {
this.handleSearch()
}
}
}, 500)
},
mergeRowMethod({ row, _rowIndex, column, visibleData }) {
const fields = ['created_at', 'username']
const cellValue1 = row['created_at']
const cellValue2 = row['username']
if (cellValue1 && cellValue2 && fields.includes(column.property)) {
const prevRow = visibleData[_rowIndex - 1]
let nextRow = visibleData[_rowIndex + 1]
if (prevRow && prevRow['created_at'] === cellValue1 && prevRow['username'] === cellValue2) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow['created_at'] === cellValue1 && nextRow['username'] === cellValue2) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
},
updateCIByself(params, editAttrName) {
const _ci = { ..._.cloneDeep(this.ci), ...params }
this.ci = _ci
const _find = this.treeViewsLevels.find((level) => level.name === editAttrName)
// 修改的字段为树形视图订阅的字段 则全部reload
setTimeout(() => {
if (_find) {
if (this.reload) {
this.reload()
}
} else {
if (this.handleSearch) {
this.handleSearch()
}
}
}, 500)
},
shareCi() {
const text = `${document.location.host}/cmdb/cidetail/${this.typeId}/${this.ciId}`
this.$copyText(text)
.then(() => {
this.$message.success(this.$t('copySuccess'))
})
.catch(() => {
this.$message.error(this.$t('cmdb.ci.copyFailed'))
})
},
handleRollbackCI() {
this.$nextTick(() => {
this.$refs.ciRollbackForm.onOpen()
})
},
async handleExport() {
this.$refs.xTable.exportData({
filename: this.$t('cmdb.ci.history'),
sheetName: 'Sheet1',
type: 'xlsx',
types: ['xlsx', 'csv', 'html', 'xml', 'txt'],
data: this.ciHistory,
isMerge: true,
isColgroup: true,
})
}
},
}
</script>
<style lang="less">
.ci-detail-tab {
height: 100%;
.ant-tabs-content {
height: calc(100% - 45px);
.ant-tabs-tabpane {
height: 100%;
}
}
.ant-tabs-bar {
margin: 0;
}
.ant-tabs-extra-content {
line-height: 44px;
}
.ci-detail-attr {
height: 100%;
overflow: auto;
padding: 24px;
.el-descriptions-item__content {
cursor: default;
&:hover a {
opacity: 1 !important;
}
}
.el-descriptions:first-child > .el-descriptions__header {
margin-top: 0;
}
.el-descriptions__header {
margin-bottom: 5px;
margin-top: 20px;
}
.ant-form-item {
margin-bottom: 0;
}
.ant-form-item-control {
line-height: 19px;
}
}
}
</style>

View File

@@ -0,0 +1,140 @@
<template>
<CustomDrawer
:title="$t('cmdb.ciType.viewAllAttr')"
:visible="visible"
placement="right"
width="800"
:bodyStyle="{ height: '100vh' }"
@close="handleClose"
>
<vxe-table
resizable
size="mini"
:span-method="mergeRowMethod"
:data="tableData"
show-overflow
show-header-overflow
border
class="ops-stripe-table"
:height="windowHeight - 160"
>
<vxe-table-column align="center" field="groupId" :title="$t('cmdb.ciType.attrGroup')" :width="100">
<template #default="{row}">
<span>{{ row.groupName }}</span>
</template>
</vxe-table-column>
<vxe-table-column field="name" :title="$t('cmdb.ciType.attrName')" :width="150"></vxe-table-column>
<vxe-table-column field="alias" :title="$t('cmdb.ciType.attrAlias')" :width="150"></vxe-table-column>
<vxe-table-column field="typeText" :title="$t('type')" :width="100"></vxe-table-column>
<vxe-table-column field="code" :title="$t('cmdb.ciType.attrCode')">
<template #default="{row}">
<a @click="copyText(row.code)" >{{ row.code }}</a>
</template>
</vxe-table-column>
</vxe-table>
</CustomDrawer>
</template>
<script>
import { mapState } from 'vuex'
import _ from 'lodash'
import { valueTypeMap } from '@/modules/cmdb/utils/const'
export default {
name: 'AllAttrDrawer',
data() {
return {
visible: false,
tableData: [],
}
},
inject: ['providerGroupsData'],
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
}),
},
methods: {
async open() {
this.visible = true
const tableData = []
const typeMap = valueTypeMap()
const providerGroupsData = _.cloneDeep(this.providerGroupsData() || {})
const groupsData = providerGroupsData?.CITypeGroups || []
const otherAttrData = providerGroupsData?.otherGroupAttributes || []
groupsData.forEach((group) => {
if (group?.attributes?.length) {
const attrArr = group.attributes.map((attr) => {
if (attr.is_password) {
attr.value_type = '7'
}
if (attr.is_link) {
attr.value_type = '8'
}
attr.groupId = group.id
attr.groupName = group.name
attr.code = ['0', '1', '6'].includes(attr.value_type) ? `{{ ${attr.name} }}` : `'''{{ ${attr.name} }}'''`
attr.typeText = typeMap?.[attr.value_type] ?? ''
return attr
})
tableData.push(...attrArr)
}
})
otherAttrData.forEach((attr) => {
if (attr.is_password) {
attr.value_type = '7'
}
if (attr.is_link) {
attr.value_type = '8'
}
attr.groupId = -1
attr.groupName = this.$t('other')
attr.code = `{{ ${attr.name} }}`
attr.typeText = typeMap?.[attr.value_type] ?? ''
})
tableData.push(...otherAttrData)
this.tableData = tableData
},
mergeRowMethod({ row, _rowIndex, column, visibleData }) {
const fields = ['groupId']
const currentValue = row.groupId
if (currentValue && fields.includes(column.property)) {
const prevRow = visibleData[_rowIndex - 1]
let nextRow = visibleData[_rowIndex + 1]
if (prevRow && prevRow.groupId === currentValue) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow.groupId === currentValue) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
},
handleClose() {
this.visible = false
},
copyText(text) {
this.$copyText(text)
.then(() => {
this.$message.success(this.$t('copySuccess'))
})
}
}
}
</script>
<style lang="less" scoped>
</style>

View File

@@ -94,6 +94,7 @@ export default {
clientCITypeList: [],
currentTab: '',
deletePlugin: false,
queryLoaded: false,
}
},
computed: {
@@ -116,7 +117,7 @@ export default {
watch: {
currentTab: {
handler() {
if (this.currentTab) {
if (this.currentTab && this.queryLoaded) {
this.$nextTick(() => {
this.$refs[`attrAdTabpaneRef`].init()
})
@@ -131,6 +132,7 @@ export default {
this.ciTypeAttributes = res.attributes.map((item) => {
return { ...item, value: item.name, label: item.name }
})
this.queryLoaded = true
if (this.currentTab) {
this.$nextTick(() => {
this.$refs[`attrAdTabpaneRef`].init()

View File

@@ -0,0 +1,156 @@
<template>
<a-row class="attr-ad-form">
<a-col :span="24">
<a-form-item
label="CIDR"
:labelCol="labelCol"
:wrapperCol="{ span: 18 }"
labelAlign="right"
style="width: 100%; margin-top: 20px"
>
<div class="cidr-tag">
<div
v-for="(item) in list"
:key="item.id"
class="cidr-tag-item"
>
<a-tooltip :title="item.value">
<span class="cidr-tag-text">{{ item.value }}</span>
</a-tooltip>
<a-icon
class="cidrv-tag-close"
type="close"
@click.stop="clickClose(item.id)"
/>
</div>
<a-input
v-if="showAddInput"
class="cidr-tag-input"
autofocus
@blur="addPreValue"
@pressEnter="showAddInput = false"
></a-input>
<a v-else class="cidr-tag-add" @click="showAddInput = true">+ {{ $t('new') }}</a>
</div>
</a-form-item>
</a-col>
</a-row>
</template>
<script>
import _ from 'lodash'
import { v4 as uuidv4 } from 'uuid'
export default {
name: 'CIDRTags',
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: Array,
default: () => [],
},
},
data() {
return {
showAddInput: false,
}
},
inject: ['provide_labelCol'],
computed: {
list: {
get() {
return this.value
},
set(newValue) {
this.$emit('change', newValue)
}
},
labelCol() {
return this.provide_labelCol()
}
},
methods: {
clickClose(id) {
const list = _.cloneDeep(this.value)
const index = list.findIndex((item) => item.id === id)
if (index !== -1) {
list.splice(index, 1)
this.$emit('change', list)
}
},
addPreValue(e) {
this.showAddInput = false
const val = e.target.value
if (!val) {
return
}
const list = _.cloneDeep(this.value)
list.push({
value: val,
id: uuidv4()
})
this.$emit('change', list)
}
}
}
</script>
<style lang="less" scoped>
.cidr-tag {
width: max-content;
max-width: 100%;
padding: 6px 9px;
border-radius: 2px;
border: 1px solid #E4E7ED;
background: #FFF;
display: flex;
flex-wrap: wrap;
gap: 8px;
&-item {
padding: 3px 6px;
background-color: #F0F5FF;
display: flex;
align-items: center;
}
&-text {
font-size: 12px;
font-weight: 400;
color: #1D2129;
line-height: 18px;
text-overflow: ellipsis;
word-break: break-all;
white-space: nowrap;
max-width: 100px;
overflow: hidden;
}
&-close {
font-size: 12px;
color: #1D2129;
margin-left: 4px;
cursor: pointer;
}
&-input {
max-width: 120px;
height: 26px;
line-height: 26px;
padding: 3px 6px;
}
&-add {
border: dashed 1px #e4e7ed;
padding: 3px 6px;
font-size: 12px;
font-weight: 400;
color: #1D2129;
line-height: 18px;
cursor: pointer;
}
}
</style>

View File

@@ -0,0 +1,77 @@
<template>
<div class="cloud-tabs">
<div
v-for="(item) in tabList"
:key="item.key"
:class="['cloud-tabs-item', activeKey === item.key ? 'cloud-tabs-item-active' : '']"
@click="clickTab(item.key)"
>
{{ $t(item.text) }}
</div>
</div>
</template>
<script>
import { tabList, TAB_KEY } from '../constants.js'
export default {
name: 'CloudTab',
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: String,
default: TAB_KEY.CUSTOM,
},
},
computed: {
activeKey: {
get() {
return this.value
},
set(newValue) {
this.$emit('change', newValue)
}
},
},
data() {
return {
tabList
}
},
methods: {
clickTab(key) {
this.$emit('change', key)
}
}
}
</script>
<style lang="less" scoped >
.cloud-tabs {
display: flex;
align-items: center;
margin-bottom: 26px;
&-item {
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 400;
color: #4E5969;
background-color: #F7F8FA;
width: 105px;
height: 32px;
cursor: pointer;
&-active {
border: solid 1px #B1C9FF;
background-color: #E1EFFF;
color: #2F54EB;
}
}
}
</style>

View File

@@ -0,0 +1,15 @@
export const TAB_KEY = {
CUSTOM: 'custom',
CONFIG: 'config'
}
export const tabList = [
{
key: TAB_KEY.CUSTOM,
text: 'cmdb.ad.tabCustom'
},
{
key: TAB_KEY.CONFIG,
text: 'cmdb.ad.tabConfig'
}
]

View File

@@ -0,0 +1,52 @@
<template>
<a-form-model
:model="formData"
labelAlign="right"
:labelCol="labelCol"
:wrapperCol="{ span: 6 }"
class="attr-ad-form"
>
<a-form-model-item :extra="`${$t('cmdb.ciType.example')}: 192.168.0.0/16`" :label="$t('cmdb.ciType.portScanLabel1')">
<a-input v-model="formData.cidr" />
</a-form-model-item>
<a-form-model-item :extra="`${$t('cmdb.ciType.example')}: 8000-8800`" :label="$t('cmdb.ciType.portScanLabel2')">
<a-input v-model="formData.ports" />
</a-form-model-item>
<a-form-model-item :extra="`${$t('cmdb.ciType.example')}: 0x1234`" :label="$t('cmdb.ciType.portScanLabel3')">
<a-input v-model="formData.enable_cidr" />
</a-form-model-item>
</a-form-model>
</template>
<script>
export default {
name: 'PortScanConfig',
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: Object,
default: () => {},
},
},
inject: ['provide_labelCol'],
computed: {
formData: {
get() {
return this.value
},
set(newValue) {
this.$emit('change', newValue)
}
},
labelCol() {
return this.provide_labelCol()
}
},
}
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,154 @@
<template>
<div class="private-cloud-wrap">
<CloudTab
v-model="formData.tabActive"
@change="handleTabChange"
/>
<a-form-model
:model="formData"
labelAlign="right"
:labelCol="labelCol"
:wrapperCol="{ span: 6 }"
class="attr-ad-form"
>
<a-form-model-item
v-if="formData.tabActive === TAB_KEY.CONFIG"
:required="true"
:label="$t('cmdb.ad.tabConfig')"
>
<a-select
showSearch
optionFilterProp="title"
v-model="formData._reference"
@change="handleSelectChange"
>
<a-select-option
v-for="(item) in accountsList"
:key="item.id"
:value="item.id"
:title="item.name"
>
{{ item.name }}
</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item :required="true" :label="$t('cmdb.ciType.host')">
<a-input
:disabled="formData.tabActive === TAB_KEY.CONFIG"
v-model="formData.host"
/>
</a-form-model-item>
<a-form-model-item :required="true" :label="$t('cmdb.ciType.account')">
<a-input
:disabled="formData.tabActive === TAB_KEY.CONFIG"
v-model="formData.account"
/>
</a-form-model-item>
<a-form-model-item v-if="formData.tabActive === TAB_KEY.CUSTOM" :required="true" :label="$t('cmdb.ciType.password')">
<a-input-password v-model="formData.password" />
</a-form-model-item>
<a-form-model-item :label="$t('cmdb.ciType.vcenterName')">
<a-input v-model="formData.vcenterName" />
</a-form-model-item>
</a-form-model>
</div>
</template>
<script>
import { getHTTPAccounts } from '@/modules/cmdb/api/discovery'
import { TAB_KEY } from '../constants.js'
import CloudTab from '../cloudTab/index.vue'
export default {
name: 'VCenterForm',
components: {
CloudTab
},
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: Object,
default: () => {},
},
},
data() {
return {
TAB_KEY,
accountsList: [],
}
},
inject: ['provide_labelCol'],
computed: {
formData: {
get() {
return this.value
},
set(newValue) {
this.$emit('change', newValue)
}
},
labelCol() {
return this.provide_labelCol()
}
},
methods: {
async init(id) {
const res = await getHTTPAccounts({
adr_id: id
})
this.accountsList = res?.length ? res : []
this.$nextTick(() => {
const { _reference = '', host = '', account = '', tabActive } = this?.formData || {}
const findSelect = this.accountsList?.find((item) => item.id === _reference)
const newFormData = findSelect?.config || {}
const changeData = {
...this.value,
_reference: findSelect?.id ?? '',
}
if (tabActive === TAB_KEY.CONFIG) {
changeData.host = newFormData?.host ?? host
changeData.account = newFormData?.account ?? account
}
this.$emit('change', changeData)
})
},
handleTabChange(key) {
if (key === TAB_KEY.CONFIG) {
this.handleSelectChange(this.formData._referenceValue)
}
},
handleSelectChange(id) {
const accountConfig = this.accountsList.find((item) => item.id === id)?.config || {}
const { host, account } = this?.value
this.$emit('change', {
...this.value,
host: accountConfig?.host ?? host ?? '',
account: accountConfig?.account ?? account ?? ''
})
}
}
}
</script>
<style lang="less" scoped>
.private-cloud-wrap {
margin-left: 17px;
.input-disabled {
/deep/ input {
color: rgba(0, 0, 0, 0.25);
background-color: #f5f5f5;
pointer-events: none;
opacity: 1;
}
}
}
</style>

View File

@@ -0,0 +1,142 @@
<template>
<div class="public-cloud-wrap">
<CloudTab
v-model="formData.tabActive"
@change="handleTabChange"
/>
<a-form-model
:model="formData"
labelAlign="right"
:labelCol="labelCol"
:wrapperCol="{ span: 6 }"
class="attr-ad-form"
>
<a-form-model-item
v-if="formData.tabActive === TAB_KEY.CONFIG"
:required="true"
:label="$t('cmdb.ad.tabConfig')"
>
<a-select
showSearch
optionFilterProp="title"
v-model="formData._reference"
@change="handleSelectChange"
>
<a-select-option
v-for="(item) in accountsList"
:key="item.id"
:value="item.id"
:title="item.name"
>
{{ item.name }}
</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item :required="true" label="key">
<a-input-password
:class="[formData.tabActive === TAB_KEY.CONFIG ? 'input-disabled' : '']"
v-model="formData.key"
/>
</a-form-model-item>
<a-form-model-item v-if="formData.tabActive === TAB_KEY.CUSTOM" :required="true" label="secret">
<a-input-password v-model="formData.secret" />
</a-form-model-item>
</a-form-model>
</div>
</template>
<script>
import { getHTTPAccounts } from '@/modules/cmdb/api/discovery'
import { TAB_KEY } from '../constants.js'
import CloudTab from '../cloudTab/index.vue'
export default {
name: 'PublicCloud',
components: {
CloudTab
},
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: Object,
default: () => {},
},
},
data() {
return {
TAB_KEY,
accountsList: []
}
},
inject: ['provide_labelCol'],
computed: {
formData: {
get() {
return this.value
},
set(newValue) {
this.$emit('change', newValue)
}
},
labelCol() {
return this.provide_labelCol()
}
},
methods: {
async init(id) {
const res = await getHTTPAccounts({
adr_id: id
})
this.accountsList = res?.length ? res : []
this.$nextTick(() => {
const { _reference = '', key = '', tabActive } = this?.formData || {}
const findSelect = this.accountsList?.find((item) => item.id === _reference)
const newFormData = findSelect?.config || {}
const changeData = {
...this.value,
_reference: findSelect?.id ?? '',
}
if (tabActive === TAB_KEY.CONFIG) {
changeData.key = newFormData?.key ?? key
}
this.$emit('change', changeData)
})
},
handleTabChange(key) {
if (key === TAB_KEY.CONFIG) {
this.handleSelectChange(this.formData._reference)
}
},
handleSelectChange(id) {
const accountConfig = this.accountsList.find((item) => item.id === id)?.config || {}
const { key } = this?.value
this.$emit('change', {
...this.value,
key: accountConfig?.key ?? key ?? '',
})
}
}
}
</script>
<style lang="less" scoped>
.public-cloud-wrap {
margin-left: 17px;
.input-disabled {
/deep/ input {
color: rgba(0, 0, 0, 0.25);
background-color: #f5f5f5;
pointer-events: none;
opacity: 1;
}
}
}
</style>

View File

@@ -32,7 +32,7 @@
</div>
<div class="attr-ad-attributemap-main">
<AttrMapTable
v-if="adrType === 'agent'"
v-if="adrType === DISCOVERY_CATEGORY_TYPE.AGENT"
ref="attrMapTable"
:ruleType="adrType"
:tableData="tableData"
@@ -53,17 +53,18 @@
:style="{ marginBottom: '20px' }"
/>
</div>
<template v-if="adrType === 'snmp'">
<template v-if="adrType === DISCOVERY_CATEGORY_TYPE.SNMP">
<div class="attr-ad-header">{{ $t('cmdb.ciType.nodeConfig') }}</div>
<a-form :form="form3" layout="inline" class="attr-ad-snmp-form">
<NodeSetting ref="nodeSetting" :initNodes="nodes" :form="form3" />
<a-form :form="nodeSettingForm" layout="inline" class="attr-ad-snmp-form">
<NodeSetting ref="nodeSetting" :initNodes="nodes" />
<CIDRTags v-model="cidrList" />
</a-form>
</template>
<div class="attr-ad-header">{{ $t('cmdb.ciType.adExecConfig') }}</div>
<a-form-model
:model="form"
:labelCol="labelCol"
labelAlign="left"
labelAlign="right"
:wrapperCol="{ span: 14 }"
class="attr-ad-form"
>
@@ -127,56 +128,32 @@
</el-popover>
</a-form-model-item>
</a-form-model>
<template v-if="adrType === 'http'">
<template v-if="adrType === DISCOVERY_CATEGORY_TYPE.HTTP">
<template v-if="isPrivateCloud">
<template v-if="privateCloudName === PRIVATE_CLOUD_NAME.VCenter">
<div class="attr-ad-header">{{ $t('cmdb.ciType.privateCloud') }}</div>
<a-form-model
:model="privateCloudForm"
labelAlign="left"
:labelCol="labelCol"
:wrapperCol="{ span: 6 }"
class="attr-ad-form"
>
<a-form-model-item :required="true" :label="$t('cmdb.ciType.host')">
<a-input v-model="privateCloudForm.host" />
</a-form-model-item>
<a-form-model-item :required="true" :label="$t('cmdb.ciType.account')">
<a-input v-model="privateCloudForm.account" />
</a-form-model-item>
<a-form-model-item :required="true" :label="$t('cmdb.ciType.password')">
<a-input-password v-model="privateCloudForm.password" />
</a-form-model-item>
<!-- <a-form-model-item :label="$t('cmdb.ciType.insecure')">
<a-switch v-model="privateCloudForm.insecure" />
</a-form-model-item> -->
<a-form-model-item :label="$t('cmdb.ciType.vcenterName')">
<a-input v-model="privateCloudForm.vcenterName" />
</a-form-model-item>
</a-form-model>
<VcenterForm
v-model="privateCloudForm"
ref="httpForm"
/>
</template>
</template>
<template v-else>
<div class="attr-ad-header">{{ $t('cmdb.ciType.cloudAccessKey') }}</div>
<!-- <div class="public-cloud-info">{{ $t('cmdb.ciType.cloudAccessKeyTip') }}</div> -->
<a-form-model
:model="form2"
labelAlign="left"
:labelCol="labelCol"
:wrapperCol="{ span: 6 }"
class="attr-ad-form"
>
<a-form-model-item :required="true" label="key">
<a-input-password v-model="form2.key" />
</a-form-model-item>
<a-form-model-item :required="true" label="secret">
<a-input-password v-model="form2.secret" />
</a-form-model-item>
</a-form-model>
<PublicCloud
v-model="publicCloudForm"
ref="httpForm"
/>
</template>
</template>
<template v-if="adrType === DISCOVERY_CATEGORY_TYPE.COMPONENT">
<div class="attr-ad-header">{{ $t('cmdb.ciType.portScanConfig') }}</div>
<PortScanConfig v-model="portScanConfigForm" />
</template>
<AttrADTest
:adtId="currentAdt.id"
/>
@@ -194,7 +171,8 @@ import { v4 as uuidv4 } from 'uuid'
import { mapState } from 'vuex'
import Vcrontab from '@/components/Crontab'
import { putCITypeDiscovery, postCITypeDiscovery } from '../../api/discovery'
import { PRIVATE_CLOUD_NAME } from '@/modules/cmdb/views/discovery/constants.js'
import { DISCOVERY_CATEGORY_TYPE, PRIVATE_CLOUD_NAME } from '@/modules/cmdb/views/discovery/constants.js'
import { TAB_KEY } from './attrAD/constants.js'
import HttpSnmpAD from '../../components/httpSnmpAD'
import AttrMapTable from '@/modules/cmdb/components/attrMapTable/index.vue'
@@ -202,6 +180,10 @@ import CMDBExprDrawer from '@/components/CMDBExprDrawer'
import NodeSetting from '@/modules/cmdb/components/nodeSetting/index.vue'
import AttrADTest from './attrADTest.vue'
import { Popover } from 'element-ui'
import VcenterForm from './attrAD/privateCloud/vcenterForm.vue'
import PublicCloud from './attrAD/publicCloud/index.vue'
import PortScanConfig from './attrAD/portScanConfig/index.vue'
import CIDRTags from './attrAD/cidrTags/index.vue'
export default {
name: 'AttrADTabpane',
@@ -212,7 +194,11 @@ export default {
NodeSetting,
AttrMapTable,
AttrADTest,
ElPopover: Popover
ElPopover: Popover,
VcenterForm,
PublicCloud,
PortScanConfig,
CIDRTags
},
props: {
adr_id: {
@@ -253,9 +239,11 @@ export default {
query_expr: '',
enabled: true,
},
form2: {
publicCloudForm: {
key: '',
secret: '',
_reference: '',
tabActive: TAB_KEY.CUSTOM,
},
privateCloudForm: {
host: '',
@@ -263,26 +251,41 @@ export default {
password: '',
// insecure: false,
vcenterName: '',
_reference: '',
tabActive: TAB_KEY.CUSTOM,
},
portScanConfigForm: {
cidr: '',
ports: '',
enable_cidr: '',
},
interval: 'cron', // interval cron
cron: '',
cronVisible: false,
intervalValue: 3,
agent_type: 'agent_id',
nodes: [
{
id: uuidv4(),
ip: '',
community: '',
community: 'public',
version: '',
},
],
form3: this.$form.createForm(this, { name: 'snmp_form' }),
cronVisible: false,
nodeSettingForm: this.$form.createForm(this, { name: 'snmp_form' }),
uniqueKey: '',
isPrivateCloud: false,
privateCloudName: '',
PRIVATE_CLOUD_NAME,
DISCOVERY_CATEGORY_TYPE,
isClient: false, // 是否前端新增临时数据
cidrList: [],
}
},
provide() {
return {
provide_labelCol: () => {
return this.labelCol
},
}
},
computed: {
@@ -306,26 +309,28 @@ export default {
]
const permissions = this?.user?.roles?.permissions
if ((permissions.includes('cmdb_admin') || permissions.includes('admin')) && this.adrType === 'agent') {
if ((permissions.includes('cmdb_admin') || permissions.includes('admin')) && this.adrType === DISCOVERY_CATEGORY_TYPE.AGENT) {
radios.unshift({ value: 'all', label: this.$t('cmdb.ciType.allNodes') })
}
if (this.adrType !== 'agent' || this?.currentAdr?.is_plugin) {
if (this.adrType !== DISCOVERY_CATEGORY_TYPE.AGENTv || this?.currentAdr?.is_plugin) {
radios.unshift({ value: 'master', label: this.$t('cmdb.ciType.masterNode') })
}
return radios
},
radioList() {
return [
{ value: 'interval', label: this.$t('cmdb.ciType.byInterval') },
{ value: 'cron', label: '按cron', layout: 'vertical' },
]
},
labelCol() {
const span = this.$i18n.locale === 'en' ? 5 : 3
const isEn = this.$i18n.locale === 'en'
return {
span
xl: {
span: isEn ? 4 : 2
},
lg: {
span: isEn ? 5 : 3
},
sm: {
span: isEn ? 6 : 4
}
}
}
},
@@ -337,7 +342,7 @@ export default {
this.uniqueKey = _find?.unique_key ?? ''
this.isClient = _findADT?.isClient ?? false
if (this.adrType === 'http') {
if (this.adrType === DISCOVERY_CATEGORY_TYPE.HTTP) {
const {
category = undefined,
key = '',
@@ -346,49 +351,85 @@ export default {
account = '',
password = '',
// insecure = false,
vcenterName = ''
vcenterName = '',
_reference = ''
} = _findADT?.extra_option ?? {}
if (_find?.option?.category === 'private_cloud') {
this.isPrivateCloud = true
this.privateCloudName = _find?.option?.en || ''
if (this.privateCloudName === PRIVATE_CLOUD_NAME.VCenter) {
this.privateCloudForm = {
host,
account,
password,
// insecure,
vcenterName,
}
switch (this.privateCloudName) {
case PRIVATE_CLOUD_NAME.VCenter:
this.privateCloudForm = {
host,
account,
password,
// insecure,
vcenterName,
_reference,
tabActive: _reference ? TAB_KEY.CONFIG : TAB_KEY.CUSTOM
}
break
default:
break
}
} else {
this.isPrivateCloud = false
this.form2 = {
this.publicCloudForm = {
key,
secret,
_reference,
tabActive: _reference ? TAB_KEY.CONFIG : TAB_KEY.CUSTOM
}
}
this.$refs.httpSnmpAd.setCurrentCate(category)
this.$nextTick(() => {
this.$refs.httpForm.init(this.adr_id)
})
}
if (this.adrType === 'snmp') {
this.nodes = _findADT?.extra_option?.nodes?.length ? _findADT?.extra_option?.nodes : [
if (this.adrType === DISCOVERY_CATEGORY_TYPE.COMPONENT) {
const {
cidr = '',
ports = '',
enable_cidr = '',
} = _findADT?.extra_option ?? {}
this.portScanConfigForm = {
cidr,
ports,
enable_cidr
}
}
if (this.adrType === DISCOVERY_CATEGORY_TYPE.SNMP) {
const nodes = _findADT?.extra_option?.nodes?.length ? _findADT?.extra_option?.nodes : [
{
id: uuidv4(),
ip: '',
community: '',
community: 'public',
version: '',
},
]
this.nodes = nodes
this.$nextTick(() => {
this.$refs.nodeSetting.initNodesFunc()
this.$nextTick(() => {
this.$refs.nodeSetting.setNodeField()
})
})
let cidrList = []
const cidr = _findADT?.extra_option?.cidr
if (Array.isArray(cidr) && cidr?.length) {
cidrList = cidr.map((v) => {
return {
id: uuidv4(),
value: v?.value ? v.value : v
}
})
}
this.cidrList = cidrList
}
if (this.adrType === 'agent') {
if (this.adrType === DISCOVERY_CATEGORY_TYPE.AGENT) {
this.tableData = (_find?.attributes || []).map((item) => {
if (_findADT.attributes) {
return {
@@ -413,15 +454,19 @@ export default {
query_expr: _findADT.query_expr || '',
enabled: _findADT?.enabled ?? true,
}
const allMachineIndex = this.agentTypeRadioList.findIndex((item) => item.value === 'all')
if (_findADT.query_expr) {
this.agent_type = 'query_expr'
} else if (_findADT.agent_id) {
this.agent_type = _findADT.agent_id === '0x0000' ? 'master' : 'agent_id'
} else if (_findADT.agent_id === '' && allMachineIndex !== -1) {
this.agent_type = 'all'
} else {
this.agent_type = this.agentTypeRadioList[0].value
}
this.interval = 'cron'
this.cron = _findADT?.cron || ''
},
@@ -432,19 +477,10 @@ export default {
const { currentAdt } = this
let params
const isError = this.validateForm()
if (isError) {
return
}
if (this.adrType === 'http') {
let cloudOption = {}
if (this.isPrivateCloud) {
if (this.privateCloudName === PRIVATE_CLOUD_NAME.VCenter) {
cloudOption = this.privateCloudForm
}
} else {
cloudOption = this.form2
if (this.adrType === DISCOVERY_CATEGORY_TYPE.HTTP) {
const { isError, data: cloudOption } = this.validateHTTPForm()
if (isError) {
return
}
params = {
@@ -454,12 +490,25 @@ export default {
},
}
}
if (this.adrType === 'snmp') {
if (this.adrType === DISCOVERY_CATEGORY_TYPE.COMPONENT) {
const portScanConfigForm = _.omitBy(this.portScanConfigForm, _.isEmpty) || {}
params = {
extra_option: { nodes: this.$refs.nodeSetting?.getNodeValue() ?? [] },
extra_option: {
...portScanConfigForm,
},
}
}
if (this.adrType === 'agent') {
if (this.adrType === DISCOVERY_CATEGORY_TYPE.SNMP) {
params = {
extra_option: {
nodes: this.$refs.nodeSetting?.getNodeValue() ?? [],
cidr: this?.cidrList?.map((item) => item.value) || []
},
}
}
if (this.adrType === DISCOVERY_CATEGORY_TYPE.AGENT) {
const $table = this.$refs.attrMapTable
const { fullData: _tableData } = $table.getTableData()
const attributes = {}
@@ -490,7 +539,7 @@ export default {
...params,
...this.form,
adr_id: currentAdt.adr_id,
cron: this.interval === 'cron' ? this.cron : null,
cron: this.cron,
}
if (this.agent_type === 'agent_id' || this.agent_type === 'all') {
@@ -525,8 +574,9 @@ export default {
}
}
// 去除合并后的旧配置
if (params.extra_option) {
params.extra_option = _.omit(params.extra_option, 'insecure')
params.extra_option = this.handleOldExtraOption(params.extra_option)
}
if (currentAdt?.isClient) {
@@ -542,42 +592,96 @@ export default {
}
},
validateForm() {
/**
* HTTP 表单校验
* 公有云 私有云
*/
validateHTTPForm() {
let isError = false
let data = {}
if (this.adrType === 'http') {
if (this.isPrivateCloud) {
if (this.privateCloudName === PRIVATE_CLOUD_NAME.VCenter) {
const vcenterErros = {
'host': `${this.$t('placeholder1')} ${this.$t('cmdb.ciType.host')}`,
'account': `${this.$t('placeholder1')} ${this.$t('cmdb.ciType.account')}`,
'password': `${this.$t('placeholder1')} ${this.$t('cmdb.ciType.password')}`
}
const findError = Object.keys(this.privateCloudForm).find((key) => !this.privateCloudForm[key] && vcenterErros[key])
if (findError) {
isError = true
this.$message.error(this.$t(vcenterErros[findError]))
}
}
} else {
const publicCloudErros = {
'key': `${this.$t('placeholder1')} key`,
'secret': `${this.$t('placeholder1')} secret`
}
const findError = Object.keys(this.form2).find((key) => !this.form2[key] && publicCloudErros[key])
if (findError) {
isError = true
this.$message.error(this.$t(publicCloudErros[findError]))
}
const formData = this?.[this.isPrivateCloud ? 'privateCloudForm' : 'publicCloudForm']
if (formData.tabActive === TAB_KEY.CONFIG) {
if (!formData._reference) {
isError = true
this.$message.error(this.$t('cmdb.ad.configErrTip'))
}
data._reference = formData._reference
if (this.privateCloudName === PRIVATE_CLOUD_NAME.VCenter) {
data.vcenterName = formData.vcenterName
}
return {
isError,
data
}
}
return isError
if (this.isPrivateCloud) {
if (this.privateCloudName === PRIVATE_CLOUD_NAME.VCenter) {
data = _.pick(this.privateCloudForm, ['host', 'account', 'password', 'vcenterName'])
const vcenterErros = {
'host': `${this.$t('placeholder1')} ${this.$t('cmdb.ciType.host')}`,
'account': `${this.$t('placeholder1')} ${this.$t('cmdb.ciType.account')}`,
'password': `${this.$t('placeholder1')} ${this.$t('cmdb.ciType.password')}`
}
const findError = Object.keys(this.privateCloudForm).find((key) => !this.privateCloudForm[key] && vcenterErros[key])
if (findError) {
isError = true
this.$message.error(this.$t(vcenterErros[findError]))
}
}
} else {
data = _.pick(this.publicCloudForm, ['key', 'secret'])
const publicCloudErros = {
'key': `${this.$t('placeholder1')} key`,
'secret': `${this.$t('placeholder1')} secret`
}
const findError = Object.keys(this.publicCloudForm).find((key) => !this.publicCloudForm[key] && publicCloudErros[key])
if (findError) {
isError = true
this.$message.error(this.$t(publicCloudErros[findError]))
}
}
return {
isError,
data
}
},
/**
* 去除多余旧配置
*/
handleOldExtraOption(option) {
let extra_option = _.cloneDeep(option)
// VCenter 旧配置
if (extra_option?.insecure) {
Reflect.deleteProperty(extra_option, 'insecure')
}
// 根据 HTTP 选项去除多余属性
const formData = this?.[this.isPrivateCloud ? 'privateCloudForm' : 'publicCloudForm']
switch (formData.tabActive) {
case TAB_KEY.CUSTOM:
Reflect.deleteProperty(extra_option, '_reference')
break
case TAB_KEY.CONFIG:
extra_option = _.omit(extra_option, ['host', 'account', 'password', 'key', 'secret'])
break
default:
break
}
return extra_option
},
handleOpenCmdb() {
this.$refs.cmdbDrawer.open()
},
copySuccess(text) {
this.form = {
...this.form,
@@ -653,6 +757,7 @@ export default {
.radio-master-tip {
font-size: 12px;
color: #86909c;
line-height: 14px;
}
}
.attr-ad-snmp-form {

View File

@@ -345,6 +345,7 @@
:canDefineScript="canDefineScript"
ref="preValueArea"
:disabled="isShowComputedArea"
:CITypeId="CITypeId"
/>
</a-form-item>
</a-col>
@@ -375,6 +376,10 @@
name="is_computed"
v-decorator="['is_computed', { rules: [], valuePropName: 'checked' }]"
/>
<div v-show="isShowComputedArea" class="computed-attr-tip">
<div>1. {{ $t('cmdb.ciType.computedAttrTip1') }}</div>
<div>2. {{ $t('cmdb.ciType.computedAttrTip2') }}</div>
</div>
<ComputedArea
showCalcComputed
ref="computedArea"
@@ -795,7 +800,13 @@ export default {
}
</script>
<style lang="less" scoped></style>
<style lang="less" scoped>
.computed-attr-tip {
font-size: 12px;
line-height: 22px;
color: #a5a9bc;
}
</style>
<style lang="less">
.attribute-edit-form {
.jsoneditor-outer {

View File

@@ -18,19 +18,19 @@
<a-button @click="handleOpenUniqueConstraint" size="small">{{ $t('cmdb.ciType.uniqueConstraint') }}</a-button>
<div>
<a-tooltip
v-for="type in Object.keys(valueTypeMap)"
:key="type"
:title="$t('cmdb.ciType.filterTips', { name: valueTypeMap[type] })"
v-for="typeKey in Object.keys(valueTypeMap)"
:key="typeKey"
:title="$t('cmdb.ciType.filterTips', { name: valueTypeMap[typeKey] })"
>
<span
@click="handleFilterType(type)"
@click="handleFilterType(typeKey)"
:class="{
'ci-types-attributes-filter': true,
'ci-types-attributes-filter-selected': attrTypeFilter.includes(type),
'ci-types-attributes-filter-selected': attrTypeFilter.includes(typeKey),
}"
>
<ops-icon :type="getPropertyIcon({ value_type: type })" />
{{ valueTypeMap[type] }}
<ops-icon :type="getPropertyIcon({ value_type: typeKey })" />
{{ valueTypeMap[typeKey] }}
</span>
</a-tooltip>
</div>
@@ -255,6 +255,12 @@ export default {
show_id: () => {
return this.show_id
},
providerGroupsData: () => {
return {
CITypeGroups: this.CITypeGroups,
otherGroupAttributes: this.otherGroupAttributes
}
}
}
},
beforeCreate() {},

View File

@@ -333,7 +333,12 @@
</a-col>
<a-col :span="24" v-if="!['6', '7'].includes(currentValueType)">
<a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }" :label="$t('cmdb.ciType.choiceValue')">
<PreValueArea ref="preValueArea" :canDefineScript="canDefineScript" :disabled="isShowComputedArea" />
<PreValueArea
ref="preValueArea"
:canDefineScript="canDefineScript"
:disabled="isShowComputedArea"
:CITypeId="CITypeId"
/>
</a-form-item>
</a-col>
<a-col :span="24" v-if="!['6', '7'].includes(currentValueType)">
@@ -363,6 +368,10 @@
name="is_computed"
v-decorator="['is_computed', { rules: [], valuePropName: 'checked' }]"
/>
<div v-show="isShowComputedArea" class="computed-attr-tip">
<div>1. {{ $t('cmdb.ciType.computedAttrTip1') }}</div>
<div>2. {{ $t('cmdb.ciType.computedAttrTip2') }}</div>
</div>
<ComputedArea ref="computedArea" v-if="isShowComputedArea" :canDefineComputed="canDefineComputed" />
</a-form-item>
</a-col>
@@ -398,6 +407,10 @@ export default {
type: Boolean,
default: true,
},
CITypeId: {
type: Number,
default: null
}
},
data() {
return {
@@ -610,6 +623,13 @@ export default {
},
}
</script>
<style lang="less" scoped>
.computed-attr-tip {
font-size: 12px;
line-height: 22px;
color: #a5a9bc;
}
</style>
<style lang="less">
.create-new-attribute {
.jsoneditor-outer {

View File

@@ -32,6 +32,8 @@ import TriggerTable from './triggerTable.vue'
import ADTab from './adTab.vue'
import GrantComp from '../../components/cmdbGrant/grantComp.vue'
const ACTIVE_KEY_STORAGE_KEY = 'ops_model_config_tab_key'
export default {
name: 'CITypeDetail',
components: {
@@ -53,11 +55,24 @@ export default {
},
data() {
return {
activeKey: '1',
activeKey: localStorage.getItem(ACTIVE_KEY_STORAGE_KEY) || '1',
}
},
beforeCreate() {},
mounted() {},
mounted() {
this.$nextTick(() => {
switch (this.activeKey) {
case '6':
this.$refs.triggerTable.getTableData()
break
case '5':
this.$refs.reconciliationTable.getTableData()
break
default:
break
}
})
},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
@@ -66,15 +81,23 @@ export default {
methods: {
changeTab(activeKey) {
this.activeKey = activeKey
localStorage.setItem(ACTIVE_KEY_STORAGE_KEY, activeKey)
this.$nextTick(() => {
if (activeKey === '1') {
this.$refs.attributesTable.getCITypeGroupData()
}
if (activeKey === '5') {
this.$refs.triggerTable.getTableData()
switch (activeKey) {
case '1':
this.$refs.attributesTable.getCITypeGroupData()
break
case '6':
this.$refs.triggerTable.getTableData()
break
case '5':
this.$refs.reconciliationTable.getTableData()
break
default:
break
}
})
},
}
},
}
</script>

View File

@@ -1,38 +1,49 @@
<template>
<a-tabs v-model="activeKey" size="small" :tabBarStyle="{ borderBottom: 'none' }">
<a-tabs v-model="activeKey" size="small" :tabBarStyle="{ borderBottom: 'none' }" @change="handleTabsChange">
<a-tab-pane key="expr" :disabled="!canDefineComputed">
<span style="font-size:12px;" slot="tab">{{ $t('cmdb.ciType.expr') }}</span>
<a-textarea v-model="compute_expr" :placeholder="`{{a}}+{{b}}`" :rows="2" :disabled="!canDefineComputed" />
</a-tab-pane>
<a-tab-pane key="script" :disabled="!canDefineComputed">
<span style="font-size:12px;" slot="tab">{{ $t('cmdb.ciType.code') }}</span>
<codemirror
style="z-index: 9999"
:options="cmOptions"
v-model="compute_script"
@input="onCodeChange"
></codemirror>
<CustomCodeMirror
codeMirrorId="cmdb-computed-attr"
ref="codemirror"
@changeCodeContent="onCodeChange"
></CustomCodeMirror>
</a-tab-pane>
<template slot="tabBarExtraContent" v-if="showCalcComputed">
<a-button type="primary" size="small" @click="handleCalcComputed">
{{ $t('cmdb.ciType.apply') }}
<template slot="tabBarExtraContent">
<a-button size="small" @click="showAllPropDrawer">
{{ $t('cmdb.ciType.viewAllAttr') }}
</a-button>
<a-tooltip :title="$t('cmdb.ciType.computeForAllCITips')">
<a-icon type="question-circle" style="margin-left:5px" />
</a-tooltip>
<AllAttrDrawer ref="allAttrDrawer" />
<template v-if="showCalcComputed">
<a-button style="margin: 0px 5px;" type="primary" size="small" @click="handleCalcComputed">
{{ $t('cmdb.ciType.apply') }}
</a-button>
<a-tooltip :title="$t('cmdb.ciType.computeForAllCITips')">
<a-icon type="question-circle" />
</a-tooltip>
</template>
</template>
</a-tabs>
</template>
<script>
import { codemirror } from 'vue-codemirror'
import AllAttrDrawer from './allAttrDrawer.vue'
import CustomCodeMirror from '@/components/CustomCodeMirror'
import 'codemirror/lib/codemirror.css'
import 'codemirror/theme/monokai.css'
require('codemirror/mode/python/python.js')
export default {
name: 'ComputedArea',
components: { codemirror },
components: {
CustomCodeMirror,
AllAttrDrawer
},
props: {
canDefineComputed: {
type: Boolean,
@@ -48,33 +59,6 @@ export default {
activeKey: 'expr', // expr script
compute_expr: '',
compute_script: 'def computed(): \n return',
cmOptions: {
lineNumbers: true,
mode: 'python',
height: '200px',
theme: 'monokai',
tabSize: 4,
indentUnit: 4,
lineWrapping: false,
readOnly: !this.canDefineComputed,
extraKeys: {
Tab: (cm) => {
if (cm.somethingSelected()) {
cm.indentSelection('add')
} else {
cm.replaceSelection(Array(cm.getOption('indentUnit') + 1).join(' '), 'end', '+input')
}
},
'Shift-Tab': (cm) => {
if (cm.somethingSelected()) {
cm.indentSelection('subtract')
} else {
const cursor = cm.getCursor()
cm.setCursor({ line: cursor.line, ch: cursor.ch - 4 })
}
},
},
},
}
},
methods: {
@@ -92,6 +76,9 @@ export default {
this.compute_script = compute_script || 'def computed(): \n return'
if (compute_script) {
this.activeKey = 'script'
this.$nextTick(() => {
this.$refs.codemirror.initCodeMirror(this.compute_script)
})
} else {
this.activeKey = 'expr'
}
@@ -108,6 +95,18 @@ export default {
},
onCodeChange(v) {
this.compute_script = v.replace('\t', ' ')
},
showAllPropDrawer() {
this.$refs.allAttrDrawer.open()
},
handleTabsChange(activeKey) {
console.log('handleTabsChange', activeKey)
if (activeKey === 'script') {
this.$nextTick(() => {
this.$refs.codemirror.initCodeMirror(this.compute_script)
})
}
}
},
}

View File

@@ -28,7 +28,7 @@
<a-tabs v-model="activeKey">
<a-tab-pane key="1" :tab="$t('cmdb.ciType.addAttribute')">
<div :style="{ overflow: 'auto', maxHeight: '480px' }">
<create-new-attribute ref="createNewAttribute" :hasFooter="false" @done="handleAddNewAttr" />
<create-new-attribute ref="createNewAttribute" :hasFooter="false" :CITypeId="CITypeId" @done="handleAddNewAttr" />
</div>
</a-tab-pane>
<a-tab-pane key="2" :tab="$t('cmdb.ciType.existedAttributes')" force-render>

View File

@@ -61,12 +61,12 @@
<a-tab-pane key="choice_other" :disabled="disabled">
<span style="font-size:12px;" slot="tab">{{ $t('cmdb.ciType.choiceOther') }}</span>
<a-row :gutter="[24, 24]">
<a-col :span="12">
<a-col :span="24">
<a-form-item
:style="{ lineHeight: '24px', marginBottom: '5px' }"
:label="$t('cmdb.ciType.ciType')"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 20 }"
:label-col="{ span: 3 }"
:wrapper-col="{ span: 12 }"
>
<treeselect
:disable-branch-nodes="true"
@@ -117,12 +117,12 @@
</treeselect>
</a-form-item>
</a-col>
<a-col :span="12" v-if="choice_other.type_ids && choice_other.type_ids.length">
<a-col :span="24" v-if="choice_other.type_ids && choice_other.type_ids.length">
<a-form-item
:style="{ marginBottom: '5px' }"
:label="$t('cmdb.ciType.attributes')"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 20 }"
:label-col="{ span: 3 }"
:wrapper-col="{ span: 12 }"
>
<treeselect
:disable-branch-nodes="true"
@@ -162,15 +162,17 @@
:style="{ marginBottom: '5px' }"
class="pre-value-filter"
:label="$t('cmdb.ciType.filter')"
:label-col="{ span: 2 }"
:wrapper-col="{ span: 22 }"
:label-col="{ span: 3 }"
:wrapper-col="{ span: 19 }"
>
<FilterComp
ref="filterComp"
<AttrFilter
ref="attrFilter"
:isDropdown="false"
:canSearchPreferenceAttrList="typeAttrs"
@setExpFromFilter="setExpFromFilter"
:CITypeId="CITypeId"
:expression="filterExp ? `q=${filterExp}` : ''"
:curModelAttrList="curModelAttrList"
@setExpFromFilter="setExpFromFilter"
/>
</a-form-item>
</a-col>
@@ -178,6 +180,42 @@
</a-tab-pane>
<a-tab-pane key="script" :disabled="disabled || !canDefineScript">
<span style="font-size:12px;" slot="tab">{{ $t('cmdb.ciType.code') }}</span>
<a-form-item
:style="{ marginBottom: '5px' }"
:label="$t('cmdb.ciType.cascadeAttr')"
:label-col="{ span: 3 }"
:wrapper-col="{ span: 19 }"
:extra="scriptCodeExtraText"
labelAlign="left"
>
<a-select
mode="multiple"
style="width: 100%"
placeholder="Please select"
optionFilterProp="title"
v-model="cascade_attributes"
>
<a-select-option
v-for="attr in curModelAttrList"
:key="attr.id"
:title="attr.name"
>
{{ attr.name }}
</a-select-option>
</a-select>
</a-form-item>
<div class="script-tip">
<div>1. {{ $t('cmdb.ciType.computedAttrTip1') }}</div>
<div>2. {{ $t('cmdb.ciType.computedAttrTip2') }}</div>
</div>
<a-button size="small" @click="showAllPropDrawer">
{{ $t('cmdb.ciType.viewAllAttr') }}
</a-button>
<AllAttrDrawer ref="allAttrDrawer" />
<CustomCodeMirror
codeMirrorId="cmdb-pre-value"
ref="codemirror"
@@ -196,16 +234,18 @@ import { defautValueColor } from '../../utils/const'
import ColorPicker from '../../components/colorPicker/index.vue'
import Webhook from '../../components/webhook'
import { getCITypeGroups } from '../../api/ciTypeGroup'
import { getCITypeCommonAttributesByTypeIds } from '../../api/CITypeAttr'
import FilterComp from '@/components/CMDBFilterComp'
import { getCITypeCommonAttributesByTypeIds, getCITypeAttributesById } from '../../api/CITypeAttr'
import AttrFilter from './preValueAttr/attrFilter/index.vue'
import AllAttrDrawer from './allAttrDrawer.vue'
import CustomCodeMirror from '@/components/CustomCodeMirror'
import 'codemirror/lib/codemirror.css'
import 'codemirror/theme/monokai.css'
require('codemirror/mode/python/python.js')
export default {
name: 'PreValueArea',
components: { draggable, PreValueTag, ColorPicker, Webhook, FilterComp, CustomCodeMirror },
components: { draggable, PreValueTag, ColorPicker, Webhook, AttrFilter, CustomCodeMirror, AllAttrDrawer },
props: {
disabled: {
type: Boolean,
@@ -215,6 +255,10 @@ export default {
type: Boolean,
default: false,
},
CITypeId: {
type: Number,
default: null,
},
},
data() {
return {
@@ -242,6 +286,13 @@ export default {
lineWrapping: true,
readOnly: this.disabled || !this.canDefineScript,
},
curModelAttrList: [], // 当前模型属性
cascade_attributes: [] // 级联属性id列表
}
},
computed: {
scriptCodeExtraText() {
return this.$t('cmdb.ciType.cascadeAttrTip') + (this.isOpenSource ? ` (${this.$t('cmdb.enterpriseVersionTip')})` : '')
}
},
watch: {
@@ -276,8 +327,18 @@ export default {
return { ..._.cloneDeep(item) }
})
})
this.getCITypeAttributesById()
},
methods: {
async getCITypeAttributesById() {
const res = await getCITypeAttributesById(this.CITypeId)
let curModelAttrList = []
if (res?.attributes?.length) {
curModelAttrList = res.attributes.filter(attr => !attr.is_password)
}
this.curModelAttrList = curModelAttrList
},
addNewValue(newValue, newStyle, newIcon) {
if (newValue) {
const idx = this.valueList.findIndex((v) => v[0] === newValue)
@@ -321,12 +382,13 @@ export default {
choice_web_hook: null,
choice_other: {
script: this.script,
cascade_attributes: this.cascade_attributes,
},
}
} else {
let choice_other = {}
if (this.choice_other.type_ids && this.choice_other.type_ids.length) {
this.$refs.filterComp.handleSubmit()
this.$refs.attrFilter.handleSubmit()
choice_other = { ...this.choice_other, filter: this.filterExp }
}
return {
@@ -355,9 +417,10 @@ export default {
const { type_ids, attr_id, filter } = choice_other
this.choice_other = { type_ids, attr_id }
this.filterExp = filter
this.cascade_attributes = choice_other?.cascade_attributes || []
if (type_ids && type_ids.length) {
this.$nextTick(() => {
this.$refs.filterComp.visibleChange(true, false)
this.$refs.attrFilter.init(true, false)
})
}
}
@@ -390,6 +453,10 @@ export default {
})
}
},
showAllPropDrawer() {
this.$refs.allAttrDrawer.open()
},
},
}
</script>
@@ -408,6 +475,12 @@ export default {
margin: 5px;
}
}
.script-tip {
font-size: 12px;
line-height: 22px;
color: #a5a9bc;
}
</style>
<style lang="less">

View File

@@ -0,0 +1,321 @@
<template>
<div>
<a-space :style="{ display: 'flex', marginBottom: '10px' }" v-for="(item, index) in ruleList" :key="item.id">
<div v-if="ruleList.length > 1" :style="{ width: '60px', height: rowHeight, position: 'relative' }">
<treeselect
v-if="index !== 0"
class="custom-treeselect"
:style="{ width: '60px', '--custom-height': rowHeight, position: 'absolute', top: '-24px' }"
v-model="item.type"
:multiple="false"
:clearable="false"
searchable
:options="ruleTypeList"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
:disabled="disabled"
>
</treeselect>
</div>
<treeselect
class="custom-treeselect"
:style="{ width: '120px', '--custom-height': rowHeight }"
v-model="item.property"
:multiple="false"
:clearable="false"
searchable
:options="canSearchPreferenceAttrList"
:normalizer="
(node) => {
return {
id: node.name,
label: node.alias || node.name,
children: node.children,
}
}
"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
<div
v-if="node.id !== '$count'"
:title="node.label"
slot="option-label"
slot-scope="{ node }"
class="property-label"
>
<ValueTypeMapIcon :attr="node.raw" />
{{ node.label }}
</div>
<div
v-else
:title="node.label"
slot="option-label"
slot-scope="{ node }"
class="property-label"
:style="{ borderBottom: '1px solid #E4E7ED', marginBottom: '8px' }"
>
<ValueTypeMapIcon :attr="node.raw" />
{{ node.label }}
</div>
<div
class="property-label"
slot="value-label"
slot-scope="{ node }"
>
<ValueTypeMapIcon :attr="node.raw" /> {{ node.label }}
</div>
</treeselect>
<treeselect
class="custom-treeselect"
:style="{ width: '90px', '--custom-height': rowHeight }"
v-model="item.exp"
:multiple="false"
:clearable="false"
searchable
:options="getExpListByProperty(item.property)"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
@select="(value) => handleChangeExp(value, item, index)"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
</treeselect>
<ValueControls
:rule="ruleList[index]"
:attrList="canSearchPreferenceAttrList"
:disabled="disabled"
:curModelAttrList="curModelAttrList"
:rowHeight="rowHeight"
@change="(value) => handleChangeValue(value, index)"
/>
<template v-if="!disabled">
<a-tooltip :title="$t('copy')">
<a class="operation" @click="handleCopyRule(item)"><ops-icon type="veops-copy"/></a>
</a-tooltip>
<a-tooltip :title="$t('delete')">
<a class="operation" @click="handleDeleteRule(item)"><a-icon type="minus-circle"/></a>
</a-tooltip>
<a-tooltip :title="$t('cmdbFilterComp.addHere')">
<a class="operation" @click="handleAddRuleAt(item)"><a-icon type="plus-circle"/></a>
</a-tooltip>
</template>
</a-space>
<div class="table-filter-add" v-if="!disabled && ruleList.length === 0">
<a @click="handleAddRule">+ {{ $t('new') }}</a>
</div>
<div class="attr-filter-tip">{{ $t('cmdb.ciType.attrFilterTip') }}{{ isOpenSource ? ` (${$t('cmdb.enterpriseVersionTip')})` : '' }}</div>
</div>
</template>
<script>
import _ from 'lodash'
import { v4 as uuidv4 } from 'uuid'
import { ruleTypeList, expList, advancedExpList, compareTypeList } from '../constants.js'
import ValueTypeMapIcon from '@/components/CMDBValueTypeMapIcon'
import ValueControls from './valueControls.vue'
export default {
name: 'Expression',
components: {
ValueTypeMapIcon,
ValueControls
},
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: Array,
default: () => [],
},
canSearchPreferenceAttrList: {
type: Array,
required: true,
default: () => [],
},
disabled: {
type: Boolean,
default: false,
},
curModelAttrList: {
type: Array,
default: () => []
}
},
data() {
return {
compareTypeList,
rowHeight: '36px'
}
},
computed: {
ruleList: {
get() {
return this.value
},
set(val) {
this.$emit('change', val)
return val
},
},
ruleTypeList() {
return ruleTypeList()
},
expList() {
return expList()
},
advancedExpList() {
return advancedExpList()
},
},
methods: {
getExpListByProperty(property) {
if (property === '$count') {
return [
{ value: 'is', label: this.$t('cmdbFilterComp.is') },
{ value: '~is', label: this.$t('cmdbFilterComp.~is') },
{ value: 'compare', label: this.$t('cmdbFilterComp.compare') }
]
}
if (property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find && ['0', '1', '3', '4', '5'].includes(_find.value_type)) {
return [
{ value: 'is', label: this.$t('cmdbFilterComp.is') },
{ value: '~is', label: this.$t('cmdbFilterComp.~is') },
{ value: '~value', label: this.$t('cmdbFilterComp.~value') }, // 为空的定义有点绕
{ value: 'value', label: this.$t('cmdbFilterComp.value') },
...this.advancedExpList
]
}
}
return [...this.expList, ...this.advancedExpList]
},
isChoiceByProperty(property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find) {
return _find.is_choice
}
return false
},
handleAddRule() {
this.ruleList.push({
id: uuidv4(),
type: 'and',
property: this.canSearchPreferenceAttrList[0]?.name,
exp: 'is',
value: null,
})
this.$emit('change', this.ruleList)
},
handleCopyRule(item) {
this.ruleList.push({ ...item, id: uuidv4() })
this.$emit('change', this.ruleList)
},
handleDeleteRule(item) {
const idx = this.ruleList.findIndex((r) => r.id === item.id)
if (idx > -1) {
this.ruleList.splice(idx, 1)
}
this.$emit('change', this.ruleList)
},
handleAddRuleAt(item) {
const idx = this.ruleList.findIndex((r) => r.id === item.id)
if (idx > -1) {
this.ruleList.splice(idx + 1, 0, {
id: uuidv4(),
type: 'and',
property: this.canSearchPreferenceAttrList[0]?.name,
exp: 'is',
value: null,
})
}
this.$emit('change', this.ruleList)
},
getChoiceValueByProperty(property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find) {
return _find.choice_value
}
return []
},
handleChangeExp({ value }, item, index) {
const _ruleList = _.cloneDeep(this.ruleList)
if (value === 'range') {
_ruleList[index] = {
..._ruleList[index],
min: '',
max: '',
exp: value,
}
} else if (value === 'compare') {
_ruleList[index] = {
..._ruleList[index],
compareType: '1',
exp: value,
}
} else {
_ruleList[index] = {
..._ruleList[index],
exp: value,
}
}
this.ruleList = _ruleList
this.$emit('change', this.ruleList)
},
handleChangeValue(value, index) {
const _ruleList = _.cloneDeep(this.ruleList)
_ruleList[index] = value
this.$emit('change', _ruleList)
}
},
}
</script>
<style lang="less" scoped>
.input-group {
display: flex;
align-items: center;
width: 150px;
&-range-icon {
margin: 0 8px;
}
input {
height: 36px;
}
}
.property-label {
width: 100%;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden
}
.attr-filter-tip {
color: #86909C;
font-size: 12px;
font-weight: 400;
}
</style>

View File

@@ -0,0 +1,211 @@
<template>
<div>
<Expression
v-model="ruleList"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)"
:disabled="false"
:curModelAttrList="curModelAttrList"
/>
</div>
</template>
<script>
import { v4 as uuidv4 } from 'uuid'
import { compareTypeList } from '../constants.js'
import Expression from './expression.vue'
export default {
name: 'AttrFilter',
components: {
Expression
},
props: {
canSearchPreferenceAttrList: {
type: Array,
required: true,
default: () => [],
},
expression: {
type: String,
default: '',
},
regQ: {
type: String,
default: '(?<=q=).+(?=&)|(?<=q=).+$',
},
CITypeId: {
type: Number,
default: null,
},
curModelAttrList: {
type: Array,
default: () => []
}
},
data() {
return {
compareTypeList,
visible: false,
ruleList: [],
filterExp: '',
}
},
methods: {
init(open, isInitOne = true) {
// isInitOne 初始化exp为空时ruleList是否默认给一条
// const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
const exp = this.expression.match(new RegExp(this.regQ, 'g'))
? this.expression.match(new RegExp(this.regQ, 'g'))[0]
: null
if (open && exp) {
const expArray = exp.split(',').map((item) => {
let has_not = ''
const key = item.split(':')[0]
const val = item
.split(':')
.slice(1)
.join(':')
let type, property, exp, value, min, max, compareType
if (key.includes('-')) {
type = 'or'
if (key.includes('~')) {
property = key.substring(2)
has_not = '~'
} else {
property = key.substring(1)
}
} else {
type = 'and'
if (key.includes('~')) {
property = key.substring(1)
has_not = '~'
} else {
property = key
}
}
const in_reg = /(?<=\().+(?=\))/g
const range_reg = /(?<=\[).+(?=\])/g
const compare_reg = /(?<=>=|<=|>(?!=)|<(?!=)).+/
if (val === '*') {
exp = has_not + 'value'
value = ''
} else if (in_reg.test(val)) {
exp = has_not + 'in'
value = val.match(in_reg)[0]
} else if (range_reg.test(val)) {
exp = has_not + 'range'
value = val.match(range_reg)[0]
min = value.split('_TO_')[0]
max = value.split('_TO_')[1]
} else if (compare_reg.test(val)) {
exp = has_not + 'compare'
value = val.match(compare_reg)[0]
const _compareType = val.substring(0, val.match(compare_reg)['index'])
const idx = compareTypeList.findIndex((item) => item.label === _compareType)
compareType = compareTypeList[idx].value
} else if (!val.includes('*')) {
exp = has_not + 'is'
value = val
} else {
const resList = [
['contain', /(?<=\*).*(?=\*)/g],
['end_with', /(?<=\*).+/g],
['start_with', /.+(?=\*)/g],
]
for (let i = 0; i < 3; i++) {
const reg = resList[i]
if (reg[1].test(val)) {
exp = has_not + reg[0]
value = val.match(reg[1])[0]
break
}
}
}
return {
id: uuidv4(),
type,
property,
exp,
value,
min,
max,
compareType,
}
})
this.ruleList = [...expArray]
} else if (open) {
const _canSearchPreferenceAttrList = this.canSearchPreferenceAttrList.filter((attr) => !attr.is_password)
this.ruleList = isInitOne
? [
{
id: uuidv4(),
type: 'and',
property:
_canSearchPreferenceAttrList && _canSearchPreferenceAttrList.length
? _canSearchPreferenceAttrList[0].name
: undefined,
exp: 'is',
value: null,
},
]
: []
}
},
handleSubmit() {
if (this.ruleList && this.ruleList.length) {
this.ruleList[0].type = 'and' // 增删后以防万一第一个不是and
this.filterExp = ''
const expList = this.ruleList.map((rule) => {
let singleRuleExp = ''
let _exp = rule.exp
if (rule.type === 'or') {
singleRuleExp += '-'
}
if (rule.exp.includes('~')) {
singleRuleExp += '~'
_exp = rule.exp.split('~')[1]
}
singleRuleExp += `${rule.property}:`
if (_exp === 'is') {
singleRuleExp += `${rule.value ?? ''}`
}
if (_exp === 'contain') {
singleRuleExp += `*${rule.value ?? ''}*`
}
if (_exp === 'start_with') {
singleRuleExp += `${rule.value ?? ''}*`
}
if (_exp === 'end_with') {
singleRuleExp += `*${rule.value ?? ''}`
}
if (_exp === 'value') {
singleRuleExp += `*`
}
if (_exp === 'in') {
singleRuleExp += `(${rule.value ?? ''})`
}
if (_exp === 'range') {
singleRuleExp += `[${rule.min}_TO_${rule.max}]`
}
if (_exp === 'compare') {
const idx = compareTypeList.findIndex((item) => item.value === rule.compareType)
singleRuleExp += `${compareTypeList[idx].label}${rule.value ?? ''}`
}
return singleRuleExp
})
this.filterExp = expList.join(',')
this.$emit('setExpFromFilter', this.filterExp)
} else {
this.$emit('setExpFromFilter', '')
}
this.visible = false
},
},
}
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,273 @@
<template>
<div>
<div class="control-group" v-if="controlType === 'choice'" >
<div
class="choice-group"
@click="handleControlType('input')"
>
<a-icon class="choice-group-icon" type="caret-down" />
</div>
<treeselect
class="custom-treeselect input-group"
:style="{ '--custom-height': rowHeight }"
:value="choiceValue"
@input="(value) => handleChange('value', value)"
:multiple="false"
:clearable="false"
searchable
:options="curModelAttrList"
:placeholder="$t('placeholder2')"
:normalizer="
(node) => {
return {
id: node.name,
label: node.name,
children: node.children,
}
}
"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
{{ node.label }}
</div>
</treeselect>
</div>
<div class="control-group" v-else>
<div
class="text-group"
@click="handleControlType('choice')"
>
<ops-icon class="text-group-icon" type="veops-text" />
</div>
<div
class="input-group"
v-if="isChoiceByProperty(rule.property) && (rule.exp === 'is' || rule.exp === '~is')"
>
<treeselect
class="custom-treeselect"
:style="{ '--custom-height': rowHeight }"
:value="rule.value"
@input="(value) => handleChange('value', value)"
:multiple="false"
:clearable="false"
searchable
:options="getChoiceValueByProperty(rule.property)"
:placeholder="$t('placeholder2')"
:normalizer="
(node) => {
return {
id: node[0],
label: node[0],
children: node.children,
}
}
"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
{{ node.label }}
</div>
</treeselect>
</div>
<div
compact
v-else-if="rule.exp === 'range' || rule.exp === '~range'"
class="input-group"
>
<a-input
class="ops-input"
:placeholder="$t('min')"
:disabled="disabled"
:value="rule.min"
@change="(e) => handleChange('min', e.target.value)"
/>
<span class="input-group-range-icon">~</span>
<a-input
class="ops-input"
v-model="rule.max"
:placeholder="$t('max')"
:disabled="disabled"
:value="rule.max"
@change="(e) => handleChange('max', e.target.value)"
/>
</div>
<div class="input-group" compact v-else-if="rule.exp === 'compare'">
<treeselect
class="custom-treeselect"
:style="{ width: '70px', '--custom-height': rowHeight, 'flex-shrink': 0 }"
:value="rule.compareType"
@input="(value) => handleChange('compareType', value)"
:multiple="false"
:clearable="false"
searchable
:options="compareTypeList"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
</treeselect>
<a-input :value="rule.value" @change="(e) => handleChange('value', e.target.value)" class="ops-input"/>
</div>
<div class="input-group" v-else-if="rule.exp !== 'value' && rule.exp !== '~value'">
<a-input
:value="rule.value"
@change="(e) => handleChange('value', e.target.value)"
:placeholder="rule.exp === 'in' || rule.exp === '~in' ? $t('cmdbFilterComp.split', { separator: ';' }) : ''"
class="ops-input"
:disabled="disabled"
></a-input>
</div>
<div v-else :style="{ width: '136px' }"></div>
</div>
</div>
</template>
<script>
import { compareTypeList } from '../constants.js'
export default {
name: 'ValueControls',
props: {
rule: {
type: Object,
default: () => {},
},
attrList: {
type: Array,
default: () => [],
},
disabled: {
type: Boolean,
default: false,
},
// 当前模型属性
curModelAttrList: {
type: Array,
default: () => []
},
// 行高
rowHeight: {
type: String,
default: ''
}
},
data() {
return {
compareTypeList,
controlType: 'input',
}
},
computed: {
choiceValue() {
const regex = /\{\{([^}]+)\}\}/g
const val = regex.exec(this?.rule?.value || '')
return val ? val?.[1]?.trim() || '' : this?.value?.value || ''
}
},
methods: {
isChoiceByProperty(property) {
const _find = this.attrList.find((item) => item.name === property)
if (_find) {
return _find.is_choice
}
return false
},
getChoiceValueByProperty(property) {
const _find = this.attrList.find((item) => item.name === property)
if (_find) {
return _find.choice_value
}
return []
},
handleControlType(type) {
this.controlType = type
},
handleChange(key, value) {
if (this.controlType === 'choice' && key === 'value') {
value = `{{ ${value} }}`
}
this.$emit('change', {
...this.rule,
[key]: value
})
}
}
}
</script>
<style lang="less" scoped>
.control-group {
display: flex;
}
.input-group {
display: flex;
align-items: center;
width: 136px;
&-range-icon {
margin: 0 8px;
}
input {
height: 36px;
}
}
.choice-group {
width: 14px;
height: 36px;
flex-shrink: 0;
background-color: #00B3CC;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&-icon {
font-size: 12px;
color: #FFFFFF;
}
}
.text-group {
width: 14px;
height: 36px;
flex-shrink: 0;
background-color: #2F54EB;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&-icon {
font-size: 12px;
color: #FFFFFF;
}
}
</style>

View File

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

View File

@@ -0,0 +1,11 @@
export const defaultConfig = {
public: {
key: '',
secret: ''
},
vcenter: {
host: '',
account: '',
password: ''
}
}

View File

@@ -0,0 +1,137 @@
<template>
<a-modal
:visible="visible"
:title="title"
:width="880"
:bodyStyle="{ maxHeight: '60vh', overflowY: 'auto' }"
@ok="handleOk"
@cancel="handleCancel"
>
<PublicTable
v-if="httpName === 'public'"
ref="publicTable"
/>
<VCenterTable
v-else-if="httpName === 'vcenter'"
ref="vcenterTable"
/>
</a-modal>
</template>
<script>
import { getHTTPAccounts, postHTTPAccounts } from '@/modules/cmdb/api/discovery'
import { defaultConfig } from './constants.js'
import PublicTable from './publicTable.vue'
import VCenterTable from './vcenterTable.vue'
export default {
name: 'AccountConfig',
components: {
PublicTable,
VCenterTable
},
props: {},
data() {
return {
visible: false,
rule: {}
}
},
computed: {
title() {
if (this?.rule?.option?.category === 'private_cloud') {
return `${this.rule?.name || ''} ${this.$t('cmdb.ciType.account')}`
}
return this.$t('cmdb.ciType.cloudAccessKey')
},
httpName() {
if (this?.rule?.option?.category === 'private_cloud') {
return this?.rule?.option?.en || ''
}
return 'public'
}
},
methods: {
async open(rule) {
if (!rule?.id) {
return
}
this.rule = rule
const res = await getHTTPAccounts({
adr_id: rule.id
})
console.log('getHTTPAccounts res', res)
this.visible = true
this.$nextTick(() => {
const data = res?.length ? this.handleAccountsData(res) : []
switch (this.httpName) {
case 'public':
this.$refs.publicTable.setData(data)
break
case 'vcenter':
this.$refs.vcenterTable.setData(data)
break
default:
break
}
})
},
handleAccountsData(accounts) {
const config = defaultConfig[this.httpName] || {}
return accounts.map((item) => {
return {
id: item?.id,
name: item?.name || '',
...config,
...(item?.config || {})
}
})
},
handleCancel() {
this.visible = false
},
async handleOk() {
let tableData = {}
switch (this.httpName) {
case 'public':
tableData = this.$refs.publicTable.getData()
break
case 'vcenter':
tableData = this.$refs.vcenterTable.getData()
break
default:
break
}
if (tableData.isError) {
return
}
const accounts = tableData.data.map((item) => {
const { name, id, ...otherConfig } = item
const newData = {
name,
config: otherConfig,
}
if (id) {
newData.id = id
}
return newData
})
postHTTPAccounts({
adr_id: this.rule.id,
accounts,
}).then(() => {
this.$message.success(this.$t('updateSuccess'))
this.handleCancel()
})
}
}
}
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,178 @@
<template>
<div class="table-wrap">
<div @click="addItem" class="add-btn" v-if="configData.length === 0">
<a-icon class="add-btn-icon" type="plus-circle" theme="twoTone" />
<span class="add-btn-text">{{ $t('cmdb.ad.addConfig') }}</span>
</div>
<template v-else>
<ops-table
:data="configData"
size="mini"
show-overflow
show-header-overflow
:row-config="{ height: 42 }"
:min-height="78"
>
<vxe-column width="170" :title="$t('name')">
<template #header="{ column }">
<span class="column-header-required">*</span>
{{ column.title }}
</template>
<template #default="{ row }">
<a-input v-model="row.name"></a-input>
</template>
</vxe-column>
<vxe-column width="300" title="key">
<template #header="{ column }">
<span class="column-header-required">*</span>
{{ column.title }}
</template>
<template #default="{ row }">
<a-input-password v-model="row.key"></a-input-password>
</template>
</vxe-column>
<vxe-column width="300" title="secret">
<template #default="{ row }">
<a-input-password v-model="row.secret"></a-input-password>
</template>
</vxe-column>
</ops-table>
<div class="actions">
<div
v-for="(item, index) in configData"
:key="item.client_id"
class="actions-item"
>
<a-icon
type="minus-circle"
class="actions-item-btn"
@click="deleteItem(index)"
/>
<a-icon
type="plus-circle"
class="actions-item-btn"
@click="addItem"
/>
</div>
</div>
</template>
</div>
</template>
<script>
import _ from 'lodash'
import { v4 as uuidv4 } from 'uuid'
import { defaultConfig } from './constants.js'
export default {
name: 'PublicTable',
data() {
return {
configData: []
}
},
methods: {
setData(data = []) {
this.configData = data.map((item) => {
return {
...item,
client_id: uuidv4()
}
})
},
getData() {
let isError = false
const keyArr = ['name', 'key', 'secret', 'id']
const data = this.configData.map((item) => {
const pickData = _.pickBy(item, (v, k) => {
return keyArr.includes(k) && v
})
return pickData
})
const errMsg = {
name: this.$t('name'),
key: 'key'
}
let errKey
for (let i = 0; i < data.length && !errKey; i++) {
const item = data[i]
const curErrKey = keyArr.find((key) => !item?.[key] && errMsg?.[key])
if (curErrKey) {
errKey = curErrKey
}
}
if (errKey) {
isError = true
this.$message.error(`${this.$t('placeholder1')} ${errMsg[errKey]}`)
}
return {
isError,
data
}
},
deleteItem(index) {
this.configData.splice(index, 1)
},
addItem() {
this.configData.push({
name: `${this.$t('cmdb.ad.defaultName')}${this.configData.length + 1}`,
...defaultConfig['public']
})
}
}
}
</script>
<style lang="less" scoped>
.table-wrap {
display: flex;
.add-btn {
padding: 5px 12px;
cursor: pointer;
border-radius: 1px;
border: 1px solid #B1C9FF;
background-color: #F4F9FF;
display: flex;
align-items: center;
justify-content: center;
&-icon {
font-size: 12px;
}
&-text {
font-size: 12px;
font-weight: 400;
color: #2F54EB;
margin-left: 6px;
}
}
.column-header-required {
color: #FD4C6A;
}
.actions {
padding-top: 36px;
margin-left: 16px;
&-item {
height: 42px;
display: flex;
align-items: center;
gap: 12px;
&-btn {
cursor: pointer;
color: #2f54eb;
}
}
}
}
</style>

View File

@@ -0,0 +1,188 @@
<template>
<div class="table-wrap">
<div @click="addItem" class="add-btn" v-if="configData.length === 0">
<a-icon class="add-btn-icon" type="plus-circle" theme="twoTone" />
<span class="add-btn-text">{{ $t('cmdb.ad.addConfig') }}</span>
</div>
<template v-else>
<ops-table
:data="configData"
size="mini"
show-overflow
show-header-overflow
:row-config="{ height: 42 }"
:min-height="78"
>
<vxe-column width="170" :title="$t('name')">
<template #header="{ column }">
<span class="column-header-required">*</span>
{{ column.title }}
</template>
<template #default="{ row }">
<a-input v-model="row.name"></a-input>
</template>
</vxe-column>
<vxe-column width="200" :title="$t('cmdb.ciType.host')">
<template #header="{ column }">
<span class="column-header-required">*</span>
{{ column.title }}
</template>
<template #default="{ row }">
<a-input v-model="row.host"></a-input>
</template>
</vxe-column>
<vxe-column width="200" :title="$t('cmdb.ciType.account')">
<template #header="{ column }">
<span class="column-header-required">*</span>
{{ column.title }}
</template>
<template #default="{ row }">
<a-input v-model="row.account"></a-input>
</template>
</vxe-column>
<vxe-column width="200" :title="$t('cmdb.ciType.password')">
<template #default="{ row }">
<a-input-password v-model="row.password"></a-input-password>
</template>
</vxe-column>
</ops-table>
<div class="actions">
<div
v-for="(item, index) in configData"
:key="item.client_id"
class="actions-item"
>
<a-icon
type="minus-circle"
class="actions-item-btn"
@click="deleteItem(index)"
/>
<a-icon
type="plus-circle"
class="actions-item-btn"
@click="addItem"
/>
</div>
</div>
</template>
</div>
</template>
<script>
import _ from 'lodash'
import { v4 as uuidv4 } from 'uuid'
import { defaultConfig } from './constants.js'
export default {
name: 'VCenterTable',
data() {
return {
configData: []
}
},
methods: {
setData(data) {
this.configData = data.map((item) => {
return {
...item,
client_id: uuidv4()
}
})
},
getData() {
let isError = false
const keyArr = ['name', 'host', 'account', 'password', 'id']
const data = this.configData.map((item) => {
const pickData = _.pickBy(item, (v, k) => {
return keyArr.includes(k) && v
})
return pickData
})
const errMsg = {
name: this.$t('name'),
host: this.$t('cmdb.ciType.host'),
account: this.$t('cmdb.ciType.account'),
}
let errKey
for (let i = 0; i < data.length && !errKey; i++) {
const item = data[i]
const curErrKey = keyArr.find((key) => !item?.[key] && errMsg?.[key])
if (curErrKey) {
errKey = curErrKey
}
}
if (errKey) {
isError = true
this.$message.error(`${this.$t('placeholder1')} ${errMsg[errKey]}`)
}
return {
isError,
data
}
},
deleteItem(index) {
this.configData.splice(index, 1)
},
addItem() {
this.configData.push({
name: `${this.$t('cmdb.ad.defaultName')}${this.configData.length + 1}`,
...defaultConfig['vcenter']
})
}
}
}
</script>
<style lang="less" scoped>
.table-wrap {
display: flex;
.add-btn {
padding: 5px 12px;
cursor: pointer;
border-radius: 1px;
border: 1px solid #B1C9FF;
background-color: #F4F9FF;
display: flex;
align-items: center;
justify-content: center;
&-icon {
font-size: 12px;
}
&-text {
font-size: 12px;
font-weight: 400;
color: #2F54EB;
margin-left: 6px;
}
}
.column-header-required {
color: #FD4C6A;
}
.actions {
padding-top: 36px;
margin-left: 16px;
&-item {
height: 42px;
display: flex;
align-items: center;
gap: 12px;
&-btn {
cursor: pointer;
color: #2f54eb;
}
}
}
}
</style>

View File

@@ -80,7 +80,14 @@
</a>
</a-space>
<a v-else @click="handleEdit"><a-icon type="eye"/></a>
<span>{{ rule.is_plugin ? 'Plugin' : $t('cmdb.custom_dashboard.default') }}</span>
<a
v-if="showHTTPAcountBtn"
class="discovery-footer-account"
@click="openAccountConfig"
>
<ops-icon type="veops-account"/>
</a>
<span class="discovery-footer-tag">{{ rule.is_plugin ? 'Plugin' : $t('cmdb.custom_dashboard.default') }}</span>
</div>
</template>
</div>
@@ -120,6 +127,10 @@ export default {
isDeletable() {
return ![this.$t('cmdb.ad.server'), this.$t('cmdb.ad.vserver'), this.$t('cmdb.ad.nic'), this.$t('cmdb.ad.disk'), 'server', 'vserver', 'NIC', 'harddisk'].includes(this.rule.name)
},
showHTTPAcountBtn() {
const showNameList = ['aliyun', 'tencentcloud', 'huaweicloud', 'aws', 'vcenter']
return this?.rule?.type === DISCOVERY_CATEGORY_TYPE.HTTP && showNameList.includes(this?.rule?.option?.en || '')
}
},
inject: {
setSelectedIds: {
@@ -142,6 +153,9 @@ export default {
this.setSelectedIds(this.rule.id, this.rule.type)
}
},
openAccountConfig() {
this.$emit('openAccountConfig')
}
},
}
</script>
@@ -264,8 +278,13 @@ export default {
.discovery-footer {
display: flex;
align-items: center;
justify-content: space-between;
> span {
&-account {
margin-left: 9px;
}
&-tag {
margin-left: auto;
color: #86909c;
background-color: #f0f5ff;
border-radius: 2px;
@@ -292,11 +311,6 @@ export default {
width: 170px;
height: 80px;
cursor: pointer;
// &:hover {
// .discovery-top {
// background-color: #f0f1f5;
// }
// }
}
.discovery-card-small:hover,
.discovery-card-small-selected {

View File

@@ -57,6 +57,7 @@
:isSelected="isSelected"
@editRule="handleOpenEditDrawer(rule, 'edit', type)"
@deleteRule="deleteRule(rule)"
@openAccountConfig="openAccountConfig(rule)"
/>
<div
v-if="showAddPlugin && type === DISCOVERY_CATEGORY_TYPE.PLUGIN"
@@ -78,6 +79,7 @@
</div>
</div>
<EditDrawer ref="editDrawer" />
<AccountConfig ref="accountConfig"/>
</div>
</template>
@@ -88,10 +90,15 @@ import { getDiscovery, deleteDiscovery } from '../../api/discovery'
import { DISCOVERY_CATEGORY_TYPE } from './constants.js'
import DiscoveryCard from './discoveryCard.vue'
import EditDrawer from './editDrawer.vue'
import AccountConfig from './accountConfig/index.vue'
export default {
name: 'AutoDiscovery',
components: { DiscoveryCard, EditDrawer },
components: {
DiscoveryCard,
EditDrawer,
AccountConfig
},
props: {
isSelected: {
type: Boolean,
@@ -104,6 +111,7 @@ export default {
DISCOVERY_CATEGORY_TYPE,
radioKey: '',
searchValue: '',
accountConfigVisible: false,
}
},
computed: {
@@ -282,6 +290,10 @@ export default {
changeRadio(key) {
this.radioKey = key === this.radioKey ? '' : key
},
openAccountConfig(rule) {
this.$refs.accountConfig.open(rule)
}
},
}

View File

@@ -388,7 +388,7 @@ export default {
this.logModalVisible = true
const logRes = await getAdcExecHistories({
type_id: this.currentType,
page_size: 1000
last_size: 1000
})
let logTextArray = []
if (logRes?.result?.length) {

View File

@@ -7,6 +7,7 @@
@search="handleSearch"
@searchFormReset="searchFormReset"
@searchFormChange="searchFormChange"
@export="handleExport"
></search-form>
<vxe-table
ref="xTable"
@@ -86,13 +87,13 @@
</template>
</vxe-column>
<vxe-column field="attr_alias" :title="$t('cmdb.history.attribute')"></vxe-column>
<vxe-column field="old" :title="$t('cmdb.history.old')"></vxe-column>
<vxe-column field="new" :title="$t('cmdb.history.new')"></vxe-column>
<vxe-column :cell-type="'string'" field="old" :title="$t('cmdb.history.old')"></vxe-column>
<vxe-column :cell-type="'string'" field="new" :title="$t('cmdb.history.new')"></vxe-column>
</vxe-table>
<pager
:current-page.sync="queryParams.page"
:page-size.sync="queryParams.page_size"
:page-sizes="[50, 100, 200]"
:page-sizes="[50, 100, 200, 500]"
:total="total"
:isLoading="loading"
@change="onChange"
@@ -391,6 +392,37 @@ export default {
filterOption(input, option) {
return option.componentOptions.children[0].text.indexOf(input) >= 0
},
async handleExport(params) {
const hide = this.$message.loading(this.$t('loading'), 0)
const res = await getCIHistoryTable({
...params,
page: this.queryParams.page,
page_size: this.queryParams.page_size,
})
hide()
const data = []
res.records.forEach((item) => {
item[0].type_id = this.handleTypeId(item[0].type_id)
item[1].forEach((subItem) => {
subItem.operate_type = this.handleOperateType(subItem.operate_type)
subItem.new = subItem.new || ''
subItem.old = subItem.old || ''
const tempObj = Object.assign(subItem, item[0])
data.push(tempObj)
})
})
this.$refs.xTable.exportData({
filename: this.$t('cmdb.history.ciChange'),
sheetName: 'Sheet1',
type: 'xlsx',
types: ['xlsx', 'csv', 'html', 'xml', 'txt'],
isMerge: true,
isColgroup: true,
data,
})
}
},
}
</script>

View File

@@ -5,6 +5,7 @@
@expandChange="handleExpandChange"
@search="handleSearch"
@searchFormReset="searchFormReset"
@export="handleExport"
></search-form>
<vxe-table
ref="xTable"
@@ -122,7 +123,7 @@
<pager
:current-page.sync="queryParams.page"
:page-size.sync="queryParams.page_size"
:page-sizes="[50, 100, 200]"
:page-sizes="[50, 100, 200, 500]"
:total="total"
:isLoading="loading"
@change="onChange"
@@ -379,6 +380,58 @@ export default {
filterOption(input, option) {
return option.componentOptions.children[0].text.indexOf(input) >= 0
},
async handleExport(params) {
const hide = this.$message.loading(this.$t('loading'), 0)
const res = await getRelationTable({
...params,
page: this.queryParams.page,
page_size: this.queryParams.page_size,
})
hide()
const data = []
res.records.forEach((item) => {
item[1].forEach((subItem) => {
subItem.operate_type = this.handleOperateType(subItem.operate_type)
subItem.relation_type_id = this.handleRelationType(subItem.relation_type_id)
subItem.first = res.cis[String(subItem.first_ci_id)]
subItem.second = res.cis[String(subItem.second_ci_id)]
const tempObj = Object.assign(subItem, item[0])
tempObj.changeDescription = this.getExportChangeDescription(tempObj)
data.push(tempObj)
})
})
this.$refs.xTable.exportData({
filename: this.$t('cmdb.history.relationChange'),
sheetName: 'Sheet1',
type: 'xlsx',
types: ['xlsx', 'csv', 'html', 'xml', 'txt'],
isMerge: true,
isColgroup: true,
data,
})
},
getExportChangeDescription(item) {
const first = item.first ? `${item.first.ci_type_alias}${item.first.unique_alias && item.first[item.first.unique] ? `${item.first.unique_alias}${item.first[item.first.unique]}` : ''}` : ''
const second = item.second ? `${item.second.ci_type_alias}${item.second.unique_alias && item.second[item.second.unique] ? `${item.second.unique_alias}${item.second[item.second.unique]}` : ''}` : ''
let center = ''
if (item.changeDescription === this.$t('cmdb.history.noUpdate')) {
center = item.relation_type_id
} else if (item.operate_type.includes(this.$t('update'))) {
center = item.changeArr.join(';')
} else if (item.operate_type.includes(this.$t('new'))) {
center = item.relation_type_id
} else if (item.operate_type.includes(this.$t('delete'))) {
center = item.relation_type_id
}
return `${first || ''} => ${center || ''} => ${second || ''}`
}
},
}
</script>

View File

@@ -27,7 +27,7 @@
<a-select-option
:value="Object.values(choice)[0]"
v-for="(choice, index) in attr.choice_value"
:key="'Search_' + attr.name + index"
:key="'Search_' + attr.name + Object.values(choice)[0] + index"
>
{{ Object.keys(choice)[0] }}
</a-select-option>
@@ -42,7 +42,7 @@
hideDisabledOptions: true,
defaultValue: [moment('00:00:00', 'HH:mm:ss'), moment('23:59:59', 'HH:mm:ss')],
}"
v-else-if="valueTypeMap[attr.value_type] == 'date' || valueTypeMap[attr.value_type] == 'datetime'"
v-else-if="attr.value_type === '3'"
/>
<a-input v-model="queryParams[attr.name]" style="width: 100%" allowClear v-else />
</a-form-item>
@@ -85,7 +85,7 @@
@change="onChange"
format="YYYY-MM-DD HH:mm"
:placeholder="[$t('cmdb.history.startTime'), $t('cmdb.history.endTime')]"
v-else-if="valueTypeMap[item.value_type] == 'date' || valueTypeMap[item.value_type] == 'datetime'"
v-else-if="attr.value_type === '3'"
:show-time="{
hideDisabledOptions: true,
defaultValue: [moment('00:00:00', 'HH:mm:ss'), moment('23:59:59', 'HH:mm:ss')],
@@ -101,6 +101,9 @@
<a-button type="primary" html-type="submit" @click="handleSearch">
{{ $t('query') }}
</a-button>
<a-button :style="{ marginLeft: '8px' }" @click="handleExport">
{{ $t('export') }}
</a-button>
<a-button :style="{ marginLeft: '8px' }" @click="handleReset">
{{ $t('reset') }}
</a-button>
@@ -155,6 +158,13 @@ export default {
this.$emit('search', this.queryParams)
},
handleExport() {
const queryParams = {
...this.queryParams
}
this.$emit('export', queryParams)
},
handleReset() {
this.queryParams = {
page: 1,

View File

@@ -5,6 +5,7 @@
@expandChange="handleExpandChange"
@search="handleSearch"
@searchFormReset="searchFormReset"
@export="handleExport"
></search-form>
<vxe-table
ref="xTable"
@@ -121,7 +122,7 @@ export default {
relationTypeList: null,
typeList: null,
userList: [],
pageSizeOptions: ['50', '100', '200'],
pageSizeOptions: ['50', '100', '200', '500'],
isExpand: false,
current: 1,
pageSize: 50,
@@ -508,6 +509,36 @@ export default {
filterOption(input, option) {
return option.componentOptions.children[0].text.indexOf(input) >= 0
},
async handleExport(params) {
const hide = this.$message.loading(this.$t('loading'), 0)
const res = await getCITypesTable({
...params,
page: this.queryParams.page,
page_size: this.queryParams.page_size,
})
hide()
res.result.forEach((item) => {
this.handleChangeDescription(item, item.operate_type)
item.operate_type = this.handleOperateType(item.operate_type)
item.type_id = this.handleTypeId(item.type_id)
item.uid = this.handleUID(item.uid)
if (item.operate_type.includes(this.$t('update'))) {
item.changeDescription = item.changeArr.join(';')
}
})
this.$refs.xTable.exportData({
filename: this.$t('cmdb.history.ciTypeChange'),
sheetName: 'Sheet1',
type: 'xlsx',
types: ['xlsx', 'csv', 'html', 'xml', 'txt'],
isMerge: true,
isColgroup: true,
data: res.result,
})
},
},
}
</script>

View File

@@ -181,12 +181,19 @@
</span>
</div>
<div v-else class="cmdb-preference-footor-unsubscribed">
<span
@click="openSubscribeSetting(item)"
><ops-icon :style="{ marginRight: '3px' }" type="cmdb-preference-subscribe" />{{
$t('cmdb.preference.sub')
}}</span
<a
@click="handleSubscribeCIType(item)"
class="cmdb-preference-footor-unsubscribed-item"
>
<ops-icon type="cmdb-ci" />{{ $t('cmdb.preference.subCITable') }}
</a>
<span class="cmdb-preference-footor-unsubscribed-gap"></span>
<a
@click="openSubscribeSetting(item, '2')"
class="cmdb-preference-footor-unsubscribed-item"
>
<ops-icon type="cmdb-tree" />{{ $t('cmdb.preference.subCITree') }}
</a>
</div>
</div>
<i></i><i></i><i></i><i></i><i></i>
@@ -221,6 +228,7 @@ import {
subscribeTreeView,
preferenceCitypeOrder,
} from '@/modules/cmdb/api/preference'
import { getCITypeAttributesByName } from '@/modules/cmdb/api/CITypeAttr'
import CollapseTransition from '@/components/CollapseTransition'
import SubscribeSetting from '../../components/subscribeSetting/subscribeSetting'
import { getCIAdcStatistics } from '../../api/ci'
@@ -381,9 +389,39 @@ export default {
this.getCITypes()
})
},
async handleSubscribeCIType(ciType) {
try {
const res = await getCITypeAttributesByName(ciType.id)
const attributes = res?.attributes || []
const subscribeList = attributes
.filter((item) => item?.default_show)
.map((item) => {
return [item?.id?.toString(), false]
})
if (subscribeList.length === 0) {
const uniqueItem = attributes.find((item) => item?.id === res?.unique_id)
if (uniqueItem) {
subscribeList.push([uniqueItem?.id?.toString(), false])
}
}
await subscribeCIType(
ciType.id,
subscribeList
)
this.$message.success(this.$t('cmdb.components.subSuccess'))
this.resetRoute()
} catch (error) {
console.error('handleSubscribeCIType failed', error)
this.$message.success(this.$t('cmdb.components.subFailed'))
}
},
openSubscribeSetting(ciType, activeKey = '1') {
this.$refs.subscribeSetting.open({ ...ciType, type_id: ciType.id }, activeKey)
},
changeGroupExpand(group) {
const _idx = this.expandKeys.findIndex((expand) => expand === group.id)
if (_idx > -1) {
@@ -653,11 +691,27 @@ export default {
}
}
.cmdb-preference-footor-unsubscribed {
text-align: center;
> span {
color: @primary-color;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 10px;
&-item {
display: flex;
align-items: center;
gap: 3px;
font-size: 12px;
color: rgba(0, 0, 0, 0.76);
&:hover {
color: #1890ff;
}
}
&-gap {
width: 1px;
height: 18px;
background-color: #e8e8e8;
}
}
.cmdb-preference-footor-subscribed {

View File

@@ -41,7 +41,7 @@ services:
- redis
cmdb-api:
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.4.8
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.4.10
container_name: cmdb-api
env_file:
- .env
@@ -71,7 +71,7 @@ services:
flask cmdb-init-acl
flask init-import-user-from-acl
flask init-department
flask cmdb-patch -v 2.4.7
flask cmdb-patch -v 2.4.10
flask cmdb-counter > counter.log 2>&1
networks:
new:
@@ -82,12 +82,9 @@ services:
interval: 5s
retries: 10
test: "ps aux|grep -v grep|grep -v '1 root'|grep gunicorn || exit 1"
volumes:
- /Users/yong.huang/PycharmProjects/oneops-api/api/lib/cmdb/auto_discovery/templates:/data/apps/cmdb/api/lib/cmdb/auto_discovery/templates
- /Users/yong.huang/PycharmProjects/oneops-api/api/lib/cmdb/auto_discovery/mapping:/data/apps/cmdb/api/lib/cmdb/auto_discovery/mapping
cmdb-ui:
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.4.8
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.4.10
container_name: cmdb-ui
depends_on:
cmdb-api:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 23 KiB