mirror of
https://github.com/veops/cmdb.git
synced 2025-09-07 05:47:00 +08:00
Compare commits
44 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
fe6373422e | ||
|
b3ea776886 | ||
|
c4d2ce313d | ||
|
20103a0fe6 | ||
|
394e2aeac6 | ||
|
8f7d78c26c | ||
|
7eecf3cec3 | ||
|
f6e9c443f7 | ||
|
857cbd82fd | ||
|
9a14296e02 | ||
|
f638b52759 | ||
|
78da728105 | ||
|
eb69029a51 | ||
|
07a097eba2 | ||
|
e843e3eac9 | ||
|
7308cfa6c2 | ||
|
64ea4fb21f | ||
|
e15cefaa38 | ||
|
f32339b969 | ||
|
131d213a73 | ||
|
ff98777689 | ||
|
383d4c88ed | ||
|
bb7157e292 | ||
|
b1a82f1a67 | ||
|
de86ea3852 | ||
|
bf05ea240e | ||
|
8ec0d619d7 | ||
|
61f8c463bc | ||
|
9b4dc3e43b | ||
|
9e69be8256 | ||
|
251b9e7fd5 | ||
|
f3cc12f1f9 | ||
|
56f03e1624 | ||
|
42ad2b6dde | ||
|
5aba1ff257 | ||
|
417e8fe349 | ||
|
02235d8cc0 | ||
|
00c7a644a2 | ||
|
f3e8757450 | ||
|
f0749341ba | ||
|
89da671e46 | ||
|
0e60aae076 | ||
|
4dfa97d404 | ||
|
9b778f9bc7 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -78,3 +78,4 @@ cmdb-ui/npm-debug.log*
|
||||
cmdb-ui/yarn-debug.log*
|
||||
cmdb-ui/yarn-error.log*
|
||||
cmdb-ui/package-lock.json
|
||||
start.sh
|
||||
|
@@ -68,6 +68,7 @@ pycryptodomex = ">=3.19.0"
|
||||
lz4 = ">=4.3.2"
|
||||
python-magic = "==0.4.27"
|
||||
jsonpath = "==0.82.2"
|
||||
networkx = ">=3.1"
|
||||
|
||||
[dev-packages]
|
||||
# Testing
|
||||
|
@@ -108,7 +108,8 @@ class AttributeManager(object):
|
||||
return []
|
||||
choice_values = choice_table.get_by(fl=["value", "option"], attr_id=attr_id)
|
||||
|
||||
return [[ValueTypeMap.serialize[value_type](choice_value['value']), choice_value['option']]
|
||||
return [[ValueTypeMap.serialize[value_type](choice_value['value']), choice_value['option'] or
|
||||
{"label": ValueTypeMap.serialize[value_type](choice_value['value'])}]
|
||||
for choice_value in choice_values]
|
||||
|
||||
@staticmethod
|
||||
@@ -140,7 +141,7 @@ class AttributeManager(object):
|
||||
attr = AttributeCache.get(_attr_id) if _attr_id else _attr
|
||||
if attr and attr.is_choice:
|
||||
choice_values = cls.get_choice_values(attr.id, attr.value_type, None, None)
|
||||
return {i[0]: i[1]['label'] for i in choice_values if i[1].get('label')}
|
||||
return {i[0]: i[1]['label'] for i in choice_values if i[1] and i[1].get('label')}
|
||||
|
||||
return {}
|
||||
|
||||
|
@@ -709,13 +709,18 @@ class CIManager(object):
|
||||
elif fields:
|
||||
_res = []
|
||||
for d in res:
|
||||
if isinstance(fields, dict) and d.get("_type") not in fields:
|
||||
_res.append(d)
|
||||
continue
|
||||
|
||||
_d = dict()
|
||||
_d["_id"], _d["_type"] = d.get("_id"), d.get("_type")
|
||||
_d["ci_type"] = d.get("ci_type")
|
||||
if unique_required:
|
||||
_d[d.get('unique')] = d.get(d.get('unique'))
|
||||
|
||||
for field in fields + ['ci_type_alias', 'unique', 'unique_alias']:
|
||||
_fields = list(fields.get(_d['_type']) or [] if isinstance(fields, dict) else fields)
|
||||
for field in _fields + ['ci_type_alias', 'unique', 'unique_alias']:
|
||||
_d[field] = d.get(field)
|
||||
_res.append(_d)
|
||||
return _res
|
||||
@@ -732,9 +737,8 @@ class CIManager(object):
|
||||
from api.lib.cmdb.search.ci.db.query_sql import QUERY_CIS_BY_IDS
|
||||
from api.lib.cmdb.search.ci.db.query_sql import QUERY_CIS_BY_VALUE_TABLE
|
||||
|
||||
if not fields:
|
||||
filter_fields_sql = ""
|
||||
else:
|
||||
filter_fields_sql = ""
|
||||
if fields and isinstance(fields, list):
|
||||
_fields = list()
|
||||
for field in fields:
|
||||
attr = AttributeCache.get(field)
|
||||
@@ -776,6 +780,10 @@ class CIManager(object):
|
||||
ci_set.add(ci_id)
|
||||
res[ci2pos[ci_id]] = ci_dict
|
||||
|
||||
if isinstance(fields, dict) and fields.get(type_id):
|
||||
if attr_name not in fields[type_id]:
|
||||
continue
|
||||
|
||||
if ret_key == RetKey.NAME:
|
||||
attr_key = attr_name
|
||||
elif ret_key == RetKey.ALIAS:
|
||||
@@ -813,7 +821,7 @@ class CIManager(object):
|
||||
if not ci_ids:
|
||||
return []
|
||||
|
||||
fields = [] if fields is None or not isinstance(fields, list) else fields
|
||||
fields = [] if not fields else fields
|
||||
|
||||
ci_id_tuple = tuple(map(int, ci_ids))
|
||||
res = cls._get_cis_from_cache(ci_id_tuple, ret_key, fields, unique_required, excludes=excludes)
|
||||
|
@@ -3,6 +3,7 @@
|
||||
from collections import defaultdict
|
||||
|
||||
import copy
|
||||
import networkx as nx
|
||||
import toposort
|
||||
from flask import abort
|
||||
from flask import current_app
|
||||
@@ -524,7 +525,14 @@ class CITypeAttributeManager(object):
|
||||
for _type_id in parent_ids + [type_id]:
|
||||
result.extend(CITypeAttributesCache.get2(_type_id))
|
||||
|
||||
return result
|
||||
attr_ids = set()
|
||||
result2 = []
|
||||
for i in result:
|
||||
if i[1].id not in attr_ids:
|
||||
result2.append(i)
|
||||
attr_ids.add(i[1].id)
|
||||
|
||||
return result2
|
||||
|
||||
@classmethod
|
||||
def get_attr_names_by_type_id(cls, type_id):
|
||||
@@ -838,6 +846,29 @@ class CITypeRelationManager(object):
|
||||
|
||||
return ids
|
||||
|
||||
@staticmethod
|
||||
def find_path(source_type_id, target_type_ids):
|
||||
source_type_id = int(source_type_id)
|
||||
target_type_ids = map(int, target_type_ids)
|
||||
|
||||
graph = nx.DiGraph()
|
||||
|
||||
def get_children(_id):
|
||||
children = CITypeRelation.get_by(parent_id=_id, to_dict=False)
|
||||
|
||||
for i in children:
|
||||
if i.child_id != _id:
|
||||
graph.add_edge(i.parent_id, i.child_id)
|
||||
get_children(i.child_id)
|
||||
|
||||
get_children(source_type_id)
|
||||
|
||||
paths = list(nx.all_simple_paths(graph, source_type_id, target_type_ids))
|
||||
|
||||
del graph
|
||||
|
||||
return paths
|
||||
|
||||
@staticmethod
|
||||
def _wrap_relation_type_dict(type_id, relation_inst):
|
||||
ci_type_dict = CITypeCache.get(type_id).to_dict()
|
||||
@@ -1399,6 +1430,10 @@ class CITypeTemplateManager(object):
|
||||
payload = dict(group_id=group_id_map.get(group['id'], group['id']),
|
||||
type_id=type_id_map.get(ci_type['id'], ci_type['id']),
|
||||
order=order)
|
||||
for i in CITypeGroupItem.get_by(type_id=payload['type_id'], to_dict=False):
|
||||
if i.group_id != payload['group_id']:
|
||||
i.soft_delete(flush=True)
|
||||
|
||||
existed = CITypeGroupItem.get_by(group_id=payload['group_id'], type_id=payload['type_id'],
|
||||
first=True, to_dict=False)
|
||||
if existed is None:
|
||||
|
@@ -10,6 +10,7 @@ from api.extensions import db
|
||||
from api.lib.cmdb.cache import AttributeCache
|
||||
from api.lib.cmdb.cache import RelationTypeCache
|
||||
from api.lib.cmdb.const import OperateType
|
||||
from api.lib.cmdb.cache import CITypeCache
|
||||
from api.lib.cmdb.perms import CIFilterPermsCRUD
|
||||
from api.lib.cmdb.resp_format import ErrFormat
|
||||
from api.lib.perm.acl.cache import UserCache
|
||||
@@ -22,6 +23,7 @@ from api.models.cmdb import CITypeHistory
|
||||
from api.models.cmdb import CITypeTrigger
|
||||
from api.models.cmdb import CITypeUniqueConstraint
|
||||
from api.models.cmdb import OperationRecord
|
||||
from api.lib.cmdb.utils import TableMap
|
||||
|
||||
|
||||
class AttributeHistoryManger(object):
|
||||
@@ -59,8 +61,23 @@ class AttributeHistoryManger(object):
|
||||
total = len(records)
|
||||
|
||||
res = {}
|
||||
show_attr_set = {}
|
||||
show_attr_cache = {}
|
||||
for record in records:
|
||||
record_id = record.OperationRecord.id
|
||||
type_id = record.OperationRecord.type_id
|
||||
ci_id = record.AttributeHistory.ci_id
|
||||
show_attr_set[ci_id] = None
|
||||
show_attr = show_attr_cache.setdefault(
|
||||
type_id,
|
||||
AttributeCache.get(
|
||||
CITypeCache.get(type_id).show_id or CITypeCache.get(type_id).unique_id) if CITypeCache.get(type_id) else None
|
||||
)
|
||||
if show_attr:
|
||||
attr_table = TableMap(attr=show_attr).table
|
||||
attr_record = attr_table.get_by(attr_id=show_attr.id, ci_id=ci_id, first=True, to_dict=False)
|
||||
show_attr_set[ci_id] = attr_record.value if attr_record else None
|
||||
|
||||
attr_hist = record.AttributeHistory.to_dict()
|
||||
attr_hist['attr'] = AttributeCache.get(attr_hist['attr_id'])
|
||||
if attr_hist['attr']:
|
||||
@@ -76,6 +93,7 @@ class AttributeHistoryManger(object):
|
||||
|
||||
if record_id not in res:
|
||||
record_dict = record.OperationRecord.to_dict()
|
||||
record_dict['show_attr_value'] = show_attr_set.get(ci_id)
|
||||
record_dict["user"] = UserCache.get(record_dict.get("uid"))
|
||||
if record_dict["user"]:
|
||||
record_dict['user'] = record_dict['user'].nickname
|
||||
|
@@ -384,14 +384,22 @@ class PreferenceManager(object):
|
||||
def add_search_option(**kwargs):
|
||||
kwargs['uid'] = current_user.uid
|
||||
|
||||
existed = PreferenceSearchOption.get_by(uid=current_user.uid,
|
||||
name=kwargs.get('name'),
|
||||
prv_id=kwargs.get('prv_id'),
|
||||
ptv_id=kwargs.get('ptv_id'),
|
||||
type_id=kwargs.get('type_id'),
|
||||
)
|
||||
if existed:
|
||||
return abort(400, ErrFormat.preference_search_option_exists)
|
||||
if kwargs['name'] in ('__recent__', '__favor__'):
|
||||
if kwargs['name'] == '__recent__':
|
||||
for i in PreferenceSearchOption.get_by(
|
||||
only_query=True, name=kwargs['name'], uid=current_user.uid).order_by(
|
||||
PreferenceSearchOption.id.desc()).offset(20):
|
||||
i.delete()
|
||||
|
||||
else:
|
||||
existed = PreferenceSearchOption.get_by(uid=current_user.uid,
|
||||
name=kwargs.get('name'),
|
||||
prv_id=kwargs.get('prv_id'),
|
||||
ptv_id=kwargs.get('ptv_id'),
|
||||
type_id=kwargs.get('type_id'),
|
||||
)
|
||||
if existed:
|
||||
return abort(400, ErrFormat.preference_search_option_exists)
|
||||
|
||||
return PreferenceSearchOption.create(**kwargs)
|
||||
|
||||
|
@@ -154,3 +154,5 @@ class ErrFormat(CommonErrFormat):
|
||||
topology_group_exists = _l("Topology group {} already exists") # 拓扑视图分组 {} 已经存在
|
||||
# 因为该分组下定义了拓扑视图,不能删除
|
||||
topo_view_exists_cannot_delete_group = _l("The group cannot be deleted because the topology view already exists")
|
||||
|
||||
relation_path_search_src_target_required = _l("Both the source model and the target model must be selected")
|
@@ -4,8 +4,8 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import copy
|
||||
import six
|
||||
import time
|
||||
|
||||
from flask import current_app
|
||||
from flask_login import current_user
|
||||
from jinja2 import Template
|
||||
@@ -66,6 +66,7 @@ class Search(object):
|
||||
self.use_id_filter = use_id_filter
|
||||
self.use_ci_filter = use_ci_filter
|
||||
self.only_ids = only_ids
|
||||
self.multi_type_has_ci_filter = False
|
||||
|
||||
self.valid_type_names = []
|
||||
self.type2filter_perms = dict()
|
||||
@@ -104,35 +105,56 @@ class Search(object):
|
||||
else:
|
||||
raise SearchError(ErrFormat.attribute_not_found.format(key))
|
||||
|
||||
def _type_query_handler(self, v, queries):
|
||||
def _type_query_handler(self, v, queries, is_sub=False):
|
||||
new_v = v[1:-1].split(";") if v.startswith("(") and v.endswith(")") else [v]
|
||||
type_num = len(new_v)
|
||||
type_id_list = []
|
||||
for _v in new_v:
|
||||
ci_type = CITypeCache.get(_v)
|
||||
|
||||
if len(new_v) == 1 and not self.sort and ci_type and ci_type.default_order_attr:
|
||||
if type_num == 1 and not self.sort and ci_type and ci_type.default_order_attr:
|
||||
self.sort = ci_type.default_order_attr
|
||||
|
||||
if ci_type is not None:
|
||||
if self.valid_type_names == "ALL" or ci_type.name in self.valid_type_names:
|
||||
self.type_id_list.append(str(ci_type.id))
|
||||
if ci_type.id in self.type2filter_perms:
|
||||
if not is_sub:
|
||||
self.type_id_list.append(str(ci_type.id))
|
||||
type_id_list.append(str(ci_type.id))
|
||||
if ci_type.id in self.type2filter_perms and not is_sub:
|
||||
ci_filter = self.type2filter_perms[ci_type.id].get('ci_filter')
|
||||
if ci_filter and self.use_ci_filter and not self.use_id_filter:
|
||||
sub = []
|
||||
ci_filter = Template(ci_filter).render(user=current_user)
|
||||
for i in ci_filter.split(','):
|
||||
if i.startswith("~") and not sub:
|
||||
queries.append(i)
|
||||
if type_num == 1:
|
||||
if i.startswith("~") and not sub:
|
||||
queries.append(i)
|
||||
else:
|
||||
sub.append(i)
|
||||
else:
|
||||
sub.append(i)
|
||||
if sub:
|
||||
queries.append(dict(operator="&", queries=sub))
|
||||
if type_num == 1:
|
||||
queries.append(dict(operator="&", queries=sub))
|
||||
else:
|
||||
if str(ci_type.id) in self.type_id_list:
|
||||
self.type_id_list.remove(str(ci_type.id))
|
||||
type_id_list.remove(str(ci_type.id))
|
||||
sub.extend([i for i in queries[1:] if isinstance(i, six.string_types)])
|
||||
|
||||
sub.insert(0, "_type:{}".format(ci_type.id))
|
||||
queries.append(dict(operator="|", queries=sub))
|
||||
self.multi_type_has_ci_filter = True
|
||||
if self.type2filter_perms[ci_type.id].get('attr_filter'):
|
||||
if not self.fl:
|
||||
self.fl = set(self.type2filter_perms[ci_type.id]['attr_filter'])
|
||||
if type_num == 1:
|
||||
if not self.fl:
|
||||
self.fl = set(self.type2filter_perms[ci_type.id]['attr_filter'])
|
||||
else:
|
||||
self.fl = set(self.fl) & set(self.type2filter_perms[ci_type.id]['attr_filter'])
|
||||
else:
|
||||
self.fl = set(self.fl) & set(self.type2filter_perms[ci_type.id]['attr_filter'])
|
||||
self.fl = self.fl or {}
|
||||
if not self.fl or isinstance(self.fl, dict):
|
||||
self.fl[ci_type.id] = set(self.type2filter_perms[ci_type.id]['attr_filter'])
|
||||
|
||||
if self.type2filter_perms[ci_type.id].get('id_filter') and self.use_id_filter:
|
||||
|
||||
@@ -146,13 +168,17 @@ class Search(object):
|
||||
else:
|
||||
raise SearchError(ErrFormat.ci_type_not_found2.format(_v))
|
||||
|
||||
if self.type_id_list:
|
||||
type_ids = ",".join(self.type_id_list)
|
||||
if type_num != len(self.type_id_list) and queries and queries[0].startswith('_type') and not is_sub:
|
||||
queries[0] = "_type:({})".format(";".join(self.type_id_list))
|
||||
|
||||
if type_id_list:
|
||||
type_ids = ",".join(type_id_list)
|
||||
_query_sql = QUERY_CI_BY_TYPE.format(type_ids)
|
||||
if self.only_type_query:
|
||||
if self.only_type_query or self.multi_type_has_ci_filter:
|
||||
return _query_sql
|
||||
else:
|
||||
return ""
|
||||
elif type_num > 1: # there must be instance-level access control
|
||||
return "select c_cis.id as ci_id from c_cis where c_cis.id=0"
|
||||
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
@@ -229,7 +255,7 @@ class Search(object):
|
||||
return ret_sql.format(query_sql, "ORDER BY B.ci_id {1} LIMIT {0:d}, {2};".format(
|
||||
(self.page - 1) * self.count, sort_type, self.count))
|
||||
|
||||
elif self.type_id_list:
|
||||
elif self.type_id_list and not self.multi_type_has_ci_filter:
|
||||
self.query_sql = "SELECT B.ci_id FROM ({0}) AS B {1}".format(
|
||||
query_sql,
|
||||
"INNER JOIN c_cis on c_cis.id=B.ci_id WHERE c_cis.type_id IN ({0}) ".format(
|
||||
@@ -254,7 +280,7 @@ class Search(object):
|
||||
def __sort_by_type(self, sort_type, query_sql):
|
||||
ret_sql = "SELECT SQL_CALC_FOUND_ROWS DISTINCT B.ci_id FROM ({0}) AS B {1}"
|
||||
|
||||
if self.type_id_list:
|
||||
if self.type_id_list and not self.multi_type_has_ci_filter:
|
||||
self.query_sql = "SELECT B.ci_id FROM ({0}) AS B {1}".format(
|
||||
query_sql,
|
||||
"INNER JOIN c_cis on c_cis.id=B.ci_id WHERE c_cis.type_id IN ({0}) ".format(
|
||||
@@ -287,7 +313,7 @@ class Search(object):
|
||||
WHERE {1}.attr_id = {3}""".format("ALIAS", table_name, query_sql, attr_id)
|
||||
new_table = _v_query_sql
|
||||
|
||||
if self.only_type_query or not self.type_id_list:
|
||||
if self.only_type_query or not self.type_id_list or self.multi_type_has_ci_filter:
|
||||
return ("SELECT SQL_CALC_FOUND_ROWS DISTINCT C.ci_id FROM ({0}) AS C ORDER BY C.value {2} "
|
||||
"LIMIT {1:d}, {3};".format(new_table, (self.page - 1) * self.count, sort_type, self.count))
|
||||
|
||||
@@ -325,7 +351,9 @@ class Search(object):
|
||||
INNER JOIN ({2}) as {3} USING(ci_id)""".format(query_sql, alias, _query_sql, alias + "A")
|
||||
|
||||
elif operator == "|" or operator == "|~":
|
||||
query_sql = "SELECT * FROM ({0}) as {1} UNION ALL ({2})".format(query_sql, alias, _query_sql)
|
||||
query_sql = "SELECT * FROM ({0}) as {1} UNION ALL SELECT * FROM ({2}) as {3}".format(query_sql, alias,
|
||||
_query_sql,
|
||||
alias + "A")
|
||||
|
||||
elif operator == "~":
|
||||
query_sql = """SELECT * FROM ({0}) as {1} LEFT JOIN ({2}) as {3} USING(ci_id)
|
||||
@@ -430,14 +458,14 @@ class Search(object):
|
||||
|
||||
return result
|
||||
|
||||
def __query_by_attr(self, q, queries, alias):
|
||||
def __query_by_attr(self, q, queries, alias, is_sub=False):
|
||||
k = q.split(":")[0].strip()
|
||||
v = "\:".join(q.split(":")[1:]).strip()
|
||||
v = v.replace("'", "\\'")
|
||||
v = v.replace('"', '\\"')
|
||||
field, field_type, operator, attr = self._attr_name_proc(k)
|
||||
if field == "_type":
|
||||
_query_sql = self._type_query_handler(v, queries)
|
||||
_query_sql = self._type_query_handler(v, queries, is_sub)
|
||||
|
||||
elif field == "_id":
|
||||
_query_sql = self._id_query_handler(v)
|
||||
@@ -484,19 +512,20 @@ class Search(object):
|
||||
|
||||
return alias, _query_sql, operator
|
||||
|
||||
def __query_build_by_field(self, queries, is_first=True, only_type_query_special=True, alias='A', operator='&'):
|
||||
def __query_build_by_field(self, queries, is_first=True, only_type_query_special=True, alias='A', operator='&',
|
||||
is_sub=False):
|
||||
query_sql = ""
|
||||
|
||||
for q in queries:
|
||||
_query_sql = ""
|
||||
if isinstance(q, dict):
|
||||
alias, _query_sql, operator = self.__query_build_by_field(q['queries'], True, True, alias)
|
||||
current_app.logger.info(_query_sql)
|
||||
current_app.logger.info((operator, is_first, alias))
|
||||
alias, _query_sql, operator = self.__query_build_by_field(q['queries'], True, True, alias, is_sub=True)
|
||||
# current_app.logger.info(_query_sql)
|
||||
# current_app.logger.info((operator, is_first, alias))
|
||||
operator = q['operator']
|
||||
|
||||
elif ":" in q and not q.startswith("*"):
|
||||
alias, _query_sql, operator = self.__query_by_attr(q, queries, alias)
|
||||
alias, _query_sql, operator = self.__query_by_attr(q, queries, alias, is_sub)
|
||||
elif q == "*":
|
||||
continue
|
||||
elif q:
|
||||
@@ -547,7 +576,6 @@ class Search(object):
|
||||
queries = handle_arg_list(self.orig_query)
|
||||
queries = self._extra_handle_query_expr(queries)
|
||||
queries = self.__confirm_type_first(queries)
|
||||
current_app.logger.debug(queries)
|
||||
|
||||
_, query_sql, _ = self.__query_build_by_field(queries)
|
||||
|
||||
@@ -585,13 +613,16 @@ class Search(object):
|
||||
return facet_result
|
||||
|
||||
def _fl_build(self):
|
||||
_fl = list()
|
||||
for f in self.fl:
|
||||
k, _, _, _ = self._attr_name_proc(f)
|
||||
if k:
|
||||
_fl.append(k)
|
||||
if isinstance(self.fl, list):
|
||||
_fl = list()
|
||||
for f in self.fl:
|
||||
k, _, _, _ = self._attr_name_proc(f)
|
||||
if k:
|
||||
_fl.append(k)
|
||||
|
||||
return _fl
|
||||
return _fl
|
||||
else:
|
||||
return self.fl
|
||||
|
||||
def search(self):
|
||||
numfound, ci_ids = self._query_build_raw()
|
||||
@@ -610,6 +641,8 @@ class Search(object):
|
||||
if ci_ids:
|
||||
response = CIManager.get_cis_by_ids(ci_ids, ret_key=self.ret_key, fields=_fl, excludes=self.excludes)
|
||||
for res in response:
|
||||
if not res:
|
||||
continue
|
||||
ci_type = res.get("ci_type")
|
||||
if ci_type not in counter.keys():
|
||||
counter[ci_type] = 0
|
||||
|
@@ -1,8 +1,11 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
import json
|
||||
import sys
|
||||
from collections import Counter
|
||||
from collections import defaultdict
|
||||
|
||||
import copy
|
||||
import json
|
||||
import networkx as nx
|
||||
import sys
|
||||
from flask import abort
|
||||
from flask import current_app
|
||||
from flask_login import current_user
|
||||
@@ -13,6 +16,7 @@ from api.lib.cmdb.cache import CITypeCache
|
||||
from api.lib.cmdb.ci import CIRelationManager
|
||||
from api.lib.cmdb.ci_type import CITypeRelationManager
|
||||
from api.lib.cmdb.const import ConstraintEnum
|
||||
from api.lib.cmdb.const import PermEnum
|
||||
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
|
||||
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2
|
||||
from api.lib.cmdb.const import ResourceTypeEnum
|
||||
@@ -25,10 +29,12 @@ from api.lib.cmdb.utils import ValueTypeMap
|
||||
from api.lib.perm.acl.acl import ACLManager
|
||||
from api.lib.perm.acl.acl import is_app_admin
|
||||
from api.models.cmdb import CI
|
||||
from api.models.cmdb import CITypeRelation
|
||||
from api.models.cmdb import RelationType
|
||||
|
||||
|
||||
class Search(object):
|
||||
def __init__(self, root_id,
|
||||
def __init__(self, root_id=None,
|
||||
level=None,
|
||||
query=None,
|
||||
fl=None,
|
||||
@@ -419,3 +425,169 @@ class Search(object):
|
||||
level_ids = _level_ids
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _get_src_ids(src):
|
||||
q = src.get('q') or ''
|
||||
if not q.startswith('_type:'):
|
||||
q = "_type:{},{}".format(src['type_id'], q)
|
||||
|
||||
return SearchFromDB(q, use_ci_filter=True, only_ids=True, count=100000).search()
|
||||
|
||||
@staticmethod
|
||||
def _filter_target_ids(target_ids, type_ids, q):
|
||||
if not q.startswith('_type:'):
|
||||
q = "_type:({}),{}".format(";".join(map(str, type_ids)), q)
|
||||
|
||||
ci_ids = SearchFromDB(q, ci_ids=target_ids, use_ci_filter=True, only_ids=True, count=100000).search()
|
||||
cis = CI.get_by(fl=['id', 'type_id'], only_query=True).filter(CI.id.in_(ci_ids))
|
||||
|
||||
return [(str(i.id), i.type_id) for i in cis]
|
||||
|
||||
@staticmethod
|
||||
def _path2level(src_type_id, target_type_ids, path):
|
||||
if not src_type_id or not target_type_ids:
|
||||
return abort(400, ErrFormat.relation_path_search_src_target_required)
|
||||
|
||||
graph = nx.DiGraph()
|
||||
graph.add_edges_from([(n, _path[idx + 1]) for _path in path for idx, n in enumerate(_path[:-1])])
|
||||
relation_types = defaultdict(dict)
|
||||
level2type = defaultdict(set)
|
||||
type2show_key = dict()
|
||||
for _path in path:
|
||||
for idx, node in enumerate(_path[1:]):
|
||||
level2type[idx + 1].add(node)
|
||||
|
||||
src = CITypeCache.get(_path[idx])
|
||||
target = CITypeCache.get(node)
|
||||
relation_type = RelationType.get_by(only_query=True).join(
|
||||
CITypeRelation, CITypeRelation.relation_type_id == RelationType.id).filter(
|
||||
CITypeRelation.parent_id == src.id).filter(CITypeRelation.child_id == target.id).first()
|
||||
relation_types[src.alias].update({target.alias: relation_type.name})
|
||||
|
||||
if src.id not in type2show_key:
|
||||
type2show_key[src.id] = AttributeCache.get(src.show_id or src.unique_id).name
|
||||
if target.id not in type2show_key:
|
||||
type2show_key[target.id] = AttributeCache.get(target.show_id or target.unique_id).name
|
||||
|
||||
nodes = graph.nodes()
|
||||
|
||||
return level2type, list(nodes), relation_types, type2show_key
|
||||
|
||||
def _build_graph(self, source_ids, source_type_id, level2type, target_type_ids, acl):
|
||||
type2filter_perms = dict()
|
||||
if not self.is_app_admin:
|
||||
res2 = acl.get_resources(ResourceTypeEnum.CI_FILTER)
|
||||
if res2:
|
||||
type2filter_perms = CIFilterPermsCRUD().get_by_ids(list(map(int, [i['name'] for i in res2])))
|
||||
|
||||
target_type_ids = set(target_type_ids)
|
||||
graph = nx.DiGraph()
|
||||
target_ids = []
|
||||
key = [(str(i), source_type_id) for i in source_ids]
|
||||
graph.add_nodes_from(key)
|
||||
for level in level2type:
|
||||
filter_type_ids = level2type[level]
|
||||
id_filter_limit = dict()
|
||||
for _type_id in filter_type_ids:
|
||||
if type2filter_perms.get(_type_id):
|
||||
_id_filter_limit, _ = self._get_ci_filter(type2filter_perms[_type_id])
|
||||
id_filter_limit.update(_id_filter_limit)
|
||||
|
||||
has_target = filter_type_ids & target_type_ids
|
||||
|
||||
res = [json.loads(x).items() for x in [i or '{}' for i in rd.get([i[0] for i in key],
|
||||
REDIS_PREFIX_CI_RELATION) or []]]
|
||||
_key = []
|
||||
for idx, _id in enumerate(key):
|
||||
valid_targets = [i for i in res[idx] if i[1] in filter_type_ids and
|
||||
(not id_filter_limit or int(i[0]) in id_filter_limit)]
|
||||
_key.extend(valid_targets)
|
||||
graph.add_edges_from(zip([_id] * len(valid_targets), valid_targets))
|
||||
|
||||
if has_target:
|
||||
target_ids.extend([j[0] for i in res for j in i if j[1] in target_type_ids])
|
||||
|
||||
key = copy.deepcopy(_key)
|
||||
|
||||
return graph, target_ids
|
||||
|
||||
@staticmethod
|
||||
def _find_paths(graph, source_ids, source_type_id, target_ids, valid_path, max_depth=6):
|
||||
paths = []
|
||||
for source_id in source_ids:
|
||||
_paths = nx.all_simple_paths(graph,
|
||||
source=(source_id, source_type_id),
|
||||
target=target_ids,
|
||||
cutoff=max_depth)
|
||||
for __path in _paths:
|
||||
if tuple([i[1] for i in __path]) in valid_path:
|
||||
paths.append([i[0] for i in __path])
|
||||
|
||||
return paths
|
||||
|
||||
@staticmethod
|
||||
def _wrap_path_result(paths, types, valid_path, target_types, type2show_key):
|
||||
ci_ids = [j for i in paths for j in i]
|
||||
|
||||
response, _, _, _, _, _ = SearchFromDB("_type:({})".format(";".join(map(str, types))),
|
||||
use_ci_filter=False,
|
||||
ci_ids=list(map(int, ci_ids)),
|
||||
count=1000000).search()
|
||||
id2ci = {str(i.get('_id')): i if i['_type'] in target_types else {
|
||||
type2show_key[i['_type']]: i[type2show_key[i['_type']]],
|
||||
"ci_type_alias": i["ci_type_alias"],
|
||||
"_type": i["_type"],
|
||||
} for i in response}
|
||||
|
||||
result = defaultdict(list)
|
||||
counter = defaultdict(int)
|
||||
|
||||
for path in paths:
|
||||
key = "-".join([id2ci.get(i, {}).get('ci_type_alias') or '' for i in path])
|
||||
if tuple([id2ci.get(i, {}).get('_type') for i in path]) in valid_path:
|
||||
counter[key] += 1
|
||||
result[key].append(path)
|
||||
|
||||
return result, counter, id2ci
|
||||
|
||||
def search_by_path(self, source, target, path):
|
||||
"""
|
||||
|
||||
:param source: {type_id: id, q: expr}
|
||||
:param target: {type_ids: [id], q: expr}
|
||||
:param path: [source_type_id, ..., target_type_id], use type id
|
||||
:return:
|
||||
"""
|
||||
acl = ACLManager('cmdb')
|
||||
if not self.is_app_admin:
|
||||
res = {i['name'] for i in acl.get_resources(ResourceTypeEnum.CI_TYPE)}
|
||||
for type_id in (source.get('type_id') and [source['type_id']] or []) + (target.get('type_ids') or []):
|
||||
_type = CITypeCache.get(type_id)
|
||||
if _type and _type.name not in res:
|
||||
return abort(403, ErrFormat.no_permission.format(_type.alias, PermEnum.READ))
|
||||
|
||||
target['type_ids'] = [i[-1] for i in path]
|
||||
level2type, types, relation_types, type2show_key = self._path2level(
|
||||
source.get('type_id'), target.get('type_ids'), path)
|
||||
if not level2type:
|
||||
return [], {}, 0, self.page, 0, {}, {}
|
||||
|
||||
source_ids = self._get_src_ids(source)
|
||||
|
||||
graph, target_ids = self._build_graph(source_ids, source['type_id'], level2type, target['type_ids'], acl)
|
||||
target_ids = self._filter_target_ids(target_ids, target['type_ids'], target.get('q') or '')
|
||||
paths = self._find_paths(graph,
|
||||
source_ids,
|
||||
source['type_id'],
|
||||
set(target_ids),
|
||||
{tuple(i): 1 for i in path})
|
||||
|
||||
numfound = len(paths)
|
||||
paths = paths[(self.page - 1) * self.count:self.page * self.count]
|
||||
response, counter, id2ci = self._wrap_path_result(paths,
|
||||
types,
|
||||
{tuple(i): 1 for i in path},
|
||||
set(target.get('type_ids') or []),
|
||||
type2show_key)
|
||||
return response, counter, len(paths), self.page, numfound, id2ci, relation_types, type2show_key
|
||||
|
@@ -97,6 +97,8 @@ class AttributeValueManager(object):
|
||||
deserialize = ValueTypeMap.deserialize[value_type]
|
||||
try:
|
||||
v = deserialize(value)
|
||||
if value_type in (ValueTypeEnum.DATE, ValueTypeEnum.DATETIME):
|
||||
return str(v)
|
||||
return v
|
||||
except ValueDeserializeError as e:
|
||||
return abort(400, ErrFormat.attribute_value_invalid2.format(alias, e))
|
||||
|
@@ -376,7 +376,7 @@ class AuditCRUD(object):
|
||||
origin=origin, current=current, extra=extra, source=source.value)
|
||||
|
||||
@classmethod
|
||||
def add_login_log(cls, username, is_ok, description, _id=None, logout_at=None):
|
||||
def add_login_log(cls, username, is_ok, description, _id=None, logout_at=None, ip=None, browser=None):
|
||||
if _id is not None:
|
||||
existed = AuditLoginLog.get_by_id(_id)
|
||||
if existed is not None:
|
||||
@@ -387,8 +387,9 @@ class AuditCRUD(object):
|
||||
is_ok=is_ok,
|
||||
description=description,
|
||||
logout_at=logout_at,
|
||||
ip=request.headers.get('X-Real-IP') or request.remote_addr,
|
||||
browser=request.headers.get('User-Agent'),
|
||||
ip=(ip or request.headers.get('X-Forwarded-For') or
|
||||
request.headers.get('X-Real-IP') or request.remote_addr or '').split(',')[0],
|
||||
browser=browser or request.headers.get('User-Agent'),
|
||||
channel=request.values.get('channel', 'web'),
|
||||
)
|
||||
|
||||
|
Binary file not shown.
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2024-08-20 13:47+0800\n"
|
||||
"POT-Creation-Date: 2024-09-26 17:57+0800\n"
|
||||
"PO-Revision-Date: 2023-12-25 20:21+0800\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language: zh\n"
|
||||
@@ -492,6 +492,10 @@ msgstr "拓扑视图分组 {} 已经存在"
|
||||
msgid "The group cannot be deleted because the topology view already exists"
|
||||
msgstr "因为该分组下定义了拓扑视图,不能删除"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:158
|
||||
msgid "Both the source model and the target model must be selected"
|
||||
msgstr "源模型和目标模型不能为空!"
|
||||
|
||||
#: api/lib/common_setting/resp_format.py:8
|
||||
msgid "Company info already existed"
|
||||
msgstr "公司信息已存在,无法创建!"
|
||||
|
@@ -1,7 +1,6 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
import datetime
|
||||
|
||||
import jwt
|
||||
import six
|
||||
from flask import abort
|
||||
@@ -17,10 +16,12 @@ from api.lib.decorator import args_required
|
||||
from api.lib.decorator import args_validate
|
||||
from api.lib.perm.acl.acl import ACLManager
|
||||
from api.lib.perm.acl.audit import AuditCRUD
|
||||
from api.lib.perm.acl.cache import AppCache
|
||||
from api.lib.perm.acl.cache import RoleCache
|
||||
from api.lib.perm.acl.cache import User
|
||||
from api.lib.perm.acl.cache import UserCache
|
||||
from api.lib.perm.acl.resp_format import ErrFormat
|
||||
from api.lib.perm.acl.role import RoleRelationCRUD
|
||||
from api.lib.perm.auth import auth_abandoned
|
||||
from api.lib.perm.auth import auth_with_app_token
|
||||
from api.models.acl import Role
|
||||
@@ -124,10 +125,17 @@ class AuthWithKeyView(APIView):
|
||||
if not user.get('username'):
|
||||
user['username'] = user.get('name')
|
||||
|
||||
return self.jsonify(user=user,
|
||||
authenticated=authenticated,
|
||||
rid=role and role.id,
|
||||
can_proxy=can_proxy)
|
||||
result = dict(user=user,
|
||||
authenticated=authenticated,
|
||||
rid=role and role.id,
|
||||
can_proxy=can_proxy)
|
||||
|
||||
if request.values.get('need_parentRoles') in current_app.config.get('BOOL_TRUE'):
|
||||
app_id = AppCache.get(request.values.get('app_id'))
|
||||
parent_ids = RoleRelationCRUD.recursive_parent_ids(role and role.id, app_id and app_id.id)
|
||||
result['user']['parentRoles'] = [RoleCache.get(rid).name for rid in set(parent_ids) if RoleCache.get(rid)]
|
||||
|
||||
return self.jsonify(result)
|
||||
|
||||
|
||||
class AuthWithTokenView(APIView):
|
||||
@@ -184,6 +192,8 @@ class LogoutView(APIView):
|
||||
def post(self):
|
||||
logout_user()
|
||||
|
||||
AuditCRUD.add_login_log(None, None, None, _id=session.get('LOGIN_ID'), logout_at=datetime.datetime.now())
|
||||
AuditCRUD.add_login_log(None, None, None,
|
||||
_id=session.get('LOGIN_ID') or request.values.get('LOGIN_ID'),
|
||||
logout_at=datetime.datetime.now())
|
||||
|
||||
self.jsonify(code=200)
|
||||
|
@@ -11,6 +11,7 @@ from flask_login import current_user
|
||||
from api.lib.decorator import args_required
|
||||
from api.lib.decorator import args_validate
|
||||
from api.lib.perm.acl.acl import ACLManager
|
||||
from api.lib.perm.acl.acl import AuditCRUD
|
||||
from api.lib.perm.acl.acl import role_required
|
||||
from api.lib.perm.acl.cache import AppCache
|
||||
from api.lib.perm.acl.cache import UserCache
|
||||
@@ -48,6 +49,13 @@ class GetUserInfoView(APIView):
|
||||
role=dict(permissions=user_info.get('parents')),
|
||||
avatar=user_info.get('avatar'))
|
||||
|
||||
if request.values.get('channel'):
|
||||
_id = AuditCRUD.add_login_log(name, True, ErrFormat.login_succeed,
|
||||
ip=request.values.get('ip'),
|
||||
browser=request.values.get('browser'))
|
||||
session['LOGIN_ID'] = _id
|
||||
result['LOGIN_ID'] = _id
|
||||
|
||||
current_app.logger.info("get user info for3: {}".format(result))
|
||||
return self.jsonify(result=result)
|
||||
|
||||
|
@@ -2,7 +2,6 @@
|
||||
|
||||
|
||||
import time
|
||||
|
||||
from flask import abort
|
||||
from flask import current_app
|
||||
from flask import request
|
||||
@@ -65,6 +64,42 @@ class CIRelationSearchView(APIView):
|
||||
result=response)
|
||||
|
||||
|
||||
class CIRelationSearchPathView(APIView):
|
||||
url_prefix = ("/ci_relations/path/s", "/ci_relations/path/search")
|
||||
|
||||
@args_required("source", "target", "path")
|
||||
def post(self):
|
||||
"""@params: page: page number
|
||||
page_size | count: page size
|
||||
source: source CIType, e.g. {type_id: 1, q: `search expr`}
|
||||
target: target CIType, e.g. {type_ids: [2], q: `search expr`}
|
||||
path: Path from the Source CIType to the Target CIType, e.g. [1, ..., 2]
|
||||
"""
|
||||
|
||||
page = get_page(request.values.get("page", 1))
|
||||
count = get_page_size(request.values.get("count") or request.values.get("page_size"))
|
||||
|
||||
source = request.values.get("source")
|
||||
target = request.values.get("target")
|
||||
path = request.values.get("path")
|
||||
|
||||
s = Search(page=page, count=count)
|
||||
try:
|
||||
(response, counter, total, page, numfound, id2ci,
|
||||
relation_types, type2show_key) = s.search_by_path(source, target, path)
|
||||
except SearchError as e:
|
||||
return abort(400, str(e))
|
||||
|
||||
return self.jsonify(numfound=numfound,
|
||||
total=total,
|
||||
page=page,
|
||||
counter=counter,
|
||||
paths=response,
|
||||
id2ci=id2ci,
|
||||
relation_types=relation_types,
|
||||
type2show_key=type2show_key)
|
||||
|
||||
|
||||
class CIRelationStatisticsView(APIView):
|
||||
url_prefix = "/ci_relations/statistics"
|
||||
|
||||
|
@@ -8,7 +8,6 @@ from api.lib.cmdb.ci_type import CITypeManager
|
||||
from api.lib.cmdb.ci_type import CITypeRelationManager
|
||||
from api.lib.cmdb.const import PermEnum
|
||||
from api.lib.cmdb.const import ResourceTypeEnum
|
||||
from api.lib.cmdb.const import RoleEnum
|
||||
from api.lib.cmdb.preference import PreferenceManager
|
||||
from api.lib.cmdb.resp_format import ErrFormat
|
||||
from api.lib.common_setting.decorator import perms_role_required
|
||||
@@ -17,7 +16,7 @@ from api.lib.decorator import args_required
|
||||
from api.lib.perm.acl.acl import ACLManager
|
||||
from api.lib.perm.acl.acl import has_perm_from_args
|
||||
from api.lib.perm.acl.acl import is_app_admin
|
||||
from api.lib.perm.acl.acl import role_required
|
||||
from api.lib.utils import handle_arg_list
|
||||
from api.resource import APIView
|
||||
|
||||
app_cli = CMDBApp()
|
||||
@@ -42,6 +41,19 @@ class GetParentsView(APIView):
|
||||
return self.jsonify(parents=CITypeRelationManager.get_parents(child_id))
|
||||
|
||||
|
||||
class CITypeRelationPathView(APIView):
|
||||
url_prefix = ("/ci_type_relations/path",)
|
||||
|
||||
@args_required("source_type_id", "target_type_ids")
|
||||
def get(self):
|
||||
source_type_id = request.values.get("source_type_id")
|
||||
target_type_ids = handle_arg_list(request.values.get("target_type_ids"))
|
||||
|
||||
paths = CITypeRelationManager.find_path(source_type_id, target_type_ids)
|
||||
|
||||
return self.jsonify(paths=paths)
|
||||
|
||||
|
||||
class CITypeRelationView(APIView):
|
||||
url_prefix = ("/ci_type_relations", "/ci_type_relations/<int:parent_id>/<int:child_id>")
|
||||
|
||||
|
@@ -56,3 +56,4 @@ colorama>=0.4.6
|
||||
lz4>=4.3.2
|
||||
python-magic==0.4.27
|
||||
jsonpath==0.82.2
|
||||
networkx>=3.1
|
||||
|
@@ -39,9 +39,9 @@ SQLALCHEMY_ENGINE_OPTIONS = {
|
||||
|
||||
# # cache
|
||||
CACHE_TYPE = 'redis'
|
||||
CACHE_REDIS_HOST = '127.0.0.1'
|
||||
CACHE_REDIS_PORT = 6379
|
||||
CACHE_REDIS_PASSWORD = ''
|
||||
CACHE_REDIS_HOST = env.str('CACHE_REDIS_HOST', default='redis')
|
||||
CACHE_REDIS_PORT = env.str('CACHE_REDIS_PORT', default='6379')
|
||||
CACHE_REDIS_PASSWORD = env.str('CACHE_REDIS_PASSWORD', default='')
|
||||
CACHE_KEY_PREFIX = 'CMDB::'
|
||||
CACHE_DEFAULT_TIMEOUT = 3000
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 3857903 */
|
||||
src: url('iconfont.woff2?t=1724653006782') format('woff2'),
|
||||
url('iconfont.woff?t=1724653006782') format('woff'),
|
||||
url('iconfont.ttf?t=1724653006782') format('truetype');
|
||||
src: url('iconfont.woff2?t=1729157759723') format('woff2'),
|
||||
url('iconfont.woff?t=1729157759723') format('woff'),
|
||||
url('iconfont.ttf?t=1729157759723') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
@@ -13,6 +13,326 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.oneterm-mysql:before {
|
||||
content: "\e9e8";
|
||||
}
|
||||
|
||||
.oneterm-redis:before {
|
||||
content: "\e9e7";
|
||||
}
|
||||
|
||||
.veops-sign_out:before {
|
||||
content: "\e9e6";
|
||||
}
|
||||
|
||||
.veops-company:before {
|
||||
content: "\e9e5";
|
||||
}
|
||||
|
||||
.veops-emails:before {
|
||||
content: "\e9e4";
|
||||
}
|
||||
|
||||
.veops-switch:before {
|
||||
content: "\e9e3";
|
||||
}
|
||||
|
||||
.qiyeweixin:before {
|
||||
content: "\e9e2";
|
||||
}
|
||||
|
||||
.veops-progress:before {
|
||||
content: "\e9e1";
|
||||
}
|
||||
|
||||
.veops-completed:before {
|
||||
content: "\e9e0";
|
||||
}
|
||||
|
||||
.itsm-ticketTime:before {
|
||||
content: "\e9df";
|
||||
}
|
||||
|
||||
.veops-notification:before {
|
||||
content: "\e9dc";
|
||||
}
|
||||
|
||||
.a-veops-account1:before {
|
||||
content: "\e9dd";
|
||||
}
|
||||
|
||||
.veops-personal:before {
|
||||
content: "\e9de";
|
||||
}
|
||||
|
||||
.itsm-customer_satisfaction2:before {
|
||||
content: "\e9da";
|
||||
}
|
||||
|
||||
.itsm-over2:before {
|
||||
content: "\e9db";
|
||||
}
|
||||
|
||||
.veops-search1:before {
|
||||
content: "\e9d9";
|
||||
}
|
||||
|
||||
.itsm-customer_satisfaction:before {
|
||||
content: "\e9d8";
|
||||
}
|
||||
|
||||
.itsm-over:before {
|
||||
content: "\e9d7";
|
||||
}
|
||||
|
||||
.itsm-request:before {
|
||||
content: "\e9d6";
|
||||
}
|
||||
|
||||
.itsm-release:before {
|
||||
content: "\e9d5";
|
||||
}
|
||||
|
||||
.veops-link:before {
|
||||
content: "\e9d4";
|
||||
}
|
||||
|
||||
.oneterm-command_record:before {
|
||||
content: "\e9d3";
|
||||
}
|
||||
|
||||
.ai-question:before {
|
||||
content: "\e9d2";
|
||||
}
|
||||
|
||||
.ai-sending:before {
|
||||
content: "\e9d1";
|
||||
}
|
||||
|
||||
.ai-dialogue:before {
|
||||
content: "\e9d0";
|
||||
}
|
||||
|
||||
.ai-report2:before {
|
||||
content: "\e9cf";
|
||||
}
|
||||
|
||||
.ai-delete:before {
|
||||
content: "\e9cd";
|
||||
}
|
||||
|
||||
.caise-knowledge:before {
|
||||
content: "\e9ce";
|
||||
}
|
||||
|
||||
.ai-article:before {
|
||||
content: "\e9cc";
|
||||
}
|
||||
|
||||
.ai-model_setup1:before {
|
||||
content: "\e9cb";
|
||||
}
|
||||
|
||||
.ai-report:before {
|
||||
content: "\e9ca";
|
||||
}
|
||||
|
||||
.ai-customer_service:before {
|
||||
content: "\e9c9";
|
||||
}
|
||||
|
||||
.oneterm-connect1:before {
|
||||
content: "\e9c6";
|
||||
}
|
||||
|
||||
.oneterm-session1:before {
|
||||
content: "\e9c7";
|
||||
}
|
||||
|
||||
.oneterm-assets:before {
|
||||
content: "\e9c8";
|
||||
}
|
||||
|
||||
.a-oneterm-ssh1:before {
|
||||
content: "\e9c3";
|
||||
}
|
||||
|
||||
.a-oneterm-ssh2:before {
|
||||
content: "\e9c4";
|
||||
}
|
||||
|
||||
.oneterm-rdp:before {
|
||||
content: "\e9c5";
|
||||
}
|
||||
|
||||
.caise-websphere:before {
|
||||
content: "\e9c2";
|
||||
}
|
||||
|
||||
.caise-vps:before {
|
||||
content: "\e9c1";
|
||||
}
|
||||
|
||||
.caise-F5:before {
|
||||
content: "\e9c0";
|
||||
}
|
||||
|
||||
.caise-HAProxy:before {
|
||||
content: "\e9bf";
|
||||
}
|
||||
|
||||
.caise-JBoss:before {
|
||||
content: "\e9be";
|
||||
}
|
||||
|
||||
.caise-dongfangtong:before {
|
||||
content: "\e9bd";
|
||||
}
|
||||
|
||||
.caise-kafka:before {
|
||||
content: "\e9b7";
|
||||
}
|
||||
|
||||
.caise-weblogic:before {
|
||||
content: "\e9b8";
|
||||
}
|
||||
|
||||
.caise-TDSQL:before {
|
||||
content: "\e9b9";
|
||||
}
|
||||
|
||||
.caise-kingbase:before {
|
||||
content: "\e9ba";
|
||||
}
|
||||
|
||||
.caise-dameng:before {
|
||||
content: "\e9bb";
|
||||
}
|
||||
|
||||
.caise-TIDB:before {
|
||||
content: "\e9bc";
|
||||
}
|
||||
|
||||
.veops-expand:before {
|
||||
content: "\e9b6";
|
||||
}
|
||||
|
||||
.caise-public_cloud:before {
|
||||
content: "\e9b1";
|
||||
}
|
||||
|
||||
.caise-system:before {
|
||||
content: "\e9b2";
|
||||
}
|
||||
|
||||
.caise-IPAM:before {
|
||||
content: "\e9b3";
|
||||
}
|
||||
|
||||
.caise-hyperV:before {
|
||||
content: "\e9b4";
|
||||
}
|
||||
|
||||
.caise-data_center2:before {
|
||||
content: "\e9b5";
|
||||
}
|
||||
|
||||
.caise-hardware:before {
|
||||
content: "\e9ad";
|
||||
}
|
||||
|
||||
.caise-computer:before {
|
||||
content: "\e9ae";
|
||||
}
|
||||
|
||||
.caise-network_devices:before {
|
||||
content: "\e9af";
|
||||
}
|
||||
|
||||
.caise-storage_device:before {
|
||||
content: "\e9b0";
|
||||
}
|
||||
|
||||
.caise-load_balancing:before {
|
||||
content: "\e9ab";
|
||||
}
|
||||
|
||||
.caise-message_queue:before {
|
||||
content: "\e9ac";
|
||||
}
|
||||
|
||||
.caise-websever:before {
|
||||
content: "\e9aa";
|
||||
}
|
||||
|
||||
.caise-middleware:before {
|
||||
content: "\e9a9";
|
||||
}
|
||||
|
||||
.caise-database:before {
|
||||
content: "\e9a7";
|
||||
}
|
||||
|
||||
.caise-business:before {
|
||||
content: "\e9a8";
|
||||
}
|
||||
|
||||
.caise-virtualization:before {
|
||||
content: "\e9a6";
|
||||
}
|
||||
|
||||
.caise-storage_pool:before {
|
||||
content: "\e9a4";
|
||||
}
|
||||
|
||||
.caise-storage_volume1:before {
|
||||
content: "\e9a5";
|
||||
}
|
||||
|
||||
.ciase-aix:before {
|
||||
content: "\e9a3";
|
||||
}
|
||||
|
||||
.caise_pool:before {
|
||||
content: "\e99b";
|
||||
}
|
||||
|
||||
.caise-ip_address:before {
|
||||
content: "\e99c";
|
||||
}
|
||||
|
||||
.caise-computer_room:before {
|
||||
content: "\e99d";
|
||||
}
|
||||
|
||||
.caise-rack:before {
|
||||
content: "\e99e";
|
||||
}
|
||||
|
||||
.caise-pc:before {
|
||||
content: "\e99f";
|
||||
}
|
||||
|
||||
.caise-bandwidth_line:before {
|
||||
content: "\e9a0";
|
||||
}
|
||||
|
||||
.caise-fiber:before {
|
||||
content: "\e9a1";
|
||||
}
|
||||
|
||||
.caise-disk_array:before {
|
||||
content: "\e9a2";
|
||||
}
|
||||
|
||||
.veops-group:before {
|
||||
content: "\e99a";
|
||||
}
|
||||
|
||||
.veops-inheritance:before {
|
||||
content: "\e999";
|
||||
}
|
||||
|
||||
.veops-department:before {
|
||||
content: "\e998";
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
@@ -5,6 +5,566 @@
|
||||
"css_prefix_text": "",
|
||||
"description": "",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "42155223",
|
||||
"name": "oneterm-mysql",
|
||||
"font_class": "oneterm-mysql",
|
||||
"unicode": "e9e8",
|
||||
"unicode_decimal": 59880
|
||||
},
|
||||
{
|
||||
"icon_id": "42155225",
|
||||
"name": "oneterm-redis",
|
||||
"font_class": "oneterm-redis",
|
||||
"unicode": "e9e7",
|
||||
"unicode_decimal": 59879
|
||||
},
|
||||
{
|
||||
"icon_id": "42154436",
|
||||
"name": "veops-sign_out",
|
||||
"font_class": "veops-sign_out",
|
||||
"unicode": "e9e6",
|
||||
"unicode_decimal": 59878
|
||||
},
|
||||
{
|
||||
"icon_id": "42154310",
|
||||
"name": "veops-company",
|
||||
"font_class": "veops-company",
|
||||
"unicode": "e9e5",
|
||||
"unicode_decimal": 59877
|
||||
},
|
||||
{
|
||||
"icon_id": "42154325",
|
||||
"name": "veops-emails",
|
||||
"font_class": "veops-emails",
|
||||
"unicode": "e9e4",
|
||||
"unicode_decimal": 59876
|
||||
},
|
||||
{
|
||||
"icon_id": "42154350",
|
||||
"name": "veops-switch",
|
||||
"font_class": "veops-switch",
|
||||
"unicode": "e9e3",
|
||||
"unicode_decimal": 59875
|
||||
},
|
||||
{
|
||||
"icon_id": "42154370",
|
||||
"name": "veops-qiyeweixin",
|
||||
"font_class": "qiyeweixin",
|
||||
"unicode": "e9e2",
|
||||
"unicode_decimal": 59874
|
||||
},
|
||||
{
|
||||
"icon_id": "42134185",
|
||||
"name": "veops-progress",
|
||||
"font_class": "veops-progress",
|
||||
"unicode": "e9e1",
|
||||
"unicode_decimal": 59873
|
||||
},
|
||||
{
|
||||
"icon_id": "42134110",
|
||||
"name": "veops-completed",
|
||||
"font_class": "veops-completed",
|
||||
"unicode": "e9e0",
|
||||
"unicode_decimal": 59872
|
||||
},
|
||||
{
|
||||
"icon_id": "42133882",
|
||||
"name": "itsm-ticketTime",
|
||||
"font_class": "itsm-ticketTime",
|
||||
"unicode": "e9df",
|
||||
"unicode_decimal": 59871
|
||||
},
|
||||
{
|
||||
"icon_id": "42122869",
|
||||
"name": "veops-notification",
|
||||
"font_class": "veops-notification",
|
||||
"unicode": "e9dc",
|
||||
"unicode_decimal": 59868
|
||||
},
|
||||
{
|
||||
"icon_id": "42122868",
|
||||
"name": "veops-account_password",
|
||||
"font_class": "a-veops-account1",
|
||||
"unicode": "e9dd",
|
||||
"unicode_decimal": 59869
|
||||
},
|
||||
{
|
||||
"icon_id": "42122861",
|
||||
"name": "veops-personal",
|
||||
"font_class": "veops-personal",
|
||||
"unicode": "e9de",
|
||||
"unicode_decimal": 59870
|
||||
},
|
||||
{
|
||||
"icon_id": "42101103",
|
||||
"name": "itsm-evaluation2",
|
||||
"font_class": "itsm-customer_satisfaction2",
|
||||
"unicode": "e9da",
|
||||
"unicode_decimal": 59866
|
||||
},
|
||||
{
|
||||
"icon_id": "42101098",
|
||||
"name": "itsm-over2",
|
||||
"font_class": "itsm-over2",
|
||||
"unicode": "e9db",
|
||||
"unicode_decimal": 59867
|
||||
},
|
||||
{
|
||||
"icon_id": "42065574",
|
||||
"name": "veops-search",
|
||||
"font_class": "veops-search1",
|
||||
"unicode": "e9d9",
|
||||
"unicode_decimal": 59865
|
||||
},
|
||||
{
|
||||
"icon_id": "42063479",
|
||||
"name": "itsm-evaluation",
|
||||
"font_class": "itsm-customer_satisfaction",
|
||||
"unicode": "e9d8",
|
||||
"unicode_decimal": 59864
|
||||
},
|
||||
{
|
||||
"icon_id": "42062436",
|
||||
"name": "itsm-over",
|
||||
"font_class": "itsm-over",
|
||||
"unicode": "e9d7",
|
||||
"unicode_decimal": 59863
|
||||
},
|
||||
{
|
||||
"icon_id": "42050642",
|
||||
"name": "itsm-requirement",
|
||||
"font_class": "itsm-request",
|
||||
"unicode": "e9d6",
|
||||
"unicode_decimal": 59862
|
||||
},
|
||||
{
|
||||
"icon_id": "42050622",
|
||||
"name": "itsm-release",
|
||||
"font_class": "itsm-release",
|
||||
"unicode": "e9d5",
|
||||
"unicode_decimal": 59861
|
||||
},
|
||||
{
|
||||
"icon_id": "41903314",
|
||||
"name": "veops-link",
|
||||
"font_class": "veops-link",
|
||||
"unicode": "e9d4",
|
||||
"unicode_decimal": 59860
|
||||
},
|
||||
{
|
||||
"icon_id": "41876664",
|
||||
"name": "oneterm-command_record",
|
||||
"font_class": "oneterm-command_record",
|
||||
"unicode": "e9d3",
|
||||
"unicode_decimal": 59859
|
||||
},
|
||||
{
|
||||
"icon_id": "41859436",
|
||||
"name": "ai-question",
|
||||
"font_class": "ai-question",
|
||||
"unicode": "e9d2",
|
||||
"unicode_decimal": 59858
|
||||
},
|
||||
{
|
||||
"icon_id": "41859414",
|
||||
"name": "ai-sending",
|
||||
"font_class": "ai-sending",
|
||||
"unicode": "e9d1",
|
||||
"unicode_decimal": 59857
|
||||
},
|
||||
{
|
||||
"icon_id": "41859374",
|
||||
"name": "ai-dialogue",
|
||||
"font_class": "ai-dialogue",
|
||||
"unicode": "e9d0",
|
||||
"unicode_decimal": 59856
|
||||
},
|
||||
{
|
||||
"icon_id": "41859191",
|
||||
"name": "ai-report2",
|
||||
"font_class": "ai-report2",
|
||||
"unicode": "e9cf",
|
||||
"unicode_decimal": 59855
|
||||
},
|
||||
{
|
||||
"icon_id": "41858720",
|
||||
"name": "ai-delete",
|
||||
"font_class": "ai-delete",
|
||||
"unicode": "e9cd",
|
||||
"unicode_decimal": 59853
|
||||
},
|
||||
{
|
||||
"icon_id": "41858484",
|
||||
"name": "caise-knowledge",
|
||||
"font_class": "caise-knowledge",
|
||||
"unicode": "e9ce",
|
||||
"unicode_decimal": 59854
|
||||
},
|
||||
{
|
||||
"icon_id": "41833445",
|
||||
"name": "ai-article",
|
||||
"font_class": "ai-article",
|
||||
"unicode": "e9cc",
|
||||
"unicode_decimal": 59852
|
||||
},
|
||||
{
|
||||
"icon_id": "41811974",
|
||||
"name": "ai-model_setup (1)",
|
||||
"font_class": "ai-model_setup1",
|
||||
"unicode": "e9cb",
|
||||
"unicode_decimal": 59851
|
||||
},
|
||||
{
|
||||
"icon_id": "41811980",
|
||||
"name": "ai-report",
|
||||
"font_class": "ai-report",
|
||||
"unicode": "e9ca",
|
||||
"unicode_decimal": 59850
|
||||
},
|
||||
{
|
||||
"icon_id": "41811915",
|
||||
"name": "ai-customer_service",
|
||||
"font_class": "ai-customer_service",
|
||||
"unicode": "e9c9",
|
||||
"unicode_decimal": 59849
|
||||
},
|
||||
{
|
||||
"icon_id": "41735717",
|
||||
"name": "oneterm-connect",
|
||||
"font_class": "oneterm-connect1",
|
||||
"unicode": "e9c6",
|
||||
"unicode_decimal": 59846
|
||||
},
|
||||
{
|
||||
"icon_id": "41735716",
|
||||
"name": "oneterm-session",
|
||||
"font_class": "oneterm-session1",
|
||||
"unicode": "e9c7",
|
||||
"unicode_decimal": 59847
|
||||
},
|
||||
{
|
||||
"icon_id": "41735703",
|
||||
"name": "oneterm-assets",
|
||||
"font_class": "oneterm-assets",
|
||||
"unicode": "e9c8",
|
||||
"unicode_decimal": 59848
|
||||
},
|
||||
{
|
||||
"icon_id": "41725683",
|
||||
"name": "oneterm-RDP",
|
||||
"font_class": "a-oneterm-ssh1",
|
||||
"unicode": "e9c3",
|
||||
"unicode_decimal": 59843
|
||||
},
|
||||
{
|
||||
"icon_id": "41725684",
|
||||
"name": "oneterm-SSH",
|
||||
"font_class": "a-oneterm-ssh2",
|
||||
"unicode": "e9c4",
|
||||
"unicode_decimal": 59844
|
||||
},
|
||||
{
|
||||
"icon_id": "41725685",
|
||||
"name": "oneterm-VNC",
|
||||
"font_class": "oneterm-rdp",
|
||||
"unicode": "e9c5",
|
||||
"unicode_decimal": 59845
|
||||
},
|
||||
{
|
||||
"icon_id": "41724497",
|
||||
"name": "caise-websphere",
|
||||
"font_class": "caise-websphere",
|
||||
"unicode": "e9c2",
|
||||
"unicode_decimal": 59842
|
||||
},
|
||||
{
|
||||
"icon_id": "41724575",
|
||||
"name": "caise-vps",
|
||||
"font_class": "caise-vps",
|
||||
"unicode": "e9c1",
|
||||
"unicode_decimal": 59841
|
||||
},
|
||||
{
|
||||
"icon_id": "41724631",
|
||||
"name": "caise-F5",
|
||||
"font_class": "caise-F5",
|
||||
"unicode": "e9c0",
|
||||
"unicode_decimal": 59840
|
||||
},
|
||||
{
|
||||
"icon_id": "41724653",
|
||||
"name": "caise-HAProxy",
|
||||
"font_class": "caise-HAProxy",
|
||||
"unicode": "e9bf",
|
||||
"unicode_decimal": 59839
|
||||
},
|
||||
{
|
||||
"icon_id": "41722953",
|
||||
"name": "caise-JBoss",
|
||||
"font_class": "caise-JBoss",
|
||||
"unicode": "e9be",
|
||||
"unicode_decimal": 59838
|
||||
},
|
||||
{
|
||||
"icon_id": "41722960",
|
||||
"name": "caise-dongfangtong",
|
||||
"font_class": "caise-dongfangtong",
|
||||
"unicode": "e9bd",
|
||||
"unicode_decimal": 59837
|
||||
},
|
||||
{
|
||||
"icon_id": "41722681",
|
||||
"name": "caise-kafka",
|
||||
"font_class": "caise-kafka",
|
||||
"unicode": "e9b7",
|
||||
"unicode_decimal": 59831
|
||||
},
|
||||
{
|
||||
"icon_id": "41722680",
|
||||
"name": "caise-weblogic",
|
||||
"font_class": "caise-weblogic",
|
||||
"unicode": "e9b8",
|
||||
"unicode_decimal": 59832
|
||||
},
|
||||
{
|
||||
"icon_id": "41722679",
|
||||
"name": "caise-TDSQL",
|
||||
"font_class": "caise-TDSQL",
|
||||
"unicode": "e9b9",
|
||||
"unicode_decimal": 59833
|
||||
},
|
||||
{
|
||||
"icon_id": "41722678",
|
||||
"name": "caise-kingbase",
|
||||
"font_class": "caise-kingbase",
|
||||
"unicode": "e9ba",
|
||||
"unicode_decimal": 59834
|
||||
},
|
||||
{
|
||||
"icon_id": "41722677",
|
||||
"name": "达梦",
|
||||
"font_class": "caise-dameng",
|
||||
"unicode": "e9bb",
|
||||
"unicode_decimal": 59835
|
||||
},
|
||||
{
|
||||
"icon_id": "41722675",
|
||||
"name": "caise-TIDB",
|
||||
"font_class": "caise-TIDB",
|
||||
"unicode": "e9bc",
|
||||
"unicode_decimal": 59836
|
||||
},
|
||||
{
|
||||
"icon_id": "41681675",
|
||||
"name": "veops-expand",
|
||||
"font_class": "veops-expand",
|
||||
"unicode": "e9b6",
|
||||
"unicode_decimal": 59830
|
||||
},
|
||||
{
|
||||
"icon_id": "41672951",
|
||||
"name": "公有云",
|
||||
"font_class": "caise-public_cloud",
|
||||
"unicode": "e9b1",
|
||||
"unicode_decimal": 59825
|
||||
},
|
||||
{
|
||||
"icon_id": "41672952",
|
||||
"name": "操作系统",
|
||||
"font_class": "caise-system",
|
||||
"unicode": "e9b2",
|
||||
"unicode_decimal": 59826
|
||||
},
|
||||
{
|
||||
"icon_id": "41673309",
|
||||
"name": "IPAM",
|
||||
"font_class": "caise-IPAM",
|
||||
"unicode": "e9b3",
|
||||
"unicode_decimal": 59827
|
||||
},
|
||||
{
|
||||
"icon_id": "41673312",
|
||||
"name": "hyperV",
|
||||
"font_class": "caise-hyperV",
|
||||
"unicode": "e9b4",
|
||||
"unicode_decimal": 59828
|
||||
},
|
||||
{
|
||||
"icon_id": "41673320",
|
||||
"name": "数据中心",
|
||||
"font_class": "caise-data_center2",
|
||||
"unicode": "e9b5",
|
||||
"unicode_decimal": 59829
|
||||
},
|
||||
{
|
||||
"icon_id": "41669141",
|
||||
"name": "硬件设备",
|
||||
"font_class": "caise-hardware",
|
||||
"unicode": "e9ad",
|
||||
"unicode_decimal": 59821
|
||||
},
|
||||
{
|
||||
"icon_id": "41669249",
|
||||
"name": "计算机",
|
||||
"font_class": "caise-computer",
|
||||
"unicode": "e9ae",
|
||||
"unicode_decimal": 59822
|
||||
},
|
||||
{
|
||||
"icon_id": "41669250",
|
||||
"name": "网络设备",
|
||||
"font_class": "caise-network_devices",
|
||||
"unicode": "e9af",
|
||||
"unicode_decimal": 59823
|
||||
},
|
||||
{
|
||||
"icon_id": "41669278",
|
||||
"name": "存储设备",
|
||||
"font_class": "caise-storage_device",
|
||||
"unicode": "e9b0",
|
||||
"unicode_decimal": 59824
|
||||
},
|
||||
{
|
||||
"icon_id": "41659452",
|
||||
"name": "负载均衡",
|
||||
"font_class": "caise-load_balancing",
|
||||
"unicode": "e9ab",
|
||||
"unicode_decimal": 59819
|
||||
},
|
||||
{
|
||||
"icon_id": "41659446",
|
||||
"name": "消息队列",
|
||||
"font_class": "caise-message_queue",
|
||||
"unicode": "e9ac",
|
||||
"unicode_decimal": 59820
|
||||
},
|
||||
{
|
||||
"icon_id": "41659424",
|
||||
"name": "websever",
|
||||
"font_class": "caise-websever",
|
||||
"unicode": "e9aa",
|
||||
"unicode_decimal": 59818
|
||||
},
|
||||
{
|
||||
"icon_id": "41655608",
|
||||
"name": "中间件",
|
||||
"font_class": "caise-middleware",
|
||||
"unicode": "e9a9",
|
||||
"unicode_decimal": 59817
|
||||
},
|
||||
{
|
||||
"icon_id": "41655599",
|
||||
"name": "数据库",
|
||||
"font_class": "caise-database",
|
||||
"unicode": "e9a7",
|
||||
"unicode_decimal": 59815
|
||||
},
|
||||
{
|
||||
"icon_id": "41655591",
|
||||
"name": "业务",
|
||||
"font_class": "caise-business",
|
||||
"unicode": "e9a8",
|
||||
"unicode_decimal": 59816
|
||||
},
|
||||
{
|
||||
"icon_id": "41655550",
|
||||
"name": "虚拟化",
|
||||
"font_class": "caise-virtualization",
|
||||
"unicode": "e9a6",
|
||||
"unicode_decimal": 59814
|
||||
},
|
||||
{
|
||||
"icon_id": "41654680",
|
||||
"name": "存储池",
|
||||
"font_class": "caise-storage_pool",
|
||||
"unicode": "e9a4",
|
||||
"unicode_decimal": 59812
|
||||
},
|
||||
{
|
||||
"icon_id": "41654676",
|
||||
"name": "存储卷",
|
||||
"font_class": "caise-storage_volume1",
|
||||
"unicode": "e9a5",
|
||||
"unicode_decimal": 59813
|
||||
},
|
||||
{
|
||||
"icon_id": "41654608",
|
||||
"name": "aix",
|
||||
"font_class": "ciase-aix",
|
||||
"unicode": "e9a3",
|
||||
"unicode_decimal": 59811
|
||||
},
|
||||
{
|
||||
"icon_id": "41654233",
|
||||
"name": "ip池",
|
||||
"font_class": "caise_pool",
|
||||
"unicode": "e99b",
|
||||
"unicode_decimal": 59803
|
||||
},
|
||||
{
|
||||
"icon_id": "41654237",
|
||||
"name": "ip地址",
|
||||
"font_class": "caise-ip_address",
|
||||
"unicode": "e99c",
|
||||
"unicode_decimal": 59804
|
||||
},
|
||||
{
|
||||
"icon_id": "41654249",
|
||||
"name": "机房",
|
||||
"font_class": "caise-computer_room",
|
||||
"unicode": "e99d",
|
||||
"unicode_decimal": 59805
|
||||
},
|
||||
{
|
||||
"icon_id": "41654271",
|
||||
"name": "机柜",
|
||||
"font_class": "caise-rack",
|
||||
"unicode": "e99e",
|
||||
"unicode_decimal": 59806
|
||||
},
|
||||
{
|
||||
"icon_id": "41654276",
|
||||
"name": "PC",
|
||||
"font_class": "caise-pc",
|
||||
"unicode": "e99f",
|
||||
"unicode_decimal": 59807
|
||||
},
|
||||
{
|
||||
"icon_id": "41654305",
|
||||
"name": "带宽线路",
|
||||
"font_class": "caise-bandwidth_line",
|
||||
"unicode": "e9a0",
|
||||
"unicode_decimal": 59808
|
||||
},
|
||||
{
|
||||
"icon_id": "41654323",
|
||||
"name": "光纤交换机",
|
||||
"font_class": "caise-fiber",
|
||||
"unicode": "e9a1",
|
||||
"unicode_decimal": 59809
|
||||
},
|
||||
{
|
||||
"icon_id": "41654369",
|
||||
"name": "磁盘阵列",
|
||||
"font_class": "caise-disk_array",
|
||||
"unicode": "e9a2",
|
||||
"unicode_decimal": 59810
|
||||
},
|
||||
{
|
||||
"icon_id": "41643869",
|
||||
"name": "veops-group",
|
||||
"font_class": "veops-group",
|
||||
"unicode": "e99a",
|
||||
"unicode_decimal": 59802
|
||||
},
|
||||
{
|
||||
"icon_id": "41637123",
|
||||
"name": "veops-inheritance",
|
||||
"font_class": "veops-inheritance",
|
||||
"unicode": "e999",
|
||||
"unicode_decimal": 59801
|
||||
},
|
||||
{
|
||||
"icon_id": "41570722",
|
||||
"name": "veops-department",
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -61,12 +61,12 @@ export default {
|
||||
)
|
||||
|
||||
// 注册富文本自定义元素
|
||||
const resume = {
|
||||
type: 'attachment',
|
||||
attachmentLabel: '',
|
||||
attachmentValue: '',
|
||||
children: [{ text: '' }], // void 元素必须有一个 children ,其中只有一个空字符串,重要!!!
|
||||
}
|
||||
// const resume = {
|
||||
// type: 'attachment',
|
||||
// attachmentLabel: '',
|
||||
// attachmentValue: '',
|
||||
// children: [{ text: '' }], // void 元素必须有一个 children ,其中只有一个空字符串,重要!!!
|
||||
// }
|
||||
|
||||
function withAttachment(editor) {
|
||||
// JS 语法
|
||||
|
@@ -177,7 +177,7 @@ export const linearIconList = [
|
||||
}]
|
||||
}, {
|
||||
value: 'icon-xianxing-application',
|
||||
label: '应用',
|
||||
label: '常用组件',
|
||||
list: [{
|
||||
value: 'icon-xianxing-yilianjie',
|
||||
label: '已连接'
|
||||
@@ -517,7 +517,7 @@ export const fillIconList = [
|
||||
}]
|
||||
}, {
|
||||
value: 'icon-shidi-application',
|
||||
label: '应用',
|
||||
label: '常用组件',
|
||||
list: [{
|
||||
value: 'icon-shidi-yilianjie',
|
||||
label: '已连接'
|
||||
@@ -729,6 +729,18 @@ export const multicolorIconList = [
|
||||
value: 'database',
|
||||
label: '数据库',
|
||||
list: [{
|
||||
value: 'caise-TIDB',
|
||||
label: 'TIDB'
|
||||
}, {
|
||||
value: 'caise-dameng',
|
||||
label: '达梦'
|
||||
}, {
|
||||
value: 'caise-kingbase',
|
||||
label: 'KingBase'
|
||||
}, {
|
||||
value: 'caise-TDSQL',
|
||||
label: 'TDSQL'
|
||||
}, {
|
||||
value: 'caise-DB2',
|
||||
label: 'DB2'
|
||||
}, {
|
||||
@@ -809,6 +821,9 @@ export const multicolorIconList = [
|
||||
value: 'system',
|
||||
label: '操作系统',
|
||||
list: [{
|
||||
value: 'ciase-aix',
|
||||
label: 'aix'
|
||||
}, {
|
||||
value: 'caise-Windows',
|
||||
label: 'Windows'
|
||||
}, {
|
||||
@@ -903,8 +918,113 @@ export const multicolorIconList = [
|
||||
}]
|
||||
}, {
|
||||
value: 'caise-application',
|
||||
label: '应用',
|
||||
label: '常用组件',
|
||||
list: [{
|
||||
value: 'caise-websphere',
|
||||
label: 'WebSphere'
|
||||
}, {
|
||||
value: 'caise-vps',
|
||||
label: 'VPS'
|
||||
}, {
|
||||
value: 'caise-F5',
|
||||
label: 'F5'
|
||||
}, {
|
||||
value: 'caise-HAProxy',
|
||||
label: 'HAProxy'
|
||||
}, {
|
||||
value: 'caise-kafka',
|
||||
label: 'kafka'
|
||||
}, {
|
||||
value: 'caise-dongfangtong',
|
||||
label: '东方通'
|
||||
}, {
|
||||
value: 'cmdb-vcenter',
|
||||
label: 'VCenter'
|
||||
}, {
|
||||
value: 'ops-KVM',
|
||||
label: 'KVM'
|
||||
}, {
|
||||
value: 'caise-JBoss',
|
||||
label: 'JBoss'
|
||||
}, {
|
||||
value: 'caise-weblogic',
|
||||
label: 'WebLogic'
|
||||
}, {
|
||||
value: 'caise-disk_array',
|
||||
label: '磁盘阵列'
|
||||
}, {
|
||||
value: 'caise-fiber',
|
||||
label: '光纤交换机'
|
||||
}, {
|
||||
value: 'caise-bandwidth_line',
|
||||
label: '带宽线路'
|
||||
}, {
|
||||
value: 'caise-pc',
|
||||
label: 'PC'
|
||||
}, {
|
||||
value: 'caise-rack',
|
||||
label: '机柜'
|
||||
}, {
|
||||
value: 'caise-computer_room',
|
||||
label: '机房'
|
||||
}, {
|
||||
value: 'caise-ip_address',
|
||||
label: 'ip地址'
|
||||
}, {
|
||||
value: 'caise_pool',
|
||||
label: 'ip池'
|
||||
}, {
|
||||
value: 'caise-storage_volume1',
|
||||
label: '存储卷'
|
||||
}, {
|
||||
value: 'caise-virtualization',
|
||||
label: '虚拟化'
|
||||
}, {
|
||||
value: 'caise-business',
|
||||
label: '业务'
|
||||
}, {
|
||||
value: 'caise-database',
|
||||
label: '数据库'
|
||||
}, {
|
||||
value: 'caise-middleware',
|
||||
label: '中间件'
|
||||
}, {
|
||||
value: 'caise-websever',
|
||||
label: 'websever'
|
||||
}, {
|
||||
value: 'caise-message_queue',
|
||||
label: '消息队列'
|
||||
}, {
|
||||
value: 'caise-load_balancing',
|
||||
label: '负载均衡'
|
||||
}, {
|
||||
value: 'caise-storage_device',
|
||||
label: '存储设备'
|
||||
}, {
|
||||
value: 'caise-network_devices',
|
||||
label: '网络设备'
|
||||
}, {
|
||||
value: 'caise-computer',
|
||||
label: '计算机'
|
||||
}, {
|
||||
value: 'caise-hardware',
|
||||
label: '硬件设备'
|
||||
}, {
|
||||
value: 'caise-data_center2',
|
||||
label: '数据中心'
|
||||
}, {
|
||||
value: 'caise-hyperV',
|
||||
label: 'hyperV'
|
||||
}, {
|
||||
value: 'caise-IPAM',
|
||||
label: 'IPAM'
|
||||
}, {
|
||||
value: 'caise-system',
|
||||
label: '操作系统'
|
||||
}, {
|
||||
value: 'caise-public_cloud',
|
||||
label: '公有云'
|
||||
}, {
|
||||
value: 'caise-data_center',
|
||||
label: '数据中心'
|
||||
}, {
|
||||
|
24
cmdb-ui/src/components/Menu/index.module.less
Normal file
24
cmdb-ui/src/components/Menu/index.module.less
Normal file
@@ -0,0 +1,24 @@
|
||||
.cmdb-side-menu-search {
|
||||
background-color: #FFFFFF !important;
|
||||
cursor: auto !important;
|
||||
|
||||
:global {
|
||||
.ant-input-affix-wrapper {
|
||||
max-width: 170px !important;
|
||||
width: 170px;
|
||||
border-radius: 30px;
|
||||
}
|
||||
|
||||
.ant-input {
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
background-color: #F7F8FA;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.ant-input-suffix {
|
||||
right: 0px !important;
|
||||
}
|
||||
}
|
||||
}
|
@@ -9,6 +9,8 @@ import {
|
||||
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'
|
||||
|
||||
const { Item, SubMenu } = Menu
|
||||
|
||||
@@ -40,7 +42,8 @@ export default {
|
||||
openKeys: [],
|
||||
selectedKeys: [],
|
||||
cachedOpenKeys: [],
|
||||
resource_type: {}
|
||||
resource_type: {},
|
||||
currentAppRoute: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -64,6 +67,7 @@ export default {
|
||||
searchResourceType({ page_size: 9999, app_id: 'cmdb' }).then(res => {
|
||||
this.resource_type = { groups: res.groups, id2perms: res.id2perms }
|
||||
})
|
||||
this.currentAppRoute = this.$route?.matched?.[0]?.name || ''
|
||||
this.updateMenu()
|
||||
},
|
||||
watch: {
|
||||
@@ -75,12 +79,14 @@ export default {
|
||||
this.openKeys = this.cachedOpenKeys
|
||||
}
|
||||
},
|
||||
$route: function () {
|
||||
$route: function (route) {
|
||||
this.currentAppRoute = route?.matched?.[0]?.name
|
||||
this.updateMenu()
|
||||
},
|
||||
},
|
||||
inject: ['reload'],
|
||||
methods: {
|
||||
...mapActions(['UpdateCMDBSEarchValue']),
|
||||
cancelAttributes(e, menu) {
|
||||
const that = this
|
||||
e.preventDefault()
|
||||
@@ -286,6 +292,47 @@ export default {
|
||||
this.$message.error(this.$t('noPermission'))
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
jumpCMDBSearch(value) {
|
||||
this.UpdateCMDBSEarchValue(value)
|
||||
|
||||
if (this.$route.name !== 'cmdb_resource_search') {
|
||||
this.$router.push({
|
||||
name: 'cmdb_resource_search',
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
renderCMDBSearch() {
|
||||
if (this.currentAppRoute !== 'cmdb' || this.collapsed) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Item class={styles['cmdb-side-menu-search']}>
|
||||
<a-input
|
||||
ref="cmdbSideMenuSearchInputRef"
|
||||
class={styles['cmdb-side-menu-search-input']}
|
||||
style={{
|
||||
border: this.$route.name === 'cmdb_resource_search' ? 'solid 1px #B1C9FF' : ''
|
||||
}}
|
||||
placeholder={this.$t('cmdbSearch')}
|
||||
onPressEnter={(e) => {
|
||||
this.jumpCMDBSearch(e.target.value)
|
||||
}}
|
||||
>
|
||||
<ops-icon
|
||||
slot="suffix"
|
||||
type="veops-search1"
|
||||
onClick={() => {
|
||||
const value = this.$refs?.cmdbSideMenuSearchInputRef?.$refs?.input?.value || ''
|
||||
this.jumpCMDBSearch(value)
|
||||
}}
|
||||
/>
|
||||
</a-input>
|
||||
</Item>
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -313,6 +360,7 @@ export default {
|
||||
// {...{ props, on: on }}
|
||||
return (
|
||||
<Menu class="ops-side-bar" selectedKeys={this.selectedKeys} {...{ props, on: on }}>
|
||||
{this.renderCMDBSearch()}
|
||||
{menuTree}
|
||||
</Menu>
|
||||
)
|
||||
|
@@ -1,183 +1,187 @@
|
||||
<template>
|
||||
<div ref="splitPane" class="split-pane" :class="direction + ' ' + appName" :style="{ flexDirection: direction }">
|
||||
<div class="pane pane-one" ref="one" :style="lengthType + ':' + paneLengthValue1">
|
||||
<slot name="one"></slot>
|
||||
</div>
|
||||
|
||||
<div class="spliter-wrap">
|
||||
<a-button
|
||||
v-show="collapsable"
|
||||
:icon="isExpanded ? 'left' : 'right'"
|
||||
class="collapse-btn"
|
||||
@click="handleExpand"
|
||||
></a-button>
|
||||
<div
|
||||
class="pane-trigger"
|
||||
@mousedown="handleMouseDown"
|
||||
:style="{ backgroundColor: triggerColor, width: `${triggerLength}px` }"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div class="pane pane-two" ref="two" :style="lengthType + ':' + paneLengthValue2">
|
||||
<slot name="two"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SplitPane',
|
||||
props: {
|
||||
direction: {
|
||||
type: String,
|
||||
default: 'row',
|
||||
},
|
||||
|
||||
min: {
|
||||
type: Number,
|
||||
default: 10,
|
||||
},
|
||||
|
||||
max: {
|
||||
type: Number,
|
||||
default: 90,
|
||||
},
|
||||
|
||||
paneLengthPixel: {
|
||||
type: Number,
|
||||
default: 220,
|
||||
},
|
||||
|
||||
triggerLength: {
|
||||
type: Number,
|
||||
default: 8,
|
||||
},
|
||||
|
||||
appName: {
|
||||
type: String,
|
||||
default: 'viewer',
|
||||
},
|
||||
|
||||
collapsable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
triggerColor: {
|
||||
type: String,
|
||||
default: '#f7f8fa',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
triggerLeftOffset: 0, // 鼠标距滑动器左(顶)侧偏移量
|
||||
isExpanded: localStorage.getItem(`${this.appName}-isExpanded`)
|
||||
? JSON.parse(localStorage.getItem(`${this.appName}-isExpanded`))
|
||||
: false,
|
||||
parentContainer: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
lengthType() {
|
||||
return this.direction === 'row' ? 'width' : 'height'
|
||||
},
|
||||
|
||||
minLengthType() {
|
||||
return this.direction === 'row' ? 'minWidth' : 'minHeight'
|
||||
},
|
||||
|
||||
paneLengthValue1() {
|
||||
return `calc(${this.paneLengthPercent}% - ${this.triggerLength / 2 + 'px'})`
|
||||
},
|
||||
|
||||
paneLengthValue2() {
|
||||
const rest = 100 - this.paneLengthPercent
|
||||
return `calc(${rest}% - ${this.triggerLength / 2 + 'px'})`
|
||||
},
|
||||
|
||||
paneLengthPercent() {
|
||||
const clientRectWidth = this.parentContainer
|
||||
? this.parentContainer.clientWidth
|
||||
: document.documentElement.getBoundingClientRect().width
|
||||
return (this.paneLengthPixel / clientRectWidth) * 100
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
isExpanded(newValue) {
|
||||
if (newValue) {
|
||||
document.querySelector(`.${this.appName} .pane-two`).style.display = 'none'
|
||||
} else {
|
||||
document.querySelector(`.${this.appName} .pane-two`).style.display = ''
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
const paneLengthPixel = localStorage.getItem(`${this.appName}-paneLengthPixel`)
|
||||
if (paneLengthPixel) {
|
||||
this.$emit('update:paneLengthPixel', Number(paneLengthPixel))
|
||||
}
|
||||
this.parentContainer = document.querySelector(`.${this.appName}`)
|
||||
if (this.isExpanded) {
|
||||
document.querySelector(`.${this.appName} .pane-two`).style.display = 'none'
|
||||
} else {
|
||||
document.querySelector(`.${this.appName} .pane-two`).style.display = ''
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 按下滑动器
|
||||
handleMouseDown(e) {
|
||||
document.addEventListener('mousemove', this.handleMouseMove)
|
||||
document.addEventListener('mouseup', this.handleMouseUp)
|
||||
if (this.direction === 'row') {
|
||||
this.triggerLeftOffset = e.pageX - e.srcElement.getBoundingClientRect().left
|
||||
} else {
|
||||
this.triggerLeftOffset = e.pageY - e.srcElement.getBoundingClientRect().top
|
||||
}
|
||||
},
|
||||
|
||||
// 按下滑动器后移动鼠标
|
||||
handleMouseMove(e) {
|
||||
this.isExpanded = false
|
||||
this.$emit('expand', this.isExpanded)
|
||||
const clientRect = this.$refs.splitPane.getBoundingClientRect()
|
||||
let paneLengthPixel = 0
|
||||
|
||||
if (this.direction === 'row') {
|
||||
const offset = e.pageX - clientRect.left - this.triggerLeftOffset + this.triggerLength / 2
|
||||
paneLengthPixel = offset
|
||||
} else {
|
||||
const offset = e.pageY - clientRect.top - this.triggerLeftOffset + this.triggerLength / 2
|
||||
paneLengthPixel = offset
|
||||
}
|
||||
|
||||
if (paneLengthPixel < this.min) {
|
||||
paneLengthPixel = this.min
|
||||
}
|
||||
if (paneLengthPixel > this.max) {
|
||||
paneLengthPixel = this.max
|
||||
}
|
||||
|
||||
this.$emit('update:paneLengthPixel', paneLengthPixel)
|
||||
|
||||
localStorage.setItem(`${this.appName}-paneLengthPixel`, paneLengthPixel)
|
||||
},
|
||||
|
||||
// 松开滑动器
|
||||
handleMouseUp() {
|
||||
document.removeEventListener('mousemove', this.handleMouseMove)
|
||||
},
|
||||
|
||||
handleExpand() {
|
||||
this.isExpanded = !this.isExpanded
|
||||
this.$emit('expand', this.isExpanded)
|
||||
localStorage.setItem(`${this.appName}-isExpanded`, this.isExpanded)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
@import './index.less';
|
||||
</style>
|
||||
<template>
|
||||
<div ref="splitPane" class="split-pane" :class="direction + ' ' + appName" :style="{ flexDirection: direction }">
|
||||
<div class="pane pane-one" ref="one" :style="lengthType + ':' + paneLengthValue1">
|
||||
<slot name="one"></slot>
|
||||
</div>
|
||||
|
||||
<div class="spliter-wrap">
|
||||
<a-button
|
||||
v-show="collapsable"
|
||||
:icon="isExpanded ? 'left' : 'right'"
|
||||
class="collapse-btn"
|
||||
@click="handleExpand"
|
||||
></a-button>
|
||||
<div
|
||||
class="pane-trigger"
|
||||
@mousedown="handleMouseDown"
|
||||
:style="{ backgroundColor: triggerColor, width: `${triggerLength}px` }"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div class="pane pane-two" ref="two" :style="lengthType + ':' + paneLengthValue2">
|
||||
<slot name="two"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SplitPane',
|
||||
props: {
|
||||
direction: {
|
||||
type: String,
|
||||
default: 'row',
|
||||
},
|
||||
|
||||
min: {
|
||||
type: Number,
|
||||
default: 10,
|
||||
},
|
||||
|
||||
max: {
|
||||
type: Number,
|
||||
default: 90,
|
||||
},
|
||||
|
||||
paneLengthPixel: {
|
||||
type: Number,
|
||||
default: 220,
|
||||
},
|
||||
|
||||
triggerLength: {
|
||||
type: Number,
|
||||
default: 8,
|
||||
},
|
||||
|
||||
appName: {
|
||||
type: String,
|
||||
default: 'viewer',
|
||||
},
|
||||
|
||||
collapsable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
triggerColor: {
|
||||
type: String,
|
||||
default: '#f7f8fa',
|
||||
},
|
||||
calcBasedParent: {
|
||||
type: Boolean,
|
||||
defualt: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
triggerLeftOffset: 0, // 鼠标距滑动器左(顶)侧偏移量
|
||||
isExpanded: localStorage.getItem(`${this.appName}-isExpanded`)
|
||||
? JSON.parse(localStorage.getItem(`${this.appName}-isExpanded`))
|
||||
: false,
|
||||
parentContainer: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
lengthType() {
|
||||
return this.direction === 'row' ? 'width' : 'height'
|
||||
},
|
||||
|
||||
minLengthType() {
|
||||
return this.direction === 'row' ? 'minWidth' : 'minHeight'
|
||||
},
|
||||
|
||||
paneLengthValue1() {
|
||||
return `calc(${this.paneLengthPercent}% - ${this.triggerLength / 2 + 'px'})`
|
||||
},
|
||||
|
||||
paneLengthValue2() {
|
||||
const rest = 100 - this.paneLengthPercent
|
||||
return `calc(${rest}% - ${this.triggerLength / 2 + 'px'})`
|
||||
},
|
||||
|
||||
paneLengthPercent() {
|
||||
const clientRectWidth = this.parentContainer && this.calcBasedParent
|
||||
? this.parentContainer.clientWidth
|
||||
: document.documentElement.getBoundingClientRect().width
|
||||
return (this.paneLengthPixel / clientRectWidth) * 100
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
isExpanded(newValue) {
|
||||
if (newValue) {
|
||||
document.querySelector(`.${this.appName} .pane-two`).style.display = 'none'
|
||||
} else {
|
||||
document.querySelector(`.${this.appName} .pane-two`).style.display = ''
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
const paneLengthPixel = localStorage.getItem(`${this.appName}-paneLengthPixel`)
|
||||
if (paneLengthPixel) {
|
||||
this.$emit('update:paneLengthPixel', Number(paneLengthPixel))
|
||||
}
|
||||
this.parentContainer = document.querySelector(`.${this.appName}`)
|
||||
if (this.isExpanded) {
|
||||
document.querySelector(`.${this.appName} .pane-two`).style.display = 'none'
|
||||
} else {
|
||||
document.querySelector(`.${this.appName} .pane-two`).style.display = ''
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 按下滑动器
|
||||
handleMouseDown(e) {
|
||||
document.addEventListener('mousemove', this.handleMouseMove)
|
||||
document.addEventListener('mouseup', this.handleMouseUp)
|
||||
if (this.direction === 'row') {
|
||||
this.triggerLeftOffset = e.pageX - e.srcElement.getBoundingClientRect().left
|
||||
} else {
|
||||
this.triggerLeftOffset = e.pageY - e.srcElement.getBoundingClientRect().top
|
||||
}
|
||||
},
|
||||
|
||||
// 按下滑动器后移动鼠标
|
||||
handleMouseMove(e) {
|
||||
this.isExpanded = false
|
||||
this.$emit('expand', this.isExpanded)
|
||||
const clientRect = this.$refs.splitPane.getBoundingClientRect()
|
||||
let paneLengthPixel = 0
|
||||
|
||||
if (this.direction === 'row') {
|
||||
const offset = e.pageX - clientRect.left - this.triggerLeftOffset + this.triggerLength / 2
|
||||
paneLengthPixel = offset
|
||||
} else {
|
||||
const offset = e.pageY - clientRect.top - this.triggerLeftOffset + this.triggerLength / 2
|
||||
paneLengthPixel = offset
|
||||
}
|
||||
|
||||
if (paneLengthPixel < this.min) {
|
||||
paneLengthPixel = this.min
|
||||
}
|
||||
if (paneLengthPixel > this.max) {
|
||||
paneLengthPixel = this.max
|
||||
}
|
||||
|
||||
this.$emit('update:paneLengthPixel', paneLengthPixel)
|
||||
|
||||
localStorage.setItem(`${this.appName}-paneLengthPixel`, paneLengthPixel)
|
||||
},
|
||||
|
||||
// 松开滑动器
|
||||
handleMouseUp() {
|
||||
document.removeEventListener('mousemove', this.handleMouseMove)
|
||||
},
|
||||
|
||||
handleExpand() {
|
||||
this.isExpanded = !this.isExpanded
|
||||
this.$emit('expand', this.isExpanded)
|
||||
localStorage.setItem(`${this.appName}-isExpanded`, this.isExpanded)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
@import './index.less';
|
||||
</style>
|
||||
|
@@ -6,7 +6,8 @@
|
||||
:paneLengthPixel.sync="paneLengthPixel"
|
||||
:appName="appName"
|
||||
:triggerColor="triggerColor"
|
||||
:triggerLength="18"
|
||||
:triggerLength="triggerLength"
|
||||
:calcBasedParent="calcBasedParent"
|
||||
>
|
||||
<template #one>
|
||||
<div class="two-column-layout-sidebar">
|
||||
@@ -37,6 +38,14 @@ export default {
|
||||
type: String,
|
||||
default: '#f7f8fa',
|
||||
},
|
||||
triggerLength: {
|
||||
type: Number,
|
||||
default: 18
|
||||
},
|
||||
calcBasedParent: {
|
||||
type: Boolean,
|
||||
defualt: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@@ -31,7 +31,7 @@ export default {
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
&:hover {
|
||||
background: linear-gradient(0deg, rgba(0, 80, 201, 0.2) 0%, rgba(174, 207, 255, 0.06) 86.76%);
|
||||
// background: linear-gradient(0deg, rgba(0, 80, 201, 0.2) 0%, rgba(174, 207, 255, 0.06) 86.76%);
|
||||
color: @layout-header-font-selected-color;
|
||||
}
|
||||
}
|
||||
|
@@ -5,7 +5,6 @@
|
||||
v-for="route in defaultShowRoutes"
|
||||
:key="route.name"
|
||||
@click="() => handleClick(route)"
|
||||
:title="$t(route.meta.title)"
|
||||
>
|
||||
{{ route.meta.title }}
|
||||
</span>
|
||||
@@ -119,7 +118,9 @@ export default {
|
||||
line-height: @layout-header-line-height;
|
||||
display: inline-block;
|
||||
}
|
||||
> span:hover,
|
||||
> span:hover {
|
||||
background-color: #f0f2f5;
|
||||
}
|
||||
.top-menu-selected {
|
||||
font-weight: bold;
|
||||
color: @layout-header-font-selected-color;
|
||||
|
@@ -10,10 +10,25 @@
|
||||
<ops-icon class="common-settings-btn-icon" type="veops-setting" />
|
||||
<span class="common-settings-btn-text">{{ $t('settings') }}</span>
|
||||
</span>
|
||||
<span class="locale" @click="changeLang">{{ locale === 'zh' ? '简中' : 'EN' }}</span>
|
||||
<a-popover
|
||||
trigger="click"
|
||||
:overlayStyle="{ width: '150px' }"
|
||||
overlayClassName="lang-popover-wrap"
|
||||
placement="bottomRight"
|
||||
:getPopupContainer="(trigger) => trigger.parentNode"
|
||||
>
|
||||
<span class="locale">{{ languageList.find((lang) => lang.key === locale).title }}</span>
|
||||
<div class="lang-menu" slot="content">
|
||||
<a
|
||||
v-for="(lang) in languageList"
|
||||
:key="lang.key"
|
||||
:class="['lang-menu-item', lang.key === locale ? 'lang-menu-item_active' : '']"
|
||||
@click="changeLang(lang.key)"
|
||||
>
|
||||
{{ lang.title }}
|
||||
</a>
|
||||
</div>
|
||||
</a-popover>
|
||||
<a-popover
|
||||
:overlayStyle="{ width: '130px' }"
|
||||
placement="bottomRight"
|
||||
overlayClassName="custom-user"
|
||||
>
|
||||
@@ -29,7 +44,7 @@
|
||||
<span>{{ $t('topMenu.logout') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<span class="action ant-dropdown-link user-dropdown-menu">
|
||||
<span class="action ant-dropdown-link user-dropdown-menu user-info-wrap">
|
||||
<a-avatar
|
||||
v-if="avatar()"
|
||||
class="avatar"
|
||||
@@ -54,6 +69,20 @@ export default {
|
||||
components: {
|
||||
DocumentLink,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
languageList: [
|
||||
{
|
||||
title: '简中',
|
||||
key: 'zh'
|
||||
},
|
||||
{
|
||||
title: 'EN',
|
||||
key: 'en'
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['user', 'locale']),
|
||||
hasBackendPermission() {
|
||||
@@ -81,14 +110,9 @@ export default {
|
||||
handleClick() {
|
||||
this.$router.push('/setting')
|
||||
},
|
||||
changeLang() {
|
||||
if (this.locale === 'zh') {
|
||||
this.SET_LOCALE('en')
|
||||
this.$i18n.locale = 'en'
|
||||
} else {
|
||||
this.SET_LOCALE('zh')
|
||||
this.$i18n.locale = 'zh'
|
||||
}
|
||||
changeLang(lang) {
|
||||
this.SET_LOCALE(lang)
|
||||
this.$i18n.locale = lang
|
||||
this.$nextTick(() => {
|
||||
setDocumentTitle(`${this.$t(this.$route.meta.title)} - ${domTitle}`)
|
||||
})
|
||||
@@ -124,6 +148,15 @@ export default {
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
.lang-popover-wrap {
|
||||
width: 70px;
|
||||
padding: 0px;
|
||||
|
||||
.ant-popover-inner-content {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@@ -150,6 +183,47 @@ export default {
|
||||
font-weight: 400;
|
||||
color: #4E5969;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.commen-settings-btn-text {
|
||||
color: #2F54EB;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lang-menu {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&-item {
|
||||
width: 100%;
|
||||
padding: 5px 10px;
|
||||
cursor: pointer;
|
||||
color: #4E5969;
|
||||
|
||||
&_active {
|
||||
color: #2F54EB;
|
||||
background-color: #f0f5ff;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #2F54EB;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-info-wrap {
|
||||
.avatar {
|
||||
transition: all 0.2s;
|
||||
border: solid 1px transparent;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.avatar {
|
||||
border-color: #2F54EB;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -108,6 +108,7 @@ export default {
|
||||
visual: 'Visual',
|
||||
default: 'default',
|
||||
tip: 'Tip',
|
||||
cmdbSearch: 'Search',
|
||||
pagination: {
|
||||
total: '{range0}-{range1} of {total} items'
|
||||
},
|
||||
|
@@ -108,6 +108,7 @@ export default {
|
||||
visual: '虚拟',
|
||||
default: '默认',
|
||||
tip: '提示',
|
||||
cmdbSearch: '搜索一下',
|
||||
pagination: {
|
||||
total: '当前展示 {range0}-{range1} 条数据, 共 {total} 条'
|
||||
},
|
||||
|
@@ -81,3 +81,11 @@ export function searchCIRelationFull(params) {
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
export function searchCIRelationPath(data) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_relations/path/s`,
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
@@ -74,3 +74,11 @@ export function getCanEditByParentIdChildId(parent_id, child_id) {
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
|
||||
export function getCITypeRelationPath(params) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_type_relations/path`,
|
||||
method: 'GET',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
BIN
cmdb-ui/src/modules/cmdb/assets/no_permission.png
Normal file
BIN
cmdb-ui/src/modules/cmdb/assets/no_permission.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.8 KiB |
Binary file not shown.
After Width: | Height: | Size: 177 KiB |
Binary file not shown.
After Width: | Height: | Size: 444 KiB |
@@ -13,7 +13,10 @@
|
||||
v-decorator="['filename', { rules: [{ required: true, message: $t('cmdb.components.filenameInputTips') }] }]"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :label="$t('cmdb.components.saveType')">
|
||||
<a-form-item
|
||||
v-if="showFileTypeSelect"
|
||||
:label="$t('cmdb.components.saveType')"
|
||||
>
|
||||
<a-select
|
||||
:placeholder="$t('cmdb.components.saveTypeTips')"
|
||||
v-decorator="[
|
||||
@@ -83,6 +86,10 @@ export default {
|
||||
type: String,
|
||||
default: 'default',
|
||||
},
|
||||
showFileTypeSelect: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
85
cmdb-ui/src/modules/cmdb/components/ciIcon/index.vue
Normal file
85
cmdb-ui/src/modules/cmdb/components/ciIcon/index.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="icon || title"
|
||||
class="ci-icon"
|
||||
:style="{
|
||||
'--size': size + 'px'
|
||||
}"
|
||||
>
|
||||
<template v-if="icon">
|
||||
<img
|
||||
v-if="icon.split('$$')[2]"
|
||||
:src="`/api/common-setting/v1/file/${icon.split('$$')[3]}`"
|
||||
/>
|
||||
<ops-icon
|
||||
v-else
|
||||
:style="{
|
||||
color: icon.split('$$')[1],
|
||||
}"
|
||||
:type="icon.split('$$')[0]"
|
||||
/>
|
||||
</template>
|
||||
<span
|
||||
class="ci-icon-letter"
|
||||
v-else
|
||||
>
|
||||
<span>
|
||||
{{ title[0].toUpperCase() }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CIIcon',
|
||||
props: {
|
||||
icon: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 如果没有icon, 默认以title 的第一个字符
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
size: {
|
||||
type: [String, Number],
|
||||
default: '12'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.ci-icon {
|
||||
font-size: var(--size);
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
& > img {
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
}
|
||||
|
||||
&-letter {
|
||||
background-color: #FFFFFF;
|
||||
color: #2f54eb;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0px 1px 2px rgba(47, 84, 235, 0.2);
|
||||
|
||||
& > span {
|
||||
transform-origin: center;
|
||||
transform: scale(0.7);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -107,8 +107,8 @@
|
||||
:type="choice[1].icon.name"
|
||||
/>
|
||||
</template>
|
||||
<a-tooltip placement="topLeft" :title="choice[1].label || choice[0]">
|
||||
<span>{{ choice[1].label || choice[0] }}</span>
|
||||
<a-tooltip placement="topLeft" :title="choice[1] ? choice[1].label || choice[0] : choice[0]">
|
||||
<span>{{ choice[1] ? choice[1].label || choice[0] : choice[0] }}</span>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
</a-select-option>
|
||||
@@ -179,7 +179,7 @@
|
||||
<a @click="openDetail(row.ci_id || row._id)">
|
||||
<a-icon type="unordered-list" />
|
||||
</a>
|
||||
<a-tooltip :title="$t('cmdb.ci.addRelation')">
|
||||
<a-tooltip :title="$t('cmdb.ci.viewRelation')">
|
||||
<a @click="openDetail(row.ci_id || row._id, 'tab_2', '2')">
|
||||
<a-icon type="retweet" />
|
||||
</a>
|
||||
@@ -375,7 +375,7 @@ export default {
|
||||
},
|
||||
|
||||
jsonEditorOk(row, column, jsonData) {
|
||||
this.$attrs.data.forEach((item) => {
|
||||
this.data.forEach((item) => {
|
||||
if (item._id === row._id) {
|
||||
item[column.property] = JSON.stringify(jsonData)
|
||||
}
|
||||
|
@@ -1,212 +1,219 @@
|
||||
<template>
|
||||
<a-modal :width="680" :title="title" :visible="visible" @ok="handleOk" @cancel="handleCancel">
|
||||
<CustomRadio
|
||||
:radioList="[
|
||||
{ value: 1, label: $t('cmdb.components.all') },
|
||||
{ value: 2, label: $t('cmdb.components.customize'), layout: 'vertical' },
|
||||
{ value: 3, label: $t('cmdb.components.none') },
|
||||
]"
|
||||
:value="radioValue"
|
||||
@change="changeRadioValue"
|
||||
>
|
||||
<template slot="extra_2" v-if="radioValue === 2">
|
||||
<treeselect
|
||||
v-if="colType === 'read_attr'"
|
||||
v-model="selectedAttr"
|
||||
:multiple="true"
|
||||
:clearable="true"
|
||||
searchable
|
||||
:options="attrGroup"
|
||||
:placeholder="$t('cmdb.ciType.selectAttributes')"
|
||||
value-consists-of="LEAF_PRIORITY"
|
||||
:limit="10"
|
||||
:limitText="(count) => `+ ${count}`"
|
||||
:normalizer="
|
||||
(node) => {
|
||||
return {
|
||||
id: node.name || -1,
|
||||
label: node.alias || node.name || $t('other'),
|
||||
title: node.alias || node.name || $t('other'),
|
||||
children: node.attributes,
|
||||
}
|
||||
}
|
||||
"
|
||||
appendToBody
|
||||
zIndex="1050"
|
||||
>
|
||||
</treeselect>
|
||||
<a-form-model
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
v-if="colType === 'read_ci'"
|
||||
:labelCol="{ span: 2 }"
|
||||
:wrapperCol="{ span: 10 }"
|
||||
ref="form"
|
||||
>
|
||||
<a-form-model-item :label="$t('name')" prop="name">
|
||||
<a-input v-model="form.name" />
|
||||
</a-form-model-item>
|
||||
<FilterComp
|
||||
ref="filterComp"
|
||||
:isDropdown="false"
|
||||
:canSearchPreferenceAttrList="canSearchPreferenceAttrList"
|
||||
@setExpFromFilter="setExpFromFilter"
|
||||
:expression="expression"
|
||||
/>
|
||||
</a-form-model>
|
||||
</template>
|
||||
</CustomRadio>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { grantCiType, revokeCiType } from '../../api/CIType'
|
||||
import { getCITypeAttributesByTypeIds } from '../../api/CITypeAttr'
|
||||
import FilterComp from '@/components/CMDBFilterComp'
|
||||
|
||||
export default {
|
||||
name: 'ReadGrantModal',
|
||||
components: { FilterComp },
|
||||
props: {
|
||||
CITypeId: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
inject: {
|
||||
provide_attrGroup: {
|
||||
from: 'attrGroup',
|
||||
},
|
||||
provide_filerPerimissions: {
|
||||
from: 'filerPerimissions',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
colType: '',
|
||||
row: {},
|
||||
radioValue: 1,
|
||||
radioStyle: {
|
||||
display: 'block',
|
||||
height: '30px',
|
||||
lineHeight: '30px',
|
||||
},
|
||||
selectedAttr: [],
|
||||
ruleList: [],
|
||||
canSearchPreferenceAttrList: [],
|
||||
expression: '',
|
||||
form: {
|
||||
name: '',
|
||||
},
|
||||
rules: {
|
||||
name: [{ required: true, message: this.$t('cmdb.components.customizeFilterName') }],
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
if (this.colType === 'read_attr') {
|
||||
return this.$t('cmdb.components.attributeGrant')
|
||||
}
|
||||
return this.$t('cmdb.components.ciGrant')
|
||||
},
|
||||
attrGroup() {
|
||||
return this.provide_attrGroup()
|
||||
},
|
||||
filerPerimissions() {
|
||||
return this.provide_filerPerimissions()
|
||||
},
|
||||
filterKey() {
|
||||
if (this.colType === 'read_attr') {
|
||||
return 'attr_filter'
|
||||
}
|
||||
return 'ci_filter'
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async open(colType, row) {
|
||||
this.visible = true
|
||||
this.colType = colType
|
||||
this.row = row
|
||||
this.form = {
|
||||
name: '',
|
||||
}
|
||||
if (this.colType === 'read_ci') {
|
||||
await getCITypeAttributesByTypeIds({ type_ids: this.CITypeId }).then((res) => {
|
||||
this.canSearchPreferenceAttrList = res.attributes.filter((item) => item.value_type !== '6')
|
||||
})
|
||||
}
|
||||
if (this.filerPerimissions[row.rid]) {
|
||||
const _tempValue = this.filerPerimissions[row.rid][this.filterKey]
|
||||
if (_tempValue && _tempValue.length) {
|
||||
this.radioValue = 2
|
||||
if (this.colType === 'read_attr') {
|
||||
this.selectedAttr = _tempValue
|
||||
} else {
|
||||
this.expression = `q=${_tempValue}`
|
||||
this.form = {
|
||||
name: this.filerPerimissions[row.rid].name || '',
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.$refs.filterComp.visibleChange(true)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
async handleOk() {
|
||||
if (this.radioValue === 1) {
|
||||
await grantCiType(this.CITypeId, this.row.rid, {
|
||||
perms: ['read'],
|
||||
attr_filter: this.colType === 'read_attr' ? [] : undefined,
|
||||
ci_filter: this.colType === 'read_ci' ? '' : undefined,
|
||||
})
|
||||
} else if (this.radioValue === 2) {
|
||||
if (this.colType === 'read_ci') {
|
||||
this.$refs.filterComp.handleSubmit()
|
||||
}
|
||||
await grantCiType(this.CITypeId, this.row.rid, {
|
||||
perms: ['read'],
|
||||
attr_filter: this.colType === 'read_attr' ? this.selectedAttr : undefined,
|
||||
ci_filter: this.colType === 'read_ci' ? this.expression.slice(2) : undefined,
|
||||
name: this.colType === 'read_ci' ? this.form.name : undefined,
|
||||
})
|
||||
} else {
|
||||
const _tempValue = this.filerPerimissions?.[this.row.rid]?.[this.filterKey]
|
||||
await revokeCiType(this.CITypeId, this.row.rid, {
|
||||
perms: ['read'],
|
||||
attr_filter: this.colType === 'read_attr' ? _tempValue : undefined,
|
||||
ci_filter: this.colType === 'read_ci' ? _tempValue : undefined,
|
||||
})
|
||||
}
|
||||
this.$emit('updateTableDataRead', this.row, this.radioValue === 1 || this.radioValue === 2)
|
||||
this.handleCancel()
|
||||
},
|
||||
handleCancel() {
|
||||
this.radioValue = 1
|
||||
this.selectedAttr = []
|
||||
if (this.$refs.form) {
|
||||
this.$refs.form.resetFields()
|
||||
}
|
||||
this.visible = false
|
||||
},
|
||||
setExpFromFilter(filterExp) {
|
||||
let expression = ''
|
||||
if (filterExp) {
|
||||
expression = `q=${filterExp}`
|
||||
}
|
||||
this.expression = expression
|
||||
},
|
||||
changeRadioValue(value) {
|
||||
if (this.id_filter) {
|
||||
this.$message.warning(this.$t('cmdb.serviceTree.grantedByServiceTreeTips'))
|
||||
} else {
|
||||
this.radioValue = value
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
<template>
|
||||
<a-modal :width="680" :title="title" :visible="visible" @ok="handleOk" @cancel="handleCancel">
|
||||
<CustomRadio
|
||||
:radioList="[
|
||||
{ value: 1, label: $t('cmdb.components.all') },
|
||||
{ value: 2, label: $t('cmdb.components.customize'), layout: 'vertical' },
|
||||
{ value: 3, label: $t('cmdb.components.none') },
|
||||
]"
|
||||
:value="radioValue"
|
||||
@change="changeRadioValue"
|
||||
>
|
||||
<template slot="extra_2" v-if="radioValue === 2">
|
||||
<treeselect
|
||||
v-if="colType === 'read_attr'"
|
||||
v-model="selectedAttr"
|
||||
:multiple="true"
|
||||
:clearable="true"
|
||||
searchable
|
||||
:options="attrGroup"
|
||||
:placeholder="$t('cmdb.ciType.selectAttributes')"
|
||||
value-consists-of="LEAF_PRIORITY"
|
||||
:limit="10"
|
||||
:limitText="(count) => `+ ${count}`"
|
||||
:normalizer="
|
||||
(node) => {
|
||||
return {
|
||||
id: node.name || -1,
|
||||
label: node.alias || node.name || $t('other'),
|
||||
title: node.alias || node.name || $t('other'),
|
||||
children: node.attributes,
|
||||
}
|
||||
}
|
||||
"
|
||||
appendToBody
|
||||
zIndex="1050"
|
||||
>
|
||||
</treeselect>
|
||||
<a-form-model
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
v-if="colType === 'read_ci'"
|
||||
:labelCol="{ span: 2 }"
|
||||
:wrapperCol="{ span: 10 }"
|
||||
ref="form"
|
||||
>
|
||||
<a-form-model-item :label="$t('name')" prop="name">
|
||||
<a-input v-model="form.name" />
|
||||
</a-form-model-item>
|
||||
<FilterComp
|
||||
ref="filterComp"
|
||||
:isDropdown="false"
|
||||
:canSearchPreferenceAttrList="canSearchPreferenceAttrList"
|
||||
@setExpFromFilter="setExpFromFilter"
|
||||
:expression="expression"
|
||||
/>
|
||||
<div class="read-ci-tip">{{ $t('cmdb.ciType.ciGrantTip') }}</div>
|
||||
</a-form-model>
|
||||
</template>
|
||||
</CustomRadio>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { grantCiType, revokeCiType } from '../../api/CIType'
|
||||
import { getCITypeAttributesByTypeIds } from '../../api/CITypeAttr'
|
||||
import FilterComp from '@/components/CMDBFilterComp'
|
||||
|
||||
export default {
|
||||
name: 'ReadGrantModal',
|
||||
components: { FilterComp },
|
||||
props: {
|
||||
CITypeId: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
inject: {
|
||||
provide_attrGroup: {
|
||||
from: 'attrGroup',
|
||||
},
|
||||
provide_filerPerimissions: {
|
||||
from: 'filerPerimissions',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
colType: '',
|
||||
row: {},
|
||||
radioValue: 1,
|
||||
radioStyle: {
|
||||
display: 'block',
|
||||
height: '30px',
|
||||
lineHeight: '30px',
|
||||
},
|
||||
selectedAttr: [],
|
||||
ruleList: [],
|
||||
canSearchPreferenceAttrList: [],
|
||||
expression: '',
|
||||
form: {
|
||||
name: '',
|
||||
},
|
||||
rules: {
|
||||
name: [{ required: true, message: this.$t('cmdb.components.customizeFilterName') }],
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
if (this.colType === 'read_attr') {
|
||||
return this.$t('cmdb.components.attributeGrant')
|
||||
}
|
||||
return this.$t('cmdb.components.ciGrant')
|
||||
},
|
||||
attrGroup() {
|
||||
return this.provide_attrGroup()
|
||||
},
|
||||
filerPerimissions() {
|
||||
return this.provide_filerPerimissions()
|
||||
},
|
||||
filterKey() {
|
||||
if (this.colType === 'read_attr') {
|
||||
return 'attr_filter'
|
||||
}
|
||||
return 'ci_filter'
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async open(colType, row) {
|
||||
this.visible = true
|
||||
this.colType = colType
|
||||
this.row = row
|
||||
this.form = {
|
||||
name: '',
|
||||
}
|
||||
if (this.colType === 'read_ci') {
|
||||
await getCITypeAttributesByTypeIds({ type_ids: this.CITypeId }).then((res) => {
|
||||
this.canSearchPreferenceAttrList = res.attributes.filter((item) => item.value_type !== '6')
|
||||
})
|
||||
}
|
||||
if (this.filerPerimissions[row.rid]) {
|
||||
const _tempValue = this.filerPerimissions[row.rid][this.filterKey]
|
||||
if (_tempValue && _tempValue.length) {
|
||||
this.radioValue = 2
|
||||
if (this.colType === 'read_attr') {
|
||||
this.selectedAttr = _tempValue
|
||||
} else {
|
||||
this.expression = `q=${_tempValue}`
|
||||
this.form = {
|
||||
name: this.filerPerimissions[row.rid].name || '',
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.$refs.filterComp.visibleChange(true)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
async handleOk() {
|
||||
if (this.radioValue === 1) {
|
||||
await grantCiType(this.CITypeId, this.row.rid, {
|
||||
perms: ['read'],
|
||||
attr_filter: this.colType === 'read_attr' ? [] : undefined,
|
||||
ci_filter: this.colType === 'read_ci' ? '' : undefined,
|
||||
})
|
||||
} else if (this.radioValue === 2) {
|
||||
if (this.colType === 'read_ci') {
|
||||
this.$refs.filterComp.handleSubmit()
|
||||
}
|
||||
await grantCiType(this.CITypeId, this.row.rid, {
|
||||
perms: ['read'],
|
||||
attr_filter: this.colType === 'read_attr' ? this.selectedAttr : undefined,
|
||||
ci_filter: this.colType === 'read_ci' ? this.expression.slice(2) : undefined,
|
||||
name: this.colType === 'read_ci' ? this.form.name : undefined,
|
||||
})
|
||||
} else {
|
||||
const _tempValue = this.filerPerimissions?.[this.row.rid]?.[this.filterKey]
|
||||
await revokeCiType(this.CITypeId, this.row.rid, {
|
||||
perms: ['read'],
|
||||
attr_filter: this.colType === 'read_attr' ? _tempValue : undefined,
|
||||
ci_filter: this.colType === 'read_ci' ? _tempValue : undefined,
|
||||
})
|
||||
}
|
||||
this.$emit('updateTableDataRead', this.row, this.radioValue === 1 || this.radioValue === 2)
|
||||
this.handleCancel()
|
||||
},
|
||||
handleCancel() {
|
||||
this.radioValue = 1
|
||||
this.selectedAttr = []
|
||||
if (this.$refs.form) {
|
||||
this.$refs.form.resetFields()
|
||||
}
|
||||
this.visible = false
|
||||
},
|
||||
setExpFromFilter(filterExp) {
|
||||
let expression = ''
|
||||
if (filterExp) {
|
||||
expression = `q=${filterExp}`
|
||||
}
|
||||
this.expression = expression
|
||||
},
|
||||
changeRadioValue(value) {
|
||||
if (this.id_filter) {
|
||||
this.$message.warning(this.$t('cmdb.serviceTree.grantedByServiceTreeTips'))
|
||||
} else {
|
||||
this.radioValue = value
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.read-ci-tip {
|
||||
font-size: 12px;
|
||||
line-height: 22px;
|
||||
color: #a5a9bc;
|
||||
}
|
||||
</style>
|
||||
|
@@ -0,0 +1,41 @@
|
||||
import i18n from '@/lang'
|
||||
|
||||
export const ruleTypeList = () => {
|
||||
return [
|
||||
{ value: 'and', label: i18n.t('cmdbFilterComp.and') },
|
||||
{ value: 'or', label: i18n.t('cmdbFilterComp.or') },
|
||||
// { value: 'not', label: '非' },
|
||||
]
|
||||
}
|
||||
|
||||
export const expList = () => {
|
||||
return [
|
||||
{ value: 'is', label: i18n.t('cmdbFilterComp.is') },
|
||||
{ value: '~is', label: i18n.t('cmdbFilterComp.~is') },
|
||||
{ value: 'contain', label: i18n.t('cmdbFilterComp.contain') },
|
||||
{ value: '~contain', label: i18n.t('cmdbFilterComp.~contain') },
|
||||
{ value: 'start_with', label: i18n.t('cmdbFilterComp.start_with') },
|
||||
{ value: '~start_with', label: i18n.t('cmdbFilterComp.~start_with') },
|
||||
{ value: 'end_with', label: i18n.t('cmdbFilterComp.end_with') },
|
||||
{ value: '~end_with', label: i18n.t('cmdbFilterComp.~end_with') },
|
||||
{ value: '~value', label: i18n.t('cmdbFilterComp.~value') }, // 为空的定义有点绕
|
||||
{ value: 'value', label: i18n.t('cmdbFilterComp.value') },
|
||||
]
|
||||
}
|
||||
|
||||
export const advancedExpList = () => {
|
||||
return [
|
||||
{ value: 'in', label: i18n.t('cmdbFilterComp.in') },
|
||||
{ value: '~in', label: i18n.t('cmdbFilterComp.~in') },
|
||||
{ value: 'range', label: i18n.t('cmdbFilterComp.range') },
|
||||
{ value: '~range', label: i18n.t('cmdbFilterComp.~range') },
|
||||
{ value: 'compare', label: i18n.t('cmdbFilterComp.compare') },
|
||||
]
|
||||
}
|
||||
|
||||
export const compareTypeList = [
|
||||
{ value: '1', label: '>' },
|
||||
{ value: '2', label: '>=' },
|
||||
{ value: '3', label: '<' },
|
||||
{ value: '4', label: '<=' },
|
||||
]
|
@@ -0,0 +1,320 @@
|
||||
<template>
|
||||
<div :style="{ lineHeight: rowHeight }">
|
||||
<a-space :style="{ display: 'flex', marginBottom: '10px' }" v-for="(item, index) in ruleList" :key="item.id">
|
||||
<div v-if="ruleList.length > 1" :style="{ width: '60px', height: rowHeight, position: 'relative' }">
|
||||
<treeselect
|
||||
v-if="index !== 0"
|
||||
class="custom-treeselect"
|
||||
:style="{ width: '60px', '--custom-height': rowHeight, position: 'absolute', top: '-24px' }"
|
||||
v-model="item.type"
|
||||
:multiple="false"
|
||||
:clearable="false"
|
||||
searchable
|
||||
:options="ruleTypeList"
|
||||
:normalizer="
|
||||
(node) => {
|
||||
return {
|
||||
id: node.value,
|
||||
label: node.label,
|
||||
children: node.children,
|
||||
}
|
||||
}
|
||||
"
|
||||
:disabled="disabled"
|
||||
>
|
||||
</treeselect>
|
||||
</div>
|
||||
<treeselect
|
||||
class="custom-treeselect"
|
||||
:style="{ width: '120px', '--custom-height': rowHeight }"
|
||||
v-model="item.property"
|
||||
:multiple="false"
|
||||
:clearable="false"
|
||||
searchable
|
||||
:options="canSearchPreferenceAttrList"
|
||||
:normalizer="
|
||||
(node) => {
|
||||
return {
|
||||
id: node.name,
|
||||
label: node.alias || node.name,
|
||||
children: node.children,
|
||||
}
|
||||
}
|
||||
"
|
||||
appendToBody
|
||||
:zIndex="1050"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<div
|
||||
v-if="node.id !== '$count'"
|
||||
:title="node.label"
|
||||
slot="option-label"
|
||||
slot-scope="{ node }"
|
||||
class="property-label"
|
||||
>
|
||||
<ValueTypeMapIcon :attr="node.raw" />
|
||||
{{ node.label }}
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
:title="node.label"
|
||||
slot="option-label"
|
||||
slot-scope="{ node }"
|
||||
class="property-label"
|
||||
:style="{ borderBottom: '1px solid #E4E7ED', marginBottom: '8px' }"
|
||||
>
|
||||
<ValueTypeMapIcon :attr="node.raw" />
|
||||
{{ node.label }}
|
||||
</div>
|
||||
<div
|
||||
class="property-label"
|
||||
slot="value-label"
|
||||
slot-scope="{ node }"
|
||||
>
|
||||
<ValueTypeMapIcon :attr="node.raw" /> {{ node.label }}
|
||||
</div>
|
||||
</treeselect>
|
||||
<treeselect
|
||||
class="custom-treeselect"
|
||||
:style="{ width: '90px', '--custom-height': rowHeight }"
|
||||
v-model="item.exp"
|
||||
:multiple="false"
|
||||
:clearable="false"
|
||||
searchable
|
||||
:options="getExpListByProperty(item.property)"
|
||||
:normalizer="
|
||||
(node) => {
|
||||
return {
|
||||
id: node.value,
|
||||
label: node.label,
|
||||
children: node.children,
|
||||
}
|
||||
}
|
||||
"
|
||||
appendToBody
|
||||
@select="(value) => handleChangeExp(value, item, index)"
|
||||
:zIndex="1050"
|
||||
:disabled="disabled"
|
||||
>
|
||||
</treeselect>
|
||||
<ValueControls
|
||||
:rule="ruleList[index]"
|
||||
:attrList="canSearchPreferenceAttrList"
|
||||
:disabled="disabled"
|
||||
:curModelAttrList="curModelAttrList"
|
||||
:rowHeight="rowHeight"
|
||||
@change="(value) => handleChangeValue(value, index)"
|
||||
/>
|
||||
<template v-if="!disabled">
|
||||
<a-tooltip :title="$t('copy')">
|
||||
<a class="operation" @click="handleCopyRule(item)"><ops-icon type="veops-copy"/></a>
|
||||
</a-tooltip>
|
||||
<a-tooltip :title="$t('delete')">
|
||||
<a class="operation" @click="handleDeleteRule(item)"><a-icon type="minus-circle"/></a>
|
||||
</a-tooltip>
|
||||
<a-tooltip :title="$t('cmdbFilterComp.addHere')">
|
||||
<a class="operation" @click="handleAddRuleAt(item)"><a-icon type="plus-circle"/></a>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</a-space>
|
||||
<div class="table-filter-add" v-if="!disabled && ruleList.length === 0">
|
||||
<a @click="handleAddRule">+ {{ $t('new') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { ruleTypeList, expList, advancedExpList, compareTypeList } from './constants.js'
|
||||
import ValueTypeMapIcon from '@/components/CMDBValueTypeMapIcon'
|
||||
import ValueControls from './valueControls.vue'
|
||||
|
||||
export default {
|
||||
name: 'Expression',
|
||||
components: {
|
||||
ValueTypeMapIcon,
|
||||
ValueControls
|
||||
},
|
||||
model: {
|
||||
prop: 'value',
|
||||
event: 'change',
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
canSearchPreferenceAttrList: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => [],
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
curModelAttrList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
compareTypeList,
|
||||
rowHeight: '36px',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
ruleList: {
|
||||
get() {
|
||||
return this.value
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('change', val)
|
||||
return val
|
||||
},
|
||||
},
|
||||
ruleTypeList() {
|
||||
return ruleTypeList()
|
||||
},
|
||||
expList() {
|
||||
return expList()
|
||||
},
|
||||
advancedExpList() {
|
||||
return advancedExpList()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getExpListByProperty(property) {
|
||||
if (property === '$count') {
|
||||
return [
|
||||
{ value: 'is', label: this.$t('cmdbFilterComp.is') },
|
||||
{ value: '~is', label: this.$t('cmdbFilterComp.~is') },
|
||||
{ value: 'compare', label: this.$t('cmdbFilterComp.compare') }
|
||||
]
|
||||
}
|
||||
if (property) {
|
||||
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
|
||||
if (_find && ['0', '1', '3', '4', '5'].includes(_find.value_type)) {
|
||||
return [
|
||||
{ value: 'is', label: this.$t('cmdbFilterComp.is') },
|
||||
{ value: '~is', label: this.$t('cmdbFilterComp.~is') },
|
||||
{ value: '~value', label: this.$t('cmdbFilterComp.~value') }, // 为空的定义有点绕
|
||||
{ value: 'value', label: this.$t('cmdbFilterComp.value') },
|
||||
...this.advancedExpList
|
||||
]
|
||||
}
|
||||
}
|
||||
return [...this.expList, ...this.advancedExpList]
|
||||
},
|
||||
isChoiceByProperty(property) {
|
||||
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
|
||||
if (_find) {
|
||||
return _find.is_choice
|
||||
}
|
||||
return false
|
||||
},
|
||||
handleAddRule() {
|
||||
this.ruleList.push({
|
||||
id: uuidv4(),
|
||||
type: 'and',
|
||||
property: this.canSearchPreferenceAttrList[0]?.name,
|
||||
exp: 'is',
|
||||
value: null,
|
||||
})
|
||||
this.$emit('change', this.ruleList)
|
||||
},
|
||||
handleCopyRule(item) {
|
||||
this.ruleList.push({ ...item, id: uuidv4() })
|
||||
this.$emit('change', this.ruleList)
|
||||
},
|
||||
handleDeleteRule(item) {
|
||||
const idx = this.ruleList.findIndex((r) => r.id === item.id)
|
||||
if (idx > -1) {
|
||||
this.ruleList.splice(idx, 1)
|
||||
}
|
||||
this.$emit('change', this.ruleList)
|
||||
},
|
||||
handleAddRuleAt(item) {
|
||||
const idx = this.ruleList.findIndex((r) => r.id === item.id)
|
||||
if (idx > -1) {
|
||||
this.ruleList.splice(idx + 1, 0, {
|
||||
id: uuidv4(),
|
||||
type: 'and',
|
||||
property: this.canSearchPreferenceAttrList[0]?.name,
|
||||
exp: 'is',
|
||||
value: null,
|
||||
})
|
||||
}
|
||||
this.$emit('change', this.ruleList)
|
||||
},
|
||||
getChoiceValueByProperty(property) {
|
||||
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
|
||||
if (_find) {
|
||||
return _find.choice_value
|
||||
}
|
||||
return []
|
||||
},
|
||||
handleChangeExp({ value }, item, index) {
|
||||
const _ruleList = _.cloneDeep(this.ruleList)
|
||||
if (value === 'range') {
|
||||
_ruleList[index] = {
|
||||
..._ruleList[index],
|
||||
min: '',
|
||||
max: '',
|
||||
exp: value,
|
||||
}
|
||||
} else if (value === 'compare') {
|
||||
_ruleList[index] = {
|
||||
..._ruleList[index],
|
||||
compareType: '1',
|
||||
exp: value,
|
||||
}
|
||||
} else {
|
||||
_ruleList[index] = {
|
||||
..._ruleList[index],
|
||||
exp: value,
|
||||
}
|
||||
}
|
||||
this.ruleList = _ruleList
|
||||
this.$emit('change', this.ruleList)
|
||||
},
|
||||
|
||||
handleChangeValue(value, index) {
|
||||
const _ruleList = _.cloneDeep(this.ruleList)
|
||||
_ruleList[index] = value
|
||||
this.$emit('change', _ruleList)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 150px;
|
||||
|
||||
&-range-icon {
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
input {
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.property-label {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden
|
||||
}
|
||||
|
||||
.attr-filter-tip {
|
||||
color: #86909C;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
</style>
|
211
cmdb-ui/src/modules/cmdb/components/conditionFilter/index.vue
Normal file
211
cmdb-ui/src/modules/cmdb/components/conditionFilter/index.vue
Normal file
@@ -0,0 +1,211 @@
|
||||
<template>
|
||||
<div>
|
||||
<Expression
|
||||
v-model="ruleList"
|
||||
:canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)"
|
||||
:disabled="false"
|
||||
:curModelAttrList="curModelAttrList"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { compareTypeList } from './constants.js'
|
||||
|
||||
import Expression from './expression.vue'
|
||||
|
||||
export default {
|
||||
name: 'AttrFilter',
|
||||
components: {
|
||||
Expression
|
||||
},
|
||||
props: {
|
||||
canSearchPreferenceAttrList: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => [],
|
||||
},
|
||||
expression: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
regQ: {
|
||||
type: String,
|
||||
default: '(?<=q=).+(?=&)|(?<=q=).+$',
|
||||
},
|
||||
CITypeIds: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
curModelAttrList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
compareTypeList,
|
||||
visible: false,
|
||||
ruleList: [],
|
||||
filterExp: '',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
init(open, isInitOne = true) {
|
||||
// isInitOne 初始化exp为空时,ruleList是否默认给一条
|
||||
// const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
|
||||
const exp = this.expression.match(new RegExp(this.regQ, 'g'))
|
||||
? this.expression.match(new RegExp(this.regQ, 'g'))[0]
|
||||
: null
|
||||
if (open && exp) {
|
||||
const expArray = exp.split(',').map((item) => {
|
||||
let has_not = ''
|
||||
const key = item.split(':')[0]
|
||||
const val = item
|
||||
.split(':')
|
||||
.slice(1)
|
||||
.join(':')
|
||||
let type, property, exp, value, min, max, compareType
|
||||
if (key.includes('-')) {
|
||||
type = 'or'
|
||||
if (key.includes('~')) {
|
||||
property = key.substring(2)
|
||||
has_not = '~'
|
||||
} else {
|
||||
property = key.substring(1)
|
||||
}
|
||||
} else {
|
||||
type = 'and'
|
||||
if (key.includes('~')) {
|
||||
property = key.substring(1)
|
||||
has_not = '~'
|
||||
} else {
|
||||
property = key
|
||||
}
|
||||
}
|
||||
|
||||
const in_reg = /(?<=\().+(?=\))/g
|
||||
const range_reg = /(?<=\[).+(?=\])/g
|
||||
const compare_reg = /(?<=>=|<=|>(?!=)|<(?!=)).+/
|
||||
if (val === '*') {
|
||||
exp = has_not + 'value'
|
||||
value = ''
|
||||
} else if (in_reg.test(val)) {
|
||||
exp = has_not + 'in'
|
||||
value = val.match(in_reg)[0]
|
||||
} else if (range_reg.test(val)) {
|
||||
exp = has_not + 'range'
|
||||
value = val.match(range_reg)[0]
|
||||
min = value.split('_TO_')[0]
|
||||
max = value.split('_TO_')[1]
|
||||
} else if (compare_reg.test(val)) {
|
||||
exp = has_not + 'compare'
|
||||
value = val.match(compare_reg)[0]
|
||||
const _compareType = val.substring(0, val.match(compare_reg)['index'])
|
||||
const idx = compareTypeList.findIndex((item) => item.label === _compareType)
|
||||
compareType = compareTypeList[idx].value
|
||||
} else if (!val.includes('*')) {
|
||||
exp = has_not + 'is'
|
||||
value = val
|
||||
} else {
|
||||
const resList = [
|
||||
['contain', /(?<=\*).*(?=\*)/g],
|
||||
['end_with', /(?<=\*).+/g],
|
||||
['start_with', /.+(?=\*)/g],
|
||||
]
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const reg = resList[i]
|
||||
if (reg[1].test(val)) {
|
||||
exp = has_not + reg[0]
|
||||
value = val.match(reg[1])[0]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
id: uuidv4(),
|
||||
type,
|
||||
property,
|
||||
exp,
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
compareType,
|
||||
}
|
||||
})
|
||||
this.ruleList = [...expArray]
|
||||
} else if (open) {
|
||||
const _canSearchPreferenceAttrList = this.canSearchPreferenceAttrList.filter((attr) => !attr.is_password)
|
||||
this.ruleList = isInitOne
|
||||
? [
|
||||
{
|
||||
id: uuidv4(),
|
||||
type: 'and',
|
||||
property:
|
||||
_canSearchPreferenceAttrList && _canSearchPreferenceAttrList.length
|
||||
? _canSearchPreferenceAttrList[0].name
|
||||
: undefined,
|
||||
exp: 'is',
|
||||
value: null,
|
||||
},
|
||||
]
|
||||
: []
|
||||
}
|
||||
},
|
||||
|
||||
handleSubmit() {
|
||||
if (this.ruleList && this.ruleList.length) {
|
||||
this.ruleList[0].type = 'and' // 增删后,以防万一第一个不是and
|
||||
this.filterExp = ''
|
||||
const expList = this.ruleList.filter((rule) => rule?.property).map((rule) => {
|
||||
let singleRuleExp = ''
|
||||
let _exp = rule.exp
|
||||
if (rule.type === 'or') {
|
||||
singleRuleExp += '-'
|
||||
}
|
||||
if (rule.exp.includes('~')) {
|
||||
singleRuleExp += '~'
|
||||
_exp = rule.exp.split('~')[1]
|
||||
}
|
||||
singleRuleExp += `${rule.property}:`
|
||||
if (_exp === 'is') {
|
||||
singleRuleExp += `${rule.value ?? ''}`
|
||||
}
|
||||
if (_exp === 'contain') {
|
||||
singleRuleExp += `*${rule.value ?? ''}*`
|
||||
}
|
||||
if (_exp === 'start_with') {
|
||||
singleRuleExp += `${rule.value ?? ''}*`
|
||||
}
|
||||
if (_exp === 'end_with') {
|
||||
singleRuleExp += `*${rule.value ?? ''}`
|
||||
}
|
||||
if (_exp === 'value') {
|
||||
singleRuleExp += `*`
|
||||
}
|
||||
if (_exp === 'in') {
|
||||
singleRuleExp += `(${rule.value ?? ''})`
|
||||
}
|
||||
if (_exp === 'range') {
|
||||
singleRuleExp += `[${rule.min}_TO_${rule.max}]`
|
||||
}
|
||||
if (_exp === 'compare') {
|
||||
const idx = compareTypeList.findIndex((item) => item.value === rule.compareType)
|
||||
singleRuleExp += `${compareTypeList[idx].label}${rule.value ?? ''}`
|
||||
}
|
||||
return singleRuleExp
|
||||
})
|
||||
this.filterExp = expList.join(',')
|
||||
this.$emit('setExpFromFilter', this.filterExp)
|
||||
} else {
|
||||
this.$emit('setExpFromFilter', '')
|
||||
}
|
||||
this.visible = false
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
@@ -0,0 +1,255 @@
|
||||
<template>
|
||||
<div class="control-group">
|
||||
<CIReferenceAttr
|
||||
v-if="getAttr(rule.property).is_reference && (rule.exp === 'is' || rule.exp === '~is')"
|
||||
class="select-filter"
|
||||
:referenceTypeId="getAttr(rule.property).reference_type_id"
|
||||
:value="rule.value"
|
||||
:disabled="disabled"
|
||||
@change="(value) => handleChange('value', value)"
|
||||
/>
|
||||
<a-select
|
||||
v-else-if="getAttr(rule.property).is_bool && (rule.exp === 'is' || rule.exp === '~is')"
|
||||
class="select-filter"
|
||||
:disabled="disabled"
|
||||
:placeholder="$t('placeholder2')"
|
||||
:value="rule.value"
|
||||
@change="(value) => handleChange('value', value)"
|
||||
>
|
||||
<a-select-option key="1">
|
||||
true
|
||||
</a-select-option>
|
||||
<a-select-option key="0">
|
||||
false
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<div
|
||||
class="input-group"
|
||||
v-else-if="isChoiceByProperty(rule.property) && (rule.exp === 'is' || rule.exp === '~is')"
|
||||
>
|
||||
<a-select
|
||||
class="select-filter"
|
||||
:style="{ width: '175px' }"
|
||||
showSearch
|
||||
:placeholder="$t('placeholder2')"
|
||||
:disabled="disabled"
|
||||
:value="rule.value"
|
||||
@change="(value) => handleChange('value', value)"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="(node) in getChoiceValueByProperty(rule.property)"
|
||||
:key="String(node[0])"
|
||||
:title="node[1] ? node[1].label || node[0] : node[0]"
|
||||
>
|
||||
<a-tooltip placement="topLeft" :title="node[1] ? node[1].label || node[0] : node[0]" >
|
||||
{{ node[1] ? node[1].label || node[0] : node[0] }}
|
||||
</a-tooltip>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<!-- <treeselect
|
||||
class="custom-treeselect"
|
||||
:style="{ '--custom-height': rowHeight }"
|
||||
:value="rule.value"
|
||||
@input="(value) => handleChange('value', value)"
|
||||
:multiple="false"
|
||||
:clearable="false"
|
||||
searchable
|
||||
:options="getChoiceValueByProperty(rule.property)"
|
||||
:placeholder="$t('placeholder2')"
|
||||
:normalizer="
|
||||
(node) => {
|
||||
return {
|
||||
id: node[0],
|
||||
label: node[1] ? node[1].label || node[0] : node[0],
|
||||
children: node.children,
|
||||
}
|
||||
}
|
||||
"
|
||||
:zIndex="1050"
|
||||
:disabled="disabled"
|
||||
appendToBody
|
||||
>
|
||||
<div
|
||||
:title="node.label"
|
||||
slot="option-label"
|
||||
slot-scope="{ node }"
|
||||
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
|
||||
>
|
||||
{{ node.label }}
|
||||
</div>
|
||||
</treeselect> -->
|
||||
</div>
|
||||
<div
|
||||
compact
|
||||
v-else-if="rule.exp === 'range' || rule.exp === '~range'"
|
||||
class="input-group"
|
||||
>
|
||||
<a-input
|
||||
class="ops-input"
|
||||
:placeholder="$t('min')"
|
||||
:disabled="disabled"
|
||||
:value="rule.min"
|
||||
@change="(e) => handleChange('min', e.target.value)"
|
||||
/>
|
||||
<span class="input-group-range-icon">~</span>
|
||||
<a-input
|
||||
class="ops-input"
|
||||
v-model="rule.max"
|
||||
:placeholder="$t('max')"
|
||||
:disabled="disabled"
|
||||
:value="rule.max"
|
||||
@change="(e) => handleChange('max', e.target.value)"
|
||||
/>
|
||||
</div>
|
||||
<div class="input-group" compact v-else-if="rule.exp === 'compare'">
|
||||
<treeselect
|
||||
class="custom-treeselect"
|
||||
:style="{ width: '70px', '--custom-height': rowHeight, 'flex-shrink': 0 }"
|
||||
:value="rule.compareType"
|
||||
@input="(value) => handleChange('compareType', value)"
|
||||
:multiple="false"
|
||||
:clearable="false"
|
||||
searchable
|
||||
:options="compareTypeList"
|
||||
:normalizer="
|
||||
(node) => {
|
||||
return {
|
||||
id: node.value,
|
||||
label: node.label,
|
||||
children: node.children,
|
||||
}
|
||||
}
|
||||
"
|
||||
:zIndex="1050"
|
||||
:disabled="disabled"
|
||||
appendToBody
|
||||
>
|
||||
</treeselect>
|
||||
<a-input :value="rule.value" @change="(e) => handleChange('value', e.target.value)" class="ops-input"/>
|
||||
</div>
|
||||
<div class="input-group" v-else-if="rule.exp !== 'value' && rule.exp !== '~value'">
|
||||
<a-input
|
||||
:value="rule.value"
|
||||
@change="(e) => handleChange('value', e.target.value)"
|
||||
:placeholder="rule.exp === 'in' || rule.exp === '~in' ? $t('cmdbFilterComp.split', { separator: ';' }) : ''"
|
||||
class="ops-input"
|
||||
:disabled="disabled"
|
||||
></a-input>
|
||||
</div>
|
||||
<div v-else :style="{ width: '136px' }"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { compareTypeList } from './constants.js'
|
||||
import CIReferenceAttr from '@/components/ciReferenceAttr/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'ValueControls',
|
||||
components: {
|
||||
CIReferenceAttr,
|
||||
},
|
||||
props: {
|
||||
rule: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
attrList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 当前模型属性
|
||||
curModelAttrList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// 行高
|
||||
rowHeight: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
compareTypeList,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
choiceValue() {
|
||||
const val = /\{\{([^}]+)\}\}/g.exec(this?.rule?.value || '')
|
||||
return val ? val?.[1]?.trim() || '' : this?.value?.value
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isChoiceByProperty(property) {
|
||||
const _find = this.attrList.find((item) => item.name === property)
|
||||
if (_find) {
|
||||
return _find.is_choice
|
||||
}
|
||||
return false
|
||||
},
|
||||
getChoiceValueByProperty(property) {
|
||||
const _find = this.attrList.find((item) => item.name === property)
|
||||
if (_find) {
|
||||
return _find.choice_value
|
||||
}
|
||||
return []
|
||||
},
|
||||
handleChange(key, value) {
|
||||
this.$emit('change', {
|
||||
...this.rule,
|
||||
[key]: value
|
||||
})
|
||||
},
|
||||
getAttr(property) {
|
||||
return this.attrList.find((item) => item.name === property) || {}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.control-group {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 136px;
|
||||
|
||||
&-range-icon {
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
input {
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.select-filter {
|
||||
height: 36px;
|
||||
width: 136px;
|
||||
|
||||
/deep/ .ant-select-selection {
|
||||
height: 36px;
|
||||
background: #f7f8fa;
|
||||
line-height: 36px;
|
||||
border: none;
|
||||
|
||||
.ant-select-selection__rendered {
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
/deep/ .vue-treeselect__control {
|
||||
background: #f7f8fa;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -65,9 +65,7 @@
|
||||
<a><a-icon type="question-circle"/></a>
|
||||
</a-tooltip>
|
||||
</a-input>
|
||||
<a-tooltip :title="$t('reset')">
|
||||
<a-button @click="reset">{{ $t('reset') }}</a-button>
|
||||
</a-tooltip>
|
||||
<a-button @click="reset">{{ $t('reset') }}</a-button>
|
||||
<FilterComp
|
||||
ref="filterComp"
|
||||
:canSearchPreferenceAttrList="canSearchPreferenceAttrList"
|
||||
@@ -310,6 +308,11 @@ export default {
|
||||
height: 32px;
|
||||
.search-form-bar-filter {
|
||||
.ops_display_wrapper(transparent);
|
||||
|
||||
&:hover {
|
||||
color: @primary-color;
|
||||
}
|
||||
|
||||
.search-form-bar-filter-icon {
|
||||
color: @primary-color;
|
||||
font-size: 12px;
|
||||
|
@@ -62,7 +62,7 @@ const cmdb_en = {
|
||||
desc: 'Reverse order',
|
||||
uniqueKey: 'Unique Identifies',
|
||||
uniqueKeySelect: 'Please select a unique identifier',
|
||||
uniqueKeyTips: 'json/password/computed/choice can not be unique identifies',
|
||||
uniqueKeyTips: 'json/password/computed/selectList can not be unique identifies',
|
||||
notfound: 'Can\'t find what you want?',
|
||||
cannotDeleteGroupTips: 'There is data under this group and cannot be deleted!',
|
||||
confirmDeleteGroup: 'Are you sure you want to delete group [{groupName}]?',
|
||||
@@ -142,7 +142,7 @@ const cmdb_en = {
|
||||
selectCIType: 'Please select a CMDB CIType',
|
||||
selectCITypeAttributes: 'Please select CIType attributes',
|
||||
selectAttributes: 'Please select attributes',
|
||||
choiceScriptDemo: 'class ChoiceValue(object):\n @staticmethod\n def values():\n """\n Execution entry, returns predefined value\n :return: Returns a list, the type of the value is the same as the type of the attribute\n For example:\n return ["online", "offline"]\n """\n return []',
|
||||
choiceScriptDemo: 'class ChoiceValue(object):\n @staticmethod\n def values():\n """\n Execution entry, returns select list\n :return: Returns a list, the type of the value is the same as the type of the attribute\n For example:\n return ["online", "offline"]\n """\n return []',
|
||||
valueExisted: 'The current value already exists!',
|
||||
addRelation: 'Add Relation',
|
||||
sourceCIType: 'Source CIType',
|
||||
@@ -286,6 +286,7 @@ const cmdb_en = {
|
||||
attrCode: 'Attr Code',
|
||||
computedAttrTip1: 'Reference attributes follow jinja2 syntax',
|
||||
computedAttrTip2: `Multi-valued attributes (lists) are rendered with [ ] included by default, if you want to remove it, the reference method is: """{{ attr_name | join(',') }}""" where commas are separators`,
|
||||
computedAttrTip3: `Cannot refer to other computed attributes`,
|
||||
example: 'Example',
|
||||
attrFilterTip: `The third column of values allows you to select attributes of this model to cascade attributes`,
|
||||
rule: 'Rule',
|
||||
@@ -306,6 +307,19 @@ const cmdb_en = {
|
||||
departmentCascadeDisplay: 'Cascade Display',
|
||||
filterUsers: 'Filter Users',
|
||||
enum: 'Enum',
|
||||
ciGrantTip: `Filter conditions can be changed dynamically using {{}} referenced variables, currently user variables are supported, such as {{user.uid}},{{user.username}},{{user.email}},{{user.nickname}}`,
|
||||
searchInputTip: 'Please search for resource keywords',
|
||||
resourceSearch: 'Resource Search',
|
||||
recentSearch: 'Recent Search',
|
||||
myCollection: 'My Collection',
|
||||
keyword: 'Keywords',
|
||||
CIType: 'CIType',
|
||||
filterPopoverLabel: 'Filter',
|
||||
conditionFilter: 'Condition Filter',
|
||||
advancedFilter: 'Advanced Filter',
|
||||
saveCondition: 'Save Condition',
|
||||
confirmClear: 'Confirm to clear?',
|
||||
currentPage: 'Current Page'
|
||||
},
|
||||
components: {
|
||||
unselectAttributes: 'Unselected',
|
||||
@@ -391,7 +405,7 @@ const cmdb_en = {
|
||||
tips2: '1. Click to download the template, and users can customize the header of the template file, including model properties and model associations',
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
tips3: '2. The red color in the template file represents the model relationship, such as the $Product. Product Name (${Model Name}. {Attribute Name}) column, which establishes the relationship with the product.',
|
||||
tips4: '3. In the download template Excel file, the predefined values of attributes will be set as dropdown options. Please note that due to the limitations of Excel itself, a single dropdown box is limited to a maximum of 255 characters. If it exceeds 255 characters, we will not set the dropdown options for this attribute',
|
||||
tips4: `3. The download template excel file will have the property's drop-down list enumeration configured as a drop-down option. Please note that due to the limitations of Excel itself, a single dropdown box is limited to a maximum of 255 characters. If it exceeds 255 characters, we will not set the dropdown options for this attribute`,
|
||||
tips5: '4. When using Excel templates, please ensure that a single file does not exceed 5000 lines.',
|
||||
},
|
||||
preference: {
|
||||
@@ -628,6 +642,7 @@ if __name__ == "__main__":
|
||||
attributeDesc: 'Attribute Description',
|
||||
selectRows: 'Select: {rows} items',
|
||||
addRelation: 'Add Relation',
|
||||
viewRelation: 'View Relation',
|
||||
all: 'All',
|
||||
batchUpdate: 'Batch Update',
|
||||
batchUpdateConfirm: 'Are you sure you want to make batch updates?',
|
||||
@@ -650,7 +665,7 @@ if __name__ == "__main__":
|
||||
tips4: 'At least one field must be selected',
|
||||
tips5: 'Search name | alias',
|
||||
tips6: 'Speed up retrieval, full-text search possible, no need to use conditional filtering\n\n json/link/password currently does not support indexing \n\nText characters longer than 190 cannot be indexed',
|
||||
tips7: 'The form of expression is a drop-down box, and the value must be in the predefined value',
|
||||
tips7: 'Whether to configure a select list',
|
||||
tips8: 'Multiple values, such as intranet IP',
|
||||
tips9: 'For front-end only',
|
||||
tips10: 'Other attributes of the CIType are computed using expressions\n\nA code snippet computes the returned value.',
|
||||
@@ -726,5 +741,22 @@ if __name__ == "__main__":
|
||||
topoViewSearchPlaceholder: 'Please enter the node name.',
|
||||
moreBtn: 'Show more({count})'
|
||||
},
|
||||
relationSearch: {
|
||||
relationSearch: 'Relation Search',
|
||||
sourceCIType: 'Source CIType',
|
||||
sourceCITypeTip: 'Please input or select',
|
||||
sourceCITYpeInput: 'Please input keywords',
|
||||
targetCIType: 'Target CIType',
|
||||
targetCITypeTip: 'Please input or select, multiple choices available',
|
||||
pathSelect: 'Path Select',
|
||||
pathSelectTip: 'Please select source CIType and target CIType first',
|
||||
saveCondition: 'Save Condition',
|
||||
conditionFilter: 'Condition Filter',
|
||||
level: 'Level',
|
||||
returnPath: 'Return Path',
|
||||
conditionName: 'Condition Name',
|
||||
path: 'Path',
|
||||
expandCondition: 'Expand Condition',
|
||||
}
|
||||
}
|
||||
export default cmdb_en
|
||||
|
@@ -62,7 +62,7 @@ const cmdb_zh = {
|
||||
desc: '倒序',
|
||||
uniqueKey: '唯一标识',
|
||||
uniqueKeySelect: '请选择唯一标识',
|
||||
uniqueKeyTips: 'json、密码、计算属性、预定义值属性不能作为唯一标识',
|
||||
uniqueKeyTips: 'json、密码、计算属性、下拉列表属性不能作为唯一标识',
|
||||
notfound: '找不到想要的?',
|
||||
cannotDeleteGroupTips: '该分组下有数据, 不能删除!',
|
||||
confirmDeleteGroup: '确定要删除分组 【{groupName}】 吗?',
|
||||
@@ -142,7 +142,7 @@ const cmdb_zh = {
|
||||
selectCIType: '请选择CMDB模型',
|
||||
selectCITypeAttributes: '请选择模型属性',
|
||||
selectAttributes: '请选择属性',
|
||||
choiceScriptDemo: 'class ChoiceValue(object):\n @staticmethod\n def values():\n """\n 执行入口, 返回预定义值\n :return: 返回一个列表, 值的类型同属性的类型\n 例如:\n return ["在线", "下线"]\n """\n return []',
|
||||
choiceScriptDemo: 'class ChoiceValue(object):\n @staticmethod\n def values():\n """\n 执行入口, 返回下拉列表\n :return: 返回一个列表, 值的类型同属性的类型\n 例如:\n return ["在线", "下线"]\n """\n return []',
|
||||
valueExisted: '当前值已存在!',
|
||||
addRelation: '新增关系',
|
||||
sourceCIType: '源模型',
|
||||
@@ -286,6 +286,7 @@ const cmdb_zh = {
|
||||
attrCode: '属性代码',
|
||||
computedAttrTip1: '引用属性遵循jinja2语法',
|
||||
computedAttrTip2: `多值属性(列表)默认呈现包括[ ], 如果要去掉, 引用方法为: """{{ attr_name | join(',') }}""" 其中逗号为分隔符`,
|
||||
computedAttrTip3: `不能引用其他计算属性`,
|
||||
example: '例如',
|
||||
attrFilterTip: '第三列值可选择本模型的属性,来实现级联属性的功能',
|
||||
rule: '规则',
|
||||
@@ -306,6 +307,19 @@ const cmdb_zh = {
|
||||
departmentCascadeDisplay: '部门级联显示',
|
||||
filterUsers: '筛选用户',
|
||||
enum: '枚举',
|
||||
ciGrantTip: `筛选条件可使用{{}}引用变量实现动态变化,目前支持用户变量,如{{user.uid}},{{user.username}},{{user.email}},{{user.nickname}}`,
|
||||
searchInputTip: '请搜索资源关键字',
|
||||
resourceSearch: '资源搜索',
|
||||
recentSearch: '最近搜索',
|
||||
myCollection: '我的收藏',
|
||||
keyword: '关键字',
|
||||
CIType: '模型',
|
||||
filterPopoverLabel: '条件过滤',
|
||||
conditionFilter: '条件过滤',
|
||||
advancedFilter: '高级筛选',
|
||||
saveCondition: '保存条件',
|
||||
confirmClear: '确认清空?',
|
||||
currentPage: '当前页'
|
||||
},
|
||||
components: {
|
||||
unselectAttributes: '未选属性',
|
||||
@@ -390,7 +404,7 @@ const cmdb_zh = {
|
||||
tips2: '1. 点击下载模板,用户可以自定义模板文件的表头,包括模型属性、模型关联',
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
tips3: '2. 模板文件中红色为模型关系,如$产品.产品名(${模型名}.{属性名})这一列就可建立和产品之间的关系',
|
||||
tips4: '3. 下载模板excel文件中会将属性的预定义值置为下拉选项,请注意,受excel本身的限制,单个下拉框限制了最多255个字符,如果超过255个字符,我们不会设置该属性的下拉选项',
|
||||
tips4: '3. 下载模板excel文件中会将属性的下拉列表枚举配置置为下拉选项,请注意,受excel本身的限制,单个下拉框限制了最多255个字符,如果超过255个字符,我们不会设置该属性的下拉选项',
|
||||
tips5: '4. 在使用excel模板时,请确保单个文件不超过5000行',
|
||||
},
|
||||
preference: {
|
||||
@@ -627,6 +641,7 @@ if __name__ == "__main__":
|
||||
attributeDesc: '查看属性配置',
|
||||
selectRows: '选取:{rows} 项',
|
||||
addRelation: '添加关系',
|
||||
viewRelation: '查看关系',
|
||||
all: '全部',
|
||||
batchUpdate: '批量修改',
|
||||
batchUpdateConfirm: '确认要批量修改吗?',
|
||||
@@ -649,7 +664,7 @@ if __name__ == "__main__":
|
||||
tips4: '必须至少选择一个字段',
|
||||
tips5: '搜索 名称 | 别名',
|
||||
tips6: '加快检索, 可以全文搜索, 无需使用条件过滤\n\n json、链接、密码目前不支持建索引 \n\n文本字符长度超过190不能建索引',
|
||||
tips7: '表现形式是下拉框, 值必须在预定义值里',
|
||||
tips7: '是否配置下拉列表',
|
||||
tips8: '多值, 比如内网IP',
|
||||
tips9: '仅针对前端',
|
||||
tips10: '模型的其他属性通过表达式的方式计算出来\n\n一个代码片段计算返回的值',
|
||||
@@ -725,5 +740,22 @@ if __name__ == "__main__":
|
||||
topoViewSearchPlaceholder: '请输入节点名字',
|
||||
moreBtn: '展示更多({count})'
|
||||
},
|
||||
relationSearch: {
|
||||
relationSearch: '关系搜索',
|
||||
sourceCIType: '源模型',
|
||||
sourceCITypeTip: '请输入或选择',
|
||||
sourceCITYpeInput: '请输入关键词',
|
||||
targetCIType: '目标模型',
|
||||
targetCITypeTip: '请输入或选择,可多选',
|
||||
pathSelect: '路径选择',
|
||||
pathSelectTip: '请先选择源模型和目标模型',
|
||||
saveCondition: '保存条件',
|
||||
conditionFilter: '条件过滤',
|
||||
level: '层级',
|
||||
returnPath: '返回路径',
|
||||
conditionName: '条件命名',
|
||||
path: '路径',
|
||||
expandCondition: '展开条件',
|
||||
}
|
||||
}
|
||||
export default cmdb_zh
|
||||
|
@@ -53,8 +53,9 @@ const genCmdbRoutes = async () => {
|
||||
{
|
||||
path: '/cmdb/resourcesearch',
|
||||
name: 'cmdb_resource_search',
|
||||
hidden: true,
|
||||
meta: { title: 'cmdb.menu.ciSearch', icon: 'ops-cmdb-search', selectedIcon: 'ops-cmdb-search', keepAlive: false },
|
||||
component: () => import('../views/resource_search/index.vue')
|
||||
component: () => import('../views/resource_search_2/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/cmdb/adc',
|
||||
@@ -117,11 +118,10 @@ const genCmdbRoutes = async () => {
|
||||
meta: { title: 'cmdb.menu.serviceTreeDefine', keepAlive: false, icon: 'ops-cmdb-preferencerelation', selectedIcon: 'ops-cmdb-preferencerelation-selected' }
|
||||
},
|
||||
{
|
||||
path: '/cmdb/modelrelation',
|
||||
name: 'model_relation',
|
||||
hideChildrenInMenu: true,
|
||||
component: () => import('../views/model_relation/index'),
|
||||
meta: { title: 'cmdb.menu.citypeRelation', keepAlive: false, icon: 'ops-cmdb-modelrelation', selectedIcon: 'ops-cmdb-modelrelation-selected' }
|
||||
path: '/cmdb/discovery',
|
||||
name: 'discovery',
|
||||
component: () => import('../views/discovery/index'),
|
||||
meta: { title: 'cmdb.menu.ad', keepAlive: false, icon: 'ops-cmdb-adr', selectedIcon: 'ops-cmdb-adr-selected' }
|
||||
},
|
||||
{
|
||||
path: '/cmdb/operationhistory',
|
||||
@@ -130,19 +130,20 @@ const genCmdbRoutes = async () => {
|
||||
component: () => import('../views/operation_history/index'),
|
||||
meta: { title: 'cmdb.menu.operationHistory', keepAlive: false, icon: 'ops-cmdb-operation', selectedIcon: 'ops-cmdb-operation-selected' }
|
||||
},
|
||||
{
|
||||
path: '/cmdb/modelrelation',
|
||||
name: 'model_relation',
|
||||
hideChildrenInMenu: true,
|
||||
component: () => import('../views/model_relation/index'),
|
||||
meta: { title: 'cmdb.menu.citypeRelation', keepAlive: false, icon: 'ops-cmdb-modelrelation', selectedIcon: 'ops-cmdb-modelrelation-selected' }
|
||||
},
|
||||
{
|
||||
path: '/cmdb/relationtype',
|
||||
name: 'relation_type',
|
||||
hideChildrenInMenu: true,
|
||||
component: () => import('../views/relation_type/index'),
|
||||
meta: { title: 'cmdb.menu.relationType', keepAlive: false, icon: 'ops-cmdb-relationtype', selectedIcon: 'ops-cmdb-relationtype-selected' }
|
||||
},
|
||||
{
|
||||
path: '/cmdb/discovery',
|
||||
name: 'discovery',
|
||||
component: () => import('../views/discovery/index'),
|
||||
meta: { title: 'cmdb.menu.ad', keepAlive: false, icon: 'ops-cmdb-adr', selectedIcon: 'ops-cmdb-adr-selected' }
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
@@ -11,7 +11,7 @@ export function sum(arr) {
|
||||
})
|
||||
}
|
||||
|
||||
const strLength = (fData) => {
|
||||
export const strLength = (fData) => {
|
||||
|
||||
if (!fData) {
|
||||
return 0
|
||||
|
@@ -128,8 +128,8 @@
|
||||
v-if="choice[1] && choice[1].icon && choice[1].icon.name"
|
||||
:type="choice[1].icon.name"
|
||||
/>
|
||||
<a-tooltip placement="topLeft" :title="choice[1].label || choice[0]" >
|
||||
{{ choice[1].label || choice[0] }}
|
||||
<a-tooltip placement="topLeft" :title="choice[1] ? choice[1].label || choice[0] : choice[0]" >
|
||||
{{ choice[1] ? choice[1].label || choice[0] : choice[0] }}
|
||||
</a-tooltip>
|
||||
</span>
|
||||
</a-select-option>
|
||||
|
@@ -134,7 +134,7 @@
|
||||
:type="choice[1].icon.name"
|
||||
/>
|
||||
</template>
|
||||
{{ choice[1].label || choice[0] }}
|
||||
{{ choice[1] ? choice[1].label || choice[0] : choice[0] }}
|
||||
</span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
|
@@ -72,8 +72,8 @@
|
||||
:type="choice[1].icon.name"
|
||||
/>
|
||||
</template>
|
||||
<a-tooltip placement="topLeft" :title="choice[1].label || choice[0]">
|
||||
{{ choice[1].label || choice[0] }}
|
||||
<a-tooltip placement="topLeft" :title="choice[1] ? choice[1].label || choice[0] : choice[0]">
|
||||
{{ choice[1] ? choice[1].label || choice[0] : choice[0] }}
|
||||
</a-tooltip>
|
||||
</span>
|
||||
</a-select-option>
|
||||
@@ -148,7 +148,6 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import moment from 'moment'
|
||||
import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue'
|
||||
import CIReferenceAttr from '@/components/ciReferenceAttr/index.vue'
|
||||
|
@@ -92,6 +92,10 @@ export default {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
|
||||
&:hover {
|
||||
color: @primary-color;
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
border-left: solid 1px @border-color-base;
|
||||
}
|
||||
@@ -99,6 +103,10 @@ export default {
|
||||
&_active {
|
||||
background-color: @primary-color;
|
||||
color: #FFFFFF;
|
||||
|
||||
&:hover {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -187,7 +187,6 @@
|
||||
<a-icon
|
||||
style="position:absolute;top:2px;left:-17px;color:#2f54eb;"
|
||||
type="question-circle"
|
||||
theme="filled"
|
||||
@click="
|
||||
(e) => {
|
||||
e.stopPropagation()
|
||||
@@ -399,6 +398,7 @@
|
||||
<div v-show="isShowComputedArea" class="computed-attr-tip">
|
||||
<div>1. {{ $t('cmdb.ciType.computedAttrTip1') }}</div>
|
||||
<div>2. {{ $t('cmdb.ciType.computedAttrTip2') }}</div>
|
||||
<div>3. {{ $t('cmdb.ciType.computedAttrTip3') }}</div>
|
||||
</div>
|
||||
<ComputedArea
|
||||
showCalcComputed
|
||||
|
@@ -178,7 +178,6 @@
|
||||
<a-icon
|
||||
style="position:absolute;top:2px;left:-17px;color:#2f54eb;"
|
||||
type="question-circle"
|
||||
theme="filled"
|
||||
@click="
|
||||
(e) => {
|
||||
e.stopPropagation()
|
||||
@@ -372,7 +371,6 @@
|
||||
<a-icon
|
||||
style="position:absolute;top:3px;left:-17px;color:#A5A9BC;"
|
||||
type="info-circle"
|
||||
theme="filled"
|
||||
@click="
|
||||
(e) => {
|
||||
e.stopPropagation()
|
||||
@@ -393,6 +391,7 @@
|
||||
<div v-show="isShowComputedArea" class="computed-attr-tip">
|
||||
<div>1. {{ $t('cmdb.ciType.computedAttrTip1') }}</div>
|
||||
<div>2. {{ $t('cmdb.ciType.computedAttrTip2') }}</div>
|
||||
<div>3. {{ $t('cmdb.ciType.computedAttrTip3') }}</div>
|
||||
</div>
|
||||
<ComputedArea ref="computedArea" v-if="isShowComputedArea" :canDefineComputed="canDefineComputed" />
|
||||
</a-form-item>
|
||||
|
@@ -107,4 +107,12 @@ export default {
|
||||
.grant-config-wrap {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.ops-tab.ant-tabs {
|
||||
/deep/ .ant-tabs-bar {
|
||||
.ant-tabs-tab:hover {
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -13,6 +13,7 @@
|
||||
:paneLengthPixel.sync="paneLengthPixel"
|
||||
appName="cmdb-ci-types"
|
||||
:triggerLength="18"
|
||||
calcBasedParent
|
||||
>
|
||||
<template #one>
|
||||
<div class="ci-types-left">
|
||||
@@ -1112,7 +1113,7 @@ export default {
|
||||
}
|
||||
|
||||
.ci-types-left-content {
|
||||
max-height: calc(100% - 45px);
|
||||
height: calc(100% - 45px);
|
||||
overflow: hidden;
|
||||
margin-top: 10px;
|
||||
|
||||
|
@@ -103,7 +103,7 @@ export default {
|
||||
await this.handleLinkAttrToCiType({ attr_id: this.targetKeys.map((i) => Number(i)) })
|
||||
if (this.currentGroup) {
|
||||
await this.updateCurrentGroup()
|
||||
const { id, name, order, attributes } = this.currentGroup
|
||||
const { name, order, attributes } = this.currentGroup
|
||||
const attrIds = attributes.filter((i) => !i.inherited).map((i) => i.id)
|
||||
this.targetKeys.forEach((key) => {
|
||||
attrIds.push(Number(key))
|
||||
@@ -141,7 +141,7 @@ export default {
|
||||
})
|
||||
if (this.currentGroup) {
|
||||
await this.updateCurrentGroup()
|
||||
const { id, name, order, attributes } = this.currentGroup
|
||||
const { name, order, attributes } = this.currentGroup
|
||||
const attrIds = attributes.filter((i) => !i.inherited).map((i) => i.id)
|
||||
attrIds.push(newAttrId)
|
||||
await createCITypeGroupById(this.CITypeId, { name, order, attributes: attrIds })
|
||||
|
@@ -198,6 +198,7 @@
|
||||
<div class="script-tip">
|
||||
<div>1. {{ $t('cmdb.ciType.computedAttrTip1') }}</div>
|
||||
<div>2. {{ $t('cmdb.ciType.computedAttrTip2') }}</div>
|
||||
<div>3. {{ $t('cmdb.ciType.computedAttrTip3') }}</div>
|
||||
</div>
|
||||
|
||||
<div class="all-attr-btn">
|
||||
@@ -450,8 +451,9 @@ export default {
|
||||
return [
|
||||
item[0],
|
||||
{
|
||||
...item[1],
|
||||
label: item?.[1]?.['label'] || item[0]
|
||||
icon: item?.[1]?.['icon'] || {},
|
||||
style: item?.[1]?.['style'] || {},
|
||||
label: item?.[1]?.['label'] || item?.[0] || ''
|
||||
}
|
||||
]
|
||||
})
|
||||
|
@@ -121,7 +121,6 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { ColorPicker } from 'element-ui'
|
||||
import CustomIconSelect from '@/components/CustomIconSelect'
|
||||
import { defautValueColor, defaultBGColors } from '@/modules/cmdb/utils/const.js'
|
||||
|
@@ -1,14 +1,16 @@
|
||||
<template>
|
||||
<div class="relation-table" :style="{ padding: '0 20px 20px' }">
|
||||
<a-button
|
||||
v-if="!isInGrantComp"
|
||||
style="margin-bottom: 10px"
|
||||
@click="handleCreate"
|
||||
type="primary"
|
||||
size="small"
|
||||
icon="plus"
|
||||
>{{ $t('cmdb.ciType.addRelation') }}</a-button
|
||||
>
|
||||
<div v-if="!isInGrantComp" class="relation-table-add">
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="handleCreate"
|
||||
ghost
|
||||
class="ops-button-ghost"
|
||||
>
|
||||
<ops-icon type="veops-increase" />
|
||||
{{ $t('create') }}
|
||||
</a-button>
|
||||
</div>
|
||||
<vxe-table
|
||||
ref="xTable"
|
||||
stripe
|
||||
@@ -653,6 +655,12 @@ export default {
|
||||
/deep/ .vxe-cell {
|
||||
max-height: max-content !important;
|
||||
}
|
||||
|
||||
&-add {
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
.table-attribute-row {
|
||||
display: inline-flex;
|
||||
|
@@ -1,13 +1,15 @@
|
||||
<template>
|
||||
<div class="ci-types-triggers">
|
||||
<div style="margin-bottom: 10px">
|
||||
<div class="ci-types-triggers-add">
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="handleAddTrigger"
|
||||
size="small"
|
||||
icon="plus"
|
||||
>{{ $t('cmdb.ciType.newTrigger') }}</a-button
|
||||
ghost
|
||||
class="ops-button-ghost"
|
||||
>
|
||||
<ops-icon type="veops-increase" />
|
||||
{{ $t('create') }}
|
||||
</a-button>
|
||||
</div>
|
||||
<ops-table
|
||||
stripe
|
||||
@@ -134,5 +136,11 @@ export default {
|
||||
<style lang="less" scoped>
|
||||
.ci-types-triggers {
|
||||
padding: 0 20px 20px;
|
||||
|
||||
&-add {
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -407,7 +407,7 @@ export default {
|
||||
this.visible = true
|
||||
this.type = type
|
||||
this.item = item
|
||||
const { category = 0, name, type_id, attr_id, level } = item
|
||||
const { category = 0, name, type_id, level } = item
|
||||
const chartType = (item.options || {}).chartType || 'count'
|
||||
const fontColor = (item.options || {}).fontColor || '#ffffff'
|
||||
const bgColor = (item.options || {}).bgColor || ['#6ABFFE', '#5375EB']
|
||||
|
@@ -344,6 +344,10 @@ export default {
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
color: @primary-color;
|
||||
}
|
||||
|
||||
&_active {
|
||||
background-color: @primary-color_3;
|
||||
color: @primary-color;
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<TwoColumnLayout appName="cmdb-adc">
|
||||
<TwoColumnLayout appName="cmdb-adc" calcBasedParent>
|
||||
<template #one>
|
||||
<div class="cmdb-adc-group" v-for="group in ci_types_list" :key="group.id">
|
||||
<p>
|
||||
|
@@ -131,7 +131,7 @@ 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'
|
||||
import { createRelation, deleteRelation, getCITypeChildren, getRelationTypes } from '@/modules/cmdb/api/CITypeRelation'
|
||||
import { createRelation, deleteRelation, getRelationTypes } from '@/modules/cmdb/api/CITypeRelation'
|
||||
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
|
@@ -45,6 +45,7 @@
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="type_id" width="100px" :title="$t('cmdb.ciType.ciType')"></vxe-column>
|
||||
<vxe-column field="show_attr_value" width="100px" :title="$t('cmdb.ci.instance')"></vxe-column>
|
||||
<vxe-column field="operate_type" width="89px" :title="$t('operation')">
|
||||
<template #header="{ column }">
|
||||
<span>{{ column.title }}</span>
|
||||
@@ -314,7 +315,7 @@ export default {
|
||||
}
|
||||
},
|
||||
mergeRowMethod({ row, _rowIndex, column, visibleData }) {
|
||||
const fields = ['created_at', 'user', 'type_id']
|
||||
const fields = ['created_at', 'user', 'type_id', 'show_attr_value']
|
||||
const cellValue = row[column.property]
|
||||
const created_at = row['created_at']
|
||||
if (column.property === 'created_at') {
|
||||
@@ -365,6 +366,22 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (column.property === 'show_attr_value') {
|
||||
if (cellValue && fields.includes(column.property)) {
|
||||
const prevRow = visibleData[_rowIndex - 1]
|
||||
let nextRow = visibleData[_rowIndex + 1]
|
||||
if (prevRow && prevRow[column.property] === cellValue && prevRow['created_at'] === created_at) {
|
||||
return { rowspan: 0, colspan: 0 }
|
||||
} else {
|
||||
let countRowspan = 1
|
||||
while (nextRow && nextRow[column.property] === cellValue && nextRow['created_at'] === created_at) {
|
||||
nextRow = visibleData[++countRowspan + _rowIndex]
|
||||
}
|
||||
if (countRowspan > 1) {
|
||||
return { rowspan: countRowspan, colspan: 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
filterUser() {
|
||||
|
@@ -85,7 +85,7 @@
|
||||
@change="onChange"
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
:placeholder="[$t('cmdb.history.startTime'), $t('cmdb.history.endTime')]"
|
||||
v-else-if="attr.value_type === '3'"
|
||||
v-else-if="item.value_type === '3'"
|
||||
:show-time="{
|
||||
hideDisabledOptions: true,
|
||||
defaultValue: [moment('00:00:00', 'HH:mm:ss'), moment('23:59:59', 'HH:mm:ss')],
|
||||
|
@@ -7,6 +7,7 @@
|
||||
:paneLengthPixel.sync="paneLengthPixel"
|
||||
:appName="`cmdb-relation-views-${viewId}`"
|
||||
:triggerLength="18"
|
||||
calcBasedParent
|
||||
>
|
||||
<template #one>
|
||||
<div class="relation-views-left" :style="{ height: `${windowHeight - 64}px` }">
|
||||
@@ -588,7 +589,7 @@ export default {
|
||||
this.calcColumns()
|
||||
}
|
||||
if (refreshType === 'refreshNumber') {
|
||||
const promises = this.treeKeys.map((key, index) => {
|
||||
this.treeKeys.map((key, index) => {
|
||||
let ancestor_ids
|
||||
if (
|
||||
Object.keys(this.level2constraint).some(
|
||||
@@ -1432,7 +1433,7 @@ export default {
|
||||
content: (h) => <div>{that.$t('confirmDelete')}</div>,
|
||||
async onOk() {
|
||||
for (let i = 0; i < that.batchTreeKey.length; i++) {
|
||||
const { splitTreeKey, firstCIObj, firstCIId, _tempTree, ancestor_ids } = that.calculateParamsFromTreeKey(
|
||||
const { splitTreeKey, _tempTree, ancestor_ids } = that.calculateParamsFromTreeKey(
|
||||
that.batchTreeKey[i],
|
||||
'delete'
|
||||
)
|
||||
|
@@ -1,105 +1,105 @@
|
||||
<template>
|
||||
<a-modal
|
||||
width="600px"
|
||||
:bodyStyle="{
|
||||
paddingTop: 0,
|
||||
}"
|
||||
:visible="visible"
|
||||
:footer="null"
|
||||
@cancel="handleCancel"
|
||||
:title="$t('view')"
|
||||
>
|
||||
<div>
|
||||
<template v-if="readCIIdFilterPermissions && readCIIdFilterPermissions.length">
|
||||
<p>
|
||||
<strong>{{ $t('cmdb.serviceTree.idAuthorizationPolicy') }}</strong>
|
||||
<a
|
||||
@click="
|
||||
() => {
|
||||
showAllReadCIIdFilterPermissions = !showAllReadCIIdFilterPermissions
|
||||
}
|
||||
"
|
||||
v-if="readCIIdFilterPermissions.length > 10"
|
||||
><a-icon
|
||||
:type="showAllReadCIIdFilterPermissions ? 'caret-down' : 'caret-up'"
|
||||
/></a>
|
||||
</p>
|
||||
<a-tag
|
||||
v-for="item in showAllReadCIIdFilterPermissions
|
||||
? readCIIdFilterPermissions
|
||||
: readCIIdFilterPermissions.slice(0, 10)"
|
||||
:key="item.name"
|
||||
color="blue"
|
||||
:style="{ marginBottom: '5px' }"
|
||||
>{{ item.name }}</a-tag
|
||||
>
|
||||
<a-tag
|
||||
:style="{ marginBottom: '5px' }"
|
||||
v-if="readCIIdFilterPermissions.length > 10 && !showAllReadCIIdFilterPermissions"
|
||||
>+{{ readCIIdFilterPermissions.length - 10 }}</a-tag
|
||||
>
|
||||
</template>
|
||||
<a-empty v-else>
|
||||
<img slot="image" :src="require('@/assets/data_empty.png')" />
|
||||
<span slot="description"> {{ $t('noData') }} </span>
|
||||
</a-empty>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ciTypeFilterPermissions, getCIType } from '../../../api/CIType'
|
||||
import FilterComp from '@/components/CMDBFilterComp'
|
||||
import { searchRole } from '@/modules/acl/api/role'
|
||||
|
||||
export default {
|
||||
name: 'ReadPermissionsModal',
|
||||
components: { FilterComp },
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
filerPerimissions: {},
|
||||
readCIIdFilterPermissions: [],
|
||||
canSearchPreferenceAttrList: [],
|
||||
showAllReadCIIdFilterPermissions: false,
|
||||
allRoles: [],
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadRoles()
|
||||
},
|
||||
methods: {
|
||||
async loadRoles() {
|
||||
const res = await searchRole({ page_size: 9999, app_id: 'cmdb', is_all: true })
|
||||
this.allRoles = res.roles
|
||||
},
|
||||
async open(treeKey) {
|
||||
this.visible = true
|
||||
const _splitTreeKey = treeKey.split('@^@').filter((item) => !!item)
|
||||
const _treeKey = _splitTreeKey.slice(_splitTreeKey.length - 1, _splitTreeKey.length)[0].split('%')
|
||||
|
||||
const typeId = _treeKey[1]
|
||||
const _treeKeyPath = _splitTreeKey.map((item) => item.split('%')[0]).join(',')
|
||||
await ciTypeFilterPermissions(typeId).then((res) => {
|
||||
this.filerPerimissions = res
|
||||
})
|
||||
const readCIIdFilterPermissions = []
|
||||
Object.entries(this.filerPerimissions).forEach(([k, v]) => {
|
||||
const { id_filter } = v
|
||||
if (id_filter && Object.keys(id_filter).includes(_treeKeyPath)) {
|
||||
const _find = this.allRoles.find((item) => item.id === Number(k))
|
||||
readCIIdFilterPermissions.push({ name: _find?.name ?? k, rid: k })
|
||||
}
|
||||
})
|
||||
this.readCIIdFilterPermissions = readCIIdFilterPermissions
|
||||
console.log(readCIIdFilterPermissions)
|
||||
},
|
||||
handleCancel() {
|
||||
this.showAllReadCIIdFilterPermissions = false
|
||||
this.visible = false
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
<template>
|
||||
<a-modal
|
||||
width="600px"
|
||||
:bodyStyle="{
|
||||
paddingTop: 0,
|
||||
}"
|
||||
:visible="visible"
|
||||
:footer="null"
|
||||
@cancel="handleCancel"
|
||||
:title="$t('view')"
|
||||
>
|
||||
<div>
|
||||
<template v-if="readCIIdFilterPermissions && readCIIdFilterPermissions.length">
|
||||
<p>
|
||||
<strong>{{ $t('cmdb.serviceTree.idAuthorizationPolicy') }}</strong>
|
||||
<a
|
||||
@click="
|
||||
() => {
|
||||
showAllReadCIIdFilterPermissions = !showAllReadCIIdFilterPermissions
|
||||
}
|
||||
"
|
||||
v-if="readCIIdFilterPermissions.length > 10"
|
||||
><a-icon
|
||||
:type="showAllReadCIIdFilterPermissions ? 'caret-down' : 'caret-up'"
|
||||
/></a>
|
||||
</p>
|
||||
<a-tag
|
||||
v-for="item in showAllReadCIIdFilterPermissions
|
||||
? readCIIdFilterPermissions
|
||||
: readCIIdFilterPermissions.slice(0, 10)"
|
||||
:key="item.name"
|
||||
color="blue"
|
||||
:style="{ marginBottom: '5px' }"
|
||||
>{{ item.name }}</a-tag
|
||||
>
|
||||
<a-tag
|
||||
:style="{ marginBottom: '5px' }"
|
||||
v-if="readCIIdFilterPermissions.length > 10 && !showAllReadCIIdFilterPermissions"
|
||||
>+{{ readCIIdFilterPermissions.length - 10 }}</a-tag
|
||||
>
|
||||
</template>
|
||||
<a-empty v-else>
|
||||
<img slot="image" :src="require('@/assets/data_empty.png')" />
|
||||
<span slot="description"> {{ $t('noData') }} </span>
|
||||
</a-empty>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ciTypeFilterPermissions } from '../../../api/CIType'
|
||||
import FilterComp from '@/components/CMDBFilterComp'
|
||||
import { searchRole } from '@/modules/acl/api/role'
|
||||
|
||||
export default {
|
||||
name: 'ReadPermissionsModal',
|
||||
components: { FilterComp },
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
filerPerimissions: {},
|
||||
readCIIdFilterPermissions: [],
|
||||
canSearchPreferenceAttrList: [],
|
||||
showAllReadCIIdFilterPermissions: false,
|
||||
allRoles: [],
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadRoles()
|
||||
},
|
||||
methods: {
|
||||
async loadRoles() {
|
||||
const res = await searchRole({ page_size: 9999, app_id: 'cmdb', is_all: true })
|
||||
this.allRoles = res.roles
|
||||
},
|
||||
async open(treeKey) {
|
||||
this.visible = true
|
||||
const _splitTreeKey = treeKey.split('@^@').filter((item) => !!item)
|
||||
const _treeKey = _splitTreeKey.slice(_splitTreeKey.length - 1, _splitTreeKey.length)[0].split('%')
|
||||
|
||||
const typeId = _treeKey[1]
|
||||
const _treeKeyPath = _splitTreeKey.map((item) => item.split('%')[0]).join(',')
|
||||
await ciTypeFilterPermissions(typeId).then((res) => {
|
||||
this.filerPerimissions = res
|
||||
})
|
||||
const readCIIdFilterPermissions = []
|
||||
Object.entries(this.filerPerimissions).forEach(([k, v]) => {
|
||||
const { id_filter } = v
|
||||
if (id_filter && Object.keys(id_filter).includes(_treeKeyPath)) {
|
||||
const _find = this.allRoles.find((item) => item.id === Number(k))
|
||||
readCIIdFilterPermissions.push({ name: _find?.name ?? k, rid: k })
|
||||
}
|
||||
})
|
||||
this.readCIIdFilterPermissions = readCIIdFilterPermissions
|
||||
console.log(readCIIdFilterPermissions)
|
||||
},
|
||||
handleCancel() {
|
||||
this.showAllReadCIIdFilterPermissions = false
|
||||
this.visible = false
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
@@ -599,7 +599,7 @@ export default {
|
||||
|
||||
preferenceAttrList.forEach((attr) => {
|
||||
if (Array.isArray(attr?.children) && attr?.children?.length) {
|
||||
attr.children = attr.filter((child) => {
|
||||
attr.children = attr.children.filter((child) => {
|
||||
return !child?.is_reference
|
||||
})
|
||||
}
|
||||
|
@@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<div :class="['attr-display', isEllipsis ? 'attr-display-ellipsis' : '']">
|
||||
<template v-if="attr.is_reference && ci[attr.name]" >
|
||||
<a
|
||||
v-for="(ciId) in (attr.is_list ? ci[attr.name] : [ci[attr.name]])"
|
||||
:key="ciId"
|
||||
:href="`/cmdb/cidetail/${attr.reference_type_id}/${ciId}`"
|
||||
target="_blank"
|
||||
>
|
||||
{{ getReferenceAttrValue(ciId) }}
|
||||
</a>
|
||||
</template>
|
||||
<span v-else-if="attr.value_type === '6' && ci[attr.name]">{{ JSON.stringify(ci[attr.name]) }}</span>
|
||||
<template v-else-if="attr.is_link && ci[attr.name]">
|
||||
<a
|
||||
v-for="(item, linkIndex) in (attr.is_list ? ci[attr.name] : [ci[attr.name]])"
|
||||
:key="linkIndex"
|
||||
:href="
|
||||
item.startsWith('http') || item.startsWith('https')
|
||||
? `${item}`
|
||||
: `http://${item}`
|
||||
"
|
||||
target="_blank"
|
||||
>
|
||||
{{ getChoiceValueLabel(item) || item }}
|
||||
</a>
|
||||
</template>
|
||||
<PasswordField
|
||||
v-else-if="attr.is_password && ci[attr.name]"
|
||||
:ci_id="ci._id"
|
||||
:attr_id="attr.id"
|
||||
></PasswordField>
|
||||
<template v-else-if="attr.is_choice">
|
||||
<span
|
||||
v-for="value in (attr.is_list ? ci[attr.name] : [ci[attr.name]])"
|
||||
:key="value"
|
||||
:style="{
|
||||
borderRadius: '4px',
|
||||
padding: '1px 5px',
|
||||
margin: '2px',
|
||||
...getChoiceValueStyle(value),
|
||||
}"
|
||||
>
|
||||
<ops-icon
|
||||
:style="{ color: getChoiceValueIcon(attr, value).color }"
|
||||
:type="getChoiceValueIcon(attr, value).name"
|
||||
/>
|
||||
<span
|
||||
v-html="markSearchValue(getChoiceValueLabel(value) || value)"
|
||||
></span>
|
||||
</span>
|
||||
</template>
|
||||
<span
|
||||
v-else
|
||||
v-html="markSearchValue((attr.is_list && Array.isArray(ci[attr.name])) ? ci[attr.name].join(',') : ci[attr.name])"
|
||||
></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PasswordField from '@/modules/cmdb/components/passwordField/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'AttrDisplay',
|
||||
components: {
|
||||
PasswordField
|
||||
},
|
||||
props: {
|
||||
attr: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
ci: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
isEllipsis: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
referenceShowAttrNameMap: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
referenceCIIdMap: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
searchValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
markSearchValue(text) {
|
||||
if (!text || !this.searchValue) {
|
||||
return text
|
||||
}
|
||||
const regex = new RegExp(`(${this.searchValue})`, 'gi')
|
||||
return String(text).replace(
|
||||
regex,
|
||||
`<span style="background-color: #D3EEFE; padding: 0 2px;">$1</span>`
|
||||
)
|
||||
},
|
||||
getChoiceValueStyle(attrValue) {
|
||||
const _find = this?.attr?.choice_value?.find?.((item) => String(item?.[0]) === String(attrValue))
|
||||
if (_find) {
|
||||
return _find?.[1]?.style || {}
|
||||
}
|
||||
return {}
|
||||
},
|
||||
getChoiceValueIcon(attrValue) {
|
||||
const _find = this?.attr?.choice_value?.find((item) => String(item?.[0]) === String(attrValue))
|
||||
if (_find) {
|
||||
return _find?.[1]?.icon || {}
|
||||
}
|
||||
return {}
|
||||
},
|
||||
getChoiceValueLabel(attrValue) {
|
||||
const _find = this?.attr?.choice_value?.find((item) => String(item?.[0]) === String(attrValue))
|
||||
if (_find) {
|
||||
return _find?.[1]?.label || ''
|
||||
}
|
||||
return ''
|
||||
},
|
||||
getReferenceAttrValue(id) {
|
||||
if (this.attr.referenceShowAttrNameMap?.[id]) {
|
||||
return this.attr.referenceShowAttrNameMap[id]
|
||||
}
|
||||
|
||||
const ci = this?.referenceCIIdMap?.[this?.attr?.reference_type_id]?.[id]
|
||||
if (!ci) {
|
||||
return id
|
||||
}
|
||||
|
||||
const attrName = this.referenceShowAttrNameMap?.[this?.attr.reference_type_id]
|
||||
return ci?.[attrName] || id
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.attr-display {
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
word-break: break-all;
|
||||
|
||||
&-ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,235 @@
|
||||
<template>
|
||||
<a-popover
|
||||
v-model="visible"
|
||||
trigger="click"
|
||||
placement="bottom"
|
||||
@visibleChange="handleVisibleChange"
|
||||
>
|
||||
<div class="filter-btn">
|
||||
<a-icon class="filter-btn-icon" type="filter" />
|
||||
<span class="filter-btn-title">{{ $t('cmdb.ciType.advancedFilter') }}</span>
|
||||
</div>
|
||||
<template slot="content">
|
||||
<div class="filter-content">
|
||||
<a-form :form="form">
|
||||
<a-form-item
|
||||
:label="$t('cmdb.ciType.ciType')"
|
||||
:label-col="formLayout.labelCol"
|
||||
:wrapper-col="formLayout.wrapperCol"
|
||||
>
|
||||
<treeselect
|
||||
:value="selectCITypeIds"
|
||||
class="custom-treeselect custom-treeselect-bgcAndBorder filter-content-ciTypes"
|
||||
:style="{
|
||||
width: '400px',
|
||||
zIndex: '1000',
|
||||
'--custom-height': '32px',
|
||||
'--custom-bg-color': '#FFF',
|
||||
'--custom-border': '1px solid #d9d9d9',
|
||||
'--custom-multiple-lineHeight': '32px',
|
||||
}"
|
||||
:multiple="true"
|
||||
:clearable="true"
|
||||
searchable
|
||||
:options="CITypeGroup"
|
||||
:limit="1"
|
||||
:limitText="(count) => `+ ${count}`"
|
||||
value-consists-of="LEAF_PRIORITY"
|
||||
:placeholder="$t('cmdb.ciType.ciType')"
|
||||
@close="closeCiTypeGroup"
|
||||
@open="openCiTypeGroup"
|
||||
@input="inputCiTypeGroup"
|
||||
:normalizer="
|
||||
(node) => {
|
||||
return {
|
||||
id: node.id || -1,
|
||||
label: node.alias || node.name || $t('other'),
|
||||
title: node.alias || node.name || $t('other'),
|
||||
children: node.ci_types,
|
||||
}
|
||||
}
|
||||
"
|
||||
>
|
||||
<div
|
||||
:title="node.label"
|
||||
slot="option-label"
|
||||
slot-scope="{ node }"
|
||||
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
|
||||
>
|
||||
{{ node.label }}
|
||||
</div>
|
||||
</treeselect>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
:label="$t('cmdb.ciType.filterPopoverLabel')"
|
||||
:label-col="formLayout.labelCol"
|
||||
:wrapper-col="formLayout.wrapperCol"
|
||||
class="filter-content-condition-filter"
|
||||
>
|
||||
<ConditionFilter
|
||||
ref="conditionFilterRef"
|
||||
:canSearchPreferenceAttrList="allAttributesList"
|
||||
:expression="expression"
|
||||
:CITypeIds="selectCITypeIds"
|
||||
:isDropdown="false"
|
||||
@setExpFromFilter="setExpFromFilter"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<div class="filter-content-action">
|
||||
<a-button
|
||||
size="small"
|
||||
@click="saveCondition(false)"
|
||||
>
|
||||
{{ $t('cmdb.ciType.saveCondition') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="saveCondition(true)"
|
||||
>
|
||||
{{ $t('confirm') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-popover>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import ConditionFilter from '@/modules/cmdb/components/conditionFilter/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'FilterPopover',
|
||||
components: {
|
||||
ConditionFilter
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
form: this.$form.createForm(this),
|
||||
formLayout: {
|
||||
labelCol: { span: 3 },
|
||||
wrapperCol: { span: 15 },
|
||||
},
|
||||
lastCiType: [],
|
||||
}
|
||||
},
|
||||
props: {
|
||||
expression: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
selectCITypeIds: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
CITypeGroup: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
allAttributesList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleVisibleChange(open) {
|
||||
if (open) {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.conditionFilterRef.init(true, false)
|
||||
})
|
||||
}
|
||||
},
|
||||
openCiTypeGroup() {
|
||||
this.lastCiType = _.cloneDeep(this.selectCITypeIds)
|
||||
},
|
||||
closeCiTypeGroup(value) {
|
||||
if (!_.isEqual(value, this.lastCiType)) {
|
||||
this.$emit('updateAllAttributesList', value)
|
||||
}
|
||||
},
|
||||
inputCiTypeGroup(value) {
|
||||
if (!value || !value.length) {
|
||||
this.$emit('updateAllAttributesList', value)
|
||||
}
|
||||
this.$emit('changeFilter', {
|
||||
name: 'selectCITypeIds',
|
||||
value
|
||||
})
|
||||
},
|
||||
setExpFromFilter(filterExp) {
|
||||
const regSort = /(?<=sort=).+/g
|
||||
const expSort = this.expression.match(regSort) ? this.expression.match(regSort)[0] : undefined
|
||||
let expression = ''
|
||||
if (filterExp) {
|
||||
expression = `q=${filterExp}`
|
||||
}
|
||||
if (expSort) {
|
||||
expression += `&sort=${expSort}`
|
||||
}
|
||||
this.$emit('changeFilter', {
|
||||
name: 'expression',
|
||||
value: expression
|
||||
})
|
||||
},
|
||||
|
||||
saveCondition(isSubmit) {
|
||||
this.$refs.conditionFilterRef.handleSubmit()
|
||||
this.$nextTick(() => {
|
||||
this.$emit('saveCondition', isSubmit)
|
||||
this.visible = false
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.filter-btn {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 13px;
|
||||
cursor: pointer;
|
||||
|
||||
&-icon {
|
||||
color: #2F54EB;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #2F54EB;
|
||||
margin-left: 3px;
|
||||
}
|
||||
}
|
||||
.filter-content {
|
||||
width: 600px;
|
||||
|
||||
&-ciTypes {
|
||||
/deep/ .vue-treeselect__value-container {
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
&-condition-filter {
|
||||
max-height: 250px;
|
||||
// overflow-y: auto;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
&-action {
|
||||
width: 100%;
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
column-gap: 21px;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,310 @@
|
||||
<template>
|
||||
<div class="history-list">
|
||||
<div
|
||||
class="history-recent"
|
||||
v-if="recentList.length"
|
||||
>
|
||||
<div class="history-title">
|
||||
<a-icon type="eye" class="history-title-icon" />
|
||||
<div class="history-title-text">{{ $t('cmdb.ciType.recentSearch') }}</div>
|
||||
|
||||
<a-popconfirm
|
||||
:title="$t('cmdb.ciType.confirmClear')"
|
||||
placement="topRight"
|
||||
@confirm="clearRecent"
|
||||
>
|
||||
<a-tooltip :title="$t('clear')" >
|
||||
<a-icon
|
||||
type="delete"
|
||||
class="history-title-clear"
|
||||
/>
|
||||
</a-tooltip>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
<div class="recent-list">
|
||||
<div
|
||||
v-for="(item) in recentList.slice(0, 10)"
|
||||
:key="item.id"
|
||||
class="recent-list-item"
|
||||
@click="clickRecent(item.option)"
|
||||
>
|
||||
<div class="recent-list-item-text">
|
||||
{{ getRecentSearchText(item.option) }}
|
||||
</div>
|
||||
<a-icon
|
||||
type="close"
|
||||
class="recent-list-item-close"
|
||||
@click.stop="deleteRecent(item.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="history-favor"
|
||||
v-if="favorList.length"
|
||||
>
|
||||
<div class="history-title">
|
||||
<ops-icon type="veops-collect" class="history-title-icon" />
|
||||
<div class="history-title-text">{{ $t('cmdb.ciType.myCollection') }}</div>
|
||||
<div class="history-title-count">({{ favorList.length }})</div>
|
||||
|
||||
<ops-icon
|
||||
type="veops-expand"
|
||||
class="history-title-expand"
|
||||
:style="{
|
||||
transform: `rotate(${isExpand ? '180deg' : '0deg'})`
|
||||
}"
|
||||
@click="isExpand = !isExpand"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="favor-list"
|
||||
:style="{ height: isExpand ? 'auto' : '30px' }"
|
||||
>
|
||||
<div
|
||||
v-for="(item) in favorList"
|
||||
:key="item.id"
|
||||
:class="['favor-list-item', detailCIId === item.option.CIId ? 'favor-list-item-selected' : '']"
|
||||
@click="showDetail(item.option)"
|
||||
>
|
||||
<CIIcon
|
||||
:icon="item.option.icon"
|
||||
:title="item.option.CITypeTitle"
|
||||
/>
|
||||
<div class="favor-list-item-title">
|
||||
{{ item.option.title }}
|
||||
</div>
|
||||
<ops-icon
|
||||
type="veops-collected"
|
||||
class="favor-list-item-collected"
|
||||
@click.stop="deleteCollect(item.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CIIcon from '@/modules/cmdb/components/ciIcon/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'HistoryList',
|
||||
components: {
|
||||
CIIcon
|
||||
},
|
||||
props: {
|
||||
recentList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
favorList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
detailCIId: {
|
||||
type: [String, Number],
|
||||
default: -1
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isExpand: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getRecentSearchText(option) {
|
||||
const textArray = []
|
||||
if (option.searchValue) {
|
||||
textArray.push(`${this.$t('cmdb.ciType.keyword')}: ${option.searchValue}`)
|
||||
}
|
||||
|
||||
if (option?.ciTypeNames?.length) {
|
||||
textArray.push(`${this.$t('cmdb.ciType.CIType')}: ${option.ciTypeNames.join(',')}`)
|
||||
}
|
||||
|
||||
if (option.expression) {
|
||||
textArray.push(`${this.$t('cmdb.ciType.conditionFilter')}: ${option.expression}`)
|
||||
}
|
||||
|
||||
return textArray.join('; ')
|
||||
},
|
||||
|
||||
clickRecent(data) {
|
||||
this.$emit('clickRecent', data)
|
||||
},
|
||||
|
||||
deleteRecent(id) {
|
||||
this.$emit('deleteRecent', id)
|
||||
},
|
||||
|
||||
deleteCollect(id) {
|
||||
this.$emit('deleteCollect', id)
|
||||
},
|
||||
|
||||
showDetail(data) {
|
||||
this.$emit('showDetail', {
|
||||
id: data.CIId,
|
||||
ciTypeId: data.CITypeId
|
||||
})
|
||||
},
|
||||
|
||||
clearRecent() {
|
||||
this.$emit('clearRecent')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.history-list {
|
||||
width: 100%;
|
||||
|
||||
.history-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&-icon {
|
||||
font-size: 12px;
|
||||
color: #2F54EB;
|
||||
}
|
||||
|
||||
&-text {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #4E5969;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
&-count {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #86909C;
|
||||
}
|
||||
|
||||
&-clear {
|
||||
margin-left: auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&-expand {
|
||||
margin-left: auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.history-recent {
|
||||
width: 100%;
|
||||
margin-top: 15px;
|
||||
|
||||
.recent-list {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
column-gap: 16px;
|
||||
row-gap: 8px;
|
||||
|
||||
&-item {
|
||||
flex-shrink: 0;
|
||||
padding: 4px 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 22px;
|
||||
background: rgba(255, 255, 255, 0.50);
|
||||
cursor: pointer;
|
||||
max-width: calc((100% - 16px) / 2);
|
||||
|
||||
&-text {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #1D2129;
|
||||
|
||||
max-width: 100%;
|
||||
text-wrap: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&-close {
|
||||
font-size: 12px;
|
||||
margin-left: 4px;
|
||||
color: #A5A9BC;
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.recent-list-item-text {
|
||||
color: #2F54EB;
|
||||
}
|
||||
|
||||
.recent-list-item-close {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.history-favor {
|
||||
width: 100%;
|
||||
margin-top: 15px;
|
||||
|
||||
.favor-list {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
column-gap: 16px;
|
||||
row-gap: 8px;
|
||||
overflow: hidden;
|
||||
min-height: 30px;
|
||||
|
||||
&-item {
|
||||
flex-shrink: 0;
|
||||
padding: 4px 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 22px;
|
||||
background: rgba(255, 255, 255, 0.90);
|
||||
cursor: pointer;
|
||||
max-width: calc((100% - 16px) / 2);
|
||||
|
||||
&-title {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
margin-left: 4px;
|
||||
color: #1D2129;
|
||||
|
||||
max-width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&-collected {
|
||||
font-size: 14px;
|
||||
margin-left: 4px;
|
||||
color: #FAD337;
|
||||
}
|
||||
|
||||
&-selected {
|
||||
border: 1px solid #7F97FA;
|
||||
background-color: rgba(255, 255, 255, 0.90);
|
||||
|
||||
.favor-list-item-title {
|
||||
color: #2F54EB;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.favor-list-item-title {
|
||||
color: #2F54EB;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,438 @@
|
||||
<template>
|
||||
<div class="instance-detail">
|
||||
<div
|
||||
class="instance-detail-hide"
|
||||
@click="hideDetail"
|
||||
>
|
||||
<a-icon class="instance-detail-hide-icon" type="right" />
|
||||
</div>
|
||||
|
||||
<div class="instance-detail-null" v-if="!ci._id" >
|
||||
<img
|
||||
:src="require('@/modules/cmdb/assets/no_permission.png')"
|
||||
class="instance-detail-null-img"
|
||||
/>
|
||||
<span class="instance-detail-null-text" >{{ $t('noData') }}</span>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div
|
||||
class="instance-detail-header"
|
||||
>
|
||||
<div class="instance-detail-header-line-1"></div>
|
||||
<div class="instance-detail-header-line-2"></div>
|
||||
<div class="instance-detail-header-row">
|
||||
<CIIcon
|
||||
:icon="ciType.icon"
|
||||
:title="ciType.name || ''"
|
||||
:size="20"
|
||||
/>
|
||||
<div class="instance-detail-header-title">
|
||||
{{ detailTitle }}
|
||||
</div>
|
||||
|
||||
<ops-icon
|
||||
:type="favorId ? 'veops-collected' : 'veops-collect'"
|
||||
:style="{ color: favorId ? '#FAD337' : '#A5A9BC' }"
|
||||
class="instance-detail-header-collect"
|
||||
@click="clickCollect"
|
||||
/>
|
||||
|
||||
<a class="instance-detail-header-share" @click="shareCi">
|
||||
<a-icon type="share-alt" />
|
||||
{{ $t('cmdb.ci.share') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="instance-detail-attr">
|
||||
<div
|
||||
v-for="(group) in attributeGroups"
|
||||
:key="group.id"
|
||||
class="instance-detail-attr-group"
|
||||
>
|
||||
<span class="instance-detail-attr-group-name">{{ group.name || $t('other') }}</span>
|
||||
|
||||
<div class="instance-detail-attr-list">
|
||||
<div
|
||||
v-for="(attr) in group.attributes"
|
||||
:key="attr.id"
|
||||
class="instance-detail-attr-item"
|
||||
>
|
||||
<a-tooltip :title="attr.alias || attr.name || ''">
|
||||
<div class="instance-detail-attr-item-label">
|
||||
<span class="instance-detail-attr-item-label-text">
|
||||
{{ attr.alias || attr.name || '' }}
|
||||
</span>
|
||||
<span class="instance-detail-attr-item-label-colon">:</span>
|
||||
</div>
|
||||
</a-tooltip>
|
||||
|
||||
<div class="instance-detail-attr-item-value">
|
||||
<AttrDisplay
|
||||
:attr="attr"
|
||||
:ci="ci"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { getCIById, searchCI } from '@/modules/cmdb/api/ci'
|
||||
import { getCITypeGroupById, getCITypes, getCIType } from '@/modules/cmdb/api/CIType'
|
||||
|
||||
import AttrDisplay from './attrDisplay.vue'
|
||||
import CIIcon from '@/modules/cmdb/components/ciIcon/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'InstanceDetail',
|
||||
components: {
|
||||
AttrDisplay,
|
||||
CIIcon
|
||||
},
|
||||
props: {
|
||||
CIId: {
|
||||
type: [String, Number],
|
||||
default: -1
|
||||
},
|
||||
CITypeId: {
|
||||
type: [String, Number],
|
||||
default: -1
|
||||
},
|
||||
favorList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
ci: {},
|
||||
ciType: {},
|
||||
attributeGroups: [],
|
||||
isNullData: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
watchParams() {
|
||||
return {
|
||||
CIId: this.CIId,
|
||||
CITypeId: this.CITypeId
|
||||
}
|
||||
},
|
||||
detailTitle() {
|
||||
const attrName = this?.ciType?.show_name || this?.ciType?.unique_name || ''
|
||||
return attrName ? (this?.ci?.[attrName] || '') : ''
|
||||
},
|
||||
favorId() {
|
||||
const id = this.favorList.find((item) => item?.option?.CIId === this.CIId)?.id
|
||||
return id ?? null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
watchParams: {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
handler(newVal) {
|
||||
if (newVal?.CIId !== -1 && newVal?.CITypeId !== -1) {
|
||||
this.initData()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async initData() {
|
||||
const ci = await this.getCI()
|
||||
if (!ci) {
|
||||
this.isNullData = true
|
||||
return
|
||||
}
|
||||
await this.getCIType()
|
||||
await this.getAttributes()
|
||||
},
|
||||
|
||||
async getCI() {
|
||||
const res = await getCIById(this.CIId)
|
||||
const ci = res.result?.[0] || {}
|
||||
this.ci = ci
|
||||
return ci
|
||||
},
|
||||
|
||||
async getCIType() {
|
||||
const res = await getCIType(this.CITypeId)
|
||||
this.ciType = res?.ci_types?.[0] || {}
|
||||
},
|
||||
|
||||
async getAttributes() {
|
||||
const res = await getCITypeGroupById(this.CITypeId, { need_other: 1 })
|
||||
this.attributeGroups = res
|
||||
this.handleReferenceAttr()
|
||||
},
|
||||
|
||||
async handleReferenceAttr() {
|
||||
const map = {}
|
||||
this.attributeGroups.forEach((group) => {
|
||||
group.attributes.forEach((attr) => {
|
||||
if (attr?.is_reference && attr?.reference_type_id && this.ci[attr.name]) {
|
||||
const ids = Array.isArray(this.ci[attr.name]) ? this.ci[attr.name] : this.ci[attr.name] ? [this.ci[attr.name]] : []
|
||||
if (ids.length) {
|
||||
if (!map?.[attr.reference_type_id]) {
|
||||
map[attr.reference_type_id] = {}
|
||||
}
|
||||
ids.forEach((id) => {
|
||||
map[attr.reference_type_id][id] = {}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (!Object.keys(map).length) {
|
||||
return
|
||||
}
|
||||
|
||||
const ciTypesRes = await getCITypes({
|
||||
type_ids: Object.keys(map).join(',')
|
||||
})
|
||||
const showAttrNameMap = {}
|
||||
ciTypesRes.ci_types.forEach((ciType) => {
|
||||
showAttrNameMap[ciType.id] = ciType?.show_name || ciType?.unique_name || ''
|
||||
})
|
||||
|
||||
const allRes = await Promise.all(
|
||||
Object.keys(map).map((key) => {
|
||||
return searchCI({
|
||||
q: `_type:${key},_id:(${Object.keys(map[key]).join(';')})`,
|
||||
count: 9999
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
const ciNameMap = {}
|
||||
allRes.forEach((res) => {
|
||||
res.result.forEach((item) => {
|
||||
ciNameMap[item._id] = item
|
||||
})
|
||||
})
|
||||
|
||||
const newAttrGroups = _.cloneDeep(this.attributeGroups)
|
||||
|
||||
newAttrGroups.forEach((group) => {
|
||||
group.attributes.forEach((attr) => {
|
||||
if (attr?.is_reference && attr?.reference_type_id) {
|
||||
attr.showAttrName = showAttrNameMap?.[attr?.reference_type_id] || ''
|
||||
|
||||
const referenceShowAttrNameMap = {}
|
||||
const referenceCIIds = this.ci[attr.name];
|
||||
(Array.isArray(referenceCIIds) ? referenceCIIds : referenceCIIds ? [referenceCIIds] : []).forEach((id) => {
|
||||
referenceShowAttrNameMap[id] = ciNameMap?.[id]?.[attr.showAttrName] ?? id
|
||||
})
|
||||
attr.referenceShowAttrNameMap = referenceShowAttrNameMap
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
this.$set(this, 'attributeGroups', newAttrGroups)
|
||||
},
|
||||
|
||||
shareCi() {
|
||||
const text = `${document.location.host}/cmdb/cidetail/${this.CITypeId}/${this.CIId}`
|
||||
this.$copyText(text)
|
||||
.then(() => {
|
||||
this.$message.success(this.$t('copySuccess'))
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.error(this.$t('cmdb.ci.copyFailed'))
|
||||
})
|
||||
},
|
||||
|
||||
clickCollect() {
|
||||
if (this.favorId) {
|
||||
this.$emit('deleteCollect', this.favorId)
|
||||
} else {
|
||||
this.$emit('addCollect', {
|
||||
CIId: this.CIId,
|
||||
CITypeId: this.CITypeId,
|
||||
title: this.detailTitle,
|
||||
icon: this.ciType?.icon,
|
||||
CITypeTitle: this.ciType?.name || ''
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
hideDetail() {
|
||||
this.$emit('hideDetail')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.instance-detail {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 2px;
|
||||
border: 1px solid #E4E7ED;
|
||||
background-color: #FFFFFF;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
|
||||
&-hide {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
margin-top: -21px;
|
||||
border-radius: 0px 2px 2px 0px;
|
||||
background-color: #2f54eb;
|
||||
width: 13px;
|
||||
height: 43px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
cursor: pointer;
|
||||
|
||||
&-icon {
|
||||
color: #FFFFFF;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #597ef7;
|
||||
}
|
||||
}
|
||||
|
||||
&-null {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
padding-top: 100px;
|
||||
|
||||
&-img {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
&-text {
|
||||
color: #86909C;
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&-header {
|
||||
width: 100%;
|
||||
height: 75px;
|
||||
background-color: #EBF0F9;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
flex-shrink: 0;
|
||||
|
||||
&-line-1 {
|
||||
height: 44px;
|
||||
width: 300px;
|
||||
position: absolute;
|
||||
right: -20px;
|
||||
top: 0px;
|
||||
transform: rotate(40deg);
|
||||
background: rgba(248, 249, 253, 0.60);
|
||||
}
|
||||
|
||||
&-line-2 {
|
||||
height: 44px;
|
||||
width: 300px;
|
||||
position: absolute;
|
||||
right: -110px;
|
||||
top: 0px;
|
||||
transform: rotate(40deg);
|
||||
background: rgba(248, 249, 253, 0.60);
|
||||
}
|
||||
|
||||
&-row {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #1D2129;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
margin-left: 9px;
|
||||
}
|
||||
|
||||
&-collect {
|
||||
margin-left: 8px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
&-share {
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-attr {
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
|
||||
&-group {
|
||||
&:not(:first-child) {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
&-name {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #1D2129;
|
||||
}
|
||||
}
|
||||
|
||||
&-item {
|
||||
margin-top: 15px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
&-label {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #86909C;
|
||||
width: 25%;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
|
||||
&-colon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-value {
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,443 @@
|
||||
<template>
|
||||
<div class="list-wrap">
|
||||
<div class="list-wrap-bg" v-if="!filterList.length">
|
||||
<img :src="require('@/modules/cmdb/assets/resourceSearch/resource_search_bg_2.png')" />
|
||||
</div>
|
||||
|
||||
<div v-if="tabList.length" class="list-tab">
|
||||
<div class="list-tab-left">
|
||||
<div class="list-tab-label">{{ $t('cmdb.ciType.currentPage') }}: </div>
|
||||
<div
|
||||
v-for="(tab) in tabList"
|
||||
:key="tab.id"
|
||||
:class="['list-tab-item', tab.id === currentTab ? 'list-tab-item-active' : '']"
|
||||
@click="clickTab(tab.id)"
|
||||
>
|
||||
<span class="list-tab-item-title">{{ tab.title }}</span>
|
||||
(<span class="list-tab-item-count">{{ tab.count }}</span>)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-button
|
||||
icon="download"
|
||||
type="primary"
|
||||
class="ops-button-ghost list-tab-export"
|
||||
ghost
|
||||
@click="handleExport"
|
||||
>
|
||||
{{ $t('download') }}
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<div v-if="filterList.length" class="list-container">
|
||||
<div
|
||||
v-for="(item) in filterList"
|
||||
:key="item._id"
|
||||
:class="['list-card', detailCIId === item.ci._id ? 'list-card-selected' : '']"
|
||||
@click="clickInstance(item.ci._id, item.ciTypeObj.id)"
|
||||
>
|
||||
<div class="list-card-header">
|
||||
<div class="list-card-model">
|
||||
<CIIcon
|
||||
:icon="item.ciTypeObj.icon"
|
||||
:title="item.ciTypeObj.name"
|
||||
/>
|
||||
<span class="list-card-model-title">{{ item.ciTypeObj.title }}</span>
|
||||
</div>
|
||||
<div class="list-card-title">{{ item.ci[item.ciTypeObj.showAttrName] }}</div>
|
||||
|
||||
<ops-icon
|
||||
v-if="getFavorId(item.ci._id)"
|
||||
type="veops-collected"
|
||||
class="list-card-collect"
|
||||
:style="{ color: '#FAD337' }"
|
||||
@click.stop="deleteCollect(item.ci._id)"
|
||||
/>
|
||||
|
||||
<ops-icon
|
||||
v-else
|
||||
type="veops-collect"
|
||||
class="list-card-collect"
|
||||
:style="{ color: '#A5A9BC' }"
|
||||
@click.stop="addCollect(item)"
|
||||
/>
|
||||
</div>
|
||||
<div class="list-card-attr">
|
||||
<div
|
||||
v-for="(attr) in item.attributes"
|
||||
:key="attr.name"
|
||||
class="list-card-attr-item"
|
||||
>
|
||||
<div class="list-card-attr-item-label">{{ attr.alias || attr.name || '' }}: </div>
|
||||
<div class="list-card-attr-item-value">
|
||||
<AttrDisplay
|
||||
:attr="attr"
|
||||
:ci="item.ci"
|
||||
:referenceShowAttrNameMap="referenceShowAttrNameMap"
|
||||
:referenceCIIdMap="referenceCIIdMap"
|
||||
:isEllipsis="true"
|
||||
:searchValue="searchValue"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ExcelJS from 'exceljs'
|
||||
import FileSaver from 'file-saver'
|
||||
import moment from 'moment'
|
||||
|
||||
import AttrDisplay from './attrDisplay.vue'
|
||||
import CIIcon from '@/modules/cmdb/components/ciIcon/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'InstanceList',
|
||||
components: {
|
||||
AttrDisplay,
|
||||
CIIcon
|
||||
},
|
||||
props: {
|
||||
list: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
tabList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
referenceShowAttrNameMap: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
referenceCIIdMap: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
favorList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
detailCIId: {
|
||||
type: [String, Number],
|
||||
default: -1
|
||||
},
|
||||
searchValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentTab: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filterList() {
|
||||
if (!this.currentTab || this.currentTab === -1) {
|
||||
return this.list
|
||||
}
|
||||
|
||||
return this.list.filter((item) => item.ciTypeObj.id === this.currentTab)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
tabList: {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
handler(newVal) {
|
||||
this.currentTab = newVal?.[0]?.id ?? ''
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clickTab(id) {
|
||||
this.currentTab = id
|
||||
},
|
||||
|
||||
getAttrLabel(attrName, attributes) {
|
||||
const label = attributes.find((attr) => attr.name === attrName)?.alias
|
||||
return label || attrName
|
||||
},
|
||||
|
||||
clickInstance(id, ciTypeId) {
|
||||
this.$emit('showDetail', {
|
||||
id,
|
||||
ciTypeId,
|
||||
})
|
||||
},
|
||||
getFavorId(ciId) {
|
||||
const id = this.favorList.find((item) => item?.option?.CIId === ciId)?.id
|
||||
return id ?? null
|
||||
},
|
||||
|
||||
addCollect(data) {
|
||||
this.$emit('addCollect', {
|
||||
CIId: data.ci._id,
|
||||
CITypeId: data.ciTypeObj.id,
|
||||
title: data.ci[data.ciTypeObj.showAttrName],
|
||||
icon: data.ciTypeObj.icon,
|
||||
CITypeTitle: data.ciTypeObj.name
|
||||
})
|
||||
},
|
||||
|
||||
deleteCollect(ciId) {
|
||||
const favorId = this.getFavorId(ciId)
|
||||
if (favorId) {
|
||||
this.$emit('deleteCollect', favorId)
|
||||
}
|
||||
},
|
||||
|
||||
handleExport() {
|
||||
const excel_name = `cmdb-${this.$t('cmdb.ciType.resourceSearch')}-${moment().format('YYYYMMDDHHmmss')}.xlsx`
|
||||
const wb = new ExcelJS.Workbook()
|
||||
|
||||
this.tabList.map((sheet) => {
|
||||
if (sheet.id === -1) {
|
||||
return
|
||||
}
|
||||
|
||||
const ws = wb.addWorksheet(sheet.title)
|
||||
this.handleSheetData({
|
||||
ws,
|
||||
sheet
|
||||
})
|
||||
})
|
||||
|
||||
wb.xlsx.writeBuffer().then((buffer) => {
|
||||
const file = new Blob([buffer], {
|
||||
type: 'application/octet-stream',
|
||||
})
|
||||
FileSaver.saveAs(file, excel_name)
|
||||
})
|
||||
},
|
||||
|
||||
handleSheetData({
|
||||
ws,
|
||||
sheet
|
||||
}) {
|
||||
const listData = this.list.filter((item) => item.ciTypeObj.id === sheet.id)
|
||||
if (!listData.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const columnMap = new Map()
|
||||
const columns = listData[0].attributes.filter((attr) => !attr.is_password).map((attr) => {
|
||||
columnMap.set(attr.name, attr)
|
||||
|
||||
return {
|
||||
header: attr.alias || attr.name || '',
|
||||
key: attr.name,
|
||||
width: 20,
|
||||
}
|
||||
})
|
||||
|
||||
ws.columns = columns
|
||||
|
||||
listData.forEach((data) => {
|
||||
const row = {}
|
||||
columns.forEach(({ key }) => {
|
||||
const value = data?.ci?.[key] ?? null
|
||||
const attr = columnMap.get(key)
|
||||
if (attr.valueType === '6') {
|
||||
row[key] = value ? JSON.stringify(value) : value
|
||||
} else if (attr.is_list && Array.isArray(value)) {
|
||||
row[key] = value.join(',')
|
||||
} else {
|
||||
row[key] = value
|
||||
}
|
||||
})
|
||||
ws.addRow(row)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.list-wrap {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-shrink: 1 !important;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&-bg {
|
||||
width: 100%;
|
||||
padding-top: 90px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.list-tab {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
column-gap: 14px;
|
||||
|
||||
&-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 14px;
|
||||
row-gap: 7px;
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
&-label {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #4E5969;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #4E5969;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
|
||||
&-count {
|
||||
color: #2F54EB;
|
||||
}
|
||||
|
||||
&-active {
|
||||
color: #2F54EB;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #2F54EB;
|
||||
}
|
||||
}
|
||||
|
||||
&-export {
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.list-container {
|
||||
width: 100%;
|
||||
margin-top: 12px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
flex-shrink: 1;
|
||||
flex-grow: 0;
|
||||
|
||||
.list-card {
|
||||
width: 100%;
|
||||
background-color: #FFF;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
cursor: pointer;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
&-selected {
|
||||
border: 1px solid #7F97FA;
|
||||
background-color: #F9FBFF;
|
||||
}
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&-model {
|
||||
border-radius: 24px;
|
||||
border: 1px solid #E4E7ED;
|
||||
background-color: #FFF;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 24px;
|
||||
padding: 0 13px;
|
||||
flex-shrink: 0;
|
||||
|
||||
&-title {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
color: #1D2129;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&-title {
|
||||
margin-left: 11px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #1D2129;
|
||||
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
|
||||
&-collect {
|
||||
font-size: 12px;
|
||||
margin-left: 9px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-attr {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
height: 25px;
|
||||
column-gap: 40px;
|
||||
row-gap: 20px;
|
||||
margin-top: 12px;
|
||||
|
||||
&-item {
|
||||
flex-shrink: 0;
|
||||
max-width: calc((100% - 160px) / 5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
|
||||
&-label {
|
||||
color: #86909C;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&-value {
|
||||
color: #1D2129;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
margin-left: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0px 2px 12px 0px rgba(147, 168, 223, 0.20);
|
||||
|
||||
.list-card-collect {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,201 @@
|
||||
<template>
|
||||
<div :class="['search-input', classType ? 'search-input-' + classType : '']">
|
||||
<a-input
|
||||
:value="searchValue"
|
||||
class="search-input-component"
|
||||
:placeholder="$t('cmdb.ciType.searchInputTip')"
|
||||
@change="handleChangeSearchValue"
|
||||
@pressEnter="saveCondition(true)"
|
||||
>
|
||||
<a-icon
|
||||
class="search-input-component-icon"
|
||||
slot="prefix"
|
||||
type="search"
|
||||
@click="saveCondition(true)"
|
||||
/>
|
||||
</a-input>
|
||||
<FilterPopover
|
||||
ref="filterPpoverRef"
|
||||
:CITypeGroup="CITypeGroup"
|
||||
:allAttributesList="allAttributesList"
|
||||
:expression="expression"
|
||||
:selectCITypeIds="selectCITypeIds"
|
||||
@changeFilter="changeFilter"
|
||||
@updateAllAttributesList="updateAllAttributesList"
|
||||
@saveCondition="saveCondition"
|
||||
/>
|
||||
|
||||
<div v-if="copyText" class="expression-display">
|
||||
<span class="expression-display-text">{{ copyText }}</span>
|
||||
<a-icon
|
||||
slot="suffix"
|
||||
type="check-circle"
|
||||
class="expression-display-icon"
|
||||
@click="handleCopyExpression"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FilterPopover from './filterPopover.vue'
|
||||
|
||||
export default {
|
||||
name: 'SearchInput',
|
||||
components: {
|
||||
FilterPopover
|
||||
},
|
||||
props: {
|
||||
searchValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
expression: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
selectCITypeIds: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
CITypeGroup: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
allAttributesList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
classType: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
// 复制文字展示,与实际文本复制内容区别在于,未选择模型时不展示所有模型拼接数据
|
||||
copyText() {
|
||||
const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
|
||||
const exp = this.expression.match(regQ) ? this.expression.match(regQ)[0] : null
|
||||
|
||||
const textArray = []
|
||||
if (this.selectCITypeIds?.length) {
|
||||
textArray.push(`_type:(${this.selectCITypeIds.join(';')})`)
|
||||
}
|
||||
if (exp) {
|
||||
textArray.push(exp)
|
||||
}
|
||||
if (this.searchValue) {
|
||||
textArray.push(`*${this.searchValue}*`)
|
||||
}
|
||||
|
||||
return textArray.length ? `q=${textArray.join(',')}` : ''
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateAllAttributesList(value) {
|
||||
this.$emit('updateAllAttributesList', value)
|
||||
},
|
||||
saveCondition(isSubmit) {
|
||||
this.$emit('saveCondition', isSubmit)
|
||||
},
|
||||
handleChangeSearchValue(e) {
|
||||
const value = e.target.value
|
||||
this.changeFilter({
|
||||
name: 'searchValue',
|
||||
value
|
||||
})
|
||||
},
|
||||
|
||||
changeFilter(data) {
|
||||
this.$emit('changeFilter', data)
|
||||
},
|
||||
|
||||
handleCopyExpression() {
|
||||
const { selectCITypeIds, expression, searchValue } = this
|
||||
const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
|
||||
const exp = expression.match(regQ) ? expression.match(regQ)[0] : null
|
||||
|
||||
const ciTypeIds = [...selectCITypeIds]
|
||||
if (!ciTypeIds.length) {
|
||||
this.CITypeGroup.forEach((item) => {
|
||||
const ids = item.ci_types.map((ci_type) => ci_type.id)
|
||||
ciTypeIds.push(...ids)
|
||||
})
|
||||
}
|
||||
const copyText = `${ciTypeIds?.length ? `_type:(${ciTypeIds.join(';')})` : ''}${exp ? `,${exp}` : ''}${searchValue ? `,*${searchValue}*` : ''}`
|
||||
|
||||
this.$copyText(copyText)
|
||||
.then(() => {
|
||||
this.$message.success(this.$t('copySuccess'))
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.error(this.$t('cmdb.ci.copyFailed'))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.search-input {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
&-component {
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
background-color: #FFFFFF;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
border-radius: 48px;
|
||||
overflow: hidden;
|
||||
|
||||
&-icon {
|
||||
color: #2F54EB;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/deep/ & > input {
|
||||
height: 100%;
|
||||
margin-left: 10px;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
&-after {
|
||||
height: 38px;
|
||||
justify-content: flex-start;
|
||||
|
||||
.search-input-component {
|
||||
max-width: 524px;
|
||||
}
|
||||
}
|
||||
|
||||
.expression-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 20px;
|
||||
max-width: 30%;
|
||||
|
||||
&-text {
|
||||
width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
margin-left: 8px;
|
||||
color: #00b42a;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
139
cmdb-ui/src/modules/cmdb/views/resource_search_2/index.vue
Normal file
139
cmdb-ui/src/modules/cmdb/views/resource_search_2/index.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<div class="resource-search">
|
||||
<div class="resource-search-tab">
|
||||
<div
|
||||
v-for="(tab) in tabList"
|
||||
:key="tab.value"
|
||||
:class="['resource-search-tab-item', tabActive === tab.value ? 'resource-search-tab-item_active' : '']"
|
||||
@click="tabActive = tab.value"
|
||||
>
|
||||
{{ $t(tab.lable) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="isInit">
|
||||
<ResourceSearchCom
|
||||
v-show="tabActive === 'resourceSearch'"
|
||||
:CITypeGroup="CITypeGroup"
|
||||
:allCITypes="allCITypes"
|
||||
/>
|
||||
<RelationSearch
|
||||
v-show="tabActive === 'relationSearch'"
|
||||
:CITypeGroup="CITypeGroup"
|
||||
:allCITypes="allCITypes"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getCITypeGroups } from '@/modules/cmdb/api/ciTypeGroup'
|
||||
import { getCITypes } from '@/modules/cmdb/api/CIType'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
import ResourceSearchCom from './resourceSearch/index.vue'
|
||||
import RelationSearch from './relationSearch/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'ResourceSearch',
|
||||
components: {
|
||||
ResourceSearchCom,
|
||||
RelationSearch
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tabActive: 'resourceSearch',
|
||||
tabList: [
|
||||
{
|
||||
lable: 'cmdb.ciType.resourceSearch',
|
||||
value: 'resourceSearch'
|
||||
},
|
||||
{
|
||||
lable: 'cmdb.relationSearch.relationSearch',
|
||||
value: 'relationSearch'
|
||||
}
|
||||
],
|
||||
CITypeGroup: [],
|
||||
allCITypes: [],
|
||||
isInit: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
cmdbSearchValue: (state) => state.app.cmdbSearchValue,
|
||||
}),
|
||||
},
|
||||
watch: {
|
||||
cmdbSearchValue: {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
handler() {
|
||||
this.tabActive = 'resourceSearch'
|
||||
}
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
try {
|
||||
await Promise.all([
|
||||
this.getCITypeGroups(),
|
||||
this.getAllCITypes()
|
||||
])
|
||||
} catch (error) {
|
||||
console.log('resource search mounted fail', error)
|
||||
}
|
||||
|
||||
this.isInit = true
|
||||
},
|
||||
methods: {
|
||||
async getCITypeGroups() {
|
||||
const res = await getCITypeGroups({ need_other: true })
|
||||
|
||||
this.CITypeGroup = res
|
||||
.filter((item) => item?.ci_types?.length)
|
||||
.map((item) => {
|
||||
item.id = `parent_${item.id || -1}`
|
||||
return item
|
||||
})
|
||||
},
|
||||
|
||||
async getAllCITypes() {
|
||||
const res = await getCITypes()
|
||||
this.allCITypes = res?.ci_types
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.resource-search {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
&-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&-item {
|
||||
padding-right: 8px;
|
||||
margin-right: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #86909C;
|
||||
cursor: pointer;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-right: solid 1px #E4E7ED;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #2F54EB;
|
||||
}
|
||||
|
||||
&_active {
|
||||
color: #2F54EB;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,516 @@
|
||||
<template>
|
||||
<div class="search-table">
|
||||
<div class="search-table-header">
|
||||
<div class="table-tab">
|
||||
<div
|
||||
v-for="(tab) in tabList"
|
||||
:key="tab.value"
|
||||
:class="['table-tab-item', tabActive === tab.value ? 'table-tab-item_active' : '']"
|
||||
@click="clickTab(tab.value)"
|
||||
>
|
||||
{{ tab.value }}
|
||||
(<span class="table-tab-item-count">{{ tab.count }}</span>)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-button
|
||||
v-if="tableData.ciList && tableData.ciList.length"
|
||||
type="primary"
|
||||
class="ops-button-ghost search-table-export"
|
||||
ghost
|
||||
@click="handleExport"
|
||||
>
|
||||
<ops-icon type="veops-export" />
|
||||
{{ $t('export') }}
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<ops-table
|
||||
ref="xTable"
|
||||
show-overflow
|
||||
:data="tableData.ciList"
|
||||
size="small"
|
||||
:height="`${tableHeight}px`"
|
||||
:cell-class-name="getCellClassName"
|
||||
:header-cell-class-name="getHeaderCellClassName"
|
||||
:checkbox-config="{ range: true }"
|
||||
:loading="isSearchLoading"
|
||||
:column-config="{ resizable: true }"
|
||||
:resizable-config="{ minWidth: 60 }"
|
||||
class="checkbox-hover-table"
|
||||
>
|
||||
<vxe-table-column
|
||||
v-if="tableData.ciList && tableData.ciList.length"
|
||||
align="center"
|
||||
type="checkbox"
|
||||
width="60"
|
||||
>
|
||||
<template #default="{row}">
|
||||
{{ getRowSeq(row) }}
|
||||
</template>
|
||||
</vxe-table-column>
|
||||
|
||||
<template
|
||||
v-if="returnPath && tableData.pathList && tableData.pathList.length"
|
||||
>
|
||||
<vxe-table-column
|
||||
v-for="(path, index) in tableData.pathList"
|
||||
class="table-path-column"
|
||||
:key="`${path.id}-${index}`"
|
||||
:title="tableData.pathList[index].name"
|
||||
:field="path.id"
|
||||
:show-header-overflow="false"
|
||||
:width="index !== tableData.pathList.length - 1 ? 160 : 100"
|
||||
>
|
||||
<template #header>
|
||||
<div class="table-path-header">
|
||||
<span
|
||||
class="table-path-header-name"
|
||||
:style="{
|
||||
maxWidth: tableData.pathList[index].relation ? '70px' : '100%'
|
||||
}"
|
||||
>
|
||||
<a-tooltip :title="tableData.pathList[index].name">
|
||||
{{ tableData.pathList[index].name }}
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<div
|
||||
class="table-path-header-right"
|
||||
v-if="tableData.pathList[index].relation"
|
||||
>
|
||||
<span class="table-path-header-line">
|
||||
<a-icon
|
||||
type="caret-right"
|
||||
class="table-path-header-line-arrow"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="table-path-header-relation"
|
||||
>
|
||||
<span class="table-path-header-relation-text">
|
||||
<a-tooltip :title="tableData.pathList[index].relation">
|
||||
{{ tableData.pathList[index].relation }}
|
||||
</a-tooltip>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ row, columnIndex }">
|
||||
<span
|
||||
v-if="columnIndex === 1"
|
||||
v-html="markSearchValue(row.pathCI[path.id])"
|
||||
></span>
|
||||
<span v-else >{{ row.pathCI[path.id] }}</span>
|
||||
</template>
|
||||
</vxe-table-column>
|
||||
</template>
|
||||
|
||||
<template v-if="tableData.ciAttr && tableData.ciAttr.length">
|
||||
<vxe-table-column
|
||||
v-for="(attr, index) in tableData.ciAttr"
|
||||
:key="`${attr.name}_${index}`"
|
||||
:title="attr.alias || attr.name || ''"
|
||||
:field="attr.name"
|
||||
:width="attr.width"
|
||||
:show-header-overflow="true"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<AttrDisplay
|
||||
:attr="attr"
|
||||
:ci="row.targetCI"
|
||||
:referenceShowAttrNameMap="referenceShowAttrNameMap"
|
||||
:referenceCIIdMap="referenceCIIdMap"
|
||||
/>
|
||||
</template>
|
||||
</vxe-table-column>
|
||||
</template>
|
||||
</ops-table>
|
||||
|
||||
<BatchDownload
|
||||
ref="batchDownload"
|
||||
:showFileTypeSelect="false"
|
||||
@batchDownload="batchDownload"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import moment from 'moment'
|
||||
import { mapState } from 'vuex'
|
||||
import ExcelJS from 'exceljs'
|
||||
import FileSaver from 'file-saver'
|
||||
import AttrDisplay from '@/modules/cmdb/views/resource_search_2/resourceSearch/components/attrDisplay.vue'
|
||||
import BatchDownload from '@/modules/cmdb/components/batchDownload/batchDownload.vue'
|
||||
|
||||
export default {
|
||||
name: 'CITable',
|
||||
components: {
|
||||
AttrDisplay,
|
||||
BatchDownload
|
||||
},
|
||||
props: {
|
||||
allTableData: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
tabActive: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
returnPath: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isHideSearchCondition: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
referenceShowAttrNameMap: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
referenceCIIdMap: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
searchValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
isSearchLoading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
windowHeight: (state) => state.windowHeight,
|
||||
}),
|
||||
tableHeight() {
|
||||
return this.isHideSearchCondition ? this.windowHeight - 308 : this.windowHeight - 458
|
||||
},
|
||||
tableData() {
|
||||
return this.allTableData?.[this.tabActive] || {}
|
||||
},
|
||||
tabList() {
|
||||
const keys = Object.keys(this.allTableData) || []
|
||||
return keys.map((key) => {
|
||||
return {
|
||||
value: key,
|
||||
count: this.allTableData?.[key]?.count || 0
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
markSearchValue(text) {
|
||||
if (!text || !this.searchValue) {
|
||||
return text
|
||||
}
|
||||
const regex = new RegExp(`(${this.searchValue})`, 'gi')
|
||||
return String(text).replace(
|
||||
regex,
|
||||
`<span style="background-color: #D3EEFE; padding: 0 2px;">$1</span>`
|
||||
)
|
||||
},
|
||||
|
||||
clickTab(tab) {
|
||||
this.$emit('updateTab', tab)
|
||||
},
|
||||
|
||||
getRowSeq(row) {
|
||||
const table = this.$refs?.['xTable']?.getVxetableRef?.() || null
|
||||
return table?.getRowSeq?.(row)
|
||||
},
|
||||
getCellClassName({ columnIndex }) {
|
||||
const pathLength = this.tableData?.pathList?.length
|
||||
if (columnIndex <= pathLength && this.returnPath) {
|
||||
return 'table-path-cell'
|
||||
}
|
||||
return ''
|
||||
},
|
||||
getHeaderCellClassName({ columnIndex }) {
|
||||
const pathLength = this.tableData?.pathList?.length
|
||||
if (columnIndex <= pathLength && this.returnPath) {
|
||||
return 'table-path-header-cell'
|
||||
}
|
||||
return ''
|
||||
},
|
||||
|
||||
handleExport() {
|
||||
const preferenceAttrList = []
|
||||
if (this.returnPath && this.tableData?.pathList?.length) {
|
||||
preferenceAttrList.push(...this.tableData.pathList.map((path) => {
|
||||
return {
|
||||
name: path.id,
|
||||
alias: path.name
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
if (this.tableData?.ciAttr?.length) {
|
||||
const ciAttr = _.cloneDeep(this.tableData.ciAttr)
|
||||
ciAttr.forEach((attr) => {
|
||||
attr.alias = attr.alias || attr.name
|
||||
})
|
||||
preferenceAttrList.push(...ciAttr)
|
||||
}
|
||||
|
||||
this.$refs.batchDownload.open({
|
||||
preferenceAttrList,
|
||||
ciTypeName: this.tabActive || '',
|
||||
})
|
||||
},
|
||||
batchDownload({ checkedKeys }) {
|
||||
const excel_name = `cmdb-${this.tabActive}-${moment().format('YYYYMMDDHHmmss')}.xlsx`
|
||||
const wb = new ExcelJS.Workbook()
|
||||
|
||||
const tableRef = this.$refs.xTable.getVxetableRef()
|
||||
let tableData = _.cloneDeep([
|
||||
...tableRef.getCheckboxReserveRecords(),
|
||||
...tableRef.getCheckboxRecords(true),
|
||||
])
|
||||
if (!tableData.length) {
|
||||
const { fullData } = tableRef.getTableData()
|
||||
tableData = _.cloneDeep(fullData)
|
||||
}
|
||||
|
||||
const ws = wb.addWorksheet(this.tabActive)
|
||||
|
||||
const pathColumns = []
|
||||
const targetColumns = []
|
||||
|
||||
if (this.returnPath) {
|
||||
const pathFilter = this.tableData.pathList.filter((path) => checkedKeys.includes(path.id))
|
||||
|
||||
pathFilter.forEach((path) => {
|
||||
pathColumns.push({
|
||||
header: path.name || '',
|
||||
key: path.id,
|
||||
width: 20,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const attrMap = new Map()
|
||||
const attrFilter = this.tableData.ciAttr.filter((attr) => checkedKeys.includes(attr.name))
|
||||
attrFilter.forEach((attr) => {
|
||||
attrMap.set(attr.name, attr)
|
||||
|
||||
targetColumns.push({
|
||||
header: attr.alias || attr.name || '',
|
||||
key: attr.name,
|
||||
width: 20,
|
||||
})
|
||||
})
|
||||
|
||||
ws.columns = [
|
||||
...pathColumns,
|
||||
...targetColumns
|
||||
]
|
||||
|
||||
tableData.forEach(({ pathCI, targetCI }) => {
|
||||
const row = {}
|
||||
if (this.returnPath) {
|
||||
pathColumns.forEach(({ key }) => {
|
||||
row[key] = pathCI?.[key] || ''
|
||||
})
|
||||
}
|
||||
|
||||
targetColumns.forEach(({ key }) => {
|
||||
const value = targetCI?.[key] ?? null
|
||||
const attr = attrMap.get(key)
|
||||
if (attr.valueType === '6') {
|
||||
row[key] = value ? JSON.stringify(value) : value
|
||||
} else if (attr.is_list && Array.isArray(value)) {
|
||||
row[key] = value.join(',')
|
||||
} else {
|
||||
row[key] = value
|
||||
}
|
||||
})
|
||||
|
||||
ws.addRow(row)
|
||||
})
|
||||
|
||||
wb.xlsx.writeBuffer().then((buffer) => {
|
||||
const file = new Blob([buffer], {
|
||||
type: 'application/octet-stream',
|
||||
})
|
||||
FileSaver.saveAs(file, excel_name)
|
||||
})
|
||||
|
||||
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
|
||||
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.search-table {
|
||||
width: 100%;
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between
|
||||
}
|
||||
|
||||
&-export {
|
||||
flex-shrink: 0;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.table-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 35px;
|
||||
padding-bottom: 6px;
|
||||
margin-bottom: 18px;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
|
||||
&-item {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #4E5969;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
|
||||
&-count {
|
||||
color: #2F54EB;
|
||||
}
|
||||
|
||||
&_active {
|
||||
color: #2F54EB;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #2F54EB;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-path-header {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&-name {
|
||||
max-width: 80px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin-left: 10px;
|
||||
margin-right: -5px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&-line {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
position: relative;
|
||||
background-color: #CACDD9;
|
||||
z-index: 0;
|
||||
|
||||
&-arrow {
|
||||
position: absolute;
|
||||
right: -6px;
|
||||
top: -6px;
|
||||
font-size: 12px;
|
||||
color: #CACDD9;
|
||||
}
|
||||
}
|
||||
|
||||
&-relation {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: #FFFFFF;
|
||||
border: solid 1px #E4E7ED;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 8px;
|
||||
border-radius: 22px;
|
||||
z-index: 2;
|
||||
max-width: 70px;
|
||||
width: fit-content;
|
||||
|
||||
&-text {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #A5A9BC;
|
||||
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-hover-table {
|
||||
/deep/ .vxe-table--body-wrapper {
|
||||
.vxe-checkbox--label {
|
||||
display: inline;
|
||||
padding-left: 0px !important;
|
||||
color: #bfbfbf;
|
||||
}
|
||||
|
||||
.vxe-icon-checkbox-unchecked {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vxe-icon-checkbox-checked ~ .vxe-checkbox--label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.vxe-cell--checkbox {
|
||||
&:hover {
|
||||
.vxe-icon-checkbox-unchecked {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.vxe-checkbox--label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/deep/ .table-path-header-cell {
|
||||
background-color: #EBEFF8 !important;
|
||||
|
||||
.vxe-cell--title {
|
||||
width: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
/deep/ .table-path-cell {
|
||||
background-color: #F9FBFF;
|
||||
}
|
||||
|
||||
/deep/ .attr-display {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<a-popover
|
||||
v-model="visible"
|
||||
trigger="click"
|
||||
placement="bottomRight"
|
||||
@visibleChange="handleVisibleChange"
|
||||
>
|
||||
<div class="search-condition-filter">
|
||||
<a-icon class="search-condition-filter-icon" type="filter" />
|
||||
|
||||
<div
|
||||
v-if="expression"
|
||||
class="search-condition-filter-flag"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<template slot="content">
|
||||
<div class="search-condition-content">
|
||||
<div class="search-condition-content-title">
|
||||
{{ $t('cmdb.relationSearch.conditionFilter') }}:
|
||||
</div>
|
||||
|
||||
<ConditionFilter
|
||||
ref="conditionFilterRef"
|
||||
:canSearchPreferenceAttrList="allAttributesList"
|
||||
:expression="expression"
|
||||
:CITypeIds="selectCITypeIds"
|
||||
:isDropdown="false"
|
||||
@setExpFromFilter="setExpFromFilter"
|
||||
/>
|
||||
|
||||
<div class="search-condition-filter-submit">
|
||||
<a-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="clickSubmit()"
|
||||
>
|
||||
{{ $t('confirm') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-popover>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ConditionFilter from '@/modules/cmdb/components/conditionFilter/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'FilterPopover',
|
||||
components: {
|
||||
ConditionFilter
|
||||
},
|
||||
props: {
|
||||
allAttributesList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
selectCITypeIds: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
expression: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleVisibleChange(open) {
|
||||
if (open) {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.conditionFilterRef.init(true, false)
|
||||
})
|
||||
}
|
||||
},
|
||||
clickSubmit() {
|
||||
this.$refs.conditionFilterRef.handleSubmit()
|
||||
this.visible = false
|
||||
},
|
||||
setExpFromFilter(filterExp) {
|
||||
const regSort = /(?<=sort=).+/g
|
||||
const expSort = this.expression.match(regSort) ? this.expression.match(regSort)[0] : undefined
|
||||
let expression = ''
|
||||
if (filterExp) {
|
||||
expression = `q=${filterExp}`
|
||||
}
|
||||
if (expSort) {
|
||||
expression += `&sort=${expSort}`
|
||||
}
|
||||
|
||||
this.$emit('changeExpression', expression)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.search-condition-filter {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
&-flag {
|
||||
position: absolute;
|
||||
right: -5px;
|
||||
bottom: -5px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 10px;
|
||||
background-color: #00B42A22;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 5px;
|
||||
background-color: #00B42A;
|
||||
}
|
||||
}
|
||||
|
||||
&-icon {
|
||||
font-size: 12px;
|
||||
color: #A5A9BC;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.search-condition-filter-icon {
|
||||
color: #2F54EB;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-condition-content {
|
||||
min-width: 500px;
|
||||
|
||||
&-title {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #4E5969;
|
||||
}
|
||||
}
|
||||
|
||||
.search-condition-filter-submit {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:title="$t('cmdb.relationSearch.saveCondition')"
|
||||
:visible="visible"
|
||||
dialogClass="save-condition-modal"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form-model
|
||||
ref="saveConditionForm"
|
||||
:model="form"
|
||||
:rules="formRule"
|
||||
:labelCol="labelCol"
|
||||
:wrapperCol="wrapperCol"
|
||||
>
|
||||
<a-form-model-item
|
||||
:label="$t('cmdb.relationSearch.conditionName')"
|
||||
prop="name"
|
||||
>
|
||||
<a-input v-model="form.name" />
|
||||
</a-form-model-item>
|
||||
</a-form-model>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SaveConditionModal',
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
labelCol() {
|
||||
return {
|
||||
span: this.$i18n.locale === 'en' ? 7 : 4
|
||||
}
|
||||
},
|
||||
wrapperCol() {
|
||||
return {
|
||||
span: this.$i18n.locale === 'en' ? 17 : 20
|
||||
}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
name: ''
|
||||
},
|
||||
formRule: {
|
||||
name: [
|
||||
{ required: true, message: this.$t('placeholder1') }
|
||||
],
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleOk() {
|
||||
this.$refs.saveConditionForm.validate((valid) => {
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$emit('ok', {
|
||||
name: this.form.name
|
||||
})
|
||||
this.handleCancel()
|
||||
})
|
||||
},
|
||||
|
||||
handleCancel() {
|
||||
this.$refs.saveConditionForm.clearValidate()
|
||||
this.form.name = ''
|
||||
this.$emit('cancel')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.save-condition-modal {
|
||||
/deep/ .ant-modal-close-x {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
}
|
||||
|
||||
/deep/ .ant-modal-body {
|
||||
padding: 24px 18px;
|
||||
}
|
||||
|
||||
/deep/ .ant-modal-footer {
|
||||
padding: 10px 18px 18px;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,728 @@
|
||||
<template>
|
||||
<div
|
||||
class="search-condition"
|
||||
:style="{
|
||||
'--label-width': this.$i18n.locale === 'en' ? '90px' : '60px'
|
||||
}"
|
||||
>
|
||||
<div class="search-condition-row">
|
||||
<div class="search-condition-label">
|
||||
{{ $t('cmdb.relationSearch.sourceCIType') }}
|
||||
</div>
|
||||
|
||||
<div class="search-condition-control">
|
||||
<treeselect
|
||||
:value="sourceCIType"
|
||||
class="custom-treeselect custom-treeselect-bgcAndBorder filter-content-ciTypes"
|
||||
:style="{
|
||||
width: '100%',
|
||||
zIndex: '1000',
|
||||
'--custom-height': '32px',
|
||||
'--custom-bg-color': '#FFF',
|
||||
'--custom-multiple-lineHeight': '32px',
|
||||
}"
|
||||
:multiple="false"
|
||||
:clearable="true"
|
||||
searchable
|
||||
:options="CITypeGroup"
|
||||
:limit="1"
|
||||
:limitText="(count) => `+ ${count}`"
|
||||
:disableBranchNodes="true"
|
||||
:placeholder="$t('cmdb.relationSearch.sourceCITypeTip')"
|
||||
:normalizer="
|
||||
(node) => {
|
||||
return {
|
||||
id: node.id || -1,
|
||||
label: node.alias || node.name || $t('other'),
|
||||
title: node.alias || node.name || $t('other'),
|
||||
children: node.ci_types,
|
||||
}
|
||||
}
|
||||
"
|
||||
@input="updateSourceCIType"
|
||||
@open="handleSourceCITypeOpen"
|
||||
>
|
||||
<div
|
||||
:title="node.label"
|
||||
slot="option-label"
|
||||
slot-scope="{ node }"
|
||||
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
|
||||
>
|
||||
{{ node.label }}
|
||||
</div>
|
||||
</treeselect>
|
||||
|
||||
<a-input-search
|
||||
class="search-condition-input"
|
||||
:placeholder="$t('cmdb.relationSearch.sourceCITYpeInput')"
|
||||
:value="sourceCITypeSearchValue"
|
||||
@change="handleSourceCITypeSearchValueChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FilterPopover
|
||||
:allAttributesList="sourceAllAttributesList"
|
||||
:selectCITypeIds="sourceCIType ? [sourceCIType] : []"
|
||||
:expression="sourceExpression"
|
||||
@changeExpression="changeSourceExpression"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="search-condition-row">
|
||||
<div class="search-condition-label">
|
||||
{{ $t('cmdb.relationSearch.targetCIType') }}
|
||||
</div>
|
||||
|
||||
<div class="search-condition-control">
|
||||
<a-select
|
||||
:value="targetCITypes"
|
||||
show-search
|
||||
optionFilterProp="children"
|
||||
mode="multiple"
|
||||
:placeholder="$t('cmdb.relationSearch.targetCITypeTip')"
|
||||
class="search-condition-select"
|
||||
@change="handleTargetCITypeChange"
|
||||
>
|
||||
<a-select-opt-group
|
||||
v-for="(key, index) in Object.keys(targetCITypeGroup)"
|
||||
:key="key"
|
||||
:label="$t('cmdb.relationSearch.level') + `${index + 1}`"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="citype in targetCITypeGroup[key]"
|
||||
:key="citype.id"
|
||||
:value="citype.id"
|
||||
>
|
||||
{{ citype.alias || citype.name }}
|
||||
</a-select-option>
|
||||
</a-select-opt-group>
|
||||
</a-select>
|
||||
</div>
|
||||
|
||||
<FilterPopover
|
||||
:allAttributesList="targetAllAttributesList"
|
||||
:selectCITypeIds="targetCITypes"
|
||||
:expression="targetExpression"
|
||||
@changeExpression="changeTargetExpression"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="search-condition-row">
|
||||
<div class="search-condition-label">
|
||||
{{ $t('cmdb.relationSearch.pathSelect') }}
|
||||
</div>
|
||||
|
||||
<div class="search-condition-control">
|
||||
<a-dropdown
|
||||
v-model="pathSelectVisible"
|
||||
:trigger="['click']"
|
||||
:getPopupContainer="(trigger) => trigger.parentElement"
|
||||
>
|
||||
<a-input
|
||||
:value="pathDisplay"
|
||||
readOnly
|
||||
:placeholder="$t('cmdb.relationSearch.pathSelectTip')"
|
||||
class="search-condition-input"
|
||||
@click="e => e.preventDefault()"
|
||||
>
|
||||
<a-icon
|
||||
slot="suffix"
|
||||
type="caret-down"
|
||||
class="search-condition-input-suffix"
|
||||
/>
|
||||
</a-input>
|
||||
<div @click="clickPathSelectDropdown" slot="overlay">
|
||||
<template v-if="allPath.length" >
|
||||
<a-checkbox-group
|
||||
:value="selectedPath"
|
||||
class="search-condition-checkbox"
|
||||
@change="handlePathChange"
|
||||
>
|
||||
<a-checkbox
|
||||
v-for="(path) in allPath"
|
||||
:key="path.value"
|
||||
:value="path.value"
|
||||
class="search-condition-checkbox-item"
|
||||
>
|
||||
<a-tooltip :title="path.pathNames">
|
||||
<span class="search-condition-checkbox-item-name">
|
||||
{{ path.pathNames }}
|
||||
</span>
|
||||
</a-tooltip>
|
||||
</a-checkbox>
|
||||
</a-checkbox-group>
|
||||
|
||||
<div class="search-condition-path-divider"></div>
|
||||
|
||||
<div class="search-condition-path-switch">
|
||||
<span>{{ $t('cmdb.relationSearch.returnPath') }}</span>
|
||||
<a-switch
|
||||
:checked="returnPath"
|
||||
@change="handleReturnPathChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="search-condition-path-null"
|
||||
>
|
||||
<img
|
||||
:src="require('@/assets/data_empty.png')"
|
||||
class="search-condition-path-null-img"
|
||||
/>
|
||||
<div class="search-condition-path-null-text">{{ $t('noData') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:class="['search-condition-submit', isSearchLoading ? 'search-condition-submit-loading' : '']"
|
||||
@click="clickSubmit"
|
||||
>
|
||||
<a-icon
|
||||
:type="isSearchLoading ? 'loading' : 'search'"
|
||||
class="search-condition-submit-icon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="search-condition-favor">
|
||||
<div class="search-condition-favor-list">
|
||||
<div
|
||||
v-for="(item) in favorList"
|
||||
:key="item.id"
|
||||
class="search-condition-favor-item"
|
||||
@click="clickFavor(item)"
|
||||
>
|
||||
<div class="search-condition-favor-name">
|
||||
{{ item.option.name }}
|
||||
</div>
|
||||
<a-icon
|
||||
@click.stop="deleteFavor(item.id)"
|
||||
type="close"
|
||||
class="search-condition-favor-close"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="search-condition-favor-right">
|
||||
<a
|
||||
class="search-condition-save"
|
||||
@click="saveCondition"
|
||||
>
|
||||
<ops-icon
|
||||
type="veops-save"
|
||||
class="search-condition-save-icon"
|
||||
/>
|
||||
<span class="search-condition-save-text">
|
||||
{{ $t('cmdb.relationSearch.saveCondition') }}
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<div
|
||||
v-if="isSearch"
|
||||
class="search-condition-hide"
|
||||
@click="hideSearchCondition"
|
||||
>
|
||||
<a-icon
|
||||
type="up"
|
||||
class="search-condition-hide-icon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SaveConditionModal
|
||||
:visible="saveConditionVisible"
|
||||
@ok="handleSaveConditionOk"
|
||||
@cancel="saveConditionVisible = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getPreferenceSearch, savePreferenceSearch, deletePreferenceSearch } from '@/modules/cmdb/api/preference'
|
||||
|
||||
import FilterPopover from './filterPopover.vue'
|
||||
import SaveConditionModal from './saveConditionModal.vue'
|
||||
|
||||
export default {
|
||||
name: 'SearchCondition',
|
||||
components: {
|
||||
FilterPopover,
|
||||
SaveConditionModal
|
||||
},
|
||||
props: {
|
||||
CITypeGroup: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
sourceCIType: {
|
||||
type: [Number, undefined],
|
||||
default: undefined
|
||||
},
|
||||
sourceCITypeSearchValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
sourceAllAttributesList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
sourceExpression: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
targetCITypes: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
targetCITypeGroup: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
targetAllAttributesList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
targetExpression: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
returnPath: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
allPath: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
selectedPath: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
isSearch: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isSearchLoading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
oldsourceCIType: undefined,
|
||||
saveConditionVisible: false,
|
||||
pathSelectVisible: false,
|
||||
|
||||
favorList: [],
|
||||
relationSearchFavorKey: '__relation_favor__'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pathDisplay() {
|
||||
return this.allPath?.filter((path) => this?.selectedPath?.includes?.(path?.value))?.map((path) => path?.pathNames)?.join(', ') || ''
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.getFavorList()
|
||||
},
|
||||
methods: {
|
||||
async getFavorList() {
|
||||
const favorList = await getPreferenceSearch({
|
||||
name: this.relationSearchFavorKey
|
||||
})
|
||||
favorList.sort((a, b) => b.id - a.id)
|
||||
this.favorList = favorList
|
||||
},
|
||||
|
||||
updateSourceCIType(value) {
|
||||
this.$emit('changeData', {
|
||||
name: 'sourceCIType',
|
||||
value
|
||||
})
|
||||
},
|
||||
|
||||
handleSourceCITypeSearchValueChange(e) {
|
||||
const value = e.target.value
|
||||
this.$emit('changeData', {
|
||||
name: 'sourceCITypeSearchValue',
|
||||
value
|
||||
})
|
||||
},
|
||||
|
||||
changeSourceExpression(expression) {
|
||||
this.$emit('changeData', {
|
||||
name: 'sourceExpression',
|
||||
value: expression
|
||||
})
|
||||
},
|
||||
|
||||
handleTargetCITypeChange(value) {
|
||||
this.$emit('changeData', {
|
||||
name: 'targetCITypes',
|
||||
value
|
||||
})
|
||||
},
|
||||
|
||||
changeTargetExpression(expression) {
|
||||
this.$emit('changeData', {
|
||||
name: 'targetExpression',
|
||||
value: expression
|
||||
})
|
||||
},
|
||||
|
||||
handlePathChange(value) {
|
||||
this.$emit('changeData', {
|
||||
name: 'selectedPath',
|
||||
value
|
||||
})
|
||||
},
|
||||
|
||||
handleReturnPathChange(checked) {
|
||||
this.$emit('changeData', {
|
||||
name: 'returnPath',
|
||||
value: checked
|
||||
})
|
||||
},
|
||||
|
||||
clickSubmit() {
|
||||
if (this.isSearchLoading) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.validateControl()) {
|
||||
return
|
||||
}
|
||||
|
||||
this.$emit('search')
|
||||
},
|
||||
|
||||
validateControl() {
|
||||
if (!this.sourceCIType) {
|
||||
this.$message.warning(`${this.$t('placeholder2')} ${this.$t('cmdb.relationSearch.sourceCIType')}`)
|
||||
return true
|
||||
}
|
||||
|
||||
if (!this.targetCITypes.length) {
|
||||
this.$message.warning(`${this.$t('placeholder2')} ${this.$t('cmdb.relationSearch.targetCIType')}`)
|
||||
return true
|
||||
}
|
||||
|
||||
if (!this.selectedPath.length) {
|
||||
this.$message.warning(`${this.$t('placeholder2')} ${this.$t('cmdb.relationSearch.path')}`)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
|
||||
saveCondition() {
|
||||
if (this.validateControl()) {
|
||||
return
|
||||
}
|
||||
|
||||
this.saveConditionVisible = true
|
||||
},
|
||||
|
||||
async handleSaveConditionOk({ name }) {
|
||||
if (this?.favorList?.length >= 10) {
|
||||
const deletePromises = this.favorList.slice(9).map((item) => {
|
||||
return deletePreferenceSearch(item.id)
|
||||
})
|
||||
await Promise.all(deletePromises)
|
||||
}
|
||||
|
||||
const option = {
|
||||
name,
|
||||
sourceCIType: this.sourceCIType,
|
||||
searchValue: this.sourceCITypeSearchValue,
|
||||
sourceExpression: this.sourceExpression,
|
||||
targetCITypes: this.targetCITypes,
|
||||
targetExpression: this.targetExpression,
|
||||
selectedPath: this.selectedPath,
|
||||
}
|
||||
|
||||
savePreferenceSearch({
|
||||
option: {
|
||||
...option
|
||||
},
|
||||
name: this.relationSearchFavorKey
|
||||
}).then(() => {
|
||||
this.$message.success(this.$t('saveSuccess'))
|
||||
this.getFavorList()
|
||||
})
|
||||
},
|
||||
|
||||
deleteFavor(id) {
|
||||
deletePreferenceSearch(id).then(() => {
|
||||
this.$message.success(this.$t('deleteSuccess'))
|
||||
this.getFavorList()
|
||||
})
|
||||
},
|
||||
|
||||
hideSearchCondition() {
|
||||
this.$emit('hideSearchCondition')
|
||||
},
|
||||
|
||||
clickPathSelectDropdown(e) {
|
||||
if (e.key === '3') {
|
||||
this.pathSelectVisible = false
|
||||
}
|
||||
},
|
||||
|
||||
clickFavor(data) {
|
||||
if (data?.option) {
|
||||
this.$emit('clickFavor', data.option)
|
||||
}
|
||||
},
|
||||
|
||||
handleSourceCITypeOpen() {
|
||||
this.pathSelectVisible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.search-condition {
|
||||
&-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
column-gap: 15px;
|
||||
}
|
||||
|
||||
&-label {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #000000;
|
||||
width: var(--label-width);
|
||||
}
|
||||
|
||||
&-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 12px;
|
||||
width: 500px;
|
||||
|
||||
/deep/ .ant-dropdown-content {
|
||||
background-color: #FFFFFF;
|
||||
padding: 14px 18px;
|
||||
width: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
&-input {
|
||||
width: 100%;
|
||||
|
||||
/deep/ .ant-input {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&-suffix {
|
||||
color: #CACDD9;
|
||||
}
|
||||
}
|
||||
|
||||
&-select {
|
||||
width: 100%;
|
||||
|
||||
/deep/ .ant-select-selection {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
&-path {
|
||||
&-divider {
|
||||
width: 100%;
|
||||
margin: 20px 0;
|
||||
height: 1px;
|
||||
background-color: #E4E7ED;
|
||||
}
|
||||
|
||||
&-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&-checkbox {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
&-item {
|
||||
margin: 0px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
/deep/ & > span:first-child {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/deep/ & > span:last-child {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&-name {
|
||||
overflow: hidden;
|
||||
text-wrap: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-path-null {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
&-img {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
&-text {
|
||||
margin-top: 12px;
|
||||
color: #A5A9BC;
|
||||
}
|
||||
}
|
||||
|
||||
&-submit {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
cursor: pointer;
|
||||
border-radius: 2px;
|
||||
background-color: #2F54EB;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&-icon {
|
||||
font-size: 12px;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
&-loading {
|
||||
background-color: #2F54EB90;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #2F54EB90;
|
||||
}
|
||||
}
|
||||
|
||||
&-favor {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24px;
|
||||
column-gap: 15px;
|
||||
|
||||
&-list {
|
||||
max-width: 500px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 14px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
&-name {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #4E5969;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
|
||||
&-close {
|
||||
font-size: 12px;
|
||||
color: #4E5969;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
color: #4E596980;
|
||||
}
|
||||
}
|
||||
|
||||
&-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: 150px;
|
||||
background-color: #EBEFF8;
|
||||
border-radius: 28px;
|
||||
padding: 2px 12px;
|
||||
column-gap: 3px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #D9E4FA;
|
||||
|
||||
.search-condition-favor-name {
|
||||
color: #2F54EB;
|
||||
}
|
||||
|
||||
.search-condition-favor-close {
|
||||
color: #2F54EB;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-save {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
column-gap: 7px;
|
||||
}
|
||||
|
||||
&-hide {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background-color: #EBEFF8;
|
||||
border-radius: 1px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
margin-left: 22px;
|
||||
|
||||
&-icon {
|
||||
font-size: 12px;
|
||||
color: #86909C;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #D9E4FA;
|
||||
|
||||
.search-condition-hide-icon {
|
||||
color: #2F54EB;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,693 @@
|
||||
<template>
|
||||
<div
|
||||
ref="relationSearchRef"
|
||||
class="relation-search"
|
||||
:style="{ height: `${windowHeight - 131}px` }"
|
||||
>
|
||||
<div class="relation-search-wrap">
|
||||
<div
|
||||
v-if="!isSearch"
|
||||
class="relation-search-title"
|
||||
>
|
||||
<ops-icon class="relation-search-title-icon" type="veops-relationship2" />
|
||||
<div class="relation-search-title-text">{{ $t('cmdb.relationSearch.relationSearch') }}</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isHideSearchCondition"
|
||||
class="relation-search-expand"
|
||||
>
|
||||
<div class="relation-search-expand-line"></div>
|
||||
|
||||
<div class="relation-search-expand-right">
|
||||
<div
|
||||
class="relation-search-expand-handle"
|
||||
@click="isHideSearchCondition = false"
|
||||
>
|
||||
<a-icon
|
||||
type="down"
|
||||
class="relation-search-expand-icon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="relation-search-expand-text"
|
||||
@click="isHideSearchCondition = false"
|
||||
>
|
||||
{{ $t('cmdb.relationSearch.expandCondition') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchCondition
|
||||
v-else
|
||||
:CITypeGroup="CITypeGroup"
|
||||
:sourceCIType="sourceCIType"
|
||||
:sourceCITypeSearchValue="sourceCITypeSearchValue"
|
||||
:sourceAllAttributesList="sourceAllAttributesList"
|
||||
:sourceExpression="sourceExpression"
|
||||
:targetCITypes="targetCITypes"
|
||||
:targetCITypeGroup="targetCITypeGroup"
|
||||
:targetAllAttributesList="targetAllAttributesList"
|
||||
:targetExpression="targetExpression"
|
||||
:returnPath="returnPath"
|
||||
:allPath="allPath"
|
||||
:selectedPath="selectedPath"
|
||||
:isSearch="isSearch"
|
||||
:isSearchLoading="isSearchLoading"
|
||||
@changeData="changeData"
|
||||
@search="handleSearch"
|
||||
@hideSearchCondition="isHideSearchCondition = true"
|
||||
@clickFavor="clickFavor"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="isSearch"
|
||||
class="relation-search-main"
|
||||
>
|
||||
<CITable
|
||||
:allTableData="allTableData"
|
||||
:tabActive="tableTabActive"
|
||||
:returnPath="returnPath"
|
||||
:isHideSearchCondition="isHideSearchCondition"
|
||||
:referenceShowAttrNameMap="referenceShowAttrNameMap"
|
||||
:referenceCIIdMap="referenceCIIdMap"
|
||||
:searchValue="sourceCITypeSearchValue"
|
||||
:isSearchLoading="isSearchLoading"
|
||||
@updateTab="(tab) => tableTabActive = tab"
|
||||
/>
|
||||
|
||||
<div class="relation-search-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,
|
||||
})
|
||||
"
|
||||
@showSizeChange="handlePageSizeChange"
|
||||
@change="changePage"
|
||||
>
|
||||
<template slot="buildOptionText" slot-scope="props">
|
||||
<span v-if="props.value !== '100000'">{{ props.value }}{{ $t('itemsPerPage') }}</span>
|
||||
<span v-if="props.value === '100000'">{{ $t('all') }}</span>
|
||||
</template>
|
||||
</a-pagination>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img
|
||||
v-if="!isSearch"
|
||||
class="relation-search-bg"
|
||||
:src="require('@/modules/cmdb/assets/resourceSearch/resource_search_bg_1.png')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
|
||||
import { getCITypeAttributesByTypeIds } from '@/modules/cmdb/api/CITypeAttr'
|
||||
import { getRecursive_level2children, getCITypeRelationPath } from '@/modules/cmdb/api/CITypeRelation'
|
||||
import { searchCIRelationPath } from '@/modules/cmdb/api/CIRelation'
|
||||
import { getCITypes } from '@/modules/cmdb/api/CIType'
|
||||
import { getSubscribeAttributes } from '@/modules/cmdb/api/preference'
|
||||
import { searchCI } from '@/modules/cmdb/api/ci'
|
||||
import { strLength } from '@/modules/cmdb/utils/helper.js'
|
||||
|
||||
import SearchCondition from './components/searchCondition.vue'
|
||||
import CITable from './components/ciTable.vue'
|
||||
|
||||
export default {
|
||||
name: 'RelationSearch',
|
||||
components: {
|
||||
SearchCondition,
|
||||
CITable
|
||||
},
|
||||
props: {
|
||||
CITypeGroup: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
allCITypes: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isSearch: false, // 是否搜索
|
||||
isHideSearchCondition: false, // 是否隐藏搜索条件
|
||||
isWatchData: true, // 是否监听数据变化
|
||||
isSearchLoading: false, // 搜索中
|
||||
|
||||
sourceCIType: undefined, // 已选源模型
|
||||
sourceCITypeSearchValue: '', // 源模型搜索关键词
|
||||
sourceAllAttributesList: [], // 源模型所有属性
|
||||
sourceExpression: '', // 源模型表达式
|
||||
|
||||
targetCITypes: [], // 目标模型
|
||||
targetCITypeGroup: {}, // 目标模型分组
|
||||
targetAllAttributesList: [], // 目标模型所有属性
|
||||
targetExpression: '', // 目标模型表达式
|
||||
|
||||
returnPath: true, // 表格是否展示路径详情
|
||||
allPath: [], // 所有路径选项
|
||||
selectedPath: [], // 已选择路径
|
||||
|
||||
// table
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
pageSizeOptions: ['50', '100', '200'],
|
||||
allTableData: {}, // 表格数据
|
||||
totalNumber: 0, // 数据总数
|
||||
tableTabActive: '', // 当前 table tab
|
||||
referenceShowAttrNameMap: {},
|
||||
referenceCIIdMap: {},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
windowHeight() {
|
||||
return this.$store.state.windowHeight
|
||||
},
|
||||
watchParams() {
|
||||
return {
|
||||
sourceCIType: this.sourceCIType,
|
||||
targetCITypes: this.targetCITypes
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
sourceCIType: {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
handler(id) {
|
||||
if (this.isWatchData) {
|
||||
this.sourceExpression = ''
|
||||
|
||||
this.targetCITypes = []
|
||||
this.targetAllAttributesList = []
|
||||
this.targetExpression = ''
|
||||
|
||||
this.selectedPath = []
|
||||
|
||||
this.getTargetCITypeGroup(id)
|
||||
this.updateSourceAllAttributesList(id)
|
||||
}
|
||||
}
|
||||
},
|
||||
targetCITypes: {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
handler(ids) {
|
||||
if (this.isWatchData) {
|
||||
this.selectedPath = []
|
||||
this.targetExpression = ''
|
||||
this.updateTargetAllAttributesList(ids)
|
||||
}
|
||||
}
|
||||
},
|
||||
watchParams: {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
handler(data) {
|
||||
if (this.isWatchData) {
|
||||
this.updateAllPath(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeData(data) {
|
||||
this[data.name] = data.value
|
||||
},
|
||||
|
||||
async updateSourceAllAttributesList(id) {
|
||||
if (id) {
|
||||
const res = await getCITypeAttributesByTypeIds({ type_ids: id })
|
||||
this.sourceAllAttributesList = res.attributes
|
||||
} else {
|
||||
this.sourceAllAttributesList = []
|
||||
}
|
||||
},
|
||||
|
||||
async getTargetCITypeGroup(id) {
|
||||
let targetCITypeGroup = {}
|
||||
if (id) {
|
||||
const res = await getRecursive_level2children(id)
|
||||
targetCITypeGroup = res
|
||||
}
|
||||
this.targetCITypeGroup = targetCITypeGroup
|
||||
},
|
||||
|
||||
async updateTargetAllAttributesList(ids) {
|
||||
if (ids?.length) {
|
||||
const res = await getCITypeAttributesByTypeIds({ type_ids: ids.join(',') })
|
||||
this.targetAllAttributesList = res.attributes
|
||||
} else {
|
||||
this.targetAllAttributesList = []
|
||||
}
|
||||
},
|
||||
|
||||
async updateAllPath(data) {
|
||||
let allPath = []
|
||||
if (
|
||||
data.sourceCIType &&
|
||||
data?.targetCITypes?.length
|
||||
) {
|
||||
const params = {
|
||||
source_type_id: data.sourceCIType,
|
||||
target_type_ids: data.targetCITypes.join(',')
|
||||
}
|
||||
|
||||
const res = await getCITypeRelationPath(params)
|
||||
|
||||
if (res?.paths?.length) {
|
||||
const sourceCIType = this.allCITypes.find((ciType) => ciType.id === data.sourceCIType)
|
||||
const sourceCITypeName = sourceCIType?.alias || sourceCIType?.name || ''
|
||||
const targetCITypeList = Object.values(this.targetCITypeGroup).reduce((acc, cur) => acc.concat(cur), [])
|
||||
|
||||
allPath = res.paths.map((ids) => {
|
||||
const [sourceId, ...targetIds] = ids
|
||||
const pathNames = [sourceCITypeName]
|
||||
|
||||
targetIds.forEach((id) => {
|
||||
const ciType = targetCITypeList.find((item) => item.id === id)
|
||||
if (ciType) {
|
||||
pathNames.push(ciType.alias || ciType.name)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
value: ids.join(','),
|
||||
sourceId,
|
||||
targetIds,
|
||||
pathNames: pathNames.join('-'),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.allPath = allPath
|
||||
},
|
||||
|
||||
async loadCI() {
|
||||
this.isSearchLoading = true
|
||||
|
||||
const path = this.selectedPath.map((item) => {
|
||||
return item?.split(',')?.map((id) => Number(id)) || []
|
||||
})
|
||||
|
||||
const params = {
|
||||
page: this.page,
|
||||
page_size: this.pageSize,
|
||||
source: {
|
||||
type_id: this.sourceCIType
|
||||
},
|
||||
target: {
|
||||
type_ids: this.targetCITypes
|
||||
},
|
||||
path
|
||||
}
|
||||
|
||||
const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
|
||||
const sourceExp = this.sourceExpression.match(regQ) ? this.sourceExpression.match(regQ)[0] : null
|
||||
const targetExp = this.targetExpression.match(regQ) ? this.targetExpression.match(regQ)[0] : null
|
||||
const sourceSearch = `${sourceExp ? `${sourceExp}` : ''}${this.sourceCITypeSearchValue ? `,*${this.sourceCITypeSearchValue}*` : ''}`
|
||||
|
||||
if (sourceSearch) {
|
||||
params.source.q = sourceSearch
|
||||
}
|
||||
if (targetExp) {
|
||||
params.target.q = targetExp
|
||||
}
|
||||
|
||||
let res = {}
|
||||
const tableData = {}
|
||||
const typeId2Attr = {}
|
||||
let pathKeyList = []
|
||||
|
||||
try {
|
||||
res = await searchCIRelationPath(params)
|
||||
|
||||
pathKeyList = Object.keys(res.paths)
|
||||
const filterAllPath = this.allPath.filter((path) => pathKeyList.includes(path.pathNames))
|
||||
const typeIds = _.uniq(
|
||||
filterAllPath.map((item) => item?.targetIds?.[item?.targetIds?.length - 1])
|
||||
)
|
||||
|
||||
const promises = typeIds.map((id) => {
|
||||
return getSubscribeAttributes(id)
|
||||
})
|
||||
const subscribedRes = await Promise.all(promises)
|
||||
typeIds.forEach((id, index) => {
|
||||
const attrList = subscribedRes?.[index]?.attributes || []
|
||||
typeId2Attr[id] = attrList
|
||||
})
|
||||
} catch (error) {
|
||||
this.isSearchLoading = false
|
||||
this.allTableData = {}
|
||||
this.totalNumber = 0
|
||||
this.tableTabActive = ''
|
||||
return
|
||||
}
|
||||
|
||||
pathKeyList.forEach((key) => {
|
||||
const pathObj = this.allPath.find((path) => path.pathNames === key)
|
||||
|
||||
const pathIdList = pathObj?.value?.split(',') || []
|
||||
const pathNameList = key?.split('-') || []
|
||||
|
||||
const pathList = pathNameList.map((name, index) => {
|
||||
let relation = ''
|
||||
if (index < pathNameList.length - 1) {
|
||||
const targetName = pathNameList[index + 1]
|
||||
const sourceRelation = res?.relation_types?.[name]
|
||||
|
||||
if (sourceRelation) {
|
||||
if (Object.keys(sourceRelation)?.includes?.(targetName)) {
|
||||
relation = sourceRelation?.[targetName] || ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: pathIdList?.[index] || '',
|
||||
name,
|
||||
relation
|
||||
}
|
||||
})
|
||||
|
||||
tableData[key] = {
|
||||
key,
|
||||
count: res.paths?.[key]?.length || 0,
|
||||
pathList,
|
||||
ciAttr: [],
|
||||
ciList: []
|
||||
}
|
||||
|
||||
if (pathObj) {
|
||||
const firstIds = res?.paths?.[key]?.[0]
|
||||
const targetId = firstIds[firstIds.length - 1]
|
||||
const ciTypeId = (res?.id2ci?.[targetId] || {})?._type
|
||||
if (ciTypeId) {
|
||||
tableData[key].ciAttr = typeId2Attr[ciTypeId]
|
||||
}
|
||||
|
||||
tableData[key].ciList = res.paths[key].map((ids) => {
|
||||
const pathCI = {}
|
||||
ids.map((id) => {
|
||||
const ci = res?.id2ci?.[id] || {}
|
||||
const showAttr = res?.type2show_key?.[ci._type] || ''
|
||||
pathCI[ci._type] = ci?.[showAttr] ?? ''
|
||||
})
|
||||
|
||||
const targetId = ids[ids.length - 1]
|
||||
const targetCI = res?.id2ci?.[targetId] || {}
|
||||
|
||||
return {
|
||||
pathCI,
|
||||
targetCI
|
||||
}
|
||||
})
|
||||
|
||||
let totalWidth = 0
|
||||
tableData[key].ciAttr.forEach((attr) => {
|
||||
const lengthList = tableData[key].ciList.map(({ targetCI }) => {
|
||||
return strLength(targetCI[attr.name])
|
||||
})
|
||||
|
||||
attr.width = Math.round(Math.min(Math.max(100, ...lengthList), 350))
|
||||
totalWidth += attr.width
|
||||
})
|
||||
|
||||
// ci 表格宽度 = 容器宽度 - path 列宽 - checkbox 宽度
|
||||
const wrapWidth = this.$refs?.relationSearchRef?.clientWidth - (tableData?.[key]?.pathList.length || 0) * 160 - 60
|
||||
|
||||
if (wrapWidth && totalWidth < wrapWidth) {
|
||||
tableData[key].ciAttr.forEach((attr) => {
|
||||
delete attr.width
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
this.$set(this, 'allTableData', tableData)
|
||||
this.allTableData = tableData
|
||||
this.totalNumber = res?.numfound ?? 0
|
||||
this.tableTabActive = Object.keys(tableData)?.[0] || ''
|
||||
this.isSearch = true
|
||||
this.isSearchLoading = false
|
||||
|
||||
const allAttr = []
|
||||
Object.values(typeId2Attr).map((attrList) => {
|
||||
allAttr.push(...attrList)
|
||||
})
|
||||
this.handlePerference(_.uniqBy(allAttr, 'id'))
|
||||
},
|
||||
|
||||
handlePerference(allAttr) {
|
||||
let needRequiredCIType = []
|
||||
allAttr.forEach((attr) => {
|
||||
if (attr?.is_reference && attr?.reference_type_id) {
|
||||
needRequiredCIType.push(attr)
|
||||
}
|
||||
})
|
||||
needRequiredCIType = _.uniq(needRequiredCIType, 'id')
|
||||
|
||||
if (!needRequiredCIType.length) {
|
||||
this.referenceShowAttrNameMap = {}
|
||||
this.referenceCIIdMap = {}
|
||||
return
|
||||
}
|
||||
|
||||
this.handleReferenceShowAttrName(needRequiredCIType)
|
||||
this.handleReferenceCIIdMap(needRequiredCIType)
|
||||
},
|
||||
|
||||
async handleReferenceShowAttrName(needRequiredCIType) {
|
||||
const res = await getCITypes({
|
||||
type_ids: needRequiredCIType.map((col) => col.reference_type_id).join(',')
|
||||
})
|
||||
|
||||
const map = {}
|
||||
res.ci_types.forEach((ciType) => {
|
||||
map[ciType.id] = ciType?.show_name || ciType?.unique_name || ''
|
||||
})
|
||||
|
||||
this.referenceShowAttrNameMap = map
|
||||
},
|
||||
|
||||
async handleReferenceCIIdMap(needRequiredCIType) {
|
||||
const map = {}
|
||||
|
||||
Object.values(this.allTableData).forEach((item) => {
|
||||
const ciList = item?.ciList || []
|
||||
ciList.forEach(({ targetCI }) => {
|
||||
needRequiredCIType.forEach((col) => {
|
||||
const ids = Array.isArray(targetCI[col.name]) ? targetCI[col.name] : targetCI[col.name] ? [targetCI[col.name]] : []
|
||||
if (ids.length) {
|
||||
if (!map?.[col.reference_type_id]) {
|
||||
map[col.reference_type_id] = {}
|
||||
}
|
||||
ids.forEach((id) => {
|
||||
map[col.reference_type_id][id] = {}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
if (!Object.keys(map).length) {
|
||||
this.referenceCIIdMap = {}
|
||||
return
|
||||
}
|
||||
|
||||
const allRes = await Promise.all(
|
||||
Object.keys(map).map((key) => {
|
||||
return searchCI({
|
||||
q: `_type:${key},_id:(${Object.keys(map[key]).join(';')})`,
|
||||
count: 9999
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
allRes.forEach((res) => {
|
||||
res.result.forEach((item) => {
|
||||
if (map?.[item._type]?.[item._id]) {
|
||||
map[item._type][item._id] = item
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
this.referenceCIIdMap = map
|
||||
},
|
||||
|
||||
handlePageSizeChange(_, pageSize) {
|
||||
this.pageSize = pageSize
|
||||
this.page = 1
|
||||
this.loadCI()
|
||||
},
|
||||
|
||||
changePage(page) {
|
||||
this.page = page
|
||||
this.loadCI()
|
||||
},
|
||||
|
||||
handleSearch() {
|
||||
this.page = 1
|
||||
this.loadCI()
|
||||
},
|
||||
|
||||
clickFavor(option) {
|
||||
this.isWatchData = false
|
||||
|
||||
this.$nextTick(async () => {
|
||||
this.sourceCIType = option?.sourceCIType || undefined
|
||||
this.sourceCITypeSearchValue = option?.searchValue || ''
|
||||
this.sourceExpression = option?.sourceExpression || ''
|
||||
this.targetCITypes = option?.targetCITypes || []
|
||||
this.targetExpression = option?.targetExpression || ''
|
||||
this.selectedPath = option?.selectedPath || []
|
||||
|
||||
await Promise.all([
|
||||
this.getTargetCITypeGroup(this.sourceCIType),
|
||||
this.updateSourceAllAttributesList(this.sourceCIType),
|
||||
this.updateTargetAllAttributesList(this.targetCITypes)
|
||||
])
|
||||
await this.updateAllPath({
|
||||
sourceCIType: this.sourceCIType,
|
||||
targetCITypes: this.targetCITypes
|
||||
})
|
||||
|
||||
this.isWatchData = true
|
||||
this.page = 1
|
||||
|
||||
this.loadCI()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.relation-search {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
&-wrap {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 30px;
|
||||
margin-top: 100px;
|
||||
|
||||
&-icon {
|
||||
font-size: 28px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
&-text {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #1D2129;
|
||||
}
|
||||
}
|
||||
|
||||
&-expand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 24px;
|
||||
|
||||
&-line {
|
||||
width: 650px;
|
||||
height: 1px;
|
||||
background-color: #E4E7ED;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
font-size: 12px;
|
||||
color: #86909C;
|
||||
}
|
||||
|
||||
&-text {
|
||||
margin-left: 5px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #A5A9BC;
|
||||
}
|
||||
|
||||
&-handle {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background-color: #EBEFF8;
|
||||
border-radius: 1px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&-right {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
.relation-search-expand-handle {
|
||||
background-color: #D9E4FA;
|
||||
}
|
||||
|
||||
.relation-search-expand-icon {
|
||||
color: #2F54EB;
|
||||
}
|
||||
|
||||
.relation-search-expand-text {
|
||||
color: #2F54EB;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-bg {
|
||||
position: absolute;
|
||||
left: -24px;
|
||||
bottom: -24px;
|
||||
width: calc(100% + 48px);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
&-main {
|
||||
width: calc(100% + 48px);
|
||||
// height: 100%;
|
||||
background-color: #FFFFFF;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
&-pagination {
|
||||
text-align: right;
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<div :class="['attr-display', isEllipsis ? 'attr-display-ellipsis' : '']">
|
||||
<template v-if="attr.is_reference && ci[attr.name]" >
|
||||
<a
|
||||
v-for="(ciId) in (attr.is_list ? ci[attr.name] : [ci[attr.name]])"
|
||||
:key="ciId"
|
||||
:href="`/cmdb/cidetail/${attr.reference_type_id}/${ciId}`"
|
||||
target="_blank"
|
||||
>
|
||||
{{ getReferenceAttrValue(ciId) }}
|
||||
</a>
|
||||
</template>
|
||||
<span v-else-if="attr.value_type === '6' && ci[attr.name]">{{ JSON.stringify(ci[attr.name]) }}</span>
|
||||
<template v-else-if="attr.is_link && ci[attr.name]">
|
||||
<a
|
||||
v-for="(item, linkIndex) in (attr.is_list ? ci[attr.name] : [ci[attr.name]])"
|
||||
:key="linkIndex"
|
||||
:href="
|
||||
item.startsWith('http') || item.startsWith('https')
|
||||
? `${item}`
|
||||
: `http://${item}`
|
||||
"
|
||||
target="_blank"
|
||||
>
|
||||
{{ getChoiceValueLabel(item) || item }}
|
||||
</a>
|
||||
</template>
|
||||
<PasswordField
|
||||
v-else-if="attr.is_password && ci[attr.name]"
|
||||
:ci_id="ci._id"
|
||||
:attr_id="attr.id"
|
||||
></PasswordField>
|
||||
<template v-else-if="attr.is_choice">
|
||||
<span
|
||||
v-for="value in (attr.is_list ? ci[attr.name] : [ci[attr.name]])"
|
||||
:key="value"
|
||||
:style="{
|
||||
borderRadius: '4px',
|
||||
padding: '1px 5px',
|
||||
margin: '2px',
|
||||
...getChoiceValueStyle(value),
|
||||
}"
|
||||
>
|
||||
<ops-icon
|
||||
:style="{ color: getChoiceValueIcon(attr, value).color }"
|
||||
:type="getChoiceValueIcon(attr, value).name"
|
||||
/>
|
||||
<span
|
||||
v-html="markSearchValue(getChoiceValueLabel(value) || value)"
|
||||
></span>
|
||||
</span>
|
||||
</template>
|
||||
<span
|
||||
v-else
|
||||
v-html="markSearchValue((attr.is_list && Array.isArray(ci[attr.name])) ? ci[attr.name].join(',') : ci[attr.name])"
|
||||
></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import PasswordField from '@/modules/cmdb/components/passwordField/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'AttrDisplay',
|
||||
components: {
|
||||
PasswordField
|
||||
},
|
||||
props: {
|
||||
attr: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
ci: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
isEllipsis: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
referenceShowAttrNameMap: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
referenceCIIdMap: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
searchValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
markSearchValue(text) {
|
||||
if (!text || !this.searchValue) {
|
||||
return text
|
||||
}
|
||||
const regex = new RegExp(`(${this.searchValue})`, 'gi')
|
||||
return String(text).replace(
|
||||
regex,
|
||||
`<span style="background-color: #D3EEFE; padding: 0 2px;">$1</span>`
|
||||
)
|
||||
},
|
||||
getChoiceValueStyle(attrValue) {
|
||||
const _find = this?.attr?.choice_value?.find?.((item) => String(item?.[0]) === String(attrValue))
|
||||
if (_find) {
|
||||
return _find?.[1]?.style || {}
|
||||
}
|
||||
return {}
|
||||
},
|
||||
getChoiceValueIcon(attrValue) {
|
||||
const _find = this?.attr?.choice_value?.find((item) => String(item?.[0]) === String(attrValue))
|
||||
if (_find) {
|
||||
return _find?.[1]?.icon || {}
|
||||
}
|
||||
return {}
|
||||
},
|
||||
getChoiceValueLabel(attrValue) {
|
||||
const _find = this?.attr?.choice_value?.find((item) => String(item?.[0]) === String(attrValue))
|
||||
if (_find) {
|
||||
return _find?.[1]?.label || ''
|
||||
}
|
||||
return ''
|
||||
},
|
||||
getReferenceAttrValue(id) {
|
||||
if (this.attr.referenceShowAttrNameMap?.[id]) {
|
||||
return this.attr.referenceShowAttrNameMap[id]
|
||||
}
|
||||
|
||||
const ci = this?.referenceCIIdMap?.[this?.attr?.reference_type_id]?.[id]
|
||||
if (!ci) {
|
||||
return id
|
||||
}
|
||||
|
||||
const attrName = this.referenceShowAttrNameMap?.[this?.attr.reference_type_id]
|
||||
return ci?.[attrName] || id
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.attr-display {
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
word-break: break-all;
|
||||
|
||||
&-ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,235 @@
|
||||
<template>
|
||||
<a-popover
|
||||
v-model="visible"
|
||||
trigger="click"
|
||||
placement="bottom"
|
||||
@visibleChange="handleVisibleChange"
|
||||
>
|
||||
<div class="filter-btn">
|
||||
<a-icon class="filter-btn-icon" type="filter" />
|
||||
<span class="filter-btn-title">{{ $t('cmdb.ciType.advancedFilter') }}</span>
|
||||
</div>
|
||||
<template slot="content">
|
||||
<div class="filter-content">
|
||||
<a-form :form="form">
|
||||
<a-form-item
|
||||
:label="$t('cmdb.ciType.ciType')"
|
||||
:label-col="formLayout.labelCol"
|
||||
:wrapper-col="formLayout.wrapperCol"
|
||||
>
|
||||
<treeselect
|
||||
:value="selectCITypeIds"
|
||||
class="custom-treeselect custom-treeselect-bgcAndBorder filter-content-ciTypes"
|
||||
:style="{
|
||||
width: '400px',
|
||||
zIndex: '1000',
|
||||
'--custom-height': '32px',
|
||||
'--custom-bg-color': '#FFF',
|
||||
'--custom-border': '1px solid #d9d9d9',
|
||||
'--custom-multiple-lineHeight': '32px',
|
||||
}"
|
||||
:multiple="true"
|
||||
:clearable="true"
|
||||
searchable
|
||||
:options="CITypeGroup"
|
||||
:limit="1"
|
||||
:limitText="(count) => `+ ${count}`"
|
||||
value-consists-of="LEAF_PRIORITY"
|
||||
:placeholder="$t('cmdb.ciType.ciType')"
|
||||
@close="closeCiTypeGroup"
|
||||
@open="openCiTypeGroup"
|
||||
@input="inputCiTypeGroup"
|
||||
:normalizer="
|
||||
(node) => {
|
||||
return {
|
||||
id: node.id || -1,
|
||||
label: node.alias || node.name || $t('other'),
|
||||
title: node.alias || node.name || $t('other'),
|
||||
children: node.ci_types,
|
||||
}
|
||||
}
|
||||
"
|
||||
>
|
||||
<div
|
||||
:title="node.label"
|
||||
slot="option-label"
|
||||
slot-scope="{ node }"
|
||||
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
|
||||
>
|
||||
{{ node.label }}
|
||||
</div>
|
||||
</treeselect>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
:label="$t('cmdb.ciType.filterPopoverLabel')"
|
||||
:label-col="formLayout.labelCol"
|
||||
:wrapper-col="formLayout.wrapperCol"
|
||||
class="filter-content-condition-filter"
|
||||
>
|
||||
<ConditionFilter
|
||||
ref="conditionFilterRef"
|
||||
:canSearchPreferenceAttrList="allAttributesList"
|
||||
:expression="expression"
|
||||
:CITypeIds="selectCITypeIds"
|
||||
:isDropdown="false"
|
||||
@setExpFromFilter="setExpFromFilter"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<div class="filter-content-action">
|
||||
<a-button
|
||||
size="small"
|
||||
@click="saveCondition(false)"
|
||||
>
|
||||
{{ $t('cmdb.ciType.saveCondition') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="saveCondition(true)"
|
||||
>
|
||||
{{ $t('confirm') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-popover>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import ConditionFilter from '@/modules/cmdb/components/conditionFilter/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'FilterPopover',
|
||||
components: {
|
||||
ConditionFilter
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
form: this.$form.createForm(this),
|
||||
formLayout: {
|
||||
labelCol: { span: 3 },
|
||||
wrapperCol: { span: 15 },
|
||||
},
|
||||
lastCiType: [],
|
||||
}
|
||||
},
|
||||
props: {
|
||||
expression: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
selectCITypeIds: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
CITypeGroup: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
allAttributesList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleVisibleChange(open) {
|
||||
if (open) {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.conditionFilterRef.init(true, false)
|
||||
})
|
||||
}
|
||||
},
|
||||
openCiTypeGroup() {
|
||||
this.lastCiType = _.cloneDeep(this.selectCITypeIds)
|
||||
},
|
||||
closeCiTypeGroup(value) {
|
||||
if (!_.isEqual(value, this.lastCiType)) {
|
||||
this.$emit('updateAllAttributesList', value)
|
||||
}
|
||||
},
|
||||
inputCiTypeGroup(value) {
|
||||
if (!value || !value.length) {
|
||||
this.$emit('updateAllAttributesList', value)
|
||||
}
|
||||
this.$emit('changeFilter', {
|
||||
name: 'selectCITypeIds',
|
||||
value
|
||||
})
|
||||
},
|
||||
setExpFromFilter(filterExp) {
|
||||
const regSort = /(?<=sort=).+/g
|
||||
const expSort = this.expression.match(regSort) ? this.expression.match(regSort)[0] : undefined
|
||||
let expression = ''
|
||||
if (filterExp) {
|
||||
expression = `q=${filterExp}`
|
||||
}
|
||||
if (expSort) {
|
||||
expression += `&sort=${expSort}`
|
||||
}
|
||||
this.$emit('changeFilter', {
|
||||
name: 'expression',
|
||||
value: expression
|
||||
})
|
||||
},
|
||||
|
||||
saveCondition(isSubmit) {
|
||||
this.$refs.conditionFilterRef.handleSubmit()
|
||||
this.$nextTick(() => {
|
||||
this.$emit('saveCondition', isSubmit)
|
||||
this.visible = false
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.filter-btn {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 13px;
|
||||
cursor: pointer;
|
||||
|
||||
&-icon {
|
||||
color: #2F54EB;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #2F54EB;
|
||||
margin-left: 3px;
|
||||
}
|
||||
}
|
||||
.filter-content {
|
||||
width: 600px;
|
||||
|
||||
&-ciTypes {
|
||||
/deep/ .vue-treeselect__value-container {
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
&-condition-filter {
|
||||
max-height: 250px;
|
||||
// overflow-y: auto;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
&-action {
|
||||
width: 100%;
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
column-gap: 21px;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,310 @@
|
||||
<template>
|
||||
<div class="history-list">
|
||||
<div
|
||||
class="history-recent"
|
||||
v-if="recentList.length"
|
||||
>
|
||||
<div class="history-title">
|
||||
<a-icon type="eye" class="history-title-icon" />
|
||||
<div class="history-title-text">{{ $t('cmdb.ciType.recentSearch') }}</div>
|
||||
|
||||
<a-popconfirm
|
||||
:title="$t('cmdb.ciType.confirmClear')"
|
||||
placement="topRight"
|
||||
@confirm="clearRecent"
|
||||
>
|
||||
<a-tooltip :title="$t('clear')" >
|
||||
<a-icon
|
||||
type="delete"
|
||||
class="history-title-clear"
|
||||
/>
|
||||
</a-tooltip>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
<div class="recent-list">
|
||||
<div
|
||||
v-for="(item) in recentList.slice(0, 10)"
|
||||
:key="item.id"
|
||||
class="recent-list-item"
|
||||
@click="clickRecent(item.option)"
|
||||
>
|
||||
<div class="recent-list-item-text">
|
||||
{{ getRecentSearchText(item.option) }}
|
||||
</div>
|
||||
<a-icon
|
||||
type="close"
|
||||
class="recent-list-item-close"
|
||||
@click.stop="deleteRecent(item.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="history-favor"
|
||||
v-if="favorList.length"
|
||||
>
|
||||
<div class="history-title">
|
||||
<ops-icon type="veops-collect" class="history-title-icon" />
|
||||
<div class="history-title-text">{{ $t('cmdb.ciType.myCollection') }}</div>
|
||||
<div class="history-title-count">({{ favorList.length }})</div>
|
||||
|
||||
<ops-icon
|
||||
type="veops-expand"
|
||||
class="history-title-expand"
|
||||
:style="{
|
||||
transform: `rotate(${isExpand ? '180deg' : '0deg'})`
|
||||
}"
|
||||
@click="isExpand = !isExpand"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="favor-list"
|
||||
:style="{ height: isExpand ? 'auto' : '30px' }"
|
||||
>
|
||||
<div
|
||||
v-for="(item) in favorList"
|
||||
:key="item.id"
|
||||
:class="['favor-list-item', detailCIId === item.option.CIId ? 'favor-list-item-selected' : '']"
|
||||
@click="showDetail(item.option)"
|
||||
>
|
||||
<CIIcon
|
||||
:icon="item.option.icon"
|
||||
:title="item.option.CITypeTitle"
|
||||
/>
|
||||
<div class="favor-list-item-title">
|
||||
{{ item.option.title }}
|
||||
</div>
|
||||
<ops-icon
|
||||
type="veops-collected"
|
||||
class="favor-list-item-collected"
|
||||
@click.stop="deleteCollect(item.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CIIcon from '@/modules/cmdb/components/ciIcon/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'HistoryList',
|
||||
components: {
|
||||
CIIcon
|
||||
},
|
||||
props: {
|
||||
recentList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
favorList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
detailCIId: {
|
||||
type: [String, Number],
|
||||
default: -1
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isExpand: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getRecentSearchText(option) {
|
||||
const textArray = []
|
||||
if (option.searchValue) {
|
||||
textArray.push(`${this.$t('cmdb.ciType.keyword')}: ${option.searchValue}`)
|
||||
}
|
||||
|
||||
if (option?.ciTypeNames?.length) {
|
||||
textArray.push(`${this.$t('cmdb.ciType.CIType')}: ${option.ciTypeNames.join(',')}`)
|
||||
}
|
||||
|
||||
if (option.expression) {
|
||||
textArray.push(`${this.$t('cmdb.ciType.conditionFilter')}: ${option.expression}`)
|
||||
}
|
||||
|
||||
return textArray.join('; ')
|
||||
},
|
||||
|
||||
clickRecent(data) {
|
||||
this.$emit('clickRecent', data)
|
||||
},
|
||||
|
||||
deleteRecent(id) {
|
||||
this.$emit('deleteRecent', id)
|
||||
},
|
||||
|
||||
deleteCollect(id) {
|
||||
this.$emit('deleteCollect', id)
|
||||
},
|
||||
|
||||
showDetail(data) {
|
||||
this.$emit('showDetail', {
|
||||
id: data.CIId,
|
||||
ciTypeId: data.CITypeId
|
||||
})
|
||||
},
|
||||
|
||||
clearRecent() {
|
||||
this.$emit('clearRecent')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.history-list {
|
||||
width: 100%;
|
||||
|
||||
.history-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&-icon {
|
||||
font-size: 12px;
|
||||
color: #2F54EB;
|
||||
}
|
||||
|
||||
&-text {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #4E5969;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
&-count {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #86909C;
|
||||
}
|
||||
|
||||
&-clear {
|
||||
margin-left: auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&-expand {
|
||||
margin-left: auto;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.history-recent {
|
||||
width: 100%;
|
||||
margin-top: 15px;
|
||||
|
||||
.recent-list {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
column-gap: 16px;
|
||||
row-gap: 8px;
|
||||
|
||||
&-item {
|
||||
flex-shrink: 0;
|
||||
padding: 4px 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 22px;
|
||||
background: rgba(255, 255, 255, 0.50);
|
||||
cursor: pointer;
|
||||
max-width: calc((100% - 16px) / 2);
|
||||
|
||||
&-text {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #1D2129;
|
||||
|
||||
max-width: 100%;
|
||||
text-wrap: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&-close {
|
||||
font-size: 12px;
|
||||
margin-left: 4px;
|
||||
color: #A5A9BC;
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.recent-list-item-text {
|
||||
color: #2F54EB;
|
||||
}
|
||||
|
||||
.recent-list-item-close {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.history-favor {
|
||||
width: 100%;
|
||||
margin-top: 15px;
|
||||
|
||||
.favor-list {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
column-gap: 16px;
|
||||
row-gap: 8px;
|
||||
overflow: hidden;
|
||||
min-height: 30px;
|
||||
|
||||
&-item {
|
||||
flex-shrink: 0;
|
||||
padding: 4px 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 22px;
|
||||
background: rgba(255, 255, 255, 0.90);
|
||||
cursor: pointer;
|
||||
max-width: calc((100% - 16px) / 2);
|
||||
|
||||
&-title {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
margin-left: 4px;
|
||||
color: #1D2129;
|
||||
|
||||
max-width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&-collected {
|
||||
font-size: 14px;
|
||||
margin-left: 4px;
|
||||
color: #FAD337;
|
||||
}
|
||||
|
||||
&-selected {
|
||||
border: 1px solid #7F97FA;
|
||||
background-color: rgba(255, 255, 255, 0.90);
|
||||
|
||||
.favor-list-item-title {
|
||||
color: #2F54EB;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.favor-list-item-title {
|
||||
color: #2F54EB;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,438 @@
|
||||
<template>
|
||||
<div class="instance-detail">
|
||||
<div
|
||||
class="instance-detail-hide"
|
||||
@click="hideDetail"
|
||||
>
|
||||
<a-icon class="instance-detail-hide-icon" type="right" />
|
||||
</div>
|
||||
|
||||
<div class="instance-detail-null" v-if="!ci._id" >
|
||||
<img
|
||||
:src="require('@/modules/cmdb/assets/no_permission.png')"
|
||||
class="instance-detail-null-img"
|
||||
/>
|
||||
<span class="instance-detail-null-text" >{{ $t('noData') }}</span>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div
|
||||
class="instance-detail-header"
|
||||
>
|
||||
<div class="instance-detail-header-line-1"></div>
|
||||
<div class="instance-detail-header-line-2"></div>
|
||||
<div class="instance-detail-header-row">
|
||||
<CIIcon
|
||||
:icon="ciType.icon"
|
||||
:title="ciType.name || ''"
|
||||
:size="20"
|
||||
/>
|
||||
<div class="instance-detail-header-title">
|
||||
{{ detailTitle }}
|
||||
</div>
|
||||
|
||||
<ops-icon
|
||||
:type="favorId ? 'veops-collected' : 'veops-collect'"
|
||||
:style="{ color: favorId ? '#FAD337' : '#A5A9BC' }"
|
||||
class="instance-detail-header-collect"
|
||||
@click="clickCollect"
|
||||
/>
|
||||
|
||||
<a class="instance-detail-header-share" @click="shareCi">
|
||||
<a-icon type="share-alt" />
|
||||
{{ $t('cmdb.ci.share') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="instance-detail-attr">
|
||||
<div
|
||||
v-for="(group) in attributeGroups"
|
||||
:key="group.id"
|
||||
class="instance-detail-attr-group"
|
||||
>
|
||||
<span class="instance-detail-attr-group-name">{{ group.name || $t('other') }}</span>
|
||||
|
||||
<div class="instance-detail-attr-list">
|
||||
<div
|
||||
v-for="(attr) in group.attributes"
|
||||
:key="attr.id"
|
||||
class="instance-detail-attr-item"
|
||||
>
|
||||
<a-tooltip :title="attr.alias || attr.name || ''">
|
||||
<div class="instance-detail-attr-item-label">
|
||||
<span class="instance-detail-attr-item-label-text">
|
||||
{{ attr.alias || attr.name || '' }}
|
||||
</span>
|
||||
<span class="instance-detail-attr-item-label-colon">:</span>
|
||||
</div>
|
||||
</a-tooltip>
|
||||
|
||||
<div class="instance-detail-attr-item-value">
|
||||
<AttrDisplay
|
||||
:attr="attr"
|
||||
:ci="ci"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { getCIById, searchCI } from '@/modules/cmdb/api/ci'
|
||||
import { getCITypeGroupById, getCITypes, getCIType } from '@/modules/cmdb/api/CIType'
|
||||
|
||||
import AttrDisplay from './attrDisplay.vue'
|
||||
import CIIcon from '@/modules/cmdb/components/ciIcon/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'InstanceDetail',
|
||||
components: {
|
||||
AttrDisplay,
|
||||
CIIcon
|
||||
},
|
||||
props: {
|
||||
CIId: {
|
||||
type: [String, Number],
|
||||
default: -1
|
||||
},
|
||||
CITypeId: {
|
||||
type: [String, Number],
|
||||
default: -1
|
||||
},
|
||||
favorList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
ci: {},
|
||||
ciType: {},
|
||||
attributeGroups: [],
|
||||
isNullData: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
watchParams() {
|
||||
return {
|
||||
CIId: this.CIId,
|
||||
CITypeId: this.CITypeId
|
||||
}
|
||||
},
|
||||
detailTitle() {
|
||||
const attrName = this?.ciType?.show_name || this?.ciType?.unique_name || ''
|
||||
return attrName ? (this?.ci?.[attrName] || '') : ''
|
||||
},
|
||||
favorId() {
|
||||
const id = this.favorList.find((item) => item?.option?.CIId === this.CIId)?.id
|
||||
return id ?? null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
watchParams: {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
handler(newVal) {
|
||||
if (newVal?.CIId !== -1 && newVal?.CITypeId !== -1) {
|
||||
this.initData()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async initData() {
|
||||
const ci = await this.getCI()
|
||||
if (!ci) {
|
||||
this.isNullData = true
|
||||
return
|
||||
}
|
||||
await this.getCIType()
|
||||
await this.getAttributes()
|
||||
},
|
||||
|
||||
async getCI() {
|
||||
const res = await getCIById(this.CIId)
|
||||
const ci = res.result?.[0] || {}
|
||||
this.ci = ci
|
||||
return ci
|
||||
},
|
||||
|
||||
async getCIType() {
|
||||
const res = await getCIType(this.CITypeId)
|
||||
this.ciType = res?.ci_types?.[0] || {}
|
||||
},
|
||||
|
||||
async getAttributes() {
|
||||
const res = await getCITypeGroupById(this.CITypeId, { need_other: 1 })
|
||||
this.attributeGroups = res
|
||||
this.handleReferenceAttr()
|
||||
},
|
||||
|
||||
async handleReferenceAttr() {
|
||||
const map = {}
|
||||
this.attributeGroups.forEach((group) => {
|
||||
group.attributes.forEach((attr) => {
|
||||
if (attr?.is_reference && attr?.reference_type_id && this.ci[attr.name]) {
|
||||
const ids = Array.isArray(this.ci[attr.name]) ? this.ci[attr.name] : this.ci[attr.name] ? [this.ci[attr.name]] : []
|
||||
if (ids.length) {
|
||||
if (!map?.[attr.reference_type_id]) {
|
||||
map[attr.reference_type_id] = {}
|
||||
}
|
||||
ids.forEach((id) => {
|
||||
map[attr.reference_type_id][id] = {}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (!Object.keys(map).length) {
|
||||
return
|
||||
}
|
||||
|
||||
const ciTypesRes = await getCITypes({
|
||||
type_ids: Object.keys(map).join(',')
|
||||
})
|
||||
const showAttrNameMap = {}
|
||||
ciTypesRes.ci_types.forEach((ciType) => {
|
||||
showAttrNameMap[ciType.id] = ciType?.show_name || ciType?.unique_name || ''
|
||||
})
|
||||
|
||||
const allRes = await Promise.all(
|
||||
Object.keys(map).map((key) => {
|
||||
return searchCI({
|
||||
q: `_type:${key},_id:(${Object.keys(map[key]).join(';')})`,
|
||||
count: 9999
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
const ciNameMap = {}
|
||||
allRes.forEach((res) => {
|
||||
res.result.forEach((item) => {
|
||||
ciNameMap[item._id] = item
|
||||
})
|
||||
})
|
||||
|
||||
const newAttrGroups = _.cloneDeep(this.attributeGroups)
|
||||
|
||||
newAttrGroups.forEach((group) => {
|
||||
group.attributes.forEach((attr) => {
|
||||
if (attr?.is_reference && attr?.reference_type_id) {
|
||||
attr.showAttrName = showAttrNameMap?.[attr?.reference_type_id] || ''
|
||||
|
||||
const referenceShowAttrNameMap = {}
|
||||
const referenceCIIds = this.ci[attr.name];
|
||||
(Array.isArray(referenceCIIds) ? referenceCIIds : referenceCIIds ? [referenceCIIds] : []).forEach((id) => {
|
||||
referenceShowAttrNameMap[id] = ciNameMap?.[id]?.[attr.showAttrName] ?? id
|
||||
})
|
||||
attr.referenceShowAttrNameMap = referenceShowAttrNameMap
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
this.$set(this, 'attributeGroups', newAttrGroups)
|
||||
},
|
||||
|
||||
shareCi() {
|
||||
const text = `${document.location.host}/cmdb/cidetail/${this.CITypeId}/${this.CIId}`
|
||||
this.$copyText(text)
|
||||
.then(() => {
|
||||
this.$message.success(this.$t('copySuccess'))
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.error(this.$t('cmdb.ci.copyFailed'))
|
||||
})
|
||||
},
|
||||
|
||||
clickCollect() {
|
||||
if (this.favorId) {
|
||||
this.$emit('deleteCollect', this.favorId)
|
||||
} else {
|
||||
this.$emit('addCollect', {
|
||||
CIId: this.CIId,
|
||||
CITypeId: this.CITypeId,
|
||||
title: this.detailTitle,
|
||||
icon: this.ciType?.icon,
|
||||
CITypeTitle: this.ciType?.name || ''
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
hideDetail() {
|
||||
this.$emit('hideDetail')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.instance-detail {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 2px;
|
||||
border: 1px solid #E4E7ED;
|
||||
background-color: #FFFFFF;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
|
||||
&-hide {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
margin-top: -21px;
|
||||
border-radius: 0px 2px 2px 0px;
|
||||
background-color: #2f54eb;
|
||||
width: 13px;
|
||||
height: 43px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
cursor: pointer;
|
||||
|
||||
&-icon {
|
||||
color: #FFFFFF;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #597ef7;
|
||||
}
|
||||
}
|
||||
|
||||
&-null {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
padding-top: 100px;
|
||||
|
||||
&-img {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
&-text {
|
||||
color: #86909C;
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&-header {
|
||||
width: 100%;
|
||||
height: 75px;
|
||||
background-color: #EBF0F9;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
flex-shrink: 0;
|
||||
|
||||
&-line-1 {
|
||||
height: 44px;
|
||||
width: 300px;
|
||||
position: absolute;
|
||||
right: -20px;
|
||||
top: 0px;
|
||||
transform: rotate(40deg);
|
||||
background: rgba(248, 249, 253, 0.60);
|
||||
}
|
||||
|
||||
&-line-2 {
|
||||
height: 44px;
|
||||
width: 300px;
|
||||
position: absolute;
|
||||
right: -110px;
|
||||
top: 0px;
|
||||
transform: rotate(40deg);
|
||||
background: rgba(248, 249, 253, 0.60);
|
||||
}
|
||||
|
||||
&-row {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #1D2129;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
margin-left: 9px;
|
||||
}
|
||||
|
||||
&-collect {
|
||||
margin-left: 8px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
&-share {
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-attr {
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
|
||||
&-group {
|
||||
&:not(:first-child) {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
&-name {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #1D2129;
|
||||
}
|
||||
}
|
||||
|
||||
&-item {
|
||||
margin-top: 15px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
&-label {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #86909C;
|
||||
width: 25%;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&-text {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
|
||||
&-colon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-value {
|
||||
margin-left: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,443 @@
|
||||
<template>
|
||||
<div class="list-wrap">
|
||||
<div class="list-wrap-bg" v-if="!filterList.length">
|
||||
<img :src="require('@/modules/cmdb/assets/resourceSearch/resource_search_bg_2.png')" />
|
||||
</div>
|
||||
|
||||
<div v-if="tabList.length" class="list-tab">
|
||||
<div class="list-tab-left">
|
||||
<div class="list-tab-label">{{ $t('cmdb.ciType.currentPage') }}: </div>
|
||||
<div
|
||||
v-for="(tab) in tabList"
|
||||
:key="tab.id"
|
||||
:class="['list-tab-item', tab.id === currentTab ? 'list-tab-item-active' : '']"
|
||||
@click="clickTab(tab.id)"
|
||||
>
|
||||
<span class="list-tab-item-title">{{ tab.title }}</span>
|
||||
(<span class="list-tab-item-count">{{ tab.count }}</span>)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-button
|
||||
icon="download"
|
||||
type="primary"
|
||||
class="ops-button-ghost list-tab-export"
|
||||
ghost
|
||||
@click="handleExport"
|
||||
>
|
||||
{{ $t('download') }}
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<div v-if="filterList.length" class="list-container">
|
||||
<div
|
||||
v-for="(item) in filterList"
|
||||
:key="item._id"
|
||||
:class="['list-card', detailCIId === item.ci._id ? 'list-card-selected' : '']"
|
||||
@click="clickInstance(item.ci._id, item.ciTypeObj.id)"
|
||||
>
|
||||
<div class="list-card-header">
|
||||
<div class="list-card-model">
|
||||
<CIIcon
|
||||
:icon="item.ciTypeObj.icon"
|
||||
:title="item.ciTypeObj.name"
|
||||
/>
|
||||
<span class="list-card-model-title">{{ item.ciTypeObj.title }}</span>
|
||||
</div>
|
||||
<div class="list-card-title">{{ item.ci[item.ciTypeObj.showAttrName] }}</div>
|
||||
|
||||
<ops-icon
|
||||
v-if="getFavorId(item.ci._id)"
|
||||
type="veops-collected"
|
||||
class="list-card-collect"
|
||||
:style="{ color: '#FAD337' }"
|
||||
@click.stop="deleteCollect(item.ci._id)"
|
||||
/>
|
||||
|
||||
<ops-icon
|
||||
v-else
|
||||
type="veops-collect"
|
||||
class="list-card-collect"
|
||||
:style="{ color: '#A5A9BC' }"
|
||||
@click.stop="addCollect(item)"
|
||||
/>
|
||||
</div>
|
||||
<div class="list-card-attr">
|
||||
<div
|
||||
v-for="(attr) in item.attributes"
|
||||
:key="attr.name"
|
||||
class="list-card-attr-item"
|
||||
>
|
||||
<div class="list-card-attr-item-label">{{ attr.alias || attr.name || '' }}: </div>
|
||||
<div class="list-card-attr-item-value">
|
||||
<AttrDisplay
|
||||
:attr="attr"
|
||||
:ci="item.ci"
|
||||
:referenceShowAttrNameMap="referenceShowAttrNameMap"
|
||||
:referenceCIIdMap="referenceCIIdMap"
|
||||
:isEllipsis="true"
|
||||
:searchValue="searchValue"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ExcelJS from 'exceljs'
|
||||
import FileSaver from 'file-saver'
|
||||
import moment from 'moment'
|
||||
|
||||
import AttrDisplay from './attrDisplay.vue'
|
||||
import CIIcon from '@/modules/cmdb/components/ciIcon/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'InstanceList',
|
||||
components: {
|
||||
AttrDisplay,
|
||||
CIIcon
|
||||
},
|
||||
props: {
|
||||
list: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
tabList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
referenceShowAttrNameMap: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
referenceCIIdMap: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
favorList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
detailCIId: {
|
||||
type: [String, Number],
|
||||
default: -1
|
||||
},
|
||||
searchValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentTab: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filterList() {
|
||||
if (!this.currentTab || this.currentTab === -1) {
|
||||
return this.list
|
||||
}
|
||||
|
||||
return this.list.filter((item) => item.ciTypeObj.id === this.currentTab)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
tabList: {
|
||||
immediate: true,
|
||||
deep: true,
|
||||
handler(newVal) {
|
||||
this.currentTab = newVal?.[0]?.id ?? ''
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clickTab(id) {
|
||||
this.currentTab = id
|
||||
},
|
||||
|
||||
getAttrLabel(attrName, attributes) {
|
||||
const label = attributes.find((attr) => attr.name === attrName)?.alias
|
||||
return label || attrName
|
||||
},
|
||||
|
||||
clickInstance(id, ciTypeId) {
|
||||
this.$emit('showDetail', {
|
||||
id,
|
||||
ciTypeId,
|
||||
})
|
||||
},
|
||||
getFavorId(ciId) {
|
||||
const id = this.favorList.find((item) => item?.option?.CIId === ciId)?.id
|
||||
return id ?? null
|
||||
},
|
||||
|
||||
addCollect(data) {
|
||||
this.$emit('addCollect', {
|
||||
CIId: data.ci._id,
|
||||
CITypeId: data.ciTypeObj.id,
|
||||
title: data.ci[data.ciTypeObj.showAttrName],
|
||||
icon: data.ciTypeObj.icon,
|
||||
CITypeTitle: data.ciTypeObj.name
|
||||
})
|
||||
},
|
||||
|
||||
deleteCollect(ciId) {
|
||||
const favorId = this.getFavorId(ciId)
|
||||
if (favorId) {
|
||||
this.$emit('deleteCollect', favorId)
|
||||
}
|
||||
},
|
||||
|
||||
handleExport() {
|
||||
const excel_name = `cmdb-${this.$t('cmdb.ciType.resourceSearch')}-${moment().format('YYYYMMDDHHmmss')}.xlsx`
|
||||
const wb = new ExcelJS.Workbook()
|
||||
|
||||
this.tabList.map((sheet) => {
|
||||
if (sheet.id === -1) {
|
||||
return
|
||||
}
|
||||
|
||||
const ws = wb.addWorksheet(sheet.title)
|
||||
this.handleSheetData({
|
||||
ws,
|
||||
sheet
|
||||
})
|
||||
})
|
||||
|
||||
wb.xlsx.writeBuffer().then((buffer) => {
|
||||
const file = new Blob([buffer], {
|
||||
type: 'application/octet-stream',
|
||||
})
|
||||
FileSaver.saveAs(file, excel_name)
|
||||
})
|
||||
},
|
||||
|
||||
handleSheetData({
|
||||
ws,
|
||||
sheet
|
||||
}) {
|
||||
const listData = this.list.filter((item) => item.ciTypeObj.id === sheet.id)
|
||||
if (!listData.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const columnMap = new Map()
|
||||
const columns = listData[0].attributes.filter((attr) => !attr.is_password).map((attr) => {
|
||||
columnMap.set(attr.name, attr)
|
||||
|
||||
return {
|
||||
header: attr.alias || attr.name || '',
|
||||
key: attr.name,
|
||||
width: 20,
|
||||
}
|
||||
})
|
||||
|
||||
ws.columns = columns
|
||||
|
||||
listData.forEach((data) => {
|
||||
const row = {}
|
||||
columns.forEach(({ key }) => {
|
||||
const value = data?.ci?.[key] ?? null
|
||||
const attr = columnMap.get(key)
|
||||
if (attr.valueType === '6') {
|
||||
row[key] = value ? JSON.stringify(value) : value
|
||||
} else if (attr.is_list && Array.isArray(value)) {
|
||||
row[key] = value.join(',')
|
||||
} else {
|
||||
row[key] = value
|
||||
}
|
||||
})
|
||||
ws.addRow(row)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.list-wrap {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex-shrink: 1 !important;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&-bg {
|
||||
width: 100%;
|
||||
padding-top: 90px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
width: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.list-tab {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
column-gap: 14px;
|
||||
|
||||
&-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 14px;
|
||||
row-gap: 7px;
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
&-label {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #4E5969;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #4E5969;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
|
||||
&-count {
|
||||
color: #2F54EB;
|
||||
}
|
||||
|
||||
&-active {
|
||||
color: #2F54EB;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #2F54EB;
|
||||
}
|
||||
}
|
||||
|
||||
&-export {
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.list-container {
|
||||
width: 100%;
|
||||
margin-top: 12px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
flex-shrink: 1;
|
||||
flex-grow: 0;
|
||||
|
||||
.list-card {
|
||||
width: 100%;
|
||||
background-color: #FFF;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
cursor: pointer;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
&-selected {
|
||||
border: 1px solid #7F97FA;
|
||||
background-color: #F9FBFF;
|
||||
}
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&-model {
|
||||
border-radius: 24px;
|
||||
border: 1px solid #E4E7ED;
|
||||
background-color: #FFF;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 24px;
|
||||
padding: 0 13px;
|
||||
flex-shrink: 0;
|
||||
|
||||
&-title {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
color: #1D2129;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&-title {
|
||||
margin-left: 11px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #1D2129;
|
||||
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
|
||||
&-collect {
|
||||
font-size: 12px;
|
||||
margin-left: 9px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-attr {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
height: 25px;
|
||||
column-gap: 40px;
|
||||
row-gap: 20px;
|
||||
margin-top: 12px;
|
||||
|
||||
&-item {
|
||||
flex-shrink: 0;
|
||||
max-width: calc((100% - 160px) / 5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
|
||||
&-label {
|
||||
color: #86909C;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&-value {
|
||||
color: #1D2129;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
margin-left: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0px 2px 12px 0px rgba(147, 168, 223, 0.20);
|
||||
|
||||
.list-card-collect {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,201 @@
|
||||
<template>
|
||||
<div :class="['search-input', classType ? 'search-input-' + classType : '']">
|
||||
<a-input
|
||||
:value="searchValue"
|
||||
class="search-input-component"
|
||||
:placeholder="$t('cmdb.ciType.searchInputTip')"
|
||||
@change="handleChangeSearchValue"
|
||||
@pressEnter="saveCondition(true)"
|
||||
>
|
||||
<a-icon
|
||||
class="search-input-component-icon"
|
||||
slot="prefix"
|
||||
type="search"
|
||||
@click="saveCondition(true)"
|
||||
/>
|
||||
</a-input>
|
||||
<FilterPopover
|
||||
ref="filterPpoverRef"
|
||||
:CITypeGroup="CITypeGroup"
|
||||
:allAttributesList="allAttributesList"
|
||||
:expression="expression"
|
||||
:selectCITypeIds="selectCITypeIds"
|
||||
@changeFilter="changeFilter"
|
||||
@updateAllAttributesList="updateAllAttributesList"
|
||||
@saveCondition="saveCondition"
|
||||
/>
|
||||
|
||||
<div v-if="copyText" class="expression-display">
|
||||
<span class="expression-display-text">{{ copyText }}</span>
|
||||
<a-icon
|
||||
slot="suffix"
|
||||
type="check-circle"
|
||||
class="expression-display-icon"
|
||||
@click="handleCopyExpression"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FilterPopover from './filterPopover.vue'
|
||||
|
||||
export default {
|
||||
name: 'SearchInput',
|
||||
components: {
|
||||
FilterPopover
|
||||
},
|
||||
props: {
|
||||
searchValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
expression: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
selectCITypeIds: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
CITypeGroup: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
allAttributesList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
classType: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
// 复制文字展示,与实际文本复制内容区别在于,未选择模型时不展示所有模型拼接数据
|
||||
copyText() {
|
||||
const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
|
||||
const exp = this.expression.match(regQ) ? this.expression.match(regQ)[0] : null
|
||||
|
||||
const textArray = []
|
||||
if (this.selectCITypeIds?.length) {
|
||||
textArray.push(`_type:(${this.selectCITypeIds.join(';')})`)
|
||||
}
|
||||
if (exp) {
|
||||
textArray.push(exp)
|
||||
}
|
||||
if (this.searchValue) {
|
||||
textArray.push(`*${this.searchValue}*`)
|
||||
}
|
||||
|
||||
return textArray.length ? `q=${textArray.join(',')}` : ''
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateAllAttributesList(value) {
|
||||
this.$emit('updateAllAttributesList', value)
|
||||
},
|
||||
saveCondition(isSubmit) {
|
||||
this.$emit('saveCondition', isSubmit)
|
||||
},
|
||||
handleChangeSearchValue(e) {
|
||||
const value = e.target.value
|
||||
this.changeFilter({
|
||||
name: 'searchValue',
|
||||
value
|
||||
})
|
||||
},
|
||||
|
||||
changeFilter(data) {
|
||||
this.$emit('changeFilter', data)
|
||||
},
|
||||
|
||||
handleCopyExpression() {
|
||||
const { selectCITypeIds, expression, searchValue } = this
|
||||
const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
|
||||
const exp = expression.match(regQ) ? expression.match(regQ)[0] : null
|
||||
|
||||
const ciTypeIds = [...selectCITypeIds]
|
||||
if (!ciTypeIds.length) {
|
||||
this.CITypeGroup.forEach((item) => {
|
||||
const ids = item.ci_types.map((ci_type) => ci_type.id)
|
||||
ciTypeIds.push(...ids)
|
||||
})
|
||||
}
|
||||
const copyText = `${ciTypeIds?.length ? `_type:(${ciTypeIds.join(';')})` : ''}${exp ? `,${exp}` : ''}${searchValue ? `,*${searchValue}*` : ''}`
|
||||
|
||||
this.$copyText(copyText)
|
||||
.then(() => {
|
||||
this.$message.success(this.$t('copySuccess'))
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message.error(this.$t('cmdb.ci.copyFailed'))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.search-input {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
&-component {
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
background-color: #FFFFFF;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
border-radius: 48px;
|
||||
overflow: hidden;
|
||||
|
||||
&-icon {
|
||||
color: #2F54EB;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/deep/ & > input {
|
||||
height: 100%;
|
||||
margin-left: 10px;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
&-after {
|
||||
height: 38px;
|
||||
justify-content: flex-start;
|
||||
|
||||
.search-input-component {
|
||||
max-width: 524px;
|
||||
}
|
||||
}
|
||||
|
||||
.expression-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 20px;
|
||||
max-width: 30%;
|
||||
|
||||
&-text {
|
||||
width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
margin-left: 8px;
|
||||
color: #00b42a;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -0,0 +1,625 @@
|
||||
<template>
|
||||
<div
|
||||
class="resource-search"
|
||||
:style="{ height: `${windowHeight - 131}px` }"
|
||||
>
|
||||
<div v-if="!isSearch" class="resource-search-before">
|
||||
<div class="resource-search-title">
|
||||
<ops-icon class="resource-search-title-icon" type="veops-resource11" />
|
||||
<span class="resource-search-title-text">{{ $t('cmdb.ciType.resourceSearch') }}</span>
|
||||
</div>
|
||||
<SearchInput
|
||||
ref="searchInputRef"
|
||||
:CITypeGroup="CITypeGroup"
|
||||
:allAttributesList="allAttributesList"
|
||||
:searchValue="searchValue"
|
||||
:selectCITypeIds="selectCITypeIds"
|
||||
:expression="expression"
|
||||
@changeFilter="changeFilter"
|
||||
@updateAllAttributesList="updateAllAttributesList"
|
||||
@saveCondition="saveCondition"
|
||||
/>
|
||||
<HistoryList
|
||||
:recentList="recentList"
|
||||
:favorList="favorList"
|
||||
:detailCIId="detailCIId"
|
||||
@clickRecent="clickRecent"
|
||||
@deleteRecent="deleteRecent"
|
||||
@clearRecent="clearRecent"
|
||||
@deleteCollect="deleteCollect"
|
||||
@showDetail="clickFavor"
|
||||
/>
|
||||
|
||||
<img class="resource-search-before-bg" :src="require('@/modules/cmdb/assets/resourceSearch/resource_search_bg_1.png')" />
|
||||
</div>
|
||||
|
||||
<div class="resource-search-after" v-else>
|
||||
<div
|
||||
class="resource-search-after-left"
|
||||
:style="{ width: showInstanceDetail ? '70%' : '100%' }"
|
||||
>
|
||||
<SearchInput
|
||||
ref="searchInputRef"
|
||||
classType="after"
|
||||
:CITypeGroup="CITypeGroup"
|
||||
:allAttributesList="allAttributesList"
|
||||
:searchValue="searchValue"
|
||||
:selectCITypeIds="selectCITypeIds"
|
||||
:expression="expression"
|
||||
@changeFilter="changeFilter"
|
||||
@updateAllAttributesList="updateAllAttributesList"
|
||||
@saveCondition="saveCondition"
|
||||
/>
|
||||
<HistoryList
|
||||
:recentList="recentList"
|
||||
:favorList="favorList"
|
||||
:detailCIId="detailCIId"
|
||||
@clickRecent="clickRecent"
|
||||
@deleteRecent="deleteRecent"
|
||||
@clearRecent="clearRecent"
|
||||
@deleteCollect="deleteCollect"
|
||||
@showDetail="clickFavor"
|
||||
/>
|
||||
<div class="resource-search-divider"></div>
|
||||
<InstanceList
|
||||
:list="instanceList"
|
||||
:tabList="ciTabList"
|
||||
:referenceShowAttrNameMap="referenceShowAttrNameMap"
|
||||
:referenceCIIdMap="referenceCIIdMap"
|
||||
:favorList="favorList"
|
||||
:detailCIId="detailCIId"
|
||||
:searchValue="currentSearchValue"
|
||||
@showDetail="showDetail"
|
||||
@addCollect="addCollect"
|
||||
@deleteCollect="deleteCollect"
|
||||
/>
|
||||
|
||||
<div class="resource-search-pagination">
|
||||
<a-pagination
|
||||
:showSizeChanger="true"
|
||||
:current="currentPage"
|
||||
size="small"
|
||||
:total="totalNumber"
|
||||
show-quick-jumper
|
||||
:page-size="pageSize"
|
||||
:page-size-options="pageSizeOptions"
|
||||
@showSizeChange="handlePageSizeChange"
|
||||
:show-total="
|
||||
(total, range) =>
|
||||
$t('pagination.total', {
|
||||
range0: range[0],
|
||||
range1: range[1],
|
||||
total,
|
||||
})
|
||||
"
|
||||
@change="changePage"
|
||||
>
|
||||
<template slot="buildOptionText" slot-scope="props">
|
||||
<span v-if="props.value !== '100000'">{{ props.value }}{{ $t('itemsPerPage') }}</span>
|
||||
<span v-if="props.value === '100000'">{{ $t('all') }}</span>
|
||||
</template>
|
||||
</a-pagination>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showInstanceDetail" class="resource-search-after-right">
|
||||
<InstanceDetail
|
||||
:CIId="detailCIId"
|
||||
:CITypeId="detailCITypeId"
|
||||
:favorList="favorList"
|
||||
@addCollect="addCollect"
|
||||
@deleteCollect="deleteCollect"
|
||||
@hideDetail="hideDetail"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { mapState } from 'vuex'
|
||||
import { getPreferenceSearch, savePreferenceSearch, getSubscribeAttributes, deletePreferenceSearch } from '@/modules/cmdb/api/preference'
|
||||
import { searchAttributes, getCITypeAttributesByTypeIds } from '@/modules/cmdb/api/CITypeAttr'
|
||||
import { searchCI } from '@/modules/cmdb/api/ci'
|
||||
import { getCITypes } from '@/modules/cmdb/api/CIType'
|
||||
|
||||
import SearchInput from './components/searchInput.vue'
|
||||
import HistoryList from './components/historyList.vue'
|
||||
import InstanceList from './components/instanceList.vue'
|
||||
import InstanceDetail from './components/instanceDetail.vue'
|
||||
|
||||
export default {
|
||||
name: 'ResourceSearchCom',
|
||||
components: {
|
||||
SearchInput,
|
||||
HistoryList,
|
||||
InstanceList,
|
||||
InstanceDetail
|
||||
},
|
||||
props: {
|
||||
CITypeGroup: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
allCITypes: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// 筛选条件
|
||||
searchValue: '', // 搜索框
|
||||
selectCITypeIds: [], // 已选模型
|
||||
expression: '', // 筛选语句
|
||||
currentSearchValue: '', // 当前已搜索语句
|
||||
|
||||
recentList: [], // 最近搜索
|
||||
favorList: [], // 我的收藏
|
||||
allAttributesList: [],
|
||||
|
||||
isSearch: false, // 是否搜索过
|
||||
currentPage: 1,
|
||||
pageSizeOptions: ['50', '100', '200', '100000'],
|
||||
pageSize: 50,
|
||||
totalNumber: 0,
|
||||
ciTabList: [],
|
||||
instanceList: [],
|
||||
referenceShowAttrNameMap: {},
|
||||
referenceCIIdMap: {},
|
||||
|
||||
showInstanceDetail: false,
|
||||
detailCIId: -1,
|
||||
detailCITypeId: -1,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
cmdbSearchValue: (state) => state.app.cmdbSearchValue,
|
||||
}),
|
||||
windowHeight() {
|
||||
return this.$store.state.windowHeight
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
cmdbSearchValue: {
|
||||
handler(value) {
|
||||
this.searchValue = value
|
||||
this.saveCondition(true)
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.initData()
|
||||
},
|
||||
methods: {
|
||||
async initData() {
|
||||
await this.getFavorList()
|
||||
|
||||
this.$nextTick(async () => {
|
||||
if (this.cmdbSearchValue) {
|
||||
this.searchValue = this.cmdbSearchValue
|
||||
this.saveCondition(true)
|
||||
} else {
|
||||
await this.getRecentList()
|
||||
}
|
||||
})
|
||||
|
||||
await this.getAllAttr()
|
||||
},
|
||||
|
||||
async getRecentList() {
|
||||
const recentList = await getPreferenceSearch({
|
||||
name: '__recent__'
|
||||
})
|
||||
recentList.sort((a, b) => b.id - a.id)
|
||||
this.recentList = recentList
|
||||
},
|
||||
|
||||
async getFavorList() {
|
||||
const favorList = await getPreferenceSearch({
|
||||
name: '__favor__'
|
||||
})
|
||||
favorList.sort((a, b) => b.id - a.id)
|
||||
this.favorList = favorList
|
||||
},
|
||||
|
||||
async getAllAttr() {
|
||||
const res = await searchAttributes({ page_size: 9999 })
|
||||
this.allAttributesList = res.attributes
|
||||
this.originAllAttributesList = res.attributes
|
||||
},
|
||||
|
||||
async updateAllAttributesList(value) {
|
||||
if (value && value.length) {
|
||||
const res = await getCITypeAttributesByTypeIds({ type_ids: value.join(',') })
|
||||
this.allAttributesList = res.attributes
|
||||
} else {
|
||||
this.allAttributesList = this.originAllAttributesList
|
||||
}
|
||||
},
|
||||
|
||||
async saveCondition(isSubmit) {
|
||||
if (
|
||||
this.searchValue ||
|
||||
this.expression ||
|
||||
this.selectCITypeIds.length
|
||||
) {
|
||||
const needDeleteList = []
|
||||
const differentList = []
|
||||
this.recentList.forEach((item) => {
|
||||
const option = item.option
|
||||
if (
|
||||
option.searchValue === this.searchValue &&
|
||||
option.expression === this.expression &&
|
||||
_.isEqual(option.ciTypeIds, this.selectCITypeIds)
|
||||
) {
|
||||
needDeleteList.push(item.id)
|
||||
} else {
|
||||
differentList.push(item.id)
|
||||
}
|
||||
})
|
||||
if (differentList.length >= 10) {
|
||||
needDeleteList.push(...differentList.slice(9))
|
||||
}
|
||||
if (needDeleteList.length) {
|
||||
await Promise.all(
|
||||
needDeleteList.map((id) => deletePreferenceSearch(id))
|
||||
)
|
||||
}
|
||||
|
||||
const ciTypeNames = this.selectCITypeIds.map((id) => {
|
||||
const ciType = this.allCITypes.find((item) => item.id === id)
|
||||
return ciType?.alias || ciType?.name || id
|
||||
})
|
||||
|
||||
await savePreferenceSearch({
|
||||
option: {
|
||||
searchValue: this.searchValue,
|
||||
expression: this.expression,
|
||||
ciTypeIds: this.selectCITypeIds,
|
||||
ciTypeNames
|
||||
},
|
||||
name: '__recent__'
|
||||
})
|
||||
this.getRecentList()
|
||||
}
|
||||
|
||||
if (isSubmit) {
|
||||
this.isSearch = true
|
||||
this.currentPage = 1
|
||||
this.hideDetail()
|
||||
this.loadInstance()
|
||||
}
|
||||
},
|
||||
|
||||
async deleteRecent(id) {
|
||||
await deletePreferenceSearch(id)
|
||||
this.getRecentList()
|
||||
},
|
||||
|
||||
async clearRecent() {
|
||||
const deletePromises = this.recentList.map((item) => {
|
||||
return deletePreferenceSearch(item.id)
|
||||
})
|
||||
await Promise.all(deletePromises)
|
||||
this.getRecentList()
|
||||
},
|
||||
|
||||
async loadInstance() {
|
||||
const { selectCITypeIds, expression, searchValue } = this
|
||||
const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
|
||||
const exp = expression.match(regQ) ? expression.match(regQ)[0] : null
|
||||
|
||||
const ciTypeIds = [...selectCITypeIds]
|
||||
if (!ciTypeIds.length) {
|
||||
this.CITypeGroup.forEach((item) => {
|
||||
const ids = item.ci_types.map((ci_type) => ci_type.id)
|
||||
ciTypeIds.push(...ids)
|
||||
})
|
||||
}
|
||||
|
||||
const res = await searchCI({
|
||||
q: `${ciTypeIds?.length ? `_type:(${ciTypeIds.join(';')})` : ''}${exp ? `,${exp}` : ''}${
|
||||
searchValue ? `,*${searchValue}*` : ''
|
||||
}`,
|
||||
count: this.pageSize,
|
||||
page: this.currentPage,
|
||||
sort: '_type'
|
||||
})
|
||||
this.currentSearchValue = searchValue
|
||||
|
||||
this.totalNumber = res?.numfound ?? 0
|
||||
if (!res?.result?.length) {
|
||||
this.ciTabList = []
|
||||
this.instanceList = []
|
||||
}
|
||||
|
||||
const ciTabMap = new Map()
|
||||
|
||||
let list = res.result
|
||||
list.forEach((item) => {
|
||||
const ciType = this.allCITypes.find((type) => type.id === item._type)
|
||||
if (ciTabMap.has(item._type)) {
|
||||
ciTabMap.get(item._type).count++
|
||||
} else {
|
||||
ciTabMap.set(item._type, {
|
||||
id: item._type,
|
||||
count: 1,
|
||||
title: ciType?.alias || ciType?.name || '',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const mapEntries = [...ciTabMap.entries()]
|
||||
const subscribedPromises = mapEntries.map((item) => {
|
||||
return getSubscribeAttributes(item[0])
|
||||
})
|
||||
const subscribedRes = await Promise.all(subscribedPromises)
|
||||
list = list.map((item) => {
|
||||
const subscribedIndex = mapEntries.findIndex((mapValue) => mapValue[0] === item._type)
|
||||
const subscribedAttr = subscribedRes?.[subscribedIndex]?.attributes || []
|
||||
const obj = {
|
||||
ci: item,
|
||||
ciTypeObj: {},
|
||||
attributes: subscribedAttr
|
||||
}
|
||||
|
||||
const ciType = this.allCITypes.find((type) => type.id === item._type)
|
||||
obj.ciTypeObj = {
|
||||
showAttrName: ciType?.show_name || ciType?.unique_key || '',
|
||||
icon: ciType?.icon || '',
|
||||
title: ciType?.alias || ciType?.name || '',
|
||||
name: ciType?.name || '',
|
||||
id: ciType.id
|
||||
}
|
||||
|
||||
return obj
|
||||
})
|
||||
|
||||
this.instanceList = list
|
||||
const ciTabList = [...ciTabMap.values()]
|
||||
if (list?.length) {
|
||||
ciTabList.unshift({
|
||||
id: -1,
|
||||
title: this.$t('all'),
|
||||
count: list?.length
|
||||
})
|
||||
}
|
||||
this.ciTabList = ciTabList
|
||||
|
||||
// 处理引用属性
|
||||
const allAttr = []
|
||||
subscribedRes.map((item) => {
|
||||
allAttr.push(...item.attributes)
|
||||
})
|
||||
this.handlePerference(_.uniqBy(allAttr, 'id'))
|
||||
},
|
||||
|
||||
handlePerference(allAttr) {
|
||||
let needRequiredCIType = []
|
||||
allAttr.forEach((attr) => {
|
||||
if (attr?.is_reference && attr?.reference_type_id) {
|
||||
needRequiredCIType.push(attr)
|
||||
}
|
||||
})
|
||||
needRequiredCIType = _.uniq(needRequiredCIType, 'id')
|
||||
|
||||
if (!needRequiredCIType.length) {
|
||||
this.referenceShowAttrNameMap = {}
|
||||
this.referenceCIIdMap = {}
|
||||
return
|
||||
}
|
||||
|
||||
this.handleReferenceShowAttrName(needRequiredCIType)
|
||||
this.handleReferenceCIIdMap(needRequiredCIType)
|
||||
},
|
||||
|
||||
async handleReferenceShowAttrName(needRequiredCIType) {
|
||||
const res = await getCITypes({
|
||||
type_ids: needRequiredCIType.map((col) => col.reference_type_id).join(',')
|
||||
})
|
||||
|
||||
const map = {}
|
||||
res.ci_types.forEach((ciType) => {
|
||||
map[ciType.id] = ciType?.show_name || ciType?.unique_name || ''
|
||||
})
|
||||
|
||||
this.referenceShowAttrNameMap = map
|
||||
},
|
||||
|
||||
async handleReferenceCIIdMap(needRequiredCIType) {
|
||||
const map = {}
|
||||
this.instanceList.forEach(({ ci }) => {
|
||||
needRequiredCIType.forEach((col) => {
|
||||
const ids = Array.isArray(ci[col.name]) ? ci[col.name] : ci[col.name] ? [ci[col.name]] : []
|
||||
if (ids.length) {
|
||||
if (!map?.[col.reference_type_id]) {
|
||||
map[col.reference_type_id] = {}
|
||||
}
|
||||
ids.forEach((id) => {
|
||||
map[col.reference_type_id][id] = {}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (!Object.keys(map).length) {
|
||||
this.referenceCIIdMap = {}
|
||||
return
|
||||
}
|
||||
|
||||
const allRes = await Promise.all(
|
||||
Object.keys(map).map((key) => {
|
||||
return searchCI({
|
||||
q: `_type:${key},_id:(${Object.keys(map[key]).join(';')})`,
|
||||
count: 9999
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
allRes.forEach((res) => {
|
||||
res.result.forEach((item) => {
|
||||
if (map?.[item._type]?.[item._id]) {
|
||||
map[item._type][item._id] = item
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
this.referenceCIIdMap = map
|
||||
},
|
||||
|
||||
clickRecent(data) {
|
||||
this.updateAllAttributesList(data.ciTypeIds || [])
|
||||
this.isSearch = true
|
||||
this.currentPage = 1
|
||||
this.searchValue = data?.searchValue || ''
|
||||
this.expression = data?.expression || ''
|
||||
this.selectCITypeIds = data?.ciTypeIds || []
|
||||
|
||||
this.hideDetail()
|
||||
this.loadInstance()
|
||||
},
|
||||
|
||||
handlePageSizeChange(_, pageSize) {
|
||||
this.pageSize = pageSize
|
||||
this.currentPage = 1
|
||||
this.loadInstance()
|
||||
},
|
||||
|
||||
changePage(page) {
|
||||
this.currentPage = page
|
||||
this.loadInstance()
|
||||
},
|
||||
|
||||
changeFilter(data) {
|
||||
this[data.name] = data.value
|
||||
},
|
||||
|
||||
showDetail(data) {
|
||||
this.detailCIId = data.id
|
||||
this.detailCITypeId = data.ciTypeId
|
||||
this.showInstanceDetail = true
|
||||
},
|
||||
|
||||
hideDetail() {
|
||||
this.detailCIId = -1
|
||||
this.detailCITypeId = -1
|
||||
this.showInstanceDetail = false
|
||||
},
|
||||
|
||||
async addCollect(data) {
|
||||
if (this?.favorList?.length >= 10) {
|
||||
const deletePromises = this.favorList.slice(9).map((item) => {
|
||||
return deletePreferenceSearch(item.id)
|
||||
})
|
||||
await Promise.all(deletePromises)
|
||||
}
|
||||
await savePreferenceSearch({
|
||||
option: {
|
||||
...data
|
||||
},
|
||||
name: '__favor__'
|
||||
})
|
||||
this.getFavorList()
|
||||
},
|
||||
|
||||
async deleteCollect(id) {
|
||||
await deletePreferenceSearch(id)
|
||||
this.getFavorList()
|
||||
},
|
||||
|
||||
clickFavor(data) {
|
||||
this.isSearch = true
|
||||
this.showDetail(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.resource-search {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
&-before {
|
||||
width: 100%;
|
||||
max-width: 725px;
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
padding-top: 100px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
& > div {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&-bg {
|
||||
position: absolute;
|
||||
left: -24px;
|
||||
bottom: -24px;
|
||||
width: calc(100% + 48px);
|
||||
z-index: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 25px;
|
||||
|
||||
&-icon {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
&-text {
|
||||
margin-left: 10px;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #1D2129;
|
||||
}
|
||||
}
|
||||
|
||||
&-after {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
&-left {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
& > div {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-right {
|
||||
margin-left: 20px;
|
||||
width: calc(30% - 20px);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&-divider {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: #E4E7ED;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
&-pagination {
|
||||
text-align: right;
|
||||
margin: 12px 0px;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -15,6 +15,7 @@
|
||||
:paneLengthPixel.sync="paneLengthPixel"
|
||||
appName="cmdb-topo-views"
|
||||
:triggerLength="18"
|
||||
calcBasedParent
|
||||
>
|
||||
<template #one>
|
||||
<a-input
|
||||
|
@@ -10,6 +10,7 @@
|
||||
:paneLengthPixel.sync="paneLengthPixel"
|
||||
appName="cmdb-tree-views"
|
||||
:triggerLength="18"
|
||||
calcBasedParent
|
||||
>
|
||||
<template #one>
|
||||
<div class="tree-views-left" :style="{ height: `${windowHeight - 64}px` }">
|
||||
|
@@ -25,7 +25,7 @@ const app = {
|
||||
color: null,
|
||||
weak: false,
|
||||
multiTab: false,
|
||||
|
||||
cmdbSearchValue: '',
|
||||
},
|
||||
mutations: {
|
||||
SET_SIDEBAR_TYPE: (state, type) => {
|
||||
@@ -76,7 +76,9 @@ const app = {
|
||||
Vue.ls.set(DEFAULT_MULTI_TAB, bool)
|
||||
state.multiTab = bool
|
||||
},
|
||||
|
||||
UPDATE_CMDB_SEARCH_VALUE: (state, value) => {
|
||||
state.cmdbSearchValue = value
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
setSidebar({ commit }, type) {
|
||||
@@ -118,6 +120,9 @@ const app = {
|
||||
ToggleMultiTab({ commit }, bool) {
|
||||
commit('TOGGLE_MULTI_TAB', bool)
|
||||
},
|
||||
UpdateCMDBSEarchValue({ commit }, value) {
|
||||
commit('UPDATE_CMDB_SEARCH_VALUE', value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user