Compare commits

...

11 Commits

Author SHA1 Message Date
pycook
5aff5d728d fix(api): check rack u slot 2024-11-27 15:39:53 +08:00
pycook
464b9d5394 chore: release v2.4.15 2024-11-27 15:14:58 +08:00
Leo Song
9c4cc20e13 Merge pull request #643 from veops/dev_ui_dcim
Dev UI dcim
2024-11-27 11:15:15 +08:00
songlh
c3c8602207 feat(ui): dcim - update rack list filter 2024-11-27 11:14:21 +08:00
songlh
c961e288af feat(ui): add dcim 2024-11-27 10:26:05 +08:00
pycook
e22b0c5290 feat(api): dcim dev (#642) 2024-11-26 18:56:59 +08:00
pycook
900cf1f617 feat(api): update ipam 2024-11-25 20:19:01 +08:00
Leo Song
f28ad4d041 Merge pull request #639 from veops/dev_ui_ipam
feat(ui): ipam - add batch assign
2024-11-13 10:04:53 +08:00
songlh
d2698b05c0 feat(ui): ipam - add batch assign 2024-11-13 10:03:13 +08:00
Leo Song
6f1332148c Merge pull request #638 from veops/dev_ui_ipam
fix(ui): ipam - filter search value error
2024-11-12 10:59:04 +08:00
songlh
5fa18eeb00 fix(ui): ipam - filter search value error 2024-11-12 10:58:09 +08:00
88 changed files with 6369 additions and 157 deletions

View File

@@ -23,6 +23,7 @@ from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.const import ValueTypeEnum
from api.lib.cmdb.dcim.rack import RackManager
from api.lib.exception import AbortException
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import UserCache
@@ -195,7 +196,7 @@ def cmdb_counter():
today = datetime.date.today()
while True:
try:
db.session.remove()
db.session.commit()
CMDBCounterCache.reset()
@@ -209,6 +210,8 @@ def cmdb_counter():
CMDBCounterCache.flush_sub_counter()
RackManager().check_u_slot()
i += 1
except:
import traceback

View File

@@ -512,7 +512,7 @@ class CMDBCounterCache(object):
result[i.type_id]['rule_count'] = len(adts) + AutoDiscoveryCITypeRelation.get_by(
ad_type_id=i.type_id, only_query=True).count()
result[i.type_id]['exec_target_count'] = len(
set([i.oneagent_id for adt in adts for i in db.session.query(
set([j.oneagent_id for adt in adts for j in db.session.query(
AutoDiscoveryRuleSyncHistory.oneagent_id).filter(
AutoDiscoveryRuleSyncHistory.adt_id == adt.id)]))

View File

@@ -357,6 +357,7 @@ class CIManager(object):
is_auto_discovery=False,
_is_admin=False,
ticket_id=None,
_sync=False,
**ci_dict):
"""
add ci
@@ -366,6 +367,7 @@ class CIManager(object):
:param is_auto_discovery: default is False
:param _is_admin: default is False
:param ticket_id:
:param _sync:
:param ci_dict:
:return:
"""
@@ -496,10 +498,16 @@ class CIManager(object):
record_id = cls.save_password(ci.id, attr_id, password_dict[attr_id], record_id, ci_type.id)
if record_id or has_dynamic: # has changed
ci_cache.apply_async(args=(ci.id, operate_type, record_id), queue=CMDB_QUEUE)
if not _sync:
ci_cache.apply_async(args=(ci.id, operate_type, record_id), queue=CMDB_QUEUE)
else:
ci_cache(ci.id, operate_type, record_id)
if ref_ci_dict: # add relations
ci_relation_add.apply_async(args=(ref_ci_dict, ci.id, current_user.uid), queue=CMDB_QUEUE)
if not _sync:
ci_relation_add.apply_async(args=(ref_ci_dict, ci.id, current_user.uid), queue=CMDB_QUEUE)
else:
ci_relation_add(ref_ci_dict, ci.id, current_user.uid)
return ci.id
@@ -1281,10 +1289,10 @@ class CIRelationManager(object):
return existed.id
@staticmethod
def delete(cr_id, apply_async=True):
def delete(cr_id, apply_async=True, valid=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':
if current_app.config.get('USE_ACL') and current_user.username != 'worker' and valid:
resource_name = CITypeRelationManager.acl_resource_name(cr.first_ci.ci_type.name, cr.second_ci.ci_type.name)
if not ACLManager().has_permission(
resource_name,
@@ -1323,7 +1331,7 @@ class CIRelationManager(object):
return cr
@classmethod
def delete_3(cls, first_ci_id, second_ci_id, apply_async=True):
def delete_3(cls, first_ci_id, second_ci_id, apply_async=True, valid=True):
cr = CIRelation.get_by(first_ci_id=first_ci_id,
second_ci_id=second_ci_id,
to_dict=False,
@@ -1333,7 +1341,7 @@ class CIRelationManager(object):
# 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, apply_async=apply_async)
cls.delete(cr.id, apply_async=apply_async, valid=valid)
return cr

View File

@@ -879,6 +879,8 @@ class CITypeRelationManager(object):
def _wrap_relation_type_dict(type_id, relation_inst):
ci_type_dict = CITypeCache.get(type_id).to_dict()
ci_type_dict["ctr_id"] = relation_inst.id
show_key = AttributeCache.get(ci_type_dict.get('show_id') or ci_type_dict['unique_id'])
ci_type_dict["show_key"] = show_key and show_key.name
ci_type_dict["attributes"] = CITypeAttributeManager.get_attributes_by_type_id(ci_type_dict["id"])
attr_filter = CIFilterPermsCRUD.get_attr_filter(type_id)
if attr_filter:
@@ -1551,7 +1553,10 @@ class CITypeTemplateManager(object):
if existed is None:
_group['type_id'] = type_id_map.get(_group['type_id'], _group['type_id'])
existed = CITypeAttributeGroup.create(flush=True, **_group)
try:
existed = CITypeAttributeGroup.create(flush=True, **_group)
except:
continue
for order, attr in enumerate(group['attributes'] or []):
item_existed = CITypeAttributeGroupItem.get_by(group_id=existed.id,

View File

@@ -123,6 +123,11 @@ class BuiltinModelEnum(BaseEnum):
IPAM_ADDRESS = "ipam_address"
IPAM_SCOPE = "ipam_scope"
DCIM_REGION = "dcim_region"
DCIM_IDC = "dcim_idc"
DCIM_SERVER_ROOM = "dcim_server_room"
DCIM_RACK = "dcim_rack"
BUILTIN_ATTRIBUTES = {
"_updated_at": _l("Update Time"),

View File

@@ -0,0 +1 @@
# -*- coding:utf-8 -*-

View File

@@ -0,0 +1,33 @@
# -*- coding:utf-8 -*-
from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.const import ExistPolicy
class DCIMBase(object):
def __init__(self):
self.type_id = None
@staticmethod
def add_relation(parent_id, child_id):
if not parent_id or not child_id:
return
CIRelationManager().add(parent_id, child_id, valid=False, apply_async=False)
def add(self, parent_id, **kwargs):
ci_id = CIManager().add(self.type_id, exist_policy=ExistPolicy.REJECT, **kwargs)
if parent_id:
self.add_relation(parent_id, ci_id)
return ci_id
@classmethod
def update(cls, _id, **kwargs):
CIManager().update(_id, **kwargs)
@classmethod
def delete(cls, _id):
CIManager().delete(_id)

View File

@@ -0,0 +1,17 @@
# -*- coding:utf-8 -*-
from api.lib.utils import BaseEnum
class RackBuiltinAttributes(BaseEnum):
U_COUNT = 'u_count'
U_START = 'u_start'
FREE_U_COUNT = 'free_u_count'
U_SLOT_ABNORMAL = 'u_slot_abnormal'
class OperateTypeEnum(BaseEnum):
ADD_DEVICE = "0"
REMOVE_DEVICE = "1"
MOVE_DEVICE = "2"

View File

@@ -0,0 +1,40 @@
from flask_login import current_user
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIManager
from api.lib.mixin import DBMixin
from api.models.cmdb import DCIMOperationHistory
class OperateHistoryManager(DBMixin):
cls = DCIMOperationHistory
@classmethod
def search(cls, page, page_size, fl=None, only_query=False, reverse=False, count_query=False,
last_size=None, **kwargs):
numfound, result = super(OperateHistoryManager, cls).search(page, page_size, fl, only_query, reverse,
count_query, last_size, **kwargs)
ci_ids = [i['ci_id'] for i in result]
id2ci = {i['_id']: i for i in (CIManager.get_cis_by_ids(ci_ids) or []) if i}
type2show_key = dict()
for i in id2ci.values():
if i.get('_type') not in type2show_key:
ci_type = CITypeCache.get(i.get('_type'))
if ci_type:
show_key = AttributeCache.get(ci_type.show_id or ci_type.unique_id)
type2show_key[i['_type']] = show_key and show_key.name
return numfound, result, id2ci, type2show_key
def _can_add(self, **kwargs):
kwargs['uid'] = current_user.uid
return kwargs
def _can_update(self, **kwargs):
pass
def _can_delete(self, **kwargs):
pass

View File

@@ -0,0 +1,19 @@
# -*- coding:utf-8 -*-
from flask import abort
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.const import BuiltinModelEnum
from api.lib.cmdb.dcim.base import DCIMBase
from api.lib.cmdb.resp_format import ErrFormat
class IDCManager(DCIMBase):
def __init__(self):
super(IDCManager, self).__init__()
self.ci_type = CITypeCache.get(BuiltinModelEnum.DCIM_IDC) or abort(
404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_IDC))
self.type_id = self.ci_type.id

View File

@@ -0,0 +1,183 @@
# -*- coding:utf-8 -*-
import itertools
import redis_lock
from flask import abort
from api.extensions import rd
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.const import BuiltinModelEnum
from api.lib.cmdb.dcim.base import DCIMBase
from api.lib.cmdb.dcim.const import OperateTypeEnum
from api.lib.cmdb.dcim.const import RackBuiltinAttributes
from api.lib.cmdb.dcim.history import OperateHistoryManager
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB
from api.lib.cmdb.search.ci_relation.search import Search as RelationSearch
class RackManager(DCIMBase):
def __init__(self):
super(RackManager, self).__init__()
self.ci_type = CITypeCache.get(BuiltinModelEnum.DCIM_RACK) or abort(
404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_RACK))
self.type_id = self.ci_type.id
@classmethod
def update(cls, _id, **kwargs):
if RackBuiltinAttributes.U_COUNT in kwargs:
devices, _, _, _, _, _ = RelationSearch(
[_id],
level=[1],
fl=[RackBuiltinAttributes.U_COUNT, RackBuiltinAttributes.U_START],
count=1000000).search()
for device in devices:
u_start = device.get(RackBuiltinAttributes.U_START)
u_count = device.get(RackBuiltinAttributes.U_COUNT) or 2
if u_start and u_start + u_count - 1 > kwargs[RackBuiltinAttributes.U_COUNT]:
return abort(400, ErrFormat.dcim_rack_u_count_invalid)
CIManager().update(_id, _sync=True, **kwargs)
if RackBuiltinAttributes.U_COUNT in kwargs:
payload = {RackBuiltinAttributes.FREE_U_COUNT: cls._calc_u_free_count(_id)}
CIManager().update(_id, _sync=True, **payload)
def delete(self, _id):
super(RackManager, self).delete(_id)
payload = {RackBuiltinAttributes.U_START: None}
_, _, second_cis = CIRelationManager.get_second_cis(_id, per_page='all')
for ci in second_cis:
CIManager().update(ci['_id'], **payload)
@staticmethod
def _calc_u_free_count(rack_id, device_id=None, u_start=None, u_count=None):
rack = CIManager.get_ci_by_id(rack_id, need_children=False)
if not rack.get(RackBuiltinAttributes.U_COUNT):
return 0
if device_id is not None and u_count is None:
ci = CIManager().get_ci_by_id(device_id, need_children=False)
u_count = ci.get(RackBuiltinAttributes.U_COUNT) or 2
if u_start and u_start + u_count - 1 > rack.get(RackBuiltinAttributes.U_COUNT):
return abort(400, ErrFormat.dcim_rack_u_slot_invalid)
devices, _, _, _, _, _ = RelationSearch(
[rack_id],
level=[1],
fl=[RackBuiltinAttributes.U_COUNT, RackBuiltinAttributes.U_START],
count=1000000).search()
u_count_sum = 0
for device in devices:
u_count_sum += (device.get(RackBuiltinAttributes.U_COUNT) or 2)
if device_id is not None:
_u_start = device.get(RackBuiltinAttributes.U_START)
_u_count = device.get(RackBuiltinAttributes.U_COUNT) or 2
if not _u_start:
continue
if device.get('_id') != device_id and set(range(u_start, u_start + u_count)) & set(
range(_u_start, _u_start + _u_count)):
return abort(400, ErrFormat.dcim_rack_u_slot_invalid)
return rack[RackBuiltinAttributes.U_COUNT] - u_count_sum
def check_u_slot(self):
racks, _, _, _, _, _ = SearchFromDB(
"_type:{}".format(self.type_id),
count=10000000,
fl=[RackBuiltinAttributes.U_START, RackBuiltinAttributes.U_COUNT, RackBuiltinAttributes.U_SLOT_ABNORMAL],
parent_node_perm_passed=True).search()
for rack in racks:
devices, _, _, _, _, _ = RelationSearch(
[rack['_id']],
level=[1],
fl=[RackBuiltinAttributes.U_COUNT, RackBuiltinAttributes.U_START],
count=1000000).search()
u_slot_sets = []
for device in devices:
u_start = device.get(RackBuiltinAttributes.U_START)
u_count = device.get(RackBuiltinAttributes.U_COUNT) or 2
if u_start is not None and str(u_start).isdigit():
u_slot_sets.append(set(range(u_start, u_start + u_count)))
if len(u_slot_sets) > 1:
u_slot_abnormal = False
for a, b in itertools.combinations(u_slot_sets, 2):
if a.intersection(b):
u_slot_abnormal = True
break
if u_slot_abnormal != rack.get(RackBuiltinAttributes.U_SLOT_ABNORMAL):
payload = {RackBuiltinAttributes.U_SLOT_ABNORMAL: u_slot_abnormal}
CIManager().update(rack['_id'], **payload)
def add_device(self, rack_id, device_id, u_start, u_count=None):
with (redis_lock.Lock(rd.r, "DCIM_RACK_OPERATE_{}".format(rack_id))):
self._calc_u_free_count(rack_id, device_id, u_start, u_count)
self.add_relation(rack_id, device_id)
payload = {RackBuiltinAttributes.U_START: u_start}
if u_count:
payload[RackBuiltinAttributes.U_COUNT] = u_count
CIManager().update(device_id, _sync=True, **payload)
payload = {
RackBuiltinAttributes.FREE_U_COUNT: self._calc_u_free_count(rack_id, device_id, u_start, u_count)}
CIManager().update(rack_id, _sync=True, **payload)
OperateHistoryManager().add(operate_type=OperateTypeEnum.ADD_DEVICE, rack_id=rack_id, ci_id=device_id)
def remove_device(self, rack_id, device_id):
with (redis_lock.Lock(rd.r, "DCIM_RACK_OPERATE_{}".format(rack_id))):
CIRelationManager.delete_3(rack_id, device_id, apply_async=False, valid=False)
payload = {RackBuiltinAttributes.FREE_U_COUNT: self._calc_u_free_count(rack_id)}
CIManager().update(rack_id, _sync=True, **payload)
payload = {RackBuiltinAttributes.U_START: None}
CIManager().update(device_id, _sync=True, **payload)
OperateHistoryManager().add(operate_type=OperateTypeEnum.REMOVE_DEVICE, rack_id=rack_id, ci_id=device_id)
def move_device(self, rack_id, device_id, to_u_start):
with (redis_lock.Lock(rd.r, "DCIM_RACK_OPERATE_{}".format(rack_id))):
payload = {RackBuiltinAttributes.FREE_U_COUNT: self._calc_u_free_count(rack_id, device_id, to_u_start)}
CIManager().update(rack_id, _sync=True, **payload)
CIManager().update(device_id, _sync=True, **{RackBuiltinAttributes.U_START: to_u_start})
OperateHistoryManager().add(operate_type=OperateTypeEnum.MOVE_DEVICE, rack_id=rack_id, ci_id=device_id)
def migrate_device(self, rack_id, device_id, to_rack_id, to_u_start):
with (redis_lock.Lock(rd.r, "DCIM_RACK_OPERATE_{}".format(rack_id))):
self._calc_u_free_count(to_rack_id, device_id, to_u_start)
if rack_id != to_rack_id:
CIRelationManager.delete_3(rack_id, device_id, apply_async=False, valid=False)
self.add_relation(to_rack_id, device_id)
payload = {
RackBuiltinAttributes.FREE_U_COUNT: self._calc_u_free_count(to_rack_id, device_id, to_u_start)}
CIManager().update(to_rack_id, _sync=True, **payload)
CIManager().update(device_id, _sync=True, **{RackBuiltinAttributes.U_START: to_u_start})
if rack_id != to_rack_id:
payload = {RackBuiltinAttributes.FREE_U_COUNT: self._calc_u_free_count(rack_id)}
CIManager().update(rack_id, _sync=True, **payload)
OperateHistoryManager().add(operate_type=OperateTypeEnum.REMOVE_DEVICE, rack_id=rack_id, ci_id=device_id)
OperateHistoryManager().add(operate_type=OperateTypeEnum.ADD_DEVICE, rack_id=to_rack_id, ci_id=device_id)

View File

@@ -0,0 +1,29 @@
# -*- coding:utf-8 -*-
from flask import abort
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.const import BuiltinModelEnum
from api.lib.cmdb.const import ExistPolicy
from api.lib.cmdb.resp_format import ErrFormat
class RegionManager(object):
def __init__(self):
self.ci_type = CITypeCache.get(BuiltinModelEnum.DCIM_REGION) or abort(
404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_REGION))
self.type_id = self.ci_type.id
def add(self, **kwargs):
return CIManager().add(self.type_id, exist_policy=ExistPolicy.REJECT, **kwargs)
@classmethod
def update(cls, _id, **kwargs):
CIManager().update(_id, **kwargs)
@classmethod
def delete(cls, _id):
CIManager().delete(_id)

View File

@@ -0,0 +1,56 @@
# -*- coding:utf-8 -*-
from flask import abort
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.const import BuiltinModelEnum
from api.lib.cmdb.dcim.base import DCIMBase
from api.lib.cmdb.dcim.const import RackBuiltinAttributes
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB
from api.models.cmdb import CI
from api.models.cmdb import CIRelation
class ServerRoomManager(DCIMBase):
def __init__(self):
super(ServerRoomManager, self).__init__()
self.ci_type = CITypeCache.get(BuiltinModelEnum.DCIM_SERVER_ROOM) or abort(
404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_SERVER_ROOM))
self.type_id = self.ci_type.id
@staticmethod
def get_racks(_id, q=None):
rack_type = CITypeCache.get(BuiltinModelEnum.DCIM_RACK) or abort(
404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_RACK))
relations = CIRelation.get_by(first_ci_id=_id, only_query=True).join(
CI, CI.id == CIRelation.second_ci_id).filter(CI.type_id == rack_type.id)
rack_ids = [i.second_ci_id for i in relations]
q = "_type:{}".format(rack_type.id) if not q else "_type:{},{}".format(rack_type.id, q)
if rack_ids:
response, _, _, _, numfound, _ = SearchFromDB(
q,
ci_ids=list(rack_ids),
count=1000000,
parent_node_perm_passed=True).search()
else:
response, numfound = [], 0
counter = dict(rack_count=numfound)
u_count = 0
free_u_count = 0
for i in response:
_u_count = i.get(RackBuiltinAttributes.U_COUNT) or 0
u_count += _u_count
free_u_count += (_u_count if i.get(RackBuiltinAttributes.FREE_U_COUNT) is None else
i.get(RackBuiltinAttributes.FREE_U_COUNT))
counter["u_count"] = u_count
counter["u_used_count"] = u_count - free_u_count
counter["device_count"] = CIRelation.get_by(only_query=True).filter(
CIRelation.first_ci_id.in_(rack_ids)).count()
return counter, response

View File

@@ -0,0 +1,85 @@
# -*- coding:utf-8 -*-
from collections import defaultdict
from flask import abort
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.const import BuiltinModelEnum
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB
from api.models.cmdb import CI
from api.models.cmdb import CIRelation
class TreeViewManager(object):
@classmethod
def get(cls):
region_type = CITypeCache.get(BuiltinModelEnum.DCIM_REGION) or abort(
404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_REGION))
idc_type = CITypeCache.get(BuiltinModelEnum.DCIM_IDC) or abort(
404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_IDC))
server_room_type = CITypeCache.get(BuiltinModelEnum.DCIM_SERVER_ROOM) or abort(
404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_SERVER_ROOM))
rack_type = CITypeCache.get(BuiltinModelEnum.DCIM_RACK) or abort(
404, ErrFormat.dcim_builtin_model_not_found.format(BuiltinModelEnum.DCIM_RACK))
relations = defaultdict(set)
ids = set()
has_parent_ids = set()
for i in CIRelation.get_by(only_query=True).join(CI, CI.id == CIRelation.first_ci_id).filter(
CI.type_id.in_([region_type.id, idc_type.id])):
relations[i.first_ci_id].add(i.second_ci_id)
ids.add(i.first_ci_id)
ids.add(i.second_ci_id)
has_parent_ids.add(i.second_ci_id)
for i in CIRelation.get_by(only_query=True).join(
CI, CI.id == CIRelation.second_ci_id).filter(CI.type_id.in_([idc_type.id, server_room_type.id])):
relations[i.first_ci_id].add(i.second_ci_id)
ids.add(i.first_ci_id)
ids.add(i.second_ci_id)
has_parent_ids.add(i.second_ci_id)
for i in CI.get_by(only_query=True).filter(CI.type_id.in_([region_type.id, idc_type.id])):
ids.add(i.id)
for _id in ids:
if _id not in has_parent_ids:
relations[None].add(_id)
type2name = dict()
type2name[region_type.id] = AttributeCache.get(region_type.show_id or region_type.unique_id).name
type2name[idc_type.id] = AttributeCache.get(idc_type.show_id or idc_type.unique_id).name
type2name[server_room_type.id] = AttributeCache.get(server_room_type.show_id or server_room_type.unique_id).name
response, _, _, _, _, _ = SearchFromDB(
"_type:({})".format(";".join(map(str, [region_type.id, idc_type.id, server_room_type.id]))),
ci_ids=list(ids),
count=1000000,
fl=list(type2name.values()),
parent_node_perm_passed=True).search()
id2ci = {i['_id']: i for i in response}
def _build_tree(_tree, parent_id=None):
tree = []
for child_id in _tree.get(parent_id, []):
children = sorted(_build_tree(_tree, child_id), key=lambda x: x['_id'])
if not id2ci.get(child_id):
continue
ci = id2ci[child_id]
if ci['ci_type'] == BuiltinModelEnum.DCIM_SERVER_ROOM:
ci['rack_count'] = CIRelation.get_by(first_ci_id=child_id, only_query=True).join(
CI, CI.id == CIRelation.second_ci_id).filter(CI.type_id == rack_type.id).count()
tree.append({'children': children, **ci})
return tree
result = sorted(_build_tree(relations), key=lambda x: x['_id'])
return result, type2name

View File

@@ -3,7 +3,6 @@
import redis_lock
from flask import abort
from api.extensions import db
from api.extensions import rd
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIManager
@@ -21,9 +20,8 @@ from api.lib.cmdb.search.ci_relation.search import Search as RelationSearch
class IpAddressManager(object):
def __init__(self):
self.ci_type = CITypeCache.get(BuiltinModelEnum.IPAM_ADDRESS)
not self.ci_type and abort(400, ErrFormat.ipam_address_model_not_found.format(
BuiltinModelEnum.IPAM_ADDRESS))
self.ci_type = CITypeCache.get(BuiltinModelEnum.IPAM_ADDRESS) or abort(
404, ErrFormat.ipam_address_model_not_found.format(BuiltinModelEnum.IPAM_ADDRESS))
self.type_id = self.ci_type.id
@@ -48,25 +46,28 @@ class IpAddressManager(object):
CIRelationManager().add(parent_id, child_id, valid=False, apply_async=False)
@staticmethod
def calc_free_count(subnet_id):
db.session.commit()
def calc_used_count(subnet_id):
q = "{}:(0;2),-{}:true".format(IPAddressBuiltinAttributes.ASSIGN_STATUS, IPAddressBuiltinAttributes.IS_USED)
return len(set(RelationSearch([subnet_id], level=[1], query=q).search(only_ids=True) or []))
return len(set(RelationSearch([subnet_id], level=[1], query=q, count=1000000).search(only_ids=True) or []))
def _update_subnet_count(self, subnet_id, assign_count, used_count=None):
@staticmethod
def _calc_assign_count(subnet_id):
q = "{}:(0;2)".format(IPAddressBuiltinAttributes.ASSIGN_STATUS)
return len(set(RelationSearch([subnet_id], level=[1], query=q, count=1000000).search(only_ids=True) or []))
def _update_subnet_count(self, subnet_id, assign_count_computed, used_count=None):
payload = {}
cur = CIManager.get_ci_by_id(subnet_id, need_children=False)
if assign_count is not None:
payload[SubnetBuiltinAttributes.ASSIGN_COUNT] = (cur.get(
SubnetBuiltinAttributes.ASSIGN_COUNT) or 0) + assign_count
if assign_count_computed:
payload[SubnetBuiltinAttributes.ASSIGN_COUNT] = self._calc_assign_count(subnet_id)
if used_count is not None:
payload[SubnetBuiltinAttributes.USED_COUNT] = used_count
payload[SubnetBuiltinAttributes.FREE_COUNT] = (cur[SubnetBuiltinAttributes.HOSTS_COUNT] -
self.calc_free_count(subnet_id))
self.calc_used_count(subnet_id))
CIManager().update(subnet_id, **payload)
def assign_ips(self, ips, subnet_id, cidr, **kwargs):
@@ -95,35 +96,28 @@ class IpAddressManager(object):
ip2ci = {ci[IPAddressBuiltinAttributes.IP]: ci for ci in cis}
ci_ids = []
status_change_num = 0
for ip in ips:
kwargs['name'] = ip
kwargs[IPAddressBuiltinAttributes.IP] = ip
if ip not in ip2ci:
ci_id = CIManager.add(self.type_id, _sync=True, **kwargs)
status_change_num += 1
else:
ci_id = ip2ci[ip]['_id']
CIManager().update(ci_id, _sync=True, **kwargs)
if IPAddressBuiltinAttributes.ASSIGN_STATUS in kwargs and (
(kwargs[IPAddressBuiltinAttributes.ASSIGN_STATUS] or 2) !=
(ip2ci[ip].get(IPAddressBuiltinAttributes.ASSIGN_STATUS) or 2)):
status_change_num += 1
ci_ids.append(ci_id)
self._add_relation(subnet_id, ci_id)
if ips and IPAddressBuiltinAttributes.ASSIGN_STATUS in kwargs:
self._update_subnet_count(subnet_id, -status_change_num if kwargs.get(
IPAddressBuiltinAttributes.ASSIGN_STATUS) == IPAddressAssignStatus.UNASSIGNED else status_change_num)
self._update_subnet_count(subnet_id, True)
if ips and IPAddressBuiltinAttributes.IS_USED in kwargs:
q = "{}:true".format(IPAddressBuiltinAttributes.IS_USED)
cur_used_ids = RelationSearch([subnet_id], level=[1], query=q).search(only_ids=True)
for _id in set(cur_used_ids) - set(ci_ids):
CIManager().update(_id, _sync=True, **{IPAddressBuiltinAttributes.IS_USED: False})
CIManager().update(_id, **{IPAddressBuiltinAttributes.IS_USED: False})
self._update_subnet_count(subnet_id, None, used_count=len(ips))
self._update_subnet_count(subnet_id, False, used_count=len(ips))
if kwargs.get(IPAddressBuiltinAttributes.ASSIGN_STATUS) in (
IPAddressAssignStatus.ASSIGNED, IPAddressAssignStatus.RESERVED):

View File

@@ -50,6 +50,10 @@ class ScanHistoryManager(DBMixin):
if scan_rule is not None:
scan_rule.update(last_scan_time=kwargs.get('start_at'))
for i in self.cls.get_by(subnet_scan_id=kwargs.get('subnet_scan_id'), only_query=True).order_by(
self.cls.id.desc()).offset(100):
i.delete()
def _can_update(self, **kwargs):
pass

View File

@@ -18,15 +18,13 @@ from api.models.cmdb import IPAMSubnetScan
class Stats(object):
def __init__(self):
self.address_type = CITypeCache.get(BuiltinModelEnum.IPAM_ADDRESS)
not self.address_type and abort(400, ErrFormat.ipam_address_model_not_found.format(
BuiltinModelEnum.IPAM_ADDRESS))
self.address_type = CITypeCache.get(BuiltinModelEnum.IPAM_ADDRESS) or abort(
404, ErrFormat.ipam_address_model_not_found.format(BuiltinModelEnum.IPAM_ADDRESS))
self.address_type_id = self.address_type.id
self.subnet_type = CITypeCache.get(BuiltinModelEnum.IPAM_SUBNET)
not self.subnet_type and abort(400, ErrFormat.ipam_address_model_not_found.format(
BuiltinModelEnum.IPAM_ADDRESS))
self.subnet_type = CITypeCache.get(BuiltinModelEnum.IPAM_SUBNET) or abort(
404, ErrFormat.ipam_address_model_not_found.format(BuiltinModelEnum.IPAM_ADDRESS))
self.subnet_type_id = self.subnet_type.id
@@ -40,8 +38,10 @@ class Stats(object):
return list(set(ci_ids) - set(has_children_ci_ids))
else:
type_id = CIManager().get_by_id(parent_id).type_id
key = [(str(parent_id), type_id)]
_type = CIManager().get_by_id(parent_id)
if not _type:
return abort(404, ErrFormat.ipam_subnet_not_found)
key = [(str(parent_id), _type.type_id)]
result = []
while True:
res = [json.loads(x).items() for x in [i or '{}' for i in rd.get(

View File

@@ -2,6 +2,7 @@
from collections import defaultdict
import datetime
import ipaddress
from flask import abort
@@ -9,7 +10,7 @@ from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.const import BuiltinModelEnum, BUILTIN_ATTRIBUTES
from api.lib.cmdb.const import BuiltinModelEnum
from api.lib.cmdb.ipam.const import OperateTypeEnum
from api.lib.cmdb.ipam.const import SubnetBuiltinAttributes
from api.lib.cmdb.ipam.history import OperateHistoryManager
@@ -22,9 +23,8 @@ from api.models.cmdb import IPAMSubnetScan
class SubnetManager(object):
def __init__(self):
self.ci_type = CITypeCache.get(BuiltinModelEnum.IPAM_SUBNET)
not self.ci_type and abort(400, ErrFormat.ipam_subnet_model_not_found.format(
BuiltinModelEnum.IPAM_SUBNET))
self.ci_type = CITypeCache.get(BuiltinModelEnum.IPAM_SUBNET) or abort(
404, ErrFormat.ipam_subnet_model_not_found.format(BuiltinModelEnum.IPAM_SUBNET))
self.type_id = self.ci_type.id
@@ -47,7 +47,7 @@ class SubnetManager(object):
new_last_update_at = ""
for i in result:
__last_update_at = max([i['updated_at'] or "", i['created_at'] or ""])
__last_update_at = max([i['rule_updated_at'] or "", i['created_at'] or ""])
if new_last_update_at < __last_update_at:
new_last_update_at = __last_update_at
@@ -131,7 +131,11 @@ class SubnetManager(object):
@staticmethod
def _is_valid_cidr(cidr):
try:
return str(ipaddress.ip_network(cidr))
cidr = ipaddress.ip_network(cidr)
if not (8 <= cidr.prefixlen <= 31):
raise ValueError
return str(cidr)
except ValueError:
return abort(400, ErrFormat.ipam_cidr_invalid_notation.format(cidr))
@@ -143,6 +147,7 @@ class SubnetManager(object):
root_nodes = set(all_nodes) - set(none_root_nodes) - set(_id and [_id] or [])
response, _, _, _, _, _ = SearchFromDB("_type:{}".format(self.type_id),
ci_ids=list(root_nodes),
count=1000000,
parent_node_perm_passed=True).search()
cur_subnet = ipaddress.ip_network(cidr)
@@ -163,6 +168,7 @@ class SubnetManager(object):
response, _, _, _, _, _ = SearchFromDB("_type:{}".format(self.type_id),
ci_ids=list(child_nodes),
count=1000000,
parent_node_perm_passed=True).search()
cur_subnet = ipaddress.ip_network(cidr)
@@ -199,7 +205,7 @@ class SubnetManager(object):
return cidr
def _add_subnet(self, cidr, **kwargs):
kwargs[SubnetBuiltinAttributes.HOSTS_COUNT] = ipaddress.ip_network(cidr).num_addresses - 2
kwargs[SubnetBuiltinAttributes.HOSTS_COUNT] = len(list(ipaddress.ip_network(cidr).hosts()))
kwargs[SubnetBuiltinAttributes.USED_COUNT] = 0
kwargs[SubnetBuiltinAttributes.ASSIGN_COUNT] = 0
kwargs[SubnetBuiltinAttributes.FREE_COUNT] = kwargs[SubnetBuiltinAttributes.HOSTS_COUNT]
@@ -240,7 +246,8 @@ class SubnetManager(object):
def _update_scan_rule(ci_id, agent_id, cron, scan_enabled=True):
existed = IPAMSubnetScan.get_by(ci_id=ci_id, first=True, to_dict=False)
if existed is not None:
existed.update(ci_id=ci_id, agent_id=agent_id, cron=cron, scan_enabled=scan_enabled)
existed.update(ci_id=ci_id, agent_id=agent_id, cron=cron, scan_enabled=scan_enabled,
rule_updated_at=datetime.datetime.now())
else:
IPAMSubnetScan.create(ci_id=ci_id, agent_id=agent_id, cron=cron, scan_enabled=scan_enabled)
@@ -273,7 +280,9 @@ class SubnetManager(object):
existed = IPAMSubnetScan.get_by(ci_id=_id, first=True, to_dict=False)
existed and existed.delete()
delete_ci_ids = []
for i in CIRelation.get_by(first_ci_id=_id, to_dict=False):
delete_ci_ids.append(i.second_ci_id)
i.delete()
cur = CIManager.get_ci_by_id(_id, need_children=False)
@@ -284,6 +293,8 @@ class SubnetManager(object):
cidr=cur.get(SubnetBuiltinAttributes.CIDR),
description=cur.get(SubnetBuiltinAttributes.CIDR))
# batch_delete_ci.apply_async(args=(delete_ci_ids,))
return _id

View File

@@ -169,3 +169,8 @@ class ErrFormat(CommonErrFormat):
ipam_subnet_cannot_delete = _l("Cannot delete because child nodes exist")
ipam_subnet_not_found = _l("Subnet is not found")
ipam_scope_cannot_delete = _l("Cannot delete because child nodes exist")
# # DCIM
dcim_builtin_model_not_found = _l("The dcim model {} does not exist")
dcim_rack_u_slot_invalid = _l("Irregularities in Rack Units")
dcim_rack_u_count_invalid = _l("The device's position is greater than the rack unit height")

View File

@@ -54,6 +54,7 @@ class CMDBApp(BaseApp):
"create_topology_view"],
},
{"page": "IPAM", "page_cn": "IPAM", "perms": ["read"]},
{"page": "DCIM", "page_cn": "数据中心", "perms": ["read"]},
]
def __init__(self):

View File

@@ -676,6 +676,7 @@ class IPAMSubnetScan(Model):
ci_id = db.Column(db.Integer, index=True, nullable=False)
scan_enabled = db.Column(db.Boolean, default=True)
rule_updated_at = db.Column(db.DateTime)
last_scan_time = db.Column(db.DateTime)
# scan rules
@@ -706,3 +707,14 @@ class IPAMOperationHistory(Model2):
cidr = db.Column(db.String(18), index=True)
operate_type = db.Column(db.Enum(*OperateTypeEnum.all()))
description = db.Column(db.Text)
class DCIMOperationHistory(Model2):
__tablename__ = "c_dcim_operation_histories"
from api.lib.cmdb.dcim.const import OperateTypeEnum
uid = db.Column(db.Integer, index=True)
rack_id = db.Column(db.Integer, index=True)
ci_id = db.Column(db.Integer, index=True)
operate_type = db.Column(db.Enum(*OperateTypeEnum.all()))

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2024-11-11 17:40+0800\n"
"POT-Creation-Date: 2024-11-26 18:54+0800\n"
"PO-Revision-Date: 2023-12-25 20:21+0800\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: zh\n"
@@ -92,11 +92,11 @@ msgstr "您没有操作权限!"
msgid "Only the creator or administrator has permission!"
msgstr "只有创建人或者管理员才有权限!"
#: api/lib/cmdb/const.py:128
#: api/lib/cmdb/const.py:133
msgid "Update Time"
msgstr "更新时间"
#: api/lib/cmdb/const.py:129
#: api/lib/cmdb/const.py:134
msgid "Updated By"
msgstr "更新人"
@@ -544,6 +544,18 @@ msgstr "因为子节点已经存在,不能删除"
msgid "Subnet is not found"
msgstr "子网不存在"
#: api/lib/cmdb/resp_format.py:174
msgid "The dcim model {} does not exist"
msgstr "DCIM模型 {} 不存在!"
#: api/lib/cmdb/resp_format.py:175
msgid "Irregularities in Rack Units"
msgstr "机架U位异常!"
#: api/lib/cmdb/resp_format.py:176
msgid "The device's position is greater than the rack unit height"
msgstr "有设备的位置大于机柜的U数!"
#: api/lib/common_setting/resp_format.py:8
msgid "Company info already existed"
msgstr "公司信息已存在,无法创建!"

View File

@@ -0,0 +1 @@
# -*- coding:utf-8 -*-

View File

@@ -0,0 +1,30 @@
# -*- coding:utf-8 -*-
from flask import request
from api.lib.cmdb.dcim.history import OperateHistoryManager
from api.lib.common_setting.decorator import perms_role_required
from api.lib.common_setting.role_perm_base import CMDBApp
from api.lib.utils import get_page
from api.lib.utils import get_page_size
from api.lib.utils import handle_arg_list
from api.resource import APIView
app_cli = CMDBApp()
class DCIMOperateHistoryView(APIView):
url_prefix = ("/dcim/history/operate",)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def get(self):
page = get_page(request.values.pop("page", 1))
page_size = get_page_size(request.values.pop("page_size", None))
operate_type = handle_arg_list(request.values.pop('operate_type', []))
if operate_type:
request.values["operate_type"] = operate_type
numfound, result, id2ci, type2show_key = OperateHistoryManager.search(page, page_size, **request.values)
return self.jsonify(numfound=numfound, result=result, id2ci=id2ci, type2show_key=type2show_key)

View File

@@ -0,0 +1,35 @@
# -*- coding:utf-8 -*-
from flask import request
from api.lib.cmdb.dcim.idc import IDCManager
from api.lib.common_setting.decorator import perms_role_required
from api.lib.common_setting.role_perm_base import CMDBApp
from api.resource import APIView
app_cli = CMDBApp()
class IDCView(APIView):
url_prefix = ("/dcim/idc", "/dcim/idc/<int:_id>")
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def post(self):
parent_id = request.values.pop("parent_id")
return self.jsonify(ci_id=IDCManager().add(parent_id, **request.values))
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def put(self, _id):
IDCManager().update(_id, **request.values)
return self.jsonify(ci_id=_id)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def delete(self, _id):
IDCManager().delete(_id)
return self.jsonify(ci_id=_id)

View File

@@ -0,0 +1,89 @@
# -*- coding:utf-8 -*-
from flask import request
from api.lib.cmdb.dcim.const import RackBuiltinAttributes
from api.lib.cmdb.dcim.rack import RackManager
from api.lib.common_setting.decorator import perms_role_required
from api.lib.common_setting.role_perm_base import CMDBApp
from api.lib.decorator import args_required
from api.resource import APIView
app_cli = CMDBApp()
class RackView(APIView):
url_prefix = ("/dcim/rack", "/dcim/rack/<int:_id>")
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
@args_required("parent_id")
def post(self):
parent_id = request.values.pop("parent_id")
return self.jsonify(ci_id=RackManager().add(parent_id, **request.values))
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def put(self, _id):
RackManager().update(_id, **request.values)
return self.jsonify(ci_id=_id)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def delete(self, _id):
RackManager().delete(_id)
return self.jsonify(ci_id=_id)
class RackDetailView(APIView):
url_prefix = ("/dcim/rack/<int:rack_id>/device/<int:device_id>",)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
@args_required(RackBuiltinAttributes.U_START)
def post(self, rack_id, device_id):
u_start = request.values.pop(RackBuiltinAttributes.U_START)
u_count = request.values.get(RackBuiltinAttributes.U_COUNT)
RackManager().add_device(rack_id, device_id, u_start, u_count)
return self.jsonify(rack_id=rack_id, device_id=device_id)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
@args_required("to_u_start")
def put(self, rack_id, device_id):
to_u_start = request.values.pop("to_u_start")
RackManager().move_device(rack_id, device_id, to_u_start)
return self.jsonify(rack_id=rack_id, device_id=device_id, to_u_start=to_u_start)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def delete(self, rack_id, device_id):
RackManager().remove_device(rack_id, device_id)
return self.jsonify(code=200)
class RackDeviceMigrateView(APIView):
url_prefix = ("/dcim/rack/<int:rack_id>/device/<int:device_id>/migrate",)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
@args_required("to_rack_id")
@args_required("to_u_start")
def put(self, rack_id, device_id):
to_rack_id = request.values.pop("to_rack_id")
to_u_start = request.values.pop("to_u_start")
RackManager().migrate_device(rack_id, device_id, to_rack_id, to_u_start)
return self.jsonify(rack_id=rack_id,
device_id=device_id,
to_u_start=to_u_start,
to_rack_id=to_rack_id)

View File

@@ -0,0 +1,33 @@
# -*- coding:utf-8 -*-
from flask import request
from api.lib.cmdb.dcim.region import RegionManager
from api.lib.common_setting.decorator import perms_role_required
from api.lib.common_setting.role_perm_base import CMDBApp
from api.resource import APIView
app_cli = CMDBApp()
class RegionView(APIView):
url_prefix = ("/dcim/region", "/dcim/region/<int:_id>")
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def post(self):
return self.jsonify(ci_id=RegionManager().add(**request.values))
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def put(self, _id):
RegionManager().update(_id, **request.values)
return self.jsonify(ci_id=_id)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def delete(self, _id):
RegionManager().delete(_id)
return self.jsonify(ci_id=_id)

View File

@@ -0,0 +1,43 @@
# -*- coding:utf-8 -*-
from flask import request
from api.lib.cmdb.dcim.server_room import ServerRoomManager
from api.lib.common_setting.decorator import perms_role_required
from api.lib.common_setting.role_perm_base import CMDBApp
from api.lib.decorator import args_required
from api.resource import APIView
app_cli = CMDBApp()
class ServerRoomView(APIView):
url_prefix = ("/dcim/server_room", "/dcim/server_room/<int:_id>", "/dcim/server_room/<int:_id>/racks")
def get(self, _id):
q = request.values.get('q')
counter, result = ServerRoomManager.get_racks(_id, q)
return self.jsonify(counter=counter, result=result)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
@args_required("parent_id")
def post(self):
parent_id = request.values.pop("parent_id")
return self.jsonify(ci_id=ServerRoomManager().add(parent_id, **request.values))
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def put(self, _id):
ServerRoomManager().update(_id, **request.values)
return self.jsonify(ci_id=_id)
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def delete(self, _id):
ServerRoomManager().delete(_id)
return self.jsonify(ci_id=_id)

View File

@@ -0,0 +1,19 @@
# -*- coding:utf-8 -*-
from api.lib.cmdb.dcim.tree_view import TreeViewManager
from api.lib.common_setting.decorator import perms_role_required
from api.lib.common_setting.role_perm_base import CMDBApp
from api.resource import APIView
app_cli = CMDBApp()
class DCIMTreeView(APIView):
url_prefix = "/dcim/tree_view"
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.DCIM,
app_cli.op.read, app_cli.admin_name)
def get(self):
result, type2name = TreeViewManager.get()
return self.jsonify(result=result, type2name=type2name)

View File

@@ -54,6 +54,66 @@
<div class="content unicode" style="display: block;">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont">&#xea02;</span>
<div class="name">veops-rear</div>
<div class="code-name">&amp;#xea02;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea03;</span>
<div class="name">veops-front</div>
<div class="code-name">&amp;#xea03;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea01;</span>
<div class="name">veops-xianggang</div>
<div class="code-name">&amp;#xea01;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xea00;</span>
<div class="name">veops-device (2)</div>
<div class="code-name">&amp;#xea00;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe9ff;</span>
<div class="name">veops-room (1)</div>
<div class="code-name">&amp;#xe9ff;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe9fe;</span>
<div class="name">veops-IDC</div>
<div class="code-name">&amp;#xe9fe;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe9fd;</span>
<div class="name">veops-region</div>
<div class="code-name">&amp;#xe9fd;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe9fb;</span>
<div class="name">veops-device</div>
<div class="code-name">&amp;#xe9fb;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe9fc;</span>
<div class="name">veops-cabinet</div>
<div class="code-name">&amp;#xe9fc;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe9f9;</span>
<div class="name">veops-data_center</div>
<div class="code-name">&amp;#xe9f9;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe9fa;</span>
<div class="name">ops-setting-holiday_management-copy</div>
@@ -6102,9 +6162,9 @@
<pre><code class="language-css"
>@font-face {
font-family: 'iconfont';
src: url('iconfont.woff2?t=1731312848138') format('woff2'),
url('iconfont.woff?t=1731312848138') format('woff'),
url('iconfont.ttf?t=1731312848138') format('truetype');
src: url('iconfont.woff2?t=1732673294759') format('woff2'),
url('iconfont.woff?t=1732673294759') format('woff'),
url('iconfont.ttf?t=1732673294759') format('truetype');
}
</code></pre>
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@@ -6130,6 +6190,96 @@
<div class="content font-class">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont veops-rear"></span>
<div class="name">
veops-rear
</div>
<div class="code-name">.veops-rear
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-front"></span>
<div class="name">
veops-front
</div>
<div class="code-name">.veops-front
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-xianggang"></span>
<div class="name">
veops-xianggang
</div>
<div class="code-name">.veops-xianggang
</div>
</li>
<li class="dib">
<span class="icon iconfont a-veops-device2"></span>
<div class="name">
veops-device (2)
</div>
<div class="code-name">.a-veops-device2
</div>
</li>
<li class="dib">
<span class="icon iconfont a-veops-room1"></span>
<div class="name">
veops-room (1)
</div>
<div class="code-name">.a-veops-room1
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-IDC"></span>
<div class="name">
veops-IDC
</div>
<div class="code-name">.veops-IDC
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-region"></span>
<div class="name">
veops-region
</div>
<div class="code-name">.veops-region
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-device"></span>
<div class="name">
veops-device
</div>
<div class="code-name">.veops-device
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-cabinet"></span>
<div class="name">
veops-cabinet
</div>
<div class="code-name">.veops-cabinet
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-data_center"></span>
<div class="name">
veops-data_center
</div>
<div class="code-name">.veops-data_center
</div>
</li>
<li class="dib">
<span class="icon iconfont ops-setting-holidays"></span>
<div class="name">
@@ -15202,6 +15352,86 @@
<div class="content symbol">
<ul class="icon_lists dib-box">
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-rear"></use>
</svg>
<div class="name">veops-rear</div>
<div class="code-name">#veops-rear</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-front"></use>
</svg>
<div class="name">veops-front</div>
<div class="code-name">#veops-front</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-xianggang"></use>
</svg>
<div class="name">veops-xianggang</div>
<div class="code-name">#veops-xianggang</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#a-veops-device2"></use>
</svg>
<div class="name">veops-device (2)</div>
<div class="code-name">#a-veops-device2</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#a-veops-room1"></use>
</svg>
<div class="name">veops-room (1)</div>
<div class="code-name">#a-veops-room1</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-IDC"></use>
</svg>
<div class="name">veops-IDC</div>
<div class="code-name">#veops-IDC</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-region"></use>
</svg>
<div class="name">veops-region</div>
<div class="code-name">#veops-region</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-device"></use>
</svg>
<div class="name">veops-device</div>
<div class="code-name">#veops-device</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-cabinet"></use>
</svg>
<div class="name">veops-cabinet</div>
<div class="code-name">#veops-cabinet</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-data_center"></use>
</svg>
<div class="name">veops-data_center</div>
<div class="code-name">#veops-data_center</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#ops-setting-holidays"></use>

View File

@@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 3857903 */
src: url('iconfont.woff2?t=1731312848138') format('woff2'),
url('iconfont.woff?t=1731312848138') format('woff'),
url('iconfont.ttf?t=1731312848138') format('truetype');
src: url('iconfont.woff2?t=1732673294759') format('woff2'),
url('iconfont.woff?t=1732673294759') format('woff'),
url('iconfont.ttf?t=1732673294759') format('truetype');
}
.iconfont {
@@ -13,6 +13,46 @@
-moz-osx-font-smoothing: grayscale;
}
.veops-rear:before {
content: "\ea02";
}
.veops-front:before {
content: "\ea03";
}
.veops-xianggang:before {
content: "\ea01";
}
.a-veops-device2:before {
content: "\ea00";
}
.a-veops-room1:before {
content: "\e9ff";
}
.veops-IDC:before {
content: "\e9fe";
}
.veops-region:before {
content: "\e9fd";
}
.veops-device:before {
content: "\e9fb";
}
.veops-cabinet:before {
content: "\e9fc";
}
.veops-data_center:before {
content: "\e9f9";
}
.ops-setting-holidays:before {
content: "\e9fa";
}

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,76 @@
"css_prefix_text": "",
"description": "",
"glyphs": [
{
"icon_id": "42510712",
"name": "veops-rear",
"font_class": "veops-rear",
"unicode": "ea02",
"unicode_decimal": 59906
},
{
"icon_id": "42510708",
"name": "veops-front",
"font_class": "veops-front",
"unicode": "ea03",
"unicode_decimal": 59907
},
{
"icon_id": "42497603",
"name": "veops-xianggang",
"font_class": "veops-xianggang",
"unicode": "ea01",
"unicode_decimal": 59905
},
{
"icon_id": "42485038",
"name": "veops-device (2)",
"font_class": "a-veops-device2",
"unicode": "ea00",
"unicode_decimal": 59904
},
{
"icon_id": "42455620",
"name": "veops-room (1)",
"font_class": "a-veops-room1",
"unicode": "e9ff",
"unicode_decimal": 59903
},
{
"icon_id": "42455607",
"name": "veops-IDC",
"font_class": "veops-IDC",
"unicode": "e9fe",
"unicode_decimal": 59902
},
{
"icon_id": "42455609",
"name": "veops-region",
"font_class": "veops-region",
"unicode": "e9fd",
"unicode_decimal": 59901
},
{
"icon_id": "42448953",
"name": "veops-device",
"font_class": "veops-device",
"unicode": "e9fb",
"unicode_decimal": 59899
},
{
"icon_id": "42448948",
"name": "veops-cabinet",
"font_class": "veops-cabinet",
"unicode": "e9fc",
"unicode_decimal": 59900
},
{
"icon_id": "42433324",
"name": "veops-data_center",
"font_class": "veops-data_center",
"unicode": "e9f9",
"unicode_decimal": 59897
},
{
"icon_id": "42337844",
"name": "ops-setting-holiday_management-copy",

Binary file not shown.

View File

@@ -139,9 +139,9 @@
:normalizer="
(node) => {
return {
id: String(node[0] || ''),
label: node[1] ? node[1].label || node[0] : node[0],
children: node.children && node.children.length ? node.children : undefined,
id: node.id,
label: node.label,
children: node.children,
}
}
"
@@ -352,8 +352,12 @@ export default {
},
getChoiceValueByProperty(property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find) {
return _find.choice_value
if (_find?.choice_value?.length) {
return _find.choice_value.map((node) => ({
id: String(node?.[0] ?? ''),
label: node?.[1]?.label || node?.[0] || '',
children: node?.children?.length ? node.children : undefined
}))
}
return []
},

View File

@@ -0,0 +1,86 @@
import { axios } from '@/utils/request'
export function getDCIMTreeView(params) {
return axios({
url: '/v0.1/dcim/tree_view ',
method: 'GET',
params
})
}
export function getDCIMById(type, id) {
return axios({
url: `/v0.1/dcim/${type}/${id}`,
method: 'GET'
})
}
export function postDCIM(type, data) {
return axios({
url: `/v0.1/dcim/${type}`,
method: 'POST',
data
})
}
export function putDCIM(type, id, data) {
return axios({
url: `/v0.1/dcim/${type}/${id}`,
method: 'PUT',
data
})
}
export function deleteDCIM(type, id) {
return axios({
url: `/v0.1/dcim/${type}/${id}`,
method: 'DELETE',
})
}
export function getDCIMRacks(id, params) {
return axios({
url: `/v0.1/dcim/server_room/${id}/racks`,
method: 'GET',
params
})
}
export function postDevice(rackId, deviceId, data) {
return axios({
url: `/v0.1/dcim/rack/${rackId}/device/${deviceId}`,
method: 'POST',
data
})
}
export function deleteDevice(rackId, deviceId) {
return axios({
url: `/v0.1/dcim/rack/${rackId}/device/${deviceId}`,
method: 'DELETE'
})
}
export function putDevice(rackId, deviceId, data) {
return axios({
url: `/v0.1/dcim/rack/${rackId}/device/${deviceId}`,
method: 'PUT',
data
})
}
export function migrateDevice(rackId, deviceId, data) {
return axios({
url: `/v0.1/dcim/rack/${rackId}/device/${deviceId}/migrate`,
method: 'PUT',
data
})
}
export function getDCIMHistoryOperate(params) {
return axios({
url: `/v0.1/dcim/history/operate`,
method: 'GET',
params
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 554 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -15,7 +15,7 @@
highlight-hover-row
:checkbox-config="{ reserve: true, highlight: true, range: true }"
:edit-config="{ trigger: 'dblclick', mode: 'row', showIcon: false }"
:sort-config="{ remote: true, trigger: 'cell' }"
:sort-config="sortConfig"
:row-key="true"
:column-key="true"
:cell-style="getCellStyle"
@@ -170,7 +170,7 @@
</template>
</template>
</vxe-table-column>
<vxe-column align="left" field="operate" fixed="right" width="80">
<vxe-column v-if="showOperation" align="left" field="operate" fixed="right" width="80">
<template #header>
<span>{{ $t('operation') }}</span>
</template>
@@ -272,6 +272,18 @@ export default {
data: {
type: Array,
default: () => []
},
sortConfig: {
type: Object,
default: () => ({
remote: true,
trigger: 'cell'
})
},
// 是否展示操作列
showOperation: {
type: Boolean,
default: true
}
},

View File

@@ -25,7 +25,8 @@ const cmdb_en = {
relationType: 'Relation Type',
ad: 'AutoDiscovery',
cidetail: 'CI Detail',
scene: 'Scene'
scene: 'Scene',
dcim: 'DCIM'
},
ciType: {
ciType: 'CIType',
@@ -837,7 +838,63 @@ if __name__ == "__main__":
onlineRatio: 'Online Ratio',
scanEnable: 'Scan Enable',
lastScanTime: 'Last Scan Time',
isSuccess: 'Is Success'
isSuccess: 'Is Success',
batchAssign: 'Batch Assign',
batchAssignInProgress: 'Assign in batches, {total} in total, {successNum} successful, {errorNum} failed',
batchAssignCompleted: 'Batch Assign Completed',
batchRecycle: 'Batch Recycle',
batchRecycleInProgress: 'Recycle in batches, {total} in total, {successNum} successful, {errorNum} failed',
batchRecycleCompleted: 'Batch Recycle Completed',
},
dcim: {
addRegion: 'Add Region',
addIDC: 'Add IDC',
addServerRoom: 'Add Server Room',
addRack: 'Add Rack',
editRegion: 'Edit Region',
editIDC: 'Edit IDC',
editServerRoom: 'Edit Server Room',
editRack: 'Edit Rack',
rackCount: 'Rack Count',
total: 'Total',
deviceCount: 'Device Count',
utilizationRation: 'Utilization Ration',
used: 'Used',
unused: 'Unused',
rackSearchTip: 'Please search for rack name',
viewDetail: 'View Detail',
deleteNode: 'Delete Node',
editNode: 'Edit Node',
roomNullTip: 'Please select the server room on the left first',
unitAbnormal: 'Unit Abnormal',
rack: 'Rack',
unitCount: 'Unit Count',
rackView: 'Rack View',
deviceList: 'Device List',
operationLog: 'Operation Log',
frontView: 'Front View',
rearView: 'Rear View',
addDevice: 'Add Device',
device: 'Device',
ciType: 'CIType',
unitStart: 'Unit Start',
toChange: 'To Change',
abnormalModalTip1: 'and',
abnormalModalTip2: ' location duplication',
abnormalModalTip3: 'Please select one of the devices to change',
remove: 'Remove',
migrate: 'Migrate',
deviceMigrate: 'Device Migrate',
migrationSuccess: 'Migration Success',
removeDeviceTip: 'Confirmed to remove {deviceName} device?',
operationTime: 'Operation Time',
operationUser: 'Operation User',
operationType: 'Operation Type',
deviceType: 'Device Type',
deviceName: 'Device Name',
removeDevice: 'Remove Device',
moveDevice: 'Move Device',
rackDetail: 'Rack Detail'
}
}
export default cmdb_en

View File

@@ -25,7 +25,8 @@ const cmdb_zh = {
relationType: '关系类型',
ad: '自动发现',
cidetail: 'CI 详情',
scene: '场景'
scene: '场景',
dcim: '数据中心'
},
ciType: {
ciType: '模型',
@@ -836,7 +837,63 @@ if __name__ == "__main__":
onlineRatio: '在线率',
scanEnable: '是否扫描',
lastScanTime: '最后扫描时间',
isSuccess: '是否成功'
isSuccess: '是否成功',
batchAssign: '批量分配',
batchAssignInProgress: '正在批量分配,共{total}个,成功{successNum}个,失败{errorNum}个',
batchAssignCompleted: '批量分配已完成',
batchRecycle: '批量回收',
batchRecycleInProgress: '正在批量回收,共{total}个,成功{successNum}个,失败{errorNum}个',
batchRecycleCompleted: '批量回收已完成',
},
dcim: {
addRegion: '新增区域',
addIDC: '新增数据中心',
addServerRoom: '新增机房',
addRack: '添加机柜',
editRegion: '编辑区域',
editIDC: '编辑数据中心',
editServerRoom: '编辑机房',
editRack: '编辑机柜',
rackCount: '机柜数',
total: '总数',
deviceCount: '设备数',
utilizationRation: '利用率',
used: '已使用',
unused: '未使用',
rackSearchTip: '请搜索机柜名称',
viewDetail: '查看详情',
deleteNode: '删除节点',
editNode: '编辑节点',
roomNullTip: '请先选择左侧的机房',
unitAbnormal: 'U位异常',
rack: '机柜',
unitCount: 'U位数',
rackView: '机柜视图',
deviceList: '设备列表',
operationLog: '操作记录',
frontView: '前视图',
rearView: '后视图',
addDevice: '添加设备',
device: '设备',
ciType: '模型',
unitStart: '起始U位',
toChange: '去修改',
abnormalModalTip1: '和',
abnormalModalTip2: ' 位置重复',
abnormalModalTip3: '请选择其中一台设备进行修改',
remove: '移除',
migrate: '迁移',
deviceMigrate: '设备迁移',
migrationSuccess: '迁移成功',
removeDeviceTip: '确认要移除 {deviceName} 设备吗?',
operationTime: '操作时间',
operationUser: '操作人',
operationType: '操作类型',
deviceType: '设备类型',
deviceName: '设备名',
removeDevice: '删除设备',
moveDevice: '移动设备',
rackDetail: '机柜详情'
}
}
export default cmdb_zh

View File

@@ -81,6 +81,12 @@ const genCmdbRoutes = async () => {
name: 'cmdb_ipam',
meta: { title: 'IPAM', appName: 'cmdb', icon: 'veops-ipam', selectedIcon: 'veops-ipam', keepAlive: false, permission: ['admin', 'cmdb_admin'] }
},
{
path: '/cmdb/dcim',
component: () => import('../views/dcim'),
name: 'cmdb_dcim',
meta: { title: 'cmdb.menu.dcim', appName: 'cmdb', icon: 'veops-data_center', selectedIcon: 'veops-data_center', keepAlive: false, permission: ['cmdb_admin', 'admin'] }
},
{
path: '/cmdb/disabled2',
name: 'cmdb_disabled2',

View File

@@ -178,7 +178,7 @@
</a-form-item>
</a-form>
</template>
<a v-if="!isEdit && !attr.is_computed && !attr.sys_computed" @click="handleEdit" :style="{ opacity: 0 }"><a-icon type="edit"/></a>
<a v-if="!isEdit && !attr.is_computed && !attr.sys_computed && showEdit" @click="handleEdit" :style="{ opacity: 0 }"><a-icon type="edit"/></a>
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
</span>
</template>
@@ -204,6 +204,10 @@ export default {
type: Object,
default: () => {},
},
showEdit: {
type: Boolean,
default: true
}
},
data() {
return {

View File

@@ -688,19 +688,19 @@ export default {
font-size: 12px;
color: #A5A9BC;
}
</style>
<style lang="less">
.ops-stripe-table .vxe-body--row.row--stripe.relation-table-divider {
background-color: #b1b8d3 !important;
}
.ops-stripe-table .vxe-body--row.relation-table-parent {
background-color: #f5f8ff !important;
}
.relation-table-divider {
td {
height: 1px !important;
line-height: 1px !important;
.ops-stripe-table {
/deep/ .relation-table-divider {
background-color: #b1b8d3 !important;
td {
height: 2px !important;
line-height: 2px !important;
}
}
/deep/ .relation-table-parent {
background-color: #f5f8ff !important;
}
}
</style>

View File

@@ -0,0 +1,331 @@
<template>
<a-modal
:visible="visible"
:width="700"
:title="$t(modalTitle)"
:confirmLoading="confirmLoading"
@ok="handleOk"
@cancel="handleCancel"
>
<a-form-model
ref="dcimFormRef"
:model="form"
:rules="formRules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
class="dcim-form"
>
<a-form-model-item
v-for="(item) in formList"
:key="item.name"
:label="item.alias || item.name"
:prop="item.name"
>
<CIReferenceAttr
v-if="item.is_reference"
:referenceTypeId="item.reference_type_id"
:isList="item.is_list"
:referenceShowAttrName="item.showAttrName"
:initSelectOption="getInitReferenceSelectOption(item)"
v-model="form[item.name]"
/>
<a-select
v-else-if="item.is_choice"
:mode="item.is_list ? 'multiple' : 'default'"
showSearch
allowClear
v-model="form[item.name]"
>
<a-icon slot="suffixIcon" type="caret-down" />
<a-select-option
v-for="(choiceItem, choiceIndex) in item.selectOption"
:key="choiceIndex"
:value="choiceItem.value"
>
{{ choiceItem.label }}
</a-select-option>
</a-select>
<a-switch
v-else-if="item.is_bool"
v-model="form[item.name]"
/>
<a-input-number
v-model="form[item.name]"
class="dcim-form-input"
v-else-if="(item.value_type === '0' || item.value_type === '1') && !item.is_list"
/>
<a-date-picker
v-else-if="(item.value_type === '4' || item.value_type === '3') && !item.is_list"
v-model="form[item.name]"
class="dcim-form-input"
:format="item.value_type === '4' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
:valueFormat="item.value_type === '4' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
:showTime="item.value_type === '4' ? false : { format: 'HH:mm:ss' }"
/>
<a-input
v-else
:placeholder="$t('placeholder1')"
v-model="form[item.name]"
/>
</a-form-model-item>
</a-form-model>
</a-modal>
</template>
<script>
import _ from 'lodash'
import { postDCIM, putDCIM } from '@/modules/cmdb/api/dcim.js'
import { getCITypes } from '@/modules/cmdb/api/CIType'
import { searchCI } from '@/modules/cmdb/api/ci'
import { DCIM_TYPE } from '../constants'
import CIReferenceAttr from '@/components/ciReferenceAttr/index.vue'
export default {
name: 'DCIMForm',
components: {
CIReferenceAttr
},
props: {
allAttrList: {
type: Object,
default: () => {}
}
},
data() {
return {
visible: false,
nodeId: null,
parentId: null,
dcimType: '',
formList: [],
form: {},
formRules: {},
confirmLoading: false,
}
},
computed: {
modalTitle() {
switch (this.dcimType) {
case DCIM_TYPE.REGION:
return this.nodeId ? 'cmdb.dcim.editRegion' : 'cmdb.dcim.addRegion'
case DCIM_TYPE.IDC:
return this.nodeId ? 'cmdb.dcim.editIDC' : 'cmdb.dcim.addIDC'
case DCIM_TYPE.SERVER_ROOM:
return this.nodeId ? 'cmdb.dcim.editServerRoom' : 'cmdb.dcim.addServerRoom'
case DCIM_TYPE.RACK:
return this.nodeId ? 'cmdb.dcim.editRack' : 'cmdb.dcim.addRack'
default:
return ''
}
}
},
methods: {
async open({
nodeId = null,
parentId = null,
dcimType = ''
}) {
this.nodeId = nodeId
let nodeData = {}
if (nodeId) {
const res = await searchCI({
q: `_id:${nodeId}`,
count: 9999
})
nodeData = res?.result?.[0] || {}
}
this.parentId = parentId
this.dcimType = dcimType
this.visible = true
const form = {}
const formRules = {}
let formList = []
let attrList = _.cloneDeep(this.allAttrList?.[dcimType]?.attributes)
attrList = attrList?.filter?.((attr) => !attr.sys_computed && !attr.is_computed) || []
if (attrList.length) {
attrList.forEach((attr) => {
let value = nodeData?.[attr.name] ?? attr?.default?.default ?? undefined
if (
Array.isArray(value) &&
['0', '1', '2', '9'].includes(attr.value_type)
) {
value = value.join(',')
}
form[attr.name] = value
if (attr?.is_choice) {
const choice_value = attr?.choice_value || []
attr.selectOption = choice_value.map((item) => {
return {
label: item?.[1]?.label || item?.[0] || '',
value: item?.[0]
}
})
}
formList.push(attr)
if (attr.is_required) {
formRules[attr.name] = [
{
required: true, message: attr?.is_choice ? this.$t('placeholder2') : this.$t('placeholder1')
}
]
}
})
}
formList = await this.handleReferenceAttr(formList, form)
this.form = form
this.formList = formList
this.formRules = formRules
},
async handleReferenceAttr(formList, ci) {
const map = {}
formList.forEach((attr) => {
if (attr?.is_reference && attr?.reference_type_id && ci[attr.name]) {
const ids = Array.isArray(ci[attr.name]) ? ci[attr.name] : ci[attr.name] ? [ci[attr.name]] : []
if (ids.length) {
if (!map?.[attr.reference_type_id]) {
map[attr.reference_type_id] = {}
}
ids.forEach((id) => {
map[attr.reference_type_id][id] = {}
})
}
}
})
if (!Object.keys(map).length) {
return formList
}
const ciTypesRes = await getCITypes({
type_ids: Object.keys(map).join(',')
})
const showAttrNameMap = {}
ciTypesRes.ci_types.forEach((ciType) => {
showAttrNameMap[ciType.id] = ciType?.show_name || ciType?.unique_name || ''
})
const allRes = await Promise.all(
Object.keys(map).map((key) => {
return searchCI({
q: `_type:${key},_id:(${Object.keys(map[key]).join(';')})`,
count: 9999
})
})
)
const ciNameMap = {}
allRes.forEach((res) => {
res.result.forEach((item) => {
ciNameMap[item._id] = item
})
})
formList.forEach((attr) => {
if (attr?.is_reference && attr?.reference_type_id) {
attr.showAttrName = showAttrNameMap?.[attr?.reference_type_id] || ''
const referenceShowAttrNameMap = {}
const referenceCIIds = ci[attr.name];
(Array.isArray(referenceCIIds) ? referenceCIIds : referenceCIIds ? [referenceCIIds] : []).forEach((id) => {
referenceShowAttrNameMap[id] = ciNameMap?.[id]?.[attr.showAttrName] ?? id
})
attr.referenceShowAttrNameMap = referenceShowAttrNameMap
}
})
return formList
},
handleCancel() {
this.visible = false
this.nodeId = null
this.parentId = null
this.dcimType = ''
this.form = {}
this.formRules = {}
this.formList = []
this.confirmLoading = false
this.$refs.dcimFormRef.clearValidate()
},
handleOk() {
this.$refs.dcimFormRef.validate(async (valid) => {
if (!valid) {
return
}
this.confirmLoading = true
try {
if (this.nodeId) {
await putDCIM(
this.dcimType,
this.nodeId,
{
...this.form,
parent_id: Number(this.parentId)
}
)
} else {
await postDCIM(
this.dcimType,
{
...this.form,
parent_id: Number(this.parentId)
}
)
}
this.$emit('ok', {
dcimType: this.dcimType,
editType: this.nodeId ? 'edit' : 'create'
})
this.handleCancel()
} catch (error) {
console.log('submit fail', error)
}
this.confirmLoading = false
})
},
getInitReferenceSelectOption(attr) {
const option = Object.keys(attr?.referenceShowAttrNameMap || {}).map((key) => {
return {
key: Number(key),
title: attr?.referenceShowAttrNameMap?.[Number(key)] ?? ''
}
})
return option
}
}
}
</script>
<style lang="less" scoped>
.dcim-form {
padding-right: 12px;
max-height: 400px;
overflow-y: auto;
overflow-x: hidden;
&-input {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,184 @@
<template>
<div class="dcim-stats">
<div
v-for="(item, index) in statsList"
:key="index"
class="dcim-stats-card"
>
<div class="dcim-stats-card-left">
<div class="dcim-stat-card-title">{{ $t(item.title) }}</div>
<div class="dcim-stats-card-row">
<div
v-for="(data, dataIndex) in item.countList"
:key="dataIndex"
class="dcim-stats-card-count"
>
<span class="dcim-stats-card-count-label">{{ $t(data.label) }}:</span>
<span class="dcim-stats-card-count-value">{{ data.value }}</span>
</div>
</div>
</div>
<div
v-if="item.icon"
class="dcim-stats-card-icon"
>
<ops-icon
:type="item.icon"
/>
</div>
<DCIMStatsChart
v-else-if="item.chartData"
:chartData="item.chartData"
:chartRatio="item.chartRatio"
/>
</div>
</div>
</template>
<script>
import DCIMStatsChart from './dcimStatsChart.vue'
export default {
name: 'DCIMStats',
props: {
statsData: {
type: Object,
default: () => {}
}
},
components: {
DCIMStatsChart
},
computed: {
statsList() {
const {
device_count = 0,
rack_count = 0,
u_count = 0,
u_used_count = 0,
} = this.statsData || {}
return [
{
title: 'cmdb.dcim.rackCount',
icon: 'veops-cabinet',
countList: [
{
label: 'cmdb.dcim.total',
value: rack_count
}
]
},
{
title: 'cmdb.dcim.deviceCount',
icon: 'veops-device',
countList: [
{
label: 'cmdb.dcim.total',
value: device_count
}
]
},
{
title: 'cmdb.dcim.utilizationRation',
countList: [
{
label: 'cmdb.dcim.used',
value: `${u_used_count}u`
},
{
label: 'cmdb.dcim.unused',
value: `${u_count - u_used_count}u`
}
],
chartRatio: u_used_count > 0 && u_count > 0 ? Math.round((u_used_count / u_count) * 100) : 0,
chartData: [
{
label: 'cmdb.dcim.used',
value: u_used_count,
color: '#009FA9'
},
{
label: 'cmdb.dcim.unused',
value: u_count - u_used_count,
color: '#17D4B0'
}
]
},
]
}
}
}
</script>
<style lang="less" scoped>
.dcim-stats {
width: 100%;
display: flex;
align-items: stretch;
column-gap: 16px;
flex-shrink: 0;
&-card {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 24px;
background-color: #F7F8FA;
filter: drop-shadow(0px 0px 12px rgba(231, 236, 239, 0.10));
&-left {
width: 100%;
margin-right: 12px;
}
&-title {
font-size: 14px;
font-weight: 400;
color: #4E5969;
}
&-row {
display: flex;
flex-wrap: wrap;
margin-top: 12px;
column-gap: 12px;
}
&-count {
flex-shrink: 0;
display: flex;
align-items: baseline;
&-label {
font-size: 14px;
font-weight: 400;
color: #1D2129;
}
&-value {
font-size: 16px;
font-weight: 700;
color: #1D2129;
margin-left: 6px;
}
}
&-icon {
width: 52px;
height: 52px;
border-radius: 52px;
background-color: #FFFFFF;
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
flex-shrink: 0;
}
}
}
</style>

View File

@@ -0,0 +1,110 @@
<template>
<div class="stats-chart">
<div
class="stats-chart-pie"
ref="statsChartRef"
></div>
<div class="stats-chart-ratio">
{{ chartRatio }}%
</div>
</div>
</template>
<script>
import * as echarts from 'echarts'
export default {
name: 'DCIMStatsChart',
props: {
chartRatio: {
type: Number,
default: 0
},
chartData: {
type: Array,
default: () => []
}
},
watch: {
chartData: {
deep: true,
immediate: true,
handler(data) {
this.updateChart(data)
}
}
},
beforeDestroy() {
if (this.chart) {
this.chart.dispose()
this.chart = null
}
},
methods: {
updateChart(data) {
const option = {
color: data?.map?.((item) => item.color) || [],
tooltip: {
show: false
},
legend: {
show: false,
},
series: [
{
type: 'pie',
radius: ['60%', '85%'],
data: data?.map((item) => {
return {
name: this.$t(item?.label),
value: item?.value || 0
}
}) || [],
itemStyle: {
borderColor: '#fff',
borderWidth: 1
},
label: {
show: false,
},
}
]
}
this.$nextTick(() => {
if (!this.chart) {
const el = this.$refs.statsChartRef
this.chart = echarts.init(el)
}
this.chart.setOption(option)
})
}
}
}
</script>
<style lang="less" scoped>
.stats-chart {
width: 60px;
height: 60px;
position: relative;
flex-shrink: 0;
&-pie {
width: 100%;
height: 100%;
}
&-ratio {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 2;
font-size: 14px;
font-weight: 700;
color: #1D2129;
}
}
</style>

View File

@@ -0,0 +1,376 @@
<template>
<div class="dcim-main" ref="rackMainRef">
<div v-if="!roomId" class="dcim-main-null">
<img class="dcim-main-null-img" :src="require(`@/modules/cmdb/assets/dcim/dcim_null.png`)"></img>
<div class="dcim-main-null-tip">{{ $t('noData') }}</div>
<div class="dcim-main-null-tip2">{{ $t('cmdb.dcim.roomNullTip') }}</div>
</div>
<template v-else>
<DCIMStats :statsData="statsData" />
<div class="dcim-main-row">
<div class="dcim-main-filter">
<a-input-search
v-model="searchValue"
:placeholder="$t('cmdb.dcim.rackSearchTip')"
class="dcim-main-row-search"
/>
<a-select
class="dcim-main-row-select"
:getPopupContainer="(trigger) => trigger.parentElement"
v-model="currentRackType"
>
<a-icon slot="suffixIcon" type="caret-down" />
<a-select-option
v-for="(item) in rackTypeSelectOption"
:key="item.value"
:value="item.value"
:class="item.value === 'unitAbnormal' ? 'dcim-main-row-select-unitAbnormal' : ''"
>
{{ item.label }}
</a-select-option>
</a-select>
</div>
<div class="dcim-main-row-right">
<div class="dcim-main-layout">
<div
v-for="(item) in layoutList"
:key="item.value"
:class="['dcim-main-layout-item', currentLayout === item.value ?'dcim-main-layout-item-active' : '']"
@click="handleChangeLayout(item.value)"
>
<ops-icon :type="item.icon" />
</div>
</div>
<a-button
type="primary"
class="ops-button-ghost"
ghost
@click="addRack"
>
<a-icon type="plus-circle" />
{{ $t('cmdb.dcim.addRack') }}
</a-button>
</div>
</div>
<div
class="rack-wrap"
>
<RackGrid
v-if="currentLayout === 'grid'"
:rackList="filterRackList"
@openRackDetail="openRackDetail"
/>
<RackTable
v-if="currentLayout === 'table'"
:rackList="filterRackList"
:columns="columns"
:preferenceAttrList="preferenceAttrList"
:CITypeId="rackCITYpe.id"
/>
</div>
<RackDetail
ref="rackDetailRef"
:roomId="roomId"
:rackCITYpe="rackCITYpe"
:rackList="rackList"
@openForm="(data) => $emit('openForm', data)"
@refreshRackList="getRackList"
/>
</template>
</div>
</template>
<script>
import _ from 'lodash'
import { DCIM_TYPE } from '../../constants.js'
import { getDCIMRacks } from '@/modules/cmdb/api/dcim.js'
import { getCITableColumns } from '@/modules/cmdb/utils/helper'
import DCIMStats from './dcimStats.vue'
import RackGrid from './rackGrid.vue'
import RackTable from './rackTable.vue'
import RackDetail from '../rackDetail/index.vue'
export default {
name: 'DCIMMain',
components: {
DCIMStats,
RackGrid,
RackTable,
RackDetail
},
props: {
roomId: {
type: String,
default: ''
},
attrObj: {
type: Object,
default: () => {}
},
rackCITYpe: {
type: Object,
default: () => {}
},
preferenceAttrList: {
type: Array,
default: () => []
}
},
data() {
return {
searchValue: '',
currentRackType: 'all',
rackList: [],
columns: [],
statsData: {},
currentLayout: 'grid',
layoutList: [
{
value: 'grid',
icon: 'veops-map_view'
},
{
value: 'table',
icon: 'monitor-list_view'
}
]
}
},
computed: {
rackTypeSelectOption() {
const selectOption = [
{
value: 'all',
label: this.$t('all')
}
]
const rackTypeAttr = this.attrObj?.attributes?.find?.((item) => item.name === 'rack_type')
if (rackTypeAttr?.choice_value?.length) {
rackTypeAttr.choice_value.map((item) => {
selectOption.push({
value: item?.[0] || '',
label: item?.[1]?.label || item?.[0] || ''
})
})
}
selectOption.push({
value: 'unitAbnormal',
label: this.$t('cmdb.dcim.unitAbnormal')
})
return selectOption
},
filterRackList() {
let rackList = _.cloneDeep(this.rackList)
if (this.searchValue) {
rackList = rackList.filter((item) => item.name.indexOf(this.searchValue) !== -1)
}
if (this.currentRackType !== 'all') {
if (this.currentRackType === 'unitAbnormal') {
rackList = rackList.filter((item) => item.u_slot_abnormal)
} else {
rackList = rackList.filter((item) => item.rack_type === this.currentRackType)
}
}
return rackList
}
},
provide() {
return {
getRackList: this.getRackList,
handleSearch: this.getRackList,
attrList: () => {
return this?.attrObj?.attributes || []
},
attributes: () => {
return this.attrObj
}
}
},
watch: {
roomId: {
immediate: true,
deep: true,
handler(id) {
if (id) {
this.initData()
} else {
this.rackList = []
this.statsData = {}
}
}
}
},
methods: {
async initData() {
try {
await this.getRackList()
} catch (error) {
console.log('initData error', error)
}
},
async getRackList() {
const res = await getDCIMRacks(this.roomId)
const rackList = res?.result || []
const jsonAttrList = this.preferenceAttrList.filter((attr) => attr.value_type === '6')
rackList.forEach((item) => {
item.free_u_count = item.free_u_count ?? 0
item.u_count = item.u_count ?? 0
item.u_used_count = item.u_count - item.free_u_count
item.u_used_ratio = item.u_used_count > 0 && item.u_count > 0 ? Math.round((item.u_used_count / item.u_count) * 100) : 0
jsonAttrList.forEach(
(jsonAttr) => (item[jsonAttr.name] = item[jsonAttr.name] ? JSON.stringify(item[jsonAttr.name]) : '')
)
})
this.getColumns(rackList)
this.rackList = rackList
this.statsData = res?.counter || {}
},
getColumns(data) {
const width = this.$refs.rackMainRef.clientWidth - 50
const columns = getCITableColumns(data, this.preferenceAttrList, width)
columns.forEach((item) => {
if (item.editRender) {
item.editRender.enabled = false
}
})
this.columns = columns
},
handleChangeLayout(value) {
if (this.currentLayout !== value) {
this.currentLayout = value
}
},
addRack() {
this.$emit('openForm', {
dcimType: DCIM_TYPE.RACK,
parentId: this.roomId
})
},
openRackDetail(data) {
this.$refs.rackDetailRef.open(data._id)
}
}
}
</script>
<style lang="less" scoped>
.dcim-main {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
&-null {
width: 100%;
padding-top: 95px;
text-align: center;
&-img {
height: 200px;
}
&-tip {
font-size: 14px;
font-weight: 400;
color: #86909C;
}
&-tip2 {
font-size: 14px;
font-weight: 400;
color: #2F54EB;
}
}
&-row {
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
margin-top: 20px;
&-search {
width: 300px;
}
&-select {
width: 120px;
margin-left: 22px;
flex-shrink: 0;
/deep/ &-unitAbnormal {
border-top: dashed 1px #e8e8e8;
}
}
&-right {
display: flex;
align-items: center;
column-gap: 21px;
}
}
&-layout {
display: flex;
align-items: center;
height: 32px;
border: solid 1px #E4E7ED;
&-item {
height: 100%;
width: 32px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
cursor: pointer;
&:not(:last-child) {
border-right: solid 1px #E4E7ED;
}
&-active {
color: #2F54EB;
background-color: #F0F5FF;
}
&:hover {
color: #2F54EB;
}
}
}
.rack-wrap {
margin-top: 22px;
margin-bottom: 22px;
height: 100%;
overflow: hidden;
}
}
</style>

View File

@@ -0,0 +1,326 @@
<template>
<div class="rack-grid">
<template v-if="rackList.length">
<div
v-for="(item, index) in rackList"
:key="index"
class="rack-grid-item"
>
<div
v-if="item.u_slot_abnormal"
class="rack-grid-item-warning"
>
<a-icon
type="warning"
theme="filled"
class="rack-grid-item-warning-icon"
/>
<span
class="rack-grid-item-warning-text"
>
{{ $t('cmdb.dcim.unitAbnormal') }}
</span>
</div>
<div class="rack-grid-item-header">
<a-tooltip :title="item.name">
<div class="rack-grid-item-name">
{{ item.name }}
</div>
</a-tooltip>
<div class="rack-grid-item-store">
{{ `${item.u_count || 0}U` }}
</div>
</div>
<img
class="rack-grid-item-img"
:src="require(`@/modules/cmdb/assets/dcim/rack.png`)"
/>
<div class="rack-grid-item-data">
<ops-icon
type="a-veops-device2"
class="rack-grid-item-data-icon"
/>
<span class="rack-grid-item-data-value">
{{ item.u_used_count }}/{{ item.u_count }}
</span>
<div class="rack-grid-item-data-progress">
<div
class="rack-grid-item-data-progress-line"
:style="{
width: item.u_used_ratio + '%'
}"
></div>
<div
class="rack-grid-item-data-progress-end"
:style="{
left: item.u_used_ratio + '%'
}"
></div>
</div>
</div>
<a
class="rack-grid-item-btn"
@click="openRackDetail(item)"
>
<span class="rack-grid-item-btn-text">{{ $t('cmdb.dcim.viewDetail') }}</span>
<a-icon type="right" class="rack-grid-item-btn-icon" />
</a>
</div>
</template>
<div v-else class="rack-grid-null">
<img class="rack-grid-null-img" :src="require(`@/assets/data_empty.png`)"></img>
<div class="rack-grid-null-text">{{ $t('noData') }}</div>
</div>
</div>
</template>
<script>
export default {
name: 'RackGrid',
props: {
rackList: {
type: Array,
default: () => []
}
},
methods: {
openRackDetail(data) {
this.$emit('openRackDetail', data)
}
}
}
</script>
<style lang="less" scoped>
.rack-grid {
display: flex;
flex-wrap: wrap;
column-gap: 27px;
row-gap: 27px;
overflow-y: auto;
overflow-x: hidden;
max-height: 100%;
padding-bottom: 57px;
&-item {
width: 205px;
height: 219px;
flex-shrink: 0;
background-color: #F9FBFF;
border-radius: 4px;
overflow: hidden;
position: relative;
cursor: pointer;
text-align: center;
transition: all 0.1s;
&-warning {
display: flex;
align-items: center;
padding: 2px 6px;
background-color: #FFDEBF;
border-radius: 2px;
width: max-content;
position: absolute;
top: 30px;
left: 50%;
transform: translateX(-50%);
&-icon {
font-size: 12px;
color: #FF7D00;
margin-right: 2.5px;
}
&-text {
font-size: 12px;
font-weight: 400;
color: #FF7D00;
}
&::after {
content: '';
position: absolute;
bottom: -6px;
left: 50%;
margin-left: -7px;
width: 0;
height: 0;
border-left: 7px solid transparent;
border-right: 7px solid transparent;
border-top: 6px solid #FFDEBF;
}
}
&-header {
width: 100%;
height: 25px;
background-color: #8FB9F712;
display: flex;
align-items: center;
justify-content: space-between;
}
&-name {
height: 25px;
line-height: 25px;
border-bottom-right-radius: 25px;
padding-left: 7px;
padding-right: 17px;
background-color: #4E5969;
font-size: 14px;
font-weight: 700;
color: #FFFFFF;
overflow: hidden;
text-overflow: ellipsis;
text-wrap: nowrap;
}
&-store {
padding-right: 10px;
font-size: 14px;
font-weight: 400;
color: #2F54EB;
margin-left: 12px;
}
&-img {
height: 112px;
margin-top: 16px;
transition: all 0.1s;
}
&-data {
margin-top: 16px;
display: flex;
align-items: center;
justify-content: center;
&-icon {
font-size: 16px;
}
&-value {
margin-left: 5px;
font-size: 14px;
font-weight: 400;
color: #4E5969;
}
&-progress {
margin-left: 6px;
width: 97px;
height: 2px;
border-radius: 2px;
background-color: #C3D0EB;
position: relative;
&-line {
height: 2px;
border-radius: 2px;
background-color: #7F97FA;
}
&-end {
position: absolute;
top: 50%;
margin-top: -8px;
margin-left: -8px;
height: 16px;
width: 16px;
border-radius: 16px;
background-color: #3044F112;
&::after {
content: '';
position: absolute;
z-index: 2;
top: 50%;
left: 50%;
margin-top: -3px;
margin-left: -3px;
background-color: #2F54EB;
width: 6px;
height: 6px;
border-radius: 6px;
}
}
}
}
&-btn {
position: absolute;
right: 17px;
bottom: 10px;
align-items: center;
display: none;
&-text {
margin-right: 2px;
font-size: 12px;
font-weight: 400;
color: #3F75FF;
}
&-icon {
font-size: 12px;
color: #3F75FF;
}
}
&:hover {
background-color: #FFFFFF;
box-shadow: 0px 22px 33px 0px rgba(41, 65, 126, 0.25);
z-index: 2;
.rack-grid-item-name {
background-color: #2F54EB;
}
.rack-grid-item-img {
margin-top: 7px;
height: 128px;
}
.rack-grid-item-data {
margin-top: 9px;
&-icon {
display: none;
}
&-progress {
width: 112px;
}
}
.rack-grid-item-btn {
display: flex;
}
}
}
&-null {
padding-top: 150px;
text-align: center;
width: 100%;
&-img {
width: 150px;
}
&-text {
margin-top: 12px;
}
}
}
</style>

View File

@@ -0,0 +1,52 @@
<template>
<div class="rack-table">
<CITable
ref="xTable"
:attrList="preferenceAttrList"
:columns="columns"
:data="rackList"
:height="tableHeight"
:sortConfig="{ remote: false, trigger: 'default' }"
:showCheckbox="false"
:showOperation="false"
/>
</div>
</template>
<script>
import { mapState } from 'vuex'
import CITable from '@/modules/cmdb/components/ciTable/index.vue'
export default {
name: 'RackTable',
components: {
CITable
},
props: {
rackList: {
type: Array,
default: () => []
},
columns: {
type: Array,
default: () => []
},
preferenceAttrList: {
type: Array,
default: () => {}
}
},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
}),
tableHeight() {
return `${this.windowHeight - 295}px`
},
}
}
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,383 @@
<template>
<div class="dcim-tree">
<div class="dcim-tree-header">
<a-input
v-model="searchValue"
class="dcim-tree-header-search"
:placeholder="$t('placeholder1')"
/>
<a-dropdown>
<a-button class="dcim-tree-header-more">
<ops-icon type="veops-more" />
</a-button>
<a-menu slot="overlay">
<a-menu-item
v-for="(type) in rootAction"
:key="type"
@click="openForm({
dcimType: type
})"
>
<a>
<a-icon
type="plus-circle"
class="dcim-tree-header-add-icon"
/>
{{ $t(addActionTitle[type]) }}
</a>
</a-menu-item>
</a-menu>
</a-dropdown>
</div>
<div class="dcim-tree-main">
<a-tree
v-if="treeData.length"
autoExpandParent
:treeData="filterTreeData"
:selectedKeys="treeKey ? [treeKey] : []"
:defaultExpandedKeys="treeKey ? [treeKey] : []"
>
<template #title="treeNodeData">
<div
class="dcim-tree-node"
@click="clickTreeNode(treeNodeData)"
>
<ops-icon
:type="treeNodeData.icon"
class="dcim-tree-node-icon"
:style="{ color: treeNodeData.iconColor }"
/>
<a-tooltip :title="treeNodeData.title">
<span
class="dcim-tree-node-title"
:style="{
color: treeKey === treeNodeData.key ? '#2F54EB' : ''
}"
>
{{ treeNodeData.title }}
</span>
</a-tooltip>
<div class="dcim-tree-node-right">
<span
v-if="treeNodeData.count"
class="dcim-tree-node-count"
>
{{ treeNodeData.count }}
</span>
<a-dropdown>
<a class="dcim-tree-node-action">
<ops-icon type="veops-more" />
</a>
<a-menu slot="overlay">
<a-menu-item
v-if="treeNodeData.addType"
@click="openForm({
dcimType: treeNodeData.addType,
parentId: treeNodeData._id
})"
>
<a-icon type="plus-circle" />
{{ $t(addActionTitle[treeNodeData.addType]) }}
</a-menu-item>
<a-menu-item
@click="openDetail(treeNodeData)"
>
<a-icon type="unordered-list" />
{{ $t('cmdb.dcim.viewDetail') }}
</a-menu-item>
<a-menu-item
@click="openForm({
dcimType: treeNodeData.dcimType,
parentId: treeNodeData.parentId,
nodeId: treeNodeData._id
})"
>
<ops-icon type="veops-edit" />
{{ $t('cmdb.dcim.editNode') }}
</a-menu-item>
<a-menu-item @click="deleteNode(treeNodeData)">
<ops-icon type="veops-delete" />
{{ $t('cmdb.dcim.deleteNode') }}
</a-menu-item>
</a-menu>
</a-dropdown>
</div>
</div>
</template>
</a-tree>
</div>
<CIDetailDrawer
ref="CIdetailRef"
:typeId="viewDetailCITypeId"
/>
</div>
</template>
<script>
import _ from 'lodash'
import { DCIM_TYPE, DCIM_TYPE_NAME_MAP } from '../constants.js'
import { deleteDCIM } from '@/modules/cmdb/api/dcim.js'
import CIDetailDrawer from '@/modules/cmdb/views/ci/modules/ciDetailDrawer.vue'
export default {
name: 'DCIMTree',
components: {
CIDetailDrawer
},
props: {
treeData: {
type: Array,
default: () => []
},
treeKey: {
type: [String, Number],
default: ''
}
},
data() {
return {
searchValue: '',
addActionTitle: {
[DCIM_TYPE.REGION]: 'cmdb.dcim.addRegion',
[DCIM_TYPE.IDC]: 'cmdb.dcim.addIDC',
[DCIM_TYPE.SERVER_ROOM]: 'cmdb.dcim.addServerRoom',
},
rootAction: [
DCIM_TYPE.REGION,
DCIM_TYPE.IDC
],
viewDetailCITypeId: 0,
viewDetailAttrObj: {}
}
},
computed: {
filterTreeData() {
if (this.searchValue) {
const treeData = _.cloneDeep(this.treeData)
// 过滤筛选
const filterTreeData = treeData.filter((data) => {
return this.handleTreeDataBySearch(data)
})
// 处理同级父节点
const newTreeData = []
treeData.forEach((item) => {
const filterNodeData = filterTreeData.find((data) => data.key === item.key)
if (filterNodeData) {
newTreeData.push(filterNodeData)
} else if (
filterTreeData.some((data) => data.parentId === item.key)
) {
newTreeData.push(item)
}
})
return newTreeData
}
return this.treeData
}
},
inject: ['getTreeData'],
provide() {
return {
handleSearch: this.refreshTreeData,
attrList: () => {
return this.viewDetailAttrObj?.attributes || []
},
attributes: () => {
return this.viewDetailAttrObj
}
}
},
methods: {
handleTreeDataBySearch(data) {
const isMatch = data?.title?.indexOf?.(this.searchValue) !== -1
if (!data?.children?.length) {
return isMatch ? data : null
}
data.children = data.children.filter((data) => {
return this.handleTreeDataBySearch(data)
})
return isMatch || data.children.length ? data : null
},
openForm({
dcimType,
nodeId = undefined,
parentId = ''
}) {
this.$emit('openForm', {
dcimType,
nodeId,
parentId
})
},
deleteNode(node) {
this.$confirm({
title: this.$t('warning'),
content: this.$t('confirmDelete'),
onOk: async () => {
await deleteDCIM(node.dcimType, node._id)
if (node.key === this.treeKey) {
this.$emit('updateTreeKey', '')
}
this.$nextTick(() => {
this.refreshTreeData()
})
},
})
},
refreshTreeData() {
this.getTreeData()
},
clickTreeNode(node) {
if (node.dcimType === DCIM_TYPE.SERVER_ROOM) {
this.$emit('updateTreeKey', node.key)
}
},
async openDetail(node) {
this.$emit('getAttrList', DCIM_TYPE_NAME_MAP[node.dcimType], node.dcimType, (allAttrList) => {
this.viewDetailCITypeId = node._type
this.viewDetailAttrObj = allAttrList[node.dcimType]
this.$nextTick(() => {
this.$refs.CIdetailRef.create(node._id)
})
})
}
}
}
</script>
<style lang="less" scoped>
.dcim-tree {
&-header {
display: flex;
align-items: center;
column-gap: 14px;
&-search {
width: 100%;
}
&-more {
flex-shrink: 0;
width: 32px;
padding: 0px;
}
&-add-icon {
margin-right: 6px;
}
}
&-main {
width: 100%;
height: 100%;
/deep/ .ant-tree {
.ant-tree-node-content-wrapper {
width: calc(100% - 24px);
padding: 0px;
display: inline-block;
height: fit-content;
.ant-tree-title {
display: inline-block;
width: 100%;
padding: 0 6px;
}
}
.ipam-tree-node_hide_expand {
.ant-tree-switcher {
display: none;
}
.ant-tree-node-content-wrapper {
width: 100%;
}
}
.ant-tree-switcher-icon {
color: #CACDD9;
}
}
}
&-node {
display: flex;
align-items: center;
height: 32px;
cursor: pointer;
&-icon {
font-size: 12px;
flex-shrink: 0;
}
&-title {
margin-left: 6px;
font-size: 14px;
font-weight: 400;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
text-wrap: nowrap;
}
&-right {
margin-left: auto;
display: flex;
align-items: center;
flex-shrink: 0;
}
&-count {
font-size: 10px;
font-weight: 400;
color: #A5A9BC;
}
&-action {
display: none;
margin-left: 3px;
font-size: 12px;
&:hover {
color: #2F54EB;
}
/deep/ .ant-dropdown-menu {
padding: 4px 0;
}
/deep/ .ant-dropdown-menu-item {
padding: 5px 12px;
}
}
&:hover {
.dcim-tree-node-action {
display: inline-block;
}
}
}
}
</style>

View File

@@ -0,0 +1,228 @@
<template>
<div
ref="deviceListRef"
class="device-list"
>
<div class="device-list-tabs">
<div
v-for="(item) in tabs"
:key="item.id"
:class="[
'device-list-tabs-item',
item.id === tabActive ? 'device-list-tabs-item_active' : ''
]"
@click="clickTab(item.id)"
>
<CIIcon :icon="item.icon" />
<span class="device-list-tabs-item-name" >{{ item.alias || item.name }}</span>
</div>
</div>
<CITable
ref="xTable"
:attrList="preferenceAttrList"
:columns="columns"
:data="deviceList"
:height="tableHeight"
:showCheckbox="false"
:showDelete="false"
:sortConfig="{ remote: false, trigger: 'default' }"
@openDetail="openDetail"
/>
<CIDetailDrawer
v-if="tabActive"
ref="CIdetailRef"
:typeId="tabActive"
/>
</div>
</template>
<script>
import _ from 'lodash'
import { mapState } from 'vuex'
import { getSubscribeAttributes } from '@/modules/cmdb/api/preference'
import { getCITableColumns } from '@/modules/cmdb/utils/helper'
import CIIcon from '@/modules/cmdb/components/ciIcon/index.vue'
import CITable from '@/modules/cmdb/components/ciTable/index.vue'
import CIDetailDrawer from '@/modules/cmdb/views/ci/modules/ciDetailDrawer.vue'
export default {
name: 'DeviceList',
components: {
CIIcon,
CITable,
CIDetailDrawer
},
props: {
allDeviceList: {
type: Array,
default: () => []
},
CITypeRelations: {
type: Array,
default: () => []
}
},
data() {
return {
tabActive: '',
tabs: [],
preferenceAttrList: [],
deviceList: [],
columns: [],
deviceCIType: {}
}
},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
}),
tableHeight() {
return `${this.windowHeight - 210}px`
},
},
inject: [
'getDeviceList',
'getRackList'
],
provide() {
return {
handleSearch: this.refreshData,
attrList: () => {
return this?.deviceCIType?.attributes || []
},
attributes: () => {
return {
attributes: this?.deviceCIType?.attributes || [],
unique_id: this?.deviceCIType?.unique_id || 0,
unique: this?.deviceCIType?.show_key || ''
}
}
}
},
watch: {
allDeviceList: {
immediate: true,
deep: true,
handler() {
this.initData()
}
}
},
methods: {
initData() {
const tabs = []
this.allDeviceList.forEach((item) => {
const CIType = this.CITypeRelations.find((CIType) => CIType.id === item._type)
tabs.push({
icon: CIType.icon,
name: item.ci_type,
alias: item.ci_type_alias,
id: item._type
})
})
this.clickTab(tabs?.[0]?.id ?? '')
this.tabs = _.uniqBy(tabs, 'id')
},
clickTab(id) {
if (id !== this.tabActive) {
this.tabActive = id
if (this.tabActive) {
this.initTableData()
} else {
this.columns = []
this.deviceList = []
}
}
},
async initTableData() {
const subscribed = await getSubscribeAttributes(this.tabActive)
this.preferenceAttrList = subscribed.attributes
const deviceList = this.allDeviceList.filter((item) => item._type === this.tabActive)
const deviceCIType = this.CITypeRelations.find((item) => item.id === this.tabActive)
this.deviceCIType = deviceCIType || {}
this.getColumns(deviceList)
this.deviceList = deviceList
},
getColumns(data) {
const width = this.$refs.deviceListRef.clientWidth - 50
const columns = getCITableColumns(data, this.preferenceAttrList, width)
columns.forEach((item) => {
if (item.editRender) {
item.editRender.enabled = false
}
})
this.columns = columns
},
refreshData() {
this.getDeviceList()
this.getRackList()
},
openDetail(id, activeTabKey, ciDetailRelationKey) {
this.$refs.CIdetailRef.create(id, activeTabKey, ciDetailRelationKey)
},
}
}
</script>
<style lang="less" scoped>
.device-list {
width: 100%;
&-tabs {
display: flex;
flex-wrap: wrap;
column-gap: 9px;
row-gap: 5px;
margin-bottom: 18px;
&-item {
flex-shrink: 0;
display: flex;
align-items: center;
cursor: pointer;
padding: 4px 12px;
background-color: #F7F8FA;
border-radius: 1px;
border: solid 1px transparent;
max-width: 100%;
&-name {
margin-left: 4px;
font-size: 12px;
font-weight: 400;
color: #1D2129;
text-overflow: ellipsis;
overflow: hidden;
text-wrap: nowrap;
}
&_active {
border-color: #B1C9FF;
background-color: #F9FBFF;
}
&:hover {
.device-list-tabs-item-name {
color: #3F75FF;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,327 @@
<template>
<CustomDrawer
width="825px"
:visible="visible"
:bodyStyle="{ height: '100vh', padding: '0px' }"
:hasTitle="false"
destroyOnClose
@close="handleClose"
>
<div class="rack-detail">
<div class="rack-header">
<div class="rack-header-left">
<div class="rack-header-name">
<span class="rack-header-name-label">
{{ $t('cmdb.dcim.rack') }}
</span>
<a-tooltip :title="rackData.name">
<span class="rack-header-name-value">
{{ rackData.name }}
</span>
</a-tooltip>
</div>
<ops-icon
type="veops-edit"
class="rack-header-edit"
@click="clickEdit"
/>
<ops-icon
type="veops-delete"
class="rack-header-delete"
@click="clickDelete"
/>
</div>
<div class="rack-header-right">
<div
v-for="(item, index) in countList"
:key="index"
class="rack-header-count"
>
<span class="rack-header-count-name">{{ $t(item.name) }}:</span>
<span class="rack-header-count-value">{{ item.value }}</span>
</div>
</div>
</div>
<a-tabs
class="rack-detail-tabs"
v-model="tabActive"
>
<a-tab-pane
key="rackView"
:tab="$t('cmdb.dcim.rackView')"
>
<RackView
:CITypeRelations="CITypeRelations"
:rackData="rackData"
:deviceList="deviceList"
:rackList="rackList"
/>
</a-tab-pane>
<a-tab-pane
key="rackDetail"
:tab="$t('cmdb.dcim.rackDetail')"
>
<RackGroupAttr
:ci="rackData"
:rackCITYpeId="rackCITYpe.id"
/>
</a-tab-pane>
<a-tab-pane
key="deviceList"
:tab="$t('cmdb.dcim.deviceList')"
>
<DeviceList
:allDeviceList="deviceList"
:CITypeRelations="CITypeRelations"
/>
</a-tab-pane>
<a-tab-pane
key="operationLog"
:tab="$t('cmdb.dcim.operationLog')"
>
<OperationLog
v-if="tabActive === 'operationLog'"
:rackId="rackId"
/>
</a-tab-pane>
</a-tabs>
</div>
</CustomDrawer>
</template>
<script>
import { DCIM_TYPE } from '../../constants.js'
import { deleteDCIM } from '@/modules/cmdb/api/dcim.js'
import { getCITypeChildren } from '@/modules/cmdb/api/CITypeRelation'
import { searchCIRelation } from '@/modules/cmdb/api/CIRelation'
import RackView from './rackView/index.vue'
import RackGroupAttr from './rackGroupAttr/index.vue'
import DeviceList from './deviceList/index.vue'
import OperationLog from './operationLog/index.vue'
export default {
name: 'RackDetail',
components: {
RackView,
RackGroupAttr,
DeviceList,
OperationLog
},
props: {
roomId: {
type: String,
default: ''
},
rackCITYpe: {
type: Object,
default: () => {}
},
rackList: {
type: Array,
default: () => []
}
},
data() {
return {
visible: false,
rackId: 0,
tabActive: 'rackView',
CITypeRelations: [],
deviceList: [],
}
},
computed: {
rackData() {
return this.rackList.find((item) => item._id === this.rackId) || {}
},
countList() {
const {
u_count = 0,
u_used_ratio = 0,
u_slot_abnormal = false
} = this.rackData
return [
{
name: 'cmdb.dcim.deviceCount',
value: this.deviceList?.length || 0
},
{
name: 'cmdb.dcim.unitCount',
value: u_count
},
{
name: 'cmdb.dcim.unitAbnormal',
value: u_slot_abnormal ? this.$t('yes') : this.$t('no')
},
{
name: 'cmdb.dcim.utilizationRation',
value: `${u_used_ratio}%`
}
]
}
},
inject: [
'getTreeData',
'getRackList'
],
provide() {
return {
getDeviceList: this.getDeviceList
}
},
methods: {
async open(rackId) {
this.rackId = rackId
this.visible = true
if (!this.CITypeRelations.length) {
const res = await getCITypeChildren(this.rackCITYpe.id)
this.CITypeRelations = res?.children || []
}
await this.getDeviceList()
},
async getDeviceList() {
if (!this.rackId) {
return
}
const res = await searchCIRelation(`root_id=${this.rackId}&level=1&count=10000`)
const deviceList = res?.result || []
deviceList.sort((a, b) => a.u_start - b.u_start)
this.deviceList = deviceList
},
handleClose() {
this.rackId = 0
this.tabActive = 'rackView'
this.visible = false
},
clickEdit() {
this.$emit('openForm', {
dcimType: DCIM_TYPE.RACK,
parentId: this.roomId,
nodeId: this.rackId
})
this.handleClose()
},
clickDelete() {
this.$confirm({
title: this.$t('warning'),
content: this.$t('confirmDelete'),
onOk: () => {
deleteDCIM(DCIM_TYPE.RACK, this.rackId).then(() => {
this.$message.success(this.$t('deleteSuccess'))
this.handleClose()
this.getRackList()
this.getTreeData()
})
},
})
},
refreshRackList() {
this.$emit('refreshRackList')
}
}
}
</script>
<style lang="less" scoped>
.rack-detail {
.rack-header {
height: 44px;
padding: 0px 20px;
display: flex;
align-items: center;
justify-content: space-between;
background-color: #F7F8FA;
&-left {
display: flex;
align-items: center;
width: 100%;
overflow: hidden;
}
&-name {
display: flex;
align-items: center;
font-size: 16px;
font-weight: 900;
color: #1D2129;
max-width: calc(100% - 48px);
&-label {
flex-shrink: 0;
}
&-value {
color: #2F54EB;
margin-left: 2px;
overflow: hidden;
text-overflow: ellipsis;
text-wrap: nowrap;
}
}
&-edit {
margin-left: 8px;
font-size: 12px;
}
&-delete {
margin-left: 12px;
font-size: 12px;
color: #FD4C6A;
}
&-right {
display: flex;
align-items: center;
column-gap: 30px;
flex-shrink: 0;
margin-left: 12px;
}
&-count {
display: flex;
align-items: center;
&-name {
font-size: 12px;
font-weight: 400;
color: #4E5969;
}
&-value {
font-size: 14px;
font-weight: 700;
color: #1D2129;
margin-left: 5px;
}
}
}
&-tabs {
margin-left: 19px;
margin-right: 19px;
/deep/ .ant-tabs-bar {
display: inline-block;
}
}
}
</style>

View File

@@ -0,0 +1,195 @@
<template>
<div class="operation-log">
<ops-table
ref="xTable"
size="small"
show-overflow
show-header-overflow
highlight-hover-row
:data="tableData"
:height="tableHeight"
:sort-config="{ remote: true }"
@sort-change="handleSortChange"
>
<vxe-table-column
:title="$t('cmdb.dcim.operationTime')"
field="created_at"
sortable
></vxe-table-column>
<vxe-table-column
:title="$t('cmdb.dcim.operationUser')"
field="operationUser"
></vxe-table-column>
<vxe-table-column
:title="$t('cmdb.dcim.operationType')"
field="operate_type"
>
<template #default="{ row }">
<div
class="operation-log-device-type"
:style="{
backgroundColor: row.deviceTypeData.backgroundColor,
color: row.deviceTypeData.textColor
}"
>
{{ $t(row.deviceTypeData.name) }}
</div>
</template>
</vxe-table-column>
<vxe-table-column
:title="$t('cmdb.dcim.deviceType')"
field="deviceType"
></vxe-table-column>
<vxe-table-column
:title="$t('cmdb.dcim.deviceName')"
field="deviceName"
></vxe-table-column>
</ops-table>
<div class="operation-log-pagination">
<a-pagination
:showSizeChanger="true"
:current="page"
size="small"
:total="totalNumber"
show-quick-jumper
:page-size="pageSize"
:page-size-options="pageSizeOptions"
:show-total="
(total, range) =>
$t('pagination.total', {
range0: range[0],
range1: range[1],
total,
})
"
@change="handleChangePage"
@showSizeChange="onShowSizeChange"
>
<template slot="buildOptionText" slot-scope="props">
<span v-if="props.value !== '100000'">{{ props.value }}{{ $t('itemsPerPage') }}</span>
<span v-if="props.value === '100000'">{{ $t('cmdb.ci.all') }}</span>
</template>
</a-pagination>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
import { getDCIMHistoryOperate } from '@/modules/cmdb/api/dcim.js'
export default {
name: 'OperationLog',
props: {
rackId: {
type: Number,
default: 0
}
},
data() {
return {
page: 1,
pageSize: 50,
pageSizeOptions: ['50', '100', '200'],
totalNumber: 0,
tableData: [],
getTableDataParams: {
reverse: 1
},
deviceTypeMap: {
0: {
textColor: '#00B42A',
backgroundColor: '#F6FFED',
name: 'cmdb.dcim.addDevice'
},
1: {
textColor: '#FD4C6A',
backgroundColor: '#FFECE8',
name: 'cmdb.dcim.removeDevice'
},
2: {
textColor: '#FF7D00',
backgroundColor: '#FFECCF',
name: 'cmdb.dcim.moveDevice'
}
}
}
},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
allEmployees: (state) => state.user.allEmployees,
}),
tableHeight() {
return `${this.windowHeight - 187}px`
},
},
mounted() {
this.getTableData()
},
methods: {
async getTableData() {
const res = await getDCIMHistoryOperate({
rack_id: this.rackId,
count: this.pageSize,
page: this.page,
...this.getTableDataParams
})
const tableData = res?.result || []
tableData.forEach((item) => {
const ci = res?.id2ci?.[item?.ci_id] || {}
const showKey = res?.type2show_key?.[ci?._type] || ''
const user = this.allEmployees.find((emp) => item.uid === emp.acl_uid)
item.operationUser = user?.nickname || ''
item.deviceType = ci?.ci_type_alias || ''
item.deviceName = ci?.[showKey] || item?.ci_id || ''
item.deviceTypeData = this.deviceTypeMap?.[item?.operate_type] || {}
})
this.tableData = tableData
this.totalNumber = res?.numfound || 0
},
handleChangePage(page) {
this.page = page
this.getTableData()
},
onShowSizeChange(_, pageSize) {
this.page = 1
this.pageSize = pageSize
this.getTableData()
},
handleSortChange(data) {
if (data?.order === 'asc') {
this.getTableDataParams.reverse = 0
} else {
this.getTableDataParams.reverse = 1
}
this.page = 1
this.getTableData()
}
}
}
</script>
<style lang="less" scoped>
.operation-log {
&-device-type {
font-size: 12px;
font-weight: 400;
line-height: 22px;
height: 22px;
padding: 0 9px;
border-radius: 1px;
display: inline-block;
}
&-pagination {
text-align: right;
margin-top: 4px;
}
}
</style>

View File

@@ -0,0 +1,151 @@
<template>
<div class="rack-group-attr">
<el-descriptions
v-for="group in attributeGroups"
class="rack-group-attr-desc"
:title="group.name || $t('other')"
:key="group.name"
border
:column="3"
>
<el-descriptions-item
v-for="attr in group.attributes"
:label="`${attr.alias || attr.name}`"
:key="attr.name"
>
<ci-detail-attr-content
:ci="ci"
:attr="attr"
:attributeGroups="attributeGroups"
:showEdit="false"
/>
</el-descriptions-item>
</el-descriptions>
</div>
</template>
<script>
import _ from 'lodash'
import { getCITypeGroupById, getCITypes } from '@/modules/cmdb/api/CIType'
import { searchCI } from '@/modules/cmdb/api/ci'
import { Descriptions, DescriptionsItem } from 'element-ui'
import CiDetailAttrContent from '@/modules/cmdb/views/ci/modules/ciDetailAttrContent.vue'
export default {
name: 'RackGroupAttr',
components: {
ElDescriptions: Descriptions,
ElDescriptionsItem: DescriptionsItem,
CiDetailAttrContent
},
props: {
ci: {
type: Object,
default: () => {}
},
rackCITYpeId: {
type: Number,
default: 0
}
},
data() {
return {
attributeGroups: []
}
},
mounted() {
this.getAttributes()
},
methods: {
getAttributes() {
getCITypeGroupById(this.rackCITYpeId, { need_other: 1 })
.then((res) => {
this.attributeGroups = res
this.handleReferenceAttr()
})
.catch((e) => {})
},
async handleReferenceAttr() {
const map = {}
this.attributeGroups.forEach((group) => {
group.attributes.forEach((attr) => {
if (attr?.is_reference && attr?.reference_type_id && this.ci[attr.name]) {
const ids = Array.isArray(this.ci[attr.name]) ? this.ci[attr.name] : this.ci[attr.name] ? [this.ci[attr.name]] : []
if (ids.length) {
if (!map?.[attr.reference_type_id]) {
map[attr.reference_type_id] = {}
}
ids.forEach((id) => {
map[attr.reference_type_id][id] = {}
})
}
}
})
})
if (!Object.keys(map).length) {
return
}
const ciTypesRes = await getCITypes({
type_ids: Object.keys(map).join(',')
})
const showAttrNameMap = {}
ciTypesRes.ci_types.forEach((ciType) => {
showAttrNameMap[ciType.id] = ciType?.show_name || ciType?.unique_name || ''
})
const allRes = await Promise.all(
Object.keys(map).map((key) => {
return searchCI({
q: `_type:${key},_id:(${Object.keys(map[key]).join(';')})`,
count: 9999
})
})
)
const ciNameMap = {}
allRes.forEach((res) => {
res.result.forEach((item) => {
ciNameMap[item._id] = item
})
})
const newAttrGroups = _.cloneDeep(this.attributeGroups)
newAttrGroups.forEach((group) => {
group.attributes.forEach((attr) => {
if (attr?.is_reference && attr?.reference_type_id) {
attr.showAttrName = showAttrNameMap?.[attr?.reference_type_id] || ''
const referenceShowAttrNameMap = {}
const referenceCIIds = this.ci[attr.name];
(Array.isArray(referenceCIIds) ? referenceCIIds : referenceCIIds ? [referenceCIIds] : []).forEach((id) => {
referenceShowAttrNameMap[id] = ciNameMap?.[id]?.[attr.showAttrName] ?? id
})
attr.referenceShowAttrNameMap = referenceShowAttrNameMap
}
})
})
this.$set(this, 'attributeGroups', newAttrGroups)
}
}
}
</script>
<style lang="less" scoped>
.rack-group-attr {
width: 100%;
height: 100%;
overflow-y: auto;
overflow-x: hidden;
&-desc {
margin-bottom: 25px;
}
}
</style>

View File

@@ -0,0 +1,129 @@
<template>
<a-modal
:visible="visible"
:okText="$t('cmdb.dcim.toChange')"
:width="350"
@ok="handleOk"
@cancel="handleCancel"
>
<div class="abnormal-modal-title">
<a-icon
type="info-circle"
theme="filled"
class="abnormal-modal-title-icon"
/>
<span class="abnormal-modal-title-text">
{{ $t('cmdb.dcim.unitAbnormal') }}
</span>
</div>
<div class="abnormal-modal-content">
<div class="abnormal-modal-content-row">
<span
v-for="(item, index) in abnormalList"
:key="item.id"
>
{{ item.CITypeName }}
<span class="abnormal-modal-content-name" >
{{ item.name }}
</span>
<span
v-if="index !== abnormalList.length - 1"
>
{{ $t('cmdb.dcim.abnormalModalTip1') }}
</span>
</span>
<span>{{ $t('cmdb.dcim.abnormalModalTip2') }}</span>
</div>
<div class="abnormal-modal-content-row">
{{ $t('cmdb.dcim.abnormalModalTip3') }}
</div>
</div>
<a-radio-group
v-model="currentSelect"
>
<a-radio
v-for="(item) in abnormalList"
:value="item.id"
:key="item.id"
>
{{ item.name }}
</a-radio>
</a-radio-group>
</a-modal>
</template>
<script>
export default {
name: 'AbnormalModal',
data() {
return {
visible: false,
abnormalList: [],
currentSelect: undefined,
}
},
methods: {
open(data) {
this.visible = true
const abnormalList = [data]
if (data?.abnormalList?.length) {
abnormalList.push(...data.abnormalList)
}
this.abnormalList = abnormalList
this.currentSelect = abnormalList?.[0]?.id ?? undefined
},
handleCancel() {
this.currentSelect = undefined
this.abnormalList = []
this.visible = false
},
handleOk() {
if (!this.currentSelect) {
return
}
const device = this.abnormalList.find((item) => item.id === this.currentSelect)
this.$emit('ok', device)
this.handleCancel()
}
}
}
</script>
<style lang="less" scoped >
.abnormal-modal-title {
display: flex;
align-items: center;
&-icon {
font-size: 18px;
color: #FF7D00;
}
&-text {
margin-left: 8px;
font-size: 16px;
font-weight: 700;
color: #1D2129;
}
}
.abnormal-modal-content {
font-size: 14px;
font-weight: 400;
line-height: 22px;
margin: 9px 0px;
color: #1D2129;
&-name {
color: #2F54EB;
}
}
</style>

View File

@@ -0,0 +1,199 @@
<template>
<div class="device-select">
<a-input-search
@search="handleSearch"
/>
<a-radio-group
v-if="CIList.length"
:value="currentSelect"
class="device-select-group"
@change="handleCIChange"
>
<a-radio
v-for="(item) in CIList"
:key="item.value"
:value="item.value"
class="device-select-item"
>
<a-tooltip :title="item.name" placement="topLeft">
{{ item.name }}
</a-tooltip>
</a-radio>
</a-radio-group>
<div v-else class="device-select-null">
<img class="device-select-null-img" :src="require(`@/assets/data_empty.png`)"></img>
<div class="device-select-null-text">{{ $t('noData') }}</div>
</div>
<div class="device-select-pagination">
<a-pagination
:showSizeChanger="true"
:current="page"
size="small"
:total="totalNumber"
show-quick-jumper
:page-size="pageSize"
:page-size-options="pageSizeOptions"
:show-total="
(total, range) =>
$t('pagination.total', {
range0: range[0],
range1: range[1],
total,
})
"
@change="handleChangePage"
@showSizeChange="onShowSizeChange"
>
<template slot="buildOptionText" slot-scope="props">
<span v-if="props.value !== '100000'">{{ props.value }}{{ $t('itemsPerPage') }}</span>
<span v-if="props.value === '100000'">{{ $t('cmdb.ci.all') }}</span>
</template>
</a-pagination>
</div>
</div>
</template>
<script>
import { searchCI } from '@/modules/cmdb/api/ci'
export default {
name: 'DeviceSelect',
props: {
currentSelect: {
type: [Number, undefined],
default: undefined
},
CITypeId: {
type: [Number, undefined],
default: undefined
},
currentCITYpe: {
type: Object,
default: () => {}
}
},
data() {
return {
page: 1,
pageSize: 20,
pageSizeOptions: ['20', '50', '100'],
totalNumber: 0,
CIList: [],
searchValue: ''
}
},
watch: {
CITypeId: {
immediate: true,
deep: true,
handler(newVal, oldVal) {
this.page = 1
this.searchValue = ''
if (newVal && newVal !== oldVal) {
this.getCIList()
} else {
this.CIList = []
this.totalNumber = 0
}
}
}
},
methods: {
async getCIList() {
const res = await searchCI({
q: `_type:${this.CITypeId}${this.searchValue ? `,*${this.searchValue}*` : ''}`,
count: this.pageSize,
page: this.page
})
let CIList = res?.result || []
if (CIList.length) {
CIList = CIList.map((item) => {
return {
value: item?._id,
name: item?.[this?.currentCITYpe?.show_key] || item?._id || '',
unitCount: item?.u_count ?? 0
}
})
}
this.CIList = CIList
this.totalNumber = res?.numfound || 0
},
handleSearch(value) {
this.searchValue = value
this.page = 1
this.getCIList()
},
handleChangePage(page) {
this.page = page
this.getCIList()
},
onShowSizeChange(_, pageSize) {
this.page = 1
this.pageSize = pageSize
this.getCIList()
},
handleCIChange(e) {
const value = e.target.value
const findCI = this.CIList.find((item) => item.value === value)
this.$emit('change', findCI)
}
}
}
</script>
<style lang="less" scoped>
.device-select {
width: 650px;
&-group {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
row-gap: 20px;
margin: 12px 0px;
max-height: 40vh;
overflow-y: auto;
overflow-x: hidden;
}
&-item {
width: 48%;
flex-shrink: 0;
overflow: hidden;
text-overflow: ellipsis;
text-wrap: nowrap;
}
&-null {
margin: 30px 0px;
text-align: center;
width: 100%;
&-img {
width: 130px;
}
&-text {
margin-top: 12px;
}
}
&-pagination {
text-align: right;
margin-top: 4px;
}
}
</style>

View File

@@ -0,0 +1,243 @@
<template>
<a-modal
:visible="visible"
:width="500"
:title="$t('cmdb.dcim.addDevice')"
@ok="handleOk"
@cancel="handleCancel"
>
<a-form-model
ref="deviceFormRef"
:model="form"
:rules="formRules"
:label-col="{ span: 5 }"
:wrapper-col="{ span: 19 }"
class="device-form"
>
<a-form-model-item
:label="$t('cmdb.dcim.ciType')"
prop="CITypeId"
>
<a-select
v-model="form.CITypeId"
showSearch
allowClear
optionFilterProp="title"
@change="handleCITypeChange"
>
<a-select-option
v-for="(item) in CITypeRelations"
:key="item.id"
:value="item.id"
:title="item.alias || item.name"
>
{{ item.alias || item.name }}
</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item
:label="$t('cmdb.dcim.device')"
prop="deviceId"
>
<a-popover trigger="click" placement="bottom">
<DeviceSelect
slot="content"
:CITypeId="form.CITypeId"
:currentCITYpe="currentCITYpe"
:currentSelect="form.deviceId"
@change="handleDeviceChange"
/>
<div
class="device-form-select"
>
{{ deviceName }}
</div>
</a-popover>
</a-form-model-item>
<a-form-model-item
:label="$t('cmdb.dcim.unitStart')"
prop="unitStart"
>
<a-input-number
v-model="form.unitStart"
:min="1"
:precision="0"
class="device-form-input"
/>
</a-form-model-item>
<a-form-model-item
v-if="showUnitCount"
:label="$t('cmdb.dcim.unitCount')"
prop="unitCount"
>
<a-input-number
v-model="form.unitCount"
:min="1"
:precision="0"
class="device-form-input"
/>
</a-form-model-item>
</a-form-model>
</a-modal>
</template>
<script>
import { postDevice } from '@/modules/cmdb/api/dcim.js'
import DeviceSelect from './deviceSelect.vue'
export default {
name: 'DeviceForm',
components: {
DeviceSelect
},
props: {
CITypeRelations: {
type: Array,
default: () => []
},
rackId: {
type: Number,
default: 0
}
},
data() {
return {
visible: false,
form: {
CITypeId: undefined,
deviceId: undefined,
unitStart: undefined,
unitCount: undefined
},
deviceName: '',
showUnitCount: true,
formRules: {
CITypeId: [
{
required: true, message: this.$t('placeholder2')
}
],
deviceId: [
{
required: true, message: this.$t('placeholder2')
}
],
unitStart: [
{
required: true, message: this.$t('placeholder1')
}
],
unitCount: [
{
required: true, message: this.$t('placeholder1')
}
]
}
}
},
computed: {
currentCITYpe() {
return this.CITypeRelations.find((CIType) => CIType?.id === this.form.CITypeId) || {}
}
},
methods: {
open(deviceData) {
this.visible = true
if (deviceData) {
this.form = {
CITypeId: deviceData?.CITypeId ?? undefined,
deviceId: deviceData?.deviceId ?? undefined,
unitStart: deviceData?.unitStart ?? undefined,
unitCount: deviceData?.unitCount ?? undefined,
}
if (this.form.unitCount) {
this.showUnitCount = false
}
this.deviceName = deviceData?.name || ''
}
},
handleCancel() {
this.form = {
CITypeId: undefined,
deviceId: undefined,
unitStart: undefined,
unitCount: undefined
}
this.deviceName = ''
this.showUnitCount = true
this.$refs.deviceFormRef.clearValidate()
this.visible = false
},
handleOk() {
this.$refs.deviceFormRef.validate(async (valid) => {
if (!valid) {
return
}
await postDevice(
this.rackId,
this.form.deviceId,
{
u_start: this.form.unitStart,
u_count: this.form.unitCount
}
)
this.handleCancel()
this.$message.success(this.$t('addSuccess'))
this.$emit('ok')
})
},
handleDeviceChange({
name,
value,
unitCount
}) {
this.form.deviceId = value
this.deviceName = name
this.form.unitCount = unitCount || undefined
this.showUnitCount = !unitCount
},
handleCITypeChange() {
this.form.deviceId = undefined
this.deviceName = ''
this.showUnitCount = true
this.form.unitCount = undefined
}
}
}
</script>
<style lang="less" scoped>
.device-form {
&-select {
border: 1px solid #e4e7ed;
border-radius: 2px;
line-height: 32px;
min-height: 32px;
padding: 0 12px;
cursor: pointer;
&:hover {
border-color: #597ef7;
}
}
&-input {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,296 @@
<template>
<div v-if="unitList.length" class="rack-view">
<div class="rack-view-col">
<RackUnitView
viewType="front"
:countList="countList"
:unitList="unitList"
:rackId="rackData._id"
@migrateDevice="migrateDevice"
@openDeviceForm="openDeviceForm"
@draggable="handleDraggable"
@refreshRackAllData="refreshRackAllData"
@openDeviceDetail="openDeviceDetail"
/>
</div>
<div class="rack-view-col">
<RackUnitView
viewType="rear"
:countList="countList"
:unitList="unitList"
:rackId="rackData._id"
@migrateDevice="migrateDevice"
@openDeviceForm="openDeviceForm"
@draggable="handleDraggable"
@refreshRackAllData="refreshRackAllData"
@openDeviceDetail="openDeviceDetail"
/>
</div>
<DeviceForm
ref="deviceFormRef"
:CITypeRelations="CITypeRelations"
:rackId="rackData._id"
@ok="refreshRackAllData"
/>
<MigrateModal
ref="migrateModalRef"
:rackList="rackList"
@ok="refreshRackAllData"
/>
<CIDetailDrawer
ref="CIdetailRef"
:typeId="deviceCITypeId"
/>
</div>
</template>
<script>
import _ from 'lodash'
import { v4 as uuidv4 } from 'uuid'
import { putDevice } from '@/modules/cmdb/api/dcim.js'
import { DEVICE_CITYPE_NAME } from '../../../constants.js'
import RackUnitView from './rackUnitView.vue'
import DeviceForm from './deviceForm/index.vue'
import MigrateModal from './migrateModal.vue'
import CIDetailDrawer from '@/modules/cmdb/views/ci/modules/ciDetailDrawer.vue'
export default {
name: 'RackView',
components: {
RackUnitView,
DeviceForm,
MigrateModal,
CIDetailDrawer
},
props: {
CITypeRelations: {
type: Array,
default: () => []
},
rackData: {
type: Object,
default: () => {}
},
deviceList: {
type: Array,
default: () => []
},
rackList: {
type: Array,
default: () => []
}
},
data() {
return {
unitList: [],
countList: [],
deviceAttrList: [],
deviceCITypeId: 0,
}
},
inject: [
'getRackList',
'getDeviceList'
],
provide() {
return {
handleSearch: this.refreshRackAllData,
attrList: () => {
return this.deviceAttrList
},
attributes: () => {
return {
attributes: this.deviceAttrList
}
}
}
},
watch: {
deviceList: {
immediate: true,
deep: true,
handler(deviceList) {
this.initData(deviceList)
}
}
},
methods: {
async initData(deviceList) {
const CITypeMap = this.CITypeRelations.reduce((map, cur) => {
map[cur.id] = cur
return map
}, {})
const _deviceList = _.cloneDeep(deviceList)
// 建立设备map, 并处理U位异常情况
const deviceMap = {}
_deviceList.forEach((device, index) => {
const CITYpe = CITypeMap?.[device?._type] || {}
device.deviceImage = this.getDeviceViewImage(CITYpe?.name)
device.name = device?.[CITYpe?.show_key] || device._id || ''
device.icon = CITYpe?.icon || ''
device.CITypeName = CITYpe?.alias || CITYpe?.name || ''
device.id = device._id
if (index > 0) {
const abnormalDevice = _deviceList.slice(0, index).find((item) => {
const unitCount = item.abnormal ? item.abnormalUnitcount : item.u_count
return item.u_start <= device.u_start && device.u_start <= (item.u_start + unitCount - 1)
})
if (abnormalDevice) {
abnormalDevice.abnormal = true
const endCount = Math.max(abnormalDevice.u_start + abnormalDevice.u_count, device.u_start + device.u_count)
abnormalDevice.abnormalUnitcount = endCount - abnormalDevice.u_start
if (abnormalDevice?.abnormalList?.length) {
abnormalDevice.abnormalList.push(device)
} else {
abnormalDevice.abnormalList = [device]
}
} else {
deviceMap[device.u_start] = device
}
} else {
deviceMap[device.u_start] = device
}
})
let unitIndex = 1
const unitList = []
while (unitIndex <= this.rackData.u_count) {
if (deviceMap[unitIndex]) {
const device = deviceMap[unitIndex]
const unitCount = device?.abnormal ? device.abnormalUnitcount : device.u_count
unitList.push({
...device,
unitCount,
type: 'device',
key: uuidv4(),
abnormal: device?.abnormal ?? false,
abnormalList: device.abnormalList
})
unitIndex += unitCount
device.assign = true
} else {
unitList.push({
type: 'gap',
unitCount: 1,
key: uuidv4()
})
unitIndex += 1
}
}
this.unitList = _.reverse(unitList)
this.countList = Array.from({ length: this.rackData.u_count }, (_, i) => this.rackData.u_count - i)
},
getDeviceViewImage(name) {
const image = {
front: require('@/modules/cmdb/assets/dcim/device/server_front.png'),
rear: require('@/modules/cmdb/assets/dcim/device/server_rear.png')
}
switch (name) {
case DEVICE_CITYPE_NAME.ROUTER:
image.front = require('@/modules/cmdb/assets/dcim/device/router_front.png')
image.rear = require('@/modules/cmdb/assets/dcim/device/router_rear.png')
break
case DEVICE_CITYPE_NAME.FIRE_WALL:
image.front = require('@/modules/cmdb/assets/dcim/device/firewall_front.png')
image.rear = require('@/modules/cmdb/assets/dcim/device/firewall_rear.png')
break
case DEVICE_CITYPE_NAME.SERVER:
image.front = require('@/modules/cmdb/assets/dcim/device/server_front.png')
image.rear = require('@/modules/cmdb/assets/dcim/device/server_rear.png')
break
case DEVICE_CITYPE_NAME.RAID:
image.front = require('@/modules/cmdb/assets/dcim/device/raid_front.png')
image.rear = require('@/modules/cmdb/assets/dcim/device/raid_rear.png')
break
case DEVICE_CITYPE_NAME.SWITCH:
case DEVICE_CITYPE_NAME.FC_SWITCH:
case DEVICE_CITYPE_NAME.F5:
image.front = require('@/modules/cmdb/assets/dcim/device/switch_front.png')
image.rear = require('@/modules/cmdb/assets/dcim/device/switch_rear.png')
break
default:
break
}
return image
},
openDeviceForm(deviceData) {
this.$refs.deviceFormRef.open(deviceData)
},
handleDraggable({
startUnit,
deviceId,
oldUnitList
}) {
putDevice(
this.rackData._id,
deviceId,
{
to_u_start: startUnit
}
).then(() => {
this.getDeviceList()
}).catch((error) => {
console.log('putDevice fail', error)
this.unitList = oldUnitList
})
},
migrateDevice(deviceId) {
this.$refs.migrateModalRef.open({
deviceId,
rackId: this.rackData._id
})
},
refreshRackAllData() {
this.getRackList()
this.getDeviceList()
},
async openDeviceDetail(data) {
const deviceCIType = this.CITypeRelations.find((item) => item.id === data._type)
this.deviceAttrList = deviceCIType?.attributes || []
this.deviceCITypeId = data?._type
this.$nextTick(() => {
this.$refs.CIdetailRef.create(data._id)
})
}
}
}
</script>
<style lang="less" scoped>
.rack-view {
display: flex;
overflow-y: auto;
overflow-x: hidden;
max-height: calc(100vh - 160px);
&-col {
width: 50%;
}
}
</style>

View File

@@ -0,0 +1,136 @@
<template>
<a-modal
:title="$t('cmdb.dcim.deviceMigrate')"
:visible="visible"
:width="500"
@ok="handleOk"
@cancel="handleCancel"
>
<a-form-model
ref="deviceMigrateFormRef"
:model="form"
:rules="formRules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
class="device-migrate"
>
<a-form-model-item
:label="$t('cmdb.dcim.rack')"
prop="to_rack_id"
>
<a-select
v-model="form.to_rack_id"
showSearch
allowClear
optionFilterProp="title"
>
<a-select-option
v-for="(rack) in rackList"
:key="rack._id"
:value="rack._id"
:title="rack.name"
>
{{ rack.name }}
</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item
:label="$t('cmdb.dcim.unitStart')"
prop="to_u_start"
>
<a-input-number
v-model="form.to_u_start"
:min="1"
:precision="0"
class="device-migrate-input"
/>
</a-form-model-item>
</a-form-model>
</a-modal>
</template>
<script>
import { migrateDevice } from '@/modules/cmdb/api/dcim.js'
export default {
name: 'MigrateModal',
props: {
rackList: {
type: Array,
default: () => []
}
},
data() {
return {
visible: false,
form: {
to_rack_id: undefined,
to_u_start: undefined,
},
formRules: {
to_rack_id: [
{
required: true, message: this.$t('placeholder2')
}
],
to_u_start: [
{
required: true, message: this.$t('placeholder1')
}
]
},
deviceId: '',
rackId: ''
}
},
methods: {
open(data) {
this.visible = true
this.deviceId = data?.deviceId || ''
this.rackId = data?.rackId || ''
},
handleCancel() {
this.deviceId = ''
this.rackId = ''
this.form = {
to_rack_id: undefined,
to_u_start: undefined,
}
this.$refs.deviceMigrateFormRef.clearValidate()
this.visible = false
},
handleOk() {
this.$refs.deviceMigrateFormRef.validate(async (valid) => {
if (!valid) {
return
}
migrateDevice(
this.rackId,
this.deviceId,
{
...this.form
}
).then(() => {
this.$message.success(this.$t('cmdb.dcim.migrationSuccess'))
this.handleCancel()
this.$emit('ok')
})
})
}
}
}
</script>
<style lang="less" scoped>
.device-migrate {
&-input {
width: 100%;
}
}
</style>

View File

@@ -0,0 +1,109 @@
<template>
<div class="rack-header">
<div class="rack-header-part-1">
<div
class="rack-header-part-1-line"
:style="{
backgroundColor: viewType === 'front' ? '#A4FFF8' : '#FFFFFF'
}"
></div>
</div>
<div
class="rack-header-part-2"
:style="{
padding: viewType === 'front' ? '0 8px' : '0 22px'
}"
>
<div
v-if="viewType === 'front'"
class="rack-header-part-2-left"
>
<RackHeaderCircle/>
<RackHeaderCircle/>
<RackHeaderCircle/>
</div>
<div class="rack-header-part-2-right">
<div
v-for="(item) in 300"
:key="item"
class="rack-header-part-2-right-item"
>
</div>
</div>
</div>
</div>
</template>
<script>
import RackHeaderCircle from './rackHeaderCircle.vue'
export default {
name: 'RackHeader',
components: {
RackHeaderCircle
},
props: {
viewType: {
type: String,
default: 'front'
}
}
}
</script>
<style lang="less" scoped>
.rack-header {
width: 100%;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
overflow: hidden;
&-part-1 {
height: 5px;
width: 100%;
background-color: #3D4151;
padding-top: 3px;
&-line {
width: 100%;
height: 0.5px;
}
}
&-part-2 {
height: 21px;
width: 100%;
background-color: #86909C;
border-bottom: solid 1px #FFFFFF;
display: flex;
align-items: center;
padding: 0 8px;
&-left {
display: flex;
align-items: center;
column-gap: 5px;
margin-right: 7px;
flex-shrink: 0;
}
&-right {
display: flex;
flex-wrap: wrap;
gap: 2px;
width: 100%;
height: 10px;
overflow: hidden;
&-item {
width: 1px;
height: 1px;
background-color: #C8CDD2;
flex-shrink: 0;
}
}
}
}
</style>

View File

@@ -0,0 +1,66 @@
<template>
<div class="circle-container">
<div class="circle shadow-1"></div>
<div class="circle shadow-2"></div>
<div class="circle shadow-3"></div>
<div class="circle inner-circle"></div>
<div class="circle inner-shadow"></div>
</div>
</template>
<script>
export default {
name: 'RackHeaderCircle'
}
</script>
<style lang="less" scoped>
.circle-container {
position: relative;
width: 6px;
height: 6px;
.circle {
position: absolute;
border-radius: 50%;
background-color: #20e757;
width: 6px;
height: 6px;
top: 0px;
left: 0px;
}
.shadow-1 {
filter: blur(4px);
opacity: 0.7;
}
.shadow-2 {
filter: blur(2px);
opacity: 0.8;
}
.shadow-3 {
filter: blur(1px);
opacity: 0.9;
}
.inner-circle {
background-color: #6cffe5;
width: 6px;
height: 6px;
top: 0px;
left: 0px;
}
.inner-shadow {
background-color: #6affe4;
width: 4px;
height: 4px;
top: 0px;
left: 0px;
filter: blur(1px);
opacity: 0.8;
}
}
</style>

View File

@@ -0,0 +1,575 @@
<template>
<div class="rack-container">
<div class="rack-title">
<ops-icon
:type="titleData.icon"
class="rack-title-icon"
/>
<span
class="rack-title-text"
>
{{ $t(titleData.text) }}
</span>
</div>
<RackHeader :viewType="viewType" />
<div
class="rack-container-main"
:style="{
flexDirection: viewType === 'front' ? 'row' : 'row-reverse'
}"
>
<div class="rack-container-main-left">
<div
v-for="(item, index) in countList"
:key="index"
class="rack-container-main-left-count"
:style="{
backgroundColor: item % 2 === 0 ? '#3D4151' : '#5E6772',
height: unitHeight + 'px',
lineHeight: unitHeight + 'px'
}"
>
{{ item }}
</div>
</div>
<div class="rack-container-main-list">
<draggable
filter=".undraggable"
:list="unitList"
@start="handleDraggableStart"
@end="handleDraggableEnd"
>
<div
v-for="(item, index) in unitList"
:key="item.key"
:class="[item.type === 'gap' || item.abnormal ? 'undraggable' : '']"
>
<div
v-if="item.type === 'device'"
:class="['rack-container-main-list-device', item.abnormal ? '' : 'rack-container-main-list-device_normal']"
:style="{
height: unitHeight * item.unitCount + 'px'
}"
@click="clickDevice(item)"
>
<div class="rack-container-main-list-device-action">
<div
class="rack-container-main-list-device-action-btn"
@click.stop="removeDevice(item)"
>
{{ $t('cmdb.dcim.remove') }}
</div>
<div
class="rack-container-main-list-device-action-btn"
@click.stop="migrateDevice(item)"
>
{{ $t('cmdb.dcim.migrate') }}
</div>
</div>
<div
v-if="item.abnormal"
class="rack-container-main-list-device-abnormal"
>
<span
class="rack-container-main-list-device-abnormal-text"
>
{{ $t('cmdb.dcim.unitAbnormal') }}
</span>
<a-icon
type="right"
class="rack-container-main-list-device-abnormal-icon"
/>
</div>
<div class="rack-container-main-list-device-header"></div>
<img
v-for="(unitIndex) in item.unitCount"
:key="unitIndex"
:src="item.deviceImage[viewType]"
/>
<div
class="rack-container-main-list-device-sider"
:style="{
right: viewType === 'front' ? '-154px' : '-157px'
}"
>
<div
v-for="(nameItem, nameIndex) in getNameList(item)"
:key="nameIndex"
class="rack-container-main-list-device-name"
@click.stop="openDeviceDetail(nameItem)"
>
<CIIcon size="14" :icon="nameItem.icon" />
<span class="rack-container-main-list-device-name-text">{{ nameItem.name }}</span>
</div>
</div>
</div>
<div
v-if="item.type === 'gap'"
:class="['rack-container-main-list-gap', viewType === 'rear' ? 'rack-container-main-list-gap_rear' : '']"
:style="{
height: unitHeight + 'px'
}"
@click="addDevice(index)"
>
<ops-icon
type="monitor-add"
class="rack-container-main-list-gap-icon"
/>
<span
class="rack-container-main-list-gap-text"
>
{{ $t('cmdb.dcim.addDevice') }}
</span>
</div>
</div>
</draggable>
</div>
<div class="rack-container-main-right">
<div class="rack-container-main-right-part-1"></div>
<div class="rack-container-main-right-part-2"></div>
<img
v-if="viewType === 'front'"
:src="require(`@/modules/cmdb/assets/dcim/rack_front_part.png`)"
class="rack-container-main-right-part-3"
/>
</div>
</div>
<div class="rack-container-footer">
<template v-if="viewType === 'front'">
<div class="rack-container-footer-dot"></div>
<div class="rack-container-footer-dot"></div>
</template>
</div>
<AbnormalModal
ref="abnormalModalRef"
@ok="editDevice"
/>
</div>
</template>
<script>
import _ from 'lodash'
import { deleteDevice } from '@/modules/cmdb/api/dcim.js'
import RackHeader from './rackHeader/index.vue'
import draggable from 'vuedraggable'
import CIIcon from '@/modules/cmdb/components/ciIcon/index.vue'
import AbnormalModal from './abnormalModal.vue'
export default {
name: 'RackUnitView',
components: {
RackHeader,
draggable,
CIIcon,
AbnormalModal
},
props: {
viewType: {
type: String,
default: 'front'
},
countList: {
type: Array,
default: () => []
},
unitList: {
type: Array,
default: () => []
},
rackId: {
type: Number,
default: 0
}
},
data() {
return {
oldDraggableList: [],
draggableDevice: {},
unitHeight: 24
}
},
computed: {
titleData() {
return {
icon: this.viewType === 'front' ? 'veops-front' : 'veops-rear',
text: this.viewType === 'front' ? 'cmdb.dcim.frontView' : 'cmdb.dcim.rearView'
}
}
},
methods: {
addDevice(index) {
const sliceUnitList = this.unitList.slice(0, index)
const unitCount = sliceUnitList.reduce((acc, cur) => acc + cur.unitCount, 0)
this.$emit('openDeviceForm', {
unitStart: this.countList.length - unitCount
})
},
editDevice(data) {
this.$emit('openDeviceForm', {
CITypeId: data?._type,
deviceId: data?.id,
unitStart: data?.u_start,
unitCount: data?.u_count,
name: data?.name
})
},
handleDraggableStart(e) {
this.oldDraggableList = _.cloneDeep(this.unitList)
this.draggableDevice = this.oldDraggableList?.[e.oldIndex] || {}
},
handleDraggableEnd(e) {
if (e.newIndex === e.oldIndex) {
return
}
const sliceUnitList = this.unitList.slice(0, e.newIndex)
const unitCount = sliceUnitList.reduce((acc, cur) => acc + cur.unitCount, 0)
/**
* 拖拽后的起始U位 = 总U数 - 该设备以上的U数 - 该设备U数 + 1
*/
const startUnit = this.countList.length - unitCount - this.draggableDevice.unitCount + 1
if (this?.draggableDevice?.id) {
this.$emit('draggable', {
startUnit,
deviceId: this.draggableDevice.id,
oldUnitList: this.oldDraggableList
})
}
this.draggableDevice = {}
this.oldDraggableList = []
},
getNameList(item) {
const nameList = [item]
if (item?.abnormalList?.length) {
nameList.push(...item.abnormalList)
}
return nameList
},
clickDevice(data) {
if (data.abnormal) {
this.$refs.abnormalModalRef.open(data)
}
},
removeDevice(data) {
const content = this.$t('cmdb.dcim.removeDeviceTip', {
deviceName: `${data.CITypeName} ${data.name}`
})
this.$confirm({
title: this.$t('warning'),
content,
onOk: () => {
deleteDevice(
this.rackId,
data.id
).then(() => {
this.$message.success(this.$t('deleteSuccess'))
this.$emit('refreshRackAllData')
})
},
})
},
migrateDevice(data) {
this.$emit('migrateDevice', data.id)
},
openDeviceDetail(deviceData) {
this.$emit('openDeviceDetail', deviceData)
}
}
}
</script>
<style lang="less" scoped>
.rack-container {
width: 236px;
.rack-title {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 14px;
&-icon {
font-size: 14px;
}
&-text {
font-size: 14px;
font-weight: 700;
color: #4E5969;
margin-left: 6px;
}
}
&-main {
display: flex;
width: 100%;
&-left {
min-width: 17px;
flex-shrink: 0;
z-index: 2;
&-count {
width: 100%;
border-bottom: solid 1px rgba(116, 138, 171, 0.25);
text-align: center;
font-size: 12px;
font-weight: 400;
color: #FFFFFF;
}
}
&-list {
width: 100%;
&-device {
background-color: #2C2D31;
border-bottom: solid 1px rgba(116, 138, 171, 0.25);
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
&-header {
width: 195px;
height: 6px;
clip-path: polygon(20px 0, 175px 0, 195px 100%, 0px 100%);
background-color: #5D6271;
}
img {
width: 195px;
height: 17px;
}
&-action {
display: none;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 1px solid #10D4FF;
background: linear-gradient(90deg, rgba(0, 0, 0, 0.80) 0%, rgba(102, 102, 102, 0.80) 100%);
align-items: center;
justify-content: center;
&-btn {
font-size: 14px;
font-weight: 400;
color: #FFFFFF;
padding: 0 10px;
cursor: pointer;
&:not(:first-child) {
border-left: solid 1px rgba(165, 169, 188, 0.44);
}
&:hover {
color: #10D4FF;
}
}
}
&-abnormal {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 1px solid #F00;
background-color: rgba(128, 47, 47, 0.66);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&-text {
color: #FFFFFF;
font-size: 14px;
font-weight: 700;
}
&-icon {
color: #FFFFFF;
font-size: 12px;
}
}
&-name {
display: flex;
align-items: center;
cursor: pointer;
&-text {
margin-left: 3px;
font-size: 12px;
font-weight: 400;
color: #1D2129;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
text-wrap: nowrap;
}
&:hover {
.rack-container-main-list-device-name-text {
color: #3F75FF;
}
}
}
&-sider {
position: absolute;
top: 0;
width: 140px;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
row-gap: 6px;
padding-left: 7px;
&::after {
content: "";
position: absolute;
top: 5%;
left: 0;
width: 4px;
height: 90%;
border: solid 1px #10D4FF;
border-left: none;
}
}
&_normal:hover {
.rack-container-main-list-device-action {
display: flex;
}
}
}
&-gap {
width: 100%;
border-bottom: solid 1px rgba(116, 138, 171, 0.25);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
background-color: #EBEFF8;
&-icon {
font-size: 12px;
display: none;
}
&-text {
font-size: 12px;
font-weight: 400;
color: rgba(0, 87, 255, 0.80);
margin-left: 6px;
display: none;
}
&_rear {
background-color: #CACDD9;
border-bottom: solid 1px #E4E7ED;
}
&:hover {
background-color: #D5DDEE;
.rack-container-main-list-gap-icon {
display: inline-block;
}
.rack-container-main-list-gap-text {
display: inline-block;
}
}
}
}
&-right {
flex-shrink: 0;
display: flex;
background-color: #86909C;
position: relative;
&-part-1 {
width: 7px;
height: 100%;
border-right: solid 1px rgba(255, 255, 255, 0.33);
}
&-part-2 {
width: 7px;
height: 100%;
background: linear-gradient(270deg, rgba(134, 144, 156, 0.00) 0%, rgba(69, 78, 89, 0.88) 100%);
filter: blur(0.25px);
}
&-part-3 {
position: absolute;
top: 50%;
left: 50%;
width: 21px;
height: 57.6px;
transform: translate(-50%, -50%);
}
}
}
&-footer {
height: 12px;
width: 100%;
background-color: #86909C;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0px 14px;
&-dot {
width: 4px;
height: 4px;
border-radius: 4px;
background-color: #E8EBEE;
border: solid 1px #FFFFFF;
box-shadow: 3px 3px 7px 0px rgba(136, 150, 163, 0.58) inset, -3px -3px 7px 0px #FFF inset;
}
}
}
</style>

View File

@@ -0,0 +1,36 @@
export const DCIM_TYPE = {
REGION: 'region',
IDC: 'idc',
SERVER_ROOM: 'server_room',
RACK: 'rack'
}
export const DCIM_CITYPE_NAME = {
REGION: 'dcim_region',
IDC: 'dcim_idc',
SERVER_ROOM: 'dcim_server_room',
RACK: 'dcim_rack'
}
export const DEVICE_CITYPE_NAME = {
SWITCH: 'switch',
FC_SWITCH: 'fc_switch',
F5: 'bigip',
ROUTER: 'router',
FIRE_WALL: 'firewall',
SERVER: 'server',
RAID: 'raid'
}
const createTypeNameMap = (typeObj, typeNameObj) => {
const map = {}
Object.keys(typeObj).forEach(key => {
map[typeObj[key]] = typeNameObj[key]
map[typeNameObj[key]] = typeObj[key]
})
return map
}
export const DCIM_TYPE_NAME_MAP = createTypeNameMap(DCIM_TYPE, DCIM_CITYPE_NAME)

View File

@@ -0,0 +1,267 @@
<template>
<TwoColumnLayout
class="dcim"
appName="cmdb-dcim"
calcBasedParent
>
<template #one>
<DCIMTree
:treeData="treeData"
:treeKey="treeKey"
@getAttrList="getAttrList"
@updateTreeKey="updateTreeKey"
@openForm="openForm"
/>
<DCIMForm
ref="dcimFormRef"
:allAttrList="allAttrList"
@ok="handleDCIMFormOk"
/>
</template>
<template #two>
<DCIMMain
v-if="!initLoading && rackCITYpe.id"
ref="dcimMainRef"
:roomId="treeKey"
:attrObj="allAttrList[DCIM_TYPE.RACK]"
:rackCITYpe="rackCITYpe"
:preferenceAttrList="rackPreferenceAttrList"
@openForm="openForm"
@refreshTreeData="getTreeData"
/>
</template>
</TwoColumnLayout>
</template>
<script>
import { getDCIMTreeView } from '@/modules/cmdb/api/dcim.js'
import { DCIM_CITYPE_NAME, DCIM_TYPE, DCIM_TYPE_NAME_MAP } from './constants.js'
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
import { getCIType } from '@/modules/cmdb/api/CIType.js'
import { getSubscribeAttributes } from '@/modules/cmdb/api/preference'
import TwoColumnLayout from '@/components/TwoColumnLayout'
import DCIMTree from './components/dcimTree.vue'
import DCIMForm from './components/dcimForm.vue'
import DCIMMain from './components/dcimMain/index.vue'
const TREE_STORAGE_KEY = 'ops_dcim_tree_active'
export default {
name: 'DCIM',
components: {
TwoColumnLayout,
DCIMTree,
DCIMForm,
DCIMMain
},
data() {
return {
DCIM_TYPE,
treeKey: localStorage.getItem(TREE_STORAGE_KEY) || '',
treeData: [],
allAttrList: {
[DCIM_TYPE.REGION]: {},
[DCIM_TYPE.IDC]: {},
[DCIM_TYPE.SERVER_ROOM]: {},
[DCIM_TYPE.RACK]: {}
},
initLoading: true,
rackCITYpe: {},
rackPreferenceAttrList: []
}
},
async mounted() {
this.initLoading = true
try {
await this.getTreeData()
await this.getRackData()
} catch (error) {
console.log('initData fail', error)
}
this.initLoading = false
},
provide() {
return {
getTreeData: this.getTreeData
}
},
methods: {
async getTreeData() {
const res = await getDCIMTreeView()
let treeData = []
if (res?.result?.length) {
treeData = res.result.map((data) => {
return this.handleTreeData(data, res.type2name)
})
}
const currentNode = this.findNodeById(treeData, this.treeKey)
if (!currentNode) {
this.updateTreeKey('')
}
const flatRreeData = []
treeData.forEach((item) => {
flatRreeData.push({
...item,
class: 'ipam-tree-node_hide_expand',
children: []
})
if (item.children.length) {
flatRreeData.push(...item.children)
}
})
this.treeData = flatRreeData
},
handleTreeData(data, type2name, parentId = '') {
const title = data?.[type2name?.[data?._type]] || ''
const dcimType = DCIM_TYPE_NAME_MAP[data.ci_type]
let icon = ''
let iconColor = '#A5A9BC'
let addType = ''
const key = String(data._id)
switch (data.ci_type) {
case DCIM_CITYPE_NAME.REGION:
icon = 'veops-region'
iconColor = '#2F54EB'
addType = DCIM_TYPE.IDC
break
case DCIM_CITYPE_NAME.IDC:
icon = 'veops-IDC'
addType = DCIM_TYPE.SERVER_ROOM
break
case DCIM_CITYPE_NAME.SERVER_ROOM:
icon = 'a-veops-room1'
break
default:
break
}
if (!data?.children?.length) {
return {
...data,
key,
title,
icon,
iconColor,
parentId,
addType,
dcimType,
count: data?.rack_count || 0
}
}
const children = data.children.map((item) => {
return this.handleTreeData(item, type2name, key)
})
return {
...data,
key,
title,
icon,
iconColor,
addType,
parentId,
children,
dcimType,
count: children.reduce((acc, item) => {
return acc + item.count
}, 0)
}
},
findNodeById(nodes, id) {
for (const node of nodes) {
if (node.key === id) {
return node
}
if (node.children) {
const foundNode = this.findNodeById(node.children, id)
if (foundNode) {
return foundNode
}
}
}
return null
},
async getRackData() {
await this.getAttrList(DCIM_CITYPE_NAME.RACK, DCIM_TYPE.RACK)
const CITypeRes = await getCIType(DCIM_CITYPE_NAME.RACK)
this.rackCITYpe = CITypeRes?.ci_types?.[0] || {}
if (this.rackCITYpe.id) {
const subscribed = await getSubscribeAttributes(this.rackCITYpe.id)
this.rackPreferenceAttrList = subscribed.attributes
}
},
async getAttrList(id, type, cb) {
if (Object.keys(this?.allAttrList?.[type] || {})?.length === 0) {
const res = await getCITypeAttributesById(id)
this.$set(this.allAttrList, type, res || {})
}
if (cb) {
cb(this.allAttrList)
}
},
async openForm(data) {
await this.getAttrList(DCIM_TYPE_NAME_MAP[data.dcimType], data.dcimType)
this.$nextTick(() => {
this.$refs.dcimFormRef.open(data)
})
},
updateTreeKey(key) {
this.treeKey = key
localStorage.setItem(TREE_STORAGE_KEY, key)
},
handleDCIMFormOk({
dcimType,
editType
}) {
switch (dcimType) {
case DCIM_TYPE.REGION:
case DCIM_TYPE.IDC:
case DCIM_TYPE.SERVER_ROOM:
this.getTreeData()
break
case DCIM_TYPE.RACK:
this.getRackList()
if (editType === 'create') {
this.getTreeData()
}
break
default:
break
}
},
getRackList() {
if (this.$refs.dcimMainRef) {
this.$refs.dcimMainRef.getRackList()
}
}
}
}
</script>
<style lang="less" scoped>
</style>

View File

@@ -9,6 +9,7 @@
<div class="ipam-tree-main">
<a-tree
v-if="treeData.length"
autoExpandParent
:treeData="filterTreeData"
:selectedKeys="treeKey ? [treeKey] : []"
:defaultExpandedKeys="treeKey ? [treeKey] : []"
@@ -238,7 +239,7 @@ export default {
}
}
& > li:first-child {
.ipam-tree-node-all {
.ant-tree-switcher {
display: none;
}

View File

@@ -185,6 +185,7 @@ export default {
showCatalogBtn: rootShowCatalogBtn,
showSubnetBtn: rootShowSubnetBtn,
parentId: '',
class: 'ipam-tree-node-all'
})
this.treeData = treeData

View File

@@ -3,6 +3,7 @@
:visible="visible"
:width="700"
:title="$t('cmdb.ipam.addressAssign')"
:confirmLoading="confirmLoading"
@ok="handleOk"
@cancel="handleCancel"
>
@@ -17,7 +18,7 @@
<a-form-model-item
label="IP"
>
{{ ipData.ip }}
<span class="assign-form-ip" >{{ ipList.join(', ') }}</span>
</a-form-model-item>
<a-form-model-item
v-for="(item) in formList"
@@ -80,37 +81,29 @@ export default {
attrList: {
type: Array,
default: () => []
},
subnetData: {
type: Object,
default: () => {}
}
},
data() {
return {
visible: false,
ipData: {},
ipList: [],
nodeId: -1,
formList: [],
form: {},
formRules: {},
statusSelectOption: [
{
value: 0,
label: 'cmdb.ipam.assigned'
},
{
value: 2,
label: 'cmdb.ipam.reserved'
}
]
confirmLoading: false,
isBatch: false
}
},
methods: {
async open({
ipData,
ipList = [],
ipData = null,
nodeId,
}) {
this.isBatch = ipList.length !== 0
this.ipList = ipList.length ? _.cloneDeep(ipList) : [ipData?.ip ?? '']
this.ipData = ipData || {}
this.nodeId = nodeId || -1
this.visible = true
@@ -237,7 +230,8 @@ export default {
this.form = {}
this.formRules = {}
this.formList = []
this.visible = false
this.confirmLoading = false
this.isBatch = false
this.$refs.assignFormRef.clearValidate()
},
@@ -248,16 +242,35 @@ export default {
return
}
await postIPAMAddress({
ips: [this.ipData.ip],
parent_id: this.nodeId,
...this.form,
subnet_mask: this?.ipData?.subnet_mask ?? undefined,
gateway: this?.ipData?.gateway ?? undefined
})
this.confirmLoading = true
if (!this.isBatch) {
await postIPAMAddress({
ips: this.ipList,
parent_id: this.nodeId,
...this.form,
subnet_mask: this?.ipData?.subnet_mask ?? undefined,
gateway: this?.ipData?.gateway ?? undefined
})
this.$emit('ok')
} else {
const ipChunk = _.chunk(this.ipList, 5)
const paramsList = ipChunk.map((ips) => ({
ips,
parent_id: this.nodeId,
...this.form,
subnet_mask: this?.ipData?.subnet_mask ?? undefined,
gateway: this?.ipData?.gateway ?? undefined
}))
this.$emit('batchAssign', {
paramsList,
ipList: this.ipList
})
}
this.$emit('ok')
this.handleCancel()
this.confirmLoading = false
})
},
@@ -280,5 +293,12 @@ export default {
max-height: 400px;
overflow-y: auto;
overflow-x: hidden;
&-ip {
max-height: 100px;
overflow-y: auto;
overflow-x: hidden;
display: block;
}
}
</style>

View File

@@ -9,12 +9,11 @@
<div class="address-null-tip2">{{ $t(addressNullTip) }}</div>
</div>
<div v-else-if="loading" class="address-loading">
<a-icon type="loading" class="address-loading-icon" />
<span class="address-loading-text">{{ $t('loading') }}</span>
</div>
<template v-else>
<a-spin
v-else
:tip="loadTip"
:spinning="loading"
>
<div class="address-header">
<div class="address-header-left">
<a-input-search
@@ -53,6 +52,15 @@
</a-select-option>
</a-select>
<div v-if="selectedIPList.length" class="ops-list-batch-action">
<span @click="clickBatchAssign">{{ $t('cmdb.ipam.batchAssign') }}</span>
<a-divider type="vertical" />
<span @click="clickBatchRecycle">{{ $t('cmdb.ipam.batchRecycle') }}</span>
<a-divider type="vertical" />
<span @click="handleExport">{{ $t('export') }}</span>
<span>{{ $t('cmdb.ci.selectRows', { rows: selectedIPList.length }) }}</span>
</div>
<div
v-if="currentLayout === 'grid'"
class="address-header-status"
@@ -85,22 +93,12 @@
</div>
<div class="address-header-right">
<a-button
type="primary"
class="ops-button-ghost"
ghost
@click="handleExport"
>
<ops-icon type="veops-export" />
{{ $t('export') }}
</a-button>
<div class="address-header-layout">
<div
v-for="(item) in layoutList"
:key="item.value"
:class="['address-header-layout-item', currentLayout === item.value ?'address-header-layout-item-active' : '']"
@click="currentLayout = item.value"
@click="handleChangeLayout(item.value)"
>
<ops-icon :type="item.icon" />
</div>
@@ -119,6 +117,7 @@
:columnWidth="columnWidth"
@openAssign="openAssign"
@recycle="handleRecycle"
@selectChange="handleTableSelectChange"
/>
<GridIP
@@ -131,13 +130,13 @@
@recycle="handleRecycle"
/>
</div>
</template>
</a-spin>
<AssignForm
ref="assignFormRef"
:attrList="attrList"
:subnetData="subnetData"
@ok="getIPList"
@batchAssign="batchAssign"
/>
</div>
</template>
@@ -188,6 +187,8 @@ export default {
referenceCIIdMap: {},
columnWidth: {},
loading: false,
selectedIPList: [],
loadTip: this.$t('loading'),
currentStatus: 'all',
filterOption: [
@@ -298,6 +299,7 @@ export default {
},
methods: {
async initData() {
this.loadTip = this.$t('loading')
this.loading = true
try {
await this.getColumns()
@@ -497,6 +499,7 @@ export default {
let tableData = []
if (this.currentLayout === 'table') {
tableData = this.$refs.tableIPRef.getCheckedTableData()
this.selectedIPList = []
} else {
tableData = this.filterIPList
}
@@ -561,6 +564,143 @@ export default {
})
},
})
},
handleChangeLayout(value) {
if (this.currentLayout !== value) {
if (value === 'grid') {
this.selectedIPList = []
}
this.currentLayout = value
}
},
handleTableSelectChange(ips) {
this.selectedIPList = ips
},
clickBatchAssign() {
this.$refs.assignFormRef.open({
nodeId: this?.nodeData?._id,
ipData: {
subnet_mask: this?.subnetData?.subnet_mask ?? undefined,
gateway: this?.subnetData?.gateway ?? undefined
},
ipList: this.selectedIPList
})
},
async batchAssign({
paramsList,
ipList
}) {
let successNum = 0
let errorNum = 0
try {
this.loading = true
this.loadTip = this.$t('cmdb.ipam.batchAssignInProgress', {
total: ipList.length,
successNum: successNum,
errorNum: errorNum,
})
await _.reduce(
paramsList,
(promiseChain, params) => {
const ipCount = params?.ips?.length ?? 0
return promiseChain.then(() => {
return postIPAMAddress(params).then(() => {
successNum += ipCount
}).catch(() => {
errorNum += ipCount
}).finally(() => {
this.loadTip = this.$t('cmdb.ipam.batchAssignInProgress', {
total: ipList.length,
successNum: successNum,
errorNum: errorNum,
})
})
})
},
Promise.resolve()
)
if (this.$refs.tableIPRef) {
this.$refs.tableIPRef.clearCheckbox()
this.selectedIPList = []
}
this.$message.success(this.$t('cmdb.ipam.batchAssignCompleted'))
this.loading = false
this.getIPList()
} catch (error) {
console.log('error', error)
}
},
clickBatchRecycle() {
this.$confirm({
title: this.$t('warning'),
content: this.$t('cmdb.ipam.recycleTip'),
onOk: () => {
this.handleBatchRecycle()
},
})
},
async handleBatchRecycle() {
let successNum = 0
let errorNum = 0
try {
this.loading = true
this.loadTip = this.$t('cmdb.ipam.batchRecycleInProgress', {
total: this.selectedIPList.length,
successNum: successNum,
errorNum: errorNum,
})
const ipChunk = _.chunk(this.selectedIPList, 5)
await _.reduce(
ipChunk,
(promiseChain, ips) => {
const ipCount = ips.length
console.log('ipCount', ipCount, successNum, errorNum)
return promiseChain.then(() => {
return postIPAMAddress({
ips,
parent_id: this.nodeData._id,
assign_status: 1
}).then(() => {
successNum += ipCount
}).catch(() => {
errorNum += ipCount
}).finally(() => {
this.loadTip = this.$t('cmdb.ipam.batchRecycleInProgress', {
total: this.selectedIPList.length,
successNum: successNum,
errorNum: errorNum,
})
})
})
},
Promise.resolve()
)
if (this.$refs.tableIPRef) {
this.$refs.tableIPRef.clearCheckbox()
this.selectedIPList = []
}
this.$message.success(this.$t('cmdb.ipam.batchRecycleCompleted'))
this.loading = false
this.getIPList()
} catch (error) {
console.log('error', error)
}
}
}
}
@@ -570,7 +710,6 @@ export default {
.address {
width: 100%;
height: fit-content;
position: relative;
&-header {
width: 100%;
@@ -695,27 +834,5 @@ export default {
color: #2F54EB;
}
}
&-loading {
width: 100%;
height: 300px;
position: absolute;
top: 0;
left: 0;
color: #000000;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 10;
&-icon {
font-size: 28px;
}
&-text {
margin-top: 12px;
}
}
}
</style>

View File

@@ -14,7 +14,7 @@
class="ops-unstripe-table checkbox-hover-table"
@checkbox-change="onSelectChange"
@checkbox-all="onSelectChange"
@checkbox-range-end="onSelectChange"
@checkbox-range-end="onSelectRangeEnd"
>
<vxe-table-column
align="center"
@@ -241,13 +241,20 @@ export default {
}
if (clearCheckbox) {
tableRef.clearCheckboxRow()
tableRef.clearCheckboxReserve()
this.clearCheckbox()
}
return tableData
},
clearCheckbox() {
const tableRef = this.$refs?.xTable?.getVxetableRef?.()
if (tableRef) {
tableRef.clearCheckboxRow()
tableRef.clearCheckboxReserve()
}
},
getReferenceAttrValue(id, col) {
const ci = this?.referenceCIIdMap?.[col?.reference_type_id]?.[id]
if (!ci) {
@@ -267,7 +274,15 @@ export default {
},
onSelectChange() {
console.log('onSelectChange')
const xTable = this.$refs.xTable.getVxetableRef()
const records = [...xTable.getCheckboxRecords(), ...xTable.getCheckboxReserveRecords()]
const ips = records.map((item) => item.ip)
this.$emit('selectChange', ips)
},
onSelectRangeEnd({ records }) {
const ips = records?.map?.((item) => item.ip) || []
this.$emit('selectChange', ips)
},
}
}

View File

@@ -41,7 +41,7 @@ services:
- redis
cmdb-api:
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.4.14
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.4.15
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
nohup flask cmdb-patch -v 2.4.14 &
nohup flask cmdb-patch -v 2.4.15 &
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.14
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.4.15
container_name: cmdb-ui
depends_on:
cmdb-api: