mirror of
https://github.com/veops/cmdb.git
synced 2025-09-05 21:07:01 +08:00
Compare commits
105 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
03fdf5c004 | ||
|
f277cf088e | ||
|
b1f8a0024b | ||
|
aae43a53b5 | ||
|
8c2cdb1ca4 | ||
|
57d4bf5548 | ||
|
5e7c6199bf | ||
|
28dca7f086 | ||
|
4a3c21eec4 | ||
|
5d28c28023 | ||
|
ba6edb3abe | ||
|
2f1d57cee1 | ||
|
4111c634d9 | ||
|
f1fababa3d | ||
|
fe6373422e | ||
|
b3ea776886 | ||
|
c4d2ce313d | ||
|
20103a0fe6 | ||
|
394e2aeac6 | ||
|
8f7d78c26c | ||
|
7eecf3cec3 | ||
|
f6e9c443f7 | ||
|
857cbd82fd | ||
|
9a14296e02 | ||
|
f638b52759 | ||
|
78da728105 | ||
|
eb69029a51 | ||
|
07a097eba2 | ||
|
e843e3eac9 | ||
|
7308cfa6c2 | ||
|
64ea4fb21f | ||
|
e15cefaa38 | ||
|
f32339b969 | ||
|
131d213a73 | ||
|
ff98777689 | ||
|
383d4c88ed | ||
|
bb7157e292 | ||
|
b1a82f1a67 | ||
|
de86ea3852 | ||
|
bf05ea240e | ||
|
8ec0d619d7 | ||
|
61f8c463bc | ||
|
9b4dc3e43b | ||
|
9e69be8256 | ||
|
251b9e7fd5 | ||
|
f3cc12f1f9 | ||
|
56f03e1624 | ||
|
42ad2b6dde | ||
|
5aba1ff257 | ||
|
417e8fe349 | ||
|
02235d8cc0 | ||
|
00c7a644a2 | ||
|
f3e8757450 | ||
|
f0749341ba | ||
|
89da671e46 | ||
|
0e60aae076 | ||
|
4dfa97d404 | ||
|
9b778f9bc7 | ||
|
eafb5f053a | ||
|
834054e216 | ||
|
a97cabbedc | ||
|
ae77852d5f | ||
|
611ee40dca | ||
|
c0d55b2126 | ||
|
2cc4499ef9 | ||
|
1268404bca | ||
|
570a9203c4 | ||
|
adae7b5519 | ||
|
8a91ec7b11 | ||
|
92fca65383 | ||
|
4b8e6c2841 | ||
|
ab240cb003 | ||
|
61e62e4740 | ||
|
1fd72d6c78 | ||
|
51e16f6b23 | ||
|
037378e384 | ||
|
631871a8cf | ||
|
6e02f6a21f | ||
|
a2224ba2ac | ||
|
11a289aac9 | ||
|
55ab04dd28 | ||
|
256a4f4844 | ||
|
018a349336 | ||
|
8f62227adb | ||
|
de51cb3e21 | ||
|
ecb069cf14 | ||
|
937cb84393 | ||
|
40a4db06b5 | ||
|
cc98f903ea | ||
|
fb7471ce04 | ||
|
e2872f041e | ||
|
250fde127c | ||
|
73dbb14944 | ||
|
73c9a6fa72 | ||
|
09d957db79 | ||
|
b73d796891 | ||
|
e7cbd0caa9 | ||
|
3e4c385d91 | ||
|
3aac012ee9 | ||
|
78d762cacc | ||
|
c668ba7d3f | ||
|
542a876ead | ||
|
68b7497bba | ||
|
dfbf3d462d | ||
|
692708fcba |
62
.github/workflows/docker-build-and-release.yaml
vendored
62
.github/workflows/docker-build-and-release.yaml
vendored
@@ -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 }}
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -78,3 +78,4 @@ cmdb-ui/npm-debug.log*
|
||||
cmdb-ui/yarn-debug.log*
|
||||
cmdb-ui/yarn-error.log*
|
||||
cmdb-ui/package-lock.json
|
||||
start.sh
|
||||
|
@@ -11,7 +11,7 @@ click = ">=5.0"
|
||||
# Api
|
||||
Flask-RESTful = "==0.3.10"
|
||||
# Database
|
||||
Flask-SQLAlchemy = "==2.5.0"
|
||||
Flask-SQLAlchemy = "==3.0.5"
|
||||
SQLAlchemy = "==1.4.49"
|
||||
PyMySQL = "==1.1.0"
|
||||
redis = "==4.6.0"
|
||||
@@ -68,6 +68,8 @@ pycryptodomex = ">=3.19.0"
|
||||
lz4 = ">=4.3.2"
|
||||
python-magic = "==0.4.27"
|
||||
jsonpath = "==0.82.2"
|
||||
networkx = ">=3.1"
|
||||
ipaddress = ">=1.0.23"
|
||||
|
||||
[dev-packages]
|
||||
# Testing
|
||||
|
@@ -1,14 +1,13 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
|
||||
import click
|
||||
import copy
|
||||
import datetime
|
||||
import json
|
||||
import requests
|
||||
import time
|
||||
import uuid
|
||||
|
||||
import click
|
||||
import requests
|
||||
from flask import current_app
|
||||
from flask.cli import with_appcontext
|
||||
from flask_login import login_user
|
||||
@@ -37,11 +36,14 @@ from api.lib.secrets.secrets import InnerKVManger
|
||||
from api.models.acl import App
|
||||
from api.models.acl import ResourceType
|
||||
from api.models.cmdb import Attribute
|
||||
from api.models.cmdb import AttributeHistory
|
||||
from api.models.cmdb import CI
|
||||
from api.models.cmdb import CIRelation
|
||||
from api.models.cmdb import CIType
|
||||
from api.models.cmdb import CITypeTrigger
|
||||
from api.models.cmdb import OperationRecord
|
||||
from api.models.cmdb import PreferenceRelationView
|
||||
from api.tasks.cmdb import batch_ci_cache
|
||||
|
||||
|
||||
@click.command()
|
||||
@@ -557,5 +559,20 @@ def cmdb_patch(version):
|
||||
existed.update(option=option, commit=False)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
if version >= "2.4.14": # update ci columns: updated_at and updated_by
|
||||
ci_ids = []
|
||||
for i in CI.get_by(only_query=True).filter(CI.updated_at.is_(None)):
|
||||
hist = AttributeHistory.get_by(ci_id=i.id, only_query=True).order_by(AttributeHistory.id.desc()).first()
|
||||
if hist is not None:
|
||||
record = OperationRecord.get_by_id(hist.record_id)
|
||||
if record is not None:
|
||||
u = UserCache.get(record.uid)
|
||||
i.update(updated_at=record.created_at, updated_by=u and u.nickname, flush=True)
|
||||
ci_ids.append(i.id)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
batch_ci_cache.apply_async(args=(ci_ids,))
|
||||
except Exception as e:
|
||||
print("cmdb patch failed: {}".format(e))
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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,7 +587,8 @@ 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)
|
||||
|
@@ -45,6 +45,7 @@ from api.lib.notify import notify_send
|
||||
from api.lib.perm.acl.acl import ACLManager
|
||||
from api.lib.perm.acl.acl import is_app_admin
|
||||
from api.lib.perm.acl.acl import validate_permission
|
||||
from api.lib.perm.acl.cache import UserCache
|
||||
from api.lib.secrets.inner import InnerCrypt
|
||||
from api.lib.secrets.vault import VaultClient
|
||||
from api.lib.utils import handle_arg_list
|
||||
@@ -113,7 +114,8 @@ class CIManager(object):
|
||||
ci_type = CITypeCache.get(ci.type_id)
|
||||
res["ci_type"] = ci_type.name
|
||||
|
||||
res.update(cls.get_cis_by_ids([str(ci_id)], fields=fields, ret_key=ret_key))
|
||||
ci_list = cls.get_cis_by_ids([str(ci_id)], fields=fields, ret_key=ret_key)
|
||||
ci_list and res.update(ci_list[0])
|
||||
|
||||
res['_type'] = ci_type.id
|
||||
res['_id'] = ci_id
|
||||
@@ -161,7 +163,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 +172,7 @@ class CIManager(object):
|
||||
:param need_children:
|
||||
:param use_master: whether to use master db
|
||||
:param valid:
|
||||
:param enum_use_label:
|
||||
:return:
|
||||
"""
|
||||
|
||||
@@ -187,18 +190,26 @@ class CIManager(object):
|
||||
|
||||
res["ci_type"] = ci_type.name
|
||||
|
||||
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
|
||||
res['ci_type_alias'] = ci_type.alias
|
||||
res['_id'] = ci_id
|
||||
res['_updated_at'] = str(ci.updated_at or '')
|
||||
res['_updated_by'] = ci.updated_by
|
||||
|
||||
return res
|
||||
|
||||
@@ -266,7 +277,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 +303,53 @@ class CIManager(object):
|
||||
|
||||
return 1
|
||||
|
||||
@staticmethod
|
||||
def _reference_to_ci_id(attr, payload):
|
||||
def __unique_value2id(_type, _v):
|
||||
value_table = TableMap(attr_name=_type.unique_id).table
|
||||
ci = value_table.get_by(attr_id=attr.id, value=_v)
|
||||
if ci is not None:
|
||||
return ci.ci_id
|
||||
|
||||
return abort(400, ErrFormat.ci_reference_invalid.format(attr.alias, _v))
|
||||
|
||||
def __valid_reference_id_existed(_id, _type_id):
|
||||
ci = CI.get_by_id(_id) or abort(404, ErrFormat.ci_reference_not_found.format(attr.alias, _id))
|
||||
|
||||
if ci.type_id != _type_id:
|
||||
return abort(400, ErrFormat.ci_reference_invalid.format(attr.alias, _id))
|
||||
|
||||
if attr.name in payload:
|
||||
k, reference_value = attr.name, payload[attr.name]
|
||||
elif attr.alias in payload:
|
||||
k, reference_value = attr.alias, payload[attr.alias]
|
||||
else:
|
||||
return
|
||||
if not reference_value:
|
||||
return
|
||||
|
||||
reference_type = None
|
||||
if isinstance(reference_value, list):
|
||||
for idx, v in enumerate(reference_value):
|
||||
if isinstance(v, dict) and v.get('unique'):
|
||||
if reference_type is None:
|
||||
reference_type = CITypeCache.get(attr.reference_type_id)
|
||||
if reference_type is not None:
|
||||
reference_value[idx] = __unique_value2id(reference_type, v)
|
||||
else:
|
||||
__valid_reference_id_existed(v, attr.reference_type_id)
|
||||
|
||||
elif isinstance(reference_value, dict) and reference_value.get('unique'):
|
||||
if reference_type is None:
|
||||
reference_type = CITypeCache.get(attr.reference_type_id)
|
||||
if reference_type is not None:
|
||||
reference_value = __unique_value2id(reference_type, reference_value)
|
||||
elif str(reference_value).isdigit():
|
||||
reference_value = int(reference_value)
|
||||
__valid_reference_id_existed(reference_value, attr.reference_type_id)
|
||||
|
||||
payload[k] = reference_value
|
||||
|
||||
@classmethod
|
||||
def add(cls, ci_type_name,
|
||||
exist_policy=ExistPolicy.REPLACE,
|
||||
@@ -328,6 +386,7 @@ class CIManager(object):
|
||||
ci_type_attrs_name = {attr.name: attr for _, attr in attrs}
|
||||
ci_type_attrs_alias = {attr.alias: attr for _, attr in attrs}
|
||||
ci_attr2type_attr = {type_attr.attr_id: type_attr for type_attr, _ in attrs}
|
||||
ci_type_attrs_name_alias = {**ci_type_attrs_name, **ci_type_attrs_alias}
|
||||
|
||||
ci = None
|
||||
record_id = None
|
||||
@@ -391,6 +450,8 @@ class CIManager(object):
|
||||
|
||||
if attr.re_check and password_dict.get(attr.id):
|
||||
value_manager.check_re(attr.re_check, attr.alias, password_dict[attr.id][0])
|
||||
elif attr.is_reference:
|
||||
cls._reference_to_ci_id(attr, ci_dict)
|
||||
|
||||
cls._valid_unique_constraint(ci_type.id, ci_dict, ci and ci.id)
|
||||
|
||||
@@ -412,7 +473,7 @@ class CIManager(object):
|
||||
else:
|
||||
ci_dict.pop(k)
|
||||
|
||||
ci_dict = {k: v for k, v in ci_dict.items() if k in ci_type_attrs_name or k in ci_type_attrs_alias}
|
||||
ci_dict = {ci_type_attrs_name_alias[k].name: v for k, v in ci_dict.items() if k in ci_type_attrs_name_alias}
|
||||
|
||||
key2attr = value_manager.valid_attr_value(ci_dict, ci_type.id, ci and ci.id,
|
||||
ci_type_attrs_name, ci_type_attrs_alias, ci_attr2type_attr)
|
||||
@@ -442,9 +503,10 @@ class CIManager(object):
|
||||
|
||||
return ci.id
|
||||
|
||||
def update(self, ci_id, _is_admin=False, ticket_id=None, __sync=False, **ci_dict):
|
||||
def update(self, ci_id, _is_admin=False, ticket_id=None, _sync=False, **ci_dict):
|
||||
now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
ci = self.confirm_ci_existed(ci_id)
|
||||
ci_type = ci.ci_type
|
||||
|
||||
attrs = CITypeAttributeManager.get_all_attributes(ci.type_id)
|
||||
ci_type_attrs_name = {attr.name: attr for _, attr in attrs}
|
||||
@@ -474,11 +536,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)
|
||||
@@ -508,19 +572,25 @@ class CIManager(object):
|
||||
for attr_id in password_dict:
|
||||
record_id = self.save_password(ci.id, attr_id, password_dict[attr_id], record_id, ci.type_id)
|
||||
|
||||
u = UserCache.get(current_user.uid)
|
||||
ci.update(updated_at=now, updated_by=u and u.nickname)
|
||||
|
||||
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)
|
||||
|
||||
u = UserCache.get(current_user.uid)
|
||||
ci.update(updated_at=now, updated_by=u and u.nickname)
|
||||
|
||||
@staticmethod
|
||||
def update_unique_value(ci_id, unique_name, unique_value):
|
||||
ci = CI.get_by_id(ci_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(ci_id)))
|
||||
@@ -577,7 +647,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
|
||||
@@ -649,13 +719,18 @@ class CIManager(object):
|
||||
elif fields:
|
||||
_res = []
|
||||
for d in res:
|
||||
if isinstance(fields, dict) and d.get("_type") not in fields:
|
||||
_res.append(d)
|
||||
continue
|
||||
|
||||
_d = dict()
|
||||
_d["_id"], _d["_type"] = d.get("_id"), d.get("_type")
|
||||
_d["ci_type"] = d.get("ci_type")
|
||||
if unique_required:
|
||||
_d[d.get('unique')] = d.get(d.get('unique'))
|
||||
|
||||
for field in fields + ['ci_type_alias', 'unique', 'unique_alias']:
|
||||
_fields = list(fields.get(_d['_type']) or [] if isinstance(fields, dict) else fields)
|
||||
for field in _fields + ['ci_type_alias', 'unique', 'unique_alias']:
|
||||
_d[field] = d.get(field)
|
||||
_res.append(_d)
|
||||
return _res
|
||||
@@ -672,9 +747,8 @@ class CIManager(object):
|
||||
from api.lib.cmdb.search.ci.db.query_sql import QUERY_CIS_BY_IDS
|
||||
from api.lib.cmdb.search.ci.db.query_sql import QUERY_CIS_BY_VALUE_TABLE
|
||||
|
||||
if not fields:
|
||||
filter_fields_sql = ""
|
||||
else:
|
||||
if fields and isinstance(fields, list):
|
||||
_fields = list()
|
||||
for field in fields:
|
||||
attr = AttributeCache.get(field)
|
||||
@@ -716,6 +790,10 @@ class CIManager(object):
|
||||
ci_set.add(ci_id)
|
||||
res[ci2pos[ci_id]] = ci_dict
|
||||
|
||||
if isinstance(fields, dict) and fields.get(type_id):
|
||||
if attr_name not in fields[type_id]:
|
||||
continue
|
||||
|
||||
if ret_key == RetKey.NAME:
|
||||
attr_key = attr_name
|
||||
elif ret_key == RetKey.ALIAS:
|
||||
@@ -753,7 +831,7 @@ class CIManager(object):
|
||||
if not ci_ids:
|
||||
return []
|
||||
|
||||
fields = [] if fields is None or not isinstance(fields, list) else fields
|
||||
fields = [] if not fields else fields
|
||||
|
||||
ci_id_tuple = tuple(map(int, ci_ids))
|
||||
res = cls._get_cis_from_cache(ci_id_tuple, ret_key, fields, unique_required, excludes=excludes)
|
||||
@@ -772,7 +850,7 @@ class CIManager(object):
|
||||
value_table = ValueTypeMap.table[ValueTypeEnum.PASSWORD]
|
||||
if current_app.config.get('SECRETS_ENGINE') == 'inner':
|
||||
if value:
|
||||
encrypt_value, status = InnerCrypt().encrypt(value)
|
||||
encrypt_value, status = InnerCrypt().encrypt(str(value))
|
||||
if not status:
|
||||
current_app.logger.error('save password failed: {}'.format(encrypt_value))
|
||||
return abort(400, ErrFormat.password_save_failed.format(encrypt_value))
|
||||
@@ -800,7 +878,7 @@ class CIManager(object):
|
||||
vault = VaultClient(current_app.config.get('VAULT_URL'), current_app.config.get('VAULT_TOKEN'))
|
||||
if value:
|
||||
try:
|
||||
vault.update("/{}/{}".format(ci_id, attr_id), dict(v=value))
|
||||
vault.update("/{}/{}".format(ci_id, attr_id), dict(v=str(value)))
|
||||
except Exception as e:
|
||||
current_app.logger.error('save password to vault failed: {}'.format(e))
|
||||
return abort(400, ErrFormat.password_save_failed.format('write vault failed'))
|
||||
@@ -1456,7 +1534,8 @@ class CITriggerManager(object):
|
||||
cls._update_old_attr_value(record_id, ci_dict)
|
||||
|
||||
if ci_id is not None:
|
||||
ci_dict = CIManager().get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
|
||||
ci_dict = CIManager().get_ci_by_id_from_db(
|
||||
ci_id, need_children=False, use_master=False, enum_use_label=True)
|
||||
|
||||
try:
|
||||
response = webhook_request(webhook, ci_dict).text
|
||||
@@ -1483,7 +1562,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)
|
||||
|
@@ -1,6 +1,9 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
import copy
|
||||
import networkx as nx
|
||||
import toposort
|
||||
from flask import abort
|
||||
from flask import current_app
|
||||
@@ -14,12 +17,14 @@ from api.lib.cmdb.cache import AttributeCache
|
||||
from api.lib.cmdb.cache import CITypeAttributeCache
|
||||
from api.lib.cmdb.cache import CITypeAttributesCache
|
||||
from api.lib.cmdb.cache import CITypeCache
|
||||
from api.lib.cmdb.const import BuiltinModelEnum
|
||||
from api.lib.cmdb.const import CITypeOperateType
|
||||
from api.lib.cmdb.const import CMDB_QUEUE
|
||||
from api.lib.cmdb.const import ConstraintEnum
|
||||
from api.lib.cmdb.const import PermEnum
|
||||
from api.lib.cmdb.const import ResourceTypeEnum
|
||||
from api.lib.cmdb.const import RoleEnum
|
||||
from api.lib.cmdb.const import SysComputedAttributes
|
||||
from api.lib.cmdb.const import ValueTypeEnum
|
||||
from api.lib.cmdb.history import CITypeHistoryManager
|
||||
from api.lib.cmdb.perms import CIFilterPermsCRUD
|
||||
@@ -61,6 +66,7 @@ class CITypeManager(object):
|
||||
"""
|
||||
manage CIType
|
||||
"""
|
||||
|
||||
cls = CIType
|
||||
|
||||
def __init__(self):
|
||||
@@ -145,7 +151,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
|
||||
@@ -183,8 +189,11 @@ class CITypeManager(object):
|
||||
|
||||
ci_type = cls.check_is_existed(type_id)
|
||||
|
||||
if ci_type.name in BuiltinModelEnum.all() and kwargs.get('name', ci_type.name) != ci_type.name:
|
||||
return abort(400, ErrFormat.builtin_type_cannot_update_name)
|
||||
|
||||
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 +243,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 +356,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 +372,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 +513,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 +531,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 +596,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:
|
||||
@@ -821,6 +852,29 @@ class CITypeRelationManager(object):
|
||||
|
||||
return ids
|
||||
|
||||
@staticmethod
|
||||
def find_path(source_type_id, target_type_ids):
|
||||
source_type_id = int(source_type_id)
|
||||
target_type_ids = map(int, target_type_ids)
|
||||
|
||||
graph = nx.DiGraph()
|
||||
|
||||
def get_children(_id):
|
||||
children = CITypeRelation.get_by(parent_id=_id, to_dict=False)
|
||||
|
||||
for i in children:
|
||||
if i.child_id != _id:
|
||||
graph.add_edge(i.parent_id, i.child_id)
|
||||
get_children(i.child_id)
|
||||
|
||||
get_children(source_type_id)
|
||||
|
||||
paths = list(nx.all_simple_paths(graph, source_type_id, target_type_ids))
|
||||
|
||||
del graph
|
||||
|
||||
return paths
|
||||
|
||||
@staticmethod
|
||||
def _wrap_relation_type_dict(type_id, relation_inst):
|
||||
ci_type_dict = CITypeCache.get(type_id).to_dict()
|
||||
@@ -846,12 +900,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 +1004,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)
|
||||
@@ -1047,6 +1101,7 @@ class CITypeAttributeGroupManager(object):
|
||||
|
||||
@staticmethod
|
||||
def get_by_type_id(type_id, need_other=False):
|
||||
_type = CITypeCache.get(type_id) or abort(404, ErrFormat.ci_type_not_found)
|
||||
parent_ids = CITypeInheritanceManager.base(type_id)
|
||||
|
||||
groups = []
|
||||
@@ -1096,6 +1151,12 @@ class CITypeAttributeGroupManager(object):
|
||||
if i.attr_id in attr2pos:
|
||||
result[attr2pos[i.attr_id][0]]['attributes'].remove(attr2pos[i.attr_id][1])
|
||||
|
||||
if (_type.name in SysComputedAttributes.type2attr and
|
||||
attr['name'] in SysComputedAttributes.type2attr[_type.name]):
|
||||
attr['sys_computed'] = True
|
||||
else:
|
||||
attr['sys_computed'] = False
|
||||
|
||||
attr2pos[i.attr_id] = [group_pos, attr]
|
||||
|
||||
group.pop('inherited_from', None)
|
||||
@@ -1323,6 +1384,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 +1399,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 +1411,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 +1425,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 +1443,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 +1659,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 +1752,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 +1774,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:
|
||||
|
@@ -1,6 +1,8 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
|
||||
from flask_babel import lazy_gettext as _l
|
||||
|
||||
from api.lib.utils import BaseEnum
|
||||
|
||||
|
||||
@@ -14,6 +16,8 @@ class ValueTypeEnum(BaseEnum):
|
||||
JSON = "6"
|
||||
PASSWORD = TEXT
|
||||
LINK = TEXT
|
||||
BOOL = "7"
|
||||
REFERENCE = INT
|
||||
|
||||
|
||||
class ConstraintEnum(BaseEnum):
|
||||
@@ -108,17 +112,42 @@ class ExecuteStatusEnum(BaseEnum):
|
||||
FAILED = '1'
|
||||
RUNNING = '2'
|
||||
|
||||
|
||||
class RelationSourceEnum(BaseEnum):
|
||||
ATTRIBUTE_VALUES = "0"
|
||||
AUTO_DISCOVERY = "1"
|
||||
|
||||
|
||||
class BuiltinModelEnum(BaseEnum):
|
||||
IPAM_SUBNET = "ipam_subnet"
|
||||
IPAM_ADDRESS = "ipam_address"
|
||||
IPAM_SCOPE = "ipam_scope"
|
||||
|
||||
|
||||
BUILTIN_ATTRIBUTES = {
|
||||
"_updated_at": _l("Update Time"),
|
||||
"_updated_by": _l("Updated By"),
|
||||
}
|
||||
|
||||
CMDB_QUEUE = "one_cmdb_async"
|
||||
REDIS_PREFIX_CI = "ONE_CMDB"
|
||||
REDIS_PREFIX_CI_RELATION = "CMDB_CI_RELATION"
|
||||
REDIS_PREFIX_CI_RELATION2 = "CMDB_CI_RELATION2"
|
||||
|
||||
BUILTIN_KEYWORDS = {'id', '_id', 'ci_id', 'type', '_type', 'ci_type', 'ticket_id'}
|
||||
BUILTIN_KEYWORDS = {'id', '_id', 'ci_id', 'type', '_type', 'ci_type', 'ticket_id', *BUILTIN_ATTRIBUTES.keys()}
|
||||
|
||||
|
||||
class SysComputedAttributes(object):
|
||||
from api.lib.cmdb.ipam.const import SubnetBuiltinAttributes
|
||||
type2attr = {
|
||||
BuiltinModelEnum.IPAM_SUBNET: {
|
||||
SubnetBuiltinAttributes.HOSTS_COUNT,
|
||||
SubnetBuiltinAttributes.ASSIGN_COUNT,
|
||||
SubnetBuiltinAttributes.USED_COUNT,
|
||||
SubnetBuiltinAttributes.FREE_COUNT
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
L_TYPE = None
|
||||
L_CI = None
|
||||
|
@@ -10,6 +10,7 @@ from api.extensions import db
|
||||
from api.lib.cmdb.cache import AttributeCache
|
||||
from api.lib.cmdb.cache import RelationTypeCache
|
||||
from api.lib.cmdb.const import OperateType
|
||||
from api.lib.cmdb.cache import CITypeCache
|
||||
from api.lib.cmdb.perms import CIFilterPermsCRUD
|
||||
from api.lib.cmdb.resp_format import ErrFormat
|
||||
from api.lib.perm.acl.cache import UserCache
|
||||
@@ -22,6 +23,7 @@ from api.models.cmdb import CITypeHistory
|
||||
from api.models.cmdb import CITypeTrigger
|
||||
from api.models.cmdb import CITypeUniqueConstraint
|
||||
from api.models.cmdb import OperationRecord
|
||||
from api.lib.cmdb.utils import TableMap
|
||||
|
||||
|
||||
class AttributeHistoryManger(object):
|
||||
@@ -59,8 +61,23 @@ class AttributeHistoryManger(object):
|
||||
total = len(records)
|
||||
|
||||
res = {}
|
||||
show_attr_set = {}
|
||||
show_attr_cache = {}
|
||||
for record in records:
|
||||
record_id = record.OperationRecord.id
|
||||
type_id = record.OperationRecord.type_id
|
||||
ci_id = record.AttributeHistory.ci_id
|
||||
show_attr_set[ci_id] = None
|
||||
show_attr = show_attr_cache.setdefault(
|
||||
type_id,
|
||||
AttributeCache.get(
|
||||
CITypeCache.get(type_id).show_id or CITypeCache.get(type_id).unique_id) if CITypeCache.get(type_id) else None
|
||||
)
|
||||
if show_attr:
|
||||
attr_table = TableMap(attr=show_attr).table
|
||||
attr_record = attr_table.get_by(attr_id=show_attr.id, ci_id=ci_id, first=True, to_dict=False)
|
||||
show_attr_set[ci_id] = attr_record.value if attr_record else None
|
||||
|
||||
attr_hist = record.AttributeHistory.to_dict()
|
||||
attr_hist['attr'] = AttributeCache.get(attr_hist['attr_id'])
|
||||
if attr_hist['attr']:
|
||||
@@ -76,6 +93,7 @@ class AttributeHistoryManger(object):
|
||||
|
||||
if record_id not in res:
|
||||
record_dict = record.OperationRecord.to_dict()
|
||||
record_dict['show_attr_value'] = show_attr_set.get(ci_id)
|
||||
record_dict["user"] = UserCache.get(record_dict.get("uid"))
|
||||
if record_dict["user"]:
|
||||
record_dict['user'] = record_dict['user'].nickname
|
||||
|
1
cmdb-api/api/lib/cmdb/ipam/__init__.py
Normal file
1
cmdb-api/api/lib/cmdb/ipam/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# -*- coding:utf-8 -*-
|
137
cmdb-api/api/lib/cmdb/ipam/address.py
Normal file
137
cmdb-api/api/lib/cmdb/ipam/address.py
Normal file
@@ -0,0 +1,137 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
import redis_lock
|
||||
from flask import abort
|
||||
|
||||
from api.extensions import db
|
||||
from api.extensions import rd
|
||||
from api.lib.cmdb.cache import CITypeCache
|
||||
from api.lib.cmdb.ci import CIManager
|
||||
from api.lib.cmdb.ci import CIRelationManager
|
||||
from api.lib.cmdb.const import BuiltinModelEnum
|
||||
from api.lib.cmdb.ipam.const import IPAddressAssignStatus
|
||||
from api.lib.cmdb.ipam.const import IPAddressBuiltinAttributes
|
||||
from api.lib.cmdb.ipam.const import OperateTypeEnum
|
||||
from api.lib.cmdb.ipam.const import SubnetBuiltinAttributes
|
||||
from api.lib.cmdb.ipam.history import OperateHistoryManager
|
||||
from api.lib.cmdb.resp_format import ErrFormat
|
||||
from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB
|
||||
from api.lib.cmdb.search.ci_relation.search import Search as RelationSearch
|
||||
|
||||
|
||||
class IpAddressManager(object):
|
||||
def __init__(self):
|
||||
self.ci_type = CITypeCache.get(BuiltinModelEnum.IPAM_ADDRESS)
|
||||
not self.ci_type and abort(400, ErrFormat.ipam_address_model_not_found.format(
|
||||
BuiltinModelEnum.IPAM_ADDRESS))
|
||||
|
||||
self.type_id = self.ci_type.id
|
||||
|
||||
@staticmethod
|
||||
def list_ip_address(parent_id):
|
||||
numfound, _, result = CIRelationManager.get_second_cis(parent_id, per_page="all")
|
||||
|
||||
return numfound, result
|
||||
|
||||
def _get_cis(self, ips):
|
||||
response, _, _, _, _, _ = SearchFromDB(
|
||||
"_type:{},{}:({})".format(self.type_id, IPAddressBuiltinAttributes.IP, ";".join(ips or [])),
|
||||
count=10000000, parent_node_perm_passed=True).search()
|
||||
|
||||
return response
|
||||
|
||||
@staticmethod
|
||||
def _add_relation(parent_id, child_id):
|
||||
if not parent_id or not child_id:
|
||||
return
|
||||
|
||||
CIRelationManager().add(parent_id, child_id, valid=False, apply_async=False)
|
||||
|
||||
@staticmethod
|
||||
def calc_free_count(subnet_id):
|
||||
db.session.commit()
|
||||
q = "{}:(0;2),-{}:true".format(IPAddressBuiltinAttributes.ASSIGN_STATUS, IPAddressBuiltinAttributes.IS_USED)
|
||||
|
||||
return len(set(RelationSearch([subnet_id], level=[1], query=q).search(only_ids=True) or []))
|
||||
|
||||
def _update_subnet_count(self, subnet_id, assign_count, used_count=None):
|
||||
payload = {}
|
||||
|
||||
cur = CIManager.get_ci_by_id(subnet_id, need_children=False)
|
||||
if assign_count is not None:
|
||||
payload[SubnetBuiltinAttributes.ASSIGN_COUNT] = (cur.get(
|
||||
SubnetBuiltinAttributes.ASSIGN_COUNT) or 0) + assign_count
|
||||
|
||||
if used_count is not None:
|
||||
payload[SubnetBuiltinAttributes.USED_COUNT] = used_count
|
||||
|
||||
payload[SubnetBuiltinAttributes.FREE_COUNT] = (cur[SubnetBuiltinAttributes.HOSTS_COUNT] -
|
||||
self.calc_free_count(subnet_id))
|
||||
CIManager().update(subnet_id, **payload)
|
||||
|
||||
def assign_ips(self, ips, subnet_id, cidr, **kwargs):
|
||||
"""
|
||||
|
||||
:param ips: ip list
|
||||
:param subnet_id: subnet id
|
||||
:param cidr: subnet cidr
|
||||
:param kwargs: other attributes for ip address
|
||||
:return:
|
||||
"""
|
||||
if subnet_id is not None:
|
||||
subnet = CIManager.get_ci_by_id(subnet_id)
|
||||
else:
|
||||
cis, _, _, _, _, _ = SearchFromDB("_type:{},{}:{}".format(
|
||||
BuiltinModelEnum.IPAM_SUBNET, SubnetBuiltinAttributes.CIDR, cidr),
|
||||
parent_node_perm_passed=True).search()
|
||||
if cis:
|
||||
subnet = cis[0]
|
||||
subnet_id = subnet['_id']
|
||||
else:
|
||||
return abort(400, ErrFormat.ipam_address_model_not_found)
|
||||
|
||||
with (redis_lock.Lock(rd.r, "IPAM_ASSIGN_ADDRESS_{}".format(subnet_id))):
|
||||
cis = self._get_cis(ips)
|
||||
ip2ci = {ci[IPAddressBuiltinAttributes.IP]: ci for ci in cis}
|
||||
|
||||
ci_ids = []
|
||||
status_change_num = 0
|
||||
for ip in ips:
|
||||
kwargs['name'] = ip
|
||||
kwargs[IPAddressBuiltinAttributes.IP] = ip
|
||||
if ip not in ip2ci:
|
||||
ci_id = CIManager.add(self.type_id, _sync=True, **kwargs)
|
||||
status_change_num += 1
|
||||
else:
|
||||
ci_id = ip2ci[ip]['_id']
|
||||
CIManager().update(ci_id, _sync=True, **kwargs)
|
||||
if IPAddressBuiltinAttributes.ASSIGN_STATUS in kwargs and (
|
||||
(kwargs[IPAddressBuiltinAttributes.ASSIGN_STATUS] or 2) !=
|
||||
(ip2ci[ip].get(IPAddressBuiltinAttributes.ASSIGN_STATUS) or 2)):
|
||||
status_change_num += 1
|
||||
ci_ids.append(ci_id)
|
||||
|
||||
self._add_relation(subnet_id, ci_id)
|
||||
|
||||
if ips and IPAddressBuiltinAttributes.ASSIGN_STATUS in kwargs:
|
||||
self._update_subnet_count(subnet_id, -status_change_num if kwargs.get(
|
||||
IPAddressBuiltinAttributes.ASSIGN_STATUS) == IPAddressAssignStatus.UNASSIGNED else status_change_num)
|
||||
|
||||
if ips and IPAddressBuiltinAttributes.IS_USED in kwargs:
|
||||
q = "{}:true".format(IPAddressBuiltinAttributes.IS_USED)
|
||||
cur_used_ids = RelationSearch([subnet_id], level=[1], query=q).search(only_ids=True)
|
||||
for _id in set(cur_used_ids) - set(ci_ids):
|
||||
CIManager().update(_id, _sync=True, **{IPAddressBuiltinAttributes.IS_USED: False})
|
||||
|
||||
self._update_subnet_count(subnet_id, None, used_count=len(ips))
|
||||
|
||||
if kwargs.get(IPAddressBuiltinAttributes.ASSIGN_STATUS) in (
|
||||
IPAddressAssignStatus.ASSIGNED, IPAddressAssignStatus.RESERVED):
|
||||
OperateHistoryManager().add(operate_type=OperateTypeEnum.ASSIGN_ADDRESS,
|
||||
cidr=subnet.get(SubnetBuiltinAttributes.CIDR),
|
||||
description=" | ".join(ips))
|
||||
|
||||
elif kwargs.get(IPAddressBuiltinAttributes.ASSIGN_STATUS) == IPAddressAssignStatus.UNASSIGNED:
|
||||
OperateHistoryManager().add(operate_type=OperateTypeEnum.REVOKE_ADDRESS,
|
||||
cidr=subnet.get(SubnetBuiltinAttributes.CIDR),
|
||||
description=" | ".join(ips))
|
35
cmdb-api/api/lib/cmdb/ipam/const.py
Normal file
35
cmdb-api/api/lib/cmdb/ipam/const.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from api.lib.utils import BaseEnum
|
||||
|
||||
|
||||
class IPAddressAssignStatus(BaseEnum):
|
||||
ASSIGNED = 0
|
||||
UNASSIGNED = 1
|
||||
RESERVED = 2
|
||||
|
||||
|
||||
class OperateTypeEnum(BaseEnum):
|
||||
ADD_SCOPE = "0"
|
||||
UPDATE_SCOPE = "1"
|
||||
DELETE_SCOPE = "2"
|
||||
ADD_SUBNET = "3"
|
||||
UPDATE_SUBNET = "4"
|
||||
DELETE_SUBNET = "5"
|
||||
ASSIGN_ADDRESS = "6"
|
||||
REVOKE_ADDRESS = "7"
|
||||
|
||||
|
||||
class SubnetBuiltinAttributes(BaseEnum):
|
||||
NAME = 'name'
|
||||
CIDR = 'cidr'
|
||||
HOSTS_COUNT = 'hosts_count'
|
||||
ASSIGN_COUNT = 'assign_count'
|
||||
USED_COUNT = 'used_count'
|
||||
FREE_COUNT = 'free_count'
|
||||
|
||||
|
||||
class IPAddressBuiltinAttributes(BaseEnum):
|
||||
IP = 'ip'
|
||||
ASSIGN_STATUS = 'assign_status' # enum: 0 - assigned 1 - unassigned 2 - reserved
|
||||
IS_USED = 'is_used' # bool
|
57
cmdb-api/api/lib/cmdb/ipam/history.py
Normal file
57
cmdb-api/api/lib/cmdb/ipam/history.py
Normal file
@@ -0,0 +1,57 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from flask_login import current_user
|
||||
|
||||
from api.lib.cmdb.ipam.const import IPAddressBuiltinAttributes
|
||||
from api.lib.mixin import DBMixin
|
||||
from api.models.cmdb import IPAMOperationHistory
|
||||
from api.models.cmdb import IPAMSubnetScan
|
||||
from api.models.cmdb import IPAMSubnetScanHistory
|
||||
|
||||
|
||||
class OperateHistoryManager(DBMixin):
|
||||
cls = IPAMOperationHistory
|
||||
|
||||
def _can_add(self, **kwargs):
|
||||
kwargs['uid'] = current_user.uid
|
||||
|
||||
return kwargs
|
||||
|
||||
def _can_update(self, **kwargs):
|
||||
pass
|
||||
|
||||
def _can_delete(self, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class ScanHistoryManager(DBMixin):
|
||||
cls = IPAMSubnetScanHistory
|
||||
|
||||
def _can_add(self, **kwargs):
|
||||
return kwargs
|
||||
|
||||
def add(self, **kwargs):
|
||||
kwargs.pop('_key', None)
|
||||
kwargs.pop('_secret', None)
|
||||
ci_id = kwargs.pop('ci_id', None)
|
||||
|
||||
existed = self.cls.get_by(exec_id=kwargs['exec_id'], first=True, to_dict=False)
|
||||
if existed is None:
|
||||
self.cls.create(**kwargs)
|
||||
else:
|
||||
existed.update(**kwargs)
|
||||
|
||||
if kwargs.get('ips'):
|
||||
from api.lib.cmdb.ipam.address import IpAddressManager
|
||||
IpAddressManager().assign_ips(kwargs['ips'], None, kwargs.get('cidr'),
|
||||
**{IPAddressBuiltinAttributes.IS_USED: 1})
|
||||
|
||||
scan_rule = IPAMSubnetScan.get_by(ci_id=ci_id, first=True, to_dict=False)
|
||||
if scan_rule is not None:
|
||||
scan_rule.update(last_scan_time=kwargs.get('start_at'))
|
||||
|
||||
def _can_update(self, **kwargs):
|
||||
pass
|
||||
|
||||
def _can_delete(self, **kwargs):
|
||||
pass
|
104
cmdb-api/api/lib/cmdb/ipam/stats.py
Normal file
104
cmdb-api/api/lib/cmdb/ipam/stats.py
Normal file
@@ -0,0 +1,104 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
|
||||
import json
|
||||
from flask import abort
|
||||
|
||||
from api.extensions import rd
|
||||
from api.lib.cmdb.cache import CITypeCache
|
||||
from api.lib.cmdb.ci import CIManager
|
||||
from api.lib.cmdb.const import BuiltinModelEnum
|
||||
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
|
||||
from api.lib.cmdb.resp_format import ErrFormat
|
||||
from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB
|
||||
from api.models.cmdb import CI
|
||||
from api.models.cmdb import CIRelation
|
||||
from api.models.cmdb import IPAMSubnetScan
|
||||
|
||||
|
||||
class Stats(object):
|
||||
def __init__(self):
|
||||
self.address_type = CITypeCache.get(BuiltinModelEnum.IPAM_ADDRESS)
|
||||
not self.address_type and abort(400, ErrFormat.ipam_address_model_not_found.format(
|
||||
BuiltinModelEnum.IPAM_ADDRESS))
|
||||
|
||||
self.address_type_id = self.address_type.id
|
||||
|
||||
self.subnet_type = CITypeCache.get(BuiltinModelEnum.IPAM_SUBNET)
|
||||
not self.subnet_type and abort(400, ErrFormat.ipam_address_model_not_found.format(
|
||||
BuiltinModelEnum.IPAM_ADDRESS))
|
||||
|
||||
self.subnet_type_id = self.subnet_type.id
|
||||
|
||||
def leaf_nodes(self, parent_id):
|
||||
if str(parent_id) == '0': # all
|
||||
ci_ids = [i.id for i in CI.get_by(type_id=self.subnet_type_id, to_dict=False)]
|
||||
has_children_ci_ids = [i.first_ci_id for i in CIRelation.get_by(
|
||||
only_query=True).join(CI, CIRelation.second_ci_id == CI.id).filter(
|
||||
CIRelation.first_ci_id.in_(ci_ids)).filter(CI.type_id == self.subnet_type_id)]
|
||||
|
||||
return list(set(ci_ids) - set(has_children_ci_ids))
|
||||
|
||||
else:
|
||||
type_id = CIManager().get_by_id(parent_id).type_id
|
||||
key = [(str(parent_id), type_id)]
|
||||
result = []
|
||||
while True:
|
||||
res = [json.loads(x).items() for x in [i or '{}' for i in rd.get(
|
||||
[i[0] for i in key], REDIS_PREFIX_CI_RELATION) or []]]
|
||||
|
||||
for idx, i in enumerate(res):
|
||||
if (not i or list(i)[0][1] == self.address_type_id) and key[idx][1] == self.subnet_type_id:
|
||||
result.append(int(key[idx][0]))
|
||||
|
||||
res = [j for i in res for j in i] # [(id, type_id)]
|
||||
|
||||
if not res:
|
||||
return result
|
||||
|
||||
key = res
|
||||
|
||||
def statistic_subnets(self, subnet_ids):
|
||||
if subnet_ids:
|
||||
response, _, _, _, _, _ = SearchFromDB(
|
||||
"_type:{}".format(self.subnet_type_id),
|
||||
ci_ids=subnet_ids,
|
||||
count=1000000,
|
||||
parent_node_perm_passed=True,
|
||||
).search()
|
||||
else:
|
||||
response = []
|
||||
|
||||
scans = IPAMSubnetScan.get_by(only_query=True).filter(IPAMSubnetScan.ci_id.in_(list(map(int, subnet_ids))))
|
||||
id2scan = {i.ci_id: i for i in scans}
|
||||
|
||||
address_num, address_free_num, address_assign_num, address_used_num = 0, 0, 0, 0
|
||||
for subnet in response:
|
||||
address_num += (subnet.get('hosts_count') or 0)
|
||||
address_free_num += (subnet.get('free_count') or 0)
|
||||
address_assign_num += (subnet.get('assign_count') or 0)
|
||||
address_used_num += (subnet.get('used_count') or 0)
|
||||
|
||||
if id2scan.get(subnet['_id']):
|
||||
subnet['scan_enabled'] = id2scan[subnet['_id']].scan_enabled
|
||||
subnet['last_scan_time'] = id2scan[subnet['_id']].last_scan_time
|
||||
else:
|
||||
subnet['scan_enabled'] = False
|
||||
subnet['last_scan_time'] = None
|
||||
|
||||
return response, address_num, address_free_num, address_assign_num, address_used_num
|
||||
|
||||
def summary(self, parent_id):
|
||||
subnet_ids = self.leaf_nodes(parent_id)
|
||||
|
||||
subnets, address_num, address_free_num, address_assign_num, address_used_num = (
|
||||
self.statistic_subnets(subnet_ids))
|
||||
|
||||
return dict(subnet_num=len(subnets),
|
||||
address_num=address_num,
|
||||
address_free_num=address_free_num,
|
||||
address_assign_num=address_assign_num,
|
||||
address_unassign_num=address_num - address_assign_num,
|
||||
address_used_num=address_used_num,
|
||||
address_used_free_num=address_num - address_used_num,
|
||||
subnets=subnets)
|
344
cmdb-api/api/lib/cmdb/ipam/subnet.py
Normal file
344
cmdb-api/api/lib/cmdb/ipam/subnet.py
Normal file
@@ -0,0 +1,344 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
import ipaddress
|
||||
from flask import abort
|
||||
|
||||
from api.lib.cmdb.cache import AttributeCache
|
||||
from api.lib.cmdb.cache import CITypeCache
|
||||
from api.lib.cmdb.ci import CIManager
|
||||
from api.lib.cmdb.ci import CIRelationManager
|
||||
from api.lib.cmdb.const import BuiltinModelEnum, BUILTIN_ATTRIBUTES
|
||||
from api.lib.cmdb.ipam.const import OperateTypeEnum
|
||||
from api.lib.cmdb.ipam.const import SubnetBuiltinAttributes
|
||||
from api.lib.cmdb.ipam.history import OperateHistoryManager
|
||||
from api.lib.cmdb.resp_format import ErrFormat
|
||||
from api.lib.cmdb.search.ci.db.search import Search as SearchFromDB
|
||||
from api.models.cmdb import CI
|
||||
from api.models.cmdb import CIRelation
|
||||
from api.models.cmdb import IPAMSubnetScan
|
||||
|
||||
|
||||
class SubnetManager(object):
|
||||
def __init__(self):
|
||||
self.ci_type = CITypeCache.get(BuiltinModelEnum.IPAM_SUBNET)
|
||||
not self.ci_type and abort(400, ErrFormat.ipam_subnet_model_not_found.format(
|
||||
BuiltinModelEnum.IPAM_SUBNET))
|
||||
|
||||
self.type_id = self.ci_type.id
|
||||
|
||||
def scan_rules(self, oneagent_id, last_update_at=None):
|
||||
result = []
|
||||
rules = IPAMSubnetScan.get_by(agent_id=oneagent_id, to_dict=True)
|
||||
ci_ids = [i['ci_id'] for i in rules]
|
||||
if ci_ids:
|
||||
response, _, _, _, _, _ = SearchFromDB("_type:{}".format(self.type_id),
|
||||
ci_ids=list(ci_ids),
|
||||
count=1000000,
|
||||
fl=[SubnetBuiltinAttributes.CIDR],
|
||||
parent_node_perm_passed=True).search()
|
||||
id2ci = {i['_id']: i for i in response}
|
||||
|
||||
for rule in rules:
|
||||
if rule['ci_id'] in id2ci:
|
||||
rule[SubnetBuiltinAttributes.CIDR] = id2ci[rule['ci_id']][SubnetBuiltinAttributes.CIDR]
|
||||
result.append(rule)
|
||||
|
||||
new_last_update_at = ""
|
||||
for i in result:
|
||||
__last_update_at = max([i['updated_at'] or "", i['created_at'] or ""])
|
||||
if new_last_update_at < __last_update_at:
|
||||
new_last_update_at = __last_update_at
|
||||
|
||||
if not last_update_at or new_last_update_at > last_update_at:
|
||||
return result, new_last_update_at
|
||||
else:
|
||||
return [], new_last_update_at
|
||||
|
||||
@staticmethod
|
||||
def get_hosts(cidr):
|
||||
try:
|
||||
return list(map(str, ipaddress.ip_network(cidr).hosts()))
|
||||
except ValueError:
|
||||
return []
|
||||
|
||||
def get_by_id(self, subnet_id):
|
||||
response, _, _, _, _, _ = SearchFromDB("_type:{}".format(self.type_id),
|
||||
ci_ids=[subnet_id],
|
||||
parent_node_perm_passed=True).search()
|
||||
scan_rule = IPAMSubnetScan.get_by(ci_id=subnet_id, first=True, to_dict=True)
|
||||
if scan_rule and response:
|
||||
scan_rule.update(response[0])
|
||||
|
||||
return scan_rule
|
||||
|
||||
def tree_view(self):
|
||||
scope = CITypeCache.get(BuiltinModelEnum.IPAM_SCOPE)
|
||||
ci_types = scope and [scope.id, self.type_id] or [self.type_id]
|
||||
|
||||
relations = defaultdict(set)
|
||||
ids = set()
|
||||
has_parent_ids = set()
|
||||
for i in CIRelation.get_by(only_query=True).join(
|
||||
CI, CI.id == CIRelation.first_ci_id).filter(CI.type_id.in_(ci_types)):
|
||||
relations[i.first_ci_id].add(i.second_ci_id)
|
||||
ids.add(i.first_ci_id)
|
||||
ids.add(i.second_ci_id)
|
||||
has_parent_ids.add(i.second_ci_id)
|
||||
for i in CIRelation.get_by(only_query=True).join(
|
||||
CI, CI.id == CIRelation.second_ci_id).filter(CI.type_id.in_(ci_types)):
|
||||
relations[i.first_ci_id].add(i.second_ci_id)
|
||||
ids.add(i.first_ci_id)
|
||||
ids.add(i.second_ci_id)
|
||||
has_parent_ids.add(i.second_ci_id)
|
||||
|
||||
for i in CI.get_by(only_query=True).filter(CI.type_id.in_(ci_types)):
|
||||
ids.add(i.id)
|
||||
|
||||
for _id in ids:
|
||||
if _id not in has_parent_ids:
|
||||
relations[None].add(_id)
|
||||
|
||||
type2name = dict()
|
||||
type2name[self.type_id] = AttributeCache.get(self.ci_type.show_id or self.ci_type.unique_id).name
|
||||
|
||||
fl = [type2name[self.type_id]]
|
||||
if scope:
|
||||
type2name[scope.id] = AttributeCache.get(scope.show_id or scope.unique_id).name
|
||||
fl.append(type2name[scope.id])
|
||||
|
||||
response, _, _, _, _, _ = SearchFromDB("_type:({})".format(";".join(map(str, ci_types))),
|
||||
ci_ids=list(ids),
|
||||
count=1000000,
|
||||
fl=list(set(fl + [SubnetBuiltinAttributes.CIDR])),
|
||||
parent_node_perm_passed=True).search()
|
||||
id2ci = {i['_id']: i for i in response}
|
||||
|
||||
def _build_tree(_tree, parent_id=None):
|
||||
tree = []
|
||||
for child_id in _tree.get(parent_id, []):
|
||||
children = sorted(_build_tree(_tree, child_id), key=lambda x: x['_id'])
|
||||
if not id2ci.get(child_id):
|
||||
continue
|
||||
tree.append({'children': children, **id2ci[child_id]})
|
||||
return tree
|
||||
|
||||
result = sorted(_build_tree(relations), key=lambda x: x['_id'])
|
||||
|
||||
return result, type2name
|
||||
|
||||
@staticmethod
|
||||
def _is_valid_cidr(cidr):
|
||||
try:
|
||||
return str(ipaddress.ip_network(cidr))
|
||||
except ValueError:
|
||||
return abort(400, ErrFormat.ipam_cidr_invalid_notation.format(cidr))
|
||||
|
||||
def _check_root_node_is_overlapping(self, cidr, _id=None):
|
||||
none_root_nodes = [i.id for i in CI.get_by(only_query=True).join(
|
||||
CIRelation, CIRelation.second_ci_id == CI.id).filter(CI.type_id == self.type_id)]
|
||||
all_nodes = [i.id for i in CI.get_by(type_id=self.type_id, to_dict=False, fl=['id'])]
|
||||
|
||||
root_nodes = set(all_nodes) - set(none_root_nodes) - set(_id and [_id] or [])
|
||||
response, _, _, _, _, _ = SearchFromDB("_type:{}".format(self.type_id),
|
||||
ci_ids=list(root_nodes),
|
||||
parent_node_perm_passed=True).search()
|
||||
|
||||
cur_subnet = ipaddress.ip_network(cidr)
|
||||
for item in response:
|
||||
if item['_id'] == _id:
|
||||
continue
|
||||
|
||||
if cur_subnet.overlaps(ipaddress.ip_network(item.get(SubnetBuiltinAttributes.CIDR))):
|
||||
return abort(400, ErrFormat.ipam_subnet_overlapped.format(cidr, item.get(SubnetBuiltinAttributes.CIDR)))
|
||||
|
||||
return cidr
|
||||
|
||||
def _check_child_node_is_overlapping(self, parent_id, cidr, _id=None):
|
||||
child_nodes = [i.second_ci_id for i in CIRelation.get_by(
|
||||
first_ci_id=parent_id, to_dict=False, fl=['second_ci_id']) if i.second_ci_id != _id]
|
||||
if not child_nodes:
|
||||
return
|
||||
|
||||
response, _, _, _, _, _ = SearchFromDB("_type:{}".format(self.type_id),
|
||||
ci_ids=list(child_nodes),
|
||||
parent_node_perm_passed=True).search()
|
||||
|
||||
cur_subnet = ipaddress.ip_network(cidr)
|
||||
for item in response:
|
||||
if item['_id'] == _id:
|
||||
continue
|
||||
|
||||
if cur_subnet.overlaps(ipaddress.ip_network(item.get(SubnetBuiltinAttributes.CIDR))):
|
||||
return abort(400, ErrFormat.ipam_subnet_overlapped.format(cidr, item.get(SubnetBuiltinAttributes.CIDR)))
|
||||
|
||||
def validate_cidr(self, parent_id, cidr, _id=None):
|
||||
cidr = self._is_valid_cidr(cidr)
|
||||
|
||||
if not parent_id:
|
||||
return self._check_root_node_is_overlapping(cidr, _id)
|
||||
|
||||
parent_subnet = CIManager().get_ci_by_id(parent_id, need_children=False)
|
||||
if parent_subnet['ci_type'] == BuiltinModelEnum.IPAM_SUBNET:
|
||||
if parent_subnet.get(SubnetBuiltinAttributes.CIDR):
|
||||
prefix = int(cidr.split('/')[1])
|
||||
if int(parent_subnet[SubnetBuiltinAttributes.CIDR].split('/')[1]) >= prefix:
|
||||
return abort(400, ErrFormat.ipam_subnet_prefix_length_invalid.format(prefix))
|
||||
|
||||
valid_subnets = [str(i) for i in
|
||||
ipaddress.ip_network(parent_subnet[SubnetBuiltinAttributes.CIDR]).subnets(
|
||||
new_prefix=prefix)]
|
||||
if cidr not in valid_subnets:
|
||||
return abort(400, ErrFormat.ipam_cidr_invalid_subnet.format(cidr, valid_subnets))
|
||||
else:
|
||||
return abort(400, ErrFormat.ipam_parent_subnet_node_cidr_cannot_empty)
|
||||
|
||||
self._check_child_node_is_overlapping(parent_id, cidr, _id)
|
||||
|
||||
return cidr
|
||||
|
||||
def _add_subnet(self, cidr, **kwargs):
|
||||
kwargs[SubnetBuiltinAttributes.HOSTS_COUNT] = ipaddress.ip_network(cidr).num_addresses - 2
|
||||
kwargs[SubnetBuiltinAttributes.USED_COUNT] = 0
|
||||
kwargs[SubnetBuiltinAttributes.ASSIGN_COUNT] = 0
|
||||
kwargs[SubnetBuiltinAttributes.FREE_COUNT] = kwargs[SubnetBuiltinAttributes.HOSTS_COUNT]
|
||||
|
||||
return CIManager().add(self.type_id, cidr=cidr, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def _add_scan_rule(ci_id, agent_id, cron, scan_enabled=True):
|
||||
IPAMSubnetScan.create(ci_id=ci_id, agent_id=agent_id, cron=cron, scan_enabled=scan_enabled)
|
||||
|
||||
@staticmethod
|
||||
def _add_relation(parent_id, child_id):
|
||||
if not parent_id or not child_id:
|
||||
return
|
||||
|
||||
CIRelationManager().add(parent_id, child_id, valid=False)
|
||||
|
||||
def add(self, cidr, parent_id, agent_id, cron, scan_enabled=True, **kwargs):
|
||||
cidr = self.validate_cidr(parent_id, cidr)
|
||||
|
||||
ci_id = self._add_subnet(cidr, **kwargs)
|
||||
|
||||
self._add_scan_rule(ci_id, agent_id, cron, scan_enabled)
|
||||
|
||||
self._add_relation(parent_id, ci_id)
|
||||
|
||||
OperateHistoryManager().add(operate_type=OperateTypeEnum.ADD_SUBNET,
|
||||
cidr=cidr,
|
||||
description=cidr)
|
||||
|
||||
return ci_id
|
||||
|
||||
@staticmethod
|
||||
def _update_subnet(_id, **kwargs):
|
||||
return CIManager().update(_id, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def _update_scan_rule(ci_id, agent_id, cron, scan_enabled=True):
|
||||
existed = IPAMSubnetScan.get_by(ci_id=ci_id, first=True, to_dict=False)
|
||||
if existed is not None:
|
||||
existed.update(ci_id=ci_id, agent_id=agent_id, cron=cron, scan_enabled=scan_enabled)
|
||||
else:
|
||||
IPAMSubnetScan.create(ci_id=ci_id, agent_id=agent_id, cron=cron, scan_enabled=scan_enabled)
|
||||
|
||||
def update(self, _id, **kwargs):
|
||||
kwargs[SubnetBuiltinAttributes.CIDR] = self.validate_cidr(kwargs.pop('parent_id', None),
|
||||
kwargs.get(SubnetBuiltinAttributes.CIDR), _id)
|
||||
|
||||
agent_id = kwargs.pop('agent_id', None)
|
||||
cron = kwargs.pop('cron', None)
|
||||
scan_enabled = kwargs.pop('scan_enabled', True)
|
||||
|
||||
cur = CIManager.get_ci_by_id(_id, need_children=False)
|
||||
|
||||
self._update_subnet(_id, **kwargs)
|
||||
|
||||
self._update_scan_rule(_id, agent_id, cron, scan_enabled)
|
||||
|
||||
OperateHistoryManager().add(operate_type=OperateTypeEnum.UPDATE_SUBNET,
|
||||
cidr=cur.get(SubnetBuiltinAttributes.CIDR),
|
||||
description="{} -> {}".format(cur.get(SubnetBuiltinAttributes.CIDR),
|
||||
kwargs.get(SubnetBuiltinAttributes.CIDR)))
|
||||
|
||||
return _id
|
||||
|
||||
@classmethod
|
||||
def delete(cls, _id):
|
||||
if CIRelation.get_by(only_query=True).filter(CIRelation.first_ci_id == _id).first():
|
||||
return abort(400, ErrFormat.ipam_subnet_cannot_delete)
|
||||
|
||||
existed = IPAMSubnetScan.get_by(ci_id=_id, first=True, to_dict=False)
|
||||
existed and existed.delete()
|
||||
|
||||
for i in CIRelation.get_by(first_ci_id=_id, to_dict=False):
|
||||
i.delete()
|
||||
|
||||
cur = CIManager.get_ci_by_id(_id, need_children=False)
|
||||
|
||||
CIManager().delete(_id)
|
||||
|
||||
OperateHistoryManager().add(operate_type=OperateTypeEnum.DELETE_SUBNET,
|
||||
cidr=cur.get(SubnetBuiltinAttributes.CIDR),
|
||||
description=cur.get(SubnetBuiltinAttributes.CIDR))
|
||||
|
||||
return _id
|
||||
|
||||
|
||||
class SubnetScopeManager(object):
|
||||
def __init__(self):
|
||||
self.ci_type = CITypeCache.get(BuiltinModelEnum.IPAM_SCOPE)
|
||||
not self.ci_type and abort(400, ErrFormat.ipam_subnet_model_not_found.format(
|
||||
BuiltinModelEnum.IPAM_SCOPE))
|
||||
|
||||
self.type_id = self.ci_type.id
|
||||
|
||||
def _add_scope(self, name):
|
||||
return CIManager().add(self.type_id, name=name)
|
||||
|
||||
@staticmethod
|
||||
def _add_relation(parent_id, child_id):
|
||||
if not parent_id or not child_id:
|
||||
return
|
||||
|
||||
CIRelationManager().add(parent_id, child_id, valid=False)
|
||||
|
||||
def add(self, parent_id, name):
|
||||
ci_id = self._add_scope(name)
|
||||
|
||||
self._add_relation(parent_id, ci_id)
|
||||
|
||||
OperateHistoryManager().add(operate_type=OperateTypeEnum.ADD_SCOPE,
|
||||
description=name)
|
||||
|
||||
return ci_id
|
||||
|
||||
@staticmethod
|
||||
def _update_scope(_id, name):
|
||||
return CIManager().update(_id, name=name)
|
||||
|
||||
def update(self, _id, name):
|
||||
cur = CIManager.get_ci_by_id(_id, need_children=False)
|
||||
|
||||
res = self._update_scope(_id, name)
|
||||
|
||||
OperateHistoryManager().add(operate_type=OperateTypeEnum.UPDATE_SCOPE,
|
||||
description="{} -> {}".format(cur.get('name'), name))
|
||||
|
||||
return res
|
||||
|
||||
@staticmethod
|
||||
def delete(_id):
|
||||
if CIRelation.get_by(first_ci_id=_id, first=True, to_dict=False):
|
||||
return abort(400, ErrFormat.ipam_scope_cannot_delete)
|
||||
|
||||
cur = CIManager.get_ci_by_id(_id, need_children=False)
|
||||
|
||||
CIManager().delete(_id)
|
||||
|
||||
OperateHistoryManager().add(operate_type=OperateTypeEnum.DELETE_SCOPE,
|
||||
description=cur.get('name'))
|
||||
|
||||
return _id
|
@@ -1,7 +1,6 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
import copy
|
||||
import functools
|
||||
|
||||
import redis_lock
|
||||
from flask import abort
|
||||
from flask import current_app
|
||||
@@ -10,6 +9,7 @@ from flask_login import current_user
|
||||
|
||||
from api.extensions import db
|
||||
from api.extensions import rd
|
||||
from api.lib.cmdb.const import BUILTIN_ATTRIBUTES
|
||||
from api.lib.cmdb.const import ResourceTypeEnum
|
||||
from api.lib.cmdb.resp_format import ErrFormat
|
||||
from api.lib.mixin import DBMixin
|
||||
@@ -27,7 +27,7 @@ class CIFilterPermsCRUD(DBMixin):
|
||||
result = {}
|
||||
for i in res:
|
||||
if i['attr_filter']:
|
||||
i['attr_filter'] = i['attr_filter'].split(',')
|
||||
i['attr_filter'] = i['attr_filter'].split(',') + list(BUILTIN_ATTRIBUTES.keys())
|
||||
|
||||
if i['rid'] not in result:
|
||||
result[i['rid']] = i
|
||||
@@ -62,7 +62,7 @@ class CIFilterPermsCRUD(DBMixin):
|
||||
result = {}
|
||||
for i in res:
|
||||
if i['attr_filter']:
|
||||
i['attr_filter'] = i['attr_filter'].split(',')
|
||||
i['attr_filter'] = i['attr_filter'].split(',') + list(BUILTIN_ATTRIBUTES.keys())
|
||||
|
||||
if i['type_id'] not in result:
|
||||
result[i['type_id']] = i
|
||||
|
@@ -1,8 +1,8 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
import copy
|
||||
|
||||
import six
|
||||
import toposort
|
||||
from flask import abort
|
||||
@@ -16,15 +16,16 @@ from api.lib.cmdb.cache import CITypeAttributesCache
|
||||
from api.lib.cmdb.cache import CITypeCache
|
||||
from api.lib.cmdb.cache import CMDBCounterCache
|
||||
from api.lib.cmdb.ci_type import CITypeAttributeManager
|
||||
from api.lib.cmdb.const import BUILTIN_ATTRIBUTES
|
||||
from api.lib.cmdb.const import ConstraintEnum
|
||||
from api.lib.cmdb.const import PermEnum
|
||||
from api.lib.cmdb.const import ResourceTypeEnum
|
||||
from api.lib.cmdb.const import RoleEnum
|
||||
from api.lib.cmdb.const import SysComputedAttributes
|
||||
from api.lib.cmdb.perms import CIFilterPermsCRUD
|
||||
from api.lib.cmdb.resp_format import ErrFormat
|
||||
from api.lib.exception import AbortException
|
||||
from api.lib.perm.acl.acl import ACLManager
|
||||
from api.models.cmdb import CITypeAttribute
|
||||
from api.models.cmdb import CITypeGroup
|
||||
from api.models.cmdb import CITypeGroupItem
|
||||
from api.models.cmdb import CITypeRelation
|
||||
@@ -132,21 +133,24 @@ class PreferenceManager(object):
|
||||
|
||||
@staticmethod
|
||||
def get_show_attributes(type_id):
|
||||
_type = CITypeCache.get(type_id) or abort(404, ErrFormat.ci_type_not_found)
|
||||
type_id = _type and _type.id
|
||||
|
||||
if not isinstance(type_id, six.integer_types):
|
||||
_type = CITypeCache.get(type_id)
|
||||
type_id = _type and _type.id
|
||||
|
||||
attrs = db.session.query(PreferenceShowAttributes, CITypeAttribute.order).join(
|
||||
CITypeAttribute, CITypeAttribute.attr_id == PreferenceShowAttributes.attr_id).filter(
|
||||
PreferenceShowAttributes.uid == current_user.uid).filter(
|
||||
PreferenceShowAttributes.type_id == type_id).filter(
|
||||
PreferenceShowAttributes.deleted.is_(False)).filter(CITypeAttribute.deleted.is_(False)).group_by(
|
||||
CITypeAttribute.attr_id).all()
|
||||
attrs = PreferenceShowAttributes.get_by(uid=current_user.uid, type_id=type_id, to_dict=False)
|
||||
|
||||
result = []
|
||||
for i in sorted(attrs, key=lambda x: x.PreferenceShowAttributes.order):
|
||||
item = i.PreferenceShowAttributes.attr.to_dict()
|
||||
item.update(dict(is_fixed=i.PreferenceShowAttributes.is_fixed))
|
||||
for i in sorted(attrs, key=lambda x: x.order):
|
||||
if i.attr_id:
|
||||
item = i.attr.to_dict()
|
||||
elif i.builtin_attr:
|
||||
item = dict(name=i.builtin_attr, alias=BUILTIN_ATTRIBUTES[i.builtin_attr])
|
||||
else:
|
||||
item = dict(name="", alias="")
|
||||
item.update(dict(is_fixed=i.is_fixed))
|
||||
result.append(item)
|
||||
|
||||
is_subscribed = True
|
||||
@@ -155,13 +159,23 @@ class PreferenceManager(object):
|
||||
choice_web_hook_parse=False,
|
||||
choice_other_parse=False)
|
||||
result = [i for i in result if i['default_show']]
|
||||
|
||||
for i in BUILTIN_ATTRIBUTES:
|
||||
result.append(dict(name=i, alias=BUILTIN_ATTRIBUTES[i]))
|
||||
|
||||
is_subscribed = False
|
||||
|
||||
for i in result:
|
||||
if i["is_choice"]:
|
||||
if i.get("is_choice"):
|
||||
i.update(dict(choice_value=AttributeManager.get_choice_values(
|
||||
i["id"], i["value_type"], i.get("choice_web_hook"), i.get("choice_other"))))
|
||||
|
||||
if (_type.name in SysComputedAttributes.type2attr and
|
||||
i['name'] in SysComputedAttributes.type2attr[_type.name]):
|
||||
i['sys_computed'] = True
|
||||
else:
|
||||
i['sys_computed'] = False
|
||||
|
||||
return is_subscribed, result
|
||||
|
||||
@classmethod
|
||||
@@ -172,24 +186,34 @@ class PreferenceManager(object):
|
||||
_attr, is_fixed = x
|
||||
else:
|
||||
_attr, is_fixed = x, False
|
||||
attr = AttributeCache.get(_attr) or abort(404, ErrFormat.attribute_not_found.format("id={}".format(_attr)))
|
||||
|
||||
if _attr in BUILTIN_ATTRIBUTES:
|
||||
attr = None
|
||||
builtin_attr = _attr
|
||||
else:
|
||||
attr = AttributeCache.get(_attr) or abort(
|
||||
404, ErrFormat.attribute_not_found.format("id={}".format(_attr)))
|
||||
builtin_attr = None
|
||||
existed = PreferenceShowAttributes.get_by(type_id=type_id,
|
||||
uid=current_user.uid,
|
||||
attr_id=attr.id,
|
||||
attr_id=attr and attr.id,
|
||||
builtin_attr=builtin_attr,
|
||||
first=True,
|
||||
to_dict=False)
|
||||
if existed is None:
|
||||
PreferenceShowAttributes.create(type_id=type_id,
|
||||
uid=current_user.uid,
|
||||
attr_id=attr.id,
|
||||
attr_id=attr and attr.id,
|
||||
builtin_attr=builtin_attr,
|
||||
order=order,
|
||||
is_fixed=is_fixed)
|
||||
else:
|
||||
existed.update(order=order, is_fixed=is_fixed)
|
||||
|
||||
attr_dict = {int(i[0]) if isinstance(i, list) else int(i): j for i, j in attr_order}
|
||||
attr_dict = {(int(i[0]) if i[0].isdigit() else i[0]) if isinstance(i, list) else
|
||||
(int(i) if i.isdigit() else i): j for i, j in attr_order}
|
||||
for i in existed_all:
|
||||
if i.attr_id not in attr_dict:
|
||||
if (i.attr_id and i.attr_id not in attr_dict) or (i.builtin_attr and i.builtin_attr not in attr_dict):
|
||||
i.soft_delete()
|
||||
|
||||
if not existed_all and attr_order:
|
||||
@@ -263,12 +287,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,6 +407,14 @@ class PreferenceManager(object):
|
||||
def add_search_option(**kwargs):
|
||||
kwargs['uid'] = current_user.uid
|
||||
|
||||
if kwargs['name'] in ('__recent__', '__favor__', '__relation_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'),
|
||||
|
@@ -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(
|
||||
@@ -150,3 +154,18 @@ class ErrFormat(CommonErrFormat):
|
||||
topology_group_exists = _l("Topology group {} already exists") # 拓扑视图分组 {} 已经存在
|
||||
# 因为该分组下定义了拓扑视图,不能删除
|
||||
topo_view_exists_cannot_delete_group = _l("The group cannot be deleted because the topology view already exists")
|
||||
|
||||
relation_path_search_src_target_required = _l("Both the source model and the target model must be selected")
|
||||
|
||||
builtin_type_cannot_update_name = _l("The names of built-in models cannot be changed")
|
||||
# # IPAM
|
||||
ipam_subnet_model_not_found = _l("The subnet model {} does not exist")
|
||||
ipam_address_model_not_found = _l("The IP Address model {} does not exist")
|
||||
ipam_cidr_invalid_notation = _l("CIDR {} is an invalid notation")
|
||||
ipam_cidr_invalid_subnet = _l("Invalid CIDR: {}, available subnets: {}")
|
||||
ipam_subnet_prefix_length_invalid = _l("Invalid subnet prefix length: {}")
|
||||
ipam_parent_subnet_node_cidr_cannot_empty = _l("parent node cidr must be required")
|
||||
ipam_subnet_overlapped = _l("{} and {} overlap")
|
||||
ipam_subnet_cannot_delete = _l("Cannot delete because child nodes exist")
|
||||
ipam_subnet_not_found = _l("Subnet is not found")
|
||||
ipam_scope_cannot_delete = _l("Cannot delete because child nodes exist")
|
||||
|
@@ -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 = """
|
||||
|
@@ -4,8 +4,8 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import copy
|
||||
import six
|
||||
import time
|
||||
|
||||
from flask import current_app
|
||||
from flask_login import current_user
|
||||
from jinja2 import Template
|
||||
@@ -15,6 +15,7 @@ from api.extensions import db
|
||||
from api.lib.cmdb.cache import AttributeCache
|
||||
from api.lib.cmdb.cache import CITypeCache
|
||||
from api.lib.cmdb.ci import CIManager
|
||||
from api.lib.cmdb.const import BUILTIN_ATTRIBUTES
|
||||
from api.lib.cmdb.const import PermEnum
|
||||
from api.lib.cmdb.const import ResourceTypeEnum
|
||||
from api.lib.cmdb.const import RetKey
|
||||
@@ -66,6 +67,7 @@ class Search(object):
|
||||
self.use_id_filter = use_id_filter
|
||||
self.use_ci_filter = use_ci_filter
|
||||
self.only_ids = only_ids
|
||||
self.multi_type_has_ci_filter = False
|
||||
|
||||
self.valid_type_names = []
|
||||
self.type2filter_perms = dict()
|
||||
@@ -104,35 +106,56 @@ class Search(object):
|
||||
else:
|
||||
raise SearchError(ErrFormat.attribute_not_found.format(key))
|
||||
|
||||
def _type_query_handler(self, v, queries):
|
||||
def _type_query_handler(self, v, queries, is_sub=False):
|
||||
new_v = v[1:-1].split(";") if v.startswith("(") and v.endswith(")") else [v]
|
||||
type_num = len(new_v)
|
||||
type_id_list = []
|
||||
for _v in new_v:
|
||||
ci_type = CITypeCache.get(_v)
|
||||
|
||||
if len(new_v) == 1 and not self.sort and ci_type and ci_type.default_order_attr:
|
||||
if type_num == 1 and not self.sort and ci_type and ci_type.default_order_attr:
|
||||
self.sort = ci_type.default_order_attr
|
||||
|
||||
if ci_type is not None:
|
||||
if self.valid_type_names == "ALL" or ci_type.name in self.valid_type_names:
|
||||
if not is_sub:
|
||||
self.type_id_list.append(str(ci_type.id))
|
||||
if ci_type.id in self.type2filter_perms:
|
||||
type_id_list.append(str(ci_type.id))
|
||||
if ci_type.id in self.type2filter_perms and not is_sub:
|
||||
ci_filter = self.type2filter_perms[ci_type.id].get('ci_filter')
|
||||
if ci_filter and self.use_ci_filter and not self.use_id_filter:
|
||||
sub = []
|
||||
ci_filter = Template(ci_filter).render(user=current_user)
|
||||
for i in ci_filter.split(','):
|
||||
if type_num == 1:
|
||||
if i.startswith("~") and not sub:
|
||||
queries.append(i)
|
||||
else:
|
||||
sub.append(i)
|
||||
else:
|
||||
sub.append(i)
|
||||
if sub:
|
||||
if type_num == 1:
|
||||
queries.append(dict(operator="&", queries=sub))
|
||||
else:
|
||||
if str(ci_type.id) in self.type_id_list:
|
||||
self.type_id_list.remove(str(ci_type.id))
|
||||
type_id_list.remove(str(ci_type.id))
|
||||
sub.extend([i for i in queries[1:] if isinstance(i, six.string_types)])
|
||||
|
||||
sub.insert(0, "_type:{}".format(ci_type.id))
|
||||
queries.append(dict(operator="|", queries=sub))
|
||||
self.multi_type_has_ci_filter = True
|
||||
if self.type2filter_perms[ci_type.id].get('attr_filter'):
|
||||
if type_num == 1:
|
||||
if not self.fl:
|
||||
self.fl = set(self.type2filter_perms[ci_type.id]['attr_filter'])
|
||||
else:
|
||||
self.fl = set(self.fl) & set(self.type2filter_perms[ci_type.id]['attr_filter'])
|
||||
else:
|
||||
self.fl = self.fl or {}
|
||||
if not self.fl or isinstance(self.fl, dict):
|
||||
self.fl[ci_type.id] = set(self.type2filter_perms[ci_type.id]['attr_filter'])
|
||||
|
||||
if self.type2filter_perms[ci_type.id].get('id_filter') and self.use_id_filter:
|
||||
|
||||
@@ -146,13 +169,17 @@ class Search(object):
|
||||
else:
|
||||
raise SearchError(ErrFormat.ci_type_not_found2.format(_v))
|
||||
|
||||
if self.type_id_list:
|
||||
type_ids = ",".join(self.type_id_list)
|
||||
if type_num != len(self.type_id_list) and queries and queries[0].startswith('_type') and not is_sub:
|
||||
queries[0] = "_type:({})".format(";".join(self.type_id_list))
|
||||
|
||||
if type_id_list:
|
||||
type_ids = ",".join(type_id_list)
|
||||
_query_sql = QUERY_CI_BY_TYPE.format(type_ids)
|
||||
if self.only_type_query:
|
||||
if self.only_type_query or self.multi_type_has_ci_filter:
|
||||
return _query_sql
|
||||
else:
|
||||
return ""
|
||||
elif type_num > 1: # there must be instance-level access control
|
||||
return "select c_cis.id as ci_id from c_cis where c_cis.id=0"
|
||||
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
@@ -229,7 +256,7 @@ class Search(object):
|
||||
return ret_sql.format(query_sql, "ORDER BY B.ci_id {1} LIMIT {0:d}, {2};".format(
|
||||
(self.page - 1) * self.count, sort_type, self.count))
|
||||
|
||||
elif self.type_id_list:
|
||||
elif self.type_id_list and not self.multi_type_has_ci_filter:
|
||||
self.query_sql = "SELECT B.ci_id FROM ({0}) AS B {1}".format(
|
||||
query_sql,
|
||||
"INNER JOIN c_cis on c_cis.id=B.ci_id WHERE c_cis.type_id IN ({0}) ".format(
|
||||
@@ -254,7 +281,7 @@ class Search(object):
|
||||
def __sort_by_type(self, sort_type, query_sql):
|
||||
ret_sql = "SELECT SQL_CALC_FOUND_ROWS DISTINCT B.ci_id FROM ({0}) AS B {1}"
|
||||
|
||||
if self.type_id_list:
|
||||
if self.type_id_list and not self.multi_type_has_ci_filter:
|
||||
self.query_sql = "SELECT B.ci_id FROM ({0}) AS B {1}".format(
|
||||
query_sql,
|
||||
"INNER JOIN c_cis on c_cis.id=B.ci_id WHERE c_cis.type_id IN ({0}) ".format(
|
||||
@@ -278,16 +305,23 @@ class Search(object):
|
||||
(self.page - 1) * self.count, sort_type, self.count))
|
||||
|
||||
def __sort_by_field(self, field, sort_type, query_sql):
|
||||
if field not in BUILTIN_ATTRIBUTES:
|
||||
|
||||
attr = AttributeCache.get(field)
|
||||
attr_id = attr.id
|
||||
|
||||
table_name = TableMap(attr=attr).table_name
|
||||
_v_query_sql = """SELECT {0}.ci_id, {1}.value
|
||||
FROM ({2}) AS {0} INNER JOIN {1} ON {1}.ci_id = {0}.ci_id
|
||||
WHERE {1}.attr_id = {3}""".format("ALIAS", table_name, query_sql, attr_id)
|
||||
_v_query_sql = """SELECT ALIAS.ci_id, {0}.value
|
||||
FROM ({1}) AS ALIAS INNER JOIN {0} ON {0}.ci_id = ALIAS.ci_id
|
||||
WHERE {0}.attr_id = {2}""".format(table_name, query_sql, attr_id)
|
||||
new_table = _v_query_sql
|
||||
else:
|
||||
_v_query_sql = """SELECT c_cis.id AS ci_id, c_cis.{0} AS value
|
||||
FROM c_cis INNER JOIN ({1}) AS ALIAS ON ALIAS.ci_id = c_cis.id""".format(
|
||||
field[1:], query_sql)
|
||||
new_table = _v_query_sql
|
||||
|
||||
if self.only_type_query or not self.type_id_list:
|
||||
if self.only_type_query or not self.type_id_list or self.multi_type_has_ci_filter:
|
||||
return ("SELECT SQL_CALC_FOUND_ROWS DISTINCT C.ci_id FROM ({0}) AS C ORDER BY C.value {2} "
|
||||
"LIMIT {1:d}, {3};".format(new_table, (self.page - 1) * self.count, sort_type, self.count))
|
||||
|
||||
@@ -325,7 +359,9 @@ class Search(object):
|
||||
INNER JOIN ({2}) as {3} USING(ci_id)""".format(query_sql, alias, _query_sql, alias + "A")
|
||||
|
||||
elif operator == "|" or operator == "|~":
|
||||
query_sql = "SELECT * FROM ({0}) as {1} UNION ALL ({2})".format(query_sql, alias, _query_sql)
|
||||
query_sql = "SELECT * FROM ({0}) as {1} UNION ALL SELECT * FROM ({2}) as {3}".format(query_sql, alias,
|
||||
_query_sql,
|
||||
alias + "A")
|
||||
|
||||
elif operator == "~":
|
||||
query_sql = """SELECT * FROM ({0}) as {1} LEFT JOIN ({2}) as {3} USING(ci_id)
|
||||
@@ -430,14 +466,14 @@ class Search(object):
|
||||
|
||||
return result
|
||||
|
||||
def __query_by_attr(self, q, queries, alias):
|
||||
def __query_by_attr(self, q, queries, alias, is_sub=False):
|
||||
k = q.split(":")[0].strip()
|
||||
v = "\:".join(q.split(":")[1:]).strip()
|
||||
v = v.replace("'", "\\'")
|
||||
v = v.replace('"', '\\"')
|
||||
field, field_type, operator, attr = self._attr_name_proc(k)
|
||||
if field == "_type":
|
||||
_query_sql = self._type_query_handler(v, queries)
|
||||
_query_sql = self._type_query_handler(v, queries, is_sub)
|
||||
|
||||
elif field == "_id":
|
||||
_query_sql = self._id_query_handler(v)
|
||||
@@ -451,6 +487,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)
|
||||
@@ -481,19 +520,20 @@ class Search(object):
|
||||
|
||||
return alias, _query_sql, operator
|
||||
|
||||
def __query_build_by_field(self, queries, is_first=True, only_type_query_special=True, alias='A', operator='&'):
|
||||
def __query_build_by_field(self, queries, is_first=True, only_type_query_special=True, alias='A', operator='&',
|
||||
is_sub=False):
|
||||
query_sql = ""
|
||||
|
||||
for q in queries:
|
||||
_query_sql = ""
|
||||
if isinstance(q, dict):
|
||||
alias, _query_sql, operator = self.__query_build_by_field(q['queries'], True, True, alias)
|
||||
current_app.logger.info(_query_sql)
|
||||
current_app.logger.info((operator, is_first, alias))
|
||||
alias, _query_sql, operator = self.__query_build_by_field(q['queries'], True, True, alias, is_sub=True)
|
||||
# current_app.logger.info(_query_sql)
|
||||
# current_app.logger.info((operator, is_first, alias))
|
||||
operator = q['operator']
|
||||
|
||||
elif ":" in q and not q.startswith("*"):
|
||||
alias, _query_sql, operator = self.__query_by_attr(q, queries, alias)
|
||||
alias, _query_sql, operator = self.__query_by_attr(q, queries, alias, is_sub)
|
||||
elif q == "*":
|
||||
continue
|
||||
elif q:
|
||||
@@ -544,7 +584,6 @@ class Search(object):
|
||||
queries = handle_arg_list(self.orig_query)
|
||||
queries = self._extra_handle_query_expr(queries)
|
||||
queries = self.__confirm_type_first(queries)
|
||||
current_app.logger.debug(queries)
|
||||
|
||||
_, query_sql, _ = self.__query_build_by_field(queries)
|
||||
|
||||
@@ -582,6 +621,7 @@ class Search(object):
|
||||
return facet_result
|
||||
|
||||
def _fl_build(self):
|
||||
if isinstance(self.fl, list):
|
||||
_fl = list()
|
||||
for f in self.fl:
|
||||
k, _, _, _ = self._attr_name_proc(f)
|
||||
@@ -589,6 +629,8 @@ class Search(object):
|
||||
_fl.append(k)
|
||||
|
||||
return _fl
|
||||
else:
|
||||
return self.fl
|
||||
|
||||
def search(self):
|
||||
numfound, ci_ids = self._query_build_raw()
|
||||
@@ -607,6 +649,8 @@ class Search(object):
|
||||
if ci_ids:
|
||||
response = CIManager.get_cis_by_ids(ci_ids, ret_key=self.ret_key, fields=_fl, excludes=self.excludes)
|
||||
for res in response:
|
||||
if not res:
|
||||
continue
|
||||
ci_type = res.get("ci_type")
|
||||
if ci_type not in counter.keys():
|
||||
counter[ci_type] = 0
|
||||
|
@@ -1,8 +1,11 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
import json
|
||||
import sys
|
||||
from collections import Counter
|
||||
from collections import defaultdict
|
||||
|
||||
import copy
|
||||
import json
|
||||
import networkx as nx
|
||||
import sys
|
||||
from flask import abort
|
||||
from flask import current_app
|
||||
from flask_login import current_user
|
||||
@@ -13,6 +16,7 @@ from api.lib.cmdb.cache import CITypeCache
|
||||
from api.lib.cmdb.ci import CIRelationManager
|
||||
from api.lib.cmdb.ci_type import CITypeRelationManager
|
||||
from api.lib.cmdb.const import ConstraintEnum
|
||||
from api.lib.cmdb.const import PermEnum
|
||||
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
|
||||
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION2
|
||||
from api.lib.cmdb.const import ResourceTypeEnum
|
||||
@@ -25,10 +29,12 @@ from api.lib.cmdb.utils import ValueTypeMap
|
||||
from api.lib.perm.acl.acl import ACLManager
|
||||
from api.lib.perm.acl.acl import is_app_admin
|
||||
from api.models.cmdb import CI
|
||||
from api.models.cmdb import CITypeRelation
|
||||
from api.models.cmdb import RelationType
|
||||
|
||||
|
||||
class Search(object):
|
||||
def __init__(self, root_id,
|
||||
def __init__(self, root_id=None,
|
||||
level=None,
|
||||
query=None,
|
||||
fl=None,
|
||||
@@ -385,9 +391,10 @@ class Search(object):
|
||||
id2children[str(i)] = item['children']
|
||||
|
||||
for lv in range(1, self.level):
|
||||
type_id = type_ids[lv]
|
||||
|
||||
if len(type_ids or []) >= lv and type2filter_perms.get(type_ids[lv]):
|
||||
id_filter_limit, _ = self._get_ci_filter(type2filter_perms[type_ids[lv]])
|
||||
if len(type_ids or []) >= lv and type2filter_perms.get(type_id):
|
||||
id_filter_limit, _ = self._get_ci_filter(type2filter_perms[type_id])
|
||||
else:
|
||||
id_filter_limit = {}
|
||||
|
||||
@@ -395,12 +402,12 @@ class Search(object):
|
||||
key, prefix = [i for i in level_ids], REDIS_PREFIX_CI_RELATION2
|
||||
else:
|
||||
key, prefix = [i.split(',')[-1] for i in level_ids], REDIS_PREFIX_CI_RELATION
|
||||
|
||||
res = [json.loads(x).items() for x in [i or '{}' for i in rd.get(key, prefix) or []]]
|
||||
res = [[i for i in x if (not id_filter_limit or (key[idx] not in id_filter_limit or
|
||||
res = [[i for i in x if i[1] == type_id and (not id_filter_limit or (key[idx] not in id_filter_limit or
|
||||
int(i[0]) in id_filter_limit[key[idx]]) or
|
||||
int(i[0]) in id_filter_limit)] for idx, x in enumerate(res)]
|
||||
_level_ids = []
|
||||
type_id = type_ids[lv]
|
||||
id2name = _get_id2name(type_id)
|
||||
for idx, node_path in enumerate(level_ids):
|
||||
for child_id, _ in (res[idx] or []):
|
||||
@@ -419,3 +426,169 @@ class Search(object):
|
||||
level_ids = _level_ids
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _get_src_ids(src):
|
||||
q = src.get('q') or ''
|
||||
if not q.startswith('_type:'):
|
||||
q = "_type:{},{}".format(src['type_id'], q)
|
||||
|
||||
return SearchFromDB(q, use_ci_filter=True, only_ids=True, count=100000).search()
|
||||
|
||||
@staticmethod
|
||||
def _filter_target_ids(target_ids, type_ids, q):
|
||||
if not q.startswith('_type:'):
|
||||
q = "_type:({}),{}".format(";".join(map(str, type_ids)), q)
|
||||
|
||||
ci_ids = SearchFromDB(q, ci_ids=target_ids, use_ci_filter=True, only_ids=True, count=100000).search()
|
||||
cis = CI.get_by(fl=['id', 'type_id'], only_query=True).filter(CI.id.in_(ci_ids))
|
||||
|
||||
return [(str(i.id), i.type_id) for i in cis]
|
||||
|
||||
@staticmethod
|
||||
def _path2level(src_type_id, target_type_ids, path):
|
||||
if not src_type_id or not target_type_ids:
|
||||
return abort(400, ErrFormat.relation_path_search_src_target_required)
|
||||
|
||||
graph = nx.DiGraph()
|
||||
graph.add_edges_from([(n, _path[idx + 1]) for _path in path for idx, n in enumerate(_path[:-1])])
|
||||
relation_types = defaultdict(dict)
|
||||
level2type = defaultdict(set)
|
||||
type2show_key = dict()
|
||||
for _path in path:
|
||||
for idx, node in enumerate(_path[1:]):
|
||||
level2type[idx + 1].add(node)
|
||||
|
||||
src = CITypeCache.get(_path[idx])
|
||||
target = CITypeCache.get(node)
|
||||
relation_type = RelationType.get_by(only_query=True).join(
|
||||
CITypeRelation, CITypeRelation.relation_type_id == RelationType.id).filter(
|
||||
CITypeRelation.parent_id == src.id).filter(CITypeRelation.child_id == target.id).first()
|
||||
relation_types[src.alias].update({target.alias: relation_type.name})
|
||||
|
||||
if src.id not in type2show_key:
|
||||
type2show_key[src.id] = AttributeCache.get(src.show_id or src.unique_id).name
|
||||
if target.id not in type2show_key:
|
||||
type2show_key[target.id] = AttributeCache.get(target.show_id or target.unique_id).name
|
||||
|
||||
nodes = graph.nodes()
|
||||
|
||||
return level2type, list(nodes), relation_types, type2show_key
|
||||
|
||||
def _build_graph(self, source_ids, source_type_id, level2type, target_type_ids, acl):
|
||||
type2filter_perms = dict()
|
||||
if not self.is_app_admin:
|
||||
res2 = acl.get_resources(ResourceTypeEnum.CI_FILTER)
|
||||
if res2:
|
||||
type2filter_perms = CIFilterPermsCRUD().get_by_ids(list(map(int, [i['name'] for i in res2])))
|
||||
|
||||
target_type_ids = set(target_type_ids)
|
||||
graph = nx.DiGraph()
|
||||
target_ids = []
|
||||
key = [(str(i), source_type_id) for i in source_ids]
|
||||
graph.add_nodes_from(key)
|
||||
for level in level2type:
|
||||
filter_type_ids = level2type[level]
|
||||
id_filter_limit = dict()
|
||||
for _type_id in filter_type_ids:
|
||||
if type2filter_perms.get(_type_id):
|
||||
_id_filter_limit, _ = self._get_ci_filter(type2filter_perms[_type_id])
|
||||
id_filter_limit.update(_id_filter_limit)
|
||||
|
||||
has_target = filter_type_ids & target_type_ids
|
||||
|
||||
res = [json.loads(x).items() for x in [i or '{}' for i in rd.get([i[0] for i in key],
|
||||
REDIS_PREFIX_CI_RELATION) or []]]
|
||||
_key = []
|
||||
for idx, _id in enumerate(key):
|
||||
valid_targets = [i for i in res[idx] if i[1] in filter_type_ids and
|
||||
(not id_filter_limit or int(i[0]) in id_filter_limit)]
|
||||
_key.extend(valid_targets)
|
||||
graph.add_edges_from(zip([_id] * len(valid_targets), valid_targets))
|
||||
|
||||
if has_target:
|
||||
target_ids.extend([j[0] for i in res for j in i if j[1] in target_type_ids])
|
||||
|
||||
key = copy.deepcopy(_key)
|
||||
|
||||
return graph, target_ids
|
||||
|
||||
@staticmethod
|
||||
def _find_paths(graph, source_ids, source_type_id, target_ids, valid_path, max_depth=6):
|
||||
paths = []
|
||||
for source_id in source_ids:
|
||||
_paths = nx.all_simple_paths(graph,
|
||||
source=(source_id, source_type_id),
|
||||
target=target_ids,
|
||||
cutoff=max_depth)
|
||||
for __path in _paths:
|
||||
if tuple([i[1] for i in __path]) in valid_path:
|
||||
paths.append([i[0] for i in __path])
|
||||
|
||||
return paths
|
||||
|
||||
@staticmethod
|
||||
def _wrap_path_result(paths, types, valid_path, target_types, type2show_key):
|
||||
ci_ids = [j for i in paths for j in i]
|
||||
|
||||
response, _, _, _, _, _ = SearchFromDB("_type:({})".format(";".join(map(str, types))),
|
||||
use_ci_filter=False,
|
||||
ci_ids=list(map(int, ci_ids)),
|
||||
count=1000000).search()
|
||||
id2ci = {str(i.get('_id')): i if i['_type'] in target_types else {
|
||||
type2show_key[i['_type']]: i[type2show_key[i['_type']]],
|
||||
"ci_type_alias": i["ci_type_alias"],
|
||||
"_type": i["_type"],
|
||||
} for i in response}
|
||||
|
||||
result = defaultdict(list)
|
||||
counter = defaultdict(int)
|
||||
|
||||
for path in paths:
|
||||
key = "-".join([id2ci.get(i, {}).get('ci_type_alias') or '' for i in path])
|
||||
if tuple([id2ci.get(i, {}).get('_type') for i in path]) in valid_path:
|
||||
counter[key] += 1
|
||||
result[key].append(path)
|
||||
|
||||
return result, counter, id2ci
|
||||
|
||||
def search_by_path(self, source, target, path):
|
||||
"""
|
||||
|
||||
:param source: {type_id: id, q: expr}
|
||||
:param target: {type_ids: [id], q: expr}
|
||||
:param path: [source_type_id, ..., target_type_id], use type id
|
||||
:return:
|
||||
"""
|
||||
acl = ACLManager('cmdb')
|
||||
if not self.is_app_admin:
|
||||
res = {i['name'] for i in acl.get_resources(ResourceTypeEnum.CI_TYPE)}
|
||||
for type_id in (source.get('type_id') and [source['type_id']] or []) + (target.get('type_ids') or []):
|
||||
_type = CITypeCache.get(type_id)
|
||||
if _type and _type.name not in res:
|
||||
return abort(403, ErrFormat.no_permission.format(_type.alias, PermEnum.READ))
|
||||
|
||||
target['type_ids'] = [i[-1] for i in path]
|
||||
level2type, types, relation_types, type2show_key = self._path2level(
|
||||
source.get('type_id'), target.get('type_ids'), path)
|
||||
if not level2type:
|
||||
return [], {}, 0, self.page, 0, {}, {}
|
||||
|
||||
source_ids = self._get_src_ids(source)
|
||||
|
||||
graph, target_ids = self._build_graph(source_ids, source['type_id'], level2type, target['type_ids'], acl)
|
||||
target_ids = self._filter_target_ids(target_ids, target['type_ids'], target.get('q') or '')
|
||||
paths = self._find_paths(graph,
|
||||
source_ids,
|
||||
source['type_id'],
|
||||
set(target_ids),
|
||||
{tuple(i): 1 for i in path})
|
||||
|
||||
numfound = len(paths)
|
||||
paths = paths[(self.page - 1) * self.count:self.page * self.count]
|
||||
response, counter, id2ci = self._wrap_path_result(paths,
|
||||
types,
|
||||
{tuple(i): 1 for i in path},
|
||||
set(target.get('type_ids') or []),
|
||||
type2show_key)
|
||||
return response, counter, len(paths), self.page, numfound, id2ci, relation_types, type2show_key
|
||||
|
@@ -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 = {
|
||||
|
@@ -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
|
||||
@@ -90,6 +97,8 @@ class AttributeValueManager(object):
|
||||
deserialize = ValueTypeMap.deserialize[value_type]
|
||||
try:
|
||||
v = deserialize(value)
|
||||
if value_type in (ValueTypeEnum.DATE, ValueTypeEnum.DATETIME):
|
||||
return str(v)
|
||||
return v
|
||||
except ValueDeserializeError as e:
|
||||
return abort(400, ErrFormat.attribute_value_invalid2.format(alias, e))
|
||||
@@ -128,14 +137,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):
|
||||
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_unique and self._check_is_unique(
|
||||
value_table, attr, ci and ci.id or ci_id, ci and ci.type_id or type_id, v)
|
||||
|
||||
self._check_is_required(ci and ci.type_id or type_id, attr, v, type_attr=type_attr)
|
||||
if attr.is_reference:
|
||||
return v
|
||||
|
||||
if v == "" and attr.value_type not in (ValueTypeEnum.TEXT,):
|
||||
v = None
|
||||
@@ -235,10 +250,19 @@ class AttributeValueManager(object):
|
||||
|
||||
try:
|
||||
if attr.is_list:
|
||||
if isinstance(value, dict):
|
||||
if value.get('op') == "delete":
|
||||
value['v'] = [ValueTypeMap.serialize[attr.value_type](
|
||||
self._deserialize_value(attr.alias, attr.value_type, i))
|
||||
for i in handle_arg_list(value['v'])]
|
||||
continue
|
||||
_value = value.get('v') or []
|
||||
else:
|
||||
_value = value
|
||||
value_list = [self._validate(attr, i, value_table, ci=None, type_id=type_id, ci_id=ci_id,
|
||||
type_attr=ci_attr2type_attr.get(attr.id))
|
||||
for i in handle_arg_list(value)]
|
||||
ci_dict[key] = value_list
|
||||
for i in handle_arg_list(_value)]
|
||||
ci_dict[key] = value_list if not isinstance(value, dict) else dict(op=value.get('op'), v=value_list)
|
||||
if not value_list:
|
||||
self._check_is_required(type_id, attr, '')
|
||||
|
||||
@@ -278,6 +302,25 @@ class AttributeValueManager(object):
|
||||
existed_values = [(ValueTypeMap.serialize[attr.value_type](i.value) if
|
||||
i.value or i.value == 0 else i.value) for i in existed_attrs]
|
||||
|
||||
if isinstance(value, dict):
|
||||
if value.get('op') == "add":
|
||||
for v in (value.get('v') or []):
|
||||
if v not in existed_values:
|
||||
value_table.create(ci_id=ci.id, attr_id=attr.id, value=v, flush=False, commit=False)
|
||||
if not attr.is_dynamic:
|
||||
changed.append((ci.id, attr.id, OperateType.ADD, None, v, ci.type_id))
|
||||
else:
|
||||
has_dynamic = True
|
||||
|
||||
elif value.get('op') == "delete":
|
||||
for v in (value.get('v') or []):
|
||||
if v in existed_values:
|
||||
existed_attrs[existed_values.index(v)].delete(flush=False, commit=False)
|
||||
if not attr.is_dynamic:
|
||||
changed.append((ci.id, attr.id, OperateType.DELETE, v, None, ci.type_id))
|
||||
else:
|
||||
has_dynamic = True
|
||||
else:
|
||||
# Comparison array starts from which position changes
|
||||
min_len = min(len(value), len(existed_values))
|
||||
index = 0
|
||||
|
@@ -53,6 +53,7 @@ class CMDBApp(BaseApp):
|
||||
"perms": ["read", "create_topology_group", "update_topology_group", "delete_topology_group",
|
||||
"create_topology_view"],
|
||||
},
|
||||
{"page": "IPAM", "page_cn": "IPAM", "perms": ["read"]},
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
|
@@ -1,6 +1,7 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
|
||||
from flask import current_app
|
||||
from sqlalchemy import func
|
||||
|
||||
from api.extensions import db
|
||||
@@ -32,11 +33,21 @@ class DBMixin(object):
|
||||
|
||||
for k in kwargs:
|
||||
if hasattr(cls.cls, k):
|
||||
if isinstance(kwargs[k], list):
|
||||
query = query.filter(getattr(cls.cls, k).in_(kwargs[k]))
|
||||
if count_query:
|
||||
_query = _query.filter(getattr(cls.cls, k).in_(kwargs[k]))
|
||||
else:
|
||||
if "*" in str(kwargs[k]):
|
||||
query = query.filter(getattr(cls.cls, k).ilike(kwargs[k].replace('*', '%')))
|
||||
if count_query:
|
||||
_query = _query.filter(getattr(cls.cls, k).ilike(kwargs[k].replace('*', '%')))
|
||||
else:
|
||||
query = query.filter(getattr(cls.cls, k) == kwargs[k])
|
||||
if count_query:
|
||||
_query = _query.filter(getattr(cls.cls, k) == kwargs[k])
|
||||
|
||||
if reverse:
|
||||
if reverse in current_app.config.get('BOOL_TRUE'):
|
||||
query = query.order_by(cls.cls.id.desc())
|
||||
|
||||
if only_query and not count_query:
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -376,7 +376,7 @@ class AuditCRUD(object):
|
||||
origin=origin, current=current, extra=extra, source=source.value)
|
||||
|
||||
@classmethod
|
||||
def add_login_log(cls, username, is_ok, description, _id=None, logout_at=None):
|
||||
def add_login_log(cls, username, is_ok, description, _id=None, logout_at=None, ip=None, browser=None):
|
||||
if _id is not None:
|
||||
existed = AuditLoginLog.get_by_id(_id)
|
||||
if existed is not None:
|
||||
@@ -387,8 +387,9 @@ class AuditCRUD(object):
|
||||
is_ok=is_ok,
|
||||
description=description,
|
||||
logout_at=logout_at,
|
||||
ip=request.headers.get('X-Real-IP') or request.remote_addr,
|
||||
browser=request.headers.get('User-Agent'),
|
||||
ip=(ip or request.headers.get('X-Forwarded-For') or
|
||||
request.headers.get('X-Real-IP') or request.remote_addr or '').split(',')[0],
|
||||
browser=browser or request.headers.get('User-Agent'),
|
||||
channel=request.values.get('channel', 'web'),
|
||||
)
|
||||
|
||||
|
@@ -71,7 +71,7 @@ class PermissionCRUD(object):
|
||||
|
||||
@classmethod
|
||||
def get_all2(cls, resource_name, resource_type_name, app_id):
|
||||
rt = ResourceType.get_by(name=resource_type_name, first=True, to_dict=False)
|
||||
rt = ResourceType.get_by(name=resource_type_name, app_id=app_id, first=True, to_dict=False)
|
||||
rt or abort(404, ErrFormat.resource_type_not_found.format(resource_type_name))
|
||||
|
||||
r = Resource.get_by(name=resource_name, resource_type_id=rt.id, app_id=app_id, first=True, to_dict=False)
|
||||
|
@@ -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}
|
||||
|
||||
@@ -249,6 +253,7 @@ class CI(Model):
|
||||
status = db.Column(db.Enum(*CIStatusEnum.all(), name="status"))
|
||||
heartbeat = db.Column(db.DateTime, default=lambda: datetime.datetime.now())
|
||||
is_auto_discovery = db.Column('a', db.Boolean, default=False)
|
||||
updated_by = db.Column(db.String(64))
|
||||
|
||||
ci_type = db.relationship("CIType", backref="c_cis.type_id")
|
||||
|
||||
@@ -471,6 +476,7 @@ class PreferenceShowAttributes(Model):
|
||||
uid = db.Column(db.Integer, index=True, nullable=False)
|
||||
type_id = db.Column(db.Integer, db.ForeignKey("c_ci_types.id"), nullable=False)
|
||||
attr_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id"))
|
||||
builtin_attr = db.Column(db.String(256), nullable=True)
|
||||
order = db.Column(db.SmallInteger, default=0)
|
||||
is_fixed = db.Column(db.Boolean, default=False)
|
||||
|
||||
@@ -530,6 +536,7 @@ class CustomDashboard(Model):
|
||||
|
||||
type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'))
|
||||
attr_id = db.Column(db.Integer, db.ForeignKey('c_attributes.id'))
|
||||
builtin_attr = db.Column(db.String(256), nullable=True)
|
||||
level = db.Column(db.Integer)
|
||||
|
||||
options = db.Column(db.JSON)
|
||||
@@ -662,3 +669,40 @@ class InnerKV(Model):
|
||||
|
||||
key = db.Column(db.String(128), index=True)
|
||||
value = db.Column(db.Text)
|
||||
|
||||
|
||||
class IPAMSubnetScan(Model):
|
||||
__tablename__ = "c_ipam_subnet_scans"
|
||||
|
||||
ci_id = db.Column(db.Integer, index=True, nullable=False)
|
||||
scan_enabled = db.Column(db.Boolean, default=True)
|
||||
last_scan_time = db.Column(db.DateTime)
|
||||
|
||||
# scan rules
|
||||
agent_id = db.Column(db.String(8), index=True)
|
||||
cron = db.Column(db.String(128))
|
||||
|
||||
|
||||
class IPAMSubnetScanHistory(Model2):
|
||||
__tablename__ = "c_ipam_subnet_scan_histories"
|
||||
|
||||
subnet_scan_id = db.Column(db.Integer, index=True)
|
||||
exec_id = db.Column(db.String(64), index=True)
|
||||
cidr = db.Column(db.String(18), index=True)
|
||||
start_at = db.Column(db.DateTime)
|
||||
end_at = db.Column(db.DateTime)
|
||||
status = db.Column(db.Integer, default=0) # 0 is ok
|
||||
stdout = db.Column(db.Text)
|
||||
ip_num = db.Column(db.Integer)
|
||||
ips = db.Column(db.JSON) # keep only the last 10 records
|
||||
|
||||
|
||||
class IPAMOperationHistory(Model2):
|
||||
__tablename__ = "c_ipam_operation_histories"
|
||||
|
||||
from api.lib.cmdb.ipam.const import OperateTypeEnum
|
||||
|
||||
uid = db.Column(db.Integer, index=True)
|
||||
cidr = db.Column(db.String(18), index=True)
|
||||
operate_type = db.Column(db.Enum(*OperateTypeEnum.all()))
|
||||
description = db.Column(db.Text)
|
||||
|
@@ -5,6 +5,7 @@ import datetime
|
||||
import json
|
||||
import redis_lock
|
||||
from flask import current_app
|
||||
from flask import has_request_context
|
||||
from flask_login import login_user
|
||||
|
||||
import api.lib.cmdb.ci
|
||||
@@ -20,10 +21,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 +41,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)
|
||||
@@ -50,10 +54,21 @@ def ci_cache(ci_id, operate_type, record_id):
|
||||
current_app.logger.info("{0} flush..........".format(ci_id))
|
||||
|
||||
if operate_type:
|
||||
if not has_request_context():
|
||||
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 +99,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 +114,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))
|
||||
|
||||
|
||||
@@ -165,6 +186,7 @@ def ci_relation_add(parent_dict, child_id, uid):
|
||||
from api.lib.cmdb.search import SearchError
|
||||
from api.lib.cmdb.search.ci import search
|
||||
|
||||
if not has_request_context():
|
||||
current_app.test_request_context().push()
|
||||
login_user(UserCache.get(uid))
|
||||
|
||||
@@ -188,7 +210,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:
|
||||
@@ -253,6 +275,7 @@ def ci_type_attribute_order_rebuild(type_id, uid):
|
||||
def calc_computed_attribute(attr_id, uid):
|
||||
from api.lib.cmdb.ci import CIManager
|
||||
|
||||
if not has_request_context():
|
||||
current_app.test_request_context().push()
|
||||
login_user(UserCache.get(uid))
|
||||
|
||||
|
Binary file not shown.
@@ -7,7 +7,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PROJECT VERSION\n"
|
||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
||||
"POT-Creation-Date: 2024-06-20 19:12+0800\n"
|
||||
"POT-Creation-Date: 2024-11-11 17:40+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"
|
||||
@@ -92,6 +92,14 @@ msgstr "您没有操作权限!"
|
||||
msgid "Only the creator or administrator has permission!"
|
||||
msgstr "只有创建人或者管理员才有权限!"
|
||||
|
||||
#: api/lib/cmdb/const.py:128
|
||||
msgid "Update Time"
|
||||
msgstr "更新时间"
|
||||
|
||||
#: api/lib/cmdb/const.py:129
|
||||
msgid "Updated By"
|
||||
msgstr "更新人"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:9
|
||||
msgid "CI Model"
|
||||
msgstr "模型配置"
|
||||
@@ -169,8 +177,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,289 +205,345 @@ 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 ""
|
||||
msgstr "CMDB数据合规检查结果"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:147
|
||||
#: api/lib/cmdb/resp_format.py:151
|
||||
msgid "Number of {} illegal: {}"
|
||||
msgstr ""
|
||||
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 "因为该分组下定义了拓扑视图,不能删除"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:158
|
||||
msgid "Both the source model and the target model must be selected"
|
||||
msgstr "源模型和目标模型不能为空!"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:160
|
||||
msgid "The names of built-in models cannot be changed"
|
||||
msgstr "内置模型的名字不能修改"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:162
|
||||
msgid "The subnet model {} does not exist"
|
||||
msgstr "子网模型 {} 不存在!"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:163
|
||||
msgid "The IP Address model {} does not exist"
|
||||
msgstr "IP地址模型 {} 不存在!"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:164
|
||||
msgid "CIDR {} is an invalid notation"
|
||||
msgstr "CIDR {} 写法不正确!"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:165
|
||||
msgid "Invalid CIDR: {}, available subnets: {}"
|
||||
msgstr "无效的CIDR: {}, 可用的子网: {}"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:166
|
||||
msgid "Invalid subnet prefix length: {}"
|
||||
msgstr "无效的子网前缀长度: {}"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:167
|
||||
msgid "parent node cidr must be required"
|
||||
msgstr "必须要有父节点"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:168
|
||||
msgid "{} and {} overlap"
|
||||
msgstr "{} 和 {} 有重叠"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:169 api/lib/cmdb/resp_format.py:171
|
||||
msgid "Cannot delete because child nodes exist"
|
||||
msgstr "因为子节点已经存在,不能删除"
|
||||
|
||||
#: api/lib/cmdb/resp_format.py:170
|
||||
msgid "Subnet is not found"
|
||||
msgstr "子网不存在"
|
||||
|
||||
#: api/lib/common_setting/resp_format.py:8
|
||||
msgid "Company info already existed"
|
||||
msgstr "公司信息已存在,无法创建!"
|
||||
|
@@ -1,7 +1,6 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
import datetime
|
||||
|
||||
import jwt
|
||||
import six
|
||||
from flask import abort
|
||||
@@ -17,10 +16,12 @@ from api.lib.decorator import args_required
|
||||
from api.lib.decorator import args_validate
|
||||
from api.lib.perm.acl.acl import ACLManager
|
||||
from api.lib.perm.acl.audit import AuditCRUD
|
||||
from api.lib.perm.acl.cache import AppCache
|
||||
from api.lib.perm.acl.cache import RoleCache
|
||||
from api.lib.perm.acl.cache import User
|
||||
from api.lib.perm.acl.cache import UserCache
|
||||
from api.lib.perm.acl.resp_format import ErrFormat
|
||||
from api.lib.perm.acl.role import RoleRelationCRUD
|
||||
from api.lib.perm.auth import auth_abandoned
|
||||
from api.lib.perm.auth import auth_with_app_token
|
||||
from api.models.acl import Role
|
||||
@@ -124,11 +125,18 @@ class AuthWithKeyView(APIView):
|
||||
if not user.get('username'):
|
||||
user['username'] = user.get('name')
|
||||
|
||||
return self.jsonify(user=user,
|
||||
result = dict(user=user,
|
||||
authenticated=authenticated,
|
||||
rid=role and role.id,
|
||||
can_proxy=can_proxy)
|
||||
|
||||
if request.values.get('need_parentRoles') in current_app.config.get('BOOL_TRUE'):
|
||||
app_id = AppCache.get(request.values.get('app_id'))
|
||||
parent_ids = RoleRelationCRUD.recursive_parent_ids(role and role.id, app_id and app_id.id)
|
||||
result['user']['parentRoles'] = [RoleCache.get(rid).name for rid in set(parent_ids) if RoleCache.get(rid)]
|
||||
|
||||
return self.jsonify(result)
|
||||
|
||||
|
||||
class AuthWithTokenView(APIView):
|
||||
url_prefix = ("/auth_with_token", "/req_token")
|
||||
@@ -184,6 +192,8 @@ class LogoutView(APIView):
|
||||
def post(self):
|
||||
logout_user()
|
||||
|
||||
AuditCRUD.add_login_log(None, None, None, _id=session.get('LOGIN_ID'), logout_at=datetime.datetime.now())
|
||||
AuditCRUD.add_login_log(None, None, None,
|
||||
_id=session.get('LOGIN_ID') or request.values.get('LOGIN_ID'),
|
||||
logout_at=datetime.datetime.now())
|
||||
|
||||
self.jsonify(code=200)
|
||||
|
@@ -11,6 +11,7 @@ from flask_login import current_user
|
||||
from api.lib.decorator import args_required
|
||||
from api.lib.decorator import args_validate
|
||||
from api.lib.perm.acl.acl import ACLManager
|
||||
from api.lib.perm.acl.acl import AuditCRUD
|
||||
from api.lib.perm.acl.acl import role_required
|
||||
from api.lib.perm.acl.cache import AppCache
|
||||
from api.lib.perm.acl.cache import UserCache
|
||||
@@ -48,6 +49,13 @@ class GetUserInfoView(APIView):
|
||||
role=dict(permissions=user_info.get('parents')),
|
||||
avatar=user_info.get('avatar'))
|
||||
|
||||
if request.values.get('channel'):
|
||||
_id = AuditCRUD.add_login_log(name, True, ErrFormat.login_succeed,
|
||||
ip=request.values.get('ip'),
|
||||
browser=request.values.get('browser'))
|
||||
session['LOGIN_ID'] = _id
|
||||
result['LOGIN_ID'] = _id
|
||||
|
||||
current_app.logger.info("get user info for3: {}".format(result))
|
||||
return self.jsonify(result=result)
|
||||
|
||||
|
@@ -24,6 +24,7 @@ from api.lib.cmdb.auto_discovery.const import PRIVILEGED_USERS
|
||||
from api.lib.cmdb.cache import AttributeCache
|
||||
from api.lib.cmdb.const import PermEnum
|
||||
from api.lib.cmdb.const import ResourceTypeEnum
|
||||
from api.lib.cmdb.ipam.subnet import SubnetManager
|
||||
from api.lib.cmdb.resp_format import ErrFormat
|
||||
from api.lib.cmdb.search import SearchError
|
||||
from api.lib.cmdb.search.ci import search as ci_search
|
||||
@@ -225,6 +226,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)
|
||||
@@ -292,9 +294,13 @@ class AutoDiscoveryRuleSyncView(APIView):
|
||||
|
||||
return self.jsonify(rules=rules, last_update_at=last_update_at)
|
||||
|
||||
rules, last_update_at = AutoDiscoveryCITypeCRUD.get(None, oneagent_id, oneagent_name, last_update_at)
|
||||
rules, last_update_at1 = AutoDiscoveryCITypeCRUD.get(None, oneagent_id, oneagent_name, last_update_at)
|
||||
|
||||
return self.jsonify(rules=rules, last_update_at=last_update_at)
|
||||
subnet_scan_rules, last_update_at2 = SubnetManager().scan_rules(oneagent_id, last_update_at)
|
||||
|
||||
return self.jsonify(rules=rules,
|
||||
subnet_scan_rules=subnet_scan_rules,
|
||||
last_update_at=max(last_update_at1 or "", last_update_at2 or ""))
|
||||
|
||||
|
||||
class AutoDiscoveryRuleSyncHistoryView(APIView):
|
||||
|
@@ -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:
|
||||
|
@@ -2,7 +2,6 @@
|
||||
|
||||
|
||||
import time
|
||||
|
||||
from flask import abort
|
||||
from flask import current_app
|
||||
from flask import request
|
||||
@@ -65,6 +64,42 @@ class CIRelationSearchView(APIView):
|
||||
result=response)
|
||||
|
||||
|
||||
class CIRelationSearchPathView(APIView):
|
||||
url_prefix = ("/ci_relations/path/s", "/ci_relations/path/search")
|
||||
|
||||
@args_required("source", "target", "path")
|
||||
def post(self):
|
||||
"""@params: page: page number
|
||||
page_size | count: page size
|
||||
source: source CIType, e.g. {type_id: 1, q: `search expr`}
|
||||
target: target CIType, e.g. {type_ids: [2], q: `search expr`}
|
||||
path: Path from the Source CIType to the Target CIType, e.g. [1, ..., 2]
|
||||
"""
|
||||
|
||||
page = get_page(request.values.get("page", 1))
|
||||
count = get_page_size(request.values.get("count") or request.values.get("page_size"))
|
||||
|
||||
source = request.values.get("source")
|
||||
target = request.values.get("target")
|
||||
path = request.values.get("path")
|
||||
|
||||
s = Search(page=page, count=count)
|
||||
try:
|
||||
(response, counter, total, page, numfound, id2ci,
|
||||
relation_types, type2show_key) = s.search_by_path(source, target, path)
|
||||
except SearchError as e:
|
||||
return abort(400, str(e))
|
||||
|
||||
return self.jsonify(numfound=numfound,
|
||||
total=total,
|
||||
page=page,
|
||||
counter=counter,
|
||||
paths=response,
|
||||
id2ci=id2ci,
|
||||
relation_types=relation_types,
|
||||
type2show_key=type2show_key)
|
||||
|
||||
|
||||
class CIRelationStatisticsView(APIView):
|
||||
url_prefix = "/ci_relations/statistics"
|
||||
|
||||
|
@@ -48,16 +48,21 @@ class CITypeView(APIView):
|
||||
if request.url.endswith("icons"):
|
||||
return self.jsonify(CITypeManager().get_icons())
|
||||
|
||||
q = request.args.get("type_name")
|
||||
|
||||
if type_id is not None:
|
||||
ci_type = CITypeCache.get(type_id)
|
||||
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)
|
||||
|
||||
ci_type = ci_type.to_dict()
|
||||
ci_type['parent_ids'] = CITypeInheritanceManager.get_parents(type_id)
|
||||
ci_types = [ci_type]
|
||||
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'])
|
||||
|
@@ -8,7 +8,6 @@ from api.lib.cmdb.ci_type import CITypeManager
|
||||
from api.lib.cmdb.ci_type import CITypeRelationManager
|
||||
from api.lib.cmdb.const import PermEnum
|
||||
from api.lib.cmdb.const import ResourceTypeEnum
|
||||
from api.lib.cmdb.const import RoleEnum
|
||||
from api.lib.cmdb.preference import PreferenceManager
|
||||
from api.lib.cmdb.resp_format import ErrFormat
|
||||
from api.lib.common_setting.decorator import perms_role_required
|
||||
@@ -17,7 +16,7 @@ from api.lib.decorator import args_required
|
||||
from api.lib.perm.acl.acl import ACLManager
|
||||
from api.lib.perm.acl.acl import has_perm_from_args
|
||||
from api.lib.perm.acl.acl import is_app_admin
|
||||
from api.lib.perm.acl.acl import role_required
|
||||
from api.lib.utils import handle_arg_list
|
||||
from api.resource import APIView
|
||||
|
||||
app_cli = CMDBApp()
|
||||
@@ -42,6 +41,19 @@ class GetParentsView(APIView):
|
||||
return self.jsonify(parents=CITypeRelationManager.get_parents(child_id))
|
||||
|
||||
|
||||
class CITypeRelationPathView(APIView):
|
||||
url_prefix = ("/ci_type_relations/path",)
|
||||
|
||||
@args_required("source_type_id", "target_type_ids")
|
||||
def get(self):
|
||||
source_type_id = request.values.get("source_type_id")
|
||||
target_type_ids = handle_arg_list(request.values.get("target_type_ids"))
|
||||
|
||||
paths = CITypeRelationManager.find_path(source_type_id, target_type_ids)
|
||||
|
||||
return self.jsonify(paths=paths)
|
||||
|
||||
|
||||
class CITypeRelationView(APIView):
|
||||
url_prefix = ("/ci_type_relations", "/ci_type_relations/<int:parent_id>/<int:child_id>")
|
||||
|
||||
|
1
cmdb-api/api/views/cmdb/ipam/__init__.py
Normal file
1
cmdb-api/api/views/cmdb/ipam/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# -*- coding:utf-8 -*-
|
39
cmdb-api/api/views/cmdb/ipam/address.py
Normal file
39
cmdb-api/api/views/cmdb/ipam/address.py
Normal file
@@ -0,0 +1,39 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from flask import request
|
||||
|
||||
from api.lib.cmdb.ipam.address import IpAddressManager
|
||||
from api.lib.common_setting.decorator import perms_role_required
|
||||
from api.lib.common_setting.role_perm_base import CMDBApp
|
||||
from api.lib.decorator import args_required
|
||||
from api.lib.utils import handle_arg_list
|
||||
from api.resource import APIView
|
||||
|
||||
app_cli = CMDBApp()
|
||||
|
||||
|
||||
class IPAddressView(APIView):
|
||||
url_prefix = ("/ipam/address",)
|
||||
|
||||
@args_required("parent_id")
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
def get(self):
|
||||
parent_id = request.args.get("parent_id")
|
||||
|
||||
numfound, result = IpAddressManager.list_ip_address(parent_id)
|
||||
|
||||
return self.jsonify(numfound=numfound, result=result)
|
||||
|
||||
@args_required("ips")
|
||||
@args_required("assign_status", value_required=False)
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
def post(self):
|
||||
ips = handle_arg_list(request.values.pop("ips"))
|
||||
parent_id = request.values.pop("parent_id", None)
|
||||
cidr = request.values.pop("cidr", None)
|
||||
|
||||
IpAddressManager().assign_ips(ips, parent_id, cidr, **request.values)
|
||||
|
||||
return self.jsonify(code=200)
|
53
cmdb-api/api/views/cmdb/ipam/histories.py
Normal file
53
cmdb-api/api/views/cmdb/ipam/histories.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from flask import request
|
||||
|
||||
from api.lib.cmdb.ipam.history import OperateHistoryManager
|
||||
from api.lib.cmdb.ipam.history import ScanHistoryManager
|
||||
from api.lib.common_setting.decorator import perms_role_required
|
||||
from api.lib.common_setting.role_perm_base import CMDBApp
|
||||
from api.lib.decorator import args_required
|
||||
from api.lib.utils import get_page
|
||||
from api.lib.utils import get_page_size
|
||||
from api.lib.utils import handle_arg_list
|
||||
from api.resource import APIView
|
||||
|
||||
app_cli = CMDBApp()
|
||||
|
||||
|
||||
class IPAMOperateHistoryView(APIView):
|
||||
url_prefix = ("/ipam/history/operate",)
|
||||
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
def get(self):
|
||||
page = get_page(request.values.pop("page", 1))
|
||||
page_size = get_page_size(request.values.pop("page_size", None))
|
||||
operate_type = handle_arg_list(request.values.pop('operate_type', []))
|
||||
if operate_type:
|
||||
request.values["operate_type"] = operate_type
|
||||
|
||||
numfound, result = OperateHistoryManager.search(page, page_size, **request.values)
|
||||
|
||||
return self.jsonify(numfound=numfound, result=result)
|
||||
|
||||
|
||||
class IPAMScanHistoryView(APIView):
|
||||
url_prefix = ("/ipam/history/scan",)
|
||||
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
def get(self):
|
||||
page = get_page(request.values.pop("page", 1))
|
||||
page_size = get_page_size(request.values.pop("page_size", None))
|
||||
|
||||
numfound, result = ScanHistoryManager.search(page, page_size, **request.values)
|
||||
|
||||
return self.jsonify(numfound=numfound, result=result)
|
||||
|
||||
@args_required("exec_id")
|
||||
def post(self):
|
||||
|
||||
ScanHistoryManager().add(**request.values)
|
||||
|
||||
return self.jsonify(code=200)
|
24
cmdb-api/api/views/cmdb/ipam/ipam_stats.py
Normal file
24
cmdb-api/api/views/cmdb/ipam/ipam_stats.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
|
||||
from flask import request
|
||||
|
||||
from api.lib.cmdb.ipam.stats import Stats
|
||||
from api.lib.common_setting.decorator import perms_role_required
|
||||
from api.lib.common_setting.role_perm_base import CMDBApp
|
||||
from api.lib.decorator import args_required
|
||||
from api.resource import APIView
|
||||
|
||||
app_cli = CMDBApp()
|
||||
|
||||
|
||||
class IPAMStatsView(APIView):
|
||||
url_prefix = '/ipam/stats'
|
||||
|
||||
@args_required("parent_id")
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
def get(self):
|
||||
parent_id = request.values.get("parent_id")
|
||||
|
||||
return self.jsonify(Stats().summary(parent_id))
|
75
cmdb-api/api/views/cmdb/ipam/subnet.py
Normal file
75
cmdb-api/api/views/cmdb/ipam/subnet.py
Normal file
@@ -0,0 +1,75 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from flask import request
|
||||
|
||||
from api.lib.cmdb.ipam.subnet import SubnetManager
|
||||
from api.lib.cmdb.ipam.subnet import SubnetScopeManager
|
||||
from api.lib.common_setting.decorator import perms_role_required
|
||||
from api.lib.common_setting.role_perm_base import CMDBApp
|
||||
from api.lib.decorator import args_required
|
||||
from api.resource import APIView
|
||||
|
||||
app_cli = CMDBApp()
|
||||
|
||||
|
||||
class SubnetView(APIView):
|
||||
url_prefix = ("/ipam/subnet", "/ipam/subnet/hosts", "/ipam/subnet/<int:_id>")
|
||||
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
def get(self, _id=None):
|
||||
if "hosts" in request.url:
|
||||
return self.jsonify(SubnetManager.get_hosts(request.values.get('cidr')))
|
||||
|
||||
if _id is not None:
|
||||
return self.jsonify(SubnetManager().get_by_id(_id))
|
||||
|
||||
result, type2name = SubnetManager().tree_view()
|
||||
|
||||
return self.jsonify(result=result, type2name=type2name)
|
||||
|
||||
@args_required("cidr")
|
||||
@args_required("parent_id", value_required=False)
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
def post(self):
|
||||
cidr = request.values.pop("cidr")
|
||||
parent_id = request.values.pop("parent_id")
|
||||
agent_id = request.values.pop("agent_id", None)
|
||||
cron = request.values.pop("cron", None)
|
||||
|
||||
return self.jsonify(SubnetManager().add(cidr, parent_id, agent_id, cron, **request.values))
|
||||
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
def put(self, _id):
|
||||
return self.jsonify(id=SubnetManager().update(_id, **request.values))
|
||||
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
def delete(self, _id):
|
||||
return self.jsonify(id=SubnetManager().delete(_id))
|
||||
|
||||
|
||||
class SubnetScopeView(APIView):
|
||||
url_prefix = ("/ipam/scope", "/ipam/scope/<int:_id>")
|
||||
|
||||
@args_required("parent_id", value_required=False)
|
||||
@args_required("name")
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
def post(self):
|
||||
parent_id = request.values.pop("parent_id")
|
||||
name = request.values.pop("name")
|
||||
|
||||
return self.jsonify(SubnetScopeManager().add(parent_id, name))
|
||||
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
def put(self, _id):
|
||||
return self.jsonify(id=SubnetScopeManager().update(_id, **request.values))
|
||||
|
||||
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.IPAM,
|
||||
app_cli.op.read, app_cli.admin_name)
|
||||
def delete(self, _id):
|
||||
return self.jsonify(id=SubnetScopeManager.delete(_id))
|
@@ -16,7 +16,7 @@ Flask-Cors==4.0.0
|
||||
Flask-Login>=0.6.2
|
||||
Flask-Migrate==2.5.2
|
||||
Flask-RESTful==0.3.10
|
||||
Flask-SQLAlchemy==2.5.0
|
||||
Flask-SQLAlchemy==3.0.5
|
||||
future==0.18.3
|
||||
gunicorn==21.0.1
|
||||
hvac==2.0.0
|
||||
@@ -56,3 +56,5 @@ colorama>=0.4.6
|
||||
lz4>=4.3.2
|
||||
python-magic==0.4.27
|
||||
jsonpath==0.82.2
|
||||
networkx>=3.1
|
||||
ipaddress>=1.0.23
|
||||
|
@@ -39,9 +39,9 @@ SQLALCHEMY_ENGINE_OPTIONS = {
|
||||
|
||||
# # cache
|
||||
CACHE_TYPE = 'redis'
|
||||
CACHE_REDIS_HOST = '127.0.0.1'
|
||||
CACHE_REDIS_PORT = 6379
|
||||
CACHE_REDIS_PASSWORD = ''
|
||||
CACHE_REDIS_HOST = env.str('CACHE_REDIS_HOST', default='redis')
|
||||
CACHE_REDIS_PORT = env.str('CACHE_REDIS_PORT', default='6379')
|
||||
CACHE_REDIS_PASSWORD = env.str('CACHE_REDIS_PASSWORD', default='')
|
||||
CACHE_KEY_PREFIX = 'CMDB::'
|
||||
CACHE_DEFAULT_TIMEOUT = 3000
|
||||
|
||||
|
@@ -3,3 +3,4 @@ VUE_APP_PREVIEW=false
|
||||
VUE_APP_API_BASE_URL=http://127.0.0.1:5000/api
|
||||
VUE_APP_BUILD_PACKAGES="ticket,calendar,acl"
|
||||
VUE_APP_IS_OUTER=true
|
||||
VUE_APP_IS_OPEN_SOURCE=true
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 3857903 */
|
||||
src: url('iconfont.woff2?t=1721959219377') format('woff2'),
|
||||
url('iconfont.woff?t=1721959219377') format('woff'),
|
||||
url('iconfont.ttf?t=1721959219377') format('truetype');
|
||||
src: url('iconfont.woff2?t=1731312848138') format('woff2'),
|
||||
url('iconfont.woff?t=1731312848138') format('woff'),
|
||||
url('iconfont.ttf?t=1731312848138') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
@@ -13,6 +13,526 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.ops-setting-holidays:before {
|
||||
content: "\e9fa";
|
||||
}
|
||||
|
||||
.ops-itsm-logs:before {
|
||||
content: "\e9f8";
|
||||
}
|
||||
|
||||
.ops-setting-workday:before {
|
||||
content: "\e9f6";
|
||||
}
|
||||
|
||||
.ops-setting-holiday:before {
|
||||
content: "\e9f7";
|
||||
}
|
||||
|
||||
.ops-setting-festival:before {
|
||||
content: "\e9f5";
|
||||
}
|
||||
|
||||
.itsm-calc:before {
|
||||
content: "\e9f4";
|
||||
}
|
||||
|
||||
.itsm-reports_4:before {
|
||||
content: "\e9f3";
|
||||
}
|
||||
|
||||
.veops-folder:before {
|
||||
content: "\e9f2";
|
||||
}
|
||||
|
||||
.veops-entire_network_:before {
|
||||
content: "\e9f1";
|
||||
}
|
||||
|
||||
.veops-subnet:before {
|
||||
content: "\e9f0";
|
||||
}
|
||||
|
||||
.veops-map_view:before {
|
||||
content: "\e9ef";
|
||||
}
|
||||
|
||||
.veops-recycle:before {
|
||||
content: "\e9ee";
|
||||
}
|
||||
|
||||
.veops-catalog:before {
|
||||
content: "\e9ed";
|
||||
}
|
||||
|
||||
.veops-ipam:before {
|
||||
content: "\e9ec";
|
||||
}
|
||||
|
||||
.cmdb-calc:before {
|
||||
content: "\e9eb";
|
||||
}
|
||||
|
||||
.ai-users:before {
|
||||
content: "\e9ea";
|
||||
}
|
||||
|
||||
.ai-tokens:before {
|
||||
content: "\e9e9";
|
||||
}
|
||||
|
||||
.oneterm-mysql:before {
|
||||
content: "\e9e8";
|
||||
}
|
||||
|
||||
.oneterm-redis:before {
|
||||
content: "\e9e7";
|
||||
}
|
||||
|
||||
.veops-sign_out:before {
|
||||
content: "\e9e6";
|
||||
}
|
||||
|
||||
.veops-company:before {
|
||||
content: "\e9e5";
|
||||
}
|
||||
|
||||
.veops-emails:before {
|
||||
content: "\e9e4";
|
||||
}
|
||||
|
||||
.veops-switch:before {
|
||||
content: "\e9e3";
|
||||
}
|
||||
|
||||
.qiyeweixin:before {
|
||||
content: "\e9e2";
|
||||
}
|
||||
|
||||
.veops-progress:before {
|
||||
content: "\e9e1";
|
||||
}
|
||||
|
||||
.veops-completed:before {
|
||||
content: "\e9e0";
|
||||
}
|
||||
|
||||
.itsm-ticketTime:before {
|
||||
content: "\e9df";
|
||||
}
|
||||
|
||||
.veops-notification:before {
|
||||
content: "\e9dc";
|
||||
}
|
||||
|
||||
.a-veops-account1:before {
|
||||
content: "\e9dd";
|
||||
}
|
||||
|
||||
.veops-personal:before {
|
||||
content: "\e9de";
|
||||
}
|
||||
|
||||
.itsm-customer_satisfaction2:before {
|
||||
content: "\e9da";
|
||||
}
|
||||
|
||||
.itsm-over2:before {
|
||||
content: "\e9db";
|
||||
}
|
||||
|
||||
.veops-search1:before {
|
||||
content: "\e9d9";
|
||||
}
|
||||
|
||||
.itsm-customer_satisfaction:before {
|
||||
content: "\e9d8";
|
||||
}
|
||||
|
||||
.itsm-over:before {
|
||||
content: "\e9d7";
|
||||
}
|
||||
|
||||
.itsm-request:before {
|
||||
content: "\e9d6";
|
||||
}
|
||||
|
||||
.itsm-release:before {
|
||||
content: "\e9d5";
|
||||
}
|
||||
|
||||
.veops-link:before {
|
||||
content: "\e9d4";
|
||||
}
|
||||
|
||||
.oneterm-command_record:before {
|
||||
content: "\e9d3";
|
||||
}
|
||||
|
||||
.ai-question:before {
|
||||
content: "\e9d2";
|
||||
}
|
||||
|
||||
.ai-sending:before {
|
||||
content: "\e9d1";
|
||||
}
|
||||
|
||||
.ai-dialogue:before {
|
||||
content: "\e9d0";
|
||||
}
|
||||
|
||||
.ai-report2:before {
|
||||
content: "\e9cf";
|
||||
}
|
||||
|
||||
.ai-delete:before {
|
||||
content: "\e9cd";
|
||||
}
|
||||
|
||||
.caise-knowledge:before {
|
||||
content: "\e9ce";
|
||||
}
|
||||
|
||||
.ai-article:before {
|
||||
content: "\e9cc";
|
||||
}
|
||||
|
||||
.ai-model_setup1:before {
|
||||
content: "\e9cb";
|
||||
}
|
||||
|
||||
.ai-report:before {
|
||||
content: "\e9ca";
|
||||
}
|
||||
|
||||
.ai-customer_service:before {
|
||||
content: "\e9c9";
|
||||
}
|
||||
|
||||
.oneterm-connect1:before {
|
||||
content: "\e9c6";
|
||||
}
|
||||
|
||||
.oneterm-session1:before {
|
||||
content: "\e9c7";
|
||||
}
|
||||
|
||||
.oneterm-assets:before {
|
||||
content: "\e9c8";
|
||||
}
|
||||
|
||||
.a-oneterm-ssh1:before {
|
||||
content: "\e9c3";
|
||||
}
|
||||
|
||||
.a-oneterm-ssh2:before {
|
||||
content: "\e9c4";
|
||||
}
|
||||
|
||||
.oneterm-rdp:before {
|
||||
content: "\e9c5";
|
||||
}
|
||||
|
||||
.caise-websphere:before {
|
||||
content: "\e9c2";
|
||||
}
|
||||
|
||||
.caise-vps:before {
|
||||
content: "\e9c1";
|
||||
}
|
||||
|
||||
.caise-F5:before {
|
||||
content: "\e9c0";
|
||||
}
|
||||
|
||||
.caise-HAProxy:before {
|
||||
content: "\e9bf";
|
||||
}
|
||||
|
||||
.caise-JBoss:before {
|
||||
content: "\e9be";
|
||||
}
|
||||
|
||||
.caise-dongfangtong:before {
|
||||
content: "\e9bd";
|
||||
}
|
||||
|
||||
.caise-kafka:before {
|
||||
content: "\e9b7";
|
||||
}
|
||||
|
||||
.caise-weblogic:before {
|
||||
content: "\e9b8";
|
||||
}
|
||||
|
||||
.caise-TDSQL:before {
|
||||
content: "\e9b9";
|
||||
}
|
||||
|
||||
.caise-kingbase:before {
|
||||
content: "\e9ba";
|
||||
}
|
||||
|
||||
.caise-dameng:before {
|
||||
content: "\e9bb";
|
||||
}
|
||||
|
||||
.caise-TIDB:before {
|
||||
content: "\e9bc";
|
||||
}
|
||||
|
||||
.veops-expand:before {
|
||||
content: "\e9b6";
|
||||
}
|
||||
|
||||
.caise-public_cloud:before {
|
||||
content: "\e9b1";
|
||||
}
|
||||
|
||||
.caise-system:before {
|
||||
content: "\e9b2";
|
||||
}
|
||||
|
||||
.caise-IPAM:before {
|
||||
content: "\e9b3";
|
||||
}
|
||||
|
||||
.caise-hyperV:before {
|
||||
content: "\e9b4";
|
||||
}
|
||||
|
||||
.caise-data_center2:before {
|
||||
content: "\e9b5";
|
||||
}
|
||||
|
||||
.caise-hardware:before {
|
||||
content: "\e9ad";
|
||||
}
|
||||
|
||||
.caise-computer:before {
|
||||
content: "\e9ae";
|
||||
}
|
||||
|
||||
.caise-network_devices:before {
|
||||
content: "\e9af";
|
||||
}
|
||||
|
||||
.caise-storage_device:before {
|
||||
content: "\e9b0";
|
||||
}
|
||||
|
||||
.caise-load_balancing:before {
|
||||
content: "\e9ab";
|
||||
}
|
||||
|
||||
.caise-message_queue:before {
|
||||
content: "\e9ac";
|
||||
}
|
||||
|
||||
.caise-websever:before {
|
||||
content: "\e9aa";
|
||||
}
|
||||
|
||||
.caise-middleware:before {
|
||||
content: "\e9a9";
|
||||
}
|
||||
|
||||
.caise-database:before {
|
||||
content: "\e9a7";
|
||||
}
|
||||
|
||||
.caise-business:before {
|
||||
content: "\e9a8";
|
||||
}
|
||||
|
||||
.caise-virtualization:before {
|
||||
content: "\e9a6";
|
||||
}
|
||||
|
||||
.caise-storage_pool:before {
|
||||
content: "\e9a4";
|
||||
}
|
||||
|
||||
.caise-storage_volume1:before {
|
||||
content: "\e9a5";
|
||||
}
|
||||
|
||||
.ciase-aix:before {
|
||||
content: "\e9a3";
|
||||
}
|
||||
|
||||
.caise_pool:before {
|
||||
content: "\e99b";
|
||||
}
|
||||
|
||||
.caise-ip_address:before {
|
||||
content: "\e99c";
|
||||
}
|
||||
|
||||
.caise-computer_room:before {
|
||||
content: "\e99d";
|
||||
}
|
||||
|
||||
.caise-rack:before {
|
||||
content: "\e99e";
|
||||
}
|
||||
|
||||
.caise-pc:before {
|
||||
content: "\e99f";
|
||||
}
|
||||
|
||||
.caise-bandwidth_line:before {
|
||||
content: "\e9a0";
|
||||
}
|
||||
|
||||
.caise-fiber:before {
|
||||
content: "\e9a1";
|
||||
}
|
||||
|
||||
.caise-disk_array:before {
|
||||
content: "\e9a2";
|
||||
}
|
||||
|
||||
.veops-group:before {
|
||||
content: "\e99a";
|
||||
}
|
||||
|
||||
.veops-inheritance:before {
|
||||
content: "\e999";
|
||||
}
|
||||
|
||||
.veops-department:before {
|
||||
content: "\e998";
|
||||
}
|
||||
|
||||
.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";
|
||||
}
|
||||
@@ -509,11 +1029,11 @@
|
||||
content: "\e914";
|
||||
}
|
||||
|
||||
.itsm-duration:before {
|
||||
.itsm-reports_3:before {
|
||||
content: "\e913";
|
||||
}
|
||||
|
||||
.itsm-workload:before {
|
||||
.itsm-reports_2:before {
|
||||
content: "\e912";
|
||||
}
|
||||
|
||||
|
File diff suppressed because one or more lines are too long
@@ -5,6 +5,916 @@
|
||||
"css_prefix_text": "",
|
||||
"description": "",
|
||||
"glyphs": [
|
||||
{
|
||||
"icon_id": "42337844",
|
||||
"name": "ops-setting-holiday_management-copy",
|
||||
"font_class": "ops-setting-holidays",
|
||||
"unicode": "e9fa",
|
||||
"unicode_decimal": 59898
|
||||
},
|
||||
{
|
||||
"icon_id": "42335414",
|
||||
"name": "itsm-system_log",
|
||||
"font_class": "ops-itsm-logs",
|
||||
"unicode": "e9f8",
|
||||
"unicode_decimal": 59896
|
||||
},
|
||||
{
|
||||
"icon_id": "42334782",
|
||||
"name": "ops-setting-adjustday",
|
||||
"font_class": "ops-setting-workday",
|
||||
"unicode": "e9f6",
|
||||
"unicode_decimal": 59894
|
||||
},
|
||||
{
|
||||
"icon_id": "42334768",
|
||||
"name": "ops-setting-holiday",
|
||||
"font_class": "ops-setting-holiday",
|
||||
"unicode": "e9f7",
|
||||
"unicode_decimal": 59895
|
||||
},
|
||||
{
|
||||
"icon_id": "42334734",
|
||||
"name": "ops-setting-festival",
|
||||
"font_class": "ops-setting-festival",
|
||||
"unicode": "e9f5",
|
||||
"unicode_decimal": 59893
|
||||
},
|
||||
{
|
||||
"icon_id": "42281202",
|
||||
"name": "itsm-count",
|
||||
"font_class": "itsm-calc",
|
||||
"unicode": "e9f4",
|
||||
"unicode_decimal": 59892
|
||||
},
|
||||
{
|
||||
"icon_id": "42270632",
|
||||
"name": "itsm-satisfaction",
|
||||
"font_class": "itsm-reports_4",
|
||||
"unicode": "e9f3",
|
||||
"unicode_decimal": 59891
|
||||
},
|
||||
{
|
||||
"icon_id": "42205149",
|
||||
"name": "veops-folder",
|
||||
"font_class": "veops-folder",
|
||||
"unicode": "e9f2",
|
||||
"unicode_decimal": 59890
|
||||
},
|
||||
{
|
||||
"icon_id": "42205128",
|
||||
"name": "veops-entire_network_",
|
||||
"font_class": "veops-entire_network_",
|
||||
"unicode": "e9f1",
|
||||
"unicode_decimal": 59889
|
||||
},
|
||||
{
|
||||
"icon_id": "42205094",
|
||||
"name": "veops-subnet",
|
||||
"font_class": "veops-subnet",
|
||||
"unicode": "e9f0",
|
||||
"unicode_decimal": 59888
|
||||
},
|
||||
{
|
||||
"icon_id": "42201912",
|
||||
"name": "veops-map_view",
|
||||
"font_class": "veops-map_view",
|
||||
"unicode": "e9ef",
|
||||
"unicode_decimal": 59887
|
||||
},
|
||||
{
|
||||
"icon_id": "42201676",
|
||||
"name": "veops-recycle",
|
||||
"font_class": "veops-recycle",
|
||||
"unicode": "e9ee",
|
||||
"unicode_decimal": 59886
|
||||
},
|
||||
{
|
||||
"icon_id": "42201586",
|
||||
"name": "veops-catalog",
|
||||
"font_class": "veops-catalog",
|
||||
"unicode": "e9ed",
|
||||
"unicode_decimal": 59885
|
||||
},
|
||||
{
|
||||
"icon_id": "42201534",
|
||||
"name": "veops-ipam",
|
||||
"font_class": "veops-ipam",
|
||||
"unicode": "e9ec",
|
||||
"unicode_decimal": 59884
|
||||
},
|
||||
{
|
||||
"icon_id": "42179262",
|
||||
"name": "cmdb-calc",
|
||||
"font_class": "cmdb-calc",
|
||||
"unicode": "e9eb",
|
||||
"unicode_decimal": 59883
|
||||
},
|
||||
{
|
||||
"icon_id": "42161413",
|
||||
"name": "ai-users",
|
||||
"font_class": "ai-users",
|
||||
"unicode": "e9ea",
|
||||
"unicode_decimal": 59882
|
||||
},
|
||||
{
|
||||
"icon_id": "42161417",
|
||||
"name": "ai-tokens",
|
||||
"font_class": "ai-tokens",
|
||||
"unicode": "e9e9",
|
||||
"unicode_decimal": 59881
|
||||
},
|
||||
{
|
||||
"icon_id": "42155223",
|
||||
"name": "oneterm-mysql",
|
||||
"font_class": "oneterm-mysql",
|
||||
"unicode": "e9e8",
|
||||
"unicode_decimal": 59880
|
||||
},
|
||||
{
|
||||
"icon_id": "42155225",
|
||||
"name": "oneterm-redis",
|
||||
"font_class": "oneterm-redis",
|
||||
"unicode": "e9e7",
|
||||
"unicode_decimal": 59879
|
||||
},
|
||||
{
|
||||
"icon_id": "42154436",
|
||||
"name": "veops-sign_out",
|
||||
"font_class": "veops-sign_out",
|
||||
"unicode": "e9e6",
|
||||
"unicode_decimal": 59878
|
||||
},
|
||||
{
|
||||
"icon_id": "42154310",
|
||||
"name": "veops-company",
|
||||
"font_class": "veops-company",
|
||||
"unicode": "e9e5",
|
||||
"unicode_decimal": 59877
|
||||
},
|
||||
{
|
||||
"icon_id": "42154325",
|
||||
"name": "veops-emails",
|
||||
"font_class": "veops-emails",
|
||||
"unicode": "e9e4",
|
||||
"unicode_decimal": 59876
|
||||
},
|
||||
{
|
||||
"icon_id": "42154350",
|
||||
"name": "veops-switch",
|
||||
"font_class": "veops-switch",
|
||||
"unicode": "e9e3",
|
||||
"unicode_decimal": 59875
|
||||
},
|
||||
{
|
||||
"icon_id": "42154370",
|
||||
"name": "veops-qiyeweixin",
|
||||
"font_class": "qiyeweixin",
|
||||
"unicode": "e9e2",
|
||||
"unicode_decimal": 59874
|
||||
},
|
||||
{
|
||||
"icon_id": "42134185",
|
||||
"name": "veops-progress",
|
||||
"font_class": "veops-progress",
|
||||
"unicode": "e9e1",
|
||||
"unicode_decimal": 59873
|
||||
},
|
||||
{
|
||||
"icon_id": "42134110",
|
||||
"name": "veops-completed",
|
||||
"font_class": "veops-completed",
|
||||
"unicode": "e9e0",
|
||||
"unicode_decimal": 59872
|
||||
},
|
||||
{
|
||||
"icon_id": "42133882",
|
||||
"name": "itsm-ticketTime",
|
||||
"font_class": "itsm-ticketTime",
|
||||
"unicode": "e9df",
|
||||
"unicode_decimal": 59871
|
||||
},
|
||||
{
|
||||
"icon_id": "42122869",
|
||||
"name": "veops-notification",
|
||||
"font_class": "veops-notification",
|
||||
"unicode": "e9dc",
|
||||
"unicode_decimal": 59868
|
||||
},
|
||||
{
|
||||
"icon_id": "42122868",
|
||||
"name": "veops-account_password",
|
||||
"font_class": "a-veops-account1",
|
||||
"unicode": "e9dd",
|
||||
"unicode_decimal": 59869
|
||||
},
|
||||
{
|
||||
"icon_id": "42122861",
|
||||
"name": "veops-personal",
|
||||
"font_class": "veops-personal",
|
||||
"unicode": "e9de",
|
||||
"unicode_decimal": 59870
|
||||
},
|
||||
{
|
||||
"icon_id": "42101103",
|
||||
"name": "itsm-evaluation2",
|
||||
"font_class": "itsm-customer_satisfaction2",
|
||||
"unicode": "e9da",
|
||||
"unicode_decimal": 59866
|
||||
},
|
||||
{
|
||||
"icon_id": "42101098",
|
||||
"name": "itsm-over2",
|
||||
"font_class": "itsm-over2",
|
||||
"unicode": "e9db",
|
||||
"unicode_decimal": 59867
|
||||
},
|
||||
{
|
||||
"icon_id": "42065574",
|
||||
"name": "veops-search",
|
||||
"font_class": "veops-search1",
|
||||
"unicode": "e9d9",
|
||||
"unicode_decimal": 59865
|
||||
},
|
||||
{
|
||||
"icon_id": "42063479",
|
||||
"name": "itsm-evaluation",
|
||||
"font_class": "itsm-customer_satisfaction",
|
||||
"unicode": "e9d8",
|
||||
"unicode_decimal": 59864
|
||||
},
|
||||
{
|
||||
"icon_id": "42062436",
|
||||
"name": "itsm-over",
|
||||
"font_class": "itsm-over",
|
||||
"unicode": "e9d7",
|
||||
"unicode_decimal": 59863
|
||||
},
|
||||
{
|
||||
"icon_id": "42050642",
|
||||
"name": "itsm-requirement",
|
||||
"font_class": "itsm-request",
|
||||
"unicode": "e9d6",
|
||||
"unicode_decimal": 59862
|
||||
},
|
||||
{
|
||||
"icon_id": "42050622",
|
||||
"name": "itsm-release",
|
||||
"font_class": "itsm-release",
|
||||
"unicode": "e9d5",
|
||||
"unicode_decimal": 59861
|
||||
},
|
||||
{
|
||||
"icon_id": "41903314",
|
||||
"name": "veops-link",
|
||||
"font_class": "veops-link",
|
||||
"unicode": "e9d4",
|
||||
"unicode_decimal": 59860
|
||||
},
|
||||
{
|
||||
"icon_id": "41876664",
|
||||
"name": "oneterm-command_record",
|
||||
"font_class": "oneterm-command_record",
|
||||
"unicode": "e9d3",
|
||||
"unicode_decimal": 59859
|
||||
},
|
||||
{
|
||||
"icon_id": "41859436",
|
||||
"name": "ai-question",
|
||||
"font_class": "ai-question",
|
||||
"unicode": "e9d2",
|
||||
"unicode_decimal": 59858
|
||||
},
|
||||
{
|
||||
"icon_id": "41859414",
|
||||
"name": "ai-sending",
|
||||
"font_class": "ai-sending",
|
||||
"unicode": "e9d1",
|
||||
"unicode_decimal": 59857
|
||||
},
|
||||
{
|
||||
"icon_id": "41859374",
|
||||
"name": "ai-dialogue",
|
||||
"font_class": "ai-dialogue",
|
||||
"unicode": "e9d0",
|
||||
"unicode_decimal": 59856
|
||||
},
|
||||
{
|
||||
"icon_id": "41859191",
|
||||
"name": "ai-report2",
|
||||
"font_class": "ai-report2",
|
||||
"unicode": "e9cf",
|
||||
"unicode_decimal": 59855
|
||||
},
|
||||
{
|
||||
"icon_id": "41858720",
|
||||
"name": "ai-delete",
|
||||
"font_class": "ai-delete",
|
||||
"unicode": "e9cd",
|
||||
"unicode_decimal": 59853
|
||||
},
|
||||
{
|
||||
"icon_id": "41858484",
|
||||
"name": "caise-knowledge",
|
||||
"font_class": "caise-knowledge",
|
||||
"unicode": "e9ce",
|
||||
"unicode_decimal": 59854
|
||||
},
|
||||
{
|
||||
"icon_id": "41833445",
|
||||
"name": "ai-article",
|
||||
"font_class": "ai-article",
|
||||
"unicode": "e9cc",
|
||||
"unicode_decimal": 59852
|
||||
},
|
||||
{
|
||||
"icon_id": "41811974",
|
||||
"name": "ai-model_setup (1)",
|
||||
"font_class": "ai-model_setup1",
|
||||
"unicode": "e9cb",
|
||||
"unicode_decimal": 59851
|
||||
},
|
||||
{
|
||||
"icon_id": "41811980",
|
||||
"name": "ai-report",
|
||||
"font_class": "ai-report",
|
||||
"unicode": "e9ca",
|
||||
"unicode_decimal": 59850
|
||||
},
|
||||
{
|
||||
"icon_id": "41811915",
|
||||
"name": "ai-customer_service",
|
||||
"font_class": "ai-customer_service",
|
||||
"unicode": "e9c9",
|
||||
"unicode_decimal": 59849
|
||||
},
|
||||
{
|
||||
"icon_id": "41735717",
|
||||
"name": "oneterm-connect",
|
||||
"font_class": "oneterm-connect1",
|
||||
"unicode": "e9c6",
|
||||
"unicode_decimal": 59846
|
||||
},
|
||||
{
|
||||
"icon_id": "41735716",
|
||||
"name": "oneterm-session",
|
||||
"font_class": "oneterm-session1",
|
||||
"unicode": "e9c7",
|
||||
"unicode_decimal": 59847
|
||||
},
|
||||
{
|
||||
"icon_id": "41735703",
|
||||
"name": "oneterm-assets",
|
||||
"font_class": "oneterm-assets",
|
||||
"unicode": "e9c8",
|
||||
"unicode_decimal": 59848
|
||||
},
|
||||
{
|
||||
"icon_id": "41725683",
|
||||
"name": "oneterm-RDP",
|
||||
"font_class": "a-oneterm-ssh1",
|
||||
"unicode": "e9c3",
|
||||
"unicode_decimal": 59843
|
||||
},
|
||||
{
|
||||
"icon_id": "41725684",
|
||||
"name": "oneterm-SSH",
|
||||
"font_class": "a-oneterm-ssh2",
|
||||
"unicode": "e9c4",
|
||||
"unicode_decimal": 59844
|
||||
},
|
||||
{
|
||||
"icon_id": "41725685",
|
||||
"name": "oneterm-VNC",
|
||||
"font_class": "oneterm-rdp",
|
||||
"unicode": "e9c5",
|
||||
"unicode_decimal": 59845
|
||||
},
|
||||
{
|
||||
"icon_id": "41724497",
|
||||
"name": "caise-websphere",
|
||||
"font_class": "caise-websphere",
|
||||
"unicode": "e9c2",
|
||||
"unicode_decimal": 59842
|
||||
},
|
||||
{
|
||||
"icon_id": "41724575",
|
||||
"name": "caise-vps",
|
||||
"font_class": "caise-vps",
|
||||
"unicode": "e9c1",
|
||||
"unicode_decimal": 59841
|
||||
},
|
||||
{
|
||||
"icon_id": "41724631",
|
||||
"name": "caise-F5",
|
||||
"font_class": "caise-F5",
|
||||
"unicode": "e9c0",
|
||||
"unicode_decimal": 59840
|
||||
},
|
||||
{
|
||||
"icon_id": "41724653",
|
||||
"name": "caise-HAProxy",
|
||||
"font_class": "caise-HAProxy",
|
||||
"unicode": "e9bf",
|
||||
"unicode_decimal": 59839
|
||||
},
|
||||
{
|
||||
"icon_id": "41722953",
|
||||
"name": "caise-JBoss",
|
||||
"font_class": "caise-JBoss",
|
||||
"unicode": "e9be",
|
||||
"unicode_decimal": 59838
|
||||
},
|
||||
{
|
||||
"icon_id": "41722960",
|
||||
"name": "caise-dongfangtong",
|
||||
"font_class": "caise-dongfangtong",
|
||||
"unicode": "e9bd",
|
||||
"unicode_decimal": 59837
|
||||
},
|
||||
{
|
||||
"icon_id": "41722681",
|
||||
"name": "caise-kafka",
|
||||
"font_class": "caise-kafka",
|
||||
"unicode": "e9b7",
|
||||
"unicode_decimal": 59831
|
||||
},
|
||||
{
|
||||
"icon_id": "41722680",
|
||||
"name": "caise-weblogic",
|
||||
"font_class": "caise-weblogic",
|
||||
"unicode": "e9b8",
|
||||
"unicode_decimal": 59832
|
||||
},
|
||||
{
|
||||
"icon_id": "41722679",
|
||||
"name": "caise-TDSQL",
|
||||
"font_class": "caise-TDSQL",
|
||||
"unicode": "e9b9",
|
||||
"unicode_decimal": 59833
|
||||
},
|
||||
{
|
||||
"icon_id": "41722678",
|
||||
"name": "caise-kingbase",
|
||||
"font_class": "caise-kingbase",
|
||||
"unicode": "e9ba",
|
||||
"unicode_decimal": 59834
|
||||
},
|
||||
{
|
||||
"icon_id": "41722677",
|
||||
"name": "达梦",
|
||||
"font_class": "caise-dameng",
|
||||
"unicode": "e9bb",
|
||||
"unicode_decimal": 59835
|
||||
},
|
||||
{
|
||||
"icon_id": "41722675",
|
||||
"name": "caise-TIDB",
|
||||
"font_class": "caise-TIDB",
|
||||
"unicode": "e9bc",
|
||||
"unicode_decimal": 59836
|
||||
},
|
||||
{
|
||||
"icon_id": "41681675",
|
||||
"name": "veops-expand",
|
||||
"font_class": "veops-expand",
|
||||
"unicode": "e9b6",
|
||||
"unicode_decimal": 59830
|
||||
},
|
||||
{
|
||||
"icon_id": "41672951",
|
||||
"name": "公有云",
|
||||
"font_class": "caise-public_cloud",
|
||||
"unicode": "e9b1",
|
||||
"unicode_decimal": 59825
|
||||
},
|
||||
{
|
||||
"icon_id": "41672952",
|
||||
"name": "操作系统",
|
||||
"font_class": "caise-system",
|
||||
"unicode": "e9b2",
|
||||
"unicode_decimal": 59826
|
||||
},
|
||||
{
|
||||
"icon_id": "41673309",
|
||||
"name": "IPAM",
|
||||
"font_class": "caise-IPAM",
|
||||
"unicode": "e9b3",
|
||||
"unicode_decimal": 59827
|
||||
},
|
||||
{
|
||||
"icon_id": "41673312",
|
||||
"name": "hyperV",
|
||||
"font_class": "caise-hyperV",
|
||||
"unicode": "e9b4",
|
||||
"unicode_decimal": 59828
|
||||
},
|
||||
{
|
||||
"icon_id": "41673320",
|
||||
"name": "数据中心",
|
||||
"font_class": "caise-data_center2",
|
||||
"unicode": "e9b5",
|
||||
"unicode_decimal": 59829
|
||||
},
|
||||
{
|
||||
"icon_id": "41669141",
|
||||
"name": "硬件设备",
|
||||
"font_class": "caise-hardware",
|
||||
"unicode": "e9ad",
|
||||
"unicode_decimal": 59821
|
||||
},
|
||||
{
|
||||
"icon_id": "41669249",
|
||||
"name": "计算机",
|
||||
"font_class": "caise-computer",
|
||||
"unicode": "e9ae",
|
||||
"unicode_decimal": 59822
|
||||
},
|
||||
{
|
||||
"icon_id": "41669250",
|
||||
"name": "网络设备",
|
||||
"font_class": "caise-network_devices",
|
||||
"unicode": "e9af",
|
||||
"unicode_decimal": 59823
|
||||
},
|
||||
{
|
||||
"icon_id": "41669278",
|
||||
"name": "存储设备",
|
||||
"font_class": "caise-storage_device",
|
||||
"unicode": "e9b0",
|
||||
"unicode_decimal": 59824
|
||||
},
|
||||
{
|
||||
"icon_id": "41659452",
|
||||
"name": "负载均衡",
|
||||
"font_class": "caise-load_balancing",
|
||||
"unicode": "e9ab",
|
||||
"unicode_decimal": 59819
|
||||
},
|
||||
{
|
||||
"icon_id": "41659446",
|
||||
"name": "消息队列",
|
||||
"font_class": "caise-message_queue",
|
||||
"unicode": "e9ac",
|
||||
"unicode_decimal": 59820
|
||||
},
|
||||
{
|
||||
"icon_id": "41659424",
|
||||
"name": "websever",
|
||||
"font_class": "caise-websever",
|
||||
"unicode": "e9aa",
|
||||
"unicode_decimal": 59818
|
||||
},
|
||||
{
|
||||
"icon_id": "41655608",
|
||||
"name": "中间件",
|
||||
"font_class": "caise-middleware",
|
||||
"unicode": "e9a9",
|
||||
"unicode_decimal": 59817
|
||||
},
|
||||
{
|
||||
"icon_id": "41655599",
|
||||
"name": "数据库",
|
||||
"font_class": "caise-database",
|
||||
"unicode": "e9a7",
|
||||
"unicode_decimal": 59815
|
||||
},
|
||||
{
|
||||
"icon_id": "41655591",
|
||||
"name": "业务",
|
||||
"font_class": "caise-business",
|
||||
"unicode": "e9a8",
|
||||
"unicode_decimal": 59816
|
||||
},
|
||||
{
|
||||
"icon_id": "41655550",
|
||||
"name": "虚拟化",
|
||||
"font_class": "caise-virtualization",
|
||||
"unicode": "e9a6",
|
||||
"unicode_decimal": 59814
|
||||
},
|
||||
{
|
||||
"icon_id": "41654680",
|
||||
"name": "存储池",
|
||||
"font_class": "caise-storage_pool",
|
||||
"unicode": "e9a4",
|
||||
"unicode_decimal": 59812
|
||||
},
|
||||
{
|
||||
"icon_id": "41654676",
|
||||
"name": "存储卷",
|
||||
"font_class": "caise-storage_volume1",
|
||||
"unicode": "e9a5",
|
||||
"unicode_decimal": 59813
|
||||
},
|
||||
{
|
||||
"icon_id": "41654608",
|
||||
"name": "aix",
|
||||
"font_class": "ciase-aix",
|
||||
"unicode": "e9a3",
|
||||
"unicode_decimal": 59811
|
||||
},
|
||||
{
|
||||
"icon_id": "41654233",
|
||||
"name": "ip池",
|
||||
"font_class": "caise_pool",
|
||||
"unicode": "e99b",
|
||||
"unicode_decimal": 59803
|
||||
},
|
||||
{
|
||||
"icon_id": "41654237",
|
||||
"name": "ip地址",
|
||||
"font_class": "caise-ip_address",
|
||||
"unicode": "e99c",
|
||||
"unicode_decimal": 59804
|
||||
},
|
||||
{
|
||||
"icon_id": "41654249",
|
||||
"name": "机房",
|
||||
"font_class": "caise-computer_room",
|
||||
"unicode": "e99d",
|
||||
"unicode_decimal": 59805
|
||||
},
|
||||
{
|
||||
"icon_id": "41654271",
|
||||
"name": "机柜",
|
||||
"font_class": "caise-rack",
|
||||
"unicode": "e99e",
|
||||
"unicode_decimal": 59806
|
||||
},
|
||||
{
|
||||
"icon_id": "41654276",
|
||||
"name": "PC",
|
||||
"font_class": "caise-pc",
|
||||
"unicode": "e99f",
|
||||
"unicode_decimal": 59807
|
||||
},
|
||||
{
|
||||
"icon_id": "41654305",
|
||||
"name": "带宽线路",
|
||||
"font_class": "caise-bandwidth_line",
|
||||
"unicode": "e9a0",
|
||||
"unicode_decimal": 59808
|
||||
},
|
||||
{
|
||||
"icon_id": "41654323",
|
||||
"name": "光纤交换机",
|
||||
"font_class": "caise-fiber",
|
||||
"unicode": "e9a1",
|
||||
"unicode_decimal": 59809
|
||||
},
|
||||
{
|
||||
"icon_id": "41654369",
|
||||
"name": "磁盘阵列",
|
||||
"font_class": "caise-disk_array",
|
||||
"unicode": "e9a2",
|
||||
"unicode_decimal": 59810
|
||||
},
|
||||
{
|
||||
"icon_id": "41643869",
|
||||
"name": "veops-group",
|
||||
"font_class": "veops-group",
|
||||
"unicode": "e99a",
|
||||
"unicode_decimal": 59802
|
||||
},
|
||||
{
|
||||
"icon_id": "41637123",
|
||||
"name": "veops-inheritance",
|
||||
"font_class": "veops-inheritance",
|
||||
"unicode": "e999",
|
||||
"unicode_decimal": 59801
|
||||
},
|
||||
{
|
||||
"icon_id": "41570722",
|
||||
"name": "veops-department",
|
||||
"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-数据中心",
|
||||
@@ -876,14 +1786,14 @@
|
||||
{
|
||||
"icon_id": "39926816",
|
||||
"name": "itsm-duration",
|
||||
"font_class": "itsm-duration",
|
||||
"font_class": "itsm-reports_3",
|
||||
"unicode": "e913",
|
||||
"unicode_decimal": 59667
|
||||
},
|
||||
{
|
||||
"icon_id": "39926833",
|
||||
"name": "itsm-workload (1)",
|
||||
"font_class": "itsm-workload",
|
||||
"font_class": "itsm-reports_2",
|
||||
"unicode": "e912",
|
||||
"unicode_decimal": 59666
|
||||
},
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -61,12 +61,12 @@ export default {
|
||||
)
|
||||
|
||||
// 注册富文本自定义元素
|
||||
const resume = {
|
||||
type: 'attachment',
|
||||
attachmentLabel: '',
|
||||
attachmentValue: '',
|
||||
children: [{ text: '' }], // void 元素必须有一个 children ,其中只有一个空字符串,重要!!!
|
||||
}
|
||||
// const resume = {
|
||||
// type: 'attachment',
|
||||
// attachmentLabel: '',
|
||||
// attachmentValue: '',
|
||||
// children: [{ text: '' }], // void 元素必须有一个 children ,其中只有一个空字符串,重要!!!
|
||||
// }
|
||||
|
||||
function withAttachment(editor) {
|
||||
// JS 语法
|
||||
|
18
cmdb-ui/src/api/cmdb.js
Normal file
18
cmdb-ui/src/api/cmdb.js
Normal 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
|
||||
})
|
||||
}
|
@@ -84,7 +84,48 @@
|
||||
: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' }"
|
||||
@@ -92,15 +133,15 @@
|
||||
:multiple="false"
|
||||
:clearable="false"
|
||||
searchable
|
||||
v-if="isChoiceByProperty(item.property) && (item.exp === 'is' || item.exp === '~is')"
|
||||
v-else-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,
|
||||
id: String(node[0] || ''),
|
||||
label: node[1] ? node[1].label || node[0] : node[0],
|
||||
children: node.children && node.children.length ? node.children : undefined,
|
||||
}
|
||||
}
|
||||
"
|
||||
@@ -199,10 +240,11 @@ 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 },
|
||||
components: { ValueTypeMapIcon, CIReferenceAttr },
|
||||
model: {
|
||||
prop: 'value',
|
||||
event: 'change',
|
||||
@@ -255,7 +297,7 @@ export default {
|
||||
getExpListByProperty(property) {
|
||||
if (property) {
|
||||
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
|
||||
if (_find && ['0', '1', '3', '4', '5'].includes(_find.value_type)) {
|
||||
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') },
|
||||
@@ -315,6 +357,9 @@ export default {
|
||||
}
|
||||
return []
|
||||
},
|
||||
getAttr(property) {
|
||||
return this.canSearchPreferenceAttrList.find((item) => item.name === property) || {}
|
||||
},
|
||||
handleChangeExp({ value }, item, index) {
|
||||
const _ruleList = _.cloneDeep(this.ruleList)
|
||||
if (value === 'range') {
|
||||
@@ -343,4 +388,20 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
<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>
|
||||
|
@@ -15,18 +15,38 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
getPropertyIcon(attr) {
|
||||
switch (attr.value_type) {
|
||||
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':
|
||||
if (attr.is_password) {
|
||||
return 'duose-password'
|
||||
}
|
||||
if (attr.is_link) {
|
||||
return 'duose-link'
|
||||
}
|
||||
return 'duose-wenben'
|
||||
case '3':
|
||||
return 'duose-datetime'
|
||||
@@ -40,6 +60,14 @@ export default {
|
||||
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 ''
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@@ -177,7 +177,7 @@ export const linearIconList = [
|
||||
}]
|
||||
}, {
|
||||
value: 'icon-xianxing-application',
|
||||
label: '应用',
|
||||
label: '常用组件',
|
||||
list: [{
|
||||
value: 'icon-xianxing-yilianjie',
|
||||
label: '已连接'
|
||||
@@ -517,7 +517,7 @@ export const fillIconList = [
|
||||
}]
|
||||
}, {
|
||||
value: 'icon-shidi-application',
|
||||
label: '应用',
|
||||
label: '常用组件',
|
||||
list: [{
|
||||
value: 'icon-shidi-yilianjie',
|
||||
label: '已连接'
|
||||
@@ -729,6 +729,18 @@ export const multicolorIconList = [
|
||||
value: 'database',
|
||||
label: '数据库',
|
||||
list: [{
|
||||
value: 'caise-TIDB',
|
||||
label: 'TIDB'
|
||||
}, {
|
||||
value: 'caise-dameng',
|
||||
label: '达梦'
|
||||
}, {
|
||||
value: 'caise-kingbase',
|
||||
label: 'KingBase'
|
||||
}, {
|
||||
value: 'caise-TDSQL',
|
||||
label: 'TDSQL'
|
||||
}, {
|
||||
value: 'caise-DB2',
|
||||
label: 'DB2'
|
||||
}, {
|
||||
@@ -809,6 +821,9 @@ export const multicolorIconList = [
|
||||
value: 'system',
|
||||
label: '操作系统',
|
||||
list: [{
|
||||
value: 'ciase-aix',
|
||||
label: 'aix'
|
||||
}, {
|
||||
value: 'caise-Windows',
|
||||
label: 'Windows'
|
||||
}, {
|
||||
@@ -903,8 +918,113 @@ export const multicolorIconList = [
|
||||
}]
|
||||
}, {
|
||||
value: 'caise-application',
|
||||
label: '应用',
|
||||
label: '常用组件',
|
||||
list: [{
|
||||
value: 'caise-websphere',
|
||||
label: 'WebSphere'
|
||||
}, {
|
||||
value: 'caise-vps',
|
||||
label: 'VPS'
|
||||
}, {
|
||||
value: 'caise-F5',
|
||||
label: 'F5'
|
||||
}, {
|
||||
value: 'caise-HAProxy',
|
||||
label: 'HAProxy'
|
||||
}, {
|
||||
value: 'caise-kafka',
|
||||
label: 'kafka'
|
||||
}, {
|
||||
value: 'caise-dongfangtong',
|
||||
label: '东方通'
|
||||
}, {
|
||||
value: 'cmdb-vcenter',
|
||||
label: 'VCenter'
|
||||
}, {
|
||||
value: 'ops-KVM',
|
||||
label: 'KVM'
|
||||
}, {
|
||||
value: 'caise-JBoss',
|
||||
label: 'JBoss'
|
||||
}, {
|
||||
value: 'caise-weblogic',
|
||||
label: 'WebLogic'
|
||||
}, {
|
||||
value: 'caise-disk_array',
|
||||
label: '磁盘阵列'
|
||||
}, {
|
||||
value: 'caise-fiber',
|
||||
label: '光纤交换机'
|
||||
}, {
|
||||
value: 'caise-bandwidth_line',
|
||||
label: '带宽线路'
|
||||
}, {
|
||||
value: 'caise-pc',
|
||||
label: 'PC'
|
||||
}, {
|
||||
value: 'caise-rack',
|
||||
label: '机柜'
|
||||
}, {
|
||||
value: 'caise-computer_room',
|
||||
label: '机房'
|
||||
}, {
|
||||
value: 'caise-ip_address',
|
||||
label: 'ip地址'
|
||||
}, {
|
||||
value: 'caise_pool',
|
||||
label: 'ip池'
|
||||
}, {
|
||||
value: 'caise-storage_volume1',
|
||||
label: '存储卷'
|
||||
}, {
|
||||
value: 'caise-virtualization',
|
||||
label: '虚拟化'
|
||||
}, {
|
||||
value: 'caise-business',
|
||||
label: '业务'
|
||||
}, {
|
||||
value: 'caise-database',
|
||||
label: '数据库'
|
||||
}, {
|
||||
value: 'caise-middleware',
|
||||
label: '中间件'
|
||||
}, {
|
||||
value: 'caise-websever',
|
||||
label: 'websever'
|
||||
}, {
|
||||
value: 'caise-message_queue',
|
||||
label: '消息队列'
|
||||
}, {
|
||||
value: 'caise-load_balancing',
|
||||
label: '负载均衡'
|
||||
}, {
|
||||
value: 'caise-storage_device',
|
||||
label: '存储设备'
|
||||
}, {
|
||||
value: 'caise-network_devices',
|
||||
label: '网络设备'
|
||||
}, {
|
||||
value: 'caise-computer',
|
||||
label: '计算机'
|
||||
}, {
|
||||
value: 'caise-hardware',
|
||||
label: '硬件设备'
|
||||
}, {
|
||||
value: 'caise-data_center2',
|
||||
label: '数据中心'
|
||||
}, {
|
||||
value: 'caise-hyperV',
|
||||
label: 'hyperV'
|
||||
}, {
|
||||
value: 'caise-IPAM',
|
||||
label: 'IPAM'
|
||||
}, {
|
||||
value: 'caise-system',
|
||||
label: '操作系统'
|
||||
}, {
|
||||
value: 'caise-public_cloud',
|
||||
label: '公有云'
|
||||
}, {
|
||||
value: 'caise-data_center',
|
||||
label: '数据中心'
|
||||
}, {
|
||||
|
24
cmdb-ui/src/components/Menu/index.module.less
Normal file
24
cmdb-ui/src/components/Menu/index.module.less
Normal file
@@ -0,0 +1,24 @@
|
||||
.cmdb-side-menu-search {
|
||||
background-color: #FFFFFF !important;
|
||||
cursor: auto !important;
|
||||
|
||||
:global {
|
||||
.ant-input-affix-wrapper {
|
||||
max-width: 170px !important;
|
||||
width: 170px;
|
||||
border-radius: 30px;
|
||||
}
|
||||
|
||||
.ant-input {
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
background-color: #F7F8FA;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.ant-input-suffix {
|
||||
right: 0px !important;
|
||||
}
|
||||
}
|
||||
}
|
@@ -9,6 +9,8 @@ import {
|
||||
import { searchResourceType } from '@/modules/acl/api/resource'
|
||||
import { roleHasPermissionToGrant } from '@/modules/acl/api/permission'
|
||||
import CMDBGrant from '@/modules/cmdb/components/cmdbGrant'
|
||||
import styles from './index.module.less'
|
||||
import { mapActions } from 'vuex'
|
||||
|
||||
const { Item, SubMenu } = Menu
|
||||
|
||||
@@ -40,7 +42,8 @@ export default {
|
||||
openKeys: [],
|
||||
selectedKeys: [],
|
||||
cachedOpenKeys: [],
|
||||
resource_type: {}
|
||||
resource_type: {},
|
||||
currentAppRoute: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -64,6 +67,7 @@ export default {
|
||||
searchResourceType({ page_size: 9999, app_id: 'cmdb' }).then(res => {
|
||||
this.resource_type = { groups: res.groups, id2perms: res.id2perms }
|
||||
})
|
||||
this.currentAppRoute = this.$route?.matched?.[0]?.name || ''
|
||||
this.updateMenu()
|
||||
},
|
||||
watch: {
|
||||
@@ -75,12 +79,14 @@ export default {
|
||||
this.openKeys = this.cachedOpenKeys
|
||||
}
|
||||
},
|
||||
$route: function () {
|
||||
$route: function (route) {
|
||||
this.currentAppRoute = route?.matched?.[0]?.name
|
||||
this.updateMenu()
|
||||
},
|
||||
},
|
||||
inject: ['reload'],
|
||||
methods: {
|
||||
...mapActions(['UpdateCMDBSEarchValue']),
|
||||
cancelAttributes(e, menu) {
|
||||
const that = this
|
||||
e.preventDefault()
|
||||
@@ -257,7 +263,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
|
||||
@@ -286,6 +292,47 @@ export default {
|
||||
this.$message.error(this.$t('noPermission'))
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
jumpCMDBSearch(value) {
|
||||
this.UpdateCMDBSEarchValue(value)
|
||||
|
||||
if (this.$route.name !== 'cmdb_resource_search') {
|
||||
this.$router.push({
|
||||
name: 'cmdb_resource_search',
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
renderCMDBSearch() {
|
||||
if (this.currentAppRoute !== 'cmdb' || this.collapsed) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Item class={styles['cmdb-side-menu-search']}>
|
||||
<a-input
|
||||
ref="cmdbSideMenuSearchInputRef"
|
||||
class={styles['cmdb-side-menu-search-input']}
|
||||
style={{
|
||||
border: this.$route.name === 'cmdb_resource_search' ? 'solid 1px #B1C9FF' : ''
|
||||
}}
|
||||
placeholder={this.$t('cmdbSearch')}
|
||||
onPressEnter={(e) => {
|
||||
this.jumpCMDBSearch(e.target.value)
|
||||
}}
|
||||
>
|
||||
<ops-icon
|
||||
slot="suffix"
|
||||
type="veops-search1"
|
||||
onClick={() => {
|
||||
const value = this.$refs?.cmdbSideMenuSearchInputRef?.$refs?.input?.value || ''
|
||||
this.jumpCMDBSearch(value)
|
||||
}}
|
||||
/>
|
||||
</a-input>
|
||||
</Item>
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -313,6 +360,7 @@ export default {
|
||||
// {...{ props, on: on }}
|
||||
return (
|
||||
<Menu class="ops-side-bar" selectedKeys={this.selectedKeys} {...{ props, on: on }}>
|
||||
{this.renderCMDBSearch()}
|
||||
{menuTree}
|
||||
</Menu>
|
||||
)
|
||||
|
@@ -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 {
|
||||
|
@@ -66,6 +66,10 @@ export default {
|
||||
type: String,
|
||||
default: '#f7f8fa',
|
||||
},
|
||||
calcBasedParent: {
|
||||
type: Boolean,
|
||||
defualt: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -95,7 +99,7 @@ export default {
|
||||
},
|
||||
|
||||
paneLengthPercent() {
|
||||
const clientRectWidth = this.parentContainer
|
||||
const clientRectWidth = this.parentContainer && this.calcBasedParent
|
||||
? this.parentContainer.clientWidth
|
||||
: document.documentElement.getBoundingClientRect().width
|
||||
return (this.paneLengthPixel / clientRectWidth) * 100
|
||||
|
@@ -6,7 +6,8 @@
|
||||
:paneLengthPixel.sync="paneLengthPixel"
|
||||
:appName="appName"
|
||||
:triggerColor="triggerColor"
|
||||
:triggerLength="18"
|
||||
:triggerLength="triggerLength"
|
||||
:calcBasedParent="calcBasedParent"
|
||||
>
|
||||
<template #one>
|
||||
<div class="two-column-layout-sidebar">
|
||||
@@ -37,6 +38,14 @@ export default {
|
||||
type: String,
|
||||
default: '#f7f8fa',
|
||||
},
|
||||
triggerLength: {
|
||||
type: Number,
|
||||
default: 18
|
||||
},
|
||||
calcBasedParent: {
|
||||
type: Boolean,
|
||||
defualt: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
178
cmdb-ui/src/components/ciReferenceAttr/index.vue
Normal file
178
cmdb-ui/src/components/ciReferenceAttr/index.vue
Normal 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>
|
@@ -31,7 +31,7 @@ export default {
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
&:hover {
|
||||
background: linear-gradient(0deg, rgba(0, 80, 201, 0.2) 0%, rgba(174, 207, 255, 0.06) 86.76%);
|
||||
// background: linear-gradient(0deg, rgba(0, 80, 201, 0.2) 0%, rgba(174, 207, 255, 0.06) 86.76%);
|
||||
color: @layout-header-font-selected-color;
|
||||
}
|
||||
}
|
||||
|
@@ -5,7 +5,6 @@
|
||||
v-for="route in defaultShowRoutes"
|
||||
:key="route.name"
|
||||
@click="() => handleClick(route)"
|
||||
:title="$t(route.meta.title)"
|
||||
>
|
||||
{{ route.meta.title }}
|
||||
</span>
|
||||
@@ -119,7 +118,9 @@ export default {
|
||||
line-height: @layout-header-line-height;
|
||||
display: inline-block;
|
||||
}
|
||||
> span:hover,
|
||||
> span:hover {
|
||||
background-color: #f0f2f5;
|
||||
}
|
||||
.top-menu-selected {
|
||||
font-weight: bold;
|
||||
color: @layout-header-font-selected-color;
|
||||
|
@@ -5,31 +5,19 @@
|
||||
<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' }"
|
||||
placement="bottomRight"
|
||||
overlayClassName="custom-user"
|
||||
>
|
||||
<template slot="content">
|
||||
<router-link :to="{ name: 'setting_person' }" :style="{ color: '#000000a6' }">
|
||||
<div class="custom-user-item">
|
||||
<a-icon type="user" :style="{ marginRight: '10px' }" />
|
||||
<span>{{ $t('topMenu.personalCenter') }}</span>
|
||||
</div>
|
||||
</router-link>
|
||||
<div @click="handleLogout" class="custom-user-item">
|
||||
<a-icon type="logout" :style="{ marginRight: '10px' }" />
|
||||
<span>{{ $t('topMenu.logout') }}</span>
|
||||
</div>
|
||||
<UserPanel />
|
||||
</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"
|
||||
@@ -48,11 +36,27 @@
|
||||
import { mapState, mapActions, mapGetters, mapMutations } from 'vuex'
|
||||
import DocumentLink from './DocumentLink.vue'
|
||||
import { setDocumentTitle, domTitle } from '@/utils/domUtil'
|
||||
import UserPanel from './userPanel.vue'
|
||||
|
||||
export default {
|
||||
name: 'UserMenu',
|
||||
components: {
|
||||
DocumentLink,
|
||||
UserPanel
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
languageList: [
|
||||
{
|
||||
title: '简中',
|
||||
key: 'zh'
|
||||
},
|
||||
{
|
||||
title: 'EN',
|
||||
key: 'en'
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState(['user', 'locale']),
|
||||
@@ -81,14 +85,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 +117,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>
|
||||
|
487
cmdb-ui/src/components/tools/userPanel.vue
Normal file
487
cmdb-ui/src/components/tools/userPanel.vue
Normal file
@@ -0,0 +1,487 @@
|
||||
<template>
|
||||
<div class="user-panel">
|
||||
<a-avatar
|
||||
class="user-panel-avatar"
|
||||
size="small"
|
||||
icon="user"
|
||||
:src="avatarSrc"
|
||||
/>
|
||||
<div class="user-panel-nickname">
|
||||
{{ userInfo.nickname }}
|
||||
</div>
|
||||
<div class="user-panel-info">
|
||||
<ops-icon
|
||||
type="veops-company"
|
||||
class="user-panel-info-icon"
|
||||
/>
|
||||
<div class="user-panel-info-text">
|
||||
{{ companyName }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-panel-info">
|
||||
<ops-icon
|
||||
type="veops-emails"
|
||||
class="user-panel-info-icon"
|
||||
/>
|
||||
<div class="user-panel-info-text">
|
||||
{{ email }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="user-panel-btn">
|
||||
<div
|
||||
v-for="(item) in userBtnGroup"
|
||||
:key="item.type"
|
||||
class="user-panel-btn-item"
|
||||
@click="clickBtnGroup(item.type)"
|
||||
>
|
||||
<ops-icon
|
||||
:type="item.icon"
|
||||
class="user-panel-btn-icon"
|
||||
/>
|
||||
<span class="user-panel-btn-title">
|
||||
{{ $t(item.title) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="user-panel-row">
|
||||
<div class="user-panel-row-label">
|
||||
{{ $t('userPanel.switchLanguage') }}
|
||||
</div>
|
||||
|
||||
<div class="user-panel-lang">
|
||||
<div
|
||||
v-for="(lang, index) in languageList"
|
||||
:key="index"
|
||||
:class="['user-panel-lang-item', lang.key === locale ? 'user-panel-lang-item_active' : '']"
|
||||
@click="changeLang(lang.key)"
|
||||
>
|
||||
{{ lang.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="user-panel-row">
|
||||
<div class="user-panel-row-label">
|
||||
{{ $t('userPanel.bindAccount') }}
|
||||
</div>
|
||||
|
||||
<div class="user-panel-bind">
|
||||
<a-tooltip
|
||||
v-for="(item) in bindList"
|
||||
:key="item.type"
|
||||
:title="$t(item.title)"
|
||||
>
|
||||
<ops-icon
|
||||
class="user-panel-bind-item"
|
||||
:type="userInfo.notice_info && userInfo.notice_info[item.type] ? item.existedIcon : item.icon"
|
||||
@click="handleBindInfo(item.type)"
|
||||
/>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="user-panel-account">
|
||||
<div
|
||||
v-for="(item, index) in accountActions"
|
||||
:key="index"
|
||||
class="user-panel-account-item"
|
||||
@click="handleLogout"
|
||||
>
|
||||
<ops-icon class="user-panel-account-icon" :type="item.icon" />
|
||||
<span class="user-panel-account-title">
|
||||
{{ $t(item.title) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapState, mapMutations } from 'vuex'
|
||||
import { setDocumentTitle, domTitle } from '@/utils/domUtil'
|
||||
import {
|
||||
bindPlatformByUid,
|
||||
unbindPlatformByUid,
|
||||
} from '@/api/employee'
|
||||
import { getCompanyInfo } from '@/api/company'
|
||||
|
||||
export default {
|
||||
name: 'UserPanel',
|
||||
data() {
|
||||
return {
|
||||
userBtnGroup: [
|
||||
{
|
||||
icon: 'veops-personal',
|
||||
title: 'userPanel.myProfile',
|
||||
type: 'myProfile'
|
||||
},
|
||||
{
|
||||
icon: 'a-veops-account1',
|
||||
title: 'userPanel.accountPassword',
|
||||
type: 'accountPassword'
|
||||
}
|
||||
],
|
||||
languageList: [
|
||||
{
|
||||
title: '简中',
|
||||
key: 'zh'
|
||||
},
|
||||
{
|
||||
title: 'EN',
|
||||
key: 'en'
|
||||
},
|
||||
],
|
||||
bindList: [
|
||||
{
|
||||
type: 'wechatApp',
|
||||
icon: 'qiyeweixin',
|
||||
existedIcon: 'wechatApp',
|
||||
title: 'wechat'
|
||||
},
|
||||
{
|
||||
type: 'feishuApp',
|
||||
icon: 'ops-setting-notice-feishu-selected',
|
||||
existedIcon: 'feishuApp',
|
||||
title: 'feishu'
|
||||
},
|
||||
{
|
||||
type: 'dingdingApp',
|
||||
icon: 'ops-setting-notice-dingding-selected',
|
||||
existedIcon: 'dingdingApp',
|
||||
title: 'dingding'
|
||||
},
|
||||
],
|
||||
accountActions: [
|
||||
{
|
||||
icon: 'veops-switch',
|
||||
title: 'userPanel.switchAccount'
|
||||
},
|
||||
{
|
||||
icon: 'veops-sign_out',
|
||||
title: 'userPanel.logout'
|
||||
},
|
||||
],
|
||||
hoverBindAccountList: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
email: (state) => state.user.email,
|
||||
locale: (state) => state.locale,
|
||||
userInfo: (state) => state.user,
|
||||
companyName: (state) => state.company.name
|
||||
}),
|
||||
avatarSrc() {
|
||||
const avatar = this.userInfo.avatar
|
||||
if (!avatar) {
|
||||
return null
|
||||
}
|
||||
|
||||
return avatar.startsWith('https') ? avatar : `/api/common-setting/v1/file/${avatar}`
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.companyName === undefined) {
|
||||
this.getCompanyInfo()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['Logout', 'GetInfo']),
|
||||
...mapMutations(['SET_LOCALE', 'SET_COMPANY_NAME']),
|
||||
async getCompanyInfo() {
|
||||
const res = await getCompanyInfo()
|
||||
const name = res?.info?.name || ''
|
||||
this.SET_COMPANY_NAME(name)
|
||||
},
|
||||
|
||||
changeLang(lang) {
|
||||
this.SET_LOCALE(lang)
|
||||
this.$i18n.locale = lang
|
||||
this.$nextTick(() => {
|
||||
setDocumentTitle(`${this.$t(this.$route.meta.title)} - ${domTitle}`)
|
||||
})
|
||||
},
|
||||
handleBindInfo(platform) {
|
||||
const isBind = this?.userInfo?.notice_info?.[platform]
|
||||
const uid = this?.userInfo?.uid
|
||||
|
||||
if (isBind) {
|
||||
this.$confirm({
|
||||
title: this.$t('warning'),
|
||||
content: this.$t('cs.person.confirmUnbind'),
|
||||
onOk: () => {
|
||||
unbindPlatformByUid(platform, uid)
|
||||
.then(() => {
|
||||
this.$message.success(this.$t('cs.person.unbindSuccess'))
|
||||
})
|
||||
.finally(() => {
|
||||
this.GetInfo()
|
||||
})
|
||||
},
|
||||
})
|
||||
} else {
|
||||
bindPlatformByUid(platform, uid)
|
||||
.then(() => {
|
||||
this.$message.success(this.$t('cs.person.bindSuccess'))
|
||||
})
|
||||
.finally(() => {
|
||||
this.GetInfo()
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
handleLogout() {
|
||||
this.$confirm({
|
||||
title: this.$t('tip'),
|
||||
content: this.$t('topMenu.confirmLogout'),
|
||||
onOk: () => {
|
||||
this.Logout()
|
||||
},
|
||||
onCancel() {},
|
||||
})
|
||||
},
|
||||
|
||||
clickBtnGroup(type) {
|
||||
switch (type) {
|
||||
case 'myProfile':
|
||||
if (this.$route.name === 'setting_person') {
|
||||
this.$bus.$emit('changeSettingPersonCurrent', '1')
|
||||
} else {
|
||||
this.$router.push({
|
||||
name: 'setting_person',
|
||||
query: {
|
||||
current: '1'
|
||||
}
|
||||
})
|
||||
}
|
||||
break
|
||||
case 'accountPassword':
|
||||
if (this.$route.name === 'setting_person') {
|
||||
this.$bus.$emit('changeSettingPersonCurrent', '2')
|
||||
} else {
|
||||
this.$router.push({
|
||||
name: 'setting_person',
|
||||
query: {
|
||||
current: '2'
|
||||
}
|
||||
})
|
||||
}
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
|
||||
handleBindAccountMouse(type, isHover) {
|
||||
const index = this.hoverBindAccountList.findIndex((item) => item === type)
|
||||
if (isHover && index === -1) {
|
||||
this.hoverBindAccountList.push(type)
|
||||
} else if (!isHover && index !== -1) {
|
||||
this.hoverBindAccountList.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.user-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 350px;
|
||||
padding: 0 20px;
|
||||
|
||||
&-avatar {
|
||||
width: 62px;
|
||||
height: 62px;
|
||||
border-radius: 62px;
|
||||
margin-top: 13px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #000000;
|
||||
background-color: #FFFFFF;
|
||||
font-size: 48px !important;
|
||||
}
|
||||
|
||||
&-nickname {
|
||||
color: #1D2129;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
&-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 6px;
|
||||
margin-top: 6px;
|
||||
max-width: 100%;
|
||||
|
||||
&-icon {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&-text {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #4E5969;
|
||||
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
&-btn {
|
||||
width: 100%;
|
||||
height: 72px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 11px;
|
||||
|
||||
&-icon {
|
||||
font-size: 22px;
|
||||
color: #CACDD9;
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #1D2129;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
&-item {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #F7F8FA;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #EBEFF8;
|
||||
|
||||
.user-panel-btn-icon {
|
||||
color: #2F54EB;
|
||||
}
|
||||
|
||||
.user-panel-btn-title {
|
||||
color: #2F54EB;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-row {
|
||||
width: 100%;
|
||||
margin-top: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
&-label {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #4E5969;
|
||||
}
|
||||
}
|
||||
|
||||
&-lang {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 28px;
|
||||
width: 108px;
|
||||
border-radius: 28px;
|
||||
overflow: hidden;
|
||||
|
||||
&-item {
|
||||
flex: 1;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #F7F8FA;
|
||||
cursor: pointer;
|
||||
|
||||
&:first-child {
|
||||
border-right: solid 1px #E4E7ED;
|
||||
}
|
||||
|
||||
&_active {
|
||||
background-color: #EBEFF8;
|
||||
color: #2F54EB;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #2F54EB;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-bind {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 22px;
|
||||
|
||||
&-item {
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&-account {
|
||||
margin-top: 22px;
|
||||
padding-top: 13px;
|
||||
padding-bottom: 20px;
|
||||
border-top: solid 1px #F0F1F5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
width: 100%;
|
||||
|
||||
&-icon {
|
||||
font-size: 14px;
|
||||
color: #CACDD9;
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-size: 14px;
|
||||
color: #86909C;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
.user-panel-account-icon {
|
||||
color: #2F54EB;
|
||||
}
|
||||
|
||||
.user-panel-account-title {
|
||||
color: #2F54EB;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -69,6 +69,8 @@ Vue.prototype.$httpError = function (err, describe) {
|
||||
|
||||
window.$message = Vue.prototype.$message
|
||||
|
||||
Vue.prototype.isOpenSource = process.env.VUE_APP_IS_OPEN_SOURCE === 'true'
|
||||
|
||||
Vue.use(Antd)
|
||||
Vue.use(Viser)
|
||||
|
||||
|
@@ -10,6 +10,7 @@ export default {
|
||||
resourceType: 'Resource Types',
|
||||
trigger: 'Triggers',
|
||||
},
|
||||
settings: 'Common Settings',
|
||||
screen: 'Big Screen',
|
||||
dashboard: 'Dashboard',
|
||||
admin: 'Admin',
|
||||
@@ -107,6 +108,7 @@ export default {
|
||||
visual: 'Visual',
|
||||
default: 'default',
|
||||
tip: 'Tip',
|
||||
cmdbSearch: 'Search',
|
||||
pagination: {
|
||||
total: '{range0}-{range1} of {total} items'
|
||||
},
|
||||
@@ -166,6 +168,15 @@ export default {
|
||||
monetaryAmount: 'monetary amount',
|
||||
custom: 'custom',
|
||||
},
|
||||
userPanel: {
|
||||
myProfile: 'My Profile',
|
||||
accountPassword: 'Password',
|
||||
notice: 'Notice',
|
||||
switchLanguage: 'Switch Language',
|
||||
bindAccount: 'Bind Account',
|
||||
switchAccount: 'Switch Account',
|
||||
logout: 'Logout'
|
||||
},
|
||||
cmdb: cmdb_en,
|
||||
cs: cs_en,
|
||||
acl: acl_en,
|
||||
|
@@ -10,6 +10,7 @@ export default {
|
||||
resourceType: '资源类型',
|
||||
trigger: '触发器',
|
||||
},
|
||||
settings: '通用设置',
|
||||
screen: '大屏',
|
||||
dashboard: '仪表盘',
|
||||
admin: '管理员',
|
||||
@@ -107,6 +108,7 @@ export default {
|
||||
visual: '虚拟',
|
||||
default: '默认',
|
||||
tip: '提示',
|
||||
cmdbSearch: '搜索一下',
|
||||
pagination: {
|
||||
total: '当前展示 {range0}-{range1} 条数据, 共 {total} 条'
|
||||
},
|
||||
@@ -166,6 +168,15 @@ export default {
|
||||
monetaryAmount: '货币金额',
|
||||
custom: '自定义',
|
||||
},
|
||||
userPanel: {
|
||||
myProfile: '个人中心',
|
||||
accountPassword: '账号密码',
|
||||
notice: '通知中心',
|
||||
switchLanguage: '切换语言',
|
||||
bindAccount: '绑定账号',
|
||||
switchAccount: '切换账号',
|
||||
logout: '退出账号'
|
||||
},
|
||||
cmdb: cmdb_zh,
|
||||
cs: cs_zh,
|
||||
acl: acl_zh,
|
||||
|
@@ -81,3 +81,11 @@ export function searchCIRelationFull(params) {
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
export function searchCIRelationPath(data) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_relations/path/s`,
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
@@ -122,6 +122,31 @@ export function deleteCITypeGroupById(groupId, data) {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取级联属性配置
|
||||
* @param {*} typeId
|
||||
* @returns
|
||||
*/
|
||||
export function getCITypeCascadeAttributes(typeId) {
|
||||
return axios({
|
||||
url: `/v0.1/cascade_attributes/ci_types/${typeId}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取级联属性数据
|
||||
* @param {*} typeId
|
||||
* @returns
|
||||
*/
|
||||
export function postCITypeCascadeAttributesValues(attrId, data) {
|
||||
return axios({
|
||||
url: `/v0.1/cascade_attributes/${attrId}/values`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function getUniqueConstraintList(type_id) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_types/${type_id}/unique_constraint`,
|
||||
|
@@ -74,3 +74,11 @@ export function getCanEditByParentIdChildId(parent_id, child_id) {
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
|
||||
export function getCITypeRelationPath(params) {
|
||||
return axios({
|
||||
url: `/v0.1/ci_type_relations/path`,
|
||||
method: 'GET',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
109
cmdb-ui/src/modules/cmdb/api/ipam.js
Normal file
109
cmdb-ui/src/modules/cmdb/api/ipam.js
Normal file
@@ -0,0 +1,109 @@
|
||||
import { axios } from '@/utils/request'
|
||||
|
||||
export function getIPAMSubnet() {
|
||||
return axios({
|
||||
url: '/v0.1/ipam/subnet',
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
|
||||
export function postIPAMSubnet(data) {
|
||||
return axios({
|
||||
url: '/v0.1/ipam/subnet',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function getIPAMSubnetById(id) {
|
||||
return axios({
|
||||
url: `/v0.1/ipam/subnet/${id}`,
|
||||
method: 'GET'
|
||||
})
|
||||
}
|
||||
|
||||
export function putIPAMSubnet(id, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ipam/subnet/${id}`,
|
||||
method: 'PUT',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteIPAMSubnet(id) {
|
||||
return axios({
|
||||
url: `/v0.1/ipam/subnet/${id}`,
|
||||
method: 'DELETE'
|
||||
})
|
||||
}
|
||||
|
||||
export function postIPAMScope(data) {
|
||||
return axios({
|
||||
url: '/v0.1/ipam/scope',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function putIPAMScope(id, data) {
|
||||
return axios({
|
||||
url: `/v0.1/ipam/scope/${id}`,
|
||||
method: 'PUT',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteIPAMScope(id) {
|
||||
return axios({
|
||||
url: `/v0.1/ipam/scope/${id}`,
|
||||
method: 'DELETE'
|
||||
})
|
||||
}
|
||||
|
||||
export function getIPAMAddress(params) {
|
||||
return axios({
|
||||
url: '/v0.1/ipam/address',
|
||||
method: 'GET',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getIPAMHosts(params) {
|
||||
return axios({
|
||||
url: '/v0.1/ipam/subnet/hosts',
|
||||
method: 'GET',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function postIPAMAddress(data) {
|
||||
return axios({
|
||||
url: '/v0.1/ipam/address',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function getIPAMHistoryOperate(params) {
|
||||
return axios({
|
||||
url: '/v0.1/ipam/history/operate',
|
||||
method: 'GET',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getIPAMHistoryScan(params) {
|
||||
return axios({
|
||||
url: '/v0.1/ipam/history/scan',
|
||||
method: 'GET',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getIPAMStats(params) {
|
||||
return axios({
|
||||
url: '/v0.1/ipam/stats',
|
||||
method: 'GET',
|
||||
params
|
||||
})
|
||||
}
|
@@ -1,4 +1,6 @@
|
||||
import { axios } from '@/utils/request'
|
||||
import { CI_DEFAULT_ATTR } from '@/modules/cmdb/utils/const.js'
|
||||
import i18n from '@/lang'
|
||||
|
||||
export function getPreference(instance = true, tree = null) {
|
||||
return axios({
|
||||
@@ -16,11 +18,35 @@ export function getPreference2(instance = true, tree = null) {
|
||||
})
|
||||
}
|
||||
|
||||
export function getSubscribeAttributes(ciTypeId) {
|
||||
return axios({
|
||||
export function getSubscribeAttributes(ciTypeId, formatDefaultAttr = true) {
|
||||
return new Promise(async (resolve) => {
|
||||
const res = await axios({
|
||||
url: `/v0.1/preference/ci_types/${ciTypeId}/attributes`,
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
if (
|
||||
formatDefaultAttr &&
|
||||
res?.attributes?.length
|
||||
) {
|
||||
res.attributes.forEach((item) => {
|
||||
switch (item.name) {
|
||||
case CI_DEFAULT_ATTR.UPDATE_USER:
|
||||
item.id = item.name
|
||||
item.alias = i18n.t('cmdb.components.updater')
|
||||
break
|
||||
case CI_DEFAULT_ATTR.UPDATE_TIME:
|
||||
item.id = item.name
|
||||
item.alias = i18n.t('cmdb.components.updateTime')
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
resolve(res)
|
||||
})
|
||||
}
|
||||
|
||||
export function getSubscribeTreeView() {
|
||||
|
BIN
cmdb-ui/src/modules/cmdb/assets/ipam_address_null.png
Normal file
BIN
cmdb-ui/src/modules/cmdb/assets/ipam_address_null.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
BIN
cmdb-ui/src/modules/cmdb/assets/no_permission.png
Normal file
BIN
cmdb-ui/src/modules/cmdb/assets/no_permission.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.8 KiB |
Binary file not shown.
After Width: | Height: | Size: 177 KiB |
Binary file not shown.
After Width: | Height: | Size: 444 KiB |
@@ -22,7 +22,7 @@
|
||||
<draggable :value="targetKeys" animation="300" @end="dragEnd" :disabled="!isSortable">
|
||||
<div
|
||||
@dblclick="changeSingleItem(item)"
|
||||
v-for="item in filteredItems"
|
||||
v-for="item in filterDefaultAttr(filteredItems)"
|
||||
:key="item.key"
|
||||
:style="{ height: '38px' }"
|
||||
>
|
||||
@@ -54,11 +54,44 @@
|
||||
</li>
|
||||
</div>
|
||||
</draggable>
|
||||
|
||||
<div
|
||||
v-if="rightDefaultAttrList.length"
|
||||
class="default-attr"
|
||||
>
|
||||
<a-divider>
|
||||
<span class="default-attr-divider">
|
||||
{{ $t('cmdb.components.default') }}
|
||||
</span>
|
||||
</a-divider>
|
||||
|
||||
<div
|
||||
v-for="(item) in rightDefaultAttrList"
|
||||
:key="item.key"
|
||||
:class="['default-attr-item', selectedKeys.includes(item.key) ? 'default-attr-item-selected' : '']"
|
||||
@click="setSelectedKeys(item)"
|
||||
@dblclick="changeSingleItem(item)"
|
||||
>
|
||||
<div
|
||||
class="default-attr-arrow"
|
||||
style="left: 17px"
|
||||
@click.stop="changeSingleItem(item)"
|
||||
>
|
||||
<a-icon type="left" />
|
||||
</div>
|
||||
<div class="default-attr-title">
|
||||
{{ $t(item.title) }}
|
||||
</div>
|
||||
<div class="default-attr-name">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="direction === 'left'" class="ant-transfer-list-content">
|
||||
<div
|
||||
@dblclick="changeSingleItem(item)"
|
||||
v-for="item in filteredItems"
|
||||
v-for="item in filterDefaultAttr(filteredItems)"
|
||||
:key="item.key"
|
||||
:style="{ height: '38px' }"
|
||||
>
|
||||
@@ -82,6 +115,39 @@
|
||||
</div>
|
||||
</li>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="leftDefaultAttrList.length"
|
||||
class="default-attr"
|
||||
>
|
||||
<a-divider>
|
||||
<span class="default-attr-divider">
|
||||
{{ $t('cmdb.components.default') }}
|
||||
</span>
|
||||
</a-divider>
|
||||
|
||||
<div
|
||||
v-for="(item) in leftDefaultAttrList"
|
||||
:key="item.key"
|
||||
:class="['default-attr-item', selectedKeys.includes(item.key) ? 'default-attr-item-selected' : '']"
|
||||
@click="setSelectedKeys(item)"
|
||||
@dblclick="changeSingleItem(item)"
|
||||
>
|
||||
<div
|
||||
class="default-attr-arrow"
|
||||
style="left: 2px"
|
||||
@click.stop="changeSingleItem(item)"
|
||||
>
|
||||
<a-icon type="right" />
|
||||
</div>
|
||||
<div class="default-attr-title">
|
||||
{{ $t(item.title) }}
|
||||
</div>
|
||||
<div class="default-attr-name">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-transfer>
|
||||
@@ -95,6 +161,7 @@
|
||||
import _ from 'lodash'
|
||||
import draggable from 'vuedraggable'
|
||||
import { ops_move_icon as OpsMoveIcon } from '@/core/icons'
|
||||
import { CI_DEFAULT_ATTR } from '@/modules/cmdb/utils/const.js'
|
||||
|
||||
export default {
|
||||
name: 'AttributesTransfer',
|
||||
@@ -130,10 +197,41 @@ export default {
|
||||
type: Number,
|
||||
default: 400,
|
||||
},
|
||||
showDefaultAttr: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedKeys: [],
|
||||
defaultAttrList: [
|
||||
{
|
||||
title: 'cmdb.components.updater',
|
||||
name: 'updater',
|
||||
key: CI_DEFAULT_ATTR.UPDATE_USER
|
||||
},
|
||||
{
|
||||
title: 'cmdb.components.updateTime',
|
||||
name: 'update time',
|
||||
key: CI_DEFAULT_ATTR.UPDATE_TIME
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
rightDefaultAttrList() {
|
||||
if (!this.showDefaultAttr) {
|
||||
return []
|
||||
}
|
||||
return this.defaultAttrList.filter((item) => this.targetKeys.includes(item.key))
|
||||
},
|
||||
|
||||
leftDefaultAttrList() {
|
||||
if (!this.showDefaultAttr) {
|
||||
return []
|
||||
}
|
||||
return this.defaultAttrList.filter((item) => !this.targetKeys.includes(item.key))
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -216,6 +314,10 @@ export default {
|
||||
}
|
||||
this.$emit('setFixedList', _fixedList)
|
||||
},
|
||||
|
||||
filterDefaultAttr(list) {
|
||||
return this.showDefaultAttr ? list.filter((item) => ![CI_DEFAULT_ATTR.UPDATE_USER, CI_DEFAULT_ATTR.UPDATE_TIME].includes(item.key)) : list
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -296,5 +398,67 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.default-attr {
|
||||
.ant-divider {
|
||||
margin: 7px 0;
|
||||
padding: 0 15px;
|
||||
|
||||
.ant-divider-inner-text {
|
||||
padding: 0 6px;
|
||||
}
|
||||
}
|
||||
|
||||
&-divider {
|
||||
font-size: 12px;
|
||||
color: #86909C;
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-size: 14px;
|
||||
line-height: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&-name {
|
||||
font-size: 11px;
|
||||
line-height: 12px;
|
||||
color: rgb(163, 163, 163);
|
||||
}
|
||||
|
||||
&-arrow {
|
||||
position: absolute;
|
||||
top: 9px;
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
background-color: #fff;
|
||||
color: @primary-color;
|
||||
border-radius: 4px;
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
&-item {
|
||||
padding-left: 34px;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 4px;
|
||||
position: relative;
|
||||
border-left: solid 2px transparent;
|
||||
margin-bottom: 6px;
|
||||
|
||||
&-selected {
|
||||
background-color: #f0f5ff;
|
||||
border-color: #2f54eb;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #f0f5ff;
|
||||
|
||||
.default-attr-arrow {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -13,7 +13,10 @@
|
||||
v-decorator="['filename', { rules: [{ required: true, message: $t('cmdb.components.filenameInputTips') }] }]"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :label="$t('cmdb.components.saveType')">
|
||||
<a-form-item
|
||||
v-if="showFileTypeSelect"
|
||||
:label="$t('cmdb.components.saveType')"
|
||||
>
|
||||
<a-select
|
||||
:placeholder="$t('cmdb.components.saveTypeTips')"
|
||||
v-decorator="[
|
||||
@@ -83,6 +86,10 @@ export default {
|
||||
type: String,
|
||||
default: 'default',
|
||||
},
|
||||
showFileTypeSelect: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
85
cmdb-ui/src/modules/cmdb/components/ciIcon/index.vue
Normal file
85
cmdb-ui/src/modules/cmdb/components/ciIcon/index.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="icon || title"
|
||||
class="ci-icon"
|
||||
:style="{
|
||||
'--size': size + 'px'
|
||||
}"
|
||||
>
|
||||
<template v-if="icon">
|
||||
<img
|
||||
v-if="icon.split('$$')[2]"
|
||||
:src="`/api/common-setting/v1/file/${icon.split('$$')[3]}`"
|
||||
/>
|
||||
<ops-icon
|
||||
v-else
|
||||
:style="{
|
||||
color: icon.split('$$')[1],
|
||||
}"
|
||||
:type="icon.split('$$')[0]"
|
||||
/>
|
||||
</template>
|
||||
<span
|
||||
class="ci-icon-letter"
|
||||
v-else
|
||||
>
|
||||
<span>
|
||||
{{ title[0].toUpperCase() }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'CIIcon',
|
||||
props: {
|
||||
icon: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
// 如果没有icon, 默认以title 的第一个字符
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
size: {
|
||||
type: [String, Number],
|
||||
default: '12'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.ci-icon {
|
||||
font-size: var(--size);
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
& > img {
|
||||
width: var(--size);
|
||||
height: var(--size);
|
||||
}
|
||||
|
||||
&-letter {
|
||||
background-color: #FFFFFF;
|
||||
color: #2f54eb;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0px 1px 2px rgba(47, 84, 235, 0.2);
|
||||
|
||||
& > span {
|
||||
transform-origin: center;
|
||||
transform: scale(0.7);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
578
cmdb-ui/src/modules/cmdb/components/ciTable/index.vue
Normal file
578
cmdb-ui/src/modules/cmdb/components/ciTable/index.vue
Normal 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>
|
@@ -53,6 +53,7 @@
|
||||
@setExpFromFilter="setExpFromFilter"
|
||||
:expression="expression"
|
||||
/>
|
||||
<div class="read-ci-tip">{{ $t('cmdb.ciType.ciGrantTip') }}</div>
|
||||
</a-form-model>
|
||||
</template>
|
||||
</CustomRadio>
|
||||
@@ -209,4 +210,10 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
<style lang="less" scoped>
|
||||
.read-ci-tip {
|
||||
font-size: 12px;
|
||||
line-height: 22px;
|
||||
color: #a5a9bc;
|
||||
}
|
||||
</style>
|
||||
|
@@ -0,0 +1,41 @@
|
||||
import i18n from '@/lang'
|
||||
|
||||
export const ruleTypeList = () => {
|
||||
return [
|
||||
{ value: 'and', label: i18n.t('cmdbFilterComp.and') },
|
||||
{ value: 'or', label: i18n.t('cmdbFilterComp.or') },
|
||||
// { value: 'not', label: '非' },
|
||||
]
|
||||
}
|
||||
|
||||
export const expList = () => {
|
||||
return [
|
||||
{ value: 'is', label: i18n.t('cmdbFilterComp.is') },
|
||||
{ value: '~is', label: i18n.t('cmdbFilterComp.~is') },
|
||||
{ value: 'contain', label: i18n.t('cmdbFilterComp.contain') },
|
||||
{ value: '~contain', label: i18n.t('cmdbFilterComp.~contain') },
|
||||
{ value: 'start_with', label: i18n.t('cmdbFilterComp.start_with') },
|
||||
{ value: '~start_with', label: i18n.t('cmdbFilterComp.~start_with') },
|
||||
{ value: 'end_with', label: i18n.t('cmdbFilterComp.end_with') },
|
||||
{ value: '~end_with', label: i18n.t('cmdbFilterComp.~end_with') },
|
||||
{ value: '~value', label: i18n.t('cmdbFilterComp.~value') }, // 为空的定义有点绕
|
||||
{ value: 'value', label: i18n.t('cmdbFilterComp.value') },
|
||||
]
|
||||
}
|
||||
|
||||
export const advancedExpList = () => {
|
||||
return [
|
||||
{ value: 'in', label: i18n.t('cmdbFilterComp.in') },
|
||||
{ value: '~in', label: i18n.t('cmdbFilterComp.~in') },
|
||||
{ value: 'range', label: i18n.t('cmdbFilterComp.range') },
|
||||
{ value: '~range', label: i18n.t('cmdbFilterComp.~range') },
|
||||
{ value: 'compare', label: i18n.t('cmdbFilterComp.compare') },
|
||||
]
|
||||
}
|
||||
|
||||
export const compareTypeList = [
|
||||
{ value: '1', label: '>' },
|
||||
{ value: '2', label: '>=' },
|
||||
{ value: '3', label: '<' },
|
||||
{ value: '4', label: '<=' },
|
||||
]
|
@@ -0,0 +1,320 @@
|
||||
<template>
|
||||
<div :style="{ lineHeight: rowHeight }">
|
||||
<a-space :style="{ display: 'flex', marginBottom: '10px' }" v-for="(item, index) in ruleList" :key="item.id">
|
||||
<div v-if="ruleList.length > 1" :style="{ width: '60px', height: rowHeight, position: 'relative' }">
|
||||
<treeselect
|
||||
v-if="index !== 0"
|
||||
class="custom-treeselect"
|
||||
:style="{ width: '60px', '--custom-height': rowHeight, position: 'absolute', top: '-24px' }"
|
||||
v-model="item.type"
|
||||
:multiple="false"
|
||||
:clearable="false"
|
||||
searchable
|
||||
:options="ruleTypeList"
|
||||
:normalizer="
|
||||
(node) => {
|
||||
return {
|
||||
id: node.value,
|
||||
label: node.label,
|
||||
children: node.children,
|
||||
}
|
||||
}
|
||||
"
|
||||
:disabled="disabled"
|
||||
>
|
||||
</treeselect>
|
||||
</div>
|
||||
<treeselect
|
||||
class="custom-treeselect"
|
||||
:style="{ width: '120px', '--custom-height': rowHeight }"
|
||||
v-model="item.property"
|
||||
:multiple="false"
|
||||
:clearable="false"
|
||||
searchable
|
||||
:options="canSearchPreferenceAttrList"
|
||||
:normalizer="
|
||||
(node) => {
|
||||
return {
|
||||
id: node.name,
|
||||
label: node.alias || node.name,
|
||||
children: node.children,
|
||||
}
|
||||
}
|
||||
"
|
||||
appendToBody
|
||||
:zIndex="1050"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<div
|
||||
v-if="node.id !== '$count'"
|
||||
:title="node.label"
|
||||
slot="option-label"
|
||||
slot-scope="{ node }"
|
||||
class="property-label"
|
||||
>
|
||||
<ValueTypeMapIcon :attr="node.raw" />
|
||||
{{ node.label }}
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
:title="node.label"
|
||||
slot="option-label"
|
||||
slot-scope="{ node }"
|
||||
class="property-label"
|
||||
:style="{ borderBottom: '1px solid #E4E7ED', marginBottom: '8px' }"
|
||||
>
|
||||
<ValueTypeMapIcon :attr="node.raw" />
|
||||
{{ node.label }}
|
||||
</div>
|
||||
<div
|
||||
class="property-label"
|
||||
slot="value-label"
|
||||
slot-scope="{ node }"
|
||||
>
|
||||
<ValueTypeMapIcon :attr="node.raw" /> {{ node.label }}
|
||||
</div>
|
||||
</treeselect>
|
||||
<treeselect
|
||||
class="custom-treeselect"
|
||||
:style="{ width: '90px', '--custom-height': rowHeight }"
|
||||
v-model="item.exp"
|
||||
:multiple="false"
|
||||
:clearable="false"
|
||||
searchable
|
||||
:options="getExpListByProperty(item.property)"
|
||||
:normalizer="
|
||||
(node) => {
|
||||
return {
|
||||
id: node.value,
|
||||
label: node.label,
|
||||
children: node.children,
|
||||
}
|
||||
}
|
||||
"
|
||||
appendToBody
|
||||
@select="(value) => handleChangeExp(value, item, index)"
|
||||
:zIndex="1050"
|
||||
:disabled="disabled"
|
||||
>
|
||||
</treeselect>
|
||||
<ValueControls
|
||||
:rule="ruleList[index]"
|
||||
:attrList="canSearchPreferenceAttrList"
|
||||
:disabled="disabled"
|
||||
:curModelAttrList="curModelAttrList"
|
||||
:rowHeight="rowHeight"
|
||||
@change="(value) => handleChangeValue(value, index)"
|
||||
/>
|
||||
<template v-if="!disabled">
|
||||
<a-tooltip :title="$t('copy')">
|
||||
<a class="operation" @click="handleCopyRule(item)"><ops-icon type="veops-copy"/></a>
|
||||
</a-tooltip>
|
||||
<a-tooltip :title="$t('delete')">
|
||||
<a class="operation" @click="handleDeleteRule(item)"><a-icon type="minus-circle"/></a>
|
||||
</a-tooltip>
|
||||
<a-tooltip :title="$t('cmdbFilterComp.addHere')">
|
||||
<a class="operation" @click="handleAddRuleAt(item)"><a-icon type="plus-circle"/></a>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</a-space>
|
||||
<div class="table-filter-add" v-if="!disabled && ruleList.length === 0">
|
||||
<a @click="handleAddRule">+ {{ $t('new') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from 'lodash'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { ruleTypeList, expList, advancedExpList, compareTypeList } from './constants.js'
|
||||
import ValueTypeMapIcon from '@/components/CMDBValueTypeMapIcon'
|
||||
import ValueControls from './valueControls.vue'
|
||||
|
||||
export default {
|
||||
name: 'Expression',
|
||||
components: {
|
||||
ValueTypeMapIcon,
|
||||
ValueControls
|
||||
},
|
||||
model: {
|
||||
prop: 'value',
|
||||
event: 'change',
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
canSearchPreferenceAttrList: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => [],
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
curModelAttrList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
compareTypeList,
|
||||
rowHeight: '36px',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
ruleList: {
|
||||
get() {
|
||||
return this.value
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('change', val)
|
||||
return val
|
||||
},
|
||||
},
|
||||
ruleTypeList() {
|
||||
return ruleTypeList()
|
||||
},
|
||||
expList() {
|
||||
return expList()
|
||||
},
|
||||
advancedExpList() {
|
||||
return advancedExpList()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getExpListByProperty(property) {
|
||||
if (property === '$count') {
|
||||
return [
|
||||
{ value: 'is', label: this.$t('cmdbFilterComp.is') },
|
||||
{ value: '~is', label: this.$t('cmdbFilterComp.~is') },
|
||||
{ value: 'compare', label: this.$t('cmdbFilterComp.compare') }
|
||||
]
|
||||
}
|
||||
if (property) {
|
||||
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
|
||||
if (_find && ['0', '1', '3', '4', '5'].includes(_find.value_type)) {
|
||||
return [
|
||||
{ value: 'is', label: this.$t('cmdbFilterComp.is') },
|
||||
{ value: '~is', label: this.$t('cmdbFilterComp.~is') },
|
||||
{ value: '~value', label: this.$t('cmdbFilterComp.~value') }, // 为空的定义有点绕
|
||||
{ value: 'value', label: this.$t('cmdbFilterComp.value') },
|
||||
...this.advancedExpList
|
||||
]
|
||||
}
|
||||
}
|
||||
return [...this.expList, ...this.advancedExpList]
|
||||
},
|
||||
isChoiceByProperty(property) {
|
||||
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
|
||||
if (_find) {
|
||||
return _find.is_choice
|
||||
}
|
||||
return false
|
||||
},
|
||||
handleAddRule() {
|
||||
this.ruleList.push({
|
||||
id: uuidv4(),
|
||||
type: 'and',
|
||||
property: this.canSearchPreferenceAttrList[0]?.name,
|
||||
exp: 'is',
|
||||
value: null,
|
||||
})
|
||||
this.$emit('change', this.ruleList)
|
||||
},
|
||||
handleCopyRule(item) {
|
||||
this.ruleList.push({ ...item, id: uuidv4() })
|
||||
this.$emit('change', this.ruleList)
|
||||
},
|
||||
handleDeleteRule(item) {
|
||||
const idx = this.ruleList.findIndex((r) => r.id === item.id)
|
||||
if (idx > -1) {
|
||||
this.ruleList.splice(idx, 1)
|
||||
}
|
||||
this.$emit('change', this.ruleList)
|
||||
},
|
||||
handleAddRuleAt(item) {
|
||||
const idx = this.ruleList.findIndex((r) => r.id === item.id)
|
||||
if (idx > -1) {
|
||||
this.ruleList.splice(idx + 1, 0, {
|
||||
id: uuidv4(),
|
||||
type: 'and',
|
||||
property: this.canSearchPreferenceAttrList[0]?.name,
|
||||
exp: 'is',
|
||||
value: null,
|
||||
})
|
||||
}
|
||||
this.$emit('change', this.ruleList)
|
||||
},
|
||||
getChoiceValueByProperty(property) {
|
||||
const _find = this.canSearchPreferenceAttrList.find((item) => item.name === property)
|
||||
if (_find) {
|
||||
return _find.choice_value
|
||||
}
|
||||
return []
|
||||
},
|
||||
handleChangeExp({ value }, item, index) {
|
||||
const _ruleList = _.cloneDeep(this.ruleList)
|
||||
if (value === 'range') {
|
||||
_ruleList[index] = {
|
||||
..._ruleList[index],
|
||||
min: '',
|
||||
max: '',
|
||||
exp: value,
|
||||
}
|
||||
} else if (value === 'compare') {
|
||||
_ruleList[index] = {
|
||||
..._ruleList[index],
|
||||
compareType: '1',
|
||||
exp: value,
|
||||
}
|
||||
} else {
|
||||
_ruleList[index] = {
|
||||
..._ruleList[index],
|
||||
exp: value,
|
||||
}
|
||||
}
|
||||
this.ruleList = _ruleList
|
||||
this.$emit('change', this.ruleList)
|
||||
},
|
||||
|
||||
handleChangeValue(value, index) {
|
||||
const _ruleList = _.cloneDeep(this.ruleList)
|
||||
_ruleList[index] = value
|
||||
this.$emit('change', _ruleList)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 150px;
|
||||
|
||||
&-range-icon {
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
input {
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.property-label {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden
|
||||
}
|
||||
|
||||
.attr-filter-tip {
|
||||
color: #86909C;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
</style>
|
211
cmdb-ui/src/modules/cmdb/components/conditionFilter/index.vue
Normal file
211
cmdb-ui/src/modules/cmdb/components/conditionFilter/index.vue
Normal file
@@ -0,0 +1,211 @@
|
||||
<template>
|
||||
<div>
|
||||
<Expression
|
||||
v-model="ruleList"
|
||||
:canSearchPreferenceAttrList="canSearchPreferenceAttrList.filter((attr) => !attr.is_password)"
|
||||
:disabled="false"
|
||||
:curModelAttrList="curModelAttrList"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { compareTypeList } from './constants.js'
|
||||
|
||||
import Expression from './expression.vue'
|
||||
|
||||
export default {
|
||||
name: 'AttrFilter',
|
||||
components: {
|
||||
Expression
|
||||
},
|
||||
props: {
|
||||
canSearchPreferenceAttrList: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => [],
|
||||
},
|
||||
expression: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
regQ: {
|
||||
type: String,
|
||||
default: '(?<=q=).+(?=&)|(?<=q=).+$',
|
||||
},
|
||||
CITypeIds: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
curModelAttrList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
compareTypeList,
|
||||
visible: false,
|
||||
ruleList: [],
|
||||
filterExp: '',
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
init(open, isInitOne = true) {
|
||||
// isInitOne 初始化exp为空时,ruleList是否默认给一条
|
||||
// const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
|
||||
const exp = this.expression.match(new RegExp(this.regQ, 'g'))
|
||||
? this.expression.match(new RegExp(this.regQ, 'g'))[0]
|
||||
: null
|
||||
if (open && exp) {
|
||||
const expArray = exp.split(',').map((item) => {
|
||||
let has_not = ''
|
||||
const key = item.split(':')[0]
|
||||
const val = item
|
||||
.split(':')
|
||||
.slice(1)
|
||||
.join(':')
|
||||
let type, property, exp, value, min, max, compareType
|
||||
if (key.includes('-')) {
|
||||
type = 'or'
|
||||
if (key.includes('~')) {
|
||||
property = key.substring(2)
|
||||
has_not = '~'
|
||||
} else {
|
||||
property = key.substring(1)
|
||||
}
|
||||
} else {
|
||||
type = 'and'
|
||||
if (key.includes('~')) {
|
||||
property = key.substring(1)
|
||||
has_not = '~'
|
||||
} else {
|
||||
property = key
|
||||
}
|
||||
}
|
||||
|
||||
const in_reg = /(?<=\().+(?=\))/g
|
||||
const range_reg = /(?<=\[).+(?=\])/g
|
||||
const compare_reg = /(?<=>=|<=|>(?!=)|<(?!=)).+/
|
||||
if (val === '*') {
|
||||
exp = has_not + 'value'
|
||||
value = ''
|
||||
} else if (in_reg.test(val)) {
|
||||
exp = has_not + 'in'
|
||||
value = val.match(in_reg)[0]
|
||||
} else if (range_reg.test(val)) {
|
||||
exp = has_not + 'range'
|
||||
value = val.match(range_reg)[0]
|
||||
min = value.split('_TO_')[0]
|
||||
max = value.split('_TO_')[1]
|
||||
} else if (compare_reg.test(val)) {
|
||||
exp = has_not + 'compare'
|
||||
value = val.match(compare_reg)[0]
|
||||
const _compareType = val.substring(0, val.match(compare_reg)['index'])
|
||||
const idx = compareTypeList.findIndex((item) => item.label === _compareType)
|
||||
compareType = compareTypeList[idx].value
|
||||
} else if (!val.includes('*')) {
|
||||
exp = has_not + 'is'
|
||||
value = val
|
||||
} else {
|
||||
const resList = [
|
||||
['contain', /(?<=\*).*(?=\*)/g],
|
||||
['end_with', /(?<=\*).+/g],
|
||||
['start_with', /.+(?=\*)/g],
|
||||
]
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const reg = resList[i]
|
||||
if (reg[1].test(val)) {
|
||||
exp = has_not + reg[0]
|
||||
value = val.match(reg[1])[0]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
id: uuidv4(),
|
||||
type,
|
||||
property,
|
||||
exp,
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
compareType,
|
||||
}
|
||||
})
|
||||
this.ruleList = [...expArray]
|
||||
} else if (open) {
|
||||
const _canSearchPreferenceAttrList = this.canSearchPreferenceAttrList.filter((attr) => !attr.is_password)
|
||||
this.ruleList = isInitOne
|
||||
? [
|
||||
{
|
||||
id: uuidv4(),
|
||||
type: 'and',
|
||||
property:
|
||||
_canSearchPreferenceAttrList && _canSearchPreferenceAttrList.length
|
||||
? _canSearchPreferenceAttrList[0].name
|
||||
: undefined,
|
||||
exp: 'is',
|
||||
value: null,
|
||||
},
|
||||
]
|
||||
: []
|
||||
}
|
||||
},
|
||||
|
||||
handleSubmit() {
|
||||
if (this.ruleList && this.ruleList.length) {
|
||||
this.ruleList[0].type = 'and' // 增删后,以防万一第一个不是and
|
||||
this.filterExp = ''
|
||||
const expList = this.ruleList.filter((rule) => rule?.property).map((rule) => {
|
||||
let singleRuleExp = ''
|
||||
let _exp = rule.exp
|
||||
if (rule.type === 'or') {
|
||||
singleRuleExp += '-'
|
||||
}
|
||||
if (rule.exp.includes('~')) {
|
||||
singleRuleExp += '~'
|
||||
_exp = rule.exp.split('~')[1]
|
||||
}
|
||||
singleRuleExp += `${rule.property}:`
|
||||
if (_exp === 'is') {
|
||||
singleRuleExp += `${rule.value ?? ''}`
|
||||
}
|
||||
if (_exp === 'contain') {
|
||||
singleRuleExp += `*${rule.value ?? ''}*`
|
||||
}
|
||||
if (_exp === 'start_with') {
|
||||
singleRuleExp += `${rule.value ?? ''}*`
|
||||
}
|
||||
if (_exp === 'end_with') {
|
||||
singleRuleExp += `*${rule.value ?? ''}`
|
||||
}
|
||||
if (_exp === 'value') {
|
||||
singleRuleExp += `*`
|
||||
}
|
||||
if (_exp === 'in') {
|
||||
singleRuleExp += `(${rule.value ?? ''})`
|
||||
}
|
||||
if (_exp === 'range') {
|
||||
singleRuleExp += `[${rule.min}_TO_${rule.max}]`
|
||||
}
|
||||
if (_exp === 'compare') {
|
||||
const idx = compareTypeList.findIndex((item) => item.value === rule.compareType)
|
||||
singleRuleExp += `${compareTypeList[idx].label}${rule.value ?? ''}`
|
||||
}
|
||||
return singleRuleExp
|
||||
})
|
||||
this.filterExp = expList.join(',')
|
||||
this.$emit('setExpFromFilter', this.filterExp)
|
||||
} else {
|
||||
this.$emit('setExpFromFilter', '')
|
||||
}
|
||||
this.visible = false
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
@@ -0,0 +1,255 @@
|
||||
<template>
|
||||
<div class="control-group">
|
||||
<CIReferenceAttr
|
||||
v-if="getAttr(rule.property).is_reference && (rule.exp === 'is' || rule.exp === '~is')"
|
||||
class="select-filter"
|
||||
:referenceTypeId="getAttr(rule.property).reference_type_id"
|
||||
:value="rule.value"
|
||||
:disabled="disabled"
|
||||
@change="(value) => handleChange('value', value)"
|
||||
/>
|
||||
<a-select
|
||||
v-else-if="getAttr(rule.property).is_bool && (rule.exp === 'is' || rule.exp === '~is')"
|
||||
class="select-filter"
|
||||
:disabled="disabled"
|
||||
:placeholder="$t('placeholder2')"
|
||||
:value="rule.value"
|
||||
@change="(value) => handleChange('value', value)"
|
||||
>
|
||||
<a-select-option key="1">
|
||||
true
|
||||
</a-select-option>
|
||||
<a-select-option key="0">
|
||||
false
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<div
|
||||
class="input-group"
|
||||
v-else-if="isChoiceByProperty(rule.property) && (rule.exp === 'is' || rule.exp === '~is')"
|
||||
>
|
||||
<a-select
|
||||
class="select-filter"
|
||||
:style="{ width: '175px' }"
|
||||
showSearch
|
||||
:placeholder="$t('placeholder2')"
|
||||
:disabled="disabled"
|
||||
:value="rule.value"
|
||||
@change="(value) => handleChange('value', value)"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="(node) in getChoiceValueByProperty(rule.property)"
|
||||
:key="String(node[0])"
|
||||
:title="node[1] ? node[1].label || node[0] : node[0]"
|
||||
>
|
||||
<a-tooltip placement="topLeft" :title="node[1] ? node[1].label || node[0] : node[0]" >
|
||||
{{ node[1] ? node[1].label || node[0] : node[0] }}
|
||||
</a-tooltip>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<!-- <treeselect
|
||||
class="custom-treeselect"
|
||||
:style="{ '--custom-height': rowHeight }"
|
||||
:value="rule.value"
|
||||
@input="(value) => handleChange('value', value)"
|
||||
:multiple="false"
|
||||
:clearable="false"
|
||||
searchable
|
||||
:options="getChoiceValueByProperty(rule.property)"
|
||||
:placeholder="$t('placeholder2')"
|
||||
:normalizer="
|
||||
(node) => {
|
||||
return {
|
||||
id: node[0],
|
||||
label: node[1] ? node[1].label || node[0] : node[0],
|
||||
children: node.children,
|
||||
}
|
||||
}
|
||||
"
|
||||
:zIndex="1050"
|
||||
:disabled="disabled"
|
||||
appendToBody
|
||||
>
|
||||
<div
|
||||
:title="node.label"
|
||||
slot="option-label"
|
||||
slot-scope="{ node }"
|
||||
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
|
||||
>
|
||||
{{ node.label }}
|
||||
</div>
|
||||
</treeselect> -->
|
||||
</div>
|
||||
<div
|
||||
compact
|
||||
v-else-if="rule.exp === 'range' || rule.exp === '~range'"
|
||||
class="input-group"
|
||||
>
|
||||
<a-input
|
||||
class="ops-input"
|
||||
:placeholder="$t('min')"
|
||||
:disabled="disabled"
|
||||
:value="rule.min"
|
||||
@change="(e) => handleChange('min', e.target.value)"
|
||||
/>
|
||||
<span class="input-group-range-icon">~</span>
|
||||
<a-input
|
||||
class="ops-input"
|
||||
v-model="rule.max"
|
||||
:placeholder="$t('max')"
|
||||
:disabled="disabled"
|
||||
:value="rule.max"
|
||||
@change="(e) => handleChange('max', e.target.value)"
|
||||
/>
|
||||
</div>
|
||||
<div class="input-group" compact v-else-if="rule.exp === 'compare'">
|
||||
<treeselect
|
||||
class="custom-treeselect"
|
||||
:style="{ width: '70px', '--custom-height': rowHeight, 'flex-shrink': 0 }"
|
||||
:value="rule.compareType"
|
||||
@input="(value) => handleChange('compareType', value)"
|
||||
:multiple="false"
|
||||
:clearable="false"
|
||||
searchable
|
||||
:options="compareTypeList"
|
||||
:normalizer="
|
||||
(node) => {
|
||||
return {
|
||||
id: node.value,
|
||||
label: node.label,
|
||||
children: node.children,
|
||||
}
|
||||
}
|
||||
"
|
||||
:zIndex="1050"
|
||||
:disabled="disabled"
|
||||
appendToBody
|
||||
>
|
||||
</treeselect>
|
||||
<a-input :value="rule.value" @change="(e) => handleChange('value', e.target.value)" class="ops-input"/>
|
||||
</div>
|
||||
<div class="input-group" v-else-if="rule.exp !== 'value' && rule.exp !== '~value'">
|
||||
<a-input
|
||||
:value="rule.value"
|
||||
@change="(e) => handleChange('value', e.target.value)"
|
||||
:placeholder="rule.exp === 'in' || rule.exp === '~in' ? $t('cmdbFilterComp.split', { separator: ';' }) : ''"
|
||||
class="ops-input"
|
||||
:disabled="disabled"
|
||||
></a-input>
|
||||
</div>
|
||||
<div v-else :style="{ width: '136px' }"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { compareTypeList } from './constants.js'
|
||||
import CIReferenceAttr from '@/components/ciReferenceAttr/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'ValueControls',
|
||||
components: {
|
||||
CIReferenceAttr,
|
||||
},
|
||||
props: {
|
||||
rule: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
attrList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
// 当前模型属性
|
||||
curModelAttrList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// 行高
|
||||
rowHeight: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
compareTypeList,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
choiceValue() {
|
||||
const val = /\{\{([^}]+)\}\}/g.exec(this?.rule?.value || '')
|
||||
return val ? val?.[1]?.trim() || '' : this?.value?.value
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isChoiceByProperty(property) {
|
||||
const _find = this.attrList.find((item) => item.name === property)
|
||||
if (_find) {
|
||||
return _find.is_choice
|
||||
}
|
||||
return false
|
||||
},
|
||||
getChoiceValueByProperty(property) {
|
||||
const _find = this.attrList.find((item) => item.name === property)
|
||||
if (_find) {
|
||||
return _find.choice_value
|
||||
}
|
||||
return []
|
||||
},
|
||||
handleChange(key, value) {
|
||||
this.$emit('change', {
|
||||
...this.rule,
|
||||
[key]: value
|
||||
})
|
||||
},
|
||||
getAttr(property) {
|
||||
return this.attrList.find((item) => item.name === property) || {}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.control-group {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 136px;
|
||||
|
||||
&-range-icon {
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
input {
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.select-filter {
|
||||
height: 36px;
|
||||
width: 136px;
|
||||
|
||||
/deep/ .ant-select-selection {
|
||||
height: 36px;
|
||||
background: #f7f8fa;
|
||||
line-height: 36px;
|
||||
border: none;
|
||||
|
||||
.ant-select-selection__rendered {
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
/deep/ .vue-treeselect__control {
|
||||
background: #f7f8fa;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -29,7 +29,7 @@
|
||||
class="category-side-children-item-corporate"
|
||||
v-if="ruleType === 'private_cloud' || (ruleType === 'http' && (categoryIndex !== 0 || itemIndex !== 0))"
|
||||
>
|
||||
企
|
||||
{{ $t('cmdb.enterpriseVersionFlag') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -67,7 +67,7 @@
|
||||
class="corporate-flag"
|
||||
v-if="ruleType === 'private_cloud' || (ruleType === 'http' && (categoryIndex !== 0 || itemIndex !== 0))"
|
||||
>
|
||||
<span class="corporate-flag-text">企</span>
|
||||
<span class="corporate-flag-text">{{ $t('cmdb.enterpriseVersionFlag') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -113,6 +113,11 @@ export default {
|
||||
this.editor.insertNode(node)
|
||||
}
|
||||
},
|
||||
destroy() {
|
||||
const editor = this.editor
|
||||
if (editor == null) return
|
||||
editor.destroy()
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
@@ -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>
|
||||
<FilterComp
|
||||
ref="filterComp"
|
||||
:canSearchPreferenceAttrList="canSearchPreferenceAttrList"
|
||||
@@ -131,6 +129,8 @@ import Treeselect from '@riophae/vue-treeselect'
|
||||
import MetadataDrawer from '../../views/ci/modules/MetadataDrawer.vue'
|
||||
import FilterComp from '@/components/CMDBFilterComp'
|
||||
import { getCITypeGroups } from '../../api/ciTypeGroup'
|
||||
import { CI_DEFAULT_ATTR } from '@/modules/cmdb/utils/const.js'
|
||||
|
||||
export default {
|
||||
name: 'SearchForm',
|
||||
components: { MetadataDrawer, FilterComp, Treeselect },
|
||||
@@ -178,7 +178,9 @@ export default {
|
||||
return '200px'
|
||||
},
|
||||
canSearchPreferenceAttrList() {
|
||||
return this.preferenceAttrList.filter((item) => item.value_type !== '6')
|
||||
return this.preferenceAttrList.filter((item) => {
|
||||
return item.value_type !== '6' && ![CI_DEFAULT_ATTR.UPDATE_USER, CI_DEFAULT_ATTR.UPDATE_TIME].includes(item.name)
|
||||
})
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
@@ -310,6 +312,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;
|
||||
|
@@ -29,6 +29,7 @@
|
||||
:fixedList="fixedList"
|
||||
@setFixedList="setFixedList"
|
||||
:height="windowHeight - 170"
|
||||
:showDefaultAttr="true"
|
||||
/>
|
||||
<div class="custom-drawer-bottom-action">
|
||||
<a-button @click="subInstanceSubmit" type="primary">{{ $t('cmdb.preference.sub') }}</a-button>
|
||||
@@ -64,7 +65,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="cmdb-subscribe-drawer-tree-main" :style="{ maxHeight: `${((windowHeight - 170) * 2) / 3}px` }">
|
||||
<div @click="changeTreeViews(attr)" v-for="attr in attrList" :key="attr.name">
|
||||
<div @click="changeTreeViews(attr)" v-for="attr in treeViewAttrList" :key="attr.name">
|
||||
<a-checkbox :checked="treeViews.includes(attr.name)" />
|
||||
{{ attr.title }}
|
||||
</div>
|
||||
@@ -90,6 +91,8 @@ import {
|
||||
} from '@/modules/cmdb/api/preference'
|
||||
import { getCITypeAttributesByName } from '@/modules/cmdb/api/CITypeAttr'
|
||||
import AttributesTransfer from '../attributesTransfer'
|
||||
import { CI_DEFAULT_ATTR } from '@/modules/cmdb/utils/const.js'
|
||||
|
||||
export default {
|
||||
name: 'SubscribeSetting',
|
||||
components: { AttributesTransfer },
|
||||
@@ -110,16 +113,32 @@ export default {
|
||||
...mapState({
|
||||
windowHeight: (state) => state.windowHeight,
|
||||
}),
|
||||
treeViewAttrList() {
|
||||
return this.attrList.filter((item) => ![CI_DEFAULT_ATTR.UPDATE_USER, CI_DEFAULT_ATTR.UPDATE_TIME].includes(item.name))
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
open(ciType = {}, activeKey = '1') {
|
||||
this.ciType = ciType
|
||||
this.activeKey = activeKey
|
||||
const updatedByKey = CI_DEFAULT_ATTR.UPDATE_USER
|
||||
const updatedAtKey = CI_DEFAULT_ATTR.UPDATE_TIME
|
||||
|
||||
getCITypeAttributesByName(ciType.type_id).then((res) => {
|
||||
const attributes = res.attributes
|
||||
const attributes = res.attributes.filter((item) => ![updatedByKey, updatedAtKey].includes(item.name))
|
||||
|
||||
;[updatedByKey, updatedAtKey].map((key) => {
|
||||
attributes.push({
|
||||
alias: key,
|
||||
name: key,
|
||||
id: key
|
||||
})
|
||||
})
|
||||
|
||||
getSubscribeAttributes(ciType.type_id).then((_res) => {
|
||||
this.instanceSubscribed = _res.is_subscribed
|
||||
const selectedAttrList = _res.attributes.map((item) => item.id.toString())
|
||||
|
||||
const attrList = attributes.map((item) => {
|
||||
return {
|
||||
key: item.id.toString(),
|
||||
@@ -188,9 +207,20 @@ export default {
|
||||
})
|
||||
},
|
||||
subInstanceSubmit() {
|
||||
const customAttr = []
|
||||
const defaultAttr = []
|
||||
this.selectedAttrList.map((attr) => {
|
||||
if ([CI_DEFAULT_ATTR.UPDATE_USER, CI_DEFAULT_ATTR.UPDATE_TIME].includes(attr)) {
|
||||
defaultAttr.push(attr)
|
||||
} else {
|
||||
customAttr.push(attr)
|
||||
}
|
||||
})
|
||||
const selectedAttrList = [...customAttr, ...defaultAttr]
|
||||
|
||||
subscribeCIType(
|
||||
this.ciType.type_id,
|
||||
this.selectedAttrList.map((item) => {
|
||||
selectedAttrList.map((item) => {
|
||||
return [item, !!this.fixedList.includes(item)]
|
||||
})
|
||||
).then((res) => {
|
||||
|
@@ -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 =
|
||||
|
@@ -2,6 +2,8 @@ const cmdb_en = {
|
||||
relation: 'Relation',
|
||||
attribute: 'Attributes',
|
||||
configTable: 'Config Table',
|
||||
enterpriseVersionFlag: 'Pro',
|
||||
enterpriseVersionTip: 'Enterprise version only',
|
||||
menu: {
|
||||
views: 'Views',
|
||||
topologyView: 'Topology Views',
|
||||
@@ -22,7 +24,8 @@ const cmdb_en = {
|
||||
operationHistory: 'Operation Audit',
|
||||
relationType: 'Relation Type',
|
||||
ad: 'AutoDiscovery',
|
||||
cidetail: 'CI Detail'
|
||||
cidetail: 'CI Detail',
|
||||
scene: 'Scene'
|
||||
},
|
||||
ciType: {
|
||||
ciType: 'CIType',
|
||||
@@ -60,7 +63,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}]?',
|
||||
@@ -115,7 +118,7 @@ const cmdb_en = {
|
||||
advancedSettings: 'Advanced Settings',
|
||||
font: 'Font',
|
||||
color: 'Color',
|
||||
choiceValue: 'Predefined value',
|
||||
choiceValue: 'Select List',
|
||||
computedAttribute: 'Computed Attribute',
|
||||
computedAttributeTips: 'The value of this attribute is calculated through an expression constructed from other attributes of the CIType or by executing a piece of code. The reference method of the attribute is: {{ attribute name }}',
|
||||
addAttribute: 'New attribute',
|
||||
@@ -128,6 +131,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',
|
||||
@@ -139,7 +143,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',
|
||||
@@ -187,6 +191,14 @@ const cmdb_en = {
|
||||
confirmDeleteTrigger: 'Are you sure to delete this trigger?',
|
||||
int: 'Integer',
|
||||
float: 'Float',
|
||||
longText: 'Long Text',
|
||||
shortText: 'Short Text',
|
||||
shortTextTip: 'Text length <= 128',
|
||||
referenceModel: 'Reference Model',
|
||||
referenceModelTip: 'Please select reference model',
|
||||
referenceModelTip1: 'For quick view of referenced model instances',
|
||||
bool: 'Bool',
|
||||
reference: 'Reference',
|
||||
text: 'Text',
|
||||
datetime: 'DateTime',
|
||||
date: 'Date',
|
||||
@@ -204,7 +216,7 @@ const cmdb_en = {
|
||||
otherGroupTips: 'Non sortable within the other group',
|
||||
filterTips: 'click to show {name}',
|
||||
attributeAssociation: 'Attribute Association',
|
||||
attributeAssociationTip1: 'Automatically establish relationships through the attributes except password, json and multiple of two models',
|
||||
attributeAssociationTip1: 'Automatically establish relationships through attribute values (except password, json, multi-value, long text, boolean, reference) of two models',
|
||||
attributeAssociationTip2: 'Double click to edit',
|
||||
attributeAssociationTip3: 'Two Attributes must be selected',
|
||||
attributeAssociationTip4: 'Please select a attribute from Source CIType',
|
||||
@@ -274,8 +286,41 @@ const cmdb_en = {
|
||||
attrAlias: 'Attr Alias',
|
||||
attrCode: 'Attr Code',
|
||||
computedAttrTip1: 'Reference attributes follow jinja2 syntax',
|
||||
computedAttrTip2: `Multi-valued attributes (lists) are rendered with [ ] included by default, if you want to remove it, the reference method is: ‘’‘{{ attr_name | join(’,‘)}}}’‘’ where commas are separators`,
|
||||
example: 'Example'
|
||||
computedAttrTip2: `Multi-valued attributes (lists) are rendered with [ ] included by default, if you want to remove it, the reference method is: """{{ attr_name | join(',') }}""" where commas are separators`,
|
||||
computedAttrTip3: `Cannot refer to other computed attributes`,
|
||||
example: 'Example',
|
||||
attrFilterTip: `The third column of values allows you to select attributes of this model to cascade attributes`,
|
||||
rule: 'Rule',
|
||||
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',
|
||||
@@ -317,7 +362,7 @@ const cmdb_en = {
|
||||
pleaseSearch: 'Please search',
|
||||
conditionFilter: 'Conditional filtering',
|
||||
attributeDesc: 'Attribute Description',
|
||||
ciSearchTips: '1. JSON/password/link attributes cannot be searched\n2. If the search content includes commas, they need to be escaped\n3. Only index attributes are searched, non-index attributes use conditional filtering',
|
||||
ciSearchTips: '1. JSON/password/link/longText/reference attributes cannot be searched\n2. If the search content includes commas, they need to be escaped\n3. Only index attributes are searched, non-index attributes use conditional filtering',
|
||||
ciSearchTips2: 'For example: q=hostname:*0.0.0.0*',
|
||||
subCIType: 'Subscription CIType',
|
||||
already: 'already',
|
||||
@@ -325,6 +370,7 @@ const cmdb_en = {
|
||||
sub: 'subscription',
|
||||
selectBelow: 'Please select below',
|
||||
subSuccess: 'Subscription successful',
|
||||
subFailed: 'Subscription failed, please try again later',
|
||||
selectMethods: 'Please select a method',
|
||||
noAuthRequest: 'No certification requested yet',
|
||||
noParamRequest: 'No parameter certification yet',
|
||||
@@ -332,6 +378,9 @@ const cmdb_en = {
|
||||
param: 'Parameter{param}',
|
||||
value: 'Value{value}',
|
||||
clear: 'Clear',
|
||||
updater: 'Update User',
|
||||
updateTime: 'Update Time',
|
||||
default: 'Default'
|
||||
},
|
||||
batch: {
|
||||
downloadFailed: 'Download failed',
|
||||
@@ -360,7 +409,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: {
|
||||
@@ -380,6 +429,8 @@ const cmdb_en = {
|
||||
yearsAgo: 'years ago',
|
||||
just: 'just now',
|
||||
searchPlaceholder: 'Please search CIType',
|
||||
subCITable: 'Data',
|
||||
subCITree: 'Tree',
|
||||
},
|
||||
custom_dashboard: {
|
||||
charts: 'Chart',
|
||||
@@ -539,7 +590,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"),
|
||||
@@ -595,6 +646,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?',
|
||||
@@ -617,7 +669,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.',
|
||||
@@ -639,6 +691,7 @@ if __name__ == "__main__":
|
||||
rollbackingTips: 'Rollbacking',
|
||||
batchRollbacking: 'Deleting {total} items in total, {successNum} items successful, {errorNum} items failed',
|
||||
baselineTips: 'Changes at this point in time will also be rollbacked, Unique ID, password and dynamic attributes do not support',
|
||||
cover: 'Cover',
|
||||
},
|
||||
serviceTree: {
|
||||
remove: 'Remove',
|
||||
@@ -692,5 +745,99 @@ if __name__ == "__main__":
|
||||
topoViewSearchPlaceholder: 'Please enter the node name.',
|
||||
moreBtn: 'Show more({count})'
|
||||
},
|
||||
relationSearch: {
|
||||
relationSearch: 'Relation Search',
|
||||
sourceCIType: 'Source CIType',
|
||||
sourceCITypeTip: 'Please input or select',
|
||||
sourceCITYpeInput: 'Please input keywords',
|
||||
targetCIType: 'Target CIType',
|
||||
targetCITypeTip: 'Please input or select, multiple choices available',
|
||||
pathSelect: 'Path Select',
|
||||
pathSelectTip: 'Please select source CIType and target CIType first',
|
||||
saveCondition: 'Save Condition',
|
||||
conditionFilter: 'Condition Filter',
|
||||
level: 'Level',
|
||||
returnPath: 'Return Path',
|
||||
conditionName: 'Condition Name',
|
||||
path: 'Path',
|
||||
expandCondition: 'Expand Condition',
|
||||
},
|
||||
ipam: {
|
||||
overview: 'Overview',
|
||||
addressAssign: 'Address Assign',
|
||||
ipSearch: 'IP Search',
|
||||
subnetList: 'Subnet List',
|
||||
history: 'History Log',
|
||||
ticket: 'Related Tickets',
|
||||
addSubnet: 'Add Subnet',
|
||||
editSubnet: 'Edit Subnet',
|
||||
addCatalog: 'Add Catalog',
|
||||
editCatalog: 'Edit Catalog',
|
||||
catalogName: 'Catalog Name',
|
||||
editName: 'Edit Name',
|
||||
editNode: 'Edit Node',
|
||||
deleteNode: 'Delete Node',
|
||||
basicInfo: 'Basic Info',
|
||||
scanRule: 'Scan Rule',
|
||||
adExecTarget: 'Execute targets',
|
||||
masterMachineTip: 'The machine where OneMaster is installed',
|
||||
oneagentIdTips: 'Please enter the hexadecimal OneAgent ID starting with 0x ID',
|
||||
selectFromCMDBTips: 'Select from CMDB',
|
||||
adInterval: 'Collection frequency',
|
||||
cronTips: 'The format is the same as crontab, for example: 0 15 * * 1-5',
|
||||
masterMachine: 'Master machine',
|
||||
specifyMachine: 'Specify machine',
|
||||
specifyMachineTips: 'Please fill in the specify machine!',
|
||||
cronRequiredTip: 'Acquisition frequency cannot be null',
|
||||
addressNullTip: ' Please select the leaf node of the left subnet tree first',
|
||||
addressNullTip2: 'Subnet prefix length must be >= 16',
|
||||
assignedOnline: 'Assigned Online',
|
||||
assignedOffline: 'Assigned Offline',
|
||||
unassignedOnline: 'Unassigned Online',
|
||||
unused: 'Unused',
|
||||
allStatus: 'All Status',
|
||||
editAssignAddress: 'Edit Assign Address',
|
||||
assign: 'Assign',
|
||||
recycle: 'Recycle',
|
||||
assignStatus: 'Assign Status',
|
||||
reserved: 'Reserved',
|
||||
assigned: 'Assigned',
|
||||
recycleTip: 'Confirmed for recycle? After recycling, the status of the segment will be changed to unassigned.',
|
||||
recycleSuccess: '{ip} Recycled successfully, status changed to: unassigned.',
|
||||
operationLog: 'Operation Log',
|
||||
scanLog: 'Scan Log',
|
||||
updateCatalog: 'Update Catalog',
|
||||
deleteCatalog: 'Delete Catalog',
|
||||
updateSubnet: 'Update Subnet',
|
||||
deleteSubnet: 'Delete Subnet',
|
||||
revokeAddress: 'Revoke Address',
|
||||
operateTime: 'Operate Time',
|
||||
operateUser: 'Operate User',
|
||||
operateType: 'Operate Type',
|
||||
subnet: 'Subnet',
|
||||
description: 'Description',
|
||||
ipNumber: 'Number of online IP',
|
||||
startTime: 'Start Time',
|
||||
endTime: 'End Time',
|
||||
scanningTime: 'Scanning Time',
|
||||
viewResult: 'View Result',
|
||||
scannedIP: 'Scanned IP',
|
||||
subnetStats: 'Subnet Stats',
|
||||
addressStats: 'Address Stats',
|
||||
onlineStats: 'Online Stats',
|
||||
assignStats: 'Assign Stats',
|
||||
total: 'Total',
|
||||
free: 'Free',
|
||||
unassigned: 'Unassigned',
|
||||
online: 'Online',
|
||||
offline: 'Offline',
|
||||
onlineUsageStats: 'Subnet Online Stats',
|
||||
subnetName: 'Subnet Name',
|
||||
addressCount: 'Address Count',
|
||||
onlineRatio: 'Online Ratio',
|
||||
scanEnable: 'Scan Enable',
|
||||
lastScanTime: 'Last Scan Time',
|
||||
isSuccess: 'Is Success'
|
||||
}
|
||||
}
|
||||
export default cmdb_en
|
||||
|
@@ -2,6 +2,8 @@ const cmdb_zh = {
|
||||
relation: '关系',
|
||||
attribute: '属性',
|
||||
configTable: '配置表格',
|
||||
enterpriseVersionFlag: '企',
|
||||
enterpriseVersionTip: '仅限企业版',
|
||||
menu: {
|
||||
views: '视图',
|
||||
topologyView: '拓扑视图',
|
||||
@@ -22,7 +24,8 @@ const cmdb_zh = {
|
||||
operationHistory: '操作审计',
|
||||
relationType: '关系类型',
|
||||
ad: '自动发现',
|
||||
cidetail: 'CI 详情'
|
||||
cidetail: 'CI 详情',
|
||||
scene: '场景'
|
||||
},
|
||||
ciType: {
|
||||
ciType: '模型',
|
||||
@@ -60,7 +63,7 @@ const cmdb_zh = {
|
||||
desc: '倒序',
|
||||
uniqueKey: '唯一标识',
|
||||
uniqueKeySelect: '请选择唯一标识',
|
||||
uniqueKeyTips: 'json、密码、计算属性、预定义值属性不能作为唯一标识',
|
||||
uniqueKeyTips: 'json、密码、计算属性、下拉列表属性不能作为唯一标识',
|
||||
notfound: '找不到想要的?',
|
||||
cannotDeleteGroupTips: '该分组下有数据, 不能删除!',
|
||||
confirmDeleteGroup: '确定要删除分组 【{groupName}】 吗?',
|
||||
@@ -115,7 +118,7 @@ const cmdb_zh = {
|
||||
advancedSettings: '高级设置',
|
||||
font: '字体',
|
||||
color: '颜色',
|
||||
choiceValue: '预定义值',
|
||||
choiceValue: '下拉列表',
|
||||
computedAttribute: '计算属性',
|
||||
computedAttributeTips: '该属性的值是通过模型的其它属性构建的表达式或者执行一段代码的方式计算而来,属性的引用方法为: {{ 属性名 }}',
|
||||
addAttribute: '新增属性',
|
||||
@@ -128,6 +131,7 @@ const cmdb_zh = {
|
||||
selectAttribute: '添加属性',
|
||||
groupExisted: '分组名称已存在',
|
||||
attributeSortedTips: '其他分组中的属性不能进行排序,如需排序请先拖至自定义的分组!',
|
||||
attributeSortedTips2: '非继承属性不能插入到继承属性前!',
|
||||
buildinAttribute: '内置字段',
|
||||
expr: '表达式',
|
||||
code: '代码',
|
||||
@@ -139,7 +143,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: '源模型',
|
||||
@@ -187,6 +191,14 @@ const cmdb_zh = {
|
||||
confirmDeleteTrigger: '确认删除该触发器吗?',
|
||||
int: '整数',
|
||||
float: '浮点数',
|
||||
longText: '长文本',
|
||||
shortText: '短文本',
|
||||
shortTextTip: '文本长度 <= 128',
|
||||
referenceModel: '引用模型',
|
||||
referenceModelTip: '请选择引用模型',
|
||||
referenceModelTip1: '用于快捷查看引用模型实例',
|
||||
bool: '布尔',
|
||||
reference: '引用',
|
||||
text: '文本',
|
||||
datetime: '日期时间',
|
||||
date: '日期',
|
||||
@@ -204,7 +216,7 @@ const cmdb_zh = {
|
||||
otherGroupTips: '其他分组属性不可排序',
|
||||
filterTips: '点击可仅查看{name}属性',
|
||||
attributeAssociation: '属性关联',
|
||||
attributeAssociationTip1: '通过2个模型的属性值(除密码、json、多值)来自动建立关系',
|
||||
attributeAssociationTip1: '通过2个模型的属性值(除密码、json、多值、长文本、布尔、引用)来自动建立关系',
|
||||
attributeAssociationTip2: '双击可编辑',
|
||||
attributeAssociationTip3: '属性关联必须选择两个属性',
|
||||
attributeAssociationTip4: '请选择原模型属性',
|
||||
@@ -275,7 +287,40 @@ const cmdb_zh = {
|
||||
attrCode: '属性代码',
|
||||
computedAttrTip1: '引用属性遵循jinja2语法',
|
||||
computedAttrTip2: `多值属性(列表)默认呈现包括[ ], 如果要去掉, 引用方法为: """{{ attr_name | join(',') }}""" 其中逗号为分隔符`,
|
||||
example: '例如'
|
||||
computedAttrTip3: `不能引用其他计算属性`,
|
||||
example: '例如',
|
||||
attrFilterTip: '第三列值可选择本模型的属性,来实现级联属性的功能',
|
||||
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: '未选属性',
|
||||
@@ -317,7 +362,7 @@ const cmdb_zh = {
|
||||
pleaseSearch: '请查找',
|
||||
conditionFilter: '条件过滤',
|
||||
attributeDesc: '属性说明',
|
||||
ciSearchTips: '1. json、密码、链接属性不能搜索\n2. 搜索内容包括逗号, 则需转义\n3. 只搜索索引属性, 非索引属性使用条件过滤',
|
||||
ciSearchTips: '1. json、密码、链接、长文本、引用属性不能搜索\n2. 搜索内容包括逗号, 则需转义\n3. 只搜索索引属性, 非索引属性使用条件过滤',
|
||||
ciSearchTips2: '例: q=hostname:*0.0.0.0*',
|
||||
subCIType: '订阅模型',
|
||||
already: '已',
|
||||
@@ -325,6 +370,7 @@ const cmdb_zh = {
|
||||
sub: '订阅',
|
||||
selectBelow: '请在下方进行选择',
|
||||
subSuccess: '订阅成功',
|
||||
subFailed: '订阅失败,请稍后再试',
|
||||
selectMethods: '请选择方式',
|
||||
noAuthRequest: '暂无请求认证',
|
||||
noParamRequest: '暂无参数认证',
|
||||
@@ -332,6 +378,9 @@ const cmdb_zh = {
|
||||
param: '参数{param}',
|
||||
value: '值{value}',
|
||||
clear: '清空',
|
||||
updater: '更新人',
|
||||
updateTime: '更新时间',
|
||||
default: '默认'
|
||||
},
|
||||
batch: {
|
||||
downloadFailed: '失败下载',
|
||||
@@ -359,7 +408,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: {
|
||||
@@ -379,6 +428,8 @@ const cmdb_zh = {
|
||||
yearsAgo: '年前',
|
||||
just: '刚刚',
|
||||
searchPlaceholder: '请搜索模型',
|
||||
subCITable: '数据订阅',
|
||||
subCITree: '层级订阅',
|
||||
},
|
||||
custom_dashboard: {
|
||||
charts: '图表',
|
||||
@@ -538,7 +589,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"),
|
||||
@@ -594,6 +645,7 @@ if __name__ == "__main__":
|
||||
attributeDesc: '查看属性配置',
|
||||
selectRows: '选取:{rows} 项',
|
||||
addRelation: '添加关系',
|
||||
viewRelation: '查看关系',
|
||||
all: '全部',
|
||||
batchUpdate: '批量修改',
|
||||
batchUpdateConfirm: '确认要批量修改吗?',
|
||||
@@ -616,7 +668,7 @@ if __name__ == "__main__":
|
||||
tips4: '必须至少选择一个字段',
|
||||
tips5: '搜索 名称 | 别名',
|
||||
tips6: '加快检索, 可以全文搜索, 无需使用条件过滤\n\n json、链接、密码目前不支持建索引 \n\n文本字符长度超过190不能建索引',
|
||||
tips7: '表现形式是下拉框, 值必须在预定义值里',
|
||||
tips7: '是否配置下拉列表',
|
||||
tips8: '多值, 比如内网IP',
|
||||
tips9: '仅针对前端',
|
||||
tips10: '模型的其他属性通过表达式的方式计算出来\n\n一个代码片段计算返回的值',
|
||||
@@ -638,6 +690,7 @@ if __name__ == "__main__":
|
||||
rollbackingTips: '正在批量回滚中',
|
||||
batchRollbacking: '正在回滚,共{total}个,成功{successNum}个,失败{errorNum}个',
|
||||
baselineTips: '该时间点的变更也会被回滚, 唯一标识、密码属性、动态属性不支持回滚',
|
||||
cover: '覆盖',
|
||||
},
|
||||
serviceTree: {
|
||||
remove: '移除',
|
||||
@@ -691,5 +744,99 @@ if __name__ == "__main__":
|
||||
topoViewSearchPlaceholder: '请输入节点名字',
|
||||
moreBtn: '展示更多({count})'
|
||||
},
|
||||
relationSearch: {
|
||||
relationSearch: '关系搜索',
|
||||
sourceCIType: '源模型',
|
||||
sourceCITypeTip: '请输入或选择',
|
||||
sourceCITYpeInput: '请输入关键词',
|
||||
targetCIType: '目标模型',
|
||||
targetCITypeTip: '请输入或选择,可多选',
|
||||
pathSelect: '路径选择',
|
||||
pathSelectTip: '请先选择源模型和目标模型',
|
||||
saveCondition: '保存条件',
|
||||
conditionFilter: '条件过滤',
|
||||
level: '层级',
|
||||
returnPath: '返回路径',
|
||||
conditionName: '条件命名',
|
||||
path: '路径',
|
||||
expandCondition: '展开条件',
|
||||
},
|
||||
ipam: {
|
||||
overview: '概览',
|
||||
addressAssign: '地址分配',
|
||||
ipSearch: 'IP查询',
|
||||
subnetList: '子网列表',
|
||||
history: '历史记录',
|
||||
ticket: '关联工单',
|
||||
addSubnet: '新增子网',
|
||||
editSubnet: '编辑子网',
|
||||
addCatalog: '新增目录',
|
||||
editCatalog: '修改目录',
|
||||
catalogName: '目录名称',
|
||||
editName: '修改名称',
|
||||
editNode: '修改节点',
|
||||
deleteNode: '删除节点',
|
||||
basicInfo: '基本信息',
|
||||
scanRule: '扫描规则',
|
||||
adExecTarget: '执行机器',
|
||||
masterMachineTip: '安装OneMaster的所在机器',
|
||||
oneagentIdTips: '请输入以0x开头的16进制OneAgent ID',
|
||||
selectFromCMDBTips: '从CMDB中选择',
|
||||
adInterval: '采集频率',
|
||||
cronTips: '格式同crontab, 例如:0 15 * * 1-5',
|
||||
masterMachine: 'Master机器',
|
||||
specifyMachine: '指定机器',
|
||||
specifyMachineTips: '请填写指定机器!',
|
||||
cronRequiredTip: '采集频率不能为空',
|
||||
addressNullTip: '请先选择左侧子网树的叶子节点',
|
||||
addressNullTip2: '子网前缀长度必须 >= 16',
|
||||
assignedOnline: '已分配在线',
|
||||
assignedOffline: '已分配离线',
|
||||
unassignedOnline: '未分配在线',
|
||||
unused: '空闲',
|
||||
allStatus: '全部状态',
|
||||
editAssignAddress: '编辑分配地址',
|
||||
assign: '分配',
|
||||
recycle: '回收',
|
||||
assignStatus: '分配状态',
|
||||
reserved: '预留',
|
||||
assigned: '已分配',
|
||||
recycleTip: '确认要回收吗?回收后该网段状态变更为:未分配',
|
||||
recycleSuccess: '{ip} 回收成功,状态变更为: 未分配',
|
||||
operationLog: '操作记录',
|
||||
scanLog: '扫描记录',
|
||||
updateCatalog: '更新目录',
|
||||
deleteCatalog: '删除目录',
|
||||
updateSubnet: '修改子网',
|
||||
deleteSubnet: '删除子网',
|
||||
revokeAddress: '地址回收',
|
||||
operateTime: '操作时间',
|
||||
operateUser: '操作人',
|
||||
operateType: '操作类型',
|
||||
subnet: '子网',
|
||||
description: '描述',
|
||||
ipNumber: '在线IP地址数',
|
||||
startTime: '开始时间',
|
||||
endTime: '结束时间',
|
||||
scanningTime: '扫描耗时',
|
||||
viewResult: '查看结果',
|
||||
scannedIP: '已扫描的IP',
|
||||
subnetStats: '子网统计',
|
||||
addressStats: '地址数统计',
|
||||
onlineStats: '在线统计',
|
||||
assignStats: '分配统计',
|
||||
total: '总数',
|
||||
free: '空闲',
|
||||
unassigned: '未分配',
|
||||
online: '在线',
|
||||
offline: '离线',
|
||||
onlineUsageStats: '子网在线统计',
|
||||
subnetName: '子网名称',
|
||||
addressCount: '地址数',
|
||||
onlineRatio: '在线率',
|
||||
scanEnable: '是否扫描',
|
||||
lastScanTime: '最后扫描时间',
|
||||
isSuccess: '是否成功'
|
||||
}
|
||||
}
|
||||
export default cmdb_zh
|
||||
|
@@ -53,8 +53,9 @@ const genCmdbRoutes = async () => {
|
||||
{
|
||||
path: '/cmdb/resourcesearch',
|
||||
name: 'cmdb_resource_search',
|
||||
hidden: true,
|
||||
meta: { title: 'cmdb.menu.ciSearch', icon: 'ops-cmdb-search', selectedIcon: 'ops-cmdb-search', keepAlive: false },
|
||||
component: () => import('../views/resource_search/index.vue')
|
||||
component: () => import('../views/resource_search_2/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/cmdb/adc',
|
||||
@@ -69,6 +70,17 @@ const genCmdbRoutes = async () => {
|
||||
meta: { title: 'cmdb.menu.cidetail', keepAlive: false },
|
||||
component: () => import('../views/ci/ciDetailPage.vue')
|
||||
},
|
||||
{
|
||||
path: '/cmdb/disabled4',
|
||||
name: 'cmdb_disabled4',
|
||||
meta: { title: 'cmdb.menu.scene', appName: 'cmdb', disabled: true, permission: ['admin', 'cmdb_admin'] },
|
||||
},
|
||||
{
|
||||
path: '/cmdb/ipam',
|
||||
component: () => import('../views/ipam'),
|
||||
name: 'cmdb_ipam',
|
||||
meta: { title: 'IPAM', appName: 'cmdb', icon: 'veops-ipam', selectedIcon: 'veops-ipam', keepAlive: false, permission: ['admin', 'cmdb_admin'] }
|
||||
},
|
||||
{
|
||||
path: '/cmdb/disabled2',
|
||||
name: 'cmdb_disabled2',
|
||||
@@ -102,7 +114,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 +129,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 +141,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' }
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
@@ -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'),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,3 +33,8 @@ export const defautValueColor = [
|
||||
]
|
||||
|
||||
export const defaultBGColors = ['#ffccc7', '#ffd8bf', '#ffe7ba', '#fff1b8', '#d9f7be', '#b5f5ec', '#bae7ff', '#d6e4ff', '#efdbff', '#ffd6e7']
|
||||
|
||||
export const CI_DEFAULT_ATTR = {
|
||||
UPDATE_USER: '_updated_by',
|
||||
UPDATE_TIME: '_updated_at'
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user