mirror of
https://github.com/veops/cmdb.git
synced 2025-09-14 07:26:54 +08:00
Compare commits
29 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
a4e686f9ae | ||
|
45bb3867da | ||
|
bb467030e2 | ||
|
0b1dfa4538 | ||
|
56a310c667 | ||
|
290a79860c | ||
|
0144ee6508 | ||
|
035171cbe8 | ||
|
f7273c96dc | ||
|
73bdd99829 | ||
|
f46214aaf8 | ||
|
a809933a5f | ||
|
5048f2a788 | ||
|
40a53a0213 | ||
|
1947af5693 | ||
|
6ec7caf5ea | ||
|
e93d894f04 | ||
|
081f35816f | ||
|
a8fadb2785 | ||
|
72c37c995d | ||
|
f8fbbe4b9a | ||
|
155ba67ecc | ||
|
9c67b1e56a | ||
|
88df3355d8 | ||
|
549056a42d | ||
|
365fdf2bab | ||
|
6bf01786d8 | ||
|
e180f549c8 | ||
|
3a7f4a31d0 |
@@ -1,6 +1,8 @@
|
||||
import click
|
||||
from flask import current_app
|
||||
from flask.cli import with_appcontext
|
||||
|
||||
from api.lib.exception import AbortException
|
||||
from api.lib.perm.acl.user import UserCRUD
|
||||
|
||||
|
||||
@@ -46,11 +48,18 @@ def add_user():
|
||||
email = click.prompt('Enter email ', confirmation_prompt=False)
|
||||
is_admin = click.prompt('Admin (Y/N) ', confirmation_prompt=False, type=bool, default=False)
|
||||
|
||||
UserCRUD.add(username=username, password=password, email=email)
|
||||
current_app.test_request_context().push()
|
||||
|
||||
if is_admin:
|
||||
app = AppCache.get('acl') or App.create(name='acl')
|
||||
acl_admin = RoleCache.get_by_name(app.id, 'acl_admin') or RoleCRUD.add_role('acl_admin', app.id, True)
|
||||
rid = RoleCache.get_by_name(None, username).id
|
||||
try:
|
||||
|
||||
RoleRelationCRUD.add(acl_admin, acl_admin.id, [rid], app.id)
|
||||
UserCRUD.add(username=username, password=password, email=email)
|
||||
|
||||
if is_admin:
|
||||
app = AppCache.get('acl') or App.create(name='acl')
|
||||
acl_admin = RoleCache.get_by_name(None, 'acl_admin') or RoleCRUD.add_role('acl_admin', app.id, True)
|
||||
rid = RoleCache.get_by_name(None, username).id
|
||||
|
||||
RoleRelationCRUD.add(acl_admin, acl_admin.id, [rid], app.id)
|
||||
|
||||
except AbortException as e:
|
||||
print(f"Failed: {e}")
|
||||
|
@@ -360,16 +360,28 @@ class CIManager(object):
|
||||
_sync=False,
|
||||
**ci_dict):
|
||||
"""
|
||||
add ci
|
||||
:param ci_type_name:
|
||||
:param exist_policy: replace or reject or need
|
||||
:param _no_attribute_policy: ignore or reject
|
||||
:param is_auto_discovery: default is False
|
||||
:param _is_admin: default is False
|
||||
:param ticket_id:
|
||||
:param _sync:
|
||||
:param ci_dict:
|
||||
:return:
|
||||
Create a new Configuration Item (CI) or update existing based on unique constraints.
|
||||
|
||||
Handles complete CI creation workflow including validation, uniqueness checks,
|
||||
password encryption, computed attributes, relationship creation, and caching.
|
||||
|
||||
Args:
|
||||
ci_type_name (str): Name of the CI type to create
|
||||
exist_policy (ExistPolicy): How to handle existing CIs (REPLACE/REJECT/NEED)
|
||||
_no_attribute_policy (ExistPolicy): How to handle unknown attributes (IGNORE/REJECT)
|
||||
is_auto_discovery (bool): Whether CI is created by auto-discovery process
|
||||
_is_admin (bool): Whether to skip permission checks
|
||||
ticket_id (int, optional): Associated ticket ID for audit trail
|
||||
_sync (bool): Whether to execute cache/relation tasks synchronously
|
||||
**ci_dict: CI attribute values as key-value pairs
|
||||
|
||||
Returns:
|
||||
int: ID of the created or updated CI
|
||||
|
||||
Raises:
|
||||
400: If unique constraints violated, required attributes missing, or validation fails
|
||||
403: If user lacks permissions for restricted attributes
|
||||
404: If CI type not found or referenced CI not exists
|
||||
"""
|
||||
now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
ci_type = CITypeManager.check_is_existed(ci_type_name)
|
||||
@@ -512,6 +524,24 @@ class CIManager(object):
|
||||
return ci.id
|
||||
|
||||
def update(self, ci_id, _is_admin=False, ticket_id=None, _sync=False, **ci_dict):
|
||||
"""
|
||||
Update an existing Configuration Item with new attribute values.
|
||||
|
||||
Performs comprehensive CI update including validation, constraint checks,
|
||||
password handling, computed attributes processing, and change tracking.
|
||||
|
||||
Args:
|
||||
ci_id (int): ID of the CI to update
|
||||
_is_admin (bool): Whether to skip permission checks
|
||||
ticket_id (int, optional): Associated ticket ID for audit trail
|
||||
_sync (bool): Whether to execute cache/relation tasks synchronously
|
||||
**ci_dict: CI attribute values to update as key-value pairs
|
||||
|
||||
Raises:
|
||||
400: If unique constraints violated or validation fails
|
||||
403: If user lacks permissions for restricted attributes
|
||||
404: If CI not found
|
||||
"""
|
||||
now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
ci = self.confirm_ci_existed(ci_id)
|
||||
ci_type = ci.ci_type
|
||||
@@ -1420,14 +1450,31 @@ class CIRelationManager(object):
|
||||
parent_attr = AttributeCache.get(parent_attr_id)
|
||||
child_attr = AttributeCache.get(child_attr_id)
|
||||
attr_value = ci_dict.get(parent_attr.name)
|
||||
if attr_value != 0 and not attr_value:
|
||||
continue
|
||||
value_table = TableMap(attr=child_attr).table
|
||||
for child in value_table.get_by(attr_id=child_attr.id, value=attr_value, only_query=True).join(
|
||||
CI, CI.id == value_table.ci_id).filter(CI.type_id == item.child_id):
|
||||
_relations.add((ci_dict['_id'], child.ci_id))
|
||||
attr_value_list = [attr_value] if not isinstance(attr_value, list) else attr_value
|
||||
|
||||
matching_cis = value_table.get_by(
|
||||
attr_id=child_attr.id,
|
||||
only_query=True
|
||||
).join(
|
||||
CI, CI.id == value_table.ci_id
|
||||
).filter(
|
||||
CI.type_id == item.child_id,
|
||||
value_table.value.in_(attr_value_list)
|
||||
).all()
|
||||
|
||||
for ci in matching_cis:
|
||||
_relations.add((ci_dict['_id'], ci.ci_id))
|
||||
|
||||
if relations is None:
|
||||
relations = _relations
|
||||
else:
|
||||
relations &= _relations
|
||||
if item.constraint == ConstraintEnum.Many2Many:
|
||||
relations |= _relations
|
||||
else:
|
||||
relations &= _relations
|
||||
|
||||
cls.delete_relations_by_source(RelationSourceEnum.ATTRIBUTE_VALUES,
|
||||
first_ci_id=ci_dict['_id'],
|
||||
@@ -1447,14 +1494,31 @@ class CIRelationManager(object):
|
||||
parent_attr = AttributeCache.get(parent_attr_id)
|
||||
child_attr = AttributeCache.get(child_attr_id)
|
||||
attr_value = ci_dict.get(child_attr.name)
|
||||
if attr_value != 0 and not attr_value:
|
||||
continue
|
||||
value_table = TableMap(attr=parent_attr).table
|
||||
for parent in value_table.get_by(attr_id=parent_attr.id, value=attr_value, only_query=True).join(
|
||||
CI, CI.id == value_table.ci_id).filter(CI.type_id == item.parent_id):
|
||||
_relations.add((parent.ci_id, ci_dict['_id']))
|
||||
attr_value_list = [attr_value] if not isinstance(attr_value, list) else attr_value
|
||||
|
||||
matching_cis = value_table.get_by(
|
||||
attr_id=parent_attr.id,
|
||||
only_query=True
|
||||
).join(
|
||||
CI, CI.id == value_table.ci_id
|
||||
).filter(
|
||||
CI.type_id == item.parent_id,
|
||||
value_table.value.in_(attr_value_list)
|
||||
).all()
|
||||
|
||||
for ci in matching_cis:
|
||||
_relations.add((ci.ci_id, ci_dict['_id']))
|
||||
|
||||
if relations is None:
|
||||
relations = _relations
|
||||
else:
|
||||
relations &= _relations
|
||||
if item.constraint == ConstraintEnum.Many2Many:
|
||||
relations |= _relations
|
||||
else:
|
||||
relations &= _relations
|
||||
|
||||
cls.delete_relations_by_source(RelationSourceEnum.ATTRIBUTE_VALUES,
|
||||
second_ci_id=ci_dict['_id'],
|
||||
|
@@ -92,7 +92,8 @@ class IpAddressManager(object):
|
||||
else:
|
||||
return abort(400, ErrFormat.ipam_address_model_not_found)
|
||||
|
||||
with (redis_lock.Lock(rd.r, "IPAM_ASSIGN_ADDRESS_{}".format(subnet_id), expire=10)):
|
||||
with (redis_lock.Lock(rd.r, "IPAM_ASSIGN_ADDRESS_{}".format(subnet_id),
|
||||
expire=60, auto_renewal=True)):
|
||||
cis = self._get_cis(subnet_id, ips)
|
||||
ip2ci = {ci[IPAddressBuiltinAttributes.IP]: ci for ci in cis}
|
||||
|
||||
|
@@ -1,9 +1,8 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
import copy
|
||||
from collections import defaultdict
|
||||
|
||||
import copy
|
||||
import six
|
||||
import toposort
|
||||
from flask import abort
|
||||
from flask import current_app
|
||||
@@ -16,6 +15,7 @@ from api.lib.cmdb.cache import CITypeAttributesCache
|
||||
from api.lib.cmdb.cache import CITypeCache
|
||||
from api.lib.cmdb.cache import CMDBCounterCache
|
||||
from api.lib.cmdb.ci_type import CITypeAttributeManager
|
||||
from api.lib.cmdb.ci_type import CITypeManager
|
||||
from api.lib.cmdb.const import BUILTIN_ATTRIBUTES
|
||||
from api.lib.cmdb.const import ConstraintEnum
|
||||
from api.lib.cmdb.const import PermEnum
|
||||
@@ -29,6 +29,7 @@ from api.lib.perm.acl.acl import ACLManager
|
||||
from api.models.cmdb import CITypeGroup
|
||||
from api.models.cmdb import CITypeGroupItem
|
||||
from api.models.cmdb import CITypeRelation
|
||||
from api.models.cmdb import PreferenceAutoSubscriptionConfig
|
||||
from api.models.cmdb import PreferenceCITypeOrder
|
||||
from api.models.cmdb import PreferenceRelationView
|
||||
from api.models.cmdb import PreferenceSearchOption
|
||||
@@ -49,15 +50,27 @@ class PreferenceManager(object):
|
||||
type2group = {}
|
||||
for i in db.session.query(CITypeGroupItem, CITypeGroup).join(
|
||||
CITypeGroup, CITypeGroup.id == CITypeGroupItem.group_id).filter(
|
||||
CITypeGroup.deleted.is_(False)).filter(CITypeGroupItem.deleted.is_(False)):
|
||||
CITypeGroup.deleted.is_(False)).filter(CITypeGroupItem.deleted.is_(False)):
|
||||
type2group[i.CITypeGroupItem.type_id] = i.CITypeGroup.to_dict()
|
||||
|
||||
types = db.session.query(PreferenceShowAttributes.type_id).filter(
|
||||
PreferenceShowAttributes.uid == current_user.uid).filter(
|
||||
PreferenceShowAttributes.deleted.is_(False)).group_by(
|
||||
PreferenceShowAttributes.type_id).all() if instance else []
|
||||
types = sorted(types, key=lambda x: {i.type_id: idx for idx, i in enumerate(
|
||||
ci_type_order) if not i.is_tree}.get(x.type_id, 1))
|
||||
if instance:
|
||||
auto_types = PreferenceManager.get_auto_subscription_types(current_user.uid)
|
||||
if auto_types is not None:
|
||||
class TypeIdObj:
|
||||
def __init__(self, type_id):
|
||||
self.type_id = type_id
|
||||
|
||||
types = [TypeIdObj(t) for t in auto_types]
|
||||
else:
|
||||
types = db.session.query(PreferenceShowAttributes.type_id).filter(
|
||||
PreferenceShowAttributes.uid == current_user.uid).filter(
|
||||
PreferenceShowAttributes.deleted.is_(False)).group_by(
|
||||
PreferenceShowAttributes.type_id).all()
|
||||
else:
|
||||
types = []
|
||||
|
||||
types = sorted(types, key=lambda x: {i.type_id: idx for idx, i in enumerate(ci_type_order)
|
||||
if not i.is_tree}.get(x.type_id, 1))
|
||||
group_types = []
|
||||
other_types = []
|
||||
group2idx = {}
|
||||
@@ -104,19 +117,26 @@ class PreferenceManager(object):
|
||||
|
||||
ci_type_order = sorted(PreferenceCITypeOrder.get_by(uid=current_user.uid, to_dict=False), key=lambda x: x.order)
|
||||
if instance:
|
||||
types = db.session.query(PreferenceShowAttributes.type_id,
|
||||
PreferenceShowAttributes.uid, PreferenceShowAttributes.created_at).filter(
|
||||
PreferenceShowAttributes.deleted.is_(False)).filter(
|
||||
PreferenceShowAttributes.uid == current_user.uid).group_by(
|
||||
PreferenceShowAttributes.uid, PreferenceShowAttributes.type_id)
|
||||
for i in types:
|
||||
result['self']['instance'].append(i.type_id)
|
||||
if str(i.created_at) > str(result['self']['type_id2subs_time'].get(i.type_id, "")):
|
||||
result['self']['type_id2subs_time'][i.type_id] = i.created_at
|
||||
# Try auto subscription first, fallback to manual if not configured
|
||||
auto_types = PreferenceManager.get_auto_subscription_types(current_user.uid)
|
||||
if auto_types is not None:
|
||||
result['self']['instance'] = auto_types
|
||||
for type_id in auto_types:
|
||||
result['self']['type_id2subs_time'][type_id] = ""
|
||||
else:
|
||||
types = db.session.query(PreferenceShowAttributes.type_id,
|
||||
PreferenceShowAttributes.uid, PreferenceShowAttributes.created_at).filter(
|
||||
PreferenceShowAttributes.deleted.is_(False)).filter(
|
||||
PreferenceShowAttributes.uid == current_user.uid).group_by(
|
||||
PreferenceShowAttributes.uid, PreferenceShowAttributes.type_id)
|
||||
for i in types:
|
||||
result['self']['instance'].append(i.type_id)
|
||||
if str(i.created_at) > str(result['self']['type_id2subs_time'].get(i.type_id, "")):
|
||||
result['self']['type_id2subs_time'][i.type_id] = i.created_at
|
||||
|
||||
instance_order = [i.type_id for i in ci_type_order if not i.is_tree]
|
||||
if len(instance_order) == len(result['self']['instance']):
|
||||
result['self']['instance'] = instance_order
|
||||
instance_order = [i.type_id for i in ci_type_order if not i.is_tree]
|
||||
if len(instance_order) == len(result['self']['instance']):
|
||||
result['self']['instance'] = instance_order
|
||||
|
||||
if tree:
|
||||
types = PreferenceTreeView.get_by(uid=current_user.uid, to_dict=False)
|
||||
@@ -131,15 +151,73 @@ class PreferenceManager(object):
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def get_auto_subscription_types(uid):
|
||||
"""Get user's auto-subscribed CI types based on config rules"""
|
||||
config = PreferenceAutoSubscriptionConfig.get_by(
|
||||
uid=uid, enabled=True, first=True, to_dict=False
|
||||
)
|
||||
|
||||
if not config:
|
||||
return None
|
||||
|
||||
all_permitted_types = PreferenceManager._get_permitted_ci_types()
|
||||
result_types = PreferenceManager._apply_subscription_config(config, all_permitted_types)
|
||||
|
||||
return result_types
|
||||
|
||||
@staticmethod
|
||||
def _get_permitted_ci_types():
|
||||
"""Get CI types that user has read permission for"""
|
||||
from api.lib.perm.acl.acl import is_app_admin
|
||||
|
||||
if not current_app.config.get('USE_ACL') or is_app_admin('cmdb'):
|
||||
return [t['id'] for t in CITypeManager.get_ci_types()]
|
||||
|
||||
# Regular user: filter by permissions
|
||||
permitted_resources = ACLManager('cmdb').get_resources(ResourceTypeEnum.CI)
|
||||
permitted_names = {r.get('name') for r in permitted_resources}
|
||||
|
||||
return [ci_type_dict['id'] for ci_type_dict in CITypeManager.get_ci_types()
|
||||
if ci_type_dict['name'] in permitted_names]
|
||||
|
||||
@staticmethod
|
||||
def _apply_subscription_config(config, all_permitted_types):
|
||||
"""Apply subscription rules: 'all' mode excludes, 'none' mode includes"""
|
||||
result_types = set()
|
||||
|
||||
if config.base_strategy == 'all':
|
||||
# Start with all types, then exclude
|
||||
result_types = set(all_permitted_types)
|
||||
|
||||
if config.group_ids:
|
||||
exclude_group_type_ids = PreferenceManager._get_types_by_group_ids(config.group_ids)
|
||||
result_types.difference_update(exclude_group_type_ids)
|
||||
|
||||
if config.type_ids:
|
||||
result_types.difference_update(config.type_ids)
|
||||
|
||||
else: # base_strategy == 'none'
|
||||
# Start empty, then include
|
||||
if config.group_ids:
|
||||
include_group_type_ids = PreferenceManager._get_types_by_group_ids(config.group_ids)
|
||||
result_types.update(t for t in include_group_type_ids if t in all_permitted_types)
|
||||
|
||||
if config.type_ids:
|
||||
result_types.update(t for t in config.type_ids if t in all_permitted_types)
|
||||
|
||||
return list(result_types)
|
||||
|
||||
@staticmethod
|
||||
def _get_types_by_group_ids(group_ids):
|
||||
return [i.type_id for i in CITypeGroupItem.get_by(
|
||||
__func_in___key_group_id=group_ids, to_dict=False, fl=['type_id'])]
|
||||
|
||||
@staticmethod
|
||||
def get_show_attributes(type_id):
|
||||
_type = CITypeCache.get(type_id) or abort(404, ErrFormat.ci_type_not_found)
|
||||
type_id = _type and _type.id
|
||||
|
||||
if not isinstance(type_id, six.integer_types):
|
||||
_type = CITypeCache.get(type_id)
|
||||
type_id = _type and _type.id
|
||||
|
||||
attrs = PreferenceShowAttributes.get_by(uid=current_user.uid, type_id=type_id, to_dict=False)
|
||||
|
||||
result = []
|
||||
@@ -170,11 +248,11 @@ class PreferenceManager(object):
|
||||
i.update(dict(choice_value=AttributeManager.get_choice_values(
|
||||
i["id"], i["value_type"], i.get("choice_web_hook"), i.get("choice_other"))))
|
||||
|
||||
if (_type.name in SysComputedAttributes.type2attr and
|
||||
i['name'] in SysComputedAttributes.type2attr[_type.name]):
|
||||
i['sys_computed'] = True
|
||||
else:
|
||||
i['sys_computed'] = False
|
||||
if (_type.name in SysComputedAttributes.type2attr and
|
||||
i['name'] in SysComputedAttributes.type2attr[_type.name]):
|
||||
i['sys_computed'] = True
|
||||
else:
|
||||
i['sys_computed'] = False
|
||||
|
||||
return is_subscribed, result
|
||||
|
||||
@@ -523,3 +601,54 @@ class PreferenceManager(object):
|
||||
db.session.rollback()
|
||||
current_app.logger.error("upsert citype order failed: {}".format(e))
|
||||
return abort(400, ErrFormat.unknown_error)
|
||||
|
||||
@staticmethod
|
||||
def get_auto_subscription_config():
|
||||
"""Get user's auto subscription configuration"""
|
||||
config = PreferenceAutoSubscriptionConfig.get_by(
|
||||
uid=current_user.uid, first=True, to_dict=True
|
||||
)
|
||||
return config
|
||||
|
||||
@staticmethod
|
||||
def create_or_update_auto_subscription_config(base_strategy, group_ids=None, type_ids=None,
|
||||
enabled=True, description=None):
|
||||
"""Create or update user's auto subscription config"""
|
||||
config = PreferenceAutoSubscriptionConfig.get_by(
|
||||
uid=current_user.uid, first=True, to_dict=False
|
||||
)
|
||||
|
||||
data = {
|
||||
'base_strategy': base_strategy,
|
||||
'group_ids': group_ids or [],
|
||||
'type_ids': type_ids or [],
|
||||
'enabled': enabled,
|
||||
'description': description
|
||||
}
|
||||
|
||||
if config:
|
||||
return config.update(**data)
|
||||
else:
|
||||
data['uid'] = current_user.uid
|
||||
return PreferenceAutoSubscriptionConfig.create(**data)
|
||||
|
||||
@staticmethod
|
||||
def delete_auto_subscription_config():
|
||||
"""Delete user's auto subscription configuration"""
|
||||
config = PreferenceAutoSubscriptionConfig.get_by(
|
||||
uid=current_user.uid, first=True, to_dict=False
|
||||
)
|
||||
if config:
|
||||
config.soft_delete()
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def toggle_auto_subscription_config(enabled):
|
||||
"""Enable or disable user's auto subscription config"""
|
||||
config = PreferenceAutoSubscriptionConfig.get_by(
|
||||
uid=current_user.uid, first=True, to_dict=False
|
||||
)
|
||||
if not config:
|
||||
return abort(404, "Auto subscription config not found")
|
||||
|
||||
return config.update(enabled=enabled)
|
||||
|
@@ -525,6 +525,17 @@ class PreferenceCITypeOrder(Model):
|
||||
is_tree = db.Column(db.Boolean, default=False) # True is tree view, False is resource view
|
||||
|
||||
|
||||
class PreferenceAutoSubscriptionConfig(Model):
|
||||
__tablename__ = "c_pasc"
|
||||
|
||||
uid = db.Column(db.Integer, index=True, nullable=False, unique=True)
|
||||
base_strategy = db.Column(db.Enum('all', 'none'), default='none', nullable=False)
|
||||
group_ids = db.Column(db.JSON)
|
||||
type_ids = db.Column(db.JSON)
|
||||
enabled = db.Column(db.Boolean, default=True, nullable=False)
|
||||
description = db.Column(db.Text)
|
||||
|
||||
|
||||
# custom
|
||||
class CustomDashboard(Model):
|
||||
__tablename__ = "c_c_d"
|
||||
|
@@ -211,3 +211,59 @@ class PreferenceCITypeOrderView(APIView):
|
||||
PreferenceManager.upsert_ci_type_order(type_ids, is_tree)
|
||||
|
||||
return self.jsonify(type_ids=type_ids, is_tree=is_tree)
|
||||
|
||||
|
||||
class PreferenceAutoSubscriptionView(APIView):
|
||||
url_prefix = "/preference/auto_subscription"
|
||||
|
||||
def get(self):
|
||||
config = PreferenceManager.get_auto_subscription_config()
|
||||
return self.jsonify(config or {})
|
||||
|
||||
@args_required("base_strategy")
|
||||
def put(self):
|
||||
base_strategy = request.values.get("base_strategy")
|
||||
group_ids = request.values.get("group_ids")
|
||||
type_ids = request.values.get("type_ids")
|
||||
enabled = request.values.get("enabled", 1) in current_app.config.get('BOOL_TRUE')
|
||||
description = request.values.get("description")
|
||||
|
||||
if base_strategy not in ['all', 'none']:
|
||||
return abort(400, "base_strategy must be 'all' or 'none'")
|
||||
|
||||
if group_ids:
|
||||
try:
|
||||
group_ids = [int(x) for x in group_ids.split(',') if x.strip()]
|
||||
except ValueError:
|
||||
return abort(400, "Invalid group_ids format")
|
||||
|
||||
if type_ids:
|
||||
try:
|
||||
type_ids = [int(x) for x in type_ids.split(',') if x.strip()]
|
||||
except ValueError:
|
||||
return abort(400, "Invalid type_ids format")
|
||||
|
||||
result = PreferenceManager.create_or_update_auto_subscription_config(
|
||||
base_strategy=base_strategy,
|
||||
group_ids=group_ids,
|
||||
type_ids=type_ids,
|
||||
enabled=enabled,
|
||||
description=description
|
||||
)
|
||||
|
||||
return self.jsonify(result.to_dict())
|
||||
|
||||
def delete(self):
|
||||
PreferenceManager.delete_auto_subscription_config()
|
||||
return self.jsonify(message="Auto subscription config deleted")
|
||||
|
||||
|
||||
class PreferenceAutoSubscriptionToggleView(APIView):
|
||||
url_prefix = "/preference/auto_subscription/toggle"
|
||||
|
||||
@args_required("enabled")
|
||||
def patch(self):
|
||||
enabled = request.values.get("enabled") in current_app.config.get('BOOL_TRUE')
|
||||
|
||||
result = PreferenceManager.toggle_auto_subscription_config(enabled)
|
||||
return self.jsonify(result.to_dict())
|
||||
|
37
cmdb-api/api/views/common_setting/system_language.py
Normal file
37
cmdb-api/api/views/common_setting/system_language.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import os
|
||||
|
||||
from api.resource import APIView
|
||||
from api.lib.perm.auth import auth_abandoned
|
||||
|
||||
prefix = "/system"
|
||||
|
||||
|
||||
class SystemLanguageView(APIView):
|
||||
url_prefix = (f"{prefix}/language",)
|
||||
|
||||
method_decorators = []
|
||||
|
||||
@auth_abandoned
|
||||
def get(self):
|
||||
"""Get system default language
|
||||
Read from environment variable SYSTEM_DEFAULT_LANGUAGE, default to Chinese if not set
|
||||
"""
|
||||
default_language = os.environ.get("SYSTEM_DEFAULT_LANGUAGE", "")
|
||||
|
||||
return self.jsonify(
|
||||
{
|
||||
"language": default_language,
|
||||
"language_name": self._get_language_name(default_language),
|
||||
}
|
||||
)
|
||||
|
||||
def _get_language_name(self, language_code):
|
||||
"""Return language name based on language code"""
|
||||
language_mapping = {
|
||||
"zh-CN": "中文(简体)",
|
||||
"zh-TW": "中文(繁体)",
|
||||
"en-US": "English",
|
||||
"ja-JP": "日本語",
|
||||
"ko-KR": "한국어",
|
||||
}
|
||||
return language_mapping.get(language_code, "")
|
@@ -5,14 +5,6 @@ if (IS_PROD) {
|
||||
plugins.push('transform-remove-console')
|
||||
}
|
||||
|
||||
// lazy load ant-design-vue
|
||||
// if your use import on Demand, Use this code
|
||||
// plugins.push(['import', {
|
||||
// 'libraryName': 'ant-design-vue',
|
||||
// 'libraryDirectory': 'es',
|
||||
// 'style': true // `style: true` 会加载 less 文件
|
||||
// }])
|
||||
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset',
|
||||
|
@@ -2,7 +2,7 @@ const ThemeColorReplacer = require('webpack-theme-color-replacer')
|
||||
const generate = require('@ant-design/colors/lib/generate').default
|
||||
|
||||
const getAntdSerials = (color) => {
|
||||
// 淡化(即less的tint)
|
||||
// Lighten (similar to less's tint)
|
||||
const lightens = new Array(9).fill().map((t, i) => {
|
||||
return ThemeColorReplacer.varyColor.lighten(color, i / 10)
|
||||
})
|
||||
@@ -13,8 +13,8 @@ const getAntdSerials = (color) => {
|
||||
|
||||
const themePluginOption = {
|
||||
fileName: 'css/theme-colors-[contenthash:8].css',
|
||||
matchColors: getAntdSerials('#2f54eb'), // 主色系列
|
||||
// 改变样式选择器,解决样式覆盖问题
|
||||
matchColors: getAntdSerials('#2f54eb'), // primary color series
|
||||
// change style selectors to solve style override issues
|
||||
changeSelector (selector) {
|
||||
switch (selector) {
|
||||
case '.ant-calendar-today .ant-calendar-date':
|
||||
|
@@ -54,6 +54,180 @@
|
||||
<div class="content unicode" style="display: block;">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">auto</div>
|
||||
<div class="code-name">&#xea28;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">oneterm-http</div>
|
||||
<div class="code-name">&#xea26;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">oneterm-https</div>
|
||||
<div class="code-name">&#xea27;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">access_period</div>
|
||||
<div class="code-name">&#xea25;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">authorization</div>
|
||||
<div class="code-name">&#xea24;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">onterm-symbolic_link</div>
|
||||
<div class="code-name">&#xea23;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">oneterm-batch_execution</div>
|
||||
<div class="code-name">&#xea20;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">oneterm-file_log-selected</div>
|
||||
<div class="code-name">&#xea21;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">oneterm-file_log</div>
|
||||
<div class="code-name">&#xea22;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">file</div>
|
||||
<div class="code-name">&#xea1f;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">folder</div>
|
||||
<div class="code-name">&#xea1e;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">mongoDB (1)</div>
|
||||
<div class="code-name">&#xea1b;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">postgreSQL (1)</div>
|
||||
<div class="code-name">&#xea1c;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">telnet (1)</div>
|
||||
<div class="code-name">&#xea1d;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">command_interception (1)</div>
|
||||
<div class="code-name">&#xea17;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">quick_commands</div>
|
||||
<div class="code-name">&#xea18;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">terminal_settings</div>
|
||||
<div class="code-name">&#xea19;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">basic_settings</div>
|
||||
<div class="code-name">&#xea1a;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">asset_management</div>
|
||||
<div class="code-name">&#xea16;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">ai-seek</div>
|
||||
<div class="code-name">&#xea15;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">ai-hate1</div>
|
||||
<div class="code-name">&#xea13;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">ai-like1</div>
|
||||
<div class="code-name">&#xea14;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">ai-like2</div>
|
||||
<div class="code-name">&#xea11;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">ai-hate2</div>
|
||||
<div class="code-name">&#xea12;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">ai-top_up</div>
|
||||
<div class="code-name">&#xea10;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">ai-top_down</div>
|
||||
<div class="code-name">&#xea0f;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">autoflow-script</div>
|
||||
<div class="code-name">&#xea0d;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">autoflow-dag</div>
|
||||
<div class="code-name">&#xea0e;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">itsm-default_line</div>
|
||||
<div class="code-name">&#xea0c;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">veops-servicetree</div>
|
||||
@@ -6210,9 +6384,9 @@
|
||||
<pre><code class="language-css"
|
||||
>@font-face {
|
||||
font-family: 'iconfont';
|
||||
src: url('iconfont.woff2?t=1735191938771') format('woff2'),
|
||||
url('iconfont.woff?t=1735191938771') format('woff'),
|
||||
url('iconfont.ttf?t=1735191938771') format('truetype');
|
||||
src: url('iconfont.woff2?t=1755240492206') format('woff2'),
|
||||
url('iconfont.woff?t=1755240492206') format('woff'),
|
||||
url('iconfont.ttf?t=1755240492206') format('truetype');
|
||||
}
|
||||
</code></pre>
|
||||
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
|
||||
@@ -6238,6 +6412,267 @@
|
||||
<div class="content font-class">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont auto"></span>
|
||||
<div class="name">
|
||||
auto
|
||||
</div>
|
||||
<div class="code-name">.auto
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont oneterm-http"></span>
|
||||
<div class="name">
|
||||
oneterm-http
|
||||
</div>
|
||||
<div class="code-name">.oneterm-http
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont oneterm-https"></span>
|
||||
<div class="name">
|
||||
oneterm-https
|
||||
</div>
|
||||
<div class="code-name">.oneterm-https
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont ops-oneterm-access_period"></span>
|
||||
<div class="name">
|
||||
access_period
|
||||
</div>
|
||||
<div class="code-name">.ops-oneterm-access_period
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont ops-oneterm-authorization"></span>
|
||||
<div class="name">
|
||||
authorization
|
||||
</div>
|
||||
<div class="code-name">.ops-oneterm-authorization
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont onterm-symbolic_link"></span>
|
||||
<div class="name">
|
||||
onterm-symbolic_link
|
||||
</div>
|
||||
<div class="code-name">.onterm-symbolic_link
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont oneterm-batch_execution"></span>
|
||||
<div class="name">
|
||||
oneterm-batch_execution
|
||||
</div>
|
||||
<div class="code-name">.oneterm-batch_execution
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont ops-oneterm-file_log-selected"></span>
|
||||
<div class="name">
|
||||
oneterm-file_log-selected
|
||||
</div>
|
||||
<div class="code-name">.ops-oneterm-file_log-selected
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont ops-oneterm-file_log"></span>
|
||||
<div class="name">
|
||||
oneterm-file_log
|
||||
</div>
|
||||
<div class="code-name">.ops-oneterm-file_log
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont file"></span>
|
||||
<div class="name">
|
||||
file
|
||||
</div>
|
||||
<div class="code-name">.file
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont folder1"></span>
|
||||
<div class="name">
|
||||
folder
|
||||
</div>
|
||||
<div class="code-name">.folder1
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont a-mongoDB1"></span>
|
||||
<div class="name">
|
||||
mongoDB (1)
|
||||
</div>
|
||||
<div class="code-name">.a-mongoDB1
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont a-postgreSQL1"></span>
|
||||
<div class="name">
|
||||
postgreSQL (1)
|
||||
</div>
|
||||
<div class="code-name">.a-postgreSQL1
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont a-telnet1"></span>
|
||||
<div class="name">
|
||||
telnet (1)
|
||||
</div>
|
||||
<div class="code-name">.a-telnet1
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont ops-oneterm-command_interception1"></span>
|
||||
<div class="name">
|
||||
command_interception (1)
|
||||
</div>
|
||||
<div class="code-name">.ops-oneterm-command_interception1
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont quick_commands"></span>
|
||||
<div class="name">
|
||||
quick_commands
|
||||
</div>
|
||||
<div class="code-name">.quick_commands
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont terminal_settings"></span>
|
||||
<div class="name">
|
||||
terminal_settings
|
||||
</div>
|
||||
<div class="code-name">.terminal_settings
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont basic_settings"></span>
|
||||
<div class="name">
|
||||
basic_settings
|
||||
</div>
|
||||
<div class="code-name">.basic_settings
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont ops-oneterm-asset-management"></span>
|
||||
<div class="name">
|
||||
asset_management
|
||||
</div>
|
||||
<div class="code-name">.ops-oneterm-asset-management
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont ai-seek"></span>
|
||||
<div class="name">
|
||||
ai-seek
|
||||
</div>
|
||||
<div class="code-name">.ai-seek
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont ai-hate1"></span>
|
||||
<div class="name">
|
||||
ai-hate1
|
||||
</div>
|
||||
<div class="code-name">.ai-hate1
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont ai-like1"></span>
|
||||
<div class="name">
|
||||
ai-like1
|
||||
</div>
|
||||
<div class="code-name">.ai-like1
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont ai-like2"></span>
|
||||
<div class="name">
|
||||
ai-like2
|
||||
</div>
|
||||
<div class="code-name">.ai-like2
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont ai-hate2"></span>
|
||||
<div class="name">
|
||||
ai-hate2
|
||||
</div>
|
||||
<div class="code-name">.ai-hate2
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont ai-top_up"></span>
|
||||
<div class="name">
|
||||
ai-top_up
|
||||
</div>
|
||||
<div class="code-name">.ai-top_up
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont ai-top_down"></span>
|
||||
<div class="name">
|
||||
ai-top_down
|
||||
</div>
|
||||
<div class="code-name">.ai-top_down
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont autoflow-script"></span>
|
||||
<div class="name">
|
||||
autoflow-script
|
||||
</div>
|
||||
<div class="code-name">.autoflow-script
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont autoflow-dag"></span>
|
||||
<div class="name">
|
||||
autoflow-dag
|
||||
</div>
|
||||
<div class="code-name">.autoflow-dag
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont itsm-default_line"></span>
|
||||
<div class="name">
|
||||
itsm-default_line
|
||||
</div>
|
||||
<div class="code-name">.itsm-default_line
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont veops-servicetree"></span>
|
||||
<div class="name">
|
||||
@@ -15472,6 +15907,238 @@
|
||||
<div class="content symbol">
|
||||
<ul class="icon_lists dib-box">
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#auto"></use>
|
||||
</svg>
|
||||
<div class="name">auto</div>
|
||||
<div class="code-name">#auto</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#oneterm-http"></use>
|
||||
</svg>
|
||||
<div class="name">oneterm-http</div>
|
||||
<div class="code-name">#oneterm-http</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#oneterm-https"></use>
|
||||
</svg>
|
||||
<div class="name">oneterm-https</div>
|
||||
<div class="code-name">#oneterm-https</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#ops-oneterm-access_period"></use>
|
||||
</svg>
|
||||
<div class="name">access_period</div>
|
||||
<div class="code-name">#ops-oneterm-access_period</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#ops-oneterm-authorization"></use>
|
||||
</svg>
|
||||
<div class="name">authorization</div>
|
||||
<div class="code-name">#ops-oneterm-authorization</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#onterm-symbolic_link"></use>
|
||||
</svg>
|
||||
<div class="name">onterm-symbolic_link</div>
|
||||
<div class="code-name">#onterm-symbolic_link</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#oneterm-batch_execution"></use>
|
||||
</svg>
|
||||
<div class="name">oneterm-batch_execution</div>
|
||||
<div class="code-name">#oneterm-batch_execution</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#ops-oneterm-file_log-selected"></use>
|
||||
</svg>
|
||||
<div class="name">oneterm-file_log-selected</div>
|
||||
<div class="code-name">#ops-oneterm-file_log-selected</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#ops-oneterm-file_log"></use>
|
||||
</svg>
|
||||
<div class="name">oneterm-file_log</div>
|
||||
<div class="code-name">#ops-oneterm-file_log</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#file"></use>
|
||||
</svg>
|
||||
<div class="name">file</div>
|
||||
<div class="code-name">#file</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#folder1"></use>
|
||||
</svg>
|
||||
<div class="name">folder</div>
|
||||
<div class="code-name">#folder1</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#a-mongoDB1"></use>
|
||||
</svg>
|
||||
<div class="name">mongoDB (1)</div>
|
||||
<div class="code-name">#a-mongoDB1</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#a-postgreSQL1"></use>
|
||||
</svg>
|
||||
<div class="name">postgreSQL (1)</div>
|
||||
<div class="code-name">#a-postgreSQL1</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#a-telnet1"></use>
|
||||
</svg>
|
||||
<div class="name">telnet (1)</div>
|
||||
<div class="code-name">#a-telnet1</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#ops-oneterm-command_interception1"></use>
|
||||
</svg>
|
||||
<div class="name">command_interception (1)</div>
|
||||
<div class="code-name">#ops-oneterm-command_interception1</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#quick_commands"></use>
|
||||
</svg>
|
||||
<div class="name">quick_commands</div>
|
||||
<div class="code-name">#quick_commands</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#terminal_settings"></use>
|
||||
</svg>
|
||||
<div class="name">terminal_settings</div>
|
||||
<div class="code-name">#terminal_settings</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#basic_settings"></use>
|
||||
</svg>
|
||||
<div class="name">basic_settings</div>
|
||||
<div class="code-name">#basic_settings</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#ops-oneterm-asset-management"></use>
|
||||
</svg>
|
||||
<div class="name">asset_management</div>
|
||||
<div class="code-name">#ops-oneterm-asset-management</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#ai-seek"></use>
|
||||
</svg>
|
||||
<div class="name">ai-seek</div>
|
||||
<div class="code-name">#ai-seek</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#ai-hate1"></use>
|
||||
</svg>
|
||||
<div class="name">ai-hate1</div>
|
||||
<div class="code-name">#ai-hate1</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#ai-like1"></use>
|
||||
</svg>
|
||||
<div class="name">ai-like1</div>
|
||||
<div class="code-name">#ai-like1</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#ai-like2"></use>
|
||||
</svg>
|
||||
<div class="name">ai-like2</div>
|
||||
<div class="code-name">#ai-like2</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#ai-hate2"></use>
|
||||
</svg>
|
||||
<div class="name">ai-hate2</div>
|
||||
<div class="code-name">#ai-hate2</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#ai-top_up"></use>
|
||||
</svg>
|
||||
<div class="name">ai-top_up</div>
|
||||
<div class="code-name">#ai-top_up</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#ai-top_down"></use>
|
||||
</svg>
|
||||
<div class="name">ai-top_down</div>
|
||||
<div class="code-name">#ai-top_down</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#autoflow-script"></use>
|
||||
</svg>
|
||||
<div class="name">autoflow-script</div>
|
||||
<div class="code-name">#autoflow-script</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#autoflow-dag"></use>
|
||||
</svg>
|
||||
<div class="name">autoflow-dag</div>
|
||||
<div class="code-name">#autoflow-dag</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#itsm-default_line"></use>
|
||||
</svg>
|
||||
<div class="name">itsm-default_line</div>
|
||||
<div class="code-name">#itsm-default_line</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<svg class="icon svg-icon" aria-hidden="true">
|
||||
<use xlink:href="#veops-servicetree"></use>
|
||||
|
@@ -1,8 +1,8 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 3857903 */
|
||||
src: url('iconfont.woff2?t=1735191938771') format('woff2'),
|
||||
url('iconfont.woff?t=1735191938771') format('woff'),
|
||||
url('iconfont.ttf?t=1735191938771') format('truetype');
|
||||
src: url('iconfont.woff2?t=1755240492206') format('woff2'),
|
||||
url('iconfont.woff?t=1755240492206') format('woff'),
|
||||
url('iconfont.ttf?t=1755240492206') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
@@ -13,6 +13,122 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.auto:before {
|
||||
content: "\ea28";
|
||||
}
|
||||
|
||||
.oneterm-http:before {
|
||||
content: "\ea26";
|
||||
}
|
||||
|
||||
.oneterm-https:before {
|
||||
content: "\ea27";
|
||||
}
|
||||
|
||||
.ops-oneterm-access_period:before {
|
||||
content: "\ea25";
|
||||
}
|
||||
|
||||
.ops-oneterm-authorization:before {
|
||||
content: "\ea24";
|
||||
}
|
||||
|
||||
.onterm-symbolic_link:before {
|
||||
content: "\ea23";
|
||||
}
|
||||
|
||||
.oneterm-batch_execution:before {
|
||||
content: "\ea20";
|
||||
}
|
||||
|
||||
.ops-oneterm-file_log-selected:before {
|
||||
content: "\ea21";
|
||||
}
|
||||
|
||||
.ops-oneterm-file_log:before {
|
||||
content: "\ea22";
|
||||
}
|
||||
|
||||
.file:before {
|
||||
content: "\ea1f";
|
||||
}
|
||||
|
||||
.folder1:before {
|
||||
content: "\ea1e";
|
||||
}
|
||||
|
||||
.a-mongoDB1:before {
|
||||
content: "\ea1b";
|
||||
}
|
||||
|
||||
.a-postgreSQL1:before {
|
||||
content: "\ea1c";
|
||||
}
|
||||
|
||||
.a-telnet1:before {
|
||||
content: "\ea1d";
|
||||
}
|
||||
|
||||
.ops-oneterm-command_interception1:before {
|
||||
content: "\ea17";
|
||||
}
|
||||
|
||||
.quick_commands:before {
|
||||
content: "\ea18";
|
||||
}
|
||||
|
||||
.terminal_settings:before {
|
||||
content: "\ea19";
|
||||
}
|
||||
|
||||
.basic_settings:before {
|
||||
content: "\ea1a";
|
||||
}
|
||||
|
||||
.ops-oneterm-asset-management:before {
|
||||
content: "\ea16";
|
||||
}
|
||||
|
||||
.ai-seek:before {
|
||||
content: "\ea15";
|
||||
}
|
||||
|
||||
.ai-hate1:before {
|
||||
content: "\ea13";
|
||||
}
|
||||
|
||||
.ai-like1:before {
|
||||
content: "\ea14";
|
||||
}
|
||||
|
||||
.ai-like2:before {
|
||||
content: "\ea11";
|
||||
}
|
||||
|
||||
.ai-hate2:before {
|
||||
content: "\ea12";
|
||||
}
|
||||
|
||||
.ai-top_up:before {
|
||||
content: "\ea10";
|
||||
}
|
||||
|
||||
.ai-top_down:before {
|
||||
content: "\ea0f";
|
||||
}
|
||||
|
||||
.autoflow-script:before {
|
||||
content: "\ea0d";
|
||||
}
|
||||
|
||||
.autoflow-dag:before {
|
||||
content: "\ea0e";
|
||||
}
|
||||
|
||||
.itsm-default_line:before {
|
||||
content: "\ea0c";
|
||||
}
|
||||
|
||||
.veops-servicetree:before {
|
||||
content: "\ea0b";
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
@@ -5,6 +5,209 @@
|
||||
"css_prefix_text": "",
|
||||
"description": "",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "45254419",
|
||||
"name": "auto",
|
||||
"font_class": "auto",
|
||||
"unicode": "ea28",
|
||||
"unicode_decimal": 59944
|
||||
},
|
||||
{
|
||||
"icon_id": "45069619",
|
||||
"name": "oneterm-http",
|
||||
"font_class": "oneterm-http",
|
||||
"unicode": "ea26",
|
||||
"unicode_decimal": 59942
|
||||
},
|
||||
{
|
||||
"icon_id": "45069616",
|
||||
"name": "oneterm-https",
|
||||
"font_class": "oneterm-https",
|
||||
"unicode": "ea27",
|
||||
"unicode_decimal": 59943
|
||||
},
|
||||
{
|
||||
"icon_id": "44939355",
|
||||
"name": "access_period",
|
||||
"font_class": "ops-oneterm-access_period",
|
||||
"unicode": "ea25",
|
||||
"unicode_decimal": 59941
|
||||
},
|
||||
{
|
||||
"icon_id": "44939354",
|
||||
"name": "authorization",
|
||||
"font_class": "ops-oneterm-authorization",
|
||||
"unicode": "ea24",
|
||||
"unicode_decimal": 59940
|
||||
},
|
||||
{
|
||||
"icon_id": "44501032",
|
||||
"name": "onterm-symbolic_link",
|
||||
"font_class": "onterm-symbolic_link",
|
||||
"unicode": "ea23",
|
||||
"unicode_decimal": 59939
|
||||
},
|
||||
{
|
||||
"icon_id": "44497221",
|
||||
"name": "oneterm-batch_execution",
|
||||
"font_class": "oneterm-batch_execution",
|
||||
"unicode": "ea20",
|
||||
"unicode_decimal": 59936
|
||||
},
|
||||
{
|
||||
"icon_id": "44497220",
|
||||
"name": "oneterm-file_log-selected",
|
||||
"font_class": "ops-oneterm-file_log-selected",
|
||||
"unicode": "ea21",
|
||||
"unicode_decimal": 59937
|
||||
},
|
||||
{
|
||||
"icon_id": "44497219",
|
||||
"name": "oneterm-file_log",
|
||||
"font_class": "ops-oneterm-file_log",
|
||||
"unicode": "ea22",
|
||||
"unicode_decimal": 59938
|
||||
},
|
||||
{
|
||||
"icon_id": "44455092",
|
||||
"name": "file",
|
||||
"font_class": "file",
|
||||
"unicode": "ea1f",
|
||||
"unicode_decimal": 59935
|
||||
},
|
||||
{
|
||||
"icon_id": "44455100",
|
||||
"name": "folder",
|
||||
"font_class": "folder1",
|
||||
"unicode": "ea1e",
|
||||
"unicode_decimal": 59934
|
||||
},
|
||||
{
|
||||
"icon_id": "44315758",
|
||||
"name": "mongoDB (1)",
|
||||
"font_class": "a-mongoDB1",
|
||||
"unicode": "ea1b",
|
||||
"unicode_decimal": 59931
|
||||
},
|
||||
{
|
||||
"icon_id": "44315757",
|
||||
"name": "postgreSQL (1)",
|
||||
"font_class": "a-postgreSQL1",
|
||||
"unicode": "ea1c",
|
||||
"unicode_decimal": 59932
|
||||
},
|
||||
{
|
||||
"icon_id": "44315755",
|
||||
"name": "telnet (1)",
|
||||
"font_class": "a-telnet1",
|
||||
"unicode": "ea1d",
|
||||
"unicode_decimal": 59933
|
||||
},
|
||||
{
|
||||
"icon_id": "44276353",
|
||||
"name": "command_interception (1)",
|
||||
"font_class": "ops-oneterm-command_interception1",
|
||||
"unicode": "ea17",
|
||||
"unicode_decimal": 59927
|
||||
},
|
||||
{
|
||||
"icon_id": "44276352",
|
||||
"name": "quick_commands",
|
||||
"font_class": "quick_commands",
|
||||
"unicode": "ea18",
|
||||
"unicode_decimal": 59928
|
||||
},
|
||||
{
|
||||
"icon_id": "44276351",
|
||||
"name": "terminal_settings",
|
||||
"font_class": "terminal_settings",
|
||||
"unicode": "ea19",
|
||||
"unicode_decimal": 59929
|
||||
},
|
||||
{
|
||||
"icon_id": "44276350",
|
||||
"name": "basic_settings",
|
||||
"font_class": "basic_settings",
|
||||
"unicode": "ea1a",
|
||||
"unicode_decimal": 59930
|
||||
},
|
||||
{
|
||||
"icon_id": "44276278",
|
||||
"name": "asset_management",
|
||||
"font_class": "ops-oneterm-asset-management",
|
||||
"unicode": "ea16",
|
||||
"unicode_decimal": 59926
|
||||
},
|
||||
{
|
||||
"icon_id": "43267802",
|
||||
"name": "ai-seek",
|
||||
"font_class": "ai-seek",
|
||||
"unicode": "ea15",
|
||||
"unicode_decimal": 59925
|
||||
},
|
||||
{
|
||||
"icon_id": "43213714",
|
||||
"name": "ai-hate1",
|
||||
"font_class": "ai-hate1",
|
||||
"unicode": "ea13",
|
||||
"unicode_decimal": 59923
|
||||
},
|
||||
{
|
||||
"icon_id": "43213712",
|
||||
"name": "ai-like1",
|
||||
"font_class": "ai-like1",
|
||||
"unicode": "ea14",
|
||||
"unicode_decimal": 59924
|
||||
},
|
||||
{
|
||||
"icon_id": "43213717",
|
||||
"name": "ai-like2",
|
||||
"font_class": "ai-like2",
|
||||
"unicode": "ea11",
|
||||
"unicode_decimal": 59921
|
||||
},
|
||||
{
|
||||
"icon_id": "43213716",
|
||||
"name": "ai-hate2",
|
||||
"font_class": "ai-hate2",
|
||||
"unicode": "ea12",
|
||||
"unicode_decimal": 59922
|
||||
},
|
||||
{
|
||||
"icon_id": "43139007",
|
||||
"name": "ai-top_up",
|
||||
"font_class": "ai-top_up",
|
||||
"unicode": "ea10",
|
||||
"unicode_decimal": 59920
|
||||
},
|
||||
{
|
||||
"icon_id": "43139017",
|
||||
"name": "ai-top_down",
|
||||
"font_class": "ai-top_down",
|
||||
"unicode": "ea0f",
|
||||
"unicode_decimal": 59919
|
||||
},
|
||||
{
|
||||
"icon_id": "43029539",
|
||||
"name": "autoflow-script",
|
||||
"font_class": "autoflow-script",
|
||||
"unicode": "ea0d",
|
||||
"unicode_decimal": 59917
|
||||
},
|
||||
{
|
||||
"icon_id": "43029538",
|
||||
"name": "autoflow-dag",
|
||||
"font_class": "autoflow-dag",
|
||||
"unicode": "ea0e",
|
||||
"unicode_decimal": 59918
|
||||
},
|
||||
{
|
||||
"icon_id": "42960865",
|
||||
"name": "itsm-default_line",
|
||||
"font_class": "itsm-default_line",
|
||||
"unicode": "ea0c",
|
||||
"unicode_decimal": 59916
|
||||
},
|
||||
{
|
||||
"icon_id": "42930714",
|
||||
"name": "veops-servicetree",
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,7 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-cmn-Hans">
|
||||
<head>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
@@ -12,17 +11,19 @@
|
||||
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %>
|
||||
<link rel="stylesheet" href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" />
|
||||
<% } %>
|
||||
<script>
|
||||
|
||||
const userAgent = navigator.userAgent
|
||||
<script>
|
||||
const userAgent = navigator.userAgent
|
||||
const isEdge = userAgent.indexOf("Edge") > -1
|
||||
const isChrome = userAgent.indexOf("Chrome") > -1 && userAgent.indexOf("Safari") > -1 && !isEdge
|
||||
if (!isChrome) {
|
||||
alert("推荐使用Chrome浏览器, 其他环境下未做严格测试!")
|
||||
const isChrome = userAgent.indexOf("Chrome") > -1 && userAgent.indexOf("Safari") > -1 && !isEdge
|
||||
const lang = (navigator.language || navigator.userLanguage || '').toLowerCase()
|
||||
if (!isChrome) {
|
||||
if (lang.startsWith('zh')) {
|
||||
alert("推荐使用Chrome浏览器,其他环境下未做严格测试!");
|
||||
} else {
|
||||
alert("It is recommended to use Chrome browser. Other environments are not strictly tested!");
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
@@ -30,10 +31,10 @@
|
||||
</noscript>
|
||||
<div id="app">
|
||||
<div id="loading-mask">
|
||||
<div class="loading-wrapper">
|
||||
<span class="loading-dot loading-dot-spin"><i></i><i></i><i></i><i></i></span>
|
||||
</div>
|
||||
<div class="loading-wrapper">
|
||||
<span class="loading-dot loading-dot-spin"><i></i><i></i><i></i><i></i></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- require cdn assets js -->
|
||||
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
|
||||
|
@@ -12,6 +12,7 @@ import zhCN from 'ant-design-vue/lib/locale-provider/zh_CN'
|
||||
import enUS from 'ant-design-vue/lib/locale-provider/en_US'
|
||||
import { AppDeviceEnquire } from '@/utils/mixin'
|
||||
import { debounce } from './utils/util'
|
||||
import { getSystemLanguage } from '@/api/system.js'
|
||||
|
||||
import { h } from 'snabbdom'
|
||||
import { DomEditor, Boot } from '@wangeditor/editor'
|
||||
@@ -45,8 +46,7 @@ export default {
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.SET_LOCALE(localStorage.getItem('ops_locale') || 'zh')
|
||||
this.$i18n.locale = localStorage.getItem('ops_locale') || 'zh'
|
||||
this.initLanguage()
|
||||
this.timer = setInterval(() => {
|
||||
this.setTime(new Date().getTime())
|
||||
}, 1000)
|
||||
@@ -60,133 +60,7 @@ export default {
|
||||
})
|
||||
)
|
||||
|
||||
// 注册富文本自定义元素
|
||||
// const resume = {
|
||||
// type: 'attachment',
|
||||
// attachmentLabel: '',
|
||||
// attachmentValue: '',
|
||||
// children: [{ text: '' }], // void 元素必须有一个 children ,其中只有一个空字符串,重要!!!
|
||||
// }
|
||||
|
||||
function withAttachment(editor) {
|
||||
// JS 语法
|
||||
const { isInline, isVoid } = editor
|
||||
const newEditor = editor
|
||||
|
||||
newEditor.isInline = (elem) => {
|
||||
const type = DomEditor.getNodeType(elem)
|
||||
if (type === 'attachment') return true // 针对 type: attachment ,设置为 inline
|
||||
return isInline(elem)
|
||||
}
|
||||
|
||||
newEditor.isVoid = (elem) => {
|
||||
const type = DomEditor.getNodeType(elem)
|
||||
if (type === 'attachment') return true // 针对 type: attachment ,设置为 void
|
||||
return isVoid(elem)
|
||||
}
|
||||
|
||||
return newEditor // 返回 newEditor ,重要!!!
|
||||
}
|
||||
Boot.registerPlugin(withAttachment)
|
||||
/**
|
||||
* 渲染“附件”元素到编辑器
|
||||
* @param elem 附件元素,即上文的 myResume
|
||||
* @param children 元素子节点,void 元素可忽略
|
||||
* @param editor 编辑器实例
|
||||
* @returns vnode 节点(通过 snabbdom.js 的 h 函数生成)
|
||||
*/
|
||||
function renderAttachment(elem, children, editor) {
|
||||
// JS 语法
|
||||
|
||||
// 获取“附件”的数据,参考上文 myResume 数据结构
|
||||
const { attachmentLabel = '', attachmentValue = '' } = elem
|
||||
|
||||
// 附件元素 vnode
|
||||
const attachVnode = h(
|
||||
// HTML tag
|
||||
'span',
|
||||
// HTML 属性、样式、事件
|
||||
{
|
||||
props: { contentEditable: false }, // HTML 属性,驼峰式写法
|
||||
style: {
|
||||
display: 'inline-block',
|
||||
margin: '0 3px',
|
||||
padding: '0 3px',
|
||||
backgroundColor: '#e6f7ff',
|
||||
border: '1px solid #91d5ff',
|
||||
borderRadius: '2px',
|
||||
color: '#1890ff',
|
||||
}, // style ,驼峰式写法
|
||||
on: {
|
||||
click() {
|
||||
console.log('clicked', attachmentValue)
|
||||
} /* 其他... */,
|
||||
},
|
||||
},
|
||||
// 子节点
|
||||
[attachmentLabel]
|
||||
)
|
||||
|
||||
return attachVnode
|
||||
}
|
||||
const renderElemConf = {
|
||||
type: 'attachment', // 新元素 type ,重要!!!
|
||||
renderElem: renderAttachment,
|
||||
}
|
||||
Boot.registerRenderElem(renderElemConf)
|
||||
|
||||
/**
|
||||
* 生成“附件”元素的 HTML
|
||||
* @param elem 附件元素,即上文的 myResume
|
||||
* @param childrenHtml 子节点的 HTML 代码,void 元素可忽略
|
||||
* @returns “附件”元素的 HTML 字符串
|
||||
*/
|
||||
function attachmentToHtml(elem, childrenHtml) {
|
||||
// JS 语法
|
||||
|
||||
// 获取附件元素的数据
|
||||
const { attachmentValue = '', attachmentLabel = '' } = elem
|
||||
|
||||
// 生成 HTML 代码
|
||||
const html = `<span data-w-e-type="attachment" data-w-e-is-void data-w-e-is-inline data-attachmentValue="${attachmentValue}" data-attachmentLabel="${attachmentLabel}">${attachmentLabel}</span>`
|
||||
|
||||
return html
|
||||
}
|
||||
const elemToHtmlConf = {
|
||||
type: 'attachment', // 新元素的 type ,重要!!!
|
||||
elemToHtml: attachmentToHtml,
|
||||
}
|
||||
Boot.registerElemToHtml(elemToHtmlConf)
|
||||
|
||||
/**
|
||||
* 解析 HTML 字符串,生成“附件”元素
|
||||
* @param domElem HTML 对应的 DOM Element
|
||||
* @param children 子节点
|
||||
* @param editor editor 实例
|
||||
* @returns “附件”元素,如上文的 myResume
|
||||
*/
|
||||
function parseAttachmentHtml(domElem, children, editor) {
|
||||
// JS 语法
|
||||
|
||||
// 从 DOM element 中获取“附件”的信息
|
||||
const attachmentValue = domElem.getAttribute('data-attachmentValue') || ''
|
||||
const attachmentLabel = domElem.getAttribute('data-attachmentLabel') || ''
|
||||
|
||||
// 生成“附件”元素(按照此前约定的数据结构)
|
||||
const myResume = {
|
||||
type: 'attachment',
|
||||
attachmentValue,
|
||||
attachmentLabel,
|
||||
children: [{ text: '' }], // void node 必须有 children ,其中有一个空字符串,重要!!!
|
||||
}
|
||||
|
||||
return myResume
|
||||
}
|
||||
const parseHtmlConf = {
|
||||
selector: 'span[data-w-e-type="attachment"]', // CSS 选择器,匹配特定的 HTML 标签
|
||||
parseElemHtml: parseAttachmentHtml,
|
||||
}
|
||||
Boot.registerParseElemHtml(parseHtmlConf)
|
||||
this.handleEditor()
|
||||
},
|
||||
beforeDestroy() {
|
||||
clearInterval(this.timer)
|
||||
@@ -200,6 +74,141 @@ export default {
|
||||
this.alive = true
|
||||
})
|
||||
},
|
||||
async initLanguage() {
|
||||
let saveLocale = localStorage.getItem('ops_locale')
|
||||
if (!saveLocale) {
|
||||
let requestLanguage = ''
|
||||
try {
|
||||
const languageRes = await getSystemLanguage()
|
||||
requestLanguage = languageRes?.language || ''
|
||||
} catch (e) {
|
||||
console.error('getSystemLanguage error:', e)
|
||||
}
|
||||
|
||||
// request language variable || user local system language
|
||||
const userLanguage = requestLanguage || navigator.language || navigator.userLanguage
|
||||
if (userLanguage.includes('zh')) {
|
||||
saveLocale = 'zh'
|
||||
} else {
|
||||
saveLocale = 'en'
|
||||
}
|
||||
}
|
||||
this.SET_LOCALE(saveLocale)
|
||||
this.$i18n.locale = saveLocale
|
||||
},
|
||||
|
||||
handleEditor() {
|
||||
// register custom rich text element: attachment
|
||||
function withAttachment(editor) {
|
||||
const { isInline, isVoid } = editor
|
||||
const newEditor = editor
|
||||
|
||||
newEditor.isInline = (elem) => {
|
||||
const type = DomEditor.getNodeType(elem)
|
||||
if (type === 'attachment') return true // For type: attachment, set to inline
|
||||
return isInline(elem)
|
||||
}
|
||||
|
||||
newEditor.isVoid = (elem) => {
|
||||
const type = DomEditor.getNodeType(elem)
|
||||
if (type === 'attachment') return true // For type: attachment ,set to void
|
||||
return isVoid(elem)
|
||||
}
|
||||
|
||||
return newEditor // Must return, important!!!
|
||||
}
|
||||
Boot.registerPlugin(withAttachment)
|
||||
/**
|
||||
* Render "attachment" element in editor
|
||||
* @param elem Attachment element
|
||||
* @param children Child nodes (ignored for void elements)
|
||||
* @param editor Editor instance
|
||||
* @returns vnode (generated by snabbdom's h function)
|
||||
*/
|
||||
function renderAttachment(elem, children, editor) {
|
||||
const { attachmentLabel = '', attachmentValue = '' } = elem
|
||||
|
||||
const attachVnode = h(
|
||||
// HTML tag
|
||||
'span',
|
||||
// HTML attr, style, event
|
||||
{
|
||||
props: { contentEditable: false },
|
||||
style: {
|
||||
display: 'inline-block',
|
||||
margin: '0 3px',
|
||||
padding: '0 3px',
|
||||
backgroundColor: '#e6f7ff',
|
||||
border: '1px solid #91d5ff',
|
||||
borderRadius: '2px',
|
||||
color: '#1890ff',
|
||||
},
|
||||
on: {
|
||||
click() {
|
||||
console.log('clicked', attachmentValue)
|
||||
}
|
||||
},
|
||||
},
|
||||
// child node
|
||||
[attachmentLabel]
|
||||
)
|
||||
|
||||
return attachVnode
|
||||
}
|
||||
const renderElemConf = {
|
||||
type: 'attachment',
|
||||
renderElem: renderAttachment,
|
||||
}
|
||||
Boot.registerRenderElem(renderElemConf)
|
||||
|
||||
/**
|
||||
* Generate HTML for "attachment" element
|
||||
* @param elem Attachment element
|
||||
* @param childrenHtml Child HTML (ignored for void elements)
|
||||
* @returns HTML string
|
||||
*/
|
||||
function attachmentToHtml(elem, childrenHtml) {
|
||||
// Getting data for attached elements
|
||||
const { attachmentValue = '', attachmentLabel = '' } = elem
|
||||
|
||||
// generate HTML
|
||||
const html = `<span data-w-e-type="attachment" data-w-e-is-void data-w-e-is-inline data-attachmentValue="${attachmentValue}" data-attachmentLabel="${attachmentLabel}">${attachmentLabel}</span>`
|
||||
|
||||
return html
|
||||
}
|
||||
const elemToHtmlConf = {
|
||||
type: 'attachment',
|
||||
elemToHtml: attachmentToHtml,
|
||||
}
|
||||
Boot.registerElemToHtml(elemToHtmlConf)
|
||||
|
||||
/**
|
||||
* Parse HTML to generate "attachment" element
|
||||
* @param domElem DOM element
|
||||
* @param children Children
|
||||
* @param editor Editor instance
|
||||
* @returns Attachment element
|
||||
*/
|
||||
function parseAttachmentHtml(domElem, children, editor) {
|
||||
// Getting “attachment” information from DOM element
|
||||
const attachmentValue = domElem.getAttribute('data-attachmentValue') || ''
|
||||
const attachmentLabel = domElem.getAttribute('data-attachmentLabel') || ''
|
||||
|
||||
const myResume = {
|
||||
type: 'attachment',
|
||||
attachmentValue,
|
||||
attachmentLabel,
|
||||
children: [{ text: '' }], // The void node must have children with an empty string in it, important!!!!
|
||||
}
|
||||
|
||||
return myResume
|
||||
}
|
||||
const parseHtmlConf = {
|
||||
selector: 'span[data-w-e-type="attachment"]', // CSS selector to match specific HTML tags
|
||||
parseElemHtml: parseAttachmentHtml,
|
||||
}
|
||||
Boot.registerParseElemHtml(parseHtmlConf)
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
8
cmdb-ui/src/api/system.js
Normal file
8
cmdb-ui/src/api/system.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import { axios } from '@/utils/request'
|
||||
|
||||
export function getSystemLanguage() {
|
||||
return axios({
|
||||
url: '/common-setting/v1/system/language',
|
||||
method: 'get',
|
||||
})
|
||||
}
|
@@ -1,41 +1,40 @@
|
||||
import i18n from '@/lang'
|
||||
|
||||
export const ruleTypeList = () => {
|
||||
return [
|
||||
{ value: 'and', label: i18n.t('cmdbFilterComp.and') },
|
||||
{ value: 'or', label: i18n.t('cmdbFilterComp.or') },
|
||||
// { value: 'not', label: '非' },
|
||||
]
|
||||
}
|
||||
|
||||
export const expList = () => {
|
||||
return [
|
||||
{ value: 'is', label: i18n.t('cmdbFilterComp.is') },
|
||||
{ value: '~is', label: i18n.t('cmdbFilterComp.~is') },
|
||||
{ value: 'contain', label: i18n.t('cmdbFilterComp.contain') },
|
||||
{ value: '~contain', label: i18n.t('cmdbFilterComp.~contain') },
|
||||
{ value: 'start_with', label: i18n.t('cmdbFilterComp.start_with') },
|
||||
{ value: '~start_with', label: i18n.t('cmdbFilterComp.~start_with') },
|
||||
{ value: 'end_with', label: i18n.t('cmdbFilterComp.end_with') },
|
||||
{ value: '~end_with', label: i18n.t('cmdbFilterComp.~end_with') },
|
||||
{ value: '~value', label: i18n.t('cmdbFilterComp.~value') }, // 为空的定义有点绕
|
||||
{ value: 'value', label: i18n.t('cmdbFilterComp.value') },
|
||||
]
|
||||
}
|
||||
|
||||
export const advancedExpList = () => {
|
||||
return [
|
||||
{ value: 'in', label: i18n.t('cmdbFilterComp.in') },
|
||||
{ value: '~in', label: i18n.t('cmdbFilterComp.~in') },
|
||||
{ value: 'range', label: i18n.t('cmdbFilterComp.range') },
|
||||
{ value: '~range', label: i18n.t('cmdbFilterComp.~range') },
|
||||
{ value: 'compare', label: i18n.t('cmdbFilterComp.compare') },
|
||||
]
|
||||
}
|
||||
|
||||
export const compareTypeList = [
|
||||
{ value: '1', label: '>' },
|
||||
{ value: '2', label: '>=' },
|
||||
{ value: '3', label: '<' },
|
||||
{ value: '4', label: '<=' },
|
||||
]
|
||||
import i18n from '@/lang'
|
||||
|
||||
export const ruleTypeList = () => {
|
||||
return [
|
||||
{ value: 'and', label: i18n.t('cmdbFilterComp.and') },
|
||||
{ value: 'or', label: i18n.t('cmdbFilterComp.or') },
|
||||
]
|
||||
}
|
||||
|
||||
export const expList = () => {
|
||||
return [
|
||||
{ value: 'is', label: i18n.t('cmdbFilterComp.is') },
|
||||
{ value: '~is', label: i18n.t('cmdbFilterComp.~is') },
|
||||
{ value: 'contain', label: i18n.t('cmdbFilterComp.contain') },
|
||||
{ value: '~contain', label: i18n.t('cmdbFilterComp.~contain') },
|
||||
{ value: 'start_with', label: i18n.t('cmdbFilterComp.start_with') },
|
||||
{ value: '~start_with', label: i18n.t('cmdbFilterComp.~start_with') },
|
||||
{ value: 'end_with', label: i18n.t('cmdbFilterComp.end_with') },
|
||||
{ value: '~end_with', label: i18n.t('cmdbFilterComp.~end_with') },
|
||||
{ value: '~value', label: i18n.t('cmdbFilterComp.~value') },
|
||||
{ value: 'value', label: i18n.t('cmdbFilterComp.value') },
|
||||
]
|
||||
}
|
||||
|
||||
export const advancedExpList = () => {
|
||||
return [
|
||||
{ value: 'in', label: i18n.t('cmdbFilterComp.in') },
|
||||
{ value: '~in', label: i18n.t('cmdbFilterComp.~in') },
|
||||
{ value: 'range', label: i18n.t('cmdbFilterComp.range') },
|
||||
{ value: '~range', label: i18n.t('cmdbFilterComp.~range') },
|
||||
{ value: 'compare', label: i18n.t('cmdbFilterComp.compare') },
|
||||
]
|
||||
}
|
||||
|
||||
export const compareTypeList = [
|
||||
{ value: '1', label: '>' },
|
||||
{ value: '2', label: '>=' },
|
||||
{ value: '3', label: '<' },
|
||||
{ value: '4', label: '<=' },
|
||||
]
|
||||
|
@@ -301,7 +301,7 @@ export default {
|
||||
return [
|
||||
{ value: 'is', label: this.$t('cmdbFilterComp.is') },
|
||||
{ value: '~is', label: this.$t('cmdbFilterComp.~is') },
|
||||
{ value: '~value', label: this.$t('cmdbFilterComp.~value') }, // 为空的定义有点绕
|
||||
{ value: '~value', label: this.$t('cmdbFilterComp.~value') },
|
||||
{ value: 'value', label: this.$t('cmdbFilterComp.value') },
|
||||
]
|
||||
}
|
||||
|
@@ -87,9 +87,11 @@ export default {
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* @param isInitOne When the initialization exp is null, does the ruleList default to giving one
|
||||
*/
|
||||
visibleChange(open, isInitOne = true) {
|
||||
// isInitOne 初始化exp为空时,ruleList是否默认给一条
|
||||
// const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
|
||||
// const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
|
||||
const exp = this.expression.match(new RegExp(this.regQ, 'g'))
|
||||
? this.expression.match(new RegExp(this.regQ, 'g'))[0]
|
||||
: null
|
||||
@@ -204,7 +206,7 @@ export default {
|
||||
},
|
||||
handleSubmit() {
|
||||
if (this.ruleList && this.ruleList.length) {
|
||||
this.ruleList[0].type = 'and' // 增删后,以防万一第一个不是and
|
||||
this.ruleList[0].type = 'and' // after add/delete, just in case the first one is not 'and'
|
||||
this.filterExp = ''
|
||||
const expList = this.ruleList.map((rule) => {
|
||||
let singleRuleExp = ''
|
||||
|
@@ -14,7 +14,7 @@
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 元素折叠过度效果
|
||||
* Collapse transition effect for elements
|
||||
*/
|
||||
export default {
|
||||
name: 'CollapseTransition',
|
||||
@@ -33,20 +33,17 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
collapseBeforeEnter(el) {
|
||||
// console.log('11, collapseBeforeEnter');
|
||||
this.oldPaddingBottom = el.style.paddingBottom
|
||||
this.oldPaddingTop = el.style.paddingTop
|
||||
// 过渡效果开始前设置元素的maxHeight为0,让元素maxHeight有一个初始值
|
||||
// set the element's maxHeight to 0 before the transition effect starts so that the element's maxHeight has an initial value
|
||||
el.style.paddingTop = '0'
|
||||
el.style.paddingBottom = '0'
|
||||
el.style.maxHeight = '0'
|
||||
},
|
||||
collapseEnter(el, done) {
|
||||
// console.log('22, collapseEnter');
|
||||
//
|
||||
this.oldOverflow = el.style.overflow
|
||||
const elHeight = el.scrollHeight
|
||||
// 过渡效果进入后将元素的maxHeight设置为元素本身的高度,将元素maxHeight设置为auto不会有过渡效果
|
||||
// After entering, set maxHeight to the element's height; setting maxHeight to auto will not have a transition effect
|
||||
if (elHeight > 0) {
|
||||
el.style.maxHeight = elHeight + 'px'
|
||||
} else {
|
||||
@@ -59,24 +56,20 @@ export default {
|
||||
// done();
|
||||
const onTransitionDone = function() {
|
||||
done()
|
||||
// console.log('enter onTransitionDone');
|
||||
el.removeEventListener('transitionend', onTransitionDone, false)
|
||||
el.removeEventListener('transitioncancel', onTransitionDone, false)
|
||||
}
|
||||
// 绑定元素的transition完成事件,在transition完成后立即完成vue的过度动效
|
||||
// Bind transition end event to finish Vue's transition immediately after the CSS transition
|
||||
el.addEventListener('transitionend', onTransitionDone, false)
|
||||
el.addEventListener('transitioncancel', onTransitionDone, false)
|
||||
},
|
||||
collapseAfterEnter(el) {
|
||||
// console.log('33, collapseAfterEnter');
|
||||
// 过渡效果完成后恢复元素的maxHeight
|
||||
// Restore maxHeight after transition is complete
|
||||
el.style.maxHeight = ''
|
||||
el.style.overflow = this.oldOverflow
|
||||
},
|
||||
|
||||
collapseBeforeLeave(el) {
|
||||
// console.log('44, collapseBeforeLeave', el.scrollHeight);
|
||||
|
||||
this.oldPaddingBottom = el.style.paddingBottom
|
||||
this.oldPaddingTop = el.style.paddingTop
|
||||
this.oldOverflow = el.style.overflow
|
||||
@@ -85,8 +78,6 @@ export default {
|
||||
el.style.overflow = 'hidden'
|
||||
},
|
||||
collapseLeave(el, done) {
|
||||
// console.log('55, collapseLeave', el.scrollHeight);
|
||||
|
||||
if (el.scrollHeight !== 0) {
|
||||
el.style.maxHeight = '0'
|
||||
el.style.paddingBottom = '0'
|
||||
@@ -95,16 +86,14 @@ export default {
|
||||
// done();
|
||||
const onTransitionDone = function() {
|
||||
done()
|
||||
// console.log('leave onTransitionDone');
|
||||
el.removeEventListener('transitionend', onTransitionDone, false)
|
||||
el.removeEventListener('transitioncancel', onTransitionDone, false)
|
||||
}
|
||||
// 绑定元素的transition完成事件,在transition完成后立即完成vue的过度动效
|
||||
// Bind transition end event to finish Vue's transition immediately after the CSS transition
|
||||
el.addEventListener('transitionend', onTransitionDone, false)
|
||||
el.addEventListener('transitioncancel', onTransitionDone, false)
|
||||
},
|
||||
collapseAfterLeave(el) {
|
||||
// console.log('66, collapseAfterLeave');
|
||||
el.style.maxHeight = ''
|
||||
el.style.overflow = this.oldOverflow
|
||||
el.style.paddingBottom = this.oldPaddingBottom
|
||||
|
@@ -1,9 +1,6 @@
|
||||
/* eslint-disable */
|
||||
/*
|
||||
!!!!!!!
|
||||
以下为凶残的cron表达式验证,胆小肾虚及心脏病者慎入!!!
|
||||
不听劝告者后果自负T T
|
||||
!!!!!!!
|
||||
cron表达式验证
|
||||
cron表达式为秒,分,时,日,月,周,年
|
||||
判断正误方法:错误的话返回错误信息,正确的话返回true
|
||||
*/
|
||||
|
@@ -47,7 +47,7 @@ export const commonIconList = ['changyong-ubuntu',
|
||||
export const linearIconList = [
|
||||
{
|
||||
value: 'database',
|
||||
label: '数据库',
|
||||
label: 'components.database',
|
||||
list: [{
|
||||
value: 'icon-xianxing-DB2',
|
||||
label: 'DB2'
|
||||
@@ -81,7 +81,7 @@ export const linearIconList = [
|
||||
}]
|
||||
}, {
|
||||
value: 'system',
|
||||
label: '操作系统',
|
||||
label: 'components.system',
|
||||
list: [{
|
||||
value: 'icon-xianxing-Windows',
|
||||
label: 'Windows'
|
||||
@@ -106,7 +106,7 @@ export const linearIconList = [
|
||||
}]
|
||||
}, {
|
||||
value: 'language',
|
||||
label: '语言',
|
||||
label: 'components.language',
|
||||
list: [{
|
||||
value: 'icon-xianxing-python',
|
||||
label: 'python'
|
||||
@@ -137,7 +137,7 @@ export const linearIconList = [
|
||||
}]
|
||||
}, {
|
||||
value: 'status',
|
||||
label: '状态',
|
||||
label: 'components.status',
|
||||
list: [{
|
||||
value: 'icon-xianxing-yiwen',
|
||||
label: '疑问'
|
||||
@@ -177,7 +177,7 @@ export const linearIconList = [
|
||||
}]
|
||||
}, {
|
||||
value: 'icon-xianxing-application',
|
||||
label: '常用组件',
|
||||
label: 'components.commonComponent',
|
||||
list: [{
|
||||
value: 'icon-xianxing-yilianjie',
|
||||
label: '已连接'
|
||||
@@ -310,7 +310,7 @@ export const linearIconList = [
|
||||
}]
|
||||
}, {
|
||||
value: 'data',
|
||||
label: '数据',
|
||||
label: 'components.data',
|
||||
list: [{
|
||||
value: 'icon-xianxing-bingzhuangtu',
|
||||
label: '饼状图'
|
||||
@@ -387,7 +387,7 @@ export const linearIconList = [
|
||||
export const fillIconList = [
|
||||
{
|
||||
value: 'database',
|
||||
label: '数据库',
|
||||
label: 'components.database',
|
||||
list: [{
|
||||
value: 'icon-shidi-DB2',
|
||||
label: 'DB2'
|
||||
@@ -421,7 +421,7 @@ export const fillIconList = [
|
||||
}]
|
||||
}, {
|
||||
value: 'system',
|
||||
label: '操作系统',
|
||||
label: 'components.system',
|
||||
list: [{
|
||||
value: 'icon-shidi-Windows',
|
||||
label: 'Windows'
|
||||
@@ -446,7 +446,7 @@ export const fillIconList = [
|
||||
}]
|
||||
}, {
|
||||
value: 'language',
|
||||
label: '语言',
|
||||
label: 'components.language',
|
||||
list: [{
|
||||
value: 'icon-shidi-python',
|
||||
label: 'python'
|
||||
@@ -477,7 +477,7 @@ export const fillIconList = [
|
||||
}]
|
||||
}, {
|
||||
value: 'status',
|
||||
label: '状态',
|
||||
label: 'components.status',
|
||||
list: [{
|
||||
value: 'icon-shidi-yiwen',
|
||||
label: '疑问'
|
||||
@@ -517,7 +517,7 @@ export const fillIconList = [
|
||||
}]
|
||||
}, {
|
||||
value: 'icon-shidi-application',
|
||||
label: '常用组件',
|
||||
label: 'components.commonComponent',
|
||||
list: [{
|
||||
value: 'icon-shidi-yilianjie',
|
||||
label: '已连接'
|
||||
@@ -650,7 +650,7 @@ export const fillIconList = [
|
||||
}]
|
||||
}, {
|
||||
value: 'data',
|
||||
label: '数据',
|
||||
label: 'components.data',
|
||||
list: [{
|
||||
value: 'icon-shidi-bingzhuangtu',
|
||||
label: '饼状图'
|
||||
@@ -727,7 +727,7 @@ export const fillIconList = [
|
||||
export const multicolorIconList = [
|
||||
{
|
||||
value: 'database',
|
||||
label: '数据库',
|
||||
label: 'components.database',
|
||||
list: [{
|
||||
value: 'caise-TIDB',
|
||||
label: 'TIDB'
|
||||
@@ -773,7 +773,7 @@ export const multicolorIconList = [
|
||||
}]
|
||||
}, {
|
||||
value: 'cloud',
|
||||
label: '云',
|
||||
label: 'components.cloud',
|
||||
list: [{
|
||||
value: 'AWS',
|
||||
label: 'AWS'
|
||||
@@ -819,7 +819,7 @@ export const multicolorIconList = [
|
||||
}]
|
||||
}, {
|
||||
value: 'system',
|
||||
label: '操作系统',
|
||||
label: 'components.system',
|
||||
list: [{
|
||||
value: 'ciase-aix',
|
||||
label: 'aix'
|
||||
@@ -847,7 +847,7 @@ export const multicolorIconList = [
|
||||
}]
|
||||
}, {
|
||||
value: 'language',
|
||||
label: '语言',
|
||||
label: 'components.language',
|
||||
list: [{
|
||||
value: 'caise-python',
|
||||
label: 'python'
|
||||
@@ -878,7 +878,7 @@ export const multicolorIconList = [
|
||||
}]
|
||||
}, {
|
||||
value: 'status',
|
||||
label: '状态',
|
||||
label: 'components.status',
|
||||
list: [{
|
||||
value: 'caise-yiwen',
|
||||
label: '疑问'
|
||||
@@ -918,7 +918,7 @@ export const multicolorIconList = [
|
||||
}]
|
||||
}, {
|
||||
value: 'caise-application',
|
||||
label: '常用组件',
|
||||
label: 'components.commonComponent',
|
||||
list: [{
|
||||
value: 'caise-websphere',
|
||||
label: 'WebSphere'
|
||||
@@ -1180,7 +1180,7 @@ export const multicolorIconList = [
|
||||
}]
|
||||
}, {
|
||||
value: 'data',
|
||||
label: '数据',
|
||||
label: 'components.data',
|
||||
list: [{
|
||||
value: 'caise-bingzhuangtu',
|
||||
label: '饼状图'
|
||||
|
@@ -33,7 +33,7 @@
|
||||
<template v-if="iconList && iconList.length">
|
||||
<template v-if="currentIconType !== '4'">
|
||||
<div v-for="category in iconList" :key="category.value">
|
||||
<h4 class="category">{{ category.label }}</h4>
|
||||
<h4 class="category">{{ $t(category.label) }}</h4>
|
||||
<div class="custom-icon-select-popover-content-wrapper">
|
||||
<div
|
||||
v-for="name in category.list"
|
||||
|
60
cmdb-ui/src/components/Ellipsis/Ellipsis.vue
Normal file
60
cmdb-ui/src/components/Ellipsis/Ellipsis.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<script>
|
||||
import Tooltip from 'ant-design-vue/es/tooltip'
|
||||
import { cutStrByFullLength, getStrFullLength } from '@/components/_util/util'
|
||||
/*
|
||||
const isSupportLineClamp = document.body.style.webkitLineClamp !== undefined;
|
||||
|
||||
const TooltipOverlayStyle = {
|
||||
overflowWrap: 'break-word',
|
||||
wordWrap: 'break-word',
|
||||
};
|
||||
*/
|
||||
|
||||
export default {
|
||||
name: 'Ellipsis',
|
||||
components: {
|
||||
Tooltip,
|
||||
},
|
||||
props: {
|
||||
prefixCls: {
|
||||
type: String,
|
||||
default: 'ant-pro-ellipsis',
|
||||
},
|
||||
tooltip: {
|
||||
type: Boolean,
|
||||
},
|
||||
length: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
lines: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
fullWidthRecognition: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getStrDom(str, fullLength) {
|
||||
return <span>{cutStrByFullLength(str, this.length) + (fullLength > this.length ? '...' : '')}</span>
|
||||
},
|
||||
getTooltip(fullStr, fullLength) {
|
||||
return (
|
||||
<Tooltip overlayStyle={{ maxWidth: '700px' }}>
|
||||
<template slot="title">{fullStr}</template>
|
||||
{this.getStrDom(fullStr, fullLength)}
|
||||
</Tooltip>
|
||||
)
|
||||
},
|
||||
},
|
||||
render() {
|
||||
const { tooltip, length } = this.$props
|
||||
const str = this.$slots.default.map((vNode) => vNode.text).join('')
|
||||
const fullLength = getStrFullLength(str)
|
||||
const strDom = tooltip && fullLength > length ? this.getTooltip(str, fullLength) : this.getStrDom(str, fullLength)
|
||||
return strDom
|
||||
},
|
||||
}
|
||||
</script>
|
3
cmdb-ui/src/components/Ellipsis/index.js
Normal file
3
cmdb-ui/src/components/Ellipsis/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import Ellipsis from './Ellipsis'
|
||||
|
||||
export default Ellipsis
|
38
cmdb-ui/src/components/Ellipsis/index.md
Normal file
38
cmdb-ui/src/components/Ellipsis/index.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Ellipsis 文本自动省略号
|
||||
|
||||
文本过长自动处理省略号,支持按照文本长度和最大行数两种方式截取。
|
||||
|
||||
|
||||
|
||||
引用方式:
|
||||
|
||||
```javascript
|
||||
import Ellipsis from '@/components/Ellipsis'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Ellipsis
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 代码演示 [demo](https://pro.loacg.com/test/home)
|
||||
|
||||
```html
|
||||
<ellipsis :length="100" tooltip>
|
||||
There were injuries alleged in three cases in 2015, and a
|
||||
fourth incident in September, according to the safety recall report. After meeting with US regulators in October, the firm decided to issue a voluntary recall.
|
||||
</ellipsis>
|
||||
```
|
||||
|
||||
|
||||
|
||||
## API
|
||||
|
||||
|
||||
参数 | 说明 | 类型 | 默认值
|
||||
----|------|-----|------
|
||||
tooltip | 移动到文本展示完整内容的提示 | boolean | -
|
||||
length | 在按照长度截取下的文本最大字符数,超过则截取省略 | number | -
|
@@ -6,9 +6,9 @@
|
||||
</div>
|
||||
<div class="content">
|
||||
<h1>{{ config[type].title }}</h1>
|
||||
<div class="desc">{{ config[type].desc }}</div>
|
||||
<div class="desc">{{ $t(config[type].desc) }}</div>
|
||||
<div class="actions">
|
||||
<a-button type="primary" @click="handleToHome">返回首页</a-button>
|
||||
<a-button type="primary" @click="handleToHome">{{ $t('exception.backToHome') }}</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -2,17 +2,17 @@ const types = {
|
||||
403: {
|
||||
img: 'https://gw.alipayobjects.com/zos/rmsportal/wZcnGqRDyhPOEYFcZDnb.svg',
|
||||
title: '403',
|
||||
desc: '抱歉,你无权访问该页面'
|
||||
desc: 'exception.desc1'
|
||||
},
|
||||
404: {
|
||||
img: 'https://gw.alipayobjects.com/zos/rmsportal/KpnpchXsobRgLElEozzI.svg',
|
||||
title: '404',
|
||||
desc: '抱歉,你访问的页面不存在或仍在开发中'
|
||||
desc: 'exception.desc2'
|
||||
},
|
||||
500: {
|
||||
img: 'https://gw.alipayobjects.com/zos/rmsportal/RVRUAYdCGeYNBWoKiIwB.svg',
|
||||
title: '500',
|
||||
desc: '抱歉,服务器出错了'
|
||||
desc: 'exception.desc3'
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,58 +0,0 @@
|
||||
<template>
|
||||
<div class="footer">
|
||||
<div class="links">
|
||||
<a
|
||||
href="https://veops.cn/"
|
||||
target="_blank"
|
||||
>维易科技</a>
|
||||
<a
|
||||
href="https://github.com/sendya/ant-design-pro-vue"
|
||||
target="_blank"
|
||||
>
|
||||
<a-icon type="github" />
|
||||
</a>
|
||||
|
||||
</div>
|
||||
<div class="copyright">
|
||||
Copyright
|
||||
<a-icon type="copyright" /> 2021-2023 <span>@维易科技</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'GlobalFooter',
|
||||
data () {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.footer {
|
||||
padding: 0 16px;
|
||||
margin: 48px 0 24px;
|
||||
text-align: center;
|
||||
|
||||
.links {
|
||||
margin-bottom: 8px;
|
||||
|
||||
a {
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
|
||||
&:hover {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.copyright {
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,2 +0,0 @@
|
||||
import GlobalFooter from './GlobalFooter'
|
||||
export default GlobalFooter
|
@@ -1,14 +1,6 @@
|
||||
import router, { resetRouter } from '@/router'
|
||||
import Menu from 'ant-design-vue/es/menu'
|
||||
import Icon from 'ant-design-vue/es/icon'
|
||||
import store from '@/store'
|
||||
import {
|
||||
subscribeCIType,
|
||||
subscribeTreeView,
|
||||
} from '@/modules/cmdb/api/preference'
|
||||
import { searchResourceType } from '@/modules/acl/api/resource'
|
||||
import { roleHasPermissionToGrant } from '@/modules/acl/api/permission'
|
||||
import CMDBGrant from '@/modules/cmdb/components/cmdbGrant'
|
||||
import styles from './index.module.less'
|
||||
import { mapActions } from 'vuex'
|
||||
|
||||
@@ -87,40 +79,6 @@ export default {
|
||||
inject: ['reload'],
|
||||
methods: {
|
||||
...mapActions(['UpdateCMDBSEarchValue']),
|
||||
cancelAttributes(e, menu) {
|
||||
const that = this
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
this.$confirm({
|
||||
title: this.$t('warning'),
|
||||
content: this.$t('cmdb.preference.confirmcancelSub2', { name: menu.meta.title }),
|
||||
onOk() {
|
||||
const citypeId = menu.meta.typeId
|
||||
const unsubCIType = subscribeCIType(citypeId, '')
|
||||
const unsubTree = subscribeTreeView(citypeId, '')
|
||||
Promise.all([unsubCIType, unsubTree]).then(() => {
|
||||
that.$message.success(that.$t('cmdb.preference.cancelSubSuccess'))
|
||||
const lastTypeId = window.localStorage.getItem('ops_ci_typeid') || undefined
|
||||
if (Number(citypeId) === Number(lastTypeId)) {
|
||||
localStorage.setItem('ops_ci_typeid', '')
|
||||
}
|
||||
const href = window.location.href
|
||||
const hrefSplit = href.split('/')
|
||||
if (Number(hrefSplit[hrefSplit.length - 1]) === Number(citypeId)) {
|
||||
that.$router.push('/cmdb/preference')
|
||||
}
|
||||
const roles = store.getters.roles
|
||||
resetRouter()
|
||||
store.dispatch('GenerateRoutes', { roles }, { root: true }).then(() => {
|
||||
router.addRoutes(store.getters.appRoutes)
|
||||
})
|
||||
if (hrefSplit[hrefSplit.length - 1] === 'preference') {
|
||||
that.reload()
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
// select menu item
|
||||
onOpenChange(openKeys) {
|
||||
if (this.mode === 'horizontal') {
|
||||
@@ -170,7 +128,6 @@ export default {
|
||||
return this.$t(`${title}`)
|
||||
},
|
||||
renderMenuItem(menu) {
|
||||
const isShowDot = menu.path.substr(0, 22) === '/cmdb/instances/types/'
|
||||
const target = menu.meta.target || null
|
||||
const tag = target && 'a' || 'router-link'
|
||||
const props = { to: { name: menu.name } }
|
||||
@@ -187,26 +144,11 @@ export default {
|
||||
<tag {...{ props, attrs }}>
|
||||
{this.renderIcon({ icon: menu.meta.icon, customIcon: menu.meta.customIcon, name: menu.meta.name, typeId: menu.meta.typeId, routeName: menu.name, selectedIcon: menu.meta.selectedIcon, })}
|
||||
<span>
|
||||
<span style={menu.meta.style} class={this.renderI18n(menu.meta.title).length > 10 ? 'scroll' : ''}>{this.renderI18n(menu.meta.title)}</span>
|
||||
{isShowDot && !menu.meta.disabled &&
|
||||
<a-popover
|
||||
overlayClassName="custom-menu-extra-submenu"
|
||||
placement="rightTop"
|
||||
arrowPointAtCenter
|
||||
autoAdjustOverflow={false}
|
||||
getPopupContainer={(trigger) => trigger}
|
||||
content={() =>
|
||||
<div>
|
||||
<div onClick={e => this.handlePerm(e, menu, 'CIType')} class="custom-menu-extra-submenu-item"><a-icon type="user-add" />{ this.renderI18n('grant') }</div>
|
||||
<div onClick={e => this.cancelAttributes(e, menu)} class="custom-menu-extra-submenu-item"><a-icon type="star" />{ this.renderI18n('cmdb.preference.cancelSub') }</div>
|
||||
</div>}
|
||||
>
|
||||
<a-icon type="menu" ref="extraEllipsis" class="custom-menu-extra-ellipsis"></a-icon>
|
||||
</a-popover>
|
||||
}
|
||||
<span style={menu.meta.style} class={this.renderI18n(menu.meta.title).length > 10 ? 'scroll' : ''}>
|
||||
{this.renderI18n(menu.meta.title)}
|
||||
</span>
|
||||
</span>
|
||||
</tag>
|
||||
{isShowDot && <CMDBGrant ref="cmdbGrantCIType" resourceType="CIType" app_id="cmdb" />}
|
||||
</Item>
|
||||
)
|
||||
},
|
||||
@@ -269,27 +211,6 @@ export default {
|
||||
)
|
||||
}
|
||||
},
|
||||
handlePerm(e, menu, resource_type_name) {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
roleHasPermissionToGrant({
|
||||
app_id: 'cmdb',
|
||||
resource_type_name,
|
||||
perm: 'grant',
|
||||
resource_name: menu.meta.name,
|
||||
}).then(res => {
|
||||
if (res.result) {
|
||||
console.log(menu)
|
||||
if (resource_type_name === 'CIType') {
|
||||
this.$refs.cmdbGrantCIType.open({ name: menu.meta.name, cmdbGrantType: 'ci', CITypeId: menu.meta?.typeId })
|
||||
} else {
|
||||
this.$refs.cmdbGrantRelationView.open({ name: menu.meta.name, cmdbGrantType: 'relation_view' })
|
||||
}
|
||||
} else {
|
||||
this.$message.error(this.$t('noPermission'))
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
jumpCMDBSearch(value) {
|
||||
this.UpdateCMDBSEarchValue(value)
|
||||
|
@@ -1,10 +0,0 @@
|
||||
import { Spin } from 'ant-design-vue'
|
||||
|
||||
export default {
|
||||
name: 'PageLoading',
|
||||
render () {
|
||||
return (<div style={{ paddingTop: 100, textAlign: 'center' }}>
|
||||
<Spin size="large" />
|
||||
</div>)
|
||||
}
|
||||
}
|
@@ -1,352 +0,0 @@
|
||||
<template>
|
||||
<div class="setting-drawer" ref="settingDrawer">
|
||||
<a-drawer
|
||||
width="300"
|
||||
placement="right"
|
||||
@close="onClose"
|
||||
:closable="false"
|
||||
:visible="visible"
|
||||
>
|
||||
<div class="setting-drawer-index-content">
|
||||
|
||||
<div :style="{ marginBottom: '24px' }">
|
||||
<h3 class="setting-drawer-index-title">整体风格设置</h3>
|
||||
|
||||
<div class="setting-drawer-index-blockChecbox">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
暗色菜单风格
|
||||
</template>
|
||||
<div class="setting-drawer-index-item" @click="handleMenuTheme('dark')">
|
||||
<img src="https://gw.alipayobjects.com/zos/rmsportal/LCkqqYNmvBEbokSDscrm.svg" alt="dark">
|
||||
<div class="setting-drawer-index-selectIcon" v-if="navTheme === 'dark'">
|
||||
<a-icon type="check"/>
|
||||
</div>
|
||||
</div>
|
||||
</a-tooltip>
|
||||
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
亮色菜单风格
|
||||
</template>
|
||||
<div class="setting-drawer-index-item" @click="handleMenuTheme('light')">
|
||||
<img src="https://gw.alipayobjects.com/zos/rmsportal/jpRkZQMyYRryryPNtyIC.svg" alt="light">
|
||||
<div class="setting-drawer-index-selectIcon" v-if="navTheme !== 'dark'">
|
||||
<a-icon type="check"/>
|
||||
</div>
|
||||
</div>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :style="{ marginBottom: '24px' }">
|
||||
<h3 class="setting-drawer-index-title">主题色</h3>
|
||||
|
||||
<div style="height: 20px">
|
||||
<a-tooltip class="setting-drawer-theme-color-colorBlock" v-for="(item, index) in colorList" :key="index">
|
||||
<template slot="title">
|
||||
{{ item.key }}
|
||||
</template>
|
||||
<a-tag :color="item.color" @click="changeColor(item.color)">
|
||||
<a-icon type="check" v-if="item.color === primaryColor"></a-icon>
|
||||
</a-tag>
|
||||
</a-tooltip>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<a-divider />
|
||||
|
||||
<div :style="{ marginBottom: '24px' }">
|
||||
<h3 class="setting-drawer-index-title">导航模式</h3>
|
||||
|
||||
<div class="setting-drawer-index-blockChecbox">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
侧边栏导航
|
||||
</template>
|
||||
<div class="setting-drawer-index-item" @click="handleLayout('sidemenu')">
|
||||
<img src="https://gw.alipayobjects.com/zos/rmsportal/JopDzEhOqwOjeNTXkoje.svg" alt="sidemenu">
|
||||
<div class="setting-drawer-index-selectIcon" v-if="layoutMode === 'sidemenu'">
|
||||
<a-icon type="check"/>
|
||||
</div>
|
||||
</div>
|
||||
</a-tooltip>
|
||||
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
顶部栏导航
|
||||
</template>
|
||||
<div class="setting-drawer-index-item" @click="handleLayout('topmenu')">
|
||||
<img src="https://gw.alipayobjects.com/zos/rmsportal/KDNDBbriJhLwuqMoxcAr.svg" alt="topmenu">
|
||||
<div class="setting-drawer-index-selectIcon" v-if="layoutMode !== 'sidemenu'">
|
||||
<a-icon type="check"/>
|
||||
</div>
|
||||
</div>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<div :style="{ marginTop: '24px' }">
|
||||
<a-list :split="false">
|
||||
<a-list-item>
|
||||
<a-tooltip slot="actions">
|
||||
<template slot="title">
|
||||
该设定仅 [顶部栏导航] 时有效
|
||||
</template>
|
||||
<a-select size="small" style="width: 80px;" :defaultValue="contentWidth" @change="handleContentWidthChange">
|
||||
<a-select-option value="Fixed">固定</a-select-option>
|
||||
<a-select-option value="Fluid" v-if="layoutMode !== 'sidemenu'">流式</a-select-option>
|
||||
</a-select>
|
||||
</a-tooltip>
|
||||
<a-list-item-meta>
|
||||
<div slot="title">内容区域宽度</div>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
<a-switch slot="actions" size="small" :defaultChecked="fixedHeader" @change="handleFixedHeader" />
|
||||
<a-list-item-meta>
|
||||
<div slot="title">固定 Header</div>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
<a-switch slot="actions" size="small" :disabled="!fixedHeader" :defaultChecked="autoHideHeader" @change="handleFixedHeaderHidden" />
|
||||
<a-list-item-meta>
|
||||
<a-tooltip slot="title" placement="left">
|
||||
<template slot="title">固定 Header 时可配置</template>
|
||||
<div :style="{ opacity: !fixedHeader ? '0.5' : '1' }">下滑时隐藏 Header</div>
|
||||
</a-tooltip>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
<a-list-item >
|
||||
<a-switch slot="actions" size="small" :disabled="(layoutMode === 'topmenu')" :defaultChecked="fixSiderbar" @change="handleFixSiderbar" />
|
||||
<a-list-item-meta>
|
||||
<div slot="title" :style="{ textDecoration: layoutMode === 'topmenu' ? 'line-through' : 'unset' }">固定侧边菜单</div>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
</div>
|
||||
</div>
|
||||
<a-divider />
|
||||
|
||||
<div :style="{ marginBottom: '24px' }">
|
||||
<h3 class="setting-drawer-index-title">其他设置</h3>
|
||||
<div>
|
||||
<a-list :split="false">
|
||||
<a-list-item>
|
||||
<a-switch slot="actions" size="small" :defaultChecked="colorWeak" @change="onColorWeak" />
|
||||
<a-list-item-meta>
|
||||
<div slot="title">色弱模式</div>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
<a-list-item>
|
||||
<a-switch slot="actions" size="small" :defaultChecked="multiTab" @change="onMultiTab" />
|
||||
<a-list-item-meta>
|
||||
<div slot="title">多页签模式</div>
|
||||
</a-list-item-meta>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
</div>
|
||||
</div>
|
||||
<a-divider />
|
||||
<div :style="{ marginBottom: '24px' }">
|
||||
<a-button
|
||||
@click="doCopy"
|
||||
icon="copy"
|
||||
block
|
||||
>拷贝设置</a-button>
|
||||
<a-alert type="warning" :style="{ marginTop: '24px' }">
|
||||
<span slot="message">
|
||||
配置栏只在开发环境用于预览,生产环境不会展现,请手动修改配置文件
|
||||
<a href="https://github.com/sendya/ant-design-pro-vue/blob/master/src/config/setting.js" target="_blank">src/config/setting.js</a>
|
||||
</span>
|
||||
</a-alert>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-drawer-index-handle" @click="toggle">
|
||||
<a-icon type="setting" v-if="!visible"/>
|
||||
<a-icon type="close" v-else/>
|
||||
</div>
|
||||
</a-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SettingItem from './SettingItem'
|
||||
import config from '@/config/setting'
|
||||
import { updateTheme, updateColorWeak, colorList } from './settingConfig'
|
||||
import { mixin, mixinDevice } from '@/utils/mixin'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SettingItem
|
||||
},
|
||||
mixins: [mixin, mixinDevice],
|
||||
data () {
|
||||
return {
|
||||
visible: true,
|
||||
colorList
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
||||
},
|
||||
mounted () {
|
||||
const vm = this
|
||||
setTimeout(() => {
|
||||
vm.visible = false
|
||||
}, 16)
|
||||
updateTheme(this.primaryColor)
|
||||
if (this.colorWeak !== config.colorWeak) {
|
||||
updateColorWeak(this.colorWeak)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showDrawer () {
|
||||
this.visible = true
|
||||
},
|
||||
onClose () {
|
||||
this.visible = false
|
||||
},
|
||||
toggle () {
|
||||
this.visible = !this.visible
|
||||
},
|
||||
onColorWeak (checked) {
|
||||
this.$store.dispatch('ToggleWeak', checked)
|
||||
updateColorWeak(checked)
|
||||
},
|
||||
onMultiTab (checked) {
|
||||
this.$store.dispatch('ToggleMultiTab', checked)
|
||||
},
|
||||
handleMenuTheme (theme) {
|
||||
this.$store.dispatch('ToggleTheme', theme)
|
||||
},
|
||||
doCopy () {
|
||||
// get current settings from mixin or this.$store.state.app, pay attention to the property name
|
||||
const text = `export default {
|
||||
primaryColor: '${this.primaryColor}', // primary color of ant design
|
||||
navTheme: '${this.navTheme}', // theme for nav menu
|
||||
layout: '${this.layoutMode}', // nav menu position: sidemenu or topmenu
|
||||
contentWidth: '${this.contentWidth}', // layout of content: Fluid or Fixed, only works when layout is topmenu
|
||||
fixedHeader: ${this.fixedHeader}, // sticky header
|
||||
fixSiderbar: ${this.fixSiderbar}, // sticky siderbar
|
||||
autoHideHeader: ${this.autoHideHeader}, // auto hide header
|
||||
colorWeak: ${this.colorWeak},
|
||||
multiTab: ${this.multiTab},
|
||||
production: process.env.NODE_ENV === 'production' && process.env.VUE_APP_PREVIEW !== 'true',
|
||||
// vue-ls options
|
||||
storageOptions: {
|
||||
namespace: 'pro__',
|
||||
name: 'ls',
|
||||
storage: 'local',
|
||||
}
|
||||
}`
|
||||
this.$copyText(text).then(message => {
|
||||
console.log('copy', message)
|
||||
this.$message.success('复制完毕')
|
||||
}).catch(err => {
|
||||
console.log('copy.err', err)
|
||||
this.$message.error('复制失败')
|
||||
})
|
||||
},
|
||||
handleLayout (mode) {
|
||||
this.$store.dispatch('ToggleLayoutMode', mode)
|
||||
// 因为顶部菜单不能固定左侧菜单栏,所以强制关闭
|
||||
this.handleFixSiderbar(false)
|
||||
},
|
||||
handleContentWidthChange (type) {
|
||||
this.$store.dispatch('ToggleContentWidth', type)
|
||||
},
|
||||
changeColor (color) {
|
||||
if (this.primaryColor !== color) {
|
||||
this.$store.dispatch('ToggleColor', color)
|
||||
updateTheme(color)
|
||||
}
|
||||
},
|
||||
handleFixedHeader (fixed) {
|
||||
this.$store.dispatch('ToggleFixedHeader', fixed)
|
||||
},
|
||||
handleFixedHeaderHidden (autoHidden) {
|
||||
this.$store.dispatch('ToggleFixedHeaderHidden', autoHidden)
|
||||
},
|
||||
handleFixSiderbar (fixed) {
|
||||
if (this.layoutMode === 'topmenu') {
|
||||
this.$store.dispatch('ToggleFixSiderbar', false)
|
||||
return
|
||||
}
|
||||
this.$store.dispatch('ToggleFixSiderbar', fixed)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.setting-drawer-index-content {
|
||||
|
||||
.setting-drawer-index-blockChecbox {
|
||||
display: flex;
|
||||
|
||||
.setting-drawer-index-item {
|
||||
margin-right: 16px;
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
width: 48px;
|
||||
}
|
||||
|
||||
.setting-drawer-index-selectIcon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
padding-top: 15px;
|
||||
padding-left: 24px;
|
||||
height: 100%;
|
||||
color: #1890ff;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
.setting-drawer-theme-color-colorBlock {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 2px;
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
margin-right: 8px;
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
|
||||
i {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.setting-drawer-index-handle {
|
||||
position: absolute;
|
||||
top: 240px;
|
||||
background: #1890ff;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
right: 300px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
z-index: 1001;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
border-radius: 4px 0 0 4px;
|
||||
|
||||
i {
|
||||
color: rgb(255, 255, 255);
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,38 +0,0 @@
|
||||
<template>
|
||||
<div class="setting-drawer-index-item">
|
||||
<h3 class="setting-drawer-index-title">{{ title }}</h3>
|
||||
<slot></slot>
|
||||
<a-divider v-if="divider"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SettingItem',
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
divider: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.setting-drawer-index-item {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.setting-drawer-index-title {
|
||||
font-size: 14px;
|
||||
color: rgba(0, 0, 0, .85);
|
||||
line-height: 22px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
@@ -1,2 +0,0 @@
|
||||
import SettingDrawer from './SettingDrawer'
|
||||
export default SettingDrawer
|
@@ -1,105 +0,0 @@
|
||||
import { message } from 'ant-design-vue/es'
|
||||
// import setting from '../setting';
|
||||
import themeColor from './themeColor.js'
|
||||
|
||||
// let lessNodesAppended
|
||||
|
||||
const colorList = [
|
||||
{
|
||||
key: '薄暮', color: '#F5222D'
|
||||
},
|
||||
{
|
||||
key: '火山', color: '#FA541C'
|
||||
},
|
||||
{
|
||||
key: '日暮', color: '#FAAD14'
|
||||
},
|
||||
{
|
||||
key: '明青', color: '#13C2C2'
|
||||
},
|
||||
{
|
||||
key: '极光绿', color: '#52C41A'
|
||||
},
|
||||
{
|
||||
key: '拂晓蓝(默认)', color: '#1890FF'
|
||||
},
|
||||
{
|
||||
key: '极客蓝', color: '#2F54EB'
|
||||
},
|
||||
{
|
||||
key: '酱紫', color: '#722ED1'
|
||||
}
|
||||
]
|
||||
|
||||
const updateTheme = newPrimaryColor => {
|
||||
const hideMessage = message.loading('正在切换主题!', 0)
|
||||
themeColor.changeColor(newPrimaryColor).finally(t => {
|
||||
hideMessage()
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
const updateTheme = primaryColor => {
|
||||
// Don't compile less in production!
|
||||
/* if (process.env.NODE_ENV === 'production') {
|
||||
return;
|
||||
} * /
|
||||
// Determine if the component is remounted
|
||||
if (!primaryColor) {
|
||||
return
|
||||
}
|
||||
const hideMessage = message.loading('正在编译主题!', 0)
|
||||
function buildIt () {
|
||||
if (!window.less) {
|
||||
return
|
||||
}
|
||||
setTimeout(() => {
|
||||
window.less
|
||||
.modifyVars({
|
||||
'@primary-color': primaryColor
|
||||
})
|
||||
.then(() => {
|
||||
hideMessage()
|
||||
})
|
||||
.catch(() => {
|
||||
message.error('Failed to update theme')
|
||||
hideMessage()
|
||||
})
|
||||
}, 200)
|
||||
}
|
||||
if (!lessNodesAppended) {
|
||||
// insert less.js and color.less
|
||||
const lessStyleNode = document.createElement('link')
|
||||
const lessConfigNode = document.createElement('script')
|
||||
const lessScriptNode = document.createElement('script')
|
||||
lessStyleNode.setAttribute('rel', 'stylesheet/less')
|
||||
lessStyleNode.setAttribute('href', '/color.less')
|
||||
lessConfigNode.innerHTML = `
|
||||
window.less = {
|
||||
async: true,
|
||||
env: 'production',
|
||||
javascriptEnabled: true
|
||||
};
|
||||
`
|
||||
lessScriptNode.src = 'https://gw.alipayobjects.com/os/lib/less.js/3.8.1/less.min.js'
|
||||
lessScriptNode.async = true
|
||||
lessScriptNode.onload = () => {
|
||||
buildIt()
|
||||
lessScriptNode.onload = null
|
||||
}
|
||||
document.body.appendChild(lessStyleNode)
|
||||
document.body.appendChild(lessConfigNode)
|
||||
document.body.appendChild(lessScriptNode)
|
||||
lessNodesAppended = true
|
||||
} else {
|
||||
buildIt()
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
const updateColorWeak = colorWeak => {
|
||||
// document.body.className = colorWeak ? 'colorWeak' : '';
|
||||
colorWeak ? document.body.classList.add('colorWeak') : document.body.classList.remove('colorWeak')
|
||||
}
|
||||
|
||||
export { updateTheme, colorList, updateColorWeak }
|
@@ -1,23 +0,0 @@
|
||||
import client from 'webpack-theme-color-replacer/client'
|
||||
import generate from '@ant-design/colors/lib/generate'
|
||||
|
||||
export default {
|
||||
getAntdSerials (color) {
|
||||
// 淡化(即less的tint)
|
||||
const lightens = new Array(9).fill().map((t, i) => {
|
||||
return client.varyColor.lighten(color, i / 10)
|
||||
})
|
||||
// colorPalette变换得到颜色值
|
||||
const colorPalettes = generate(color)
|
||||
return lightens.concat(colorPalettes)
|
||||
},
|
||||
changeColor (newColor) {
|
||||
var options = {
|
||||
newColors: this.getAntdSerials(newColor), // new colors array, one-to-one corresponde with `matchColors`
|
||||
changeUrl (cssUrl) {
|
||||
return `/${cssUrl}` // while router is not `hash` mode, it needs absolute path
|
||||
}
|
||||
}
|
||||
return client.changer.changeColor(options, Promise)
|
||||
}
|
||||
}
|
@@ -2,10 +2,12 @@ import MultiTab from '@/components/MultiTab'
|
||||
import Result from '@/components/Result'
|
||||
import TagSelect from '@/components/TagSelect'
|
||||
import ExceptionPage from '@/components/Exception'
|
||||
import Ellipsis from '@/components/Ellipsis'
|
||||
|
||||
export {
|
||||
MultiTab,
|
||||
Result,
|
||||
ExceptionPage,
|
||||
TagSelect
|
||||
TagSelect,
|
||||
Ellipsis
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<a-tooltip>
|
||||
<template slot="title">文档中心</template>
|
||||
<template slot="title">{{ $t('documentCenter') }}</template>
|
||||
<span class="document-link">
|
||||
<a-icon type="question-circle" @click="handleClick" />
|
||||
</span>
|
||||
|
@@ -38,7 +38,7 @@
|
||||
<script>
|
||||
import store from '@/store'
|
||||
import { gridSvg, top_agent, top_acl } from '@/core/icons'
|
||||
import { getPreference } from '@/modules/cmdb/api/preference'
|
||||
|
||||
export default {
|
||||
name: 'TopMenu',
|
||||
components: { gridSvg, top_agent, top_acl },
|
||||
@@ -77,18 +77,7 @@ export default {
|
||||
async handleClick(route) {
|
||||
this.visible = false
|
||||
if (route.name !== this.current) {
|
||||
if (route.name === 'cmdb') {
|
||||
const preference = await getPreference()
|
||||
const lastTypeId = window.localStorage.getItem('ops_ci_typeid') || undefined
|
||||
if (lastTypeId && preference.type_ids.some((item) => item === Number(lastTypeId))) {
|
||||
this.$router.push(`/cmdb/instances/types/${lastTypeId}`)
|
||||
} else {
|
||||
this.$router.push('/cmdb/dashboard')
|
||||
}
|
||||
} else {
|
||||
this.$router.push(route.redirect)
|
||||
}
|
||||
// this.current = route.name
|
||||
this.$router.push(route.redirect)
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable */
|
||||
import Vue from 'vue'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
@@ -11,16 +10,23 @@ import i18n from '@/lang'
|
||||
|
||||
NProgress.configure({ showSpinner: false })
|
||||
|
||||
// 不用认证的页面
|
||||
const whitePath = ['/user/login', '/user/logout', '/user/register', '/api/sso/login', '/api/sso/logout', '/user/forgetPassword']
|
||||
// pages that do not require authentication
|
||||
const whitePath = [
|
||||
'/user/login',
|
||||
'/user/logout',
|
||||
'/user/register',
|
||||
'/api/sso/login',
|
||||
'/api/sso/logout',
|
||||
'/user/forgetPassword'
|
||||
]
|
||||
|
||||
// 此处不处理登录, 只处理 是否有用户信息的认证 前端permission的处理 axios处理401 -> 登录
|
||||
// 登录页面处理处理 是否使用单点登录
|
||||
// Only handle user info authentication here, not login logic.
|
||||
// Frontend permission handling; axios handles 401 -> login.
|
||||
// Login page handles whether to use SSO.
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
NProgress.start() // start progress bar
|
||||
to.meta && (!!to.meta.title && setDocumentTitle(`${i18n.t(to.meta.title)} - ${domTitle}`))
|
||||
|
||||
const authed = store.state.authed
|
||||
const auth_type = localStorage.getItem('ops_auth_type')
|
||||
if (whitePath.includes(to.path)) {
|
||||
next()
|
||||
@@ -28,17 +34,17 @@ router.beforeEach(async (to, from, next) => {
|
||||
store.dispatch('GetAuthDataEnable')
|
||||
store.dispatch('GetInfo').then(res => {
|
||||
const roles = res.result && res.result.role
|
||||
store.dispatch("loadAllUsers")
|
||||
store.dispatch("loadAllEmployees")
|
||||
store.dispatch("loadAllDepartments")
|
||||
store.dispatch('loadAllUsers')
|
||||
store.dispatch('loadAllEmployees')
|
||||
store.dispatch('loadAllDepartments')
|
||||
store.dispatch('GenerateRoutes', { roles }).then(() => {
|
||||
router.addRoutes(store.getters.appRoutes)
|
||||
const redirect = decodeURIComponent(from.query.redirect || to.path)
|
||||
if (to.path === redirect) {
|
||||
// hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
|
||||
// Ensure addRoutes is complete, set replace: true so navigation will not leave a history record
|
||||
next({ ...to, replace: true })
|
||||
} else {
|
||||
// 跳转到目的路由
|
||||
// Redirect to the target route
|
||||
next({ path: redirect })
|
||||
}
|
||||
})
|
||||
|
@@ -109,9 +109,29 @@ export default {
|
||||
default: 'default',
|
||||
tip: 'Tip',
|
||||
cmdbSearch: 'Search',
|
||||
requestError: 'An error occurred, please try again later',
|
||||
requestServiceError: 'Unknown error on the server, please contact the administrator',
|
||||
requestWait: 'The modification has been submitted, please wait for review ({time} seconds)',
|
||||
documentCenter: 'Document Center',
|
||||
exception: {
|
||||
backToHome: 'Back to home page',
|
||||
desc1: 'Sorry, you are not authorized to access this page',
|
||||
desc2: 'Sorry, the page you are visiting does not exist or is still under development',
|
||||
desc3: 'Sorry, server error'
|
||||
},
|
||||
pagination: {
|
||||
total: '{range0}-{range1} of {total} items'
|
||||
},
|
||||
components: {
|
||||
colorTagSelectTip: 'Enter or select tags',
|
||||
database: 'Database',
|
||||
system: 'System',
|
||||
language: 'Language',
|
||||
status: 'Status',
|
||||
commonComponent: 'Common Component',
|
||||
data: 'Data',
|
||||
cloud: 'Cloud'
|
||||
},
|
||||
topMenu: {
|
||||
personalCenter: 'My Profile',
|
||||
logout: 'Logout',
|
||||
|
@@ -109,9 +109,29 @@ export default {
|
||||
default: '默认',
|
||||
tip: '提示',
|
||||
cmdbSearch: '搜索一下',
|
||||
requestError: '出现错误,请稍后再试',
|
||||
requestServiceError: '服务端未知错误, 请联系管理员!',
|
||||
requestWait: '修改已提交,请等待审核({time}s)',
|
||||
documentCenter: '文档中心',
|
||||
exception: {
|
||||
backToHome: '返回首页',
|
||||
desc1: '抱歉,你无权访问该页面',
|
||||
desc2: '抱歉,你访问的页面不存在或仍在开发中',
|
||||
desc3: '抱歉,服务器出错了'
|
||||
},
|
||||
pagination: {
|
||||
total: '当前展示 {range0}-{range1} 条数据, 共 {total} 条'
|
||||
},
|
||||
components: {
|
||||
colorTagSelectTip: '选择或输入(回车确定)标签',
|
||||
database: '数据库',
|
||||
system: '操作系统',
|
||||
language: '语言',
|
||||
status: '状态',
|
||||
commonComponent: '常用组件',
|
||||
data: '数据',
|
||||
cloud: '云'
|
||||
},
|
||||
topMenu: {
|
||||
personalCenter: '个人中心',
|
||||
logout: '退出登录',
|
||||
|
@@ -63,8 +63,6 @@ import RouteView from './RouteView'
|
||||
import MultiTab from '@/components/MultiTab'
|
||||
import SideMenu from '@/components/Menu/SideMenu'
|
||||
import GlobalHeader from '@/components/GlobalHeader'
|
||||
import GlobalFooter from '@/components/GlobalFooter'
|
||||
import SettingDrawer from '@/components/SettingDrawer'
|
||||
|
||||
export default {
|
||||
name: 'BasicLayout',
|
||||
@@ -74,8 +72,6 @@ export default {
|
||||
MultiTab,
|
||||
SideMenu,
|
||||
GlobalHeader,
|
||||
GlobalFooter,
|
||||
SettingDrawer,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable */
|
||||
import '@babel/polyfill'
|
||||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
@@ -14,10 +13,9 @@ import i18n from './lang'
|
||||
|
||||
import iconFont from '../public/iconfont/iconfont'
|
||||
|
||||
// 存在直接crash的风险 还未到
|
||||
const customIcon = Icon.createFromIconfontCN(iconFont)
|
||||
Vue.component('ops-icon', customIcon)
|
||||
var vue;
|
||||
var vue
|
||||
|
||||
async function start() {
|
||||
const _vue = new Vue({
|
||||
|
@@ -63,7 +63,14 @@
|
||||
show-quick-jumper
|
||||
:current="tablePage.currentPage"
|
||||
:total="tablePage.total"
|
||||
:show-total="(total, range) => `当前展示 ${range[0]}-${range[1]} 条数据, 共 ${total} 条`"
|
||||
:show-total="
|
||||
(total, range) =>
|
||||
$t('pagination.total', {
|
||||
range0: range[0],
|
||||
range1: range[1],
|
||||
total,
|
||||
})
|
||||
"
|
||||
:page-size="tablePage.pageSize"
|
||||
:default-current="1"
|
||||
:page-size-options="pageSizeOptions"
|
||||
|
@@ -138,7 +138,14 @@
|
||||
show-quick-jumper
|
||||
:current="tablePage.currentPage"
|
||||
:total="tablePage.total"
|
||||
:show-total="(total, range) => `当前展示 ${range[0]}-${range[1]} 条数据, 共 ${total} 条`"
|
||||
:show-total="
|
||||
(total, range) =>
|
||||
$t('pagination.total', {
|
||||
range0: range[0],
|
||||
range1: range[1],
|
||||
total,
|
||||
})
|
||||
"
|
||||
:page-size="tablePage.pageSize"
|
||||
:default-current="1"
|
||||
:page-size-options="pageSizeOptions"
|
||||
|
@@ -107,7 +107,14 @@
|
||||
show-quick-jumper
|
||||
:current="tablePage.currentPage"
|
||||
:total="tablePage.total"
|
||||
:show-total="(total, range) => `当前展示 ${range[0]}-${range[1]} 条数据, 共 ${total} 条`"
|
||||
:show-total="
|
||||
(total, range) =>
|
||||
$t('pagination.total', {
|
||||
range0: range[0],
|
||||
range1: range[1],
|
||||
total,
|
||||
})
|
||||
"
|
||||
:page-size="tablePage.pageSize"
|
||||
:default-current="1"
|
||||
:page-size-options="pageSizeOptions"
|
||||
|
@@ -157,3 +157,18 @@ export function preferenceCitypeOrder(data) {
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
export function getAutoSubscription() {
|
||||
return axios({
|
||||
url: '/v0.1/preference/auto_subscription',
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
export function putAutoSubscription(data) {
|
||||
return axios({
|
||||
url: '/v0.1/preference/auto_subscription',
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
@@ -1,4 +1,9 @@
|
||||
export const getCurrentRowStyle = ({ row }, addedRids) => {
|
||||
const idx = addedRids.findIndex(item => item.rid === row.rid)
|
||||
return idx > -1 ? 'background-color:#E0E7FF!important' : ''
|
||||
}
|
||||
export const getCurrentRowStyle = ({ row }, addedRids) => {
|
||||
const idx = addedRids.findIndex(item => item.rid === row.rid)
|
||||
return idx > -1 ? 'background-color:#E0E7FF!important' : ''
|
||||
}
|
||||
|
||||
export const getCurrentRowClass = ({ row }, addedRids) => {
|
||||
const idx = addedRids.findIndex(item => item.rid === row.rid)
|
||||
return idx > -1 ? 'grant-table-row-focus' : ''
|
||||
}
|
||||
|
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<a-select
|
||||
v-bind="$attrs"
|
||||
v-model="currenCIType"
|
||||
style="width: 100%"
|
||||
:showSearch="true"
|
||||
:filterOption="filterOption"
|
||||
:placeholder="placeholder || $t('placeholder2')"
|
||||
>
|
||||
<a-select-opt-group
|
||||
v-for="group in selectOptions"
|
||||
:key="group.id"
|
||||
:label="group.name || $t('other')"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="type in group.ci_types"
|
||||
:key="type.id"
|
||||
:value="type.id"
|
||||
:data-alias="type.alias"
|
||||
:data-name="type.name"
|
||||
>
|
||||
{{ type.alias || type.name || $t('other') }}
|
||||
<span v-if="type.name" class="select-option-name">({{ type.name }})</span>
|
||||
</a-select-option>
|
||||
</a-select-opt-group>
|
||||
</a-select>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { getCITypeGroupsConfig } from '@/modules/cmdb/api/ciTypeGroup'
|
||||
|
||||
export default {
|
||||
name: 'CMDBTypeSelectAntd',
|
||||
model: {
|
||||
prop: 'value',
|
||||
event: 'change',
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: [String, Number, Array],
|
||||
default: undefined,
|
||||
},
|
||||
CITypeGroup: {
|
||||
type: Array,
|
||||
default: undefined
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectOptions: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currenCIType: {
|
||||
get() {
|
||||
return this.value
|
||||
},
|
||||
set(newVal, oldVal) {
|
||||
if (newVal !== oldVal) {
|
||||
this.$emit('change', newVal)
|
||||
}
|
||||
return newVal
|
||||
},
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
CITypeGroup: {
|
||||
deep: true,
|
||||
handler() {
|
||||
this.handleSelectOptions()
|
||||
}
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
this.handleSelectOptions()
|
||||
},
|
||||
methods: {
|
||||
async handleSelectOptions() {
|
||||
let rawCITypeGroup = []
|
||||
if (this.CITypeGroup) {
|
||||
rawCITypeGroup = this.CITypeGroup
|
||||
} else {
|
||||
rawCITypeGroup = await getCITypeGroupsConfig({ need_other: true })
|
||||
}
|
||||
|
||||
this.selectOptions = _.cloneDeep(rawCITypeGroup).filter((group) => group?.ci_types?.length)
|
||||
},
|
||||
filterOption(input, option) {
|
||||
const attrs = option?.data?.attrs || {}
|
||||
const alias = attrs?.['data-alias']?.toLowerCase?.() || ''
|
||||
const name = attrs?.['data-name']?.toLowerCase?.() || ''
|
||||
return alias.indexOf(input.toLowerCase()) >= 0 || name.indexOf(input.toLowerCase()) >= 0
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.select-option-name {
|
||||
font-size: 12px;
|
||||
color: #A5A9BC;
|
||||
}
|
||||
</style>
|
@@ -81,8 +81,6 @@
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import router, { resetRouter } from '@/router'
|
||||
import store from '@/store'
|
||||
import {
|
||||
subscribeCIType,
|
||||
getSubscribeAttributes,
|
||||
@@ -223,9 +221,8 @@ export default {
|
||||
selectedAttrList.map((item) => {
|
||||
return [item, !!this.fixedList.includes(item)]
|
||||
})
|
||||
).then((res) => {
|
||||
).then(() => {
|
||||
this.$message.success(this.$t('cmdb.components.subSuccess'))
|
||||
this.resetRoute()
|
||||
if (this.selectedAttrList.length > 0) {
|
||||
this.instanceSubscribed = true
|
||||
} else {
|
||||
@@ -233,13 +230,7 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
resetRoute() {
|
||||
resetRouter()
|
||||
const roles = store.getters.roles
|
||||
store.dispatch('GenerateRoutes', { roles }, { root: true }).then(() => {
|
||||
router.addRoutes(store.getters.appRoutes)
|
||||
})
|
||||
},
|
||||
|
||||
setTargetKeys(targetKeys) {
|
||||
this.selectedAttrList = targetKeys
|
||||
},
|
||||
|
@@ -222,12 +222,15 @@ const cmdb_en = {
|
||||
otherGroupTips: 'Non sortable within the other group',
|
||||
filterTips: 'click to show {name}',
|
||||
attributeAssociation: 'Attribute Association',
|
||||
attributeAssociationTip1: 'Automatically establish relationships through attribute values (except password, json, multi-value, long text, boolean, reference) of two models',
|
||||
attributeAssociationTip1: 'Automatically establish relationships through attribute values (except password, json, long text, boolean, reference) of two models',
|
||||
attributeAssociationTip2: 'Double click to edit',
|
||||
attributeAssociationTip3: 'Two Attributes must be selected',
|
||||
attributeAssociationTip4: 'Please select a attribute from Source CIType',
|
||||
attributeAssociationTip5: 'Please select a attribute from Target CIType',
|
||||
attributeAssociationTip6: 'Cannot be deleted again.',
|
||||
attributeAssociationTip7: '1. The attribute value types of the source model and target model must be consistent.',
|
||||
attributeAssociationTip8: '2. One To Many: Source model can select multiple value attributes',
|
||||
attributeAssociationTip9: '3. Many To Many: Either the source model or the target model can be a multi-valued attribute.',
|
||||
show: 'show attribute',
|
||||
setAsShow: 'Set as show attribute',
|
||||
cancelSetAsShow: 'Cancel show attribute',
|
||||
@@ -460,6 +463,20 @@ const cmdb_en = {
|
||||
searchPlaceholder: 'Please search CIType',
|
||||
subCITable: 'Data',
|
||||
subCITree: 'Tree',
|
||||
autoSub: 'Auto Subscription',
|
||||
autoSub2: 'Click To Enable Auto Subscribe',
|
||||
autoSubScope: 'Auto Subscription Scope',
|
||||
subscribeAllModel: 'Subscribe All Models',
|
||||
selectiveSubscription: 'Selective Subscription',
|
||||
excludeGroup: 'Exclude Group',
|
||||
excludeModel: 'Exclude Model',
|
||||
selectGroup: 'Select Group',
|
||||
selectModel: 'Select Model',
|
||||
isEnable: 'Is Enable',
|
||||
enableAutoSubTip: 'After enabling automatic subscription, the model list in the resource data menu will only be based on the results of automatic subscription.',
|
||||
tips1: 'Please go to',
|
||||
tips2: ' Preference ',
|
||||
tips3: 'page first to complete your subscription!'
|
||||
},
|
||||
custom_dashboard: {
|
||||
charts: 'Chart',
|
||||
@@ -721,7 +738,9 @@ if __name__ == "__main__":
|
||||
batchRollbacking: 'Deleting {total} items in total, {successNum} items successful, {errorNum} items failed',
|
||||
baselineTips: 'Changes at this point in time will also be rollbacked, Unique ID, password and dynamic attributes do not support',
|
||||
cover: 'Cover',
|
||||
detail: 'Detail'
|
||||
detail: 'Detail',
|
||||
upstream: 'Upstream',
|
||||
downstream: 'Downstream'
|
||||
},
|
||||
serviceTree: {
|
||||
remove: 'Remove',
|
||||
@@ -746,7 +765,6 @@ if __name__ == "__main__":
|
||||
searchTips: 'Search in service tree'
|
||||
},
|
||||
tree: {
|
||||
tips1: 'Please go to Preference page first to complete your subscription!',
|
||||
subSettings: 'Settings',
|
||||
},
|
||||
topo: {
|
||||
|
@@ -222,12 +222,15 @@ const cmdb_zh = {
|
||||
otherGroupTips: '其他分组属性不可排序',
|
||||
filterTips: '点击可仅查看{name}属性',
|
||||
attributeAssociation: '属性关联',
|
||||
attributeAssociationTip1: '通过2个模型的属性值(除密码、json、多值、长文本、布尔、引用)来自动建立关系',
|
||||
attributeAssociationTip1: '通过2个模型的属性值(除密码、json、长文本、布尔、引用)来自动建立关系',
|
||||
attributeAssociationTip2: '双击可编辑',
|
||||
attributeAssociationTip3: '属性关联必须选择两个属性',
|
||||
attributeAssociationTip4: '请选择原模型属性',
|
||||
attributeAssociationTip5: '请选择目标模型属性',
|
||||
attributeAssociationTip6: '不可再删除',
|
||||
attributeAssociationTip7: '1. 源模型和目标模型的属性值类型必须保持一致',
|
||||
attributeAssociationTip8: '2. 一对多:源模型可选多值属性',
|
||||
attributeAssociationTip9: '3. 多对多:源模型和目标模型其中任何一个可为多值属性',
|
||||
show: '展示属性',
|
||||
setAsShow: '设置为展示属性',
|
||||
cancelSetAsShow: '取消设置为展示属性',
|
||||
@@ -459,6 +462,20 @@ const cmdb_zh = {
|
||||
searchPlaceholder: '请搜索模型',
|
||||
subCITable: '数据订阅',
|
||||
subCITree: '层级订阅',
|
||||
autoSub: '自动订阅',
|
||||
autoSub2: '点击开启自动订阅',
|
||||
autoSubScope: '自动订阅范围',
|
||||
subscribeAllModel: '订阅所有模型',
|
||||
selectiveSubscription: '选择性订阅',
|
||||
excludeGroup: '排除分组',
|
||||
excludeModel: '排除模型',
|
||||
selectGroup: '选择分组',
|
||||
selectModel: '选择模型',
|
||||
isEnable: '是否启用',
|
||||
enableAutoSubTip: '开启自动订阅后,资源数据菜单的模型列表只以自动订阅的结果为准',
|
||||
tips1: '请先到',
|
||||
tips2: ' 我的订阅 ',
|
||||
tips3: '页面完成订阅!',
|
||||
},
|
||||
custom_dashboard: {
|
||||
charts: '图表',
|
||||
@@ -720,7 +737,9 @@ if __name__ == "__main__":
|
||||
batchRollbacking: '正在回滚,共{total}个,成功{successNum}个,失败{errorNum}个',
|
||||
baselineTips: '该时间点的变更也会被回滚, 唯一标识、密码属性、动态属性不支持回滚',
|
||||
cover: '覆盖',
|
||||
detail: '详情'
|
||||
detail: '详情',
|
||||
upstream: '上游',
|
||||
downstream: '下游'
|
||||
},
|
||||
serviceTree: {
|
||||
remove: '移除',
|
||||
@@ -745,7 +764,6 @@ if __name__ == "__main__":
|
||||
searchTips: '在服务树中筛选'
|
||||
},
|
||||
tree: {
|
||||
tips1: '请先到 我的订阅 页面完成订阅!',
|
||||
subSettings: '订阅设置',
|
||||
},
|
||||
topo: {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { RouteView, BasicLayout } from '@/layouts'
|
||||
import { getPreference, getRelationView } from '@/modules/cmdb/api/preference'
|
||||
import { getRelationView } from '@/modules/cmdb/api/preference'
|
||||
|
||||
const genCmdbRoutes = async () => {
|
||||
const routes = {
|
||||
@@ -7,6 +7,7 @@ const genCmdbRoutes = async () => {
|
||||
name: 'cmdb',
|
||||
component: BasicLayout,
|
||||
meta: { title: 'CMDB', keepAlive: false },
|
||||
redirect: '/cmdb/instances/types',
|
||||
children: [
|
||||
// preference
|
||||
// views
|
||||
@@ -39,12 +40,10 @@ const genCmdbRoutes = async () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/cmdb/resourceviews',
|
||||
path: '/cmdb/instances/types/:typeId?',
|
||||
name: 'cmdb_resource_views',
|
||||
component: RouteView,
|
||||
meta: { title: 'cmdb.menu.ciTable', icon: 'ops-cmdb-resource', selectedIcon: 'ops-cmdb-resource', keepAlive: true },
|
||||
hideChildrenInMenu: false,
|
||||
children: []
|
||||
component: () => import(`../views/ci/index`),
|
||||
meta: { title: 'cmdb.menu.ciTable', icon: 'ops-cmdb-resource', selectedIcon: 'ops-cmdb-resource', keepAlive: false }
|
||||
},
|
||||
{
|
||||
path: '/cmdb/tree_views',
|
||||
@@ -71,7 +70,7 @@ const genCmdbRoutes = async () => {
|
||||
{
|
||||
path: '/cmdb/adc',
|
||||
name: 'cmdb_auto_discovery_ci',
|
||||
meta: { title: 'cmdb.menu.adCIs', icon: 'ops-cmdb-adc', selectedIcon: 'ops-cmdb-adc', keepAlive: false },
|
||||
meta: { title: 'cmdb.menu.adCIs', icon: 'ops-cmdb-adc', selectedIcon: 'ops-cmdb-adc', keepAlive: false, permission: ['admin', 'cmdb_admin'] },
|
||||
component: () => import('../views/discoveryCI/index.vue')
|
||||
},
|
||||
{
|
||||
@@ -176,35 +175,8 @@ const genCmdbRoutes = async () => {
|
||||
}
|
||||
]
|
||||
}
|
||||
// Dynamically add subscription items and business relationships
|
||||
const [preference, relation] = await Promise.all([getPreference(), getRelationView()])
|
||||
const resourceViewsIndex = routes.children.findIndex(item => item.name === 'cmdb_resource_views')
|
||||
preference.group_types.forEach(group => {
|
||||
if (preference.group_types.length > 1) {
|
||||
routes.children[resourceViewsIndex].children.push({
|
||||
path: `/cmdb/instances/types/group${group.id}`,
|
||||
name: `cmdb_instances_group_${group.id}`,
|
||||
meta: { title: group.name || 'other', disabled: true, style: 'margin-left: 12px' },
|
||||
})
|
||||
}
|
||||
group.ci_types.forEach(item => {
|
||||
routes.children[resourceViewsIndex].children.push({
|
||||
path: `/cmdb/instances/types/${item.id}`,
|
||||
component: () => import(`../views/ci/index`),
|
||||
name: `cmdb_${item.id}`,
|
||||
meta: { title: item.alias, keepAlive: false, typeId: item.id, name: item.name, customIcon: item.icon },
|
||||
// hideChildrenInMenu: true // Force display of MenuItem instead of SubMenu
|
||||
})
|
||||
})
|
||||
})
|
||||
const lastTypeId = window.localStorage.getItem('ops_ci_typeid') || undefined
|
||||
if (lastTypeId && preference.type_ids.some(item => item === Number(lastTypeId))) {
|
||||
routes.redirect = `/cmdb/instances/types/${lastTypeId}`
|
||||
} else if (routes.children[resourceViewsIndex]?.children?.length > 0) {
|
||||
routes.redirect = routes.children[resourceViewsIndex].children.find(item => !item.hidden && !item.meta.disabled)?.path
|
||||
} else {
|
||||
routes.redirect = '/cmdb/dashboard'
|
||||
}
|
||||
// get service tree dynamic display menu
|
||||
const relation = await getRelationView()
|
||||
|
||||
if (relation?.name2id?.length === 0) {
|
||||
const relationViewRouteIndex = routes.children?.findIndex?.((route) => route.name === 'cmdb_relation_views')
|
||||
|
@@ -1,19 +1,14 @@
|
||||
<template>
|
||||
<div>
|
||||
<p class="cmdb-batch-upload-label"><span>*</span>1. {{ $t('cmdb.batch.selectCIType') }}</p>
|
||||
<a-select
|
||||
showSearch
|
||||
:placeholder="$t('cmdb.batch.selectCITypeTips')"
|
||||
@change="selectCiType"
|
||||
:style="{ width: '50%', marginBottom: '1em' }"
|
||||
class="ops-select"
|
||||
:filter-option="filterOption"
|
||||
<CMDBTypeSelectAntd
|
||||
v-model="selectNum"
|
||||
>
|
||||
<a-select-option v-for="ciType in ciTypeList" :key="ciType.name" :value="ciType.id">{{
|
||||
ciType.alias
|
||||
}}</a-select-option>
|
||||
</a-select>
|
||||
ref="CMDBTypeSelectAntd"
|
||||
:placeholder="$t('cmdb.batch.selectCITypeTips')"
|
||||
:style="{ width: '50%', marginBottom: '1em' }"
|
||||
:CITypeGroup="CITypeGroup"
|
||||
@change="selectCiType"
|
||||
/>
|
||||
<p class="cmdb-batch-upload-label"> 2. {{ $t('cmdb.batch.downloadTemplate') }}</p>
|
||||
<a-button
|
||||
:style="{ marginBottom: '1em' }"
|
||||
@@ -99,16 +94,21 @@ import _ from 'lodash'
|
||||
import { mapState } from 'vuex'
|
||||
import ExcelJS from 'exceljs'
|
||||
import FileSaver from 'file-saver'
|
||||
import { getCITypes } from '@/modules/cmdb/api/CIType'
|
||||
import { getCITypeGroupsConfig } from '@/modules/cmdb/api/ciTypeGroup'
|
||||
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
|
||||
import { getCITypeParent, getCanEditByParentIdChildId } from '@/modules/cmdb/api/CITypeRelation'
|
||||
import { searchPermResourceByRoleId } from '@/modules/acl/api/permission'
|
||||
|
||||
import CMDBTypeSelectAntd from '@/modules/cmdb/components/cmdbTypeSelect/cmdbTypeSelectAntd'
|
||||
|
||||
export default {
|
||||
name: 'CiTypeChoice',
|
||||
components: {
|
||||
CMDBTypeSelectAntd
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
ciTypeList: [],
|
||||
CITypeGroup: [],
|
||||
ciTypeName: '',
|
||||
selectNum: undefined,
|
||||
selectCiTypeAttrList: [],
|
||||
@@ -132,11 +132,16 @@ export default {
|
||||
resource_type_id: 'CIType',
|
||||
app_id: 'cmdb',
|
||||
})
|
||||
getCITypes().then((res) => {
|
||||
this.ciTypeList = res.ci_types.filter((type) => {
|
||||
const _findRe = resources.find((re) => re.name === type.name)
|
||||
return _findRe?.permissions.includes('create') ?? false
|
||||
|
||||
getCITypeGroupsConfig({ need_other: true }).then((res) => {
|
||||
const CITypeGroup = res || []
|
||||
CITypeGroup.forEach((group) => {
|
||||
group.ci_types = (group.ci_types || []).filter((type) => {
|
||||
const _find = resources.find((resource) => resource.name === type.name)
|
||||
return _find?.permissions?.includes?.('create') ?? false
|
||||
})
|
||||
})
|
||||
this.CITypeGroup = CITypeGroup.filter((group) => group?.ci_types?.length)
|
||||
})
|
||||
},
|
||||
watch: {
|
||||
@@ -152,18 +157,27 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
selectCiType(el) {
|
||||
selectCiType(id) {
|
||||
// Callback function when a template type is selected
|
||||
getCITypeAttributesById(el).then((res) => {
|
||||
getCITypeAttributesById(id).then((res) => {
|
||||
this.$emit('getCiTypeAttr', res)
|
||||
this.selectCiTypeAttrList = res
|
||||
})
|
||||
|
||||
this.ciTypeList.forEach((item) => {
|
||||
if (this.selectNum === item.id) {
|
||||
this.ciTypeName = item.alias || item.name
|
||||
}
|
||||
})
|
||||
const selectOptions = this.$refs?.CMDBTypeSelectAntd?.selectOptions
|
||||
let ciTypeName = ''
|
||||
if (selectOptions?.length) {
|
||||
selectOptions.forEach((option) => {
|
||||
if (option?.ci_types?.length) {
|
||||
option.ci_types.forEach((type) => {
|
||||
if (type?.id === id) {
|
||||
ciTypeName = type.alias || type.name
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
this.ciTypeName = ciTypeName
|
||||
},
|
||||
|
||||
openModal() {
|
||||
@@ -188,9 +202,6 @@ export default {
|
||||
this.checkedAttrs = this.selectCiTypeAttrList.attributes.map((item) => item.alias || item.name)
|
||||
})
|
||||
},
|
||||
filterOption(input, option) {
|
||||
return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
},
|
||||
handleCancel() {
|
||||
this.visible = false
|
||||
},
|
||||
|
File diff suppressed because it is too large
Load Diff
833
cmdb-ui/src/modules/cmdb/views/ci/instanceList.vue
Normal file
833
cmdb-ui/src/modules/cmdb/views/ci/instanceList.vue
Normal file
@@ -0,0 +1,833 @@
|
||||
<template>
|
||||
<div id="ciIndex" class="cmdb-ci">
|
||||
<a-spin :tip="loadTip" :spinning="loading" >
|
||||
<div class="cmdb-views-header">
|
||||
<span>
|
||||
<span class="cmdb-views-header-title">{{ CIType.alias || CIType.name }}</span>
|
||||
<span
|
||||
@click="
|
||||
() => {
|
||||
$refs.metadataDrawer.open(typeId)
|
||||
}
|
||||
"
|
||||
class="cmdb-views-header-metadata"
|
||||
>
|
||||
<a-icon type="info-circle" />{{ $t('cmdb.ci.attributeDesc') }}
|
||||
</span>
|
||||
</span>
|
||||
<a-space>
|
||||
<a-button
|
||||
type="primary"
|
||||
class="ops-button-ghost"
|
||||
ghost
|
||||
@click="$refs.create.handleOpen(true, 'create')"
|
||||
><ops-icon type="veops-increase" />
|
||||
{{ $t('create') }}
|
||||
</a-button>
|
||||
<EditAttrsPopover :typeId="typeId" class="operation-icon" @refresh="refreshAfterEditAttrs">
|
||||
<a-button
|
||||
type="primary"
|
||||
ghost
|
||||
class="ops-button-ghost"
|
||||
><ops-icon type="veops-configuration_table" />{{ $t('cmdb.configTable') }}</a-button
|
||||
>
|
||||
</EditAttrsPopover>
|
||||
<a-dropdown v-model="visible">
|
||||
<a-button type="primary" ghost class="ops-button-ghost">···</a-button>
|
||||
<a-menu slot="overlay" @click="handleMenuClick">
|
||||
<a-menu-item @click="handlePerm" key="grant">
|
||||
<a-icon type="user-add" />
|
||||
{{ $t('grant') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item
|
||||
v-if="!autoSub.enabled"
|
||||
key="cancelSub"
|
||||
@click="unsubscribe"
|
||||
>
|
||||
<a-icon type="star" />
|
||||
{{ $t('cmdb.preference.cancelSub') }}
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
</a-space>
|
||||
</div>
|
||||
<div class="cmdb-ci-main">
|
||||
<SearchForm
|
||||
ref="search"
|
||||
@refresh="handleSearch"
|
||||
:preferenceAttrList="preferenceAttrList"
|
||||
:typeId="typeId"
|
||||
:selectedRowKeys="selectedRowKeys"
|
||||
@copyExpression="copyExpression"
|
||||
>
|
||||
<PreferenceSearch
|
||||
ref="preferenceSearch"
|
||||
v-show="!selectedRowKeys.length"
|
||||
@getQAndSort="getQAndSort"
|
||||
@setParamsFromPreferenceSearch="setParamsFromPreferenceSearch"
|
||||
/>
|
||||
<div class="ops-list-batch-action" v-show="!!selectedRowKeys.length">
|
||||
<span @click="$refs.create.handleOpen(true, 'update')">{{ $t('update') }}</span>
|
||||
<a-divider type="vertical" />
|
||||
<span @click="openBatchDownload">{{ $t('download') }}</span>
|
||||
<a-divider type="vertical" />
|
||||
<span @click="batchDelete">{{ $t('delete') }}</span>
|
||||
<a-divider type="vertical" />
|
||||
<span @click="batchRollback">{{ $t('cmdb.ci.rollback') }}</span>
|
||||
<span>{{ $t('cmdb.ci.selectRows', { rows: selectedRowKeys.length }) }}</span>
|
||||
</div>
|
||||
</SearchForm>
|
||||
<CiDetailDrawer ref="detail" :typeId="typeId" />
|
||||
|
||||
<CITable
|
||||
ref="xTable"
|
||||
:id="`cmdb-ci-${typeId}`"
|
||||
:loading="loading"
|
||||
:attrList="preferenceAttrList"
|
||||
:columns="columns"
|
||||
:passwordValue="passwordValue"
|
||||
:data="instanceList"
|
||||
:height="tableHeight"
|
||||
@onSelectChange="onSelectChange"
|
||||
@edit-closed="handleEditClose"
|
||||
@edit-actived="handleEditActived"
|
||||
@sort-change="handleSortCol"
|
||||
@openDetail="openDetail"
|
||||
@deleteCI="deleteCI"
|
||||
/>
|
||||
|
||||
<div :style="{ textAlign: 'right', marginTop: '4px' }">
|
||||
<a-pagination
|
||||
:showSizeChanger="true"
|
||||
:current="currentPage"
|
||||
size="small"
|
||||
:total="totalNumber"
|
||||
show-quick-jumper
|
||||
:page-size="pageSize"
|
||||
:page-size-options="pageSizeOptions"
|
||||
@showSizeChange="onShowSizeChange"
|
||||
:show-total="
|
||||
(total, range) =>
|
||||
$t('pagination.total', {
|
||||
range0: range[0],
|
||||
range1: range[1],
|
||||
total,
|
||||
})
|
||||
"
|
||||
@change="
|
||||
(page) => {
|
||||
currentPage = page
|
||||
}
|
||||
"
|
||||
>
|
||||
<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>
|
||||
<create-instance-form
|
||||
ref="create"
|
||||
:typeIdFromProp="typeId"
|
||||
@reload="reloadData"
|
||||
@submit="batchUpdate"
|
||||
/>
|
||||
<BatchDownload ref="batchDownload" @batchDownload="batchDownload" />
|
||||
<CiRollbackForm ref="ciRollbackForm" @batchRollbackAsync="batchRollbackAsync($event)" :ciIds="selectedRowKeys" />
|
||||
<MetadataDrawer ref="metadataDrawer" />
|
||||
<CMDBGrant ref="cmdbGrant" resourceTypeName="CIType" app_id="cmdb" />
|
||||
</div>
|
||||
</a-spin>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import Sortable from 'sortablejs'
|
||||
|
||||
import { searchCI, updateCI, deleteCI } from '@/modules/cmdb/api/ci'
|
||||
import { getSubscribeAttributes, subscribeCIType, subscribeTreeView } from '@/modules/cmdb/api/preference'
|
||||
import { getCITypeAttributesById, getAttrPassword } from '@/modules/cmdb/api/CITypeAttr'
|
||||
import { roleHasPermissionToGrant } from '@/modules/acl/api/permission'
|
||||
import { searchResourceType } from '@/modules/acl/api/resource'
|
||||
import { CIBaselineRollback } from '@/modules/cmdb/api/history'
|
||||
|
||||
import { getCITableColumns } from '../../utils/helper'
|
||||
import { intersection } from '@/utils/functions/set'
|
||||
import BatchDownload from '../../components/batchDownload/batchDownload.vue'
|
||||
import PreferenceSearch from '../../components/preferenceSearch/preferenceSearch.vue'
|
||||
import MetadataDrawer from './modules/MetadataDrawer.vue'
|
||||
import CMDBGrant from '../../components/cmdbGrant'
|
||||
import CiRollbackForm from './modules/ciRollbackForm.vue'
|
||||
import SearchForm from '@/modules/cmdb/components/searchForm/SearchForm.vue'
|
||||
import CreateInstanceForm from './modules/CreateInstanceForm'
|
||||
import CiDetailDrawer from './modules/ciDetailDrawer.vue'
|
||||
import EditAttrsPopover from './modules/editAttrsPopover'
|
||||
import CITable from '@/modules/cmdb/components/ciTable/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'InstanceList',
|
||||
components: {
|
||||
SearchForm,
|
||||
CreateInstanceForm,
|
||||
CiDetailDrawer,
|
||||
EditAttrsPopover,
|
||||
BatchDownload,
|
||||
PreferenceSearch,
|
||||
MetadataDrawer,
|
||||
CMDBGrant,
|
||||
CiRollbackForm,
|
||||
CITable
|
||||
},
|
||||
props: {
|
||||
typeId: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
},
|
||||
CIType: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
autoSub: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
windowHeight() {
|
||||
return this.$store.state.windowHeight
|
||||
},
|
||||
tableHeight() {
|
||||
// if (this.selectedRowKeys && this.selectedRowKeys.length) {
|
||||
// return this.windowHeight - 246
|
||||
// }
|
||||
return this.windowHeight - 240
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tableData: [],
|
||||
loading: false,
|
||||
currentPage: 1,
|
||||
pageSizeOptions: ['50', '100', '200', '100000'],
|
||||
pageSize: 50,
|
||||
totalNumber: 0,
|
||||
loadTip: '',
|
||||
form: this.$form.createForm(this),
|
||||
preferenceAttrList: [],
|
||||
|
||||
instanceList: [],
|
||||
columns: [],
|
||||
// custom table alert & rowSelection
|
||||
selectedRowKeys: [],
|
||||
// Check whether to edit
|
||||
initialInstanceList: [],
|
||||
sortByTable: undefined,
|
||||
isEditActive: false,
|
||||
attrList: [],
|
||||
attributes: {},
|
||||
// Table drag parameters
|
||||
tableDragClassName: [],
|
||||
|
||||
resource_type: {},
|
||||
|
||||
initialPasswordValue: {},
|
||||
passwordValue: {},
|
||||
lastEditCiId: null,
|
||||
isContinueCloseEdit: true,
|
||||
visible: false,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
currentPage: function(newVal, oldVal) {
|
||||
this.loadTableData(this.sortByTable)
|
||||
},
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
handleSearch: this.handleSearch,
|
||||
setPreferenceSearchCurrent: this.setPreferenceSearchCurrent,
|
||||
attrList: () => {
|
||||
return this.attrList
|
||||
},
|
||||
attributes: () => {
|
||||
return this.attributes
|
||||
},
|
||||
filterCompPreferenceSearch: () => {
|
||||
return { type_id: this.typeId }
|
||||
},
|
||||
resource_type: () => {
|
||||
return this.resource_type
|
||||
}
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
this.loading = true
|
||||
await this.getAttributeList()
|
||||
await this.loadPreferenceAttrList()
|
||||
await this.loadTableData()
|
||||
this.loading = false
|
||||
|
||||
this.$nextTick(() => {
|
||||
const loadingNode = document.getElementsByClassName('ant-drawer-mask')
|
||||
if (loadingNode?.style) {
|
||||
loadingNode.style.zIndex = 8
|
||||
}
|
||||
})
|
||||
setTimeout(() => {
|
||||
this.columnDrop()
|
||||
}, 1000)
|
||||
},
|
||||
beforeDestroy() {
|
||||
// window.onkeypress = null
|
||||
if (this.sortable) {
|
||||
this.sortable.destroy()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async getAttributeList() {
|
||||
await getCITypeAttributesById(this.typeId).then((res) => {
|
||||
this.attrList = res.attributes
|
||||
this.attributes = res
|
||||
})
|
||||
},
|
||||
handleSearch() {
|
||||
this.$refs.xTable.getVxetableRef().clearSort()
|
||||
this.sortByTable = undefined
|
||||
this.$nextTick(() => {
|
||||
if (this.currentPage === 1) {
|
||||
this.reloadData()
|
||||
} else {
|
||||
this.currentPage = 1
|
||||
}
|
||||
})
|
||||
},
|
||||
async loadTableData(sortByTable = undefined) {
|
||||
try {
|
||||
this.loading = true
|
||||
// If fuzzy search is possible, queryParam can be deleted later.
|
||||
// const queryParams = this.$refs['search'].queryParam || {}
|
||||
const fuzzySearch = this.$refs['search'].fuzzySearch
|
||||
const expression = this.$refs['search'].expression || ''
|
||||
const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
|
||||
const regSort = /(?<=sort=).+/g
|
||||
|
||||
const exp = expression.match(regQ) ? expression.match(regQ)[0] : null
|
||||
let sort
|
||||
if (sortByTable) {
|
||||
sort = sortByTable
|
||||
} else {
|
||||
sort = expression.match(regSort) ? expression.match(regSort)[0] : undefined
|
||||
}
|
||||
const res = await searchCI({
|
||||
q: `_type:${this.typeId}${exp ? `,${exp}` : ''}${fuzzySearch ? `,*${fuzzySearch}*` : ''}`,
|
||||
count: this.pageSize,
|
||||
page: this.currentPage,
|
||||
sort,
|
||||
})
|
||||
this.totalNumber = res['numfound']
|
||||
this.columns = this.getColumns(res.result, this.preferenceAttrList)
|
||||
this.columns.forEach((col) => {
|
||||
if (col.is_password) {
|
||||
this.initialPasswordValue[col.field] = ''
|
||||
this.passwordValue[col.field] = ''
|
||||
}
|
||||
})
|
||||
const jsonAttrList = this.attrList.filter((attr) => attr.value_type === '6')
|
||||
this.instanceList = res['result'].map((item) => {
|
||||
jsonAttrList.forEach(
|
||||
(jsonAttr) => (item[jsonAttr.name] = item[jsonAttr.name] ? JSON.stringify(item[jsonAttr.name]) : '')
|
||||
)
|
||||
return { ..._.cloneDeep(item) }
|
||||
})
|
||||
this.initialInstanceList = _.cloneDeep(this.instanceList)
|
||||
this.$nextTick(() => {
|
||||
// this.setSelectRows()
|
||||
this.$refs.xTable.getVxetableRef().refreshColumn()
|
||||
})
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
getColumns(data, attrList) {
|
||||
const width = document.getElementById('ciIndex').clientWidth - 50
|
||||
return getCITableColumns(data, attrList, width)
|
||||
},
|
||||
setSelectRows() {
|
||||
const cached = new Set(this.selectedRowKeys)
|
||||
const loaded = new Set(this.instanceList.map((i) => i.ci_id || i._id))
|
||||
|
||||
const inter = Array.from(intersection(cached, loaded))
|
||||
|
||||
if (inter.length === this.instanceList.length) {
|
||||
this.$refs['xTable'].getVxetableRef().setAllCheckboxRow(true)
|
||||
} else {
|
||||
const rows = []
|
||||
inter.forEach((rid) => {
|
||||
rows.push(this.$refs['xTable'].getVxetableRef().getRowById(rid))
|
||||
})
|
||||
this.$refs['xTable'].getVxetableRef().setCheckboxRow(rows, true)
|
||||
}
|
||||
},
|
||||
async loadPreferenceAttrList() {
|
||||
const subscribed = await getSubscribeAttributes(this.typeId)
|
||||
this.preferenceAttrList = subscribed.attributes // All columns that have been subscribed
|
||||
},
|
||||
onSelectChange(records) {
|
||||
this.selectedRowKeys = records.map((i) => i.ci_id || i._id)
|
||||
},
|
||||
reloadData() {
|
||||
this.loadTableData()
|
||||
},
|
||||
|
||||
handleEditClose({ row, rowIndex, column }) {
|
||||
if (!this.isContinueCloseEdit) {
|
||||
return
|
||||
}
|
||||
const $table = this.$refs['xTable'].getVxetableRef()
|
||||
const data = {}
|
||||
this.columns.forEach((item) => {
|
||||
if (
|
||||
!(item.field in this.initialPasswordValue) &&
|
||||
!_.isEqual(row[item.field], this.initialInstanceList[rowIndex][item.field])
|
||||
) {
|
||||
data[item.field] = row[item.field] ?? null
|
||||
}
|
||||
})
|
||||
Object.keys(this.initialPasswordValue).forEach((key) => {
|
||||
if (this.initialPasswordValue[key] !== this.passwordValue[key]) {
|
||||
data[key] = this.passwordValue[key]
|
||||
row[key] = this.passwordValue[key]
|
||||
}
|
||||
})
|
||||
this.isEditActive = false
|
||||
this.lastEditCiId = null
|
||||
if (JSON.stringify(data) !== '{}') {
|
||||
updateCI(row.ci_id || row._id, data)
|
||||
.then(() => {
|
||||
this.$message.success(this.$t('saveSuccess'))
|
||||
$table.reloadRow(row, null)
|
||||
const _initialInstanceList = _.cloneDeep(this.initialInstanceList)
|
||||
_initialInstanceList[rowIndex] = {
|
||||
..._initialInstanceList[rowIndex],
|
||||
...data,
|
||||
}
|
||||
this.initialInstanceList = _initialInstanceList
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
this.loadTableData()
|
||||
})
|
||||
}
|
||||
this.columns.forEach((col) => {
|
||||
if (col.is_password) {
|
||||
this.initialPasswordValue[col.field] = ''
|
||||
this.passwordValue[col.field] = ''
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async openBatchDownload() {
|
||||
this.$refs.batchDownload.open({
|
||||
preferenceAttrList: this.preferenceAttrList.filter((attr) => !attr?.is_reference),
|
||||
ciTypeName: this.CIType.alias || this.CIType.name,
|
||||
})
|
||||
},
|
||||
batchDownload({ filename, type, checkedKeys }) {
|
||||
const jsonAttrList = []
|
||||
checkedKeys.forEach((key) => {
|
||||
const _find = this.attrList.find((attr) => attr.name === key)
|
||||
if (_find && _find.value_type === '6') jsonAttrList.push(key)
|
||||
})
|
||||
const data = _.cloneDeep([
|
||||
...this.$refs.xTable.getVxetableRef().getCheckboxReserveRecords(),
|
||||
...this.$refs.xTable.getVxetableRef().getCheckboxRecords(true),
|
||||
])
|
||||
this.$refs.xTable.getVxetableRef().exportData({
|
||||
filename,
|
||||
type,
|
||||
columnFilterMethod({ column }) {
|
||||
return checkedKeys.includes(column.property)
|
||||
},
|
||||
data: [
|
||||
...data.map((item) => {
|
||||
jsonAttrList.forEach((jsonAttr) => (item[jsonAttr] = item[jsonAttr] ? JSON.stringify(item[jsonAttr]) : ''))
|
||||
return { ...item }
|
||||
}),
|
||||
],
|
||||
})
|
||||
this.selectedRowKeys = []
|
||||
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
|
||||
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
|
||||
},
|
||||
batchUpdate(values) {
|
||||
const that = this
|
||||
this.$confirm({
|
||||
title: that.$t('warning'),
|
||||
content: that.$t('cmdb.ci.batchUpdateConfirm'),
|
||||
async onOk() {
|
||||
that.batchUpdateAsync(values)
|
||||
},
|
||||
})
|
||||
},
|
||||
async batchUpdateAsync(values) {
|
||||
let successNum = 0
|
||||
let errorNum = 0
|
||||
this.loading = true
|
||||
this.loadTip = this.$t('cmdb.ci.batchUpdateInProgress') + '...'
|
||||
const payload = {}
|
||||
Object.keys(values).forEach((key) => {
|
||||
// Field values support blanking
|
||||
// There are currently field values that do not support blanking and will be returned by the backend.
|
||||
if (values[key] === undefined || values[key] === null) {
|
||||
payload[key] = null
|
||||
} else {
|
||||
payload[key] = values[key]
|
||||
}
|
||||
})
|
||||
this.$refs.create.visible = false
|
||||
const key = 'updatable'
|
||||
let errorMsg = ''
|
||||
for (let i = 0; i < this.selectedRowKeys.length; i++) {
|
||||
await updateCI(this.selectedRowKeys[i], payload, false)
|
||||
.then(() => {
|
||||
successNum += 1
|
||||
})
|
||||
.catch((error) => {
|
||||
errorMsg = errorMsg + '\n' + `${this.selectedRowKeys[i]}:${error.response?.data?.message ?? ''}`
|
||||
this.$notification.warning({
|
||||
key,
|
||||
message: this.$t('warning'),
|
||||
description: errorMsg,
|
||||
duration: 0,
|
||||
style: { whiteSpace: 'break-spaces', overflow: 'auto', maxHeight: this.windowHeight - 80 + 'px' },
|
||||
})
|
||||
errorNum += 1
|
||||
})
|
||||
.finally(() => {
|
||||
this.loadTip = this.$t('cmdb.ci.batchUpdateInProgress2', {
|
||||
total: this.selectedRowKeys.length,
|
||||
successNum: successNum,
|
||||
errorNum: errorNum,
|
||||
})
|
||||
})
|
||||
}
|
||||
this.loading = false
|
||||
this.loadTip = ''
|
||||
this.selectedRowKeys = []
|
||||
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
|
||||
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
|
||||
this.reloadData()
|
||||
},
|
||||
batchDelete() {
|
||||
const that = this
|
||||
this.$confirm({
|
||||
title: that.$t('warning'),
|
||||
content: that.$t('confirmDelete'),
|
||||
onOk() {
|
||||
that.batchDeleteAsync()
|
||||
},
|
||||
})
|
||||
},
|
||||
async batchDeleteAsync() {
|
||||
let successNum = 0
|
||||
let errorNum = 0
|
||||
this.loading = true
|
||||
this.loadTip = this.$t('cmdb.ci.batchDeleting')
|
||||
const floor = Math.ceil(this.selectedRowKeys.length / 6)
|
||||
for (let i = 0; i < floor; i++) {
|
||||
const itemList = this.selectedRowKeys.slice(6 * i, 6 * i + 6)
|
||||
const promises = itemList.map((x) => deleteCI(x, false))
|
||||
await Promise.allSettled(promises)
|
||||
.then((res) => {
|
||||
res.forEach((r) => {
|
||||
if (r.status === 'fulfilled') {
|
||||
successNum += 1
|
||||
} else {
|
||||
errorNum += 1
|
||||
}
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
this.loadTip = this.$t('cmdb.ci.batchDeleting2', {
|
||||
total: this.selectedRowKeys.length,
|
||||
successNum: successNum,
|
||||
errorNum: errorNum,
|
||||
})
|
||||
})
|
||||
}
|
||||
this.loading = false
|
||||
this.loadTip = ''
|
||||
this.selectedRowKeys = []
|
||||
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
|
||||
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
|
||||
this.$nextTick(() => {
|
||||
if (this.currentPage === 1) {
|
||||
this.loadTableData()
|
||||
} else {
|
||||
this.currentPage = 1
|
||||
}
|
||||
})
|
||||
},
|
||||
deleteCI(record) {
|
||||
const that = this
|
||||
this.$confirm({
|
||||
title: that.$t('warning'),
|
||||
content: that.$t('confirmDelete'),
|
||||
onOk() {
|
||||
deleteCI(record.ci_id || record._id).then((res) => {
|
||||
// that.$refs.table.refresh(true)
|
||||
that.$message.success(that.$t('deleteSuccess'))
|
||||
that.reloadData()
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
batchRollback() {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.ciRollbackForm.onOpen(true)
|
||||
})
|
||||
},
|
||||
async batchRollbackAsync(params) {
|
||||
const mask = document.querySelector('.ant-drawer-mask')
|
||||
const oldValue = mask.style.zIndex
|
||||
mask.style.zIndex = 2
|
||||
let successNum = 0
|
||||
let errorNum = 0
|
||||
this.loading = true
|
||||
this.loadTip = this.$t('cmdb.ci.rollbackingTips')
|
||||
const floor = Math.ceil(this.selectedRowKeys.length / 6)
|
||||
for (let i = 0; i < floor; i++) {
|
||||
const itemList = this.selectedRowKeys.slice(6 * i, 6 * i + 6)
|
||||
const promises = itemList.map((x) => CIBaselineRollback(x, params))
|
||||
await Promise.allSettled(promises)
|
||||
.then((res) => {
|
||||
res.forEach((r) => {
|
||||
if (r.status === 'fulfilled') {
|
||||
successNum += 1
|
||||
} else {
|
||||
errorNum += 1
|
||||
}
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
this.loadTip = this.$t('cmdb.ci.batchRollbacking', {
|
||||
total: this.selectedRowKeys.length,
|
||||
successNum: successNum,
|
||||
errorNum: errorNum,
|
||||
})
|
||||
})
|
||||
}
|
||||
this.loading = false
|
||||
this.loadTip = ''
|
||||
mask.style.zIndex = oldValue
|
||||
this.selectedRowKeys = []
|
||||
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
|
||||
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
|
||||
this.$nextTick(() => {
|
||||
if (this.currentPage === 1) {
|
||||
this.loadTableData()
|
||||
} else {
|
||||
this.currentPage = 1
|
||||
}
|
||||
})
|
||||
},
|
||||
async refreshAfterEditAttrs() {
|
||||
await this.loadPreferenceAttrList()
|
||||
await this.loadTableData()
|
||||
},
|
||||
onShowSizeChange(current, pageSize) {
|
||||
this.pageSize = pageSize
|
||||
if (this.currentPage === 1) {
|
||||
this.reloadData()
|
||||
} else {
|
||||
this.currentPage = 1
|
||||
}
|
||||
setTimeout(() => {
|
||||
// this.setSelectRows()
|
||||
}, 500)
|
||||
},
|
||||
handleSortCol({ column, property, order, sortBy, sortList, $event }) {
|
||||
let sortByTable
|
||||
if (order === 'asc') {
|
||||
sortByTable = property
|
||||
} else if (order === 'desc') {
|
||||
sortByTable = `-${property}`
|
||||
}
|
||||
this.sortByTable = sortByTable
|
||||
this.$nextTick(() => {
|
||||
if (this.currentPage === 1) {
|
||||
this.loadTableData(sortByTable)
|
||||
} else {
|
||||
this.currentPage = 1
|
||||
}
|
||||
})
|
||||
},
|
||||
columnDrop() {
|
||||
this.$nextTick(() => {
|
||||
const xTable = this.$refs?.xTable?.getVxetableRef?.()
|
||||
if (!xTable) {
|
||||
return
|
||||
}
|
||||
|
||||
this.sortable = Sortable.create(
|
||||
xTable.$el.querySelector('.body--wrapper>.vxe-table--header .vxe-header--row'),
|
||||
{
|
||||
handle: '.vxe-handle',
|
||||
onChoose: () => {
|
||||
const header = xTable.$el.querySelector('.body--wrapper>.vxe-table--header .vxe-header--row')
|
||||
const classNameList = []
|
||||
header.childNodes.forEach((item) => {
|
||||
classNameList.push(item.classList[1])
|
||||
})
|
||||
this.tableDragClassName = classNameList
|
||||
},
|
||||
onEnd: (params) => {
|
||||
// 由于开启了虚拟滚动,newIndex和oldIndex是虚拟的
|
||||
const { newIndex, oldIndex } = params
|
||||
// 从tableDragClassName拿到colid
|
||||
const fromColid = this.tableDragClassName[oldIndex]
|
||||
const toColid = this.tableDragClassName[newIndex]
|
||||
const fromColumn = xTable.getColumnById(fromColid)
|
||||
const toColumn = xTable.getColumnById(toColid)
|
||||
const fromIndex = xTable.getColumnIndex(fromColumn)
|
||||
const toIndex = xTable.getColumnIndex(toColumn)
|
||||
const tableColumn = xTable.getColumns()
|
||||
const currRow = tableColumn.splice(fromIndex, 1)[0]
|
||||
tableColumn.splice(toIndex, 0, currRow)
|
||||
xTable.loadColumn(tableColumn)
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
},
|
||||
handleEditActived() {
|
||||
this.isEditActive = true
|
||||
const passwordCol = this.columns.filter((col) => col.is_password)
|
||||
this.$nextTick(() => {
|
||||
const editRecord = this.$refs.xTable.getVxetableRef().getEditRecord()
|
||||
const { row, column } = editRecord
|
||||
if (passwordCol.length && this.lastEditCiId !== row._id) {
|
||||
this.$nextTick(async () => {
|
||||
for (let i = 0; i < passwordCol.length; i++) {
|
||||
await getAttrPassword(row._id, passwordCol[i].attr_id).then((res) => {
|
||||
this.initialPasswordValue[passwordCol[i].field] = res.value
|
||||
this.passwordValue[passwordCol[i].field] = res.value
|
||||
})
|
||||
}
|
||||
this.isContinueCloseEdit = false
|
||||
await this.$refs.xTable.getVxetableRef().clearEdit()
|
||||
this.isContinueCloseEdit = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.xTable.getVxetableRef().setEditCell(row, column.field)
|
||||
})
|
||||
})
|
||||
}
|
||||
this.lastEditCiId = row._id
|
||||
})
|
||||
},
|
||||
getQAndSort() {
|
||||
const fuzzySearch = this.$refs['search'].fuzzySearch || ''
|
||||
const expression = this.$refs['search'].expression || ''
|
||||
this.$refs.preferenceSearch.savePreference({ fuzzySearch, expression })
|
||||
},
|
||||
setParamsFromPreferenceSearch(item) {
|
||||
const { fuzzySearch, expression } = item.option
|
||||
this.$refs.search.fuzzySearch = fuzzySearch
|
||||
this.$refs.search.expression = expression
|
||||
this.selectedRowKeys = []
|
||||
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
|
||||
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
|
||||
this.$refs.xTable.getVxetableRef().clearSort()
|
||||
this.sortByTable = undefined
|
||||
this.$nextTick(() => {
|
||||
if (this.currentPage === 1) {
|
||||
this.loadTableData()
|
||||
} else {
|
||||
this.currentPage = 1
|
||||
}
|
||||
})
|
||||
},
|
||||
setPreferenceSearchCurrent(id = null) {
|
||||
this.$refs.preferenceSearch.currentPreferenceSearch = id
|
||||
},
|
||||
copyExpression() {
|
||||
const expression = this.$refs['search'].expression || ''
|
||||
const fuzzySearch = this.$refs['search'].fuzzySearch
|
||||
|
||||
const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
|
||||
|
||||
const exp = expression.match(regQ) ? expression.match(regQ)[0] : null
|
||||
const text = `q=_type:${this.typeId}${exp ? `,${exp}` : ''}${fuzzySearch ? `,*${fuzzySearch}*` : ''}`
|
||||
this.$copyText(text)
|
||||
.then(() => {
|
||||
this.$message.success(this.$t('copySuccess'))
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.error(this.$t('cmdb.ci.copyFailed'))
|
||||
})
|
||||
},
|
||||
unsubscribe() {
|
||||
this.$confirm({
|
||||
title: this.$t('warning'),
|
||||
content: this.$t('cmdb.preference.confirmcancelSub2', {
|
||||
name: `${this.CIType.alias || this.CIType.name}`,
|
||||
}),
|
||||
onOk: () => {
|
||||
const promises = [subscribeCIType(this.typeId, ''), subscribeTreeView(this.typeId, '')]
|
||||
Promise.all(promises).then(() => {
|
||||
this.$message.success(this.$t('cmdb.preference.cancelSubSuccess'))
|
||||
this.$emit('unSubscribe')
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
handlePerm() {
|
||||
roleHasPermissionToGrant({
|
||||
app_id: 'cmdb',
|
||||
resource_type_name: 'CIType',
|
||||
perm: 'grant',
|
||||
resource_name: this.CIType.name,
|
||||
}).then((res) => {
|
||||
if (res.result) {
|
||||
searchResourceType({ page_size: 9999, app_id: 'cmdb' }).then((res) => {
|
||||
this.resource_type = { groups: res.groups, id2perms: res.id2perms }
|
||||
this.$nextTick(() => {
|
||||
this.$refs.cmdbGrant.open({
|
||||
name: this.CIType.name,
|
||||
cmdbGrantType: 'ci',
|
||||
CITypeId: this.typeId,
|
||||
})
|
||||
})
|
||||
})
|
||||
} else {
|
||||
this.$message.error(this.$t('noPermission'))
|
||||
}
|
||||
})
|
||||
},
|
||||
handleMenuClick(e) {
|
||||
if (e.key === 'grant') {
|
||||
this.visible = false
|
||||
}
|
||||
},
|
||||
openDetail(id, activeTabKey, ciDetailRelationKey) {
|
||||
this.$refs.detail.create(id, activeTabKey, ciDetailRelationKey)
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
@import '../index.less';
|
||||
</style>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.cmdb-ci {
|
||||
background-color: #fff;
|
||||
padding: 20px;
|
||||
border-radius: @border-radius-box;
|
||||
height: calc(100vh - 64px);
|
||||
overflow: auto;
|
||||
margin-bottom: -24px;
|
||||
}
|
||||
</style>
|
@@ -191,6 +191,10 @@ export default {
|
||||
CIReferenceAttr
|
||||
},
|
||||
props: {
|
||||
typeIdFromProp: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
typeIdFromRelation: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
@@ -235,7 +239,7 @@ export default {
|
||||
if (this.typeIdFromRelation) {
|
||||
return this.typeIdFromRelation
|
||||
}
|
||||
return this.$router.currentRoute.meta.typeId
|
||||
return this.typeIdFromProp
|
||||
},
|
||||
valueTypeMap() {
|
||||
return valueTypeMap()
|
||||
|
@@ -23,15 +23,25 @@ export default {
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
icon: '',
|
||||
title: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
findCIType() {
|
||||
return this.ci_types?.find?.((item) => item?.id === this.ci?._type)
|
||||
},
|
||||
icon() {
|
||||
return this?.findCiType?.icon || ''
|
||||
},
|
||||
title() {
|
||||
return this?.ci?.[this.findCIType?.show_name] || this?.ci?.[this.findCIType?.unique_key] || ''
|
||||
return this.ci_types?.find?.((item) => item?.id === this.ci?._type) || {}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
findCIType: {
|
||||
deep: true,
|
||||
immediate: true,
|
||||
handler(val) {
|
||||
this.icon = val?.icon || ''
|
||||
this.title = this?.ci?.[val?.show_name] || this?.ci?.[val?.unique_key] || ''
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -5,29 +5,44 @@
|
||||
<div class="ci-relation-table-wrap">
|
||||
<div class="ci-relation-table-tab">
|
||||
<div
|
||||
v-for="(item) in tabList"
|
||||
:key="item.value"
|
||||
:class="`tab-item ${item.value === currentTab ? 'tab-item-active' : ''}`"
|
||||
@click="clickTab(item.value)"
|
||||
v-for="(group) in tabList"
|
||||
:key="group.key"
|
||||
class="tab-group"
|
||||
>
|
||||
<span class="tab-item-name">
|
||||
<a-tooltip :title="item.name">
|
||||
<span class="tab-item-name-text">{{ item.name }}</span>
|
||||
</a-tooltip>
|
||||
<span
|
||||
v-if="item.count"
|
||||
class="tab-item-name-count"
|
||||
>
|
||||
({{ item.count }})
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
v-if="item.value === currentTab && item.showAdd"
|
||||
class="tab-item-add"
|
||||
@click="openAddModal(item)"
|
||||
<div
|
||||
v-if="group.name"
|
||||
class="tab-group-name"
|
||||
>
|
||||
<a-icon type="plus" />
|
||||
</span>
|
||||
{{ group.name }}
|
||||
</div>
|
||||
<div
|
||||
v-for="(item) in group.list"
|
||||
:key="item.key"
|
||||
:class="`tab-item ${item.key === currentTab ? 'tab-item-active' : ''}`"
|
||||
:style="{
|
||||
paddingLeft: item.key === 'all' ? '8px' : '16px'
|
||||
}"
|
||||
@click="clickTab(item.key)"
|
||||
>
|
||||
<span class="tab-item-name">
|
||||
<a-tooltip :title="item.name">
|
||||
<span class="tab-item-name-text">{{ item.name }}</span>
|
||||
</a-tooltip>
|
||||
<span
|
||||
v-if="item.count"
|
||||
class="tab-item-name-count"
|
||||
>
|
||||
({{ item.count }})
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
v-if="item.key === currentTab && item.showAdd"
|
||||
class="tab-item-add"
|
||||
@click="openAddModal(item)"
|
||||
>
|
||||
<a-icon type="plus" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -37,7 +52,7 @@
|
||||
>
|
||||
<div
|
||||
v-for="(item) in tableIDList"
|
||||
:key="item.id"
|
||||
:key="item.key"
|
||||
class="ci-relation-table-item"
|
||||
>
|
||||
<div
|
||||
@@ -51,8 +66,8 @@
|
||||
<vxe-grid
|
||||
bordered
|
||||
size="mini"
|
||||
:columns="allColumns[item.id]"
|
||||
:data="allCIList[item.id]"
|
||||
:columns="allColumns[item.value]"
|
||||
:data="allCIList[item.key]"
|
||||
overflow
|
||||
showOverflow="tooltip"
|
||||
showHeaderOverflow="tooltip"
|
||||
@@ -77,9 +92,9 @@
|
||||
@confirm="deleteRelation(row)"
|
||||
>
|
||||
<a
|
||||
:disabled="!allCanEdit[item.id]"
|
||||
:disabled="!allCanEdit[item.value]"
|
||||
:style="{
|
||||
color: !allCanEdit[item.id] ? 'rgba(0, 0, 0, 0.25)' : 'red',
|
||||
color: !allCanEdit[item.value] ? 'rgba(0, 0, 0, 0.25)' : 'red',
|
||||
}"
|
||||
>
|
||||
<a-icon type="delete" />
|
||||
@@ -105,6 +120,9 @@ import { getSubscribeAttributes } from '@/modules/cmdb/api/preference'
|
||||
import CIDetailTableTitle from './ciDetailTableTitle.vue'
|
||||
import AddTableModal from '@/modules/cmdb/views/relation_views/modules/AddTableModal.vue'
|
||||
|
||||
const PARENT_KEY = 'parents'
|
||||
const CHILDREN_KEY = 'children'
|
||||
|
||||
export default {
|
||||
name: 'CIRelationTable',
|
||||
components: {
|
||||
@@ -151,24 +169,26 @@ export default {
|
||||
},
|
||||
|
||||
computed: {
|
||||
tabListFlat() {
|
||||
return this.tabList.reduce((list, group) => list.concat(group.list), [])
|
||||
},
|
||||
tableIDList() {
|
||||
let baseIDs = []
|
||||
const baseKeys = this.currentTab === 'all'
|
||||
? this.tabListFlat.filter(item => item.value !== 'all').map(item => item.key)
|
||||
: [this.currentTab]
|
||||
|
||||
switch (this.currentTab) {
|
||||
case 'all':
|
||||
baseIDs = this.tabList.filter((item) => item.value !== 'all').map((item) => item.value)
|
||||
break
|
||||
default:
|
||||
baseIDs = [this.currentTab]
|
||||
break
|
||||
}
|
||||
return baseKeys.filter((key) => this.allCIList?.[key]?.length).map((key) => {
|
||||
const findTab = this.tabListFlat.find((item) => item.key === key) || {}
|
||||
|
||||
return baseIDs.filter((id) => this.allCIList?.[id]?.length).map((id) => {
|
||||
const findTab = this.tabList.find((item) => item.value === id) || {}
|
||||
let name = findTab?.name || ''
|
||||
if (name && findTab?.value === this.ci._type) {
|
||||
name = `${findTab?.isParent ? this.$t('cmdb.ci.upstream') : this.$t('cmdb.ci.downstream')} - ${name}`
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
name: findTab?.name || '',
|
||||
key,
|
||||
value: findTab?.value || '',
|
||||
name,
|
||||
count: findTab?.count || ''
|
||||
}
|
||||
})
|
||||
@@ -195,10 +215,13 @@ export default {
|
||||
|
||||
const cloneRelationData = _.cloneDeep(relationData)
|
||||
|
||||
const allCITypes = [
|
||||
...cloneRelationData.parentCITypeList,
|
||||
...cloneRelationData.childCITypeList
|
||||
]
|
||||
const allCITypes = _.uniqBy(
|
||||
[
|
||||
...cloneRelationData.parentCITypeList,
|
||||
...cloneRelationData.childCITypeList
|
||||
],
|
||||
'id'
|
||||
)
|
||||
await this.handleSubscribeAttributes(allCITypes)
|
||||
|
||||
const {
|
||||
@@ -231,25 +254,48 @@ export default {
|
||||
...childCIs
|
||||
}
|
||||
|
||||
const tabList = this.allCITypes.map((item) => {
|
||||
return {
|
||||
name: item?.alias ?? item?.name ?? '',
|
||||
value: item.id,
|
||||
count: this.allCIList?.[item.id]?.length || 0,
|
||||
showAdd: this.allCanEdit?.[item.id] ?? false
|
||||
}
|
||||
})
|
||||
tabList.unshift({
|
||||
name: this.$t('all'),
|
||||
value: 'all',
|
||||
count: Object.values(this.allCIList).reduce((acc, cur) => acc + (cur?.length || 0), 0),
|
||||
showAdd: false
|
||||
})
|
||||
const tabList = []
|
||||
|
||||
tabList[0] = {
|
||||
name: '',
|
||||
key: 'all',
|
||||
list: [{
|
||||
name: this.$t('all'),
|
||||
key: 'all',
|
||||
value: 'all',
|
||||
count: Object.values(this.allCIList).reduce((acc, cur) => acc + (cur?.length || 0), 0),
|
||||
showAdd: false
|
||||
}]
|
||||
}
|
||||
tabList[1] = {
|
||||
name: this.$t('cmdb.ci.upstream'),
|
||||
key: PARENT_KEY,
|
||||
list: this.buildTabList(cloneRelationData.parentCITypeList, PARENT_KEY, true)
|
||||
}
|
||||
tabList[2] = {
|
||||
name: this.$t('cmdb.ci.downstream'),
|
||||
key: CHILDREN_KEY,
|
||||
list: this.buildTabList(cloneRelationData.childCITypeList, CHILDREN_KEY, false)
|
||||
}
|
||||
this.tabList = tabList
|
||||
|
||||
this.handleReferenceCINameMap()
|
||||
},
|
||||
|
||||
buildTabList(list, keyPrefix, isParent) {
|
||||
return list.map((item) => {
|
||||
const key = `${keyPrefix}-${item.id}`
|
||||
return {
|
||||
name: item?.alias ?? item?.name ?? '',
|
||||
key,
|
||||
isParent,
|
||||
value: item.id,
|
||||
count: this.allCIList?.[key]?.length || 0,
|
||||
showAdd: this.allCanEdit?.[item.id] ?? false
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
handleCITypeList(list, isParent) {
|
||||
const CIColumns = {}
|
||||
const CIJSONAttr = {}
|
||||
@@ -362,11 +408,12 @@ export default {
|
||||
})
|
||||
this.formatCI(item)
|
||||
item.isParent = isParent
|
||||
const CIKey = `${isParent ? PARENT_KEY : CHILDREN_KEY}-${item._type}`
|
||||
|
||||
if (item._type in cis) {
|
||||
cis[item._type].push(item)
|
||||
if (CIKey in cis) {
|
||||
cis[CIKey].push(item)
|
||||
} else {
|
||||
cis[item._type] = [item]
|
||||
cis[CIKey] = [item]
|
||||
}
|
||||
})
|
||||
|
||||
@@ -395,9 +442,11 @@ export default {
|
||||
async handleReferenceCINameMap() {
|
||||
const referenceCINameMap = {}
|
||||
this.allCITypes.forEach((CIType) => {
|
||||
const CIKey = `${CIType.isParent ? PARENT_KEY : CHILDREN_KEY}-${CIType.id}`
|
||||
|
||||
CIType.attributes.forEach((attr) => {
|
||||
if (attr?.is_reference && attr?.reference_type_id) {
|
||||
const currentCIList = this.allCIList[CIType.id]
|
||||
const currentCIList = this.allCIList[CIKey]
|
||||
if (currentCIList?.length) {
|
||||
currentCIList.forEach((ci) => {
|
||||
const ids = Array.isArray(ci[attr.name]) ? ci[attr.name] : ci[attr.name] ? [ci[attr.name]] : []
|
||||
@@ -458,8 +507,8 @@ export default {
|
||||
return this.referenceCINameMap?.[typeId]?.[id] || id
|
||||
},
|
||||
|
||||
clickTab(value) {
|
||||
this.currentTab = value
|
||||
clickTab(key) {
|
||||
this.currentTab = key
|
||||
},
|
||||
|
||||
deleteRelation(row) {
|
||||
@@ -483,7 +532,7 @@ export default {
|
||||
},
|
||||
this.ciId,
|
||||
ciType,
|
||||
ciType?.isParent ? 'parents' : 'children'
|
||||
tabData?.isParent ? 'parents' : 'children'
|
||||
)
|
||||
},
|
||||
|
||||
@@ -509,12 +558,26 @@ export default {
|
||||
&-tab {
|
||||
flex-shrink: 0;
|
||||
width: 160px;
|
||||
max-height: 300px;
|
||||
min-height: 300px;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 6px 0px;
|
||||
border-right: solid 1px #E4E7ED;
|
||||
|
||||
.tab-group {
|
||||
width: 100%;
|
||||
|
||||
&-name {
|
||||
padding-left: 8px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
width: 100%;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, .45);
|
||||
}
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
height: 32px;
|
||||
width: 100%;
|
||||
@@ -583,6 +646,9 @@ export default {
|
||||
padding: 15px 17px;
|
||||
overflow: hidden;
|
||||
min-height: 300px;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
&-item {
|
||||
|
@@ -132,10 +132,11 @@ export default {
|
||||
}
|
||||
|
||||
const _findCiType = ci_types_list.find((item) => item.id === this.typeId)
|
||||
const unique_id = _findCiType.show_id || this.attributes().unique_id
|
||||
const unique_name = _findCiType.show_name || this.attributes().unique
|
||||
const unique_id = _findCiType.show_id || _findCiType.unique_id
|
||||
const _findUnique = this.attrList().find((attr) => attr.id === unique_id)
|
||||
const unique_name = _findUnique?.name
|
||||
const unique_alias = _findUnique?.alias || _findUnique?.name || ''
|
||||
|
||||
const nodes = {
|
||||
isRoot: true,
|
||||
id: `Root_${this.typeId}`,
|
||||
|
@@ -50,13 +50,19 @@
|
||||
<vxe-column :width="300" field="attributeAssociation" :edit-render="{}">
|
||||
<template #header>
|
||||
<span>
|
||||
<a-tooltip :title="$t('cmdb.ciType.attributeAssociationTip1')">
|
||||
<a-tooltip>
|
||||
<template #title>
|
||||
<div>{{ $t('cmdb.ciType.attributeAssociationTip1') }}</div>
|
||||
<div>{{ $t('cmdb.ciType.attributeAssociationTip7') }}</div>
|
||||
<div>{{ $t('cmdb.ciType.attributeAssociationTip8') }}</div>
|
||||
<div>{{ $t('cmdb.ciType.attributeAssociationTip9') }}</div>
|
||||
</template>
|
||||
<a><a-icon type="question-circle"/></a>
|
||||
</a-tooltip>
|
||||
{{ $t('cmdb.ciType.attributeAssociation') }}
|
||||
<span :style="{ fontSize: '10px', fontWeight: 'normal' }" class="text-color-4">{{
|
||||
$t('cmdb.ciType.attributeAssociationTip2')
|
||||
}}</span>
|
||||
<span :style="{ fontSize: '10px', fontWeight: 'normal' }" class="text-color-4">
|
||||
{{ $t('cmdb.ciType.attributeAssociationTip2') }}
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
<template #default="{row}">
|
||||
@@ -88,7 +94,13 @@
|
||||
optionFilterProp="title"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="attr in filterAttributes(row.isParent ? row.attributes : attributes)"
|
||||
v-for="attr in filterAttributes(
|
||||
row.isParent ? row.attributes : attributes,
|
||||
item.childAttrId,
|
||||
row.isParent ? attributes : row.attributes,
|
||||
'parent',
|
||||
row.constraint
|
||||
)"
|
||||
:key="attr.id"
|
||||
:value="attr.id"
|
||||
:title="attr.alias || attr.name"
|
||||
@@ -107,7 +119,13 @@
|
||||
optionFilterProp="title"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="attr in filterAttributes(row.isParent ? attributes : row.attributes)"
|
||||
v-for="attr in filterAttributes(
|
||||
row.isParent ? attributes : row.attributes,
|
||||
item.parentAttrId,
|
||||
row.isParent ? row.attributes : attributes,
|
||||
'child',
|
||||
row.constraint
|
||||
)"
|
||||
:key="attr.id"
|
||||
:value="attr.id"
|
||||
:title="attr.alias || attr.name"
|
||||
@@ -171,23 +189,12 @@
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item :label="$t('cmdb.ciType.dstCIType')">
|
||||
<a-select
|
||||
showSearch
|
||||
<CMDBTypeSelectAntd
|
||||
v-decorator="['ci_type_id', { rules: [{ required: true, message: $t('cmdb.ciType.dstCITypeTips') }] }]"
|
||||
name="ci_type_id"
|
||||
:placeholder="$t('cmdb.ciType.dstCITypeTips')"
|
||||
v-decorator="['ci_type_id', { rules: [{ required: true, message: $t('cmdb.ciType.dstCITypeTips') }] }]"
|
||||
:filterOption="filterOption"
|
||||
@change="changeChild"
|
||||
>
|
||||
<a-select-option
|
||||
:value="CIType.id"
|
||||
:key="CIType.id"
|
||||
v-for="CIType in CITypes"
|
||||
>
|
||||
{{ CIType.alias || CIType.name }}
|
||||
<span class="model-select-name">({{ CIType.name }})</span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item :label="$t('cmdb.ciType.relationType')">
|
||||
@@ -211,6 +218,7 @@
|
||||
'constraint',
|
||||
{ rules: [{ required: true, message: $t('cmdb.ciType.relationConstraintTips') }] },
|
||||
]"
|
||||
@change="handleFormConstraintChange"
|
||||
>
|
||||
<a-select-option value="0">{{ $t('cmdb.ciType.one2Many') }}</a-select-option>
|
||||
<a-select-option value="1">{{ $t('cmdb.ciType.one2One') }}</a-select-option>
|
||||
@@ -218,6 +226,11 @@
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item :label="$t('cmdb.ciType.attributeAssociation')">
|
||||
<template #extra>
|
||||
<div>{{ $t('cmdb.ciType.attributeAssociationTip7') }}</div>
|
||||
<div>{{ $t('cmdb.ciType.attributeAssociationTip8') }}</div>
|
||||
<div>{{ $t('cmdb.ciType.attributeAssociationTip9') }}</div>
|
||||
</template>
|
||||
<a-row
|
||||
v-for="item in modalAttrList"
|
||||
:key="item.id"
|
||||
@@ -229,7 +242,15 @@
|
||||
allowClear
|
||||
v-model="item.parentAttrId"
|
||||
>
|
||||
<a-select-option v-for="attr in filterAttributes(attributes)" :key="attr.id">
|
||||
<a-select-option
|
||||
v-for="attr in filterAttributes(
|
||||
attributes,
|
||||
item.childAttrId,
|
||||
modalChildAttributes,
|
||||
'parent'
|
||||
)"
|
||||
:key="attr.id"
|
||||
>
|
||||
{{ attr.alias || attr.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
@@ -245,7 +266,15 @@
|
||||
allowClear
|
||||
v-model="item.childAttrId"
|
||||
>
|
||||
<a-select-option v-for="attr in filterAttributes(modalChildAttributes)" :key="attr.id">
|
||||
<a-select-option
|
||||
v-for="attr in filterAttributes(
|
||||
modalChildAttributes,
|
||||
item.parentAttrId,
|
||||
attributes,
|
||||
'child'
|
||||
)"
|
||||
:key="attr.id"
|
||||
>
|
||||
{{ attr.alias || attr.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
@@ -285,12 +314,14 @@ import { getCITypes } from '@/modules/cmdb/api/CIType'
|
||||
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import CMDBGrant from '../../components/cmdbGrant'
|
||||
import CMDBGrant from '@/modules/cmdb/components/cmdbGrant'
|
||||
import CMDBTypeSelectAntd from '@/modules/cmdb/components/cmdbTypeSelect/cmdbTypeSelectAntd'
|
||||
|
||||
export default {
|
||||
name: 'RelationTable',
|
||||
components: {
|
||||
CMDBGrant,
|
||||
CMDBTypeSelectAntd
|
||||
},
|
||||
props: {
|
||||
CITypeId: {
|
||||
@@ -513,13 +544,6 @@ export default {
|
||||
cmdbGrantType: 'type_relation',
|
||||
})
|
||||
},
|
||||
filterOption(input, option) {
|
||||
const inputValue = input.toLowerCase()
|
||||
const alias = option.componentOptions.children[0].text.toLowerCase()
|
||||
const name = option.componentOptions.children[1]?.elm?.innerHTML?.toLowerCase?.() ?? ''
|
||||
|
||||
return alias.indexOf(inputValue) >= 0 || name.indexOf(inputValue) >= 0
|
||||
},
|
||||
rowClass({ row }) {
|
||||
if (row.isDivider) return 'relation-table-divider'
|
||||
if (row.isParent) return 'relation-table-parent'
|
||||
@@ -604,20 +628,41 @@ export default {
|
||||
this.modalAttrList.forEach((item) => {
|
||||
item.childAttrId = undefined
|
||||
})
|
||||
getCITypeAttributesById(value).then((res) => {
|
||||
this.modalChildAttributes = res?.attributes ?? []
|
||||
})
|
||||
if (value) {
|
||||
getCITypeAttributesById(value).then((res) => {
|
||||
this.modalChildAttributes = res?.attributes ?? []
|
||||
})
|
||||
}
|
||||
},
|
||||
filterAttributes(attributes) {
|
||||
// filter password/json/is_list/longText/bool/reference
|
||||
return attributes.filter((attr) => {
|
||||
|
||||
filterAttributes(attributes, relationAttrId, relationAttrs, type, constraint) {
|
||||
const relationAttr = relationAttrs.find((attr) => attr.id === relationAttrId)
|
||||
|
||||
// filter password/json/longText/bool/reference
|
||||
let filterAttrs = attributes.filter((attr) => {
|
||||
if (attr.value_type === '2' && !attr.is_index) {
|
||||
return false
|
||||
}
|
||||
|
||||
return !attr.is_password && !attr.is_list && attr.value_type !== '6' && !attr.is_bool && !attr.is_reference
|
||||
return !attr.is_password && attr.value_type !== '6' && !attr.is_bool && !attr.is_reference
|
||||
})
|
||||
|
||||
if (relationAttr) {
|
||||
filterAttrs = filterAttrs.filter((attr) => attr.value_type === relationAttr?.value_type)
|
||||
}
|
||||
|
||||
const constraintValue = Number(constraint ?? this.form.getFieldValue('constraint'))
|
||||
if (
|
||||
(constraintValue === 0 && type === 'child') ||
|
||||
constraintValue === 1 ||
|
||||
(constraintValue === 2 && relationAttr?.is_list)
|
||||
) {
|
||||
return filterAttrs.filter((attr) => !attr.is_list)
|
||||
}
|
||||
|
||||
return filterAttrs
|
||||
},
|
||||
|
||||
addTableAttr() {
|
||||
this.tableAttrList.push({
|
||||
id: uuidv4(),
|
||||
@@ -653,6 +698,13 @@ export default {
|
||||
if (index !== -1) {
|
||||
this.modalAttrList.splice(index, 1)
|
||||
}
|
||||
},
|
||||
|
||||
handleFormConstraintChange() {
|
||||
this.modalAttrList.forEach((item) => {
|
||||
item.parentAttrId = undefined
|
||||
item.childAttrId = undefined
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@@ -51,31 +51,21 @@
|
||||
:label="$t('cmdb.ciType.ciType')"
|
||||
prop="type_ids"
|
||||
>
|
||||
<a-select
|
||||
show-search
|
||||
optionFilterProp="children"
|
||||
@change="changeCIType"
|
||||
<CMDBTypeSelectAntd
|
||||
v-model="form.type_ids"
|
||||
:placeholder="$t('cmdb.ciType.selectCIType')"
|
||||
mode="multiple"
|
||||
>
|
||||
<a-select-option v-for="ci_type in ci_types" :key="ci_type.id" :value="ci_type.id">{{
|
||||
ci_type.alias || ci_type.name
|
||||
}}</a-select-option>
|
||||
</a-select>
|
||||
:CITypeGroup="CITypeGroup"
|
||||
:placeholder="$t('cmdb.ciType.selectCIType')"
|
||||
@change="changeCIType"
|
||||
/>
|
||||
</a-form-model-item>
|
||||
<a-form-model-item v-else :label="$t('cmdb.ciType.ciType')" prop="type_id">
|
||||
<a-select
|
||||
show-search
|
||||
optionFilterProp="children"
|
||||
@change="changeCIType"
|
||||
<CMDBTypeSelectAntd
|
||||
v-model="form.type_id"
|
||||
:CITypeGroup="CITypeGroup"
|
||||
:placeholder="$t('cmdb.ciType.selectCIType')"
|
||||
>
|
||||
<a-select-option v-for="ci_type in ci_types" :key="ci_type.id" :value="ci_type.id">{{
|
||||
ci_type.alias || ci_type.name
|
||||
}}</a-select-option>
|
||||
</a-select>
|
||||
@change="changeCIType"
|
||||
/>
|
||||
</a-form-model-item>
|
||||
<a-form-model-item
|
||||
:label="$t('cmdb.custom_dashboard.dimensions')"
|
||||
@@ -309,17 +299,26 @@
|
||||
<script>
|
||||
import Chart from './chart.vue'
|
||||
import { dashboardCategory } from './constant'
|
||||
import { postCustomDashboard, putCustomDashboard, postCustomDashboardPreview } from '../../api/customDashboard'
|
||||
import { getCITypeAttributesByTypeIds, getCITypeCommonAttributesByTypeIds } from '../../api/CITypeAttr'
|
||||
import { getRecursive_level2children } from '../../api/CITypeRelation'
|
||||
import { getLastLayout } from '../../utils/helper'
|
||||
import { postCustomDashboard, putCustomDashboard, postCustomDashboardPreview } from '@/modules/cmdb/api/customDashboard'
|
||||
import { getCITypeAttributesByTypeIds, getCITypeCommonAttributesByTypeIds } from '@/modules/cmdb/api/CITypeAttr'
|
||||
import { getRecursive_level2children } from '@/modules/cmdb/api/CITypeRelation'
|
||||
import { getCITypeGroupsConfig } from '@/modules/cmdb/api/ciTypeGroup'
|
||||
import { getLastLayout } from '@/modules/cmdb/utils/helper'
|
||||
|
||||
import FilterComp from '@/components/CMDBFilterComp'
|
||||
import ColorPicker from './colorPicker.vue'
|
||||
import ColorListPicker from './colorListPicker.vue'
|
||||
import CMDBTypeSelectAntd from '@/modules/cmdb/components/cmdbTypeSelect/cmdbTypeSelectAntd'
|
||||
|
||||
export default {
|
||||
name: 'ChartForm',
|
||||
components: { Chart, FilterComp, ColorPicker, ColorListPicker },
|
||||
components: {
|
||||
Chart,
|
||||
FilterComp,
|
||||
ColorPicker,
|
||||
ColorListPicker,
|
||||
CMDBTypeSelectAntd
|
||||
},
|
||||
props: {
|
||||
ci_types: {
|
||||
type: Array,
|
||||
@@ -365,6 +364,7 @@ export default {
|
||||
level2children: {},
|
||||
isShadow: false,
|
||||
changeCITypeRequestValue: null,
|
||||
CITypeGroup: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -484,6 +484,7 @@ export default {
|
||||
showIcon,
|
||||
tableCategory: ret === 'cis' ? 2 : 1,
|
||||
}
|
||||
this.getCITypeGroup()
|
||||
},
|
||||
handleclose() {
|
||||
this.attributes = []
|
||||
@@ -491,6 +492,9 @@ export default {
|
||||
this.isShowPreview = false
|
||||
this.visible = false
|
||||
},
|
||||
async getCITypeGroup() {
|
||||
this.CITypeGroup = await getCITypeGroupsConfig({ need_other: true })
|
||||
},
|
||||
changeCIType(value) {
|
||||
this.form.attr_ids = []
|
||||
this.commonAttributes = []
|
||||
|
@@ -1,79 +1,82 @@
|
||||
<template>
|
||||
<div ref="wrapRef">
|
||||
<div class="table-header">
|
||||
<SearchForm
|
||||
ref="search"
|
||||
:preferenceAttrList="preferenceAttrList"
|
||||
:typeId="addressCITypeId"
|
||||
@copyExpression="copyExpression"
|
||||
@refresh="handleSearch"
|
||||
<a-spin :tip="loadTip" :spinning="loading" >
|
||||
<div class="table-header">
|
||||
<SearchForm
|
||||
ref="search"
|
||||
:preferenceAttrList="preferenceAttrList"
|
||||
:typeId="addressCITypeId"
|
||||
:selectedRowKeys="selectedRowKeys"
|
||||
@copyExpression="copyExpression"
|
||||
@refresh="handleSearch"
|
||||
>
|
||||
<div class="ops-list-batch-action" v-show="!!selectedRowKeys.length">
|
||||
<span @click="$refs.create.handleOpen(true, 'update')">{{ $t('update') }}</span>
|
||||
<a-divider type="vertical" />
|
||||
<span @click="openBatchDownload">{{ $t('download') }}</span>
|
||||
<a-divider type="vertical" />
|
||||
<span @click="batchDelete">{{ $t('delete') }}</span>
|
||||
<span>{{ $t('cmdb.ci.selectRows', { rows: selectedRowKeys.length }) }}</span>
|
||||
</div>
|
||||
</SearchForm>
|
||||
|
||||
<div class="table-header-right">
|
||||
<EditAttrsPopover
|
||||
:typeId="addressCITypeId"
|
||||
@refresh="refreshAfterEditAttrs"
|
||||
>
|
||||
<a-button
|
||||
type="primary"
|
||||
ghost
|
||||
class="ops-button-ghost"
|
||||
>
|
||||
<ops-icon type="veops-configuration_table" />
|
||||
{{ $t('cmdb.configTable') }}
|
||||
</a-button>
|
||||
</EditAttrsPopover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CITable
|
||||
ref="xTable"
|
||||
:loading="loading"
|
||||
:attrList="preferenceAttrList"
|
||||
:columns="columns"
|
||||
:data="instanceList"
|
||||
:height="tableHeight"
|
||||
@sort-change="handleSortCol"
|
||||
@openDetail="openDetail"
|
||||
@deleteCI="deleteCI"
|
||||
@onSelectChange="onSelectChange"
|
||||
/>
|
||||
|
||||
<div class="table-header-right">
|
||||
<EditAttrsPopover
|
||||
:typeId="addressCITypeId"
|
||||
@refresh="refreshAfterEditAttrs"
|
||||
<div class="table-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"
|
||||
>
|
||||
<a-button
|
||||
type="primary"
|
||||
ghost
|
||||
class="ops-button-ghost"
|
||||
>
|
||||
<ops-icon type="veops-configuration_table" />
|
||||
{{ $t('cmdb.configTable') }}
|
||||
</a-button>
|
||||
</EditAttrsPopover>
|
||||
<a-button
|
||||
v-if="instanceList && instanceList.length"
|
||||
type="primary"
|
||||
class="ops-button-ghost"
|
||||
ghost
|
||||
@click="handleExport"
|
||||
>
|
||||
<ops-icon type="veops-export" />
|
||||
{{ $t('export') }}
|
||||
</a-button>
|
||||
<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>
|
||||
|
||||
<CITable
|
||||
ref="xTable"
|
||||
:loading="loading"
|
||||
:attrList="preferenceAttrList"
|
||||
:columns="columns"
|
||||
:data="instanceList"
|
||||
:height="tableHeight"
|
||||
@sort-change="handleSortCol"
|
||||
@openDetail="openDetail"
|
||||
@deleteCI="deleteCI"
|
||||
/>
|
||||
|
||||
<div class="table-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>
|
||||
</a-spin>
|
||||
|
||||
<BatchDownload
|
||||
ref="batchDownload"
|
||||
@@ -82,6 +85,12 @@
|
||||
/>
|
||||
|
||||
<CIDetailDrawer ref="detail" :typeId="addressCITypeId" />
|
||||
|
||||
<CreateInstanceForm
|
||||
ref="create"
|
||||
:typeIdFromRelation="addressCITypeId"
|
||||
@submit="batchUpdate"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -90,7 +99,7 @@ import _ from 'lodash'
|
||||
import { mapState } from 'vuex'
|
||||
import ExcelJS from 'exceljs'
|
||||
import FileSaver from 'file-saver'
|
||||
import { searchCI, deleteCI } from '@/modules/cmdb/api/ci'
|
||||
import { searchCI, deleteCI, updateCI } from '@/modules/cmdb/api/ci'
|
||||
import { getSubscribeAttributes } from '@/modules/cmdb/api/preference'
|
||||
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
|
||||
import { getCITableColumns } from '@/modules/cmdb/utils/helper'
|
||||
@@ -100,6 +109,7 @@ import CITable from '@/modules/cmdb/components/ciTable/index.vue'
|
||||
import BatchDownload from '@/modules/cmdb/components/batchDownload/batchDownload.vue'
|
||||
import CIDetailDrawer from '@/modules/cmdb/views/ci/modules/ciDetailDrawer.vue'
|
||||
import EditAttrsPopover from '@/modules/cmdb/views/ci/modules/editAttrsPopover.vue'
|
||||
import CreateInstanceForm from '@/modules/cmdb/views/ci/modules/CreateInstanceForm'
|
||||
|
||||
export default {
|
||||
name: 'IPSearch',
|
||||
@@ -108,7 +118,8 @@ export default {
|
||||
CITable,
|
||||
BatchDownload,
|
||||
CIDetailDrawer,
|
||||
EditAttrsPopover
|
||||
EditAttrsPopover,
|
||||
CreateInstanceForm
|
||||
},
|
||||
props: {
|
||||
addressCIType: {
|
||||
@@ -122,6 +133,7 @@ export default {
|
||||
pageSize: 50,
|
||||
pageSizeOptions: ['50', '100', '200'],
|
||||
loading: false,
|
||||
loadTip: '',
|
||||
sortByTable: undefined,
|
||||
|
||||
instanceList: [],
|
||||
@@ -130,6 +142,7 @@ export default {
|
||||
preferenceAttrList: [],
|
||||
attrList: [],
|
||||
attributes: {},
|
||||
selectedRowKeys: [],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -275,7 +288,7 @@ export default {
|
||||
})
|
||||
},
|
||||
|
||||
handleExport() {
|
||||
openBatchDownload() {
|
||||
this.$refs.batchDownload.open({
|
||||
preferenceAttrList: this.preferenceAttrList,
|
||||
ciTypeName: this.$t('cmdb.ipam.ipSearch') || '',
|
||||
@@ -336,6 +349,7 @@ export default {
|
||||
FileSaver.saveAs(file, `${filename}.xlsx`)
|
||||
})
|
||||
|
||||
this.selectedRowKeys = []
|
||||
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
|
||||
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
|
||||
},
|
||||
@@ -361,6 +375,120 @@ export default {
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
onSelectChange(records) {
|
||||
this.selectedRowKeys = records.map((i) => i.ci_id || i._id)
|
||||
},
|
||||
|
||||
batchDelete() {
|
||||
this.$confirm({
|
||||
title: this.$t('warning'),
|
||||
content: this.$t('confirmDelete'),
|
||||
onOk: () => {
|
||||
this.batchDeleteAsync()
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
async batchDeleteAsync() {
|
||||
let successNum = 0
|
||||
let errorNum = 0
|
||||
this.loading = true
|
||||
this.loadTip = this.$t('cmdb.ci.batchDeleting')
|
||||
|
||||
const floor = Math.ceil(this.selectedRowKeys.length / 6)
|
||||
for (let i = 0; i < floor; i++) {
|
||||
const itemList = this.selectedRowKeys.slice(6 * i, 6 * i + 6)
|
||||
const promises = itemList.map((x) => deleteCI(x, false))
|
||||
await Promise.allSettled(promises)
|
||||
.then((res) => {
|
||||
res.forEach((r) => {
|
||||
if (r.status === 'fulfilled') {
|
||||
successNum += 1
|
||||
} else {
|
||||
errorNum += 1
|
||||
}
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
this.loadTip = this.$t('cmdb.ci.batchDeleting2', {
|
||||
total: this.selectedRowKeys.length,
|
||||
successNum: successNum,
|
||||
errorNum: errorNum,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
this.loading = false
|
||||
this.loadTip = ''
|
||||
this.selectedRowKeys = []
|
||||
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
|
||||
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
|
||||
this.$nextTick(() => {
|
||||
this.page = 1
|
||||
this.getTableData()
|
||||
})
|
||||
},
|
||||
|
||||
batchUpdate(values) {
|
||||
this.$confirm({
|
||||
title: this.$t('warning'),
|
||||
content: this.$t('cmdb.ci.batchUpdateConfirm'),
|
||||
onOk: () => {
|
||||
this.batchUpdateAsync(values)
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
async batchUpdateAsync(values) {
|
||||
let successNum = 0
|
||||
let errorNum = 0
|
||||
this.loading = true
|
||||
this.loadTip = this.$t('cmdb.ci.batchUpdateInProgress') + '...'
|
||||
|
||||
const payload = {}
|
||||
Object.keys(values).forEach((key) => {
|
||||
if (values[key] === undefined || values[key] === null) {
|
||||
payload[key] = null
|
||||
} else {
|
||||
payload[key] = values[key]
|
||||
}
|
||||
})
|
||||
this.$refs.create.visible = false
|
||||
const key = 'updatable'
|
||||
let errorMsg = ''
|
||||
|
||||
for (let i = 0; i < this.selectedRowKeys.length; i++) {
|
||||
await updateCI(this.selectedRowKeys[i], payload, false)
|
||||
.then(() => {
|
||||
successNum += 1
|
||||
})
|
||||
.catch((error) => {
|
||||
errorMsg = errorMsg + '\n' + `${this.selectedRowKeys[i]}:${error.response?.data?.message ?? ''}`
|
||||
this.$notification.warning({
|
||||
key,
|
||||
message: this.$t('warning'),
|
||||
description: errorMsg,
|
||||
duration: 0,
|
||||
style: { whiteSpace: 'break-spaces', overflow: 'auto', maxHeight: this.windowHeight - 80 + 'px' },
|
||||
})
|
||||
errorNum += 1
|
||||
})
|
||||
.finally(() => {
|
||||
this.loadTip = this.$t('cmdb.ci.batchUpdateInProgress2', {
|
||||
total: this.selectedRowKeys.length,
|
||||
successNum: successNum,
|
||||
errorNum: errorNum,
|
||||
})
|
||||
})
|
||||
}
|
||||
this.loading = false
|
||||
this.loadTip = ''
|
||||
this.selectedRowKeys = []
|
||||
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
|
||||
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
|
||||
this.getTableData()
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@@ -1,79 +1,82 @@
|
||||
<template>
|
||||
<div ref="wrapRef">
|
||||
<div class="table-header">
|
||||
<SearchForm
|
||||
ref="search"
|
||||
:preferenceAttrList="preferenceAttrList"
|
||||
:typeId="subnetCITypeId"
|
||||
@copyExpression="copyExpression"
|
||||
@refresh="handleSearch"
|
||||
<a-spin :tip="loadTip" :spinning="loading" >
|
||||
<div class="table-header">
|
||||
<SearchForm
|
||||
ref="search"
|
||||
:preferenceAttrList="preferenceAttrList"
|
||||
:typeId="subnetCITypeId"
|
||||
:selectedRowKeys="selectedRowKeys"
|
||||
@copyExpression="copyExpression"
|
||||
@refresh="handleSearch"
|
||||
>
|
||||
<div class="ops-list-batch-action" v-show="!!selectedRowKeys.length">
|
||||
<span @click="$refs.create.handleOpen(true, 'update')">{{ $t('update') }}</span>
|
||||
<a-divider type="vertical" />
|
||||
<span @click="openBatchDownload">{{ $t('download') }}</span>
|
||||
<a-divider type="vertical" />
|
||||
<span @click="batchDelete">{{ $t('delete') }}</span>
|
||||
<span>{{ $t('cmdb.ci.selectRows', { rows: selectedRowKeys.length }) }}</span>
|
||||
</div>
|
||||
</SearchForm>
|
||||
|
||||
<div class="table-header-right">
|
||||
<EditAttrsPopover
|
||||
:typeId="subnetCITypeId"
|
||||
@refresh="refreshAfterEditAttrs"
|
||||
>
|
||||
<a-button
|
||||
type="primary"
|
||||
ghost
|
||||
class="ops-button-ghost"
|
||||
>
|
||||
<ops-icon type="veops-configuration_table" />
|
||||
{{ $t('cmdb.configTable') }}
|
||||
</a-button>
|
||||
</EditAttrsPopover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CITable
|
||||
ref="xTable"
|
||||
:loading="loading"
|
||||
:attrList="preferenceAttrList"
|
||||
:columns="columns"
|
||||
:data="instanceList"
|
||||
:height="tableHeight"
|
||||
@sort-change="handleSortCol"
|
||||
@openDetail="openDetail"
|
||||
@deleteCI="deleteCI"
|
||||
@onSelectChange="onSelectChange"
|
||||
/>
|
||||
|
||||
<div class="table-header-right">
|
||||
<EditAttrsPopover
|
||||
:typeId="subnetCITypeId"
|
||||
@refresh="refreshAfterEditAttrs"
|
||||
<div class="table-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"
|
||||
>
|
||||
<a-button
|
||||
type="primary"
|
||||
ghost
|
||||
class="ops-button-ghost"
|
||||
>
|
||||
<ops-icon type="veops-configuration_table" />
|
||||
{{ $t('cmdb.configTable') }}
|
||||
</a-button>
|
||||
</EditAttrsPopover>
|
||||
<a-button
|
||||
v-if="instanceList && instanceList.length"
|
||||
type="primary"
|
||||
class="ops-button-ghost"
|
||||
ghost
|
||||
@click="handleExport"
|
||||
>
|
||||
<ops-icon type="veops-export" />
|
||||
{{ $t('export') }}
|
||||
</a-button>
|
||||
<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>
|
||||
|
||||
<CITable
|
||||
ref="xTable"
|
||||
:loading="loading"
|
||||
:attrList="preferenceAttrList"
|
||||
:columns="columns"
|
||||
:data="instanceList"
|
||||
:height="tableHeight"
|
||||
@sort-change="handleSortCol"
|
||||
@openDetail="openDetail"
|
||||
@deleteCI="deleteCI"
|
||||
/>
|
||||
|
||||
<div class="table-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>
|
||||
</a-spin>
|
||||
|
||||
<BatchDownload
|
||||
ref="batchDownload"
|
||||
@@ -82,6 +85,12 @@
|
||||
/>
|
||||
|
||||
<CIDetailDrawer ref="detail" :typeId="subnetCITypeId" />
|
||||
|
||||
<CreateInstanceForm
|
||||
ref="create"
|
||||
:typeIdFromRelation="subnetCITypeId"
|
||||
@submit="batchUpdate"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -90,7 +99,7 @@ import _ from 'lodash'
|
||||
import { mapState } from 'vuex'
|
||||
import ExcelJS from 'exceljs'
|
||||
import FileSaver from 'file-saver'
|
||||
import { searchCI, deleteCI } from '@/modules/cmdb/api/ci'
|
||||
import { searchCI, deleteCI, updateCI } from '@/modules/cmdb/api/ci'
|
||||
import { getSubscribeAttributes } from '@/modules/cmdb/api/preference'
|
||||
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
|
||||
import { getCITableColumns } from '@/modules/cmdb/utils/helper'
|
||||
@@ -100,6 +109,7 @@ import CITable from '@/modules/cmdb/components/ciTable/index.vue'
|
||||
import BatchDownload from '@/modules/cmdb/components/batchDownload/batchDownload.vue'
|
||||
import CIDetailDrawer from '@/modules/cmdb/views/ci/modules/ciDetailDrawer.vue'
|
||||
import EditAttrsPopover from '@/modules/cmdb/views/ci/modules/editAttrsPopover.vue'
|
||||
import CreateInstanceForm from '@/modules/cmdb/views/ci/modules/CreateInstanceForm'
|
||||
|
||||
export default {
|
||||
name: 'SubnetList',
|
||||
@@ -108,7 +118,8 @@ export default {
|
||||
CITable,
|
||||
BatchDownload,
|
||||
CIDetailDrawer,
|
||||
EditAttrsPopover
|
||||
EditAttrsPopover,
|
||||
CreateInstanceForm
|
||||
},
|
||||
props: {
|
||||
subnetCIType: {
|
||||
@@ -122,6 +133,7 @@ export default {
|
||||
pageSize: 50,
|
||||
pageSizeOptions: ['50', '100', '200'],
|
||||
loading: false,
|
||||
loadTip: '',
|
||||
sortByTable: undefined,
|
||||
|
||||
instanceList: [],
|
||||
@@ -130,6 +142,7 @@ export default {
|
||||
preferenceAttrList: [],
|
||||
attrList: [],
|
||||
attributes: {},
|
||||
selectedRowKeys: [],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -275,7 +288,7 @@ export default {
|
||||
})
|
||||
},
|
||||
|
||||
handleExport() {
|
||||
openBatchDownload() {
|
||||
this.$refs.batchDownload.open({
|
||||
preferenceAttrList: this.preferenceAttrList,
|
||||
ciTypeName: this.$t('cmdb.ipam.subnetList') || '',
|
||||
@@ -336,6 +349,7 @@ export default {
|
||||
FileSaver.saveAs(file, `${filename}.xlsx`)
|
||||
})
|
||||
|
||||
this.selectedRowKeys = []
|
||||
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
|
||||
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
|
||||
},
|
||||
@@ -362,6 +376,120 @@ export default {
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
onSelectChange(records) {
|
||||
this.selectedRowKeys = records.map((i) => i.ci_id || i._id)
|
||||
},
|
||||
|
||||
batchDelete() {
|
||||
this.$confirm({
|
||||
title: this.$t('warning'),
|
||||
content: this.$t('confirmDelete'),
|
||||
onOk: () => {
|
||||
this.batchDeleteAsync()
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
async batchDeleteAsync() {
|
||||
let successNum = 0
|
||||
let errorNum = 0
|
||||
this.loading = true
|
||||
this.loadTip = this.$t('cmdb.ci.batchDeleting')
|
||||
|
||||
const floor = Math.ceil(this.selectedRowKeys.length / 6)
|
||||
for (let i = 0; i < floor; i++) {
|
||||
const itemList = this.selectedRowKeys.slice(6 * i, 6 * i + 6)
|
||||
const promises = itemList.map((x) => deleteCI(x, false))
|
||||
await Promise.allSettled(promises)
|
||||
.then((res) => {
|
||||
res.forEach((r) => {
|
||||
if (r.status === 'fulfilled') {
|
||||
successNum += 1
|
||||
} else {
|
||||
errorNum += 1
|
||||
}
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
this.loadTip = this.$t('cmdb.ci.batchDeleting2', {
|
||||
total: this.selectedRowKeys.length,
|
||||
successNum: successNum,
|
||||
errorNum: errorNum,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
this.loading = false
|
||||
this.loadTip = ''
|
||||
this.selectedRowKeys = []
|
||||
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
|
||||
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
|
||||
this.$nextTick(() => {
|
||||
this.page = 1
|
||||
this.getTableData()
|
||||
})
|
||||
},
|
||||
|
||||
batchUpdate(values) {
|
||||
this.$confirm({
|
||||
title: this.$t('warning'),
|
||||
content: this.$t('cmdb.ci.batchUpdateConfirm'),
|
||||
onOk: () => {
|
||||
this.batchUpdateAsync(values)
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
async batchUpdateAsync(values) {
|
||||
let successNum = 0
|
||||
let errorNum = 0
|
||||
this.loading = true
|
||||
this.loadTip = this.$t('cmdb.ci.batchUpdateInProgress') + '...'
|
||||
|
||||
const payload = {}
|
||||
Object.keys(values).forEach((key) => {
|
||||
if (values[key] === undefined || values[key] === null) {
|
||||
payload[key] = null
|
||||
} else {
|
||||
payload[key] = values[key]
|
||||
}
|
||||
})
|
||||
this.$refs.create.visible = false
|
||||
const key = 'updatable'
|
||||
let errorMsg = ''
|
||||
|
||||
for (let i = 0; i < this.selectedRowKeys.length; i++) {
|
||||
await updateCI(this.selectedRowKeys[i], payload, false)
|
||||
.then(() => {
|
||||
successNum += 1
|
||||
})
|
||||
.catch((error) => {
|
||||
errorMsg = errorMsg + '\n' + `${this.selectedRowKeys[i]}:${error.response?.data?.message ?? ''}`
|
||||
this.$notification.warning({
|
||||
key,
|
||||
message: this.$t('warning'),
|
||||
description: errorMsg,
|
||||
duration: 0,
|
||||
style: { whiteSpace: 'break-spaces', overflow: 'auto', maxHeight: this.windowHeight - 80 + 'px' },
|
||||
})
|
||||
errorNum += 1
|
||||
})
|
||||
.finally(() => {
|
||||
this.loadTip = this.$t('cmdb.ci.batchUpdateInProgress2', {
|
||||
total: this.selectedRowKeys.length,
|
||||
successNum: successNum,
|
||||
errorNum: errorNum,
|
||||
})
|
||||
})
|
||||
}
|
||||
this.loading = false
|
||||
this.loadTip = ''
|
||||
this.selectedRowKeys = []
|
||||
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
|
||||
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
|
||||
this.getTableData()
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@@ -14,35 +14,26 @@
|
||||
>
|
||||
<a-form :form="form" @submit="handleSubmit" :label-col="{ span: 6 }" :wrapper-col="{ span: 14 }">
|
||||
<a-form-item :label="$t('cmdb.ciType.sourceCIType')">
|
||||
<a-select
|
||||
showSearch
|
||||
name="source_ci_type_id"
|
||||
<CMDBTypeSelectAntd
|
||||
v-decorator="[
|
||||
'source_ci_type_id',
|
||||
{ rules: [{ required: true, message: $t('cmdb.ciType.sourceCITypeTips') }] },
|
||||
]"
|
||||
name="source_ci_type_id"
|
||||
:CITypeGroup="CITypeGroups"
|
||||
@change="handleSourceTypeChange"
|
||||
:filterOption="filterOption"
|
||||
>
|
||||
<a-select-option :value="CIType.id" :key="CIType.id" v-for="CIType in displayCITypes">
|
||||
{{ CIType.alias || CIType.name }}
|
||||
<span class="model-select-name">({{ CIType.name }})</span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :label="$t('cmdb.ciType.dstCIType')">
|
||||
<a-select
|
||||
showSearch
|
||||
<CMDBTypeSelectAntd
|
||||
v-decorator="[
|
||||
'ci_type_id',
|
||||
{ rules: [{ required: true, message: $t('cmdb.ciType.dstCITypeTips') }] },
|
||||
]"
|
||||
name="ci_type_id"
|
||||
v-decorator="['ci_type_id', { rules: [{ required: true, message: $t('cmdb.ciType.dstCITypeTips') }] }]"
|
||||
:CITypeGroup="CITypeGroups"
|
||||
@change="handleTargetTypeChange"
|
||||
:filterOption="filterOption"
|
||||
>
|
||||
<a-select-option :value="CIType.id" :key="CIType.id" v-for="CIType in displayTargetCITypes">
|
||||
{{ CIType.alias || CIType.name }}
|
||||
<span class="model-select-name">({{ CIType.name }})</span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item :label="$t('cmdb.ciType.relationType')">
|
||||
@@ -65,6 +56,7 @@
|
||||
'constraint',
|
||||
{ rules: [{ required: true, message: $t('cmdb.ciType.relationConstraintTips') }] },
|
||||
]"
|
||||
@change="handleConstraintChange"
|
||||
>
|
||||
<a-select-option value="0">{{ $t('cmdb.ciType.one2Many') }}</a-select-option>
|
||||
<a-select-option value="1">{{ $t('cmdb.ciType.one2One') }}</a-select-option>
|
||||
@@ -72,6 +64,11 @@
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item :label="$t('cmdb.ciType.attributeAssociation')">
|
||||
<template #extra>
|
||||
<div>{{ $t('cmdb.ciType.attributeAssociationTip7') }}</div>
|
||||
<div>{{ $t('cmdb.ciType.attributeAssociationTip8') }}</div>
|
||||
<div>{{ $t('cmdb.ciType.attributeAssociationTip9') }}</div>
|
||||
</template>
|
||||
<a-row
|
||||
v-for="item in modalAttrList"
|
||||
:key="item.id"
|
||||
@@ -83,7 +80,10 @@
|
||||
allowClear
|
||||
v-model="item.parentAttrId"
|
||||
>
|
||||
<a-select-option v-for="attr in filterAttributes(modalParentAttributes)" :key="attr.id">
|
||||
<a-select-option
|
||||
v-for="attr in filterAttributes(modalParentAttributes, item.childAttrId, modalChildAttributes, 'parent')"
|
||||
:key="attr.id"
|
||||
>
|
||||
{{ attr.alias || attr.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
@@ -99,7 +99,10 @@
|
||||
allowClear
|
||||
v-model="item.childAttrId"
|
||||
>
|
||||
<a-select-option v-for="attr in filterAttributes(modalChildAttributes)" :key="attr.id">
|
||||
<a-select-option
|
||||
v-for="attr in filterAttributes(modalChildAttributes, item.parentAttrId, modalParentAttributes, 'child')"
|
||||
:key="attr.id"
|
||||
>
|
||||
{{ attr.alias || attr.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
@@ -127,7 +130,6 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ModelRelationTable from './modules/modelRelationTable.vue'
|
||||
import { searchResourceType } from '@/modules/acl/api/resource'
|
||||
import { getCITypeGroupsConfig } from '@/modules/cmdb/api/ciTypeGroup'
|
||||
import { getCITypes } from '@/modules/cmdb/api/CIType'
|
||||
@@ -135,10 +137,14 @@ import { createRelation, deleteRelation, getRelationTypes } from '@/modules/cmdb
|
||||
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import ModelRelationTable from './modules/modelRelationTable.vue'
|
||||
import CMDBTypeSelectAntd from '@/modules/cmdb/components/cmdbTypeSelect/cmdbTypeSelectAntd'
|
||||
|
||||
export default {
|
||||
name: 'Index',
|
||||
components: {
|
||||
ModelRelationTable,
|
||||
CMDBTypeSelectAntd
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -189,7 +195,7 @@ export default {
|
||||
'1': this.$t('cmdb.ciType.one2One'),
|
||||
'2': this.$t('cmdb.ciType.many2Many'),
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
@@ -362,18 +368,33 @@ export default {
|
||||
this.modalChildAttributes = res?.attributes ?? []
|
||||
})
|
||||
},
|
||||
filterOption(input, option) {
|
||||
return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
|
||||
},
|
||||
filterAttributes(attributes) {
|
||||
// filter password/json/is_list/longText/bool/reference
|
||||
return attributes.filter((attr) => {
|
||||
|
||||
filterAttributes(attributes, relationAttrId, relationAttrs, type) {
|
||||
const relationAttr = relationAttrs.find((attr) => attr.id === relationAttrId)
|
||||
|
||||
// filter password/json/longText/bool/reference
|
||||
let filterAttrs = attributes.filter((attr) => {
|
||||
if (attr.value_type === '2' && !attr.is_index) {
|
||||
return false
|
||||
}
|
||||
|
||||
return !attr.is_password && !attr.is_list && attr.value_type !== '6' && !attr.is_bool && !attr.is_reference
|
||||
return !attr.is_password && attr.value_type !== '6' && !attr.is_bool && !attr.is_reference
|
||||
})
|
||||
|
||||
if (relationAttr) {
|
||||
filterAttrs = filterAttrs.filter((attr) => attr.value_type === relationAttr?.value_type)
|
||||
}
|
||||
|
||||
const constraintValue = Number(this.form.getFieldValue('constraint'))
|
||||
if (
|
||||
(constraintValue === 0 && type === 'child') ||
|
||||
constraintValue === 1 ||
|
||||
(constraintValue === 2 && relationAttr?.is_list)
|
||||
) {
|
||||
return filterAttrs.filter((attr) => !attr.is_list)
|
||||
}
|
||||
|
||||
return filterAttrs
|
||||
},
|
||||
|
||||
addModalAttr() {
|
||||
@@ -393,6 +414,13 @@ export default {
|
||||
if (index !== -1) {
|
||||
this.modalAttrList.splice(index, 1)
|
||||
}
|
||||
},
|
||||
|
||||
handleConstraintChange() {
|
||||
this.modalAttrList.forEach((item) => {
|
||||
item.parentAttrId = undefined
|
||||
item.childAttrId = undefined
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@@ -15,7 +15,7 @@
|
||||
@edit-closed="handleEditClose"
|
||||
@edit-actived="handleEditActived"
|
||||
>
|
||||
<vxe-column field="created_at" :title="$t('created_at')" sortable width="159px"></vxe-column>
|
||||
<vxe-column field="created_at" :title="$t('created_at')" sortable width="170"></vxe-column>
|
||||
<vxe-column field="parent.alias" :title="$t('cmdb.ciType.sourceCIType')"></vxe-column>
|
||||
<vxe-column
|
||||
field="relation_type_id"
|
||||
@@ -38,7 +38,13 @@
|
||||
<vxe-column :width="300" field="attributeAssociation" :edit-render="{}">
|
||||
<template #header>
|
||||
<span>
|
||||
<a-tooltip :title="$t('cmdb.ciType.attributeAssociationTip1')">
|
||||
<a-tooltip>
|
||||
<template #title>
|
||||
<div>{{ $t('cmdb.ciType.attributeAssociationTip1') }}</div>
|
||||
<div>{{ $t('cmdb.ciType.attributeAssociationTip7') }}</div>
|
||||
<div>{{ $t('cmdb.ciType.attributeAssociationTip8') }}</div>
|
||||
<div>{{ $t('cmdb.ciType.attributeAssociationTip9') }}</div>
|
||||
</template>
|
||||
<a><a-icon type="question-circle"/></a>
|
||||
</a-tooltip>
|
||||
{{ $t('cmdb.ciType.attributeAssociation') }}
|
||||
@@ -76,7 +82,7 @@
|
||||
optionFilterProp="title"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="attr in filterAttributes(type2attributes[row.parent_id])"
|
||||
v-for="attr in filterAttributes(row, item.childAttrId, 'parent')"
|
||||
:key="attr.id"
|
||||
:value="attr.id"
|
||||
:title="attr.alias || attr.name"
|
||||
@@ -95,7 +101,7 @@
|
||||
optionFilterProp="title"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="attr in filterAttributes(type2attributes[row.child_id])"
|
||||
v-for="attr in filterAttributes(row, item.parentAttrId, 'child')"
|
||||
:key="attr.id"
|
||||
:value="attr.id"
|
||||
:title="attr.alias || attr.name"
|
||||
@@ -298,15 +304,37 @@ export default {
|
||||
const _find = attributes.find((attr) => attr.id === id)
|
||||
return _find?.alias ?? _find?.name ?? id
|
||||
},
|
||||
filterAttributes(attributes) {
|
||||
// filter password/json/is_list/longText/bool/reference
|
||||
return attributes.filter((attr) => {
|
||||
|
||||
filterAttributes(row, relationAttrId, type) {
|
||||
const { parent_id, child_id, constraint } = row
|
||||
const currentAttrs = this.type2attributes?.[child_id] || []
|
||||
|
||||
const relationAttrs = this.type2attributes?.[parent_id] || []
|
||||
const relationAttr = relationAttrs.find((attr) => attr.id === relationAttrId)
|
||||
|
||||
// filter password/json/longText/bool/reference
|
||||
let filterAttrs = currentAttrs.filter((attr) => {
|
||||
if (attr.value_type === '2' && !attr.is_index) {
|
||||
return false
|
||||
}
|
||||
|
||||
return !attr.is_password && !attr.is_list && attr.value_type !== '6' && !attr.is_bool && !attr.is_reference
|
||||
return !attr.is_password && attr.value_type !== '6' && !attr.is_bool && !attr.is_reference
|
||||
})
|
||||
|
||||
if (relationAttr) {
|
||||
filterAttrs = filterAttrs.filter((attr) => attr.value_type === relationAttr?.value_type)
|
||||
}
|
||||
|
||||
const constraintValue = Number(constraint)
|
||||
if (
|
||||
(constraintValue === 0 && type === 'child') ||
|
||||
constraintValue === 1 ||
|
||||
(constraintValue === 2 && relationAttr?.is_list)
|
||||
) {
|
||||
return filterAttrs.filter((attr) => !attr.is_list)
|
||||
}
|
||||
|
||||
return filterAttrs
|
||||
},
|
||||
|
||||
addTableAttr() {
|
||||
|
@@ -0,0 +1,177 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:title="$t('cmdb.preference.autoSub')"
|
||||
:visible="visible"
|
||||
:width="600"
|
||||
@cancel="handleCancel"
|
||||
@ok="handleOk"
|
||||
>
|
||||
<a-form-model
|
||||
ref="autuSubFormRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:label-col="{ span: 7 }"
|
||||
:wrapper-col="{ span: 15 }"
|
||||
>
|
||||
<a-form-model-item
|
||||
:label="$t('cmdb.preference.autoSubScope')"
|
||||
prop="base_strategy"
|
||||
>
|
||||
<a-radio-group
|
||||
v-model="form.base_strategy"
|
||||
:options="baseStrategyOptions"
|
||||
/>
|
||||
</a-form-model-item>
|
||||
<a-form-model-item
|
||||
:label="form.base_strategy === 'all' ? $t('cmdb.preference.excludeGroup') : $t('cmdb.preference.selectGroup')"
|
||||
prop="group_ids"
|
||||
>
|
||||
<a-select
|
||||
v-model="form.group_ids"
|
||||
mode="multiple"
|
||||
optionFilterProp="title"
|
||||
:options="groupSelectOptions"
|
||||
/>
|
||||
</a-form-model-item>
|
||||
|
||||
<a-form-model-item
|
||||
:label="form.base_strategy === 'all' ? $t('cmdb.preference.excludeModel') : $t('cmdb.preference.selectModel')"
|
||||
prop="type_ids"
|
||||
>
|
||||
<a-select
|
||||
v-model="form.type_ids"
|
||||
mode="multiple"
|
||||
optionFilterProp="title"
|
||||
>
|
||||
<a-select-opt-group
|
||||
v-for="(group) in modelSelectOptions"
|
||||
:key="group.value"
|
||||
:title="group.label"
|
||||
>
|
||||
<span slot="label">{{ group.label }}</span>
|
||||
<a-select-option
|
||||
v-for="(type) in group.children"
|
||||
:key="type.value"
|
||||
:value="type.value"
|
||||
:title="type.label"
|
||||
>
|
||||
{{ type.label }}
|
||||
</a-select-option>
|
||||
</a-select-opt-group>
|
||||
</a-select>
|
||||
</a-form-model-item>
|
||||
|
||||
<a-form-model-item
|
||||
:label="$t('cmdb.preference.isEnable')"
|
||||
prop="enabled"
|
||||
:extra="$t('cmdb.preference.enableAutoSubTip')"
|
||||
>
|
||||
<a-switch v-model="form.enabled" />
|
||||
</a-form-model-item>
|
||||
</a-form-model>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { putAutoSubscription } from '@/modules/cmdb/api/preference.js'
|
||||
|
||||
export default {
|
||||
name: 'AutoSubscribe',
|
||||
props: {
|
||||
ciType: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
autoSub: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
form: {
|
||||
base_strategy: 'all',
|
||||
group_ids: [],
|
||||
type_ids: [],
|
||||
enabled: true,
|
||||
},
|
||||
rules: {
|
||||
base_strategy: [{ required: true, message: this.$t('placeholder2') }],
|
||||
},
|
||||
baseStrategyOptions: [
|
||||
{
|
||||
label: this.$t('cmdb.preference.subscribeAllModel'),
|
||||
value: 'all'
|
||||
},
|
||||
{
|
||||
label: this.$t('cmdb.preference.selectiveSubscription'),
|
||||
value: 'none'
|
||||
}
|
||||
],
|
||||
groupSelectOptions: [],
|
||||
modelSelectOptions: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async open() {
|
||||
this.form = {
|
||||
base_strategy: this.autoSub?.base_strategy || 'all',
|
||||
group_ids: this.autoSub?.group_ids || [],
|
||||
type_ids: this.autoSub?.type_ids || [],
|
||||
enabled: this.autoSub?.enabled ?? true
|
||||
}
|
||||
|
||||
this.groupSelectOptions = this.ciType.map((group) => {
|
||||
return {
|
||||
label: group.name,
|
||||
title: group.name,
|
||||
value: group.id
|
||||
}
|
||||
})
|
||||
|
||||
const modelSelectOptions = this.ciType.filter((group) => group?.ci_types?.length)
|
||||
this.modelSelectOptions = modelSelectOptions.map((group) => {
|
||||
return {
|
||||
label: group.name,
|
||||
value: group.id,
|
||||
children: group.ci_types.map((type) => {
|
||||
return {
|
||||
label: type.alias || type.name,
|
||||
value: type.id
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
this.visible = true
|
||||
},
|
||||
handleCancel() {
|
||||
this.visible = false
|
||||
},
|
||||
handleOk() {
|
||||
this.$refs.autuSubFormRef.validate(async (valid) => {
|
||||
if (valid) {
|
||||
const { base_strategy, group_ids, type_ids, enabled } = this.form
|
||||
|
||||
const params = {
|
||||
base_strategy: base_strategy,
|
||||
group_ids: group_ids.join(','),
|
||||
type_ids: type_ids.join(','),
|
||||
enabled: enabled
|
||||
}
|
||||
|
||||
putAutoSubscription(params).then(() => {
|
||||
this.$message.success(this.$t('saveSuccess'))
|
||||
this.handleCancel()
|
||||
this.$emit('ok')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
@@ -86,13 +86,15 @@
|
||||
</div>
|
||||
<span class="cmdb-preference-group-content-title">{{ ciType.alias || ciType.name }}</span>
|
||||
<span class="cmdb-preference-group-content-action">
|
||||
<a-tooltip :title="$t('cmdb.preference.cancelSub')">
|
||||
<span
|
||||
@click="unsubscribe(ciType, group.type)"
|
||||
><ops-icon type="cmdb-preference-cancel-subscribe" />
|
||||
</span>
|
||||
</a-tooltip>
|
||||
<a-divider type="vertical" :style="{ margin: '0 3px' }" />
|
||||
<template v-if="!enableAutoSub || subType.type === 'tree'">
|
||||
<a-tooltip :title="$t('cmdb.preference.cancelSub')">
|
||||
<span
|
||||
@click="unsubscribe(ciType, group.type)"
|
||||
><ops-icon type="cmdb-preference-cancel-subscribe" />
|
||||
</span>
|
||||
</a-tooltip>
|
||||
<a-divider type="vertical" :style="{ margin: '0 3px' }" />
|
||||
</template>
|
||||
<a-tooltip :title="$t('cmdb.preference.editSub')">
|
||||
<span
|
||||
@click="openSubscribeSetting(ciType, `${index + 1}`)"
|
||||
@@ -108,11 +110,23 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="cmdb-preference-right">
|
||||
<a-input-search
|
||||
v-model="searchValue"
|
||||
:style="{ width: '300px', marginBottom: '20px' }"
|
||||
:placeholder="$t('cmdb.preference.searchPlaceholder')"
|
||||
/>
|
||||
<div class="cmdb-preference-right-header">
|
||||
<a-input-search
|
||||
v-model="searchValue"
|
||||
class="cmdb-preference-right-header-search"
|
||||
:placeholder="$t('cmdb.preference.searchPlaceholder')"
|
||||
/>
|
||||
<div
|
||||
:class="[
|
||||
'cmdb-preference-right-header-auto',
|
||||
enableAutoSub ? 'cmdb-preference-right-header-auto_enable' : ''
|
||||
]"
|
||||
@click="openAutoSubModal"
|
||||
>
|
||||
<ops-icon type="auto" />
|
||||
<span>{{ enableAutoSub ? $t('cmdb.preference.autoSub') : $t('cmdb.preference.autoSub2') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="group in filterCiTypeData" :key="group.id">
|
||||
<p
|
||||
@click="changeGroupExpand(group)"
|
||||
@@ -154,14 +168,6 @@
|
||||
{{ item.alias || item.name }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="cmdb-preference-colleague">
|
||||
<span
|
||||
v-if="type_id2users[item.id] && type_id2users[item.id].length"
|
||||
>{{ type_id2users[item.id].length > 99 ? '99+' : type_id2users[item.id].length
|
||||
}}{{ $t('cmdb.preference.peopleSub') }}</span
|
||||
>
|
||||
<span v-else>{{ $t('cmdb.preference.noSub') }}</span>
|
||||
</div>
|
||||
<div class="cmdb-preference-progress">
|
||||
<div class="cmdb-preference-progress-info">
|
||||
<span>{{ $t('cmdb.menu.ad') }}</span>
|
||||
@@ -173,11 +179,20 @@
|
||||
</div>
|
||||
<a-divider :style="{ margin: '10px 0 3px 0' }" />
|
||||
<div class="cmdb-preference-footor-subscribed" v-if="item.is_subscribed">
|
||||
<span><a-icon type="clock-circle" :style="{ marginRight: '3px' }" />{{ getsubscribedDays(item) }}</span>
|
||||
<span
|
||||
:style="{
|
||||
opacity: enableAutoSub ? 0 : 1
|
||||
}"
|
||||
>
|
||||
<a-icon type="clock-circle" :style="{ marginRight: '3px' }" />{{ getsubscribedDays(item) }}
|
||||
</span>
|
||||
<span>
|
||||
<a-tooltip :title="$t('cmdb.preference.cancelSub')">
|
||||
<span @click="unsubscribe(item)"><ops-icon type="cmdb-preference-cancel-subscribe" /> </span>
|
||||
</a-tooltip>
|
||||
<template v-if="!enableAutoSub">
|
||||
<a-tooltip :title="$t('cmdb.preference.cancelSub')">
|
||||
<span @click="unsubscribe(item)"><ops-icon type="cmdb-preference-cancel-subscribe" /> </span>
|
||||
</a-tooltip>
|
||||
<a-divider type="vertical" :style="{ margin: '0 3px' }" />
|
||||
</template>
|
||||
<a-divider type="vertical" :style="{ margin: '0 3px' }" />
|
||||
<a-tooltip :title="$t('cmdb.preference.editSub')">
|
||||
<span @click="openSubscribeSetting(item)"><ops-icon type="cmdb-preference-subscribe"/></span>
|
||||
@@ -185,13 +200,15 @@
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="cmdb-preference-footor-unsubscribed">
|
||||
<a
|
||||
@click="handleSubscribeCIType(item)"
|
||||
class="cmdb-preference-footor-unsubscribed-item"
|
||||
>
|
||||
<ops-icon type="cmdb-ci" />{{ $t('cmdb.preference.subCITable') }}
|
||||
</a>
|
||||
<span class="cmdb-preference-footor-unsubscribed-gap"></span>
|
||||
<template v-if="!enableAutoSub">
|
||||
<a
|
||||
@click="handleSubscribeCIType(item)"
|
||||
class="cmdb-preference-footor-unsubscribed-item"
|
||||
>
|
||||
<ops-icon type="cmdb-ci" />{{ $t('cmdb.preference.subCITable') }}
|
||||
</a>
|
||||
<span class="cmdb-preference-footor-unsubscribed-gap"></span>
|
||||
</template>
|
||||
<a
|
||||
@click="openSubscribeSetting(item, '2')"
|
||||
class="cmdb-preference-footor-unsubscribed-item"
|
||||
@@ -209,17 +226,21 @@
|
||||
ref="subscribeSetting"
|
||||
@reload="
|
||||
() => {
|
||||
resetRoute()
|
||||
initData()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<AutoSubscribe
|
||||
ref="autoSubRef"
|
||||
:ciType="citypeData"
|
||||
:autoSub="autoSub"
|
||||
@ok="initData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import router, { resetRouter } from '@/router'
|
||||
import store from '@/store'
|
||||
import { mapState } from 'vuex'
|
||||
import moment from 'moment'
|
||||
import draggable from 'vuedraggable'
|
||||
@@ -238,10 +259,12 @@ import SubscribeSetting from '../../components/subscribeSetting/subscribeSetting
|
||||
import { getCIAdcStatistics } from '../../api/ci'
|
||||
import { ops_move_icon as OpsMoveIcon } from '@/core/icons'
|
||||
import { SUB_NET_CITYPE_NAME, SCOPE_CITYPE_NAME, ADDRESS_CITYPE_NAME } from '../ipam/constants.js'
|
||||
import AutoSubscribe from './components/autoSubscribe.vue'
|
||||
import { getAutoSubscription } from '@/modules/cmdb/api/preference.js'
|
||||
|
||||
export default {
|
||||
name: 'Preference',
|
||||
components: { CollapseTransition, SubscribeSetting, draggable, OpsMoveIcon, Ellipsis },
|
||||
components: { CollapseTransition, SubscribeSetting, draggable, OpsMoveIcon, Ellipsis, AutoSubscribe },
|
||||
data() {
|
||||
return {
|
||||
citypeData: [],
|
||||
@@ -253,6 +276,7 @@ export default {
|
||||
type_id2users: {},
|
||||
myPreferences: [],
|
||||
searchValue: '',
|
||||
autoSub: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -275,11 +299,19 @@ export default {
|
||||
}
|
||||
return this.citypeData
|
||||
},
|
||||
enableAutoSub() {
|
||||
return this?.autoSub?.enabled ?? false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getCITypes(true)
|
||||
this.getAutoSubscription()
|
||||
},
|
||||
methods: {
|
||||
initData() {
|
||||
this.getCITypes()
|
||||
this.getAutoSubscription()
|
||||
},
|
||||
async getCITypes(isInit = false) {
|
||||
const [ciTypeGroup, pref, pref2, statistics] = await Promise.all([
|
||||
getCITypeGroups({ need_other: true }),
|
||||
@@ -350,6 +382,12 @@ export default {
|
||||
}, 300)
|
||||
}
|
||||
},
|
||||
|
||||
async getAutoSubscription() {
|
||||
const res = await getAutoSubscription()
|
||||
this.autoSub = res || {}
|
||||
},
|
||||
|
||||
getsubscribedDays(item) {
|
||||
const subscribedTime = this.self.type_id2subs_time[item.id]
|
||||
moment.duration(moment().diff(moment(subscribedTime)))
|
||||
@@ -396,21 +434,11 @@ export default {
|
||||
}
|
||||
}
|
||||
that.$message.success(that.$t('cmdb.preference.cancelSubSuccess'))
|
||||
that.resetRoute()
|
||||
that.initData()
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
resetRoute() {
|
||||
const roles = store.getters.roles
|
||||
store.dispatch('GenerateRoutes', { roles }, { root: true }).then(() => {
|
||||
resetRouter()
|
||||
this.$nextTick(() => {
|
||||
router.addRoutes(store.getters.appRoutes)
|
||||
this.getCITypes()
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
async handleSubscribeCIType(ciType) {
|
||||
try {
|
||||
@@ -433,7 +461,7 @@ export default {
|
||||
subscribeList
|
||||
)
|
||||
this.$message.success(this.$t('cmdb.components.subSuccess'))
|
||||
this.resetRoute()
|
||||
this.initData()
|
||||
} catch (error) {
|
||||
console.error('handleSubscribeCIType failed', error)
|
||||
this.$message.success(this.$t('cmdb.components.subFailed'))
|
||||
@@ -461,7 +489,7 @@ export default {
|
||||
})
|
||||
preferenceCitypeOrder({ type_ids: typeIds, is_tree: false })
|
||||
.then(() => {
|
||||
this.resetRoute()
|
||||
this.initData()
|
||||
})
|
||||
.catch(() => {
|
||||
this.getCITypes(false)
|
||||
@@ -487,13 +515,17 @@ export default {
|
||||
preferenceCitypeOrder({ type_ids: typeIds, is_tree: isTree })
|
||||
.then(() => {
|
||||
if (!isTree) {
|
||||
this.resetRoute()
|
||||
this.initData()
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.getCITypes(false)
|
||||
})
|
||||
},
|
||||
|
||||
openAutoSubModal() {
|
||||
this.$refs.autoSubRef.open()
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -629,6 +661,45 @@ export default {
|
||||
height: 100%;
|
||||
padding-top: 24px;
|
||||
|
||||
&-header {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&-search {
|
||||
width: 300px;
|
||||
margin-right: 14px;
|
||||
}
|
||||
|
||||
&-auto {
|
||||
background: linear-gradient(90deg, #16D9E3 0%, #30C7EC 47%, #46AEF7 100%);
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
color: #FFFFFF;
|
||||
cursor: pointer;
|
||||
padding: 0 12px;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
span {
|
||||
margin-left: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&_enable {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-group-title {
|
||||
width: 300px;
|
||||
margin-bottom: 20px;
|
||||
@@ -651,7 +722,7 @@ export default {
|
||||
.cmdb-preference-type {
|
||||
display: inline-block;
|
||||
width: 195px;
|
||||
height: 155px;
|
||||
height: 127px;
|
||||
border-radius: @border-radius-box;
|
||||
background-color: #fff;
|
||||
box-shadow: ~'0px 2px 8px @{primary-color}15';
|
||||
|
@@ -30,13 +30,13 @@
|
||||
>新增</a-button
|
||||
>
|
||||
</SearchForm>
|
||||
<vxe-table
|
||||
<ops-table
|
||||
ref="xTable"
|
||||
row-id="_id"
|
||||
:data="tableData"
|
||||
:height="tableHeight"
|
||||
highlight-hover-row
|
||||
:checkbox-config="{ reserve: true }"
|
||||
:checkbox-config="{ reserve: true, highlight: true, range: true }"
|
||||
@checkbox-change="onSelectChange"
|
||||
@checkbox-all="onSelectChange"
|
||||
show-overflow="tooltip"
|
||||
@@ -76,7 +76,7 @@
|
||||
<span v-if="col.value_type == '6' && row[col.field]">{{ JSON.stringify(row[col.field]) }}</span>
|
||||
</template>
|
||||
</vxe-table-column>
|
||||
</vxe-table>
|
||||
</ops-table>
|
||||
<a-pagination
|
||||
v-model="currentPage"
|
||||
size="small"
|
||||
@@ -216,7 +216,7 @@ export default {
|
||||
this.totalNumber = res.numfound
|
||||
this.columns = this.getColumns(res.result, this.preferenceAttrList)
|
||||
this.$nextTick(() => {
|
||||
const _table = this.$refs.xTable
|
||||
const _table = this.$refs.xTable?.getVxetableRef?.()
|
||||
if (_table) {
|
||||
_table.refreshColumn()
|
||||
}
|
||||
@@ -316,7 +316,11 @@ export default {
|
||||
|
||||
onSelectChange() {},
|
||||
handleClose() {
|
||||
this.$refs.xTable.clearCheckboxRow()
|
||||
const _table = this.$refs.xTable?.getVxetableRef?.()
|
||||
if (_table) {
|
||||
_table.clearCheckboxRow()
|
||||
}
|
||||
|
||||
this.currentPage = 1
|
||||
this.expression = ''
|
||||
this.isFocusExpression = false
|
||||
@@ -324,8 +328,10 @@ export default {
|
||||
this.showCreateBtn = true
|
||||
},
|
||||
async handleOk() {
|
||||
const selectRecordsCurrent = this.$refs.xTable.getCheckboxRecords()
|
||||
const selectRecordsReserved = this.$refs.xTable.getCheckboxReserveRecords()
|
||||
const _table = this.$refs.xTable?.getVxetableRef?.()
|
||||
const selectRecordsCurrent = _table?.getCheckboxRecords?.() || []
|
||||
const selectRecordsReserved = _table?.getCheckboxReserveRecords?.() || []
|
||||
|
||||
const ciIds = [...selectRecordsCurrent, ...selectRecordsReserved].map((record) => record._id)
|
||||
if (ciIds.length) {
|
||||
if (this.type === 'children') {
|
||||
|
@@ -238,9 +238,11 @@
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :label="$t('cmdb.topo.centralNodeType')" prop="central_node_type">
|
||||
<a-select @change="CITypeChange" v-decorator="['central_node_type', { rules: [{ required: true, message: $t('cmdb.topo.typeRequired') }]}]" :showSearch="true" optionFilterProp="label">
|
||||
<a-select-option v-for="t in ciTypes" :key="t.id" :value="t.id" :label="t.alias">{{ t.alias }}</a-select-option>
|
||||
</a-select>
|
||||
<CMDBTypeSelectAntd
|
||||
v-decorator="['central_node_type', { rules: [{ required: true, message: $t('cmdb.topo.typeRequired') }]}]"
|
||||
:placeholder="$t('cmdb.ciType.selectModel')"
|
||||
@change="CITypeChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :label="$t('cmdb.topo.filterInstances')" prop="central_node_instances">
|
||||
<a-input
|
||||
@@ -325,7 +327,6 @@ import draggable from 'vuedraggable'
|
||||
import emptyImage from '@/assets/data_empty.png'
|
||||
import SplitPane from '@/components/SplitPane'
|
||||
import { ops_move_icon as OpsMoveIcon } from '@/core/icons'
|
||||
import { getCITypeGroups } from '@/modules/cmdb/api/ciTypeGroup'
|
||||
import { roleHasPermissionToGrant } from '@/modules/acl/api/permission'
|
||||
import { searchResourceType } from '@/modules/acl/api/resource'
|
||||
import SeeksRelationGraph from '@/modules/cmdb/3rd/relation-graph'
|
||||
@@ -336,6 +337,7 @@ import { searchCI } from '@/modules/cmdb/api/ci'
|
||||
import { getTopoGroups, postTopoGroup, putTopoGroupByGId, putTopoGroupsOrder, deleteTopoGroup, getTopoView, addTopoView, updateTopoView, deleteTopoView, getRelationsByTypeId, previewTopoView, showTopoView } from '@/modules/cmdb/api/topology'
|
||||
import CMDBExprDrawer from '@/components/CMDBExprDrawer'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import CMDBTypeSelectAntd from '@/modules/cmdb/components/cmdbTypeSelect/cmdbTypeSelectAntd'
|
||||
|
||||
const currentTopoKey = 'ops_cmdb_topo_currentId'
|
||||
export default {
|
||||
@@ -348,6 +350,7 @@ export default {
|
||||
SeeksRelationGraph,
|
||||
RelationGraph,
|
||||
CMDBGrant,
|
||||
CMDBTypeSelectAntd
|
||||
},
|
||||
data() {
|
||||
const defaultOptions = {
|
||||
@@ -421,7 +424,6 @@ export default {
|
||||
currentId: null,
|
||||
topoGroups: [],
|
||||
CITypeId: null,
|
||||
ciTypes: [],
|
||||
|
||||
startId: null,
|
||||
endId: null,
|
||||
@@ -500,15 +502,6 @@ export default {
|
||||
this.currentId = _currentId
|
||||
}
|
||||
this.loadTopoViews(!_currentId)
|
||||
let ciTypes = []
|
||||
getCITypeGroups({ need_other: true }).then((res) => {
|
||||
res.forEach((item) => {
|
||||
if (item.ci_types && item.ci_types.length) {
|
||||
ciTypes = ciTypes.concat(item.ci_types)
|
||||
}
|
||||
})
|
||||
this.ciTypes = ciTypes
|
||||
})
|
||||
this.showTopoView(this.currentId.split('%')[1])
|
||||
},
|
||||
computed: {
|
||||
|
@@ -1,7 +1,13 @@
|
||||
<template>
|
||||
<div :style="{ marginBottom: '-24px' }">
|
||||
<div v-if="!subscribeTreeViewCiTypesLoading && subscribeTreeViewCiTypes.length === 0">
|
||||
<a-alert :message="$t('cmdb.tree.tips1')" banner></a-alert>
|
||||
<a-alert banner>
|
||||
<template #message>
|
||||
<span>{{ $t('cmdb.preference.tips1') }}</span>
|
||||
<router-link to="/cmdb/preference">{{ $t('cmdb.preference.tips2') }}</router-link>
|
||||
<span>{{ $t('cmdb.preference.tips3') }}</span>
|
||||
</template>
|
||||
</a-alert>
|
||||
</div>
|
||||
<div class="tree-views" v-else>
|
||||
<SplitPane
|
||||
|
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable */
|
||||
import { UserLayout, BasicLayout, RouteView } from '@/layouts'
|
||||
import appConfig from '@/config/app'
|
||||
import { getAppAclRouter } from './utils'
|
||||
@@ -7,7 +6,7 @@ import store from '../store'
|
||||
export const generatorDynamicRouter = async () => {
|
||||
const packages = []
|
||||
const { apps = undefined } = store.getters.userInfo
|
||||
for (let appName of appConfig.buildModules) {
|
||||
for (const appName of appConfig.buildModules) {
|
||||
if (!apps || !apps.length || apps.includes(appName)) {
|
||||
const module = await import(`@/modules/${appName}/index.js`)
|
||||
const r = await module.default.route()
|
||||
@@ -91,7 +90,7 @@ export const generatorDynamicRouter = async () => {
|
||||
component: () => import(/* webpackChunkName: "setting" */ '@/views/setting/auth/index')
|
||||
},
|
||||
]
|
||||
},])
|
||||
}, ])
|
||||
return routes
|
||||
}
|
||||
|
||||
|
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable */
|
||||
import Vue from 'vue'
|
||||
import Router from 'vue-router'
|
||||
import { constantRouterMap } from '@/router/config'
|
||||
|
@@ -184,7 +184,16 @@ const user = {
|
||||
}).catch(() => {
|
||||
resolve()
|
||||
}).finally(() => {
|
||||
window.location.href = '/user/logout'
|
||||
let logoutURL = '/user/logout'
|
||||
const fullPath = window.location.pathname + window.location.search
|
||||
if (
|
||||
fullPath &&
|
||||
fullPath !== '/'
|
||||
) {
|
||||
logoutURL += `?redirect=${fullPath}`
|
||||
}
|
||||
|
||||
window.location.href = logoutURL
|
||||
})
|
||||
})
|
||||
},
|
||||
|
@@ -1,5 +1,3 @@
|
||||
/* eslint-disable */
|
||||
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import app from './global/app'
|
||||
|
@@ -1,10 +1,8 @@
|
||||
/* eslint-disable */
|
||||
export function intersection(thisSet, otherSet) {
|
||||
//初始化一个新集合,用于表示交集。
|
||||
// 初始化一个新集合,用于表示交集。
|
||||
var interSectionSet = new Set()
|
||||
var values = Array.from(thisSet)
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
|
||||
if (otherSet.has(values[i])) {
|
||||
interSectionSet.add(values[i])
|
||||
}
|
||||
@@ -20,8 +18,8 @@ export function union(thisSet, otherSet) {
|
||||
unionSet.add(values[i])
|
||||
}
|
||||
values = Array.from(otherSet)
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
unionSet.add(values[i])
|
||||
for (var j = 0; j < values.length; j++) {
|
||||
unionSet.add(values[j])
|
||||
}
|
||||
|
||||
return unionSet
|
||||
@@ -31,7 +29,6 @@ export function difference(thisSet, otherSet) {
|
||||
var differenceSet = new Set()
|
||||
var values = Array.from(thisSet)
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
|
||||
if (!otherSet.has(values[i])) {
|
||||
differenceSet.add(values[i])
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ import notification from 'ant-design-vue/es/notification'
|
||||
import { ACCESS_TOKEN } from '@/store/global/mutation-types'
|
||||
import router from '@/router'
|
||||
import store from '@/store'
|
||||
import i18n from '@/lang'
|
||||
|
||||
// 创建 axios 实例
|
||||
const service = axios.create({
|
||||
@@ -20,15 +21,16 @@ const err = (error) => {
|
||||
console.log(error)
|
||||
const reg = /5\d{2}/g
|
||||
if (error.response && reg.test(error.response.status)) {
|
||||
const errorMsg = ((error.response || {}).data || {}).message || '服务端未知错误, 请联系管理员!'
|
||||
const errorMsg = ((error.response || {}).data || {}).message || i18n.t('requestServiceError')
|
||||
message.error(errorMsg)
|
||||
} else if (error.response.status === 412) {
|
||||
let seconds = 5
|
||||
notification.warning({
|
||||
key: 'notification',
|
||||
message: 'WARNING',
|
||||
description:
|
||||
'修改已提交,请等待审核(5s)',
|
||||
description: i18n.t('requestWait', {
|
||||
time: 5,
|
||||
}),
|
||||
duration: 5,
|
||||
})
|
||||
let interval = setInterval(() => {
|
||||
@@ -41,14 +43,15 @@ const err = (error) => {
|
||||
notification.warning({
|
||||
key: 'notification',
|
||||
message: 'WARNING',
|
||||
description:
|
||||
`修改已提交,请等待审核(${seconds}s)`,
|
||||
description: i18n.t('requestWait', {
|
||||
time: seconds,
|
||||
}),
|
||||
duration: seconds
|
||||
})
|
||||
}, 1000)
|
||||
} else if (error.config.url === '/api/v0.1/ci_types/can_define_computed' || error.config.isShowMessage === false) {
|
||||
} else {
|
||||
const errorMsg = ((error.response || {}).data || {}).message || '出现错误,请稍后再试'
|
||||
const errorMsg = ((error.response || {}).data || {}).message || i18n.t('requestError')
|
||||
message.error(`${errorMsg}`)
|
||||
}
|
||||
if (error.response) {
|
||||
|
@@ -1,8 +1,10 @@
|
||||
import _ from 'lodash'
|
||||
import i18n from '@/lang'
|
||||
|
||||
export function timeFix() {
|
||||
const time = new Date()
|
||||
const hour = time.getHours()
|
||||
return hour < 9 ? '早上好' : hour <= 11 ? '上午好' : hour <= 13 ? '中午好' : hour < 20 ? '下午好' : '晚上好'
|
||||
return hour < 9 ? i18n.t('cs.login.welcomeTime1') : hour <= 11 ? i18n.t('cs.login.welcomeTime2') : hour <= 13 ? i18n.t('cs.login.welcomeTime3') : hour < 20 ? i18n.t('cs.login.welcomeTime4') : i18n.t('cs.login.welcomeTime5')
|
||||
}
|
||||
|
||||
export function welcome() {
|
||||
|
@@ -23,7 +23,7 @@
|
||||
<a-form-model-item :label="$t('cs.auth.oauth2.tokenUrl')" prop="token_url">
|
||||
<a-input v-model="form.token_url" :placeholder="$t('cs.auth.oauth2.tokenUrlPlaceholder')" />
|
||||
</a-form-model-item>
|
||||
<SpanTitle>其他</SpanTitle>
|
||||
<SpanTitle>{{ $t('cs.auth.other') }}</SpanTitle>
|
||||
<a-form-model-item :label="$t('cs.auth.oauth2.userInfo')" prop="user_info" :wrapper-col="{ span: 15 }">
|
||||
<vue-json-editor
|
||||
:style="{ '--custom-height': `${200}px` }"
|
||||
|
@@ -445,6 +445,27 @@ const cs_en = {
|
||||
test: 'Test',
|
||||
selectApp: 'Select App',
|
||||
},
|
||||
login: {
|
||||
loginText: 'OneOps making operations simple',
|
||||
username: 'Username/Email',
|
||||
usernameRequired: 'Please input Username/Email',
|
||||
password: 'Password',
|
||||
passwordRequired: 'Please input Password',
|
||||
captcha: 'Captcha',
|
||||
captchaRequired: 'Please input Captcha',
|
||||
loginBtn: 'Login',
|
||||
autoLogin: 'Auto Login',
|
||||
otherLoginWay: 'Other Login',
|
||||
welcomeMessage: 'Welcome',
|
||||
welcomeDesc: '{name} Welcome Back',
|
||||
welcomeTime1: 'Good Morning',
|
||||
welcomeTime2: 'Good Morning',
|
||||
welcomeTime3: 'Good Afternoon',
|
||||
welcomeTime4: 'Good Afternoon',
|
||||
welcomeTime5: 'Good Evening',
|
||||
oneDeviceLogin: 'Login on one device only',
|
||||
logoutSoon: 'Logging Out Soon...',
|
||||
}
|
||||
}
|
||||
|
||||
export default cs_en
|
||||
|
@@ -443,5 +443,26 @@ const cs_zh = {
|
||||
test: '测试',
|
||||
selectApp: '选择应用',
|
||||
},
|
||||
login: {
|
||||
loginText: '维易科技 让运维变简单',
|
||||
username: '用户名/邮箱',
|
||||
usernameRequired: '请输入用户名或邮箱',
|
||||
password: '密码',
|
||||
passwordRequired: '请输入密码',
|
||||
captcha: '图片验证码',
|
||||
captchaRequired: '请输入验证码',
|
||||
loginBtn: '登录',
|
||||
autoLogin: '自动登录',
|
||||
otherLoginWay: '其他登录方式',
|
||||
welcomeMessage: '欢迎',
|
||||
welcomeDesc: '{name} 欢迎回来',
|
||||
welcomeTime1: '早上好',
|
||||
welcomeTime2: '上午好',
|
||||
welcomeTime3: '中午好',
|
||||
welcomeTime4: '下午好',
|
||||
welcomeTime5: '晚上好',
|
||||
oneDeviceLogin: '只能在一个设备上登录',
|
||||
logoutSoon: '即将登出...',
|
||||
}
|
||||
}
|
||||
export default cs_zh
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="ops-login">
|
||||
<div class="ops-login-left">
|
||||
<span>维易科技 让运维变简单</span>
|
||||
<span>{{ $t('cs.login.loginText') }}</span>
|
||||
</div>
|
||||
<div class="ops-login-right">
|
||||
<img src="../../assets/logo_VECMDB.png" />
|
||||
@@ -12,7 +12,7 @@
|
||||
@submit="handleSubmit"
|
||||
hideRequiredMark
|
||||
:colon="false">
|
||||
<a-form-item label="用户名/邮箱">
|
||||
<a-form-item :label="$t('cs.login.username')">
|
||||
<a-input
|
||||
size="large"
|
||||
type="text"
|
||||
@@ -20,7 +20,10 @@
|
||||
v-decorator="[
|
||||
'username',
|
||||
{
|
||||
rules: [{ required: true, message: '请输入用户名或邮箱' }, { validator: handleUsernameOrEmail }],
|
||||
rules: [
|
||||
{ required: true, message: $t('cs.login.usernameRequired') },
|
||||
{ validator: handleUsernameOrEmail }
|
||||
],
|
||||
validateTrigger: 'change',
|
||||
},
|
||||
]"
|
||||
@@ -28,19 +31,24 @@
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="密码">
|
||||
<a-form-item :label="$t('cs.login.password')">
|
||||
<a-input
|
||||
size="large"
|
||||
type="password"
|
||||
autocomplete="false"
|
||||
class="ops-input"
|
||||
v-decorator="['password', { rules: [{ required: true, message: '请输入密码' }], validateTrigger: 'blur' }]"
|
||||
v-decorator="[
|
||||
'password',
|
||||
{ rules: [{ required: true, message: $t('cs.login.passwordRequired') }], validateTrigger: 'blur' }
|
||||
]"
|
||||
>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-checkbox v-decorator="['rememberMe', { valuePropName: 'checked' }]">自动登录</a-checkbox>
|
||||
<a-checkbox v-decorator="['rememberMe', { valuePropName: 'checked' }]">
|
||||
{{ $t('cs.login.autoLogin') }}
|
||||
</a-checkbox>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item style="margin-top:24px">
|
||||
@@ -51,17 +59,21 @@
|
||||
class="login-button"
|
||||
:loading="state.loginBtn"
|
||||
:disabled="state.loginBtn"
|
||||
>登录</a-button
|
||||
>
|
||||
{{ $t('cs.login.loginBtn') }}
|
||||
</a-button>
|
||||
<a-checkbox
|
||||
v-if="enable_list && enable_list.length === 1 && enable_list[0].auth_type === 'LDAP'"
|
||||
v-if="hasLDAP"
|
||||
v-model="auth_with_ldap"
|
||||
>LDAP</a-checkbox
|
||||
>
|
||||
LDAP
|
||||
</a-checkbox>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<template v-if="_enable_list && _enable_list.length >= 1">
|
||||
<a-divider style="font-size:14px">其他登录方式</a-divider>
|
||||
<a-divider style="font-size:14px">
|
||||
{{ $t('cs.login.otherLoginWay') }}
|
||||
</a-divider>
|
||||
<div style="text-align:center">
|
||||
<span v-for="(item, index) in _enable_list" :key="item.auth_type">
|
||||
<ops-icon :type="item.auth_type" />
|
||||
@@ -104,21 +116,20 @@ export default {
|
||||
computed: {
|
||||
...mapState({ auth_enable: (state) => state?.user?.auth_enable ?? {} }),
|
||||
enable_list() {
|
||||
return this.auth_enable.enable_list ?? []
|
||||
return this.auth_enable?.enable_list ?? []
|
||||
},
|
||||
hasLDAP() {
|
||||
return this.enable_list.some((en) => en.auth_type === 'LDAP')
|
||||
},
|
||||
_enable_list() {
|
||||
return this.enable_list.filter((en) => en.auth_type !== 'LDAP')
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
enable_list: {
|
||||
hasLDAP: {
|
||||
immediate: true,
|
||||
handler(newVal) {
|
||||
if (newVal && newVal.length === 1 && newVal[0].auth_type === 'LDAP') {
|
||||
this.auth_with_ldap = true
|
||||
} else {
|
||||
this.auth_with_ldap = false
|
||||
}
|
||||
this.auth_with_ldap = newVal
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -142,7 +153,7 @@ export default {
|
||||
handleSubmit(e) {
|
||||
e.preventDefault()
|
||||
const {
|
||||
enable_list,
|
||||
hasLDAP,
|
||||
form: { validateFields },
|
||||
state,
|
||||
customActiveKey,
|
||||
@@ -160,10 +171,7 @@ export default {
|
||||
delete loginParams.username
|
||||
loginParams.username = values.username
|
||||
loginParams.password = appConfig.useEncryption ? md5(values.password) : values.password
|
||||
loginParams.auth_with_ldap =
|
||||
enable_list && enable_list.length === 1 && enable_list[0].auth_type === 'LDAP'
|
||||
? Number(auth_with_ldap)
|
||||
: undefined
|
||||
loginParams.auth_with_ldap = hasLDAP ? Number(auth_with_ldap) : undefined
|
||||
|
||||
localStorage.setItem('ops_auth_type', '')
|
||||
Login({ userInfo: loginParams })
|
||||
@@ -186,8 +194,8 @@ export default {
|
||||
// 延迟 1 秒显示欢迎信息
|
||||
setTimeout(() => {
|
||||
this.$notification.success({
|
||||
message: '欢迎',
|
||||
description: `${timeFix()},欢迎回来`,
|
||||
message: this.$t('cs.login.welcomeMessage'),
|
||||
description: this.$t('cs.login.welcomeDesc', { name: timeFix() }),
|
||||
})
|
||||
}, 1000)
|
||||
},
|
||||
|
@@ -64,7 +64,13 @@ export default {
|
||||
await this.GetAuthDataEnable()
|
||||
this.loading = false
|
||||
if (!this._enable_list.length || this._enable_list.length > 1) {
|
||||
this.$router.push('/user/login')
|
||||
let loginURL = '/user/login'
|
||||
const redirect = this.$route?.query?.redirect
|
||||
if (redirect) {
|
||||
loginURL += `?redirect=${redirect}`
|
||||
}
|
||||
|
||||
this.$router.push(loginURL)
|
||||
}
|
||||
if (this.auth_auto_redirect) {
|
||||
this.time = 0
|
||||
|
@@ -14,12 +14,11 @@ module.exports = {
|
||||
plugins: [
|
||||
// Ignore all locale files of moment.js
|
||||
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
|
||||
// 生成仅包含颜色的替换样式(主题色等)
|
||||
// TODO 需要增加根据环境不开启主题需求
|
||||
// generate theme color replacement styles
|
||||
new ThemeColorReplacer({
|
||||
fileName: 'css/theme-colors-[contenthash:8].css',
|
||||
matchColors: getAntdSerials('#2f54eb'), // 主色系列
|
||||
// 改变样式选择器,解决样式覆盖问题
|
||||
matchColors: getAntdSerials('#2f54eb'), // primary color series
|
||||
// change style selectors to solve style override issues
|
||||
changeSelector(selector) {
|
||||
switch (selector) {
|
||||
case '.ant-calendar-today .ant-calendar-date':
|
||||
@@ -63,29 +62,14 @@ module.exports = {
|
||||
.options({
|
||||
name: 'assets/[name].[hash:8].[ext]',
|
||||
})
|
||||
/* svgRule.oneOf('inline')
|
||||
.resourceQuery(/inline/)
|
||||
.use('vue-svg-loader')
|
||||
.loader('vue-svg-loader')
|
||||
.end()
|
||||
.end()
|
||||
.oneOf('external')
|
||||
.use('file-loader')
|
||||
.loader('file-loader')
|
||||
.options({
|
||||
name: 'assets/[name].[hash:8].[ext]'
|
||||
})
|
||||
*/
|
||||
},
|
||||
|
||||
css: {
|
||||
loaderOptions: {
|
||||
less: {
|
||||
modifyVars: {
|
||||
/* less 变量覆盖,用于自定义 ant design 主题 */
|
||||
// override less variables for custom ant design theme
|
||||
'primary-color': '#2f54eb',
|
||||
// 'link-color': '#F5222D',
|
||||
// 'border-radius-base': '4px',
|
||||
},
|
||||
javascriptEnabled: true,
|
||||
},
|
||||
@@ -119,7 +103,7 @@ module.exports = {
|
||||
}
|
||||
|
||||
function getAntdSerials(color) {
|
||||
// 淡化(即less的tint)
|
||||
// Lighten (similar to less's tint)
|
||||
const lightens = new Array(9).fill().map((t, i) => {
|
||||
return ThemeColorReplacer.varyColor.lighten(color, i / 10)
|
||||
})
|
||||
|
@@ -41,13 +41,14 @@ services:
|
||||
- redis
|
||||
|
||||
cmdb-api:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.5.2
|
||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.5.4
|
||||
container_name: cmdb-api
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
TZ: Asia/Shanghai
|
||||
WAIT_HOSTS: cmdb-db:3306, cmdb-cache:6379
|
||||
SYSTEM_DEFAULT_LANGUAGE: # en-US, zh-CN
|
||||
depends_on:
|
||||
cmdb-db:
|
||||
condition: service_healthy
|
||||
@@ -84,7 +85,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.5.2
|
||||
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.5.4
|
||||
container_name: cmdb-ui
|
||||
depends_on:
|
||||
cmdb-api:
|
||||
|
Reference in New Issue
Block a user