Compare commits

..

47 Commits

Author SHA1 Message Date
pycook
eafb5f053a fix(api): custom dashboard for enum type 2024-08-26 22:31:58 +08:00
Leo Song
834054e216 Merge pull request #604 from veops/dev_ui_240826
feat: export remove reference attr
2024-08-26 22:22:43 +08:00
LH_R
a97cabbedc feat: export remove reference attr 2024-08-26 22:21:25 +08:00
Leo Song
ae77852d5f Merge pull request #603 from veops/dev_ui_240826
fix(ui): define value filter error
2024-08-26 21:40:03 +08:00
LH_R
611ee40dca fix(ui): define value filter error 2024-08-26 21:38:02 +08:00
pycook
c0d55b2126 Merge branch 'master' of github.com:veops/cmdb 2024-08-26 19:50:44 +08:00
pycook
2cc4499ef9 fix(api): custom dashboard 2024-08-26 19:50:22 +08:00
Leo Song
1268404bca Merge pull request #602 from veops/dev_ui_240826
fix(ui): menu icon display
2024-08-26 19:49:46 +08:00
songlh
570a9203c4 fix(ui): menu icon display 2024-08-26 19:47:23 +08:00
pycook
adae7b5519 chore: release v2.4.11 2024-08-26 18:44:23 +08:00
Leo Song
8a91ec7b11 Merge pull request #601 from veops/dev_ui_240826
fix(ui): some bugs
2024-08-26 18:35:44 +08:00
songlh
92fca65383 fix(ui): some bugs 2024-08-26 18:34:42 +08:00
Leo Song
4b8e6c2841 Merge pull request #600 from veops/dev_ui_240826
fix(ui): update builtIn params
2024-08-26 16:03:37 +08:00
songlh
ab240cb003 fix(ui): update builtIn params 2024-08-26 16:02:05 +08:00
Leo Song
61e62e4740 Merge pull request #599 from veops/dev_ui_240826
feat(ui) update CMDBFilterComp label
2024-08-26 15:16:07 +08:00
songlh
1fd72d6c78 feat(ui) update CMDBFilterComp label 2024-08-26 15:14:52 +08:00
Leo Song
51e16f6b23 Merge pull request #598 from veops/dev_ui_240826
Dev UI 240826
2024-08-26 15:09:01 +08:00
songlh
037378e384 fix(ui): create ad plugin params 2024-08-26 15:08:19 +08:00
songlh
631871a8cf feat(ui): update ci type choice config 2024-08-26 15:05:11 +08:00
pycook
6e02f6a21f fix(api): in query 2024-08-26 13:29:03 +08:00
pycook
a2224ba2ac Merge pull request #597 from veops/dev_api_0826
feat(api): enum supports
2024-08-26 12:15:05 +08:00
pycook
11a289aac9 feat(api): enum supports 2024-08-26 12:14:14 +08:00
Leo Song
55ab04dd28 Merge pull request #596 from thexqn/fix_order_bug
修复在继承模型的情况下,非继承属性与继承属性的排序以及其他分组的排序提示问题Fix order bug
2024-08-26 11:20:15 +08:00
thexqn
256a4f4844 清理多余的router-view 2024-08-23 16:55:21 +08:00
thexqn
018a349336 feat: 修复在继承模型的情况下,非继承属性与继承属性的排序以及其他分组的排序的问题 2024-08-23 16:50:37 +08:00
thexqn
8f62227adb feat: 修复在继承模型的情况下,非继承属性与继承属性的排序以及其他分组的排序的问题 2024-08-23 16:29:29 +08:00
thexqn
de51cb3e21 Merge branch 'veops:master' into master 2024-08-23 14:56:12 +08:00
Leo Song
ecb069cf14 Merge pull request #594 from veops/dev_ui_240820
feat(ui): add bool and reference type
2024-08-20 15:31:45 +08:00
songlh
937cb84393 feat(ui): add bool and reference type 2024-08-20 15:31:11 +08:00
pycook
40a4db06b5 Merge pull request #593 from veops/dev_api_0820
feat(api): supports bool and reference
2024-08-20 13:51:44 +08:00
pycook
cc98f903ea feat(api): supports bool and reference 2024-08-20 13:49:51 +08:00
kinyXu
fb7471ce04 feat: add attribute sorted tips for non-inherited attributes 2024-08-20 11:48:44 +08:00
Leo Song
e2872f041e Merge pull request #591 from veops/dev_ui_240813
refactor(ui): ci table
2024-08-13 17:15:15 +08:00
songlh
250fde127c refactor(ui): ci table 2024-08-13 17:14:05 +08:00
pycook
73dbb14944 Merge pull request #590 from lgphone/patch-1
bugfix: cmdb-api  auto_discovery add unique_value param
2024-08-07 16:05:56 +08:00
YangEver
73c9a6fa72 bugfix: cmdb-api auto_discovery add unique_value param
自动发现接口需要根据unique_value参数进行数据唯一性校验,此参数为必填项
2024-08-07 15:50:42 +08:00
Leo Song
09d957db79 Merge pull request #589 from veops/dev_ui_240807
Dev UI 240807
2024-08-07 14:42:01 +08:00
songlh
b73d796891 fix(ui): dashboard chart config 2024-08-07 14:41:22 +08:00
songlh
e7cbd0caa9 feat(ui): update common settings btn 2024-08-07 14:40:54 +08:00
pycook
3e4c385d91 fix(api): Dashboard using display attributes 2024-08-06 19:59:16 +08:00
pycook
3aac012ee9 chore: release v2.4.10 2024-07-31 16:42:26 +08:00
Leo Song
78d762cacc Merge pull request #588 from veops/dev_ui_240731
feat(ui): update ci type
2024-07-31 16:01:17 +08:00
songlh
c668ba7d3f feat(ui): update ci type 2024-07-31 16:00:40 +08:00
pycook
542a876ead fix(api): delete item for multi-value attributes 2024-07-30 20:05:21 +08:00
pycook
68b7497bba Merge pull request #587 from thexqn/master
修复在用了计算属性的情况下,批量上传功能可能出现的错误.
2024-07-30 09:18:30 +08:00
thexqn
dfbf3d462d 修复在用了计算属性的情况下,批量上传功能可能出现的错误. 2024-07-30 01:17:53 +08:00
pycook
692708fcba feat(api): Multi-valued attribute values ​​support adding and deleting 2024-07-29 19:55:07 +08:00
97 changed files with 8227 additions and 3106 deletions

View File

@@ -5,9 +5,9 @@ on:
branches:
- master
tags: ["v*"]
pull_request:
branches:
- master
# pull_request:
# branches:
# - master
env:
# Use docker.io for Docker Hub if empty
@@ -49,31 +49,31 @@ jobs:
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ env.REGISTRY_SERVER_ADDRESS }}/cmdb-api:${{ env.TAG }}
release-ui-images:
runs-on: ubuntu-latest
needs: [setup-environment]
permissions:
contents: read
packages: write
timeout-minutes: 90
steps:
- name: Checkout Repo
uses: actions/checkout@v4
- name: Login to GitHub Package Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push CMDB-UI Docker image
uses: docker/build-push-action@v6
with:
file: docker/Dockerfile-UI
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ env.REGISTRY_SERVER_ADDRESS }}/cmdb-ui:${{ env.TAG }}
# release-ui-images:
# runs-on: ubuntu-latest
# needs: [setup-environment]
# permissions:
# contents: read
# packages: write
# timeout-minutes: 90
# steps:
# - name: Checkout Repo
# uses: actions/checkout@v4
# - name: Login to GitHub Package Registry
# uses: docker/login-action@v2
# with:
# registry: ghcr.io
# username: ${{ github.repository_owner }}
# password: ${{ secrets.GITHUB_TOKEN }}
# - name: Set up QEMU
# uses: docker/setup-qemu-action@v3
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v3
# - name: Build and push CMDB-UI Docker image
# uses: docker/build-push-action@v6
# with:
# file: docker/Dockerfile-UI
# context: .
# platforms: linux/amd64,linux/arm64
# push: true
# tags: ${{ env.REGISTRY_SERVER_ADDRESS }}/cmdb-ui:${{ env.TAG }}

View File

@@ -135,6 +135,15 @@ class AttributeManager(object):
choice_table and choice_table.get_by(attr_id=_id, only_query=True).delete()
db.session.flush()
@classmethod
def get_enum_map(cls, _attr_id, _attr=None):
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 {}
@classmethod
def search_attributes(cls, name=None, alias=None, page=1, page_size=None):
"""
@@ -167,24 +176,30 @@ class AttributeManager(object):
def get_attribute_by_name(self, name):
attr = Attribute.get_by(name=name, first=True)
if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"],
attr["choice_web_hook"], attr.get("choice_other"))
attr["choice_value"] = self.get_choice_values(attr["id"],
attr["value_type"],
attr["choice_web_hook"],
attr.get("choice_other"))
return attr
def get_attribute_by_alias(self, alias):
attr = Attribute.get_by(alias=alias, first=True)
if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"],
attr["choice_web_hook"], attr.get("choice_other"))
attr["choice_value"] = self.get_choice_values(attr["id"],
attr["value_type"],
attr["choice_web_hook"],
attr.get("choice_other"))
return attr
def get_attribute_by_id(self, _id):
attr = Attribute.get_by_id(_id).to_dict()
if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"],
attr["choice_web_hook"], attr.get("choice_other"))
attr["choice_value"] = self.get_choice_values(attr["id"],
attr["value_type"],
attr["choice_web_hook"],
attr.get("choice_other"))
return attr

View File

@@ -2,12 +2,13 @@
from __future__ import unicode_literals
from collections import defaultdict
import datetime
import os
import yaml
from flask import current_app
import json
from api.extensions import cache
from api.extensions import db
from api.lib.cmdb.custom_dashboard import CustomDashboardManager
@@ -254,7 +255,7 @@ class CMDBCounterCache(object):
@classmethod
def set(cls, result):
cache.set(cls.KEY, result, timeout=0)
cache.set(cls.KEY, json.loads(json.dumps(result)), timeout=0)
@classmethod
def reset(cls):
@@ -276,7 +277,7 @@ class CMDBCounterCache(object):
cls.set(result)
return result
return json.loads(json.dumps(result))
@classmethod
def update(cls, custom, flush=True):
@@ -298,25 +299,36 @@ class CMDBCounterCache(object):
result[custom['id']] = res
cls.set(result)
return res
return json.loads(json.dumps(res))
@staticmethod
def relation_counter(type_id, level, other_filer, type_ids):
@classmethod
def relation_counter(cls, type_id, level, other_filer, type_ids):
from api.lib.cmdb.search.ci_relation.search import Search as RelSearch
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
from api.lib.cmdb.attribute import AttributeManager
query = "_type:{}".format(type_id)
if other_filer:
query = "{},{}".format(query, other_filer)
s = search(query, count=1000000)
try:
type_names, _, _, _, _, _ = s.search()
except SearchError as e:
current_app.logger.error(e)
return
root_type = CITypeCache.get(type_id)
show_attr_id = root_type and root_type.show_id
show_attr = AttributeCache.get(show_attr_id)
type_id_names = [(str(i.get('_id')), i.get(i.get('unique'))) for i in type_names]
type_id_names = []
for i in type_names:
attr_value = i.get(show_attr and show_attr.name) or i.get(i.get('unique'))
enum_map = AttributeManager.get_enum_map(show_attr_id or i.get('unique'))
s = RelSearch([i[0] for i in type_id_names], level, other_filer or '')
type_id_names.append((str(i.get('_id')), enum_map.get(attr_value, attr_value)))
s = RelSearch([i[0] for i in type_id_names], level)
try:
stats = s.statistics(type_ids, need_filter=False)
except SearchError as e:
@@ -346,11 +358,12 @@ class CMDBCounterCache(object):
return result
@staticmethod
def attribute_counter(custom):
@classmethod
def attribute_counter(cls, custom):
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
from api.lib.cmdb.utils import ValueTypeMap
from api.lib.cmdb.attribute import AttributeManager
custom.setdefault('options', {})
type_id = custom.get('type_id')
@@ -366,16 +379,24 @@ class CMDBCounterCache(object):
other_filter = "{}".format(other_filter) if other_filter else ''
if custom['options'].get('ret') == 'cis':
enum_map = {}
for _attr_id in attr_ids:
_attr = AttributeCache.get(_attr_id)
if _attr:
enum_map[_attr.alias] = AttributeManager.get_enum_map(_attr_id)
query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter)
s = search(query, fl=attr_ids, ret_key='alias', count=100)
try:
cis, _, _, _, _, _ = s.search()
cis = [{k: (enum_map.get(k) or {}).get(v, v) for k, v in ci.items()} for ci in cis]
except SearchError as e:
current_app.logger.error(e)
return
return cis
origin_result = dict()
result = dict()
# level = 1
query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter)
@@ -385,13 +406,18 @@ class CMDBCounterCache(object):
except SearchError as e:
current_app.logger.error(e)
return
enum_map1 = AttributeManager.get_enum_map(attr_ids[0])
for i in (list(facet.values()) or [[]])[0]:
result[ValueTypeMap.serialize2[attr2value_type[0]](str(i[0]))] = i[1]
k = ValueTypeMap.serialize2[attr2value_type[0]](str(i[0]))
result[enum_map1.get(k, k)] = i[1]
origin_result[k] = i[1]
if len(attr_ids) == 1:
return result
# level = 2
for v in result:
enum_map2 = AttributeManager.get_enum_map(attr_ids[1])
for v in origin_result:
query = "_type:({}),{},{}:{}".format(";".join(map(str, type_ids)), other_filter, attr_ids[0], v)
s = search(query, fl=attr_ids, facet=[attr_ids[1]], count=1)
try:
@@ -399,18 +425,22 @@ class CMDBCounterCache(object):
except SearchError as e:
current_app.logger.error(e)
return
result[v] = dict()
result[enum_map1.get(v, v)] = dict()
origin_result[v] = dict()
for i in (list(facet.values()) or [[]])[0]:
result[v][ValueTypeMap.serialize2[attr2value_type[1]](str(i[0]))] = i[1]
k = ValueTypeMap.serialize2[attr2value_type[1]](str(i[0]))
result[enum_map1.get(v, v)][enum_map2.get(k, k)] = i[1]
origin_result[v][k] = i[1]
if len(attr_ids) == 2:
return result
# level = 3
for v1 in result:
if not isinstance(result[v1], dict):
enum_map3 = AttributeManager.get_enum_map(attr_ids[2])
for v1 in origin_result:
if not isinstance(result[enum_map1.get(v1, v1)], dict):
continue
for v2 in result[v1]:
for v2 in origin_result[v1]:
query = "_type:({}),{},{}:{},{}:{}".format(";".join(map(str, type_ids)), other_filter,
attr_ids[0], v1, attr_ids[1], v2)
s = search(query, fl=attr_ids, facet=[attr_ids[2]], count=1)
@@ -419,9 +449,10 @@ class CMDBCounterCache(object):
except SearchError as e:
current_app.logger.error(e)
return
result[v1][v2] = dict()
result[enum_map1.get(v1, v1)][enum_map2.get(v2, v2)] = dict()
for i in (list(facet.values()) or [[]])[0]:
result[v1][v2][ValueTypeMap.serialize2[attr2value_type[2]](str(i[0]))] = i[1]
k = ValueTypeMap.serialize2[attr2value_type[2]](str(i[0]))
result[enum_map1.get(v1, v1)][enum_map2.get(v2, v2)][enum_map3.get(k, k)] = i[1]
return result
@@ -525,19 +556,18 @@ class CMDBCounterCache(object):
@classmethod
def flush_sub_counter(cls):
result = dict(type_id2users=dict())
result = dict(type_id2users=defaultdict(list))
types = db.session.query(PreferenceShowAttributes.type_id,
PreferenceShowAttributes.uid, PreferenceShowAttributes.created_at).filter(
PreferenceShowAttributes.deleted.is_(False)).group_by(
PreferenceShowAttributes.uid, PreferenceShowAttributes.type_id)
for i in types:
result['type_id2users'].setdefault(i.type_id, []).append(i.uid)
result['type_id2users'][i.type_id].append(i.uid)
types = PreferenceTreeView.get_by(to_dict=False)
for i in types:
result['type_id2users'].setdefault(i.type_id, [])
if i.uid not in result['type_id2users'][i.type_id]:
result['type_id2users'][i.type_id].append(i.uid)
@@ -557,11 +587,12 @@ class AutoDiscoveryMappingCache(object):
def get(cls, name):
res = cache.get(cls.PREFIX.format(name)) or {}
if not res:
path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "auto_discovery/mapping/{}.yaml".format(name))
path = os.path.join(os.path.abspath(os.path.dirname(__file__)),
"auto_discovery/mapping/{}.yaml".format(name))
if os.path.exists(path):
with open(path, 'r') as f:
mapping = yaml.safe_load(f)
res = mapping.get('mapping') or {}
res and cache.set(cls.PREFIX.format(name), res, timeout=0)
return res
return res

View File

@@ -161,7 +161,7 @@ class CIManager(object):
@classmethod
def get_ci_by_id_from_db(cls, ci_id, ret_key=RetKey.NAME, fields=None, need_children=True, use_master=False,
valid=False):
valid=False, enum_use_label=False):
"""
:param ci_id:
@@ -170,6 +170,7 @@ class CIManager(object):
:param need_children:
:param use_master: whether to use master db
:param valid:
:param enum_use_label:
:return:
"""
@@ -187,13 +188,19 @@ class CIManager(object):
res["ci_type"] = ci_type.name
fields = CITypeAttributeManager.get_attr_names_by_type_id(ci.type_id) if not fields else fields
enum_map = dict()
if not enum_use_label:
fields = CITypeAttributeManager.get_attr_names_by_type_id(ci.type_id) if not fields else fields
else:
fields, enum_map = CITypeAttributeManager.get_attr_names_label_enum(
ci.type_id) if not fields else (fields, {})
unique_key = AttributeCache.get(ci_type.unique_id)
_res = AttributeValueManager().get_attr_values(fields,
ci_id,
ret_key=ret_key,
unique_key=unique_key,
use_master=use_master)
use_master=use_master,
enum_map=enum_map)
res.update(_res)
res['_type'] = ci_type.id
@@ -266,7 +273,7 @@ class CIManager(object):
value_table = TableMap(attr_name=id2name[attr_id]).table
values = value_table.get_by(attr_id=attr_id,
value=ci_dict.get(id2name[attr_id]) or None,
value=ci_dict.get(id2name[attr_id]),
only_query=True).join(
CI, CI.id == value_table.ci_id).filter(CI.type_id == type_id)
_ci_ids = set([i.ci_id for i in values])
@@ -292,6 +299,53 @@ class CIManager(object):
return 1
@staticmethod
def _reference_to_ci_id(attr, payload):
def __unique_value2id(_type, _v):
value_table = TableMap(attr_name=_type.unique_id).table
ci = value_table.get_by(attr_id=attr.id, value=_v)
if ci is not None:
return ci.ci_id
return abort(400, ErrFormat.ci_reference_invalid.format(attr.alias, _v))
def __valid_reference_id_existed(_id, _type_id):
ci = CI.get_by_id(_id) or abort(404, ErrFormat.ci_reference_not_found.format(attr.alias, _id))
if ci.type_id != _type_id:
return abort(400, ErrFormat.ci_reference_invalid.format(attr.alias, _id))
if attr.name in payload:
k, reference_value = attr.name, payload[attr.name]
elif attr.alias in payload:
k, reference_value = attr.alias, payload[attr.alias]
else:
return
if not reference_value:
return
reference_type = None
if isinstance(reference_value, list):
for idx, v in enumerate(reference_value):
if isinstance(v, dict) and v.get('unique'):
if reference_type is None:
reference_type = CITypeCache.get(attr.reference_type_id)
if reference_type is not None:
reference_value[idx] = __unique_value2id(reference_type, v)
else:
__valid_reference_id_existed(v, attr.reference_type_id)
elif isinstance(reference_value, dict) and reference_value.get('unique'):
if reference_type is None:
reference_type = CITypeCache.get(attr.reference_type_id)
if reference_type is not None:
reference_value = __unique_value2id(reference_type, reference_value)
elif str(reference_value).isdigit():
reference_value = int(reference_value)
__valid_reference_id_existed(reference_value, attr.reference_type_id)
payload[k] = reference_value
@classmethod
def add(cls, ci_type_name,
exist_policy=ExistPolicy.REPLACE,
@@ -328,6 +382,7 @@ class CIManager(object):
ci_type_attrs_name = {attr.name: attr for _, attr in attrs}
ci_type_attrs_alias = {attr.alias: attr for _, attr in attrs}
ci_attr2type_attr = {type_attr.attr_id: type_attr for type_attr, _ in attrs}
ci_type_attrs_name_alias = {**ci_type_attrs_name, **ci_type_attrs_alias}
ci = None
record_id = None
@@ -391,6 +446,8 @@ class CIManager(object):
if attr.re_check and password_dict.get(attr.id):
value_manager.check_re(attr.re_check, attr.alias, password_dict[attr.id][0])
elif attr.is_reference:
cls._reference_to_ci_id(attr, ci_dict)
cls._valid_unique_constraint(ci_type.id, ci_dict, ci and ci.id)
@@ -412,7 +469,7 @@ class CIManager(object):
else:
ci_dict.pop(k)
ci_dict = {k: v for k, v in ci_dict.items() if k in ci_type_attrs_name or k in ci_type_attrs_alias}
ci_dict = {ci_type_attrs_name_alias[k].name: v for k, v in ci_dict.items() if k in ci_type_attrs_name_alias}
key2attr = value_manager.valid_attr_value(ci_dict, ci_type.id, ci and ci.id,
ci_type_attrs_name, ci_type_attrs_alias, ci_attr2type_attr)
@@ -442,9 +499,10 @@ class CIManager(object):
return ci.id
def update(self, ci_id, _is_admin=False, ticket_id=None, __sync=False, **ci_dict):
def update(self, ci_id, _is_admin=False, ticket_id=None, _sync=False, **ci_dict):
now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
ci = self.confirm_ci_existed(ci_id)
ci_type = ci.ci_type
attrs = CITypeAttributeManager.get_all_attributes(ci.type_id)
ci_type_attrs_name = {attr.name: attr for _, attr in attrs}
@@ -474,11 +532,13 @@ class CIManager(object):
if attr.re_check and password_dict.get(attr.id):
value_manager.check_re(attr.re_check, attr.alias, password_dict[attr.id][0])
elif attr.is_reference:
self._reference_to_ci_id(attr, ci_dict)
limit_attrs = self._valid_ci_for_no_read(ci) if not _is_admin else {}
record_id = None
with redis_lock.Lock(rd.r, ci.ci_type.name):
with redis_lock.Lock(rd.r, ci_type.name):
db.session.commit()
self._valid_unique_constraint(ci.type_id, ci_dict, ci_id)
@@ -509,14 +569,14 @@ class CIManager(object):
record_id = self.save_password(ci.id, attr_id, password_dict[attr_id], record_id, ci.type_id)
if record_id or has_dynamic: # has changed
if not __sync:
if not _sync:
ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE)
else:
ci_cache(ci_id, OperateType.UPDATE, record_id)
ref_ci_dict = {k: v for k, v in ci_dict.items() if k.startswith("$") and "." in k}
if ref_ci_dict:
if not __sync:
if not _sync:
ci_relation_add.apply_async(args=(ref_ci_dict, ci.id), queue=CMDB_QUEUE)
else:
ci_relation_add(ref_ci_dict, ci.id)
@@ -577,7 +637,7 @@ class CIManager(object):
if ci_dict:
AttributeHistoryManger.add(None, ci_id, [(None, OperateType.DELETE, ci_dict, None)], ci.type_id)
ci_delete.apply_async(args=(ci_id,), queue=CMDB_QUEUE)
ci_delete.apply_async(args=(ci_id, ci.type_id), queue=CMDB_QUEUE)
delete_id_filter.apply_async(args=(ci_id,), queue=CMDB_QUEUE)
return ci_id
@@ -772,7 +832,7 @@ class CIManager(object):
value_table = ValueTypeMap.table[ValueTypeEnum.PASSWORD]
if current_app.config.get('SECRETS_ENGINE') == 'inner':
if value:
encrypt_value, status = InnerCrypt().encrypt(value)
encrypt_value, status = InnerCrypt().encrypt(str(value))
if not status:
current_app.logger.error('save password failed: {}'.format(encrypt_value))
return abort(400, ErrFormat.password_save_failed.format(encrypt_value))
@@ -800,7 +860,7 @@ class CIManager(object):
vault = VaultClient(current_app.config.get('VAULT_URL'), current_app.config.get('VAULT_TOKEN'))
if value:
try:
vault.update("/{}/{}".format(ci_id, attr_id), dict(v=value))
vault.update("/{}/{}".format(ci_id, attr_id), dict(v=str(value)))
except Exception as e:
current_app.logger.error('save password to vault failed: {}'.format(e))
return abort(400, ErrFormat.password_save_failed.format('write vault failed'))
@@ -1456,7 +1516,8 @@ class CITriggerManager(object):
cls._update_old_attr_value(record_id, ci_dict)
if ci_id is not None:
ci_dict = CIManager().get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
ci_dict = CIManager().get_ci_by_id_from_db(
ci_id, need_children=False, use_master=False, enum_use_label=True)
try:
response = webhook_request(webhook, ci_dict).text
@@ -1483,7 +1544,8 @@ class CITriggerManager(object):
with app.app_context():
if ci_id is not None:
ci_dict = CIManager().get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
ci_dict = CIManager().get_ci_by_id_from_db(
ci_id, need_children=False, use_master=False, enum_use_label=True)
if operate_type == OperateType.UPDATE:
cls._update_old_attr_value(record_id, ci_dict)

View File

@@ -1,5 +1,7 @@
# -*- coding:utf-8 -*-
from collections import defaultdict
import copy
import toposort
from flask import abort
@@ -145,7 +147,7 @@ class CITypeManager(object):
kwargs["alias"] = kwargs["name"] if not kwargs.get("alias") else kwargs["alias"]
cls._validate_unique(name=kwargs['name'])
cls._validate_unique(alias=kwargs['alias'])
# cls._validate_unique(alias=kwargs['alias'])
kwargs["unique_id"] = unique_key.id
kwargs['uid'] = current_user.uid
@@ -184,7 +186,7 @@ class CITypeManager(object):
ci_type = cls.check_is_existed(type_id)
cls._validate_unique(type_id=type_id, name=kwargs.get('name'))
cls._validate_unique(type_id=type_id, alias=kwargs.get('alias') or kwargs.get('name'))
# cls._validate_unique(type_id=type_id, alias=kwargs.get('alias') or kwargs.get('name'))
unique_key = kwargs.pop("unique_key", None)
unique_key = AttributeCache.get(unique_key)
@@ -234,6 +236,10 @@ class CITypeManager(object):
if CITypeInheritance.get_by(parent_id=type_id, first=True):
return abort(400, ErrFormat.ci_type_inheritance_cannot_delete)
reference = Attribute.get_by(reference_type_id=type_id, first=True, to_dict=False)
if reference is not None:
return abort(400, ErrFormat.ci_type_referenced_cannot_delete.format(reference.alias))
relation_views = PreferenceRelationView.get_by(to_dict=False)
for rv in relation_views:
for item in (rv.cr_ids or []):
@@ -343,9 +349,9 @@ class CITypeInheritanceManager(object):
@classmethod
def add(cls, parent_ids, child_id):
rels = {}
rels = defaultdict(set)
for i in cls.cls.get_by(to_dict=False):
rels.setdefault(i.child_id, set()).add(i.parent_id)
rels[i.child_id].add(i.parent_id)
try:
toposort_flatten(rels)
@@ -359,7 +365,7 @@ class CITypeInheritanceManager(object):
existed = cls.cls.get_by(parent_id=parent_id, child_id=child_id, first=True, to_dict=False)
if existed is None:
rels.setdefault(child_id, set()).add(parent_id)
rels[child_id].add(parent_id)
try:
toposort_flatten(rels)
except toposort.CircularDependencyError as e:
@@ -500,14 +506,13 @@ class CITypeAttributeManager(object):
def __init__(self):
pass
@staticmethod
def get_attr_name(ci_type_name, key):
@classmethod
def get_attr_name(cls, ci_type_name, key):
ci_type = CITypeCache.get(ci_type_name)
if ci_type is None:
return
for i in CITypeAttributesCache.get(ci_type.id):
attr = AttributeCache.get(i.attr_id)
for _, attr in cls.get_all_attributes(ci_type.id):
if attr and (attr.name == key or attr.alias == key):
return attr.name
@@ -525,6 +530,18 @@ class CITypeAttributeManager(object):
def get_attr_names_by_type_id(cls, type_id):
return [attr.name for _, attr in cls.get_all_attributes(type_id)]
@classmethod
def get_attr_names_label_enum(cls, type_id):
attr_names, enum_map = list(), defaultdict(dict)
for _, attr in cls.get_all_attributes(type_id):
attr_names.append(attr.name)
if attr.is_choice and not attr.choice_other and not attr.choice_web_hook:
_map = AttributeManager.get_enum_map(attr.id)
if _map:
enum_map[attr.name].update(_map)
return attr_names, enum_map
@staticmethod
def get_attributes_by_type_id(type_id, choice_web_hook_parse=True, choice_other_parse=True):
has_config_perm = ACLManager('cmdb').has_permission(
@@ -565,10 +582,10 @@ class CITypeAttributeManager(object):
CITypeManager.get_name_by_id(type_id), ResourceTypeEnum.CI, PermEnum.CONFIG)
result = {type_id: [i for _, i in cls.get_all_attributes(type_id)] for type_id in type_ids}
attr2types = {}
attr2types = defaultdict(list)
for type_id in result:
for i in result[type_id]:
attr2types.setdefault(i.id, []).append(type_id)
attr2types[i.id].append(type_id)
attrs = []
for attr_id in attr2types:
@@ -846,12 +863,12 @@ class CITypeRelationManager(object):
@classmethod
def recursive_level2children(cls, parent_id):
result = dict()
result = defaultdict(list)
def get_children(_id, level):
children = CITypeRelation.get_by(parent_id=_id, to_dict=False)
if children:
result.setdefault(level + 1, []).extend([i.child.to_dict() for i in children])
result[level + 1].extend([i.child.to_dict() for i in children])
for i in children:
if i.child_id != _id:
@@ -950,10 +967,10 @@ class CITypeRelationManager(object):
p = CITypeManager.check_is_existed(parent)
c = CITypeManager.check_is_existed(child)
rels = {}
rels = defaultdict(set)
for i in CITypeRelation.get_by(to_dict=False):
rels.setdefault(i.child_id, set()).add(i.parent_id)
rels.setdefault(c.id, set()).add(p.id)
rels[i.child_id].add(i.parent_id)
rels[c.id].add(p.id)
try:
toposort_flatten(rels)
@@ -1323,6 +1340,7 @@ class CITypeTemplateManager(object):
def _import_attributes(self, type2attributes):
attributes = [attr for type_id in type2attributes for attr in type2attributes[type_id]]
attrs = []
references = []
for i in copy.deepcopy(attributes):
if i.pop('inherited', None):
continue
@@ -1337,6 +1355,10 @@ class CITypeTemplateManager(object):
if not choice_value:
i['is_choice'] = False
if i.get('reference_type_id'):
references.append(copy.deepcopy(i))
i.pop('reference_type_id')
attrs.append((i, choice_value))
attr_id_map = self.__import(Attribute, [i[0] for i in copy.deepcopy(attrs)])
@@ -1345,7 +1367,7 @@ class CITypeTemplateManager(object):
if choice_value and not i.get('choice_web_hook') and not i.get('choice_other'):
AttributeManager.add_choice_values(attr_id_map.get(i['id'], i['id']), i['value_type'], choice_value)
return attr_id_map
return attr_id_map, references
def _import_ci_types(self, ci_types, attr_id_map):
for i in ci_types:
@@ -1359,6 +1381,11 @@ class CITypeTemplateManager(object):
return self.__import(CIType, ci_types)
def _import_reference_attributes(self, attrs, type_id_map):
for attr in attrs:
attr['reference_type_id'] = type_id_map.get(attr['reference_type_id'], attr['reference_type_id'])
self.__import(Attribute, attrs)
def _import_ci_type_groups(self, ci_type_groups, type_id_map):
_ci_type_groups = copy.deepcopy(ci_type_groups)
for i in _ci_type_groups:
@@ -1584,13 +1611,15 @@ class CITypeTemplateManager(object):
import time
s = time.time()
attr_id_map = self._import_attributes(tpt.get('type2attributes') or {})
attr_id_map, references = self._import_attributes(tpt.get('type2attributes') or {})
current_app.logger.info('import attributes cost: {}'.format(time.time() - s))
s = time.time()
ci_type_id_map = self._import_ci_types(tpt.get('ci_types') or [], attr_id_map)
current_app.logger.info('import ci_types cost: {}'.format(time.time() - s))
self._import_reference_attributes(references, ci_type_id_map)
s = time.time()
self._import_ci_type_groups(tpt.get('ci_type_groups') or [], ci_type_id_map)
current_app.logger.info('import ci_type_groups cost: {}'.format(time.time() - s))
@@ -1675,6 +1704,16 @@ class CITypeTemplateManager(object):
type_ids.extend(extend_type_ids)
ci_types.extend(CITypeManager.get_ci_types(type_ids=extend_type_ids))
# handle reference type
references = Attribute.get_by(only_query=True).join(
CITypeAttribute, CITypeAttribute.attr_id == Attribute.id).filter(
CITypeAttribute.type_id.in_(type_ids)).filter(CITypeAttribute.deleted.is_(False)).filter(
Attribute.reference_type_id.isnot(None))
reference_type_ids = list(set([i.reference_type_id for i in references if i.reference_type_id]))
if reference_type_ids:
type_ids.extend(reference_type_ids)
ci_types.extend(CITypeManager.get_ci_types(type_ids=reference_type_ids))
tpt = dict(
ci_types=ci_types,
relation_types=[i.to_dict() for i in RelationTypeManager.get_all()],
@@ -1687,6 +1726,7 @@ class CITypeTemplateManager(object):
icons=dict()
)
tpt['ci_type_groups'] = CITypeGroupManager.get(ci_types=tpt['ci_types'], type_ids=type_ids)
tpt['ci_type_groups'] = [i for i in tpt['ci_type_groups'] if i.get('ci_types')]
def get_icon_value(icon):
try:

View File

@@ -14,6 +14,8 @@ class ValueTypeEnum(BaseEnum):
JSON = "6"
PASSWORD = TEXT
LINK = TEXT
BOOL = "7"
REFERENCE = INT
class ConstraintEnum(BaseEnum):

View File

@@ -1,8 +1,9 @@
# -*- coding:utf-8 -*-
import copy
from collections import defaultdict
import copy
import six
import toposort
from flask import abort
@@ -263,12 +264,12 @@ class PreferenceManager(object):
else:
views = _views
view2cr_ids = dict()
view2cr_ids = defaultdict(list)
name2view = dict()
result = dict()
name2id = list()
for view in views:
view2cr_ids.setdefault(view['name'], []).extend(view['cr_ids'])
view2cr_ids[view['name']].extend(view['cr_ids'])
name2id.append([view['name'], view['id']])
name2view[view['name']] = view

View File

@@ -44,6 +44,8 @@ class ErrFormat(CommonErrFormat):
unique_value_not_found = _l("The model's primary key {} does not exist!") # 模型的主键 {} 不存在!
unique_key_required = _l("Primary key {} is missing") # 主键字段 {} 缺失
ci_is_already_existed = _l("CI already exists!") # CI 已经存在!
ci_reference_not_found = _l("{}: CI reference {} does not exist!") # {}: CI引用 {} 不存在!
ci_reference_invalid = _l("{}: CI reference {} is illegal!") # {}, CI引用 {} 不合法!
relation_constraint = _l("Relationship constraint: {}, verification failed") # 关系约束: {}, 校验失败
# 多对多关系 限制: 模型 {} <-> {} 已经存在多对多关系!
m2m_relation_constraint = _l(
@@ -63,6 +65,8 @@ class ErrFormat(CommonErrFormat):
ci_exists_and_cannot_delete_inheritance = _l(
"The inheritance cannot be deleted because the CI already exists") # 因为CI已经存在不能删除继承关系
ci_type_inheritance_cannot_delete = _l("The model is inherited and cannot be deleted") # 该模型被继承, 不能删除
ci_type_referenced_cannot_delete = _l(
"The model is referenced by attribute {} and cannot be deleted") # 该模型被属性 {} 引用, 不能删除
# 因为关系视图 {} 引用了该模型,不能删除模型
ci_relation_view_exists_and_cannot_delete_type = _l(

View File

@@ -56,7 +56,7 @@ QUERY_CI_BY_ATTR_NAME = """
SELECT {0}.ci_id
FROM {0}
WHERE {0}.attr_id={1:d}
AND {0}.value {2}
AND ({0}.value {2})
"""
QUERY_CI_BY_ID = """

View File

@@ -451,6 +451,9 @@ class Search(object):
if field_type == ValueTypeEnum.DATE and len(v) == 10:
v = "{} 00:00:00".format(v)
if field_type == ValueTypeEnum.BOOL and "*" not in str(v):
v = str(int(v in current_app.config.get('BOOL_TRUE')))
# in query
if v.startswith("(") and v.endswith(")"):
_query_sql = self._in_query_handler(attr, v, is_not)

View File

@@ -7,6 +7,7 @@ import json
import re
import six
from flask import current_app
import api.models.cmdb as model
from api.lib.cmdb.cache import AttributeCache
@@ -64,6 +65,7 @@ class ValueTypeMap(object):
ValueTypeEnum.DATETIME: str2datetime,
ValueTypeEnum.DATE: str2date,
ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x,
ValueTypeEnum.BOOL: lambda x: x in current_app.config.get('BOOL_TRUE'),
}
serialize = {
@@ -74,6 +76,7 @@ class ValueTypeMap(object):
ValueTypeEnum.DATE: lambda x: x.strftime("%Y-%m-%d") if not isinstance(x, six.string_types) else x,
ValueTypeEnum.DATETIME: lambda x: x.strftime("%Y-%m-%d %H:%M:%S") if not isinstance(x, six.string_types) else x,
ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x,
ValueTypeEnum.BOOL: lambda x: x in current_app.config.get('BOOL_TRUE'),
}
serialize2 = {
@@ -84,6 +87,7 @@ class ValueTypeMap(object):
ValueTypeEnum.DATE: lambda x: (x.decode() if not isinstance(x, six.string_types) else x).split()[0],
ValueTypeEnum.DATETIME: lambda x: x.decode() if not isinstance(x, six.string_types) else x,
ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x,
ValueTypeEnum.BOOL: lambda x: x in current_app.config.get('BOOL_TRUE'),
}
choice = {
@@ -105,6 +109,7 @@ class ValueTypeMap(object):
'index_{0}'.format(ValueTypeEnum.TIME): model.CIIndexValueText,
'index_{0}'.format(ValueTypeEnum.FLOAT): model.CIIndexValueFloat,
'index_{0}'.format(ValueTypeEnum.JSON): model.CIValueJson,
'index_{0}'.format(ValueTypeEnum.BOOL): model.CIIndexValueInteger,
}
table_name = {
@@ -117,6 +122,7 @@ class ValueTypeMap(object):
'index_{0}'.format(ValueTypeEnum.TIME): 'c_value_index_texts',
'index_{0}'.format(ValueTypeEnum.FLOAT): 'c_value_index_floats',
'index_{0}'.format(ValueTypeEnum.JSON): 'c_value_json',
'index_{0}'.format(ValueTypeEnum.BOOL): 'c_value_index_integers',
}
es_type = {

View File

@@ -3,13 +3,13 @@
from __future__ import unicode_literals
import copy
import imp
import copy
import jinja2
import os
import re
import tempfile
import jinja2
from flask import abort
from flask import current_app
from jinja2schema import infer
@@ -47,7 +47,7 @@ class AttributeValueManager(object):
"""
return AttributeCache.get(key)
def get_attr_values(self, fields, ci_id, ret_key="name", unique_key=None, use_master=False):
def get_attr_values(self, fields, ci_id, ret_key="name", unique_key=None, use_master=False, enum_map=None):
"""
:param fields:
@@ -55,6 +55,7 @@ class AttributeValueManager(object):
:param ret_key: It can be name or alias
:param unique_key: primary attribute
:param use_master: Only for master-slave read-write separation
:param enum_map:
:return:
"""
res = dict()
@@ -76,6 +77,12 @@ class AttributeValueManager(object):
else:
res[field_name] = ValueTypeMap.serialize[attr.value_type](rs[0].value) if rs else None
if enum_map and field_name in enum_map:
if attr.is_list:
res[field_name] = [enum_map[field_name].get(i, i) for i in res[field_name]]
else:
res[field_name] = enum_map[field_name].get(res[field_name], res[field_name])
if unique_key is not None and attr.id == unique_key.id and rs:
res['unique'] = unique_key.name
res['unique_alias'] = unique_key.alias
@@ -128,14 +135,20 @@ class AttributeValueManager(object):
return abort(400, ErrFormat.attribute_value_invalid2.format(alias, value))
def _validate(self, attr, value, value_table, ci=None, type_id=None, ci_id=None, type_attr=None):
ci = ci or {}
v = self._deserialize_value(attr.alias, attr.value_type, value)
if not attr.is_reference:
ci = ci or {}
v = self._deserialize_value(attr.alias, attr.value_type, value)
attr.is_choice and value and self._check_is_choice(attr, attr.value_type, v)
else:
v = value or None
attr.is_choice and value and self._check_is_choice(attr, attr.value_type, v)
attr.is_unique and self._check_is_unique(
value_table, attr, ci and ci.id or ci_id, ci and ci.type_id or type_id, v)
self._check_is_required(ci and ci.type_id or type_id, attr, v, type_attr=type_attr)
if attr.is_reference:
return v
if v == "" and attr.value_type not in (ValueTypeEnum.TEXT,):
v = None
@@ -235,10 +248,19 @@ class AttributeValueManager(object):
try:
if attr.is_list:
if isinstance(value, dict):
if value.get('op') == "delete":
value['v'] = [ValueTypeMap.serialize[attr.value_type](
self._deserialize_value(attr.alias, attr.value_type, i))
for i in handle_arg_list(value['v'])]
continue
_value = value.get('v') or []
else:
_value = value
value_list = [self._validate(attr, i, value_table, ci=None, type_id=type_id, ci_id=ci_id,
type_attr=ci_attr2type_attr.get(attr.id))
for i in handle_arg_list(value)]
ci_dict[key] = value_list
for i in handle_arg_list(_value)]
ci_dict[key] = value_list if not isinstance(value, dict) else dict(op=value.get('op'), v=value_list)
if not value_list:
self._check_is_required(type_id, attr, '')
@@ -278,28 +300,47 @@ class AttributeValueManager(object):
existed_values = [(ValueTypeMap.serialize[attr.value_type](i.value) if
i.value or i.value == 0 else i.value) for i in existed_attrs]
# Comparison array starts from which position changes
min_len = min(len(value), len(existed_values))
index = 0
while index < min_len:
if value[index] != existed_values[index]:
break
index += 1
if isinstance(value, dict):
if value.get('op') == "add":
for v in (value.get('v') or []):
if v not in existed_values:
value_table.create(ci_id=ci.id, attr_id=attr.id, value=v, flush=False, commit=False)
if not attr.is_dynamic:
changed.append((ci.id, attr.id, OperateType.ADD, None, v, ci.type_id))
else:
has_dynamic = True
# Delete first and then add to ensure id sorting
for idx in range(index, len(existed_attrs)):
existed_attr = existed_attrs[idx]
existed_attr.delete(flush=False, commit=False)
if not attr.is_dynamic:
changed.append((ci.id, attr.id, OperateType.DELETE, existed_values[idx], None, ci.type_id))
else:
has_dynamic = True
for idx in range(index, len(value)):
value_table.create(ci_id=ci.id, attr_id=attr.id, value=value[idx], flush=False, commit=False)
if not attr.is_dynamic:
changed.append((ci.id, attr.id, OperateType.ADD, None, value[idx], ci.type_id))
else:
has_dynamic = True
elif value.get('op') == "delete":
for v in (value.get('v') or []):
if v in existed_values:
existed_attrs[existed_values.index(v)].delete(flush=False, commit=False)
if not attr.is_dynamic:
changed.append((ci.id, attr.id, OperateType.DELETE, v, None, ci.type_id))
else:
has_dynamic = True
else:
# Comparison array starts from which position changes
min_len = min(len(value), len(existed_values))
index = 0
while index < min_len:
if value[index] != existed_values[index]:
break
index += 1
# Delete first and then add to ensure id sorting
for idx in range(index, len(existed_attrs)):
existed_attr = existed_attrs[idx]
existed_attr.delete(flush=False, commit=False)
if not attr.is_dynamic:
changed.append((ci.id, attr.id, OperateType.DELETE, existed_values[idx], None, ci.type_id))
else:
has_dynamic = True
for idx in range(index, len(value)):
value_table.create(ci_id=ci.id, attr_id=attr.id, value=value[idx], flush=False, commit=False)
if not attr.is_dynamic:
changed.append((ci.id, attr.id, OperateType.ADD, None, value[idx], ci.type_id))
else:
has_dynamic = True
else:
existed_attr = value_table.get_by(attr_id=attr.id, ci_id=ci.id, first=True, to_dict=False)
existed_value = existed_attr and existed_attr.value

View File

@@ -58,7 +58,7 @@ def _request_messenger(subject, body, tos, sender, payload):
def notify_send(subject, body, methods, tos, payload=None):
payload = payload or {}
payload = {k: v or '' for k, v in payload.items()}
payload = {k: '' if v is None else v for k, v in payload.items()}
subject = Template(subject).render(payload)
body = Template(body).render(payload)

View File

@@ -105,6 +105,10 @@ class Attribute(Model):
is_password = db.Column(db.Boolean, default=False)
is_sortable = db.Column(db.Boolean, default=False)
is_dynamic = db.Column(db.Boolean, default=False)
is_bool = db.Column(db.Boolean, default=False)
is_reference = db.Column(db.Boolean, default=False)
reference_type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'))
default = db.Column(db.JSON) # {"default": None}

View File

@@ -20,10 +20,12 @@ from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2
from api.lib.cmdb.const import RelationSourceEnum
from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.cmdb.utils import TableMap
from api.lib.decorator import flush_db
from api.lib.decorator import reconnect_db
from api.lib.perm.acl.cache import UserCache
from api.lib.utils import handle_arg_list
from api.models.cmdb import Attribute
from api.models.cmdb import AutoDiscoveryCI
from api.models.cmdb import AutoDiscoveryCIType
from api.models.cmdb import AutoDiscoveryCITypeRelation
@@ -38,6 +40,7 @@ from api.models.cmdb import CITypeAttribute
def ci_cache(ci_id, operate_type, record_id):
from api.lib.cmdb.ci import CITriggerManager
from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.ci_type import CITypeAttributeManager
m = api.lib.cmdb.ci.CIManager()
ci_dict = m.get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
@@ -53,7 +56,17 @@ def ci_cache(ci_id, operate_type, record_id):
current_app.test_request_context().push()
login_user(UserCache.get('worker'))
CITriggerManager.fire(operate_type, ci_dict, record_id)
_, enum_map = CITypeAttributeManager.get_attr_names_label_enum(ci_dict.get('_type'))
payload = dict()
for k, v in ci_dict.items():
if k in enum_map:
if isinstance(v, list):
payload[k] = [enum_map[k].get(i, i) for i in v]
else:
payload[k] = enum_map[k].get(v, v)
else:
payload[k] = v
CITriggerManager.fire(operate_type, payload, record_id)
ci_dict and CIRelationManager.build_by_attribute(ci_dict)
@@ -84,7 +97,7 @@ def batch_ci_cache(ci_ids, ): # only for attribute change index
@celery.task(name="cmdb.ci_delete", queue=CMDB_QUEUE)
@reconnect_db
def ci_delete(ci_id):
def ci_delete(ci_id, type_id):
current_app.logger.info(ci_id)
if current_app.config.get("USE_ES"):
@@ -99,6 +112,12 @@ def ci_delete(ci_id):
adt.update(updated_at=datetime.datetime.now())
instance.delete()
for attr in Attribute.get_by(reference_type_id=type_id, to_dict=False):
table = TableMap(attr=attr).table
for i in getattr(table, 'get_by')(attr_id=attr.id, value=ci_id, to_dict=False):
i.delete()
ci_cache(i.ci_id, None, None)
current_app.logger.info("{0} delete..........".format(ci_id))
@@ -188,7 +207,7 @@ def ci_relation_add(parent_dict, child_id, uid):
for ci in response:
try:
CIRelationManager.add(ci['_id'], child_id)
ci_relation_cache(ci['_id'], child_id)
ci_relation_cache(ci['_id'], child_id, None)
except Exception as e:
current_app.logger.warning(e)
finally:

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2024-06-20 19:12+0800\n"
"POT-Creation-Date: 2024-08-20 13:47+0800\n"
"PO-Revision-Date: 2023-12-25 20:21+0800\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: zh\n"
@@ -16,7 +16,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.14.0\n"
"Generated-By: Babel 2.16.0\n"
#: api/lib/resp_format.py:7
msgid "unauthorized"
@@ -169,8 +169,8 @@ msgstr "目前只允许 属性创建人、管理员 删除属性!"
#: api/lib/cmdb/resp_format.py:37
msgid ""
"Attribute field names cannot be built-in fields: id, _id, ci_id, type, "
"_type, ci_type"
msgstr "属性字段名不能是内置字段: id, _id, ci_id, type, _type, ci_type"
"_type, ci_type, ticket_id"
msgstr "属性字段名不能是内置字段: id, _id, ci_id, type, _type, ci_type, ci_type, ticket_id"
#: api/lib/cmdb/resp_format.py:39
msgid "Predefined value: Other model request parameters are illegal!"
@@ -197,286 +197,298 @@ msgid "CI already exists!"
msgstr "CI 已经存在!"
#: api/lib/cmdb/resp_format.py:47
msgid "{}: CI reference {} does not exist!"
msgstr "{}: CI引用 {} 不存在!"
#: api/lib/cmdb/resp_format.py:48
msgid "{}: CI reference {} is illegal!"
msgstr "{}, CI引用 {} 不合法!"
#: api/lib/cmdb/resp_format.py:49
msgid "Relationship constraint: {}, verification failed"
msgstr "关系约束: {}, 校验失败"
#: api/lib/cmdb/resp_format.py:49
#: api/lib/cmdb/resp_format.py:51
msgid ""
"Many-to-many relationship constraint: Model {} <-> {} already has a many-"
"to-many relationship!"
msgstr "多对多关系 限制: 模型 {} <-> {} 已经存在多对多关系!"
#: api/lib/cmdb/resp_format.py:52
#: api/lib/cmdb/resp_format.py:54
msgid "CI relationship: {} does not exist"
msgstr "CI关系: {} 不存在"
#: api/lib/cmdb/resp_format.py:55
#: api/lib/cmdb/resp_format.py:57
msgid "In search expressions, not supported before parentheses: or, not"
msgstr "搜索表达式里小括号前不支持: 或、非"
#: api/lib/cmdb/resp_format.py:57
#: api/lib/cmdb/resp_format.py:59
msgid "Model {} does not exist"
msgstr "模型 {} 不存在"
#: api/lib/cmdb/resp_format.py:58
#: api/lib/cmdb/resp_format.py:60
msgid "Model {} already exists"
msgstr "模型 {} 已经存在"
#: api/lib/cmdb/resp_format.py:59
#: api/lib/cmdb/resp_format.py:61
msgid "The primary key is undefined or has been deleted"
msgstr "主键未定义或者已被删除"
#: api/lib/cmdb/resp_format.py:60
#: api/lib/cmdb/resp_format.py:62
msgid "Only the creator can delete it!"
msgstr "只有创建人才能删除它!"
#: api/lib/cmdb/resp_format.py:61
#: api/lib/cmdb/resp_format.py:63
msgid "The model cannot be deleted because the CI already exists"
msgstr "因为CI已经存在不能删除模型"
#: api/lib/cmdb/resp_format.py:63
#: api/lib/cmdb/resp_format.py:65
msgid "The inheritance cannot be deleted because the CI already exists"
msgstr "因为CI已经存在不能删除继承关系"
#: api/lib/cmdb/resp_format.py:65
#: api/lib/cmdb/resp_format.py:67
msgid "The model is inherited and cannot be deleted"
msgstr "该模型被继承, 不能删除"
#: api/lib/cmdb/resp_format.py:68
msgid "The model is referenced by attribute {} and cannot be deleted"
msgstr "该模型被属性 {} 引用, 不能删除"
#: api/lib/cmdb/resp_format.py:72
msgid ""
"The model cannot be deleted because the model is referenced by the "
"relational view {}"
msgstr "因为关系视图 {} 引用了该模型,不能删除模型"
#: api/lib/cmdb/resp_format.py:70
#: api/lib/cmdb/resp_format.py:74
msgid "Model group {} does not exist"
msgstr "模型分组 {} 不存在"
#: api/lib/cmdb/resp_format.py:71
#: api/lib/cmdb/resp_format.py:75
msgid "Model group {} already exists"
msgstr "模型分组 {} 已经存在"
#: api/lib/cmdb/resp_format.py:72
#: api/lib/cmdb/resp_format.py:76
msgid "Model relationship {} does not exist"
msgstr "模型关系 {} 不存在"
#: api/lib/cmdb/resp_format.py:73
#: api/lib/cmdb/resp_format.py:77
msgid "Attribute group {} already exists"
msgstr "属性分组 {} 已存在"
#: api/lib/cmdb/resp_format.py:74
#: api/lib/cmdb/resp_format.py:78
msgid "Attribute group {} does not exist"
msgstr "属性分组 {} 不存在"
#: api/lib/cmdb/resp_format.py:76
#: api/lib/cmdb/resp_format.py:80
msgid "Attribute group <{0}> - attribute <{1}> does not exist"
msgstr "属性组<{0}> - 属性<{1}> 不存在"
#: api/lib/cmdb/resp_format.py:77
#: api/lib/cmdb/resp_format.py:81
msgid "The unique constraint already exists!"
msgstr "唯一约束已经存在!"
#: api/lib/cmdb/resp_format.py:79
#: api/lib/cmdb/resp_format.py:83
msgid "Uniquely constrained attributes cannot be JSON and multi-valued"
msgstr "唯一约束的属性不能是 JSON 和 多值"
#: api/lib/cmdb/resp_format.py:80
#: api/lib/cmdb/resp_format.py:84
msgid "Duplicated trigger"
msgstr "重复的触发器"
#: api/lib/cmdb/resp_format.py:81
#: api/lib/cmdb/resp_format.py:85
msgid "Trigger {} does not exist"
msgstr "触发器 {} 不存在"
#: api/lib/cmdb/resp_format.py:82
#: api/lib/cmdb/resp_format.py:86
msgid "Duplicated reconciliation rule"
msgstr ""
#: api/lib/cmdb/resp_format.py:83
#: api/lib/cmdb/resp_format.py:87
msgid "Reconciliation rule {} does not exist"
msgstr "关系类型 {} 不存在"
#: api/lib/cmdb/resp_format.py:85
#: api/lib/cmdb/resp_format.py:89
msgid "Operation record {} does not exist"
msgstr "操作记录 {} 不存在"
#: api/lib/cmdb/resp_format.py:86
#: api/lib/cmdb/resp_format.py:90
msgid "Unique identifier cannot be deleted"
msgstr "不能删除唯一标识"
#: api/lib/cmdb/resp_format.py:87
#: api/lib/cmdb/resp_format.py:91
msgid "Cannot delete default sorted attributes"
msgstr "不能删除默认排序的属性"
#: api/lib/cmdb/resp_format.py:89
#: api/lib/cmdb/resp_format.py:93
msgid "No node selected"
msgstr "没有选择节点"
#: api/lib/cmdb/resp_format.py:90
#: api/lib/cmdb/resp_format.py:94
msgid "This search option does not exist!"
msgstr "该搜索选项不存在!"
#: api/lib/cmdb/resp_format.py:91
#: api/lib/cmdb/resp_format.py:95
msgid "This search option has a duplicate name!"
msgstr "该搜索选项命名重复!"
#: api/lib/cmdb/resp_format.py:93
#: api/lib/cmdb/resp_format.py:97
msgid "Relationship type {} already exists"
msgstr "关系类型 {} 已经存在"
#: api/lib/cmdb/resp_format.py:94
#: api/lib/cmdb/resp_format.py:98
msgid "Relationship type {} does not exist"
msgstr "关系类型 {} 不存在"
#: api/lib/cmdb/resp_format.py:96
#: api/lib/cmdb/resp_format.py:100
msgid "Invalid attribute value: {}"
msgstr "无效的属性值: {}"
#: api/lib/cmdb/resp_format.py:97
#: api/lib/cmdb/resp_format.py:101
msgid "{} Invalid value: {}"
msgstr "{} 无效的值: {}"
#: api/lib/cmdb/resp_format.py:98
#: api/lib/cmdb/resp_format.py:102
msgid "{} is not in the predefined values"
msgstr "{} 不在预定义值里"
#: api/lib/cmdb/resp_format.py:100
#: api/lib/cmdb/resp_format.py:104
msgid "The value of attribute {} must be unique, {} already exists"
msgstr "属性 {} 的值必须是唯一的, 当前值 {} 已存在"
#: api/lib/cmdb/resp_format.py:101
#: api/lib/cmdb/resp_format.py:105
msgid "Attribute {} value must exist"
msgstr "属性 {} 值必须存在"
#: api/lib/cmdb/resp_format.py:102
#: api/lib/cmdb/resp_format.py:106
msgid "Out of range value, the maximum value is 2147483647"
msgstr "超过最大值限制, 最大值是2147483647"
#: api/lib/cmdb/resp_format.py:104
#: api/lib/cmdb/resp_format.py:108
msgid "Unknown error when adding or modifying attribute value: {}"
msgstr "新增或者修改属性值未知错误: {}"
#: api/lib/cmdb/resp_format.py:106
#: api/lib/cmdb/resp_format.py:110
msgid "Duplicate custom name"
msgstr "订制名重复"
#: api/lib/cmdb/resp_format.py:108
#: api/lib/cmdb/resp_format.py:112
msgid "Number of models exceeds limit: {}"
msgstr "模型数超过限制: {}"
#: api/lib/cmdb/resp_format.py:109
#: api/lib/cmdb/resp_format.py:113
msgid "The number of CIs exceeds the limit: {}"
msgstr "CI数超过限制: {}"
#: api/lib/cmdb/resp_format.py:111
#: api/lib/cmdb/resp_format.py:115
msgid "Auto-discovery rule: {} already exists!"
msgstr "自动发现规则: {} 已经存在!"
#: api/lib/cmdb/resp_format.py:112
#: api/lib/cmdb/resp_format.py:116
msgid "Auto-discovery rule: {} does not exist!"
msgstr "自动发现规则: {} 不存在!"
#: api/lib/cmdb/resp_format.py:114
#: api/lib/cmdb/resp_format.py:118
msgid "This auto-discovery rule is referenced by the model and cannot be deleted!"
msgstr "该自动发现规则被模型引用, 不能删除!"
#: api/lib/cmdb/resp_format.py:116
#: api/lib/cmdb/resp_format.py:120
msgid "The application of auto-discovery rules cannot be defined repeatedly!"
msgstr "自动发现规则的应用不能重复定义!"
#: api/lib/cmdb/resp_format.py:117
#: api/lib/cmdb/resp_format.py:121
msgid "The auto-discovery you want to modify: {} does not exist!"
msgstr "您要修改的自动发现: {} 不存在!"
#: api/lib/cmdb/resp_format.py:118
#: api/lib/cmdb/resp_format.py:122
msgid "Attribute does not include unique identifier: {}"
msgstr "属性字段没有包括唯一标识: {}"
#: api/lib/cmdb/resp_format.py:119
#: api/lib/cmdb/resp_format.py:123
msgid "The auto-discovery instance does not exist!"
msgstr "自动发现的实例不存在!"
#: api/lib/cmdb/resp_format.py:120
#: api/lib/cmdb/resp_format.py:124
msgid "The model is not associated with this auto-discovery!"
msgstr "模型并未关联该自动发现!"
#: api/lib/cmdb/resp_format.py:121
#: api/lib/cmdb/resp_format.py:125
msgid "Only the creator can modify the Secret!"
msgstr "只有创建人才能修改Secret!"
#: api/lib/cmdb/resp_format.py:123
#: api/lib/cmdb/resp_format.py:127
msgid "This rule already has auto-discovery instances and cannot be deleted!"
msgstr "该规则已经有自动发现的实例, 不能被删除!"
#: api/lib/cmdb/resp_format.py:125
#: api/lib/cmdb/resp_format.py:129
msgid "The default auto-discovery rule is already referenced by model {}!"
msgstr "该默认的自动发现规则 已经被模型 {} 引用!"
#: api/lib/cmdb/resp_format.py:127
#: api/lib/cmdb/resp_format.py:131
msgid "The unique_key method must return a non-empty string!"
msgstr "unique_key方法必须返回非空字符串!"
#: api/lib/cmdb/resp_format.py:128
#: api/lib/cmdb/resp_format.py:132
msgid "The attributes method must return a list"
msgstr "attributes方法必须返回的是list"
#: api/lib/cmdb/resp_format.py:130
#: api/lib/cmdb/resp_format.py:134
msgid "The list returned by the attributes method cannot be empty!"
msgstr "attributes方法返回的list不能为空!"
#: api/lib/cmdb/resp_format.py:132
#: api/lib/cmdb/resp_format.py:136
msgid "Only administrators can define execution targets as: all nodes!"
msgstr "只有管理员才可以定义执行机器为: 所有节点!"
#: api/lib/cmdb/resp_format.py:133
#: api/lib/cmdb/resp_format.py:137
msgid "Execute targets permission check failed: {}"
msgstr "执行机器权限检查不通过: {}"
#: api/lib/cmdb/resp_format.py:135
#: api/lib/cmdb/resp_format.py:139
msgid "CI filter authorization must be named!"
msgstr "CI过滤授权 必须命名!"
#: api/lib/cmdb/resp_format.py:136
#: api/lib/cmdb/resp_format.py:140
msgid "CI filter authorization is currently not supported or query"
msgstr "CI过滤授权 暂时不支持 或 查询"
#: api/lib/cmdb/resp_format.py:139
#: api/lib/cmdb/resp_format.py:143
msgid "You do not have permission to operate attribute {}!"
msgstr "您没有属性 {} 的操作权限!"
#: api/lib/cmdb/resp_format.py:140
#: api/lib/cmdb/resp_format.py:144
msgid "You do not have permission to operate this CI!"
msgstr "您没有该CI的操作权限!"
#: api/lib/cmdb/resp_format.py:142
#: api/lib/cmdb/resp_format.py:146
msgid "Failed to save password: {}"
msgstr "保存密码失败: {}"
#: api/lib/cmdb/resp_format.py:143
#: api/lib/cmdb/resp_format.py:147
msgid "Failed to get password: {}"
msgstr "获取密码失败: {}"
#: api/lib/cmdb/resp_format.py:145
#: api/lib/cmdb/resp_format.py:149
msgid "Scheduling time format error"
msgstr "{}格式错误,应该为:%Y-%m-%d %H:%M:%S"
#: api/lib/cmdb/resp_format.py:146
#: api/lib/cmdb/resp_format.py:150
msgid "CMDB data reconciliation results"
msgstr ""
#: api/lib/cmdb/resp_format.py:147
#: api/lib/cmdb/resp_format.py:151
msgid "Number of {} illegal: {}"
msgstr ""
#: api/lib/cmdb/resp_format.py:149
#: api/lib/cmdb/resp_format.py:153
msgid "Topology view {} already exists"
msgstr "拓扑视图 {} 已经存在"
#: api/lib/cmdb/resp_format.py:150
#: api/lib/cmdb/resp_format.py:154
msgid "Topology group {} already exists"
msgstr "拓扑视图分组 {} 已经存在"
#: api/lib/cmdb/resp_format.py:152
#: api/lib/cmdb/resp_format.py:156
msgid "The group cannot be deleted because the topology view already exists"
msgstr "因为该分组下定义了拓扑视图,不能删除"

View File

@@ -225,6 +225,7 @@ class AutoDiscoveryCIView(APIView):
@args_required("type_id")
@args_required("adt_id")
@args_required("instance")
@args_required("unique_value")
def post(self):
request.values.pop("_key", None)
request.values.pop("_secret", None)

View File

@@ -15,7 +15,7 @@ from api.lib.cmdb.const import ResourceTypeEnum, PermEnum
from api.lib.cmdb.const import RetKey
from api.lib.cmdb.perms import has_perm_for_ci
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
from api.lib.cmdb.search.ci import search as ci_search
from api.lib.decorator import args_required
from api.lib.perm.acl.acl import has_perm_from_args
from api.lib.utils import get_page
@@ -160,7 +160,7 @@ class CISearchView(APIView):
use_id_filter = request.values.get("use_id_filter", False) in current_app.config.get('BOOL_TRUE')
start = time.time()
s = search(query, fl, facet, page, ret_key, count, sort, excludes, use_id_filter=use_id_filter)
s = ci_search(query, fl, facet, page, ret_key, count, sort, excludes, use_id_filter=use_id_filter)
try:
response, counter, total, page, numfound, facet = s.search()
except SearchError as e:

View File

@@ -48,16 +48,21 @@ class CITypeView(APIView):
if request.url.endswith("icons"):
return self.jsonify(CITypeManager().get_icons())
q = request.args.get("type_name")
q = request.values.get("type_name")
type_ids = handle_arg_list(request.values.get("type_ids"))
type_ids = type_ids or (type_id and [type_id])
if type_ids:
ci_types = []
for _type_id in type_ids:
ci_type = CITypeCache.get(_type_id)
if ci_type is None:
return abort(404, ErrFormat.ci_type_not_found)
if type_id is not None:
ci_type = CITypeCache.get(type_id)
if ci_type is None:
return abort(404, ErrFormat.ci_type_not_found)
ci_type = ci_type.to_dict()
ci_type['parent_ids'] = CITypeInheritanceManager.get_parents(type_id)
ci_types = [ci_type]
ci_type = ci_type.to_dict()
ci_type['parent_ids'] = CITypeInheritanceManager.get_parents(_type_id)
ci_type['show_name'] = ci_type.get('show_id') and AttributeCache.get(ci_type['show_id']).name
ci_type['unique_name'] = ci_type['unique_id'] and AttributeCache.get(ci_type['unique_id']).name
ci_types.append(ci_type)
elif type_name is not None:
ci_type = CITypeCache.get(type_name).to_dict()
ci_type['parent_ids'] = CITypeInheritanceManager.get_parents(ci_type['id'])

View File

@@ -3,3 +3,4 @@ VUE_APP_PREVIEW=false
VUE_APP_API_BASE_URL=http://127.0.0.1:5000/api
VUE_APP_BUILD_PACKAGES="ticket,calendar,acl"
VUE_APP_IS_OUTER=true
VUE_APP_IS_OPEN_SOURCE=true

View File

@@ -54,6 +54,204 @@
<div class="content unicode" style="display: block;">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont">&#xe998;</span>
<div class="name">veops-department</div>
<div class="code-name">&amp;#xe998;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe997;</span>
<div class="name">duose-changwenben (1)</div>
<div class="code-name">&amp;#xe997;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe995;</span>
<div class="name">duose-quote</div>
<div class="code-name">&amp;#xe995;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe996;</span>
<div class="name">duose-boole</div>
<div class="code-name">&amp;#xe996;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe994;</span>
<div class="name">veops-rule1</div>
<div class="code-name">&amp;#xe994;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe993;</span>
<div class="name">veops-operation_report</div>
<div class="code-name">&amp;#xe993;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe992;</span>
<div class="name">veops-ranking1</div>
<div class="code-name">&amp;#xe992;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe98f;</span>
<div class="name">veops-ranking2</div>
<div class="code-name">&amp;#xe98f;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe990;</span>
<div class="name">veops-ranking3</div>
<div class="code-name">&amp;#xe990;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe991;</span>
<div class="name">veops-ranking4</div>
<div class="code-name">&amp;#xe991;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe98d;</span>
<div class="name">veops-title5</div>
<div class="code-name">&amp;#xe98d;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe98e;</span>
<div class="name">veops-repair (1)</div>
<div class="code-name">&amp;#xe98e;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe988;</span>
<div class="name">veops-ticket</div>
<div class="code-name">&amp;#xe988;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe989;</span>
<div class="name">veops-model4</div>
<div class="code-name">&amp;#xe989;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe98a;</span>
<div class="name">veops-resource2 (1)</div>
<div class="code-name">&amp;#xe98a;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe98b;</span>
<div class="name">veops-relationship3</div>
<div class="code-name">&amp;#xe98b;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe98c;</span>
<div class="name">veops-title6</div>
<div class="code-name">&amp;#xe98c;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe97a;</span>
<div class="name">veops-resource1 (1)</div>
<div class="code-name">&amp;#xe97a;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe97b;</span>
<div class="name">veops-model1 (1)</div>
<div class="code-name">&amp;#xe97b;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe97c;</span>
<div class="name">veops-relationship1</div>
<div class="code-name">&amp;#xe97c;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe97d;</span>
<div class="name">veops-title1</div>
<div class="code-name">&amp;#xe97d;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe97e;</span>
<div class="name">veops-title2</div>
<div class="code-name">&amp;#xe97e;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe97f;</span>
<div class="name">veops-model2</div>
<div class="code-name">&amp;#xe97f;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe980;</span>
<div class="name">veops-resource2</div>
<div class="code-name">&amp;#xe980;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe981;</span>
<div class="name">veops-warehousing</div>
<div class="code-name">&amp;#xe981;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe982;</span>
<div class="name">veops-relationship2</div>
<div class="code-name">&amp;#xe982;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe983;</span>
<div class="name">veops-title3</div>
<div class="code-name">&amp;#xe983;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe984;</span>
<div class="name">veops-rule2</div>
<div class="code-name">&amp;#xe984;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe985;</span>
<div class="name">veops-model3</div>
<div class="code-name">&amp;#xe985;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe986;</span>
<div class="name">veops-title4</div>
<div class="code-name">&amp;#xe986;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe987;</span>
<div class="name">veops-rule3</div>
<div class="code-name">&amp;#xe987;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe978;</span>
<div class="name">veops-decline</div>
<div class="code-name">&amp;#xe978;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe979;</span>
<div class="name">veops-rise</div>
<div class="code-name">&amp;#xe979;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe96f;</span>
<div class="name">caise-数据中心</div>
@@ -5322,9 +5520,9 @@
<pre><code class="language-css"
>@font-face {
font-family: 'iconfont';
src: url('iconfont.woff2?t=1721959219377') format('woff2'),
url('iconfont.woff?t=1721959219377') format('woff'),
url('iconfont.ttf?t=1721959219377') format('truetype');
src: url('iconfont.woff2?t=1724653006782') format('woff2'),
url('iconfont.woff?t=1724653006782') format('woff'),
url('iconfont.ttf?t=1724653006782') format('truetype');
}
</code></pre>
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@@ -5350,6 +5548,303 @@
<div class="content font-class">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont veops-department"></span>
<div class="name">
veops-department
</div>
<div class="code-name">.veops-department
</div>
</li>
<li class="dib">
<span class="icon iconfont duose-changwenben1"></span>
<div class="name">
duose-changwenben (1)
</div>
<div class="code-name">.duose-changwenben1
</div>
</li>
<li class="dib">
<span class="icon iconfont duose-quote"></span>
<div class="name">
duose-quote
</div>
<div class="code-name">.duose-quote
</div>
</li>
<li class="dib">
<span class="icon iconfont duose-boole"></span>
<div class="name">
duose-boole
</div>
<div class="code-name">.duose-boole
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-rule1"></span>
<div class="name">
veops-rule1
</div>
<div class="code-name">.veops-rule1
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-operation_report"></span>
<div class="name">
veops-operation_report
</div>
<div class="code-name">.veops-operation_report
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-ranking1"></span>
<div class="name">
veops-ranking1
</div>
<div class="code-name">.veops-ranking1
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-ranking2"></span>
<div class="name">
veops-ranking2
</div>
<div class="code-name">.veops-ranking2
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-ranking3"></span>
<div class="name">
veops-ranking3
</div>
<div class="code-name">.veops-ranking3
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-ranking4"></span>
<div class="name">
veops-ranking4
</div>
<div class="code-name">.veops-ranking4
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-title5"></span>
<div class="name">
veops-title5
</div>
<div class="code-name">.veops-title5
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-repair1"></span>
<div class="name">
veops-repair (1)
</div>
<div class="code-name">.veops-repair1
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-ticket"></span>
<div class="name">
veops-ticket
</div>
<div class="code-name">.veops-ticket
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-model4"></span>
<div class="name">
veops-model4
</div>
<div class="code-name">.veops-model4
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-resource21"></span>
<div class="name">
veops-resource2 (1)
</div>
<div class="code-name">.veops-resource21
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-relationship3"></span>
<div class="name">
veops-relationship3
</div>
<div class="code-name">.veops-relationship3
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-title6"></span>
<div class="name">
veops-title6
</div>
<div class="code-name">.veops-title6
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-resource11"></span>
<div class="name">
veops-resource1 (1)
</div>
<div class="code-name">.veops-resource11
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-model11"></span>
<div class="name">
veops-model1 (1)
</div>
<div class="code-name">.veops-model11
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-relationship1"></span>
<div class="name">
veops-relationship1
</div>
<div class="code-name">.veops-relationship1
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-title1"></span>
<div class="name">
veops-title1
</div>
<div class="code-name">.veops-title1
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-title2"></span>
<div class="name">
veops-title2
</div>
<div class="code-name">.veops-title2
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-model2"></span>
<div class="name">
veops-model2
</div>
<div class="code-name">.veops-model2
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-resource2"></span>
<div class="name">
veops-resource2
</div>
<div class="code-name">.veops-resource2
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-warehousing"></span>
<div class="name">
veops-warehousing
</div>
<div class="code-name">.veops-warehousing
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-relationship2"></span>
<div class="name">
veops-relationship2
</div>
<div class="code-name">.veops-relationship2
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-title3"></span>
<div class="name">
veops-title3
</div>
<div class="code-name">.veops-title3
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-rule2"></span>
<div class="name">
veops-rule2
</div>
<div class="code-name">.veops-rule2
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-model3"></span>
<div class="name">
veops-model3
</div>
<div class="code-name">.veops-model3
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-title4"></span>
<div class="name">
veops-title4
</div>
<div class="code-name">.veops-title4
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-rule3"></span>
<div class="name">
veops-rule3
</div>
<div class="code-name">.veops-rule3
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-decline"></span>
<div class="name">
veops-decline
</div>
<div class="code-name">.veops-decline
</div>
</li>
<li class="dib">
<span class="icon iconfont veops-rise"></span>
<div class="name">
veops-rise
</div>
<div class="code-name">.veops-rise
</div>
</li>
<li class="dib">
<span class="icon iconfont caise-data_center"></span>
<div class="name">
@@ -13252,6 +13747,270 @@
<div class="content symbol">
<ul class="icon_lists dib-box">
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-department"></use>
</svg>
<div class="name">veops-department</div>
<div class="code-name">#veops-department</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#duose-changwenben1"></use>
</svg>
<div class="name">duose-changwenben (1)</div>
<div class="code-name">#duose-changwenben1</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#duose-quote"></use>
</svg>
<div class="name">duose-quote</div>
<div class="code-name">#duose-quote</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#duose-boole"></use>
</svg>
<div class="name">duose-boole</div>
<div class="code-name">#duose-boole</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-rule1"></use>
</svg>
<div class="name">veops-rule1</div>
<div class="code-name">#veops-rule1</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-operation_report"></use>
</svg>
<div class="name">veops-operation_report</div>
<div class="code-name">#veops-operation_report</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-ranking1"></use>
</svg>
<div class="name">veops-ranking1</div>
<div class="code-name">#veops-ranking1</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-ranking2"></use>
</svg>
<div class="name">veops-ranking2</div>
<div class="code-name">#veops-ranking2</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-ranking3"></use>
</svg>
<div class="name">veops-ranking3</div>
<div class="code-name">#veops-ranking3</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-ranking4"></use>
</svg>
<div class="name">veops-ranking4</div>
<div class="code-name">#veops-ranking4</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-title5"></use>
</svg>
<div class="name">veops-title5</div>
<div class="code-name">#veops-title5</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-repair1"></use>
</svg>
<div class="name">veops-repair (1)</div>
<div class="code-name">#veops-repair1</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-ticket"></use>
</svg>
<div class="name">veops-ticket</div>
<div class="code-name">#veops-ticket</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-model4"></use>
</svg>
<div class="name">veops-model4</div>
<div class="code-name">#veops-model4</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-resource21"></use>
</svg>
<div class="name">veops-resource2 (1)</div>
<div class="code-name">#veops-resource21</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-relationship3"></use>
</svg>
<div class="name">veops-relationship3</div>
<div class="code-name">#veops-relationship3</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-title6"></use>
</svg>
<div class="name">veops-title6</div>
<div class="code-name">#veops-title6</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-resource11"></use>
</svg>
<div class="name">veops-resource1 (1)</div>
<div class="code-name">#veops-resource11</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-model11"></use>
</svg>
<div class="name">veops-model1 (1)</div>
<div class="code-name">#veops-model11</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-relationship1"></use>
</svg>
<div class="name">veops-relationship1</div>
<div class="code-name">#veops-relationship1</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-title1"></use>
</svg>
<div class="name">veops-title1</div>
<div class="code-name">#veops-title1</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-title2"></use>
</svg>
<div class="name">veops-title2</div>
<div class="code-name">#veops-title2</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-model2"></use>
</svg>
<div class="name">veops-model2</div>
<div class="code-name">#veops-model2</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-resource2"></use>
</svg>
<div class="name">veops-resource2</div>
<div class="code-name">#veops-resource2</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-warehousing"></use>
</svg>
<div class="name">veops-warehousing</div>
<div class="code-name">#veops-warehousing</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-relationship2"></use>
</svg>
<div class="name">veops-relationship2</div>
<div class="code-name">#veops-relationship2</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-title3"></use>
</svg>
<div class="name">veops-title3</div>
<div class="code-name">#veops-title3</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-rule2"></use>
</svg>
<div class="name">veops-rule2</div>
<div class="code-name">#veops-rule2</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-model3"></use>
</svg>
<div class="name">veops-model3</div>
<div class="code-name">#veops-model3</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-title4"></use>
</svg>
<div class="name">veops-title4</div>
<div class="code-name">#veops-title4</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-rule3"></use>
</svg>
<div class="name">veops-rule3</div>
<div class="code-name">#veops-rule3</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-decline"></use>
</svg>
<div class="name">veops-decline</div>
<div class="code-name">#veops-decline</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#veops-rise"></use>
</svg>
<div class="name">veops-rise</div>
<div class="code-name">#veops-rise</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#caise-data_center"></use>

View File

@@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 3857903 */
src: url('iconfont.woff2?t=1721959219377') format('woff2'),
url('iconfont.woff?t=1721959219377') format('woff'),
url('iconfont.ttf?t=1721959219377') format('truetype');
src: url('iconfont.woff2?t=1724653006782') format('woff2'),
url('iconfont.woff?t=1724653006782') format('woff'),
url('iconfont.ttf?t=1724653006782') format('truetype');
}
.iconfont {
@@ -13,6 +13,138 @@
-moz-osx-font-smoothing: grayscale;
}
.veops-department:before {
content: "\e998";
}
.duose-changwenben1:before {
content: "\e997";
}
.duose-quote:before {
content: "\e995";
}
.duose-boole:before {
content: "\e996";
}
.veops-rule1:before {
content: "\e994";
}
.veops-operation_report:before {
content: "\e993";
}
.veops-ranking1:before {
content: "\e992";
}
.veops-ranking2:before {
content: "\e98f";
}
.veops-ranking3:before {
content: "\e990";
}
.veops-ranking4:before {
content: "\e991";
}
.veops-title5:before {
content: "\e98d";
}
.veops-repair1:before {
content: "\e98e";
}
.veops-ticket:before {
content: "\e988";
}
.veops-model4:before {
content: "\e989";
}
.veops-resource21:before {
content: "\e98a";
}
.veops-relationship3:before {
content: "\e98b";
}
.veops-title6:before {
content: "\e98c";
}
.veops-resource11:before {
content: "\e97a";
}
.veops-model11:before {
content: "\e97b";
}
.veops-relationship1:before {
content: "\e97c";
}
.veops-title1:before {
content: "\e97d";
}
.veops-title2:before {
content: "\e97e";
}
.veops-model2:before {
content: "\e97f";
}
.veops-resource2:before {
content: "\e980";
}
.veops-warehousing:before {
content: "\e981";
}
.veops-relationship2:before {
content: "\e982";
}
.veops-title3:before {
content: "\e983";
}
.veops-rule2:before {
content: "\e984";
}
.veops-model3:before {
content: "\e985";
}
.veops-title4:before {
content: "\e986";
}
.veops-rule3:before {
content: "\e987";
}
.veops-decline:before {
content: "\e978";
}
.veops-rise:before {
content: "\e979";
}
.caise-data_center:before {
content: "\e96f";
}

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,237 @@
"css_prefix_text": "",
"description": "",
"glyphs": [
{
"icon_id": "41570722",
"name": "veops-department",
"font_class": "veops-department",
"unicode": "e998",
"unicode_decimal": 59800
},
{
"icon_id": "41437322",
"name": "duose-changwenben (1)",
"font_class": "duose-changwenben1",
"unicode": "e997",
"unicode_decimal": 59799
},
{
"icon_id": "41363381",
"name": "duose-quote",
"font_class": "duose-quote",
"unicode": "e995",
"unicode_decimal": 59797
},
{
"icon_id": "41363378",
"name": "duose-boole",
"font_class": "duose-boole",
"unicode": "e996",
"unicode_decimal": 59798
},
{
"icon_id": "41341306",
"name": "veops-rule1",
"font_class": "veops-rule1",
"unicode": "e994",
"unicode_decimal": 59796
},
{
"icon_id": "41337509",
"name": "veops-operation_report",
"font_class": "veops-operation_report",
"unicode": "e993",
"unicode_decimal": 59795
},
{
"icon_id": "41335526",
"name": "veops-ranking1",
"font_class": "veops-ranking1",
"unicode": "e992",
"unicode_decimal": 59794
},
{
"icon_id": "41335530",
"name": "veops-ranking2",
"font_class": "veops-ranking2",
"unicode": "e98f",
"unicode_decimal": 59791
},
{
"icon_id": "41335529",
"name": "veops-ranking3",
"font_class": "veops-ranking3",
"unicode": "e990",
"unicode_decimal": 59792
},
{
"icon_id": "41335528",
"name": "veops-ranking4",
"font_class": "veops-ranking4",
"unicode": "e991",
"unicode_decimal": 59793
},
{
"icon_id": "41334746",
"name": "veops-title5",
"font_class": "veops-title5",
"unicode": "e98d",
"unicode_decimal": 59789
},
{
"icon_id": "41334744",
"name": "veops-repair (1)",
"font_class": "veops-repair1",
"unicode": "e98e",
"unicode_decimal": 59790
},
{
"icon_id": "41334753",
"name": "veops-ticket",
"font_class": "veops-ticket",
"unicode": "e988",
"unicode_decimal": 59784
},
{
"icon_id": "41334751",
"name": "veops-model4",
"font_class": "veops-model4",
"unicode": "e989",
"unicode_decimal": 59785
},
{
"icon_id": "41334752",
"name": "veops-resource2 (1)",
"font_class": "veops-resource21",
"unicode": "e98a",
"unicode_decimal": 59786
},
{
"icon_id": "41334750",
"name": "veops-relationship3",
"font_class": "veops-relationship3",
"unicode": "e98b",
"unicode_decimal": 59787
},
{
"icon_id": "41334748",
"name": "veops-title6",
"font_class": "veops-title6",
"unicode": "e98c",
"unicode_decimal": 59788
},
{
"icon_id": "41334404",
"name": "veops-resource1 (1)",
"font_class": "veops-resource11",
"unicode": "e97a",
"unicode_decimal": 59770
},
{
"icon_id": "41334505",
"name": "veops-model1 (1)",
"font_class": "veops-model11",
"unicode": "e97b",
"unicode_decimal": 59771
},
{
"icon_id": "41334533",
"name": "veops-relationship1",
"font_class": "veops-relationship1",
"unicode": "e97c",
"unicode_decimal": 59772
},
{
"icon_id": "41334535",
"name": "veops-title1",
"font_class": "veops-title1",
"unicode": "e97d",
"unicode_decimal": 59773
},
{
"icon_id": "41334572",
"name": "veops-title2",
"font_class": "veops-title2",
"unicode": "e97e",
"unicode_decimal": 59774
},
{
"icon_id": "41334579",
"name": "veops-model2",
"font_class": "veops-model2",
"unicode": "e97f",
"unicode_decimal": 59775
},
{
"icon_id": "41334590",
"name": "veops-resource2",
"font_class": "veops-resource2",
"unicode": "e980",
"unicode_decimal": 59776
},
{
"icon_id": "41334594",
"name": "veops-warehousing",
"font_class": "veops-warehousing",
"unicode": "e981",
"unicode_decimal": 59777
},
{
"icon_id": "41334598",
"name": "veops-relationship2",
"font_class": "veops-relationship2",
"unicode": "e982",
"unicode_decimal": 59778
},
{
"icon_id": "41334602",
"name": "veops-title3",
"font_class": "veops-title3",
"unicode": "e983",
"unicode_decimal": 59779
},
{
"icon_id": "41334603",
"name": "veops-rule2",
"font_class": "veops-rule2",
"unicode": "e984",
"unicode_decimal": 59780
},
{
"icon_id": "41334604",
"name": "veops-model3",
"font_class": "veops-model3",
"unicode": "e985",
"unicode_decimal": 59781
},
{
"icon_id": "41334725",
"name": "veops-title4",
"font_class": "veops-title4",
"unicode": "e986",
"unicode_decimal": 59782
},
{
"icon_id": "41334739",
"name": "veops-rule3",
"font_class": "veops-rule3",
"unicode": "e987",
"unicode_decimal": 59783
},
{
"icon_id": "41334004",
"name": "veops-decline",
"font_class": "veops-decline",
"unicode": "e978",
"unicode_decimal": 59768
},
{
"icon_id": "41333990",
"name": "veops-rise",
"font_class": "veops-rise",
"unicode": "e979",
"unicode_decimal": 59769
},
{
"icon_id": "41143117",
"name": "caise-数据中心",

Binary file not shown.

18
cmdb-ui/src/api/cmdb.js Normal file
View File

@@ -0,0 +1,18 @@
import { axios } from '@/utils/request'
export function searchCI(params, isShowMessage = true) {
return axios({
url: `/v0.1/ci/s`,
method: 'GET',
params: params,
isShowMessage
})
}
export function getCIType(CITypeName, parameter) {
return axios({
url: `/v0.1/ci_types/${CITypeName}`,
method: 'GET',
params: parameter
})
}

View File

@@ -1,346 +1,407 @@
<template>
<div>
<a-space :style="{ display: 'flex', marginBottom: '10px' }" v-for="(item, index) in ruleList" :key="item.id">
<div :style="{ width: '70px', height: '24px', position: 'relative' }">
<treeselect
v-if="index"
class="custom-treeselect"
:style="{ width: '70px', '--custom-height': '24px', position: 'absolute', top: '-17px', left: 0 }"
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: '130px', '--custom-height': '24px' }"
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
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
<ValueTypeMapIcon :attr="node.raw" />
{{ node.label }}
</div>
<div
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
slot="value-label"
slot-scope="{ node }"
>
<ValueTypeMapIcon :attr="node.raw" /> {{ node.label }}
</div>
</treeselect>
<treeselect
class="custom-treeselect"
:style="{ width: '100px', '--custom-height': '24px' }"
v-model="item.exp"
:multiple="false"
:clearable="false"
searchable
:options="[...getExpListByProperty(item.property), ...advancedExpList]"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
@select="(value) => handleChangeExp(value, item, index)"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
</treeselect>
<treeselect
class="custom-treeselect"
:style="{ width: '175px', '--custom-height': '24px' }"
v-model="item.value"
:multiple="false"
:clearable="false"
searchable
v-if="isChoiceByProperty(item.property) && (item.exp === 'is' || item.exp === '~is')"
:options="getChoiceValueByProperty(item.property)"
:placeholder="$t('placeholder2')"
:normalizer="
(node) => {
return {
id: node[0],
label: node[0],
children: node.children,
}
}
"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
<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-group
size="small"
compact
v-else-if="item.exp === 'range' || item.exp === '~range'"
:style="{ width: '175px' }"
>
<a-input
class="ops-input"
size="small"
v-model="item.min"
:style="{ width: '78px' }"
:placeholder="$t('min')"
:disabled="disabled"
/>
~
<a-input
class="ops-input"
size="small"
v-model="item.max"
:style="{ width: '78px' }"
:placeholder="$t('max')"
:disabled="disabled"
/>
</a-input-group>
<a-input-group size="small" compact v-else-if="item.exp === 'compare'" :style="{ width: '175px' }">
<treeselect
class="custom-treeselect"
:style="{ width: '60px', '--custom-height': '24px' }"
v-model="item.compareType"
:multiple="false"
:clearable="false"
searchable
:options="compareTypeList"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
</treeselect>
<a-input class="ops-input" v-model="item.value" size="small" style="width: 113px" />
</a-input-group>
<a-input
v-else-if="item.exp !== 'value' && item.exp !== '~value'"
size="small"
v-model="item.value"
:placeholder="item.exp === 'in' || item.exp === '~in' ? $t('cmdbFilterComp.split', { separator: ';' }) : ''"
class="ops-input"
:style="{ width: '175px' }"
:disabled="disabled"
></a-input>
<div v-else :style="{ width: '175px' }"></div>
<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)"><ops-icon type="icon-xianxing-delete"/></a>
</a-tooltip>
<a-tooltip :title="$t('cmdbFilterComp.addHere')" v-if="needAddHere">
<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">
<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'
import ValueTypeMapIcon from '../CMDBValueTypeMapIcon'
export default {
name: 'Expression',
components: { ValueTypeMapIcon },
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: Array,
default: () => [],
},
canSearchPreferenceAttrList: {
type: Array,
required: true,
default: () => [],
},
needAddHere: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
},
data() {
return {
compareTypeList,
}
},
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) {
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') },
]
}
return this.expList
}
return this.expList
},
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, 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)
},
},
}
</script>
<style></style>
<template>
<div>
<a-space :style="{ display: 'flex', marginBottom: '10px' }" v-for="(item, index) in ruleList" :key="item.id">
<div :style="{ width: '70px', height: '24px', position: 'relative' }">
<treeselect
v-if="index"
class="custom-treeselect"
:style="{ width: '70px', '--custom-height': '24px', position: 'absolute', top: '-17px', left: 0 }"
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: '130px', '--custom-height': '24px' }"
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
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
<ValueTypeMapIcon :attr="node.raw" />
{{ node.label }}
</div>
<div
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
slot="value-label"
slot-scope="{ node }"
>
<ValueTypeMapIcon :attr="node.raw" /> {{ node.label }}
</div>
</treeselect>
<treeselect
class="custom-treeselect"
:style="{ width: '100px', '--custom-height': '24px' }"
v-model="item.exp"
:multiple="false"
:clearable="false"
searchable
:options="[...getExpListByProperty(item.property), ...advancedExpList]"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
@select="(value) => handleChangeExp(value, item, index)"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
<div
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
<a-tooltip :title="node.label">
{{ node.label }}
</a-tooltip>
</div>
<div
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
slot="value-label"
slot-scope="{ node }"
>
<a-tooltip :title="node.label">
{{ node.label }}
</a-tooltip>
</div>
</treeselect>
<CIReferenceAttr
v-if="getAttr(item.property).is_reference && (item.exp === 'is' || item.exp === '~is')"
:style="{ width: '175px' }"
class="select-filter-component"
:referenceTypeId="getAttr(item.property).reference_type_id"
:disabled="disabled"
v-model="item.value"
/>
<a-select
v-else-if="getAttr(item.property).is_bool && (item.exp === 'is' || item.exp === '~is')"
v-model="item.value"
class="select-filter-component"
:style="{ width: '175px' }"
:disabled="disabled"
:placeholder="$t('placeholder2')"
>
<a-select-option key="1">
true
</a-select-option>
<a-select-option key="0">
false
</a-select-option>
</a-select>
<treeselect
class="custom-treeselect"
:style="{ width: '175px', '--custom-height': '24px' }"
v-model="item.value"
:multiple="false"
:clearable="false"
searchable
v-else-if="isChoiceByProperty(item.property) && (item.exp === 'is' || item.exp === '~is')"
:options="getChoiceValueByProperty(item.property)"
:placeholder="$t('placeholder2')"
:normalizer="
(node) => {
return {
id: String(node[0] || ''),
label: node[1] ? node[1].label || node[0] : node[0],
children: node.children && node.children.length ? node.children : undefined,
}
}
"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
<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-group
size="small"
compact
v-else-if="item.exp === 'range' || item.exp === '~range'"
:style="{ width: '175px' }"
>
<a-input
class="ops-input"
size="small"
v-model="item.min"
:style="{ width: '78px' }"
:placeholder="$t('min')"
:disabled="disabled"
/>
~
<a-input
class="ops-input"
size="small"
v-model="item.max"
:style="{ width: '78px' }"
:placeholder="$t('max')"
:disabled="disabled"
/>
</a-input-group>
<a-input-group size="small" compact v-else-if="item.exp === 'compare'" :style="{ width: '175px' }">
<treeselect
class="custom-treeselect"
:style="{ width: '60px', '--custom-height': '24px' }"
v-model="item.compareType"
:multiple="false"
:clearable="false"
searchable
:options="compareTypeList"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
</treeselect>
<a-input class="ops-input" v-model="item.value" size="small" style="width: 113px" />
</a-input-group>
<a-input
v-else-if="item.exp !== 'value' && item.exp !== '~value'"
size="small"
v-model="item.value"
:placeholder="item.exp === 'in' || item.exp === '~in' ? $t('cmdbFilterComp.split', { separator: ';' }) : ''"
class="ops-input"
:style="{ width: '175px' }"
:disabled="disabled"
></a-input>
<div v-else :style="{ width: '175px' }"></div>
<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)"><ops-icon type="icon-xianxing-delete"/></a>
</a-tooltip>
<a-tooltip :title="$t('cmdbFilterComp.addHere')" v-if="needAddHere">
<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">
<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'
import ValueTypeMapIcon from '../CMDBValueTypeMapIcon'
import CIReferenceAttr from '../ciReferenceAttr/index.vue'
export default {
name: 'Expression',
components: { ValueTypeMapIcon, CIReferenceAttr },
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: Array,
default: () => [],
},
canSearchPreferenceAttrList: {
type: Array,
required: true,
default: () => [],
},
needAddHere: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
},
data() {
return {
compareTypeList,
}
},
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) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find && (['0', '1', '3', '4', '5'].includes(_find.value_type) || _find.is_reference || _find.is_bool)) {
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') },
]
}
return this.expList
}
return this.expList
},
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, 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 []
},
getAttr(property) {
return this.canSearchPreferenceAttrList.find((item) => item.name === property) || {}
},
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)
},
},
}
</script>
<style lang="less" scoped>
.select-filter-component {
height: 24px;
/deep/ .ant-select-selection {
height: 24px;
background: #f7f8fa;
line-height: 24px;
border: none;
.ant-select-selection__rendered {
height: 24px;
line-height: 24px;
}
}
}
</style>

View File

@@ -1,49 +1,77 @@
<template>
<span>
<ops-icon :type="getPropertyIcon(attr)" />
</span>
</template>
<script>
export default {
name: 'ValueTypeIcon',
props: {
attr: {
type: Object,
default: () => {},
},
},
methods: {
getPropertyIcon(attr) {
switch (attr.value_type) {
case '0':
return 'duose-shishu'
case '1':
return 'duose-fudianshu'
case '2':
if (attr.is_password) {
return 'duose-password'
}
if (attr.is_link) {
return 'duose-link'
}
return 'duose-wenben'
case '3':
return 'duose-datetime'
case '4':
return 'duose-date'
case '5':
return 'duose-time'
case '6':
return 'duose-json'
case '7':
return 'duose-password'
case '8':
return 'duose-link'
}
},
},
}
</script>
<style></style>
<template>
<span>
<ops-icon :type="getPropertyIcon(attr)" />
</span>
</template>
<script>
export default {
name: 'ValueTypeIcon',
props: {
attr: {
type: Object,
default: () => {},
},
},
methods: {
getPropertyIcon(attr) {
let valueType = attr.value_type
if (valueType === '2') {
if (attr.is_password) {
valueType = '7'
} else if (attr.is_link) {
valueType = '8'
} else if (!attr.is_index) {
valueType = '9'
}
}
if (
valueType === '7' &&
attr.is_bool
) {
valueType = '10'
}
if (
valueType === '0' &&
attr.is_reference
) {
valueType = '11'
}
switch (valueType) {
case '0':
return 'duose-shishu'
case '1':
return 'duose-fudianshu'
case '2':
return 'duose-wenben'
case '3':
return 'duose-datetime'
case '4':
return 'duose-date'
case '5':
return 'duose-time'
case '6':
return 'duose-json'
case '7':
return 'duose-password'
case '8':
return 'duose-link'
case '9':
return 'duose-changwenben1'
case '10':
return 'duose-boole'
case '11':
return 'duose-quote'
default:
return ''
}
},
},
}
</script>
<style></style>

View File

@@ -257,7 +257,7 @@ export default {
const props = {}
if (this.$route.name === routeName && selectedIcon) {
return <ops-icon type={selectedIcon}></ops-icon>
} else if (icon.startsWith('ops-') || icon.startsWith('icon-xianxing') || icon.startsWith('icon-shidi')) {
} else if (icon.startsWith('ops-') || icon.startsWith('icon-xianxing') || icon.startsWith('icon-shidi') || icon.startsWith('veops-')) {
return <ops-icon type={icon}></ops-icon>
} else {
typeof (icon) === 'object' ? props.component = icon : props.type = icon

View File

@@ -61,7 +61,13 @@
</template>
</div>
</div>
<a-input ref="regInput" :placeholder="$t('regexSelect.placeholder')" :value="current.label" @change="changeLabel">
<a-input
ref="regInput"
:placeholder="$t('regexSelect.placeholder')"
:value="current.label"
:disabled="disabled"
@change="changeLabel"
>
</a-input>
</a-popover>
</template>
@@ -88,6 +94,10 @@ export default {
type: Array,
default: () => [],
},
disabled: {
type: Boolean,
default: false,
}
},
data() {
return {

View File

@@ -0,0 +1,178 @@
<template>
<div class="reference-attr-select-wrap">
<a-select
v-bind="$attrs"
v-model="selectCIIds"
optionFilterProp="title"
:mode="isList ? 'multiple' : 'default'"
showSearch
allowClear
:getPopupContainer="(trigger) => trigger.parentElement"
class="reference-attr-select"
:maxTagCount="2"
@dropdownVisibleChange="handleDropdownVisibleChange"
@search="handleSearch"
@change="handleChange"
>
<template v-if="!isInit">
<a-select-option
v-for="(item) in initSelectOption"
:key="item.key"
:title="item.title"
>
{{ item.title }}
</a-select-option>
</template>
<a-select-option
v-for="(item) in options"
:key="item.key"
:title="item.title"
>
{{ item.title }}
</a-select-option>
</a-select>
</div>
</template>
<script>
import _ from 'lodash'
import debounce from 'lodash/debounce'
import { searchCI, getCIType } from '@/api/cmdb'
export default {
name: 'CIReferenceAttr',
props: {
value: {
type: [Number, String, Array],
default: () => '',
},
isList: {
type: Boolean,
default: false,
},
referenceShowAttrName: {
type: String,
default: ''
},
referenceTypeId: {
type: [String, Number],
default: ''
},
initSelectOption: {
type: Array,
default: () => []
}
},
model: {
prop: 'value',
event: 'change',
},
data() {
return {
isInit: false,
options: [],
innerReferenceShowAttrName: ''
}
},
watch: {
referenceTypeId: {
immediate: true,
deep: true,
handler() {
this.isInit = false
}
}
},
computed: {
selectCIIds: {
get() {
if (this.isList) {
return this.value || []
} else {
return this.value ? Number(this.value) : ''
}
},
set(val) {
this.$emit('change', val ?? (this.isList ? [] : null))
return val
},
},
},
methods: {
async handleDropdownVisibleChange(open) {
if (!this.isInit && open && this.referenceTypeId) {
this.isInit = true
if (!this.referenceShowAttrName) {
const res = await getCIType(this.referenceTypeId)
const ciType = res?.ci_types?.[0]
this.innerReferenceShowAttrName = ciType?.show_name || ciType?.unique_name || ''
}
const attrName = this.referenceShowAttrName || this.innerReferenceShowAttrName || ''
if (!attrName) {
return
}
const res = await searchCI({
q: `_type:${this.referenceTypeId}`,
fl: attrName,
count: 25,
})
let options = res?.result?.map((item) => {
return {
key: item._id,
title: String(item?.[attrName] ?? '')
}
})
options = _.uniqBy([...this.initSelectOption, ...options], 'key')
this.options = options
}
},
handleSearch: debounce(async function(v) {
const attrName = this.referenceShowAttrName || this.innerReferenceShowAttrName || ''
if (!attrName || !this.referenceTypeId) {
return
}
const res = await searchCI({
q: `_type:${this.referenceTypeId}${v ? ',*' + v + '*' : ''}`,
fl: attrName,
count: v ? 100 : 25,
})
this.options = res?.result?.map((item) => {
return {
key: item._id,
title: String(item?.[attrName] ?? '')
}
})
}, 300),
handleChange(v) {
if (Array.isArray(v) ? !v.length : !v) {
this.handleSearch()
}
}
}
}
</script>
<style lang="less" scoped>
.reference-attr-select-wrap {
width: 100%;
.reference-attr-select {
width: 100%;
/deep/ .ant-select-dropdown {
z-index: 15;
}
}
}
</style>

View File

@@ -5,12 +5,12 @@
<span
v-if="hasBackendPermission"
@click="handleClick"
class="action"
style="width: 40px; display: flex; justify-content: center"
class="common-settings-btn"
>
<a-icon type="setting" />
<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' ? 'English' : '中文' }}</span>
<span class="locale" @click="changeLang">{{ locale === 'zh' ? '简中' : 'EN' }}</span>
<a-popover
trigger="click"
:overlayStyle="{ width: '150px' }"
@@ -118,8 +118,38 @@ export default {
.locale {
cursor: pointer;
padding: 0 8px;
&:hover {
color: @primary-color;
}
}
</style>
<style lang="less" scoped>
.user-wrapper {
.common-settings-btn {
cursor: pointer;
padding: 0px 18px;
background-color: #F0F5FF;
border-radius: 22px;
height: 26px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 8px;
&-icon {
font-size: 12px;
color: #2F54EB;
}
&-text {
margin-left: 4px;
font-size: 12px;
font-weight: 400;
color: #4E5969;
}
}
}
</style>

View File

@@ -69,6 +69,8 @@ Vue.prototype.$httpError = function (err, describe) {
window.$message = Vue.prototype.$message
Vue.prototype.isOpenSource = process.env.VUE_APP_IS_OPEN_SOURCE === 'true'
Vue.use(Antd)
Vue.use(Viser)

View File

@@ -10,6 +10,7 @@ export default {
resourceType: 'Resource Types',
trigger: 'Triggers',
},
settings: 'Common Settings',
screen: 'Big Screen',
dashboard: 'Dashboard',
admin: 'Admin',

View File

@@ -10,6 +10,7 @@ export default {
resourceType: '资源类型',
trigger: '触发器',
},
settings: '通用设置',
screen: '大屏',
dashboard: '仪表盘',
admin: '管理员',

View File

@@ -1,232 +1,257 @@
import { axios } from '@/utils/request'
/**
* 获取 所有的 ci_types
* @param parameter
* @returns {AxiosPromise}
*/
export function getCITypes(parameter) {
return axios({
url: '/v0.1/ci_types',
method: 'GET',
params: parameter
})
}
/**
* 获取 某个 ci_types
* @param CITypeName
* @param parameter
* @returns {AxiosPromise}
*/
export function getCIType(CITypeName, parameter) {
return axios({
url: `/v0.1/ci_types/${CITypeName}`,
method: 'GET',
params: parameter
})
}
/**
* 创建 ci_type
* @param data
* @returns {AxiosPromise}
*/
export function createCIType(data) {
return axios({
url: '/v0.1/ci_types',
method: 'POST',
data: data
})
}
/**
* 更新 ci_type
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function updateCIType(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}`,
method: 'PUT',
data: data
})
}
/**
* 删除 ci_type
* @param CITypeId
* @returns {AxiosPromise}
*/
export function deleteCIType(CITypeId) {
return axios({
url: `/v0.1/ci_types/${CITypeId}`,
method: 'DELETE'
})
}
/**
* 获取 某个 ci_type 的分组
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function getCITypeGroupById(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attribute_groups`,
method: 'GET',
params: data
})
}
/**
* 保存 某个 ci_type 的分组
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function createCITypeGroupById(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attribute_groups`,
method: 'POST',
data: data
})
}
/**
* 修改 某个 ci_type 的分组
* @param groupId
* @param data
* @returns {AxiosPromise}
*/
export function updateCITypeGroupById(groupId, data) {
return axios({
url: `/v0.1/ci_types/attribute_groups/${groupId}`,
method: 'PUT',
data: data
})
}
/**
* 删除 某个 ci_type 的分组
* @param groupId
* @param data
* @returns {AxiosPromise}
*/
export function deleteCITypeGroupById(groupId, data) {
return axios({
url: `/v0.1/ci_types/attribute_groups/${groupId}`,
method: 'delete',
data: data
})
}
export function getUniqueConstraintList(type_id) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint`,
method: 'get',
})
}
export function addUniqueConstraint(type_id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint`,
method: 'post',
data: data
})
}
export function updateUniqueConstraint(type_id, id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint/${id}`,
method: 'put',
data: data
})
}
export function deleteUniqueConstraint(type_id, id) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint/${id}`,
method: 'delete',
})
}
export function getTriggerList(type_id) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers`,
method: 'get',
})
}
export function addTrigger(type_id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers`,
method: 'post',
data: data
})
}
export function updateTrigger(type_id, id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers/${id}`,
method: 'put',
data: data
})
}
export function deleteTrigger(type_id, id) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers/${id}`,
method: 'delete',
})
}
// CMDB的模型和实例的授权接口
export function grantCiType(type_id, rid, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/roles/${rid}/grant`,
method: 'post',
data
})
}
// CMDB的模型和实例的删除授权接口
export function revokeCiType(type_id, rid, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/roles/${rid}/revoke`,
method: 'post',
data
})
}
// CMDB的模型和实例的过滤的权限
export function ciTypeFilterPermissions(type_id) {
return axios({
url: `/v0.1/ci_types/${type_id}/filters/permissions`,
method: 'get',
})
}
// parent_ids, child_id
export function postCiTypeInheritance(data) {
return axios({
url: `/v0.1/ci_types/inheritance`,
method: 'post',
data
})
}
// parent_id, child_id
export function deleteCiTypeInheritance(data) {
return axios({
url: `/v0.1/ci_types/inheritance`,
method: 'delete',
data
})
}
export function getCITypeIcons() {
return axios({
url: '/v0.1/ci_types/icons',
method: 'GET',
})
}
import { axios } from '@/utils/request'
/**
* 获取 所有的 ci_types
* @param parameter
* @returns {AxiosPromise}
*/
export function getCITypes(parameter) {
return axios({
url: '/v0.1/ci_types',
method: 'GET',
params: parameter
})
}
/**
* 获取 某个 ci_types
* @param CITypeName
* @param parameter
* @returns {AxiosPromise}
*/
export function getCIType(CITypeName, parameter) {
return axios({
url: `/v0.1/ci_types/${CITypeName}`,
method: 'GET',
params: parameter
})
}
/**
* 创建 ci_type
* @param data
* @returns {AxiosPromise}
*/
export function createCIType(data) {
return axios({
url: '/v0.1/ci_types',
method: 'POST',
data: data
})
}
/**
* 更新 ci_type
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function updateCIType(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}`,
method: 'PUT',
data: data
})
}
/**
* 删除 ci_type
* @param CITypeId
* @returns {AxiosPromise}
*/
export function deleteCIType(CITypeId) {
return axios({
url: `/v0.1/ci_types/${CITypeId}`,
method: 'DELETE'
})
}
/**
* 获取 某个 ci_type 的分组
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function getCITypeGroupById(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attribute_groups`,
method: 'GET',
params: data
})
}
/**
* 保存 某个 ci_type 的分组
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function createCITypeGroupById(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attribute_groups`,
method: 'POST',
data: data
})
}
/**
* 修改 某个 ci_type 的分组
* @param groupId
* @param data
* @returns {AxiosPromise}
*/
export function updateCITypeGroupById(groupId, data) {
return axios({
url: `/v0.1/ci_types/attribute_groups/${groupId}`,
method: 'PUT',
data: data
})
}
/**
* 删除 某个 ci_type 的分组
* @param groupId
* @param data
* @returns {AxiosPromise}
*/
export function deleteCITypeGroupById(groupId, data) {
return axios({
url: `/v0.1/ci_types/attribute_groups/${groupId}`,
method: 'delete',
data: data
})
}
/**
* 获取级联属性配置
* @param {*} typeId
* @returns
*/
export function getCITypeCascadeAttributes(typeId) {
return axios({
url: `/v0.1/cascade_attributes/ci_types/${typeId}`,
method: 'get'
})
}
/**
* 获取级联属性数据
* @param {*} typeId
* @returns
*/
export function postCITypeCascadeAttributesValues(attrId, data) {
return axios({
url: `/v0.1/cascade_attributes/${attrId}/values`,
method: 'post',
data
})
}
export function getUniqueConstraintList(type_id) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint`,
method: 'get',
})
}
export function addUniqueConstraint(type_id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint`,
method: 'post',
data: data
})
}
export function updateUniqueConstraint(type_id, id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint/${id}`,
method: 'put',
data: data
})
}
export function deleteUniqueConstraint(type_id, id) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint/${id}`,
method: 'delete',
})
}
export function getTriggerList(type_id) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers`,
method: 'get',
})
}
export function addTrigger(type_id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers`,
method: 'post',
data: data
})
}
export function updateTrigger(type_id, id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers/${id}`,
method: 'put',
data: data
})
}
export function deleteTrigger(type_id, id) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers/${id}`,
method: 'delete',
})
}
// CMDB的模型和实例的授权接口
export function grantCiType(type_id, rid, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/roles/${rid}/grant`,
method: 'post',
data
})
}
// CMDB的模型和实例的删除授权接口
export function revokeCiType(type_id, rid, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/roles/${rid}/revoke`,
method: 'post',
data
})
}
// CMDB的模型和实例的过滤的权限
export function ciTypeFilterPermissions(type_id) {
return axios({
url: `/v0.1/ci_types/${type_id}/filters/permissions`,
method: 'get',
})
}
// parent_ids, child_id
export function postCiTypeInheritance(data) {
return axios({
url: `/v0.1/ci_types/inheritance`,
method: 'post',
data
})
}
// parent_id, child_id
export function deleteCiTypeInheritance(data) {
return axios({
url: `/v0.1/ci_types/inheritance`,
method: 'delete',
data
})
}
export function getCITypeIcons() {
return axios({
url: '/v0.1/ci_types/icons',
method: 'GET',
})
}

View File

@@ -0,0 +1,578 @@
<template>
<div class="ci-table-wrap">
<ops-table
:id="id"
border
keep-source
show-overflow
resizable
ref="xTable"
size="small"
:data="data"
:loading="loading"
:row-config="{ useKey: true, keyField: '_id' }"
show-header-overflow
highlight-hover-row
:checkbox-config="{ reserve: true, highlight: true, range: true }"
:edit-config="{ trigger: 'dblclick', mode: 'row', showIcon: false }"
:sort-config="{ remote: true, trigger: 'cell' }"
:row-key="true"
:column-key="true"
:cell-style="getCellStyle"
:scroll-y="{ enabled: true, gt: 20 }"
:scroll-x="{ enabled: true, gt: 20 }"
class="ops-unstripe-table checkbox-hover-table"
:custom-config="{ storage: true }"
@checkbox-change="onSelectChange"
@checkbox-all="onSelectChange"
@checkbox-range-end="onSelectRangeEnd"
v-bind="$attrs"
v-on="$listeners"
>
<vxe-column
align="center"
type="checkbox"
width="60"
:fixed="isCheckboxFixed ? 'left' : ''"
v-if="showCheckbox"
>
<template #default="{row}">
{{ getRowSeq(row) }}
</template>
</vxe-column>
<vxe-table-column
v-for="(col, index) in columns"
:key="`${col.field}_${index}`"
:title="col.title"
:field="col.field"
:width="col.width"
:sortable="col.sortable"
:edit-render="getColumnsEditRender(col)"
:cell-type="col.value_type === '2' ? 'string' : 'auto'"
:fixed="col.is_fixed ? 'left' : ''"
>
<template #header>
<span class="vxe-handle">
<OpsMoveIcon class="header-move-icon" />
<span>{{ col.title }}</span>
</span>
</template>
<template v-if="col.is_choice || col.is_password || col.is_bool || col.is_reference" #edit="{ row }">
<CIReferenceAttr
v-if="col.is_reference"
:referenceTypeId="col.reference_type_id"
:isList="col.is_list"
:referenceShowAttrName="referenceShowAttrNameMap[col.reference_type_id] || ''"
:initSelectOption="getInitReferenceSelectOption(row[col.field], col)"
v-model="row[col.field]"
/>
<a-switch
v-else-if="col.is_bool"
v-model="row[col.field]"
/>
<vxe-input v-else-if="col.is_password" v-model="passwordValue[col.field]" />
<a-select
v-if="col.is_choice"
v-model="row[col.field]"
:getPopupContainer="(trigger) => trigger.parentElement"
:style="{ width: '100%', height: '32px' }"
:placeholder="$t('placeholder2')"
:showArrow="false"
:mode="col.is_list ? 'multiple' : 'default'"
class="ci-table-edit-select"
allowClear
showSearch
>
<a-select-option
v-for="(choice, idx) in col.filters"
:value="choice[0]"
:key="'edit_' + col.field + idx"
>
<span
:style="{
...(choice[1] ? choice[1].style : {}),
display: 'inline-flex',
alignItems: 'center'
}"
>
<template v-if="choice[1] && choice[1].icon && choice[1].icon.name">
<img
v-if="choice[1].icon.id && choice[1].icon.url"
:src="`/api/common-setting/v1/file/${choice[1].icon.url}`"
:style="{ maxHeight: '13px', maxWidth: '13px', marginRight: '5px' }"
/>
<ops-icon
v-else
:style="{ color: choice[1].icon.color, marginRight: '5px' }"
: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>
</span>
</a-select-option>
</a-select>
</template>
<template
v-if="col.value_type === '6' || col.is_link || col.is_password || col.is_choice || col.is_reference"
#default="{ row }"
>
<template v-if="col.is_reference" >
<a
v-for="(ciId) in (col.is_list ? row[col.field] : [row[col.field]])"
:key="ciId"
:href="`/cmdb/cidetail/${col.reference_type_id}/${ciId}`"
target="_blank"
>
{{ getReferenceAttrValue(ciId, col) }}
</a>
</template>
<span v-else-if="col.value_type === '6' && row[col.field]">{{ row[col.field] }}</span>
<template v-else-if="col.is_link && row[col.field]">
<a
v-for="(item, linkIndex) in (col.is_list ? row[col.field] : [row[col.field]])"
:key="linkIndex"
:href="
item.startsWith('http') || item.startsWith('https')
? `${item}`
: `http://${item}`
"
target="_blank"
>
{{ getChoiceValueLabel(col, item) || item }}
</a>
</template>
<PasswordField
v-else-if="col.is_password && row[col.field]"
:ci_id="row._id"
:attr_id="col.attr_id"
></PasswordField>
<template v-else-if="col.is_choice">
<span
v-for="value in (col.is_list ? row[col.field] : [row[col.field]])"
:key="value"
:style="getChoiceValueStyle(col, value)"
class="column-default-choice"
>
<img
v-if="getChoiceValueIcon(col, value).id && getChoiceValueIcon(col, value).url"
:src="`/api/common-setting/v1/file/${getChoiceValueIcon(col, value).url}`"
:style="{ maxHeight: '13px', maxWidth: '13px', marginRight: '5px' }"
/>
<ops-icon
v-else-if="getChoiceValueIcon(col, value).name"
:style="{ color: getChoiceValueIcon(col, value).color, marginRight: '5px' }"
:type="getChoiceValueIcon(col, value).name"
/>
{{ getChoiceValueLabel(col, value) || value }}
</span>
</template>
</template>
</vxe-table-column>
<vxe-column align="left" field="operate" fixed="right" width="80">
<template #header>
<span>{{ $t('operation') }}</span>
</template>
<template #default="{ row }">
<a-space>
<a @click="openDetail(row.ci_id || row._id)">
<a-icon type="unordered-list" />
</a>
<a-tooltip :title="$t('cmdb.ci.addRelation')">
<a @click="openDetail(row.ci_id || row._id, 'tab_2', '2')">
<a-icon type="retweet" />
</a>
</a-tooltip>
<a v-if="showDelete" @click="deleteCI(row)" :style="{ color: 'red' }">
<a-icon type="delete" />
</a>
</a-space>
</template>
</vxe-column>
<template #empty>
<div
v-if="loading"
class="ci-table-loading"
>
{{ loadingTip || $t('loading') }}
</div>
<div v-else>
<img :style="{ width: '200px' }" :src="require('@/assets/data_empty.png')" />
<div>{{ $t('noData') }}</div>
</div>
</template>
<template #loading>
<div class="ci-table-loading">{{ loadingTip || $t('loading') }}</div>
</template>
</ops-table>
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
</div>
</template>
<script>
import _ from 'lodash'
import { getCITypes } from '@/modules/cmdb/api/CIType'
import { searchCI } from '@/modules/cmdb/api/ci'
import JsonEditor from '../JsonEditor/jsonEditor.vue'
import PasswordField from '../passwordField/index.vue'
import { ops_move_icon as OpsMoveIcon } from '@/core/icons'
import CIReferenceAttr from '@/components/ciReferenceAttr/index.vue'
export default {
name: 'CITable',
components: {
JsonEditor,
PasswordField,
OpsMoveIcon,
CIReferenceAttr
},
props: {
// table ID
id: {
type: String,
default: ''
},
// table Loading
loading: {
type: Boolean,
default: false,
},
// ci 属性列表
attrList: {
type: Array,
default: () => []
},
// table column
columns: {
type: Array,
default: () => []
},
passwordValue: {
type: Object,
default: () => {}
},
// 加载提示
loadingTip: {
type: String,
default: ''
},
// 是否展示复选框
showCheckbox: {
type: Boolean,
default: true
},
// 是否展示删除按钮
showDelete: {
type: Boolean,
default: true
},
// 表格数据
data: {
type: Array,
default: () => []
}
},
data() {
return {
referenceShowAttrNameMap: {},
referenceCIIdMap: {},
}
},
computed: {
isCheckboxFixed() {
const idx = this.columns.findIndex((item) => item.is_fixed)
return idx > -1
},
tableDataWatch() {
return {
data: this.data,
columns: this.columns
}
},
referenceCIIdWatch() {
const referenceTypeCol = this.columns?.filter((col) => col?.is_reference && col?.reference_type_id) || []
if (!this.data?.length || !referenceTypeCol?.length) {
return []
}
const ids = []
this.data.forEach((row) => {
referenceTypeCol.forEach((col) => {
if (row[col.field]) {
ids.push(...(Array.isArray(row[col.field]) ? row[col.field] : [row[col.field]]))
}
})
})
return _.uniq(ids)
}
},
watch: {
columns: {
immediate: true,
deep: true,
handler(newVal) {
this.handleReferenceShowAttrName(newVal)
}
},
referenceCIIdWatch: {
immediate: true,
deep: true,
handler() {
this.handleReferenceCIIdMap()
}
}
},
methods: {
getVxetableRef() {
return this?.$refs?.['xTable']?.getVxetableRef?.() || null
},
onSelectChange() {
const xTable = this.getVxetableRef()
const records = [...xTable.getCheckboxRecords(), ...xTable.getCheckboxReserveRecords()]
this.$emit('onSelectChange', records)
},
onSelectRangeEnd({ records }) {
this.$emit('onSelectChange', records)
},
getCellStyle({ row, rowIndex, $rowIndex, column, columnIndex, $columnIndex }) {
const { property } = column
const _find = this.attrList.find((attr) => attr.name === property)
if (
_find &&
_find.option &&
_find.option.fontOptions &&
row[`${property}`] !== undefined &&
row[`${property}`] !== null
) {
return { ..._find.option.fontOptions }
}
},
getColumnsEditRender(col) {
const _editRender = {
...col.editRender,
}
if (col.value_type === '6') {
_editRender.events = { focus: this.handleFocusJson }
}
return _editRender
},
handleFocusJson({ column, row }) {
this.$refs.jsonEditor.open(column, row)
},
jsonEditorOk(row, column, jsonData) {
this.$attrs.data.forEach((item) => {
if (item._id === row._id) {
item[column.property] = JSON.stringify(jsonData)
}
})
this.getVxetableRef().refreshColumn()
},
getChoiceValueStyle(col, colValue) {
const _find = col.filters.find((item) => String(item[0]) === String(colValue))
if (_find) {
return _find[1]?.style || {}
}
return {}
},
getChoiceValueIcon(col, colValue) {
const _find = col.filters.find((item) => String(item[0]) === String(colValue))
if (_find) {
return _find[1]?.icon || {}
}
return {}
},
getChoiceValueLabel(col, colValue) {
const _find = col?.filters?.find((item) => String(item[0]) === String(colValue))
if (_find) {
return _find[1]?.label || ''
}
return ''
},
/**
* 开启当前 ci 详情弹窗
*/
openDetail(id, activeTabKey, ciDetailRelationKey) {
this.$emit('openDetail', id, activeTabKey, ciDetailRelationKey)
},
deleteCI(row) {
this.$emit('deleteCI', row)
},
getRowSeq(row) {
return this.getVxetableRef().getRowSeq(row)
},
async handleReferenceShowAttrName(columns) {
const needRequiredCITypeIds = columns?.filter((col) => col?.is_reference && col?.reference_type_id).map((col) => col.reference_type_id) || []
if (!needRequiredCITypeIds.length) {
this.referenceShowAttrNameMap = {}
return
}
const res = await getCITypes({
type_ids: needRequiredCITypeIds.join(',')
})
const map = {}
res.ci_types.forEach((ciType) => {
map[ciType.id] = ciType?.show_name || ciType?.unique_name || ''
})
this.referenceShowAttrNameMap = map
},
async handleReferenceCIIdMap() {
const referenceTypeCol = this.columns.filter((col) => col?.is_reference && col?.reference_type_id) || []
if (!this.data?.length || !referenceTypeCol?.length) {
this.referenceCIIdMap = {}
return
}
const map = {}
this.data.forEach((row) => {
referenceTypeCol.forEach((col) => {
const ids = Array.isArray(row[col.field]) ? row[col.field] : row[col.field] ? [row[col.field]] : []
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
},
getReferenceAttrValue(id, col) {
const ci = this?.referenceCIIdMap?.[col?.reference_type_id]?.[id]
if (!ci) {
return id
}
const attrName = this.referenceShowAttrNameMap?.[col.reference_type_id]
return ci?.[attrName] || id
},
getInitReferenceSelectOption(value, col) {
const ids = Array.isArray(value) ? value : value ? [value] : []
if (!ids.length) {
return []
}
const map = this?.referenceCIIdMap?.[col?.reference_type_id]
const attrName = this.referenceShowAttrNameMap?.[col?.reference_type_id]
const option = (Array.isArray(value) ? value : [value]).map((id) => {
return {
key: id,
title: map?.[id]?.[attrName] || id
}
})
return option
}
}
}
</script>
<style lang="less" scoped>
.ci-table-wrap {
.ci-table-loading {
width: 100%;
line-height: 200px;
}
.header-move-icon {
width: 17px;
height: 17px;
display: none;
position: absolute;
left: -3px;
top: 12px;
}
.column-default-choice {
border-radius: 4px;
padding: 1px 5px;
margin: 2px;
vertical-align: bottom;
display: inline-flex;
align-items: center;
}
}
.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;
}
}
}
}
}
</style>

View File

@@ -29,7 +29,7 @@
class="category-side-children-item-corporate"
v-if="ruleType === 'private_cloud' || (ruleType === 'http' && (categoryIndex !== 0 || itemIndex !== 0))"
>
{{ $t('cmdb.enterpriseVersionFlag') }}
</span>
</div>
</div>
@@ -67,7 +67,7 @@
class="corporate-flag"
v-if="ruleType === 'private_cloud' || (ruleType === 'http' && (categoryIndex !== 0 || itemIndex !== 0))"
>
<span class="corporate-flag-text"></span>
<span class="corporate-flag-text">{{ $t('cmdb.enterpriseVersionFlag') }}</span>
</div>
</div>
</div>

View File

@@ -113,6 +113,11 @@ export default {
this.editor.insertNode(node)
}
},
destroy() {
const editor = this.editor
if (editor == null) return
editor.destroy()
}
},
}
</script>

View File

@@ -105,7 +105,7 @@ export default {
return { method, url, parameters, body, headers, authorization }
},
setParams(params) {
const { method, url, parameters, body, headers, authorization = {} } = params ?? {}
const { method = 'GET', url = '', parameters = {}, body = '', headers = {}, authorization = {} } = params ?? {}
this.method = method
this.url = url
this.$refs.Parameters.parameters =

View File

@@ -2,6 +2,8 @@ const cmdb_en = {
relation: 'Relation',
attribute: 'Attributes',
configTable: 'Config Table',
enterpriseVersionFlag: 'Pro',
enterpriseVersionTip: 'Enterprise version only',
menu: {
views: 'Views',
topologyView: 'Topology Views',
@@ -115,7 +117,7 @@ const cmdb_en = {
advancedSettings: 'Advanced Settings',
font: 'Font',
color: 'Color',
choiceValue: 'Predefined value',
choiceValue: 'Select List',
computedAttribute: 'Computed Attribute',
computedAttributeTips: 'The value of this attribute is calculated through an expression constructed from other attributes of the CIType or by executing a piece of code. The reference method of the attribute is: {{ attribute name }}',
addAttribute: 'New attribute',
@@ -128,6 +130,7 @@ const cmdb_en = {
selectAttribute: 'Select Attribute',
groupExisted: 'Group name already exists',
attributeSortedTips: 'Attributes in other groups cannot be sorted. If you need to sort, please drag them to a custom group first!',
attributeSortedTips2: 'Non-inherited attributes cannot be inserted before inherited attributes!',
buildinAttribute: 'built-in attributes',
expr: 'Expression',
code: 'Code',
@@ -187,6 +190,14 @@ const cmdb_en = {
confirmDeleteTrigger: 'Are you sure to delete this trigger?',
int: 'Integer',
float: 'Float',
longText: 'Long Text',
shortText: 'Short Text',
shortTextTip: 'Text length <= 128',
referenceModel: 'Reference Model',
referenceModelTip: 'Please select reference model',
referenceModelTip1: 'For quick view of referenced model instances',
bool: 'Bool',
reference: 'Reference',
text: 'Text',
datetime: 'DateTime',
date: 'Date',
@@ -204,7 +215,7 @@ const cmdb_en = {
otherGroupTips: 'Non sortable within the other group',
filterTips: 'click to show {name}',
attributeAssociation: 'Attribute Association',
attributeAssociationTip1: 'Automatically establish relationships through the attributes except password, json and multiple of two models',
attributeAssociationTip1: 'Automatically establish relationships through attribute values (except password, json, multi-value, long text, boolean, reference) of two models',
attributeAssociationTip2: 'Double click to edit',
attributeAssociationTip3: 'Two Attributes must be selected',
attributeAssociationTip4: 'Please select a attribute from Source CIType',
@@ -274,8 +285,27 @@ const cmdb_en = {
attrAlias: 'Attr Alias',
attrCode: 'Attr Code',
computedAttrTip1: 'Reference attributes follow jinja2 syntax',
computedAttrTip2: `Multi-valued attributes (lists) are rendered with [ ] included by default, if you want to remove it, the reference method is: {{ attr_name | join(,)}}} where commas are separators`,
example: 'Example'
computedAttrTip2: `Multi-valued attributes (lists) are rendered with [ ] included by default, if you want to remove it, the reference method is: """{{ attr_name | join(',') }}""" where commas are separators`,
example: 'Example',
attrFilterTip: `The third column of values allows you to select attributes of this model to cascade attributes`,
rule: 'Rule',
cascadeAttr: 'Cascade',
cascadeAttrTip: 'Cascading attributes note the order',
enumValue: 'Value',
label: 'Label',
valueInputTip: 'Please input value',
enumValueTip2: 'Enumeration values cannot be repeated',
builtin: 'Built In',
department: 'Department',
user: 'User',
userGroup: 'User Group',
departmentTip: 'Scroll down to select all departments in the company structure for common settings',
userGroupSelectTip: 'Please select user group',
displayValue: 'Display Value',
displayValueSelectTip: 'Please select Display Value',
departmentCascadeDisplay: 'Cascade Display',
filterUsers: 'Filter Users',
enum: 'Enum',
},
components: {
unselectAttributes: 'Unselected',
@@ -317,7 +347,7 @@ const cmdb_en = {
pleaseSearch: 'Please search',
conditionFilter: 'Conditional filtering',
attributeDesc: 'Attribute Description',
ciSearchTips: '1. JSON/password/link attributes cannot be searched\n2. If the search content includes commas, they need to be escaped\n3. Only index attributes are searched, non-index attributes use conditional filtering',
ciSearchTips: '1. JSON/password/link/longText/reference attributes cannot be searched\n2. If the search content includes commas, they need to be escaped\n3. Only index attributes are searched, non-index attributes use conditional filtering',
ciSearchTips2: 'For example: q=hostname:*0.0.0.0*',
subCIType: 'Subscription CIType',
already: 'already',
@@ -325,6 +355,7 @@ const cmdb_en = {
sub: 'subscription',
selectBelow: 'Please select below',
subSuccess: 'Subscription successful',
subFailed: 'Subscription failed, please try again later',
selectMethods: 'Please select a method',
noAuthRequest: 'No certification requested yet',
noParamRequest: 'No parameter certification yet',
@@ -380,6 +411,8 @@ const cmdb_en = {
yearsAgo: 'years ago',
just: 'just now',
searchPlaceholder: 'Please search CIType',
subCITable: 'Data',
subCITree: 'Tree',
},
custom_dashboard: {
charts: 'Chart',
@@ -539,7 +572,7 @@ class AutoDiscovery(object):
"""
Define attribute fields
:return: Returns a list of attribute fields. The list items are (name, type, description). The name must be in English.
type: String Integer Float Date DateTime Time JSON
type: String Integer Float Date DateTime Time JSON Bool Reference
For example:
return [
("ci_type", "String", "CIType name"),
@@ -639,6 +672,7 @@ if __name__ == "__main__":
rollbackingTips: 'Rollbacking',
batchRollbacking: 'Deleting {total} items in total, {successNum} items successful, {errorNum} items failed',
baselineTips: 'Changes at this point in time will also be rollbacked, Unique ID, password and dynamic attributes do not support',
cover: 'Cover',
},
serviceTree: {
remove: 'Remove',

View File

@@ -2,6 +2,8 @@ const cmdb_zh = {
relation: '关系',
attribute: '属性',
configTable: '配置表格',
enterpriseVersionFlag: '企',
enterpriseVersionTip: '仅限企业版',
menu: {
views: '视图',
topologyView: '拓扑视图',
@@ -115,7 +117,7 @@ const cmdb_zh = {
advancedSettings: '高级设置',
font: '字体',
color: '颜色',
choiceValue: '预定义值',
choiceValue: '下拉列表',
computedAttribute: '计算属性',
computedAttributeTips: '该属性的值是通过模型的其它属性构建的表达式或者执行一段代码的方式计算而来,属性的引用方法为: {{ 属性名 }}',
addAttribute: '新增属性',
@@ -128,6 +130,7 @@ const cmdb_zh = {
selectAttribute: '添加属性',
groupExisted: '分组名称已存在',
attributeSortedTips: '其他分组中的属性不能进行排序,如需排序请先拖至自定义的分组!',
attributeSortedTips2: '非继承属性不能插入到继承属性前!',
buildinAttribute: '内置字段',
expr: '表达式',
code: '代码',
@@ -187,6 +190,14 @@ const cmdb_zh = {
confirmDeleteTrigger: '确认删除该触发器吗?',
int: '整数',
float: '浮点数',
longText: '长文本',
shortText: '短文本',
shortTextTip: '文本长度 <= 128',
referenceModel: '引用模型',
referenceModelTip: '请选择引用模型',
referenceModelTip1: '用于快捷查看引用模型实例',
bool: '布尔',
reference: '引用',
text: '文本',
datetime: '日期时间',
date: '日期',
@@ -204,7 +215,7 @@ const cmdb_zh = {
otherGroupTips: '其他分组属性不可排序',
filterTips: '点击可仅查看{name}属性',
attributeAssociation: '属性关联',
attributeAssociationTip1: '通过2个模型的属性值(除密码、json、多值)来自动建立关系',
attributeAssociationTip1: '通过2个模型的属性值(除密码、json、多值、长文本、布尔、引用)来自动建立关系',
attributeAssociationTip2: '双击可编辑',
attributeAssociationTip3: '属性关联必须选择两个属性',
attributeAssociationTip4: '请选择原模型属性',
@@ -274,8 +285,27 @@ const cmdb_zh = {
attrAlias: '属性别名',
attrCode: '属性代码',
computedAttrTip1: '引用属性遵循jinja2语法',
computedAttrTip2: `多值属性(列表)默认呈现包括[ ], 如果要去掉, 引用方法为: """{{ attr_name | join(',')}}""" 其中逗号为分隔符`,
example: '例如'
computedAttrTip2: `多值属性(列表)默认呈现包括[ ], 如果要去掉, 引用方法为: """{{ attr_name | join(',') }}""" 其中逗号为分隔符`,
example: '例如',
attrFilterTip: '第三列值可选择本模型的属性,来实现级联属性的功能',
rule: '规则',
cascadeAttr: '级联',
cascadeAttrTip: '级联属性注意顺序',
enumValue: '枚举值',
label: '标签',
valueInputTip: '请输入枚举值',
enumValueTip2: '枚举值不能重复',
builtin: '内置',
department: '部门',
user: '用户',
userGroup: '用户组',
departmentTip: '下拉选择为通用设置公司架构里的所有部门',
userGroupSelectTip: '请选择用户组',
displayValue: '展示值',
displayValueSelectTip: '请选择展示值',
departmentCascadeDisplay: '部门级联显示',
filterUsers: '筛选用户',
enum: '枚举',
},
components: {
unselectAttributes: '未选属性',
@@ -317,7 +347,7 @@ const cmdb_zh = {
pleaseSearch: '请查找',
conditionFilter: '条件过滤',
attributeDesc: '属性说明',
ciSearchTips: '1. json、密码、链接属性不能搜索\n2. 搜索内容包括逗号, 则需转义\n3. 只搜索索引属性, 非索引属性使用条件过滤',
ciSearchTips: '1. json、密码、链接、长文本、引用属性不能搜索\n2. 搜索内容包括逗号, 则需转义\n3. 只搜索索引属性, 非索引属性使用条件过滤',
ciSearchTips2: '例: q=hostname:*0.0.0.0*',
subCIType: '订阅模型',
already: '已',
@@ -325,6 +355,7 @@ const cmdb_zh = {
sub: '订阅',
selectBelow: '请在下方进行选择',
subSuccess: '订阅成功',
subFailed: '订阅失败,请稍后再试',
selectMethods: '请选择方式',
noAuthRequest: '暂无请求认证',
noParamRequest: '暂无参数认证',
@@ -379,6 +410,8 @@ const cmdb_zh = {
yearsAgo: '年前',
just: '刚刚',
searchPlaceholder: '请搜索模型',
subCITable: '数据订阅',
subCITree: '层级订阅',
},
custom_dashboard: {
charts: '图表',
@@ -538,7 +571,7 @@ class AutoDiscovery(object):
"""
Define attribute fields
:return: Returns a list of attribute fields. The list items are (name, type, description). The name must be in English.
type: String Integer Float Date DateTime Time JSON
type: String Integer Float Date DateTime Time JSON Bool Reference
For example:
return [
("ci_type", "String", "CIType name"),
@@ -638,6 +671,7 @@ if __name__ == "__main__":
rollbackingTips: '正在批量回滚中',
batchRollbacking: '正在回滚,共{total}个,成功{successNum}个,失败{errorNum}个',
baselineTips: '该时间点的变更也会被回滚, 唯一标识、密码属性、动态属性不支持回滚',
cover: '覆盖',
},
serviceTree: {
remove: '移除',

View File

@@ -102,7 +102,7 @@ const genCmdbRoutes = async () => {
name: 'cmdb_ci_type',
component: RouteView,
redirect: '/cmdb/ci_type',
meta: { title: 'cmdb.menu.backendManage', icon: 'setting', permission: ['cmdb_admin', 'OneOPS_Application_Admin', 'admin'], },
meta: { title: 'cmdb.menu.backendManage', icon: 'veops-setting2', selectedIcon: 'veops-setting2', permission: ['cmdb_admin', 'OneOPS_Application_Admin', 'admin'], },
children: [
{
path: '/cmdb/customdashboard',

View File

@@ -4,13 +4,16 @@ export const valueTypeMap = () => {
return {
'0': i18n.t('cmdb.ciType.int'),
'1': i18n.t('cmdb.ciType.float'),
'2': i18n.t('cmdb.ciType.text'),
'2': i18n.t('cmdb.ciType.shortText'),
'3': i18n.t('cmdb.ciType.datetime'),
'4': i18n.t('cmdb.ciType.date'),
'5': i18n.t('cmdb.ciType.time'),
'6': 'JSON',
'7': i18n.t('cmdb.ciType.password'),
'8': i18n.t('cmdb.ciType.link')
'8': i18n.t('cmdb.ciType.link'),
'9': i18n.t('cmdb.ciType.longText'),
'10': i18n.t('cmdb.ciType.bool'),
'11': i18n.t('cmdb.ciType.reference'),
}
}

View File

@@ -1,219 +1,270 @@
/* eslint-disable */
import _ from 'lodash'
import XLSX from 'xlsx'
import XLSXS from 'xlsx-js-style'
export function sum(arr) {
if (!arr.length) {
return 0
}
return arr.reduce(function (prev, curr, idx, arr) {
return prev + curr
})
}
const strLength = (fData) => {
if (!fData) {
return 0
}
if (fData.length && typeof fData === 'object') {
fData = fData.join(' ')
}
let intLength = 0
for (let i = 0; i < fData.length; i++) {
if ((fData.charCodeAt(i) < 0) || (fData.charCodeAt(i) > 255)) {
intLength = intLength + 2
}
else {
intLength = intLength + 1
}
}
return Math.floor(intLength * 7)
}
String.prototype.pxWidth = function (font) {
// re-use canvas object for better performance
const canvas = String.prototype.pxWidth.canvas || (String.prototype.pxWidth.canvas = document.createElement("canvas")),
context = canvas.getContext("2d");
font && (context.font = font);
const metrics = context.measureText(this);
return metrics.width;
}
export function getCITableColumns(data, attrList, width = 1600, height) {
// 计算出来 主table的列表 布局属性
const _attrList = _.orderBy(attrList, ['is_fixed'], ['desc'])
const columns = []
for (let attr of _attrList) {
const editRender = { name: 'input' }
switch (attr.value_type) {
case '0':
editRender['props'] = { 'type': 'float' }
break
case '1':
editRender['props'] = { 'type': 'float' }
break
case '2':
editRender['attrs'] = { 'type': 'text' }
break
case '3':
editRender['props'] = { 'type': 'datetime' }
break
case "4":
editRender['props'] = { 'type': 'date' }
break
case '5':
editRender['props'] = { 'type': 'time' }
break
case '6':
editRender['props'] = { 'type': 'text' }
break
default:
editRender['props'] = { 'type': 'text' }
break
}
if (attr.is_choice) {
editRender.name = '$select'
editRender.options = attr.choice_value ? attr.choice_value.map(item => { return { label: item, value: item } }) : []
delete editRender.props
}
columns.push({
attr_id: attr.id,
editRender,
title: attr.alias || attr.name,
field: attr.name,
value_type: attr.value_type,
sortable: !!attr.is_sortable,
filters: attr.is_choice ? attr.choice_value : null,
width: Math.min(Math.max(100, ...data.map(item => strLength(item[attr.name]))), 350),
is_link: attr.is_link,
is_password: attr.is_password,
is_list: attr.is_list,
is_choice: attr.is_choice,
is_fixed: attr.is_fixed,
})
}
const totalWidth = sum(columns.map(col => col.width))
if (totalWidth < width) {
columns.map(item => {
// if (item.width === 100) {
delete item.width
// }
})
}
return columns
}
export const getPropertyStyle = (attr) => {
switch (attr.value_type) {
case '0':
return { color: '#cf1322', backgroundColor: '#fff1f0' }
case '1':
return { color: '#d4b106', backgroundColor: '#feffe6' }
case '2':
return { color: '#d46b08', backgroundColor: '#fff7e6' }
case '3':
return { color: '#531dab', backgroundColor: '#f9f0ff' }
case '4':
return { color: '#389e0d', backgroundColor: '#f6ffed' }
case '5':
return { color: '#08979c', backgroundColor: '#e6fffb' }
case '6':
return { color: '#c41d7f', backgroundColor: '#fff0f6' }
case '7':
return { color: '#0390CC', backgroundColor: '#e6fffb' }
case '8':
return { color: '#144BD9', backgroundColor: '#fff0f6' }
}
}
export const getPropertyIcon = (attr) => {
switch (attr.value_type) {
case '0':
return 'duose-shishu'
case '1':
return 'duose-fudianshu'
case '2':
if (attr.is_password) {
return 'duose-password'
}
if (attr.is_link) {
return 'duose-link'
}
return 'duose-wenben'
case '3':
return 'duose-datetime'
case '4':
return 'duose-date'
case '5':
return 'duose-time'
case '6':
return 'duose-json'
case '7':
return 'duose-password'
case '8':
return 'duose-link'
}
}
export const getLastLayout = (data, x1 = 0, y1 = 0, w1 = 0) => {
const _tempData = _.orderBy(data, ['y', 'x'], ['asc', 'asc'])
if (!_tempData.length) {
return { xLast: 0, yLast: 0, wLast: 0 }
}
const { x, y, w } = _tempData[_tempData.length - 1]
if (y < y1) {
return { xLast: x1, yLast: y1, wLast: w1 }
} else if (y > y1) {
return { xLast: x, yLast: y, wLast: w }
} else {
const xLast = _.max([x, x1])
return { xLast, yLast: y, wLast: xLast === x ? w : w1 }
}
}
// 数字加逗号
export const toThousands = (num = 0) => {
return num.toString().replace(/\d+/, function (n) {
return n.replace(/(\d)(?=(?:\d{3})+$)/g, '$1,')
})
}
export const downloadExcel = (data, fileName = `${moment().format('YYYY-MM-DD HH:mm:ss')}.xls`) => {
// STEP 1: Create a new workbook
const wb = XLSXS.utils.book_new()
// STEP 2: Create data rows and styles
const rowArray = data
// STEP 3: Create worksheet with rows; Add worksheet to workbook
const ws = XLSXS.utils.aoa_to_sheet(rowArray)
XLSXS.utils.book_append_sheet(wb, ws, fileName)
let maxColumnNumber = 1 // 默认最大列数
rowArray.forEach(item => { if (item.length > maxColumnNumber) { maxColumnNumber = item.length } })
// 添加列宽
ws['!cols'] = (rowArray[0].map(item => {
return { width: 22 }
}))
// // 添加行高
// ws['!rows'] = [{ 'hpt': 80 }]
// STEP 4: Write Excel file to browser #导出
XLSXS.writeFile(wb, fileName + '.xlsx')
}
export const getAllParentNodesLabel = (node, label) => {
if (node.parentNode) {
return getAllParentNodesLabel(node.parentNode, `${node.parentNode.label}-${label}`)
}
return label
}
export const getTreeSelectLabel = (node) => {
return `${getAllParentNodesLabel(node, node.label)}`
/* eslint-disable */
import _ from 'lodash'
import XLSX from 'xlsx'
import XLSXS from 'xlsx-js-style'
export function sum(arr) {
if (!arr.length) {
return 0
}
return arr.reduce(function (prev, curr, idx, arr) {
return prev + curr
})
}
const strLength = (fData) => {
if (!fData) {
return 0
}
if (fData.length && typeof fData === 'object') {
fData = fData.join(' ')
}
let intLength = 0
for (let i = 0; i < fData.length; i++) {
if ((fData.charCodeAt(i) < 0) || (fData.charCodeAt(i) > 255)) {
intLength = intLength + 2
}
else {
intLength = intLength + 1
}
}
return Math.floor(intLength * 7)
}
String.prototype.pxWidth = function (font) {
// re-use canvas object for better performance
const canvas = String.prototype.pxWidth.canvas || (String.prototype.pxWidth.canvas = document.createElement("canvas")),
context = canvas.getContext("2d");
font && (context.font = font);
const metrics = context.measureText(this);
return metrics.width;
}
export function getCITableColumns(data, attrList, width = 1600, height) {
// 计算出来 主table的列表 布局属性
const _attrList = _.orderBy(attrList, ['is_fixed'], ['desc'])
const columns = []
for (let attr of _attrList) {
const editRender = { name: 'input' }
switch (attr.value_type) {
case '0':
editRender['props'] = { 'type': 'float' }
break
case '1':
editRender['props'] = { 'type': 'float' }
break
case '2':
editRender['attrs'] = { 'type': 'text' }
break
case '3':
editRender['props'] = { 'type': 'datetime' }
break
case "4":
editRender['props'] = { 'type': 'date' }
break
case '5':
editRender['props'] = { 'type': 'time' }
break
case '6':
editRender['props'] = { 'type': 'text' }
break
default:
editRender['props'] = { 'type': 'text' }
break
}
if (attr.is_choice) {
editRender.name = '$select'
editRender.options = attr.choice_value ? attr.choice_value.map(item => { return { label: item, value: item } }) : []
delete editRender.props
}
columns.push({
attr_id: attr.id,
editRender,
title: attr.alias || attr.name,
field: attr.name,
value_type: attr.value_type,
sortable: !!attr.is_sortable,
filters: attr.is_choice ? attr.choice_value : null,
choice_builtin: null,
width: Math.min(Math.max(100, ...data.map(item => strLength(item[attr.name]))), 350),
is_link: attr.is_link,
is_password: attr.is_password,
is_list: attr.is_list,
is_choice: attr.is_choice,
is_fixed: attr.is_fixed,
is_bool: attr.is_bool,
is_reference: attr.is_reference,
reference_type_id: attr.reference_type_id
})
}
const totalWidth = sum(columns.map(col => col.width))
if (totalWidth < width) {
columns.map(item => {
// if (item.width === 100) {
delete item.width
// }
})
}
return columns
}
export const getPropertyStyle = (attr) => {
switch (attr.value_type) {
case '0':
return { color: '#cf1322', backgroundColor: '#fff1f0' }
case '1':
return { color: '#d4b106', backgroundColor: '#feffe6' }
case '2':
return { color: '#d46b08', backgroundColor: '#fff7e6' }
case '3':
return { color: '#531dab', backgroundColor: '#f9f0ff' }
case '4':
return { color: '#389e0d', backgroundColor: '#f6ffed' }
case '5':
return { color: '#08979c', backgroundColor: '#e6fffb' }
case '6':
return { color: '#c41d7f', backgroundColor: '#fff0f6' }
case '7':
return { color: '#0390CC', backgroundColor: '#e6fffb' }
case '8':
return { color: '#144BD9', backgroundColor: '#fff0f6' }
}
}
export const getPropertyIcon = (attr) => {
switch (attr.value_type) {
case '0':
if (attr.is_reference) {
return 'duose-quote'
}
return 'duose-shishu'
case '1':
return 'duose-fudianshu'
case '2':
if (attr.is_password) {
return 'duose-password'
}
if (attr.is_link) {
return 'duose-link'
}
if (attr.is_index === false) {
return 'duose-changwenben1'
}
return 'duose-wenben'
case '3':
return 'duose-datetime'
case '4':
return 'duose-date'
case '5':
return 'duose-time'
case '6':
return 'duose-json'
case '7':
if (attr.is_bool) {
return 'duose-boole'
}
return 'duose-password'
case '8':
return 'duose-link'
case '9':
return 'duose-changwenben1'
case '10':
return 'duose-boole'
case '11':
return 'duose-quote'
default:
return ''
}
}
export const getPropertyType = (attr) => {
if (attr.is_password) {
return '7'
}
if (attr.is_link) {
return '8'
}
switch (attr.value_type) {
case '0':
if (attr.is_reference) {
return '11'
}
return '0'
case '2':
if (!attr.is_index) {
return '9'
}
return '2'
case '7':
if (attr.is_bool) {
return '10'
}
return '7'
default:
return attr?.value_type ?? ''
}
}
export const getLastLayout = (data, x1 = 0, y1 = 0, w1 = 0) => {
const _tempData = _.orderBy(data, ['y', 'x'], ['asc', 'asc'])
if (!_tempData.length) {
return { xLast: 0, yLast: 0, wLast: 0 }
}
const { x, y, w } = _tempData[_tempData.length - 1]
if (y < y1) {
return { xLast: x1, yLast: y1, wLast: w1 }
} else if (y > y1) {
return { xLast: x, yLast: y, wLast: w }
} else {
const xLast = _.max([x, x1])
return { xLast, yLast: y, wLast: xLast === x ? w : w1 }
}
}
// 数字加逗号
export const toThousands = (num = 0) => {
return num.toString().replace(/\d+/, function (n) {
return n.replace(/(\d)(?=(?:\d{3})+$)/g, '$1,')
})
}
export const downloadExcel = (data, fileName = `${moment().format('YYYY-MM-DD HH:mm:ss')}.xls`) => {
// STEP 1: Create a new workbook
const wb = XLSXS.utils.book_new()
// STEP 2: Create data rows and styles
const rowArray = data
// STEP 3: Create worksheet with rows; Add worksheet to workbook
const ws = XLSXS.utils.aoa_to_sheet(rowArray)
XLSXS.utils.book_append_sheet(wb, ws, fileName)
let maxColumnNumber = 1 // 默认最大列数
rowArray.forEach(item => { if (item.length > maxColumnNumber) { maxColumnNumber = item.length } })
// 添加列宽
ws['!cols'] = (rowArray[0].map(item => {
return { width: 22 }
}))
// // 添加行高
// ws['!rows'] = [{ 'hpt': 80 }]
// STEP 4: Write Excel file to browser #导出
XLSXS.writeFile(wb, fileName + '.xlsx')
}
export const getAllParentNodesLabel = (node, label) => {
if (node.parentNode) {
return getAllParentNodesLabel(node.parentNode, `${node.parentNode.label}-${label}`)
}
return label
}
export const getTreeSelectLabel = (node) => {
return `${getAllParentNodesLabel(node, node.label)}`
}

View File

@@ -74,205 +74,24 @@
</div>
</SearchForm>
<CiDetailDrawer ref="detail" :typeId="typeId" />
<ops-table
:id="`cmdb-ci-${typeId}`"
border
keep-source
show-overflow
resizable
<CITable
ref="xTable"
size="small"
:row-config="{ useKey: true, keyField: '_id' }"
:height="tableHeight"
show-header-overflow
highlight-hover-row
:id="`cmdb-ci-${typeId}`"
:loading="loading"
:attrList="preferenceAttrList"
:columns="columns"
:passwordValue="passwordValue"
:data="instanceList"
@checkbox-change="onSelectChange"
@checkbox-all="onSelectChange"
@checkbox-range-end="onSelectRangeEnd"
:checkbox-config="{ reserve: true, highlight: true, range: true }"
:height="tableHeight"
@onSelectChange="onSelectChange"
@edit-closed="handleEditClose"
@edit-actived="handleEditActived"
:edit-config="{ trigger: 'dblclick', mode: 'row', showIcon: false }"
:sort-config="{ remote: true, trigger: 'cell' }"
@sort-change="handleSortCol"
:row-key="true"
:column-key="true"
:cell-style="getCellStyle"
:scroll-y="{ enabled: true, gt: 20 }"
:scroll-x="{ enabled: true, gt: 0 }"
class="ops-unstripe-table checkbox-hover-table"
:custom-config="{ storage: true }"
>
<vxe-column align="center" type="checkbox" width="60" :fixed="isCheckboxFixed ? 'left' : ''">
<template #default="{row}">
{{ getRowSeq(row) }}
</template>
</vxe-column>
<vxe-table-column
v-for="(col, index) in columns"
:key="`${col.field}_${index}`"
:title="col.title"
:field="col.field"
:width="col.width"
:sortable="col.sortable"
:edit-render="getColumnsEditRender(col)"
:cell-type="col.value_type === '2' ? 'string' : 'auto'"
:fixed="col.is_fixed ? 'left' : ''"
>
<template #header>
<span class="vxe-handle">
<OpsMoveIcon
style="width: 17px; height: 17px; display: none; position: absolute; left: -3px; top: 12px"
/>
<span>{{ col.title }}</span>
</span>
</template>
<template v-if="col.is_choice || col.is_password" #edit="{ row }">
<vxe-input v-if="col.is_password" v-model="passwordValue[col.field]" />
<a-select
:getPopupContainer="(trigger) => trigger.parentElement"
:style="{ width: '100%', height: '32px' }"
v-model="row[col.field]"
:v-bind="$t('placeholder2')"
v-if="col.is_choice"
:showArrow="false"
:mode="col.is_list ? 'multiple' : 'default'"
class="ci-table-edit-select"
allowClear
showSearch
>
<a-select-option
:value="choice[0]"
:key="'edit_' + col.field + idx"
v-for="(choice, idx) in col.filters"
>
<span
:style="{ ...(choice[1] ? choice[1].style : {}), display: 'inline-flex', alignItems: 'center' }"
>
<template v-if="choice[1] && choice[1].icon && choice[1].icon.name">
<img
v-if="choice[1].icon.id && choice[1].icon.url"
:src="`/api/common-setting/v1/file/${choice[1].icon.url}`"
:style="{ maxHeight: '13px', maxWidth: '13px', marginRight: '5px' }"
/>
<ops-icon
v-else
:style="{ color: choice[1].icon.color, marginRight: '5px' }"
:type="choice[1].icon.name"
/>
</template>
<span>{{ choice[0] }}</span>
</span>
</a-select-option>
</a-select>
</template>
<template
v-if="col.value_type === '6' || col.is_link || col.is_password || col.is_choice"
#default="{ row }"
>
<span v-if="col.value_type === '6' && row[col.field]">{{ row[col.field] }}</span>
<template v-else-if="col.is_link && row[col.field]">
<a
v-for="(item, linkIndex) in (col.is_list ? row[col.field] : [row[col.field]])"
:key="linkIndex"
:href="
item.startsWith('http') || item.startsWith('https')
? `${item}`
: `http://${item}`
"
target="_blank"
>
{{ item }}
</a>
</template>
<PasswordField
v-else-if="col.is_password && row[col.field]"
:ci_id="row._id"
:attr_id="col.attr_id"
></PasswordField>
<template v-else-if="col.is_choice">
<template v-if="col.is_list">
<span
v-for="value in row[col.field]"
:key="value"
:style="{
borderRadius: '4px',
padding: '1px 5px',
margin: '2px',
verticalAlign: 'bottom',
...getChoiceValueStyle(col, value),
display: 'inline-flex',
alignItems: 'center',
}"
>
<img
v-if="getChoiceValueIcon(col, value).id && getChoiceValueIcon(col, value).url"
:src="`/api/common-setting/v1/file/${getChoiceValueIcon(col, value).url}`"
:style="{ maxHeight: '13px', maxWidth: '13px', marginRight: '5px' }"
/>
<ops-icon
v-else-if="getChoiceValueIcon(col, value).name"
:style="{ color: getChoiceValueIcon(col, value).color, marginRight: '5px' }"
:type="getChoiceValueIcon(col, value).name"
/>{{ value }}
</span>
</template>
<span
v-else
:style="{
borderRadius: '4px',
padding: '1px 5px',
margin: '2px 0',
verticalAlign: 'bottom',
...getChoiceValueStyle(col, row[col.field]),
display: 'inline-flex',
alignItems: 'center',
}"
>
<img
v-if="getChoiceValueIcon(col, row[col.field]).id && getChoiceValueIcon(col, row[col.field]).url"
:src="`/api/common-setting/v1/file/${getChoiceValueIcon(col, row[col.field]).url}`"
:style="{ maxHeight: '13px', maxWidth: '13px', marginRight: '5px' }"
/>
<ops-icon
v-else-if="getChoiceValueIcon(col, row[col.field]).name"
:style="{ color: getChoiceValueIcon(col, row[col.field]).color, marginRight: '5px' }"
:type="getChoiceValueIcon(col, row[col.field]).name"
/>
{{ row[col.field] }}
</span>
</template>
</template>
</vxe-table-column>
<vxe-column align="left" field="operate" fixed="right" width="80">
<template #header>
<span>{{ $t('operation') }}</span>
</template>
<template #default="{ row }">
<a-space>
<a @click="$refs.detail.create(row.ci_id || row._id)">
<a-icon type="unordered-list" />
</a>
<a-tooltip :title="$t('cmdb.ci.addRelation')">
<a @click="$refs.detail.create(row.ci_id || row._id, 'tab_2', '2')">
<a-icon type="retweet" />
</a>
</a-tooltip>
<a @click="deleteCI(row)" :style="{ color: 'red' }">
<a-icon type="delete" />
</a>
</a-space>
</template>
</vxe-column>
<template #empty>
<div v-if="loading" style="height: 200px; line-height: 200px">{{ $t('loading') }}</div>
<div v-else>
<img :style="{ width: '200px' }" :src="require('@/assets/data_empty.png')" />
<div>{{ $t('noData') }}</div>
</div>
</template>
</ops-table>
@openDetail="openDetail"
@deleteCI="deleteCI"
/>
<div :style="{ textAlign: 'right', marginTop: '4px' }">
<a-pagination
:showSizeChanger="true"
@@ -304,7 +123,6 @@
</a-pagination>
</div>
<create-instance-form ref="create" @reload="reloadData" @submit="batchUpdate" />
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
<BatchDownload ref="batchDownload" @batchDownload="batchDownload" />
<ci-rollback-form ref="ciRollbackForm" @batchRollbackAsync="batchRollbackAsync($event)" :ciIds="selectedRowKeys" />
<MetadataDrawer ref="metadataDrawer" />
@@ -324,7 +142,6 @@ import SearchForm from '../../components/searchForm/SearchForm.vue'
import CreateInstanceForm from './modules/CreateInstanceForm'
import CiDetailDrawer from './modules/ciDetailDrawer.vue'
import EditAttrsPopover from './modules/editAttrsPopover'
import JsonEditor from '../../components/JsonEditor/jsonEditor.vue'
import { searchCI, updateCI, deleteCI } from '@/modules/cmdb/api/ci'
import { getSubscribeAttributes, subscribeCIType, subscribeTreeView } from '@/modules/cmdb/api/preference'
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
@@ -332,15 +149,14 @@ import { roleHasPermissionToGrant } from '@/modules/acl/api/permission'
import { searchResourceType } from '@/modules/acl/api/resource'
import { getCITableColumns } from '../../utils/helper'
import { intersection } from '@/utils/functions/set'
import PasswordField from '../../components/passwordField/index.vue'
import BatchDownload from '../../components/batchDownload/batchDownload.vue'
import PreferenceSearch from '../../components/preferenceSearch/preferenceSearch.vue'
import MetadataDrawer from './modules/MetadataDrawer.vue'
import CMDBGrant from '../../components/cmdbGrant'
import { ops_move_icon as OpsMoveIcon } from '@/core/icons'
import { getAttrPassword } from '../../api/CITypeAttr'
import CiRollbackForm from './modules/ciRollbackForm.vue'
import { CIBaselineRollback } from '../../api/history'
import CITable from '@/modules/cmdb/components/ciTable/index.vue'
export default {
name: 'InstanceList',
@@ -348,24 +164,18 @@ export default {
SearchForm,
CreateInstanceForm,
CiDetailDrawer,
JsonEditor,
PasswordField,
EditAttrsPopover,
BatchDownload,
PreferenceSearch,
MetadataDrawer,
CMDBGrant,
OpsMoveIcon,
CiRollbackForm,
CITable
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
isCheckboxFixed() {
const idx = this.columns.findIndex((item) => item.is_fixed)
return idx > -1
},
tableHeight() {
// if (this.selectedRowKeys && this.selectedRowKeys.length) {
// return this.windowHeight - 246
@@ -551,12 +361,7 @@ export default {
const subscribed = await getSubscribeAttributes(this.typeId)
this.preferenceAttrList = subscribed.attributes // All columns that have been subscribed
},
onSelectChange() {
const xTable = this.$refs.xTable.getVxetableRef()
const records = [...xTable.getCheckboxRecords(), ...xTable.getCheckboxReserveRecords()]
this.selectedRowKeys = records.map((i) => i.ci_id || i._id)
},
onSelectRangeEnd({ records }) {
onSelectChange(records) {
this.selectedRowKeys = records.map((i) => i.ci_id || i._id)
},
reloadData() {
@@ -612,7 +417,7 @@ export default {
async openBatchDownload() {
this.$refs.batchDownload.open({
preferenceAttrList: this.preferenceAttrList,
preferenceAttrList: this.preferenceAttrList.filter((attr) => !attr?.is_reference),
ciTypeName: this.$route.meta.title || this.$route.meta.name,
})
},
@@ -660,13 +465,12 @@ export default {
this.loadTip = this.$t('cmdb.ci.batchUpdateInProgress') + '...'
const payload = {}
Object.keys(values).forEach((key) => {
if (values[key] || values[key] === 0) {
payload[key] = values[key]
}
// Field values support blanking
// There are currently field values that do not support blanking and will be returned by the backend.
if (values[key] === undefined || values[key] === null) {
payload[key] = null
} else {
payload[key] = values[key]
}
})
this.$refs.create.visible = false
@@ -820,28 +624,6 @@ export default {
await this.loadPreferenceAttrList()
await this.loadTableData()
},
getColumnsEditRender(col) {
const _editRender = {
...col.editRender,
}
if (col.value_type === '6') {
_editRender.events = { focus: this.handleFocusJson }
}
return _editRender
},
handleFocusJson({ column, row }) {
this.$refs.jsonEditor.open(column, row)
},
jsonEditorOk(row, column, jsonData) {
// The backend writes data at different speeds. You can modify the table data directly without pulling the interface.
// this.reloadData()
this.instanceList.forEach((item) => {
if (item._id === row._id) {
item[column.property] = JSON.stringify(jsonData)
}
})
this.$refs.xTable.getVxetableRef().refreshColumn()
},
onShowSizeChange(current, pageSize) {
this.pageSize = pageSize
if (this.currentPage === 1) {
@@ -903,23 +685,6 @@ export default {
)
})
},
// tableFilterChangeEvent({ column, property, values, datas, filterList, $event }) {
// console.log(111)
// },
getChoiceValueStyle(col, colValue) {
const _find = col.filters.find((item) => String(item[0]) === String(colValue))
if (_find) {
return _find[1]?.style || {}
}
return {}
},
getChoiceValueIcon(col, colValue) {
const _find = col.filters.find((item) => String(item[0]) === String(colValue))
if (_find) {
return _find[1]?.icon || {}
}
return {}
},
handleEditActived() {
this.isEditActive = true
const passwordCol = this.columns.filter((col) => col.is_password)
@@ -945,19 +710,6 @@ export default {
this.lastEditCiId = row._id
})
},
getCellStyle({ row, rowIndex, $rowIndex, column, columnIndex, $columnIndex }) {
const { property } = column
const _find = this.preferenceAttrList.find((attr) => attr.name === property)
if (
_find &&
_find.option &&
_find.option.fontOptions &&
row[`${property}`] !== undefined &&
row[`${property}`] !== null
) {
return { ..._find.option.fontOptions }
}
},
getQAndSort() {
const fuzzySearch = this.$refs['search'].fuzzySearch || ''
const expression = this.$refs['search'].expression || ''
@@ -1055,8 +807,8 @@ export default {
this.visible = false
}
},
getRowSeq(row) {
return this.$refs.xTable.getVxetableRef().getRowSeq(row)
openDetail(id, activeTabKey, ciDetailRelationKey) {
this.$refs.detail.create(id, activeTabKey, ciDetailRelationKey)
}
},
}
@@ -1075,33 +827,4 @@ export default {
overflow: auto;
margin-bottom: -24px;
}
.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;
}
}
}
}
}
</style>

View File

@@ -1,431 +1,547 @@
<template>
<CustomDrawer
:title="title + CIType.alias"
width="800"
@close="handleClose"
:maskClosable="false"
:visible="visible"
wrapClassName="create-instance-form"
:bodyStyle="{ paddingTop: 0 }"
:headerStyle="{ borderBottom: 'none' }"
>
<div class="custom-drawer-bottom-action">
<a-button @click="handleClose">{{ $t('cancel') }}</a-button>
<a-button type="primary" @click="createInstance">{{ $t('submit') }}</a-button>
</div>
<template v-if="action === 'create'">
<template v-for="group in attributesByGroup">
<CreateInstanceFormByGroup
:ref="`createInstanceFormByGroup_${group.id}`"
:key="group.id || group.name"
:group="group"
@handleFocusInput="handleFocusInput"
:attributeList="attributeList"
/>
</template>
<template v-if="parentsType && parentsType.length">
<a-divider style="font-size:14px;margin:14px 0;font-weight:700;">{{
$t('cmdb.menu.citypeRelation')
}}</a-divider>
<a-form>
<a-row :gutter="24" align="top" type="flex">
<a-col :span="12" v-for="item in parentsType" :key="item.id">
<a-form-item :label="item.alias || item.name" :colon="false">
<a-input-group compact style="width: 100%">
<a-select v-model="parentsForm[item.name].attr">
<a-select-option
:title="attr.alias || attr.name"
v-for="attr in item.attributes"
:key="attr.name"
:value="attr.name"
>
{{ attr.alias || attr.name }}
</a-select-option>
</a-select>
<a-input
:placeholder="$t('cmdb.ci.tips1')"
v-model="parentsForm[item.name].value"
style="width: 50%"
/>
</a-input-group>
</a-form-item>
</a-col>
</a-row>
</a-form>
</template>
</template>
<template v-if="action === 'update'">
<a-form :form="form">
<p>{{ $t('cmdb.ci.tips2') }}</p>
<a-row :gutter="24" v-for="list in batchUpdateLists" :key="list.name">
<a-col :span="11">
<a-form-item>
<el-select showSearch size="small" filterable v-model="list.name" :placeholder="$t('cmdb.ci.tips3')">
<el-option
v-for="attr in attributeList"
:key="attr.name"
:value="attr.name"
:disabled="batchUpdateLists.findIndex((item) => item.name === attr.name) > -1"
:label="attr.alias || attr.name"
>
</el-option>
</el-select>
</a-form-item>
</a-col>
<a-col :span="11">
<a-form-item>
<a-select
:style="{ width: '100%' }"
v-decorator="[list.name, { rules: [{ required: false }] }]"
:placeholder="$t('placeholder2')"
v-if="getFieldType(list.name).split('%%')[0] === 'select'"
:mode="getFieldType(list.name).split('%%')[1] === 'multiple' ? 'multiple' : 'default'"
showSearch
allowClear
>
<a-select-option
:value="choice[0]"
:key="'New_' + choice + choice_idx"
v-for="(choice, choice_idx) in getSelectFieldOptions(list.name)"
>
<span :style="choice[1] ? choice[1].style || {} : {}">
<ops-icon
:style="{ color: choice[1].icon.color }"
v-if="choice[1] && choice[1].icon && choice[1].icon.name"
:type="choice[1].icon.name"
/>
{{ choice[0] }}
</span>
</a-select-option>
</a-select>
<a-input-number
v-decorator="[list.name, { rules: [{ required: false }] }]"
style="width: 100%"
v-if="getFieldType(list.name) === 'input_number'"
/>
<a-date-picker
v-decorator="[list.name, { rules: [{ required: false }] }]"
style="width: 100%"
:format="getFieldType(list.name) == '4' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
:valueFormat="getFieldType(list.name) == '4' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
v-if="getFieldType(list.name) === '4' || getFieldType(list.name) === '3'"
:showTime="getFieldType(list.name) === '4' ? false : { format: 'HH:mm:ss' }"
/>
<a-input
v-if="getFieldType(list.name) === 'input'"
@focus="(e) => handleFocusInput(e, list)"
v-decorator="[list.name, { rules: [{ required: false }] }]"
/>
</a-form-item>
</a-col>
<a-col :span="2">
<a-form-item>
<a :style="{ color: 'red', marginTop: '2px' }" @click="handleDelete(list.name)">
<a-icon type="delete" />
</a>
</a-form-item>
</a-col>
</a-row>
<a-button type="primary" ghost icon="plus" @click="handleAdd">{{ $t('cmdb.ci.newUpdateField') }}</a-button>
</a-form>
</template>
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
</CustomDrawer>
</template>
<script>
import _ from 'lodash'
import moment from 'moment'
import { Select, Option } from 'element-ui'
import { getCIType, getCITypeGroupById } from '@/modules/cmdb/api/CIType'
import { addCI } from '@/modules/cmdb/api/ci'
import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue'
import { valueTypeMap } from '../../../utils/const'
import CreateInstanceFormByGroup from './createInstanceFormByGroup.vue'
import { getCITypeParent, getCanEditByParentIdChildId } from '@/modules/cmdb/api/CITypeRelation'
export default {
name: 'CreateInstanceForm',
components: {
ElSelect: Select,
ElOption: Option,
JsonEditor,
CreateInstanceFormByGroup,
},
props: {
typeIdFromRelation: {
type: Number,
default: 0,
},
},
data() {
return {
action: '',
form: this.$form.createForm(this),
visible: false,
attributeList: [],
CIType: {},
batchUpdateLists: [],
editAttr: null,
attributesByGroup: [],
parentsType: [],
parentsForm: {},
canEdit: {},
}
},
computed: {
title() {
return this.action === 'create' ? this.$t('create') + ' ' : this.$t('cmdb.ci.batchUpdate') + ' '
},
typeId() {
if (this.typeIdFromRelation) {
return this.typeIdFromRelation
}
return this.$router.currentRoute.meta.typeId
},
valueTypeMap() {
return valueTypeMap()
},
},
provide() {
return {
getFieldType: this.getFieldType,
}
},
inject: ['attrList'],
methods: {
moment,
async getCIType() {
await getCIType(this.typeId).then((res) => {
this.CIType = res.ci_types[0]
})
},
async getAttributeList() {
const _attrList = this.attrList()
this.attributeList = _attrList.sort((x, y) => y.is_required - x.is_required)
await getCITypeGroupById(this.typeId).then((res1) => {
const _attributesByGroup = res1.map((g) => {
g.attributes = g.attributes.filter((attr) => !attr.is_computed)
return g
})
const attrHasGroupIds = []
res1.forEach((g) => {
const id = g.attributes.map((attr) => attr.id)
attrHasGroupIds.push(...id)
})
const otherGroupAttr = this.attributeList.filter(
(attr) => !attrHasGroupIds.includes(attr.id) && !attr.is_computed
)
if (otherGroupAttr.length) {
_attributesByGroup.push({ id: -1, name: this.$t('other'), attributes: otherGroupAttr })
}
console.log(otherGroupAttr, _attributesByGroup)
this.attributesByGroup = _attributesByGroup
})
},
createInstance() {
const _this = this
if (_this.action === 'update') {
this.form.validateFields((err, values) => {
if (err) {
return
}
Object.keys(values).forEach((k) => {
const _tempFind = this.attributeList.find((item) => item.name === k)
if (
_tempFind.value_type === '3' &&
values[k] &&
Object.prototype.toString.call(values[k]) === '[object Object]'
) {
values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
}
if (
_tempFind.value_type === '4' &&
values[k] &&
Object.prototype.toString.call(values[k]) === '[object Object]'
) {
values[k] = values[k].format('YYYY-MM-DD')
}
if (_tempFind.value_type === '6') {
values[k] = values[k] ? JSON.parse(values[k]) : undefined
}
})
_this.$emit('submit', values)
})
} else {
let values = {}
for (let i = 0; i < this.attributesByGroup.length; i++) {
const data = this.$refs[`createInstanceFormByGroup_${this.attributesByGroup[i].id}`][0].getData()
if (data === 'error') {
return
}
values = { ...values, ...data }
}
Object.keys(values).forEach((k) => {
const _tempFind = this.attributeList.find((item) => item.name === k)
if (
_tempFind.value_type === '3' &&
values[k] &&
Object.prototype.toString.call(values[k]) === '[object Object]'
) {
values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
}
if (
_tempFind.value_type === '4' &&
values[k] &&
Object.prototype.toString.call(values[k]) === '[object Object]'
) {
values[k] = values[k].format('YYYY-MM-DD')
}
if (_tempFind.value_type === '6') {
values[k] = values[k] ? JSON.parse(values[k]) : undefined
}
})
values.ci_type = _this.typeId
console.log(this.parentsForm)
Object.keys(this.parentsForm).forEach((type) => {
if (this.parentsForm[type].value) {
values[`$${type}.${this.parentsForm[type].attr}`] = this.parentsForm[type].value
}
})
addCI(values).then((res) => {
_this.$message.success(this.$t('addSuccess'))
_this.visible = false
_this.$emit('reload', { ci_id: res.ci_id })
})
}
// this.form.validateFields((err, values) => {
// if (err) {
// _this.$message.error('字段填写不符合要求!')
// return
// }
// Object.keys(values).forEach((k) => {
// if (Object.prototype.toString.call(values[k]) === '[object Object]' && values[k]) {
// values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
// }
// const _tempFind = this.attributeList.find((item) => item.name === k)
// if (_tempFind.value_type === '6') {
// values[k] = values[k] ? JSON.parse(values[k]) : undefined
// }
// })
// if (_this.action === 'update') {
// _this.$emit('submit', values)
// return
// }
// values.ci_type = _this.typeId
// console.log(values)
// this.attributesByGroup.forEach((group) => {
// this.$refs[`createInstanceFormByGroup_${group.id}`][0].getData()
// })
// console.log(1111)
// // addCI(values).then((res) => {
// // _this.$message.success('新增成功!')
// // _this.visible = false
// // _this.$emit('reload')
// // })
// })
},
handleClose() {
this.visible = false
},
handleOpen(visible, action) {
this.visible = visible
this.action = action
this.$nextTick(() => {
this.form.resetFields()
Promise.all([this.getCIType(), this.getAttributeList()]).then(() => {
this.batchUpdateLists = [{ name: this.attributeList[0].name }]
})
if (action === 'create') {
getCITypeParent(this.typeId).then(async (res) => {
for (let i = 0; i < res.parents.length; i++) {
await getCanEditByParentIdChildId(res.parents[i].id, this.typeId).then((p_res) => {
this.canEdit = {
..._.cloneDeep(this.canEdit),
[res.parents[i].id]: p_res.result,
}
})
}
this.parentsType = res.parents.filter((parent) => this.canEdit[parent.id])
const _parentsForm = {}
res.parents.forEach((item) => {
const _find = item.attributes.find((attr) => attr.id === item.unique_id)
_parentsForm[item.name] = { attr: _find.name, value: '' }
})
this.parentsForm = _parentsForm
})
}
})
},
getFieldType(name) {
const _find = this.attributeList.find((item) => item.name === name)
if (_find) {
if (_find.is_choice) {
if (_find.is_list) {
return 'select%%multiple'
}
return 'select'
} else if ((_find.value_type === '0' || _find.value_type === '1') && !_find.is_list) {
return 'input_number'
} else if (_find.value_type === '4' || _find.value_type === '3') {
return _find.value_type
} else {
return 'input'
}
}
return 'input'
},
getSelectFieldOptions(name) {
const _find = this.attributeList.find((item) => item.name === name)
if (_find) {
return _find.choice_value
}
return []
},
handleAdd() {
this.batchUpdateLists.push({ name: undefined })
},
handleDelete(name) {
const _idx = this.batchUpdateLists.findIndex((item) => item.name === name)
if (_idx > -1) {
this.batchUpdateLists.splice(_idx, 1)
}
},
// filterOption(input, option) {
// return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
// },
handleFocusInput(e, attr) {
console.log(attr)
const _tempFind = this.attributeList.find((item) => item.name === attr.name)
if (_tempFind.value_type === '6') {
this.editAttr = attr
e.srcElement.blur()
const jsonData = this.form.getFieldValue(attr.name)
this.$refs.jsonEditor.open(null, null, jsonData ? JSON.parse(jsonData) : {})
} else {
this.editAttr = null
}
},
jsonEditorOk(jsonData) {
this.form.setFieldsValue({ [this.editAttr.name]: JSON.stringify(jsonData) })
},
},
}
</script>
<style lang="less">
.create-instance-form {
.ant-form-item {
margin-bottom: 5px;
}
.ant-drawer-body {
overflow-y: auto;
max-height: calc(100vh - 110px);
}
}
</style>
<template>
<CustomDrawer
:title="title + CIType.alias"
width="800"
@close="handleClose"
:maskClosable="false"
:visible="visible"
wrapClassName="create-instance-form"
:bodyStyle="{ paddingTop: 0 }"
:headerStyle="{ borderBottom: 'none' }"
>
<div class="custom-drawer-bottom-action">
<a-button @click="handleClose">{{ $t('cancel') }}</a-button>
<a-button type="primary" @click="createInstance">{{ $t('submit') }}</a-button>
</div>
<template v-if="action === 'create'">
<template v-for="group in attributesByGroup">
<CreateInstanceFormByGroup
:ref="`createInstanceFormByGroup_${group.id}`"
:key="group.id || group.name"
:group="group"
:attributeList="attributeList"
@handleFocusInput="handleFocusInput"
/>
</template>
<template v-if="parentsType && parentsType.length">
<a-divider style="font-size:14px;margin:14px 0;font-weight:700;">{{
$t('cmdb.menu.citypeRelation')
}}</a-divider>
<a-form>
<a-row :gutter="24" align="top" type="flex">
<a-col :span="12" v-for="item in parentsType" :key="item.id">
<a-form-item :label="item.alias || item.name" :colon="false">
<a-input-group compact style="width: 100%">
<a-select v-model="parentsForm[item.name].attr">
<a-select-option
:title="attr.alias || attr.name"
v-for="attr in filterAttributes(item.attributes)"
:key="attr.name"
:value="attr.name"
>
{{ attr.alias || attr.name }}
</a-select-option>
</a-select>
<a-input
:placeholder="$t('cmdb.ci.tips1')"
v-model="parentsForm[item.name].value"
style="width: 50%"
/>
</a-input-group>
</a-form-item>
</a-col>
</a-row>
</a-form>
</template>
</template>
<template v-if="action === 'update'">
<a-form :form="form">
<p>{{ $t('cmdb.ci.tips2') }}</p>
<a-row :gutter="8" v-for="list in batchUpdateLists" :key="list.name">
<a-col :span="6">
<a-form-item>
<el-select showSearch size="small" filterable v-model="list.name" :placeholder="$t('cmdb.ci.tips3')">
<el-option
v-for="attr in attributeList"
:key="attr.name"
:value="attr.name"
:disabled="batchUpdateLists.findIndex((item) => item.name === attr.name) > -1"
:label="attr.alias || attr.name"
>
</el-option>
</el-select>
</a-form-item>
</a-col>
<a-col v-if="showListOperation(list.name)" :span="3">
<a-form-item>
<el-select size="small" filterable v-model="list.operation" :placeholder="$t('placeholder2')">
<el-option
v-for="(option) in listOperationOptions"
:key="option.value"
:value="option.value"
:label="$t(option.label)"
>
</el-option>
</el-select>
</a-form-item>
</a-col>
<a-col :span="showListOperation(list.name) ? 10 : 13">
<a-form-item>
<CIReferenceAttr
v-if="getAttr(list.name).is_reference"
:referenceTypeId="getAttr(list.name).reference_type_id"
:isList="getAttr(list.name).is_list"
v-decorator="[
list.name,
{
initialValue: getAttr(list.name).is_list ? [] : ''
}
]"
/>
<a-switch
v-else-if="getAttr(list.name).is_bool"
v-decorator="[
list.name,
{
valuePropName: 'checked',
initialValue: false
}
]"
/>
<a-select
:style="{ width: '100%' }"
v-decorator="[list.name, { rules: getDecoratorRules(list) }]"
:placeholder="$t('placeholder2')"
v-else-if="getFieldType(list.name).split('%%')[0] === 'select'"
:mode="getFieldType(list.name).split('%%')[1] === 'multiple' ? 'multiple' : 'default'"
showSearch
allowClear
>
<a-select-option
:value="choice[0]"
:key="'New_' + choice + choice_idx"
v-for="(choice, choice_idx) in getSelectFieldOptions(list.name)"
>
<span :style="choice[1] ? choice[1].style || {} : {}">
<ops-icon
:style="{ color: choice[1].icon.color }"
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>
</span>
</a-select-option>
</a-select>
<a-input-number
v-decorator="[list.name, { rules: getDecoratorRules(list) }]"
style="width: 100%"
v-else-if="getFieldType(list.name) === 'input_number'"
/>
<a-date-picker
v-decorator="[list.name, { rules: getDecoratorRules(list) }]"
style="width: 100%"
:format="getFieldType(list.name) == '4' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
:valueFormat="getFieldType(list.name) == '4' ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
v-else-if="getFieldType(list.name) === '4' || getFieldType(list.name) === '3'"
:showTime="getFieldType(list.name) === '4' ? false : { format: 'HH:mm:ss' }"
/>
<a-input
v-else-if="getFieldType(list.name) === 'input'"
@focus="(e) => handleFocusInput(e, list)"
v-decorator="[list.name, { rules: getDecoratorRules(list) }]"
/>
</a-form-item>
</a-col>
<a-col :span="2">
<a-form-item>
<a :style="{ color: 'red', marginTop: '2px' }" @click="handleDelete(list.name)">
<a-icon type="delete" />
</a>
</a-form-item>
</a-col>
</a-row>
<a-button type="primary" ghost icon="plus" @click="handleAdd">{{ $t('cmdb.ci.newUpdateField') }}</a-button>
</a-form>
</template>
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
</CustomDrawer>
</template>
<script>
import _ from 'lodash'
import moment from 'moment'
import { Select, Option } from 'element-ui'
import { getCIType, getCITypeGroupById } from '@/modules/cmdb/api/CIType'
import { addCI } from '@/modules/cmdb/api/ci'
import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue'
import { valueTypeMap } from '../../../utils/const'
import CreateInstanceFormByGroup from './createInstanceFormByGroup.vue'
import { getCITypeParent, getCanEditByParentIdChildId } from '@/modules/cmdb/api/CITypeRelation'
import CIReferenceAttr from '@/components/ciReferenceAttr/index.vue'
export default {
name: 'CreateInstanceForm',
components: {
ElSelect: Select,
ElOption: Option,
JsonEditor,
CreateInstanceFormByGroup,
CIReferenceAttr
},
props: {
typeIdFromRelation: {
type: Number,
default: 0,
},
},
data() {
return {
action: '',
form: this.$form.createForm(this),
visible: false,
attributeList: [],
CIType: {},
batchUpdateLists: [],
editAttr: null,
attributesByGroup: [],
parentsType: [],
parentsForm: {},
canEdit: {},
listOperationOptions: [
{
value: 'cover',
label: 'cmdb.ci.cover'
},
{
value: 'add',
label: 'add'
},
{
value: 'delete',
label: 'delete'
}
]
}
},
computed: {
title() {
return this.action === 'create' ? this.$t('create') + ' ' : this.$t('cmdb.ci.batchUpdate') + ' '
},
typeId() {
if (this.typeIdFromRelation) {
return this.typeIdFromRelation
}
return this.$router.currentRoute.meta.typeId
},
valueTypeMap() {
return valueTypeMap()
},
},
provide() {
return {
getFieldType: this.getFieldType,
}
},
inject: ['attrList'],
methods: {
moment,
async getCIType() {
await getCIType(this.typeId).then((res) => {
this.CIType = res.ci_types[0]
})
},
async getAttributeList() {
const _attrList = this.attrList()
this.attributeList = _attrList.sort((x, y) => y.is_required - x.is_required)
await getCITypeGroupById(this.typeId).then((res1) => {
const _attributesByGroup = res1.map((g) => {
g.attributes = g.attributes.filter((attr) => !attr.is_computed)
return g
})
const attrHasGroupIds = []
res1.forEach((g) => {
const id = g.attributes.map((attr) => attr.id)
attrHasGroupIds.push(...id)
})
const otherGroupAttr = this.attributeList.filter(
(attr) => !attrHasGroupIds.includes(attr.id) && !attr.is_computed
)
if (otherGroupAttr.length) {
_attributesByGroup.push({ id: -1, name: this.$t('other'), attributes: otherGroupAttr })
}
console.log(otherGroupAttr, _attributesByGroup)
this.attributesByGroup = _attributesByGroup
})
},
createInstance() {
const _this = this
if (_this.action === 'update') {
this.form.validateFields({ force: true }, (err, values) => {
if (err) {
return
}
Object.keys(values).forEach((k) => {
const _tempFind = this.attributeList.find((item) => item.name === k)
if (_tempFind.is_reference) {
values[k] = values[k] ? values[k] : null
}
if (
_tempFind.value_type === '3' &&
values[k] &&
Object.prototype.toString.call(values[k]) === '[object Object]'
) {
values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
}
if (
_tempFind.value_type === '4' &&
values[k] &&
Object.prototype.toString.call(values[k]) === '[object Object]'
) {
values[k] = values[k].format('YYYY-MM-DD')
}
if (_tempFind.value_type === '6') {
values[k] = values[k] ? JSON.parse(values[k]) : undefined
}
if (_tempFind.is_list) {
const operation = this.batchUpdateLists?.find((item) => item.name === k)?.operation || 'cover'
switch (operation) {
case 'add':
case 'delete':
values[k] = {
op: operation,
v: values[k]
}
break
default:
break
}
}
})
_this.$emit('submit', values)
})
} else {
let values = {}
for (let i = 0; i < this.attributesByGroup.length; i++) {
const data = this.$refs[`createInstanceFormByGroup_${this.attributesByGroup[i].id}`][0].getData()
if (data === 'error') {
return
}
values = { ...values, ...data }
}
Object.keys(values).forEach((k) => {
const _tempFind = this.attributeList.find((item) => item.name === k)
if (_tempFind.is_reference) {
values[k] = values[k] ? values[k] : null
}
if (
_tempFind.value_type === '3' &&
values[k] &&
Object.prototype.toString.call(values[k]) === '[object Object]'
) {
values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
}
if (
_tempFind.value_type === '4' &&
values[k] &&
Object.prototype.toString.call(values[k]) === '[object Object]'
) {
values[k] = values[k].format('YYYY-MM-DD')
}
if (_tempFind.value_type === '6') {
values[k] = values[k] ? JSON.parse(values[k]) : undefined
}
})
values.ci_type = _this.typeId
console.log(this.parentsForm)
Object.keys(this.parentsForm).forEach((type) => {
if (this.parentsForm[type].value) {
values[`$${type}.${this.parentsForm[type].attr}`] = this.parentsForm[type].value
}
})
addCI(values).then((res) => {
_this.$message.success(this.$t('addSuccess'))
_this.visible = false
_this.$emit('reload', { ci_id: res.ci_id })
})
}
// this.form.validateFields((err, values) => {
// if (err) {
// _this.$message.error('字段填写不符合要求!')
// return
// }
// Object.keys(values).forEach((k) => {
// if (Object.prototype.toString.call(values[k]) === '[object Object]' && values[k]) {
// values[k] = values[k].format('YYYY-MM-DD HH:mm:ss')
// }
// const _tempFind = this.attributeList.find((item) => item.name === k)
// if (_tempFind.value_type === '6') {
// values[k] = values[k] ? JSON.parse(values[k]) : undefined
// }
// })
// if (_this.action === 'update') {
// _this.$emit('submit', values)
// return
// }
// values.ci_type = _this.typeId
// console.log(values)
// this.attributesByGroup.forEach((group) => {
// this.$refs[`createInstanceFormByGroup_${group.id}`][0].getData()
// })
// console.log(1111)
// // addCI(values).then((res) => {
// // _this.$message.success('新增成功!')
// // _this.visible = false
// // _this.$emit('reload')
// // })
// })
},
handleClose() {
this.visible = false
},
handleOpen(visible, action) {
this.visible = visible
this.action = action
this.$nextTick(() => {
this.form.resetFields()
Promise.all([this.getCIType(), this.getAttributeList()]).then(() => {
this.batchUpdateLists = [{
name: this.attributeList?.[0]?.name || undefined,
operation: 'cover'
}]
})
if (action === 'create') {
getCITypeParent(this.typeId).then(async (res) => {
for (let i = 0; i < res.parents.length; i++) {
await getCanEditByParentIdChildId(res.parents[i].id, this.typeId).then((p_res) => {
this.canEdit = {
..._.cloneDeep(this.canEdit),
[res.parents[i].id]: p_res.result,
}
})
}
this.parentsType = res.parents.filter((parent) => this.canEdit[parent.id])
const _parentsForm = {}
res.parents.forEach((item) => {
const _find = item.attributes.find((attr) => attr.id === item.unique_id)
_parentsForm[item.name] = { attr: _find.name, value: '' }
})
this.parentsForm = _parentsForm
})
}
})
},
getFieldType(name) {
const _find = this.attributeList.find((item) => item.name === name)
if (_find) {
if (_find.is_choice) {
if (_find.is_list) {
return 'select%%multiple'
}
return 'select'
} else if ((_find.value_type === '0' || _find.value_type === '1') && !_find.is_list) {
return 'input_number'
} else if (_find.value_type === '4' || _find.value_type === '3') {
return _find.value_type
} else {
return 'input'
}
}
return 'input'
},
getAttr(name) {
return this.attributeList.find((item) => item.name === name) ?? {}
},
getSelectFieldOptions(name) {
const _find = this.attributeList.find((item) => item.name === name)
if (_find) {
return _find.choice_value
}
return []
},
handleAdd() {
this.batchUpdateLists.push({
name: undefined,
operation: 'cover'
})
},
handleDelete(name) {
const _idx = this.batchUpdateLists.findIndex((item) => item.name === name)
if (_idx > -1) {
this.batchUpdateLists.splice(_idx, 1)
}
},
// filterOption(input, option) {
// return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
// },
handleFocusInput(e, attr) {
console.log(attr)
const _tempFind = this.attributeList.find((item) => item.name === attr.name)
if (_tempFind.value_type === '6') {
this.editAttr = attr
e.srcElement.blur()
const jsonData = this.form.getFieldValue(attr.name)
this.$refs.jsonEditor.open(null, null, jsonData ? JSON.parse(jsonData) : {})
} else {
this.editAttr = null
}
},
jsonEditorOk(jsonData) {
this.form.setFieldsValue({ [this.editAttr.name]: JSON.stringify(jsonData) })
},
showListOperation(name) {
if (!name) {
return false
}
const attr = this.attributeList.find((attr) => attr.name === name)
return attr && attr.is_list
},
getDecoratorRules(data) {
const { name, operation } = data
const isList = this.showListOperation(name)
const rules = [
{ required: false }
]
if (isList && ['delete', 'add'].includes(operation)) {
rules[0] = {
required: true,
message: this.$t('placeholder1')
}
}
return rules
},
filterAttributes(attributes) {
return attributes.filter((attr) => {
return !attr.is_bool && !attr.is_reference
})
},
},
}
</script>
<style lang="less">
.create-instance-form {
.ant-form-item {
margin-bottom: 5px;
}
.ant-drawer-body {
overflow-y: auto;
height: calc(100vh - 110px);
}
}
</style>

View File

@@ -79,6 +79,8 @@
import XEUtils from 'xe-utils'
import { getCITypeAttributesByName } from '@/modules/cmdb/api/CITypeAttr'
import { valueTypeMap } from '@/modules/cmdb/utils/const'
import { getPropertyType } from '@/modules/cmdb/utils/helper'
export default {
name: 'MetadataDrawer',
data() {
@@ -187,12 +189,7 @@ export default {
this.loading = true
const { attributes = [] } = await getCITypeAttributesByName(this.typeId)
this.tableData = attributes.map((attr) => {
if (attr.is_password) {
attr.value_type = '7'
}
if (attr.is_link) {
attr.value_type = '8'
}
attr.value_type = getPropertyType(attr)
return attr
})
this.loading = false

View File

@@ -1,9 +1,19 @@
<template>
<span :id="`ci-detail-attr-${attr.name}`">
<span v-if="!isEdit || attr.value_type === '6'">
<template v-if="attr.is_reference" >
<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"
>
{{ attr.referenceShowAttrNameMap ? attr.referenceShowAttrNameMap[ciId] || ciId : ciId }}
</a>
</template>
<PasswordField
:style="{ display: 'inline-block' }"
v-if="attr.is_password && ci[attr.name]"
v-else-if="attr.is_password && ci[attr.name]"
:ci_id="ci._id"
:attr_id="attr.id"
></PasswordField>
@@ -32,7 +42,7 @@
:style="{ color: getChoiceValueIcon(attr, value).color, marginRight: '5px' }"
:type="getChoiceValueIcon(attr, value).name"
/>
{{ value }}</span
{{ getChoiceValueLabel(attr, value) || value }}</span
>
</template>
<span
@@ -56,7 +66,7 @@
:style="{ color: getChoiceValueIcon(attr, ci[attr.name]).color, marginRight: '5px' }"
:type="getChoiceValueIcon(attr, ci[attr.name]).name"
/>
{{ ci[attr.name] }}
{{ getChoiceValueLabel(attr, ci[attr.name]) || ci[attr.name] }}
</span>
</template>
<template v-else-if="attr.is_list">
@@ -67,6 +77,29 @@
<template v-else>
<a-form :form="form">
<a-form-item label="" :colon="false">
<CIReferenceAttr
v-if="attr.is_reference"
:referenceTypeId="attr.reference_type_id"
:isList="attr.is_list"
:referenceShowAttrName="attr.showAttrName"
:initSelectOption="getInitReferenceSelectOption(attr)"
v-decorator="[
attr.name,
{
rules: [{ required: attr.is_required, message: $t('placeholder2') + `${attr.alias || attr.name}` }],
}
]"
/>
<a-switch
v-else-if="attr.is_bool"
v-decorator="[
attr.name,
{
rules: [{ required: attr.is_required }],
valuePropName: 'checked',
}
]"
/>
<a-select
:style="{ width: '100%' }"
v-decorator="[
@@ -76,7 +109,7 @@
},
]"
:placeholder="$t('placeholder2')"
v-if="attr.is_choice"
v-else-if="attr.is_choice"
:mode="attr.is_list ? 'multiple' : 'default'"
showSearch
allowClear
@@ -101,7 +134,7 @@
:type="choice[1].icon.name"
/>
</template>
{{ choice[0] }}
{{ choice[1].label || choice[0] }}
</span>
</a-select-option>
</a-select>
@@ -157,10 +190,11 @@ import { updateCI } from '@/modules/cmdb/api/ci'
import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue'
import PasswordField from '../../../components/passwordField/index.vue'
import { getAttrPassword } from '../../../api/CITypeAttr'
import CIReferenceAttr from '@/components/ciReferenceAttr/index.vue'
export default {
name: 'CiDetailAttrContent',
components: { JsonEditor, PasswordField },
components: { JsonEditor, PasswordField, CIReferenceAttr },
props: {
ci: {
type: Object,
@@ -209,7 +243,7 @@ export default {
}
this.isEdit = true
this.$nextTick(async () => {
if (this.attr.is_list && !this.attr.is_choice) {
if (this.attr.is_list && !this.attr.is_choice && !this.attr.is_reference) {
this.form.setFieldsValue({
[`${this.attr.name}`]: Array.isArray(this.ci[this.attr.name])
? this.ci[this.attr.name].join(',')
@@ -237,6 +271,10 @@ export default {
.then(() => {
this.$message.success(this.$t('updateSuccess'))
this.$emit('updateCIByself', { [`${this.attr.name}`]: newData }, this.attr.name)
if (this.attr.is_reference) {
this.$emit('refreshReferenceAttr')
}
})
.catch(() => {
this.$emit('refresh', this.attr.name)
@@ -280,9 +318,28 @@ export default {
}
return {}
},
getChoiceValueLabel(col, colValue) {
const _find = col.choice_value.find((item) => String(item[0]) === String(colValue))
if (_find) {
return _find[1]?.label || ''
}
return ''
},
getName(name) {
return name ?? ''
},
getInitReferenceSelectOption(attr) {
const option = Object.keys(attr?.referenceShowAttrNameMap || {}).map((key) => {
return {
key: Number(key),
title: attr?.referenceShowAttrNameMap?.[key] ?? ''
}
})
return option
}
},
}
</script>

View File

@@ -38,6 +38,16 @@
resizable
class="ops-stripe-table"
>
<template #reference_default="{ row, column }">
<a
v-for="(id) in (column.params.attr.is_list ? row[column.field] : [row[column.field]])"
:key="id"
:href="`/cmdb/cidetail/${column.params.attr.reference_type_id}/${id}`"
target="_blank"
>
{{ id }}
</a>
</template>
<template #operation_default="{ row }">
<a-popconfirm
arrowPointAtCenter
@@ -85,6 +95,16 @@
resizable
class="ops-stripe-table"
>
<template #reference_default="{ row, column }">
<a
v-for="(id) in (column.params.attr.is_list ? row[column.field] : [row[column.field]])"
:key="id"
:href="`/cmdb/cidetail/${column.params.attr.reference_type_id}/${id}`"
target="_blank"
>
{{ id }}
</a>
</template>
<template #operation_default="{ row }">
<a-popconfirm
arrowPointAtCenter
@@ -213,6 +233,7 @@ export default {
this.firstCIJsonAttr[item._type].forEach((attr) => {
item[`${attr}`] = item[`${attr}`] ? JSON.stringify(item[`${attr}`]) : ''
})
this.formatCI(item, this.firstCIColumns)
if (item.ci_type in firstCIs) {
firstCIs[item.ci_type].push(item)
} else {
@@ -231,6 +252,7 @@ export default {
this.secondCIJsonAttr[item._type].forEach((attr) => {
item[`${attr}`] = item[`${attr}`] ? JSON.stringify(item[`${attr}`]) : ''
})
this.formatCI(item, this.secondCIColumns)
if (item.ci_type in secondCIs) {
secondCIs[item.ci_type].push(item)
} else {
@@ -241,6 +263,26 @@ export default {
})
.catch((e) => {})
},
formatCI(ci, columns) {
Object.keys(ci).forEach((key) => {
const attr = columns?.[ci?._type]?.find((item) => item?.params?.attr?.name === key)?.params?.attr
if (attr?.is_choice && attr?.choice_value?.length) {
if (attr?.is_list) {
ci[key] = ci[key].map((value) => {
const label = attr?.choice_value?.find((choice) => choice?.[0] === value)?.[1]?.label
return label || ci[key]
})
} else {
const label = attr?.choice_value?.find((choice) => choice?.[0] === ci[key])?.[1]?.label
ci[key] = label || ci[key]
}
}
})
return ci
},
async getParentCITypes() {
const res = await getCITypeParent(this.typeId)
this.parentCITypes = res.parents
@@ -258,7 +300,22 @@ export default {
const columns = []
const jsonAttr = []
item.attributes.forEach((attr) => {
columns.push({ key: 'p_' + attr.id, field: attr.name, title: attr.alias, minWidth: '100px' })
const column = {
key: 'p_' + attr.id,
field: attr.name,
title: attr.alias,
minWidth: '100px',
params: {
attr
},
}
if (attr.is_reference) {
column.slots = {
default: 'reference_default'
}
}
columns.push(column)
if (attr.value_type === '6') {
jsonAttr.push(attr.name)
}
@@ -299,7 +356,22 @@ export default {
const columns = []
const jsonAttr = []
item.attributes.forEach((attr) => {
columns.push({ key: 'c_' + attr.id, field: attr.name, title: attr.alias, minWidth: '100px' })
const column = {
key: 'c_' + attr.id,
field: attr.name,
title: attr.alias,
minWidth: '100px',
params: {
attr
},
}
if (attr.is_reference) {
column.slots = {
default: 'reference_default'
}
}
columns.push(column)
if (attr.value_type === '6') {
jsonAttr.push(attr.name)
}

View File

@@ -20,7 +20,7 @@
:key="attr.name"
v-for="attr in group.attributes"
>
<ci-detail-attr-content :ci="ci" :attr="attr" @refresh="refresh" @updateCIByself="updateCIByself" />
<ci-detail-attr-content :ci="ci" :attr="attr" @refresh="refresh" @updateCIByself="updateCIByself" @refreshReferenceAttr="handleReferenceAttr" />
</el-descriptions-item>
</el-descriptions>
</div>
@@ -137,7 +137,7 @@ import _ from 'lodash'
import { Descriptions, DescriptionsItem } from 'element-ui'
import { getCITypeGroupById, getCITypes } from '@/modules/cmdb/api/CIType'
import { getCIHistory, judgeItsmInstalled } from '@/modules/cmdb/api/history'
import { getCIById } from '@/modules/cmdb/api/ci'
import { getCIById, searchCI } from '@/modules/cmdb/api/ci'
import CiDetailAttrContent from './ciDetailAttrContent.vue'
import CiDetailRelation from './ciDetailRelation.vue'
import TriggerTable from '../../operation_history/modules/triggerTable.vue'
@@ -244,9 +244,78 @@ export default {
getCITypeGroupById(this.typeId, { need_other: 1 })
.then((res) => {
this.attributeGroups = res
this.handleReferenceAttr()
})
.catch((e) => {})
},
async handleReferenceAttr() {
const map = {}
this.attributeGroups.forEach((group) => {
group.attributes.forEach((attr) => {
if (attr?.is_reference && attr?.reference_type_id && this.ci[attr.name]) {
const ids = Array.isArray(this.ci[attr.name]) ? this.ci[attr.name] : this.ci[attr.name] ? [this.ci[attr.name]] : []
if (ids.length) {
if (!map?.[attr.reference_type_id]) {
map[attr.reference_type_id] = {}
}
ids.forEach((id) => {
map[attr.reference_type_id][id] = {}
})
}
}
})
})
if (!Object.keys(map).length) {
return
}
const ciTypesRes = await getCITypes({
type_ids: Object.keys(map).join(',')
})
const showAttrNameMap = {}
ciTypesRes.ci_types.forEach((ciType) => {
showAttrNameMap[ciType.id] = ciType?.show_name || ciType?.unique_name || ''
})
const allRes = await Promise.all(
Object.keys(map).map((key) => {
return searchCI({
q: `_type:${key},_id:(${Object.keys(map[key]).join(';')})`,
count: 9999
})
})
)
const ciNameMap = {}
allRes.forEach((res) => {
res.result.forEach((item) => {
ciNameMap[item._id] = item
})
})
const newAttrGroups = _.cloneDeep(this.attributeGroups)
newAttrGroups.forEach((group) => {
group.attributes.forEach((attr) => {
if (attr?.is_reference && attr?.reference_type_id) {
attr.showAttrName = showAttrNameMap?.[attr?.reference_type_id] || ''
const referenceShowAttrNameMap = {}
const referenceCIIds = this.ci[attr.name];
(Array.isArray(referenceCIIds) ? referenceCIIds : referenceCIIds ? [referenceCIIds] : []).forEach((id) => {
referenceShowAttrNameMap[id] = ciNameMap?.[id]?.[attr.showAttrName] ?? id
})
attr.referenceShowAttrNameMap = referenceShowAttrNameMap
}
})
})
this.$set(this, 'attributeGroups', newAttrGroups)
},
async getCI() {
await getCIById(this.ciId)
.then((res) => {

View File

@@ -16,24 +16,40 @@
:key="attr.name + attr_idx"
>
<a-form-item :label="attr.alias || attr.name" :colon="false">
<CIReferenceAttr
v-if="attr.is_reference"
:referenceTypeId="attr.reference_type_id"
:isList="attr.is_list"
v-decorator="[
attr.name,
{
rules: [{ required: attr.is_required, message: $t('placeholder2') + `${attr.alias || attr.name}` }],
initialValue: attr.is_list ? [] : ''
}
]"
/>
<a-switch
v-else-if="attr.is_bool"
v-decorator="[
attr.name,
{
rules: [{ required: false }],
valuePropName: 'checked',
initialValue: attr.default ? Boolean(attr.default.default) : false
}
]"
/>
<a-select
:style="{ width: '100%' }"
v-decorator="[
attr.name,
{
rules: [{ required: attr.is_required, message: $t('placeholder2') + `${attr.alias || attr.name}` }],
initialValue:
attr.default && attr.default.default
? attr.is_list
? Array.isArray(attr.default.default)
? attr.default.default
: attr.default.default.split(',')
: attr.default.default
: attr.is_list ? [] : null,
initialValue: getChoiceDefault(attr),
},
]"
:placeholder="$t('placeholder2')"
v-if="attr.is_choice"
v-else-if="attr.is_choice"
:mode="attr.is_list ? 'multiple' : 'default'"
showSearch
allowClear
@@ -56,7 +72,9 @@
:type="choice[1].icon.name"
/>
</template>
{{ choice[0] }}
<a-tooltip placement="topLeft" :title="choice[1].label || choice[0]">
{{ choice[1].label || choice[0] }}
</a-tooltip>
</span>
</a-select-option>
</a-select>
@@ -67,7 +85,7 @@
attr.name,
{
rules: [{ required: attr.is_required, message: $t('placeholder2') + `${attr.alias || attr.name}` }],
initialValue: attr.default && attr.default.default ? attr.default.default : '',
initialValue: attr.default && attr.default.default ? Array.isArray(attr.default.default) ? attr.default.default.join(',') : attr.default.default : '',
},
]"
>
@@ -133,10 +151,14 @@
import _ from 'lodash'
import moment from 'moment'
import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue'
import CIReferenceAttr from '@/components/ciReferenceAttr/index.vue'
export default {
name: 'CreateInstanceFormByGroup',
components: { JsonEditor },
components: {
JsonEditor,
CIReferenceAttr
},
props: {
group: {
type: Object,
@@ -146,6 +168,10 @@ export default {
type: Array,
default: () => [],
},
ciTypeId: {
type: [Number, String],
default: ''
}
},
inject: ['getFieldType'],
data() {
@@ -183,6 +209,35 @@ export default {
jsonEditorOk(jsonData) {
this.form.setFieldsValue({ [this.editAttr.name]: JSON.stringify(jsonData) })
},
getChoiceDefault(attr) {
if (!attr?.default?.default) {
return attr.is_list ? [] : null
}
if (attr.is_list) {
let defaultValue = []
if (Array.isArray(attr.default.default)) {
defaultValue = attr.default.default
} else {
defaultValue = String(attr.default.default).split(',')
}
if (['0', '1', '11'].includes(attr.value_type)) {
defaultValue = defaultValue?.map((item) => {
const numberValue = Number(item)
return Number.isNaN(numberValue) ? item : numberValue
})
}
return defaultValue
}
let defaultValue = attr.default.default
if (['0', '1', '11'].includes(attr.value_type)) {
const numberValue = Number(defaultValue)
defaultValue = Number.isNaN(numberValue) ? attr.default.default : numberValue
}
return defaultValue
}
},
}
</script>

View File

@@ -39,6 +39,7 @@
import { mapState } from 'vuex'
import _ from 'lodash'
import { valueTypeMap } from '@/modules/cmdb/utils/const'
import { getPropertyType } from '@/modules/cmdb/utils/helper'
export default {
name: 'AllAttrDrawer',
@@ -48,7 +49,18 @@ export default {
tableData: [],
}
},
inject: ['providerGroupsData'],
inject: {
providerGroupsData: {
default: () => {
return () => {
return {
CITypeGroups: [],
otherGroupAttributes: [],
}
}
}
}
},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
@@ -84,15 +96,10 @@ export default {
})
otherAttrData.forEach((attr) => {
if (attr.is_password) {
attr.value_type = '7'
}
if (attr.is_link) {
attr.value_type = '8'
}
attr.value_type = getPropertyType(attr)
attr.groupId = -1
attr.groupName = '其他'
attr.groupName = this.$t('other')
attr.code = `{{ ${attr.name} }}`
attr.typeText = typeMap?.[attr.value_type] ?? ''
})

View File

@@ -51,7 +51,7 @@
@pushCITypeList="pushCITypeList"
@addPlugin="openEditDrawer(null, 'add', 'plugin')"
/>
<EditDrawer ref="editDrawer" :is_inner="false" @updateNotInner="updateNotInner" />
<EditDrawer ref="editDrawer" :isDiscoveryPage="false" @updateNotInner="updateNotInner" />
</div>
</template>

View File

@@ -454,10 +454,15 @@ export default {
query_expr: _findADT.query_expr || '',
enabled: _findADT?.enabled ?? true,
}
const allMachineIndex = this.agentTypeRadioList.findIndex((item) => item.value === 'all')
if (_findADT.query_expr) {
this.agent_type = 'query_expr'
} else if (_findADT.agent_id) {
this.agent_type = _findADT.agent_id === '0x0000' ? 'master' : 'agent_id'
} else if (_findADT.agent_id === '' && allMachineIndex !== -1) {
this.agent_type = 'all'
} else {
this.agent_type = this.agentTypeRadioList[0].value
}

View File

@@ -21,9 +21,7 @@
<div :class="{ 'attribute-card-name': true, 'attribute-card-name-default-show': property.default_show }">
{{ property.alias || property.name }}
</div>
<div v-if="property.is_password" class="attribute-card_value-type">{{ $t('cmdb.ciType.password') }}</div>
<div v-else-if="property.is_link" class="attribute-card_value-type">{{ $t('cmdb.ciType.link') }}</div>
<div v-else class="attribute-card_value-type">{{ valueTypeMap[property.value_type] }}</div>
<div class="attribute-card_value-type">{{ valueTypeMap[getPropertyType(property)] }}</div>
</div>
<div
class="attribute-card-trigger"
@@ -74,7 +72,10 @@
!isUnique &&
!['6'].includes(property.value_type) &&
!property.is_password &&
!property.is_list
!property.is_list &&
!property.is_reference &&
!property.is_bool &&
!(Array.isArray(property.choice_value) ? property.choice_value.length > 0 : false)
"
:title="$t(isShowId ? 'cmdb.ciType.cancelSetAsShow' : 'cmdb.ciType.setAsShow')"
>
@@ -101,6 +102,8 @@ import ValueTypeIcon from '@/components/CMDBValueTypeMapIcon'
import { valueTypeMap } from '../../utils/const'
import TriggerForm from './triggerForm.vue'
import { updateCIType } from '@/modules/cmdb/api/CIType'
import { getPropertyType } from '../../utils/helper'
export default {
name: 'AttributeCard',
inject: {
@@ -191,6 +194,7 @@ export default {
},
},
methods: {
getPropertyType,
handleEdit() {
this.$emit('edit')
},

View File

@@ -0,0 +1,78 @@
<template>
<a-form-item
:label="$t('cmdb.ciType.referenceModel')"
:extra="$t('cmdb.ciType.referenceModelTip1')"
:label-col="formItemLayout.labelCol"
:wrapper-col="formItemLayout.wrapperCol"
>
<a-select
allowClear
v-decorator="['reference_type_id', {
rules: [{ required: true, message: $t('cmdb.ciType.referenceModelTip') }],
initialValue: ''
}]"
showSearch
optionFilterProp="title"
@dropdownVisibleChange="handleDropdownVisibleChange"
>
<a-select-option
v-for="(item) in options"
:key="item.value"
:title="item.label"
>
{{ item.label }}
</a-select-option>
</a-select>
</a-form-item>
</template>
<script>
import { getCITypes } from '@/modules/cmdb/api/CIType'
export default {
name: 'ReferenceModelSelect',
props: {
form: {
type: Object,
required: true,
},
isLazyRequire: {
type: Boolean,
default: true
},
formItemLayout: {
type: Object,
default: () => {}
}
},
data() {
return {
isInit: false,
options: []
}
},
mounted() {
if (!this.isLazyRequire) {
this.getSelectOptions()
}
},
methods: {
handleDropdownVisibleChange(open) {
if (!this.isInit && open) {
this.getSelectOptions()
}
},
async getSelectOptions() {
this.isInit = true
const res = await getCITypes()
this.options = res.ci_types.map((ciType) => {
return {
value: ciType.id,
label: ciType?.alias || ciType?.name || ''
}
})
}
}
}
</script>

View File

@@ -60,14 +60,20 @@
v-decorator="['value_type', { rules: [{ required: true }] }]"
@change="handleChangeValueType"
>
<a-select-option :value="key" :key="key" v-for="(value, key) in valueTypeMap">{{ value }}</a-select-option>
<a-select-option :value="key" :key="key" v-for="(value, key) in valueTypeMap">
<ops-icon :type="getPropertyIcon({ value_type: key })" />
<span class="value-type-text">{{ value }}</span>
</a-select-option>
</a-select>
</a-form-item></a-col
>
<a-col :span="currentValueType === '6' ? 24 : 12">
<a-col
v-if="currentValueType !== '11'"
:span="currentValueType === '6' ? 24 : 12"
>
<a-form-item
:label-col="{ span: currentValueType === '6' ? 4 : 8 }"
:wrapper-col="{ span: currentValueType === '6' ? 18 : 12 }"
:wrapper-col="{ span: currentValueType === '6' ? 18 : 15 }"
:label="$t('cmdb.ciType.defaultValue')"
>
<template>
@@ -77,6 +83,10 @@
v-decorator="['default_value', { rules: [{ required: false }] }]"
>
</a-input>
<a-switch
v-else-if="currentValueType === '10'"
v-decorator="['default_value', { rules: [{ required: false }], valuePropName: 'checked' }]"
/>
<a-select
v-decorator="['default_value', { rules: [{ required: false }] }]"
mode="tags"
@@ -95,12 +105,7 @@
</a-input-number>
<a-input
style="width: 100%"
v-else-if="
currentValueType === '2' ||
currentValueType === '5' ||
currentValueType === '7' ||
currentValueType === '8'
"
v-else-if="['2', '5', '7', '8', '9'].includes(currentValueType)"
v-decorator="['default_value', { rules: [{ required: false }] }]"
>
</a-input>
@@ -157,7 +162,18 @@
</a-form-item>
</a-col>
<a-col :span="currentValueType === '2' ? 6 : 0" v-if="currentValueType !== '6'">
<a-col
v-if="currentValueType === '11'"
:span="12"
>
<ReferenceModelSelect
:form="form"
:isLazyRequire="false"
:formItemLayout="formItemLayout"
/>
</a-col>
<!-- <a-col :span="currentValueType === '2' ? 6 : 0" v-if="currentValueType !== '6'">
<a-form-item
:hidden="currentValueType === '2' ? false : true"
:label-col="horizontalFormItemLayout.labelCol"
@@ -189,10 +205,10 @@
v-decorator="['is_index', { rules: [], valuePropName: 'checked' }]"
/>
</a-form-item>
</a-col>
</a-col> -->
<a-col :span="6" v-if="currentValueType !== '6' && currentValueType !== '7'">
<a-form-item
:label-col="currentValueType === '2' ? { span: 8 } : horizontalFormItemLayout.labelCol"
:label-col="horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol"
:label="$t('cmdb.ciType.unique')"
>
@@ -206,7 +222,7 @@
</a-col>
<a-col :span="6">
<a-form-item
:label-col="['2', '6', '7'].findIndex(i => currentValueType === i) === -1 ? { span: 8 } : horizontalFormItemLayout.labelCol"
:label-col="horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol"
:label="$t('required')"
>
@@ -219,7 +235,7 @@
</a-col>
<a-col :span="6">
<a-form-item
:label-col="currentValueType === '2' ? { span: 12 } : horizontalFormItemLayout.labelCol"
:label-col="horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol"
>
<template slot="label">
@@ -228,9 +244,8 @@
>{{ $t('cmdb.ciType.defaultShow') }}
<a-tooltip :title="$t('cmdb.ciType.defaultShowTips')">
<a-icon
style="position:absolute;top:2px;left:-17px;color:#2f54eb;"
type="question-circle"
theme="filled"
style="position:absolute;top:2px;left:-17px;color:#A5A9BC;"
type="info-circle"
@click="
(e) => {
e.stopPropagation()
@@ -250,7 +265,7 @@
</a-col>
<a-col :span="6" v-if="currentValueType !== '6' && currentValueType !== '7'">
<a-form-item
:label-col="currentValueType === '2' ? horizontalFormItemLayout.labelCol : { span: 8 }"
:label-col="horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol"
:label="$t('cmdb.ciType.isSortable')"
>
@@ -263,9 +278,9 @@
</a-form-item>
</a-col>
<a-col :span="6" v-if="currentValueType !== '6' && currentValueType !== '7'">
<a-col :span="6" v-if="!['6', '7', '10'].includes(currentValueType)">
<a-form-item
:label-col="currentValueType === '2' ? { span: 8 } : horizontalFormItemLayout.labelCol"
:label-col="horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol"
>
<template slot="label">
@@ -274,9 +289,8 @@
>{{ $t('cmdb.ciType.list') }}
<a-tooltip :title="$t('cmdb.ciType.listTips')">
<a-icon
style="position:absolute;top:3px;left:-17px;color:#2f54eb;"
type="question-circle"
theme="filled"
style="position:absolute;top:3px;left:-17px;color:#A5A9BC;"
type="info-circle"
@click="
(e) => {
e.stopPropagation()
@@ -297,7 +311,7 @@
</a-col>
<a-col span="6">
<a-form-item
:label-col="['2', '6', '7'].findIndex(i => currentValueType === i) === -1 ? { span: 12 } : horizontalFormItemLayout.labelCol"
:label-col="horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol"
>
<template slot="label">
@@ -306,9 +320,8 @@
>{{ $t('cmdb.ciType.isDynamic') }}
<a-tooltip :title="$t('cmdb.ciType.dynamicTips')">
<a-icon
style="position:absolute;top:3px;left:-17px;color:#2f54eb;"
type="question-circle"
theme="filled"
style="position:absolute;top:3px;left:-17px;color:#A5A9BC;"
type="info-circle"
@click="
(e) => {
e.stopPropagation()
@@ -328,27 +341,36 @@
</a-col>
<a-divider style="font-size:14px;margin-top:6px;">{{ $t('cmdb.ciType.advancedSettings') }}</a-divider>
<a-row>
<a-col :span="24" v-if="!['6'].includes(currentValueType)">
<a-col :span="24">
<a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 12 }" :label="$t('cmdb.ciType.reg')">
<RegSelect :isShowErrorMsg="false" v-model="re_check" :limitedFormat="getLimitedFormat()" />
<RegSelect
:isShowErrorMsg="false"
:limitedFormat="getLimitedFormat()"
:disabled="['6', '10', '11'].includes(currentValueType)"
v-model="re_check"
/>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }" :label="$t('cmdb.ciType.font')">
<FontArea ref="fontArea" />
<FontArea ref="fontArea" :fontColorDisabled="['8', '11'].includes(currentValueType)" />
</a-form-item>
</a-col>
<a-col :span="24" v-if="!['6', '7'].includes(currentValueType)">
<a-col :span="24" v-if="!['6', '7', '10', '11'].includes(currentValueType)">
<a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }" :label="$t('cmdb.ciType.choiceValue')">
<PreValueArea
v-if="drawerVisible"
:canDefineScript="canDefineScript"
ref="preValueArea"
:disabled="isShowComputedArea"
:CITypeId="CITypeId"
:enumValueType="enumValueType"
/>
<a-button type="primary" size="small" ghost @click="resetPreValue" >{{ $t('reset') }}</a-button>
</a-form-item>
</a-col>
<a-col :span="24" v-if="!['6', '7'].includes(currentValueType)">
<a-col :span="24" v-if="!['6', '7', '10', '11'].includes(currentValueType)">
<a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
<template slot="label">
<span
@@ -356,9 +378,8 @@
>{{ $t('cmdb.ciType.computedAttribute') }}
<a-tooltip :title="$t('cmdb.ciType.computedAttributeTips')">
<a-icon
style="position:absolute;top:3px;left:-17px;color:#2f54eb;"
type="question-circle"
theme="filled"
style="position:absolute;top:3px;left:-17px;color:#A5A9BC;"
type="info-circle"
@click="
(e) => {
e.stopPropagation()
@@ -414,14 +435,17 @@ import {
calcComputedAttribute,
} from '@/modules/cmdb/api/CITypeAttr'
import { valueTypeMap } from '../../utils/const'
import { getPropertyType, getPropertyIcon } from '../../utils/helper'
import ComputedArea from './computedArea.vue'
import PreValueArea from './preValueArea.vue'
import FontArea from './fontArea.vue'
import RegSelect from '@/components/RegexSelect'
import ReferenceModelSelect from './attributeEdit/referenceModelSelect.vue'
import { ENUM_VALUE_TYPE } from './preValueAttr/constants.js'
export default {
name: 'AttributeEditForm',
components: { ComputedArea, PreValueArea, vueJsonEditor, FontArea, RegSelect },
components: { ComputedArea, PreValueArea, vueJsonEditor, FontArea, RegSelect, ReferenceModelSelect },
props: {
CITypeId: {
type: Number,
@@ -450,6 +474,7 @@ export default {
defaultForDatetime: '',
re_check: {},
enumValueType: ENUM_VALUE_TYPE.INPUT
}
},
@@ -466,7 +491,7 @@ export default {
return formLayout === 'horizontal'
? {
labelCol: { span: 8 },
wrapperCol: { span: 12 },
wrapperCol: { span: 15 },
}
: {}
},
@@ -483,6 +508,7 @@ export default {
},
mounted() {},
methods: {
getPropertyIcon,
async handleCreate() {
try {
await canDefineComputed()
@@ -515,9 +541,7 @@ export default {
}
}
if (property === 'is_list') {
this.form.setFieldsValue({
default_value: checked ? [] : '',
})
this.handleSwitchIsList(checked)
}
if (checked && property === 'is_sortable') {
this.$message.warning(this.$t('cmdb.ciType.addAttributeTips1'))
@@ -535,6 +559,26 @@ export default {
}
},
handleSwitchIsList(checked) {
let defaultValue = checked ? [] : ''
switch (this.currentValueType) {
case '2':
case '9':
defaultValue = ''
break
case '10':
defaultValue = checked ? '' : false
break
default:
break
}
this.form.setFieldsValue({
default_value: defaultValue,
})
},
async handleEdit(record, attributes) {
try {
await canDefineComputed()
@@ -543,12 +587,7 @@ export default {
this.canDefineComputed = false
}
const _record = _.cloneDeep(record)
if (_record.is_password) {
_record.value_type = '7'
}
if (_record.is_link) {
_record.value_type = '8'
}
_record.value_type = getPropertyType(_record)
this.drawerTitle = this.$t('cmdb.ciType.editAttribute')
this.drawerVisible = true
this.record = _record
@@ -572,8 +611,13 @@ export default {
is_dynamic: _record.is_dynamic,
})
}
if (_record.value_type === '11') {
this.form.setFieldsValue({
reference_type_id: _record.reference_type_id
})
}
console.log(_record)
if (!['6'].includes(_record.value_type) && _record.re_check) {
if (!['6', '10', '11'].includes(_record.value_type) && _record.re_check) {
this.re_check = {
value: _record.re_check,
}
@@ -582,7 +626,11 @@ export default {
}
if (_record.default) {
this.$nextTick(() => {
if (_record.value_type === '0') {
if (_record.value_type === '10') {
this.form.setFieldsValue({
default_value: Boolean(_record.default.default),
})
} else if (_record.value_type === '0') {
if (_record.is_list) {
this.$nextTick(() => {
this.form.setFieldsValue({
@@ -638,7 +686,23 @@ export default {
})
}
const _find = attributes.find((item) => item.id === _record.id)
if (!['6', '7'].includes(_record.value_type)) {
if (!['6', '7', '10', '11'].includes(_record.value_type)) {
switch (_record.value_type) {
case '0':
case '1':
this.enumValueType = ENUM_VALUE_TYPE.NUMBER
break
case '3':
this.enumValueType = ENUM_VALUE_TYPE.DATE_TIME
break
case '4':
this.enumValueType = ENUM_VALUE_TYPE.DATE
break
default:
this.enumValueType = ENUM_VALUE_TYPE.INPUT
break
}
this.$refs.preValueArea.setData({
choice_value: (_find || {}).choice_value || [],
choice_web_hook: _record.choice_web_hook,
@@ -668,10 +732,10 @@ export default {
})
}
delete values['default_show']
delete values['is_required']
const { default_value } = values
if (values.value_type === '0' && default_value) {
if (values.value_type === '10') {
values.default = { default: values.is_list ? default_value : Boolean(default_value) }
} else if (values.value_type === '0' && default_value) {
if (values.is_list) {
values.default = { default: default_value || null }
} else {
@@ -699,29 +763,56 @@ export default {
values.default = { default: null }
}
delete values.default_value
if (values.is_computed) {
const computedAreaData = this.$refs.computedArea.getData()
values = { ...values, ...computedAreaData }
} else {
// If it is a non-computed attribute, check to see if there is a predefined value
if (!['6', '7'].includes(values.value_type)) {
if (!['6', '7', '10', '11'].includes(values.value_type)) {
const preValueAreaData = this.$refs.preValueArea.getData()
// 预定义值校验错误
if (preValueAreaData?.isError) {
return
}
values = { ...values, ...preValueAreaData }
}
}
delete values['default_show']
delete values['is_required']
delete values.default_value
const fontOptions = this.$refs.fontArea.getData()
if (values.value_type === '7') {
values.value_type = '2'
values.is_password = true
}
if (values.value_type === '8') {
values.value_type = '2'
values.is_link = true
}
if (values.value_type !== '6') {
if (!['6', '10', '11'].includes(values.value_type)) {
values.re_check = this.re_check?.value ?? null
}
// 重置数据类型
switch (values.value_type) {
case '7':
values.value_type = '2'
values.is_password = true
break
case '8':
values.value_type = '2'
values.is_link = true
break
case '9':
values.value_type = '2'
break
case '10':
values.value_type = '7'
values.is_bool = true
break
case '11':
values.value_type = '0'
values.is_reference = true
break
default:
break
}
if (values.id) {
await this.updateAttribute(values.id, { ...values, option: { fontOptions } }, isCalcComputed)
} else {
@@ -763,10 +854,19 @@ export default {
},
changeDefaultForDatetime(value) {
this.defaultForDatetime = value
if (value === '$custom_time') {
this.form.setFieldsValue({
default_value: undefined,
})
switch (value) {
case '$custom_time':
this.form.setFieldsValue({
default_value: undefined,
})
break
case '$updated_at':
this.form.setFieldsValue({
is_dynamic: true,
})
break
default:
break
}
},
onClick({ key }) {
@@ -794,6 +894,12 @@ export default {
}
return []
},
resetPreValue() {
if (this.$refs.preValueArea) {
this.$refs.preValueArea.resetData()
}
}
},
watch: {},
}
@@ -805,6 +911,9 @@ export default {
line-height: 22px;
color: #a5a9bc;
}
.value-type-text {
margin-left: 4px;
}
</style>
<style lang="less">
.attribute-edit-form {

View File

@@ -16,21 +16,21 @@
<a-space style="margin-bottom: 10px">
<a-button @click="handleAddGroup" size="small" icon="plus">{{ $t('cmdb.ciType.group') }}</a-button>
<a-button @click="handleOpenUniqueConstraint" size="small">{{ $t('cmdb.ciType.uniqueConstraint') }}</a-button>
<div>
<div class="ci-types-attributes-flex">
<a-tooltip
v-for="type in Object.keys(valueTypeMap)"
:key="type"
:title="$t('cmdb.ciType.filterTips', { name: valueTypeMap[type] })"
v-for="item in valueTypeMap"
:key="item.key"
:title="$t('cmdb.ciType.filterTips', { name: item.value })"
>
<span
@click="handleFilterType(type)"
@click="handleFilterType(item.key)"
:class="{
'ci-types-attributes-filter': true,
'ci-types-attributes-filter-selected': attrTypeFilter.includes(type),
'ci-types-attributes-filter-selected': attrTypeFilter.includes(item.key),
}"
>
<ops-icon :type="getPropertyIcon({ value_type: type })" />
{{ valueTypeMap[type] }}
<ops-icon :type="getPropertyIcon({ value_type: item.key })" />
{{ item.value }}
</span>
</a-tooltip>
</div>
@@ -200,7 +200,7 @@ import AttributeEditForm from './attributeEditForm.vue'
import NewCiTypeAttrModal from './newCiTypeAttrModal.vue'
import UniqueConstraint from './uniqueConstraint.vue'
import { valueTypeMap } from '../../utils/const'
import { getPropertyIcon } from '../../utils/helper'
import { getPropertyIcon, getPropertyType } from '../../utils/helper'
export default {
name: 'AttributesTable',
@@ -233,6 +233,7 @@ export default {
attrTypeFilter: [],
unique: '',
show_id: null,
groupMaxCount: {},
}
},
computed: {
@@ -243,7 +244,12 @@ export default {
return this.$store.state.windowHeight
},
valueTypeMap() {
return valueTypeMap()
const map = valueTypeMap()
const keys = ['0', '1', '2', '9', '3', '4', '5', '6', '7', '8', '10', '11']
return keys.map((key) => ({
key,
value: map[key]
}))
},
},
provide() {
@@ -335,6 +341,7 @@ export default {
})
this.CITypeGroups = values[1]
this.CITypeGroups.forEach((g) => {
this.groupMaxCount[g.name] = g.attributes.filter(a => a.inherited).length
g.attributes.forEach((a) => {
a.is_required = (temp[a.id] && temp[a.id].is_required) || false
a.default_show = (temp[a.id] && temp[a.id].default_show) || false
@@ -470,44 +477,43 @@ export default {
handleChange(e, group) {
console.log('changess', group)
if (e.hasOwnProperty('moved') && e.moved.oldIndex !== e.moved.newIndex) {
if (group === -1) {
this.$message.error(this.$t('cmdb.ciType.attributeSortedTips'))
if (group === -1 || group === null) {
this.refreshPage(this.$t('cmdb.ciType.attributeSortedTips'))
} else if (e.moved.newIndex < this.groupMaxCount[group]) {
this.refreshPage(this.$t('cmdb.ciType.attributeSortedTips2'))
} else {
transferCITypeAttrIndex(this.CITypeId, {
from: { attr_id: e.moved.element.id, group_name: group },
to: { order: e.moved.newIndex, group_name: group },
to: { order: e.moved.newIndex, group_name: group }
})
.then((res) => this.$message.success(this.$t('updateSuccess')))
.catch(() => {
this.abortDraggable()
})
.then(() => this.$message.success(this.$t('updateSuccess')))
.catch(() => this.init())
}
}
if (e.hasOwnProperty('added')) {
this.addRemoveGroupFlag = { to: { group_name: group, order: e.added.newIndex }, inited: true }
}
if (e.hasOwnProperty('removed')) {
this.$nextTick(() => {
transferCITypeAttrIndex(this.CITypeId, {
from: { attr_id: e.removed.element.id, group_name: group },
to: { group_name: this.addRemoveGroupFlag.to.group_name, order: this.addRemoveGroupFlag.to.order },
})
.then((res) => this.$message.success(this.$t('saveSuccess')))
.catch(() => {
this.abortDraggable()
})
.finally(() => {
this.addRemoveGroupFlag = {}
if (this.addRemoveGroupFlag.to.order < this.groupMaxCount[this.addRemoveGroupFlag.to.group_name]) {
this.refreshPage(this.$t('cmdb.ciType.attributeSortedTips2'))
} else {
transferCITypeAttrIndex(this.CITypeId, {
from: { attr_id: e.removed.element.id, group_name: group },
to: { group_name: this.addRemoveGroupFlag.to.group_name, order: this.addRemoveGroupFlag.to.order }
})
.then(() => this.$message.success(this.$t('saveSuccess')))
.catch(() => this.init())
.finally(() => {
this.addRemoveGroupFlag = {}
})
}
})
}
},
abortDraggable() {
this.$nextTick(() => {
this.$router.push({ name: 'ci_type' })
})
refreshPage(errorMessage) {
this.$message.error(errorMessage)
this.init()
},
updatePropertyIndex() {
const attributes = [] // All attributes
@@ -585,26 +591,11 @@ export default {
if (!attrTypeFilter.length) {
return true
} else {
if (attrTypeFilter.includes('7') && attr.is_password) {
return true
}
if (attrTypeFilter.includes('8') && attr.is_link) {
return true
}
if (
attrTypeFilter.includes(attr.value_type) &&
attr.value_type === '2' &&
(attr.is_password || attr.is_link)
) {
return false
}
if (attrTypeFilter.includes(attr.value_type)) {
return true
}
return false
const valueType = getPropertyType(attr)
return attrTypeFilter.includes(valueType)
}
})
},
}
},
}
</script>
@@ -618,6 +609,12 @@ export default {
.ci-types-attributes {
padding: 0 20px;
overflow-y: auto;
&-flex {
display: flex;
flex-wrap: wrap;
}
.ci-types-attributes-filter {
color: @text-color_4;
cursor: pointer;

View File

@@ -46,16 +46,21 @@
v-decorator="['value_type', { rules: [{ required: true }], initialValue: '2' }]"
@change="handleChangeValueType"
>
<a-select-option :value="key" :key="key" v-for="(value, key) in valueTypeMap">
{{ value }}
<span class="value-type-des" v-if="key === '3'">yyyy-mm-dd HH:MM:SS</span>
<span class="value-type-des" v-if="key === '4'">yyyy-mm-dd</span>
<span class="value-type-des" v-if="key === '5'">HH:MM:SS</span>
<a-select-option :value="item.key" :key="item.key" v-for="(item) in valueTypeMap">
<ops-icon :type="getPropertyIcon({ value_type: item.key })" />
<span class="value-type-text">{{ item.value }}</span>
<span class="value-type-des" v-if="item.key === '2'">{{ $t('cmdb.ciType.shortTextTip') }}</span>
<span class="value-type-des" v-if="item.key === '3'">yyyy-mm-dd HH:MM:SS</span>
<span class="value-type-des" v-if="item.key === '4'">yyyy-mm-dd</span>
<span class="value-type-des" v-if="item.key === '5'">HH:MM:SS</span>
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="currentValueType === '6' ? 24 : 12">
<a-col
v-if="currentValueType !== '11'"
:span="currentValueType === '6' ? 24 : 12"
>
<a-form-item
:label-col="{ span: currentValueType === '6' ? 4 : 8 }"
:wrapper-col="{ span: currentValueType === '6' ? 18 : 15 }"
@@ -68,6 +73,10 @@
v-decorator="['default_value', { rules: [{ required: false }] }]"
>
</a-input>
<a-switch
v-else-if="currentValueType === '10'"
v-decorator="['default_value', { rules: [{ required: false }], valuePropName: 'checked' }]"
/>
<a-input-number
style="width: 100%"
v-else-if="currentValueType === '1'"
@@ -86,12 +95,7 @@
</a-select>
<a-input
style="width: 100%"
v-else-if="
currentValueType === '2' ||
currentValueType === '5' ||
currentValueType === '7' ||
currentValueType === '8'
"
v-else-if="['2', '5', '7', '8', '9'].includes(currentValueType)"
v-decorator="['default_value', { rules: [{ required: false }] }]"
>
</a-input>
@@ -148,9 +152,19 @@
</template>
</a-form-item>
</a-col>
<a-col
v-if="currentValueType === '11'"
:span="12"
>
<ReferenceModelSelect
:form="form"
:formItemLayout="formItemLayout"
/>
</a-col>
</a-row>
<a-col :span="currentValueType === '2' ? 6 : 0" v-if="currentValueType !== '6'">
<!-- <a-col :span="currentValueType === '2' ? 6 : 0" v-if="currentValueType !== '6'">
<a-form-item
:hidden="currentValueType === '2' ? false : true"
:label-col="horizontalFormItemLayout.labelCol"
@@ -182,10 +196,10 @@
v-decorator="['is_index', { rules: [], valuePropName: 'checked', initialValue: true }]"
/>
</a-form-item>
</a-col>
</a-col> -->
<a-col :span="6" v-if="currentValueType !== '6' && currentValueType !== '7'">
<a-form-item
:label-col="currentValueType === '2' ? { span: 8 } : horizontalFormItemLayout.labelCol"
:label-col="horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol"
:label="$t('cmdb.ciType.unique')"
>
@@ -199,7 +213,7 @@
</a-col>
<a-col :span="6">
<a-form-item
:label-col="['2', '6', '7'].findIndex(i => currentValueType === i) === -1 ? { span: 8 } : horizontalFormItemLayout.labelCol"
:label-col="horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol"
:label="$t('required')"
>
@@ -212,7 +226,7 @@
</a-col>
<a-col :span="6">
<a-form-item
:label-col="currentValueType === '2' ? { span: 12 } : horizontalFormItemLayout.labelCol"
:label-col="horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol"
>
<template slot="label">
@@ -221,9 +235,8 @@
>{{ $t('cmdb.ciType.defaultShow') }}
<a-tooltip :title="$t('cmdb.ciType.defaultShowTips')">
<a-icon
style="position:absolute;top:2px;left:-17px;color:#2f54eb;"
type="question-circle"
theme="filled"
style="position:absolute;top:2px;left:-17px;color:#A5A9BC;"
type="info-circle"
@click="
(e) => {
e.stopPropagation()
@@ -243,7 +256,7 @@
</a-col>
<a-col :span="6" v-if="currentValueType !== '6' && currentValueType !== '7'">
<a-form-item
:label-col="currentValueType === '2' ? horizontalFormItemLayout.labelCol : { span: 8 }"
:label-col="horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol"
:label="$t('cmdb.ciType.isSortable')"
>
@@ -256,20 +269,19 @@
</a-form-item>
</a-col>
<a-col :span="6" v-if="currentValueType !== '6' && currentValueType !== '7'">
<a-col :span="6" v-if="!['6', '7', '10'].includes(currentValueType)">
<a-form-item
:label-col="currentValueType === '2' ? { span: 8 } : horizontalFormItemLayout.labelCol"
:label-col="horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol"
>
<template slot="label">
<span
style="position:relative;white-space:pre;"
>{{ $t('cmdb.ciType.list') }}
>
<a-tooltip :title="$t('cmdb.ciType.listTips')">
<a-icon
style="position:absolute;top:3px;left:-17px;color:#2f54eb;"
type="question-circle"
theme="filled"
style="position:absolute;top:3px;left:-17px;color:#A5A9BC;"
type="info-circle"
@click="
(e) => {
e.stopPropagation()
@@ -278,6 +290,7 @@
"
/>
</a-tooltip>
{{ $t('cmdb.ciType.list') }}
</span>
</template>
<a-switch
@@ -290,18 +303,17 @@
</a-col>
<a-col span="6">
<a-form-item
:label-col="['2', '6', '7'].findIndex(i => currentValueType === i) === -1 ? { span: 12 } : horizontalFormItemLayout.labelCol"
:label-col="horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol"
>
<template slot="label">
<span
style="position:relative;white-space:pre;"
>{{ $t('cmdb.ciType.isDynamic') }}
>
<a-tooltip :title="$t('cmdb.ciType.dynamicTips')">
<a-icon
style="position:absolute;top:3px;left:-17px;color:#2f54eb;"
type="question-circle"
theme="filled"
style="position:absolute;top:3px;left:-17px;color:#A5A9BC;"
type="info-circle"
@click="
(e) => {
e.stopPropagation()
@@ -310,6 +322,7 @@
"
/>
</a-tooltip>
{{ $t('cmdb.ciType.isDynamic') }}
</span>
</template>
<a-switch
@@ -321,31 +334,44 @@
</a-col>
<a-divider style="font-size:14px;margin-top:6px;">{{ $t('cmdb.ciType.advancedSettings') }}</a-divider>
<a-row>
<a-col :span="24" v-if="!['6'].includes(currentValueType)">
<a-col :span="24">
<a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 12 }" :label="$t('cmdb.ciType.reg')">
<RegSelect :isShowErrorMsg="false" v-model="re_check" :limitedFormat="getLimitedFormat()" />
<RegSelect
v-model="re_check"
:isShowErrorMsg="false"
:limitedFormat="getLimitedFormat()"
:disabled="['6', '10', '11'].includes(currentValueType)"
/>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }" :label="$t('cmdb.ciType.font')">
<FontArea ref="fontArea" />
<FontArea ref="fontArea" :fontColorDisabled="['8', '11'].includes(currentValueType)" />
</a-form-item>
</a-col>
<a-col :span="24" v-if="!['6', '7'].includes(currentValueType)">
<a-col :span="24" v-if="!['6', '7', '10', '11'].includes(currentValueType)">
<a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }" :label="$t('cmdb.ciType.choiceValue')">
<PreValueArea ref="preValueArea" :canDefineScript="canDefineScript" :disabled="isShowComputedArea" />
<PreValueArea
ref="preValueArea"
:canDefineScript="canDefineScript"
:disabled="isShowComputedArea"
:CITypeId="CITypeId"
:enumValueType="enumValueType"
/>
<a-button type="primary" size="small" ghost @click="resetPreValue" >{{ $t('reset') }}</a-button>
</a-form-item>
</a-col>
<a-col :span="24" v-if="!['6', '7'].includes(currentValueType)">
<a-col :span="24" v-if="!['6', '7', '10', '11'].includes(currentValueType)">
<a-form-item :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
<template slot="label">
<span
style="position:relative;white-space:pre;"
>{{ $t('cmdb.ciType.computedAttribute') }}
>
<a-tooltip :title="$t('cmdb.ciType.computedAttributeTips')">
<a-icon
style="position:absolute;top:3px;left:-17px;color:#2f54eb;"
type="question-circle"
style="position:absolute;top:3px;left:-17px;color:#A5A9BC;"
type="info-circle"
theme="filled"
@click="
(e) => {
@@ -355,6 +381,7 @@
"
/>
</a-tooltip>
{{ $t('cmdb.ciType.computedAttribute') }}
</span>
</template>
<a-switch
@@ -387,6 +414,9 @@ import ComputedArea from './computedArea.vue'
import PreValueArea from './preValueArea.vue'
import FontArea from './fontArea.vue'
import RegSelect from '@/components/RegexSelect'
import { getPropertyIcon } from '../../utils/helper'
import ReferenceModelSelect from './attributeEdit/referenceModelSelect.vue'
import { ENUM_VALUE_TYPE } from './preValueAttr/constants.js'
export default {
name: 'CreateNewAttribute',
@@ -396,12 +426,17 @@ export default {
vueJsonEditor,
FontArea,
RegSelect,
ReferenceModelSelect,
},
props: {
hasFooter: {
type: Boolean,
default: true,
},
CITypeId: {
type: Number,
default: null
}
},
data() {
return {
@@ -424,17 +459,24 @@ export default {
defaultForDatetime: '',
re_check: {},
enumValueType: ENUM_VALUE_TYPE.INPUT
}
},
computed: {
valueTypeMap() {
return valueTypeMap()
const map = valueTypeMap()
const keys = ['0', '1', '2', '9', '3', '4', '5', '6', '7', '8', '10', '11']
return keys.map((key) => ({
key,
value: map[key]
}))
},
canDefineScript() {
return this.canDefineComputed
},
},
methods: {
getPropertyIcon,
handleSubmit(isCloseModal = true) {
this.form.validateFields(async (err, values) => {
if (!err) {
@@ -445,9 +487,10 @@ export default {
console.log(values)
const { is_required, default_show, default_value, is_dynamic } = values
const data = { is_required, default_show, is_dynamic }
delete values.is_required
delete values.default_show
if (values.value_type === '0' && default_value) {
if (values.value_type === '10') {
values.default = { default: values.is_list ? (default_value || null) : Boolean(default_value) }
} else if (values.value_type === '0' && default_value) {
if (values.is_list) {
values.default = { default: default_value || null }
} else {
@@ -474,45 +517,63 @@ export default {
} else {
values.default = { default: null }
}
delete values.default_value
if (values.is_computed) {
const computedAreaData = this.$refs.computedArea.getData()
values = { ...values, ...computedAreaData }
} else {
// If it is a non-computed attribute, check to see if there is a predefined value
if (!['6', '7'].includes(values.value_type)) {
if (!['6', '7', '10', '11'].includes(values.value_type)) {
const preValueAreaData = this.$refs.preValueArea.getData()
// 预定义值校验错误
if (preValueAreaData?.isError) {
return
}
values = { ...values, ...preValueAreaData }
}
}
delete values.is_required
delete values.default_show
delete values.default_value
const fontOptions = this.$refs.fontArea.getData()
// is_index: except for text, the index is hidden, text index default is true
// 5 types in the box, is_index=true
// json, password, link is_index=false
if (['6', '7', '8'].includes(values.value_type)) {
values.is_index = false
} else if (values.value_type !== '2') {
values.is_index = true
}
if (values.value_type === '7') {
values.value_type = '2'
values.is_password = true
}
if (values.value_type === '8') {
values.value_type = '2'
values.is_link = true
}
if (values.value_type !== '6') {
values.re_check = this.re_check?.value ?? null
// 索引
values.is_index = !['6', '7', '8', '9', '11'].includes(values.value_type)
// 重置数据类型
switch (values.value_type) {
case '7':
values.value_type = '2'
values.is_password = true
break
case '8':
values.value_type = '2'
values.is_link = true
break
case '9':
values.value_type = '2'
break
case '10':
values.value_type = '7'
values.is_bool = true
break
case '11':
values.value_type = '0'
values.is_reference = true
break
default:
break
}
const { attr_id } = await createAttribute({ ...values, option: { fontOptions } })
this.form.resetFields()
this.currentValueType = '2'
if (!['6'].includes(values.value_type) && !values.is_password) {
if (this?.$refs?.preValueArea) {
this.$refs.preValueArea.valueList = []
}
this.currentValueType = '2'
this.$emit('done', attr_id, data, isCloseModal)
} else {
throw new Error()
@@ -531,11 +592,34 @@ export default {
}
},
handleChangeValueType(value) {
this.currentValueType = value
this.$nextTick(() => {
this.form.setFieldsValue({
default_value: this.form.getFieldValue('is_list') || value === '0' ? [] : null,
})
this.currentValueType = value
if (['6', '10', '11'].includes(value)) {
this.re_check = {}
}
switch (value) {
case '0':
case '1':
this.enumValueType = ENUM_VALUE_TYPE.NUMBER
break
case '3':
this.enumValueType = ENUM_VALUE_TYPE.DATE_TIME
break
case '4':
this.enumValueType = ENUM_VALUE_TYPE.DATE
break
default:
this.enumValueType = ENUM_VALUE_TYPE.INPUT
break
}
if (['0', '1', '3', '4'].includes(value)) {
if (this.$refs.preValueArea) {
this.$refs.preValueArea.initEnumValue()
}
}
this.handleSwitchType({ valueType: value })
})
},
onChange(checked, property) {
@@ -551,9 +635,7 @@ export default {
}
}
if (property === 'is_list') {
this.form.setFieldsValue({
default_value: checked ? [] : '',
})
this.handleSwitchType({ checked })
}
if (checked && property === 'is_sortable') {
this.$message.warning(this.$t('cmdb.ciType.addAttributeTips1'))
@@ -570,6 +652,33 @@ export default {
})
}
},
handleSwitchType({
checked,
valueType
}) {
checked = checked ?? this.form.getFieldValue('is_list')
valueType = valueType ?? this.currentValueType
let defaultValue = checked || valueType === '0' ? [] : ''
switch (valueType) {
case '2':
case '9':
defaultValue = ''
break
case '10':
defaultValue = checked ? '' : false
break
default:
break
}
this.form.setFieldsValue({
default_value: defaultValue,
})
},
onJsonChange(value) {
this.default_value_json_right = true
},
@@ -583,10 +692,19 @@ export default {
},
changeDefaultForDatetime(value) {
this.defaultForDatetime = value
if (value === '$custom_time') {
this.form.setFieldsValue({
default_value: undefined,
})
switch (value) {
case '$custom_time':
this.form.setFieldsValue({
default_value: undefined,
})
break
case '$updated_at':
this.form.setFieldsValue({
is_dynamic: true,
})
break
default:
break
}
},
onClick({ key }) {
@@ -611,6 +729,12 @@ export default {
}
return []
},
resetPreValue() {
if (this.$refs.preValueArea) {
this.$refs.preValueArea.resetData()
}
}
},
}
</script>
@@ -620,6 +744,9 @@ export default {
line-height: 22px;
color: #a5a9bc;
}
.value-type-text {
margin: 0 4px;
}
</style>
<style lang="less">
.create-new-attribute {

View File

@@ -32,6 +32,8 @@ import TriggerTable from './triggerTable.vue'
import ADTab from './adTab.vue'
import GrantComp from '../../components/cmdbGrant/grantComp.vue'
const ACTIVE_KEY_STORAGE_KEY = 'ops_model_config_tab_key'
export default {
name: 'CITypeDetail',
components: {
@@ -53,11 +55,21 @@ export default {
},
data() {
return {
activeKey: '1',
activeKey: localStorage.getItem(ACTIVE_KEY_STORAGE_KEY) || '1',
}
},
beforeCreate() {},
mounted() {},
mounted() {
this.$nextTick(() => {
switch (this.activeKey) {
case '5':
this.$refs.triggerTable.getTableData()
break
default:
break
}
})
},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
@@ -66,15 +78,20 @@ export default {
methods: {
changeTab(activeKey) {
this.activeKey = activeKey
localStorage.setItem(ACTIVE_KEY_STORAGE_KEY, activeKey)
this.$nextTick(() => {
if (activeKey === '1') {
this.$refs.attributesTable.getCITypeGroupData()
}
if (activeKey === '5') {
this.$refs.triggerTable.getTableData()
switch (activeKey) {
case '1':
this.$refs.attributesTable.getCITypeGroupData()
break
case '5':
this.$refs.triggerTable.getTableData()
break
default:
break
}
})
},
}
},
}
</script>

View File

@@ -1,17 +1,16 @@
<template>
<a-tabs v-model="activeKey" size="small" :tabBarStyle="{ borderBottom: 'none' }">
<a-tabs v-model="activeKey" size="small" :tabBarStyle="{ borderBottom: 'none' }" @change="handleTabsChange">
<a-tab-pane key="expr" :disabled="!canDefineComputed">
<span style="font-size:12px;" slot="tab">{{ $t('cmdb.ciType.expr') }}</span>
<a-textarea v-model="compute_expr" :placeholder="`{{a}}+{{b}}`" :rows="2" :disabled="!canDefineComputed" />
</a-tab-pane>
<a-tab-pane key="script" :disabled="!canDefineComputed">
<span style="font-size:12px;" slot="tab">{{ $t('cmdb.ciType.code') }}</span>
<codemirror
style="z-index: 9999"
:options="cmOptions"
v-model="compute_script"
@input="onCodeChange"
></codemirror>
<CustomCodeMirror
codeMirrorId="cmdb-computed-attr"
ref="codemirror"
@changeCodeContent="onCodeChange"
></CustomCodeMirror>
</a-tab-pane>
<template slot="tabBarExtraContent">
<a-button size="small" @click="showAllPropDrawer">
@@ -33,7 +32,8 @@
<script>
import AllAttrDrawer from './allAttrDrawer.vue'
import { codemirror } from 'vue-codemirror'
import CustomCodeMirror from '@/components/CustomCodeMirror'
import 'codemirror/lib/codemirror.css'
import 'codemirror/theme/monokai.css'
@@ -41,7 +41,7 @@ require('codemirror/mode/python/python.js')
export default {
name: 'ComputedArea',
components: {
codemirror,
CustomCodeMirror,
AllAttrDrawer
},
props: {
@@ -59,33 +59,6 @@ export default {
activeKey: 'expr', // expr script
compute_expr: '',
compute_script: 'def computed(): \n return',
cmOptions: {
lineNumbers: true,
mode: 'python',
height: '200px',
theme: 'monokai',
tabSize: 4,
indentUnit: 4,
lineWrapping: false,
readOnly: !this.canDefineComputed,
extraKeys: {
Tab: (cm) => {
if (cm.somethingSelected()) {
cm.indentSelection('add')
} else {
cm.replaceSelection(Array(cm.getOption('indentUnit') + 1).join(' '), 'end', '+input')
}
},
'Shift-Tab': (cm) => {
if (cm.somethingSelected()) {
cm.indentSelection('subtract')
} else {
const cursor = cm.getCursor()
cm.setCursor({ line: cursor.line, ch: cursor.ch - 4 })
}
},
},
},
}
},
methods: {
@@ -103,6 +76,9 @@ export default {
this.compute_script = compute_script || 'def computed(): \n return'
if (compute_script) {
this.activeKey = 'script'
this.$nextTick(() => {
this.$refs.codemirror.initCodeMirror(this.compute_script)
})
} else {
this.activeKey = 'expr'
}
@@ -122,6 +98,15 @@ export default {
},
showAllPropDrawer() {
this.$refs.allAttrDrawer.open()
},
handleTabsChange(activeKey) {
console.log('handleTabsChange', activeKey)
if (activeKey === 'script') {
this.$nextTick(() => {
this.$refs.codemirror.initCodeMirror(this.compute_script)
})
}
}
},
}

View File

@@ -21,7 +21,13 @@
<a-icon type="underline" />
</div>
<div :style="{ width: '100px', marginLeft: '10px', display: 'inline-flex', alignItems: 'center' }">
<a-icon type="font-colors" /><el-color-picker size="mini" v-model="fontOptions.color"> </el-color-picker>
<a-icon type="font-colors" />
<el-color-picker
size="mini"
:disabled="fontColorDisabled"
v-model="fontOptions.color"
>
</el-color-picker>
</div>
</div>
</template>
@@ -30,6 +36,12 @@
import _ from 'lodash'
export default {
name: 'FontArea',
props: {
fontColorDisabled: {
type: Boolean,
default: false
}
},
data() {
return {
fontOptions: {
@@ -57,7 +69,11 @@ export default {
if (flag) {
return undefined
} else {
return this.fontOptions
const fontOptions = _.cloneDeep(this.fontOptions)
if (this.fontColorDisabled) {
Reflect.deleteProperty(fontOptions, 'color')
}
return fontOptions
}
},
setData({ fontOptions = {} }) {

View File

@@ -545,7 +545,7 @@ export default {
},
showIdSelectOptions() {
const _showIdSelectOptions = this.currentTypeAttrs.filter(
(item) => item.id !== this.unique_id && !['6'].includes(item.value_type) && !item.is_password && !item.is_list
(item) => item.id !== this.unique_id && !['6'].includes(item.value_type) && !item.is_password && !item.is_list && !item.is_bool && !item.is_reference && !item?.choice_value?.length
)
if (this.showIdFilterInput) {
return _showIdSelectOptions.filter(
@@ -898,6 +898,7 @@ export default {
this.loadCITypes()
this.loading = false
this.drawerVisible = false
this.isInherit = false
}, 1000)
},
async updateCIType(CITypeId, data) {
@@ -916,6 +917,7 @@ export default {
this.loadCITypes()
this.loading = false
this.drawerVisible = false
this.isInherit = false
}, 1000)
})
})

View File

@@ -28,7 +28,7 @@
<a-tabs v-model="activeKey">
<a-tab-pane key="1" :tab="$t('cmdb.ciType.addAttribute')">
<div :style="{ overflow: 'auto', maxHeight: '480px' }">
<create-new-attribute ref="createNewAttribute" :hasFooter="false" @done="handleAddNewAttr" />
<create-new-attribute ref="createNewAttribute" :hasFooter="false" :CITypeId="CITypeId" @done="handleAddNewAttr" />
</div>
</a-tab-pane>
<a-tab-pane key="2" :tab="$t('cmdb.ciType.existedAttributes')" force-render>

View File

@@ -7,33 +7,22 @@
:tabBarStyle="{ borderBottom: 'none' }"
>
<a-tab-pane key="define" :disabled="disabled">
<span style="font-size:12px;" slot="tab">{{ $t('define') }}</span>
<PreValueTag type="add" :item="[]" @add="addNewValue" :disabled="disabled">
<template #default>
<a-button
:style="{ marginBottom: '10px', fontSize: '12px', padding: '1px 7px' }"
type="primary"
ghost
:disabled="disabled"
size="small"
>
<a-icon type="plus" />{{ $t('add') }}</a-button
>
</template>
</PreValueTag>
<draggable :list="valueList" handle=".handle" :disabled="disabled">
<PreValueTag
:disabled="disabled"
v-for="(item, index) in valueList"
:key="`${item[0]}_${index}`"
:item="item"
@deleteValue="deleteValue"
@editValue="editValue"
/>
</draggable>
<span style="font-size:14px;" slot="tab">{{ $t('cmdb.ciType.enum') }}</span>
<PreValueDefine
v-model="valueList"
:disabled="disabled"
:enumValueType="enumValueType"
/>
</a-tab-pane>
<a-tab-pane key="builtin" :disabled="disabled">
<div class="tab-builtin" slot="tab">
<span class="tab-builtin-title">{{ $t('cmdb.ciType.builtin') }}</span>
<span v-if="isOpenSource" class="tab-builtin-tag">Pro</span>
</div>
<PreValueBuiltIn ref="builtInRef" />
</a-tab-pane>
<a-tab-pane key="webhook" :disabled="disabled">
<span style="font-size:12px;" slot="tab">Webhook</span>
<span style="font-size:14px;" slot="tab">Webhook</span>
<Webhook ref="webhook" style="margin-top:10px" />
<a-form-model :model="form">
<a-col :span="24">
@@ -59,14 +48,14 @@
</a-form-model>
</a-tab-pane>
<a-tab-pane key="choice_other" :disabled="disabled">
<span style="font-size:12px;" slot="tab">{{ $t('cmdb.ciType.choiceOther') }}</span>
<span style="font-size:14px;" slot="tab">{{ $t('cmdb.ciType.choiceOther') }}</span>
<a-row :gutter="[24, 24]">
<a-col :span="12">
<a-col :span="24">
<a-form-item
:style="{ lineHeight: '24px', marginBottom: '5px' }"
:label="$t('cmdb.ciType.ciType')"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 20 }"
:label-col="{ span: 3 }"
:wrapper-col="{ span: 12 }"
>
<treeselect
:disable-branch-nodes="true"
@@ -117,12 +106,12 @@
</treeselect>
</a-form-item>
</a-col>
<a-col :span="12" v-if="choice_other.type_ids && choice_other.type_ids.length">
<a-col :span="24" v-if="choice_other.type_ids && choice_other.type_ids.length">
<a-form-item
:style="{ marginBottom: '5px' }"
:label="$t('cmdb.ciType.attributes')"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 20 }"
:label-col="{ span: 3 }"
:wrapper-col="{ span: 12 }"
>
<treeselect
:disable-branch-nodes="true"
@@ -162,22 +151,62 @@
:style="{ marginBottom: '5px' }"
class="pre-value-filter"
:label="$t('cmdb.ciType.filter')"
:label-col="{ span: 2 }"
:wrapper-col="{ span: 22 }"
:label-col="{ span: 3 }"
:wrapper-col="{ span: 19 }"
>
<FilterComp
ref="filterComp"
<AttrFilter
ref="attrFilter"
:isDropdown="false"
:canSearchPreferenceAttrList="typeAttrs"
@setExpFromFilter="setExpFromFilter"
:CITypeId="CITypeId"
:expression="filterExp ? `q=${filterExp}` : ''"
:curModelAttrList="curModelAttrList"
@setExpFromFilter="setExpFromFilter"
/>
</a-form-item>
</a-col>
</a-row>
</a-tab-pane>
<a-tab-pane key="script" :disabled="disabled || !canDefineScript">
<span style="font-size:12px;" slot="tab">{{ $t('cmdb.ciType.code') }}</span>
<span style="font-size:14px;" slot="tab">{{ $t('cmdb.ciType.code') }}</span>
<a-form-item
:style="{ marginBottom: '5px' }"
:label="$t('cmdb.ciType.cascadeAttr')"
:label-col="{ span: $i18n.locale === 'en' ? 3 : 2 }"
:wrapper-col="{ span: 19 }"
:extra="scriptCodeExtraText"
labelAlign="left"
>
<a-select
mode="multiple"
style="width: 100%"
:placeholder="$t('placeholder2')"
optionFilterProp="title"
v-model="cascade_attributes"
>
<a-select-option
v-for="attr in curModelAttrList"
:key="attr.id"
:title="attr.name"
>
{{ attr.name }}
</a-select-option>
</a-select>
</a-form-item>
<div class="script-tip">
<div>1. {{ $t('cmdb.ciType.computedAttrTip1') }}</div>
<div>2. {{ $t('cmdb.ciType.computedAttrTip2') }}</div>
</div>
<div class="all-attr-btn">
<a-button size="small" @click="showAllPropDrawer">
{{ $t('cmdb.ciType.viewAllAttr') }}
</a-button>
</div>
<AllAttrDrawer ref="allAttrDrawer" />
<CustomCodeMirror
codeMirrorId="cmdb-pre-value"
ref="codemirror"
@@ -196,16 +225,21 @@ import { defautValueColor } from '../../utils/const'
import ColorPicker from '../../components/colorPicker/index.vue'
import Webhook from '../../components/webhook'
import { getCITypeGroups } from '../../api/ciTypeGroup'
import { getCITypeCommonAttributesByTypeIds } from '../../api/CITypeAttr'
import FilterComp from '@/components/CMDBFilterComp'
import { getCITypeCommonAttributesByTypeIds, getCITypeAttributesById } from '../../api/CITypeAttr'
import AttrFilter from './preValueAttr/attrFilter/index.vue'
import AllAttrDrawer from './allAttrDrawer.vue'
import PreValueDefine from './preValueAttr/define/index.vue'
import PreValueBuiltIn from './preValueAttr/builtin/index.vue'
import { ENUM_VALUE_TYPE } from './preValueAttr/constants.js'
import CustomCodeMirror from '@/components/CustomCodeMirror'
import 'codemirror/lib/codemirror.css'
import 'codemirror/theme/monokai.css'
require('codemirror/mode/python/python.js')
export default {
name: 'PreValueArea',
components: { draggable, PreValueTag, ColorPicker, Webhook, FilterComp, CustomCodeMirror },
components: { draggable, PreValueTag, ColorPicker, Webhook, AttrFilter, CustomCodeMirror, AllAttrDrawer, PreValueDefine, PreValueBuiltIn },
props: {
disabled: {
type: Boolean,
@@ -215,12 +249,30 @@ export default {
type: Boolean,
default: false,
},
CITypeId: {
type: Number,
default: null,
},
// eslint-disable-next-line vue/require-default-prop
enumValueType: {
type: String,
defualt: ENUM_VALUE_TYPE.INPUT
}
},
data() {
return {
defautValueColor,
activeKey: 'define', // define webhook
valueList: [],
valueList: [
[
'',
{
style: {},
icon: {},
label: ''
}
]
],
form: {
ret_key: '',
},
@@ -242,6 +294,13 @@ export default {
lineWrapping: true,
readOnly: this.disabled || !this.canDefineScript,
},
curModelAttrList: [], // 当前模型属性
cascade_attributes: [] // 级联属性id列表
}
},
computed: {
scriptCodeExtraText() {
return this.$t('cmdb.ciType.cascadeAttrTip') + (this.isOpenSource ? ` (${this.$t('cmdb.enterpriseVersionTip')})` : '')
}
},
watch: {
@@ -276,41 +335,41 @@ export default {
return { ..._.cloneDeep(item) }
})
})
this.getCITypeAttributesById()
},
methods: {
addNewValue(newValue, newStyle, newIcon) {
if (newValue) {
const idx = this.valueList.findIndex((v) => v[0] === newValue)
if (idx > -1) {
this.$message.warning(this.$t('cmdb.ciType.valueExisted'))
} else {
this.valueList.push([newValue, { style: newStyle, icon: { ...newIcon } }])
}
async getCITypeAttributesById() {
if (!this.CITypeId) {
this.curModelAttrList = []
return
}
},
deleteValue(item) {
const _valueList = _.cloneDeep(this.valueList)
const idx = _valueList.findIndex((v) => v[0] === item[0])
if (idx > -1) {
_valueList.splice(idx, 1)
this.valueList = _valueList
}
},
editValue(item, newValue, newStyle, newIcon) {
const _valueList = _.cloneDeep(this.valueList)
const idx = _valueList.findIndex((v) => v[0] === item[0])
if (idx > -1) {
_valueList[idx] = [newValue, { style: newStyle, icon: { ...newIcon } }]
this.valueList = _valueList
const res = await getCITypeAttributesById(this.CITypeId)
let curModelAttrList = []
if (res?.attributes?.length) {
curModelAttrList = res.attributes.filter(attr => !attr.is_password)
}
this.curModelAttrList = curModelAttrList
},
getData() {
if (this.activeKey === 'define') {
if (this.activeKey === 'builtin') {
return {
choice_value: this.valueList,
choice_value: [],
choice_web_hook: null,
choice_other: null,
}
} else if (this.activeKey === 'define') {
if (this.validateDefine()) {
return {
isError: true
}
}
return {
choice_value: this.valueList.filter((item) => !['', null, undefined].includes(item?.[0])),
choice_web_hook: null,
choice_other: null
}
} else if (this.activeKey === 'webhook') {
const choice_web_hook = this.$refs.webhook.getParams()
choice_web_hook.ret_key = this.form.ret_key
@@ -321,21 +380,36 @@ export default {
choice_web_hook: null,
choice_other: {
script: this.script,
cascade_attributes: this.cascade_attributes,
},
}
} else {
let choice_other = {}
if (this.choice_other.type_ids && this.choice_other.type_ids.length) {
this.$refs.filterComp.handleSubmit()
this.$refs.attrFilter.handleSubmit()
choice_other = { ...this.choice_other, filter: this.filterExp }
}
return {
choice_value: [],
choice_web_hook: null,
choice_other,
choice_other
}
}
},
validateDefine() {
const valueList = this.valueList.filter((item) => {
return !['', null, undefined].includes(item?.[0])
})
const isRepeat = _.uniq(valueList.map(item => item?.[0])).length !== valueList.length
if (isRepeat) {
this.$message.warning(this.$t('cmdb.ciType.enumValueTip2'))
return true
}
return false
},
setData({ choice_value, choice_web_hook, choice_other }) {
if (choice_web_hook) {
this.activeKey = 'webhook'
@@ -355,14 +429,34 @@ export default {
const { type_ids, attr_id, filter } = choice_other
this.choice_other = { type_ids, attr_id }
this.filterExp = filter
this.cascade_attributes = choice_other?.cascade_attributes || []
if (type_ids && type_ids.length) {
this.$nextTick(() => {
this.$refs.filterComp.visibleChange(true, false)
this.$refs.attrFilter.init(true, false)
})
}
}
} else {
this.valueList = choice_value
let valueList = [[
'',
{
style: {},
icon: {},
label: ''
}
]]
if (choice_value?.length) {
valueList = choice_value.map((item) => {
return [
item[0],
{
...item[1],
label: item?.[1]?.['label'] || item[0]
}
]
})
}
this.valueList = valueList
this.activeKey = 'define'
}
const dom = document.querySelector('#preValueArea .ant-tabs-ink-bar')
@@ -373,6 +467,56 @@ export default {
dom.style.backgroundColor = '#2f54eb'
}
},
resetData() {
this.activeKey = 'define'
this.$set(this, 'valueList', [
[
'',
{
style: {},
icon: {},
label: ''
}
]
])
this.$nextTick(() => {
if (this.$refs.builtInRef) {
this.$refs.builtInRef.setData({})
}
if (this.$refs.webhook) {
this.$refs.webhook.setParams({})
}
this.form.ret_key = ''
this.script = ''
this.cascade_attributes = []
if (this.$refs.codemirror) {
this.$refs.codemirror.initCodeMirror('')
}
this.choice_other = {
type_ids: undefined,
attr_id: undefined
}
if (this.$refs.attrFilter) {
this.$refs.attrFilter.init(true, false)
}
})
},
initEnumValue() {
if (this.valueList) {
const valueList = _.cloneDeep(this.valueList)
valueList.forEach((item) => {
item[0] = ''
})
this.$set(this, 'valueList', valueList)
}
},
setExpFromFilter(filterExp) {
if (filterExp) {
this.filterExp = `${filterExp}`
@@ -390,11 +534,33 @@ export default {
})
}
},
showAllPropDrawer() {
this.$refs.allAttrDrawer.open()
},
},
}
</script>
<style lang="less" scoped>
.tab-builtin {
display: flex;
align-items: center;
&-title {
font-size: 14px;
}
&-tag {
background-color: #E1EFFF;
color: #2F54EB;
font-size: 10px;
font-weight: 400;
padding: 0 3px;
margin-left: 3px;
}
}
.pre-value-edit-color {
display: flex;
flex-direction: row;
@@ -408,6 +574,18 @@ export default {
margin: 5px;
}
}
.script-tip {
font-size: 12px;
line-height: 22px;
color: #a5a9bc;
}
.all-attr-btn {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
</style>
<style lang="less">

View File

@@ -0,0 +1,321 @@
<template>
<div>
<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,
}
}
"
@select="(value) => handleChangeExp(value, item, index)"
appendToBody
: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 class="attr-filter-tip">{{ $t('cmdb.ciType.attrFilterTip') }}{{ isOpenSource ? ` (${$t('cmdb.enterpriseVersionTip')})` : '' }}</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>

View 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=).+$',
},
CITypeId: {
type: Number,
default: null,
},
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.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>

View File

@@ -0,0 +1,320 @@
<template>
<div>
<div class="control-group" v-if="controlType === 'choice'" >
<div
class="choice-group"
@click="handleControlType('input')"
>
<a-icon class="choice-group-icon" type="caret-down" />
</div>
<treeselect
class="custom-treeselect input-group"
:style="{ '--custom-height': rowHeight }"
:value="choiceValue"
@input="(value) => handleChange('value', value)"
:multiple="false"
:clearable="false"
searchable
:options="curModelAttrList"
:placeholder="$t('placeholder2')"
:normalizer="
(node) => {
return {
id: node.name,
label: node.name,
children: node.children,
}
}
"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
<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 class="control-group" v-else>
<div
class="text-group"
@click="handleControlType('choice')"
>
<ops-icon class="text-group-icon" type="veops-text" />
</div>
<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')"
>
<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: String(node[0] || ''),
label: node[1] ? node[1].label || node[0] : node[0],
children: node.children && node.children.length ? node.children : undefined,
}
}
"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
<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,
}
}
"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
</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>
</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,
controlType: 'input',
}
},
computed: {
choiceValue() {
const regex = /\{\{([^}]+)\}\}/g
const val = regex.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 []
},
handleControlType(type) {
this.controlType = type
},
handleChange(key, value) {
if (this.controlType === 'choice' && key === 'value') {
value = `{{ ${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;
}
}
.choice-group {
width: 14px;
height: 36px;
flex-shrink: 0;
background-color: #00B3CC;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&-icon {
font-size: 12px;
color: #FFFFFF;
}
}
.text-group {
width: 14px;
height: 36px;
flex-shrink: 0;
background-color: #2F54EB;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&-icon {
font-size: 12px;
color: #FFFFFF;
}
}
.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;
}
}
}
</style>

View File

@@ -0,0 +1,104 @@
export const BUILT_IN_TYPE = {
DEPARTMENT: 'department', // 部门
USER: 'user', // 用户
USER_GROUP: 'userGroup' // 用户组
}
export const DISPLAY_VALUE_SELECT = [
{ label: 'cs.companyStructure.nickname', value: 'nickname' },
{ label: 'cs.companyStructure.email', value: 'email' },
{ label: 'cs.companyStructure.mobile', value: 'mobile' },
]
export const USER_FILTER_SELECT = [
{ label: 'cs.companyStructure.nickname', value: 'nickname' },
{ label: 'cs.companyStructure.username', value: 'username' },
{ label: 'cs.companyStructure.email', value: 'email' },
{
label: 'cs.companyStructure.sex',
value: 'sex',
is_choice: true,
choice_value: [
{ label: 'cs.companyStructure.male', value: '男' },
{ label: 'cs.companyStructure.female', value: '女' },
],
},
{ label: 'cs.companyStructure.mobile', value: 'mobile' },
{ label: 'cs.companyStructure.departmentName', value: 'department_name' },
{ label: 'cs.companyStructure.positionName', value: 'position_name' },
{ label: 'cs.companyStructure.supervisor', value: 'direct_supervisor_id' },
{ label: 'cs.companyStructure.annualLeave', value: 'annual_leave' },
{ label: 'cs.companyStructure.currentCompany', value: 'current_company' },
{ label: 'cs.companyStructure.dfcEntryDate', value: 'dfc_entry_date' },
{ label: 'cs.companyStructure.entryDate', value: 'entry_date' },
{
label: 'cs.companyStructure.isInternship',
value: 'is_internship',
is_choice: true,
choice_value: [
{ label: 'cs.companyStructure.fullTime', value: 0 },
{ label: 'cs.companyStructure.internship', value: 1 },
],
},
{ label: 'cs.companyStructure.leaveDate', value: 'leave_date' },
{ label: 'cs.companyStructure.idCard', value: 'id_card' },
{ label: 'cs.companyStructure.nation', value: 'nation' },
{ label: 'cs.companyStructure.idPlace', value: 'id_place' },
{
label: 'cs.companyStructure.party',
value: 'party',
is_choice: true,
choice_value: [
{ label: 'cs.companyStructure.partyMember', value: '党员' },
{ label: 'cs.companyStructure.member', value: '团员' },
{ label: 'cs.companyStructure.masses', value: '群众' },
],
},
{
label: 'cs.companyStructure.householdRegistrationType',
value: 'household_registration_type',
is_choice: true,
choice_value: [
{ label: 'cs.companyStructure.town', value: '城镇' },
{ label: 'cs.companyStructure.agriculture', value: '农业' },
],
},
{ label: 'cs.companyStructure.hometown', value: 'hometown' },
{
label: 'cs.companyStructure.marry',
value: 'marry',
is_choice: true,
choice_value: [
{ label: 'cs.companyStructure.unmarried', value: '未婚' },
{ label: 'cs.companyStructure.married', value: '已婚' },
],
},
{
label: 'cs.companyStructure.maxDegree',
value: 'max_degree',
is_choice: true,
choice_value: [
{ label: 'cs.companyStructure.phd', value: '博士' },
{ label: 'cs.companyStructure.master', value: '硕士' },
{
label: 'cs.companyStructure.undergraduate',
value: '本科',
},
{ label: 'cs.companyStructure.specialist', value: '专科' },
{ label: 'cs.companyStructure.highSchool', value: '高中' },
],
},
{ label: 'cs.companyStructure.emergencyPerson', value: 'emergency_person' },
{ label: 'cs.companyStructure.emergencyPhone', value: 'emergency_phone' },
{ label: 'cs.companyStructure.bankCardNumber', value: 'bank_card_number' },
{ label: 'cs.companyStructure.bankCardName', value: 'bank_card_name' },
{ label: 'cs.companyStructure.openingBank', value: 'opening_bank' },
{ label: 'cs.companyStructure.accountOpeningLocation', value: 'account_opening_location' },
{ label: 'cs.companyStructure.birthDate', value: 'birth_date' },
{ label: 'cs.companyStructure.nationalityRegion', value: 'nationality_region' },
{ label: 'cs.companyStructure.birthPlace', value: 'birth_place' },
{ label: 'cs.companyStructure.firstEntryDate', value: 'first_entry_date' },
{ label: 'cs.companyStructure.estimatedDepartureDate', value: 'estimated_departure_date' },
// { label: '角色', value: 'roles' },
{ label: 'cs.companyStructure.lastLogin', value: 'last_login' },
]

View File

@@ -0,0 +1,279 @@
<template>
<div class="builtin">
<div class="builtin-tab">
<div
v-for="(item) in tabList"
:key="item.key"
:class="['builtin-tab-item', activeKey === item.key ? 'builtin-tab-item_active' : '']"
@click="clickTab(item.key)"
>
<ops-icon :type="item.icon" class="builtin-tab-item-icon" />
<span class="builtin-tab-item-title">{{ $t(item.title) }}</span>
</div>
</div>
<div
v-if="activeKey === BUILT_IN_TYPE.DEPARTMENT"
class="builtin-department"
>
<a-icon class="builtin-department-icon" type="info-circle" />
<span class="builtin-department-tip">{{ $t('cmdb.ciType.departmentTip') }}</span>
</div>
<a-form
:form="form"
v-show="activeKey === BUILT_IN_TYPE.USER"
class="builtin-user"
>
<a-form-item
:label="$t('cmdb.ciType.filterUsers')"
:label-col="formLayout.labelCol"
:wrapper-col="formLayout.wrapperCol"
>
<UserFilterComp
ref="userFilterRef"
/>
</a-form-item>
<a-form-item
:label="$t('cmdb.ciType.departmentCascadeDisplay')"
v-if="activeKey === BUILT_IN_TYPE.USER"
:label-col="formLayout.labelCol"
:wrapper-col="formLayout.wrapperCol"
>
<a-switch
v-decorator="['cascade_display', { rules: [{ required: false }], valuePropName: 'checked', initialValue: false }]"
></a-switch>
</a-form-item>
<a-form-item
v-if="activeKey === BUILT_IN_TYPE.USER"
:label="$t('cmdb.ciType.displayValue')"
:label-col="formLayout.labelCol"
:wrapper-col="formLayout.wrapperCol"
>
<a-select
class="builtin-select"
v-decorator="['display_value', { rules: [{ required: true, message: $t('cmdb.ciType.displayValueSelectTip') }], initialValue: 'nickname' }]"
showSearch
optionFilterProp="title"
:placeholder="$t('cmdb.ciType.displayValueSelectTip')"
>
<a-select-option
v-for="(item) in DISPLAY_VALUE_SELECT"
:value="item.value"
:key="item.value"
:title="$t(item.label)"
>
{{ $t(item.label) }}
</a-select-option>
</a-select>
</a-form-item>
</a-form>
<a-form
:form="form"
v-if="activeKey === BUILT_IN_TYPE.USER_GROUP"
class="builtin-usergroup"
>
<a-form-item
:label="$t('cmdb.ciType.userGroup')"
:label-col="formLayout.labelCol"
:wrapper-col="formLayout.wrapperCol"
>
<a-select
class="builtin-select"
v-decorator="['user_group_key', { rules: [{ required: true, message: $t('cmdb.ciType.userGroupSelectTip') }] }]"
showSearch
optionFilterProp="title"
:placeholder="$t('cmdb.ciType.userGroupSelectTip')"
>
<a-select-option
v-for="(item) in userGroupList"
:value="item.group_id"
:key="item.group_id"
:title="item.group_name"
>
{{ item.group_name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item
:label-col="formLayout.labelCol"
:wrapper-col="formLayout.wrapperCol"
:label="$t('cmdb.ciType.displayValue')"
>
<a-select
class="builtin-select"
v-decorator="['display_value', { rules: [{ required: true, message: $t('cmdb.ciType.displayValueSelectTip') }], initialValue: 'nickname' }]"
showSearch
optionFilterProp="title"
:placeholder="$t('cmdb.ciType.displayValueSelectTip')"
>
<a-select-option
v-for="(item) in DISPLAY_VALUE_SELECT"
:value="item.value"
:key="item.value"
:title="$t(item.label)"
>
{{ $t(item.label) }}
</a-select-option>
</a-select>
</a-form-item>
</a-form>
</div>
</template>
<script>
import { BUILT_IN_TYPE, DISPLAY_VALUE_SELECT } from './constants.js'
import UserFilterComp from './userFilterComp/index.vue'
export default {
name: 'PreValueBuiltIn',
components: { UserFilterComp },
data() {
return {
BUILT_IN_TYPE,
DISPLAY_VALUE_SELECT,
activeKey: BUILT_IN_TYPE.DEPARTMENT,
tabList: [
{
key: BUILT_IN_TYPE.DEPARTMENT,
title: 'cmdb.ciType.department',
icon: 'veops-department'
},
{
key: BUILT_IN_TYPE.USER,
title: 'cmdb.ciType.user',
icon: 'icon-shidi-yonghu'
},
{
key: BUILT_IN_TYPE.USER_GROUP,
title: 'cmdb.ciType.userGroup',
icon: 'ops-setting-group'
}
],
userGroupList: [],
allFlatEmployees: [],
form: this.$form.createForm(this),
formLayout: {
labelCol: { span: 5 },
wrapperCol: { span: 16 },
}
}
},
methods: {
setData(data) {
this.activeKey = data?.builtin_type || BUILT_IN_TYPE.DEPARTMENT
this.$nextTick(() => {
this.form.setFieldsValue({
cascade_display: data?.cascade_display ?? false,
display_value: data?.display_value ?? undefined,
user_group_key: data?.user_group_key ?? undefined
})
this.$refs.userFilterRef.setRuleList(data?.filter_rule_list || [])
})
},
getData() {
let params = {}
this.form.validateFields({ force: true }, (err, values) => {
if (err) {
params.isError = true
return
}
params = {
...values,
builtin_type: this.activeKey,
}
if (this.activeKey === BUILT_IN_TYPE.USER) {
params.filter_rule_list = this.$refs.userFilterRef.getRuleList()
}
})
return params
},
async clickTab(key) {
this.activeKey = key
},
}
}
</script>
<style lang="less" scoped>
.builtin {
width: 100%;
&-tab {
padding: 4px 80px 20px;
display: flex;
align-items: center;
gap: 60px;
&-item {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
border: solid 1px #E4E7ED;
border-radius: 2px;
padding: 0 20px;
min-width: 72px;
height: 64px;
cursor: pointer;
&-icon {
font-size: 20px;
color: #A5A9BC;
}
&-title {
font-size: 14px;
font-weight: 400;
line-height: 14px;
margin-top: 4px;
}
&_active {
border-color: #B1C9FF;
.builtin-tab-item-icon {
color: #7F97FA;
}
.builtin-tab-item-title {
color: #2F54EB;
}
}
}
}
&-department {
display: flex;
align-items: center;
margin-left: 60px;
&-icon {
font-size: 12px;
color: #A5A9BC;
}
&-tip {
color: #4E5969;
font-size: 14px;
font-weight: 400;
margin-left: 4px;
}
}
&-select {
width: 240px;
}
}
</style>

View File

@@ -0,0 +1,16 @@
export const ruleTypeList = [
{ value: '&', label: 'cs.components.and' },
{ value: '|', label: 'cs.components.or' },
]
export const expList = [
{ value: 1, label: 'cs.components.equal' },
{ value: 2, label: 'cs.components.notEqual' },
{ value: 7, label: 'cs.components.isEmpty' },
{ value: 8, label: 'cs.components.isNotEmpty' },
]
export const compareTypeList = [
{ value: 5, label: 'cs.components.moreThan' },
{ value: 6, label: 'cs.components.lessThan' },
]

View File

@@ -0,0 +1,143 @@
<template>
<treeselect
class="employee-tree-select"
:style="{ width: '100px' }"
:disable-branch-nodes="!multiple"
:multiple="multiple"
:options="employeeTreeSelectOption"
:placeholder="readOnly ? '' : placeholder || $t('cs.components.selectEmployee')"
v-model="treeValue"
:max-height="150"
:noChildrenText="$t('cs.components.empty')"
:noOptionsText="$t('cs.components.empty')"
:class="className ? className : 'ops-setting-treeselect'"
value-consists-of="LEAF_PRIORITY"
:limit="limit"
:limitText="(count) => `+ ${count}`"
v-bind="$attrs"
:zIndex="1050"
:flat="flat"
>
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
{{ node.label }}
</div>
</treeselect>
</template>
<script>
import Treeselect from '@riophae/vue-treeselect'
import { formatOption } from '@/utils/util'
export default {
name: 'EmployeeTreeSelect',
components: {
Treeselect,
},
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: [String, Array, Number, null],
default: null,
},
multiple: {
type: Boolean,
default: false,
},
className: {
type: String,
default: 'ops-setting-treeselect',
},
placeholder: {
type: String,
default: '',
},
idType: {
type: Number,
default: 3,
},
departmentKey: {
type: String,
default: 'department_id',
},
employeeKey: {
type: String,
default: 'employee_id',
},
limit: {
type: Number,
default: 20,
},
flat: {
type: Boolean,
default: false,
},
},
data() {
return {}
},
inject: {
provide_allTreeDepAndEmp: {
from: 'provide_allTreeDepAndEmp',
default: () => {
return () => {
return []
}
}
},
readOnly: {
from: 'readOnly',
default: false,
},
},
computed: {
treeValue: {
get() {
return this.value
},
set(val) {
this.$emit('change', val)
return val
},
},
allTreeDepAndEmp() {
return this.provide_allTreeDepAndEmp()
},
employeeTreeSelectOption() {
const option = formatOption(this.allTreeDepAndEmp, this.idType, false, this.departmentKey, this.employeeKey)
return option
},
},
methods: {},
}
</script>
<style lang="less" scoped>
.employee-tree-select {
/deep/ .vue-treeselect__menu {
width: 100px;
overflow-x: auto;
& > .vue-treeselect__list {
width: fit-content;
min-width: 100%;
}
.vue-treeselect__label-container {
width: max-content;
}
.vue-treeselect__option {
width: max-content;
min-width: 100%;
}
}
}
</style>

View File

@@ -0,0 +1,333 @@
<template>
<div class="user-filter">
<a-button
v-if="!ruleList.length"
type="primary"
ghost
size="small"
class="add-btn"
@click="handleAddRule"
>
<a-icon type="plus" />
{{ $t('add') }}
</a-button>
<template v-else>
<a-space :style="{ display: 'flex', marginBottom: '10px' }" v-for="(item, index) in ruleList" :key="item.id">
<div :style="{ width: '50px', height: '24px', position: 'relative' }">
<treeselect
v-if="index"
class="custom-treeselect"
:style="{ width: '50px', '--custom-height': '36px', position: 'absolute', top: '-30px', left: 0 }"
v-model="item.relation"
:multiple="false"
:clearable="false"
searchable
:options="ruleTypeList"
:normalizer="
(node) => {
return {
id: node.value,
label: $t(node.label),
children: node.children,
}
}
"
>
</treeselect>
</div>
<treeselect
class="custom-treeselect"
:style="{ width: '140px', '--custom-height': '36px' }"
v-model="item.column"
:multiple="false"
:clearable="false"
searchable
:options="userFilterSelect"
:maxHeight="150"
:normalizer="
(node) => {
return {
id: node.value,
label: $t(node.label),
children: node.children,
}
}
"
>
<div
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
<a-tooltip :title="$t(node.label)">
{{ $t(node.label) }}
</a-tooltip>
</div>
<div
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
slot="value-label"
slot-scope="{ node }"
>
<a-tooltip :title="$t(node.label)">
{{ $t(node.label) }}
</a-tooltip>
</div>
</treeselect>
<treeselect
class="custom-treeselect"
:style="{ width: '90px', '--custom-height': '36px' }"
v-model="item.operator"
:multiple="false"
:clearable="false"
searchable
:options="[...expList, ...compareTypeList]"
:maxHeight="150"
:normalizer="
(node) => {
return {
id: node.value,
label: $t(node.label),
children: node.children,
}
}
"
@select="(value) => handleChangeExp(value, item, index)"
>
<div
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
<a-tooltip :title="$t(node.label)">
{{ $t(node.label) }}
</a-tooltip>
</div>
<div
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
slot="value-label"
slot-scope="{ node }"
>
<a-tooltip :title="$t(node.label)">
{{ $t(node.label) }}
</a-tooltip>
</div>
</treeselect>
<treeselect
class="custom-treeselect"
:style="{ width: '100px', '--custom-height': '36px' }"
v-model="item.value"
:multiple="false"
:clearable="false"
searchable
v-if="isChoiceByProperty(item.column) && (item.operator === 1 || item.operator === 2)"
:options="getChoiceValueByProperty(item.column)"
:placeholder="$t('cs.components.selectPlaceholder')"
:normalizer="
(node) => {
return {
id: node.value,
label: $t(node.label),
children: node.children,
}
}
"
>
<div
:title="$t(node.label)"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
{{ $t(node.label) }}
</div>
</treeselect>
<div v-else-if="item.column === 'direct_supervisor_id' && (item.operator === 1 || item.operator === 2)">
<EmployeeTreeSelect v-model="item.value" className="custom-treeselect"/>
</div>
<a-input
v-else-if="item.operator !== 7 && item.operator !== 8"
size="small"
v-model="item.value"
:placeholder="item.exp === 'in' || item.exp === '~in' ? $t('cs.components.operatorInPlaceholder') : ''"
class="ops-input"
></a-input>
<a-tooltip :title="$t('cs.components.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)"><ops-icon type="veops-reduce"/></a>
</a-tooltip>
<a-tooltip :title="$t('add')">
<a class="operation" @click="handleAddRule"><ops-icon type="veops-increase"/></a>
</a-tooltip>
</a-space>
</template>
</div>
</template>
<script>
import _ from 'lodash'
import { v4 as uuidv4 } from 'uuid'
import Treeselect from '@riophae/vue-treeselect'
import { ruleTypeList, expList, compareTypeList } from './constants'
import { USER_FILTER_SELECT } from '../constants'
import EmployeeTreeSelect from './employeeTreeSelect.vue'
export default {
name: 'UserFilterComp',
components: {
Treeselect,
EmployeeTreeSelect
},
data() {
return {
USER_FILTER_SELECT,
ruleTypeList,
expList,
compareTypeList,
ruleList: [],
filterExp: '',
userFilterSelect: [],
}
},
methods: {
async setRuleList(ruleList) {
this.ruleList = ruleList?.length ? ruleList : []
},
handleAddRule() {
this.ruleList.push({
id: uuidv4(),
relation: '&',
column: this.userFilterSelect?.[0]?.value ?? null,
operator: 1,
value: null,
})
},
handleCopyRule(item) {
this.ruleList.push({ ...item, id: uuidv4() })
},
handleDeleteRule(item) {
const idx = this.ruleList.findIndex((r) => r.id === item.id)
if (idx > -1) {
this.ruleList.splice(idx, 1)
}
},
getRuleList() {
if (this.ruleList && this.ruleList.length) {
const ruleList = _.cloneDeep(this.ruleList)
ruleList.forEach((item, index) => {
if (item.column === 'direct_supervisor_id') {
ruleList[index].value = item.value ? (item.value + '').includes('-') ? +item.value.split('-')[1] : +item.value : 0
}
})
if (ruleList?.length > 0) {
ruleList[0].relation = '&'
}
return ruleList.map((rule) => {
return _.omit(rule, 'id')
})
}
return []
},
handleChangeExp({ value }, item, index) {
const _ruleList = _.cloneDeep(this.ruleList)
if (value === 7 || value === 8) {
_ruleList[index] = {
..._ruleList[index],
value: null,
operator: value
}
} else {
_ruleList[index] = {
..._ruleList[index],
operator: value,
}
}
this.ruleList = _ruleList
},
filterOption(input, option) {
return option.componentOptions.children[1].children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
},
isChoiceByProperty(column) {
const _find = this.userFilterSelect.find((item) => item.value === column)
if (_find) {
return _find.is_choice
}
return false
},
getChoiceValueByProperty(column) {
const _find = this.userFilterSelect.find((item) => item.value === column)
if (_find) {
return _find.choice_value
}
return []
},
},
}
</script>
<style lang="less" scoped>
.user-filter {
display: flex;
flex-direction: column;
line-height: 36px;
.ops-input {
height: 36px;
width: 100px;
}
}
.table-filter {
.table-filter-add {
margin-top: 10px;
& > a {
padding: 2px 8px;
&:hover {
background-color: #f0faff;
border-radius: 5px;
}
}
}
.table-filter-extra-icon {
padding: 0px 2px;
&:hover {
display: inline-block;
border-radius: 5px;
background-color: #f0faff;
}
}
}
.add-btn {
font-size: 12px;
width: 80px;
margin-top: 10px;
}
</style>
<style lang="less" scoped>
.table-filter-extra-operation {
.ant-popover-inner-content {
padding: 3px 4px;
.operation {
cursor: pointer;
width: 90px;
height: 30px;
line-height: 30px;
padding: 3px 4px;
border-radius: 5px;
transition: all 0.3s;
&:hover {
background-color: #f0faff;
}
> .anticon {
margin-right: 10px;
}
}
}
}
</style>

View File

@@ -0,0 +1,48 @@
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: '<=' },
]
export const ENUM_VALUE_TYPE = {
INPUT: 'input',
DATE: 'date',
DATE_TIME: 'dateTIme',
NUMBER: 'number'
}

View File

@@ -0,0 +1,310 @@
<template>
<div>
<a-popover
:visible="popoverVisible"
placement="bottom"
trigger="focus"
overlayClassName="pre-value-edit-popover"
@visibleChange="handleVisibleChange"
>
<div @click="popoverVisible = true" ref="popoverLabelRef">
<a-input
v-show="!labelData.label || popoverVisible"
type="text"
:style="{ width: '210px' }"
:value="labelData.label"
@change="changeLabel"
>
</a-input>
<div
class="pre-value-tag"
:style="labelData.style ? labelData.style : {}"
v-show="!popoverVisible && labelData.label"
>
<span>
<img
v-if="labelData.icon.id && labelData.icon.url"
:src="`/api/common-setting/v1/file/${labelData.icon.url}`"
:style="{ maxHeight: '12px', maxWidth: '12px', marginRight: '5px' }"
/>
<ops-icon
v-else-if="labelData.icon.name"
:type="labelData.icon.name"
:style="{ marginRight: '5px', color: labelData.icon.color || '#595959' }"
/>
<a-tooltip :title="labelData.label">
<span class="pre-value-tag-text">{{ labelData.label }}</span>
</a-tooltip>
</span>
</div>
</div>
<div ref="preValueEdit" slot="content">
<a-divider orientation="left" style="margin:8px 0;color:gray;font-size:10px;">{{ $t('icon') }}</a-divider>
<CustomIconSelect
:style="{ marginLeft: '10px' }"
:value="labelData.icon"
@change="changeIcon"
/>
<a-divider orientation="left" style="margin:8px 0;color:gray;font-size:10px;">{{ $t('cmdb.ciType.font') }}</a-divider>
<div :style="{ display: 'flex', justifyContent: 'space-around' }">
<div
@click="changeFontStyle('fontWeight', 'bold')"
:class="`attributes-font-icon ${labelData.style.fontWeight === 'bold' ? 'attributes-font-icon-selected' : ''}`"
>
<a-icon type="bold" />
</div>
<div
@click="changeFontStyle('fontStyle', 'italic')"
:class="`attributes-font-icon ${labelData.style.fontStyle === 'italic' ? 'attributes-font-icon-selected' : ''}`"
>
<a-icon type="italic" />
</div>
<div
@click="changeFontStyle('textDecoration', 'underline')"
:class="
`attributes-font-icon ${labelData.style.textDecoration === 'underline' ? 'attributes-font-icon-selected' : ''}`
"
>
<a-icon type="underline" />
</div>
</div>
<a-divider orientation="left" style="margin:8px 0;color:gray;font-size:10px;">{{ $t('cmdb.ciType.color') }}</a-divider>
<div :style="{ display: 'flex', justifyContent: 'space-around' }">
<div class="attributes-font-color">
<a-icon type="font-colors" />
<el-color-picker
size="mini"
:value="labelData.style.color"
@change="(v) => changeFontStyle('color', v)"
:predefine="defaultBGColors"
>
</el-color-picker>
</div>
<div class="attributes-font-color">
<a-icon type="bg-colors" />
<el-color-picker
size="mini"
:value="labelData.style.backgroundColor"
@change="(v) => changeFontStyle('backgroundColor', v)"
:predefine="defaultBGColors"
>
</el-color-picker>
</div>
</div>
<a-divider orientation="left" style="margin:8px 0;color:gray;font-size:10px;">{{ $t('operation') }}</a-divider>
<div style="text-align:right;">
<a-tooltip
:title="$t('delete')"
>
<a>
<a-icon
@click="handleDelete"
style="margin-right:10px;color:red;"
type="delete"
/>
</a>
</a-tooltip>
<a-tooltip
:title="$t('confirm')"
>
<a>
<a-icon @click="popoverVisible = false" style="margin-right:10px;color:green;" type="check"/>
</a>
</a-tooltip>
</div>
</div>
</a-popover>
</div>
</template>
<script>
import _ from 'lodash'
import { ColorPicker } from 'element-ui'
import CustomIconSelect from '@/components/CustomIconSelect'
import { defautValueColor, defaultBGColors } from '@/modules/cmdb/utils/const.js'
import lang from 'element-ui/lib/locale/lang/en'
import locale from 'element-ui/lib/locale'
locale.use(lang)
export default {
name: 'DefineLabel',
components: { ElColorPicker: ColorPicker, CustomIconSelect },
props: {
labelData: {
type: Object,
default: () => {},
},
},
data() {
return {
defautValueColor,
defaultBGColors,
popoverVisible: false,
}
},
mounted() {
document.addEventListener('click', this.eventListener)
},
beforeDestroy() {
document.removeEventListener('click', this.eventListener)
},
methods: {
eventListener(e) {
if (this.popoverVisible) {
const dom = this.$refs.preValueEdit
const dom_label = this.$refs.popoverLabelRef
const dom_icon = document.getElementById(`custom-icon-select-popover`)
e.stopPropagation()
e.preventDefault()
if (dom) {
const isSelf =
dom.contains(e.target) || (dom_label && dom_label.contains(e.target)) || (dom_icon && dom_icon.contains(e.target))
if (!isSelf) {
this.popoverVisible = false
}
}
}
},
handleDelete() {
this.popoverVisible = false
this.$emit('deleteData')
},
changeFontStyle(key, value) {
const style = {
...(this.labelData.style || {}),
[key]: this.labelData.style[key] === value ? 'initial' : value,
}
this.$emit('change', 'style', style)
},
changeLabel(e) {
const value = e.target.value
this.$emit('change', 'label', value)
},
changeIcon(value) {
this.$emit('change', 'icon', value)
},
handleVisibleChange(v) {
if (!v) {
this.popoverVisible = false
}
}
},
}
</script>
<style lang="less" scoped>
.pre-value-edit-color {
display: flex;
flex-direction: row;
justify-content: space-between;
flex-wrap: wrap;
.pre-value-edit-color-item {
cursor: pointer;
display: inline-block;
width: 25px;
height: 20px;
margin: 5px;
}
}
.pre-value-tag {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
position: relative;
cursor: pointer;
max-width: 100%;
&-text {
overflow: hidden;
text-wrap: nowrap;
text-overflow: ellipsis;
max-width: 100%;
}
> span {
display: flex;
align-items: center;
}
&:hover .pre-value-tag-dropdown-icon {
display: inline !important;
}
.pre-value-tag-dropdown {
font-size: 10px;
color: #999999;
&:hover {
color: #2f54eb;
}
.pre-value-tag-dropdown-icon {
display: none;
position: absolute;
right: -10px;
top: 8px;
}
}
}
</style>
<style lang="less">
.pre-value-tag-input {
border: none;
border-bottom: 1px solid #d9d9d9;
font-size: 12px;
&:focus {
box-shadow: none;
}
}
.pre-value-edit-popover.ant-popover-placement-top .ant-popover-content {
margin-bottom: -10px;
}
.pre-value-edit-popover.ant-popover-placement-bottom .ant-popover-content {
margin-top: -10px;
}
.pre-value-edit-popover {
.ant-popover-content {
width: 150px;
.ant-popover-arrow {
display: none;
}
.ant-popover-inner-content {
padding: 3px 4px;
}
}
.attributes-font-icon {
cursor: pointer;
display: inline-block;
width: 30px;
height: 30px;
position: relative;
border: 1px solid #fff;
&:hover {
background-color: #eeeeee;
border-color: #606266;
}
> i {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
.attributes-font-icon-selected {
background-color: #eeeeee;
}
.attributes-font-color {
display: inline-flex;
align-items: center;
width: 50%;
justify-content: center;
}
}
</style>

View File

@@ -0,0 +1,245 @@
<template>
<div class="define-wrap">
<a-button
v-if="!defineList.length"
type="primary"
ghost
:disabled="disabled"
size="small"
class="add-btn"
@click="addData"
>
<a-icon type="plus" />
{{ $t('add') }}
</a-button>
<vxe-table
v-else
ref="xTable"
:data="defineList"
size="mini"
show-header-overflow
:row-config="{ height: 46 }"
:min-height="75"
border="outer"
class="define-wrap-table"
>
<vxe-column field="value" width="230" :title="$t('cmdb.ciType.enumValue')">
<template #header="{ column }">
<span class="table-header-required">*</span>
{{ column.title }}
</template>
<template #default="{ row, rowIndex }">
<a-input
v-if="enumValueType === ENUM_VALUE_TYPE.INPUT"
:value="row.value"
:placeholder="$t('cmdb.ciType.valueInputTip')"
@change="(e) => changeValue(rowIndex, e.target.value)"
></a-input>
<a-input-number
v-else-if="enumValueType === ENUM_VALUE_TYPE.NUMBER"
:value="row.value"
@change="(v) => changeValue(rowIndex, v)"
>
</a-input-number>
<a-date-picker
v-else
style="width: 100%"
:value="row.value"
:format="enumValueType === ENUM_VALUE_TYPE.DATE ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'"
:showTime="enumValueType === ENUM_VALUE_TYPE.DATE ? false : { format: 'HH:mm:ss' }"
@change="(e) => changeDate(rowIndex, e)"
/>
</template>
</vxe-column>
<vxe-column width="230" :title="$t('cmdb.ciType.label')">
<template #default="{ row, rowIndex }">
<DefineLabel
:labelData="row"
@change="(key, value) => changeStyle(rowIndex, key, value)"
@deleteData="handleClear(rowIndex)"
/>
</template>
</vxe-column>
</vxe-table>
<div class="define-wrap-action">
<div
v-for="(item, index) in defineList"
:key="item.id"
class="define-wrap-action-item"
>
<a-icon
type="plus-circle"
class="define-wrap-action-item-icon"
@click="addData(index)"
/>
<a-icon
type="minus-circle"
class="define-wrap-action-item-icon"
@click="deleteData(index)"
/>
</div>
</div>
</div>
</template>
<script>
import { v4 as uuidv4 } from 'uuid'
import _ from 'lodash'
import DefineLabel from './defineLabel.vue'
import { ENUM_VALUE_TYPE } from '../constants.js'
export default {
name: 'PreValueDefine',
components: {
DefineLabel
},
props: {
value: {
type: Array,
default: () => []
},
disabled: {
type: Boolean,
default: false
},
// 枚举值控件类型
enumValueType: {
type: String,
default: ENUM_VALUE_TYPE.INPUT
}
},
model: {
prop: 'value',
event: 'change',
},
data() {
return {
ENUM_VALUE_TYPE
}
},
computed: {
defineList: {
get() {
return this.value.map((item) => {
return {
value: item?.[0] ?? '',
...(item?.[1] ?? {}),
id: uuidv4()
}
})
},
set(val) {
this.$emit('change', val.map((item) => {
return [
item?.value ?? '',
{
style: item?.style ?? {},
icon: item?.icon ?? {},
label: item?.label ?? '',
}
]
}))
return val
},
},
},
methods: {
addData(index) {
const list = _.cloneDeep(this.value)
list.splice(index + 1, 0, [
'',
{
style: {},
icon: {},
label: ''
}
])
this.$emit('change', list)
},
deleteData(index) {
if (this.value.length <= 1) {
this.$message.error(this.$t('cmdb.ad.deleteTip'))
return
}
const list = _.cloneDeep(this.value)
list.splice(index, 1)
this.$emit('change', list)
},
changeValue(rowIndex, value) {
const list = _.cloneDeep(this.value)
list[rowIndex][0] = value
this.$emit('change', list)
},
changeDate(rowIndex, e) {
const format = this.enumValueType === ENUM_VALUE_TYPE.DATE ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm:ss'
const value = e.format(format)
const list = _.cloneDeep(this.value)
list[rowIndex][0] = value
this.$emit('change', list)
},
changeStyle(rowIndex, key, value) {
const list = _.cloneDeep(this.value)
list[rowIndex][1] = {
...list[rowIndex][1],
[key]: value
}
this.$emit('change', list)
},
handleClear(rowIndex) {
const list = _.cloneDeep(this.value)
list[rowIndex][1] = {
style: {},
icon: {},
label: ''
}
this.$emit('change', list)
}
}
}
</script>
<style lang="less" scoped>
.define-wrap {
display: flex;
.add-btn {
font-size: 12px;
padding: 1px 7px;
}
&-table {
flex-shrink: 0;
.table-header-required {
color: #FD4C6A;
}
/deep/ .ant-input-number {
width: 100%;
}
}
&-action {
flex-shrink: 0;
margin-left: 11px;
padding-top: 36px;
&-item {
display: flex;
align-items: center;
height: 46px;
gap: 12px;
&-icon {
cursor: pointer;
color: #2F54EB;
}
}
}
}
</style>

View File

@@ -137,6 +137,7 @@ import {
getCITypeChildren,
getCITypeParent
} from '../../api/CITypeRelation.js'
import { getCITypeAttributesById } from '../../api/CITypeAttr'
export default {
name: 'RelationAutoDiscovery',
@@ -169,7 +170,18 @@ export default {
methods: {
async getCITypeAttributes() {
const res = await getCITypeAttributes(this.CITypeId)
this.ciTypeADTAttributes = res.map((item) => {
const attr = await getCITypeAttributesById(this.CITypeId)
const filterAttr = res.filter((name) => {
const currentAttr = attr?.attributes?.find((item) => item?.name === name)
if (!currentAttr) {
return true
}
return this.filterAttributes(name)
})
this.ciTypeADTAttributes = filterAttr.map((item) => {
return {
id: item,
value: item,
@@ -239,7 +251,7 @@ export default {
const peer_type_id = item.peer_type_id
const attributes = this?.relationOptions?.find((option) => option?.value === peer_type_id)?.attributes
item.attributes = attributes
item.attributes = attributes.filter((attr) => this.filterAttributes(attr))
item.peer_attr_id = undefined
})
},
@@ -288,6 +300,15 @@ export default {
this.getCITypeRelations()
}
},
filterAttributes(attr) {
// filter password/json/is_list/longText/bool/reference
if (attr?.value_type === '2' && !attr?.is_index) {
return false
}
return !attr?.is_password && !attr?.is_list && attr?.value_type !== '6' && !attr?.is_bool && !attr?.is_reference
},
},
}
</script>

View File

@@ -179,6 +179,7 @@
>
<a-select-option :value="CIType.id" :key="CIType.id" v-for="CIType in CITypes">
{{ CIType.alias || CIType.name }}
<span class="model-select-name">({{ CIType.name }})</span>
</a-select-option>
</a-select>
</a-form-item>
@@ -598,8 +599,14 @@ export default {
})
},
filterAttributes(attributes) {
// filter password/json/is_list
return attributes.filter((attr) => !attr.is_password && !attr.is_list && attr.value_type !== '6')
// filter password/json/is_list/longText/bool/reference
return attributes.filter((attr) => {
if (attr.value_type === '2' && !attr.is_index) {
return false
}
return !attr.is_password && !attr.is_list && attr.value_type !== '6' && !attr.is_bool && !attr.is_reference
})
},
addTableAttr() {
this.tableAttrList.push({
@@ -668,6 +675,11 @@ export default {
.modal-attribute-action {
margin-left: 5px;
}
.model-select-name {
font-size: 12px;
color: #A5A9BC;
}
</style>
<style lang="less">

View File

@@ -471,10 +471,15 @@ export default {
this.dateForm = _.cloneDeep(this.defaultDateForm)
this.notifies = _.cloneDeep(this.defaultNotify)
this.category = 1
this.triggerAction = '1'
this.filterExp = ''
this.selectedBot = undefined
this.visible = false
if (this.$refs.noticeContent) {
this.$refs.noticeContent.destroy()
}
this.$nextTick(() => {
this.visible = false
})
},
filterChange(value) {
this.filterValue = value

View File

@@ -362,6 +362,7 @@ export default {
commonAttributes: [],
level2children: {},
isShadow: false,
changeCITypeRequestValue: null,
}
},
computed: {
@@ -425,6 +426,14 @@ export default {
this.fontColor = fontColor
this.bgColor = bgColor
}
if (type_ids?.length || type_id) {
const requireTypeIds = type_id ? [type_id] : type_ids
await getCITypeAttributesByTypeIds({ type_ids: requireTypeIds.join(',') }).then((res) => {
this.attributes = res.attributes
})
}
if (type_ids && type_ids.length) {
await getCITypeAttributesByTypeIds({ type_ids: type_ids.join(',') }).then((res) => {
this.attributes = res.attributes
@@ -483,19 +492,26 @@ export default {
changeCIType(value) {
this.form.attr_ids = []
this.commonAttributes = []
this.changeCITypeRequestValue = value
if ((Array.isArray(value) && value.length) || (!Array.isArray(value) && value)) {
getCITypeAttributesByTypeIds({ type_ids: Array.isArray(value) ? value.join(',') : value }).then((res) => {
this.attributes = res.attributes
if (this.changeCITypeRequestValue === value) {
this.attributes = res.attributes
}
})
}
if (!Array.isArray(value) && value) {
getRecursive_level2children(value).then((res) => {
this.level2children = res
if (this.changeCITypeRequestValue === value) {
this.level2children = res
}
})
}
if ((['bar', 'line', 'pie'].includes(this.chartType) && this.form.category === 1) || this.chartType === 'table') {
getCITypeCommonAttributesByTypeIds({ type_ids: Array.isArray(value) ? value.join(',') : value }).then((res) => {
this.commonAttributes = res.attributes
if (this.changeCITypeRequestValue === value) {
this.commonAttributes = res.attributes
}
})
}
},

View File

@@ -73,6 +73,7 @@ export const category_1_bar_options = (data, options) => {
type: 'bar',
stack: options?.barStack ?? 'total',
barGap: 0,
barMaxWidth: '16px',
emphasis: {
focus: 'series'
},
@@ -242,6 +243,7 @@ export const category_2_bar_options = (data, options, chartType) => {
label: {
show: false,
},
barMaxWidth: '16px',
areaStyle: chartType === 'line' && options?.isShadow ? {
opacity: 0.5,
color: {

View File

@@ -22,11 +22,11 @@
<a-form-model-item :label="$t('name')" prop="name">
<a-input v-model="form.name" />
</a-form-model-item>
<a-form-model-item :label="$t('icon')" v-if="is_inner">
<a-form-model-item :label="$t('icon')">
<CustomIconSelect v-model="customIcon" :style="{ marginTop: '6px' }" />
</a-form-model-item>
<a-form-model-item :label="$t('cmdb.ad.mode')" prop="is_plugin">
<a-radio-group v-model="form.is_plugin" @change="changeIsPlugin" :disabled="!is_inner">
<a-radio-group v-model="form.is_plugin" @change="changeIsPlugin" :disabled="true">
<a-radio :value="false">{{ $t('cmdb.custom_dashboard.default') }}</a-radio>
<a-radio :value="true">plugin</a-radio>
</a-radio-group>
@@ -137,9 +137,9 @@ export default {
AgentTable
},
props: {
is_inner: {
isDiscoveryPage: {
type: Boolean,
default: true,
default: false,
},
},
data() {
@@ -152,7 +152,7 @@ export default {
ruleData: {},
type: 'add',
adType: '',
form: { name: '', is_plugin: false },
form: { name: '', is_plugin: true },
rules: {},
customIcon: { name: '', color: '' },
tableData: [],
@@ -195,11 +195,9 @@ export default {
this.type = type
this.ruleData = data
this.adType = adType
if (!this.is_inner) {
this.form = {
name: '',
is_plugin: true,
}
this.form = {
name: '',
is_plugin: true,
}
if (adType === DISCOVERY_CATEGORY_TYPE.HTTP || adType === DISCOVERY_CATEGORY_TYPE.SNMP) {
return
@@ -225,7 +223,7 @@ export default {
// eslint-disable-next-line no-useless-escape
this.plugin_script = this.default_plugin_script
}
if (data?.is_plugin || !this.is_inner) {
if (this.form?.is_plugin) {
this.$nextTick(() => {
this.$refs.codemirror.initCodeMirror(this.plugin_script)
})
@@ -281,7 +279,7 @@ export default {
const params = {
...this.form,
type,
is_inner: this.is_inner,
is_inner: !this.form.is_plugin,
option: { icon: this.customIcon },
attributes: this.form.is_plugin
? undefined
@@ -301,14 +299,14 @@ export default {
this.type = 'edit'
this.ruleData = res
this.$message.success(this.$t('updateSuccess'))
if (this.is_inner) {
if (this.isDiscoveryPage) {
this.getDiscovery()
}
return
}
this.handleClose()
console.log(this.is_inner)
if (this.is_inner) {
if (this.isDiscoveryPage) {
this.$message.success(this.$t('saveSuccess'))
this.getDiscovery()
} else {

View File

@@ -78,7 +78,7 @@
<p class="setting-discovery-empty-text">{{ $t('noData') }}</p>
</div>
</div>
<EditDrawer ref="editDrawer" />
<EditDrawer ref="editDrawer" :isDiscoveryPage="true" />
<AccountConfig ref="accountConfig"/>
</div>
</template>

View File

@@ -24,9 +24,10 @@
@change="handleSourceTypeChange"
:filterOption="filterOption"
>
<a-select-option :value="CIType.id" :key="CIType.id" v-for="CIType in displayCITypes">{{
CIType.alias || CIType.name
}}</a-select-option>
<a-select-option :value="CIType.id" :key="CIType.id" v-for="CIType in displayCITypes">
{{ CIType.alias || CIType.name }}
<span class="model-select-name">({{ CIType.name }})</span>
</a-select-option>
</a-select>
</a-form-item>
<a-form-item :label="$t('cmdb.ciType.dstCIType')">
@@ -39,6 +40,7 @@
>
<a-select-option :value="CIType.id" :key="CIType.id" v-for="CIType in displayTargetCITypes">
{{ CIType.alias || CIType.name }}
<span class="model-select-name">({{ CIType.name }})</span>
</a-select-option>
</a-select>
</a-form-item>
@@ -364,8 +366,14 @@ export default {
return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
},
filterAttributes(attributes) {
// filter password/json/is_list
return attributes.filter((attr) => !attr.is_password && !attr.is_list && attr.value_type !== '6')
// filter password/json/is_list/longText/bool/reference
return attributes.filter((attr) => {
if (attr.value_type === '2' && !attr.is_index) {
return false
}
return !attr.is_password && !attr.is_list && attr.value_type !== '6' && !attr.is_bool && !attr.is_reference
})
},
addModalAttr() {
@@ -402,4 +410,9 @@ export default {
.modal-attribute-action {
margin-left: 5px;
}
.model-select-name {
font-size: 12px;
color: #A5A9BC;
}
</style>

View File

@@ -306,8 +306,14 @@ export default {
return _find?.alias ?? _find?.name ?? id
},
filterAttributes(attributes) {
// filter password/json/is_list
return attributes.filter((attr) => !attr.is_password && !attr.is_list && attr.value_type !== '6')
// filter password/json/is_list/longText/bool/reference
return attributes.filter((attr) => {
if (attr.value_type === '2' && !attr.is_index) {
return false
}
return !attr.is_password && !attr.is_list && attr.value_type !== '6' && !attr.is_bool && !attr.is_reference
})
},
addTableAttr() {

View File

@@ -181,12 +181,19 @@
</span>
</div>
<div v-else class="cmdb-preference-footor-unsubscribed">
<span
@click="openSubscribeSetting(item)"
><ops-icon :style="{ marginRight: '3px' }" type="cmdb-preference-subscribe" />{{
$t('cmdb.preference.sub')
}}</span
<a
@click="handleSubscribeCIType(item)"
class="cmdb-preference-footor-unsubscribed-item"
>
<ops-icon type="cmdb-ci" />{{ $t('cmdb.preference.subCITable') }}
</a>
<span class="cmdb-preference-footor-unsubscribed-gap"></span>
<a
@click="openSubscribeSetting(item, '2')"
class="cmdb-preference-footor-unsubscribed-item"
>
<ops-icon type="cmdb-tree" />{{ $t('cmdb.preference.subCITree') }}
</a>
</div>
</div>
<i></i><i></i><i></i><i></i><i></i>
@@ -221,6 +228,7 @@ import {
subscribeTreeView,
preferenceCitypeOrder,
} from '@/modules/cmdb/api/preference'
import { getCITypeAttributesByName } from '@/modules/cmdb/api/CITypeAttr'
import CollapseTransition from '@/components/CollapseTransition'
import SubscribeSetting from '../../components/subscribeSetting/subscribeSetting'
import { getCIAdcStatistics } from '../../api/ci'
@@ -381,9 +389,39 @@ export default {
this.getCITypes()
})
},
async handleSubscribeCIType(ciType) {
try {
const res = await getCITypeAttributesByName(ciType.id)
const attributes = res?.attributes || []
const subscribeList = attributes
.filter((item) => item?.default_show)
.map((item) => {
return [item?.id?.toString(), false]
})
if (subscribeList.length === 0) {
const uniqueItem = attributes.find((item) => item?.id === res?.unique_id)
if (uniqueItem) {
subscribeList.push([uniqueItem?.id?.toString(), false])
}
}
await subscribeCIType(
ciType.id,
subscribeList
)
this.$message.success(this.$t('cmdb.components.subSuccess'))
this.resetRoute()
} catch (error) {
console.error('handleSubscribeCIType failed', error)
this.$message.success(this.$t('cmdb.components.subFailed'))
}
},
openSubscribeSetting(ciType, activeKey = '1') {
this.$refs.subscribeSetting.open({ ...ciType, type_id: ciType.id }, activeKey)
},
changeGroupExpand(group) {
const _idx = this.expandKeys.findIndex((expand) => expand === group.id)
if (_idx > -1) {
@@ -653,11 +691,27 @@ export default {
}
}
.cmdb-preference-footor-unsubscribed {
text-align: center;
> span {
color: @primary-color;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 10px;
&-item {
display: flex;
align-items: center;
gap: 3px;
font-size: 12px;
color: rgba(0, 0, 0, 0.76);
&:hover {
color: #1890ff;
}
}
&-gap {
width: 1px;
height: 18px;
background-color: #e8e8e8;
}
}
.cmdb-preference-footor-subscribed {

View File

@@ -163,204 +163,26 @@
</div>
</a-space>
</SearchForm>
<vxe-table
:id="`cmdb-relation-${viewId}-${currentTypeId}`"
border
keep-source
show-overflow
resizable
<CITable
ref="xTable"
size="small"
row-id="_id"
:height="tableHeight"
:id="`cmdb-relation-${viewId}-${currentTypeId}`"
:loading="loading"
show-header-overflow
highlight-hover-row
:attrList="preferenceAttrList"
:columns="columns"
:passwordValue="passwordValue"
:data="instanceList"
@checkbox-change="onSelectChange"
@checkbox-all="onSelectChange"
@checkbox-range-start="checkboxRangeStart"
@checkbox-range-change="checkboxRangeChange"
@checkbox-range-end="checkboxRangeEnd"
:checkbox-config="{ reserve: true, range: true }"
:height="tableHeight"
:showCheckbox="isLeaf"
:showDelete="isLeaf"
@onSelectChange="onSelectChange"
@edit-closed="handleEditClose"
@edit-actived="handleEditActived"
:edit-config="{ trigger: 'dblclick', mode: 'row', showIcon: false }"
:sort-config="{ remote: true, trigger: 'cell' }"
@sort-change="handleSortCol"
:row-key="true"
:column-key="true"
:cell-style="getCellStyle"
:scroll-y="{ enabled: true, gt: 20 }"
:scroll-x="{ enabled: true, gt: 0 }"
class="ops-unstripe-table checkbox-hover-table"
:custom-config="{ storage: true }"
>
<vxe-column v-if="isLeaf" align="center" type="checkbox" width="50" fixed="left">
<template #default="{row}">
{{ getRowSeq(row) }}
</template>
</vxe-column>
<vxe-table-column
v-for="(col, index) in columns"
:key="`${col.field}_${index}`"
:title="col.title"
:field="col.field"
:width="col.width"
:sortable="col.sortable"
:edit-render="getColumnsEditRender(col)"
:cell-type="col.value_type === '2' ? 'string' : 'auto'"
:fixed="col.is_fixed ? 'left' : ''"
>
<template #header>
<span class="vxe-handle">
<OpsMoveIcon
style="width: 17px; height: 17px; display: none; position: absolute; left: -3px; top: 12px"
/>
{{ col.title }}</span
>
</template>
<template v-if="col.is_choice || col.is_password" #edit="{ row }">
<vxe-input v-if="col.is_password" v-model="passwordValue[col.field]" />
<a-select
:getPopupContainer="(trigger) => trigger.parentElement"
:style="{ width: '100%', height: '32px' }"
v-model="row[col.field]"
:placeholder="$t('placeholder2')"
v-if="col.is_choice"
:showArrow="false"
:mode="col.is_list ? 'multiple' : 'default'"
class="ci-table-edit-select"
allowClear
>
<a-select-option
:value="choice[0]"
:key="'edit_' + col.field + idx"
v-for="(choice, idx) in col.filters"
>
<span
:style="{ ...(choice[1] ? choice[1].style : {}), display: 'inline-flex', alignItems: 'center' }"
>
<template v-if="choice[1] && choice[1].icon && choice[1].icon.name">
<img
v-if="choice[1].icon.id && choice[1].icon.url"
:src="`/api/common-setting/v1/file/${choice[1].icon.url}`"
:style="{ maxHeight: '13px', maxWidth: '13px', marginRight: '5px' }"
/>
<ops-icon
v-else
:style="{ color: choice[1].icon.color, marginRight: '5px' }"
:type="choice[1].icon.name"
/>
</template>
{{ choice[0] }}
</span>
</a-select-option>
</a-select>
</template>
<template
v-if="col.value_type === '6' || col.is_link || col.is_password || col.is_choice"
#default="{row}"
>
<span v-if="col.value_type === '6' && row[col.field]">{{ row[col.field] }}</span>
<a
v-else-if="col.is_link && row[col.field]"
:href="
row[col.field].startsWith('http') || row[col.field].startsWith('https')
? `${row[col.field]}`
: `http://${row[col.field]}`
"
target="_blank"
>{{ row[col.field] }}</a
>
<PasswordField
v-else-if="col.is_password && row[col.field]"
:ci_id="row._id"
:attr_id="col.attr_id"
></PasswordField>
<template v-else-if="col.is_choice">
<template v-if="col.is_list">
<span
v-for="value in row[col.field]"
:key="value"
:style="{
borderRadius: '4px',
padding: '1px 5px',
margin: '2px',
...getChoiceValueStyle(col, value),
display: 'inline-flex',
alignItems: 'center',
}"
>
<img
v-if="getChoiceValueIcon(col, value).id && getChoiceValueIcon(col, value).url"
:src="`/api/common-setting/v1/file/${getChoiceValueIcon(col, value).url}`"
:style="{ maxHeight: '13px', maxWidth: '13px', marginRight: '5px' }"
/>
<ops-icon
v-else
:style="{ color: getChoiceValueIcon(col, value).color, marginRight: '5px' }"
:type="getChoiceValueIcon(col, value).name"
/>{{ value }}</span
>
</template>
<span
v-else-if="row[col.field]"
:style="{
borderRadius: '4px',
padding: '1px 5px',
margin: '2px 0',
...getChoiceValueStyle(col, row[col.field]),
display: 'inline-flex',
alignItems: 'center',
}"
>
<img
v-if="getChoiceValueIcon(col, row[col.field]).id && getChoiceValueIcon(col, row[col.field]).url"
:src="`/api/common-setting/v1/file/${getChoiceValueIcon(col, row[col.field]).url}`"
:style="{ maxHeight: '13px', maxWidth: '13px', marginRight: '5px' }"
/>
<ops-icon
v-else
:style="{ color: getChoiceValueIcon(col, row[col.field]).color, marginRight: '5px' }"
:type="getChoiceValueIcon(col, row[col.field]).name"
/>{{ row[col.field] }}</span
>
</template>
</template>
</vxe-table-column>
<vxe-column align="left" field="operate" fixed="right" width="80">
<template #header>
<span>{{ $t('operation') }}</span>
</template>
<template #default="{ row }">
<a-space>
<a @click="$refs.detail.create(row.ci_id || row._id)">
<a-icon type="unordered-list" />
</a>
<a-tooltip :title="$t('cmdb.ci.addRelation')">
<a @click="$refs.detail.create(row.ci_id || row._id, 'tab_2', '2')">
<a-icon type="retweet" />
</a>
</a-tooltip>
<template v-if="isLeaf">
<a-tooltip :title="$t('cmdb.ciType.deleteInstance')">
<a @click="deleteCI(row)" :style="{ color: 'red' }">
<a-icon type="delete" />
</a>
</a-tooltip>
</template>
</a-space>
</template>
</vxe-column>
<template #empty>
<div v-if="loading" style="height: 200px; line-height: 200px">{{ $t('loading') }}</div>
<div v-else>
<img :style="{ width: '200px' }" :src="require('@/assets/data_empty.png')" />
<div>{{ $t('noData') }}</div>
</div>
</template>
</vxe-table>
@openDetail="openDetail"
@deleteCI="deleteCI"
/>
<div :style="{ textAlign: 'right', marginTop: '4px' }">
<a-pagination
:showSizeChanger="true"
@@ -405,7 +227,6 @@
@reload="sumbitFromCreateInstance"
@submit="batchUpdateFromCreateInstance"
/>
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
<BatchDownload ref="batchDownload" @batchDownload="batchDownload" />
<ReadPermissionsModal ref="readPermissionsModal" />
<RevokeModal ref="revokeModal" @handleRevoke="handleRevoke" />
@@ -440,16 +261,14 @@ import SplitPane from '@/components/SplitPane'
import EditAttrsPopover from '../ci/modules/editAttrsPopover.vue'
import CiDetailDrawer from '../ci/modules/ciDetailDrawer.vue'
import CreateInstanceForm from '../ci/modules/CreateInstanceForm'
import JsonEditor from '../../components/JsonEditor/jsonEditor.vue'
import BatchDownload from '../../components/batchDownload/batchDownload.vue'
import PasswordField from '../../components/passwordField/index.vue'
import PreferenceSearch from '../../components/preferenceSearch/preferenceSearch.vue'
import CMDBGrant from '../../components/cmdbGrant'
import GrantModal from '../../components/cmdbGrant/grantModal.vue'
import { ops_move_icon as OpsMoveIcon } from '@/core/icons'
import { getAttrPassword } from '../../api/CITypeAttr'
import ReadPermissionsModal from './modules/ReadPermissionsModal.vue'
import RevokeModal from '../../components/cmdbGrant/revokeModal.vue'
import CITable from '@/modules/cmdb/components/ciTable/index.vue'
export default {
name: 'RelationViews',
@@ -464,13 +283,11 @@ export default {
EditAttrsPopover,
CiDetailDrawer,
CreateInstanceForm,
JsonEditor,
BatchDownload,
PasswordField,
PreferenceSearch,
OpsMoveIcon,
ReadPermissionsModal,
RevokeModal,
CITable
},
data() {
return {
@@ -635,7 +452,7 @@ export default {
refreshTable() {
this.selectedRowKeys = []
this.sortByTable = undefined
const xTable = this.$refs.xTable
const xTable = this.$refs.xTable.getVxetableRef()
if (xTable) {
xTable.clearCheckboxRow()
xTable.clearCheckboxReserve()
@@ -815,8 +632,8 @@ export default {
},
changeCIType(typeId) {
this.$refs.xTable.clearCheckboxRow()
this.$refs.xTable.clearCheckboxReserve()
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
this.$refs.search.reset()
this.selectedRowKeys = []
this.currentTypeId = [typeId]
@@ -983,8 +800,8 @@ export default {
if (keys) {
const _tempKeys = keys.split('@^@').filter((item) => item !== '')
if (_tempKeys.length === this.levels.length) {
this.$refs.xTable.clearCheckboxRow()
this.$refs.xTable.clearCheckboxReserve()
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
this.selectedRowKeys = []
}
this.treeKeys = _tempKeys
@@ -1073,7 +890,10 @@ export default {
this.currentView = `${this.viewId}`
this.typeId = this.levels[0][0]
this.viewOption = this.relationViews.views[this.viewName].option ?? {}
this.refreshTable()
this.$nextTick(() => {
this.refreshTable()
})
}
})
},
@@ -1097,7 +917,7 @@ export default {
}
})
this.$nextTick(() => {
this.$refs.xTable.refreshColumn()
this.$refs.xTable.getVxetableRef().refreshColumn()
})
},
calculateParamsFromTreeKey(treeKey, menuKey) {
@@ -1167,23 +987,8 @@ export default {
}
}
},
onSelectChange({ records, reserves }) {
this.selectedRowKeys = [...records, ...reserves]
},
checkboxRangeStart(e) {
const xTable = this.$refs.xTable
const lastSelected = xTable.getCheckboxRecords()
const selectedReserve = xTable.getCheckboxReserveRecords()
this.lastSelected = [...lastSelected, ...selectedReserve]
},
checkboxRangeChange(e) {
const xTable = this.$refs.xTable
xTable.setCheckboxRow(this.lastSelected, true)
},
checkboxRangeEnd(e) {
const xTable = this.$refs.xTable
this.lastSelected = []
this.selectedRowKeys = [...xTable.getCheckboxRecords(), ...xTable.getCheckboxReserveRecords()]
onSelectChange(records) {
this.selectedRowKeys = records
},
batchDeleteCIRelation() {
const currentShowType = this.showTypes.find((item) => item.id === Number(this.currentTypeId[0]))
@@ -1214,8 +1019,8 @@ export default {
[first_ci_id],
ancestor_ids
).then((res) => {
that.$refs.xTable.clearCheckboxRow()
that.$refs.xTable.clearCheckboxReserve()
that.$refs.xTable.getVxetableRef().clearCheckboxRow()
that.$refs.xTable.getVxetableRef().clearCheckboxReserve()
that.selectedRowKeys = []
that.loadData({ parameter: {}, refreshType: 'refreshNumber' })
})
@@ -1273,7 +1078,7 @@ export default {
},
columnDrop() {
this.$nextTick(() => {
const xTable = this.$refs.xTable
const xTable = this.$refs.xTable.getVxetableRef()
this.sortable = Sortable.create(
xTable.$el.querySelector('.body--wrapper>.vxe-table--header .vxe-header--row'),
{
@@ -1305,49 +1110,13 @@ export default {
)
})
},
getChoiceValueStyle(col, colValue) {
const _find = col.filters.find((item) => String(item[0]) === String(colValue))
if (_find) {
return _find[1]?.style || {}
}
return {}
},
getChoiceValueIcon(col, colValue) {
const _find = col.filters.find((item) => String(item[0]) === String(colValue))
if (_find) {
return _find[1]?.icon || {}
}
return {}
},
getCellStyle({ row, rowIndex, $rowIndex, column, columnIndex, $columnIndex }) {
const { property } = column
const _find = this.preferenceAttrList.find((attr) => attr.name === property)
if (
_find &&
_find.option &&
_find.option.fontOptions &&
row[`${property}`] !== undefined &&
row[`${property}`] !== null
) {
return { ..._find.option.fontOptions }
}
},
refreshAfterEditAttrs() {
this.loadColumns()
},
getColumnsEditRender(col) {
const _editRender = {
...col.editRender,
}
if (col.value_type === '6') {
_editRender.events = { focus: this.handleFocusJson }
}
return _editRender
},
handleEditActived() {
const passwordCol = this.columns.filter((col) => col.is_password)
this.$nextTick(() => {
const editRecord = this.$refs.xTable.getEditRecord()
const editRecord = this.$refs.xTable.getVxetableRef().getEditRecord()
const { row, column } = editRecord
if (passwordCol.length && this.lastEditCiId !== row._id) {
this.$nextTick(async () => {
@@ -1358,10 +1127,10 @@ export default {
})
}
this.isContinueCloseEdit = false
await this.$refs.xTable.clearEdit()
await this.$refs.xTable.getVxetableRef().clearEdit()
this.isContinueCloseEdit = true
this.$nextTick(() => {
this.$refs.xTable.setEditCell(row, column.field)
this.$refs.xTable.getVxetableRef().setEditCell(row, column.field)
})
})
}
@@ -1372,7 +1141,7 @@ export default {
if (!this.isContinueCloseEdit) {
return
}
const $table = this.$refs['xTable']
const $table = this.$refs['xTable'].getVxetableRef()
const data = {}
this.columns.forEach((item) => {
if (
@@ -1490,7 +1259,7 @@ export default {
},
async openBatchDownload() {
this.$refs.batchDownload.open({
preferenceAttrList: this.preferenceAttrList,
preferenceAttrList: this.preferenceAttrList.filter((attr) => !attr?.is_reference),
ciTypeName: this.$route.meta.name,
})
},
@@ -1501,10 +1270,10 @@ export default {
if (_find && _find.value_type === '6') jsonAttrList.push(key)
})
const data = _.cloneDeep([
...this.$refs.xTable.getCheckboxReserveRecords(),
...this.$refs.xTable.getCheckboxRecords(true),
...this.$refs.xTable.getVxetableRef().getCheckboxReserveRecords(),
...this.$refs.xTable.getVxetableRef().getCheckboxRecords(true),
])
this.$refs.xTable.exportData({
this.$refs.xTable.getVxetableRef().exportData({
filename,
type,
columnFilterMethod({ column }) {
@@ -1518,8 +1287,8 @@ export default {
],
})
this.selectedRowKeys = []
this.$refs.xTable.clearCheckboxRow()
this.$refs.xTable.clearCheckboxReserve()
this.$refs.xTable.getVxetableRef().clearCheckboxRow()
this.$refs.xTable.getVxetableRef().clearCheckboxReserve()
},
batchDelete() {
const that = this
@@ -1543,25 +1312,13 @@ export default {
.finally(() => {
that.loading = false
that.selectedRowKeys = []
that.$refs.xTable.clearCheckboxRow()
that.$refs.xTable.clearCheckboxReserve()
that.$refs.xTable.getVxetableRef().clearCheckboxRow()
that.$refs.xTable.getVxetableRef().clearCheckboxReserve()
that.loadData({ parameter: {}, refreshType: 'refreshNumber' })
})
},
})
},
handleFocusJson({ column, row }) {
this.$refs.jsonEditor.open(column, row)
},
jsonEditorOk(row, column, jsonData) {
// 后端写数据有快慢不拉接口直接修改table的数据
this.instanceList.forEach((item) => {
if (item._id === row._id) {
item[column.property] = JSON.stringify(jsonData)
}
})
this.$refs.xTable.refreshColumn()
},
relationViewRefreshNumber() {
this.loadData({ parameter: {}, refreshType: 'refreshNumber' })
},
@@ -1585,7 +1342,9 @@ export default {
})
},
setPreferenceSearchCurrent(id = null) {
this.$refs.preferenceSearch.currentPreferenceSearch = id
this.$nextTick(() => {
this.$refs.preferenceSearch.currentPreferenceSearch = id
})
},
copyExpression() {
const expression = this.$refs['search'].expression || ''
@@ -1846,8 +1605,8 @@ export default {
return array
},
getRowSeq(row) {
return this.$refs.xTable.getRowSeq(row)
openDetail(id, activeTabKey, ciDetailRelationKey) {
this.$refs.detail.create(id, activeTabKey, ciDetailRelationKey)
}
},
}
@@ -1916,36 +1675,6 @@ export default {
background-color: #fff;
padding: 20px;
border-radius: @border-radius-box;
.checkbox-hover-table {
.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;
}
}
}
}
}
}
}
</style>

View File

@@ -1,250 +1,276 @@
<template>
<a-modal
v-model="visible"
width="90%"
:closable="false"
:centered="true"
:maskClosable="false"
:destroyOnClose="true"
@cancel="handleClose"
@ok="handleOk"
>
<div :style="{ width: '100%' }" id="add-table-modal">
<a-spin :spinning="loading">
<SearchForm
ref="searchForm"
:typeId="addTypeId"
:preferenceAttrList="preferenceAttrList"
@refresh="handleSearch"
>
<a-button
@click="
() => {
$refs.createInstanceForm.handleOpen(true, 'create')
}
"
slot="extraContent"
type="primary"
size="small"
>新增</a-button
>
</SearchForm>
<vxe-table
ref="xTable"
row-id="_id"
:data="tableData"
:height="tableHeight"
highlight-hover-row
:checkbox-config="{ reserve: true }"
@checkbox-change="onSelectChange"
@checkbox-all="onSelectChange"
show-overflow="tooltip"
show-header-overflow="tooltip"
:scroll-y="{ enabled: true, gt: 50 }"
:scroll-x="{ enabled: true, gt: 0 }"
class="ops-stripe-table"
>
<vxe-column align="center" type="checkbox" width="60" fixed="left"></vxe-column>
<vxe-table-column
v-for="col in columns"
:key="col.field"
:title="col.title"
:field="col.field"
:width="col.width"
:sortable="col.sortable"
>
<template #default="{row}" v-if="col.value_type === '6'">
<span v-if="col.value_type === '6' && row[col.field]">{{ JSON.stringify(row[col.field]) }}</span>
</template>
</vxe-table-column>
</vxe-table>
<a-pagination
v-model="currentPage"
size="small"
:total="totalNumber"
show-quick-jumper
:page-size="50"
:show-total="
(total, range) =>
$t('pagination.total', {
range0: range[0],
range1: range[1],
total,
})
"
:style="{ textAlign: 'right', marginTop: '10px' }"
@change="handleChangePage"
/>
</a-spin>
</div>
<CreateInstanceForm
ref="createInstanceForm"
:typeIdFromRelation="addTypeId"
@reload="
() => {
currentPage = 1
getTableData(true)
}
"
/>
</a-modal>
</template>
<script>
import { searchCI } from '@/modules/cmdb/api/ci'
import { getSubscribeAttributes } from '@/modules/cmdb/api/preference'
import { batchUpdateCIRelationChildren, batchUpdateCIRelationParents } from '@/modules/cmdb/api/CIRelation'
import { getCITableColumns } from '../../../utils/helper'
import SearchForm from '../../../components/searchForm/SearchForm.vue'
import CreateInstanceForm from '../../ci/modules/CreateInstanceForm.vue'
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
export default {
name: 'AddTableModal',
components: { SearchForm, CreateInstanceForm },
data() {
return {
visible: false,
currentPage: 1,
totalNumber: 0,
tableData: [],
columns: [],
ciObj: {},
ciId: null,
addTypeId: null,
loading: false,
expression: '',
isFocusExpression: false,
type: 'children',
preferenceAttrList: [],
ancestor_ids: undefined,
attrList1: [],
}
},
computed: {
tableHeight() {
return this.$store.state.windowHeight - 250
},
placeholder() {
return this.isFocusExpression ? this.$t('cmdb.serviceTreetips1') : this.$t('cmdb.serviceTreetips2')
},
width() {
return this.isFocusExpression ? '500px' : '100px'
},
},
provide() {
return {
attrList: () => {
return this.attrList
},
}
},
watch: {},
methods: {
async openModal(ciObj, ciId, addTypeId, type, ancestor_ids = undefined) {
console.log(ciObj, ciId, addTypeId, type)
this.visible = true
this.ciObj = ciObj
this.ciId = ciId
this.addTypeId = addTypeId
this.type = type
this.ancestor_ids = ancestor_ids
await getSubscribeAttributes(addTypeId).then((res) => {
this.preferenceAttrList = res.attributes // 已经订阅的全部列
})
getCITypeAttributesById(addTypeId).then((res) => {
this.attrList = res.attributes
})
this.getTableData(true)
},
async getTableData(isInit) {
if (this.addTypeId) {
await this.fetchData(isInit)
}
},
async fetchData(isInit) {
this.loading = true
// if (isInit) {
// const subscribed = await getSubscribeAttributes(this.addTypeId)
// this.preferenceAttrList = subscribed.attributes // 已经订阅的全部列
// }
let sort, fuzzySearch, expression, exp
if (!isInit) {
fuzzySearch = this.$refs['searchForm'].fuzzySearch
expression = this.$refs['searchForm'].expression || ''
const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
exp = expression.match(regQ) ? expression.match(regQ)[0] : null
}
await searchCI({
q: `_type:${this.addTypeId}${exp ? `,${exp}` : ''}${fuzzySearch ? `,*${fuzzySearch}*` : ''}`,
count: 50,
page: this.currentPage,
sort,
})
.then((res) => {
this.tableData = res.result
this.totalNumber = res.numfound
this.columns = this.getColumns(res.result, this.preferenceAttrList)
this.$nextTick(() => {
const _table = this.$refs.xTable
if (_table) {
_table.refreshColumn()
}
this.loading = false
})
})
.catch(() => {
this.loading = false
})
},
getColumns(data, attrList) {
const modalDom = document.getElementById('add-table-modal')
if (modalDom) {
const width = modalDom.clientWidth - 50
return getCITableColumns(data, attrList, width)
}
return []
},
onSelectChange() {},
handleClose() {
this.$refs.xTable.clearCheckboxRow()
this.currentPage = 1
this.expression = ''
this.isFocusExpression = false
this.visible = false
},
async handleOk() {
const selectRecordsCurrent = this.$refs.xTable.getCheckboxRecords()
const selectRecordsReserved = this.$refs.xTable.getCheckboxReserveRecords()
const ciIds = [...selectRecordsCurrent, ...selectRecordsReserved].map((record) => record._id)
if (ciIds.length) {
if (this.type === 'children') {
await batchUpdateCIRelationChildren(ciIds, [this.ciId], this.ancestor_ids)
} else {
await batchUpdateCIRelationParents(ciIds, [this.ciId])
}
setTimeout(() => {
this.$message.success(this.$t('addSuccess'))
this.handleClose()
this.$emit('reload')
}, 500)
} else {
this.handleClose()
this.$emit('reload')
}
},
handleSearch() {
this.currentPage = 1
this.fetchData()
},
handleChangePage(page, pageSize) {
this.currentPage = page
this.fetchData()
},
},
}
</script>
<style lang="less" scoped></style>
<template>
<a-modal
v-model="visible"
width="90%"
:closable="false"
:centered="true"
:maskClosable="false"
:destroyOnClose="true"
@cancel="handleClose"
@ok="handleOk"
>
<div :style="{ width: '100%' }" id="add-table-modal">
<a-spin :spinning="loading">
<SearchForm
ref="searchForm"
:typeId="addTypeId"
:preferenceAttrList="preferenceAttrList"
@refresh="handleSearch"
>
<a-button
@click="
() => {
$refs.createInstanceForm.handleOpen(true, 'create')
}
"
slot="extraContent"
type="primary"
size="small"
>新增</a-button
>
</SearchForm>
<vxe-table
ref="xTable"
row-id="_id"
:data="tableData"
:height="tableHeight"
highlight-hover-row
:checkbox-config="{ reserve: true }"
@checkbox-change="onSelectChange"
@checkbox-all="onSelectChange"
show-overflow="tooltip"
show-header-overflow="tooltip"
:scroll-y="{ enabled: true, gt: 50 }"
:scroll-x="{ enabled: true, gt: 0 }"
class="ops-stripe-table"
>
<vxe-column align="center" type="checkbox" width="60" fixed="left"></vxe-column>
<vxe-table-column
v-for="col in columns"
:key="col.field"
:title="col.title"
:field="col.field"
:width="col.width"
:sortable="col.sortable"
>
<template v-if="col.is_reference" #default="{row}">
<a
v-for="(id) in (col.is_list ? row[col.field] : [row[col.field]])"
:key="id"
:href="`/cmdb/cidetail/${col.reference_type_id}/${id}`"
target="_blank"
>
{{ id }}
</a>
</template>
<template #default="{row}" v-else-if="col.is_choice">
<span
v-for="value in (col.is_list ? row[col.field] : [row[col.field]])"
:key="value"
>
{{ getChoiceValueLabel(col, value) || value }}
</span>
</template>
<template #default="{row}" v-else-if="col.value_type == '6'">
<span v-if="col.value_type == '6' && row[col.field]">{{ JSON.stringify(row[col.field]) }}</span>
</template>
</vxe-table-column>
</vxe-table>
<a-pagination
v-model="currentPage"
size="small"
:total="totalNumber"
show-quick-jumper
:page-size="50"
:show-total="
(total, range) =>
$t('pagination.total', {
range0: range[0],
range1: range[1],
total,
})
"
:style="{ textAlign: 'right', marginTop: '10px' }"
@change="handleChangePage"
/>
</a-spin>
</div>
<CreateInstanceForm
ref="createInstanceForm"
:typeIdFromRelation="addTypeId"
@reload="
() => {
currentPage = 1
getTableData(true)
}
"
/>
</a-modal>
</template>
<script>
import { searchCI } from '@/modules/cmdb/api/ci'
import { getSubscribeAttributes } from '@/modules/cmdb/api/preference'
import { batchUpdateCIRelationChildren, batchUpdateCIRelationParents } from '@/modules/cmdb/api/CIRelation'
import { getCITableColumns } from '../../../utils/helper'
import SearchForm from '../../../components/searchForm/SearchForm.vue'
import CreateInstanceForm from '../../ci/modules/CreateInstanceForm.vue'
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
export default {
name: 'AddTableModal',
components: { SearchForm, CreateInstanceForm },
data() {
return {
visible: false,
currentPage: 1,
totalNumber: 0,
tableData: [],
columns: [],
ciObj: {},
ciId: null,
addTypeId: null,
loading: false,
expression: '',
isFocusExpression: false,
type: 'children',
preferenceAttrList: [],
ancestor_ids: undefined,
attrList1: [],
}
},
computed: {
tableHeight() {
return this.$store.state.windowHeight - 250
},
placeholder() {
return this.isFocusExpression ? this.$t('cmdb.serviceTreetips1') : this.$t('cmdb.serviceTreetips2')
},
width() {
return this.isFocusExpression ? '500px' : '100px'
},
},
provide() {
return {
attrList: () => {
return this.attrList
},
}
},
watch: {},
methods: {
async openModal(ciObj, ciId, addTypeId, type, ancestor_ids = undefined) {
console.log(ciObj, ciId, addTypeId, type)
this.visible = true
this.ciObj = ciObj
this.ciId = ciId
this.addTypeId = addTypeId
this.type = type
this.ancestor_ids = ancestor_ids
await getSubscribeAttributes(addTypeId).then((res) => {
this.preferenceAttrList = res.attributes // 已经订阅的全部列
})
getCITypeAttributesById(addTypeId).then((res) => {
this.attrList = res.attributes
})
this.getTableData(true)
},
async getTableData(isInit) {
if (this.addTypeId) {
await this.fetchData(isInit)
}
},
async fetchData(isInit) {
this.loading = true
// if (isInit) {
// const subscribed = await getSubscribeAttributes(this.addTypeId)
// this.preferenceAttrList = subscribed.attributes // 已经订阅的全部列
// }
let sort, fuzzySearch, expression, exp
if (!isInit) {
fuzzySearch = this.$refs['searchForm'].fuzzySearch
expression = this.$refs['searchForm'].expression || ''
const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
exp = expression.match(regQ) ? expression.match(regQ)[0] : null
}
await searchCI({
q: `_type:${this.addTypeId}${exp ? `,${exp}` : ''}${fuzzySearch ? `,*${fuzzySearch}*` : ''}`,
count: 50,
page: this.currentPage,
sort,
})
.then((res) => {
this.tableData = res.result
this.totalNumber = res.numfound
this.columns = this.getColumns(res.result, this.preferenceAttrList)
this.$nextTick(() => {
const _table = this.$refs.xTable
if (_table) {
_table.refreshColumn()
}
this.loading = false
})
})
.catch(() => {
this.loading = false
})
},
getColumns(data, attrList) {
const modalDom = document.getElementById('add-table-modal')
if (modalDom) {
const width = modalDom.clientWidth - 50
return getCITableColumns(data, attrList, width)
}
return []
},
onSelectChange() {},
handleClose() {
this.$refs.xTable.clearCheckboxRow()
this.currentPage = 1
this.expression = ''
this.isFocusExpression = false
this.visible = false
},
async handleOk() {
const selectRecordsCurrent = this.$refs.xTable.getCheckboxRecords()
const selectRecordsReserved = this.$refs.xTable.getCheckboxReserveRecords()
const ciIds = [...selectRecordsCurrent, ...selectRecordsReserved].map((record) => record._id)
if (ciIds.length) {
if (this.type === 'children') {
await batchUpdateCIRelationChildren(ciIds, [this.ciId], this.ancestor_ids)
} else {
await batchUpdateCIRelationParents(ciIds, [this.ciId])
}
setTimeout(() => {
this.$message.success(this.$t('addSuccess'))
this.handleClose()
this.$emit('reload')
}, 500)
} else {
this.handleClose()
this.$emit('reload')
}
},
handleSearch() {
this.currentPage = 1
this.fetchData()
},
handleChangePage(page, pageSize) {
this.currentPage = page
this.fetchData()
},
getChoiceValueLabel(col, colValue) {
const _find = col.filters.find((item) => String(item[0]) === String(colValue))
if (_find) {
return _find[1]?.label || ''
}
return ''
},
},
}
</script>
<style lang="less" scoped></style>

View File

@@ -101,8 +101,18 @@
:minWidth="100"
:cell-type="col.value_type === '2' ? 'string' : 'auto'"
>
<template v-if="col.value_type === '6' || col.is_link || col.is_password || col.is_choice" #default="{row}">
<span v-if="col.value_type === '6' && row[col.field]">{{ JSON.stringify(row[col.field]) }}</span>
<template v-if="col.value_type === '6' || col.is_link || col.is_password || col.is_choice || col.is_reference" #default="{row}">
<template v-if="col.is_reference && row[col.field]" >
<a
v-for="(ciId) in (col.is_list ? row[col.field] : [row[col.field]])"
:key="ciId"
:href="`/cmdb/cidetail/${col.reference_type_id}/${ciId}`"
target="_blank"
>
{{ getReferenceAttrValue(ciId, col) }}
</a>
</template>
<span v-else-if="col.value_type === '6' && row[col.field]">{{ JSON.stringify(row[col.field]) }}</span>
<template v-else-if="col.is_link && row[col.field]">
<a
v-for="(item, linkIndex) in (col.is_list ? row[col.field] : [row[col.field]])"
@@ -114,7 +124,7 @@
"
target="_blank"
>
{{ item }}
{{ getChoiceValueLabel(col, item) || item }}
</a>
</template>
<PasswordField
@@ -133,11 +143,13 @@
margin: '2px',
...getChoiceValueStyle(col, value),
}"
><ops-icon
:style="{ color: getChoiceValueIcon(col, value).color }"
:type="getChoiceValueIcon(col, value).name"
/>{{ value }}</span
>
<ops-icon
:style="{ color: getChoiceValueIcon(col, value).color }"
:type="getChoiceValueIcon(col, value).name"
/>
{{ getChoiceValueLabel(col, value) || value }}
</span>
</template>
<span
v-else
@@ -152,8 +164,8 @@
:style="{ color: getChoiceValueIcon(col, row[col.field]).color }"
:type="getChoiceValueIcon(col, row[col.field]).name"
/>
{{ row[col.field] }}</span
>
{{ getChoiceValueLabel(col, row[col.field]) || row[col.field] }}
</span>
</template>
</template>
</vxe-column>
@@ -254,6 +266,8 @@ export default {
sortByTable: undefined,
loading: false,
columnsGroup: [],
referenceShowAttrNameMap: {},
referenceCIIdMap: {},
}
},
computed: {
@@ -433,12 +447,99 @@ export default {
await Promise.all(promises1).then(() => {
this.columnsGroup = [..._commonColumnsGroup, ..._columnsGroup]
this.instanceList = res['result']
this.handlePerference()
})
})
.finally(() => {
this.loading = false
})
},
handlePerference() {
let needRequiredCIType = []
this.columnsGroup.forEach((group) => {
group.children.forEach((col) => {
if (col?.is_reference && col?.reference_type_id) {
needRequiredCIType.push(col)
}
})
})
needRequiredCIType = _.uniq(needRequiredCIType)
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((row) => {
needRequiredCIType.forEach((col) => {
const ids = Array.isArray(row[col.field]) ? row[col.field] : row[col.field] ? [row[col.field]] : []
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
},
getReferenceAttrValue(id, col) {
const ci = this?.referenceCIIdMap?.[col?.reference_type_id]?.[id]
if (!ci) {
return id
}
const attrName = this.referenceShowAttrNameMap?.[col.reference_type_id]
return ci?.[attrName] || id
},
getColumns(data, attrList) {
const width = document.getElementById('resource_search').clientWidth - 50
return getCITableColumns(data, attrList, width).map((item) => {
@@ -483,12 +584,29 @@ export default {
}
return {}
},
getChoiceValueLabel(col, colValue) {
const _find = col?.filters?.find((item) => String(item[0]) === String(colValue))
if (_find) {
return _find[1]?.label || ''
}
return ''
},
handleExport() {
const preferenceAttrList = [
{ id: `ci_type_alias`, value: 'ci_type_alias', label: this.$t('cmdb.ciType.ciType') },
...this.columnsGroup,
]
preferenceAttrList.forEach((attr) => {
if (Array.isArray(attr?.children) && attr?.children?.length) {
attr.children = attr.filter((child) => {
return !child?.is_reference
})
}
})
this.$refs.batchDownload.open({
preferenceAttrList: [
{ id: `ci_type_alias`, value: 'ci_type_alias', label: this.$t('cmdb.ciType.ciType') },
...this.columnsGroup,
],
preferenceAttrList,
})
},
batchDownload({ filename, type, checkedKeys }) {
@@ -512,6 +630,7 @@ export default {
return { ...item }
}),
],
original: true,
download: false,
})
this.selectedRowKeys = []

View File

@@ -1185,7 +1185,23 @@ export default {
if (!res.result.length) {
this.handleNullNodeTips(this.$t('cmdb.topo.noInstancePerm'))
} else {
this.currentNodeValues = res.result[0]
const currentNodeValues = res.result[0]
Object.keys(currentNodeValues).forEach((key) => {
const attr = this.currentNodeAttributes.find((attr) => attr.name === key)
if (attr?.choice_value?.length) {
if (Array.isArray(currentNodeValues[key])) {
currentNodeValues[key] = currentNodeValues[key].map((value) => {
const choice = attr.choice_value.find((choice) => value === choice?.[0])
return choice?.[1]?.label || value
})
} else {
const choice = attr.choice_value.find((choice) => currentNodeValues[key] === choice?.[0])
currentNodeValues[key] = choice?.[1]?.label || currentNodeValues[key]
}
}
})
this.currentNodeValues = currentNodeValues
}
}).catch(error => {
this.handleNullNodeTips(((error.response || {}).data || {}).message)

View File

@@ -144,209 +144,25 @@
</template>
</div>
</SearchForm>
<ops-table
:id="`cmdb-tree-${typeId}`"
border
<CITable
ref="xTable"
size="small"
keep-source
:id="`cmdb-tree-${typeId}`"
:loading="loading"
:attrList="currentAttrList"
:columns="columns"
:passwordValue="passwordValue"
:data="instanceList"
highlight-hover-row
show-overflow
show-header-overflow
row-id="_id"
resizable
:row-key="true"
:column-key="true"
:sort-config="{ remote: true, trigger: 'cell' }"
@sort-change="handleSortCol"
:cell-style="getCellStyle"
:scroll-y="{ enabled: true, gt: 20 }"
:scroll-x="{ enabled: true, gt: 0 }"
:height="`${windowHeight - 240}px`"
@checkbox-change="onSelectChange"
@checkbox-all="onSelectChange"
@checkbox-range-end="onSelectRangeEnd"
:checkbox-config="{ reserve: true, highlight: true, range: true }"
:loadingTip="loadTip"
@onSelectChange="onSelectChange"
@edit-closed="handleEditClose"
@edit-actived="handleEditActived"
:edit-config="{ trigger: 'dblclick', mode: 'row', showIcon: false }"
class="ops-unstripe-table checkbox-hover-table"
:custom-config="{ storage: true }"
>
<vxe-column align="center" type="checkbox" width="60" :fixed="isCheckboxFixed ? 'left' : ''">
<template #default="{row}">
{{ getRowSeq(row) }}
</template>
</vxe-column>
<vxe-table-column
v-for="(col, index) in columns"
:key="`${col.field}_${index}`"
:title="col.title"
:field="col.field"
:width="col.width"
:sortable="col.sortable"
:edit-render="getColumnsEditRender(col)"
:cell-type="col.value_type === '2' ? 'string' : 'auto'"
:fixed="col.is_fixed ? 'left' : ''"
>
<template #header>
<span class="vxe-handle">
<OpsMoveIcon
style="width: 17px; height: 17px; display: none; position: absolute; left: -3px; top: 12px"
/>
{{ col.title }}</span
>
</template>
<template v-if="col.is_choice || col.is_password" #edit="{ row }">
<vxe-input v-if="col.is_password" v-model="passwordValue[col.field]" />
<a-select
:getPopupContainer="(trigger) => trigger.parentElement"
:style="{ width: '100%', height: '32px' }"
v-model="row[col.field]"
:placeholder="$t('placeholder2')"
v-if="col.is_choice"
:showArrow="false"
:mode="col.is_list ? 'multiple' : 'default'"
class="ci-table-edit-select"
allowClear
>
<a-select-option
:value="choice[0]"
:key="'edit_' + col.field + idx"
v-for="(choice, idx) in col.filters"
>
<span
:style="{ ...(choice[1] ? choice[1].style : {}), display: 'inline-flex', alignItems: 'center' }"
>
<template v-if="choice[1] && choice[1].icon && choice[1].icon.name">
<img
v-if="choice[1].icon.id && choice[1].icon.url"
:src="`/api/common-setting/v1/file/${choice[1].icon.url}`"
:style="{ maxHeight: '13px', maxWidth: '13px', marginRight: '5px' }"
/>
<ops-icon
v-else
:style="{ color: choice[1].icon.color, marginRight: '5px' }"
:type="choice[1].icon.name"
/>
</template>
{{ choice[0] }}
</span>
</a-select-option>
</a-select>
</template>
<template
v-if="col.value_type === '6' || col.is_link || col.is_password || col.is_choice"
#default="{row}"
>
<span v-if="col.value_type === '6' && row[col.field]">{{ row[col.field] }}</span>
<template v-else-if="col.is_link && row[col.field]">
<a
v-for="(item, linkIndex) in (col.is_list ? row[col.field] : [row[col.field]])"
:key="linkIndex"
:href="
item.startsWith('http') || item.startsWith('https')
? `${item}`
: `http://${item}`
"
target="_blank"
>
{{ item }}
</a>
</template>
<PasswordField
v-else-if="col.is_password && row[col.field]"
:ci_id="row._id"
:attr_id="col.attr_id"
></PasswordField>
<template v-else-if="col.is_choice">
<template v-if="col.is_list">
<span
v-for="value in row[col.field]"
:key="value"
:style="{
borderRadius: '4px',
padding: '1px 5px',
margin: '2px',
...getChoiceValueStyle(col, value),
display: 'inline-flex',
alignItems: 'center',
}"
>
<img
v-if="getChoiceValueIcon(col, value).id && getChoiceValueIcon(col, value).url"
:src="`/api/common-setting/v1/file/${getChoiceValueIcon(col, value).url}`"
:style="{ maxHeight: '13px', maxWidth: '13px', marginRight: '5px' }"
/>
<ops-icon
v-else
:style="{ color: getChoiceValueIcon(col, value).color, marginRight: '5px' }"
:type="getChoiceValueIcon(col, value).name"
/>{{ value }}</span
>
</template>
<span
v-else-if="row[col.field]"
:style="{
borderRadius: '4px',
padding: '1px 5px',
margin: '2px 0',
...getChoiceValueStyle(col, row[col.field]),
display: 'inline-flex',
alignItems: 'center',
}"
>
<img
v-if="getChoiceValueIcon(col, row[col.field]).id && getChoiceValueIcon(col, row[col.field]).url"
:src="`/api/common-setting/v1/file/${getChoiceValueIcon(col, row[col.field]).url}`"
:style="{ maxHeight: '13px', maxWidth: '13px', marginRight: '5px' }"
/>
<ops-icon
v-else
:style="{ color: getChoiceValueIcon(col, row[col.field]).color, marginRight: '5px' }"
:type="getChoiceValueIcon(col, row[col.field]).name"
/>{{ row[col.field] }}</span
>
</template>
</template>
</vxe-table-column>
<vxe-table-column align="left" field="operate" fixed="right" width="120">
<template #header>
<span>{{ $t('operation') }}</span>
</template>
<template #default="{ row }">
<a-space>
<a @click="$refs.detail.create(row.ci_id || row._id)">
<a-icon type="unordered-list" />
</a>
<a-tooltip :title="$t('cmdb.ci.addRelation')">
<a @click="$refs.detail.create(row.ci_id || row._id, 'tab_2', '2')">
<a-icon type="retweet" />
</a>
</a-tooltip>
<template>
<a-tooltip :title="$t('cmdb.ciType.deleteInstance')">
<a @click="deleteCI(row)" :style="{ color: 'red' }">
<a-icon type="delete" />
</a>
</a-tooltip>
</template>
</a-space>
</template>
</vxe-table-column>
<template #empty>
<div v-if="loading" style="height: 200px; line-height: 200px">{{ $t('loading') }}</div>
<div v-else>
<img :style="{ width: '200px' }" :src="require('@/assets/data_empty.png')" />
<div>{{ $t('noData') }}</div>
</div>
</template>
<template #loading>
<div style="height: 200px; line-height: 200px">{{ loadTip || $t('loading') }}</div>
</template>
</ops-table>
@sort-change="handleSortCol"
@openDetail="openDetail"
@deleteCI="deleteCI"
/>
<div :style="{ textAlign: 'right', marginTop: '4px' }">
<a-pagination
:showSizeChanger="true"
@@ -398,7 +214,6 @@
@reload="sumbitFromCreateInstance"
@submit="batchUpdateFromCreateInstance"
/>
<JsonEditor ref="jsonEditor" @jsonEditorOk="jsonEditorOk" />
<BatchDownload ref="batchDownload" @batchDownload="batchDownload" />
<MetadataDrawer ref="metadataDrawer" />
</div>
@@ -420,38 +235,36 @@ import { getCITypes } from '@/modules/cmdb/api/CIType'
import { getCITableColumns } from '../../utils/helper'
import SearchForm from '../../components/searchForm/SearchForm.vue'
import SubscribeSetting from '../../components/subscribeSetting/subscribeSetting'
import PasswordField from '../../components/passwordField/index.vue'
import SplitPane from '@/components/SplitPane'
import TreeViewsNode from './modules/treeViewsNode.vue'
import EditAttrsPopover from '../ci/modules/editAttrsPopover.vue'
import CiDetailDrawer from '../ci/modules/ciDetailDrawer.vue'
import CreateInstanceForm from '../ci/modules/CreateInstanceForm'
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
import JsonEditor from '../../components/JsonEditor/jsonEditor.vue'
import BatchDownload from '../../components/batchDownload/batchDownload.vue'
import PreferenceSearch from '../../components/preferenceSearch/preferenceSearch.vue'
import MetadataDrawer from '../ci/modules/MetadataDrawer.vue'
import { intersection } from '@/utils/functions/set'
import { ops_move_icon as OpsMoveIcon } from '@/core/icons'
import { getAttrPassword } from '../../api/CITypeAttr'
import CITable from '@/modules/cmdb/components/ciTable/index.vue'
export default {
name: 'TreeViews',
components: {
SearchForm,
SubscribeSetting,
PasswordField,
SplitPane,
TreeViewsNode,
EditAttrsPopover,
CiDetailDrawer,
CreateInstanceForm,
JsonEditor,
BatchDownload,
PreferenceSearch,
MetadataDrawer,
OpsMoveIcon,
draggable,
CITable
},
data() {
return {
@@ -516,10 +329,6 @@ export default {
const _find = this.subscribeTreeViewCiTypes.find((item) => item.type_id === Number(this.typeId))
return _find?.id
},
isCheckboxFixed() {
const idx = this.columns.findIndex((item) => item.is_fixed)
return idx > -1
},
currentCiTypeName() {
const _find = this.citypes.find((item) => Number(item.id) === Number(this.typeId))
return _find?.alias || _find?.name || ''
@@ -566,10 +375,9 @@ export default {
},
methods: {
async getAttributeList() {
await getCITypeAttributesById(Number(this.typeId)).then((res) => {
this.attrList = res.attributes
this.attributes = res
})
const res = await getCITypeAttributesById(Number(this.typeId))
this.attrList = res.attributes
this.attributes = res
},
async getTreeViews() {
this.subscribeTreeViewCiTypesLoading = true
@@ -710,10 +518,18 @@ export default {
wrapTreeData(facet) {
// 切面
console.log('facet', facet)
const _treeData = Object.values(facet)[0].map((item) => {
let title = item[0]
const attr = this.attrList.find((attr) => attr.name === item[2])
if (attr?.choice_value?.length) {
const choice = attr.choice_value.find((choice) => item[0] === choice?.[0])
if (choice?.[1]?.label) {
title = choice[1].label
}
}
return {
title: item[0],
title: title,
childLength: item[1],
key: this.treeKeys.join(this.keySplit) + this.keySplit + item[0],
isLeaf: this.levels.length - 1 === this.treeKeys.length,
@@ -858,33 +674,6 @@ export default {
this.currentPage = 1
this.handleLoadInstance({ sortByTable })
},
getChoiceValueStyle(col, colValue) {
const _find = col.filters.find((item) => String(item[0]) === String(colValue))
if (_find) {
return _find[1]?.style || {}
}
return {}
},
getChoiceValueIcon(col, colValue) {
const _find = col.filters.find((item) => String(item[0]) === String(colValue))
if (_find) {
return _find[1]?.icon || {}
}
return {}
},
getCellStyle({ row, rowIndex, $rowIndex, column, columnIndex, $columnIndex }) {
const { property } = column
const _find = this.currentAttrList.find((attr) => attr.name === property)
if (
_find &&
_find.option &&
_find.option.fontOptions &&
row[`${property}`] !== undefined &&
row[`${property}`] !== null
) {
return { ..._find.option.fontOptions }
}
},
onNodeClick(keys, type) {
console.log(keys)
if (keys) {
@@ -921,9 +710,7 @@ export default {
},
})
},
onSelectChange(e) {
const xTable = this.$refs.xTable.getVxetableRef()
const records = [...xTable.getCheckboxRecords(), ...xTable.getCheckboxReserveRecords()]
onSelectChange(records) {
this.selectedRowKeys = records.map((i) => i.ci_id || i._id)
},
setSelectRows() {
@@ -942,9 +729,6 @@ export default {
this.$refs['xTable'].getVxetableRef().setCheckboxRow(rows, true)
}
},
onSelectRangeEnd({ records }) {
this.selectedRowKeys = records.map((i) => i.ci_id || i._id)
},
handleEditActived() {
const passwordCol = this.columns.filter((col) => col.is_password)
this.$nextTick(() => {
@@ -1023,30 +807,8 @@ export default {
}
})
},
jsonEditorOk(row, column, jsonData) {
// 后端写数据有快慢不拉接口直接修改table的数据
// this.reloadData()
this.instanceList.forEach((item) => {
if (item._id === row._id) {
item[column.property] = JSON.stringify(jsonData)
}
})
this.$refs.xTable.getVxetableRef().refreshColumn()
},
getColumnsEditRender(col) {
const _editRender = {
...col.editRender,
}
if (col.value_type === '6') {
_editRender.events = { focus: this.handleFocusJson }
}
return _editRender
},
handleFocusJson({ column, row }) {
this.$refs.jsonEditor.open(column, row)
},
async openBatchDownload() {
this.$refs.batchDownload.open({ preferenceAttrList: this.currentAttrList, ciTypeName: this.currentCiTypeName })
this.$refs.batchDownload.open({ preferenceAttrList: this.currentAttrList.filter((attr) => !attr?.is_reference), ciTypeName: this.currentCiTypeName })
},
batchDownload({ filename, type, checkedKeys }) {
console.log(filename, type)
@@ -1237,8 +999,8 @@ export default {
}
)
},
getRowSeq(row) {
return this.$refs.xTable.getVxetableRef().getRowSeq(row)
openDetail(id, activeTabKey, ciDetailRelationKey) {
this.$refs.detail.create(id, activeTabKey, ciDetailRelationKey)
}
},
}
@@ -1348,36 +1110,6 @@ export default {
overflow: auto;
width: 100%;
border-radius: @border-radius-box;
.checkbox-hover-table {
.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;
}
}
}
}
}
}
}
</style>

View File

@@ -14,7 +14,7 @@ export default {
name: 'TreeViewsNode',
props: {
title: {
type: [String, Number],
type: [String, Number, Boolean],
default: '',
},
treeKey: {

View File

@@ -41,7 +41,7 @@ services:
- redis
cmdb-api:
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.4.9
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.4.11
container_name: cmdb-api
env_file:
- .env
@@ -71,7 +71,7 @@ services:
flask cmdb-init-acl
flask init-import-user-from-acl
flask init-department
flask cmdb-patch -v 2.4.9
flask cmdb-patch -v 2.4.11
flask cmdb-counter > counter.log 2>&1
networks:
new:
@@ -84,7 +84,7 @@ services:
test: "ps aux|grep -v grep|grep -v '1 root'|grep gunicorn || exit 1"
cmdb-ui:
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.4.9
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.4.11
container_name: cmdb-ui
depends_on:
cmdb-api: