Compare commits

...

22 Commits

Author SHA1 Message Date
pycook
be4fc62218 chore: release v2.5.2 2025-04-17 21:19:46 +08:00
LH_R
ff4ce4dbe0 feat(ui): CI - update JSON type attr tooltip display 2025-04-16 20:54:11 +08:00
LH_R
dda1fce46a feat(ui): CI - relation table by subscribed attr 2025-04-16 20:53:54 +08:00
pycook
fbf59e7b44 perf(api): net device auto discovery 2025-04-16 19:55:02 +08:00
LH_R
4ae67d1f0f feat(ui): CI - hide groups without attr 2025-04-15 19:01:31 +08:00
LH_R
b56cf5bb3d feat(ui): CI - update default style of attr(JSON type) 2025-04-15 16:36:24 +08:00
Leo Song
53e8d34c68 Merge pull request #693 from veops/dev_ui_250411
fix(ui): dcim - device select name display
2025-04-11 15:51:50 +08:00
LH_R
c62e4032e3 fix(ui): dcim - device select name display 2025-04-11 15:51:12 +08:00
Leo Song
108c11071a Merge pull request #692 from veops/dev_ui_250410
dev_ ui_250410
2025-04-10 16:42:21 +08:00
LH_R
debb25f65b feat(ui): CI - update relation layout style 2025-04-10 16:38:42 +08:00
LH_R
f26dd65d07 feat(ui): CIType[relation] - update select filterOption 2025-04-10 16:14:00 +08:00
Leo Song
cb2726c890 Merge pull request #691 from veops/dev_ui_250409
feat(ui): CI Type[AD] - update snmp configuration style
2025-04-09 10:02:01 +08:00
LH_R
2003fd4a48 feat(ui): CI Type[AD] - update snmp configuration style 2025-04-09 10:01:35 +08:00
Leo Song
1bbf8c10b5 Merge pull request #689 from veops/dev_ui_250408
feat(ui): CI Type[AD] - update scanning configuration
2025-04-08 15:53:03 +08:00
LH_R
93e919b73f feat(ui): CI Type[AD] - update scanning configuration 2025-04-08 15:52:30 +08:00
thexqn
435bb2a2c8 fix(api): 使用 ast.literal_eval 代替 eval,取消不正确的计算属性值返回。 (#688)
* fix(api): 使用 ast.literal_eval 代替 eval,取消不正确的计算属性值返回。

* fix(api): 修复属性值计算逻辑,直接返回渲染结果。
2025-04-04 19:19:42 +08:00
pycook
5ceb8ff6f9 feat: update nginx configuration client_max_body_size 2025-03-26 21:02:28 +08:00
pycook
47332aca3c feat(api): Replace imp with importlib 2025-02-23 22:08:38 +08:00
LH_R
f24cb55585 docs: add CODE_OF_CONDUCT 2025-02-13 16:48:07 +08:00
LH_R
f1594550e0 docs: update README 2025-02-13 16:44:57 +08:00
pycook
a025c844bc chore: release v2.5.1 2025-02-10 20:22:17 +08:00
pycook
1a03a0b800 fix(api): get citype 2025-02-10 20:20:22 +08:00
33 changed files with 1684 additions and 772 deletions

13
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,13 @@
# Contributor Code of Conduct
As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
We are committed to making participation in this project a harassment-free experience for everyone, regardless of the level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion.
Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
This Code of Conduct is adapted from the [Contributor Covenant](https://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)

View File

@@ -36,7 +36,7 @@
- **多维度视图展示**:包括资源视图、层级视图、关系视图等,帮助运维人员全面管理资源。
- **细粒度权限控制**:通过精确的访问控制和完备的操作日志保障系统的安全性。
- **全面的资源搜索功能**:支持灵活的资源和关系搜索,快速定位和操作资源。
- **集成 IP 地址管理IPAM和数据中心基础设施管理DCIM功能**:简化网络资源和数据中心设备的管理。
- **集成 IP 地址管理IPAM和数据中心基础设施管理DCIM**:简化网络资源和数据中心设备的管理。
更多详细功能,请移步 [维易科技官网](https://veops.cn) 进行了解。
@@ -82,6 +82,11 @@
</tr>
</table>
## 关注我们
欢迎 Star 加关注,第一时间获取更新动态!
![star us](https://github.com/user-attachments/assets/f9056d5a-171c-4f53-9fec-d40c9e5ff94d)
## 快速开始
@@ -103,7 +108,7 @@
## 接入公司
+ 欢迎使用开源CMDB的公司在 [#112](https://github.com/veops/cmdb/issues/112) 登记
+ 欢迎使用开源CMDB的公司和团队,在 [#112](https://github.com/veops/cmdb/issues/112) 登记
## 代码贡献
我们欢迎所有开发者贡献代码,改善和扩展这个项目。请先阅读我们的[贡献指南](docs/CONTRIBUTING.md)。此外,您还可以通过社交媒体、活动和分享来支持 Veops 的开源。

View File

@@ -12,6 +12,7 @@ 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 DEFAULT_INNER
from api.lib.cmdb.auto_discovery.const import NET_DEVICE_NAMES
from api.lib.cmdb.auto_discovery.const import PRIVILEGED_USERS
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import AutoDiscoveryMappingCache
@@ -252,6 +253,7 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
:return:
"""
result = []
db.session.commit()
rules = cls.cls.get_by(to_dict=True)
for rule in rules:
@@ -718,6 +720,12 @@ class AutoDiscoveryCICRUD(DBMixin):
build_relations_for_ad_accept.apply_async(args=(adc.to_dict(), ci_id, ad_key2attr), queue=CMDB_QUEUE)
ci_type = CITypeCache.get(adc.type_id)
if ci_type and ci_type.name in NET_DEVICE_NAMES and 'ports' in adc.instance:
from api.tasks.cmdb import add_net_device_ports
add_net_device_ports.apply_async(args=(ci_id, adc.instance['ports']),
queue=CMDB_QUEUE)
adc.update(is_accept=True,
accept_by=nickname or current_user.nickname,
accept_time=datetime.datetime.now(),

View File

@@ -4,6 +4,8 @@ from api.lib.cmdb.const import AutoDiscoveryType
PRIVILEGED_USERS = ("cmdb_agent", "worker", "admin")
NET_DEVICE_NAMES = {"switch", 'router', 'firewall', 'printer'}
DEFAULT_INNER = [
dict(name="阿里云", en="aliyun", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-aliyun'}, "en": "aliyun"}),
@@ -41,8 +43,12 @@ DEFAULT_INNER = [
option={'icon': {'name': 'caise-luyouqi'}}),
dict(name="防火墙", type=AutoDiscoveryType.SNMP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-fanghuoqiang'}}),
dict(name="打印机", type=AutoDiscoveryType.SNMP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-dayinji'}}),
# dict(name="打印机", type=AutoDiscoveryType.SNMP, is_inner=True, is_plugin=False,
# option={'icon': {'name': 'caise-dayinji'}}),
dict(name="光纤交换机", type=AutoDiscoveryType.SNMP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-fiber'}}),
dict(name="F5", type=AutoDiscoveryType.SNMP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-F5'}}),
]
CLOUD_MAP = {

View File

@@ -1,37 +1,74 @@
[{
"name":"manufacturer",
"type": "文本",
"example":"HUAWEI Technology Co.,Ltd",
"desc":"制造产商"
},{
"name":"sn",
"type": "文本",
"example":"102030059898",
"desc":"设备序列号"
},{
"name":"device_name",
"type": "文本",
"example":"USG6525E",
"desc":"设备名称"
},{
"name":"device_model",
"type": "文本",
"example":"2011.2.321.1.205",
"desc":"设备细分类型 结合相关产商获取相应的产品类型"
},{
"name":"description",
"type": "文本",
"example":"Huawei Vwersatile Routing Platform Software",
"desc":"设备描述"
},{
"name":"manager_ip",
"type": "文本",
"example":"192.168.1.1",
"desc":"管理ip"
}, {
"name":"ips",
"type": "文本、多值",
"example":"192.168.1.1, 192.168.1.2",
"desc":"ips"
}
[
{
"name": "manufacturer",
"type": "文本",
"example": "Huawei",
"desc": "制造产商"
},
{
"name": "sn",
"type": "文本",
"example": "102030059898",
"desc": "设备序列号"
},
{
"name": "name",
"type": "文本",
"example": "USG6525E",
"desc": "设备名称"
},
{
"name": "model",
"type": "文本",
"example": "2011.2.321.1.205",
"desc": "设备细分类型 结合相关产商获取相应的产品类型"
},
{
"name": "description",
"type": "文本",
"example": "Huawei Vwersatile Routing Platform Software",
"desc": "设备描述"
},
{
"name": "manager_ip",
"type": "文本",
"example": "192.168.1.1",
"desc": "管理ip"
},
{
"name": "ips",
"type": "文本、多值",
"example": "192.168.1.1, 192.168.1.2",
"desc": "ips"
},
{
"name": "uptime",
"type": "文本",
"example": "2023-04-15 10:00:00",
"desc": "启动时间"
},
{
"name": "snmp_version",
"type": "文本",
"example": "v2c",
"desc": "SNMP版本"
},
{
"name": "port_num",
"type": "整数",
"example": 24,
"desc": "端口数量"
},
{
"name": "ports",
"type": "json",
"example": "",
"desc": "设备的端口列表"
},
{
"name": "neighbors",
"type": "json",
"example": "",
"desc": "设备的邻居列表"
}
]

View File

@@ -1399,6 +1399,7 @@ class CITypeTemplateManager(object):
i.pop('order', None)
i.pop('choice_web_hook', None)
i.pop('choice_other', None)
i.pop('choice_builtin', None)
i.pop('order', None)
i.pop('inherited_from', None)
choice_value = i.pop('choice_value', None)

View File

@@ -3,7 +3,7 @@
from __future__ import unicode_literals
import imp
import importlib.util
import copy
import jinja2
@@ -180,14 +180,15 @@ class AttributeValueManager(object):
@staticmethod
def _compute_attr_value_from_expr(expr, ci_dict):
t = jinja2.Template(expr).render(ci_dict)
try:
return eval(t)
result = jinja2.Template(expr).render(ci_dict)
return result
except Exception as e:
current_app.logger.warning(str(e))
return t
current_app.logger.warning(
f"Expression evaluation error - Expression: '{expr}'"
f"Input parameters: {ci_dict}, Error type: {type(e).__name__}, Error message: {str(e)}"
)
return None
@staticmethod
def _compute_attr_value_from_script(script, ci_dict):
script = jinja2.Template(script).render(ci_dict)
@@ -198,11 +199,11 @@ class AttributeValueManager(object):
try:
path = script_f.name
dir_name, name = os.path.dirname(path), os.path.basename(path)[:-3]
name = os.path.basename(path)[:-3]
fp, path, desc = imp.find_module(name, [dir_name])
mod = imp.load_module(name, fp, path, desc)
spec = importlib.util.spec_from_file_location(name, path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
if hasattr(mod, 'computed'):
return mod.computed()

View File

@@ -376,6 +376,29 @@ def build_relations_for_ad_accept(adc, ci_id, ad_key2attr):
pass
@celery.task(name="cmdb.add_net_device_ports", queue=CMDB_QUEUE)
@reconnect_db
def add_net_device_ports(ci_id, ports):
from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.cache import CITypeCache
port_type = CITypeCache.get("net_port")
if not port_type:
current_app.logger.warning("CIType net port is not found")
return
for port in ports:
try:
port_id = CIManager.add(port_type.id, is_auto_discovery=True, _is_admin=True, **port)
CIRelationManager.add(ci_id, port_id,
valid=False,
source=RelationSourceEnum.AUTO_DISCOVERY)
except Exception as e:
current_app.logger.warning("add_net_device_ports failed: {}".format(e))
@celery.task(name="cmdb.dcim_calc_u_free_count", queue=CMDB_QUEUE)
@reconnect_db
def dcim_calc_u_free_count():

View File

@@ -64,9 +64,13 @@ class CITypeView(APIView):
ci_type['unique_name'] = ci_type['unique_id'] and AttributeCache.get(ci_type['unique_id']).name
ci_types.append(ci_type)
elif type_name is not None:
ci_type = CITypeCache.get(type_name).to_dict()
ci_type['parent_ids'] = CITypeInheritanceManager.get_parents(ci_type['id'])
ci_types = [ci_type]
ci_type = CITypeCache.get(type_name)
if ci_type is not None:
ci_type = ci_type.to_dict()
ci_type['parent_ids'] = CITypeInheritanceManager.get_parents(ci_type['id'])
ci_types = [ci_type]
else:
ci_types = []
else:
ci_types = CITypeManager().get_ci_types(q)
count = len(ci_types)

View File

@@ -132,6 +132,7 @@ export default {
/deep/ .ant-select-selection {
height: 28px;
line-height: 28px;
border: none;
.ant-select-selection__rendered {
height: 28px;

View File

@@ -21,7 +21,7 @@
</template>
<span
class="ci-icon-letter"
v-else
v-else-if="title"
>
<span>
{{ title[0].toUpperCase() }}

View File

@@ -1,147 +0,0 @@
<template>
<div class="node-setting-wrap">
<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-model="row.version"
:placeholder="$t('cmdb.ciType.nodeSettingVersionTip')"
allowClear
class="node-setting-select"
>
<a-select-option value="1">
v1
</a-select-option>
<a-select-option value="2c">
v2c
</a-select-option>
</a-select>
</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>
<script>
import _ from 'lodash'
import { v4 as uuidv4 } from 'uuid'
export default {
name: 'MonitorNodeSetting',
props: {
initNodes: {
type: Array,
default: () => [],
},
form: {
type: Object,
default: null,
},
},
data() {
return {
nodes: [],
}
},
methods: {
initNodesFunc() {
this.nodes = _.cloneDeep(this.initNodes)
},
addNode() {
const newNode = {
id: uuidv4(),
ip: '',
community: 'public',
version: '',
}
this.nodes.push(newNode)
},
removeNode(removeId, minLength) {
if (this.nodes.length <= minLength) {
this.$message.error('不可再删除!')
return
}
const _idx = this.nodes.findIndex((item) => item.id === removeId)
if (_idx > -1) {
this.nodes.splice(_idx, 1)
}
},
copyNode(id) {
const copyNode = this.nodes.find((item) => item.id === id)
if (copyNode) {
const newNode = {
...copyNode,
id: uuidv4(),
}
this.nodes.push(newNode)
}
},
getNodeValue() {
const nodes = this.nodes.map((node) => {
return _.pick(node, ['ip', 'community', 'version'])
})
return nodes
},
},
}
</script>
<style lang="less" scoped>
.node-setting-wrap {
margin-left: 17px;
width: 600px;
.ant-row {
/deep/ .ant-input-clear-icon {
color: rgba(0,0,0,.25);
&:hover {
color: rgba(0, 0, 0, 0.45);
}
}
}
.node-setting-select {
width: 150px;
}
}
.action {
height: 36px;
display: flex;
align-items: center;
gap: 12px;
}
</style>

View File

@@ -77,6 +77,7 @@ const cmdb_en = {
confirmDeleteADT: 'Do you confirm to delete [{pluginName}]',
attributeMap: 'Attribute mapping',
nodeConfig: 'Node Configuration',
scanningParameter: 'Scanning Parameter',
autoDiscovery: 'AutoDiscovery',
node: 'Node',
adExecConfig: 'Execute configuration',
@@ -253,6 +254,25 @@ const cmdb_en = {
checkModalColumn4: 'Last checkup time',
testModalTitle: 'Automated discovery testing',
attrMapTableAttrPlaceholder: 'Please edit the name',
SNMPConfiguration: 'SNMP Configuration',
nodeList: 'Node List',
defaultVersion: 'Default Version',
defaultCommunity: 'Default Community',
timeout: 'Timeout',
retryCount: 'Retry Count',
scanningConfiguration: 'Scanning Configuration',
initialNode: 'Initial Node',
defaultGateway: 'Default Gateway',
recursiveOrNot: 'Recursive Or Not',
recursiveTip: 'Scanning Configuration: When disabling recursion, the node list must be configured.',
maximumDepth: 'Maximum Depth',
snmpFormTip1: 'If SNMP is not the default, Community and version need to be configured separately',
snmpFormTip2: 'Timeout for establishing SNMP connection',
snmpFormTip3: 'Number of retries to establish an SNMP connection',
snmpFormTip4: 'The first node to start scanning, or recursively from the default gateway if unconfigured',
snmpFormTip5: 'Enabled by default to discover all network devices and topology relationships as much as possible, and disabled to scan only the devices in the node list',
snmpFormTip6: 'Depth of network device topology',
snmpFormTip7: 'The results of the scan are filtered with CIDR, not filtered if not configured. Format: 192.168.1.0/24',
nodeSettingIp: 'Network device IP address',
nodeSettingIpTip: 'Please enter the ip address',
nodeSettingIpTip1: 'ip address format error',
@@ -701,6 +721,7 @@ if __name__ == "__main__":
batchRollbacking: 'Deleting {total} items in total, {successNum} items successful, {errorNum} items failed',
baselineTips: 'Changes at this point in time will also be rollbacked, Unique ID, password and dynamic attributes do not support',
cover: 'Cover',
detail: 'Detail'
},
serviceTree: {
remove: 'Remove',

View File

@@ -77,6 +77,7 @@ const cmdb_zh = {
confirmDeleteADT: '确认删除 【{pluginName}】',
attributeMap: '字段映射',
nodeConfig: '节点配置',
scanningParameter: '扫描参数',
autoDiscovery: '自动发现属性',
node: '节点',
adExecConfig: '执行配置',
@@ -253,6 +254,25 @@ const cmdb_zh = {
checkModalColumn4: '最近检查时间',
testModalTitle: '自动发现测试',
attrMapTableAttrPlaceholder: '请编辑名称',
SNMPConfiguration: 'SNMP配置',
nodeList: '节点列表',
defaultVersion: '默认版本',
defaultCommunity: '默认 Community',
timeout: '超时时间',
retryCount: '重试次数',
scanningConfiguration: '扫描配置',
initialNode: '初始节点',
defaultGateway: '默认网关',
recursiveOrNot: '是否递归',
recursiveTip: '扫描配置关闭递归时, 必须配置节点列表',
maximumDepth: '最大深度',
snmpFormTip1: '如果不是默认的SNMP, Community和版本需要单独配置',
snmpFormTip2: '建立SNMP连接的超时时间',
snmpFormTip3: '建立SNMP连接的重试次数',
snmpFormTip4: '开始扫描的第一个节点,如果不配置则是从默认网关开始递归扫描',
snmpFormTip5: '默认开启,表示尽可能发现所有网络设备和拓扑关系, 如果关闭,则仅扫描节点列表里的设备',
snmpFormTip6: '网络设备拓扑的深度',
snmpFormTip7: '扫描的结果用CIDR进行过滤不配置则不会过滤。格式: 192.168.1.0/24',
nodeSettingIp: '网络设备IP地址',
nodeSettingIpTip: '请输入 ip 地址',
nodeSettingIpTip1: 'ip地址格式错误',
@@ -700,6 +720,7 @@ if __name__ == "__main__":
batchRollbacking: '正在回滚,共{total}个,成功{successNum}个,失败{errorNum}个',
baselineTips: '该时间点的变更也会被回滚, 唯一标识、密码属性、动态属性不支持回滚',
cover: '覆盖',
detail: '详情'
},
serviceTree: {
remove: '移除',

View File

@@ -17,7 +17,15 @@
:ci_id="ci._id"
:attr_id="attr.id"
></PasswordField>
<template v-else-if="attr.value_type === '6'">{{ JSON.stringify(ci[attr.name] || {}) }}</template>
<a-tooltip
v-else-if="attr.value_type === '6'"
:title="JSON.stringify(ci[attr.name] || {})"
overlayClassName="ci-detail-attr-json-tooltip"
>
<span class="ci-detail-attr-json">
{{ JSON.stringify(ci[attr.name] || {}) }}
</span>
</a-tooltip>
<template v-else-if="attr.is_choice">
<template v-if="attr.is_list">
<span
@@ -348,4 +356,22 @@ export default {
}
</script>
<style></style>
<style lang="less" scoped>
.ci-detail-attr-json {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
</style>
<style lang="less">
.ci-detail-attr-json-tooltip {
.ant-tooltip-content {
max-height: 300px;
overflow-y: auto;
}
}
</style>

View File

@@ -0,0 +1,57 @@
<template>
<div class="ci-detail-table-title">
{{ title }}
</div>
</template>
<script>
export default {
name: 'CIDetailTableTitle',
props: {
title: {
type: String,
default: ''
}
}
}
</script>
<style lang="less" scoped>
.ci-detail-table-title {
height: 42px;
width: 100%;
display: flex;
align-items: center;
font-size: 16px;
font-weight: 700;
color: @text-color_1;
padding: 0px 20px;
position: relative;
overflow: hidden;
background: #EBF0F9;
&::before {
content: "";
height: 44px;
width: 300px;
background: #F8F9FD60;
transform: rotate(40deg);
position: absolute;
top: 0px;
left: 25%;
}
&::after {
content: "";
height: 44px;
width: 300px;
background: #F8F9FD60;
transform: rotate(40deg);
position: absolute;
top: 0px;
left: calc(25% + 100px);
}
}
</style>

View File

@@ -0,0 +1,54 @@
<template>
<div class="ci-detail-title">
<CIIcon :icon="icon" size="20" />
<span class="ci-detail-title-text">{{ title }}</span>
</div>
</template>
<script>
import CIIcon from '@/modules/cmdb/components/ciIcon'
export default {
name: 'CIDetailTitle',
components: {
CIIcon
},
props: {
ci: {
type: Object,
default: () => {}
},
ci_types: {
type: Array,
default: () => []
}
},
computed: {
findCIType() {
return this.ci_types?.find?.((item) => item?.id === this.ci?._type)
},
icon() {
return this?.findCiType?.icon || ''
},
title() {
return this?.ci?.[this.findCIType?.show_name] || this?.ci?.[this.findCIType?.unique_key] || ''
}
}
}
</script>
<style lang="less" scoped>
.ci-detail-title {
display: flex;
align-items: center;
width: 100%;
column-gap: 9px;
&-text {
width: 100%;
font-size: 16px;
font-weight: 700;
color: @text-color_1;
}
}
</style>

View File

@@ -0,0 +1,607 @@
<template>
<div v-if="allCITypes.length" class="ci-relation-table">
<CIDetailTableTitle :title="$t('cmdb.relation')" />
<div class="ci-relation-table-wrap">
<div class="ci-relation-table-tab">
<div
v-for="(item) in tabList"
:key="item.value"
:class="`tab-item ${item.value === currentTab ? 'tab-item-active' : ''}`"
@click="clickTab(item.value)"
>
<span class="tab-item-name">
<a-tooltip :title="item.name">
<span class="tab-item-name-text">{{ item.name }}</span>
</a-tooltip>
<span
v-if="item.count"
class="tab-item-name-count"
>
({{ item.count }})
</span>
</span>
<span
v-if="item.value === currentTab && item.showAdd"
class="tab-item-add"
@click="openAddModal(item)"
>
<a-icon type="plus" />
</span>
</div>
</div>
<div
class="ci-relation-table-container"
v-if="tableIDList.length"
>
<div
v-for="(item) in tableIDList"
:key="item.id"
class="ci-relation-table-item"
>
<div
v-if="currentTab === 'all'"
class="ci-relation-table-item-name"
>
<span class="ci-relation-table-item-name-text">{{ item.name }}</span>
<span class="ci-relation-table-item-name-count">({{ item.count }})</span>
</div>
<vxe-grid
bordered
size="mini"
:columns="allColumns[item.id]"
:data="allCIList[item.id]"
overflow
showOverflow="tooltip"
showHeaderOverflow="tooltip"
resizable
class="ops-stripe-table"
max-height="300px"
>
<template #reference_default="{ row, column }">
<a
v-for="(id) in (column.params.attr.is_list ? row[column.field] : [row[column.field]])"
:key="id"
:href="`/cmdb/cidetail/${column.params.attr.reference_type_id}/${id}`"
target="_blank"
>
{{ getReferenceName(id, column) }}
</a>
</template>
<template #operation_default="{ row }">
<a-popconfirm
arrowPointAtCenter
:title="$t('cmdb.ci.confirmDeleteRelation')"
@confirm="deleteRelation(row)"
>
<a
:disabled="!allCanEdit[item.id]"
:style="{
color: !allCanEdit[item.id] ? 'rgba(0, 0, 0, 0.25)' : 'red',
}"
>
<a-icon type="delete" />
</a>
</a-popconfirm>
</template>
</vxe-grid>
</div>
</div>
</div>
<AddTableModal ref="addTableModal" @reload="refreshTableData" />
</div>
</template>
<script>
import _ from 'lodash'
import { getCanEditByParentIdChildId } from '@/modules/cmdb/api/CITypeRelation'
import { deleteCIRelationView } from '@/modules/cmdb/api/CIRelation'
import { searchCI } from '@/modules/cmdb/api/ci'
import { getSubscribeAttributes } from '@/modules/cmdb/api/preference'
import CIDetailTableTitle from './ciDetailTableTitle.vue'
import AddTableModal from '@/modules/cmdb/views/relation_views/modules/AddTableModal.vue'
export default {
name: 'CIRelationTable',
components: {
CIDetailTableTitle,
AddTableModal
},
inject: {
ci_types: { from: 'ci_types' },
relationViewRefreshNumber: {
from: 'relationViewRefreshNumber',
default: () => null,
},
},
props: {
ciId: {
type: Number,
default: 0,
},
typeId: {
type: Number,
default: 0,
},
ci: {
type: Object,
default: () => {},
},
relationData: {
type: Object,
default: () => {}
}
},
data() {
return {
tabList: [],
currentTab: 'all',
allCITypes: [],
allColumns: {},
allJSONAttr: {},
allCIList: {},
allCanEdit: {},
referenceCINameMap: {}
}
},
computed: {
tableIDList() {
let baseIDs = []
switch (this.currentTab) {
case 'all':
baseIDs = this.tabList.filter((item) => item.value !== 'all').map((item) => item.value)
break
default:
baseIDs = [this.currentTab]
break
}
return baseIDs.filter((id) => this.allCIList?.[id]?.length).map((id) => {
const findTab = this.tabList.find((item) => item.value === id) || {}
return {
id,
name: findTab?.name || '',
count: findTab?.count || ''
}
})
}
},
watch: {
relationData: {
immediate: true,
deep: true,
handler(val) {
this.init(val)
}
}
},
methods: {
async init(relationData) {
const ci_types_list = this.ci_types()
const _findCiType = ci_types_list.find((item) => item.id === this.typeId)
if (!_findCiType) {
return
}
const cloneRelationData = _.cloneDeep(relationData)
const allCITypes = [
...cloneRelationData.parentCITypeList,
...cloneRelationData.childCITypeList
]
await this.handleSubscribeAttributes(allCITypes)
const {
columns: parentColumns,
jsonAttr: parentJSONAttr,
} = this.handleCITypeList(cloneRelationData.parentCITypeList, true)
const {
columns: childColumns,
jsonAttr: childJSONAttr,
} = this.handleCITypeList(cloneRelationData.childCITypeList, false)
this.allCITypes = allCITypes
this.allColumns = {
...parentColumns,
...childColumns
}
this.allJSONAttr = {
...parentJSONAttr,
...childJSONAttr
}
await this.getCanEditList(this.allCITypes)
const [parentCIs, childCIs] = await Promise.all([
this.handleCIList(cloneRelationData.parentCIList, true),
this.handleCIList(cloneRelationData.childCIList, false)
])
this.allCIList = {
...parentCIs,
...childCIs
}
const tabList = this.allCITypes.map((item) => {
return {
name: item?.alias ?? item?.name ?? '',
value: item.id,
count: this.allCIList?.[item.id]?.length || 0,
showAdd: this.allCanEdit?.[item.id] ?? false
}
})
tabList.unshift({
name: this.$t('all'),
value: 'all',
count: Object.values(this.allCIList).reduce((acc, cur) => acc + (cur?.length || 0), 0),
showAdd: false
})
this.tabList = tabList
this.handleReferenceCINameMap()
},
handleCITypeList(list, isParent) {
const CIColumns = {}
const CIJSONAttr = {}
list.forEach((item) => {
const columns = []
const jsonAttr = []
item.isParent = isParent
item.attributes.forEach((attr) => {
const column = {
key: 'p_' + attr.id,
field: attr.name,
title: attr.alias,
minWidth: '100px',
params: {
attr
},
}
if (attr.is_reference) {
column.slots = {
default: 'reference_default'
}
}
columns.push(column)
if (attr.value_type === '6') {
jsonAttr.push(attr.name)
}
})
CIJSONAttr[item.id] = jsonAttr
CIColumns[item.id] = columns
CIColumns[item.id].push({
key: 'p_operation',
field: 'operation',
title: this.$t('operation'),
width: '60px',
fixed: 'right',
slots: {
default: 'operation_default',
},
align: 'center',
})
})
return {
columns: CIColumns,
jsonAttr: CIJSONAttr
}
},
async getCanEditList(allCITypes) {
const promises = allCITypes.map((ciType) => {
let parentId = ciType.id
let childId = this.typeId
if (!ciType.isParent) {
parentId = this.typeId
childId = ciType.id
}
return getCanEditByParentIdChildId(parentId, childId).then((res) => {
return { id: ciType.id, canEdit: res.result }
})
})
const allCanEdit = {}
const res = await Promise.all(promises)
if (res?.length) {
res.map((item) => {
allCanEdit[item.id] = item.canEdit
})
}
this.allCanEdit = allCanEdit
},
async handleSubscribeAttributes(allCITypes) {
const promises = allCITypes.map((ciType, index) => {
return getSubscribeAttributes(ciType.id).then((res) => {
return {
...(res || {}),
id: ciType.id,
indexInAll: index
}
})
})
const res = await Promise.all(promises)
if (res?.length) {
res.forEach((item) => {
if (
allCITypes?.[item.indexInAll]?.attributes &&
item?.is_subscribed
) {
allCITypes[item.indexInAll].attributes = item.attributes
}
})
}
return allCITypes
},
async handleCIList(ciList, isParent) {
const cis = {}
ciList.forEach((item) => {
this.allJSONAttr[item._type].forEach((attr) => {
item[`${attr}`] = item[`${attr}`] ? JSON.stringify(item[`${attr}`]) : ''
})
this.formatCI(item)
item.isParent = isParent
if (item._type in cis) {
cis[item._type].push(item)
} else {
cis[item._type] = [item]
}
})
return cis
},
formatCI(ci) {
Object.keys(ci).forEach((key) => {
const attr = this.allColumns?.[ci?._type]?.find((item) => item?.params?.attr?.name === key)?.params?.attr
if (attr?.is_choice && attr?.choice_value?.length) {
if (attr?.is_list) {
ci[key] = ci[key].map((value) => {
const label = attr?.choice_value?.find((choice) => choice?.[0] === value)?.[1]?.label
return label || ci[key]
})
} else {
const label = attr?.choice_value?.find((choice) => choice?.[0] === ci[key])?.[1]?.label
ci[key] = label || ci[key]
}
}
})
return ci
},
async handleReferenceCINameMap() {
const referenceCINameMap = {}
this.allCITypes.forEach((CIType) => {
CIType.attributes.forEach((attr) => {
if (attr?.is_reference && attr?.reference_type_id) {
const currentCIList = this.allCIList[CIType.id]
if (currentCIList?.length) {
currentCIList.forEach((ci) => {
const ids = Array.isArray(ci[attr.name]) ? ci[attr.name] : ci[attr.name] ? [ci[attr.name]] : []
if (ids.length) {
if (!referenceCINameMap?.[attr.reference_type_id]) {
referenceCINameMap[attr.reference_type_id] = {}
}
ids.forEach((id) => {
referenceCINameMap[attr.reference_type_id][id] = ''
})
}
})
}
}
})
})
if (!Object.keys(referenceCINameMap).length) {
return
}
const allRes = await Promise.all(
Object.keys(referenceCINameMap).map((key) => {
return searchCI({
q: `_type:${key},_id:(${Object.keys(referenceCINameMap[key]).join(';')})`,
count: 9999
})
})
)
const CITypeList = this.ci_types()
const showNameMap = {}
Object.keys(referenceCINameMap).forEach((id) => {
const CIType = CITypeList.find((CIType) => Number(CIType.id) === Number(id))
showNameMap[id] = {
show_name: CIType?.show_name,
unique_key: CIType?.unique_key
}
})
allRes.forEach((res) => {
res.result.forEach((item) => {
if (referenceCINameMap?.[item._type]?.[item._id] === '') {
const showName = showNameMap?.[item._type]
referenceCINameMap[item._type][item._id] = item?.[showName?.show_name] ?? item?.[showName?.unique_key] ?? ''
}
})
})
this.referenceCINameMap = referenceCINameMap
},
getReferenceName(id, column) {
const typeId = column?.params?.attr?.reference_type_id
return this.referenceCINameMap?.[typeId]?.[id] || id
},
clickTab(value) {
this.currentTab = value
},
deleteRelation(row) {
const first_ci_id = row?.isParent ? row?._id : this.ciId
const second_ci_id = row?.isParent ? this.ciId : row?._id
deleteCIRelationView(first_ci_id, second_ci_id).then(() => {
this.refreshTableData()
if (this.relationViewRefreshNumber) {
this.relationViewRefreshNumber()
}
})
},
openAddModal(tabData) {
const ciType = this.allCITypes.find((item) => item.id === tabData.value)
this.$refs.addTableModal.openModal(
{
[`${this.ci.unique}`]: this.ci?.[this.ci.unique]
},
this.ciId,
ciType,
ciType?.isParent ? 'parents' : 'children'
)
},
async refreshTableData() {
this.$emit('refreshRelationCI')
}
}
}
</script>
<style lang="less" scoped>
.ci-relation-table {
width: 100%;
margin-top: 32px;
&-wrap {
border: solid 1px #E4E7ED;
border-top: none;
display: flex;
width: 100%;
}
&-tab {
flex-shrink: 0;
width: 160px;
max-height: 300px;
overflow-y: auto;
overflow-x: hidden;
padding: 6px 0px;
border-right: solid 1px #E4E7ED;
.tab-item {
height: 32px;
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding-left: 16px;
padding-right: 10px;
background-color: #FFFFFF;
cursor: pointer;
&-name {
font-size: 14px;
color: @text-color_1;
display: flex;
align-items: baseline;
max-width: calc(100% - 16px);
&-text {
text-overflow: ellipsis;
text-wrap: nowrap;
overflow: hidden;
color: @text-color_2;
}
&-count {
color: @text-color_3;
font-size: 12px;
}
}
&-add {
width: 14px;
height: 14px;
border-radius: 14px;
background-color: #FFFFFF;
display: none;
align-items: center;
justify-content: center;
color: @primary-color;
font-size: 12px;
}
&-active {
background-color: #F0F5FF;
.tab-item-name-text {
color: @text-color_1;
}
}
&:hover {
.tab-item-name-text {
color: @text-color_1;
}
.tab-item-add {
display: flex;
}
}
}
}
&-container {
width: 100%;
padding: 15px 17px;
overflow: hidden;
min-height: 300px;
}
&-item {
margin-bottom: 16px;
&-name {
margin-bottom: 12px;
font-size: 14px;
font-weight: 700;
color: @text-color_1;
display: flex;
align-items: baseline;
&-count {
font-size: 12px;
color: @text-color_3;
}
}
}
}
</style>

View File

@@ -0,0 +1,95 @@
import { getCITypeChildren, getCITypeParent } from '@/modules/cmdb/api/CITypeRelation'
import { searchCIRelation } from '@/modules/cmdb/api/CIRelation'
const RelationMixin = {
data() {
return {
relationData: {
parentCITypeList: [],
childCITypeList: [],
parentCIList: [],
childCIList: []
}
}
},
methods: {
async initRelationData(typeId, ciId) {
const {
parentCITypeList,
childCITypeList
} = await this.getRelationCITypeList(typeId)
const {
parentCIList,
childCIList
} = await this.getRelationCIList(ciId)
this.relationData = {
parentCITypeList,
childCITypeList,
parentCIList,
childCIList
}
},
async getRelationCITypeList(typeId) {
let parentCITypeList = []
let childCITypeList = []
if (typeId) {
parentCITypeList = await this.getParentCITypeList(typeId)
childCITypeList = await this.getChildCITypeList(typeId)
}
return {
parentCITypeList,
childCITypeList
}
},
async getRelationCIList(ciId) {
let parentCIList = []
let childCIList = []
if (ciId) {
parentCIList = await this.getParentCIList(ciId)
childCIList = await this.getChildCIList(ciId)
}
return {
parentCIList,
childCIList
}
},
async refreshRelationCI(ciId) {
const {
parentCIList,
childCIList
} = await this.getRelationCIList(ciId)
this.relationData.parentCIList = parentCIList
this.relationData.childCIList = childCIList
},
async getParentCITypeList(typeId) {
const res = await getCITypeParent(typeId)
return res?.parents || []
},
async getChildCITypeList(typeId) {
const res = await getCITypeChildren(typeId)
return res.children || []
},
async getParentCIList(ciId) {
const res = await searchCIRelation(`root_id=${ciId}&level=1&reverse=1&count=10000`)
return res?.result || []
},
async getChildCIList(ciId) {
const res = await searchCIRelation(`root_id=${ciId}&level=1&reverse=0&count=10000`)
return res?.result || []
}
}
}
export default RelationMixin

View File

@@ -1,146 +1,16 @@
<template>
<div class="ci-detail-relation">
<a-radio-group v-model="activeKey" size="small" @change="handleChangeActiveKey">
<a-radio-button value="1">
{{ $t('cmdb.ci.topo') }}
</a-radio-button>
<a-radio-button value="2">
{{ $t('cmdb.ci.table') }}
</a-radio-button>
</a-radio-group>
<CiDetailRelationTopo ref="ciDetailRelationTopo" v-if="activeKey === '1'" />
<template v-if="activeKey === '2'">
<template v-for="parent in parentCITypes">
<div :key="'ctr_' + parent.ctr_id">
<div class="ci-detail-relation-table-title">
{{ parent.alias || parent.name }}
<a
:disabled="!canEdit[parent.id]"
@click="
() => {
$refs.addTableModal.openModal({ [`${ci.unique}`]: ci[ci.unique] }, ci._id, parent, 'parents')
}
"
><a-icon
type="plus-square"
/></a>
<span v-if="!canEdit[parent.id]">{{ $t('cmdb.ci.m2mTips') }}</span>
</div>
<vxe-grid
v-if="firstCIs[parent.name]"
bordered
size="mini"
:columns="firstCIColumns[parent.id]"
:data="firstCIs[parent.name]"
overflow
showOverflow="tooltip"
showHeaderOverflow="tooltip"
resizable
class="ops-stripe-table"
>
<template #reference_default="{ row, column }">
<a
v-for="(id) in (column.params.attr.is_list ? row[column.field] : [row[column.field]])"
:key="id"
:href="`/cmdb/cidetail/${column.params.attr.reference_type_id}/${id}`"
target="_blank"
>
{{ getReferenceName(id, column) }}
</a>
</template>
<template #operation_default="{ row }">
<a-popconfirm
arrowPointAtCenter
:title="$t('cmdb.ci.confirmDeleteRelation')"
@confirm="deleteRelation(row._id, ciId)"
>
<a
:disabled="!canEdit[parent.id]"
:style="{
color: !canEdit[parent.id] ? 'rgba(0, 0, 0, 0.25)' : 'red',
}"
><a-icon
type="delete"
/></a>
</a-popconfirm>
</template>
</vxe-grid>
</div>
</template>
<a-divider />
<template v-for="child in childCITypes">
<div :key="'ctr_' + child.ctr_id">
<div class="ci-detail-relation-table-title">
{{ child.alias || child.name }}
<a
:disabled="!canEdit[child.id]"
@click="
() => {
$refs.addTableModal.openModal({ [`${ci.unique}`]: ci[ci.unique] }, ci._id, child, 'children')
}
"
><a-icon
type="plus-square"
/></a>
<span v-if="!canEdit[child.id]">{{ $t('cmdb.ci.m2mTips') }}</span>
</div>
<vxe-grid
v-if="secondCIs[child.name]"
bordered
size="mini"
:columns="secondCIColumns[child.id]"
:data="secondCIs[child.name]"
showOverflow="tooltip"
showHeaderOverflow="tooltip"
resizable
class="ops-stripe-table"
>
<template #reference_default="{ row, column }">
<a
v-for="(id) in (column.params.attr.is_list ? row[column.field] : [row[column.field]])"
:key="id"
:href="`/cmdb/cidetail/${column.params.attr.reference_type_id}/${id}`"
target="_blank"
>
{{ getReferenceName(id, column) }}
</a>
</template>
<template #operation_default="{ row }">
<a-popconfirm
arrowPointAtCenter
:title="$t('cmdb.ci.confirmDeleteRelation')"
@confirm="deleteRelation(ciId, row._id)"
>
<a
:disabled="!canEdit[child.id]"
:style="{
color: !canEdit[child.id] ? 'rgba(0, 0, 0, 0.25)' : 'red',
}"
><a-icon
type="delete"
/></a>
</a-popconfirm>
</template>
</vxe-grid>
</div>
</template>
</template>
<AddTableModal ref="addTableModal" @reload="reload" />
<CiDetailRelationTopo ref="ciDetailRelationTopo"/>
</div>
</template>
<script>
import _ from 'lodash'
import { getCITypeChildren, getCITypeParent, getCanEditByParentIdChildId } from '@/modules/cmdb/api/CITypeRelation'
import { searchCIRelation, deleteCIRelationView } from '@/modules/cmdb/api/CIRelation'
import { searchCI } from '@/modules/cmdb/api/ci'
import CiDetailRelationTopo from './ciDetailRelationTopo/index.vue'
import Node from './ciDetailRelationTopo/node.js'
import AddTableModal from '../../relation_views/modules/AddTableModal.vue'
export default {
name: 'CiDetailRelation',
components: { CiDetailRelationTopo, AddTableModal },
name: 'CIDetailRelation',
components: { CiDetailRelationTopo },
props: {
ciId: {
type: Number,
@@ -154,41 +24,32 @@ export default {
type: Object,
default: () => {},
},
initQueryLoading: {
type: Boolean,
default: false,
relationData: {
type: Object,
default: () => {}
}
},
data() {
return {
activeKey: '1',
parentCITypes: [],
childCITypes: [],
firstCIs: {},
firstCIColumns: {},
secondCIs: {},
secondCIColumns: {},
firstCIJsonAttr: {},
secondCIJsonAttr: {},
canEdit: {},
topoData: {
nodes: {},
edges: []
},
referenceCINameMap: {}
}
},
computed: {
exsited_ci() {
const _exsited_ci = [this.ciId]
this.parentCITypes.forEach((parent) => {
this.relationData.parentCITypeList.forEach((parent) => {
if (this.firstCIs[parent.name]) {
this.firstCIs[parent.name].forEach((parentCi) => {
_exsited_ci.push(parentCi._id)
})
}
})
this.childCITypes.forEach((child) => {
this.relationData.childCITypeList.forEach((child) => {
if (this.secondCIs[child.name]) {
this.secondCIs[child.name].forEach((childCi) => {
_exsited_ci.push(childCi._id)
@@ -207,314 +68,59 @@ export default {
default: () => null,
},
},
mounted() {
if (!this.initQueryLoading) {
this.init(true)
watch: {
relationData: {
immediate: true,
deep: true,
handler(val) {
this.init(val)
}
}
},
methods: {
async init(isFirst) {
async init(relationData) {
const ci_types_list = this.ci_types()
const _findCiType = ci_types_list.find((item) => item.id === this.typeId)
if (!_findCiType) {
return
}
await Promise.all([this.getParentCITypes(), this.getChildCITypes()])
Promise.all([this.getFirstCIs(), this.getSecondCIs()]).then(() => {
this.handleTopoData()
if (
isFirst &&
this.$refs.ciDetailRelationTopo &&
ci_types_list.length
) {
this.getFirstCIs(relationData.parentCIList)
this.getSecondCIs(relationData.childCIList)
this.handleTopoData()
this.$nextTick(() => {
if (this.$refs.ciDetailRelationTopo) {
this.$refs.ciDetailRelationTopo.exsited_ci = this.exsited_ci
this.$refs.ciDetailRelationTopo.setTopoData(this.topoData)
}
this.handleReferenceCINameMap()
})
},
async getFirstCIs() {
await searchCIRelation(`root_id=${Number(this.ciId)}&level=1&reverse=1&count=10000`)
.then((res) => {
const firstCIs = {}
res.result.forEach((item) => {
this.firstCIJsonAttr[item._type].forEach((attr) => {
item[`${attr}`] = item[`${attr}`] ? JSON.stringify(item[`${attr}`]) : ''
})
this.formatCI(item, this.firstCIColumns)
if (item.ci_type in firstCIs) {
firstCIs[item.ci_type].push(item)
} else {
firstCIs[item.ci_type] = [item]
}
})
this.firstCIs = firstCIs
})
.catch((e) => {})
},
async getSecondCIs() {
await searchCIRelation(`root_id=${Number(this.ciId)}&level=1&reverse=0&count=10000`)
.then((res) => {
const secondCIs = {}
res.result.forEach((item) => {
this.secondCIJsonAttr[item._type].forEach((attr) => {
item[`${attr}`] = item[`${attr}`] ? JSON.stringify(item[`${attr}`]) : ''
})
this.formatCI(item, this.secondCIColumns)
if (item.ci_type in secondCIs) {
secondCIs[item.ci_type].push(item)
} else {
secondCIs[item.ci_type] = [item]
}
})
this.secondCIs = secondCIs
})
.catch((e) => {})
},
formatCI(ci, columns) {
Object.keys(ci).forEach((key) => {
const attr = columns?.[ci?._type]?.find((item) => item?.params?.attr?.name === key)?.params?.attr
if (attr?.is_choice && attr?.choice_value?.length) {
if (attr?.is_list) {
ci[key] = ci[key].map((value) => {
const label = attr?.choice_value?.find((choice) => choice?.[0] === value)?.[1]?.label
return label || ci[key]
})
} else {
const label = attr?.choice_value?.find((choice) => choice?.[0] === ci[key])?.[1]?.label
ci[key] = label || ci[key]
}
async getFirstCIs(parentCIList) {
const firstCIs = {}
parentCIList.forEach((item) => {
if (item.ci_type in firstCIs) {
firstCIs[item.ci_type].push(item)
} else {
firstCIs[item.ci_type] = [item]
}
})
return ci
this.firstCIs = firstCIs
},
async getParentCITypes() {
const res = await getCITypeParent(this.typeId)
this.parentCITypes = res.parents
for (let i = 0; i < res.parents.length; i++) {
await getCanEditByParentIdChildId(res.parents[i].id, this.typeId).then((p_res) => {
this.canEdit = {
..._.cloneDeep(this.canEdit),
[res.parents[i].id]: p_res.result,
}
})
}
const firstCIColumns = {}
const firstCIJsonAttr = {}
res.parents.forEach((item) => {
const columns = []
const jsonAttr = []
item.attributes.forEach((attr) => {
const column = {
key: 'p_' + attr.id,
field: attr.name,
title: attr.alias,
minWidth: '100px',
params: {
attr
},
}
if (attr.is_reference) {
column.slots = {
default: 'reference_default'
}
}
columns.push(column)
if (attr.value_type === '6') {
jsonAttr.push(attr.name)
}
})
firstCIJsonAttr[item.id] = jsonAttr
firstCIColumns[item.id] = columns
firstCIColumns[item.id].push({
key: 'p_operation',
field: 'operation',
title: this.$t('operation'),
width: '60px',
fixed: 'right',
slots: {
default: 'operation_default',
},
align: 'center',
})
})
this.firstCIColumns = firstCIColumns
this.firstCIJsonAttr = firstCIJsonAttr
},
async getChildCITypes() {
const res = await getCITypeChildren(this.typeId)
this.childCITypes = res.children
for (let i = 0; i < res.children.length; i++) {
await getCanEditByParentIdChildId(this.typeId, res.children[i].id).then((c_res) => {
this.canEdit = {
..._.cloneDeep(this.canEdit),
[res.children[i].id]: c_res.result,
}
})
}
const secondCIColumns = {}
const secondCIJsonAttr = {}
res.children.forEach((item) => {
const columns = []
const jsonAttr = []
item.attributes.forEach((attr) => {
const column = {
key: 'c_' + attr.id,
field: attr.name,
title: attr.alias,
minWidth: '100px',
params: {
attr
},
}
if (attr.is_reference) {
column.slots = {
default: 'reference_default'
}
}
columns.push(column)
if (attr.value_type === '6') {
jsonAttr.push(attr.name)
}
})
secondCIJsonAttr[item.id] = jsonAttr
secondCIColumns[item.id] = columns
secondCIColumns[item.id].push({
key: 'c_operation',
field: 'operation',
title: this.$t('operation'),
width: '60px',
fixed: 'right',
slots: {
default: 'operation_default',
},
align: 'center',
})
})
this.secondCIColumns = secondCIColumns
this.secondCIJsonAttr = secondCIJsonAttr
},
async handleReferenceCINameMap() {
const CITypes = _.unionBy(
[
...this.parentCITypes,
...this.childCITypes
],
'id'
)
const CIList = _.unionBy(
_.flatten(
[
...Object.values(this.firstCIs),
...Object.values(this.secondCIs)
]
),
'_id'
)
const CIMap = {}
CIList.forEach((ci) => {
if (!CIMap[ci._type]) {
CIMap[ci._type] = []
}
CIMap[ci._type].push(ci)
})
const referenceCINameMap = {}
CITypes.forEach((CIType) => {
CIType.attributes.forEach((attr) => {
if (attr?.is_reference && attr?.reference_type_id) {
const currentCIList = CIMap[CIType.id]
if (currentCIList?.length) {
currentCIList.forEach((ci) => {
const ids = Array.isArray(ci[attr.name]) ? ci[attr.name] : ci[attr.name] ? [ci[attr.name]] : []
if (ids.length) {
if (!referenceCINameMap?.[attr.reference_type_id]) {
referenceCINameMap[attr.reference_type_id] = {}
}
ids.forEach((id) => {
referenceCINameMap[attr.reference_type_id][id] = ''
})
}
})
}
}
})
})
if (!Object.keys(referenceCINameMap).length) {
return
}
const allRes = await Promise.all(
Object.keys(referenceCINameMap).map((key) => {
return searchCI({
q: `_type:${key},_id:(${Object.keys(referenceCINameMap[key]).join(';')})`,
count: 9999
})
})
)
const CITypeList = this.ci_types()
const showNameMap = {}
Object.keys(referenceCINameMap).forEach((id) => {
const CIType = CITypeList.find((CIType) => Number(CIType.id) === Number(id))
showNameMap[id] = {
show_name: CIType?.show_name,
unique_key: CIType?.unique_key
async getSecondCIs(childCIList) {
const secondCIs = {}
childCIList.forEach((item) => {
if (item.ci_type in secondCIs) {
secondCIs[item.ci_type].push(item)
} else {
secondCIs[item.ci_type] = [item]
}
})
allRes.forEach((res) => {
res.result.forEach((item) => {
if (referenceCINameMap?.[item._type]?.[item._id] === '') {
const showName = showNameMap?.[item._type]
referenceCINameMap[item._type][item._id] = item?.[showName?.show_name] ?? item?.[showName?.unique_key] ?? ''
}
})
})
this.referenceCINameMap = referenceCINameMap
this.secondCIs = secondCIs
},
getReferenceName(id, column) {
const typeId = column?.params?.attr?.reference_type_id
return this.referenceCINameMap?.[typeId]?.[id] || id
},
reload() {
this.init()
},
deleteRelation(first_ci_id, second_ci_id) {
deleteCIRelationView(first_ci_id, second_ci_id).then((res) => {
this.init()
if (this.relationViewRefreshNumber) {
this.relationViewRefreshNumber()
}
})
},
handleChangeActiveKey(e) {
if (e.target.value === '1') {
this.$nextTick(() => {
this.$refs.ciDetailRelationTopo.exsited_ci = this.exsited_ci
this.$refs.ciDetailRelationTopo.setTopoData(this.topoData)
})
}
},
handleTopoData() {
const ci_types_list = this.ci_types()
if (!ci_types_list?.length) {
@@ -555,7 +161,7 @@ export default {
children: [],
}
const edges = []
this.parentCITypes.forEach((parent) => {
this.relationData.parentCITypeList.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
@@ -598,7 +204,7 @@ export default {
})
}
})
this.childCITypes.forEach((child) => {
this.relationData.childCITypeList.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
@@ -653,12 +259,5 @@ export default {
<style lang="less" scoped>
.ci-detail-relation {
height: 100%;
.ci-detail-relation-table-title {
font-size: 16px;
font-weight: 700;
margin-top: 20px;
margin-bottom: 5px;
color: #303133;
}
}
</style>

View File

@@ -2,7 +2,7 @@
<div
id="ci-detail-relation-topo"
class="ci-detail-relation-topo"
:style="{ width: '100%', marginTop: '20px', height: 'calc(100% - 44px)' }"
:style="{ width: '100%', height: '100%' }"
></div>
</template>
@@ -25,7 +25,6 @@ export default {
}
},
inject: ['ci_types'],
mounted() {},
methods: {
init() {
const root = document.getElementById('ci-detail-relation-topo')

View File

@@ -6,29 +6,79 @@
{{ $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" @refreshReferenceAttr="handleReferenceAttr" />
</el-descriptions-item>
</el-descriptions>
<span slot="tab"><a-icon type="book" />{{ $t('cmdb.ci.detail') }}</span>
<div class="ci-detail-table">
<CIDetailTitle :ci="ci" :ci_types="ci_types" />
<div class="ci-detail-table-attr">
<CIDetailTableTitle :title="$t('cmdb.attribute')" />
<div class="ci-detail-table-attr-wrap">
<div
v-for="group in attributeGroups"
:key="group.name"
class="ci-detail-table-attr-group"
>
<div class="ci-detail-table-attr-group-name">
{{ group.name || $t('other') }}
</div>
<a-row :gutter="[18, 14]">
<a-col
v-for="attr in group.attributes"
:key="attr.name"
:span="8"
>
<a-row :gutter="[8, 0]">
<a-col :span="8">
<span class="ci-detail-table-attr-label">
<a-tooltip :title="attr.alias || attr.name">
<span class="ci-detail-table-attr-label-text">{{ attr.alias || attr.name }}</span>
</a-tooltip>
<span class="ci-detail-table-attr-label-colon">:</span>
</span>
</a-col>
<a-col
:span="16"
class="ci-detail-table-attr-content"
>
<CIDetailAttrContent
:ci="ci"
:attr="attr"
:attributeGroups="attributeGroups"
@updateChoiceValue="updateChoiceValue"
@refresh="refresh"
@updateCIByself="updateCIByself"
@refreshReferenceAttr="handleReferenceAttr"
/>
</a-col>
</a-row>
</a-col>
</a-row>
</div>
</div>
</div>
<CIRelationTable
:ciId="ciId"
:typeId="typeId"
:ci="ci"
:relationData="relationData"
@refreshRelationCI="refreshRelationCI(ciId)"
/>
</div>
</a-tab-pane>
<a-tab-pane key="tab_2">
<span slot="tab"><a-icon type="branches" />{{ $t('cmdb.relation') }}</span>
<span slot="tab"><a-icon type="branches" />{{ $t('cmdb.ci.topo') }}</span>
<div :style="{ height: '100%', padding: '24px', overflow: 'auto' }">
<ci-detail-relation ref="ciDetailRelation" :ciId="ciId" :typeId="typeId" :ci="ci" :initQueryLoading="initQueryLoading" />
<CIDetailRelation
:ciId="ciId"
:typeId="typeId"
:ci="ci"
:relationData="relationData"
/>
</div>
</a-tab-pane>
<a-tab-pane key="tab_3">
@@ -42,7 +92,7 @@
<ops-icon type="veops-export" />{{ $t('export') }}
</a-button>
</a-space>
<ci-rollback-form ref="ciRollbackForm" :ciIds="[ciId]" @getCIHistory="getCIHistory" />
<CIRollbackForm ref="ciRollbackForm" :ciIds="[ciId]" @getCIHistory="getCIHistory" />
<vxe-table
ref="xTable"
show-overflow
@@ -134,25 +184,33 @@
<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, searchCI } from '@/modules/cmdb/api/ci'
import CiDetailAttrContent from './ciDetailAttrContent.vue'
import CiDetailRelation from './ciDetailRelation.vue'
import RelationMixin from './ciDetailMixin/relationMixin.js'
import CIDetailTitle from './ciDetailComponent/ciDetailTitle.vue'
import CIDetailTableTitle from './ciDetailComponent/ciDetailTableTitle.vue'
import CIDetailAttrContent from './ciDetailAttrContent.vue'
import CIRelationTable from './ciDetailComponent/ciRelationTable.vue'
import CIDetailRelation from './ciDetailRelation.vue'
import TriggerTable from '../../operation_history/modules/triggerTable.vue'
import RelatedItsmTable from './ciDetailRelatedItsmTable.vue'
import CiRollbackForm from './ciRollbackForm.vue'
import CIRollbackForm from './ciRollbackForm.vue'
export default {
name: 'CiDetailTab',
mixins: [RelationMixin],
components: {
ElDescriptions: Descriptions,
ElDescriptionsItem: DescriptionsItem,
CiDetailAttrContent,
CiDetailRelation,
CIDetailAttrContent,
CIDetailRelation,
TriggerTable,
RelatedItsmTable,
CiRollbackForm,
CIRollbackForm,
CIDetailTitle,
CIDetailTableTitle,
CIRelationTable
},
props: {
typeId: {
@@ -218,15 +276,11 @@ export default {
},
},
methods: {
async create(ciId, activeTabKey = 'tab_1', ciDetailRelationKey = '1') {
async create(ciId, activeTabKey = 'tab_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) {
@@ -234,16 +288,15 @@ export default {
this.getCIHistory()
const ciTypeRes = await getCITypes()
this.ci_types = ciTypeRes.ci_types
if (this.activeTabKey === 'tab_2') {
this.$refs.ciDetailRelation.init(true)
}
this.initRelationData(this.typeId, this.ciId)
}
this.initQueryLoading = false
},
getAttributes() {
getCITypeGroupById(this.typeId, { need_other: 1 })
.then((res) => {
this.attributeGroups = res
this.attributeGroups = (res || []).filter((group) => group?.attributes?.length)
this.handleReferenceAttr()
})
@@ -509,23 +562,68 @@ export default {
.ant-tabs-extra-content {
line-height: 44px;
}
.ci-detail-attr {
.ci-detail-table {
height: 100%;
overflow: auto;
overflow-x: hidden;
overflow-y: auto;
padding: 24px;
.el-descriptions-item__content {
cursor: default;
&:hover a {
opacity: 1 !important;
&-attr {
width: 100%;
margin-top: 14px;
&-wrap {
padding: 13px;
width: 100%;
border: solid 1px #E4E7ED;
border-top: none;
}
&-group {
&:not(:last-child) {
margin-bottom: 16px;
}
&-name {
font-size: 14px;
font-weight: 700;
color: @text-color_1;
margin-bottom: 7.5px;
width: 100%;
text-align: left;
display: flex;
justify-content: flex-start;
}
}
&-label {
font-size: 14px;
font-weight: 400;
color: @text-color_3;
display: inline-flex;
max-width: 100%;
&-text {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
text-wrap: nowrap;
}
&-colon {
flex-shrink: 0;
}
}
&-content {
overflow-wrap: break-word;
&: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;
}

View File

@@ -0,0 +1,85 @@
<template>
<a-form-model
:model="formData"
:labelCol="labelCol"
:wrapperCol="{ span: 6 }"
class="attr-ad-form"
>
<a-form-model-item
:label="$t('cmdb.ciType.defaultVersion')"
>
<a-select
v-model="formData.version"
allowClear
>
<a-select-option value="1">
v1
</a-select-option>
<a-select-option value="2c">
v2c
</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item
:label="$t('cmdb.ciType.defaultCommunity')"
>
<a-input v-model="formData.community" />
</a-form-model-item>
<a-form-model-item
:label="$t('cmdb.ciType.timeout')"
:extra="$t('cmdb.ciType.snmpFormTip2')"
>
<a-input-number
v-model="formData.timeout"
:min="0"
:precision="0"
/>
</a-form-model-item>
<a-form-model-item
:label="$t('cmdb.ciType.retryCount')"
:extra="$t('cmdb.ciType.snmpFormTip3')"
>
<a-input-number
v-model="formData.retries"
:min="0"
:precision="0"
/>
</a-form-model-item>
</a-form-model>
</template>
<script>
export default {
name: 'SNMPConfig',
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: Object,
default: () => {},
},
},
inject: ['provide_labelCol'],
computed: {
formData: {
get() {
return this.value
},
set(newValue) {
this.$emit('change', newValue)
}
},
labelCol() {
return this.provide_labelCol()
}
}
}
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,68 @@
<template>
<a-form-model
:model="formData"
:labelCol="labelCol"
:wrapperCol="{ span: 6 }"
class="attr-ad-form"
>
<a-form-model-item
:label="$t('cmdb.ciType.initialNode')"
:extra="$t('cmdb.ciType.snmpFormTip4')"
>
<a-input
v-model="formData.initial_node"
:placeholder="$t('cmdb.ciType.defaultGateway')"
/>
</a-form-model-item>
<a-form-model-item
:label="$t('cmdb.ciType.recursiveOrNot')"
:extra="$t('cmdb.ciType.snmpFormTip5')"
>
<a-switch v-model="formData.recursive_scan" />
</a-form-model-item>
<a-form-model-item
:label="$t('cmdb.ciType.maximumDepth')"
:extra="$t('cmdb.ciType.snmpFormTip6')"
>
<a-input-number
v-model="formData.max_depth"
:min="0"
:precision="0"
/>
</a-form-model-item>
</a-form-model>
</template>
<script>
export default {
name: 'SNMPScanningConfig',
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()
}
},
methods: {
}
}
</script>

View File

@@ -1,40 +1,35 @@
<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"
<a-form-item
label="CIDR"
:labelCol="labelCol"
:wrapperCol="{ span: 6 }"
:extra="$t('cmdb.ciType.snmpFormTip7')"
>
<div class="cidr-tag">
<div
v-for="(item) in list"
:key="item.id"
class="cidr-tag-item"
>
<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>
<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>
</template>
<script>

View File

@@ -0,0 +1,160 @@
<template>
<a-form-item
:labelCol="labelCol"
:wrapperCol="{ span: 18 }"
>
<span slot="label">
{{ $t('cmdb.ciType.nodeList') }}
<a-tooltip :title="$t('cmdb.ciType.snmpFormTip1')">
<a-icon type="question-circle" />
</a-tooltip>
</span>
<div class="node-setting-wrap">
<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-model="row.version"
:placeholder="$t('cmdb.ciType.nodeSettingVersionTip')"
allowClear
class="node-setting-select"
>
<a-select-option value="1">
v1
</a-select-option>
<a-select-option value="2c">
v2c
</a-select-option>
</a-select>
</template>
</vxe-column>
<vxe-column min-wdith="90">
<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>
</a-form-item>
</template>
<script>
import _ from 'lodash'
import { v4 as uuidv4 } from 'uuid'
export default {
name: 'MonitorNodeSetting',
inject: ['provide_labelCol'],
props: {
form: {
type: Object,
default: null,
},
},
data() {
return {
nodes: [],
}
},
computed: {
labelCol() {
return this.provide_labelCol()
}
},
methods: {
initNodesFunc(nodes) {
this.nodes = _.cloneDeep(nodes)
},
addNode() {
const newNode = {
id: uuidv4(),
ip: '',
community: 'public',
version: '',
}
this.nodes.push(newNode)
},
removeNode(removeId, minLength) {
if (this.nodes.length <= minLength) {
this.$message.error('不可再删除!')
return
}
const _idx = this.nodes.findIndex((item) => item.id === removeId)
if (_idx > -1) {
this.nodes.splice(_idx, 1)
}
},
copyNode(id) {
const copyNode = this.nodes.find((item) => item.id === id)
if (copyNode) {
const newNode = {
...copyNode,
id: uuidv4(),
}
this.nodes.push(newNode)
}
},
getNodeValue() {
const nodes = this.nodes.map((node) => {
return _.pick(node, ['ip', 'community', 'version'])
})
return nodes
},
},
}
</script>
<style lang="less" scoped>
.node-setting-wrap {
max-width: 600px;
.ant-row {
/deep/ .ant-input-clear-icon {
color: rgba(0,0,0,.25);
&:hover {
color: rgba(0, 0, 0, 0.45);
}
}
}
.node-setting-select {
width: 150px;
}
}
.action {
height: 36px;
display: flex;
align-items: center;
gap: 12px;
}
</style>

View File

@@ -54,11 +54,20 @@
/>
</div>
<template v-if="adrType === DISCOVERY_CATEGORY_TYPE.SNMP">
<div class="attr-ad-header">{{ $t('cmdb.ciType.nodeConfig') }}</div>
<a-form :form="nodeSettingForm" layout="inline" class="attr-ad-snmp-form">
<NodeSetting ref="nodeSetting" :initNodes="nodes" />
<CIDRTags v-model="cidrList" />
</a-form>
<div class="attr-ad-header">{{ $t('cmdb.ciType.scanningParameter') }}</div>
<div class="attr-ad-form attr-ad-snmp-form">
<div class="attr-ad-snmp-form-title">
{{ $t('cmdb.ciType.SNMPConfiguration') }}
</div>
<NodeSetting ref="nodeSetting" />
<SNMPConfig v-model="SNMPScanningConfigForm" />
<div class="attr-ad-snmp-form-title">
{{ $t('cmdb.ciType.scanningConfiguration') }}
</div>
<SNMPScanningConfig v-model="SNMPScanningConfigForm" />
<CIDRTags v-model="SNMPScanningConfigForm.cidr" />
</div>
</template>
<div class="attr-ad-header">{{ $t('cmdb.ciType.adExecConfig') }}</div>
<a-form-model
@@ -177,13 +186,15 @@ import { TAB_KEY } from './attrAD/constants.js'
import HttpSnmpAD from '../../components/httpSnmpAD'
import AttrMapTable from '@/modules/cmdb/components/attrMapTable/index.vue'
import CMDBExprDrawer from '@/components/CMDBExprDrawer'
import NodeSetting from '@/modules/cmdb/components/nodeSetting/index.vue'
import NodeSetting from './attrAD/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'
import SNMPScanningConfig from './attrAD/SNMPScanningConfig/index.vue'
import SNMPConfig from './attrAD/SNMPConfig/index.vue'
export default {
name: 'AttrADTabpane',
@@ -198,7 +209,9 @@ export default {
VcenterForm,
PublicCloud,
PortScanConfig,
CIDRTags
CIDRTags,
SNMPScanningConfig,
SNMPConfig
},
props: {
adr_id: {
@@ -263,14 +276,6 @@ export default {
cronVisible: false,
intervalValue: 3,
agent_type: 'agent_id',
nodes: [
{
id: uuidv4(),
ip: '',
community: 'public',
version: '',
},
],
nodeSettingForm: this.$form.createForm(this, { name: 'snmp_form' }),
uniqueKey: '',
isPrivateCloud: false,
@@ -278,7 +283,16 @@ export default {
PRIVATE_CLOUD_NAME,
DISCOVERY_CATEGORY_TYPE,
isClient: false, // 是否前端新增临时数据
cidrList: [],
SNMPScanningConfigForm: {
version: '2c',
community: 'public',
timeout: 5,
retries: 3,
initial_node: '',
recursive_scan: true,
max_depth: 5,
cidr: []
}, // snmp scanning config form data
}
},
provide() {
@@ -323,13 +337,13 @@ export default {
const isEn = this.$i18n.locale === 'en'
return {
xl: {
span: isEn ? 4 : 2
span: isEn ? 4 : 3
},
lg: {
span: isEn ? 5 : 3
span: isEn ? 5 : 4
},
sm: {
span: isEn ? 6 : 4
span: isEn ? 6 : 5
}
}
}
@@ -404,7 +418,13 @@ export default {
}
if (this.adrType === DISCOVERY_CATEGORY_TYPE.SNMP) {
const nodes = _findADT?.extra_option?.nodes?.length ? _findADT?.extra_option?.nodes : [
const extra_option = _findADT?.extra_option ?? {}
const {
nodes,
cidr = []
} = extra_option
const initializeNodes = nodes?.length ? nodes : [
{
id: uuidv4(),
ip: '',
@@ -412,13 +432,11 @@ export default {
version: '',
},
]
this.nodes = nodes
this.$nextTick(() => {
this.$refs.nodeSetting.initNodesFunc()
this.$refs.nodeSetting.initNodesFunc(initializeNodes)
})
let cidrList = []
const cidr = _findADT?.extra_option?.cidr
if (Array.isArray(cidr) && cidr?.length) {
cidrList = cidr.map((v) => {
return {
@@ -427,7 +445,16 @@ export default {
}
})
}
this.cidrList = cidrList
this.SNMPScanningConfigForm = {
version: extra_option?.version ?? '2c',
community: extra_option?.community ?? 'public',
timeout: extra_option?.timeout ?? 5,
retries: extra_option?.retries ?? 3,
initial_node: extra_option?.initial_node ?? '',
recursive_scan: extra_option?.recursive_scan ?? true,
max_depth: extra_option?.max_depth ?? 5,
cidr: cidrList
}
}
if (this.adrType === DISCOVERY_CATEGORY_TYPE.AGENT) {
this.tableData = (_find?.attributes || []).map((item) => {
@@ -501,12 +528,27 @@ export default {
}
if (this.adrType === DISCOVERY_CATEGORY_TYPE.SNMP) {
const {
cidr,
...otherConfigForm
} = this.SNMPScanningConfigForm
const nodes = this.$refs.nodeSetting?.getNodeValue() ?? []
params = {
extra_option: {
nodes: this.$refs.nodeSetting?.getNodeValue() ?? [],
cidr: this?.cidrList?.map((item) => item.value) || []
...otherConfigForm,
nodes,
cidr: cidr?.map((item) => item.value) || []
},
}
if (
!otherConfigForm?.recursive_scan &&
nodes?.some((item) => !item?.ip)
) {
this.$message.error(this.$t('cmdb.ciType.recursiveTip'))
return
}
}
if (this.adrType === DISCOVERY_CATEGORY_TYPE.AGENT) {
const $table = this.$refs.attrMapTable
@@ -761,8 +803,18 @@ export default {
}
}
.attr-ad-snmp-form {
.ant-form-item {
margin-bottom: 0;
&-title {
font-size: 16px;
color: #000000;
margin-bottom: 12px;
}
/deep/ .ant-input-number {
width: 100%;
}
/deep/ .ant-form-extra {
font-size: 12px;
}
}
</style>

View File

@@ -179,7 +179,11 @@
:filterOption="filterOption"
@change="changeChild"
>
<a-select-option :value="CIType.id" :key="CIType.id" v-for="CIType in CITypes">
<a-select-option
:value="CIType.id"
:key="CIType.id"
v-for="CIType in CITypes"
>
{{ CIType.alias || CIType.name }}
<span class="model-select-name">({{ CIType.name }})</span>
</a-select-option>
@@ -510,7 +514,11 @@ export default {
})
},
filterOption(input, option) {
return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
const inputValue = input.toLowerCase()
const alias = option.componentOptions.children[0].text.toLowerCase()
const name = option.componentOptions.children[1]?.elm?.innerHTML?.toLowerCase?.() ?? ''
return alias.indexOf(inputValue) >= 0 || name.indexOf(inputValue) >= 0
},
rowClass({ row }) {
if (row.isDivider) return 'relation-table-divider'

View File

@@ -112,11 +112,18 @@ export default {
})
let CIList = res?.result || []
const {
show_key = '',
unique_id = '',
attributes = []
} = this?.currentCITYpe || {}
const unique_key = attributes?.find((attr) => attr?.id === unique_id)?.name || ''
if (CIList.length) {
CIList = CIList.map((item) => {
return {
value: item?._id,
name: item?.[this?.currentCITYpe?.show_key] || item?._id || '',
name: item?.[show_key] || item?.[unique_key] || item?._id || '',
unitCount: item?.u_count ?? 0
}
})

View File

@@ -1,6 +1,6 @@
services:
cmdb-db:
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-db:2.3
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-db:2.5
container_name: cmdb-db
env_file:
- .env
@@ -24,7 +24,7 @@ services:
- '23306:3306'
cmdb-cache:
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-cache:2.3
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-cache:2.5
container_name: cmdb-cache
environment:
TZ: Asia/Shanghai
@@ -41,7 +41,7 @@ services:
- redis
cmdb-api:
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.4.17
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.5.2
container_name: cmdb-api
env_file:
- .env
@@ -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.17
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.5.2
container_name: cmdb-ui
depends_on:
cmdb-api:

View File

@@ -80,6 +80,12 @@ For more detailed features, please visit the [official website](https://veops.cn
</tr>
</table>
## Getting started & staying tuned with us
Star us, and you will receive all releases notifications from GitHub without any delay!
![star us](https://github.com/user-attachments/assets/f9056d5a-171c-4f53-9fec-d40c9e5ff94d)
## Quick Start
### 1. Set up

View File

@@ -48,7 +48,7 @@ max_connections=1000
slow_query_log = ON
slow_query_log_file = /tmp/mysql_slow.log
long_query_time = 1
sql_mode="STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION"
sql_mode="STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION"
#log-error = /var/log/mysql/error.log
# By default we only accept connections from localhost
#bind-address = 127.0.0.1

View File

@@ -26,6 +26,8 @@ server {
application/rss+xml
image/svg+xml;
client_max_body_size 100m;
root /etc/nginx/html;
location / {
root /etc/nginx/html;