mirror of
https://github.com/veops/cmdb.git
synced 2025-09-04 03:44:04 +08:00
Compare commits
30 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
330b64edb3 | ||
|
63a3074cb7 | ||
|
31b8cf49dc | ||
|
b01c335456 | ||
|
002fef09e2 | ||
|
175778a162 | ||
|
5050a1bef5 | ||
|
46a6cf67d6 | ||
|
4e857c2775 | ||
|
835df1bdeb | ||
|
579339d13c | ||
|
629967ce82 | ||
|
3a00bfd236 | ||
|
2e97ebd895 | ||
|
eb6a813cbc | ||
|
ff78face48 | ||
|
d55433c438 | ||
|
daf0254616 | ||
|
6b32009955 | ||
|
d53288c1fb | ||
|
586d820a08 | ||
|
6776be4599 | ||
|
ff2b8ea198 | ||
|
ed46a1e1c1 | ||
|
0dc614fb46 | ||
|
bc66d33ce0 | ||
|
d5db68d7d0 | ||
|
b22b8b286b | ||
|
dd4f3b0e9c | ||
|
688f4e0ea4 |
29
README.md
29
README.md
@@ -73,7 +73,8 @@
|
||||
## 安装
|
||||
|
||||
### Docker 一键快速构建
|
||||
> 方法一
|
||||
|
||||
[//]: # (> 方法一)
|
||||
- 第一步: 先安装 Docker 环境, 以及Docker Compose (v2)
|
||||
- 第二步: 拷贝项目
|
||||
```shell
|
||||
@@ -83,13 +84,20 @@ git clone https://github.com/veops/cmdb.git
|
||||
```
|
||||
docker compose up -d
|
||||
```
|
||||
> 方法二, 该方法适用于linux系统
|
||||
- 第一步: 先安装 Docker 环境, 以及Docker Compose (v2)
|
||||
- 第二步: 直接使用项目根目录下的install.sh 文件进行 `安装`、`启动`、`暂停`、`查状态`、`删除`、`卸载`
|
||||
```shell
|
||||
curl -so install.sh https://raw.githubusercontent.com/veops/cmdb/deploy_on_kylin_docker/install.sh
|
||||
sh install.sh install
|
||||
```
|
||||
|
||||
[//]: # (> 方法二, 该方法适用于linux系统)
|
||||
|
||||
[//]: # (- 第一步: 先安装 Docker 环境, 以及Docker Compose (v2))
|
||||
|
||||
[//]: # (- 第二步: 直接使用项目根目录下的install.sh 文件进行 `安装`、`启动`、`暂停`、`查状态`、`删除`、`卸载`)
|
||||
|
||||
[//]: # (```shell)
|
||||
|
||||
[//]: # (curl -so install.sh https://raw.githubusercontent.com/veops/cmdb/deploy_on_kylin_docker/install.sh)
|
||||
|
||||
[//]: # (sh install.sh install)
|
||||
|
||||
[//]: # (```)
|
||||
|
||||
### [本地开发环境搭建](docs/local.md)
|
||||
|
||||
@@ -105,4 +113,7 @@ sh install.sh install
|
||||
|
||||
_**欢迎关注公众号(维易科技OneOps),关注后可加入微信群,进行产品和技术交流。**_
|
||||
|
||||

|
||||
|
||||
<p align="center">
|
||||
<img src="docs/images/wechat.png" alt="公众号: 维易科技OneOps" />
|
||||
</p>
|
||||
|
@@ -67,6 +67,7 @@ colorama = ">=0.4.6"
|
||||
pycryptodomex = ">=3.19.0"
|
||||
lz4 = ">=4.3.2"
|
||||
python-magic = "==0.4.27"
|
||||
jsonpath = "==0.82.2"
|
||||
|
||||
[dev-packages]
|
||||
# Testing
|
||||
|
@@ -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'):
|
||||
|
@@ -2,6 +2,7 @@
|
||||
import copy
|
||||
import datetime
|
||||
import json
|
||||
import jsonpath
|
||||
import os
|
||||
from flask import abort
|
||||
from flask import current_app
|
||||
@@ -9,14 +10,14 @@ from flask_login import current_user
|
||||
from sqlalchemy import func
|
||||
|
||||
from api.extensions import db
|
||||
from api.lib.cmdb.auto_discovery.const import ClOUD_MAP
|
||||
from api.lib.cmdb.auto_discovery.const import CLOUD_MAP
|
||||
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.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
|
||||
@@ -32,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
|
||||
@@ -39,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__))
|
||||
@@ -223,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:
|
||||
@@ -240,21 +243,29 @@ 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)
|
||||
|
||||
for rule in rules:
|
||||
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 not rule['enabled']:
|
||||
continue
|
||||
|
||||
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 isinstance(rule.get("extra_option"), dict):
|
||||
decrypt_account(rule['extra_option'], 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)
|
||||
@@ -271,6 +282,12 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
|
||||
result.append(rule)
|
||||
break
|
||||
elif not rule['agent_id'] and not rule['query_expr'] and rule['adr_id']:
|
||||
try:
|
||||
if not int(oneagent_id, 16): # excludes master
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
adr = AutoDiscoveryRuleCRUD.get_by_id(rule['adr_id'])
|
||||
if not adr:
|
||||
continue
|
||||
@@ -279,7 +296,6 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
|
||||
|
||||
result.append(rule)
|
||||
|
||||
|
||||
ad_rules_updated_at = (SystemConfigManager.get('ad_rules_updated_at') or {}).get('option', {}).get('v') or ""
|
||||
new_last_update_at = ""
|
||||
for i in result:
|
||||
@@ -353,27 +369,31 @@ 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":
|
||||
kwargs.setdefault('extra_option', dict)
|
||||
if adr.type == AutoDiscoveryType.HTTP:
|
||||
kwargs.setdefault('extra_option', dict())
|
||||
en_name = None
|
||||
for i in DEFAULT_INNER:
|
||||
if i['name'] == adr.name:
|
||||
en_name = i['en']
|
||||
break
|
||||
if en_name and kwargs['extra_option'].get('category'):
|
||||
for item in ClOUD_MAP[en_name]:
|
||||
for item in CLOUD_MAP[en_name]:
|
||||
if item["collect_key_map"].get(kwargs['extra_option']['category']):
|
||||
kwargs["extra_option"]["collect_key"] = item["collect_key_map"][
|
||||
kwargs['extra_option']['category']]
|
||||
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)
|
||||
@@ -391,20 +411,27 @@ 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":
|
||||
kwargs.setdefault('extra_option', dict)
|
||||
if adr.type == AutoDiscoveryType.HTTP:
|
||||
kwargs.setdefault('extra_option', dict())
|
||||
en_name = None
|
||||
for i in DEFAULT_INNER:
|
||||
if i['name'] == adr.name:
|
||||
en_name = i['en']
|
||||
break
|
||||
if en_name and kwargs['extra_option'].get('category'):
|
||||
for item in ClOUD_MAP[en_name]:
|
||||
for item in CLOUD_MAP[en_name]:
|
||||
if item["collect_key_map"].get(kwargs['extra_option']['category']):
|
||||
kwargs["extra_option"]["collect_key"] = item["collect_key_map"][
|
||||
kwargs['extra_option']['category']]
|
||||
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'))
|
||||
|
||||
@@ -428,13 +455,12 @@ 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 inst.agent_id != kwargs.get('agent_id') or inst.query_expr != kwargs.get('query_expr'):
|
||||
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):
|
||||
item.delete(commit=False)
|
||||
db.session.commit()
|
||||
@@ -479,6 +505,7 @@ class AutoDiscoveryCITypeRelationCRUD(DBMixin):
|
||||
def get_all(cls, type_ids=None):
|
||||
res = cls.cls.get_by(to_dict=False)
|
||||
return [i for i in res if type_ids is None or i.ad_type_id in type_ids]
|
||||
|
||||
@classmethod
|
||||
def get_by_type_id(cls, type_id, to_dict=False):
|
||||
return cls.cls.get_by(ad_type_id=type_id, to_dict=to_dict)
|
||||
@@ -543,7 +570,6 @@ class AutoDiscoveryCICRUD(DBMixin):
|
||||
adts = AutoDiscoveryCITypeCRUD.get_by_type_id(type_id)
|
||||
for adt in adts:
|
||||
attr_names |= set((adt.attributes or {}).values())
|
||||
|
||||
return [attr for attr in attributes if attr['name'] in attr_names]
|
||||
|
||||
@classmethod
|
||||
@@ -673,38 +699,24 @@ 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]: 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'))
|
||||
if mapping:
|
||||
ci_dict = {k: (mapping.get(k) or {}).get(str(v), v) for k, v in ci_dict.items()}
|
||||
if path_mapping:
|
||||
ci_dict = {k: jsonpath.jsonpath(v, path_mapping[k]) if k in path_mapping else v
|
||||
for k, v in ci_dict.items()}
|
||||
ci_id = CIManager.add(adc.type_id, is_auto_discovery=True, _is_admin=True, **ci_dict)
|
||||
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)
|
||||
except:
|
||||
try:
|
||||
CIRelationManager.add(relation_ci_id, ci_id, valid=False)
|
||||
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,
|
||||
@@ -715,7 +727,7 @@ class AutoDiscoveryCICRUD(DBMixin):
|
||||
class AutoDiscoveryHTTPManager(object):
|
||||
@staticmethod
|
||||
def get_categories(name):
|
||||
categories = (ClOUD_MAP.get(name) or {}) or []
|
||||
categories = (CLOUD_MAP.get(name) or {}) or []
|
||||
for item in copy.deepcopy(categories):
|
||||
item.pop('map', None)
|
||||
item.pop('collect_key_map', None)
|
||||
@@ -738,16 +750,52 @@ class AutoDiscoveryHTTPManager(object):
|
||||
|
||||
@staticmethod
|
||||
def get_attributes(provider, resource):
|
||||
for item in (ClOUD_MAP.get(provider) or {}):
|
||||
for item in (CLOUD_MAP.get(provider) or {}):
|
||||
for _resource in (item.get('map') or {}):
|
||||
if _resource == resource:
|
||||
tpt = item['map'][_resource]
|
||||
if isinstance(tpt, dict):
|
||||
tpt = tpt.get('template')
|
||||
if tpt and os.path.exists(os.path.join(PWD, tpt)):
|
||||
with open(os.path.join(PWD, tpt)) as f:
|
||||
return json.loads(f.read())
|
||||
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def get_mapping(provider, resource):
|
||||
for item in (CLOUD_MAP.get(provider) or {}):
|
||||
for _resource in (item.get('map') or {}):
|
||||
if _resource == resource:
|
||||
mapping = item['map'][_resource]
|
||||
if not isinstance(mapping, dict):
|
||||
return {}
|
||||
name = mapping.get('mapping')
|
||||
mapping = AutoDiscoveryMappingCache.get(name)
|
||||
if isinstance(mapping, dict):
|
||||
return {mapping[key][provider]['key'].split('.')[0]: key for key in mapping if
|
||||
(mapping[key].get(provider) or {}).get('key')}
|
||||
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def get_predefined_value_mapping(provider, resource):
|
||||
for item in (CLOUD_MAP.get(provider) or {}):
|
||||
for _resource in (item.get('map') or {}):
|
||||
if _resource == resource:
|
||||
mapping = item['map'][_resource]
|
||||
if not isinstance(mapping, dict):
|
||||
return {}, {}
|
||||
name = mapping.get('mapping')
|
||||
mapping = AutoDiscoveryMappingCache.get(name)
|
||||
if isinstance(mapping, dict):
|
||||
return ({key: mapping[key][provider].get('map') for key in mapping if
|
||||
mapping[key].get(provider, {}).get('map')},
|
||||
{key: mapping[key][provider]['key'].split('.', 1)[1] for key in mapping if
|
||||
((mapping[key].get(provider) or {}).get('key') or '').split('.')[1:]})
|
||||
|
||||
return {}, {}
|
||||
|
||||
|
||||
class AutoDiscoverySNMPManager(object):
|
||||
|
||||
@@ -828,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
|
||||
|
@@ -12,17 +12,28 @@ DEFAULT_INNER = [
|
||||
dict(name="华为云", en="huaweicloud", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
|
||||
option={'icon': {'name': 'caise-huaweiyun'}, "en": "huaweicloud"}),
|
||||
dict(name="AWS", en="aws", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
|
||||
option={'icon': {'name': 'caise-aws'}}),
|
||||
option={'icon': {'name': 'caise-aws'}, "en": "aws"}),
|
||||
|
||||
dict(name="VCenter", en="vcenter", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
|
||||
option={'icon': {'name': 'cmdb-vcenter'}, "category": "private_cloud", "en": "vcenter"}),
|
||||
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'}}),
|
||||
@@ -34,14 +45,14 @@ DEFAULT_INNER = [
|
||||
option={'icon': {'name': 'caise-dayinji'}}),
|
||||
]
|
||||
|
||||
ClOUD_MAP = {
|
||||
CLOUD_MAP = {
|
||||
"aliyun": [
|
||||
{
|
||||
"category": "计算",
|
||||
"items": ["云服务器 ECS", "云服务器 Disk"],
|
||||
"map": {
|
||||
"云服务器 ECS": "templates/aliyun_ecs.json",
|
||||
"云服务器 Disk": "templates/aliyun_ecs_disk2.json",
|
||||
"云服务器 ECS": {"template": "templates/aliyun_ecs.json", "mapping": "ecs"},
|
||||
"云服务器 Disk": {"template": "templates/aliyun_ecs_disk.json", "mapping": "evs"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"云服务器 ECS": "ali.ecs",
|
||||
@@ -57,10 +68,10 @@ ClOUD_MAP = {
|
||||
"交换机Switch",
|
||||
],
|
||||
"map": {
|
||||
"内容分发CDN": "templates/aliyun_cdn.json",
|
||||
"负载均衡SLB": "templates/aliyun_slb.json",
|
||||
"专有网络VPC": "templates/aliyun_vpc.json",
|
||||
"交换机Switch": "templates/aliyun_switch.json",
|
||||
"内容分发CDN": {"template": "templates/aliyun_cdn.json", "mapping": "CDN"},
|
||||
"负载均衡SLB": {"template": "templates/aliyun_slb.json", "mapping": "loadbalancer"},
|
||||
"专有网络VPC": {"template": "templates/aliyun_vpc.json", "mapping": "vpc"},
|
||||
"交换机Switch": {"template": "templates/aliyun_switch.json", "mapping": "vswitch"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"内容分发CDN": "ali.cdn",
|
||||
@@ -73,8 +84,8 @@ ClOUD_MAP = {
|
||||
"category": "存储",
|
||||
"items": ["块存储EBS", "对象存储OSS"],
|
||||
"map": {
|
||||
"块存储EBS": "templates/aliyun_ebs.json",
|
||||
"对象存储OSS": "templates/aliyun_oss.json",
|
||||
"块存储EBS": {"template": "templates/aliyun_ebs.json", "mapping": "evs"},
|
||||
"对象存储OSS": {"template": "templates/aliyun_oss.json", "mapping": "objectStorage"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"块存储EBS": "ali.ebs",
|
||||
@@ -85,9 +96,9 @@ ClOUD_MAP = {
|
||||
"category": "数据库",
|
||||
"items": ["云数据库RDS MySQL", "云数据库RDS PostgreSQL", "云数据库 Redis"],
|
||||
"map": {
|
||||
"云数据库RDS MySQL": "templates/aliyun_rds_mysql.json",
|
||||
"云数据库RDS PostgreSQL": "templates/aliyun_rds_postgre.json",
|
||||
"云数据库 Redis": "templates/aliyun_redis.json",
|
||||
"云数据库RDS MySQL": {"template": "templates/aliyun_rds_mysql.json", "mapping": "mysql"},
|
||||
"云数据库RDS PostgreSQL": {"template": "templates/aliyun_rds_postgre.json", "mapping": "postgresql"},
|
||||
"云数据库 Redis": {"template": "templates/aliyun_redis.json", "mapping": "redis"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"云数据库RDS MySQL": "ali.rds_mysql",
|
||||
@@ -101,7 +112,7 @@ ClOUD_MAP = {
|
||||
"category": "计算",
|
||||
"items": ["云服务器 CVM"],
|
||||
"map": {
|
||||
"云服务器 CVM": "templates/tencent_cvm.json",
|
||||
"云服务器 CVM": {"template": "templates/tencent_cvm.json", "mapping": "ecs"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"云服务器 CVM": "tencent.cvm",
|
||||
@@ -111,7 +122,7 @@ ClOUD_MAP = {
|
||||
"category": "CDN与边缘",
|
||||
"items": ["内容分发CDN"],
|
||||
"map": {
|
||||
"内容分发CDN": "templates/tencent_cdn.json",
|
||||
"内容分发CDN": {"template": "templates/tencent_cdn.json", "mapping": "CDN"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"内容分发CDN": "tencent.cdn",
|
||||
@@ -121,9 +132,9 @@ ClOUD_MAP = {
|
||||
"category": "网络",
|
||||
"items": ["负载均衡CLB", "私有网络VPC", "子网"],
|
||||
"map": {
|
||||
"负载均衡CLB": "templates/tencent_clb.json",
|
||||
"私有网络VPC": "templates/tencent_vpc.json",
|
||||
"子网": "templates/tencent_subnet.json",
|
||||
"负载均衡CLB": {"template": "templates/tencent_clb.json", "mapping": "loadbalancer"},
|
||||
"私有网络VPC": {"template": "templates/tencent_vpc.json", "mapping": "vpc"},
|
||||
"子网": {"template": "templates/tencent_subnet.json", "mapping": "vswitch"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"负载均衡CLB": "tencent.clb",
|
||||
@@ -135,21 +146,21 @@ ClOUD_MAP = {
|
||||
"category": "存储",
|
||||
"items": ["云硬盘CBS", "对象存储COS"],
|
||||
"map": {
|
||||
"云硬盘CBS": "templates/tencent_cbs.json",
|
||||
"对象存储OSS": "templates/tencent_cos.json",
|
||||
"云硬盘CBS": {"template": "templates/tencent_cbs.json", "mapping": "evs"},
|
||||
"对象存储COS": {"template": "templates/tencent_cos.json", "mapping": "objectStorage"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"云硬盘CBS": "tencent.cbs",
|
||||
"对象存储OSS": "tencent.cos",
|
||||
"对象存储COS": "tencent.cos",
|
||||
},
|
||||
},
|
||||
{
|
||||
"category": "数据库",
|
||||
"items": ["云数据库 MySQL", "云数据库 PostgreSQL", "云数据库 Redis"],
|
||||
"map": {
|
||||
"云数据库 MySQL": "templates/tencent_rdb.json",
|
||||
"云数据库 PostgreSQL": "templates/tencent_postgres.json",
|
||||
"云数据库 Redis": "templates/tencent_redis.json",
|
||||
"云数据库 MySQL": {"template": "templates/tencent_rdb.json", "mapping": "mysql"},
|
||||
"云数据库 PostgreSQL": {"template": "templates/tencent_postgres.json", "mapping": "postgresql"},
|
||||
"云数据库 Redis": {"template": "templates/tencent_redis.json", "mapping": "redis"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"云数据库 MySQL": "tencent.rdb",
|
||||
@@ -163,7 +174,7 @@ ClOUD_MAP = {
|
||||
"category": "计算",
|
||||
"items": ["云服务器 ECS"],
|
||||
"map": {
|
||||
"云服务器 ECS": "templates/huaweicloud_ecs.json",
|
||||
"云服务器 ECS": {"template": "templates/huaweicloud_ecs.json", "mapping": "ecs"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"云服务器 ECS": "huawei.ecs",
|
||||
@@ -173,7 +184,7 @@ ClOUD_MAP = {
|
||||
"category": "CDN与智能边缘",
|
||||
"items": ["内容分发网络CDN"],
|
||||
"map": {
|
||||
"内容分发网络CDN": "templates/huawei_cdn.json",
|
||||
"内容分发网络CDN": {"template": "templates/huawei_cdn.json", "mapping": "CDN"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"内容分发网络CDN": "huawei.cdn",
|
||||
@@ -183,9 +194,9 @@ ClOUD_MAP = {
|
||||
"category": "网络",
|
||||
"items": ["弹性负载均衡ELB", "虚拟私有云VPC", "子网"],
|
||||
"map": {
|
||||
"弹性负载均衡ELB": "templates/huawei_elb.json",
|
||||
"虚拟私有云VPC": "templates/huawei_vpc.json",
|
||||
"子网": "templates/huawei_subnet.json",
|
||||
"弹性负载均衡ELB": {"template": "templates/huawei_elb.json", "mapping": "loadbalancer"},
|
||||
"虚拟私有云VPC": {"template": "templates/huawei_vpc.json", "mapping": "vpc"},
|
||||
"子网": {"template": "templates/huawei_subnet.json", "mapping": "vswitch"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"弹性负载均衡ELB": "huawei.elb",
|
||||
@@ -197,8 +208,8 @@ ClOUD_MAP = {
|
||||
"category": "存储",
|
||||
"items": ["云硬盘EVS", "对象存储OBS"],
|
||||
"map": {
|
||||
"云硬盘EVS": "templates/huawei_evs.json",
|
||||
"对象存储OBS": "templates/huawei_obs.json",
|
||||
"云硬盘EVS": {"template": "templates/huawei_evs.json", "mapping": "evs"},
|
||||
"对象存储OBS": {"template": "templates/huawei_obs.json", "mapping": "objectStorage"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"云硬盘EVS": "huawei.evs",
|
||||
@@ -209,8 +220,8 @@ ClOUD_MAP = {
|
||||
"category": "数据库",
|
||||
"items": ["云数据库RDS MySQL", "云数据库RDS PostgreSQL"],
|
||||
"map": {
|
||||
"云数据库RDS MySQL": "templates/huawei_rds_mysql.json",
|
||||
"云数据库RDSPostgreSQL": "templates/huaweirds_postgre.json",
|
||||
"云数据库RDS MySQL": {"template": "templates/huawei_rds_mysql.json", "mapping": "mysql"},
|
||||
"云数据库RDS PostgreSQL": {"template": "templates/huawei_rds_postgre.json", "mapping": "postgresql"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"云数据库RDS MySQL": "huawei.rds_mysql",
|
||||
@@ -221,7 +232,7 @@ ClOUD_MAP = {
|
||||
"category": "应用中间件",
|
||||
"items": ["分布式缓存Redis"],
|
||||
"map": {
|
||||
"分布式缓存Redis": "templates/huawei_dcs.json",
|
||||
"分布式缓存Redis": {"template": "templates/huawei_dcs.json", "mapping": "redis"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"分布式缓存Redis": "huawei.dcs",
|
||||
@@ -233,7 +244,7 @@ ClOUD_MAP = {
|
||||
"category": "计算",
|
||||
"items": ["云服务器 EC2"],
|
||||
"map": {
|
||||
"云服务器 EC2": "templates/aws_ec2.json",
|
||||
"云服务器 EC2": {"template": "templates/aws_ec2.json", "mapping": "ecs"},
|
||||
},
|
||||
"collect_key_map": {
|
||||
"云服务器 EC2": "aws.ec2",
|
||||
@@ -283,7 +294,7 @@ ClOUD_MAP = {
|
||||
"items": ["数据存储", "数据存储集群"],
|
||||
"map": {
|
||||
"数据存储": "templates/vsphere_datastore.json",
|
||||
"数据存储集群": "templates/vsphere.storage_pod.json",
|
||||
"数据存储集群": "templates/vsphere_storage_pod.json",
|
||||
},
|
||||
"collect_key_map": {
|
||||
"数据存储": "vsphere.datastore",
|
||||
@@ -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",
|
||||
},
|
||||
|
@@ -3,6 +3,8 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import yaml
|
||||
|
||||
from flask import current_app
|
||||
|
||||
@@ -546,3 +548,20 @@ class CMDBCounterCache(object):
|
||||
@classmethod
|
||||
def get_sub_counter(cls):
|
||||
return cache.get(cls.KEY3) or cls.flush_sub_counter()
|
||||
|
||||
|
||||
class AutoDiscoveryMappingCache(object):
|
||||
PREFIX = 'CMDB::AutoDiscovery::Mapping::{}'
|
||||
|
||||
@classmethod
|
||||
def get(cls, name):
|
||||
res = cache.get(cls.PREFIX.format(name)) or {}
|
||||
if not res:
|
||||
path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "auto_discovery/mapping/{}.yaml".format(name))
|
||||
if os.path.exists(path):
|
||||
with open(path, 'r') as f:
|
||||
mapping = yaml.safe_load(f)
|
||||
res = mapping.get('mapping') or {}
|
||||
res and cache.set(cls.PREFIX.format(name), res, timeout=0)
|
||||
|
||||
return res
|
@@ -4,12 +4,12 @@
|
||||
import copy
|
||||
import datetime
|
||||
import json
|
||||
import threading
|
||||
|
||||
import redis_lock
|
||||
import threading
|
||||
from flask import abort
|
||||
from flask import current_app
|
||||
from flask_login import current_user
|
||||
from sqlalchemy.orm import aliased
|
||||
from werkzeug.exceptions import BadRequest
|
||||
|
||||
from api.extensions import db
|
||||
@@ -28,6 +28,7 @@ from api.lib.cmdb.const import ExistPolicy
|
||||
from api.lib.cmdb.const import OperateType
|
||||
from api.lib.cmdb.const import PermEnum
|
||||
from api.lib.cmdb.const import REDIS_PREFIX_CI
|
||||
from api.lib.cmdb.const import RelationSourceEnum
|
||||
from api.lib.cmdb.const import ResourceTypeEnum
|
||||
from api.lib.cmdb.const import RetKey
|
||||
from api.lib.cmdb.const import ValueTypeEnum
|
||||
@@ -217,7 +218,7 @@ class CIManager(object):
|
||||
|
||||
@classmethod
|
||||
def get_ad_statistics(cls):
|
||||
return CMDBCounterCache.get_adc_counter()
|
||||
return CMDBCounterCache.get_adc_counter() or {}
|
||||
|
||||
@staticmethod
|
||||
def ci_is_exist(unique_key, unique_value, type_id):
|
||||
@@ -318,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))
|
||||
|
||||
@@ -381,6 +382,7 @@ class CIManager(object):
|
||||
for _, attr in attrs:
|
||||
if attr.is_computed:
|
||||
computed_attrs.append(attr.to_dict())
|
||||
ci_dict[attr.name] = None
|
||||
elif attr.is_password:
|
||||
if attr.name in ci_dict:
|
||||
password_dict[attr.id] = (ci_dict.pop(attr.name), attr.is_dynamic)
|
||||
@@ -388,10 +390,7 @@ class CIManager(object):
|
||||
password_dict[attr.id] = (ci_dict.pop(attr.alias), attr.is_dynamic)
|
||||
|
||||
if attr.re_check and password_dict.get(attr.id):
|
||||
value_manager.check_re(attr.re_check, password_dict[attr.id][0])
|
||||
|
||||
if computed_attrs:
|
||||
value_manager.handle_ci_compute_attributes(ci_dict, computed_attrs, ci)
|
||||
value_manager.check_re(attr.re_check, attr.alias, password_dict[attr.id][0])
|
||||
|
||||
cls._valid_unique_constraint(ci_type.id, ci_dict, ci and ci.id)
|
||||
|
||||
@@ -418,6 +417,9 @@ class CIManager(object):
|
||||
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)
|
||||
|
||||
if computed_attrs:
|
||||
value_manager.handle_ci_compute_attributes(ci_dict, computed_attrs, ci)
|
||||
|
||||
operate_type = OperateType.UPDATE if ci is not None else OperateType.ADD
|
||||
try:
|
||||
ci = ci or CI.create(type_id=ci_type.id, is_auto_discovery=is_auto_discovery)
|
||||
@@ -463,6 +465,7 @@ class CIManager(object):
|
||||
for _, attr in attrs:
|
||||
if attr.is_computed:
|
||||
computed_attrs.append(attr.to_dict())
|
||||
ci_dict[attr.name] = None
|
||||
elif attr.is_password:
|
||||
if attr.name in ci_dict:
|
||||
password_dict[attr.id] = (ci_dict.pop(attr.name), attr.is_dynamic)
|
||||
@@ -470,10 +473,7 @@ class CIManager(object):
|
||||
password_dict[attr.id] = (ci_dict.pop(attr.alias), attr.is_dynamic)
|
||||
|
||||
if attr.re_check and password_dict.get(attr.id):
|
||||
value_manager.check_re(attr.re_check, password_dict[attr.id][0])
|
||||
|
||||
if computed_attrs:
|
||||
value_manager.handle_ci_compute_attributes(ci_dict, computed_attrs, ci)
|
||||
value_manager.check_re(attr.re_check, attr.alias, password_dict[attr.id][0])
|
||||
|
||||
limit_attrs = self._valid_ci_for_no_read(ci) if not _is_admin else {}
|
||||
|
||||
@@ -486,6 +486,10 @@ class CIManager(object):
|
||||
ci_dict = {k: v for k, v in ci_dict.items() if k in ci_type_attrs_name}
|
||||
key2attr = value_manager.valid_attr_value(ci_dict, ci.type_id, ci.id, ci_type_attrs_name,
|
||||
ci_attr2type_attr=ci_attr2type_attr)
|
||||
|
||||
if computed_attrs:
|
||||
value_manager.handle_ci_compute_attributes(ci_dict, computed_attrs, ci)
|
||||
|
||||
if limit_attrs:
|
||||
for k in copy.deepcopy(ci_dict):
|
||||
if k not in limit_attrs:
|
||||
@@ -1133,7 +1137,14 @@ class CIRelationManager(object):
|
||||
return abort(400, ErrFormat.relation_constraint.format("1-N"))
|
||||
|
||||
@classmethod
|
||||
def add(cls, first_ci_id, second_ci_id, more=None, relation_type_id=None, ancestor_ids=None, valid=True):
|
||||
def add(cls, first_ci_id, second_ci_id,
|
||||
more=None,
|
||||
relation_type_id=None,
|
||||
ancestor_ids=None,
|
||||
valid=True,
|
||||
apply_async=True,
|
||||
source=None,
|
||||
uid=None):
|
||||
|
||||
first_ci = CIManager.confirm_ci_existed(first_ci_id)
|
||||
second_ci = CIManager.confirm_ci_existed(second_ci_id)
|
||||
@@ -1145,9 +1156,10 @@ class CIRelationManager(object):
|
||||
first=True)
|
||||
if existed is not None:
|
||||
if existed.relation_type_id != relation_type_id and relation_type_id is not None:
|
||||
existed.update(relation_type_id=relation_type_id)
|
||||
source = existed.source or source
|
||||
existed.update(relation_type_id=relation_type_id, source=source)
|
||||
|
||||
CIRelationHistoryManager().add(existed, OperateType.UPDATE)
|
||||
CIRelationHistoryManager().add(existed, OperateType.UPDATE, uid=uid)
|
||||
else:
|
||||
if relation_type_id is None:
|
||||
type_relation = CITypeRelation.get_by(parent_id=first_ci.type_id,
|
||||
@@ -1177,11 +1189,13 @@ class CIRelationManager(object):
|
||||
existed = CIRelation.create(first_ci_id=first_ci_id,
|
||||
second_ci_id=second_ci_id,
|
||||
relation_type_id=relation_type_id,
|
||||
ancestor_ids=ancestor_ids)
|
||||
|
||||
CIRelationHistoryManager().add(existed, OperateType.ADD)
|
||||
|
||||
ci_relation_cache.apply_async(args=(first_ci_id, second_ci_id, ancestor_ids), queue=CMDB_QUEUE)
|
||||
ancestor_ids=ancestor_ids,
|
||||
source=source)
|
||||
CIRelationHistoryManager().add(existed, OperateType.ADD, uid=uid)
|
||||
if apply_async:
|
||||
ci_relation_cache.apply_async(args=(first_ci_id, second_ci_id, ancestor_ids), queue=CMDB_QUEUE)
|
||||
else:
|
||||
ci_relation_cache(first_ci_id, second_ci_id, ancestor_ids)
|
||||
|
||||
if more is not None:
|
||||
existed.upadte(more=more)
|
||||
@@ -1189,7 +1203,7 @@ class CIRelationManager(object):
|
||||
return existed.id
|
||||
|
||||
@staticmethod
|
||||
def delete(cr_id):
|
||||
def delete(cr_id, apply_async=True):
|
||||
cr = CIRelation.get_by_id(cr_id) or abort(404, ErrFormat.relation_not_found.format("id={}".format(cr_id)))
|
||||
|
||||
if current_app.config.get('USE_ACL') and current_user.username != 'worker':
|
||||
@@ -1205,8 +1219,12 @@ class CIRelationManager(object):
|
||||
his_manager = CIRelationHistoryManager()
|
||||
his_manager.add(cr, operate_type=OperateType.DELETE)
|
||||
|
||||
ci_relation_delete.apply_async(args=(cr.first_ci_id, cr.second_ci_id, cr.ancestor_ids), queue=CMDB_QUEUE)
|
||||
delete_id_filter.apply_async(args=(cr.second_ci_id,), queue=CMDB_QUEUE)
|
||||
if apply_async:
|
||||
ci_relation_delete.apply_async(args=(cr.first_ci_id, cr.second_ci_id, cr.ancestor_ids), queue=CMDB_QUEUE)
|
||||
delete_id_filter.apply_async(args=(cr.second_ci_id,), queue=CMDB_QUEUE)
|
||||
else:
|
||||
ci_relation_delete(cr.first_ci_id, cr.second_ci_id, cr.ancestor_ids)
|
||||
delete_id_filter(cr.second_ci_id)
|
||||
|
||||
return cr_id
|
||||
|
||||
@@ -1221,23 +1239,23 @@ class CIRelationManager(object):
|
||||
if cr is not None:
|
||||
cls.delete(cr.id)
|
||||
|
||||
ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id, ancestor_ids), queue=CMDB_QUEUE)
|
||||
delete_id_filter.apply_async(args=(second_ci_id,), queue=CMDB_QUEUE)
|
||||
# ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id, ancestor_ids), queue=CMDB_QUEUE)
|
||||
# delete_id_filter.apply_async(args=(second_ci_id,), queue=CMDB_QUEUE)
|
||||
|
||||
return cr
|
||||
|
||||
@classmethod
|
||||
def delete_3(cls, first_ci_id, second_ci_id):
|
||||
def delete_3(cls, first_ci_id, second_ci_id, apply_async=True):
|
||||
cr = CIRelation.get_by(first_ci_id=first_ci_id,
|
||||
second_ci_id=second_ci_id,
|
||||
to_dict=False,
|
||||
first=True)
|
||||
|
||||
if cr is not None:
|
||||
ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id, cr.ancestor_ids), queue=CMDB_QUEUE)
|
||||
delete_id_filter.apply_async(args=(second_ci_id,), queue=CMDB_QUEUE)
|
||||
# ci_relation_delete.apply_async(args=(first_ci_id, second_ci_id, cr.ancestor_ids), queue=CMDB_QUEUE)
|
||||
# delete_id_filter.apply_async(args=(second_ci_id,), queue=CMDB_QUEUE)
|
||||
|
||||
cls.delete(cr.id)
|
||||
cls.delete(cr.id, apply_async=apply_async)
|
||||
|
||||
return cr
|
||||
|
||||
@@ -1276,6 +1294,27 @@ class CIRelationManager(object):
|
||||
for ci_id in ci_ids:
|
||||
cls.delete_2(parent_id, ci_id, ancestor_ids=ancestor_ids)
|
||||
|
||||
@classmethod
|
||||
def delete_relations_by_source(cls, source,
|
||||
first_ci_id=None, second_ci_type_id=None,
|
||||
second_ci_id=None, first_ci_type_id=None,
|
||||
added=None):
|
||||
existed = []
|
||||
if first_ci_id is not None and second_ci_type_id is not None:
|
||||
existed = [(i.first_ci_id, i.second_ci_id) for i in CIRelation.get_by(
|
||||
source=source, first_ci_id=first_ci_id, only_query=True).join(
|
||||
CI, CIRelation.second_ci_id == CI.id).filter(CI.type_id == second_ci_type_id)]
|
||||
|
||||
if second_ci_id is not None and first_ci_type_id is not None:
|
||||
existed = [(i.first_ci_id, i.second_ci_id) for i in CIRelation.get_by(
|
||||
source=source, second_ci_id=second_ci_id, only_query=True).join(
|
||||
CI, CIRelation.first_ci_id == CI.id).filter(CI.type_id == first_ci_type_id)]
|
||||
|
||||
deleted = set(existed) - set(added or [])
|
||||
|
||||
for first, second in deleted:
|
||||
cls.delete_3(first, second, apply_async=False)
|
||||
|
||||
@classmethod
|
||||
def build_by_attribute(cls, ci_dict):
|
||||
type_id = ci_dict['_type']
|
||||
@@ -1296,8 +1335,15 @@ class CIRelationManager(object):
|
||||
relations = _relations
|
||||
else:
|
||||
relations &= _relations
|
||||
|
||||
cls.delete_relations_by_source(RelationSourceEnum.ATTRIBUTE_VALUES,
|
||||
first_ci_id=ci_dict['_id'],
|
||||
second_ci_type_id=item.child_id,
|
||||
added=relations)
|
||||
for parent_ci_id, child_ci_id in (relations or []):
|
||||
CIRelationManager.add(parent_ci_id, child_ci_id, valid=False)
|
||||
cls.add(parent_ci_id, child_ci_id,
|
||||
valid=False,
|
||||
source=RelationSourceEnum.ATTRIBUTE_VALUES)
|
||||
|
||||
parent_items = CITypeRelation.get_by(child_id=type_id, only_query=True).filter(
|
||||
CITypeRelation.child_attr_ids.isnot(None))
|
||||
@@ -1316,11 +1362,18 @@ class CIRelationManager(object):
|
||||
relations = _relations
|
||||
else:
|
||||
relations &= _relations
|
||||
|
||||
cls.delete_relations_by_source(RelationSourceEnum.ATTRIBUTE_VALUES,
|
||||
second_ci_id=ci_dict['_id'],
|
||||
first_ci_type_id=item.parent_id,
|
||||
added=relations)
|
||||
for parent_ci_id, child_ci_id in (relations or []):
|
||||
CIRelationManager.add(parent_ci_id, child_ci_id, valid=False)
|
||||
cls.add(parent_ci_id, child_ci_id,
|
||||
valid=False,
|
||||
source=RelationSourceEnum.ATTRIBUTE_VALUES)
|
||||
|
||||
@classmethod
|
||||
def rebuild_all_by_attribute(cls, ci_type_relation):
|
||||
def rebuild_all_by_attribute(cls, ci_type_relation, uid):
|
||||
relations = None
|
||||
for parent_attr_id, child_attr_id in zip(ci_type_relation['parent_attr_ids'] or [],
|
||||
ci_type_relation['child_attr_ids'] or []):
|
||||
@@ -1352,11 +1405,29 @@ class CIRelationManager(object):
|
||||
else:
|
||||
relations &= _relations
|
||||
|
||||
t1 = aliased(CI)
|
||||
t2 = aliased(CI)
|
||||
query = db.session.query(CIRelation).join(t1, t1.id == CIRelation.first_ci_id).join(
|
||||
t2, t2.id == CIRelation.second_ci_id).filter(t1.type_id == ci_type_relation['parent_id']).filter(
|
||||
t2.type_id == ci_type_relation['child_id'])
|
||||
for i in query:
|
||||
db.session.delete(i)
|
||||
ci_relation_delete(i.first_ci_id, i.second_ci_id, i.ancestor_ids)
|
||||
try:
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
current_app.logger.error(e)
|
||||
db.session.rollback()
|
||||
|
||||
for parent_ci_id, child_ci_id in (relations or []):
|
||||
try:
|
||||
cls.add(parent_ci_id, child_ci_id, valid=False)
|
||||
except:
|
||||
pass
|
||||
cls.add(parent_ci_id, child_ci_id,
|
||||
valid=False,
|
||||
apply_async=False,
|
||||
source=RelationSourceEnum.ATTRIBUTE_VALUES,
|
||||
uid=uid)
|
||||
except Exception as e:
|
||||
current_app.logger.error(e)
|
||||
|
||||
|
||||
class CITriggerManager(object):
|
||||
|
@@ -993,7 +993,7 @@ class CITypeRelationManager(object):
|
||||
if ((parent_attr_ids and parent_attr_ids != old_parent_attr_ids) or
|
||||
(child_attr_ids and child_attr_ids != old_child_attr_ids)):
|
||||
from api.tasks.cmdb import rebuild_relation_for_attribute_changed
|
||||
rebuild_relation_for_attribute_changed.apply_async(args=(existed.to_dict(),))
|
||||
rebuild_relation_for_attribute_changed.apply_async(args=(existed.to_dict(), current_user.uid))
|
||||
|
||||
CITypeHistoryManager.add(CITypeOperateType.ADD_RELATION, p.id,
|
||||
change=dict(parent=p.to_dict(), child=c.to_dict(), relation_type_id=relation_type_id))
|
||||
@@ -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'))
|
||||
|
@@ -41,23 +41,23 @@ class OperateType(BaseEnum):
|
||||
|
||||
|
||||
class CITypeOperateType(BaseEnum):
|
||||
ADD = "0" # 新增模型
|
||||
UPDATE = "1" # 修改模型
|
||||
DELETE = "2" # 删除模型
|
||||
ADD_ATTRIBUTE = "3" # 新增属性
|
||||
UPDATE_ATTRIBUTE = "4" # 修改属性
|
||||
DELETE_ATTRIBUTE = "5" # 删除属性
|
||||
ADD_TRIGGER = "6" # 新增触发器
|
||||
UPDATE_TRIGGER = "7" # 修改触发器
|
||||
DELETE_TRIGGER = "8" # 删除触发器
|
||||
ADD_UNIQUE_CONSTRAINT = "9" # 新增联合唯一
|
||||
UPDATE_UNIQUE_CONSTRAINT = "10" # 修改联合唯一
|
||||
DELETE_UNIQUE_CONSTRAINT = "11" # 删除联合唯一
|
||||
ADD_RELATION = "12" # 新增关系
|
||||
DELETE_RELATION = "13" # 删除关系
|
||||
ADD_RECONCILIATION = "14" # 新增数据合规
|
||||
UPDATE_RECONCILIATION = "15" # 修改数据合规
|
||||
DELETE_RECONCILIATION = "16" # 删除数据合规
|
||||
ADD = "0" # add CIType
|
||||
UPDATE = "1" # update CIType
|
||||
DELETE = "2" # delete CIType
|
||||
ADD_ATTRIBUTE = "3"
|
||||
UPDATE_ATTRIBUTE = "4"
|
||||
DELETE_ATTRIBUTE = "5"
|
||||
ADD_TRIGGER = "6"
|
||||
UPDATE_TRIGGER = "7"
|
||||
DELETE_TRIGGER = "8"
|
||||
ADD_UNIQUE_CONSTRAINT = "9"
|
||||
UPDATE_UNIQUE_CONSTRAINT = "10"
|
||||
DELETE_UNIQUE_CONSTRAINT = "11"
|
||||
ADD_RELATION = "12"
|
||||
DELETE_RELATION = "13"
|
||||
ADD_RECONCILIATION = "14"
|
||||
UPDATE_RECONCILIATION = "15"
|
||||
DELETE_RECONCILIATION = "16"
|
||||
|
||||
|
||||
class RetKey(BaseEnum):
|
||||
@@ -93,7 +93,7 @@ class RoleEnum(BaseEnum):
|
||||
class AutoDiscoveryType(BaseEnum):
|
||||
AGENT = "agent"
|
||||
SNMP = "snmp"
|
||||
HTTP = "http" # cloud
|
||||
HTTP = "http" # cloud
|
||||
COMPONENTS = "components"
|
||||
|
||||
|
||||
@@ -108,13 +108,17 @@ class ExecuteStatusEnum(BaseEnum):
|
||||
FAILED = '1'
|
||||
RUNNING = '2'
|
||||
|
||||
class RelationSourceEnum(BaseEnum):
|
||||
ATTRIBUTE_VALUES = "0"
|
||||
AUTO_DISCOVERY = "1"
|
||||
|
||||
|
||||
CMDB_QUEUE = "one_cmdb_async"
|
||||
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
|
||||
|
@@ -232,8 +232,8 @@ class AttributeHistoryManger(object):
|
||||
|
||||
class CIRelationHistoryManager(object):
|
||||
@staticmethod
|
||||
def add(rel_obj, operate_type=OperateType.ADD):
|
||||
record = OperationRecord.create(uid=current_user.uid)
|
||||
def add(rel_obj, operate_type=OperateType.ADD, uid=None):
|
||||
record = OperationRecord.create(uid=uid or current_user.uid)
|
||||
|
||||
CIRelationHistory.create(relation_id=rel_obj.id,
|
||||
record_id=record.id,
|
||||
|
@@ -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!") # 预定义值: 其他模型请求参数不合法!
|
||||
|
||||
|
@@ -28,13 +28,31 @@ def string2int(x):
|
||||
return v
|
||||
|
||||
|
||||
def str2datetime(x):
|
||||
def str2date(x):
|
||||
|
||||
try:
|
||||
return datetime.datetime.strptime(x, "%Y-%m-%d").date()
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return datetime.datetime.strptime(x, "%Y-%m-%d %H:%M:%S")
|
||||
try:
|
||||
return datetime.datetime.strptime(x, "%Y-%m-%d %H:%M:%S").date()
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
def str2datetime(x):
|
||||
|
||||
x = x.replace('T', ' ')
|
||||
x = x.replace('Z', '')
|
||||
|
||||
try:
|
||||
return datetime.datetime.strptime(x, "%Y-%m-%d %H:%M:%S")
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return datetime.datetime.strptime(x, "%Y-%m-%d %H:%M")
|
||||
|
||||
|
||||
|
||||
class ValueTypeMap(object):
|
||||
@@ -44,7 +62,7 @@ class ValueTypeMap(object):
|
||||
ValueTypeEnum.TEXT: lambda x: x,
|
||||
ValueTypeEnum.TIME: lambda x: TIME_RE.findall(x)[0],
|
||||
ValueTypeEnum.DATETIME: str2datetime,
|
||||
ValueTypeEnum.DATE: str2datetime,
|
||||
ValueTypeEnum.DATE: str2date,
|
||||
ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x,
|
||||
}
|
||||
|
||||
|
@@ -94,7 +94,7 @@ class AttributeValueManager(object):
|
||||
except ValueDeserializeError as e:
|
||||
return abort(400, ErrFormat.attribute_value_invalid2.format(alias, e))
|
||||
except ValueError:
|
||||
return abort(400, ErrFormat.attribute_value_invalid.format(value))
|
||||
return abort(400, ErrFormat.attribute_value_invalid2.format(alias, value))
|
||||
|
||||
@staticmethod
|
||||
def _check_is_choice(attr, value_type, value):
|
||||
@@ -123,9 +123,9 @@ class AttributeValueManager(object):
|
||||
return abort(400, ErrFormat.attribute_value_required.format(attr.alias))
|
||||
|
||||
@staticmethod
|
||||
def check_re(expr, value):
|
||||
def check_re(expr, alias, value):
|
||||
if not re.compile(expr).match(str(value)):
|
||||
return abort(400, ErrFormat.attribute_value_invalid.format(value))
|
||||
return abort(400, ErrFormat.attribute_value_invalid2.format(alias, value))
|
||||
|
||||
def _validate(self, attr, value, value_table, ci=None, type_id=None, ci_id=None, type_attr=None):
|
||||
ci = ci or {}
|
||||
@@ -141,7 +141,7 @@ class AttributeValueManager(object):
|
||||
v = None
|
||||
|
||||
if attr.re_check and value:
|
||||
self.check_re(attr.re_check, value)
|
||||
self.check_re(attr.re_check, attr.alias, value)
|
||||
|
||||
return v
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -2,7 +2,6 @@
|
||||
|
||||
|
||||
import datetime
|
||||
|
||||
from sqlalchemy.dialects.mysql import DOUBLE
|
||||
|
||||
from api.extensions import db
|
||||
@@ -11,6 +10,7 @@ from api.lib.cmdb.const import CIStatusEnum
|
||||
from api.lib.cmdb.const import CITypeOperateType
|
||||
from api.lib.cmdb.const import ConstraintEnum
|
||||
from api.lib.cmdb.const import OperateType
|
||||
from api.lib.cmdb.const import RelationSourceEnum
|
||||
from api.lib.cmdb.const import ValueTypeEnum
|
||||
from api.lib.database import Model
|
||||
from api.lib.database import Model2
|
||||
@@ -260,6 +260,7 @@ class CIRelation(Model):
|
||||
second_ci_id = db.Column(db.Integer, db.ForeignKey("c_cis.id"), nullable=False)
|
||||
relation_type_id = db.Column(db.Integer, db.ForeignKey("c_relation_types.id"), nullable=False)
|
||||
more = db.Column(db.Integer, db.ForeignKey("c_cis.id"))
|
||||
source = db.Column(db.Enum(*RelationSourceEnum.all()), name="source")
|
||||
|
||||
ancestor_ids = db.Column(db.String(128), index=True)
|
||||
|
||||
@@ -578,6 +579,7 @@ class AutoDiscoveryCIType(Model):
|
||||
|
||||
extra_option = db.Column(db.JSON)
|
||||
uid = db.Column(db.Integer, index=True)
|
||||
enabled = db.Column(db.Boolean, default=True)
|
||||
|
||||
|
||||
class AutoDiscoveryCITypeRelation(Model):
|
||||
@@ -634,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"
|
||||
|
||||
|
@@ -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)
|
||||
@@ -58,10 +60,10 @@ def ci_cache(ci_id, operate_type, record_id):
|
||||
|
||||
@celery.task(name="cmdb.rebuild_relation_for_attribute_changed", queue=CMDB_QUEUE)
|
||||
@reconnect_db
|
||||
def rebuild_relation_for_attribute_changed(ci_type_relation):
|
||||
def rebuild_relation_for_attribute_changed(ci_type_relation, uid):
|
||||
from api.lib.cmdb.ci import CIRelationManager
|
||||
|
||||
CIRelationManager.rebuild_all_by_attribute(ci_type_relation)
|
||||
CIRelationManager.rebuild_all_by_attribute(ci_type_relation, uid)
|
||||
|
||||
|
||||
@celery.task(name="cmdb.batch_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
|
||||
|
@@ -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
|
||||
@@ -111,6 +113,7 @@ class AutoDiscoveryRuleTemplateFileView(APIView):
|
||||
class AutoDiscoveryRuleHTTPView(APIView):
|
||||
url_prefix = ("/adr/http/<string:name>/categories",
|
||||
"/adr/http/<string:name>/attributes",
|
||||
"/adr/http/<string:name>/mapping",
|
||||
"/adr/snmp/<string:name>/attributes",
|
||||
"/adr/components/<string:name>/attributes",)
|
||||
|
||||
@@ -125,6 +128,10 @@ class AutoDiscoveryRuleHTTPView(APIView):
|
||||
resource = request.values.get('resource')
|
||||
return self.jsonify(AutoDiscoveryHTTPManager.get_attributes(name, resource))
|
||||
|
||||
if "mapping" in request.url:
|
||||
resource = request.values.get('resource')
|
||||
return self.jsonify(AutoDiscoveryHTTPManager.get_mapping(name, resource))
|
||||
|
||||
return self.jsonify(AutoDiscoveryHTTPManager.get_categories(name))
|
||||
|
||||
|
||||
@@ -144,6 +151,11 @@ class AutoDiscoveryCITypeView(APIView):
|
||||
i['extra_option'].pop('secret', None)
|
||||
else:
|
||||
i['extra_option']['secret'] = AESCrypto.decrypt(i['extra_option']['secret'])
|
||||
if isinstance(i.get("extra_option"), dict) and i['extra_option'].get('password'):
|
||||
if not (current_user.username == "cmdb_agent" or current_user.uid == i['uid']):
|
||||
i['extra_option'].pop('password', None)
|
||||
else:
|
||||
i['extra_option']['password'] = AESCrypto.decrypt(i['extra_option']['password'])
|
||||
|
||||
return self.jsonify(res)
|
||||
|
||||
@@ -262,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'):
|
||||
@@ -318,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,
|
||||
@@ -345,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)
|
||||
|
@@ -55,3 +55,4 @@ pycryptodomex>=3.19.0
|
||||
colorama>=0.4.6
|
||||
lz4>=4.3.2
|
||||
python-magic==0.4.27
|
||||
jsonpath==0.82.2
|
||||
|
@@ -54,6 +54,84 @@
|
||||
<div class="content unicode" style="display: block;">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">caise-数据中心</div>
|
||||
<div class="code-name">&#xe96f;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">caise-文件夹</div>
|
||||
<div class="code-name">&#xe970;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">caise-资源池</div>
|
||||
<div class="code-name">&#xe971;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">caise-网络</div>
|
||||
<div class="code-name">&#xe972;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">caise-分布式交换机</div>
|
||||
<div class="code-name">&#xe973;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">caise-标准式交换机</div>
|
||||
<div class="code-name">&#xe974;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">caise-主机集群</div>
|
||||
<div class="code-name">&#xe975;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">caise-数据存储集群</div>
|
||||
<div class="code-name">&#xe976;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">caise-数据存储</div>
|
||||
<div class="code-name">&#xe977;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">veops-account</div>
|
||||
<div class="code-name">&#xe96e;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">veops-collect</div>
|
||||
<div class="code-name">&#xe96d;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">veops-collected</div>
|
||||
<div class="code-name">&#xe96c;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">veops-text</div>
|
||||
<div class="code-name">&#xe96b;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></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>
|
||||
|
@@ -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
@@ -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.
Binary file not shown.
Binary file not shown.
@@ -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: '已连接'
|
||||
}, {
|
||||
|
@@ -57,7 +57,9 @@ export default {
|
||||
computed: {
|
||||
...mapState(['user', 'locale']),
|
||||
hasBackendPermission() {
|
||||
return this.user?.detailPermissions?.backend?.length
|
||||
const isAdmin = this?.user?.roles?.permissions?.includes('acl_admin')
|
||||
|
||||
return isAdmin || this.user?.detailPermissions?.backend?.length
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
@@ -52,6 +52,32 @@ export function getSnmpAttributes(type, name) {
|
||||
})
|
||||
}
|
||||
|
||||
export function getHttpAttrMapping(name, resource) {
|
||||
return axios({
|
||||
url: `/v0.1/adr/http/${name}/mapping`,
|
||||
method: 'GET',
|
||||
params: {
|
||||
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}`,
|
||||
|
@@ -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
|
||||
})
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -252,6 +252,8 @@ export default {
|
||||
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
min-width: 100px;
|
||||
text-align: center;
|
||||
|
||||
&:hover {
|
||||
background-color: @layout-sidebar-selected-color;
|
||||
|
@@ -9,7 +9,7 @@
|
||||
@clickCategory="setCurrentCate"
|
||||
/>
|
||||
<template v-else>
|
||||
<a-select v-if="isCloud" :style="{ marginBottom: '10px', minWidth: '120px' }" v-model="currentCate">
|
||||
<a-select v-if="isCloud" :style="{ marginBottom: '10px', minWidth: '200px' }" v-model="currentCate">
|
||||
<a-select-option v-for="cate in categoriesSelect" :key="cate" :value="cate">{{ cate }}</a-select-option>
|
||||
</a-select>
|
||||
<AttrMapTable
|
||||
@@ -29,7 +29,9 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getHttpCategories, getHttpAttributes, getSnmpAttributes } from '../../api/discovery'
|
||||
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'
|
||||
@@ -69,6 +71,10 @@ export default {
|
||||
uniqueKey: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
currentAdt: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -77,6 +83,7 @@ export default {
|
||||
categoriesSelect: [],
|
||||
currentCate: '',
|
||||
tableData: [],
|
||||
httpAttrMap: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -95,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: {
|
||||
@@ -103,13 +110,7 @@ export default {
|
||||
immediate: true,
|
||||
handler(newVal) {
|
||||
if (newVal) {
|
||||
getHttpAttributes(this.ruleName, { resource: newVal }).then((res) => {
|
||||
if (this.isEdit) {
|
||||
this.formatTableData(res)
|
||||
} else {
|
||||
this.tableData = res
|
||||
}
|
||||
})
|
||||
this.getHttpAttr(newVal)
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -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)
|
||||
@@ -140,7 +141,7 @@ export default {
|
||||
})
|
||||
this.categoriesSelect = categoriesSelect
|
||||
if (this.isEdit && categoriesSelect?.length) {
|
||||
this.currentCate = categoriesSelect[0]
|
||||
this.currentCate = this?.currentAdt?.extra_option?.category || categoriesSelect[0]
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -158,28 +159,54 @@ export default {
|
||||
},
|
||||
formatTableData(list) {
|
||||
const _findADT = this.adCITypeList.find((item) => Number(item.adr_id) === Number(this.currentTab))
|
||||
this.tableData = (list || []).map((item) => {
|
||||
if (_findADT.attributes) {
|
||||
return {
|
||||
...item,
|
||||
attr: _findADT.attributes[`${item.name}`],
|
||||
}
|
||||
} else {
|
||||
this.tableData = (list || []).map((val) => {
|
||||
const item = _.cloneDeep(val)
|
||||
|
||||
if (_findADT?.attributes?.[item.name]) {
|
||||
item.attr = _findADT.attributes[item.name]
|
||||
}
|
||||
|
||||
const attrMapName = this.httpAttrMap?.[item?.name]
|
||||
|
||||
if (
|
||||
this.isEdit &&
|
||||
!item.attr &&
|
||||
attrMapName &&
|
||||
this.ciTypeAttributes.some((ele) => ele.name === attrMapName)
|
||||
) {
|
||||
item.attr = attrMapName
|
||||
}
|
||||
|
||||
if (!item.attr) {
|
||||
const _find = this.ciTypeAttributes.find((ele) => ele.name === item.name)
|
||||
if (_find) {
|
||||
return {
|
||||
...item,
|
||||
attr: _find.name,
|
||||
}
|
||||
item.attr = _find.name
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
return item
|
||||
})
|
||||
},
|
||||
getTableData() {
|
||||
const $table = this.$refs.attrMapTable
|
||||
const { fullData } = $table.getTableData()
|
||||
return fullData || []
|
||||
},
|
||||
|
||||
async getHttpAttr(val) {
|
||||
await this.getHttpAttrMapping(this.ruleName, val)
|
||||
getHttpAttributes(this.ruleName, { resource: val }).then((res) => {
|
||||
if (this.isEdit) {
|
||||
this.formatTableData(res)
|
||||
} else {
|
||||
this.tableData = res
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async getHttpAttrMapping(name, resource) {
|
||||
const res = await getHttpAttrMapping(name, resource)
|
||||
this.httpAttrMap = res || {}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -236,7 +236,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',
|
||||
@@ -259,6 +259,23 @@ const cmdb_en = {
|
||||
account: 'Account',
|
||||
insecure: 'Certificate Validation',
|
||||
vcenterName: 'Platform Name',
|
||||
resourceSearchTip1: 'Please use conditional filtering for CI filtering and copy and paste the filter expression into the fill-in box in the previous step.',
|
||||
resourceSearchTip2: 'Note 1: Please use the green button to the right of the expression to copy it',
|
||||
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'
|
||||
},
|
||||
components: {
|
||||
unselectAttributes: 'Unselected',
|
||||
@@ -566,7 +583,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',
|
||||
|
@@ -236,7 +236,7 @@ const cmdb_zh = {
|
||||
checkModalColumn4: '最近检查时间',
|
||||
testModalTitle: '自动发现测试',
|
||||
attrMapTableAttrPlaceholder: '请编辑名称',
|
||||
nodeSettingIp: 'ip地址',
|
||||
nodeSettingIp: '网络设备IP地址',
|
||||
nodeSettingIpTip: '请输入 ip 地址',
|
||||
nodeSettingIpTip1: 'ip地址格式错误',
|
||||
nodeSettingCommunity: 'Community',
|
||||
@@ -259,6 +259,23 @@ const cmdb_zh = {
|
||||
account: '账号',
|
||||
insecure: '是否证书验证',
|
||||
vcenterName: '虚拟平台名',
|
||||
resourceSearchTip1: '请使用条件过滤进行CI筛选,并将过滤表达式复制粘贴到上一步填写框中。',
|
||||
resourceSearchTip2: '注1:请使用表达式右侧的绿色按钮进行复制',
|
||||
resourceSearchTip3: '注2:如不需要筛选,请直接点击灰色按钮进行复制粘贴,即可配置为所有节点',
|
||||
enable: '开启',
|
||||
enableTip: '确定切换开启状态吗',
|
||||
portScanConfig: '端口扫描配置',
|
||||
portScanLabel1: 'CIDR',
|
||||
portScanLabel2: '端口范围',
|
||||
portScanLabel3: 'AgentID',
|
||||
viewAllAttr: '查看所有属性',
|
||||
attrGroup: '属性分组',
|
||||
attrName: '属性名称',
|
||||
attrAlias: '属性别名',
|
||||
attrCode: '属性代码',
|
||||
computedAttrTip1: '引用属性遵循jinja2语法',
|
||||
computedAttrTip2: `多值属性(列表)默认呈现包括[ ], 如果要去掉, 引用方法为: """{{ attr_name | join(',')}}""" 其中逗号为分隔符`,
|
||||
example: '例如'
|
||||
},
|
||||
components: {
|
||||
unselectAttributes: '未选属性',
|
||||
@@ -565,7 +582,13 @@ if __name__ == "__main__":
|
||||
discoveryCardResoureTip: '自动发现的资源类型数',
|
||||
addPlugin: '新增插件',
|
||||
pluginSearchTip: '请搜索规则',
|
||||
innerFlag: '内置'
|
||||
innerFlag: '内置',
|
||||
defaultName: '默认名称',
|
||||
deleteTip: '不可再删除',
|
||||
tabCustom: '自定义',
|
||||
tabConfig: '已有配置',
|
||||
addConfig: '添加配置',
|
||||
configErrTip: '请选择配置'
|
||||
},
|
||||
ci: {
|
||||
attributeDesc: '查看属性配置',
|
||||
|
@@ -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"
|
||||
/>
|
||||
|
@@ -105,7 +105,7 @@ export default {
|
||||
default: true,
|
||||
},
|
||||
attrList: {
|
||||
type: Array,
|
||||
type: Function,
|
||||
default: () => [],
|
||||
}
|
||||
},
|
||||
|
@@ -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>
|
||||
|
@@ -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;
|
||||
|
@@ -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()
|
||||
})
|
||||
},
|
||||
|
@@ -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>
|
||||
|
140
cmdb-ui/src/modules/cmdb/views/ci_types/allAttrDrawer.vue
Normal file
140
cmdb-ui/src/modules/cmdb/views/ci_types/allAttrDrawer.vue
Normal 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 = '其他'
|
||||
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>
|
@@ -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()
|
||||
|
@@ -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>
|
@@ -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>
|
15
cmdb-ui/src/modules/cmdb/views/ci_types/attrAD/constants.js
Normal file
15
cmdb-ui/src/modules/cmdb/views/ci_types/attrAD/constants.js
Normal 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'
|
||||
}
|
||||
]
|
@@ -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>
|
@@ -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>
|
@@ -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>
|
@@ -14,10 +14,25 @@
|
||||
<span>{{ $t('edit') }}</span>
|
||||
</a-space>
|
||||
</a>
|
||||
<div class="attr-ad-header">{{ $t('cmdb.ciType.attributeMap') }}</div>
|
||||
<div class="attr-ad-header attr-ad-header_between">
|
||||
{{ $t('cmdb.ciType.attributeMap') }}
|
||||
<div class="attr-ad-open">
|
||||
<span class="attr-ad-open-label">{{ $t('cmdb.ciType.enable') }}</span>
|
||||
<a-switch v-model="form.enabled" v-if="isClient" />
|
||||
<a-popconfirm
|
||||
v-else
|
||||
:title="$t('cmdb.ciType.enableTip')"
|
||||
:ok-text="$t('confirm')"
|
||||
:cancel-text="$t('cancel')"
|
||||
@confirm="changeEnabled"
|
||||
>
|
||||
<a-switch :checked="form.enabled" />
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
</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"
|
||||
@@ -34,20 +49,22 @@
|
||||
:adCITypeList="adCITypeList"
|
||||
:currentTab="adr_id"
|
||||
:uniqueKey="uniqueKey"
|
||||
:currentAdt="currentAdt"
|
||||
: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"
|
||||
>
|
||||
@@ -111,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"
|
||||
/>
|
||||
@@ -173,11 +166,13 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
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'
|
||||
@@ -185,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',
|
||||
@@ -195,7 +194,11 @@ export default {
|
||||
NodeSetting,
|
||||
AttrMapTable,
|
||||
AttrADTest,
|
||||
ElPopover: Popover
|
||||
ElPopover: Popover,
|
||||
VcenterForm,
|
||||
PublicCloud,
|
||||
PortScanConfig,
|
||||
CIDRTags
|
||||
},
|
||||
props: {
|
||||
adr_id: {
|
||||
@@ -234,36 +237,55 @@ export default {
|
||||
agent_id: '',
|
||||
auto_accept: false,
|
||||
query_expr: '',
|
||||
enabled: true,
|
||||
},
|
||||
form2: {
|
||||
publicCloudForm: {
|
||||
key: '',
|
||||
secret: '',
|
||||
_reference: '',
|
||||
tabActive: TAB_KEY.CUSTOM,
|
||||
},
|
||||
privateCloudForm: {
|
||||
host: '',
|
||||
account: '',
|
||||
password: '',
|
||||
insecure: false,
|
||||
// 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
|
||||
PRIVATE_CLOUD_NAME,
|
||||
DISCOVERY_CATEGORY_TYPE,
|
||||
isClient: false, // 是否前端新增临时数据
|
||||
cidrList: [],
|
||||
}
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
provide_labelCol: () => {
|
||||
return this.labelCol
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -287,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
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -316,8 +340,9 @@ export default {
|
||||
const _find = this.adrList.find((item) => Number(item.id) === Number(this.adr_id))
|
||||
const _findADT = this.adCITypeList.find((item) => Number(item.id) === Number(this.currentAdt.id))
|
||||
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 = '',
|
||||
@@ -325,50 +350,86 @@ export default {
|
||||
host = '',
|
||||
account = '',
|
||||
password = '',
|
||||
insecure = false,
|
||||
vcenterName = ''
|
||||
// insecure = false,
|
||||
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 {
|
||||
@@ -391,6 +452,7 @@ export default {
|
||||
auto_accept: _findADT?.auto_accept || false,
|
||||
agent_id: _findADT?.agent_id && _findADT?.agent_id !== '0x0000' ? _findADT.agent_id : '',
|
||||
query_expr: _findADT.query_expr || '',
|
||||
enabled: _findADT?.enabled ?? true,
|
||||
}
|
||||
if (_findADT.query_expr) {
|
||||
this.agent_type = 'query_expr'
|
||||
@@ -400,7 +462,6 @@ export default {
|
||||
this.agent_type = this.agentTypeRadioList[0].value
|
||||
}
|
||||
|
||||
this.interval = 'cron'
|
||||
this.cron = _findADT?.cron || ''
|
||||
},
|
||||
|
||||
@@ -411,19 +472,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 = {
|
||||
@@ -433,12 +485,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 = {}
|
||||
@@ -469,7 +534,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') {
|
||||
@@ -504,6 +569,11 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
// 去除合并后的旧配置
|
||||
if (params.extra_option) {
|
||||
params.extra_option = this.handleOldExtraOption(params.extra_option)
|
||||
}
|
||||
|
||||
if (currentAdt?.isClient) {
|
||||
postCITypeDiscovery(this.CITypeId, params).then((res) => {
|
||||
this.$message.success(this.$t('saveSuccess'))
|
||||
@@ -517,42 +587,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,
|
||||
@@ -562,6 +686,17 @@ export default {
|
||||
hideCron() {
|
||||
this.cronVisible = false
|
||||
},
|
||||
changeEnabled() {
|
||||
if (!this.isClient) {
|
||||
putCITypeDiscovery(this.currentAdt.id, {
|
||||
enabled: !this.form.enabled
|
||||
}).then((res) => {
|
||||
this.form.enabled = !this.form.enabled
|
||||
this.$message.success(this.$t('saveSuccess'))
|
||||
this.$emit('handleSave', res.id)
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -572,6 +707,26 @@ export default {
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
|
||||
.attr-ad-header_between {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.attr-ad-open {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 0px 20px;
|
||||
|
||||
&-label {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.attr-ad-attributemap-main {
|
||||
margin-left: 17px;
|
||||
}
|
||||
@@ -597,6 +752,7 @@ export default {
|
||||
.radio-master-tip {
|
||||
font-size: 12px;
|
||||
color: #86909c;
|
||||
line-height: 14px;
|
||||
}
|
||||
}
|
||||
.attr-ad-snmp-form {
|
||||
|
@@ -375,6 +375,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 +799,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 {
|
||||
|
@@ -255,6 +255,12 @@ export default {
|
||||
show_id: () => {
|
||||
return this.show_id
|
||||
},
|
||||
providerGroupsData: () => {
|
||||
return {
|
||||
CITypeGroups: this.CITypeGroups,
|
||||
otherGroupAttributes: this.otherGroupAttributes
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeCreate() {},
|
||||
@@ -645,7 +651,7 @@ export default {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-start;
|
||||
min-height: 20px;
|
||||
> i {
|
||||
width: 182px;
|
||||
|
@@ -363,6 +363,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>
|
||||
@@ -610,6 +614,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 {
|
||||
|
@@ -14,17 +14,18 @@
|
||||
<TriggerTable ref="triggerTable" :CITypeId="CITypeId"></TriggerTable>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="6" :tab="$t('cmdb.ciType.grant')">
|
||||
<template v-if="activeKey === '6'">
|
||||
<div class="grant-config-wrap" :style="{ maxHeight: `${windowHeight - 150}px` }" v-if="activeKey === '6'">
|
||||
<GrantComp :CITypeId="CITypeId" resourceType="CIType" :resourceTypeName="CITypeName"></GrantComp>
|
||||
<div class="citype-detail-title">{{ $t('cmdb.components.relationGrant') }}</div>
|
||||
<RelationTable isInGrantComp :CITypeId="CITypeId" :CITypeName="CITypeName"></RelationTable>
|
||||
</template>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import AttributesTable from './attributesTable'
|
||||
import RelationTable from './relationTable'
|
||||
import TriggerTable from './triggerTable.vue'
|
||||
@@ -57,6 +58,11 @@ export default {
|
||||
},
|
||||
beforeCreate() {},
|
||||
mounted() {},
|
||||
computed: {
|
||||
...mapState({
|
||||
windowHeight: (state) => state.windowHeight,
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
changeTab(activeKey) {
|
||||
this.activeKey = activeKey
|
||||
@@ -81,4 +87,7 @@ export default {
|
||||
margin-left: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.grant-config-wrap {
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
|
@@ -13,18 +13,26 @@
|
||||
@input="onCodeChange"
|
||||
></codemirror>
|
||||
</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 AllAttrDrawer from './allAttrDrawer.vue'
|
||||
import { codemirror } from 'vue-codemirror'
|
||||
import 'codemirror/lib/codemirror.css'
|
||||
import 'codemirror/theme/monokai.css'
|
||||
@@ -32,7 +40,10 @@ import 'codemirror/theme/monokai.css'
|
||||
require('codemirror/mode/python/python.js')
|
||||
export default {
|
||||
name: 'ComputedArea',
|
||||
components: { codemirror },
|
||||
components: {
|
||||
codemirror,
|
||||
AllAttrDrawer
|
||||
},
|
||||
props: {
|
||||
canDefineComputed: {
|
||||
type: Boolean,
|
||||
@@ -108,6 +119,9 @@ export default {
|
||||
},
|
||||
onCodeChange(v) {
|
||||
this.compute_script = v.replace('\t', ' ')
|
||||
},
|
||||
showAllPropDrawer() {
|
||||
this.$refs.allAttrDrawer.open()
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@@ -586,7 +586,7 @@ export default {
|
||||
searchResourceType({ page_size: 9999, app_id: 'cmdb' }).then((res) => {
|
||||
this.resource_type = { groups: res.groups, id2perms: res.id2perms }
|
||||
})
|
||||
this.loadCITypes(!_currentId)
|
||||
this.loadCITypes(!_currentId, true)
|
||||
this.getAttributes()
|
||||
},
|
||||
methods: {
|
||||
@@ -598,7 +598,7 @@ export default {
|
||||
handleSearch(e) {
|
||||
this.searchValue = e.target.value
|
||||
},
|
||||
async loadCITypes(isResetCurrentId = false) {
|
||||
async loadCITypes(isResetCurrentId = false, isInit = false) {
|
||||
const groups = await getCITypeGroupsConfig({ need_other: true })
|
||||
let alreadyReset = false
|
||||
if (isResetCurrentId) {
|
||||
@@ -618,6 +618,21 @@ export default {
|
||||
g.ci_types = []
|
||||
}
|
||||
})
|
||||
|
||||
if (isInit) {
|
||||
const isMatch = groups.some((g) => {
|
||||
const matchGroup = `${g?.id}%null%null` === this.currentId
|
||||
const matchCITypes = g?.ci_types?.some((item) => `${g?.id}%${item?.id}%${item?.name}` === this.currentId)
|
||||
return matchGroup || matchCITypes
|
||||
})
|
||||
|
||||
if (!isMatch) {
|
||||
if (groups?.[0]?.ci_types?.[0]?.id) {
|
||||
this.currentId = `${groups[0].id}%${groups[0].ci_types[0].id}%${groups[0].ci_types[0].name}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.CITypeGroups = groups
|
||||
localStorage.setItem('ops_cityps_currentId', this.currentId)
|
||||
})
|
||||
|
@@ -33,7 +33,7 @@
|
||||
>
|
||||
<template
|
||||
slot="children"
|
||||
slot-scope="{ props: { direction, selectedKeys }, on: { itemSelect } }"
|
||||
slot-scope="{ props: { direction, selectedKeys }, on: { itemSelect, itemSelectAll } }"
|
||||
>
|
||||
<a-tree
|
||||
v-if="direction === 'left'"
|
||||
@@ -41,15 +41,15 @@
|
||||
checkable
|
||||
:checkedKeys="[...selectedKeys, ...targetKeys]"
|
||||
:treeData="treeData"
|
||||
:checkStrictly="true"
|
||||
:checkStrictly="false"
|
||||
@check="
|
||||
(_, props) => {
|
||||
onChecked(_, props, [...selectedKeys, ...targetKeys], itemSelect);
|
||||
onChecked(_, props, [...selectedKeys, ...targetKeys], itemSelect, itemSelectAll);
|
||||
}
|
||||
"
|
||||
@select="
|
||||
(_, props) => {
|
||||
onChecked(_, props, [...selectedKeys, ...targetKeys], itemSelect);
|
||||
onChecked(_, props, [...selectedKeys, ...targetKeys], itemSelect, itemSelectAll);
|
||||
}
|
||||
"
|
||||
/>
|
||||
@@ -85,10 +85,10 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
transferDataSource() {
|
||||
const dataSource = this.CITypeGroups.reduce((acc, item) => {
|
||||
const types = _.cloneDeep(item?.ci_types || [])
|
||||
const dataSource = this.CITypeGroups.reduce((acc, group) => {
|
||||
const types = _.cloneDeep(group?.ci_types || [])
|
||||
types.forEach((item) => {
|
||||
item.key = String(item.id)
|
||||
item.key = `${group.id}-${item.id}`
|
||||
item.title = item?.alias || item?.name || this.$t('other')
|
||||
})
|
||||
return acc.concat(types)
|
||||
@@ -100,7 +100,7 @@ export default {
|
||||
let newTreeData = treeData.map((item) => {
|
||||
const childrenKeys = []
|
||||
const children = (item.ci_types || []).map((child) => {
|
||||
const key = String(child?.id)
|
||||
const key = `${item.id}-${child.id}`
|
||||
const disabled = this.targetKeys.includes(key)
|
||||
childrenKeys.push(key)
|
||||
|
||||
@@ -108,7 +108,6 @@ export default {
|
||||
key,
|
||||
title: child?.alias || child?.name || this.$t('other'),
|
||||
disabled,
|
||||
checkable: true,
|
||||
children: []
|
||||
}
|
||||
})
|
||||
@@ -118,25 +117,46 @@ export default {
|
||||
children,
|
||||
childrenKeys,
|
||||
disabled: children.every((item) => item.disabled),
|
||||
checkable: false,
|
||||
selectable: false
|
||||
}
|
||||
})
|
||||
console.log('treeData', newTreeData)
|
||||
newTreeData = newTreeData.filter((item) => item.children.length > 0)
|
||||
|
||||
return newTreeData
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onChange(targetKeys, direction, moveKeys) {
|
||||
this.targetKeys = targetKeys
|
||||
const childKeys = []
|
||||
const newTargetKeys = [...targetKeys]
|
||||
|
||||
if (direction === 'right') {
|
||||
// 如果是选中父节点,添加时去除父节点,添加其子节点
|
||||
this.treeData.forEach((item) => {
|
||||
const parentIndex = newTargetKeys.findIndex((key) => item.key === key)
|
||||
if (parentIndex !== -1) {
|
||||
newTargetKeys.splice(parentIndex, 1)
|
||||
childKeys.push(...item.childrenKeys)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const uniqTargetKeys = _.uniq([...newTargetKeys, ...childKeys])
|
||||
this.targetKeys = uniqTargetKeys
|
||||
},
|
||||
onChecked(_, e, checkedKeys, itemSelect) {
|
||||
onChecked(_, e, checkedKeys, itemSelect, itemSelectAll) {
|
||||
const { eventKey } = e.node
|
||||
const selected = checkedKeys.indexOf(eventKey) === -1
|
||||
|
||||
itemSelect(eventKey, selected)
|
||||
const childrenKeys = this.treeData.find((item) => item.key === eventKey)?.childrenKeys || []
|
||||
// 如果当前点击是子节点,处理其联动父节点
|
||||
this.treeData.forEach((item) => {
|
||||
if (item.childrenKeys.includes(eventKey)) {
|
||||
if (selected && item.childrenKeys.every((childKey) => [eventKey, ...checkedKeys].includes(childKey))) {
|
||||
itemSelect(item.key, true)
|
||||
} else if (!selected) {
|
||||
itemSelect(item.key, false)
|
||||
}
|
||||
}
|
||||
})
|
||||
itemSelectAll([eventKey, ...childrenKeys], selected)
|
||||
},
|
||||
handleCancel() {
|
||||
this.$emit('cancel')
|
||||
@@ -152,7 +172,7 @@ export default {
|
||||
const hide = this.$message.loading(this.$t('loading'), 0)
|
||||
|
||||
try {
|
||||
const typeIds = this.targetKeys.join(',')
|
||||
const typeIds = this.getTypeIds(this.targetKeys)
|
||||
const res = await exportCITypeGroups({
|
||||
type_ids: typeIds
|
||||
})
|
||||
@@ -180,6 +200,13 @@ export default {
|
||||
hide()
|
||||
this.btnLoading = false
|
||||
})
|
||||
},
|
||||
getTypeIds(targetKeys) {
|
||||
let typeIds = targetKeys?.map((key) => {
|
||||
return this?.transferDataSource?.find((node) => node?.key === key)?.id || ''
|
||||
})
|
||||
typeIds = typeIds.filter((id) => id)
|
||||
return typeIds?.join(',')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,14 +214,26 @@ export default {
|
||||
|
||||
<style lang="less" scoped>
|
||||
.model-export-transfer {
|
||||
/deep/ .ant-transfer-list-body {
|
||||
overflow: auto;
|
||||
}
|
||||
/deep/ .ant-transfer-list {
|
||||
.ant-transfer-list-body {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/deep/ .ant-transfer-list-header-title {
|
||||
color: @primary-color;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
&:first-child {
|
||||
.ant-transfer-list-header {
|
||||
.ant-transfer-list-header-selected {
|
||||
span:first-child {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ant-transfer-list-header-title {
|
||||
color: @primary-color;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -0,0 +1,11 @@
|
||||
export const defaultConfig = {
|
||||
public: {
|
||||
key: '',
|
||||
secret: ''
|
||||
},
|
||||
vcenter: {
|
||||
host: '',
|
||||
account: '',
|
||||
password: ''
|
||||
}
|
||||
}
|
137
cmdb-ui/src/modules/cmdb/views/discovery/accountConfig/index.vue
Normal file
137
cmdb-ui/src/modules/cmdb/views/discovery/accountConfig/index.vue
Normal 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>
|
@@ -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>
|
@@ -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>
|
@@ -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 {
|
||||
|
@@ -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)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@@ -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) {
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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,
|
||||
|
@@ -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>
|
||||
|
@@ -179,7 +179,10 @@
|
||||
:data="instanceList"
|
||||
@checkbox-change="onSelectChange"
|
||||
@checkbox-all="onSelectChange"
|
||||
:checkbox-config="{ reserve: true, trigger: 'cell' }"
|
||||
@checkbox-range-start="checkboxRangeStart"
|
||||
@checkbox-range-change="checkboxRangeChange"
|
||||
@checkbox-range-end="checkboxRangeEnd"
|
||||
:checkbox-config="{ reserve: true, range: true }"
|
||||
@edit-closed="handleEditClose"
|
||||
@edit-actived="handleEditActived"
|
||||
:edit-config="{ trigger: 'dblclick', mode: 'row', showIcon: false }"
|
||||
@@ -528,6 +531,8 @@ export default {
|
||||
isFullSearch: false,
|
||||
fullTreeData: [],
|
||||
filterFullTreeData: [],
|
||||
|
||||
lastSelected: [], // checkbox range 记录
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -1165,6 +1170,21 @@ export default {
|
||||
onSelectChange({ records, reserves }) {
|
||||
this.selectedRowKeys = [...records, ...reserves]
|
||||
},
|
||||
checkboxRangeStart(e) {
|
||||
const xTable = this.$refs.xTable
|
||||
const lastSelected = xTable.getCheckboxRecords()
|
||||
const selectedReserve = xTable.getCheckboxReserveRecords()
|
||||
this.lastSelected = [...lastSelected, ...selectedReserve]
|
||||
},
|
||||
checkboxRangeChange(e) {
|
||||
const xTable = this.$refs.xTable
|
||||
xTable.setCheckboxRow(this.lastSelected, true)
|
||||
},
|
||||
checkboxRangeEnd(e) {
|
||||
const xTable = this.$refs.xTable
|
||||
this.lastSelected = []
|
||||
this.selectedRowKeys = [...xTable.getCheckboxRecords(), ...xTable.getCheckboxReserveRecords()]
|
||||
},
|
||||
batchDeleteCIRelation() {
|
||||
const currentShowType = this.showTypes.find((item) => item.id === Number(this.currentTypeId[0]))
|
||||
const that = this
|
||||
|
@@ -18,6 +18,11 @@
|
||||
>{{ $t('download') }}</a-button
|
||||
>
|
||||
</div>
|
||||
<div v-if="fromCronJob" class="resource-search-tip">
|
||||
<div class="resource-search-tip-item">{{ $t('cmdb.ciType.resourceSearchTip1') }}</div>
|
||||
<div class="resource-search-tip-item">{{ $t('cmdb.ciType.resourceSearchTip2') }}</div>
|
||||
<div class="resource-search-tip-item">{{ $t('cmdb.ciType.resourceSearchTip3') }}</div>
|
||||
</div>
|
||||
<SearchForm
|
||||
ref="search"
|
||||
:type="type"
|
||||
@@ -44,7 +49,7 @@
|
||||
size="small"
|
||||
row-id="_id"
|
||||
:loading="loading"
|
||||
:height="fromCronJob ? windowHeight - 180 : windowHeight - 240"
|
||||
:height="fromCronJob ? windowHeight - 280 : windowHeight - 240"
|
||||
show-header-overflow
|
||||
highlight-hover-row
|
||||
:data="instanceList"
|
||||
@@ -98,16 +103,20 @@
|
||||
>
|
||||
<template v-if="col.value_type === '6' || col.is_link || col.is_password || col.is_choice" #default="{row}">
|
||||
<span v-if="col.value_type === '6' && row[col.field]">{{ JSON.stringify(row[col.field]) }}</span>
|
||||
<a
|
||||
v-else-if="col.is_link && row[col.field]"
|
||||
:href="
|
||||
row[col.field].startsWith('http') || row[col.field].startsWith('https')
|
||||
? `${row[col.field]}`
|
||||
: `http://${row[col.field]}`
|
||||
"
|
||||
target="_blank"
|
||||
>{{ row[col.field] }}</a
|
||||
>
|
||||
<template v-else-if="col.is_link && row[col.field]">
|
||||
<a
|
||||
v-for="(item, linkIndex) in (col.is_list ? row[col.field] : [row[col.field]])"
|
||||
:key="linkIndex"
|
||||
:href="
|
||||
item.startsWith('http') || item.startsWith('https')
|
||||
? `${item}`
|
||||
: `http://${item}`
|
||||
"
|
||||
target="_blank"
|
||||
>
|
||||
{{ item }}
|
||||
</a>
|
||||
</template>
|
||||
<PasswordField
|
||||
v-else-if="col.is_password && row[col.field]"
|
||||
:ci_id="row._id"
|
||||
@@ -568,5 +577,14 @@ export default {
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: @border-radius-box;
|
||||
|
||||
&-tip {
|
||||
margin-bottom: 16px;
|
||||
|
||||
&-item {
|
||||
font-size: 12px;
|
||||
color: @text-color_4
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -1225,7 +1225,7 @@ export default {
|
||||
topoViewJsonData.nodes.keys().forEach((key) => {
|
||||
const node = topoViewJsonData?.nodes?.get(key)
|
||||
if (node?.data?.btnType !== 'more') {
|
||||
node.opacity = node?.text?.indexOf(v) !== -1 ? 1 : 0.1
|
||||
node.opacity = `${node?.text ?? ''}`?.indexOf?.(v) !== -1 ? 1 : 0.1
|
||||
}
|
||||
})
|
||||
const instance = this.$refs.showTopoView.getInstance()
|
||||
|
@@ -242,16 +242,20 @@
|
||||
#default="{row}"
|
||||
>
|
||||
<span v-if="col.value_type === '6' && row[col.field]">{{ row[col.field] }}</span>
|
||||
<a
|
||||
v-else-if="col.is_link && row[col.field]"
|
||||
:href="
|
||||
row[col.field].startsWith('http') || row[col.field].startsWith('https')
|
||||
? `${row[col.field]}`
|
||||
: `http://${row[col.field]}`
|
||||
"
|
||||
target="_blank"
|
||||
>{{ row[col.field] }}</a
|
||||
>
|
||||
<template v-else-if="col.is_link && row[col.field]">
|
||||
<a
|
||||
v-for="(item, linkIndex) in (col.is_list ? row[col.field] : [row[col.field]])"
|
||||
:key="linkIndex"
|
||||
:href="
|
||||
item.startsWith('http') || item.startsWith('https')
|
||||
? `${item}`
|
||||
: `http://${item}`
|
||||
"
|
||||
target="_blank"
|
||||
>
|
||||
{{ item }}
|
||||
</a>
|
||||
</template>
|
||||
<PasswordField
|
||||
v-else-if="col.is_password && row[col.field]"
|
||||
:ci_id="row._id"
|
||||
|
@@ -41,7 +41,7 @@ services:
|
||||
- redis
|
||||
|
||||
cmdb-api:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.4.7
|
||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.4.9
|
||||
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.9
|
||||
flask cmdb-counter > counter.log 2>&1
|
||||
networks:
|
||||
new:
|
||||
@@ -84,7 +84,7 @@ services:
|
||||
test: "ps aux|grep -v grep|grep -v '1 root'|grep gunicorn || exit 1"
|
||||
|
||||
cmdb-ui:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.4.7
|
||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.4.9
|
||||
container_name: cmdb-ui
|
||||
depends_on:
|
||||
cmdb-api:
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 23 KiB |
Reference in New Issue
Block a user