Compare commits

..

54 Commits

Author SHA1 Message Date
pycook
251b9e7fd5 chore: release v2.4.12 2024-09-03 14:18:53 +08:00
Leo Song
f3cc12f1f9 Merge pull request #610 from veops/dev_ui_240903
fix(ui): build error
2024-09-03 13:15:43 +08:00
songlh
56f03e1624 fix(ui): build error 2024-09-03 13:14:56 +08:00
Leo Song
42ad2b6dde Merge pull request #609 from veops/dev_ui_240903
feat: update resource search
2024-09-03 11:30:08 +08:00
songlh
5aba1ff257 feat: update resource search 2024-09-03 11:29:32 +08:00
pycook
417e8fe349 perf(api): resource search supports recent searches and my favorites 2024-09-02 16:56:06 +08:00
Leo Song
02235d8cc0 Merge pull request #607 from veops/dev_ui_240828
fix(ui): ci choice attr error
2024-08-28 18:52:43 +08:00
songlh
00c7a644a2 fix(ui): ci choice attr error 2024-08-28 18:52:20 +08:00
pycook
f3e8757450 fix(api): CIType templates import 2024-08-28 17:52:15 +08:00
Leo Song
f0749341ba Merge pull request #606 from veops/dev_ui_240828
feat(ui): update ui
2024-08-28 16:55:45 +08:00
songlh
89da671e46 feat(ui): update ui 2024-08-28 16:55:07 +08:00
Leo Song
0e60aae076 Merge pull request #605 from veops/dev_ui_240827
Dev UI 240827
2024-08-27 10:33:30 +08:00
songlh
4dfa97d404 fix(ui): resource search export error 2024-08-27 10:32:53 +08:00
songlh
9b778f9bc7 fix(ui): update create attr icon 2024-08-27 10:32:25 +08:00
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
112 changed files with 11315 additions and 2767 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

@@ -108,7 +108,8 @@ class AttributeManager(object):
return []
choice_values = choice_table.get_by(fl=["value", "option"], attr_id=attr_id)
return [[ValueTypeMap.serialize[value_type](choice_value['value']), choice_value['option']]
return [[ValueTypeMap.serialize[value_type](choice_value['value']), choice_value['option'] or
{"label": ValueTypeMap.serialize[value_type](choice_value['value'])}]
for choice_value in choice_values]
@staticmethod
@@ -135,6 +136,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] and i[1].get('label')}
return {}
@classmethod
def search_attributes(cls, name=None, alias=None, page=1, page_size=None):
"""
@@ -167,24 +177,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,
@@ -329,7 +383,7 @@ class CIManager(object):
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
password_dict = {}
@@ -392,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)
@@ -443,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}
@@ -475,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)
@@ -510,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)
@@ -578,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
@@ -773,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))
@@ -801,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'))
@@ -1457,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
@@ -1484,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
@@ -519,12 +524,31 @@ class CITypeAttributeManager(object):
for _type_id in parent_ids + [type_id]:
result.extend(CITypeAttributesCache.get2(_type_id))
return result
attr_ids = set()
result2 = []
for i in result:
if i[1].id not in attr_ids:
result2.append(i)
attr_ids.add(i[1].id)
return result2
@classmethod
def get_attr_names_by_type_id(cls, type_id):
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 +589,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 +870,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 +974,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 +1347,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 +1362,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 +1374,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 +1388,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:
@@ -1372,6 +1406,10 @@ class CITypeTemplateManager(object):
payload = dict(group_id=group_id_map.get(group['id'], group['id']),
type_id=type_id_map.get(ci_type['id'], ci_type['id']),
order=order)
for i in CITypeGroupItem.get_by(type_id=payload['type_id'], to_dict=False):
if i.group_id != payload['group_id']:
i.soft_delete(flush=True)
existed = CITypeGroupItem.get_by(group_id=payload['group_id'], type_id=payload['type_id'],
first=True, to_dict=False)
if existed is None:
@@ -1584,13 +1622,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 +1715,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 +1737,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
@@ -383,14 +384,22 @@ class PreferenceManager(object):
def add_search_option(**kwargs):
kwargs['uid'] = current_user.uid
existed = PreferenceSearchOption.get_by(uid=current_user.uid,
name=kwargs.get('name'),
prv_id=kwargs.get('prv_id'),
ptv_id=kwargs.get('ptv_id'),
type_id=kwargs.get('type_id'),
)
if existed:
return abort(400, ErrFormat.preference_search_option_exists)
if kwargs['name'] in ('__recent__', '__favor__'):
if kwargs['name'] == '__recent__':
for i in PreferenceSearchOption.get_by(
only_query=True, name=kwargs['name'], uid=current_user.uid).order_by(
PreferenceSearchOption.id.desc()).offset(20):
i.delete()
else:
existed = PreferenceSearchOption.get_by(uid=current_user.uid,
name=kwargs.get('name'),
prv_id=kwargs.get('prv_id'),
ptv_id=kwargs.get('ptv_id'),
type_id=kwargs.get('type_id'),
)
if existed:
return abort(400, ErrFormat.preference_search_option_exists)
return PreferenceSearchOption.create(**kwargs)

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
@@ -239,7 +252,7 @@ class AttributeValueManager(object):
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'])]
for i in handle_arg_list(value['v'])]
continue
_value = value.get('v') or []
else:

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'])

File diff suppressed because it is too large Load Diff

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=1725331691589') format('woff2'),
url('iconfont.woff?t=1725331691589') format('woff'),
url('iconfont.ttf?t=1725331691589') format('truetype');
}
.iconfont {
@@ -13,6 +13,258 @@
-moz-osx-font-smoothing: grayscale;
}
.veops-expand:before {
content: "\e9b6";
}
.caise-public_cloud:before {
content: "\e9b1";
}
.caise-system:before {
content: "\e9b2";
}
.caise-IPAM:before {
content: "\e9b3";
}
.caise-hyperV:before {
content: "\e9b4";
}
.caise-data_center2:before {
content: "\e9b5";
}
.caise-hardware:before {
content: "\e9ad";
}
.caise-computer:before {
content: "\e9ae";
}
.caise-network_devices:before {
content: "\e9af";
}
.caise-storage_device:before {
content: "\e9b0";
}
.caise-load_balancing:before {
content: "\e9ab";
}
.caise-message_queue:before {
content: "\e9ac";
}
.caise-websever:before {
content: "\e9aa";
}
.caise-middleware:before {
content: "\e9a9";
}
.caise-database:before {
content: "\e9a7";
}
.caise-business:before {
content: "\e9a8";
}
.caise-virtualization:before {
content: "\e9a6";
}
.caise-storage_pool:before {
content: "\e9a4";
}
.caise-storage_volume1:before {
content: "\e9a5";
}
.ciase-aix:before {
content: "\e9a3";
}
.caise_pool:before {
content: "\e99b";
}
.caise-ip_address:before {
content: "\e99c";
}
.caise-computer_room:before {
content: "\e99d";
}
.caise-rack:before {
content: "\e99e";
}
.caise-pc:before {
content: "\e99f";
}
.caise-bandwidth_line:before {
content: "\e9a0";
}
.caise-fiber:before {
content: "\e9a1";
}
.caise-disk_array:before {
content: "\e9a2";
}
.veops-group:before {
content: "\e99a";
}
.veops-inheritance:before {
content: "\e999";
}
.veops-department:before {
content: "\e998";
}
.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,447 @@
"css_prefix_text": "",
"description": "",
"glyphs": [
{
"icon_id": "41681675",
"name": "veops-expand",
"font_class": "veops-expand",
"unicode": "e9b6",
"unicode_decimal": 59830
},
{
"icon_id": "41672951",
"name": "公有云",
"font_class": "caise-public_cloud",
"unicode": "e9b1",
"unicode_decimal": 59825
},
{
"icon_id": "41672952",
"name": "操作系统",
"font_class": "caise-system",
"unicode": "e9b2",
"unicode_decimal": 59826
},
{
"icon_id": "41673309",
"name": "IPAM",
"font_class": "caise-IPAM",
"unicode": "e9b3",
"unicode_decimal": 59827
},
{
"icon_id": "41673312",
"name": "hyperV",
"font_class": "caise-hyperV",
"unicode": "e9b4",
"unicode_decimal": 59828
},
{
"icon_id": "41673320",
"name": "数据中心",
"font_class": "caise-data_center2",
"unicode": "e9b5",
"unicode_decimal": 59829
},
{
"icon_id": "41669141",
"name": "硬件设备",
"font_class": "caise-hardware",
"unicode": "e9ad",
"unicode_decimal": 59821
},
{
"icon_id": "41669249",
"name": "计算机",
"font_class": "caise-computer",
"unicode": "e9ae",
"unicode_decimal": 59822
},
{
"icon_id": "41669250",
"name": "网络设备",
"font_class": "caise-network_devices",
"unicode": "e9af",
"unicode_decimal": 59823
},
{
"icon_id": "41669278",
"name": "存储设备",
"font_class": "caise-storage_device",
"unicode": "e9b0",
"unicode_decimal": 59824
},
{
"icon_id": "41659452",
"name": "负载均衡",
"font_class": "caise-load_balancing",
"unicode": "e9ab",
"unicode_decimal": 59819
},
{
"icon_id": "41659446",
"name": "消息队列",
"font_class": "caise-message_queue",
"unicode": "e9ac",
"unicode_decimal": 59820
},
{
"icon_id": "41659424",
"name": "websever",
"font_class": "caise-websever",
"unicode": "e9aa",
"unicode_decimal": 59818
},
{
"icon_id": "41655608",
"name": "中间件",
"font_class": "caise-middleware",
"unicode": "e9a9",
"unicode_decimal": 59817
},
{
"icon_id": "41655599",
"name": "数据库",
"font_class": "caise-database",
"unicode": "e9a7",
"unicode_decimal": 59815
},
{
"icon_id": "41655591",
"name": "业务",
"font_class": "caise-business",
"unicode": "e9a8",
"unicode_decimal": 59816
},
{
"icon_id": "41655550",
"name": "虚拟化",
"font_class": "caise-virtualization",
"unicode": "e9a6",
"unicode_decimal": 59814
},
{
"icon_id": "41654680",
"name": "存储池",
"font_class": "caise-storage_pool",
"unicode": "e9a4",
"unicode_decimal": 59812
},
{
"icon_id": "41654676",
"name": "存储卷",
"font_class": "caise-storage_volume1",
"unicode": "e9a5",
"unicode_decimal": 59813
},
{
"icon_id": "41654608",
"name": "aix",
"font_class": "ciase-aix",
"unicode": "e9a3",
"unicode_decimal": 59811
},
{
"icon_id": "41654233",
"name": "ip池",
"font_class": "caise_pool",
"unicode": "e99b",
"unicode_decimal": 59803
},
{
"icon_id": "41654237",
"name": "ip地址",
"font_class": "caise-ip_address",
"unicode": "e99c",
"unicode_decimal": 59804
},
{
"icon_id": "41654249",
"name": "机房",
"font_class": "caise-computer_room",
"unicode": "e99d",
"unicode_decimal": 59805
},
{
"icon_id": "41654271",
"name": "机柜",
"font_class": "caise-rack",
"unicode": "e99e",
"unicode_decimal": 59806
},
{
"icon_id": "41654276",
"name": "PC",
"font_class": "caise-pc",
"unicode": "e99f",
"unicode_decimal": 59807
},
{
"icon_id": "41654305",
"name": "带宽线路",
"font_class": "caise-bandwidth_line",
"unicode": "e9a0",
"unicode_decimal": 59808
},
{
"icon_id": "41654323",
"name": "光纤交换机",
"font_class": "caise-fiber",
"unicode": "e9a1",
"unicode_decimal": 59809
},
{
"icon_id": "41654369",
"name": "磁盘阵列",
"font_class": "caise-disk_array",
"unicode": "e9a2",
"unicode_decimal": 59810
},
{
"icon_id": "41643869",
"name": "veops-group",
"font_class": "veops-group",
"unicode": "e99a",
"unicode_decimal": 59802
},
{
"icon_id": "41637123",
"name": "veops-inheritance",
"font_class": "veops-inheritance",
"unicode": "e999",
"unicode_decimal": 59801
},
{
"icon_id": "41570722",
"name": "veops-department",
"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.

View File

@@ -61,12 +61,12 @@ export default {
)
// 注册富文本自定义元素
const resume = {
type: 'attachment',
attachmentLabel: '',
attachmentValue: '',
children: [{ text: '' }], // void 元素必须有一个 children 其中只有一个空字符串重要
}
// const resume = {
// type: 'attachment',
// attachmentLabel: '',
// attachmentValue: '',
// children: [{ text: '' }], // void 元素必须有一个 children 其中只有一个空字符串重要
// }
function withAttachment(editor) {
// JS 语法

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

@@ -905,6 +905,84 @@ export const multicolorIconList = [
value: 'caise-application',
label: '应用',
list: [{
value: 'caise-disk_array',
label: '磁盘阵列'
}, {
value: 'caise-fiber',
label: '光纤交换机'
}, {
value: 'caise-bandwidth_line',
label: '带宽线路'
}, {
value: 'caise-pc',
label: 'PC'
}, {
value: 'caise-rack',
label: '机柜'
}, {
value: 'caise-computer_room',
label: '机房'
}, {
value: 'caise-ip_address',
label: 'ip地址'
}, {
value: 'caise_pool',
label: 'ip池'
}, {
value: 'ciase-aix',
label: 'aix'
}, {
value: 'caise-storage_volume1',
label: '存储卷'
}, {
value: 'caise-virtualization',
label: '虚拟化'
}, {
value: 'caise-business',
label: '业务'
}, {
value: 'caise-database',
label: '数据库'
}, {
value: 'caise-middleware',
label: '中间件'
}, {
value: 'caise-websever',
label: 'websever'
}, {
value: 'caise-message_queue',
label: '消息队列'
}, {
value: 'caise-load_balancing',
label: '负载均衡'
}, {
value: 'caise-storage_device',
label: '存储设备'
}, {
value: 'caise-network_devices',
label: '网络设备'
}, {
value: 'caise-computer',
label: '计算机'
}, {
value: 'caise-hardware',
label: '硬件设备'
}, {
value: 'caise-data_center2',
label: '数据中心'
}, {
value: 'caise-hyperV',
label: 'hyperV'
}, {
value: 'caise-IPAM',
label: 'IPAM'
}, {
value: 'caise-system',
label: '操作系统'
}, {
value: 'caise-public_cloud',
label: '公有云'
}, {
value: 'caise-data_center',
label: '数据中心'
}, {

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

@@ -31,7 +31,7 @@ export default {
text-align: center;
border-radius: 4px;
&:hover {
background: linear-gradient(0deg, rgba(0, 80, 201, 0.2) 0%, rgba(174, 207, 255, 0.06) 86.76%);
// background: linear-gradient(0deg, rgba(0, 80, 201, 0.2) 0%, rgba(174, 207, 255, 0.06) 86.76%);
color: @layout-header-font-selected-color;
}
}

View File

@@ -5,7 +5,6 @@
v-for="route in defaultShowRoutes"
:key="route.name"
@click="() => handleClick(route)"
:title="$t(route.meta.title)"
>
{{ route.meta.title }}
</span>
@@ -119,7 +118,9 @@ export default {
line-height: @layout-header-line-height;
display: inline-block;
}
> span:hover,
> span:hover {
background-color: #f0f2f5;
}
.top-menu-selected {
font-weight: bold;
color: @layout-header-font-selected-color;

View File

@@ -5,15 +5,30 @@
<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>
<a-popover
trigger="click"
:overlayStyle="{ width: '150px' }"
overlayClassName="lang-popover-wrap"
placement="bottomRight"
:getPopupContainer="(trigger) => trigger.parentNode"
>
<span class="locale">{{ languageList.find((lang) => lang.key === locale).title }}</span>
<div class="lang-menu" slot="content">
<a
v-for="(lang) in languageList"
:key="lang.key"
:class="['lang-menu-item', lang.key === locale ? 'lang-menu-item_active' : '']"
@click="changeLang(lang.key)"
>
{{ lang.title }}
</a>
</div>
</a-popover>
<a-popover
:overlayStyle="{ width: '130px' }"
placement="bottomRight"
overlayClassName="custom-user"
>
@@ -29,7 +44,7 @@
<span>{{ $t('topMenu.logout') }}</span>
</div>
</template>
<span class="action ant-dropdown-link user-dropdown-menu">
<span class="action ant-dropdown-link user-dropdown-menu user-info-wrap">
<a-avatar
v-if="avatar()"
class="avatar"
@@ -54,6 +69,20 @@ export default {
components: {
DocumentLink,
},
data() {
return {
languageList: [
{
title: '简中',
key: 'zh'
},
{
title: 'EN',
key: 'en'
},
]
}
},
computed: {
...mapState(['user', 'locale']),
hasBackendPermission() {
@@ -81,14 +110,9 @@ export default {
handleClick() {
this.$router.push('/setting')
},
changeLang() {
if (this.locale === 'zh') {
this.SET_LOCALE('en')
this.$i18n.locale = 'en'
} else {
this.SET_LOCALE('zh')
this.$i18n.locale = 'zh'
}
changeLang(lang) {
this.SET_LOCALE(lang)
this.$i18n.locale = lang
this.$nextTick(() => {
setDocumentTitle(`${this.$t(this.$route.meta.title)} - ${domTitle}`)
})
@@ -118,8 +142,88 @@ export default {
.locale {
cursor: pointer;
padding: 0 8px;
&:hover {
color: @primary-color;
}
}
.lang-popover-wrap {
width: 70px;
padding: 0px;
.ant-popover-inner-content {
padding: 0px;
}
}
</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;
}
&:hover {
.commen-settings-btn-text {
color: #2F54EB;
}
}
}
.lang-menu {
width: 100%;
display: flex;
flex-direction: column;
&-item {
width: 100%;
padding: 5px 10px;
cursor: pointer;
color: #4E5969;
&_active {
color: #2F54EB;
background-color: #f0f5ff;
}
&:hover {
color: #2F54EB;
}
}
}
.user-info-wrap {
.avatar {
transition: all 0.2s;
border: solid 1px transparent;
}
&:hover {
.avatar {
border-color: #2F54EB;
}
}
}
}
</style>

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: '管理员',

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 KiB

View File

@@ -0,0 +1,85 @@
<template>
<div
v-if="icon || title"
class="ci-icon"
:style="{
'--size': size + 'px'
}"
>
<template v-if="icon">
<img
v-if="icon.split('$$')[2]"
:src="`/api/common-setting/v1/file/${icon.split('$$')[3]}`"
/>
<ops-icon
v-else
:style="{
color: icon.split('$$')[1],
}"
:type="icon.split('$$')[0]"
/>
</template>
<span
class="ci-icon-letter"
v-else
>
<span>
{{ title[0].toUpperCase() }}
</span>
</span>
</div>
</template>
<script>
export default {
name: 'CIIcon',
props: {
icon: {
type: String,
default: ''
},
// 如果没有icon, 默认以title 的第一个字符
title: {
type: String,
default: ''
},
size: {
type: [String, Number],
default: '12'
}
}
}
</script>
<style lang="less" scoped>
.ci-icon {
font-size: var(--size);
width: var(--size);
height: var(--size);
display: flex;
align-items: center;
justify-content: center;
& > img {
width: var(--size);
height: var(--size);
}
&-letter {
background-color: #FFFFFF;
color: #2f54eb;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
border-radius: 2px;
box-shadow: 0px 1px 2px rgba(47, 84, 235, 0.2);
& > span {
transform-origin: center;
transform: scale(0.7);
}
}
}
</style>

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] ? choice[1].label || choice[0] : choice[0]">
<span>{{ choice[1] ? choice[1].label || choice[0] : 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.viewRelation')">
<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.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

@@ -1,212 +1,219 @@
<template>
<a-modal :width="680" :title="title" :visible="visible" @ok="handleOk" @cancel="handleCancel">
<CustomRadio
:radioList="[
{ value: 1, label: $t('cmdb.components.all') },
{ value: 2, label: $t('cmdb.components.customize'), layout: 'vertical' },
{ value: 3, label: $t('cmdb.components.none') },
]"
:value="radioValue"
@change="changeRadioValue"
>
<template slot="extra_2" v-if="radioValue === 2">
<treeselect
v-if="colType === 'read_attr'"
v-model="selectedAttr"
:multiple="true"
:clearable="true"
searchable
:options="attrGroup"
:placeholder="$t('cmdb.ciType.selectAttributes')"
value-consists-of="LEAF_PRIORITY"
:limit="10"
:limitText="(count) => `+ ${count}`"
:normalizer="
(node) => {
return {
id: node.name || -1,
label: node.alias || node.name || $t('other'),
title: node.alias || node.name || $t('other'),
children: node.attributes,
}
}
"
appendToBody
zIndex="1050"
>
</treeselect>
<a-form-model
:model="form"
:rules="rules"
v-if="colType === 'read_ci'"
:labelCol="{ span: 2 }"
:wrapperCol="{ span: 10 }"
ref="form"
>
<a-form-model-item :label="$t('name')" prop="name">
<a-input v-model="form.name" />
</a-form-model-item>
<FilterComp
ref="filterComp"
:isDropdown="false"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList"
@setExpFromFilter="setExpFromFilter"
:expression="expression"
/>
</a-form-model>
</template>
</CustomRadio>
</a-modal>
</template>
<script>
import { grantCiType, revokeCiType } from '../../api/CIType'
import { getCITypeAttributesByTypeIds } from '../../api/CITypeAttr'
import FilterComp from '@/components/CMDBFilterComp'
export default {
name: 'ReadGrantModal',
components: { FilterComp },
props: {
CITypeId: {
type: Number,
default: null,
},
},
inject: {
provide_attrGroup: {
from: 'attrGroup',
},
provide_filerPerimissions: {
from: 'filerPerimissions',
},
},
data() {
return {
visible: false,
colType: '',
row: {},
radioValue: 1,
radioStyle: {
display: 'block',
height: '30px',
lineHeight: '30px',
},
selectedAttr: [],
ruleList: [],
canSearchPreferenceAttrList: [],
expression: '',
form: {
name: '',
},
rules: {
name: [{ required: true, message: this.$t('cmdb.components.customizeFilterName') }],
},
}
},
computed: {
title() {
if (this.colType === 'read_attr') {
return this.$t('cmdb.components.attributeGrant')
}
return this.$t('cmdb.components.ciGrant')
},
attrGroup() {
return this.provide_attrGroup()
},
filerPerimissions() {
return this.provide_filerPerimissions()
},
filterKey() {
if (this.colType === 'read_attr') {
return 'attr_filter'
}
return 'ci_filter'
},
},
methods: {
async open(colType, row) {
this.visible = true
this.colType = colType
this.row = row
this.form = {
name: '',
}
if (this.colType === 'read_ci') {
await getCITypeAttributesByTypeIds({ type_ids: this.CITypeId }).then((res) => {
this.canSearchPreferenceAttrList = res.attributes.filter((item) => item.value_type !== '6')
})
}
if (this.filerPerimissions[row.rid]) {
const _tempValue = this.filerPerimissions[row.rid][this.filterKey]
if (_tempValue && _tempValue.length) {
this.radioValue = 2
if (this.colType === 'read_attr') {
this.selectedAttr = _tempValue
} else {
this.expression = `q=${_tempValue}`
this.form = {
name: this.filerPerimissions[row.rid].name || '',
}
this.$nextTick(() => {
this.$refs.filterComp.visibleChange(true)
})
}
}
}
},
async handleOk() {
if (this.radioValue === 1) {
await grantCiType(this.CITypeId, this.row.rid, {
perms: ['read'],
attr_filter: this.colType === 'read_attr' ? [] : undefined,
ci_filter: this.colType === 'read_ci' ? '' : undefined,
})
} else if (this.radioValue === 2) {
if (this.colType === 'read_ci') {
this.$refs.filterComp.handleSubmit()
}
await grantCiType(this.CITypeId, this.row.rid, {
perms: ['read'],
attr_filter: this.colType === 'read_attr' ? this.selectedAttr : undefined,
ci_filter: this.colType === 'read_ci' ? this.expression.slice(2) : undefined,
name: this.colType === 'read_ci' ? this.form.name : undefined,
})
} else {
const _tempValue = this.filerPerimissions?.[this.row.rid]?.[this.filterKey]
await revokeCiType(this.CITypeId, this.row.rid, {
perms: ['read'],
attr_filter: this.colType === 'read_attr' ? _tempValue : undefined,
ci_filter: this.colType === 'read_ci' ? _tempValue : undefined,
})
}
this.$emit('updateTableDataRead', this.row, this.radioValue === 1 || this.radioValue === 2)
this.handleCancel()
},
handleCancel() {
this.radioValue = 1
this.selectedAttr = []
if (this.$refs.form) {
this.$refs.form.resetFields()
}
this.visible = false
},
setExpFromFilter(filterExp) {
let expression = ''
if (filterExp) {
expression = `q=${filterExp}`
}
this.expression = expression
},
changeRadioValue(value) {
if (this.id_filter) {
this.$message.warning(this.$t('cmdb.serviceTree.grantedByServiceTreeTips'))
} else {
this.radioValue = value
}
},
},
}
</script>
<style></style>
<template>
<a-modal :width="680" :title="title" :visible="visible" @ok="handleOk" @cancel="handleCancel">
<CustomRadio
:radioList="[
{ value: 1, label: $t('cmdb.components.all') },
{ value: 2, label: $t('cmdb.components.customize'), layout: 'vertical' },
{ value: 3, label: $t('cmdb.components.none') },
]"
:value="radioValue"
@change="changeRadioValue"
>
<template slot="extra_2" v-if="radioValue === 2">
<treeselect
v-if="colType === 'read_attr'"
v-model="selectedAttr"
:multiple="true"
:clearable="true"
searchable
:options="attrGroup"
:placeholder="$t('cmdb.ciType.selectAttributes')"
value-consists-of="LEAF_PRIORITY"
:limit="10"
:limitText="(count) => `+ ${count}`"
:normalizer="
(node) => {
return {
id: node.name || -1,
label: node.alias || node.name || $t('other'),
title: node.alias || node.name || $t('other'),
children: node.attributes,
}
}
"
appendToBody
zIndex="1050"
>
</treeselect>
<a-form-model
:model="form"
:rules="rules"
v-if="colType === 'read_ci'"
:labelCol="{ span: 2 }"
:wrapperCol="{ span: 10 }"
ref="form"
>
<a-form-model-item :label="$t('name')" prop="name">
<a-input v-model="form.name" />
</a-form-model-item>
<FilterComp
ref="filterComp"
:isDropdown="false"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList"
@setExpFromFilter="setExpFromFilter"
:expression="expression"
/>
<div class="read-ci-tip">{{ $t('cmdb.ciType.ciGrantTip') }}</div>
</a-form-model>
</template>
</CustomRadio>
</a-modal>
</template>
<script>
import { grantCiType, revokeCiType } from '../../api/CIType'
import { getCITypeAttributesByTypeIds } from '../../api/CITypeAttr'
import FilterComp from '@/components/CMDBFilterComp'
export default {
name: 'ReadGrantModal',
components: { FilterComp },
props: {
CITypeId: {
type: Number,
default: null,
},
},
inject: {
provide_attrGroup: {
from: 'attrGroup',
},
provide_filerPerimissions: {
from: 'filerPerimissions',
},
},
data() {
return {
visible: false,
colType: '',
row: {},
radioValue: 1,
radioStyle: {
display: 'block',
height: '30px',
lineHeight: '30px',
},
selectedAttr: [],
ruleList: [],
canSearchPreferenceAttrList: [],
expression: '',
form: {
name: '',
},
rules: {
name: [{ required: true, message: this.$t('cmdb.components.customizeFilterName') }],
},
}
},
computed: {
title() {
if (this.colType === 'read_attr') {
return this.$t('cmdb.components.attributeGrant')
}
return this.$t('cmdb.components.ciGrant')
},
attrGroup() {
return this.provide_attrGroup()
},
filerPerimissions() {
return this.provide_filerPerimissions()
},
filterKey() {
if (this.colType === 'read_attr') {
return 'attr_filter'
}
return 'ci_filter'
},
},
methods: {
async open(colType, row) {
this.visible = true
this.colType = colType
this.row = row
this.form = {
name: '',
}
if (this.colType === 'read_ci') {
await getCITypeAttributesByTypeIds({ type_ids: this.CITypeId }).then((res) => {
this.canSearchPreferenceAttrList = res.attributes.filter((item) => item.value_type !== '6')
})
}
if (this.filerPerimissions[row.rid]) {
const _tempValue = this.filerPerimissions[row.rid][this.filterKey]
if (_tempValue && _tempValue.length) {
this.radioValue = 2
if (this.colType === 'read_attr') {
this.selectedAttr = _tempValue
} else {
this.expression = `q=${_tempValue}`
this.form = {
name: this.filerPerimissions[row.rid].name || '',
}
this.$nextTick(() => {
this.$refs.filterComp.visibleChange(true)
})
}
}
}
},
async handleOk() {
if (this.radioValue === 1) {
await grantCiType(this.CITypeId, this.row.rid, {
perms: ['read'],
attr_filter: this.colType === 'read_attr' ? [] : undefined,
ci_filter: this.colType === 'read_ci' ? '' : undefined,
})
} else if (this.radioValue === 2) {
if (this.colType === 'read_ci') {
this.$refs.filterComp.handleSubmit()
}
await grantCiType(this.CITypeId, this.row.rid, {
perms: ['read'],
attr_filter: this.colType === 'read_attr' ? this.selectedAttr : undefined,
ci_filter: this.colType === 'read_ci' ? this.expression.slice(2) : undefined,
name: this.colType === 'read_ci' ? this.form.name : undefined,
})
} else {
const _tempValue = this.filerPerimissions?.[this.row.rid]?.[this.filterKey]
await revokeCiType(this.CITypeId, this.row.rid, {
perms: ['read'],
attr_filter: this.colType === 'read_attr' ? _tempValue : undefined,
ci_filter: this.colType === 'read_ci' ? _tempValue : undefined,
})
}
this.$emit('updateTableDataRead', this.row, this.radioValue === 1 || this.radioValue === 2)
this.handleCancel()
},
handleCancel() {
this.radioValue = 1
this.selectedAttr = []
if (this.$refs.form) {
this.$refs.form.resetFields()
}
this.visible = false
},
setExpFromFilter(filterExp) {
let expression = ''
if (filterExp) {
expression = `q=${filterExp}`
}
this.expression = expression
},
changeRadioValue(value) {
if (this.id_filter) {
this.$message.warning(this.$t('cmdb.serviceTree.grantedByServiceTreeTips'))
} else {
this.radioValue = value
}
},
},
}
</script>
<style lang="less" scoped>
.read-ci-tip {
font-size: 12px;
line-height: 22px;
color: #a5a9bc;
}
</style>

View File

@@ -0,0 +1,41 @@
import i18n from '@/lang'
export const ruleTypeList = () => {
return [
{ value: 'and', label: i18n.t('cmdbFilterComp.and') },
{ value: 'or', label: i18n.t('cmdbFilterComp.or') },
// { value: 'not', label: '非' },
]
}
export const expList = () => {
return [
{ value: 'is', label: i18n.t('cmdbFilterComp.is') },
{ value: '~is', label: i18n.t('cmdbFilterComp.~is') },
{ value: 'contain', label: i18n.t('cmdbFilterComp.contain') },
{ value: '~contain', label: i18n.t('cmdbFilterComp.~contain') },
{ value: 'start_with', label: i18n.t('cmdbFilterComp.start_with') },
{ value: '~start_with', label: i18n.t('cmdbFilterComp.~start_with') },
{ value: 'end_with', label: i18n.t('cmdbFilterComp.end_with') },
{ value: '~end_with', label: i18n.t('cmdbFilterComp.~end_with') },
{ value: '~value', label: i18n.t('cmdbFilterComp.~value') }, // 为空的定义有点绕
{ value: 'value', label: i18n.t('cmdbFilterComp.value') },
]
}
export const advancedExpList = () => {
return [
{ value: 'in', label: i18n.t('cmdbFilterComp.in') },
{ value: '~in', label: i18n.t('cmdbFilterComp.~in') },
{ value: 'range', label: i18n.t('cmdbFilterComp.range') },
{ value: '~range', label: i18n.t('cmdbFilterComp.~range') },
{ value: 'compare', label: i18n.t('cmdbFilterComp.compare') },
]
}
export const compareTypeList = [
{ value: '1', label: '>' },
{ value: '2', label: '>=' },
{ value: '3', label: '<' },
{ value: '4', label: '<=' },
]

View File

@@ -0,0 +1,320 @@
<template>
<div :style="{ lineHeight: rowHeight }">
<a-space :style="{ display: 'flex', marginBottom: '10px' }" v-for="(item, index) in ruleList" :key="item.id">
<div v-if="ruleList.length > 1" :style="{ width: '60px', height: rowHeight, position: 'relative' }">
<treeselect
v-if="index !== 0"
class="custom-treeselect"
:style="{ width: '60px', '--custom-height': rowHeight, position: 'absolute', top: '-24px' }"
v-model="item.type"
:multiple="false"
:clearable="false"
searchable
:options="ruleTypeList"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
:disabled="disabled"
>
</treeselect>
</div>
<treeselect
class="custom-treeselect"
:style="{ width: '120px', '--custom-height': rowHeight }"
v-model="item.property"
:multiple="false"
:clearable="false"
searchable
:options="canSearchPreferenceAttrList"
:normalizer="
(node) => {
return {
id: node.name,
label: node.alias || node.name,
children: node.children,
}
}
"
appendToBody
:zIndex="1050"
:disabled="disabled"
>
<div
v-if="node.id !== '$count'"
:title="node.label"
slot="option-label"
slot-scope="{ node }"
class="property-label"
>
<ValueTypeMapIcon :attr="node.raw" />
{{ node.label }}
</div>
<div
v-else
:title="node.label"
slot="option-label"
slot-scope="{ node }"
class="property-label"
:style="{ borderBottom: '1px solid #E4E7ED', marginBottom: '8px' }"
>
<ValueTypeMapIcon :attr="node.raw" />
{{ node.label }}
</div>
<div
class="property-label"
slot="value-label"
slot-scope="{ node }"
>
<ValueTypeMapIcon :attr="node.raw" /> {{ node.label }}
</div>
</treeselect>
<treeselect
class="custom-treeselect"
:style="{ width: '90px', '--custom-height': rowHeight }"
v-model="item.exp"
:multiple="false"
:clearable="false"
searchable
:options="getExpListByProperty(item.property)"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
appendToBody
@select="(value) => handleChangeExp(value, item, index)"
:zIndex="1050"
:disabled="disabled"
>
</treeselect>
<ValueControls
:rule="ruleList[index]"
:attrList="canSearchPreferenceAttrList"
:disabled="disabled"
:curModelAttrList="curModelAttrList"
:rowHeight="rowHeight"
@change="(value) => handleChangeValue(value, index)"
/>
<template v-if="!disabled">
<a-tooltip :title="$t('copy')">
<a class="operation" @click="handleCopyRule(item)"><ops-icon type="veops-copy"/></a>
</a-tooltip>
<a-tooltip :title="$t('delete')">
<a class="operation" @click="handleDeleteRule(item)"><a-icon type="minus-circle"/></a>
</a-tooltip>
<a-tooltip :title="$t('cmdbFilterComp.addHere')">
<a class="operation" @click="handleAddRuleAt(item)"><a-icon type="plus-circle"/></a>
</a-tooltip>
</template>
</a-space>
<div class="table-filter-add" v-if="!disabled && ruleList.length === 0">
<a @click="handleAddRule">+ {{ $t('new') }}</a>
</div>
</div>
</template>
<script>
import _ from 'lodash'
import { v4 as uuidv4 } from 'uuid'
import { ruleTypeList, expList, advancedExpList, compareTypeList } from './constants.js'
import ValueTypeMapIcon from '@/components/CMDBValueTypeMapIcon'
import ValueControls from './valueControls.vue'
export default {
name: 'Expression',
components: {
ValueTypeMapIcon,
ValueControls
},
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: Array,
default: () => [],
},
canSearchPreferenceAttrList: {
type: Array,
required: true,
default: () => [],
},
disabled: {
type: Boolean,
default: false,
},
curModelAttrList: {
type: Array,
default: () => []
}
},
data() {
return {
compareTypeList,
rowHeight: '36px',
}
},
computed: {
ruleList: {
get() {
return this.value
},
set(val) {
this.$emit('change', val)
return val
},
},
ruleTypeList() {
return ruleTypeList()
},
expList() {
return expList()
},
advancedExpList() {
return advancedExpList()
}
},
methods: {
getExpListByProperty(property) {
if (property === '$count') {
return [
{ value: 'is', label: this.$t('cmdbFilterComp.is') },
{ value: '~is', label: this.$t('cmdbFilterComp.~is') },
{ value: 'compare', label: this.$t('cmdbFilterComp.compare') }
]
}
if (property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find && ['0', '1', '3', '4', '5'].includes(_find.value_type)) {
return [
{ value: 'is', label: this.$t('cmdbFilterComp.is') },
{ value: '~is', label: this.$t('cmdbFilterComp.~is') },
{ value: '~value', label: this.$t('cmdbFilterComp.~value') }, // 为空的定义有点绕
{ value: 'value', label: this.$t('cmdbFilterComp.value') },
...this.advancedExpList
]
}
}
return [...this.expList, ...this.advancedExpList]
},
isChoiceByProperty(property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find) {
return _find.is_choice
}
return false
},
handleAddRule() {
this.ruleList.push({
id: uuidv4(),
type: 'and',
property: this.canSearchPreferenceAttrList[0]?.name,
exp: 'is',
value: null,
})
this.$emit('change', this.ruleList)
},
handleCopyRule(item) {
this.ruleList.push({ ...item, id: uuidv4() })
this.$emit('change', this.ruleList)
},
handleDeleteRule(item) {
const idx = this.ruleList.findIndex((r) => r.id === item.id)
if (idx > -1) {
this.ruleList.splice(idx, 1)
}
this.$emit('change', this.ruleList)
},
handleAddRuleAt(item) {
const idx = this.ruleList.findIndex((r) => r.id === item.id)
if (idx > -1) {
this.ruleList.splice(idx + 1, 0, {
id: uuidv4(),
type: 'and',
property: this.canSearchPreferenceAttrList[0]?.name,
exp: 'is',
value: null,
})
}
this.$emit('change', this.ruleList)
},
getChoiceValueByProperty(property) {
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
if (_find) {
return _find.choice_value
}
return []
},
handleChangeExp({ value }, item, index) {
const _ruleList = _.cloneDeep(this.ruleList)
if (value === 'range') {
_ruleList[index] = {
..._ruleList[index],
min: '',
max: '',
exp: value,
}
} else if (value === 'compare') {
_ruleList[index] = {
..._ruleList[index],
compareType: '1',
exp: value,
}
} else {
_ruleList[index] = {
..._ruleList[index],
exp: value,
}
}
this.ruleList = _ruleList
this.$emit('change', this.ruleList)
},
handleChangeValue(value, index) {
const _ruleList = _.cloneDeep(this.ruleList)
_ruleList[index] = value
this.$emit('change', _ruleList)
},
},
}
</script>
<style lang="less" scoped>
.input-group {
display: flex;
align-items: center;
width: 150px;
&-range-icon {
margin: 0 8px;
}
input {
height: 36px;
}
}
.property-label {
width: 100%;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden
}
.attr-filter-tip {
color: #86909C;
font-size: 12px;
font-weight: 400;
}
</style>

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=).+$',
},
CITypeIds: {
type: Array,
default: () => [],
},
curModelAttrList: {
type: Array,
default: () => []
}
},
data() {
return {
compareTypeList,
visible: false,
ruleList: [],
filterExp: '',
}
},
methods: {
init(open, isInitOne = true) {
// isInitOne 初始化exp为空时ruleList是否默认给一条
// const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
const exp = this.expression.match(new RegExp(this.regQ, 'g'))
? this.expression.match(new RegExp(this.regQ, 'g'))[0]
: null
if (open && exp) {
const expArray = exp.split(',').map((item) => {
let has_not = ''
const key = item.split(':')[0]
const val = item
.split(':')
.slice(1)
.join(':')
let type, property, exp, value, min, max, compareType
if (key.includes('-')) {
type = 'or'
if (key.includes('~')) {
property = key.substring(2)
has_not = '~'
} else {
property = key.substring(1)
}
} else {
type = 'and'
if (key.includes('~')) {
property = key.substring(1)
has_not = '~'
} else {
property = key
}
}
const in_reg = /(?<=\().+(?=\))/g
const range_reg = /(?<=\[).+(?=\])/g
const compare_reg = /(?<=>=|<=|>(?!=)|<(?!=)).+/
if (val === '*') {
exp = has_not + 'value'
value = ''
} else if (in_reg.test(val)) {
exp = has_not + 'in'
value = val.match(in_reg)[0]
} else if (range_reg.test(val)) {
exp = has_not + 'range'
value = val.match(range_reg)[0]
min = value.split('_TO_')[0]
max = value.split('_TO_')[1]
} else if (compare_reg.test(val)) {
exp = has_not + 'compare'
value = val.match(compare_reg)[0]
const _compareType = val.substring(0, val.match(compare_reg)['index'])
const idx = compareTypeList.findIndex((item) => item.label === _compareType)
compareType = compareTypeList[idx].value
} else if (!val.includes('*')) {
exp = has_not + 'is'
value = val
} else {
const resList = [
['contain', /(?<=\*).*(?=\*)/g],
['end_with', /(?<=\*).+/g],
['start_with', /.+(?=\*)/g],
]
for (let i = 0; i < 3; i++) {
const reg = resList[i]
if (reg[1].test(val)) {
exp = has_not + reg[0]
value = val.match(reg[1])[0]
break
}
}
}
return {
id: uuidv4(),
type,
property,
exp,
value,
min,
max,
compareType,
}
})
this.ruleList = [...expArray]
} else if (open) {
const _canSearchPreferenceAttrList = this.canSearchPreferenceAttrList.filter((attr) => !attr.is_password)
this.ruleList = isInitOne
? [
{
id: uuidv4(),
type: 'and',
property:
_canSearchPreferenceAttrList && _canSearchPreferenceAttrList.length
? _canSearchPreferenceAttrList[0].name
: undefined,
exp: 'is',
value: null,
},
]
: []
}
},
handleSubmit() {
if (this.ruleList && this.ruleList.length) {
this.ruleList[0].type = 'and' // 增删后以防万一第一个不是and
this.filterExp = ''
const expList = this.ruleList.filter((rule) => rule?.property).map((rule) => {
let singleRuleExp = ''
let _exp = rule.exp
if (rule.type === 'or') {
singleRuleExp += '-'
}
if (rule.exp.includes('~')) {
singleRuleExp += '~'
_exp = rule.exp.split('~')[1]
}
singleRuleExp += `${rule.property}:`
if (_exp === 'is') {
singleRuleExp += `${rule.value ?? ''}`
}
if (_exp === 'contain') {
singleRuleExp += `*${rule.value ?? ''}*`
}
if (_exp === 'start_with') {
singleRuleExp += `${rule.value ?? ''}*`
}
if (_exp === 'end_with') {
singleRuleExp += `*${rule.value ?? ''}`
}
if (_exp === 'value') {
singleRuleExp += `*`
}
if (_exp === 'in') {
singleRuleExp += `(${rule.value ?? ''})`
}
if (_exp === 'range') {
singleRuleExp += `[${rule.min}_TO_${rule.max}]`
}
if (_exp === 'compare') {
const idx = compareTypeList.findIndex((item) => item.value === rule.compareType)
singleRuleExp += `${compareTypeList[idx].label}${rule.value ?? ''}`
}
return singleRuleExp
})
this.filterExp = expList.join(',')
this.$emit('setExpFromFilter', this.filterExp)
} else {
this.$emit('setExpFromFilter', '')
}
this.visible = false
},
},
}
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,255 @@
<template>
<div class="control-group">
<CIReferenceAttr
v-if="getAttr(rule.property).is_reference && (rule.exp === 'is' || rule.exp === '~is')"
class="select-filter"
:referenceTypeId="getAttr(rule.property).reference_type_id"
:value="rule.value"
:disabled="disabled"
@change="(value) => handleChange('value', value)"
/>
<a-select
v-else-if="getAttr(rule.property).is_bool && (rule.exp === 'is' || rule.exp === '~is')"
class="select-filter"
:disabled="disabled"
:placeholder="$t('placeholder2')"
:value="rule.value"
@change="(value) => handleChange('value', value)"
>
<a-select-option key="1">
true
</a-select-option>
<a-select-option key="0">
false
</a-select-option>
</a-select>
<div
class="input-group"
v-else-if="isChoiceByProperty(rule.property) && (rule.exp === 'is' || rule.exp === '~is')"
>
<a-select
class="select-filter"
:style="{ width: '175px' }"
showSearch
:placeholder="$t('placeholder2')"
:disabled="disabled"
:value="rule.value"
@change="(value) => handleChange('value', value)"
>
<a-select-option
v-for="(node) in getChoiceValueByProperty(rule.property)"
:key="String(node[0])"
:title="node[1] ? node[1].label || node[0] : node[0]"
>
<a-tooltip placement="topLeft" :title="node[1] ? node[1].label || node[0] : node[0]" >
{{ node[1] ? node[1].label || node[0] : node[0] }}
</a-tooltip>
</a-select-option>
</a-select>
<!-- <treeselect
class="custom-treeselect"
:style="{ '--custom-height': rowHeight }"
:value="rule.value"
@input="(value) => handleChange('value', value)"
:multiple="false"
:clearable="false"
searchable
:options="getChoiceValueByProperty(rule.property)"
:placeholder="$t('placeholder2')"
:normalizer="
(node) => {
return {
id: node[0],
label: node[1] ? node[1].label || node[0] : node[0],
children: node.children,
}
}
"
:zIndex="1050"
:disabled="disabled"
appendToBody
>
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
{{ node.label }}
</div>
</treeselect> -->
</div>
<div
compact
v-else-if="rule.exp === 'range' || rule.exp === '~range'"
class="input-group"
>
<a-input
class="ops-input"
:placeholder="$t('min')"
:disabled="disabled"
:value="rule.min"
@change="(e) => handleChange('min', e.target.value)"
/>
<span class="input-group-range-icon">~</span>
<a-input
class="ops-input"
v-model="rule.max"
:placeholder="$t('max')"
:disabled="disabled"
:value="rule.max"
@change="(e) => handleChange('max', e.target.value)"
/>
</div>
<div class="input-group" compact v-else-if="rule.exp === 'compare'">
<treeselect
class="custom-treeselect"
:style="{ width: '70px', '--custom-height': rowHeight, 'flex-shrink': 0 }"
:value="rule.compareType"
@input="(value) => handleChange('compareType', value)"
:multiple="false"
:clearable="false"
searchable
:options="compareTypeList"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
children: node.children,
}
}
"
:zIndex="1050"
:disabled="disabled"
appendToBody
>
</treeselect>
<a-input :value="rule.value" @change="(e) => handleChange('value', e.target.value)" class="ops-input"/>
</div>
<div class="input-group" v-else-if="rule.exp !== 'value' && rule.exp !== '~value'">
<a-input
:value="rule.value"
@change="(e) => handleChange('value', e.target.value)"
:placeholder="rule.exp === 'in' || rule.exp === '~in' ? $t('cmdbFilterComp.split', { separator: ';' }) : ''"
class="ops-input"
:disabled="disabled"
></a-input>
</div>
<div v-else :style="{ width: '136px' }"></div>
</div>
</template>
<script>
import { compareTypeList } from './constants.js'
import CIReferenceAttr from '@/components/ciReferenceAttr/index.vue'
export default {
name: 'ValueControls',
components: {
CIReferenceAttr,
},
props: {
rule: {
type: Object,
default: () => {},
},
attrList: {
type: Array,
default: () => [],
},
disabled: {
type: Boolean,
default: false,
},
// 当前模型属性
curModelAttrList: {
type: Array,
default: () => []
},
// 行高
rowHeight: {
type: String,
default: ''
},
},
data() {
return {
compareTypeList,
}
},
computed: {
choiceValue() {
const val = /\{\{([^}]+)\}\}/g.exec(this?.rule?.value || '')
return val ? val?.[1]?.trim() || '' : this?.value?.value
}
},
methods: {
isChoiceByProperty(property) {
const _find = this.attrList.find((item) => item.name === property)
if (_find) {
return _find.is_choice
}
return false
},
getChoiceValueByProperty(property) {
const _find = this.attrList.find((item) => item.name === property)
if (_find) {
return _find.choice_value
}
return []
},
handleChange(key, value) {
this.$emit('change', {
...this.rule,
[key]: value
})
},
getAttr(property) {
return this.attrList.find((item) => item.name === property) || {}
},
}
}
</script>
<style lang="less" scoped>
.control-group {
display: flex;
}
.input-group {
display: flex;
align-items: center;
width: 136px;
&-range-icon {
margin: 0 8px;
}
input {
height: 36px;
}
}
.select-filter {
height: 36px;
width: 136px;
/deep/ .ant-select-selection {
height: 36px;
background: #f7f8fa;
line-height: 36px;
border: none;
.ant-select-selection__rendered {
height: 36px;
line-height: 36px;
}
}
/deep/ .vue-treeselect__control {
background: #f7f8fa;
border: none;
}
}
</style>

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

@@ -65,9 +65,7 @@
<a><a-icon type="question-circle"/></a>
</a-tooltip>
</a-input>
<a-tooltip :title="$t('reset')">
<a-button @click="reset">{{ $t('reset') }}</a-button>
</a-tooltip>
<a-button @click="reset">{{ $t('reset') }}</a-button>
<FilterComp
ref="filterComp"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList"
@@ -310,6 +308,11 @@ export default {
height: 32px;
.search-form-bar-filter {
.ops_display_wrapper(transparent);
&:hover {
color: @primary-color;
}
.search-form-bar-filter-icon {
color: @primary-color;
font-size: 12px;

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

@@ -62,7 +62,7 @@ const cmdb_en = {
desc: 'Reverse order',
uniqueKey: 'Unique Identifies',
uniqueKeySelect: 'Please select a unique identifier',
uniqueKeyTips: 'json/password/computed/choice can not be unique identifies',
uniqueKeyTips: 'json/password/computed/selectList can not be unique identifies',
notfound: 'Can\'t find what you want?',
cannotDeleteGroupTips: 'There is data under this group and cannot be deleted!',
confirmDeleteGroup: 'Are you sure you want to delete group [{groupName}]?',
@@ -117,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',
@@ -130,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',
@@ -141,7 +142,7 @@ const cmdb_en = {
selectCIType: 'Please select a CMDB CIType',
selectCITypeAttributes: 'Please select CIType attributes',
selectAttributes: 'Please select attributes',
choiceScriptDemo: 'class ChoiceValue(object):\n @staticmethod\n def values():\n """\n Execution entry, returns predefined value\n :return: Returns a list, the type of the value is the same as the type of the attribute\n For example:\n return ["online", "offline"]\n """\n return []',
choiceScriptDemo: 'class ChoiceValue(object):\n @staticmethod\n def values():\n """\n Execution entry, returns select list\n :return: Returns a list, the type of the value is the same as the type of the attribute\n For example:\n return ["online", "offline"]\n """\n return []',
valueExisted: 'The current value already exists!',
addRelation: 'Add Relation',
sourceCIType: 'Source CIType',
@@ -189,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',
@@ -206,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',
@@ -282,6 +291,34 @@ const cmdb_en = {
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',
ciGrantTip: `Filter conditions can be changed dynamically using {{}} referenced variables, currently user variables are supported, such as {{user.uid}},{{user.username}},{{user.email}},{{user.nickname}}`,
searchInputTip: 'Please search for resource keywords',
resourceSearch: 'Resource Search',
recentSearch: 'Recent Search',
myCollection: 'My Collection',
keyword: 'Keywords',
CIType: 'CIType',
filterPopoverLabel: 'Filter',
conditionFilter: 'Condition Filter',
advancedFilter: 'Advanced Filter',
saveCondition: 'Save Condition',
confirmClear: 'Confirm to clear?',
currentPage: 'Current Page'
},
components: {
unselectAttributes: 'Unselected',
@@ -323,7 +360,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',
@@ -367,7 +404,7 @@ const cmdb_en = {
tips2: '1. Click to download the template, and users can customize the header of the template file, including model properties and model associations',
// eslint-disable-next-line no-template-curly-in-string
tips3: '2. The red color in the template file represents the model relationship, such as the $Product. Product Name (${Model Name}. {Attribute Name}) column, which establishes the relationship with the product.',
tips4: '3. In the download template Excel file, the predefined values of attributes will be set as dropdown options. Please note that due to the limitations of Excel itself, a single dropdown box is limited to a maximum of 255 characters. If it exceeds 255 characters, we will not set the dropdown options for this attribute',
tips4: `3. The download template excel file will have the property's drop-down list enumeration configured as a drop-down option. Please note that due to the limitations of Excel itself, a single dropdown box is limited to a maximum of 255 characters. If it exceeds 255 characters, we will not set the dropdown options for this attribute`,
tips5: '4. When using Excel templates, please ensure that a single file does not exceed 5000 lines.',
},
preference: {
@@ -548,7 +585,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"),
@@ -604,6 +641,7 @@ if __name__ == "__main__":
attributeDesc: 'Attribute Description',
selectRows: 'Select: {rows} items',
addRelation: 'Add Relation',
viewRelation: 'View Relation',
all: 'All',
batchUpdate: 'Batch Update',
batchUpdateConfirm: 'Are you sure you want to make batch updates?',
@@ -626,7 +664,7 @@ if __name__ == "__main__":
tips4: 'At least one field must be selected',
tips5: 'Search name | alias',
tips6: 'Speed up retrieval, full-text search possible, no need to use conditional filtering\n\n json/link/password currently does not support indexing \n\nText characters longer than 190 cannot be indexed',
tips7: 'The form of expression is a drop-down box, and the value must be in the predefined value',
tips7: 'Whether to configure a select list',
tips8: 'Multiple values, such as intranet IP',
tips9: 'For front-end only',
tips10: 'Other attributes of the CIType are computed using expressions\n\nA code snippet computes the returned value.',

View File

@@ -62,7 +62,7 @@ const cmdb_zh = {
desc: '倒序',
uniqueKey: '唯一标识',
uniqueKeySelect: '请选择唯一标识',
uniqueKeyTips: 'json、密码、计算属性、预定义值属性不能作为唯一标识',
uniqueKeyTips: 'json、密码、计算属性、下拉列表属性不能作为唯一标识',
notfound: '找不到想要的?',
cannotDeleteGroupTips: '该分组下有数据, 不能删除!',
confirmDeleteGroup: '确定要删除分组 【{groupName}】 吗?',
@@ -117,7 +117,7 @@ const cmdb_zh = {
advancedSettings: '高级设置',
font: '字体',
color: '颜色',
choiceValue: '预定义值',
choiceValue: '下拉列表',
computedAttribute: '计算属性',
computedAttributeTips: '该属性的值是通过模型的其它属性构建的表达式或者执行一段代码的方式计算而来,属性的引用方法为: {{ 属性名 }}',
addAttribute: '新增属性',
@@ -130,6 +130,7 @@ const cmdb_zh = {
selectAttribute: '添加属性',
groupExisted: '分组名称已存在',
attributeSortedTips: '其他分组中的属性不能进行排序,如需排序请先拖至自定义的分组!',
attributeSortedTips2: '非继承属性不能插入到继承属性前!',
buildinAttribute: '内置字段',
expr: '表达式',
code: '代码',
@@ -141,7 +142,7 @@ const cmdb_zh = {
selectCIType: '请选择CMDB模型',
selectCITypeAttributes: '请选择模型属性',
selectAttributes: '请选择属性',
choiceScriptDemo: 'class ChoiceValue(object):\n @staticmethod\n def values():\n """\n 执行入口, 返回预定义值\n :return: 返回一个列表, 值的类型同属性的类型\n 例如:\n return ["在线", "下线"]\n """\n return []',
choiceScriptDemo: 'class ChoiceValue(object):\n @staticmethod\n def values():\n """\n 执行入口, 返回下拉列表\n :return: 返回一个列表, 值的类型同属性的类型\n 例如:\n return ["在线", "下线"]\n """\n return []',
valueExisted: '当前值已存在!',
addRelation: '新增关系',
sourceCIType: '源模型',
@@ -189,6 +190,14 @@ const cmdb_zh = {
confirmDeleteTrigger: '确认删除该触发器吗?',
int: '整数',
float: '浮点数',
longText: '长文本',
shortText: '短文本',
shortTextTip: '文本长度 <= 128',
referenceModel: '引用模型',
referenceModelTip: '请选择引用模型',
referenceModelTip1: '用于快捷查看引用模型实例',
bool: '布尔',
reference: '引用',
text: '文本',
datetime: '日期时间',
date: '日期',
@@ -206,7 +215,7 @@ const cmdb_zh = {
otherGroupTips: '其他分组属性不可排序',
filterTips: '点击可仅查看{name}属性',
attributeAssociation: '属性关联',
attributeAssociationTip1: '通过2个模型的属性值(除密码、json、多值)来自动建立关系',
attributeAssociationTip1: '通过2个模型的属性值(除密码、json、多值、长文本、布尔、引用)来自动建立关系',
attributeAssociationTip2: '双击可编辑',
attributeAssociationTip3: '属性关联必须选择两个属性',
attributeAssociationTip4: '请选择原模型属性',
@@ -282,6 +291,34 @@ const cmdb_zh = {
rule: '规则',
cascadeAttr: '级联',
cascadeAttrTip: '级联属性注意顺序',
enumValue: '枚举值',
label: '标签',
valueInputTip: '请输入枚举值',
enumValueTip2: '枚举值不能重复',
builtin: '内置',
department: '部门',
user: '用户',
userGroup: '用户组',
departmentTip: '下拉选择为通用设置公司架构里的所有部门',
userGroupSelectTip: '请选择用户组',
displayValue: '展示值',
displayValueSelectTip: '请选择展示值',
departmentCascadeDisplay: '部门级联显示',
filterUsers: '筛选用户',
enum: '枚举',
ciGrantTip: `筛选条件可使用{{}}引用变量实现动态变化,目前支持用户变量,如{{user.uid}},{{user.username}},{{user.email}},{{user.nickname}}`,
searchInputTip: '请搜索资源关键字',
resourceSearch: '资源搜索',
recentSearch: '最近搜索',
myCollection: '我的收藏',
keyword: '关键字',
CIType: '模型',
filterPopoverLabel: '条件过滤',
conditionFilter: '条件过滤',
advancedFilter: '高级筛选',
saveCondition: '保存条件',
confirmClear: '确认清空?',
currentPage: '当前页'
},
components: {
unselectAttributes: '未选属性',
@@ -323,7 +360,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: '已',
@@ -366,7 +403,7 @@ const cmdb_zh = {
tips2: '1. 点击下载模板,用户可以自定义模板文件的表头,包括模型属性、模型关联',
// eslint-disable-next-line no-template-curly-in-string
tips3: '2. 模板文件中红色为模型关系,如$产品.产品名(${模型名}.{属性名})这一列就可建立和产品之间的关系',
tips4: '3. 下载模板excel文件中会将属性的预定义值置为下拉选项请注意受excel本身的限制单个下拉框限制了最多255个字符如果超过255个字符我们不会设置该属性的下拉选项',
tips4: '3. 下载模板excel文件中会将属性的下拉列表枚举配置置为下拉选项请注意受excel本身的限制单个下拉框限制了最多255个字符如果超过255个字符我们不会设置该属性的下拉选项',
tips5: '4. 在使用excel模板时请确保单个文件不超过5000行',
},
preference: {
@@ -547,7 +584,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"),
@@ -603,6 +640,7 @@ if __name__ == "__main__":
attributeDesc: '查看属性配置',
selectRows: '选取:{rows} 项',
addRelation: '添加关系',
viewRelation: '查看关系',
all: '全部',
batchUpdate: '批量修改',
batchUpdateConfirm: '确认要批量修改吗?',
@@ -625,7 +663,7 @@ if __name__ == "__main__":
tips4: '必须至少选择一个字段',
tips5: '搜索 名称 | 别名',
tips6: '加快检索, 可以全文搜索, 无需使用条件过滤\n\n json、链接、密码目前不支持建索引 \n\n文本字符长度超过190不能建索引',
tips7: '表现形式是下拉框, 值必须在预定义值里',
tips7: '是否配置下拉列表',
tips8: '多值, 比如内网IP',
tips9: '仅针对前端',
tips10: '模型的其他属性通过表达式的方式计算出来\n\n一个代码片段计算返回的值',

View File

@@ -54,7 +54,7 @@ const genCmdbRoutes = async () => {
path: '/cmdb/resourcesearch',
name: 'cmdb_resource_search',
meta: { title: 'cmdb.menu.ciSearch', icon: 'ops-cmdb-search', selectedIcon: 'ops-cmdb-search', keepAlive: false },
component: () => import('../views/resource_search/index.vue')
component: () => import('../views/resource_search_2/index.vue')
},
{
path: '/cmdb/adc',
@@ -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',
@@ -117,11 +117,10 @@ const genCmdbRoutes = async () => {
meta: { title: 'cmdb.menu.serviceTreeDefine', keepAlive: false, icon: 'ops-cmdb-preferencerelation', selectedIcon: 'ops-cmdb-preferencerelation-selected' }
},
{
path: '/cmdb/modelrelation',
name: 'model_relation',
hideChildrenInMenu: true,
component: () => import('../views/model_relation/index'),
meta: { title: 'cmdb.menu.citypeRelation', keepAlive: false, icon: 'ops-cmdb-modelrelation', selectedIcon: 'ops-cmdb-modelrelation-selected' }
path: '/cmdb/discovery',
name: 'discovery',
component: () => import('../views/discovery/index'),
meta: { title: 'cmdb.menu.ad', keepAlive: false, icon: 'ops-cmdb-adr', selectedIcon: 'ops-cmdb-adr-selected' }
},
{
path: '/cmdb/operationhistory',
@@ -130,19 +129,20 @@ const genCmdbRoutes = async () => {
component: () => import('../views/operation_history/index'),
meta: { title: 'cmdb.menu.operationHistory', keepAlive: false, icon: 'ops-cmdb-operation', selectedIcon: 'ops-cmdb-operation-selected' }
},
{
path: '/cmdb/modelrelation',
name: 'model_relation',
hideChildrenInMenu: true,
component: () => import('../views/model_relation/index'),
meta: { title: 'cmdb.menu.citypeRelation', keepAlive: false, icon: 'ops-cmdb-modelrelation', selectedIcon: 'ops-cmdb-modelrelation-selected' }
},
{
path: '/cmdb/relationtype',
name: 'relation_type',
hideChildrenInMenu: true,
component: () => import('../views/relation_type/index'),
meta: { title: 'cmdb.menu.relationType', keepAlive: false, icon: 'ops-cmdb-relationtype', selectedIcon: 'ops-cmdb-relationtype-selected' }
},
{
path: '/cmdb/discovery',
name: 'discovery',
component: () => import('../views/discovery/index'),
meta: { title: 'cmdb.menu.ad', keepAlive: false, icon: 'ops-cmdb-adr', selectedIcon: 'ops-cmdb-adr-selected' }
},
}
]
}
]

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

@@ -35,7 +35,7 @@
<a-select v-model="parentsForm[item.name].attr">
<a-select-option
:title="attr.alias || attr.name"
v-for="attr in item.attributes"
v-for="attr in filterAttributes(item.attributes)"
:key="attr.name"
:value="attr.name"
>
@@ -87,11 +87,32 @@
</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-if="getFieldType(list.name).split('%%')[0] === 'select'"
v-else-if="getFieldType(list.name).split('%%')[0] === 'select'"
:mode="getFieldType(list.name).split('%%')[1] === 'multiple' ? 'multiple' : 'default'"
showSearch
allowClear
@@ -107,25 +128,27 @@
v-if="choice[1] && choice[1].icon && choice[1].icon.name"
:type="choice[1].icon.name"
/>
{{ choice[0] }}
<a-tooltip placement="topLeft" :title="choice[1] ? choice[1].label || choice[0] : choice[0]" >
{{ choice[1] ? choice[1].label || choice[0] : choice[0] }}
</a-tooltip>
</span>
</a-select-option>
</a-select>
<a-input-number
v-decorator="[list.name, { rules: getDecoratorRules(list) }]"
style="width: 100%"
v-if="getFieldType(list.name) === 'input_number'"
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-if="getFieldType(list.name) === '4' || getFieldType(list.name) === '3'"
v-else-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'"
v-else-if="getFieldType(list.name) === 'input'"
@focus="(e) => handleFocusInput(e, list)"
v-decorator="[list.name, { rules: getDecoratorRules(list) }]"
/>
@@ -156,6 +179,7 @@ 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',
@@ -164,6 +188,7 @@ export default {
ElOption: Option,
JsonEditor,
CreateInstanceFormByGroup,
CIReferenceAttr
},
props: {
typeIdFromRelation: {
@@ -261,6 +286,11 @@ export default {
}
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] &&
@@ -309,6 +339,11 @@ export default {
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] &&
@@ -426,6 +461,9 @@ export default {
}
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) {
@@ -487,7 +525,12 @@ export default {
}
return rules
}
},
filterAttributes(attributes) {
return attributes.filter((attr) => {
return !attr.is_bool && !attr.is_reference
})
},
},
}
</script>
@@ -498,7 +541,7 @@ export default {
}
.ant-drawer-body {
overflow-y: auto;
max-height: calc(100vh - 110px);
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] ? choice[1].label || choice[0] : 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] ? choice[1].label || choice[0] : choice[0]">
{{ choice[1] ? choice[1].label || choice[0] : 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 : '',
},
]"
>
@@ -130,13 +148,16 @@
</template>
<script>
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 +167,10 @@ export default {
type: Array,
default: () => [],
},
ciTypeId: {
type: [Number, String],
default: ''
}
},
inject: ['getFieldType'],
data() {
@@ -183,6 +208,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

@@ -92,6 +92,10 @@ export default {
font-size: 14px;
font-weight: 400;
&:hover {
color: @primary-color;
}
&:not(:first-child) {
border-left: solid 1px @border-color-base;
}
@@ -99,6 +103,10 @@ export default {
&_active {
background-color: @primary-color;
color: #FFFFFF;
&:hover {
color: #FFFFFF;
}
}
}
}

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,12 +96,7 @@ 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 = this.$t('other')

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

@@ -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"
@@ -171,7 +187,6 @@
<a-icon
style="position:absolute;top:2px;left:-17px;color:#2f54eb;"
type="question-circle"
theme="filled"
@click="
(e) => {
e.stopPropagation()
@@ -189,10 +204,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 +221,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 +234,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 +243,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 +264,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 +277,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 +288,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 +310,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 +319,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,17 +340,22 @@
</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"
@@ -346,10 +363,13 @@
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
@@ -357,9 +377,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()
@@ -415,14 +434,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,
@@ -451,6 +473,7 @@ export default {
defaultForDatetime: '',
re_check: {},
enumValueType: ENUM_VALUE_TYPE.INPUT
}
},
@@ -467,7 +490,7 @@ export default {
return formLayout === 'horizontal'
? {
labelCol: { span: 8 },
wrapperCol: { span: 12 },
wrapperCol: { span: 15 },
}
: {}
},
@@ -484,6 +507,7 @@ export default {
},
mounted() {},
methods: {
getPropertyIcon,
async handleCreate() {
try {
await canDefineComputed()
@@ -516,9 +540,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'))
@@ -536,6 +558,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()
@@ -544,12 +586,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
@@ -573,8 +610,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,
}
@@ -583,7 +625,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({
@@ -639,7 +685,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,
@@ -669,10 +731,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 {
@@ -700,29 +762,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 {
@@ -764,10 +853,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 }) {
@@ -795,6 +893,12 @@ export default {
}
return []
},
resetPreValue() {
if (this.$refs.preValueArea) {
this.$refs.preValueArea.resetData()
}
}
},
watch: {},
}
@@ -806,6 +910,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="typeKey in Object.keys(valueTypeMap)"
:key="typeKey"
:title="$t('cmdb.ciType.filterTips', { name: valueTypeMap[typeKey] })"
v-for="item in valueTypeMap"
:key="item.key"
:title="$t('cmdb.ciType.filterTips', { name: item.value })"
>
<span
@click="handleFilterType(typeKey)"
@click="handleFilterType(item.key)"
:class="{
'ci-types-attributes-filter': true,
'ci-types-attributes-filter-selected': attrTypeFilter.includes(typeKey),
'ci-types-attributes-filter-selected': attrTypeFilter.includes(item.key),
}"
>
<ops-icon :type="getPropertyIcon({ value_type: typeKey })" />
{{ valueTypeMap[typeKey] }}
<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"
@@ -164,7 +178,6 @@
<a-icon
style="position:absolute;top:2px;left:-17px;color:#2f54eb;"
type="question-circle"
theme="filled"
@click="
(e) => {
e.stopPropagation()
@@ -182,10 +195,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 +212,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 +225,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 +234,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 +255,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 +268,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 +289,7 @@
"
/>
</a-tooltip>
{{ $t('cmdb.ciType.list') }}
</span>
</template>
<a-switch
@@ -290,18 +302,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 +321,7 @@
"
/>
</a-tooltip>
{{ $t('cmdb.ciType.isDynamic') }}
</span>
</template>
<a-switch
@@ -321,37 +333,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"
: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"
theme="filled"
style="position:absolute;top:3px;left:-17px;color:#A5A9BC;"
type="info-circle"
@click="
(e) => {
e.stopPropagation()
@@ -360,6 +379,7 @@
"
/>
</a-tooltip>
{{ $t('cmdb.ciType.computedAttribute') }}
</span>
</template>
<a-switch
@@ -392,6 +412,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',
@@ -401,6 +424,7 @@ export default {
vueJsonEditor,
FontArea,
RegSelect,
ReferenceModelSelect,
},
props: {
hasFooter: {
@@ -433,17 +457,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) {
@@ -454,9 +485,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 {
@@ -483,45 +515,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()
@@ -540,11 +590,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) {
@@ -560,9 +633,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'))
@@ -579,6 +650,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
},
@@ -592,10 +690,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 }) {
@@ -620,6 +727,12 @@ export default {
}
return []
},
resetPreValue() {
if (this.$refs.preValueArea) {
this.$refs.preValueArea.resetData()
}
}
},
}
</script>
@@ -629,6 +742,9 @@ export default {
line-height: 22px;
color: #a5a9bc;
}
.value-type-text {
margin: 0 4px;
}
</style>
<style lang="less">
.create-new-attribute {

View File

@@ -62,11 +62,8 @@ export default {
mounted() {
this.$nextTick(() => {
switch (this.activeKey) {
case '6':
this.$refs.triggerTable.getTableData()
break
case '5':
this.$refs.reconciliationTable.getTableData()
this.$refs.triggerTable.getTableData()
break
default:
break
@@ -87,11 +84,8 @@ export default {
case '1':
this.$refs.attributesTable.getCITypeGroupData()
break
case '6':
this.$refs.triggerTable.getTableData()
break
case '5':
this.$refs.reconciliationTable.getTableData()
this.$refs.triggerTable.getTableData()
break
default:
break
@@ -113,4 +107,12 @@ export default {
.grant-config-wrap {
overflow: auto;
}
.ops-tab.ant-tabs {
/deep/ .ant-tabs-bar {
.ant-tabs-tab:hover {
color: @primary-color;
}
}
}
</style>

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

@@ -103,7 +103,7 @@ export default {
await this.handleLinkAttrToCiType({ attr_id: this.targetKeys.map((i) => Number(i)) })
if (this.currentGroup) {
await this.updateCurrentGroup()
const { id, name, order, attributes } = this.currentGroup
const { name, order, attributes } = this.currentGroup
const attrIds = attributes.filter((i) => !i.inherited).map((i) => i.id)
this.targetKeys.forEach((key) => {
attrIds.push(Number(key))
@@ -141,7 +141,7 @@ export default {
})
if (this.currentGroup) {
await this.updateCurrentGroup()
const { id, name, order, attributes } = this.currentGroup
const { name, order, attributes } = this.currentGroup
const attrIds = attributes.filter((i) => !i.inherited).map((i) => i.id)
attrIds.push(newAttrId)
await createCITypeGroupById(this.CITypeId, { name, order, attributes: attrIds })

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,7 +48,7 @@
</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="24">
<a-form-item
@@ -179,11 +168,11 @@
</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: 3 }"
:label-col="{ span: $i18n.locale === 'en' ? 3 : 2 }"
:wrapper-col="{ span: 19 }"
:extra="scriptCodeExtraText"
labelAlign="left"
@@ -191,7 +180,7 @@
<a-select
mode="multiple"
style="width: 100%"
placeholder="Please select"
:placeholder="$t('placeholder2')"
optionFilterProp="title"
v-model="cascade_attributes"
>
@@ -211,9 +200,11 @@
<div>2. {{ $t('cmdb.ciType.computedAttrTip2') }}</div>
</div>
<a-button size="small" @click="showAllPropDrawer">
{{ $t('cmdb.ciType.viewAllAttr') }}
</a-button>
<div class="all-attr-btn">
<a-button size="small" @click="showAllPropDrawer">
{{ $t('cmdb.ciType.viewAllAttr') }}
</a-button>
</div>
<AllAttrDrawer ref="allAttrDrawer" />
<CustomCodeMirror
@@ -237,6 +228,9 @@ import { getCITypeGroups } from '../../api/ciTypeGroup'
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'
@@ -245,7 +239,7 @@ require('codemirror/mode/python/python.js')
export default {
name: 'PreValueArea',
components: { draggable, PreValueTag, ColorPicker, Webhook, AttrFilter, CustomCodeMirror, AllAttrDrawer },
components: { draggable, PreValueTag, ColorPicker, Webhook, AttrFilter, CustomCodeMirror, AllAttrDrawer, PreValueDefine, PreValueBuiltIn },
props: {
disabled: {
type: Boolean,
@@ -259,12 +253,26 @@ export default {
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: '',
},
@@ -331,6 +339,10 @@ export default {
},
methods: {
async getCITypeAttributesById() {
if (!this.CITypeId) {
this.curModelAttrList = []
return
}
const res = await getCITypeAttributesById(this.CITypeId)
let curModelAttrList = []
if (res?.attributes?.length) {
@@ -339,39 +351,25 @@ export default {
this.curModelAttrList = curModelAttrList
},
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 } }])
}
}
},
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
}
},
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
@@ -394,10 +392,24 @@ export default {
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'
@@ -425,7 +437,27 @@ export default {
}
}
} else {
this.valueList = choice_value
let valueList = [[
'',
{
style: {},
icon: {},
label: ''
}
]]
if (choice_value?.length) {
valueList = choice_value.map((item) => {
return [
item[0],
{
icon: item?.[1]?.['icon'] || {},
style: item?.[1]?.['style'] || {},
label: item?.[1]?.['label'] || item?.[0] || ''
}
]
})
}
this.valueList = valueList
this.activeKey = 'define'
}
const dom = document.querySelector('#preValueArea .ant-tabs-ink-bar')
@@ -436,6 +468,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}`
@@ -462,6 +544,24 @@ export default {
</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;
@@ -481,6 +581,12 @@ export default {
line-height: 22px;
color: #a5a9bc;
}
.all-attr-btn {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
</style>
<style lang="less">

View File

@@ -47,9 +47,32 @@
>
<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-if="isChoiceByProperty(rule.property) && (rule.exp === 'is' || rule.exp === '~is')"
v-else-if="isChoiceByProperty(rule.property) && (rule.exp === 'is' || rule.exp === '~is')"
>
<treeselect
class="custom-treeselect"
@@ -64,9 +87,9 @@
:normalizer="
(node) => {
return {
id: node[0],
label: node[0],
children: node.children,
id: String(node[0] || ''),
label: node[1] ? node[1].label || node[0] : node[0],
children: node.children && node.children.length ? node.children : undefined,
}
}
"
@@ -148,9 +171,13 @@
<script>
import { compareTypeList } from '../constants.js'
import CIReferenceAttr from '@/components/ciReferenceAttr/index.vue'
export default {
name: 'ValueControls',
components: {
CIReferenceAttr
},
props: {
rule: {
type: Object,
@@ -215,7 +242,10 @@ export default {
...this.rule,
[key]: value
})
}
},
getAttr(property) {
return this.attrList.find((item) => item.name === property) || {}
},
}
}
</script>
@@ -270,4 +300,21 @@ export default {
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

@@ -39,3 +39,10 @@ export const compareTypeList = [
{ 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,309 @@
<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 { 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

@@ -1,14 +1,16 @@
<template>
<div class="relation-table" :style="{ padding: '0 20px 20px' }">
<a-button
v-if="!isInGrantComp"
style="margin-bottom: 10px"
@click="handleCreate"
type="primary"
size="small"
icon="plus"
>{{ $t('cmdb.ciType.addRelation') }}</a-button
>
<div v-if="!isInGrantComp" class="relation-table-add">
<a-button
type="primary"
@click="handleCreate"
ghost
class="ops-button-ghost"
>
<ops-icon type="veops-increase" />
{{ $t('create') }}
</a-button>
</div>
<vxe-table
ref="xTable"
stripe
@@ -179,6 +181,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 +601,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({
@@ -646,6 +655,12 @@ export default {
/deep/ .vxe-cell {
max-height: max-content !important;
}
&-add {
margin-bottom: 10px;
display: flex;
justify-content: flex-end;
}
}
.table-attribute-row {
display: inline-flex;
@@ -668,6 +683,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

@@ -1,13 +1,15 @@
<template>
<div class="ci-types-triggers">
<div style="margin-bottom: 10px">
<div class="ci-types-triggers-add">
<a-button
type="primary"
@click="handleAddTrigger"
size="small"
icon="plus"
>{{ $t('cmdb.ciType.newTrigger') }}</a-button
ghost
class="ops-button-ghost"
>
<ops-icon type="veops-increase" />
{{ $t('create') }}
</a-button>
</div>
<ops-table
stripe
@@ -134,5 +136,11 @@ export default {
<style lang="less" scoped>
.ci-types-triggers {
padding: 0 20px 20px;
&-add {
margin-bottom: 10px;
display: flex;
justify-content: flex-end;
}
}
</style>

View File

@@ -362,6 +362,7 @@ export default {
commonAttributes: [],
level2children: {},
isShadow: false,
changeCITypeRequestValue: null,
}
},
computed: {
@@ -406,7 +407,7 @@ export default {
this.visible = true
this.type = type
this.item = item
const { category = 0, name, type_id, attr_id, level } = item
const { category = 0, name, type_id, level } = item
const chartType = (item.options || {}).chartType || 'count'
const fontColor = (item.options || {}).fontColor || '#ffffff'
const bgColor = (item.options || {}).bgColor || ['#6ABFFE', '#5375EB']
@@ -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>
@@ -344,6 +344,10 @@ export default {
cursor: pointer;
flex-shrink: 0;
&:hover {
color: @primary-color;
}
&_active {
background-color: @primary-color_3;
color: @primary-color;

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>
@@ -129,7 +131,7 @@ import ModelRelationTable from './modules/modelRelationTable.vue'
import { searchResourceType } from '@/modules/acl/api/resource'
import { getCITypeGroupsConfig } from '@/modules/cmdb/api/ciTypeGroup'
import { getCITypes } from '@/modules/cmdb/api/CIType'
import { createRelation, deleteRelation, getCITypeChildren, getRelationTypes } from '@/modules/cmdb/api/CITypeRelation'
import { createRelation, deleteRelation, getRelationTypes } from '@/modules/cmdb/api/CITypeRelation'
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
import { v4 as uuidv4 } from 'uuid'
@@ -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

@@ -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()
@@ -771,7 +588,7 @@ export default {
this.calcColumns()
}
if (refreshType === 'refreshNumber') {
const promises = this.treeKeys.map((key, index) => {
this.treeKeys.map((key, index) => {
let ancestor_ids
if (
Object.keys(this.level2constraint).some(
@@ -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 || ''
@@ -1673,7 +1432,7 @@ export default {
content: (h) => <div>{that.$t('confirmDelete')}</div>,
async onOk() {
for (let i = 0; i < that.batchTreeKey.length; i++) {
const { splitTreeKey, firstCIObj, firstCIId, _tempTree, ancestor_ids } = that.calculateParamsFromTreeKey(
const { splitTreeKey, _tempTree, ancestor_ids } = that.calculateParamsFromTreeKey(
that.batchTreeKey[i],
'delete'
)
@@ -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

@@ -1,105 +1,105 @@
<template>
<a-modal
width="600px"
:bodyStyle="{
paddingTop: 0,
}"
:visible="visible"
:footer="null"
@cancel="handleCancel"
:title="$t('view')"
>
<div>
<template v-if="readCIIdFilterPermissions && readCIIdFilterPermissions.length">
<p>
<strong>{{ $t('cmdb.serviceTree.idAuthorizationPolicy') }}</strong>
<a
@click="
() => {
showAllReadCIIdFilterPermissions = !showAllReadCIIdFilterPermissions
}
"
v-if="readCIIdFilterPermissions.length > 10"
><a-icon
:type="showAllReadCIIdFilterPermissions ? 'caret-down' : 'caret-up'"
/></a>
</p>
<a-tag
v-for="item in showAllReadCIIdFilterPermissions
? readCIIdFilterPermissions
: readCIIdFilterPermissions.slice(0, 10)"
:key="item.name"
color="blue"
:style="{ marginBottom: '5px' }"
>{{ item.name }}</a-tag
>
<a-tag
:style="{ marginBottom: '5px' }"
v-if="readCIIdFilterPermissions.length > 10 && !showAllReadCIIdFilterPermissions"
>+{{ readCIIdFilterPermissions.length - 10 }}</a-tag
>
</template>
<a-empty v-else>
<img slot="image" :src="require('@/assets/data_empty.png')" />
<span slot="description"> {{ $t('noData') }} </span>
</a-empty>
</div>
</a-modal>
</template>
<script>
import { ciTypeFilterPermissions, getCIType } from '../../../api/CIType'
import FilterComp from '@/components/CMDBFilterComp'
import { searchRole } from '@/modules/acl/api/role'
export default {
name: 'ReadPermissionsModal',
components: { FilterComp },
data() {
return {
visible: false,
filerPerimissions: {},
readCIIdFilterPermissions: [],
canSearchPreferenceAttrList: [],
showAllReadCIIdFilterPermissions: false,
allRoles: [],
}
},
mounted() {
this.loadRoles()
},
methods: {
async loadRoles() {
const res = await searchRole({ page_size: 9999, app_id: 'cmdb', is_all: true })
this.allRoles = res.roles
},
async open(treeKey) {
this.visible = true
const _splitTreeKey = treeKey.split('@^@').filter((item) => !!item)
const _treeKey = _splitTreeKey.slice(_splitTreeKey.length - 1, _splitTreeKey.length)[0].split('%')
const typeId = _treeKey[1]
const _treeKeyPath = _splitTreeKey.map((item) => item.split('%')[0]).join(',')
await ciTypeFilterPermissions(typeId).then((res) => {
this.filerPerimissions = res
})
const readCIIdFilterPermissions = []
Object.entries(this.filerPerimissions).forEach(([k, v]) => {
const { id_filter } = v
if (id_filter && Object.keys(id_filter).includes(_treeKeyPath)) {
const _find = this.allRoles.find((item) => item.id === Number(k))
readCIIdFilterPermissions.push({ name: _find?.name ?? k, rid: k })
}
})
this.readCIIdFilterPermissions = readCIIdFilterPermissions
console.log(readCIIdFilterPermissions)
},
handleCancel() {
this.showAllReadCIIdFilterPermissions = false
this.visible = false
},
},
}
</script>
<style></style>
<template>
<a-modal
width="600px"
:bodyStyle="{
paddingTop: 0,
}"
:visible="visible"
:footer="null"
@cancel="handleCancel"
:title="$t('view')"
>
<div>
<template v-if="readCIIdFilterPermissions && readCIIdFilterPermissions.length">
<p>
<strong>{{ $t('cmdb.serviceTree.idAuthorizationPolicy') }}</strong>
<a
@click="
() => {
showAllReadCIIdFilterPermissions = !showAllReadCIIdFilterPermissions
}
"
v-if="readCIIdFilterPermissions.length > 10"
><a-icon
:type="showAllReadCIIdFilterPermissions ? 'caret-down' : 'caret-up'"
/></a>
</p>
<a-tag
v-for="item in showAllReadCIIdFilterPermissions
? readCIIdFilterPermissions
: readCIIdFilterPermissions.slice(0, 10)"
:key="item.name"
color="blue"
:style="{ marginBottom: '5px' }"
>{{ item.name }}</a-tag
>
<a-tag
:style="{ marginBottom: '5px' }"
v-if="readCIIdFilterPermissions.length > 10 && !showAllReadCIIdFilterPermissions"
>+{{ readCIIdFilterPermissions.length - 10 }}</a-tag
>
</template>
<a-empty v-else>
<img slot="image" :src="require('@/assets/data_empty.png')" />
<span slot="description"> {{ $t('noData') }} </span>
</a-empty>
</div>
</a-modal>
</template>
<script>
import { ciTypeFilterPermissions } from '../../../api/CIType'
import FilterComp from '@/components/CMDBFilterComp'
import { searchRole } from '@/modules/acl/api/role'
export default {
name: 'ReadPermissionsModal',
components: { FilterComp },
data() {
return {
visible: false,
filerPerimissions: {},
readCIIdFilterPermissions: [],
canSearchPreferenceAttrList: [],
showAllReadCIIdFilterPermissions: false,
allRoles: [],
}
},
mounted() {
this.loadRoles()
},
methods: {
async loadRoles() {
const res = await searchRole({ page_size: 9999, app_id: 'cmdb', is_all: true })
this.allRoles = res.roles
},
async open(treeKey) {
this.visible = true
const _splitTreeKey = treeKey.split('@^@').filter((item) => !!item)
const _treeKey = _splitTreeKey.slice(_splitTreeKey.length - 1, _splitTreeKey.length)[0].split('%')
const typeId = _treeKey[1]
const _treeKeyPath = _splitTreeKey.map((item) => item.split('%')[0]).join(',')
await ciTypeFilterPermissions(typeId).then((res) => {
this.filerPerimissions = res
})
const readCIIdFilterPermissions = []
Object.entries(this.filerPerimissions).forEach(([k, v]) => {
const { id_filter } = v
if (id_filter && Object.keys(id_filter).includes(_treeKeyPath)) {
const _find = this.allRoles.find((item) => item.id === Number(k))
readCIIdFilterPermissions.push({ name: _find?.name ?? k, rid: k })
}
})
this.readCIIdFilterPermissions = readCIIdFilterPermissions
console.log(readCIIdFilterPermissions)
},
handleCancel() {
this.showAllReadCIIdFilterPermissions = false
this.visible = false
},
},
}
</script>
<style></style>

Some files were not shown because too many files have changed in this diff Show More