Compare commits

..

54 Commits

Author SHA1 Message Date
pycook
c1813f525d chore: update docker compose 2024-06-27 21:33:19 +08:00
pycook
b405e28498 chore: release v2.4.7 2024-06-27 21:30:02 +08:00
pycook
fa32758462 perf(api): CIType templates download (#567) 2024-06-27 20:54:38 +08:00
Leo Song
29995b660a Merge pull request #566 from veops/dev_ui_0627
feat: update model config
2024-06-27 19:41:58 +08:00
songlh
b96fc06a62 feat: update model config 2024-06-27 19:41:24 +08:00
Leo Song
c7f30b63ff Merge pull request #565 from veops/dev_ui_0625
fix(ui): auto discovery
2024-06-25 17:36:00 +08:00
songlh
5bff69a8a8 fix(ui): auto discovery 2024-06-25 17:35:29 +08:00
pycook
d5e60fab88 chore: update ui dockerfile 2024-06-24 21:40:38 +08:00
Jared Tan
190f452118 chore: fix UI docker build and makes UI/API docker build parallel execution (#563)
* try fix UI docker build

* parallel execution

* polish

* polish

* polish

* update
2024-06-24 21:00:57 +08:00
Leo Song
98a4824364 Merge pull request #564 from veops/dev_ui_ad_0624
feat: update ad ui
2024-06-24 14:26:33 +08:00
songlh
c0f9baea79 feat: update ad ui 2024-06-24 14:25:56 +08:00
pycook
d4b661c77f fix(api): commands cmdb-patch 2024-06-21 18:22:56 +08:00
pycook
75cd7bde77 fix(api): auto discovery permission 2024-06-21 12:47:12 +08:00
Leo Song
ec912d3a65 Merge pull request #562 from veops/fix_ui_2.4.6
fix(ui): some bugs
2024-06-21 11:49:53 +08:00
songlh
42f02b4986 fix(ui): some bugs 2024-06-21 11:49:12 +08:00
simontigers
a13b999820 Merge pull request #561 from veops/dev_common_perm
fix(api): auto_discovery add new perms
2024-06-21 10:25:35 +08:00
simontigers
5f53b0dd0e fix(api): auto_discovery add new perms 2024-06-21 10:25:13 +08:00
Leo Song
df22085ff9 Merge pull request #560 from veops/fix_ui_topology
fix(ui): topology view error
2024-06-20 22:21:27 +08:00
LH_R
06148b402d fix(ui): topology view error 2024-06-20 22:20:25 +08:00
pycook
3fe020505a chore: release v2.4.6 2024-06-20 20:31:10 +08:00
pycook
b34e83124f perf(api): auto discovery has been upgraded (#559) 2024-06-20 20:30:04 +08:00
Leo Song
cdc52d3f80 Merge pull request #558 from veops/dev_ui_ad
fix: build error
2024-06-20 20:03:40 +08:00
LH_R
b3a80d5678 fix: build error 2024-06-20 19:54:15 +08:00
Leo Song
a2e3061bba Merge pull request #557 from veops/dev_ui_ad
feat(ui): auto discovery
2024-06-20 17:29:06 +08:00
songlh
a8eb5126ea feat(ui): auto discovery 2024-06-20 17:28:09 +08:00
pycook
adac2129fc chore: update Dockerfile-UI 2024-06-20 13:20:42 +08:00
pycook
e660c901ce chore: update Dockerfile-UI 2024-06-20 11:07:57 +08:00
pycook
ff002c0a1e chore: update Dockerfile-UI 2024-06-20 09:47:56 +08:00
Leo Song
88593d6da7 Merge pull request #555 from veops/fix_ui_lint
fix(ui): lint error
2024-06-18 11:42:53 +08:00
songlh
6fa0dd5bc5 fix(ui): lint error 2024-06-18 11:42:22 +08:00
Jared Tan
3200942373 polish ci and remove es build (#553) 2024-06-18 10:31:33 +08:00
pycook
4fd705cc59 feat(api): add table c_ad_ci_type_relations 2024-06-18 10:22:04 +08:00
Jared Tan
74827ce187 add workflow (#552) 2024-06-18 09:29:00 +08:00
Leo Song
4ed1eb6062 Merge pull request #551 from veops/fix_bug_538
fix: issue #538
2024-06-17 14:41:51 +08:00
songlh
7792204658 fix: issue #538 2024-06-17 14:41:24 +08:00
Leo Song
8621108906 Merge pull request #550 from veops/fix_bug_operation_history
fix: operation history table
2024-06-14 17:27:49 +08:00
songlh
6437af19b9 fix: operation history table 2024-06-14 17:27:13 +08:00
Leo Song
735ddb334c Merge pull request #542 from veops/fix_issue_540
fix: issue #540
2024-06-12 15:00:08 +08:00
songlh
4a8032202e fix: issue #540 2024-06-12 14:59:14 +08:00
Leo Song
c7acea6422 Merge pull request #539 from veops/fix_computed_code
fix: computed code area tab
2024-06-11 15:03:20 +08:00
songlh
ac4c93de8e fix: computed code area tab 2024-06-11 15:02:37 +08:00
pycook
8d044cf935 chore(docker compose): add api health check 2024-06-09 20:58:27 +08:00
pycook
54747fa789 feat(ui): update iconfont 2024-06-07 10:41:26 +08:00
pycook
545f1bb30b Dev dynamic attribute (#535)
* feat: dynamic attribute

* feat(api): dynamic attribute
2024-06-07 10:39:40 +08:00
pycook
dc77bca17c feat: dynamic attribute (#534) 2024-06-07 10:29:32 +08:00
Leo Song
4973278c5a Merge pull request #532 from veops/fix_bug_530
fix: ci topo expand error
2024-06-06 14:06:26 +08:00
songlh
d1c9361e47 fix: ci topo expand error 2024-06-06 14:05:32 +08:00
Leo Song
28c57cacd9 Merge pull request #531 from veops/dev_ui_240606
feat: update topology view
2024-06-06 11:10:35 +08:00
songlh
711dcc4bd7 feat: update topology view 2024-06-06 11:08:58 +08:00
simontigers
491d3cce00 Merge pull request #529 from veops/fix_decorator_perms_role_required
fix: decorator_perms_role_required
2024-06-04 19:23:58 +08:00
simontigers
27354a3927 fix: decorator_perms_role_required 2024-06-04 19:23:22 +08:00
Leo Song
78495eb976 Merge pull request #528 from veops/feat/dev_ui_240604
feat(ui): update model relation
2024-06-04 12:05:42 +08:00
songlh
ae900c7d3b feat(ui): update model relation 2024-06-04 12:04:26 +08:00
pycook
50134e6a0b feat(api): attribute association supports multiple groups (#527) 2024-06-04 11:34:54 +08:00
92 changed files with 13439 additions and 5938 deletions

0
.github/config.yml vendored
View File

View File

@@ -0,0 +1,79 @@
name: docker-images-build-and-release
on:
push:
branches:
- master
tags: ["v*"]
pull_request:
branches:
- master
env:
# Use docker.io for Docker Hub if empty
REGISTRY_SERVER_ADDRESS: ghcr.io/veops
TAG: ${{ github.sha }}
jobs:
setup-environment:
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v4
release-api-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-API Docker image
uses: docker/build-push-action@v6
with:
file: docker/Dockerfile-API
context: .
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 }}

View File

@@ -1,6 +1,4 @@
MYSQL_ROOT_PASSWORD ?= root include ./Makefile.variable
MYSQL_PORT ?= 3306
REDIS_PORT ?= 6379
default: help default: help
help: ## display this help help: ## display this help
@@ -50,3 +48,25 @@ clean: ## remove unwanted files like .pyc's
lint: ## check style with flake8 lint: ## check style with flake8
flake8 --exclude=env . flake8 --exclude=env .
.PHONY: lint .PHONY: lint
api-docker-build:
export DOCKER_CLI_EXPERIMENTAL=enabled ;\
! ( docker buildx ls | grep multi-platform-builder ) && docker buildx create --use --platform=$(BUILD_ARCH) --name multi-platform-builder ;\
docker buildx build \
--builder multi-platform-builder \
--platform=$(BUILD_ARCH) \
--tag $(REGISTRY)/cmdb-api:$(CMDB_DOCKER_VERSION) \
--tag $(REGISTRY)/cmdb-api:latest \
-f docker/Dockerfile-API \
.
ui-docker-build:
export DOCKER_CLI_EXPERIMENTAL=enabled ;\
! ( docker buildx ls | grep multi-platform-builder ) && docker buildx create --use --platform=$(BUILD_ARCH) --name multi-platform-builder ;\
docker buildx build \
--builder multi-platform-builder \
--platform=$(BUILD_ARCH) \
--tag $(REGISTRY)/cmdb-ui:$(CMDB_DOCKER_VERSION) \
--tag $(REGISTRY)/cmdb-ui:latest \
-f docker/Dockerfile-UI \
.

21
Makefile.variable Normal file
View File

@@ -0,0 +1,21 @@
SHELL := /bin/bash -o pipefail
MYSQL_ROOT_PASSWORD ?= root
MYSQL_PORT ?= 3306
REDIS_PORT ?= 6379
LATEST_TAG_DIFF:=$(shell git describe --tags --abbrev=8)
LATEST_COMMIT:=$(VERSION)-dev-$(shell git rev-parse --short=8 HEAD)
BUILD_ARCH ?= linux/amd64,linux/arm64
# Set your version by env or using latest tags from git
CMDB_VERSION?=$(LATEST_TAG_DIFF)
ifeq ($(CMDB_VERSION),)
#fall back to last commit
CMDB_VERSION=$(LATEST_COMMIT)
endif
COMMIT_VERSION:=$(LATEST_COMMIT)
CMDB_DOCKER_VERSION:=${CMDB_VERSION}
CMDB_CHART_VERSION:=$(shell echo ${CMDB_VERSION} | sed 's/^v//g' )
REGISTRY ?= local

View File

@@ -190,6 +190,7 @@ def cmdb_counter():
login_user(UserCache.get('worker')) login_user(UserCache.get('worker'))
i = 0 i = 0
today = datetime.date.today()
while True: while True:
try: try:
db.session.remove() db.session.remove()
@@ -200,6 +201,10 @@ def cmdb_counter():
CMDBCounterCache.flush_adc_counter() CMDBCounterCache.flush_adc_counter()
i = 0 i = 0
if datetime.date.today() != today:
CMDBCounterCache.clear_ad_exec_history()
today = datetime.date.today()
CMDBCounterCache.flush_sub_counter() CMDBCounterCache.flush_sub_counter()
i += 1 i += 1
@@ -493,3 +498,64 @@ def cmdb_agent_init():
click.echo("Key : {}".format(click.style(user.key, bg='red'))) click.echo("Key : {}".format(click.style(user.key, bg='red')))
click.echo("Secret: {}".format(click.style(user.secret, bg='red'))) click.echo("Secret: {}".format(click.style(user.secret, bg='red')))
@click.command()
@click.option(
'-v',
'--version',
help='input cmdb version, e.g. 2.4.6',
required=True,
)
@with_appcontext
def cmdb_patch(version):
"""
CMDB upgrade patch
"""
version = version[1:] if version.lower().startswith("v") else version
try:
if version >= '2.4.6':
from api.models.cmdb import CITypeRelation
for cr in CITypeRelation.get_by(to_dict=False):
if hasattr(cr, 'parent_attr_id') and cr.parent_attr_id and not cr.parent_attr_ids:
parent_attr_ids, child_attr_ids = [cr.parent_attr_id], [cr.child_attr_id]
cr.update(parent_attr_ids=parent_attr_ids, child_attr_ids=child_attr_ids, commit=False)
db.session.commit()
from api.models.cmdb import AutoDiscoveryCIType, AutoDiscoveryCITypeRelation
from api.lib.cmdb.cache import CITypeCache, AttributeCache
for adt in AutoDiscoveryCIType.get_by(to_dict=False):
if adt.relation:
if not AutoDiscoveryCITypeRelation.get_by(ad_type_id=adt.type_id):
peer_type = CITypeCache.get(list(adt.relation.values())[0]['type_name'])
peer_type_id = peer_type and peer_type.id
peer_attr = AttributeCache.get(list(adt.relation.values())[0]['attr_name'])
peer_attr_id = peer_attr and peer_attr.id
if peer_type_id and peer_attr_id:
AutoDiscoveryCITypeRelation.create(ad_type_id=adt.type_id,
ad_key=list(adt.relation.keys())[0],
peer_type_id=peer_type_id,
peer_attr_id=peer_attr_id,
commit=False)
if hasattr(adt, 'interval') and adt.interval and not adt.cron:
adt.cron = "*/{} * * * *".format(adt.interval // 60 or 1)
db.session.commit()
if version >= "2.4.7":
from api.lib.cmdb.auto_discovery.const import DEFAULT_INNER
from api.models.cmdb import AutoDiscoveryRule
for i in DEFAULT_INNER:
existed = AutoDiscoveryRule.get_by(name=i['name'], first=True, to_dict=False)
if existed is not None:
if "en" in i['option'] and 'en' not in (existed.option or {}):
option = copy.deepcopy(existed.option)
option['en'] = i['option']['en']
existed.update(option=option, commit=False)
db.session.commit()
except Exception as e:
print("cmdb patch failed: {}".format(e))

View File

@@ -1,34 +1,48 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
import copy
import datetime import datetime
import json import json
import os import os
from flask import abort
from flask import current_app
from flask_login import current_user
from sqlalchemy import func
from api.extensions import db from api.extensions import db
from api.lib.cmdb.auto_discovery.const import ClOUD_MAP from api.lib.cmdb.auto_discovery.const import ClOUD_MAP
from api.lib.cmdb.auto_discovery.const import DEFAULT_INNER
from api.lib.cmdb.auto_discovery.const import PRIVILEGED_USERS
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeAttributeCache from api.lib.cmdb.cache import CITypeAttributeCache
from api.lib.cmdb.cache import CITypeCache from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIManager from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.ci import CIRelationManager from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.ci_type import CITypeGroupManager from api.lib.cmdb.ci_type import CITypeGroupManager
from api.lib.cmdb.const import AutoDiscoveryType from api.lib.cmdb.const import AutoDiscoveryType
from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.cmdb.const import PermEnum from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.custom_dashboard import SystemConfigManager
from api.lib.cmdb.resp_format import ErrFormat from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.search import SearchError 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.common_setting.role_perm_base import CMDBApp
from api.lib.mixin import DBMixin from api.lib.mixin import DBMixin
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 is_app_admin
from api.lib.perm.acl.acl import validate_permission from api.lib.perm.acl.acl import validate_permission
from api.lib.utils import AESCrypto from api.lib.utils import AESCrypto
from api.models.cmdb import AutoDiscoveryCI from api.models.cmdb import AutoDiscoveryCI
from api.models.cmdb import AutoDiscoveryCIType from api.models.cmdb import AutoDiscoveryCIType
from api.models.cmdb import AutoDiscoveryCITypeRelation
from api.models.cmdb import AutoDiscoveryCounter
from api.models.cmdb import AutoDiscoveryExecHistory
from api.models.cmdb import AutoDiscoveryRule from api.models.cmdb import AutoDiscoveryRule
from flask import abort from api.models.cmdb import AutoDiscoveryRuleSyncHistory
from flask import current_app from api.tasks.cmdb import write_ad_rule_sync_history
from flask_login import current_user
from sqlalchemy import func
PWD = os.path.abspath(os.path.dirname(__file__)) PWD = os.path.abspath(os.path.dirname(__file__))
app_cli = CMDBApp()
def parse_plugin_script(script): def parse_plugin_script(script):
@@ -96,14 +110,30 @@ class AutoDiscoveryRuleCRUD(DBMixin):
else: else:
self.cls.create(**rule) self.cls.create(**rule)
def _can_add(self, **kwargs): def _can_add(self, valid=True, **kwargs):
self.cls.get_by(name=kwargs['name']) and abort(400, ErrFormat.adr_duplicate.format(kwargs['name'])) self.cls.get_by(name=kwargs['name']) and abort(400, ErrFormat.adr_duplicate.format(kwargs['name']))
if kwargs.get('is_plugin') and kwargs.get('plugin_script'): if kwargs.get('is_plugin') and kwargs.get('plugin_script') and valid:
kwargs = check_plugin_script(**kwargs) kwargs = check_plugin_script(**kwargs)
acl = ACLManager(app_cli.app_name)
has_perm = True
try:
if not acl.has_permission(app_cli.op.Auto_Discovery,
app_cli.resource_type_name,
app_cli.op.create_plugin) and not is_app_admin(app_cli.app_name):
has_perm = False
except Exception:
if not is_app_admin(app_cli.app_name):
return abort(403, ErrFormat.role_required.format(app_cli.admin_name))
if not has_perm:
return abort(403, ErrFormat.no_permission.format(
app_cli.op.Auto_Discovery, app_cli.op.create_plugin))
kwargs['owner'] = current_user.uid
return kwargs return kwargs
def _can_update(self, **kwargs): def _can_update(self, valid=True, **kwargs):
existed = self.cls.get_by_id(kwargs['_id']) or abort( existed = self.cls.get_by_id(kwargs['_id']) or abort(
404, ErrFormat.adr_not_found.format("id={}".format(kwargs['_id']))) 404, ErrFormat.adr_not_found.format("id={}".format(kwargs['_id'])))
@@ -115,6 +145,22 @@ class AutoDiscoveryRuleCRUD(DBMixin):
if other and other.id != existed.id: if other and other.id != existed.id:
return abort(400, ErrFormat.adr_duplicate.format(kwargs['name'])) return abort(400, ErrFormat.adr_duplicate.format(kwargs['name']))
if existed.is_plugin and valid:
acl = ACLManager(app_cli.app_name)
has_perm = True
try:
if not acl.has_permission(app_cli.op.Auto_Discovery,
app_cli.resource_type_name,
app_cli.op.update_plugin) and not is_app_admin(app_cli.app_name):
has_perm = False
except Exception:
if not is_app_admin(app_cli.app_name):
return abort(403, ErrFormat.role_required.format(app_cli.admin_name))
if not has_perm:
return abort(403, ErrFormat.no_permission.format(
app_cli.op.Auto_Discovery, app_cli.op.update_plugin))
return existed return existed
def update(self, _id, **kwargs): def update(self, _id, **kwargs):
@@ -122,21 +168,44 @@ class AutoDiscoveryRuleCRUD(DBMixin):
if kwargs.get('is_plugin') and kwargs.get('plugin_script'): if kwargs.get('is_plugin') and kwargs.get('plugin_script'):
kwargs = check_plugin_script(**kwargs) kwargs = check_plugin_script(**kwargs)
for item in AutoDiscoveryCIType.get_by(adr_id=_id, to_dict=False):
item.update(updated_at=datetime.datetime.now())
return super(AutoDiscoveryRuleCRUD, self).update(_id, filter_none=False, **kwargs) return super(AutoDiscoveryRuleCRUD, self).update(_id, filter_none=False, **kwargs)
def _can_delete(self, **kwargs): def _can_delete(self, **kwargs):
if AutoDiscoveryCIType.get_by(adr_id=kwargs['_id'], first=True): if AutoDiscoveryCIType.get_by(adr_id=kwargs['_id'], first=True):
return abort(400, ErrFormat.adr_referenced) return abort(400, ErrFormat.adr_referenced)
return self._can_update(**kwargs) existed = self.cls.get_by_id(kwargs['_id']) or abort(
404, ErrFormat.adr_not_found.format("id={}".format(kwargs['_id'])))
if existed.is_plugin:
acl = ACLManager(app_cli.app_name)
has_perm = True
try:
if not acl.has_permission(app_cli.op.Auto_Discovery,
app_cli.resource_type_name,
app_cli.op.delete_plugin) and not is_app_admin(app_cli.app_name):
has_perm = False
except Exception:
if not is_app_admin(app_cli.app_name):
return abort(403, ErrFormat.role_required.format(app_cli.admin_name))
if not has_perm:
return abort(403, ErrFormat.no_permission.format(
app_cli.op.Auto_Discovery, app_cli.op.delete_plugin))
return existed
class AutoDiscoveryCITypeCRUD(DBMixin): class AutoDiscoveryCITypeCRUD(DBMixin):
cls = AutoDiscoveryCIType cls = AutoDiscoveryCIType
@classmethod @classmethod
def get_all(cls): def get_all(cls, type_ids=None):
return cls.cls.get_by(to_dict=False) res = cls.cls.get_by(to_dict=False)
return [i for i in res if type_ids is None or i.type_id in type_ids]
@classmethod @classmethod
def get_by_id(cls, _id): def get_by_id(cls, _id):
@@ -147,25 +216,51 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
return cls.cls.get_by(type_id=type_id, to_dict=False) return cls.cls.get_by(type_id=type_id, to_dict=False)
@classmethod @classmethod
def get(cls, ci_id, oneagent_id, last_update_at=None): def get_ad_attributes(cls, type_id):
result = []
adts = cls.get_by_type_id(type_id)
for adt in adts:
adr = AutoDiscoveryRuleCRUD.get_by_id(adt.adr_id)
if not adr:
continue
if adr.type == "http":
for i in DEFAULT_INNER:
if adr.name == i['name']:
attrs = AutoDiscoveryHTTPManager.get_attributes(
i['en'], (adt.extra_option or {}).get('category')) or []
result.extend([i.get('name') for i in attrs])
break
elif adr.type == "snmp":
attributes = AutoDiscoverySNMPManager.get_attributes()
result.extend([i.get('name') for i in (attributes or [])])
else:
result.extend([i.get('name') for i in (adr.attributes or [])])
return sorted(list(set(result)))
@classmethod
def get(cls, ci_id, oneagent_id, oneagent_name, last_update_at=None):
result = [] result = []
rules = cls.cls.get_by(to_dict=True) rules = cls.cls.get_by(to_dict=True)
for rule in rules: for rule in rules:
if rule.get('relation'):
continue
if isinstance(rule.get("extra_option"), dict) and rule['extra_option'].get('secret'): if isinstance(rule.get("extra_option"), dict) and rule['extra_option'].get('secret'):
if not (current_user.username == "cmdb_agent" or current_user.uid == rule['uid']): if not (current_user.username in PRIVILEGED_USERS or current_user.uid == rule['uid']):
rule['extra_option'].pop('secret', None) rule['extra_option'].pop('secret', None)
else: else:
rule['extra_option']['secret'] = AESCrypto.decrypt(rule['extra_option']['secret']) rule['extra_option']['secret'] = AESCrypto.decrypt(rule['extra_option']['secret'])
if isinstance(rule.get("extra_option"), dict) and rule['extra_option'].get('password'):
if not (current_user.username in PRIVILEGED_USERS or current_user.uid == rule['uid']):
rule['extra_option'].pop('password', None)
else:
rule['extra_option']['password'] = AESCrypto.decrypt(rule['extra_option']['password'])
if oneagent_id and rule['agent_id'] == oneagent_id: if oneagent_id and rule['agent_id'] == oneagent_id:
result.append(rule) result.append(rule)
elif rule['query_expr']: elif rule['query_expr']:
query = rule['query_expr'].lstrip('q').lstrip('=') query = rule['query_expr'].lstrip('q').lstrip('=')
s = search(query, fl=['_id'], count=1000000) s = ci_search(query, fl=['_id'], count=1000000)
try: try:
response, _, _, _, _, _ = s.search() response, _, _, _, _, _ = s.search()
except SearchError as e: except SearchError as e:
@@ -182,19 +277,21 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
if adr.type in (AutoDiscoveryType.SNMP, AutoDiscoveryType.HTTP): if adr.type in (AutoDiscoveryType.SNMP, AutoDiscoveryType.HTTP):
continue continue
if not rule['updated_at']:
continue
result.append(rule) result.append(rule)
ad_rules_updated_at = (SystemConfigManager.get('ad_rules_updated_at') or {}).get('option', {}).get('v') or ""
new_last_update_at = "" new_last_update_at = ""
for i in result: for i in result:
i['adr'] = AutoDiscoveryRule.get_by_id(i['adr_id']).to_dict() i['adr'] = AutoDiscoveryRule.get_by_id(i['adr_id']).to_dict()
i['adr'].pop("attributes", None)
__last_update_at = max([i['updated_at'] or "", i['created_at'] or "", __last_update_at = max([i['updated_at'] or "", i['created_at'] or "",
i['adr']['created_at'] or "", i['adr']['updated_at'] or ""]) i['adr']['created_at'] or "", i['adr']['updated_at'] or "", ad_rules_updated_at])
if new_last_update_at < __last_update_at: if new_last_update_at < __last_update_at:
new_last_update_at = __last_update_at new_last_update_at = __last_update_at
write_ad_rule_sync_history.apply_async(args=(result, oneagent_id, oneagent_name, datetime.datetime.now()),
queue=CMDB_QUEUE)
if not last_update_at or 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 return result, new_last_update_at
else: else:
@@ -213,7 +310,7 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
agent_id = agent_id.strip() agent_id = agent_id.strip()
q = "op_duty:{0},-rd_duty:{0},oneagent_id:{1}" q = "op_duty:{0},-rd_duty:{0},oneagent_id:{1}"
s = search(q.format(current_user.username, agent_id.strip())) s = ci_search(q.format(current_user.username, agent_id.strip()))
try: try:
response, _, _, _, _, _ = s.search() response, _, _, _, _, _ = s.search()
if response: if response:
@@ -222,7 +319,7 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
current_app.logger.warning(e) current_app.logger.warning(e)
return abort(400, str(e)) return abort(400, str(e))
s = search(q.format(current_user.nickname, agent_id.strip())) s = ci_search(q.format(current_user.nickname, agent_id.strip()))
try: try:
response, _, _, _, _, _ = s.search() response, _, _, _, _, _ = s.search()
if response: if response:
@@ -236,7 +333,7 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
if query_expr.startswith('q='): if query_expr.startswith('q='):
query_expr = query_expr[2:] query_expr = query_expr[2:]
s = search(query_expr, count=1000000) s = ci_search(query_expr, count=1000000)
try: try:
response, _, _, _, _, _ = s.search() response, _, _, _, _, _ = s.search()
for i in response: for i in response:
@@ -254,19 +351,35 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
def _can_add(**kwargs): def _can_add(**kwargs):
if kwargs.get('adr_id'): if kwargs.get('adr_id'):
AutoDiscoveryRule.get_by_id(kwargs['adr_id']) or abort( adr = AutoDiscoveryRule.get_by_id(kwargs['adr_id']) or abort(
404, ErrFormat.adr_not_found.format("id={}".format(kwargs['adr_id']))) 404, ErrFormat.adr_not_found.format("id={}".format(kwargs['adr_id'])))
# if not adr.is_plugin: if adr.type == "http":
# other = self.cls.get_by(adr_id=adr.id, first=True, to_dict=False) kwargs.setdefault('extra_option', dict)
# if other: en_name = None
# ci_type = CITypeCache.get(other.type_id) for i in DEFAULT_INNER:
# return abort(400, ErrFormat.adr_default_ref_once.format(ci_type.alias)) if i['name'] == adr.name:
en_name = i['en']
break
if en_name and kwargs['extra_option'].get('category'):
for item in ClOUD_MAP[en_name]:
if item["collect_key_map"].get(kwargs['extra_option']['category']):
kwargs["extra_option"]["collect_key"] = item["collect_key_map"][
kwargs['extra_option']['category']]
break
if kwargs.get('is_plugin') and kwargs.get('plugin_script'): if kwargs.get('is_plugin') and kwargs.get('plugin_script'):
kwargs = check_plugin_script(**kwargs) kwargs = check_plugin_script(**kwargs)
if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('secret'): if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('secret'):
kwargs['extra_option']['secret'] = AESCrypto.encrypt(kwargs['extra_option']['secret']) kwargs['extra_option']['secret'] = AESCrypto.encrypt(kwargs['extra_option']['secret'])
if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('password'):
kwargs['extra_option']['password'] = AESCrypto.encrypt(kwargs['extra_option']['password'])
ci_type = CITypeCache.get(kwargs['type_id'])
unique = AttributeCache.get(ci_type.unique_id)
if unique and unique.name not in (kwargs.get('attributes') or {}).values():
current_app.logger.warning((unique.name, kwargs.get('attributes'), ci_type.alias))
return abort(400, ErrFormat.ad_not_unique_key.format(unique.name))
kwargs['uid'] = current_user.uid kwargs['uid'] = current_user.uid
@@ -276,11 +389,37 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
existed = self.cls.get_by_id(kwargs['_id']) or abort( existed = self.cls.get_by_id(kwargs['_id']) or abort(
404, ErrFormat.ad_not_found.format("id={}".format(kwargs['_id']))) 404, ErrFormat.ad_not_found.format("id={}".format(kwargs['_id'])))
self.__valid_exec_target(kwargs.get('agent_id'), kwargs.get('query_expr')) adr = AutoDiscoveryRule.get_by_id(existed.adr_id) or abort(
404, ErrFormat.adr_not_found.format("id={}".format(existed.adr_id)))
if adr.type == "http":
kwargs.setdefault('extra_option', dict)
en_name = None
for i in DEFAULT_INNER:
if i['name'] == adr.name:
en_name = i['en']
break
if en_name and kwargs['extra_option'].get('category'):
for item in ClOUD_MAP[en_name]:
if item["collect_key_map"].get(kwargs['extra_option']['category']):
kwargs["extra_option"]["collect_key"] = item["collect_key_map"][
kwargs['extra_option']['category']]
break
if 'attributes' in kwargs:
self.__valid_exec_target(kwargs.get('agent_id'), kwargs.get('query_expr'))
ci_type = CITypeCache.get(existed.type_id)
unique = AttributeCache.get(ci_type.unique_id)
if unique and unique.name not in (kwargs.get('attributes') or {}).values():
current_app.logger.warning((unique.name, kwargs.get('attributes'), ci_type.alias))
return abort(400, ErrFormat.ad_not_unique_key.format(unique.name))
if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('secret'): if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('secret'):
if current_user.uid != existed.uid: if current_user.uid != existed.uid:
return abort(403, ErrFormat.adt_secret_no_permission) return abort(403, ErrFormat.adt_secret_no_permission)
if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('password'):
if current_user.uid != existed.uid:
return abort(403, ErrFormat.adt_secret_no_permission)
return existed return existed
@@ -291,8 +430,21 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('secret'): if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('secret'):
kwargs['extra_option']['secret'] = AESCrypto.encrypt(kwargs['extra_option']['secret']) kwargs['extra_option']['secret'] = AESCrypto.encrypt(kwargs['extra_option']['secret'])
if isinstance(kwargs.get('extra_option'), dict) and kwargs['extra_option'].get('password'):
kwargs['extra_option']['password'] = AESCrypto.encrypt(kwargs['extra_option']['password'])
return super(AutoDiscoveryCITypeCRUD, self).update(_id, filter_none=False, **kwargs) inst = self._can_update(_id=_id, **kwargs)
if inst.agent_id != kwargs.get('agent_id') or inst.query_expr != kwargs.get('query_expr'):
for item in AutoDiscoveryRuleSyncHistory.get_by(adt_id=inst.id, to_dict=False):
item.delete(commit=False)
db.session.commit()
SystemConfigManager.create_or_update("ad_rules_updated_at",
dict(v=datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")))
obj = inst.update(_id=_id, filter_none=False, **kwargs)
return obj
def _can_delete(self, **kwargs): def _can_delete(self, **kwargs):
if AutoDiscoveryCICRUD.get_by_adt_id(kwargs['_id']): if AutoDiscoveryCICRUD.get_by_adt_id(kwargs['_id']):
@@ -303,6 +455,60 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
return existed return existed
def delete(self, _id):
inst = self._can_delete(_id=_id)
inst.soft_delete()
for item in AutoDiscoveryRuleSyncHistory.get_by(adt_id=inst.id, to_dict=False):
item.delete(commit=False)
db.session.commit()
attributes = self.get_ad_attributes(inst.type_id)
for item in AutoDiscoveryCITypeRelationCRUD.get_by_type_id(inst.type_id):
if item.ad_key not in attributes:
item.soft_delete()
return inst
class AutoDiscoveryCITypeRelationCRUD(DBMixin):
cls = AutoDiscoveryCITypeRelation
@classmethod
def get_all(cls, type_ids=None):
res = cls.cls.get_by(to_dict=False)
return [i for i in res if type_ids is None or i.ad_type_id in type_ids]
@classmethod
def get_by_type_id(cls, type_id, to_dict=False):
return cls.cls.get_by(ad_type_id=type_id, to_dict=to_dict)
def upsert(self, ad_type_id, relations):
existed = self.cls.get_by(ad_type_id=ad_type_id, to_dict=False)
existed = {(i.ad_key, i.peer_type_id, i.peer_attr_id): i for i in existed}
new = []
for r in relations:
k = (r.get('ad_key'), r.get('peer_type_id'), r.get('peer_attr_id'))
if len(list(filter(lambda x: x, k))) == 3 and k not in existed:
self.cls.create(ad_type_id=ad_type_id, **r)
new.append(k)
for deleted in set(existed.keys()) - set(new):
existed[deleted].soft_delete()
return self.get_by_type_id(ad_type_id, to_dict=True)
def _can_add(self, **kwargs):
pass
def _can_update(self, **kwargs):
pass
def _can_delete(self, **kwargs):
pass
class AutoDiscoveryCICRUD(DBMixin): class AutoDiscoveryCICRUD(DBMixin):
cls = AutoDiscoveryCI cls = AutoDiscoveryCI
@@ -391,16 +597,24 @@ class AutoDiscoveryCICRUD(DBMixin):
changed = False changed = False
if existed is not None: if existed is not None:
if existed.instance != kwargs['instance']: if existed.instance != kwargs['instance']:
instance = copy.deepcopy(existed.instance) or {}
instance.update(kwargs['instance'])
kwargs['instance'] = instance
existed.update(filter_none=False, **kwargs) existed.update(filter_none=False, **kwargs)
AutoDiscoveryExecHistoryCRUD().add(type_id=adt.type_id,
stdout="update resource: {}".format(kwargs.get('unique_value')))
changed = True changed = True
else: else:
existed = self.cls.create(**kwargs) existed = self.cls.create(**kwargs)
AutoDiscoveryExecHistoryCRUD().add(type_id=adt.type_id,
stdout="add resource: {}".format(kwargs.get('unique_value')))
changed = True changed = True
if adt.auto_accept and changed: if adt.auto_accept and changed:
try: try:
self.accept(existed) self.accept(existed)
except Exception as e: except Exception as e:
current_app.logger.error(e)
return abort(400, str(e)) return abort(400, str(e))
elif changed: elif changed:
existed.update(is_accept=False, accept_time=None, accept_by=None, filter_none=False) existed.update(is_accept=False, accept_time=None, accept_by=None, filter_none=False)
@@ -420,6 +634,13 @@ class AutoDiscoveryCICRUD(DBMixin):
inst.delete() inst.delete()
adt = AutoDiscoveryCIType.get_by_id(inst.adt_id)
if adt:
adt.update(updated_at=datetime.datetime.now())
AutoDiscoveryExecHistoryCRUD().add(type_id=inst.type_id,
stdout="delete resource: {}".format(inst.unique_value))
self._after_delete(inst) self._after_delete(inst)
return inst return inst
@@ -435,6 +656,13 @@ class AutoDiscoveryCICRUD(DBMixin):
not is_app_admin("cmdb") and validate_permission(ci_type.name, ResourceTypeEnum.CI, PermEnum.DELETE, "cmdb") not is_app_admin("cmdb") and validate_permission(ci_type.name, ResourceTypeEnum.CI, PermEnum.DELETE, "cmdb")
existed.delete() existed.delete()
adt = AutoDiscoveryCIType.get_by_id(existed.adt_id)
if adt:
adt.update(updated_at=datetime.datetime.now())
AutoDiscoveryExecHistoryCRUD().add(type_id=type_id,
stdout="delete resource: {}".format(unique_value))
# TODO: delete ci # TODO: delete ci
@classmethod @classmethod
@@ -447,32 +675,34 @@ class AutoDiscoveryCICRUD(DBMixin):
ci_id = None ci_id = None
if adt.attributes: if adt.attributes:
ci_dict = {adt.attributes[k]: v for k, v in adc.instance.items() if k in adt.attributes} ci_dict = {adt.attributes[k]: v for k, v in adc.instance.items() if k in adt.attributes}
ci_id = CIManager.add(adc.type_id, is_auto_discovery=True, **ci_dict) ci_id = CIManager.add(adc.type_id, is_auto_discovery=True, _is_admin=True, **ci_dict)
AutoDiscoveryExecHistoryCRUD().add(type_id=adt.type_id,
stdout="accept resource: {}".format(adc.unique_value))
relation_adts = AutoDiscoveryCIType.get_by(type_id=adt.type_id, adr_id=None, to_dict=False) relation_ads = AutoDiscoveryCITypeRelation.get_by(ad_type_id=adt.type_id, to_dict=False)
for r_adt in relation_adts: for r_adt in relation_ads:
if not r_adt.relation or ci_id is None: ad_key = r_adt.ad_key
if not adc.instance.get(ad_key):
continue continue
for ad_key in r_adt.relation:
if not adc.instance.get(ad_key): ad_key_values = [adc.instance.get(ad_key)] if not isinstance(
continue adc.instance.get(ad_key), list) else adc.instance.get(ad_key)
cmdb_key = r_adt.relation[ad_key] for ad_key_value in ad_key_values:
query = "_type:{},{}:{}".format(cmdb_key.get('type_name'), cmdb_key.get('attr_name'), query = "_type:{},{}:{}".format(r_adt.peer_type_id, r_adt.peer_attr_id, ad_key_value)
adc.instance.get(ad_key)) s = ci_search(query, use_ci_filter=False, count=1000000)
s = search(query)
try: try:
response, _, _, _, _, _ = s.search() response, _, _, _, _, _ = s.search()
except SearchError as e: except SearchError as e:
current_app.logger.warning(e) current_app.logger.warning(e)
return abort(400, str(e)) return abort(400, str(e))
relation_ci_id = response and response[0]['_id'] for relation_ci in response:
if relation_ci_id: relation_ci_id = relation_ci['_id']
try: try:
CIRelationManager.add(ci_id, relation_ci_id) CIRelationManager.add(ci_id, relation_ci_id, valid=False)
except: except:
try: try:
CIRelationManager.add(relation_ci_id, ci_id) CIRelationManager.add(relation_ci_id, ci_id, valid=False)
except: except:
pass pass
@@ -485,14 +715,36 @@ class AutoDiscoveryCICRUD(DBMixin):
class AutoDiscoveryHTTPManager(object): class AutoDiscoveryHTTPManager(object):
@staticmethod @staticmethod
def get_categories(name): def get_categories(name):
return (ClOUD_MAP.get(name) or {}).get('categories') or [] categories = (ClOUD_MAP.get(name) or {}) or []
for item in copy.deepcopy(categories):
item.pop('map', None)
item.pop('collect_key_map', None)
return categories
def get_resources(self, name):
en_name = None
for i in DEFAULT_INNER:
if i['name'] == name:
en_name = i['en']
break
if en_name:
categories = self.get_categories(en_name)
return [j for i in categories for j in i['items']]
return []
@staticmethod @staticmethod
def get_attributes(name, category): def get_attributes(provider, resource):
tpt = ((ClOUD_MAP.get(name) or {}).get('map') or {}).get(category) for item in (ClOUD_MAP.get(provider) or {}):
if tpt and os.path.exists(os.path.join(PWD, tpt)): for _resource in (item.get('map') or {}):
with open(os.path.join(PWD, tpt)) as f: if _resource == resource:
return json.loads(f.read()) tpt = item['map'][_resource]
if tpt and os.path.exists(os.path.join(PWD, tpt)):
with open(os.path.join(PWD, tpt)) as f:
return json.loads(f.read())
return [] return []
@@ -506,3 +758,73 @@ class AutoDiscoverySNMPManager(object):
return json.loads(f.read()) return json.loads(f.read())
return [] return []
class AutoDiscoveryComponentsManager(object):
@staticmethod
def get_attributes(name):
if os.path.exists(os.path.join(PWD, "templates/{}.json".format(name))):
with open(os.path.join(PWD, "templates/{}.json".format(name))) as f:
return json.loads(f.read())
return []
class AutoDiscoveryRuleSyncHistoryCRUD(DBMixin):
cls = AutoDiscoveryRuleSyncHistory
def _can_add(self, **kwargs):
pass
def _can_update(self, **kwargs):
pass
def _can_delete(self, **kwargs):
pass
def upsert(self, **kwargs):
existed = self.cls.get_by(adt_id=kwargs.get('adt_id'),
oneagent_id=kwargs.get('oneagent_id'),
oneagent_name=kwargs.get('oneagent_name'),
first=True,
to_dict=False)
if existed is not None:
existed.update(**kwargs)
else:
self.cls.create(**kwargs)
class AutoDiscoveryExecHistoryCRUD(DBMixin):
cls = AutoDiscoveryExecHistory
def _can_add(self, **kwargs):
pass
def _can_update(self, **kwargs):
pass
def _can_delete(self, **kwargs):
pass
class AutoDiscoveryCounterCRUD(DBMixin):
cls = AutoDiscoveryCounter
def get(self, type_id):
res = self.cls.get_by(type_id=type_id, first=True, to_dict=True)
if res is None:
return dict(rule_count=0, exec_target_count=0, instance_count=0, accept_count=0,
this_month_count=0, this_week_count=0, last_month_count=0, last_week_count=0)
return res
def _can_add(self, **kwargs):
pass
def _can_update(self, **kwargs):
pass
def _can_delete(self, **kwargs):
pass

View File

@@ -2,16 +2,28 @@
from api.lib.cmdb.const import AutoDiscoveryType from api.lib.cmdb.const import AutoDiscoveryType
DEFAULT_HTTP = [ PRIVILEGED_USERS = ("cmdb_agent", "worker", "admin")
dict(name="阿里云", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-aliyun'}}), DEFAULT_INNER = [
dict(name="腾讯云", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False, dict(name="阿里云", en="aliyun", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-tengxunyun'}}), option={'icon': {'name': 'caise-aliyun'}, "en": "aliyun"}),
dict(name="华为云", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False, dict(name="腾讯云", en="tencentcloud", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-huaweiyun'}}), option={'icon': {'name': 'caise-tengxunyun'}, "en": "tencentcloud"}),
dict(name="AWS", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False, dict(name="华为云", en="huaweicloud", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-huaweiyun'}, "en": "huaweicloud"}),
dict(name="AWS", en="aws", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-aws'}}), option={'icon': {'name': 'caise-aws'}}),
dict(name="VCenter", en="vcenter", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'cmdb-vcenter'}, "category": "private_cloud", "en": "vcenter"}),
dict(name="KVM", en="kvm", type=AutoDiscoveryType.HTTP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'ops-KVM'}, "category": "private_cloud", "en": "kvm"}),
dict(name="Nginx", en="nginx", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-nginx'}, "en": "nginx"}),
dict(name="Redis", en="redis", type=AutoDiscoveryType.COMPONENTS, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-redis'}, "en": "redis"}),
dict(name="交换机", type=AutoDiscoveryType.SNMP, is_inner=True, is_plugin=False, dict(name="交换机", type=AutoDiscoveryType.SNMP, is_inner=True, is_plugin=False,
option={'icon': {'name': 'caise-jiaohuanji'}}), option={'icon': {'name': 'caise-jiaohuanji'}}),
dict(name="路由器", type=AutoDiscoveryType.SNMP, is_inner=True, is_plugin=False, dict(name="路由器", type=AutoDiscoveryType.SNMP, is_inner=True, is_plugin=False,
@@ -23,31 +35,306 @@ DEFAULT_HTTP = [
] ]
ClOUD_MAP = { ClOUD_MAP = {
"aliyun": { "aliyun": [
"categories": ["云服务器 ECS"], {
"map": { "category": "计算",
"云服务器 ECS": "templates/aliyun_ecs.json", "items": ["云服务器 ECS", "云服务器 Disk"],
} "map": {
}, "云服务器 ECS": "templates/aliyun_ecs.json",
"云服务器 Disk": "templates/aliyun_ecs_disk2.json",
"tencentcloud": { },
"categories": ["云服务器 CVM"], "collect_key_map": {
"map": { "云服务器 ECS": "ali.ecs",
"云服务器 CVM": "templates/tencent_cvm.json", "云服务器 Disk": "ali.ecs_disk",
} },
}, },
{
"huaweicloud": { "category": "网络与CDN",
"categories": ["云服务器 ECS"], "items": [
"map": { "内容分发CDN",
"云服务器 ECS": "templates/huaweicloud_ecs.json", "负载均衡SLB",
} "专有网络VPC",
}, "交换机Switch",
],
"aws": { "map": {
"categories": ["云服务器 EC2"], "内容分发CDN": "templates/aliyun_cdn.json",
"map": { "负载均衡SLB": "templates/aliyun_slb.json",
"云服务器 EC2": "templates/aws_ec2.json", "专有网络VPC": "templates/aliyun_vpc.json",
} "交换机Switch": "templates/aliyun_switch.json",
}, },
"collect_key_map": {
"内容分发CDN": "ali.cdn",
"负载均衡SLB": "ali.slb",
"专有网络VPC": "ali.vpc",
"交换机Switch": "ali.switch",
},
},
{
"category": "存储",
"items": ["块存储EBS", "对象存储OSS"],
"map": {
"块存储EBS": "templates/aliyun_ebs.json",
"对象存储OSS": "templates/aliyun_oss.json",
},
"collect_key_map": {
"块存储EBS": "ali.ebs",
"对象存储OSS": "ali.oss",
},
},
{
"category": "数据库",
"items": ["云数据库RDS MySQL", "云数据库RDS PostgreSQL", "云数据库 Redis"],
"map": {
"云数据库RDS MySQL": "templates/aliyun_rds_mysql.json",
"云数据库RDS PostgreSQL": "templates/aliyun_rds_postgre.json",
"云数据库 Redis": "templates/aliyun_redis.json",
},
"collect_key_map": {
"云数据库RDS MySQL": "ali.rds_mysql",
"云数据库RDS PostgreSQL": "ali.rds_postgre",
"云数据库 Redis": "ali.redis",
},
},
],
"tencentcloud": [
{
"category": "计算",
"items": ["云服务器 CVM"],
"map": {
"云服务器 CVM": "templates/tencent_cvm.json",
},
"collect_key_map": {
"云服务器 CVM": "tencent.cvm",
},
},
{
"category": "CDN与边缘",
"items": ["内容分发CDN"],
"map": {
"内容分发CDN": "templates/tencent_cdn.json",
},
"collect_key_map": {
"内容分发CDN": "tencent.cdn",
},
},
{
"category": "网络",
"items": ["负载均衡CLB", "私有网络VPC", "子网"],
"map": {
"负载均衡CLB": "templates/tencent_clb.json",
"私有网络VPC": "templates/tencent_vpc.json",
"子网": "templates/tencent_subnet.json",
},
"collect_key_map": {
"负载均衡CLB": "tencent.clb",
"私有网络VPC": "tencent.vpc",
"子网": "tencent.subnet",
},
},
{
"category": "存储",
"items": ["云硬盘CBS", "对象存储COS"],
"map": {
"云硬盘CBS": "templates/tencent_cbs.json",
"对象存储OSS": "templates/tencent_cos.json",
},
"collect_key_map": {
"云硬盘CBS": "tencent.cbs",
"对象存储OSS": "tencent.cos",
},
},
{
"category": "数据库",
"items": ["云数据库 MySQL", "云数据库 PostgreSQL", "云数据库 Redis"],
"map": {
"云数据库 MySQL": "templates/tencent_rdb.json",
"云数据库 PostgreSQL": "templates/tencent_postgres.json",
"云数据库 Redis": "templates/tencent_redis.json",
},
"collect_key_map": {
"云数据库 MySQL": "tencent.rdb",
"云数据库 PostgreSQL": "tencent.rds_postgres",
"云数据库 Redis": "tencent.redis",
},
},
],
"huaweicloud": [
{
"category": "计算",
"items": ["云服务器 ECS"],
"map": {
"云服务器 ECS": "templates/huaweicloud_ecs.json",
},
"collect_key_map": {
"云服务器 ECS": "huawei.ecs",
},
},
{
"category": "CDN与智能边缘",
"items": ["内容分发网络CDN"],
"map": {
"内容分发网络CDN": "templates/huawei_cdn.json",
},
"collect_key_map": {
"内容分发网络CDN": "huawei.cdn",
},
},
{
"category": "网络",
"items": ["弹性负载均衡ELB", "虚拟私有云VPC", "子网"],
"map": {
"弹性负载均衡ELB": "templates/huawei_elb.json",
"虚拟私有云VPC": "templates/huawei_vpc.json",
"子网": "templates/huawei_subnet.json",
},
"collect_key_map": {
"弹性负载均衡ELB": "huawei.elb",
"虚拟私有云VPC": "huawei.vpc",
"子网": "huawei.subnet",
},
},
{
"category": "存储",
"items": ["云硬盘EVS", "对象存储OBS"],
"map": {
"云硬盘EVS": "templates/huawei_evs.json",
"对象存储OBS": "templates/huawei_obs.json",
},
"collect_key_map": {
"云硬盘EVS": "huawei.evs",
"对象存储OBS": "huawei.obs",
},
},
{
"category": "数据库",
"items": ["云数据库RDS MySQL", "云数据库RDS PostgreSQL"],
"map": {
"云数据库RDS MySQL": "templates/huawei_rds_mysql.json",
"云数据库RDSPostgreSQL": "templates/huaweirds_postgre.json",
},
"collect_key_map": {
"云数据库RDS MySQL": "huawei.rds_mysql",
"云数据库RDS PostgreSQL": "huawei.rds_postgre",
},
},
{
"category": "应用中间件",
"items": ["分布式缓存Redis"],
"map": {
"分布式缓存Redis": "templates/huawei_dcs.json",
},
"collect_key_map": {
"分布式缓存Redis": "huawei.dcs",
},
},
],
"aws": [
{
"category": "计算",
"items": ["云服务器 EC2"],
"map": {
"云服务器 EC2": "templates/aws_ec2.json",
},
"collect_key_map": {
"云服务器 EC2": "aws.ec2",
},
},
{"category": "网络与CDN", "items": [], "map": {}, "collect_key_map": {}},
],
"vcenter": [
{
"category": "计算",
"items": [
"主机",
"虚拟机",
"主机集群"
],
"map": {
"主机": "templates/vsphere_host.json",
"虚拟机": "templates/vsphere_vm.json",
"主机集群": "templates/vsphere_cluster.json",
},
"collect_key_map": {
"主机": "vsphere.host",
"虚拟机": "vsphere.vm",
"主机集群": "vsphere.cluster",
},
},
{
"category": "网络",
"items": [
"网络",
"标准交换机",
"分布式交换机",
],
"map": {
"网络": "templates/vsphere_network.json",
"标准交换机": "templates/vsphere_standard_switch.json",
"分布式交换机": "templates/vsphere_distributed_switch.json",
},
"collect_key_map": {
"网络": "vsphere.network",
"标准交换机": "vsphere.standard_switch",
"分布式交换机": "vsphere.distributed_switch",
},
},
{
"category": "存储",
"items": ["数据存储", "数据存储集群"],
"map": {
"数据存储": "templates/vsphere_datastore.json",
"数据存储集群": "templates/vsphere.storage_pod.json",
},
"collect_key_map": {
"数据存储": "vsphere.datastore",
"数据存储集群": "vsphere.storage_pod",
},
},
{
"category": "其他",
"items": ["资源池", "数据中心", "文件夹"],
"map": {
"资源池": "templates/vsphere_datastore.json",
"数据中心": "templates/vsphere_datacenter.json",
"文件夹": "templates/vsphere_folder.json",
},
"collect_key_map": {
"资源池": "vsphere.pool",
"数据中心": "vsphere.datacenter",
"文件夹": "vsphere.folder",
},
},
],
"kvm": [
{
"category": "计算",
"items": ["虚拟机"],
"map": {
"虚拟机": "templates/kvm_vm.json",
},
"collect_key_map": {
"虚拟机": "kvm.vm",
},
},
{
"category": "存储",
"items": ["存储"],
"map": {
"存储": "templates/kvm_storage.json",
},
"collect_key_map": {
"存储": "kvm.storage",
},
},
{
"category": "network",
"items": ["网络"],
"map": {
"网络": "templates/kvm_network.json",
},
"collect_key_map": {
"网络": "kvm.network",
},
},
],
} }

View File

@@ -2,12 +2,19 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import datetime
from flask import current_app from flask import current_app
from api.extensions import cache from api.extensions import cache
from api.extensions import db from api.extensions import db
from api.lib.cmdb.custom_dashboard import CustomDashboardManager from api.lib.cmdb.custom_dashboard import CustomDashboardManager
from api.models.cmdb import Attribute from api.models.cmdb import Attribute, AutoDiscoveryExecHistory
from api.models.cmdb import AutoDiscoveryCI
from api.models.cmdb import AutoDiscoveryCIType
from api.models.cmdb import AutoDiscoveryCITypeRelation
from api.models.cmdb import AutoDiscoveryCounter
from api.models.cmdb import AutoDiscoveryRuleSyncHistory
from api.models.cmdb import CI from api.models.cmdb import CI
from api.models.cmdb import CIType from api.models.cmdb import CIType
from api.models.cmdb import CITypeAttribute from api.models.cmdb import CITypeAttribute
@@ -448,7 +455,67 @@ class CMDBCounterCache(object):
cache.set(cls.KEY2, result, timeout=0) cache.set(cls.KEY2, result, timeout=0)
return result res = db.session.query(AutoDiscoveryCI.created_at,
AutoDiscoveryCI.updated_at,
AutoDiscoveryCI.adt_id,
AutoDiscoveryCI.type_id,
AutoDiscoveryCI.is_accept).filter(AutoDiscoveryCI.deleted.is_(False))
today = datetime.datetime.today()
this_month = datetime.datetime(today.year, today.month, 1)
last_month = this_month - datetime.timedelta(days=1)
last_month = datetime.datetime(last_month.year, last_month.month, 1)
this_week = today - datetime.timedelta(days=datetime.date.weekday(today))
this_week = datetime.datetime(this_week.year, this_week.month, this_week.day)
last_week = this_week - datetime.timedelta(days=7)
last_week = datetime.datetime(last_week.year, last_week.month, last_week.day)
result = dict()
for i in res:
if i.type_id not in result:
result[i.type_id] = dict(instance_count=0, accept_count=0,
this_month_count=0, this_week_count=0, last_month_count=0, last_week_count=0)
adts = AutoDiscoveryCIType.get_by(type_id=i.type_id, to_dict=False)
result[i.type_id]['rule_count'] = len(adts) + AutoDiscoveryCITypeRelation.get_by(
ad_type_id=i.type_id, only_query=True).count()
result[i.type_id]['exec_target_count'] = len(
set([i.oneagent_id for adt in adts for i in db.session.query(
AutoDiscoveryRuleSyncHistory.oneagent_id).filter(
AutoDiscoveryRuleSyncHistory.adt_id == adt.id)]))
result[i.type_id]['instance_count'] += 1
if i.is_accept:
result[i.type_id]['accept_count'] += 1
if last_month <= i.created_at < this_month:
result[i.type_id]['last_month_count'] += 1
elif i.created_at >= this_month:
result[i.type_id]['this_month_count'] += 1
if last_week <= i.created_at < this_week:
result[i.type_id]['last_week_count'] += 1
elif i.created_at >= this_week:
result[i.type_id]['this_week_count'] += 1
for type_id in result:
existed = AutoDiscoveryCounter.get_by(type_id=type_id, first=True, to_dict=False)
if existed is None:
AutoDiscoveryCounter.create(type_id=type_id, **result[type_id])
else:
existed.update(**result[type_id])
for i in AutoDiscoveryCounter.get_by(to_dict=False):
if i.type_id not in result:
i.delete()
@classmethod
def clear_ad_exec_history(cls):
ci_types = CIType.get_by(to_dict=False)
for ci_type in ci_types:
for i in AutoDiscoveryExecHistory.get_by(type_id=ci_type.id, only_query=True).order_by(
AutoDiscoveryExecHistory.id.desc()).offset(50000):
i.delete(commit=False)
db.session.commit()
@classmethod @classmethod
def get_adc_counter(cls): def get_adc_counter(cls):

View File

@@ -223,7 +223,7 @@ class CIManager(object):
def ci_is_exist(unique_key, unique_value, type_id): def ci_is_exist(unique_key, unique_value, type_id):
""" """
:param unique_key: is a attribute :param unique_key: is an attribute
:param unique_value: :param unique_value:
:param type_id: :param type_id:
:return: :return:
@@ -383,12 +383,12 @@ class CIManager(object):
computed_attrs.append(attr.to_dict()) computed_attrs.append(attr.to_dict())
elif attr.is_password: elif attr.is_password:
if attr.name in ci_dict: if attr.name in ci_dict:
password_dict[attr.id] = ci_dict.pop(attr.name) password_dict[attr.id] = (ci_dict.pop(attr.name), attr.is_dynamic)
elif attr.alias in ci_dict: elif attr.alias in ci_dict:
password_dict[attr.id] = ci_dict.pop(attr.alias) password_dict[attr.id] = (ci_dict.pop(attr.alias), attr.is_dynamic)
if attr.re_check and password_dict.get(attr.id): if attr.re_check and password_dict.get(attr.id):
value_manager.check_re(attr.re_check, password_dict[attr.id]) value_manager.check_re(attr.re_check, password_dict[attr.id][0])
if computed_attrs: if computed_attrs:
value_manager.handle_ci_compute_attributes(ci_dict, computed_attrs, ci) value_manager.handle_ci_compute_attributes(ci_dict, computed_attrs, ci)
@@ -421,7 +421,8 @@ class CIManager(object):
operate_type = OperateType.UPDATE if ci is not None else OperateType.ADD operate_type = OperateType.UPDATE if ci is not None else OperateType.ADD
try: try:
ci = ci or CI.create(type_id=ci_type.id, is_auto_discovery=is_auto_discovery) ci = ci or CI.create(type_id=ci_type.id, is_auto_discovery=is_auto_discovery)
record_id = value_manager.create_or_update_attr_value(ci, ci_dict, key2attr, ticket_id=ticket_id) record_id, has_dynamic = value_manager.create_or_update_attr_value(
ci, ci_dict, key2attr, ticket_id=ticket_id)
except BadRequest as e: except BadRequest as e:
if existed is None: if existed is None:
cls.delete(ci.id) cls.delete(ci.id)
@@ -431,7 +432,7 @@ class CIManager(object):
for attr_id in password_dict: for attr_id in password_dict:
record_id = cls.save_password(ci.id, attr_id, password_dict[attr_id], record_id, ci_type.id) record_id = cls.save_password(ci.id, attr_id, password_dict[attr_id], record_id, ci_type.id)
if record_id: # has change if record_id or has_dynamic: # has changed
ci_cache.apply_async(args=(ci.id, operate_type, record_id), queue=CMDB_QUEUE) ci_cache.apply_async(args=(ci.id, operate_type, record_id), queue=CMDB_QUEUE)
if ref_ci_dict: # add relations if ref_ci_dict: # add relations
@@ -440,7 +441,6 @@ class CIManager(object):
return ci.id 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):
current_app.logger.info((ci_id, ci_dict, __sync))
now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
ci = self.confirm_ci_existed(ci_id) ci = self.confirm_ci_existed(ci_id)
@@ -465,12 +465,12 @@ class CIManager(object):
computed_attrs.append(attr.to_dict()) computed_attrs.append(attr.to_dict())
elif attr.is_password: elif attr.is_password:
if attr.name in ci_dict: if attr.name in ci_dict:
password_dict[attr.id] = ci_dict.pop(attr.name) password_dict[attr.id] = (ci_dict.pop(attr.name), attr.is_dynamic)
elif attr.alias in ci_dict: elif attr.alias in ci_dict:
password_dict[attr.id] = ci_dict.pop(attr.alias) password_dict[attr.id] = (ci_dict.pop(attr.alias), attr.is_dynamic)
if attr.re_check and password_dict.get(attr.id): if attr.re_check and password_dict.get(attr.id):
value_manager.check_re(attr.re_check, password_dict[attr.id]) value_manager.check_re(attr.re_check, password_dict[attr.id][0])
if computed_attrs: if computed_attrs:
value_manager.handle_ci_compute_attributes(ci_dict, computed_attrs, ci) value_manager.handle_ci_compute_attributes(ci_dict, computed_attrs, ci)
@@ -495,7 +495,8 @@ class CIManager(object):
ci_dict.pop(k) ci_dict.pop(k)
try: try:
record_id = value_manager.create_or_update_attr_value(ci, ci_dict, key2attr, ticket_id=ticket_id) record_id, has_dynamic = value_manager.create_or_update_attr_value(
ci, ci_dict, key2attr, ticket_id=ticket_id)
except BadRequest as e: except BadRequest as e:
raise e raise e
@@ -503,25 +504,25 @@ class CIManager(object):
for attr_id in password_dict: for attr_id in password_dict:
record_id = self.save_password(ci.id, attr_id, password_dict[attr_id], record_id, ci.type_id) record_id = self.save_password(ci.id, attr_id, password_dict[attr_id], record_id, ci.type_id)
if record_id: # has change 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) ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE)
else: else:
ci_cache((ci_id, OperateType.UPDATE, record_id)) 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} ref_ci_dict = {k: v for k, v in ci_dict.items() if k.startswith("$") and "." in k}
if ref_ci_dict: if ref_ci_dict:
if not __sync: if not __sync:
ci_relation_add.apply_async(args=(ref_ci_dict, ci.id), queue=CMDB_QUEUE) ci_relation_add.apply_async(args=(ref_ci_dict, ci.id), queue=CMDB_QUEUE)
else: else:
ci_relation_add((ref_ci_dict, ci.id)) ci_relation_add(ref_ci_dict, ci.id)
@staticmethod @staticmethod
def update_unique_value(ci_id, unique_name, unique_value): 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))) ci = CI.get_by_id(ci_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(ci_id)))
key2attr = {unique_name: AttributeCache.get(unique_name)} key2attr = {unique_name: AttributeCache.get(unique_name)}
record_id = AttributeValueManager().create_or_update_attr_value(ci, {unique_name: unique_value}, key2attr) record_id, _ = AttributeValueManager().create_or_update_attr_value(ci, {unique_name: unique_value}, key2attr)
ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE) ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE)
@@ -736,7 +737,7 @@ class CIManager(object):
fields=None, value_tables=None, unique_required=False, excludes=None): fields=None, value_tables=None, unique_required=False, excludes=None):
""" """
:param ci_ids: list of CI instance ID, eg. ['1', '2'] :param ci_ids: list of CI instance ID, e.g. ['1', '2']
:param ret_key: name, id or alias :param ret_key: name, id or alias
:param fields: :param fields:
:param value_tables: :param value_tables:
@@ -761,6 +762,7 @@ class CIManager(object):
@classmethod @classmethod
def save_password(cls, ci_id, attr_id, value, record_id, type_id): def save_password(cls, ci_id, attr_id, value, record_id, type_id):
value, is_dynamic = value
changed = None changed = None
encrypt_value = None encrypt_value = None
value_table = ValueTypeMap.table[ValueTypeEnum.PASSWORD] value_table = ValueTypeMap.table[ValueTypeEnum.PASSWORD]
@@ -777,14 +779,18 @@ class CIManager(object):
if existed is None: if existed is None:
if value: if value:
value_table.create(ci_id=ci_id, attr_id=attr_id, value=encrypt_value) value_table.create(ci_id=ci_id, attr_id=attr_id, value=encrypt_value)
changed = [(ci_id, attr_id, OperateType.ADD, '', PASSWORD_DEFAULT_SHOW, type_id)] if not is_dynamic:
changed = [(ci_id, attr_id, OperateType.ADD, '', PASSWORD_DEFAULT_SHOW, type_id)]
elif existed.value != encrypt_value: elif existed.value != encrypt_value:
if value: if value:
existed.update(ci_id=ci_id, attr_id=attr_id, value=encrypt_value) existed.update(ci_id=ci_id, attr_id=attr_id, value=encrypt_value)
changed = [(ci_id, attr_id, OperateType.UPDATE, PASSWORD_DEFAULT_SHOW, PASSWORD_DEFAULT_SHOW, type_id)] if not is_dynamic:
changed = [(ci_id, attr_id, OperateType.UPDATE, PASSWORD_DEFAULT_SHOW,
PASSWORD_DEFAULT_SHOW, type_id)]
else: else:
existed.delete() existed.delete()
changed = [(ci_id, attr_id, OperateType.DELETE, PASSWORD_DEFAULT_SHOW, '', type_id)] if not is_dynamic:
changed = [(ci_id, attr_id, OperateType.DELETE, PASSWORD_DEFAULT_SHOW, '', type_id)]
if current_app.config.get('SECRETS_ENGINE') == 'vault': if current_app.config.get('SECRETS_ENGINE') == 'vault':
vault = VaultClient(current_app.config.get('VAULT_URL'), current_app.config.get('VAULT_TOKEN')) vault = VaultClient(current_app.config.get('VAULT_URL'), current_app.config.get('VAULT_TOKEN'))
@@ -1274,52 +1280,83 @@ class CIRelationManager(object):
def build_by_attribute(cls, ci_dict): def build_by_attribute(cls, ci_dict):
type_id = ci_dict['_type'] type_id = ci_dict['_type']
child_items = CITypeRelation.get_by(parent_id=type_id, only_query=True).filter( child_items = CITypeRelation.get_by(parent_id=type_id, only_query=True).filter(
CITypeRelation.parent_attr_id.isnot(None)) CITypeRelation.parent_attr_ids.isnot(None))
for item in child_items: for item in child_items:
parent_attr = AttributeCache.get(item.parent_attr_id) relations = None
child_attr = AttributeCache.get(item.child_attr_id) for parent_attr_id, child_attr_id in zip(item.parent_attr_ids, item.child_attr_ids):
attr_value = ci_dict.get(parent_attr.name) _relations = set()
value_table = TableMap(attr=child_attr).table parent_attr = AttributeCache.get(parent_attr_id)
for child in value_table.get_by(attr_id=child_attr.id, value=attr_value, only_query=True).join( child_attr = AttributeCache.get(child_attr_id)
CI, CI.id == value_table.ci_id).filter(CI.type_id == item.child_id): attr_value = ci_dict.get(parent_attr.name)
CIRelationManager.add(ci_dict['_id'], child.ci_id, valid=False) value_table = TableMap(attr=child_attr).table
for child in value_table.get_by(attr_id=child_attr.id, value=attr_value, only_query=True).join(
CI, CI.id == value_table.ci_id).filter(CI.type_id == item.child_id):
_relations.add((ci_dict['_id'], child.ci_id))
if relations is None:
relations = _relations
else:
relations &= _relations
for parent_ci_id, child_ci_id in (relations or []):
CIRelationManager.add(parent_ci_id, child_ci_id, valid=False)
parent_items = CITypeRelation.get_by(child_id=type_id, only_query=True).filter( parent_items = CITypeRelation.get_by(child_id=type_id, only_query=True).filter(
CITypeRelation.child_attr_id.isnot(None)) CITypeRelation.child_attr_ids.isnot(None))
for item in parent_items: for item in parent_items:
parent_attr = AttributeCache.get(item.parent_attr_id) relations = None
child_attr = AttributeCache.get(item.child_attr_id) for parent_attr_id, child_attr_id in zip(item.parent_attr_ids, item.child_attr_ids):
attr_value = ci_dict.get(child_attr.name) _relations = set()
value_table = TableMap(attr=parent_attr).table parent_attr = AttributeCache.get(parent_attr_id)
for parent in value_table.get_by(attr_id=parent_attr.id, value=attr_value, only_query=True).join( child_attr = AttributeCache.get(child_attr_id)
CI, CI.id == value_table.ci_id).filter(CI.type_id == item.parent_id): attr_value = ci_dict.get(child_attr.name)
CIRelationManager.add(parent.ci_id, ci_dict['_id'], valid=False) value_table = TableMap(attr=parent_attr).table
for parent in value_table.get_by(attr_id=parent_attr.id, value=attr_value, only_query=True).join(
CI, CI.id == value_table.ci_id).filter(CI.type_id == item.parent_id):
_relations.add((parent.ci_id, ci_dict['_id']))
if relations is None:
relations = _relations
else:
relations &= _relations
for parent_ci_id, child_ci_id in (relations or []):
CIRelationManager.add(parent_ci_id, child_ci_id, valid=False)
@classmethod @classmethod
def rebuild_all_by_attribute(cls, ci_type_relation): def rebuild_all_by_attribute(cls, ci_type_relation):
parent_attr = AttributeCache.get(ci_type_relation['parent_attr_id']) relations = None
child_attr = AttributeCache.get(ci_type_relation['child_attr_id']) for parent_attr_id, child_attr_id in zip(ci_type_relation['parent_attr_ids'] or [],
if not parent_attr or not child_attr: ci_type_relation['child_attr_ids'] or []):
return
parent_value_table = TableMap(attr=parent_attr).table _relations = set()
child_value_table = TableMap(attr=child_attr).table parent_attr = AttributeCache.get(parent_attr_id)
child_attr = AttributeCache.get(child_attr_id)
if not parent_attr or not child_attr:
continue
parent_values = parent_value_table.get_by(attr_id=parent_attr.id, only_query=True).join( parent_value_table = TableMap(attr=parent_attr).table
CI, CI.id == parent_value_table.ci_id).filter(CI.type_id == ci_type_relation['parent_id']) child_value_table = TableMap(attr=child_attr).table
child_values = child_value_table.get_by(attr_id=child_attr.id, only_query=True).join(
CI, CI.id == child_value_table.ci_id).filter(CI.type_id == ci_type_relation['child_id'])
child_value2ci_ids = {} parent_values = parent_value_table.get_by(attr_id=parent_attr.id, only_query=True).join(
for child in child_values: CI, CI.id == parent_value_table.ci_id).filter(CI.type_id == ci_type_relation['parent_id'])
child_value2ci_ids.setdefault(child.value, []).append(child.ci_id) child_values = child_value_table.get_by(attr_id=child_attr.id, only_query=True).join(
CI, CI.id == child_value_table.ci_id).filter(CI.type_id == ci_type_relation['child_id'])
for parent in parent_values: child_value2ci_ids = {}
for child_ci_id in child_value2ci_ids.get(parent.value, []): for child in child_values:
try: child_value2ci_ids.setdefault(child.value, []).append(child.ci_id)
cls.add(parent.ci_id, child_ci_id, valid=False)
except: for parent in parent_values:
pass for child_ci_id in child_value2ci_ids.get(parent.value, []):
_relations.add((parent.ci_id, child_ci_id))
if relations is None:
relations = _relations
else:
relations &= _relations
for parent_ci_id, child_ci_id in (relations or []):
try:
cls.add(parent_ci_id, child_ci_id, valid=False)
except:
pass
class CITriggerManager(object): class CITriggerManager(object):

View File

@@ -1,7 +1,6 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
import copy import copy
import toposort import toposort
from flask import abort from flask import abort
from flask import current_app from flask import current_app
@@ -35,6 +34,7 @@ from api.lib.perm.acl.acl import validate_permission
from api.models.cmdb import Attribute from api.models.cmdb import Attribute
from api.models.cmdb import AutoDiscoveryCI from api.models.cmdb import AutoDiscoveryCI
from api.models.cmdb import AutoDiscoveryCIType from api.models.cmdb import AutoDiscoveryCIType
from api.models.cmdb import AutoDiscoveryCITypeRelation
from api.models.cmdb import CI from api.models.cmdb import CI
from api.models.cmdb import CIFilterPerms from api.models.cmdb import CIFilterPerms
from api.models.cmdb import CIType from api.models.cmdb import CIType
@@ -49,12 +49,12 @@ from api.models.cmdb import CITypeTrigger
from api.models.cmdb import CITypeUniqueConstraint from api.models.cmdb import CITypeUniqueConstraint
from api.models.cmdb import CustomDashboard from api.models.cmdb import CustomDashboard
from api.models.cmdb import PreferenceCITypeOrder from api.models.cmdb import PreferenceCITypeOrder
from api.models.cmdb import TopologyView
from api.models.cmdb import PreferenceRelationView from api.models.cmdb import PreferenceRelationView
from api.models.cmdb import PreferenceSearchOption from api.models.cmdb import PreferenceSearchOption
from api.models.cmdb import PreferenceShowAttributes from api.models.cmdb import PreferenceShowAttributes
from api.models.cmdb import PreferenceTreeView from api.models.cmdb import PreferenceTreeView
from api.models.cmdb import RelationType from api.models.cmdb import RelationType
from api.models.cmdb import TopologyView
class CITypeManager(object): class CITypeManager(object):
@@ -83,7 +83,7 @@ class CITypeManager(object):
self.cls.id, self.cls.icon, self.cls.name).filter(self.cls.deleted.is_(False))} self.cls.id, self.cls.icon, self.cls.name).filter(self.cls.deleted.is_(False))}
@staticmethod @staticmethod
def get_ci_types(type_name=None, like=True): def get_ci_types(type_name=None, like=True, type_ids=None):
resources = None resources = None
if current_app.config.get('USE_ACL') and not is_app_admin('cmdb'): if current_app.config.get('USE_ACL') and not is_app_admin('cmdb'):
resources = set([i.get('name') for i in ACLManager().get_resources(ResourceTypeEnum.CI_TYPE)]) resources = set([i.get('name') for i in ACLManager().get_resources(ResourceTypeEnum.CI_TYPE)])
@@ -92,6 +92,9 @@ class CITypeManager(object):
CIType.get_by_like(name=type_name) if like else CIType.get_by(name=type_name)) CIType.get_by_like(name=type_name) if like else CIType.get_by(name=type_name))
res = list() res = list()
for type_dict in ci_types: for type_dict in ci_types:
if type_ids is not None and type_dict['id'] not in type_ids:
continue
attr = AttributeCache.get(type_dict["unique_id"]) attr = AttributeCache.get(type_dict["unique_id"])
type_dict["unique_key"] = attr and attr.name type_dict["unique_key"] = attr and attr.name
if type_dict.get('show_id'): if type_dict.get('show_id'):
@@ -228,6 +231,9 @@ class CITypeManager(object):
if CI.get_by(type_id=type_id, first=True, to_dict=False) is not None: if CI.get_by(type_id=type_id, first=True, to_dict=False) is not None:
return abort(400, ErrFormat.ci_exists_and_cannot_delete_type) return abort(400, ErrFormat.ci_exists_and_cannot_delete_type)
if CITypeInheritance.get_by(parent_id=type_id, first=True):
return abort(400, ErrFormat.ci_type_inheritance_cannot_delete)
relation_views = PreferenceRelationView.get_by(to_dict=False) relation_views = PreferenceRelationView.get_by(to_dict=False)
for rv in relation_views: for rv in relation_views:
for item in (rv.cr_ids or []): for item in (rv.cr_ids or []):
@@ -251,16 +257,22 @@ class CITypeManager(object):
for item in AutoDiscoveryCI.get_by(type_id=type_id, to_dict=False): for item in AutoDiscoveryCI.get_by(type_id=type_id, to_dict=False):
item.delete(commit=False) item.delete(commit=False)
for item in AutoDiscoveryCITypeRelation.get_by(ad_type_id=type_id, to_dict=False):
item.soft_delete(commit=False)
for item in AutoDiscoveryCITypeRelation.get_by(peer_type_id=type_id, to_dict=False):
item.soft_delete(commit=False)
for item in CITypeInheritance.get_by(parent_id=type_id, to_dict=False): for item in CITypeInheritance.get_by(parent_id=type_id, to_dict=False):
item.delete(commit=False) item.soft_delete(commit=False)
for item in CITypeInheritance.get_by(child_id=type_id, to_dict=False): for item in CITypeInheritance.get_by(child_id=type_id, to_dict=False):
item.delete(commit=False) item.soft_delete(commit=False)
try: try:
from api.models.cmdb import CITypeReconciliation from api.models.cmdb import CITypeReconciliation
for item in CITypeReconciliation.get_by(type_id=type_id, to_dict=False): for item in CITypeReconciliation.get_by(type_id=type_id, to_dict=False):
item.delete(commit=False) item.soft_delete(commit=False)
except Exception: except Exception:
pass pass
@@ -282,6 +294,12 @@ class CITypeManager(object):
class CITypeInheritanceManager(object): class CITypeInheritanceManager(object):
cls = CITypeInheritance cls = CITypeInheritance
@classmethod
def get_all(cls, type_ids=None):
res = cls.cls.get_by(to_dict=True)
return [i for i in res if type_ids is None or (i['parent_id'] in type_ids and i['child_id'] in type_ids)]
@classmethod @classmethod
def get_parents(cls, type_id): def get_parents(cls, type_id):
return [i.parent_id for i in cls.cls.get_by(child_id=type_id, to_dict=False)] return [i.parent_id for i in cls.cls.get_by(child_id=type_id, to_dict=False)]
@@ -377,7 +395,7 @@ class CITypeGroupManager(object):
cls = CITypeGroup cls = CITypeGroup
@staticmethod @staticmethod
def get(need_other=None, config_required=True): def get(need_other=None, config_required=True, type_ids=None, ci_types=None):
resources = None resources = None
if current_app.config.get('USE_ACL'): if current_app.config.get('USE_ACL'):
resources = ACLManager('cmdb').get_resources(ResourceTypeEnum.CI) resources = ACLManager('cmdb').get_resources(ResourceTypeEnum.CI)
@@ -391,6 +409,8 @@ class CITypeGroupManager(object):
for group in groups: for group in groups:
for t in sorted(CITypeGroupItem.get_by(group_id=group['id']), key=lambda x: x['order'] or 0): for t in sorted(CITypeGroupItem.get_by(group_id=group['id']), key=lambda x: x['order'] or 0):
ci_type = CITypeCache.get(t['type_id']).to_dict() ci_type = CITypeCache.get(t['type_id']).to_dict()
if type_ids is not None and ci_type['id'] not in type_ids:
continue
if resources is None or (ci_type and ci_type['name'] in resources): if resources is None or (ci_type and ci_type['name'] in resources):
ci_type['permissions'] = resources[ci_type['name']] if resources is not None else None ci_type['permissions'] = resources[ci_type['name']] if resources is not None else None
ci_type['inherited'] = True if CITypeInheritanceManager.get_parents(ci_type['id']) else False ci_type['inherited'] = True if CITypeInheritanceManager.get_parents(ci_type['id']) else False
@@ -398,7 +418,7 @@ class CITypeGroupManager(object):
group_types.add(t["type_id"]) group_types.add(t["type_id"])
if need_other: if need_other:
ci_types = CITypeManager.get_ci_types() ci_types = CITypeManager.get_ci_types(type_ids=type_ids) if ci_types is None else ci_types
other_types = dict(ci_types=[]) other_types = dict(ci_types=[])
for ci_type in ci_types: for ci_type in ci_types:
if ci_type["id"] not in group_types and (resources is None or ci_type['name'] in resources): if ci_type["id"] not in group_types and (resources is None or ci_type['name'] in resources):
@@ -519,6 +539,8 @@ class CITypeAttributeManager(object):
attrs = CITypeAttributesCache.get(_type_id) attrs = CITypeAttributesCache.get(_type_id)
for attr in sorted(attrs, key=lambda x: (x.order, x.id)): for attr in sorted(attrs, key=lambda x: (x.order, x.id)):
attr_dict = AttributeManager().get_attribute(attr.attr_id, choice_web_hook_parse, choice_other_parse) attr_dict = AttributeManager().get_attribute(attr.attr_id, choice_web_hook_parse, choice_other_parse)
if not attr_dict:
continue
attr_dict["is_required"] = attr.is_required attr_dict["is_required"] = attr.is_required
attr_dict["order"] = attr.order attr_dict["order"] = attr.order
attr_dict["default_show"] = attr.default_show attr_dict["default_show"] = attr.default_show
@@ -527,7 +549,6 @@ class CITypeAttributeManager(object):
if not has_config_perm: if not has_config_perm:
attr_dict.pop('choice_web_hook', None) attr_dict.pop('choice_web_hook', None)
attr_dict.pop('choice_other', None) attr_dict.pop('choice_other', None)
if attr_dict['id'] not in id2pos: if attr_dict['id'] not in id2pos:
id2pos[attr_dict['id']] = len(result) id2pos[attr_dict['id']] = len(result)
result.append(attr_dict) result.append(attr_dict)
@@ -592,7 +613,6 @@ class CITypeAttributeManager(object):
if existed is not None: if existed is not None:
continue continue
current_app.logger.debug(attr_id)
CITypeAttribute.create(type_id=type_id, attr_id=attr_id, **kwargs) CITypeAttribute.create(type_id=type_id, attr_id=attr_id, **kwargs)
attr = AttributeCache.get(attr_id) attr = AttributeCache.get(attr_id)
@@ -686,9 +706,19 @@ class CITypeAttributeManager(object):
item = CITypeTrigger.get_by(type_id=_type_id, attr_id=attr_id, to_dict=False, first=True) item = CITypeTrigger.get_by(type_id=_type_id, attr_id=attr_id, to_dict=False, first=True)
item and item.soft_delete(commit=False) item and item.soft_delete(commit=False)
for item in (CITypeRelation.get_by(parent_id=type_id, parent_attr_id=attr_id, to_dict=False) + for item in (CITypeRelation.get_by(parent_id=type_id, to_dict=False) +
CITypeRelation.get_by(child_id=type_id, child_attr_id=attr_id, to_dict=False)): CITypeRelation.get_by(child_id=type_id, to_dict=False)):
item.soft_delete(commit=False) if item.parent_id == type_id and attr_id in (item.parent_attr_ids or []):
item_dict = item.to_dict()
pop_idx = item.parent_attr_ids.index(attr_id)
elif item.child_id == type_id and attr_id in (item.child_attr_ids or []):
item_dict = item.to_dict()
pop_idx = item.child_attr_ids.index(attr_id)
else:
continue
item.update(parent_attr_ids=item_dict['parent_attr_ids'].pop(pop_idx),
child_attr_ids=item_dict['child_attr_ids'].pop(pop_idx),
commit=False)
db.session.commit() db.session.commit()
@@ -749,21 +779,29 @@ class CITypeRelationManager(object):
""" """
@staticmethod @staticmethod
def get(): def get(type_ids=None):
res = CITypeRelation.get_by(to_dict=False) res = CITypeRelation.get_by(to_dict=False)
type2attributes = dict() type2attributes = dict()
result = []
for idx, item in enumerate(res): for idx, item in enumerate(res):
_item = item.to_dict() _item = item.to_dict()
if type_ids is not None and _item['parent_id'] not in type_ids and _item['child_id'] not in type_ids:
continue
res[idx] = _item res[idx] = _item
res[idx]['parent'] = item.parent.to_dict() res[idx]['parent'] = item.parent.to_dict()
if item.parent_id not in type2attributes: if item.parent_id not in type2attributes:
type2attributes[item.parent_id] = [i[1].to_dict() for i in CITypeAttributesCache.get2(item.parent_id)] type2attributes[item.parent_id] = [i[1].to_dict() for i in
CITypeAttributeManager.get_all_attributes(item.parent_id)]
res[idx]['child'] = item.child.to_dict() res[idx]['child'] = item.child.to_dict()
if item.child_id not in type2attributes: if item.child_id not in type2attributes:
type2attributes[item.child_id] = [i[1].to_dict() for i in CITypeAttributesCache.get2(item.child_id)] type2attributes[item.child_id] = [i[1].to_dict() for i in
CITypeAttributeManager.get_all_attributes(item.child_id)]
res[idx]['relation_type'] = item.relation_type.to_dict() res[idx]['relation_type'] = item.relation_type.to_dict()
return res, type2attributes result.append(res[idx])
return result, type2attributes
@staticmethod @staticmethod
def get_child_type_ids(type_id, level): def get_child_type_ids(type_id, level):
@@ -795,8 +833,8 @@ class CITypeRelationManager(object):
ci_type_dict["relation_type"] = relation_inst.relation_type.name ci_type_dict["relation_type"] = relation_inst.relation_type.name
ci_type_dict["constraint"] = relation_inst.constraint ci_type_dict["constraint"] = relation_inst.constraint
ci_type_dict["parent_attr_id"] = relation_inst.parent_attr_id ci_type_dict["parent_attr_ids"] = relation_inst.parent_attr_ids
ci_type_dict["child_attr_id"] = relation_inst.child_attr_id ci_type_dict["child_attr_ids"] = relation_inst.child_attr_ids
return ci_type_dict return ci_type_dict
@@ -837,7 +875,9 @@ class CITypeRelationManager(object):
if ci_type is None: if ci_type is None:
return nodes, edges return nodes, edges
nodes.append(ci_type.to_dict()) ci_type_dict = ci_type.to_dict()
ci_type_dict.setdefault('level', [0])
nodes.append(ci_type_dict)
node_ids.add(ci_type.id) node_ids.add(ci_type.id)
def _find(_id, lv): def _find(_id, lv):
@@ -906,7 +946,7 @@ class CITypeRelationManager(object):
@classmethod @classmethod
def add(cls, parent, child, relation_type_id, constraint=ConstraintEnum.One2Many, def add(cls, parent, child, relation_type_id, constraint=ConstraintEnum.One2Many,
parent_attr_id=None, child_attr_id=None): parent_attr_ids=None, child_attr_ids=None):
p = CITypeManager.check_is_existed(parent) p = CITypeManager.check_is_existed(parent)
c = CITypeManager.check_is_existed(child) c = CITypeManager.check_is_existed(child)
@@ -921,21 +961,22 @@ class CITypeRelationManager(object):
current_app.logger.warning(str(e)) current_app.logger.warning(str(e))
return abort(400, ErrFormat.circular_dependency_error) return abort(400, ErrFormat.circular_dependency_error)
old_parent_attr_id = None old_parent_attr_ids, old_child_attr_ids = None, None
existed = cls._get(p.id, c.id) existed = cls._get(p.id, c.id)
if existed is not None: if existed is not None:
old_parent_attr_id = existed.parent_attr_id old_parent_attr_ids = copy.deepcopy(existed.parent_attr_ids)
old_child_attr_ids = copy.deepcopy(existed.child_attr_ids)
existed = existed.update(relation_type_id=relation_type_id, existed = existed.update(relation_type_id=relation_type_id,
constraint=constraint, constraint=constraint,
parent_attr_id=parent_attr_id, parent_attr_ids=parent_attr_ids,
child_attr_id=child_attr_id, child_attr_ids=child_attr_ids,
filter_none=False) filter_none=False)
else: else:
existed = CITypeRelation.create(parent_id=p.id, existed = CITypeRelation.create(parent_id=p.id,
child_id=c.id, child_id=c.id,
relation_type_id=relation_type_id, relation_type_id=relation_type_id,
parent_attr_id=parent_attr_id, parent_attr_ids=parent_attr_ids,
child_attr_id=child_attr_id, child_attr_ids=child_attr_ids,
constraint=constraint) constraint=constraint)
if current_app.config.get("USE_ACL"): if current_app.config.get("USE_ACL"):
@@ -949,10 +990,10 @@ class CITypeRelationManager(object):
current_user.username, current_user.username,
ResourceTypeEnum.CI_TYPE_RELATION) ResourceTypeEnum.CI_TYPE_RELATION)
if parent_attr_id and parent_attr_id != old_parent_attr_id: if ((parent_attr_ids and parent_attr_ids != old_parent_attr_ids) or
if parent_attr_id and parent_attr_id != existed.parent_attr_id: (child_attr_ids and child_attr_ids != old_child_attr_ids)):
from api.tasks.cmdb import rebuild_relation_for_attribute_changed from api.tasks.cmdb import rebuild_relation_for_attribute_changed
rebuild_relation_for_attribute_changed.apply_async(args=(existed.to_dict())) rebuild_relation_for_attribute_changed.apply_async(args=(existed.to_dict(),))
CITypeHistoryManager.add(CITypeOperateType.ADD_RELATION, p.id, CITypeHistoryManager.add(CITypeOperateType.ADD_RELATION, p.id,
change=dict(parent=p.to_dict(), child=c.to_dict(), relation_type_id=relation_type_id)) change=dict(parent=p.to_dict(), child=c.to_dict(), relation_type_id=relation_type_id))
@@ -1009,7 +1050,8 @@ class CITypeAttributeGroupManager(object):
parent_ids = CITypeInheritanceManager.base(type_id) parent_ids = CITypeInheritanceManager.base(type_id)
groups = [] groups = []
id2type = {i: CITypeCache.get(i).alias for i in parent_ids} id2type = {i: CITypeCache.get(i) for i in parent_ids}
id2type = {k: v.alias for k, v in id2type.items() if v}
for _type_id in parent_ids + [type_id]: for _type_id in parent_ids + [type_id]:
_groups = CITypeAttributeGroup.get_by(type_id=_type_id) _groups = CITypeAttributeGroup.get_by(type_id=_type_id)
_groups = sorted(_groups, key=lambda x: x["order"] or 0) _groups = sorted(_groups, key=lambda x: x["order"] or 0)
@@ -1246,8 +1288,8 @@ class CITypeTemplateManager(object):
id2obj_dicts[added_id].get('child_id'), id2obj_dicts[added_id].get('child_id'),
id2obj_dicts[added_id].get('relation_type_id'), id2obj_dicts[added_id].get('relation_type_id'),
id2obj_dicts[added_id].get('constraint'), id2obj_dicts[added_id].get('constraint'),
id2obj_dicts[added_id].get('parent_attr_id'), id2obj_dicts[added_id].get('parent_attr_ids'),
id2obj_dicts[added_id].get('child_attr_id'), id2obj_dicts[added_id].get('child_attr_ids'),
) )
else: else:
obj = cls.create(flush=True, **id2obj_dicts[added_id]) obj = cls.create(flush=True, **id2obj_dicts[added_id])
@@ -1283,13 +1325,14 @@ class CITypeTemplateManager(object):
attributes = [attr for type_id in type2attributes for attr in type2attributes[type_id]] attributes = [attr for type_id in type2attributes for attr in type2attributes[type_id]]
attrs = [] attrs = []
for i in copy.deepcopy(attributes): for i in copy.deepcopy(attributes):
if i.pop('inherited', None):
continue
i.pop('default_show', None) i.pop('default_show', None)
i.pop('is_required', None) i.pop('is_required', None)
i.pop('order', None) i.pop('order', None)
i.pop('choice_web_hook', None) i.pop('choice_web_hook', None)
i.pop('choice_other', None) i.pop('choice_other', None)
i.pop('order', None) i.pop('order', None)
i.pop('inherited', None)
i.pop('inherited_from', None) i.pop('inherited_from', None)
choice_value = i.pop('choice_value', None) choice_value = i.pop('choice_value', None)
if not choice_value: if not choice_value:
@@ -1309,6 +1352,7 @@ class CITypeTemplateManager(object):
for i in ci_types: for i in ci_types:
i.pop("unique_key", None) i.pop("unique_key", None)
i.pop("show_name", None) i.pop("show_name", None)
i.pop("parent_ids", None)
i['unique_id'] = attr_id_map.get(i['unique_id'], i['unique_id']) i['unique_id'] = attr_id_map.get(i['unique_id'], i['unique_id'])
if i.get('show_id'): if i.get('show_id'):
i['show_id'] = attr_id_map.get(i['show_id'], i['show_id']) i['show_id'] = attr_id_map.get(i['show_id'], i['show_id'])
@@ -1346,7 +1390,7 @@ class CITypeTemplateManager(object):
return self.__import(RelationType, relation_types) return self.__import(RelationType, relation_types)
@staticmethod @staticmethod
def _import_ci_type_relations(ci_type_relations, type_id_map, relation_type_id_map): def _import_ci_type_relations(ci_type_relations, type_id_map, relation_type_id_map, attr_id_map):
for i in ci_type_relations: for i in ci_type_relations:
i.pop('parent', None) i.pop('parent', None)
i.pop('child', None) i.pop('child', None)
@@ -1356,15 +1400,32 @@ class CITypeTemplateManager(object):
i['child_id'] = type_id_map.get(i['child_id'], i['child_id']) i['child_id'] = type_id_map.get(i['child_id'], i['child_id'])
i['relation_type_id'] = relation_type_id_map.get(i['relation_type_id'], i['relation_type_id']) i['relation_type_id'] = relation_type_id_map.get(i['relation_type_id'], i['relation_type_id'])
i['parent_attr_ids'] = [attr_id_map.get(attr_id, attr_id) for attr_id in i.get('parent_attr_ids') or []]
i['child_attr_ids'] = [attr_id_map.get(attr_id, attr_id) for attr_id in i.get('child_attr_ids') or []]
try: try:
CITypeRelationManager.add(i.get('parent_id'), CITypeRelationManager.add(i.get('parent_id'),
i.get('child_id'), i.get('child_id'),
i.get('relation_type_id'), i.get('relation_type_id'),
i.get('constraint'), i.get('constraint'),
parent_attr_ids=i.get('parent_attr_ids', []),
child_attr_ids=i.get('child_attr_ids', []),
) )
except BadRequest: except Exception:
pass pass
@staticmethod
def _import_ci_type_inheritance(ci_type_inheritance, type_id_map):
for i in ci_type_inheritance:
i['parent_id'] = type_id_map.get(i['parent_id'])
i['child_id'] = type_id_map.get(i['child_id'])
if i['parent_id'] and i['child_id']:
try:
CITypeInheritanceManager.add([i.get('parent_id')], i.get('child_id'))
except BadRequest:
pass
@staticmethod @staticmethod
def _import_type_attributes(type2attributes, type_id_map, attr_id_map): def _import_type_attributes(type2attributes, type_id_map, attr_id_map):
for type_id in type2attributes: for type_id in type2attributes:
@@ -1376,6 +1437,9 @@ class CITypeTemplateManager(object):
handled = set() handled = set()
for attr in type2attributes[type_id]: for attr in type2attributes[type_id]:
if attr.get('inherited'):
continue
payload = dict(type_id=type_id_map.get(int(type_id), type_id), payload = dict(type_id=type_id_map.get(int(type_id), type_id),
attr_id=attr_id_map.get(attr['id'], attr['id']), attr_id=attr_id_map.get(attr['id'], attr['id']),
default_show=attr['default_show'], default_show=attr['default_show'],
@@ -1439,6 +1503,9 @@ class CITypeTemplateManager(object):
for rule in rules: for rule in rules:
ci_type = CITypeCache.get(rule.pop('type_name', None)) ci_type = CITypeCache.get(rule.pop('type_name', None))
if ci_type is None:
continue
adr = rule.pop('adr', {}) or {} adr = rule.pop('adr', {}) or {}
if ci_type: if ci_type:
@@ -1451,10 +1518,10 @@ class CITypeTemplateManager(object):
if ad_rule: if ad_rule:
rule['adr_id'] = ad_rule.id rule['adr_id'] = ad_rule.id
ad_rule.update(**adr) ad_rule.update(valid=False, **adr)
elif adr: elif adr:
ad_rule = AutoDiscoveryRuleCRUD().add(**adr) ad_rule = AutoDiscoveryRuleCRUD().add(valid=False, **adr)
rule['adr_id'] = ad_rule.id rule['adr_id'] = ad_rule.id
else: else:
continue continue
@@ -1483,6 +1550,23 @@ class CITypeTemplateManager(object):
except Exception as e: except Exception as e:
current_app.logger.warning("import auto discovery rules failed: {}".format(e)) current_app.logger.warning("import auto discovery rules failed: {}".format(e))
@staticmethod
def _import_auto_discovery_relation_rules(rules):
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCITypeRelationCRUD
for rule in rules:
ad_ci_type = CITypeCache.get(rule.pop('ad_type_name', None))
peer_ci_type = CITypeCache.get(rule.pop('peer_type_name', None))
peer_attr = AttributeCache.get(rule.pop('peer_attr_name', None))
if ad_ci_type and peer_attr and peer_ci_type:
if not AutoDiscoveryCITypeRelation.get_by(
ad_type_id=ad_ci_type.id, ad_key=rule.get('ad_key'),
peer_attr_id=peer_attr.id, peer_type_id=peer_ci_type.id):
AutoDiscoveryCITypeRelationCRUD().add(ad_type_id=ad_ci_type.id,
ad_key=rule.get('ad_key'),
peer_attr_id=peer_attr.id,
peer_type_id=peer_ci_type.id)
@staticmethod @staticmethod
def _import_icons(icons): def _import_icons(icons):
from api.lib.common_setting.upload_file import CommonFileCRUD from api.lib.common_setting.upload_file import CommonFileCRUD
@@ -1494,6 +1578,8 @@ class CITypeTemplateManager(object):
current_app.logger.warning("save icon failed: {}".format(e)) current_app.logger.warning("save icon failed: {}".format(e))
def import_template(self, tpt): def import_template(self, tpt):
db.session.commit()
import time import time
s = time.time() s = time.time()
attr_id_map = self._import_attributes(tpt.get('type2attributes') or {}) attr_id_map = self._import_attributes(tpt.get('type2attributes') or {})
@@ -1512,9 +1598,14 @@ class CITypeTemplateManager(object):
current_app.logger.info('import relation_types cost: {}'.format(time.time() - s)) current_app.logger.info('import relation_types cost: {}'.format(time.time() - s))
s = time.time() s = time.time()
self._import_ci_type_relations(tpt.get('ci_type_relations') or [], ci_type_id_map, relation_type_id_map) self._import_ci_type_relations(tpt.get('ci_type_relations') or [],
ci_type_id_map, relation_type_id_map, attr_id_map)
current_app.logger.info('import ci_type_relations cost: {}'.format(time.time() - s)) current_app.logger.info('import ci_type_relations cost: {}'.format(time.time() - s))
s = time.time()
self._import_ci_type_inheritance(tpt.get('ci_type_inheritance') or [], ci_type_id_map)
current_app.logger.info('import ci_type_inheritance cost: {}'.format(time.time() - s))
s = time.time() s = time.time()
self._import_type_attributes(tpt.get('type2attributes') or {}, ci_type_id_map, attr_id_map) self._import_type_attributes(tpt.get('type2attributes') or {}, ci_type_id_map, attr_id_map)
current_app.logger.info('import type2attributes cost: {}'.format(time.time() - s)) current_app.logger.info('import type2attributes cost: {}'.format(time.time() - s))
@@ -1527,26 +1618,73 @@ class CITypeTemplateManager(object):
self._import_auto_discovery_rules(tpt.get('ci_type_auto_discovery_rules') or []) self._import_auto_discovery_rules(tpt.get('ci_type_auto_discovery_rules') or [])
current_app.logger.info('import ci_type_auto_discovery_rules cost: {}'.format(time.time() - s)) current_app.logger.info('import ci_type_auto_discovery_rules cost: {}'.format(time.time() - s))
s = time.time()
self._import_auto_discovery_relation_rules(tpt.get('ci_type_auto_discovery_relation_rules') or [])
current_app.logger.info('import ci_type_auto_discovery_relation_rules cost: {}'.format(time.time() - s))
s = time.time() s = time.time()
self._import_icons(tpt.get('icons') or {}) self._import_icons(tpt.get('icons') or {})
current_app.logger.info('import icons cost: {}'.format(time.time() - s)) current_app.logger.info('import icons cost: {}'.format(time.time() - s))
@staticmethod @staticmethod
def export_template(): def export_template(type_ids=None):
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCITypeCRUD from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCITypeCRUD
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCITypeRelationCRUD
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryRuleCRUD from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryRuleCRUD
from api.lib.common_setting.upload_file import CommonFileCRUD from api.lib.common_setting.upload_file import CommonFileCRUD
ci_types = CITypeManager.get_ci_types(type_ids=type_ids)
extend_type_ids = []
for ci_type in ci_types:
if ci_type.get('parent_ids'):
extend_type_ids.extend(CITypeInheritanceManager.base(ci_type['id']))
extend_type_ids = list(set(extend_type_ids) - set(type_ids))
ci_type_relations = CITypeRelationManager.get(type_ids=type_ids)[0]
for i in ci_type_relations:
if i['parent_id'] not in type_ids:
extend_type_ids.append(i['parent_id'])
if i['child_id'] not in type_ids:
extend_type_ids.append(i['child_id'])
ad_relation_rules = AutoDiscoveryCITypeRelationCRUD.get_all(type_ids=type_ids)
rules = []
for r in ad_relation_rules:
if r.peer_type_id not in type_ids:
extend_type_ids.append(r.peer_type_id)
r = r.to_dict()
r['ad_type_name'] = CITypeCache.get(r.pop('ad_type_id')).name
peer_type_id = r.pop("peer_type_id")
peer_type_name = CITypeCache.get(peer_type_id).name
if not peer_type_name:
peer_type = CITypeCache.get(peer_type_id)
peer_type_name = peer_type and peer_type.name
r['peer_type_name'] = peer_type_name
peer_attr_id = r.pop("peer_attr_id")
peer_attr = AttributeCache.get(peer_attr_id)
r['peer_attr_name'] = peer_attr and peer_attr.name
rules.append(r)
ci_type_auto_discovery_relation_rules = rules
if extend_type_ids:
extend_type_ids = list(set(extend_type_ids))
type_ids.extend(extend_type_ids)
ci_types.extend(CITypeManager.get_ci_types(type_ids=extend_type_ids))
tpt = dict( tpt = dict(
ci_types=CITypeManager.get_ci_types(), ci_types=ci_types,
ci_type_groups=CITypeGroupManager.get(),
relation_types=[i.to_dict() for i in RelationTypeManager.get_all()], relation_types=[i.to_dict() for i in RelationTypeManager.get_all()],
ci_type_relations=CITypeRelationManager.get()[0], ci_type_relations=ci_type_relations,
ci_type_inheritance=CITypeInheritanceManager.get_all(type_ids=type_ids),
ci_type_auto_discovery_rules=list(), ci_type_auto_discovery_rules=list(),
ci_type_auto_discovery_relation_rules=ci_type_auto_discovery_relation_rules,
type2attributes=dict(), type2attributes=dict(),
type2attribute_group=dict(), type2attribute_group=dict(),
icons=dict() icons=dict()
) )
tpt['ci_type_groups'] = CITypeGroupManager.get(ci_types=tpt['ci_types'], type_ids=type_ids)
def get_icon_value(icon): def get_icon_value(icon):
try: try:
@@ -1554,12 +1692,13 @@ class CITypeTemplateManager(object):
except: except:
return "" return ""
ad_rules = AutoDiscoveryCITypeCRUD.get_all() type_id2name = {i['id']: i['name'] for i in tpt['ci_types']}
ad_rules = AutoDiscoveryCITypeCRUD.get_all(type_ids=type_ids)
rules = [] rules = []
for r in ad_rules: for r in ad_rules:
r = r.to_dict() r = r.to_dict()
ci_type = CITypeCache.get(r.pop('type_id'))
r['type_name'] = ci_type and ci_type.name r['type_name'] = type_id2name.get(r.pop('type_id'))
if r.get('adr_id'): if r.get('adr_id'):
adr = AutoDiscoveryRuleCRUD.get_by_id(r.pop('adr_id')) adr = AutoDiscoveryRuleCRUD.get_by_id(r.pop('adr_id'))
r['adr_name'] = adr and adr.name r['adr_name'] = adr and adr.name
@@ -1593,65 +1732,6 @@ class CITypeTemplateManager(object):
return tpt return tpt
@staticmethod
def export_template_by_type(type_id):
ci_type = CITypeCache.get(type_id) or abort(404, ErrFormat.ci_type_not_found2.format("id={}".format(type_id)))
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCITypeCRUD
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryRuleCRUD
from api.lib.common_setting.upload_file import CommonFileCRUD
tpt = dict(
ci_types=CITypeManager.get_ci_types(type_name=ci_type.name, like=False),
ci_type_auto_discovery_rules=list(),
type2attributes=dict(),
type2attribute_group=dict(),
icons=dict()
)
def get_icon_value(icon):
try:
return CommonFileCRUD().get_file_binary_str(icon)
except:
return ""
ad_rules = AutoDiscoveryCITypeCRUD.get_by_type_id(ci_type.id)
rules = []
for r in ad_rules:
r = r.to_dict()
r['type_name'] = ci_type and ci_type.name
if r.get('adr_id'):
adr = AutoDiscoveryRuleCRUD.get_by_id(r.pop('adr_id'))
r['adr_name'] = adr and adr.name
r['adr'] = adr and adr.to_dict() or {}
icon_url = r['adr'].get('option', {}).get('icon', {}).get('url')
if icon_url and icon_url not in tpt['icons']:
tpt['icons'][icon_url] = get_icon_value(icon_url)
rules.append(r)
tpt['ci_type_auto_discovery_rules'] = rules
for ci_type in tpt['ci_types']:
if ci_type['icon'] and len(ci_type['icon'].split('$$')) > 3:
icon_url = ci_type['icon'].split('$$')[3]
if icon_url not in tpt['icons']:
tpt['icons'][icon_url] = get_icon_value(icon_url)
tpt['type2attributes'][ci_type['id']] = CITypeAttributeManager.get_attributes_by_type_id(
ci_type['id'], choice_web_hook_parse=False, choice_other_parse=False)
for attr in tpt['type2attributes'][ci_type['id']]:
for i in (attr.get('choice_value') or []):
if (i[1] or {}).get('icon', {}).get('url') and len(i[1]['icon']['url'].split('$$')) > 3:
icon_url = i[1]['icon']['url'].split('$$')[3]
if icon_url not in tpt['icons']:
tpt['icons'][icon_url] = get_icon_value(icon_url)
tpt['type2attribute_group'][ci_type['id']] = CITypeAttributeGroupManager.get_by_type_id(ci_type['id'])
return tpt
class CITypeUniqueConstraintManager(object): class CITypeUniqueConstraintManager(object):
@staticmethod @staticmethod

View File

@@ -55,9 +55,9 @@ class CITypeOperateType(BaseEnum):
DELETE_UNIQUE_CONSTRAINT = "11" # 删除联合唯一 DELETE_UNIQUE_CONSTRAINT = "11" # 删除联合唯一
ADD_RELATION = "12" # 新增关系 ADD_RELATION = "12" # 新增关系
DELETE_RELATION = "13" # 删除关系 DELETE_RELATION = "13" # 删除关系
ADD_RECONCILIATION = "14" # 删除关系 ADD_RECONCILIATION = "14" # 新增数据合规
UPDATE_RECONCILIATION = "15" # 删除关系 UPDATE_RECONCILIATION = "15" # 修改数据合规
DELETE_RECONCILIATION = "16" # 删除关系 DELETE_RECONCILIATION = "16" # 删除数据合规
class RetKey(BaseEnum): class RetKey(BaseEnum):
@@ -93,7 +93,8 @@ class RoleEnum(BaseEnum):
class AutoDiscoveryType(BaseEnum): class AutoDiscoveryType(BaseEnum):
AGENT = "agent" AGENT = "agent"
SNMP = "snmp" SNMP = "snmp"
HTTP = "http" HTTP = "http" # cloud
COMPONENTS = "components"
class AttributeDefaultValueEnum(BaseEnum): class AttributeDefaultValueEnum(BaseEnum):

View File

@@ -13,6 +13,7 @@ from api.lib.cmdb.const import OperateType
from api.lib.cmdb.perms import CIFilterPermsCRUD from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.cmdb.resp_format import ErrFormat from api.lib.cmdb.resp_format import ErrFormat
from api.lib.perm.acl.cache import UserCache from api.lib.perm.acl.cache import UserCache
from api.models.cmdb import CI
from api.models.cmdb import Attribute from api.models.cmdb import Attribute
from api.models.cmdb import AttributeHistory from api.models.cmdb import AttributeHistory
from api.models.cmdb import CIRelationHistory from api.models.cmdb import CIRelationHistory
@@ -306,7 +307,7 @@ class CITriggerHistoryManager(object):
def get(page, page_size, type_id=None, trigger_id=None, operate_type=None): def get(page, page_size, type_id=None, trigger_id=None, operate_type=None):
query = CITriggerHistory.get_by(only_query=True) query = CITriggerHistory.get_by(only_query=True)
if type_id: if type_id:
query = query.filter(CITriggerHistory.type_id == type_id) query = query.join(CI, CI.id == CITriggerHistory.ci_id).filter(CI.type_id == type_id)
if trigger_id: if trigger_id:
query = query.filter(CITriggerHistory.trigger_id == trigger_id) query = query.filter(CITriggerHistory.trigger_id == trigger_id)

View File

@@ -62,6 +62,7 @@ class ErrFormat(CommonErrFormat):
"The model cannot be deleted because the CI already exists") # 因为CI已经存在不能删除模型 "The model cannot be deleted because the CI already exists") # 因为CI已经存在不能删除模型
ci_exists_and_cannot_delete_inheritance = _l( ci_exists_and_cannot_delete_inheritance = _l(
"The inheritance cannot be deleted because the CI already exists") # 因为CI已经存在不能删除继承关系 "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_relation_view_exists_and_cannot_delete_type = _l( ci_relation_view_exists_and_cannot_delete_type = _l(

View File

@@ -16,7 +16,7 @@ from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
from api.lib.cmdb.const import ResourceTypeEnum from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.resp_format import ErrFormat from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.search import SearchError 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.perm.acl.acl import ACLManager 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 is_app_admin
from api.models.cmdb import TopologyView from api.models.cmdb import TopologyView
@@ -172,12 +172,13 @@ class TopologyViewManager(object):
_type = CITypeCache.get(central_node_type) _type = CITypeCache.get(central_node_type)
if not _type: if not _type:
return dict(nodes=nodes, links=links) return dict(nodes=nodes, links=links)
type2meta = {_type.id: _type.icon}
root_ids = [] root_ids = []
show_key = AttributeCache.get(_type.show_id or _type.unique_id) show_key = AttributeCache.get(_type.show_id or _type.unique_id)
q = (central_node_instances[2:] if central_node_instances.startswith('q=') else q = (central_node_instances[2:] if central_node_instances.startswith('q=') else
central_node_instances) central_node_instances)
s = search(q, fl=['_id', show_key.name], use_id_filter=False, use_ci_filter=False, count=1000000) s = ci_search(q, fl=['_id', show_key.name], use_id_filter=False, use_ci_filter=False, count=1000000)
try: try:
response, _, _, _, _, _ = s.search() response, _, _, _, _, _ = s.search()
except SearchError as e: except SearchError as e:
@@ -192,7 +193,6 @@ class TopologyViewManager(object):
prefix = REDIS_PREFIX_CI_RELATION prefix = REDIS_PREFIX_CI_RELATION
key = list(map(str, root_ids)) key = list(map(str, root_ids))
id2node = {} id2node = {}
type2meta = {}
for level in sorted([i for i in path.keys() if int(i) > 0]): for level in sorted([i for i in path.keys() if int(i) > 0]):
type_ids = {int(i) for i in path[level]} type_ids = {int(i) for i in path[level]}
@@ -238,7 +238,7 @@ class TopologyViewManager(object):
type2show[type_id] = attr.name type2show[type_id] = attr.name
if id2node: if id2node:
s = search("_id:({})".format(';'.join(id2node.keys())), fl=list(fl), s = ci_search("_id:({})".format(';'.join(id2node.keys())), fl=list(fl),
use_id_filter=False, use_ci_filter=False, count=1000000) use_id_filter=False, use_ci_filter=False, count=1000000)
try: try:
response, _, _, _, _, _ = s.search() response, _, _, _, _, _ = s.search()

View File

@@ -266,6 +266,7 @@ class AttributeValueManager(object):
:return: :return:
""" """
changed = [] changed = []
has_dynamic = False
for key, value in ci_dict.items(): for key, value in ci_dict.items():
attr = key2attr.get(key) attr = key2attr.get(key)
if not attr: if not attr:
@@ -289,10 +290,16 @@ class AttributeValueManager(object):
for idx in range(index, len(existed_attrs)): for idx in range(index, len(existed_attrs)):
existed_attr = existed_attrs[idx] existed_attr = existed_attrs[idx]
existed_attr.delete(flush=False, commit=False) existed_attr.delete(flush=False, commit=False)
changed.append((ci.id, attr.id, OperateType.DELETE, existed_values[idx], None, ci.type_id)) if not attr.is_dynamic:
changed.append((ci.id, attr.id, OperateType.DELETE, existed_values[idx], None, ci.type_id))
else:
has_dynamic = True
for idx in range(index, len(value)): for idx in range(index, len(value)):
value_table.create(ci_id=ci.id, attr_id=attr.id, value=value[idx], flush=False, commit=False) value_table.create(ci_id=ci.id, attr_id=attr.id, value=value[idx], flush=False, commit=False)
changed.append((ci.id, attr.id, OperateType.ADD, None, value[idx], ci.type_id)) if not attr.is_dynamic:
changed.append((ci.id, attr.id, OperateType.ADD, None, value[idx], ci.type_id))
else:
has_dynamic = True
else: else:
existed_attr = value_table.get_by(attr_id=attr.id, ci_id=ci.id, first=True, to_dict=False) existed_attr = value_table.get_by(attr_id=attr.id, ci_id=ci.id, first=True, to_dict=False)
existed_value = existed_attr and existed_attr.value existed_value = existed_attr and existed_attr.value
@@ -301,7 +308,10 @@ class AttributeValueManager(object):
if existed_value is None and value is not None: if existed_value is None and value is not None:
value_table.create(ci_id=ci.id, attr_id=attr.id, value=value, flush=False, commit=False) value_table.create(ci_id=ci.id, attr_id=attr.id, value=value, flush=False, commit=False)
changed.append((ci.id, attr.id, OperateType.ADD, None, value, ci.type_id)) if not attr.is_dynamic:
changed.append((ci.id, attr.id, OperateType.ADD, None, value, ci.type_id))
else:
has_dynamic = True
else: else:
if existed_value != value and existed_attr: if existed_value != value and existed_attr:
if value is None: if value is None:
@@ -309,16 +319,22 @@ class AttributeValueManager(object):
else: else:
existed_attr.update(value=value, flush=False, commit=False) existed_attr.update(value=value, flush=False, commit=False)
changed.append((ci.id, attr.id, OperateType.UPDATE, existed_value, value, ci.type_id)) if not attr.is_dynamic:
changed.append((ci.id, attr.id, OperateType.UPDATE, existed_value, value, ci.type_id))
else:
has_dynamic = True
try: if changed or has_dynamic:
db.session.commit() try:
except Exception as e: db.session.commit()
db.session.rollback() except Exception as e:
current_app.logger.warning(str(e)) db.session.rollback()
return abort(400, ErrFormat.attribute_value_unknown_error.format(e.args[0])) current_app.logger.warning(str(e))
return abort(400, ErrFormat.attribute_value_unknown_error.format(e.args[0]))
return self.write_change2(changed, ticket_id=ticket_id) return self.write_change2(changed, ticket_id=ticket_id), has_dynamic
else:
return None, has_dynamic
@staticmethod @staticmethod
def delete_attr_value(attr_id, ci_id, commit=True): def delete_attr_value(attr_id, ci_id, commit=True):

View File

@@ -3,6 +3,7 @@ import functools
from flask import abort, session from flask import abort, session
from api.lib.common_setting.acl import ACLManager from api.lib.common_setting.acl import ACLManager
from api.lib.common_setting.resp_format import ErrFormat from api.lib.common_setting.resp_format import ErrFormat
from api.lib.perm.acl.acl import is_app_admin
def perms_role_required(app_name, resource_type_name, resource_name, perm, role_name=None): def perms_role_required(app_name, resource_type_name, resource_name, perm, role_name=None):
@@ -16,7 +17,7 @@ def perms_role_required(app_name, resource_type_name, resource_name, perm, role_
except Exception as e: except Exception as e:
# resource_type not exist, continue check role # resource_type not exist, continue check role
if role_name: if role_name:
if role_name not in session.get("acl", {}).get("parentRoles", []): if role_name not in session.get("acl", {}).get("parentRoles", []) and not is_app_admin(app_name):
abort(403, ErrFormat.role_required.format(role_name)) abort(403, ErrFormat.role_required.format(role_name))
return func(*args, **kwargs) return func(*args, **kwargs)
@@ -25,7 +26,7 @@ def perms_role_required(app_name, resource_type_name, resource_name, perm, role_
if not has_perms: if not has_perms:
if role_name: if role_name:
if role_name not in session.get("acl", {}).get("parentRoles", []): if role_name not in session.get("acl", {}).get("parentRoles", []) and not is_app_admin(app_name):
abort(403, ErrFormat.role_required.format(role_name)) abort(403, ErrFormat.role_required.format(role_name))
else: else:
abort(403, ErrFormat.resource_no_permission.format(resource_name, perm)) abort(403, ErrFormat.resource_no_permission.format(resource_name, perm))

View File

@@ -48,7 +48,7 @@ class CMDBApp(BaseApp):
{"page": "Model_Relationships", "page_cn": "模型关系", "perms": ["read"]}, {"page": "Model_Relationships", "page_cn": "模型关系", "perms": ["read"]},
{"page": "Operation_Audit", "page_cn": "操作审计", "perms": ["read"]}, {"page": "Operation_Audit", "page_cn": "操作审计", "perms": ["read"]},
{"page": "Relationship_Types", "page_cn": "关系类型", "perms": ["read"]}, {"page": "Relationship_Types", "page_cn": "关系类型", "perms": ["read"]},
{"page": "Auto_Discovery", "page_cn": "自动发现", "perms": ["read"]}, {"page": "Auto_Discovery", "page_cn": "自动发现", "perms": ["read", "create_plugin", "update_plugin", "delete_plugin"]},
{"page": "TopologyView", "page_cn": "拓扑视图", {"page": "TopologyView", "page_cn": "拓扑视图",
"perms": ["read", "create_topology_group", "update_topology_group", "delete_topology_group", "perms": ["read", "create_topology_group", "update_topology_group", "delete_topology_group",
"create_topology_view"], "create_topology_view"],

View File

@@ -8,6 +8,8 @@ from api.extensions import db
from api.lib.utils import get_page from api.lib.utils import get_page
from api.lib.utils import get_page_size from api.lib.utils import get_page_size
__author__ = 'pycook'
class DBMixin(object): class DBMixin(object):
cls = None cls = None
@@ -17,13 +19,18 @@ class DBMixin(object):
page = get_page(page) page = get_page(page)
page_size = get_page_size(page_size) page_size = get_page_size(page_size)
if fl is None: if fl is None:
query = db.session.query(cls.cls).filter(cls.cls.deleted.is_(False)) query = db.session.query(cls.cls)
else: else:
query = db.session.query(*[getattr(cls.cls, i) for i in fl]).filter(cls.cls.deleted.is_(False)) query = db.session.query(*[getattr(cls.cls, i) for i in fl])
_query = None _query = None
if count_query: if count_query:
_query = db.session.query(func.count(cls.cls.id)).filter(cls.cls.deleted.is_(False)) _query = db.session.query(func.count(cls.cls.id))
if hasattr(cls.cls, 'deleted'):
query = query.filter(cls.cls.deleted.is_(False))
if _query:
_query = _query.filter(cls.cls.deleted.is_(False))
for k in kwargs: for k in kwargs:
if hasattr(cls.cls, k): if hasattr(cls.cls, k):

View File

@@ -79,7 +79,8 @@ class PermissionCRUD(object):
return r and cls.get_all(r.id) return r and cls.get_all(r.id)
@staticmethod @staticmethod
def grant(rid, perms, resource_id=None, group_id=None, rebuild=True, source=AuditOperateSource.acl): def grant(rid, perms, resource_id=None, group_id=None, rebuild=True,
source=AuditOperateSource.acl, force_update=False):
app_id = None app_id = None
rt_id = None rt_id = None
@@ -106,8 +107,23 @@ class PermissionCRUD(object):
if not perms: if not perms:
perms = [i.get('name') for i in ResourceTypeCRUD.get_perms(group.resource_type_id)] perms = [i.get('name') for i in ResourceTypeCRUD.get_perms(group.resource_type_id)]
_role_permissions = [] if force_update:
revoke_role_permissions = []
existed_perms = RolePermission.get_by(rid=rid,
app_id=app_id,
group_id=group_id,
resource_id=resource_id,
to_dict=False)
for role_perm in existed_perms:
perm = PermissionCache.get(role_perm.perm_id, rt_id)
if perm and perm.name not in perms:
role_perm.soft_delete()
revoke_role_permissions.append(role_perm)
AuditCRUD.add_permission_log(app_id, AuditOperateType.revoke, rid, rt_id,
revoke_role_permissions, source=source)
_role_permissions = []
for _perm in set(perms): for _perm in set(perms):
perm = PermissionCache.get(_perm, rt_id) perm = PermissionCache.get(_perm, rt_id)
if not perm: if not perm:

View File

@@ -51,12 +51,12 @@ def _auth_with_key():
user, authenticated = User.query.authenticate_with_key(key, secret, req_args, path) user, authenticated = User.query.authenticate_with_key(key, secret, req_args, path)
if user and authenticated: if user and authenticated:
login_user(user) login_user(user)
reset_session(user) # reset_session(user)
return True return True
role, authenticated = Role.query.authenticate_with_key(key, secret, req_args, path) role, authenticated = Role.query.authenticate_with_key(key, secret, req_args, path)
if role and authenticated: if role and authenticated:
reset_session(None, role=role.name) # reset_session(None, role=role.name)
return True return True
return False return False

View File

@@ -29,6 +29,6 @@ class CommonErrFormat(object):
role_required = _l("Role {} can only operate!") # 角色 {} 才能操作! role_required = _l("Role {} can only operate!") # 角色 {} 才能操作!
user_not_found = _l("User {} does not exist") # 用户 {} 不存在 user_not_found = _l("User {} does not exist") # 用户 {} 不存在
no_permission = _l("You do not have {} permission for resource: {}!") # 您没有资源: {} 的{}权限! no_permission = _l("For resource: {}, you do not have {} permission!") # 您没有资源: {} 的{}权限!
no_permission2 = _l("You do not have permission to operate!") # 您没有操作权限! no_permission2 = _l("You do not have permission to operate!") # 您没有操作权限!
no_permission_only_owner = _l("Only the creator or administrator has permission!") # 只有创建人或者管理员才有权限! no_permission_only_owner = _l("Only the creator or administrator has permission!") # 只有创建人或者管理员才有权限!

View File

@@ -79,8 +79,11 @@ class CITypeRelation(Model):
relation_type_id = db.Column(db.Integer, db.ForeignKey("c_relation_types.id"), nullable=False) relation_type_id = db.Column(db.Integer, db.ForeignKey("c_relation_types.id"), nullable=False)
constraint = db.Column(db.Enum(*ConstraintEnum.all()), default=ConstraintEnum.One2Many) constraint = db.Column(db.Enum(*ConstraintEnum.all()), default=ConstraintEnum.One2Many)
parent_attr_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id")) parent_attr_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id")) # CMDB > 2.4.5: deprecated
child_attr_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id")) child_attr_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id")) # CMDB > 2.4.5: deprecated
parent_attr_ids = db.Column(db.JSON) # [parent_attr_id, ]
child_attr_ids = db.Column(db.JSON) # [child_attr_id, ]
parent = db.relationship("CIType", primaryjoin="CIType.id==CITypeRelation.parent_id") parent = db.relationship("CIType", primaryjoin="CIType.id==CITypeRelation.parent_id")
child = db.relationship("CIType", primaryjoin="CIType.id==CITypeRelation.child_id") child = db.relationship("CIType", primaryjoin="CIType.id==CITypeRelation.child_id")
@@ -101,6 +104,7 @@ class Attribute(Model):
is_link = db.Column(db.Boolean, default=False) is_link = db.Column(db.Boolean, default=False)
is_password = db.Column(db.Boolean, default=False) is_password = db.Column(db.Boolean, default=False)
is_sortable = db.Column(db.Boolean, default=False) is_sortable = db.Column(db.Boolean, default=False)
is_dynamic = db.Column(db.Boolean, default=False)
default = db.Column(db.JSON) # {"default": None} default = db.Column(db.JSON) # {"default": None}
@@ -562,20 +566,29 @@ class AutoDiscoveryCIType(Model):
attributes = db.Column(db.JSON) # {ad_key: cmdb_key} attributes = db.Column(db.JSON) # {ad_key: cmdb_key}
relation = db.Column(db.JSON) # [{ad_key: {type_id: x, attr_id: x}}] relation = db.Column(db.JSON) # [{ad_key: {type_id: x, attr_id: x}}], CMDB > 2.4.5: deprecated
auto_accept = db.Column(db.Boolean, default=False) auto_accept = db.Column(db.Boolean, default=False)
agent_id = db.Column(db.String(8), index=True) agent_id = db.Column(db.String(8), index=True)
query_expr = db.Column(db.Text) query_expr = db.Column(db.Text)
interval = db.Column(db.Integer) # seconds interval = db.Column(db.Integer) # seconds, > 2.4.5: deprecated
cron = db.Column(db.String(128)) cron = db.Column(db.String(128))
extra_option = db.Column(db.JSON) extra_option = db.Column(db.JSON)
uid = db.Column(db.Integer, index=True) uid = db.Column(db.Integer, index=True)
class AutoDiscoveryCITypeRelation(Model):
__tablename__ = "c_ad_ci_type_relations"
ad_type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'), nullable=False)
ad_key = db.Column(db.String(128))
peer_type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'), nullable=False)
peer_attr_id = db.Column(db.Integer, db.ForeignKey('c_attributes.id'), nullable=False)
class AutoDiscoveryCI(Model): class AutoDiscoveryCI(Model):
__tablename__ = "c_ad_ci" __tablename__ = "c_ad_ci"
@@ -591,6 +604,36 @@ class AutoDiscoveryCI(Model):
accept_time = db.Column(db.DateTime) accept_time = db.Column(db.DateTime)
class AutoDiscoveryRuleSyncHistory(Model2):
__tablename__ = "c_ad_rule_sync_histories"
adt_id = db.Column(db.Integer, db.ForeignKey('c_ad_ci_types.id'))
oneagent_id = db.Column(db.String(8))
oneagent_name = db.Column(db.String(64))
sync_at = db.Column(db.DateTime, default=datetime.datetime.now())
class AutoDiscoveryExecHistory(Model2):
__tablename__ = "c_ad_exec_histories"
type_id = db.Column(db.Integer, index=True)
stdout = db.Column(db.Text)
class AutoDiscoveryCounter(Model2):
__tablename__ = "c_ad_counter"
type_id = db.Column(db.Integer, index=True)
rule_count = db.Column(db.Integer, default=0)
exec_target_count = db.Column(db.Integer, default=0)
instance_count = db.Column(db.Integer, default=0)
accept_count = db.Column(db.Integer, default=0)
this_month_count = db.Column(db.Integer, default=0)
this_week_count = db.Column(db.Integer, default=0)
last_month_count = db.Column(db.Integer, default=0)
last_week_count = db.Column(db.Integer, default=0)
class CIFilterPerms(Model): class CIFilterPerms(Model):
__tablename__ = "c_ci_filter_perms" __tablename__ = "c_ci_filter_perms"

View File

@@ -2,6 +2,7 @@
import json import json
import datetime
import redis_lock import redis_lock
from flask import current_app from flask import current_app
@@ -25,6 +26,8 @@ from api.lib.utils import handle_arg_list
from api.models.cmdb import CI from api.models.cmdb import CI
from api.models.cmdb import CIRelation from api.models.cmdb import CIRelation
from api.models.cmdb import CITypeAttribute from api.models.cmdb import CITypeAttribute
from api.models.cmdb import AutoDiscoveryCI
from api.models.cmdb import AutoDiscoveryCIType
@celery.task(name="cmdb.ci_cache", queue=CMDB_QUEUE) @celery.task(name="cmdb.ci_cache", queue=CMDB_QUEUE)
@@ -87,6 +90,13 @@ def ci_delete(ci_id):
else: else:
rd.delete(ci_id, REDIS_PREFIX_CI) rd.delete(ci_id, REDIS_PREFIX_CI)
instance = AutoDiscoveryCI.get_by(ci_id=ci_id, to_dict=False, first=True)
if instance is not None:
adt = AutoDiscoveryCIType.get_by_id(instance.adt_id)
if adt:
adt.update(updated_at=datetime.datetime.now())
instance.delete()
current_app.logger.info("{0} delete..........".format(ci_id)) current_app.logger.info("{0} delete..........".format(ci_id))
@@ -249,3 +259,21 @@ def calc_computed_attribute(attr_id, uid):
cis = CI.get_by(type_id=i.type_id, to_dict=False) cis = CI.get_by(type_id=i.type_id, to_dict=False)
for ci in cis: for ci in cis:
cim.update(ci.id, {}) cim.update(ci.id, {})
@celery.task(name="cmdb.write_ad_rule_sync_history", queue=CMDB_QUEUE)
@reconnect_db
def write_ad_rule_sync_history(rules, oneagent_id, oneagent_name, sync_at):
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryRuleSyncHistoryCRUD
for rule in rules:
AutoDiscoveryRuleSyncHistoryCRUD().upsert(adt_id=rule['id'],
oneagent_id=oneagent_id,
oneagent_name=oneagent_name,
sync_at=sync_at,
commit=False)
try:
db.session.commit()
except Exception as e:
current_app.logger.error("write auto discovery rule sync history failed: {}".format(e))
db.session.rollback()

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PROJECT VERSION\n" "Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2024-05-28 18:05+0800\n" "POT-Creation-Date: 2024-06-20 19:12+0800\n"
"PO-Revision-Date: 2023-12-25 20:21+0800\n" "PO-Revision-Date: 2023-12-25 20:21+0800\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: zh\n" "Language: zh\n"
@@ -81,7 +81,7 @@ msgid "User {} does not exist"
msgstr "用户 {} 不存在" msgstr "用户 {} 不存在"
#: api/lib/resp_format.py:32 #: api/lib/resp_format.py:32
msgid "You do not have {} permission for resource: {}!" msgid "For resource: {}, you do not have {} permission!"
msgstr "您没有资源: {} 的{}权限!" msgstr "您没有资源: {} 的{}权限!"
#: api/lib/resp_format.py:33 #: api/lib/resp_format.py:33
@@ -238,241 +238,245 @@ msgstr "因为CI已经存在不能删除模型"
msgid "The inheritance cannot be deleted because the CI already exists" msgid "The inheritance cannot be deleted because the CI already exists"
msgstr "因为CI已经存在不能删除继承关系" msgstr "因为CI已经存在不能删除继承关系"
#: api/lib/cmdb/resp_format.py:67 #: api/lib/cmdb/resp_format.py:65
msgid "The model is inherited and cannot be deleted"
msgstr "该模型被继承, 不能删除"
#: api/lib/cmdb/resp_format.py:68
msgid "" msgid ""
"The model cannot be deleted because the model is referenced by the " "The model cannot be deleted because the model is referenced by the "
"relational view {}" "relational view {}"
msgstr "因为关系视图 {} 引用了该模型,不能删除模型" msgstr "因为关系视图 {} 引用了该模型,不能删除模型"
#: api/lib/cmdb/resp_format.py:69 #: api/lib/cmdb/resp_format.py:70
msgid "Model group {} does not exist" msgid "Model group {} does not exist"
msgstr "模型分组 {} 不存在" msgstr "模型分组 {} 不存在"
#: api/lib/cmdb/resp_format.py:70 #: api/lib/cmdb/resp_format.py:71
msgid "Model group {} already exists" msgid "Model group {} already exists"
msgstr "模型分组 {} 已经存在" msgstr "模型分组 {} 已经存在"
#: api/lib/cmdb/resp_format.py:71 #: api/lib/cmdb/resp_format.py:72
msgid "Model relationship {} does not exist" msgid "Model relationship {} does not exist"
msgstr "模型关系 {} 不存在" msgstr "模型关系 {} 不存在"
#: api/lib/cmdb/resp_format.py:72 #: api/lib/cmdb/resp_format.py:73
msgid "Attribute group {} already exists" msgid "Attribute group {} already exists"
msgstr "属性分组 {} 已存在" msgstr "属性分组 {} 已存在"
#: api/lib/cmdb/resp_format.py:73 #: api/lib/cmdb/resp_format.py:74
msgid "Attribute group {} does not exist" msgid "Attribute group {} does not exist"
msgstr "属性分组 {} 不存在" msgstr "属性分组 {} 不存在"
#: api/lib/cmdb/resp_format.py:75 #: api/lib/cmdb/resp_format.py:76
msgid "Attribute group <{0}> - attribute <{1}> does not exist" msgid "Attribute group <{0}> - attribute <{1}> does not exist"
msgstr "属性组<{0}> - 属性<{1}> 不存在" msgstr "属性组<{0}> - 属性<{1}> 不存在"
#: api/lib/cmdb/resp_format.py:76 #: api/lib/cmdb/resp_format.py:77
msgid "The unique constraint already exists!" msgid "The unique constraint already exists!"
msgstr "唯一约束已经存在!" msgstr "唯一约束已经存在!"
#: api/lib/cmdb/resp_format.py:78 #: api/lib/cmdb/resp_format.py:79
msgid "Uniquely constrained attributes cannot be JSON and multi-valued" msgid "Uniquely constrained attributes cannot be JSON and multi-valued"
msgstr "唯一约束的属性不能是 JSON 和 多值" msgstr "唯一约束的属性不能是 JSON 和 多值"
#: api/lib/cmdb/resp_format.py:79 #: api/lib/cmdb/resp_format.py:80
msgid "Duplicated trigger" msgid "Duplicated trigger"
msgstr "重复的触发器" msgstr "重复的触发器"
#: api/lib/cmdb/resp_format.py:80 #: api/lib/cmdb/resp_format.py:81
msgid "Trigger {} does not exist" msgid "Trigger {} does not exist"
msgstr "触发器 {} 不存在" msgstr "触发器 {} 不存在"
#: api/lib/cmdb/resp_format.py:81 #: api/lib/cmdb/resp_format.py:82
msgid "Duplicated reconciliation rule" msgid "Duplicated reconciliation rule"
msgstr "" msgstr ""
#: api/lib/cmdb/resp_format.py:82 #: api/lib/cmdb/resp_format.py:83
msgid "Reconciliation rule {} does not exist" msgid "Reconciliation rule {} does not exist"
msgstr "关系类型 {} 不存在" msgstr "关系类型 {} 不存在"
#: api/lib/cmdb/resp_format.py:84 #: api/lib/cmdb/resp_format.py:85
msgid "Operation record {} does not exist" msgid "Operation record {} does not exist"
msgstr "操作记录 {} 不存在" msgstr "操作记录 {} 不存在"
#: api/lib/cmdb/resp_format.py:85 #: api/lib/cmdb/resp_format.py:86
msgid "Unique identifier cannot be deleted" msgid "Unique identifier cannot be deleted"
msgstr "不能删除唯一标识" msgstr "不能删除唯一标识"
#: api/lib/cmdb/resp_format.py:86 #: api/lib/cmdb/resp_format.py:87
msgid "Cannot delete default sorted attributes" msgid "Cannot delete default sorted attributes"
msgstr "不能删除默认排序的属性" msgstr "不能删除默认排序的属性"
#: api/lib/cmdb/resp_format.py:88 #: api/lib/cmdb/resp_format.py:89
msgid "No node selected" msgid "No node selected"
msgstr "没有选择节点" msgstr "没有选择节点"
#: api/lib/cmdb/resp_format.py:89 #: api/lib/cmdb/resp_format.py:90
msgid "This search option does not exist!" msgid "This search option does not exist!"
msgstr "该搜索选项不存在!" msgstr "该搜索选项不存在!"
#: api/lib/cmdb/resp_format.py:90 #: api/lib/cmdb/resp_format.py:91
msgid "This search option has a duplicate name!" msgid "This search option has a duplicate name!"
msgstr "该搜索选项命名重复!" msgstr "该搜索选项命名重复!"
#: api/lib/cmdb/resp_format.py:92 #: api/lib/cmdb/resp_format.py:93
msgid "Relationship type {} already exists" msgid "Relationship type {} already exists"
msgstr "关系类型 {} 已经存在" msgstr "关系类型 {} 已经存在"
#: api/lib/cmdb/resp_format.py:93 #: api/lib/cmdb/resp_format.py:94
msgid "Relationship type {} does not exist" msgid "Relationship type {} does not exist"
msgstr "关系类型 {} 不存在" msgstr "关系类型 {} 不存在"
#: api/lib/cmdb/resp_format.py:95 #: api/lib/cmdb/resp_format.py:96
msgid "Invalid attribute value: {}" msgid "Invalid attribute value: {}"
msgstr "无效的属性值: {}" msgstr "无效的属性值: {}"
#: api/lib/cmdb/resp_format.py:96 #: api/lib/cmdb/resp_format.py:97
msgid "{} Invalid value: {}" msgid "{} Invalid value: {}"
msgstr "{} 无效的值: {}" msgstr "{} 无效的值: {}"
#: api/lib/cmdb/resp_format.py:97 #: api/lib/cmdb/resp_format.py:98
msgid "{} is not in the predefined values" msgid "{} is not in the predefined values"
msgstr "{} 不在预定义值里" msgstr "{} 不在预定义值里"
#: api/lib/cmdb/resp_format.py:99 #: api/lib/cmdb/resp_format.py:100
msgid "The value of attribute {} must be unique, {} already exists" msgid "The value of attribute {} must be unique, {} already exists"
msgstr "属性 {} 的值必须是唯一的, 当前值 {} 已存在" msgstr "属性 {} 的值必须是唯一的, 当前值 {} 已存在"
#: api/lib/cmdb/resp_format.py:100 #: api/lib/cmdb/resp_format.py:101
msgid "Attribute {} value must exist" msgid "Attribute {} value must exist"
msgstr "属性 {} 值必须存在" msgstr "属性 {} 值必须存在"
#: api/lib/cmdb/resp_format.py:101 #: api/lib/cmdb/resp_format.py:102
msgid "Out of range value, the maximum value is 2147483647" msgid "Out of range value, the maximum value is 2147483647"
msgstr "超过最大值限制, 最大值是2147483647" msgstr "超过最大值限制, 最大值是2147483647"
#: api/lib/cmdb/resp_format.py:103 #: api/lib/cmdb/resp_format.py:104
msgid "Unknown error when adding or modifying attribute value: {}" msgid "Unknown error when adding or modifying attribute value: {}"
msgstr "新增或者修改属性值未知错误: {}" msgstr "新增或者修改属性值未知错误: {}"
#: api/lib/cmdb/resp_format.py:105 #: api/lib/cmdb/resp_format.py:106
msgid "Duplicate custom name" msgid "Duplicate custom name"
msgstr "订制名重复" msgstr "订制名重复"
#: api/lib/cmdb/resp_format.py:107 #: api/lib/cmdb/resp_format.py:108
msgid "Number of models exceeds limit: {}" msgid "Number of models exceeds limit: {}"
msgstr "模型数超过限制: {}" msgstr "模型数超过限制: {}"
#: api/lib/cmdb/resp_format.py:108 #: api/lib/cmdb/resp_format.py:109
msgid "The number of CIs exceeds the limit: {}" msgid "The number of CIs exceeds the limit: {}"
msgstr "CI数超过限制: {}" msgstr "CI数超过限制: {}"
#: api/lib/cmdb/resp_format.py:110 #: api/lib/cmdb/resp_format.py:111
msgid "Auto-discovery rule: {} already exists!" msgid "Auto-discovery rule: {} already exists!"
msgstr "自动发现规则: {} 已经存在!" msgstr "自动发现规则: {} 已经存在!"
#: api/lib/cmdb/resp_format.py:111 #: api/lib/cmdb/resp_format.py:112
msgid "Auto-discovery rule: {} does not exist!" msgid "Auto-discovery rule: {} does not exist!"
msgstr "自动发现规则: {} 不存在!" msgstr "自动发现规则: {} 不存在!"
#: api/lib/cmdb/resp_format.py:113 #: api/lib/cmdb/resp_format.py:114
msgid "This auto-discovery rule is referenced by the model and cannot be deleted!" msgid "This auto-discovery rule is referenced by the model and cannot be deleted!"
msgstr "该自动发现规则被模型引用, 不能删除!" msgstr "该自动发现规则被模型引用, 不能删除!"
#: api/lib/cmdb/resp_format.py:115 #: api/lib/cmdb/resp_format.py:116
msgid "The application of auto-discovery rules cannot be defined repeatedly!" msgid "The application of auto-discovery rules cannot be defined repeatedly!"
msgstr "自动发现规则的应用不能重复定义!" msgstr "自动发现规则的应用不能重复定义!"
#: api/lib/cmdb/resp_format.py:116 #: api/lib/cmdb/resp_format.py:117
msgid "The auto-discovery you want to modify: {} does not exist!" msgid "The auto-discovery you want to modify: {} does not exist!"
msgstr "您要修改的自动发现: {} 不存在!" msgstr "您要修改的自动发现: {} 不存在!"
#: api/lib/cmdb/resp_format.py:117 #: api/lib/cmdb/resp_format.py:118
msgid "Attribute does not include unique identifier: {}" msgid "Attribute does not include unique identifier: {}"
msgstr "属性字段没有包括唯一标识: {}" msgstr "属性字段没有包括唯一标识: {}"
#: api/lib/cmdb/resp_format.py:118 #: api/lib/cmdb/resp_format.py:119
msgid "The auto-discovery instance does not exist!" msgid "The auto-discovery instance does not exist!"
msgstr "自动发现的实例不存在!" msgstr "自动发现的实例不存在!"
#: api/lib/cmdb/resp_format.py:119 #: api/lib/cmdb/resp_format.py:120
msgid "The model is not associated with this auto-discovery!" msgid "The model is not associated with this auto-discovery!"
msgstr "模型并未关联该自动发现!" msgstr "模型并未关联该自动发现!"
#: api/lib/cmdb/resp_format.py:120 #: api/lib/cmdb/resp_format.py:121
msgid "Only the creator can modify the Secret!" msgid "Only the creator can modify the Secret!"
msgstr "只有创建人才能修改Secret!" msgstr "只有创建人才能修改Secret!"
#: api/lib/cmdb/resp_format.py:122 #: api/lib/cmdb/resp_format.py:123
msgid "This rule already has auto-discovery instances and cannot be deleted!" msgid "This rule already has auto-discovery instances and cannot be deleted!"
msgstr "该规则已经有自动发现的实例, 不能被删除!" msgstr "该规则已经有自动发现的实例, 不能被删除!"
#: api/lib/cmdb/resp_format.py:124 #: api/lib/cmdb/resp_format.py:125
msgid "The default auto-discovery rule is already referenced by model {}!" msgid "The default auto-discovery rule is already referenced by model {}!"
msgstr "该默认的自动发现规则 已经被模型 {} 引用!" msgstr "该默认的自动发现规则 已经被模型 {} 引用!"
#: api/lib/cmdb/resp_format.py:126 #: api/lib/cmdb/resp_format.py:127
msgid "The unique_key method must return a non-empty string!" msgid "The unique_key method must return a non-empty string!"
msgstr "unique_key方法必须返回非空字符串!" msgstr "unique_key方法必须返回非空字符串!"
#: api/lib/cmdb/resp_format.py:127 #: api/lib/cmdb/resp_format.py:128
msgid "The attributes method must return a list" msgid "The attributes method must return a list"
msgstr "attributes方法必须返回的是list" msgstr "attributes方法必须返回的是list"
#: api/lib/cmdb/resp_format.py:129 #: api/lib/cmdb/resp_format.py:130
msgid "The list returned by the attributes method cannot be empty!" msgid "The list returned by the attributes method cannot be empty!"
msgstr "attributes方法返回的list不能为空!" msgstr "attributes方法返回的list不能为空!"
#: api/lib/cmdb/resp_format.py:131 #: api/lib/cmdb/resp_format.py:132
msgid "Only administrators can define execution targets as: all nodes!" msgid "Only administrators can define execution targets as: all nodes!"
msgstr "只有管理员才可以定义执行机器为: 所有节点!" msgstr "只有管理员才可以定义执行机器为: 所有节点!"
#: api/lib/cmdb/resp_format.py:132 #: api/lib/cmdb/resp_format.py:133
msgid "Execute targets permission check failed: {}" msgid "Execute targets permission check failed: {}"
msgstr "执行机器权限检查不通过: {}" msgstr "执行机器权限检查不通过: {}"
#: api/lib/cmdb/resp_format.py:134 #: api/lib/cmdb/resp_format.py:135
msgid "CI filter authorization must be named!" msgid "CI filter authorization must be named!"
msgstr "CI过滤授权 必须命名!" msgstr "CI过滤授权 必须命名!"
#: api/lib/cmdb/resp_format.py:135 #: api/lib/cmdb/resp_format.py:136
msgid "CI filter authorization is currently not supported or query" msgid "CI filter authorization is currently not supported or query"
msgstr "CI过滤授权 暂时不支持 或 查询" msgstr "CI过滤授权 暂时不支持 或 查询"
#: api/lib/cmdb/resp_format.py:138 #: api/lib/cmdb/resp_format.py:139
msgid "You do not have permission to operate attribute {}!" msgid "You do not have permission to operate attribute {}!"
msgstr "您没有属性 {} 的操作权限!" msgstr "您没有属性 {} 的操作权限!"
#: api/lib/cmdb/resp_format.py:139 #: api/lib/cmdb/resp_format.py:140
msgid "You do not have permission to operate this CI!" msgid "You do not have permission to operate this CI!"
msgstr "您没有该CI的操作权限!" msgstr "您没有该CI的操作权限!"
#: api/lib/cmdb/resp_format.py:141 #: api/lib/cmdb/resp_format.py:142
msgid "Failed to save password: {}" msgid "Failed to save password: {}"
msgstr "保存密码失败: {}" msgstr "保存密码失败: {}"
#: api/lib/cmdb/resp_format.py:142 #: api/lib/cmdb/resp_format.py:143
msgid "Failed to get password: {}" msgid "Failed to get password: {}"
msgstr "获取密码失败: {}" msgstr "获取密码失败: {}"
#: api/lib/cmdb/resp_format.py:144 #: api/lib/cmdb/resp_format.py:145
msgid "Scheduling time format error" msgid "Scheduling time format error"
msgstr "{}格式错误,应该为:%Y-%m-%d %H:%M:%S" msgstr "{}格式错误,应该为:%Y-%m-%d %H:%M:%S"
#: api/lib/cmdb/resp_format.py:145 #: api/lib/cmdb/resp_format.py:146
msgid "CMDB data reconciliation results" msgid "CMDB data reconciliation results"
msgstr "" msgstr ""
#: api/lib/cmdb/resp_format.py:146 #: api/lib/cmdb/resp_format.py:147
msgid "Number of {} illegal: {}" msgid "Number of {} illegal: {}"
msgstr "" msgstr ""
#: api/lib/cmdb/resp_format.py:148 #: api/lib/cmdb/resp_format.py:149
msgid "Topology view {} already exists" msgid "Topology view {} already exists"
msgstr "拓扑视图 {} 已经存在" msgstr "拓扑视图 {} 已经存在"
#: api/lib/cmdb/resp_format.py:149 #: api/lib/cmdb/resp_format.py:150
msgid "Topology group {} already exists" msgid "Topology group {} already exists"
msgstr "拓扑视图分组 {} 已经存在" msgstr "拓扑视图分组 {} 已经存在"
#: api/lib/cmdb/resp_format.py:151 #: api/lib/cmdb/resp_format.py:152
msgid "The group cannot be deleted because the topology view already exists" msgid "The group cannot be deleted because the topology view already exists"
msgstr "因为该分组下定义了拓扑视图,不能删除" msgstr "因为该分组下定义了拓扑视图,不能删除"

View File

@@ -1,24 +1,30 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
import copy
import json import json
from io import BytesIO import uuid
from flask import abort from flask import abort
from flask import current_app from flask import current_app
from flask import request from flask import request
from flask_login import current_user from flask_login import current_user
from io import BytesIO
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCICRUD from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCICRUD
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCITypeCRUD from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCITypeCRUD
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCITypeRelationCRUD
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryComponentsManager
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryCounterCRUD
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryExecHistoryCRUD
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryHTTPManager from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryHTTPManager
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryRuleCRUD from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryRuleCRUD
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoveryRuleSyncHistoryCRUD
from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoverySNMPManager from api.lib.cmdb.auto_discovery.auto_discovery import AutoDiscoverySNMPManager
from api.lib.cmdb.auto_discovery.const import DEFAULT_HTTP from api.lib.cmdb.auto_discovery.const import DEFAULT_INNER
from api.lib.cmdb.auto_discovery.const import PRIVILEGED_USERS
from api.lib.cmdb.const import PermEnum from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.resp_format import ErrFormat from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.search import SearchError 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.decorator import args_required
from api.lib.decorator import args_validate from api.lib.decorator import args_validate
from api.lib.perm.acl.acl import has_perm_from_args from api.lib.perm.acl.acl import has_perm_from_args
@@ -37,14 +43,19 @@ class AutoDiscoveryRuleView(APIView):
rebuild = False rebuild = False
exists = {i['name'] for i in res} exists = {i['name'] for i in res}
for i in DEFAULT_HTTP: for i in copy.deepcopy(DEFAULT_INNER):
if i['name'] not in exists: if i['name'] not in exists:
i.pop('en', None)
AutoDiscoveryRuleCRUD().add(**i) AutoDiscoveryRuleCRUD().add(**i)
rebuild = True rebuild = True
if rebuild: if rebuild:
_, res = AutoDiscoveryRuleCRUD.search(page=1, page_size=100000, **request.values) _, res = AutoDiscoveryRuleCRUD.search(page=1, page_size=100000, **request.values)
for i in res:
if i['type'] == 'http':
i['resources'] = AutoDiscoveryHTTPManager().get_resources(i['name'])
return self.jsonify(res) return self.jsonify(res)
@args_required("name", value_required=True) @args_required("name", value_required=True)
@@ -98,24 +109,34 @@ class AutoDiscoveryRuleTemplateFileView(APIView):
class AutoDiscoveryRuleHTTPView(APIView): class AutoDiscoveryRuleHTTPView(APIView):
url_prefix = ("/adr/http/<string:name>/categories", "/adr/http/<string:name>/attributes", url_prefix = ("/adr/http/<string:name>/categories",
"/adr/snmp/<string:name>/attributes") "/adr/http/<string:name>/attributes",
"/adr/snmp/<string:name>/attributes",
"/adr/components/<string:name>/attributes",)
def get(self, name): def get(self, name):
if "snmp" in request.url: if "snmp" in request.url:
return self.jsonify(AutoDiscoverySNMPManager.get_attributes()) return self.jsonify(AutoDiscoverySNMPManager.get_attributes())
if "components" in request.url:
return self.jsonify(AutoDiscoveryComponentsManager.get_attributes(name))
if "attributes" in request.url: if "attributes" in request.url:
category = request.values.get('category') resource = request.values.get('resource')
return self.jsonify(AutoDiscoveryHTTPManager.get_attributes(name, category)) return self.jsonify(AutoDiscoveryHTTPManager.get_attributes(name, resource))
return self.jsonify(AutoDiscoveryHTTPManager.get_categories(name)) return self.jsonify(AutoDiscoveryHTTPManager.get_categories(name))
class AutoDiscoveryCITypeView(APIView): class AutoDiscoveryCITypeView(APIView):
url_prefix = ("/adt/ci_types/<int:type_id>", "/adt/<int:adt_id>") url_prefix = ("/adt/ci_types/<int:type_id>",
"/adt/ci_types/<int:type_id>/attributes",
"/adt/<int:adt_id>")
def get(self, type_id): def get(self, type_id):
if "attributes" in request.url:
return self.jsonify(AutoDiscoveryCITypeCRUD.get_ad_attributes(type_id))
_, res = AutoDiscoveryCITypeCRUD.search(page=1, page_size=100000, type_id=type_id, **request.values) _, res = AutoDiscoveryCITypeCRUD.search(page=1, page_size=100000, type_id=type_id, **request.values)
for i in res: for i in res:
if isinstance(i.get("extra_option"), dict) and i['extra_option'].get('secret'): if isinstance(i.get("extra_option"), dict) and i['extra_option'].get('secret'):
@@ -146,6 +167,27 @@ class AutoDiscoveryCITypeView(APIView):
return self.jsonify(adt_id=adt_id) return self.jsonify(adt_id=adt_id)
class AutoDiscoveryCITypeRelationView(APIView):
url_prefix = ("/adt/ci_types/<int:type_id>/relations", "/adt/relations/<int:_id>")
def get(self, type_id):
_, res = AutoDiscoveryCITypeRelationCRUD.search(page=1, page_size=100000, ad_type_id=type_id, **request.values)
return self.jsonify(res)
@args_required("relations")
def post(self, type_id):
return self.jsonify(AutoDiscoveryCITypeRelationCRUD().upsert(type_id, request.values['relations']))
def put(self):
return self.post()
def delete(self, _id):
AutoDiscoveryCITypeRelationCRUD().delete(_id)
return self.jsonify(id=_id)
class AutoDiscoveryCIView(APIView): class AutoDiscoveryCIView(APIView):
url_prefix = ("/adc", "/adc/<int:adc_id>", "/adc/ci_types/<int:type_id>/attributes", "/adc/ci_types") url_prefix = ("/adc", "/adc/<int:adc_id>", "/adc/ci_types/<int:type_id>/attributes", "/adc/ci_types")
@@ -213,16 +255,15 @@ class AutoDiscoveryRuleSyncView(APIView):
url_prefix = ("/adt/sync",) url_prefix = ("/adt/sync",)
def get(self): def get(self):
if current_user.username not in ("cmdb_agent", "worker", "admin"): if current_user.username not in PRIVILEGED_USERS:
return abort(403) return abort(403)
oneagent_name = request.values.get('oneagent_name') oneagent_name = request.values.get('oneagent_name')
oneagent_id = request.values.get('oneagent_id') oneagent_id = request.values.get('oneagent_id')
last_update_at = request.values.get('last_update_at') last_update_at = request.values.get('last_update_at')
query = "{},oneagent_id:{}".format(oneagent_name, oneagent_id) query = "oneagent_id:{}".format(oneagent_id)
current_app.logger.info(query) s = ci_search(query)
s = search(query)
try: try:
response, _, _, _, _, _ = s.search() response, _, _, _, _, _ = s.search()
except SearchError as e: except SearchError as e:
@@ -230,7 +271,77 @@ class AutoDiscoveryRuleSyncView(APIView):
current_app.logger.error(traceback.format_exc()) current_app.logger.error(traceback.format_exc())
return abort(400, str(e)) return abort(400, str(e))
ci_id = response and response[0]["_id"] for res in response:
rules, last_update_at = AutoDiscoveryCITypeCRUD.get(ci_id, oneagent_id, last_update_at) if res.get('{}_name'.format(res['ci_type'])) == oneagent_name or oneagent_name == res.get('oneagent_name'):
ci_id = res["_id"]
rules, last_update_at = AutoDiscoveryCITypeCRUD.get(ci_id, oneagent_id, oneagent_name, last_update_at)
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)
return self.jsonify(rules=rules, last_update_at=last_update_at) return self.jsonify(rules=rules, last_update_at=last_update_at)
class AutoDiscoveryRuleSyncHistoryView(APIView):
url_prefix = ("/adt/<int:adt_id>/sync/histories",)
def get(self, adt_id):
page = get_page(request.values.pop('page', 1))
page_size = get_page_size(request.values.pop('page_size', None))
numfound, res = AutoDiscoveryRuleSyncHistoryCRUD.search(page=page,
page_size=page_size,
adt_id=adt_id,
**request.values)
return self.jsonify(page=page,
page_size=page_size,
numfound=numfound,
total=len(res),
result=res)
class AutoDiscoveryTestView(APIView):
url_prefix = ("/adt/<int:adt_id>/test", "/adt/test/<string:exec_id>/result")
def get(self, exec_id):
return self.jsonify(stdout="1\n2\n3", exec_id=exec_id)
def post(self, adt_id):
return self.jsonify(exec_id=uuid.uuid4().hex)
class AutoDiscoveryExecHistoryView(APIView):
url_prefix = ("/adc/exec/histories",)
@args_required('type_id')
def get(self):
page = get_page(request.values.pop('page', 1))
page_size = get_page_size(request.values.pop('page_size', None))
numfound, res = AutoDiscoveryExecHistoryCRUD.search(page=page,
page_size=page_size,
**request.values)
return self.jsonify(page=page,
page_size=page_size,
numfound=numfound,
total=len(res),
result=res)
@args_required('type_id')
@args_required('stdout')
def post(self):
AutoDiscoveryExecHistoryCRUD().add(type_id=request.values.get('type_id'),
stdout=request.values.get('stdout'))
return self.jsonify(code=200)
class AutoDiscoveryCounterView(APIView):
url_prefix = ("/adc/counter",)
@args_required('type_id')
def get(self):
type_id = request.values.get('type_id')
return self.jsonify(AutoDiscoveryCounterCRUD().get(type_id))

View File

@@ -268,6 +268,7 @@ class CIBaselineView(APIView):
return self.jsonify(CIManager().baseline(list(map(int, ci_ids)), before_date)) return self.jsonify(CIManager().baseline(list(map(int, ci_ids)), before_date))
@args_required("before_date") @args_required("before_date")
@has_perm_for_ci("ci_id", ResourceTypeEnum.CI, PermEnum.UPDATE, CIManager.get_type)
def post(self, ci_id): def post(self, ci_id):
if 'rollback' in request.url: if 'rollback' in request.url:
before_date = request.values.get('before_date') before_date = request.values.get('before_date')

View File

@@ -51,7 +51,11 @@ class CITypeView(APIView):
q = request.args.get("type_name") q = request.args.get("type_name")
if type_id is not None: if type_id is not None:
ci_type = CITypeCache.get(type_id).to_dict() 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_type['parent_ids'] = CITypeInheritanceManager.get_parents(type_id)
ci_types = [ci_type] ci_types = [ci_type]
elif type_name is not None: elif type_name is not None:
@@ -357,15 +361,13 @@ class CITypeAttributeGroupView(APIView):
class CITypeTemplateView(APIView): class CITypeTemplateView(APIView):
url_prefix = ("/ci_types/template/import", "/ci_types/template/export", "/ci_types/<int:type_id>/template/export") url_prefix = ("/ci_types/template/import", "/ci_types/template/export")
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Model_Configuration, @perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Model_Configuration,
app_cli.op.download_CIType, app_cli.admin_name) app_cli.op.download_CIType, app_cli.admin_name)
def get(self, type_id=None): # export def get(self): # export
if type_id is not None: type_ids = list(map(int, handle_arg_list(request.values.get('type_ids')))) or None
return self.jsonify(dict(ci_type_template=CITypeTemplateManager.export_template_by_type(type_id))) return self.jsonify(dict(ci_type_template=CITypeTemplateManager.export_template(type_ids=type_ids)))
return self.jsonify(dict(ci_type_template=CITypeTemplateManager.export_template()))
@perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Model_Configuration, @perms_role_required(app_cli.app_name, app_cli.resource_type_name, app_cli.op.Model_Configuration,
app_cli.op.download_CIType, app_cli.admin_name) app_cli.op.download_CIType, app_cli.admin_name)

View File

@@ -57,10 +57,10 @@ class CITypeRelationView(APIView):
def post(self, parent_id, child_id): def post(self, parent_id, child_id):
relation_type_id = request.values.get("relation_type_id") relation_type_id = request.values.get("relation_type_id")
constraint = request.values.get("constraint") constraint = request.values.get("constraint")
parent_attr_id = request.values.get("parent_attr_id") parent_attr_ids = request.values.get("parent_attr_ids")
child_attr_id = request.values.get("child_attr_id") child_attr_ids = request.values.get("child_attr_ids")
ctr_id = CITypeRelationManager.add(parent_id, child_id, relation_type_id, constraint, ctr_id = CITypeRelationManager.add(parent_id, child_id, relation_type_id, constraint,
parent_attr_id, child_attr_id) parent_attr_ids, child_attr_ids)
return self.jsonify(ctr_id=ctr_id) return self.jsonify(ctr_id=ctr_id)

View File

@@ -31,6 +31,7 @@ marshmallow==2.20.2
more-itertools==5.0.0 more-itertools==5.0.0
msgpack-python==0.5.6 msgpack-python==0.5.6
Pillow>=10.0.1 Pillow>=10.0.1
pycryptodome==3.12.0
cryptography>=41.0.2 cryptography>=41.0.2
PyJWT==2.4.0 PyJWT==2.4.0
PyMySQL==1.1.0 PyMySQL==1.1.0

View File

@@ -59,7 +59,7 @@
"vue-template-compiler": "2.6.11", "vue-template-compiler": "2.6.11",
"vuedraggable": "^2.23.0", "vuedraggable": "^2.23.0",
"vuex": "^3.1.1", "vuex": "^3.1.1",
"vxe-table": "3.6.9", "vxe-table": "3.7.10",
"vxe-table-plugin-export-xlsx": "2.0.0", "vxe-table-plugin-export-xlsx": "2.0.0",
"xe-utils": "3", "xe-utils": "3",
"xlsx": "0.15.0", "xlsx": "0.15.0",

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
@font-face { @font-face {
font-family: "iconfont"; /* Project id 3857903 */ font-family: "iconfont"; /* Project id 3857903 */
src: url('iconfont.woff2?t=1716896994700') format('woff2'), src: url('iconfont.woff2?t=1719487341033') format('woff2'),
url('iconfont.woff?t=1716896994700') format('woff'), url('iconfont.woff?t=1719487341033') format('woff'),
url('iconfont.ttf?t=1716896994700') format('truetype'); url('iconfont.ttf?t=1719487341033') format('truetype');
} }
.iconfont { .iconfont {
@@ -13,6 +13,262 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.veops-markdown:before {
content: "\e96a";
}
.veops-bar_horizontal:before {
content: "\e860";
}
.veops-gauge:before {
content: "\e965";
}
.veops-heatmap:before {
content: "\e966";
}
.veops-treemap:before {
content: "\e967";
}
.veops-radar:before {
content: "\e968";
}
.veops-data:before {
content: "\e969";
}
.veops-import:before {
content: "\e963";
}
.veops-batch_operation:before {
content: "\e964";
}
.cmdb-enterprise_edition:before {
content: "\e962";
}
.ops-KVM:before {
content: "\e961";
}
.cmdb-vcenter:before {
content: "\e960";
}
.cmdb-manual_warehousing:before {
content: "\e95f";
}
.cmdb-not_warehousing:before {
content: "\e95d";
}
.cmdb-warehousing:before {
content: "\e95e";
}
.cmdb-prompt:before {
content: "\e95c";
}
.cmdb-arrow:before {
content: "\e95b";
}
.cmdb-automatic_inventory:before {
content: "\e95a";
}
.cmdb-week_additions:before {
content: "\e959";
}
.cmdb-month_additions:before {
content: "\e958";
}
.cmdb-rule:before {
content: "\e955";
}
.cmdb-executing_machine:before {
content: "\e956";
}
.cmdb-resource:before {
content: "\e957";
}
.cmdb-discovery_resources:before {
content: "\e954";
}
.cmdb-association:before {
content: "\e953";
}
.ops-is_dynamic-disabled:before {
content: "\e952";
}
.itsm-pdf:before {
content: "\e951";
}
.monitor-sqlserver:before {
content: "\e950";
}
.monitor-dig2:before {
content: "\e94d";
}
.monitor-base2:before {
content: "\e94e";
}
.monitor-foreground1:before {
content: "\e94f";
}
.monitor-log2:before {
content: "\e945";
}
.monitor-backgroud1:before {
content: "\e946";
}
.monitor-port1:before {
content: "\e947";
}
.monitor-ipmi2:before {
content: "\e948";
}
.monitor-process2:before {
content: "\e949";
}
.monitor-snmp2:before {
content: "\e94a";
}
.monitor-performance1:before {
content: "\e94b";
}
.monitor-testing1:before {
content: "\e94c";
}
.monitor-ping2:before {
content: "\e941";
}
.monitor-prometheus:before {
content: "\e942";
}
.monitor-websocket2:before {
content: "\e943";
}
.monitor-traceroute2:before {
content: "\e944";
}
.monitor-port:before {
content: "\e93c";
}
.monitor-base1:before {
content: "\e93d";
}
.monitor-backgroud:before {
content: "\e93e";
}
.monitor-dig1:before {
content: "\e93f";
}
.monitor-foreground:before {
content: "\e940";
}
.monitor-log1:before {
content: "\e934";
}
.monitor-process1:before {
content: "\e935";
}
.monitor-testing:before {
content: "\e936";
}
.monitor-snmp1:before {
content: "\e937";
}
.monitor-performance:before {
content: "\e938";
}
.monitor-traceroute1:before {
content: "\e939";
}
.monitor-ping1:before {
content: "\e93a";
}
.monitor-ipmi1:before {
content: "\e93b";
}
.a-monitor-prometheus1:before {
content: "\e932";
}
.monitor-websocket1:before {
content: "\e933";
}
.monitor-group_expansion1:before {
content: "\e930";
}
.monitor-group_collapse1:before {
content: "\e931";
}
.monitor-group_expansion:before {
content: "\e92e";
}
.monitor-group_collapse:before {
content: "\e92f";
}
.monitor-list_view:before {
content: "\e92d";
}
.monitor-group_view:before {
content: "\e92c";
}
.ops-topology_view:before { .ops-topology_view:before {
content: "\e92b"; content: "\e92b";
} }
@@ -21,7 +277,7 @@
content: "\e92a"; content: "\e92a";
} }
.a-Group427319324:before { .monitor-add2:before {
content: "\e929"; content: "\e929";
} }
@@ -125,10 +381,6 @@
content: "\e862"; content: "\e862";
} }
.a-veops-import1:before {
content: "\e860";
}
.monitor-ip:before { .monitor-ip:before {
content: "\e807"; content: "\e807";
} }

File diff suppressed because one or more lines are too long

View File

@@ -5,6 +5,454 @@
"css_prefix_text": "", "css_prefix_text": "",
"description": "", "description": "",
"glyphs": [ "glyphs": [
{
"icon_id": "40896913",
"name": "veops-markdown",
"font_class": "veops-markdown",
"unicode": "e96a",
"unicode_decimal": 59754
},
{
"icon_id": "40896859",
"name": "veops-bar_horizontal",
"font_class": "veops-bar_horizontal",
"unicode": "e860",
"unicode_decimal": 59488
},
{
"icon_id": "40896881",
"name": "veops-gauge",
"font_class": "veops-gauge",
"unicode": "e965",
"unicode_decimal": 59749
},
{
"icon_id": "40896882",
"name": "veops-heatmap",
"font_class": "veops-heatmap",
"unicode": "e966",
"unicode_decimal": 59750
},
{
"icon_id": "40896884",
"name": "veops-treemap",
"font_class": "veops-treemap",
"unicode": "e967",
"unicode_decimal": 59751
},
{
"icon_id": "40896887",
"name": "veops-radar",
"font_class": "veops-radar",
"unicode": "e968",
"unicode_decimal": 59752
},
{
"icon_id": "40896905",
"name": "veops-data",
"font_class": "veops-data",
"unicode": "e969",
"unicode_decimal": 59753
},
{
"icon_id": "40872369",
"name": "veops-import",
"font_class": "veops-import",
"unicode": "e963",
"unicode_decimal": 59747
},
{
"icon_id": "40872361",
"name": "veops-batch_operation",
"font_class": "veops-batch_operation",
"unicode": "e964",
"unicode_decimal": 59748
},
{
"icon_id": "40834860",
"name": "cmdb-enterprise_edition",
"font_class": "cmdb-enterprise_edition",
"unicode": "e962",
"unicode_decimal": 59746
},
{
"icon_id": "40832458",
"name": "ops-KVM",
"font_class": "ops-KVM",
"unicode": "e961",
"unicode_decimal": 59745
},
{
"icon_id": "40822644",
"name": "cmdb-vcenter",
"font_class": "cmdb-vcenter",
"unicode": "e960",
"unicode_decimal": 59744
},
{
"icon_id": "40795271",
"name": "cmdb-manual_warehousing",
"font_class": "cmdb-manual_warehousing",
"unicode": "e95f",
"unicode_decimal": 59743
},
{
"icon_id": "40791408",
"name": "cmdb-not_warehousing",
"font_class": "cmdb-not_warehousing",
"unicode": "e95d",
"unicode_decimal": 59741
},
{
"icon_id": "40791401",
"name": "cmdb-warehousing",
"font_class": "cmdb-warehousing",
"unicode": "e95e",
"unicode_decimal": 59742
},
{
"icon_id": "40731588",
"name": "cmdb-prompt",
"font_class": "cmdb-prompt",
"unicode": "e95c",
"unicode_decimal": 59740
},
{
"icon_id": "40722326",
"name": "cmdb-arrow",
"font_class": "cmdb-arrow",
"unicode": "e95b",
"unicode_decimal": 59739
},
{
"icon_id": "40711364",
"name": "cmdb-automatic_inventory",
"font_class": "cmdb-automatic_inventory",
"unicode": "e95a",
"unicode_decimal": 59738
},
{
"icon_id": "40711409",
"name": "cmdb-week_additions",
"font_class": "cmdb-week_additions",
"unicode": "e959",
"unicode_decimal": 59737
},
{
"icon_id": "40711428",
"name": "cmdb-month_additions",
"font_class": "cmdb-month_additions",
"unicode": "e958",
"unicode_decimal": 59736
},
{
"icon_id": "40711344",
"name": "cmdb-rule",
"font_class": "cmdb-rule",
"unicode": "e955",
"unicode_decimal": 59733
},
{
"icon_id": "40711349",
"name": "cmdb-executing_machine",
"font_class": "cmdb-executing_machine",
"unicode": "e956",
"unicode_decimal": 59734
},
{
"icon_id": "40711356",
"name": "cmdb-resource",
"font_class": "cmdb-resource",
"unicode": "e957",
"unicode_decimal": 59735
},
{
"icon_id": "40705423",
"name": "cmdb-discovery_resources",
"font_class": "cmdb-discovery_resources",
"unicode": "e954",
"unicode_decimal": 59732
},
{
"icon_id": "40701723",
"name": "cmdb-association",
"font_class": "cmdb-association",
"unicode": "e953",
"unicode_decimal": 59731
},
{
"icon_id": "40645466",
"name": "ops-is_dynamic-disabled",
"font_class": "ops-is_dynamic-disabled",
"unicode": "e952",
"unicode_decimal": 59730
},
{
"icon_id": "40590472",
"name": "itsm-pdf",
"font_class": "itsm-pdf",
"unicode": "e951",
"unicode_decimal": 59729
},
{
"icon_id": "40552176",
"name": "monitor-sqlserver",
"font_class": "monitor-sqlserver",
"unicode": "e950",
"unicode_decimal": 59728
},
{
"icon_id": "40548499",
"name": "monitor-dig",
"font_class": "monitor-dig2",
"unicode": "e94d",
"unicode_decimal": 59725
},
{
"icon_id": "40548507",
"name": "monitor-base",
"font_class": "monitor-base2",
"unicode": "e94e",
"unicode_decimal": 59726
},
{
"icon_id": "40548498",
"name": "monitor-foreground",
"font_class": "monitor-foreground1",
"unicode": "e94f",
"unicode_decimal": 59727
},
{
"icon_id": "40548504",
"name": "monitor-log",
"font_class": "monitor-log2",
"unicode": "e945",
"unicode_decimal": 59717
},
{
"icon_id": "40548508",
"name": "monitor-backgroud",
"font_class": "monitor-backgroud1",
"unicode": "e946",
"unicode_decimal": 59718
},
{
"icon_id": "40548502",
"name": "monitor-port",
"font_class": "monitor-port1",
"unicode": "e947",
"unicode_decimal": 59719
},
{
"icon_id": "40548501",
"name": "monitor-ipmi",
"font_class": "monitor-ipmi2",
"unicode": "e948",
"unicode_decimal": 59720
},
{
"icon_id": "40548511",
"name": "monitor-process",
"font_class": "monitor-process2",
"unicode": "e949",
"unicode_decimal": 59721
},
{
"icon_id": "40548506",
"name": "monitor-snmp",
"font_class": "monitor-snmp2",
"unicode": "e94a",
"unicode_decimal": 59722
},
{
"icon_id": "40548500",
"name": "monitor-performance",
"font_class": "monitor-performance1",
"unicode": "e94b",
"unicode_decimal": 59723
},
{
"icon_id": "40548510",
"name": "monitor-testing",
"font_class": "monitor-testing1",
"unicode": "e94c",
"unicode_decimal": 59724
},
{
"icon_id": "40548503",
"name": "monitor-ping",
"font_class": "monitor-ping2",
"unicode": "e941",
"unicode_decimal": 59713
},
{
"icon_id": "40548509",
"name": "monitor-prometheus",
"font_class": "monitor-prometheus",
"unicode": "e942",
"unicode_decimal": 59714
},
{
"icon_id": "40548505",
"name": "monitor-websocket",
"font_class": "monitor-websocket2",
"unicode": "e943",
"unicode_decimal": 59715
},
{
"icon_id": "40548512",
"name": "monitor-traceroute",
"font_class": "monitor-traceroute2",
"unicode": "e944",
"unicode_decimal": 59716
},
{
"icon_id": "40548205",
"name": "monitor-port",
"font_class": "monitor-port",
"unicode": "e93c",
"unicode_decimal": 59708
},
{
"icon_id": "40548204",
"name": "monitor-base",
"font_class": "monitor-base1",
"unicode": "e93d",
"unicode_decimal": 59709
},
{
"icon_id": "40548203",
"name": "monitor-backgroud",
"font_class": "monitor-backgroud",
"unicode": "e93e",
"unicode_decimal": 59710
},
{
"icon_id": "40548202",
"name": "monitor-dig",
"font_class": "monitor-dig1",
"unicode": "e93f",
"unicode_decimal": 59711
},
{
"icon_id": "40548201",
"name": "monitor-foreground",
"font_class": "monitor-foreground",
"unicode": "e940",
"unicode_decimal": 59712
},
{
"icon_id": "40548213",
"name": "monitor-log",
"font_class": "monitor-log1",
"unicode": "e934",
"unicode_decimal": 59700
},
{
"icon_id": "40548212",
"name": "monitor-process",
"font_class": "monitor-process1",
"unicode": "e935",
"unicode_decimal": 59701
},
{
"icon_id": "40548211",
"name": "monitor-testing",
"font_class": "monitor-testing",
"unicode": "e936",
"unicode_decimal": 59702
},
{
"icon_id": "40548210",
"name": "monitor-snmp",
"font_class": "monitor-snmp1",
"unicode": "e937",
"unicode_decimal": 59703
},
{
"icon_id": "40548209",
"name": "monitor-performance",
"font_class": "monitor-performance",
"unicode": "e938",
"unicode_decimal": 59704
},
{
"icon_id": "40548208",
"name": "monitor-traceroute",
"font_class": "monitor-traceroute1",
"unicode": "e939",
"unicode_decimal": 59705
},
{
"icon_id": "40548207",
"name": "monitor-ping",
"font_class": "monitor-ping1",
"unicode": "e93a",
"unicode_decimal": 59706
},
{
"icon_id": "40548206",
"name": "monitor-ipmi",
"font_class": "monitor-ipmi1",
"unicode": "e93b",
"unicode_decimal": 59707
},
{
"icon_id": "40548217",
"name": "monitor-prometheus (1)",
"font_class": "a-monitor-prometheus1",
"unicode": "e932",
"unicode_decimal": 59698
},
{
"icon_id": "40548214",
"name": "monitor-websocket",
"font_class": "monitor-websocket1",
"unicode": "e933",
"unicode_decimal": 59699
},
{
"icon_id": "40521692",
"name": "monitor-group_expansion",
"font_class": "monitor-group_expansion1",
"unicode": "e930",
"unicode_decimal": 59696
},
{
"icon_id": "40521691",
"name": "monitor-group_collapse",
"font_class": "monitor-group_collapse1",
"unicode": "e931",
"unicode_decimal": 59697
},
{
"icon_id": "40520774",
"name": "monitor-group_expansion",
"font_class": "monitor-group_expansion",
"unicode": "e92e",
"unicode_decimal": 59694
},
{
"icon_id": "40520787",
"name": "monitor-group_collapse",
"font_class": "monitor-group_collapse",
"unicode": "e92f",
"unicode_decimal": 59695
},
{
"icon_id": "40519707",
"name": "monitor-list_view",
"font_class": "monitor-list_view",
"unicode": "e92d",
"unicode_decimal": 59693
},
{
"icon_id": "40519711",
"name": "monitor-group_view",
"font_class": "monitor-group_view",
"unicode": "e92c",
"unicode_decimal": 59692
},
{ {
"icon_id": "40499246", "icon_id": "40499246",
"name": "ops-topology_view", "name": "ops-topology_view",
@@ -22,7 +470,7 @@
{ {
"icon_id": "40372105", "icon_id": "40372105",
"name": "monitor-add2", "name": "monitor-add2",
"font_class": "a-Group427319324", "font_class": "monitor-add2",
"unicode": "e929", "unicode": "e929",
"unicode_decimal": 59689 "unicode_decimal": 59689
}, },
@@ -201,13 +649,6 @@
"unicode": "e862", "unicode": "e862",
"unicode_decimal": 59490 "unicode_decimal": 59490
}, },
{
"icon_id": "40306881",
"name": "veops-import",
"font_class": "a-veops-import1",
"unicode": "e860",
"unicode_decimal": 59488
},
{ {
"icon_id": "40262335", "icon_id": "40262335",
"name": "monitor-ip (1)", "name": "monitor-ip (1)",

Binary file not shown.

View File

@@ -10,10 +10,10 @@ export const regList = () => {
{ id: 'landline', label: i18n.t('regexSelect.landline'), value: '^(?:(?:\\d{3}-)?\\d{8}|^(?:\\d{4}-)?\\d{7,8})(?:-\\d+)?$', message: '请输入正确座机' }, { id: 'landline', label: i18n.t('regexSelect.landline'), value: '^(?:(?:\\d{3}-)?\\d{8}|^(?:\\d{4}-)?\\d{7,8})(?:-\\d+)?$', message: '请输入正确座机' },
{ id: 'zipCode', label: i18n.t('regexSelect.zipCode'), value: '^(0[1-7]|1[0-356]|2[0-7]|3[0-6]|4[0-7]|5[1-7]|6[1-7]|7[0-5]|8[013-6])\\d{4}$', message: '请输入正确邮政编码' }, { id: 'zipCode', label: i18n.t('regexSelect.zipCode'), value: '^(0[1-7]|1[0-356]|2[0-7]|3[0-6]|4[0-7]|5[1-7]|6[1-7]|7[0-5]|8[013-6])\\d{4}$', message: '请输入正确邮政编码' },
{ id: 'IDCard', label: i18n.t('regexSelect.IDCard'), value: '(^[1-9]\\d{5}(18|19|([23]\\d))\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$)|(^[1-9]\\d{5}\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}$)', message: '请输入正确身份证号' }, { id: 'IDCard', label: i18n.t('regexSelect.IDCard'), value: '(^[1-9]\\d{5}(18|19|([23]\\d))\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$)|(^[1-9]\\d{5}\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}$)', message: '请输入正确身份证号' },
{ id: 'ip', label: i18n.t('regexSelect.ip'), value: '^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$', message: '请输入正确IP地址' }, { id: 'ip', label: i18n.t('regexSelect.ip'), value: '^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$', message: '请输入正确IP地址' },
{ id: 'email', label: i18n.t('regexSelect.email'), value: '^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\.\\w+([-.]\\w+)*$', message: '请输入正确邮箱' }, { id: 'email', label: i18n.t('regexSelect.email'), value: '^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$', message: '请输入正确邮箱' },
{ id: 'link', label: i18n.t('regexSelect.link'), value: '^(https:\/\/www\.|http:\/\/www\.|https:\/\/|http:\/\/)?[a-zA-Z0-9]{2,}(\.[a-zA-Z0-9]{2,})(\.[a-zA-Z0-9]{2,})?$', message: '请输入链接' }, { id: 'link', label: i18n.t('regexSelect.link'), value: '^(https:\/\/www\\.|http:\/\/www\\.|https:\/\/|http:\/\/)?[a-zA-Z0-9]{2,}(\\.[a-zA-Z0-9]{2,})(\\.[a-zA-Z0-9]{2,})?$', message: '请输入链接' },
{ id: 'monetaryAmount', label: i18n.t('regexSelect.monetaryAmount'), value: '^-?\\d+(,\\d{3})*(\.\\d{1,2})?$', message: '请输入货币金额' }, { id: 'monetaryAmount', label: i18n.t('regexSelect.monetaryAmount'), value: '^-?\\d+(,\\d{3})*(\\.\\d{1,2})?$', message: '请输入货币金额' },
{ id: 'custom', label: i18n.t('regexSelect.custom'), value: '', message: '' } { id: 'custom', label: i18n.t('regexSelect.custom'), value: '', message: '' }
] ]
} }

View File

@@ -160,7 +160,7 @@ export default {
landline: 'landline', landline: 'landline',
zipCode: 'zip code', zipCode: 'zip code',
IDCard: 'ID card', IDCard: 'ID card',
ip: 'IP', ip: 'IPv4',
email: 'email', email: 'email',
link: 'link', link: 'link',
monetaryAmount: 'monetary amount', monetaryAmount: 'monetary amount',

View File

@@ -160,7 +160,7 @@ export default {
landline: '座机', landline: '座机',
zipCode: '邮政编码', zipCode: '邮政编码',
IDCard: '身份证号', IDCard: '身份证号',
ip: 'IP地址', ip: 'IPv4地址',
email: '邮箱', email: '邮箱',
link: '链接', link: '链接',
monetaryAmount: '货币金额', monetaryAmount: '货币金额',

View File

@@ -50,3 +50,13 @@ export const putCITypeGroups = (data) => {
data: data data: data
}) })
} }
// 导出模型分组
export function exportCITypeGroups(params) {
return axios({
url: `${urlPrefix}/ci_types/template/export`,
method: 'GET',
params: params,
timeout: 30 * 1000,
})
}

View File

@@ -45,11 +45,11 @@ export function getHttpAttributes(name, params) {
}) })
} }
export function getSnmpAttributes(name) { export function getSnmpAttributes(type, name) {
return axios({ return axios({
url: `/v0.1/adr/snmp/${name}/attributes`, url: `/v0.1/adr/${type}/${name}/attributes`,
method: 'GET', method: 'GET',
}) })
} }
export function getCITypeDiscovery(type_id) { export function getCITypeDiscovery(type_id) {
@@ -118,3 +118,65 @@ export function deleteAdc(adc_id) {
method: 'DELETE', method: 'DELETE',
}) })
} }
export function getAdcCounter(params) {
return axios({
url: `v0.1/adc/counter`,
method: 'GET',
params
})
}
export function getAdcExecHistories(params) {
return axios({
url: `v0.1/adc/exec/histories`,
method: 'GET',
params
})
}
export function getAdtSyncHistories(adt_id) {
return axios({
url: `/v0.1/adt/${adt_id}/sync/histories`,
method: 'GET',
params: {
page_size: 9999
}
})
}
export function postAdtTest(adt_id) {
return axios({
url: `/v0.1/adt/${adt_id}/test`,
method: 'POST',
})
}
export function getAdtTestResult(exec_id) {
return axios({
url: `/v0.1/adt/test/${exec_id}/result`,
method: 'GET'
})
}
export function getCITypeAttributes(type_id) {
return axios({
url: `/v0.1/adt/ci_types/${type_id}/attributes`,
method: 'GET',
})
}
export function getCITypeRelations(type_id) {
return axios({
url: `/v0.1/adt/ci_types/${type_id}/relations`,
method: 'GET',
})
}
export function postCITypeRelations(type_id, data) {
return axios({
url: `/v0.1/adt/ci_types/${type_id}/relations`,
method: 'POST',
data
})
}

View File

@@ -0,0 +1,196 @@
<template>
<div class="attr-map-table">
<div class="attr-map-table-left">
<div class="attr-map-table-title">{{ $t('cmdb.ciType.attributes') }}</div>
<vxe-table
ref="attr-xTable-left"
size="mini"
:data="tableData"
:scroll-y="{ enabled: true }"
:min-height="78"
>
<vxe-column field="attr" :title="$t('name')">
<template #default="{ row }">
<div class="attr-select">
<span
v-if="uniqueKey"
:style="{
opacity: uniqueKey === row.name ? 1 : 0
}"
class="attr-select-unique"
>
*
</span>
<vxe-select
filterable
clearable
v-model="row.attr"
type="text"
:options="ciTypeAttributes"
transfer
:placeholder="$t('cmdb.ciType.attrMapTableAttrPlaceholder')"
></vxe-select>
</div>
</template>
</vxe-column>
</vxe-table>
</div>
<div class="attr-map-table-right">
<div class="attr-map-table-title">{{ $t('cmdb.ciType.autoDiscovery') }}</div>
<vxe-table
ref="attr-xTable-right"
size="mini"
show-overflow
show-header-overflow
:data="tableData"
:scroll-y="{ enabled: true }"
:row-config="{ height: 42 }"
:min-height="78"
>
<vxe-column field="name" :title="$t('name')"></vxe-column>
<vxe-column field="type" :title="$t('type')"></vxe-column>
<vxe-column v-if="ruleType !== 'agent'" field="example" :title="$t('cmdb.components.example')">
<template #default="{row}">
<span v-if="row.type === 'json'">{{ JSON.stringify(row.example) }}</span>
<span v-else>{{ row.example }}</span>
</template>
</vxe-column>
<vxe-column field="desc" :title="$t('desc')"></vxe-column>
</vxe-table>
</div>
<div class="attr-map-table-link">
<div
v-for="item in tableData"
:key="item._X_ROW_KEY"
class="attr-map-table-link-item"
>
<div class="attr-map-table-link-left"></div>
<div class="attr-map-table-link-right"></div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'AttrMapTable',
props: {
tableData: {
type: Array,
default: () => [],
},
ciTypeAttributes: {
type: Array,
default: () => [],
},
ruleType: {
type: String,
default: '',
},
uniqueKey: {
type: String,
default: '',
}
},
data() {
return {}
},
methods: {
getTableData() {
const leftTable = this.$refs?.['attr-xTable-left']
const rightTable = this.$refs?.['attr-xTable-right']
const { fullData: leftFullData } = leftTable.getTableData()
const { fullData: rightFullData } = rightTable.getTableData()
const fullData = leftFullData.map((item, index) => {
return {
...(rightFullData?.[index] || {}),
...(item || {})
}
})
return {
fullData
}
},
}
}
</script>
<style lang="less" scoped>
.attr-map-table {
display: flex;
justify-content: space-between;
position: relative;
&-left {
width: 30%;
}
&-right {
width: calc(70% - 60px);
}
&-title {
font-size: 14px;
font-weight: 700;
line-height: 22px;
margin-bottom: 12px;
}
&-link {
position: absolute;
z-index: 10;
bottom: 0;
left: calc(30% - 6px);
width: 66px;
&-item {
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: calc(42px - 12px);
width: 100%;
&:last-child {
margin-bottom: calc(21px - 6px);
}
&::after {
content: '';
position: absolute;
top: 50%;
left: 0;
width: 100%;
height: 1px;
background-color: @border-color-base;
z-index: -1;
}
}
&-left {
width: 12px;
height: 12px;
background-color: @primary-color;
border: solid 3px #E2E7FC;
border-radius: 50%;
}
&-right {
width: 2px;
height: 10px;
border-radius: 1px 0px 0px 1px;
background-color: @primary-color;
}
}
.attr-select {
display: flex;
align-items: center;
gap: 10px;
&-unique {
color: #FD4C6A;
}
}
}
</style>

View File

@@ -0,0 +1,39 @@
<template>
<vxe-table
size="mini"
stripe
class="ops-stripe-table"
show-overflow
keep-source
ref="xTable"
resizable
height="100%"
:data="tableData"
:scroll-y="{enabled: true}"
>
<vxe-column field="name" :title="$t('name')" width="100"> </vxe-column>
<vxe-column field="type" :title="$t('type')" width="80"> </vxe-column>
<vxe-column field="example" :title="$t('cmdb.components.example')">
<template #default="{row}">
<span v-if="row.type === 'object'">{{ row.example ? JSON.stringify(row.example) : '' }}</span>
<span v-else>{{ row.example }}</span>
</template>
</vxe-column>
<vxe-column field="desc" :title="$t('desc')"> </vxe-column>
</vxe-table>
</template>
<script>
export default {
name: 'ADPreviewTable',
props: {
tableData: {
type: Array,
default: () => [],
},
}
}
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,298 @@
<template>
<div class="http-ad-category">
<div class="http-ad-category-preview" v-if="currentCate && isPreviewDetail">
<div class="category-side">
<div
v-for="(category, categoryIndex) in categories"
:key="category.category"
class="category-side-item"
>
<div class="category-side-title">
<div class="category-side-title">
<a-icon
v-if="categoryIndex === 0"
type="left"
@click="clickBack"
/>
{{ category.category }}
</div>
</div>
<div class="category-side-children">
<div
v-for="(item, itemIndex) in category.items"
:key="item"
:class="['category-side-children-item', item === currentCate ? 'category-side-children-item_active' : '']"
@click="clickCategory(item)"
>
{{ item }}
<span
class="category-side-children-item-corporate"
v-if="ruleType === 'private_cloud' || (ruleType === 'http' && (categoryIndex !== 0 || itemIndex !== 0))"
>
</span>
</div>
</div>
</div>
</div>
<div class="category-table">
<ADPreviewTable
:tableData="tableData"
/>
</div>
</div>
<template v-else>
<a-input-search
class="category-search"
:placeholder="$t('cmdb.ad.httpSearchPlaceHolder')"
@search="onSearchHttp"
/>
<div class="category-main">
<div
v-for="(category, categoryIndex) in filterCategories"
:key="category.category"
class="category-item"
>
<div class="category-title">{{ category.category }}</div>
<div class="category-children">
<div
v-for="(item, itemIndex) in category.items"
:key="item"
:class="['category-children-item', item === currentCate ? 'category-children-item_active' : '']"
@click="clickCategory(item)"
>
{{ item }}
<div
class="corporate-flag"
v-if="ruleType === 'private_cloud' || (ruleType === 'http' && (categoryIndex !== 0 || itemIndex !== 0))"
>
<span class="corporate-flag-text"></span>
</div>
</div>
</div>
</div>
</div>
<div class="corporate-tip">
{{ $t('cmdb.ad.corporateTip') }} <a href="mailto:bd@veops.cn">bd@veops.cn</a>
</div>
</template>
</div>
</template>
<script>
import _ from 'lodash'
import ADPreviewTable from './adPreviewTable.vue'
export default {
name: 'HttpADCategory',
components: {
ADPreviewTable
},
props: {
categories: {
type: Array,
default: () => {},
},
currentCate: {
type: String,
default: ''
},
tableData: {
type: Array,
default: () => [],
},
ruleType: {
type: String,
default: 'http',
},
},
data() {
return {
searchValue: '',
isPreviewDetail: false,
}
},
computed: {
filterCategories() {
const categories = _.cloneDeep(this.categories)
const filterCategories = categories.filter((category) => {
category.items = category.items.filter((item) => {
return item?.indexOf(this.searchValue) !== -1
})
return category?.items?.length > 0
})
return filterCategories
}
},
methods: {
onSearchHttp(v) {
this.searchValue = v
},
clickCategory(item) {
this.$emit('clickCategory', item)
this.isPreviewDetail = true
},
clickBack() {
this.isPreviewDetail = false
}
}
}
</script>
<style lang="less" scoped>
.http-ad-category {
&-preview {
display: flex;
width: 100%;
height: calc(100vh - 156px);
justify-content: space-between;
}
.category-side {
flex-shrink: 0;
width: 150px;
height: 100%;
border-right: solid 1px @border-color-base;
padding-right: 10px;
&-item {
&:not(:last-child) {
margin-bottom: 24px;
}
.category-side-title {
font-size: 12px;
font-weight: 400;
color: @text-color_4;
}
.category-side-children {
margin-top: 5px;
&-item {
padding: 7px 10px;
background-color: @layout-content-background;
border-radius: @border-radius-base;
color: @text-color_2;
font-size: 12px;
font-weight: 400;
cursor: pointer;
position: relative;
margin-top: 5px;
display: flex;
align-items: center;
justify-content: space-between;
&:hover {
background-color: @layout-sidebar-selected-color;
color: @layout-header-font-selected-color;
}
&_active {
background-color: @layout-sidebar-selected-color;
color: @layout-header-font-selected-color;
}
&-corporate {
flex-shrink: 0;
width: 18px;
height: 18px;
background-color: #E1EFFF;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 50%;
color: #2F54EB;
font-size: 12px;
}
}
}
}
}
.category-table {
width: calc(100% - 150px - 10px - 16px);
flex-shrink: 0;
height: 100%;
}
.category-search {
width: 254px;
}
.category-main {
.category-item {
margin-top: 24px;
.category-title {
font-size: 14px;
font-weight: 700;
}
.category-children {
margin-top: 8px;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 19px;
&-item {
padding: 18px 19px;
background-color: @layout-content-background;
border-radius: @border-radius-base;
color: @text-color_2;
font-size: 14px;
font-weight: 400;
cursor: pointer;
position: relative;
&:hover {
background-color: @layout-sidebar-selected-color;
color: @layout-header-font-selected-color;
}
&_active {
background-color: @layout-sidebar-selected-color;
color: @layout-header-font-selected-color;
}
}
}
}
}
.corporate-tip {
margin-top: 20px;
}
.corporate-flag {
position: absolute;
top: 0;
right: 0;
z-index: 4;
width: 38px;
height: 28px;
border-left: 38px solid transparent;
border-top: 28px solid @primary-color_4;
&-text {
width: 37px;
position: absolute;
top: -28px;
right: 3px;
text-align: right;
color: @primary-color;
font-size: 10px;
font-weight: 400;
}
}
}
</style>

View File

@@ -1,68 +1,46 @@
<template> <template>
<div> <div class="http-snmp-ad">
<a-select v-if="ruleType === 'http'" :style="{ marginBottom: '10px' }" v-model="currentCate"> <HttpADCategory
<a-select-option v-for="cate in categories" :key="cate" :value="cate">{{ cate }}</a-select-option> v-if="!isEdit && isCloud"
</a-select> :categories="categories"
<vxe-table :currentCate="currentCate"
size="mini" :tableData="tableData"
stripe :ruleType="ruleType"
class="ops-stripe-table" @clickCategory="setCurrentCate"
show-overflow />
keep-source <template v-else>
ref="xTable" <a-select v-if="isCloud" :style="{ marginBottom: '10px', minWidth: '120px' }" v-model="currentCate">
resizable <a-select-option v-for="cate in categoriesSelect" :key="cate" :value="cate">{{ cate }}</a-select-option>
:data="tableData" </a-select>
:edit-config="isEdit ? { trigger: 'click', mode: 'cell' } : {}" <AttrMapTable
> v-if="isEdit"
<template v-if="isEdit"> ref="attrMapTable"
<vxe-colgroup :title="$t('cmdb.ciType.autoDiscovery')"> :ruleType="ruleType"
<vxe-column field="name" :title="$t('name')" width="100"> </vxe-column> :tableData="tableData"
<vxe-column field="type" :title="$t('type')" width="80"> </vxe-column> :ciTypeAttributes="ciTypeAttributes"
<vxe-column field="example" :title="$t('cmdb.components.example')"> :uniqueKey="uniqueKey"
<template #default="{row}"> />
<span v-if="row.type === 'json'">{{ JSON.stringify(row.example) }}</span> <ADPreviewTable
<span v-else>{{ row.example }}</span> v-else
</template> :tableData="tableData"
</vxe-column> />
<vxe-column field="desc" :title="$t('desc')"> </vxe-column> </template>
</vxe-colgroup>
<vxe-colgroup :title="$t('cmdb.ciType.attributes')">
<vxe-column field="attr" :title="$t('name')" :edit-render="{}">
<template #default="{row}">
{{ row.attr }}
</template>
<template #edit="{ row }">
<vxe-select
filterable
clearable
v-model="row.attr"
type="text"
:options="ciTypeAttributes"
transfer
></vxe-select>
</template>
</vxe-column>
</vxe-colgroup>
</template>
<template v-else>
<vxe-column field="name" :title="$t('name')" width="100"> </vxe-column>
<vxe-column field="type" :title="$t('type')" width="80"> </vxe-column>
<vxe-column field="example" :title="$t('cmdb.components.example')">
<template #default="{row}">
<span v-if="row.type === 'object'">{{ JSON.stringify(row.example) }}</span>
<span v-else>{{ row.example }}</span>
</template>
</vxe-column>
<vxe-column field="desc" :title="$t('desc')"> </vxe-column>
</template>
</vxe-table>
</div> </div>
</template> </template>
<script> <script>
import { getHttpCategories, getHttpAttributes, getSnmpAttributes } from '../../api/discovery' import { getHttpCategories, getHttpAttributes, getSnmpAttributes } from '../../api/discovery'
import AttrMapTable from '@/modules/cmdb/components/attrMapTable/index.vue'
import ADPreviewTable from './adPreviewTable.vue'
import HttpADCategory from './httpADCategory.vue'
export default { export default {
name: 'HttpSnmpAD', name: 'HttpSnmpAD',
components: {
AttrMapTable,
ADPreviewTable,
HttpADCategory
},
props: { props: {
ruleName: { ruleName: {
type: String, type: String,
@@ -88,10 +66,15 @@ export default {
type: Number, type: Number,
default: 0, default: 0,
}, },
uniqueKey: {
type: String,
default: '',
}
}, },
data() { data() {
return { return {
categories: [], categories: [],
categoriesSelect: [],
currentCate: '', currentCate: '',
tableData: [], tableData: [],
} }
@@ -107,15 +90,20 @@ export default {
腾讯云: { name: 'tencentcloud' }, 腾讯云: { name: 'tencentcloud' },
华为云: { name: 'huaweicloud' }, 华为云: { name: 'huaweicloud' },
AWS: { name: 'aws' }, AWS: { name: 'aws' },
VCenter: { name: 'vcenter' },
KVM: { name: 'kvm' },
} }
}, },
isCloud() {
return ['http', 'private_cloud'].includes(this.ruleType)
}
}, },
watch: { watch: {
currentCate: { currentCate: {
immediate: true, immediate: true,
handler(newVal) { handler(newVal) {
if (newVal) { if (newVal) {
getHttpAttributes(this.httpMap[`${this.ruleName}`].name, { category: newVal }).then((res) => { getHttpAttributes(this.ruleName, { resource: newVal }).then((res) => {
if (this.isEdit) { if (this.isEdit) {
this.formatTableData(res) this.formatTableData(res)
} else { } else {
@@ -131,8 +119,8 @@ export default {
this.currentCate = '' this.currentCate = ''
this.$nextTick(() => { this.$nextTick(() => {
const { ruleType, ruleName } = newVal const { ruleType, ruleName } = newVal
if (ruleType === 'snmp' && ruleName) { if (['snmp', 'components'].includes(ruleType) && ruleName) {
getSnmpAttributes(ruleName).then((res) => { getSnmpAttributes(ruleType, ruleName).then((res) => {
if (this.isEdit) { if (this.isEdit) {
this.formatTableData(res) this.formatTableData(res)
} else { } else {
@@ -140,11 +128,19 @@ export default {
} }
}) })
} }
if (ruleType === 'http' && ruleName) {
getHttpCategories(this.httpMap[`${this.ruleName}`].name).then((res) => { if (this.isCloud && ruleName) {
getHttpCategories(this.ruleName).then((res) => {
this.categories = res this.categories = res
if (res && res.length) { const categoriesSelect = []
this.currentCate = res[0] res.forEach((category) => {
if (category?.items?.length) {
categoriesSelect.push(...category.items)
}
})
this.categoriesSelect = categoriesSelect
if (this.isEdit && categoriesSelect?.length) {
this.currentCate = categoriesSelect[0]
} }
}) })
} }
@@ -181,12 +177,16 @@ export default {
}) })
}, },
getTableData() { getTableData() {
const $table = this.$refs.xTable const $table = this.$refs.attrMapTable
const { fullData } = $table.getTableData() const { fullData } = $table.getTableData()
return fullData || [] return fullData || []
}, }
}, },
} }
</script> </script>
<style></style> <style>
.http-snmp-ad {
height: 100%;
}
</style>

View File

@@ -0,0 +1,207 @@
<template>
<div class="node-setting-wrap">
<a-row v-for="(node) in nodes" :key="node.id">
<a-col :span="6">
<a-form-item :label="$t('cmdb.ciType.nodeSettingIp')">
<a-input
allowClear
v-decorator="[
`node_ip_${node.id}`,
{
rules: [
{ required: false, message: $t('cmdb.ciType.nodeSettingIpTip') },
{
pattern:
'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$',
message: $t('cmdb.ciType.nodeSettingIpTip1'),
trigger: 'blur',
},
],
},
]"
:placeholder="$t('cmdb.ciType.nodeSettingIpTip')"
/>
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item :label="$t('cmdb.ciType.nodeSettingCommunity')">
<a-input
allowClear
v-decorator="[
`node_community_${node.id}`,
{
rules: [{ required: false, message: $t('cmdb.ciType.nodeSettingCommunityTip') }],
},
]"
:placeholder="$t('cmdb.ciType.nodeSettingCommunityTip')"
/>
</a-form-item>
</a-col>
<a-col :span="5">
<a-form-item :label="$t('cmdb.ciType.nodeSettingVersion')">
<a-select
v-decorator="[
`node_version_${node.id}`,
{
rules: [{ required: false, message: $t('cmdb.ciType.nodeSettingVersionTip') }],
},
]"
:placeholder="$t('cmdb.ciType.nodeSettingVersionTip')"
allowClear
class="node-setting-select"
>
<a-select-option value="1">
v1
</a-select-option>
<a-select-option value="2c">
v2c
</a-select-option>
<a-select-option value="3">
v3
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="3">
<div class="action">
<a @click="() => copyNode(node.id)">
<a-icon type="copy" />
</a>
<a @click="() => removeNode(node.id, 1)">
<a-icon type="minus-circle" />
</a>
<a @click="addNode">
<a-icon type="plus-circle" />
</a>
</div>
</a-col>
</a-row>
</div>
</template>
<script>
import _ from 'lodash'
import { v4 as uuidv4 } from 'uuid'
export default {
name: 'MonitorNodeSetting',
props: {
initNodes: {
type: Array,
default: () => [],
},
form: {
type: Object,
default: null,
},
},
data() {
return {
nodes: [],
}
},
methods: {
initNodesFunc() {
this.nodes = _.cloneDeep(this.initNodes)
},
addNode() {
const newNode = {
id: uuidv4(),
ip: '',
community: '',
version: '',
}
this.nodes.push(newNode)
this.$nextTick(() => {
this.form.setFieldsValue({
[`node_ip_${newNode.id}`]: newNode.ip,
[`node_community_${newNode.id}`]: newNode.community,
[`node_version_${newNode.id}`]: newNode.version,
})
})
},
removeNode(removeId, minLength) {
if (this.nodes.length <= minLength) {
this.$message.error('不可再删除!')
return
}
const _idx = this.nodes.findIndex((item) => item.id === removeId)
if (_idx > -1) {
this.nodes.splice(_idx, 1)
}
},
copyNode(id) {
const newNode = {
id: uuidv4(),
ip: this.form.getFieldValue(`node_ip_${id}`),
community: this.form.getFieldValue(`node_community_${id}`),
version: this.form.getFieldValue(`node_version_${id}`),
}
this.nodes.push(newNode)
this.$nextTick(() => {
this.form.setFieldsValue({
[`node_ip_${newNode.id}`]: newNode.ip,
[`node_community_${newNode.id}`]: newNode.community,
[`node_version_${newNode.id}`]: newNode.version,
})
})
},
getInfoValuesFromForm(values) {
return this.nodes.map((item) => {
return {
id: item.id,
ip: values[`node_ip_${item.id}`],
community: values[`node_community_${item.id}`],
version: values[`node_version_${item.id}`],
}
})
},
setNodeField() {
if (this.nodes && this.nodes.length) {
this.nodes.forEach((item) => {
this.form.setFieldsValue({
[`node_ip_${item.id}`]: item.ip,
[`node_community_${item.id}`]: item.community,
[`node_version_${item.id}`]: item.version,
})
})
}
},
getNodeValue() {
const values = this.form.getFieldsValue()
return this.getInfoValuesFromForm(values)
},
},
}
</script>
<style lang="less" scoped>
.node-setting-wrap {
margin-left: 17px;
.ant-row {
// display: flex;
/deep/ .ant-input-clear-icon {
color: rgba(0,0,0,.25);
&:hover {
color: rgba(0, 0, 0, 0.45);
}
}
}
.node-setting-select {
width: 150px;
}
}
.action {
height: 36px;
display: flex;
align-items: center;
gap: 12px;
}
</style>

View File

@@ -1,317 +1,319 @@
<template> <template>
<div> <div>
<div id="search-form-bar" class="search-form-bar"> <div id="search-form-bar" class="search-form-bar">
<div :style="{ display: 'inline-flex', alignItems: 'center' }"> <div :style="{ display: 'inline-flex', alignItems: 'center' }">
<a-space> <a-space>
<treeselect <treeselect
v-if="type === 'resourceSearch'" v-if="type === 'resourceSearch'"
class="custom-treeselect custom-treeselect-bgcAndBorder" class="custom-treeselect custom-treeselect-bgcAndBorder"
:style="{ :style="{
width: '200px', width: '200px',
marginRight: '10px', marginRight: '10px',
'--custom-height': '32px', '--custom-height': '32px',
'--custom-bg-color': '#fff', '--custom-bg-color': '#fff',
'--custom-border': '1px solid #d9d9d9', '--custom-border': '1px solid #d9d9d9',
'--custom-multiple-lineHeight': '16px', '--custom-multiple-lineHeight': '16px',
}" }"
v-model="currenCiType" v-model="currenCiType"
:multiple="true" :multiple="true"
:clearable="true" :clearable="true"
searchable searchable
:options="ciTypeGroup" :options="ciTypeGroup"
:limit="1" :limit="1"
:limitText="(count) => `+ ${count}`" :limitText="(count) => `+ ${count}`"
value-consists-of="LEAF_PRIORITY" value-consists-of="LEAF_PRIORITY"
:placeholder="$t('cmdb.ciType.ciType')" :placeholder="$t('cmdb.ciType.ciType')"
@close="closeCiTypeGroup" @close="closeCiTypeGroup"
@open="openCiTypeGroup" @open="openCiTypeGroup"
@input="inputCiTypeGroup" @input="inputCiTypeGroup"
:normalizer=" :normalizer="
(node) => { (node) => {
return { return {
id: node.id || -1, id: node.id || -1,
label: node.alias || node.name || $t('other'), label: node.alias || node.name || $t('other'),
title: node.alias || node.name || $t('other'), title: node.alias || node.name || $t('other'),
children: node.ci_types, children: node.ci_types,
} }
} }
" "
> >
<div <div
:title="node.label" :title="node.label"
slot="option-label" slot="option-label"
slot-scope="{ node }" slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }" :style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
> >
{{ node.label }} {{ node.label }}
</div> </div>
</treeselect> </treeselect>
<a-input <a-input
v-model="fuzzySearch" v-model="fuzzySearch"
:style="{ display: 'inline-block', width: '200px' }" :style="{ display: 'inline-block', width: '200px' }"
:placeholder="$t('cmdb.components.pleaseSearch')" :placeholder="$t('cmdb.components.pleaseSearch')"
@pressEnter="emitRefresh" @pressEnter="emitRefresh"
> >
<a-icon <a-icon
type="search" type="search"
slot="suffix" slot="suffix"
:style="{ color: fuzzySearch ? '#2f54eb' : '#d9d9d9', cursor: 'pointer' }" :style="{ color: fuzzySearch ? '#2f54eb' : '#d9d9d9', cursor: 'pointer' }"
@click="emitRefresh" @click="emitRefresh"
/> />
<a-tooltip slot="prefix" placement="bottom" :overlayStyle="{ maxWidth: '550px', whiteSpace: 'pre-line' }"> <a-tooltip slot="prefix" placement="bottom" :overlayStyle="{ maxWidth: '550px', whiteSpace: 'pre-line' }">
<template slot="title"> <template slot="title">
{{ $t('cmdb.components.ciSearchTips') }} {{ $t('cmdb.components.ciSearchTips') }}
</template> </template>
<a><a-icon type="question-circle"/></a> <a><a-icon type="question-circle"/></a>
</a-tooltip> </a-tooltip>
</a-input> </a-input>
<a-tooltip :title="$t('reset')"> <a-tooltip :title="$t('reset')">
<a-button @click="reset">{{ $t('reset') }}</a-button> <a-button @click="reset">{{ $t('reset') }}</a-button>
</a-tooltip> </a-tooltip>
<FilterComp <FilterComp
ref="filterComp" ref="filterComp"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList" :canSearchPreferenceAttrList="canSearchPreferenceAttrList"
@setExpFromFilter="setExpFromFilter" @setExpFromFilter="setExpFromFilter"
:expression="expression" :expression="expression"
placement="bottomLeft" placement="bottomLeft"
> >
<div slot="popover_item" class="search-form-bar-filter"> <div slot="popover_item" class="search-form-bar-filter">
<a-icon class="search-form-bar-filter-icon" type="filter" /> <a-icon class="search-form-bar-filter-icon" type="filter" />
{{ $t('cmdb.components.conditionFilter') }} {{ $t('cmdb.components.conditionFilter') }}
<a-icon class="search-form-bar-filter-icon" type="down" :style="{ color: '#d9d9d9' }" /> <a-icon class="search-form-bar-filter-icon" type="down" :style="{ color: '#d9d9d9' }" />
</div> </div>
</FilterComp> </FilterComp>
<a-input <a-input
v-if="isShowExpression" v-if="isShowExpression"
v-model="expression" v-model="expression"
v-show="!selectedRowKeys.length" v-show="!selectedRowKeys.length"
@focus=" @focus="
() => { () => {
isFocusExpression = true isFocusExpression = true
} }
" "
@blur=" @blur="
() => { () => {
isFocusExpression = false isFocusExpression = false
} }
" "
:class="{ 'ci-searchform-expression': true, 'ci-searchform-expression-has-value': expression }" :class="{ 'ci-searchform-expression': true, 'ci-searchform-expression-has-value': expression }"
:style="{ width }" :style="{ width }"
:placeholder="placeholder" :placeholder="placeholder"
@keyup.enter="emitRefresh" @keyup.enter="emitRefresh"
> >
<a-icon slot="suffix" type="check-circle" @click="handleCopyExpression" /> <a-icon slot="suffix" type="check-circle" @click="handleCopyExpression" />
</a-input> </a-input>
<slot></slot> <slot></slot>
</a-space> </a-space>
</div> </div>
<a-space> <a-space>
<slot name="extraContent"></slot> <slot name="extraContent"></slot>
<a-tooltip :title="$t('cmdb.components.attributeDesc')" v-if="type === 'relationView'"> <a-tooltip :title="$t('cmdb.components.attributeDesc')" v-if="type === 'relationView'">
<a <a
@click=" @click="
() => { () => {
$refs.metadataDrawer.open(typeId) $refs.metadataDrawer.open(typeId)
} }
" "
><a-icon ><a-icon
v-if="type === 'relationView'" v-if="type === 'relationView'"
type="question-circle" type="question-circle"
/></a> /></a>
</a-tooltip> </a-tooltip>
</a-space> </a-space>
</div> </div>
<MetadataDrawer ref="metadataDrawer" /> <MetadataDrawer ref="metadataDrawer" />
</div> </div>
</template> </template>
<script> <script>
import _ from 'lodash' import _ from 'lodash'
import Treeselect from '@riophae/vue-treeselect' import Treeselect from '@riophae/vue-treeselect'
import MetadataDrawer from '../../views/ci/modules/MetadataDrawer.vue' import MetadataDrawer from '../../views/ci/modules/MetadataDrawer.vue'
import FilterComp from '@/components/CMDBFilterComp' import FilterComp from '@/components/CMDBFilterComp'
import { getCITypeGroups } from '../../api/ciTypeGroup' import { getCITypeGroups } from '../../api/ciTypeGroup'
export default { export default {
name: 'SearchForm', name: 'SearchForm',
components: { MetadataDrawer, FilterComp, Treeselect }, components: { MetadataDrawer, FilterComp, Treeselect },
props: { props: {
preferenceAttrList: { preferenceAttrList: {
type: Array, type: Array,
required: true, required: true,
}, },
isShowExpression: { isShowExpression: {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
typeId: { typeId: {
type: Number, type: Number,
default: null, default: null,
}, },
type: { type: {
type: String, type: String,
default: '', default: '',
}, },
selectedRowKeys: { selectedRowKeys: {
type: Array, type: Array,
default: () => [], default: () => [],
}, },
}, },
data() { data() {
return { return {
// Advanced Search Expand/Close // Advanced Search Expand/Close
advanced: false, advanced: false,
queryParam: {}, queryParam: {},
isFocusExpression: false, isFocusExpression: false,
expression: '', expression: '',
fuzzySearch: '', fuzzySearch: '',
currenCiType: [], currenCiType: [],
ciTypeGroup: [], ciTypeGroup: [],
lastCiType: [], lastCiType: [],
} }
}, },
computed: { computed: {
placeholder() { placeholder() {
return this.isFocusExpression ? this.$t('cmdb.components.ciSearchTips2') : this.$t('cmdb.ciType.expr') return this.isFocusExpression ? this.$t('cmdb.components.ciSearchTips2') : this.$t('cmdb.ciType.expr')
}, },
width() { width() {
return '200px' return '200px'
}, },
canSearchPreferenceAttrList() { canSearchPreferenceAttrList() {
return this.preferenceAttrList.filter((item) => item.value_type !== '6') return this.preferenceAttrList.filter((item) => item.value_type !== '6')
}, },
}, },
watch: { watch: {
'$route.path': function(newValue, oldValue) { '$route.path': function(newValue, oldValue) {
this.queryParam = {} this.queryParam = {}
this.expression = '' this.expression = ''
this.fuzzySearch = '' this.fuzzySearch = ''
}, },
}, },
inject: { inject: {
setPreferenceSearchCurrent: { setPreferenceSearchCurrent: {
from: 'setPreferenceSearchCurrent', from: 'setPreferenceSearchCurrent',
default: null, default: null,
}, },
}, },
mounted() { mounted() {
if (this.type === 'resourceSearch') { if (this.type === 'resourceSearch') {
this.getCITypeGroups() this.getCITypeGroups()
} }
if (this.typeId) { if (this.typeId) {
this.currenCiType = [this.typeId] this.currenCiType = [this.typeId]
} }
}, },
methods: { methods: {
// toggleAdvanced() { // toggleAdvanced() {
// this.advanced = !this.advanced // this.advanced = !this.advanced
// }, // },
getCITypeGroups() { getCITypeGroups() {
getCITypeGroups({ need_other: true }).then((res) => { getCITypeGroups({ need_other: true }).then((res) => {
this.ciTypeGroup = res this.ciTypeGroup = res
.filter((item) => item.ci_types && item.ci_types.length) .filter((item) => item.ci_types && item.ci_types.length)
.map((item) => { .map((item) => {
item.id = `parent_${item.id || -1}` item.id = `parent_${item.id || -1}`
return { ..._.cloneDeep(item) } return { ..._.cloneDeep(item) }
}) })
}) })
}, },
reset() { reset() {
this.queryParam = {} this.queryParam = {}
this.expression = '' this.expression = ''
this.fuzzySearch = '' this.fuzzySearch = ''
this.currenCiType = [] if (this.type !== 'resourceView') {
this.emitRefresh() this.currenCiType = []
}, }
setExpFromFilter(filterExp) { this.emitRefresh()
const regSort = /(?<=sort=).+/g },
const expSort = this.expression.match(regSort) ? this.expression.match(regSort)[0] : undefined setExpFromFilter(filterExp) {
let expression = '' const regSort = /(?<=sort=).+/g
if (filterExp) { const expSort = this.expression.match(regSort) ? this.expression.match(regSort)[0] : undefined
expression = `q=${filterExp}` let expression = ''
} if (filterExp) {
if (expSort) { expression = `q=${filterExp}`
expression += `&sort=${expSort}` }
} if (expSort) {
this.expression = expression expression += `&sort=${expSort}`
this.emitRefresh() }
}, this.expression = expression
handleSubmit() { this.emitRefresh()
this.$refs.filterComp.handleSubmit() },
}, handleSubmit() {
openCiTypeGroup() { this.$refs.filterComp.handleSubmit()
this.lastCiType = _.cloneDeep(this.currenCiType) },
}, openCiTypeGroup() {
closeCiTypeGroup(value) { this.lastCiType = _.cloneDeep(this.currenCiType)
if (!_.isEqual(value, this.lastCiType)) { },
this.$emit('updateAllAttributesList', value) closeCiTypeGroup(value) {
} if (!_.isEqual(value, this.lastCiType)) {
}, this.$emit('updateAllAttributesList', value)
inputCiTypeGroup(value) { }
if (!value || !value.length) { },
this.$emit('updateAllAttributesList', value) inputCiTypeGroup(value) {
} if (!value || !value.length) {
}, this.$emit('updateAllAttributesList', value)
emitRefresh() { }
if (this.setPreferenceSearchCurrent) { },
this.setPreferenceSearchCurrent(null) emitRefresh() {
} if (this.setPreferenceSearchCurrent) {
this.$nextTick(() => { this.setPreferenceSearchCurrent(null)
this.$emit('refresh', true) }
}) this.$nextTick(() => {
}, this.$emit('refresh', true)
handleCopyExpression() { })
this.$emit('copyExpression') },
}, handleCopyExpression() {
}, this.$emit('copyExpression')
} },
</script> },
<style lang="less"> }
@import '../../views/index.less'; </script>
.ci-searchform-expression { <style lang="less">
> input { @import '../../views/index.less';
border-bottom: 2px solid #d9d9d9; .ci-searchform-expression {
border-top: none; > input {
border-left: none; border-bottom: 2px solid #d9d9d9;
border-right: none; border-top: none;
&:hover, border-left: none;
&:focus { border-right: none;
border-bottom: 2px solid @primary-color; &:hover,
} &:focus {
&:focus { border-bottom: 2px solid @primary-color;
box-shadow: 0 2px 2px -2px #1f78d133; }
} &:focus {
} box-shadow: 0 2px 2px -2px #1f78d133;
.ant-input-suffix { }
color: #d9d9d9; }
cursor: pointer; .ant-input-suffix {
} color: #d9d9d9;
} cursor: pointer;
.ci-searchform-expression-has-value .ant-input-suffix { }
color: @func-color_3; }
} .ci-searchform-expression-has-value .ant-input-suffix {
.cmdb-search-form { color: @func-color_3;
.ant-form-item-label { }
overflow: hidden; .cmdb-search-form {
text-overflow: ellipsis; .ant-form-item-label {
white-space: nowrap; overflow: hidden;
} text-overflow: ellipsis;
} white-space: nowrap;
</style> }
}
<style lang="less" scoped> </style>
.search-form-bar {
margin-bottom: 20px; <style lang="less" scoped>
display: flex; .search-form-bar {
justify-content: space-between; margin-bottom: 20px;
align-items: center; display: flex;
height: 32px; justify-content: space-between;
.search-form-bar-filter { align-items: center;
.ops_display_wrapper(transparent); height: 32px;
.search-form-bar-filter-icon { .search-form-bar-filter {
color: @primary-color; .ops_display_wrapper(transparent);
font-size: 12px; .search-form-bar-filter-icon {
} color: @primary-color;
} font-size: 12px;
} }
</style> }
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -101,10 +101,14 @@
:cell-style="getCellStyle" :cell-style="getCellStyle"
:scroll-y="{ enabled: true, gt: 20 }" :scroll-y="{ enabled: true, gt: 20 }"
:scroll-x="{ enabled: true, gt: 0 }" :scroll-x="{ enabled: true, gt: 0 }"
class="ops-unstripe-table" class="ops-unstripe-table checkbox-hover-table"
:custom-config="{ storage: true }" :custom-config="{ storage: true }"
> >
<vxe-column align="center" type="checkbox" width="60" :fixed="isCheckboxFixed ? 'left' : ''"></vxe-column> <vxe-column align="center" type="checkbox" width="60" :fixed="isCheckboxFixed ? 'left' : ''">
<template #default="{row}">
{{ getRowSeq(row) }}
</template>
</vxe-column>
<vxe-table-column <vxe-table-column
v-for="(col, index) in columns" v-for="(col, index) in columns"
:key="`${col.field}_${index}`" :key="`${col.field}_${index}`"
@@ -167,16 +171,20 @@
#default="{ row }" #default="{ row }"
> >
<span v-if="col.value_type === '6' && row[col.field]">{{ row[col.field] }}</span> <span v-if="col.value_type === '6' && row[col.field]">{{ row[col.field] }}</span>
<a <template v-else-if="col.is_link && row[col.field]">
v-else-if="col.is_link && row[col.field]" <a
:href=" v-for="(item, linkIndex) in (col.is_list ? row[col.field] : [row[col.field]])"
row[col.field].startsWith('http') || row[col.field].startsWith('https') :key="linkIndex"
? `${row[col.field]}` :href="
: `http://${row[col.field]}` item.startsWith('http') || item.startsWith('https')
" ? `${item}`
target="_blank" : `http://${item}`
>{{ row[col.field] }}</a "
> target="_blank"
>
{{ item }}
</a>
</template>
<PasswordField <PasswordField
v-else-if="col.is_password && row[col.field]" v-else-if="col.is_password && row[col.field]"
:ci_id="row._id" :ci_id="row._id"
@@ -1044,6 +1052,9 @@ export default {
this.visible = false this.visible = false
} }
}, },
getRowSeq(row) {
return this.$refs.xTable.getVxetableRef().getRowSeq(row)
}
}, },
} }
</script> </script>
@@ -1061,4 +1072,33 @@ export default {
overflow: auto; overflow: auto;
margin-bottom: -24px; margin-bottom: -24px;
} }
.checkbox-hover-table {
/deep/ .vxe-table--body-wrapper {
.vxe-checkbox--label {
display: inline;
padding-left: 0px !important;
color: #bfbfbf;
}
.vxe-icon-checkbox-unchecked {
display: none;
}
.vxe-icon-checkbox-checked ~ .vxe-checkbox--label {
display: none;
}
.vxe-cell--checkbox {
&:hover {
.vxe-icon-checkbox-unchecked {
display: inline;
}
.vxe-checkbox--label {
display: none;
}
}
}
}
}
</style> </style>

View File

@@ -163,6 +163,12 @@ export default {
width: 110, width: 110,
help: this.$t('cmdb.ci.tips10'), help: this.$t('cmdb.ci.tips10'),
}, },
{
field: 'is_dynamic',
title: this.$t('cmdb.ciType.isDynamic'),
width: 110,
help: this.$t('cmdb.ciType.dynamicTips'),
},
] ]
}, },
}, },

View File

@@ -269,7 +269,7 @@ export default {
return { nodes, edges } return { nodes, edges }
}, },
exsited_ci() { exsited_ci() {
const _exsited_ci = [this.typeId] const _exsited_ci = [this.ciId]
this.parentCITypes.forEach((parent) => { this.parentCITypes.forEach((parent) => {
if (this.firstCIs[parent.name]) { if (this.firstCIs[parent.name]) {
this.firstCIs[parent.name].forEach((parentCi) => { this.firstCIs[parent.name].forEach((parentCi) => {

View File

@@ -100,35 +100,37 @@ export default {
const r = res.result[i] const r = res.result[i]
if (!this.exsited_ci.includes(r._id)) { if (!this.exsited_ci.includes(r._id)) {
const _findCiType = ci_types_list.find((item) => item.id === r._type) const _findCiType = ci_types_list.find((item) => item.id === r._type)
const { attributes } = await getCITypeAttributesById(_findCiType.id) if (_findCiType) {
const unique_id = _findCiType.show_id || _findCiType.unique_id const { attributes } = await getCITypeAttributesById(_findCiType.id)
const _findUnique = attributes.find((attr) => attr.id === unique_id) const unique_id = _findCiType.show_id || _findCiType.unique_id
const unique_name = _findUnique?.name const _findUnique = attributes.find((attr) => attr.id === unique_id)
const unique_alias = _findUnique?.alias || _findUnique?.name || '' const unique_name = _findUnique?.name
newNodes.push({ const unique_alias = _findUnique?.alias || _findUnique?.name || ''
id: `${r._id}`, newNodes.push({
Class: Node, id: `${r._id}`,
title: r.ci_type_alias || r.ci_type, Class: Node,
name: r.ci_type, title: r.ci_type_alias || r.ci_type,
side: side, name: r.ci_type,
unique_alias, side: side,
unique_name, unique_alias,
unique_value: r[unique_name], unique_name,
children: [], unique_value: r[unique_name],
icon: _findCiType?.icon || '', children: [],
endpoints: [ icon: _findCiType?.icon || '',
{ endpoints: [
id: 'left', {
orientation: [-1, 0], id: 'left',
pos: [0, 0.5], orientation: [-1, 0],
}, pos: [0, 0.5],
{ },
id: 'right', {
orientation: [1, 0], id: 'right',
pos: [0, 0.5], orientation: [1, 0],
}, pos: [0, 0.5],
], },
}) ],
})
}
} }
newEdges.push({ newEdges.push({
id: `${r._id}`, id: `${r._id}`,

View File

@@ -29,7 +29,7 @@
? attr.default.default ? attr.default.default
: attr.default.default.split(',') : attr.default.default.split(',')
: attr.default.default : attr.default.default
: null, : attr.is_list ? [] : null,
}, },
]" ]"
:placeholder="$t('placeholder2')" :placeholder="$t('placeholder2')"

View File

@@ -1,6 +1,10 @@
<template> <template>
<a-modal width="800px" :visible="visible" @ok="handleOK" @cancel="handleCancel" :closable="false"> <a-modal width="800px" :visible="visible" @ok="handleOK" @cancel="handleCancel" :closable="false">
<Discovery :isSelected="true" :style="{ maxHeight: '75vh', overflow: 'auto' }" /> <Discovery
:isSelected="true"
:style="{ maxHeight: '75vh', overflow: 'auto' }"
v-if="visible"
/>
<template #footer> <template #footer>
<a-space> <a-space>
<a-button @click="handleCancel">{{ $t('cancel') }}</a-button> <a-button @click="handleCancel">{{ $t('cancel') }}</a-button>
@@ -14,7 +18,7 @@
<script> <script>
import _ from 'lodash' import _ from 'lodash'
import Discovery from '../discovery' import Discovery from '../discovery'
import { postCITypeDiscovery } from '../../api/discovery'
export default { export default {
name: 'ADModal', name: 'ADModal',
components: { Discovery }, components: { Discovery },
@@ -49,20 +53,17 @@ export default {
}, },
async handleOK() { async handleOK() {
if (this.selectedIds && this.selectedIds.length) { if (this.selectedIds && this.selectedIds.length) {
const promises = this.selectedIds.map(({ id, type }) => { const adCITypeList = this.selectedIds.map((item, index) => {
return postCITypeDiscovery(this.CITypeId, { adr_id: id, interval: type === 'agent' ? 300 : 3600 }) return {
adr_id: item.id,
id: new Date().getTime() + index,
extra_option: {
alias: ''
},
isClient: true,
}
}) })
await Promise.all(promises) this.$emit('pushCITypeList', adCITypeList)
.then((res) => {
this.getCITypeDiscovery(res[0].id)
this.$message.success(this.$t('addSuccess'))
})
.catch(() => {
this.getCITypeDiscovery()
})
.finally(() => {
this.handleCancel()
})
} }
this.handleCancel() this.handleCancel()
}, },

View File

@@ -0,0 +1,105 @@
<template>
<div class="ad-container" :style="{ height: `${windowHeight - 130}px` }">
<div class="ad-btns">
<div
:class="['ad-btns-item', activeKey === item.key ? 'ad-btns-item_active' : '']"
v-for="item in tabs"
:key="item.key"
@click="changeTab(item.key)"
>
{{ $t(item.label) }}
</div>
</div>
<AttrAD
v-if="activeKey === AD_TAB_KEY.ATTR"
:CITypeId="CITypeId"
></AttrAD>
<RelationAD
v-else-if="activeKey === AD_TAB_KEY.RELATION"
:CITypeId="CITypeId"
></RelationAD>
</div>
</template>
<script>
import { mapState } from 'vuex'
import AttrAD from './attrAD.vue'
import RelationAD from './relationAD.vue'
const AD_TAB_KEY = {
ATTR: '1',
RELATION: '2'
}
export default {
name: 'ADTab',
components: {
AttrAD,
RelationAD,
},
props: {
CITypeId: {
type: Number,
default: null,
},
},
data() {
return {
AD_TAB_KEY,
activeKey: AD_TAB_KEY.ATTR,
tabs: [
{
key: AD_TAB_KEY.ATTR,
label: 'cmdb.ciType.attributeAD'
},
{
key: AD_TAB_KEY.RELATION,
label: 'cmdb.ciType.relationAD'
}
]
}
},
computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
}),
},
methods: {
changeTab(activeKey) {
this.activeKey = activeKey
}
}
}
</script>
<style lang="less" scoped>
.ad-btns {
display: inline-flex;
align-items: center;
border: solid 1px @border-color-base;
margin-left: 17px;
margin-bottom: 15px;
&-item {
display: flex;
align-items: center;
justify-content: center;
padding: 6px 20px;
background-color: #FFFFFF;
cursor: pointer;
color: @text-color_2;
font-size: 14px;
font-weight: 400;
&:not(:first-child) {
border-left: solid 1px @border-color-base;
}
&_active {
background-color: @primary-color;
color: #FFFFFF;
}
}
}
</style>

View File

@@ -1,37 +1,28 @@
<template> <template>
<div class="attr-ad" :style="{ height: `${windowHeight - 130}px` }"> <div class="attr-ad" :style="{ height: `${windowHeight - 130}px` }">
<div v-if="adCITypeList && adCITypeList.length"> <div v-if="adCITypeList && adCITypeList.length">
<a-tabs size="small" v-model="currentTab"> <AttrADTabs
<a-tab-pane v-for="item in adCITypeList" :key="item.id"> :adCITypeList="adCITypeList"
<a-space slot="tab"> :currentTab="currentTab"
<span v-if="item.extra_option && item.extra_option.alias">{{ item.extra_option.alias }}</span> :getADCITypeParam="getADCITypeParam"
<span v-else>{{ getADCITypeParam(item.adr_id) }}</span> @changeTab="changeTab"
<a-icon type="close-circle" @click="(e) => deleteADT(e, item)" /> @changeAlias="changeAlias"
</a-space> @deleteADT="deleteADT"
<AttrADTabpane @clickAdd="() => $refs.adModal.open()"
:ref="`attrAdTabpane_${item.id}`" />
:adr_id="item.adr_id" <AttrADTabpane
:adrList="adrList" :key="`attrAdTabpane_${currentTab}`"
:adCITypeList="adCITypeList" :ref="`attrAdTabpaneRef`"
:currentAdt="item" :adr_id="currentADData.adr_id"
:ciTypeAttributes="ciTypeAttributes" :CITypeId="CITypeId"
:currentAdr="getADCITypeParam(item.adr_id, undefined, true)" :adrList="adrList"
@openEditDrawer="(data, type, adType) => openEditDrawer(data, type, adType)" :adCITypeList="adCITypeList"
@handleSave="getCITypeDiscovery" :currentAdt="currentADData"
/> :ciTypeAttributes="ciTypeAttributes"
</a-tab-pane> :currentAdr="getADCITypeParam(currentADData.adr_id, undefined, true)"
<a-space @openEditDrawer="(data, type, adType) => openEditDrawer(data, type, adType)"
@click=" @handleSave="saveTabpane"
() => { />
$refs.adModal.open()
}
"
slot="tabBarExtraContent"
:style="{ cursor: 'pointer' }"
>
<ops-icon type="icon-xianxing-tianjia" :style="{ color: '#2F54EB' }" /><a>{{ $t('add') }}</a>
</a-space>
</a-tabs>
</div> </div>
<a-empty <a-empty
v-else v-else
@@ -54,28 +45,41 @@
{{ $t('add') }} {{ $t('add') }}
</a-button> </a-button>
</a-empty> </a-empty>
<ADModal ref="adModal" :CITypeId="CITypeId" @addPlugin="openEditDrawer(null, 'add', 'agent')" /> <ADModal
ref="adModal"
:CITypeId="CITypeId"
@pushCITypeList="pushCITypeList"
@addPlugin="openEditDrawer(null, 'add', 'plugin')"
/>
<EditDrawer ref="editDrawer" :is_inner="false" @updateNotInner="updateNotInner" /> <EditDrawer ref="editDrawer" :is_inner="false" @updateNotInner="updateNotInner" />
</div> </div>
</template> </template>
<script> <script>
import _ from 'lodash'
import { mapState } from 'vuex' import { mapState } from 'vuex'
import ADModal from './adModal.vue'
import { import {
getDiscovery, getDiscovery,
getCITypeDiscovery, getCITypeDiscovery,
deleteCITypeDiscovery, deleteCITypeDiscovery,
postCITypeDiscovery,
deleteDiscovery, deleteDiscovery,
putCITypeDiscovery
} from '../../api/discovery' } from '../../api/discovery'
import { getCITypeAttributesById } from '../../api/CITypeAttr' import { getCITypeAttributesById } from '../../api/CITypeAttr'
import ADModal from './adModal.vue'
import AttrADTabpane from './attrADTabpane.vue' import AttrADTabpane from './attrADTabpane.vue'
import EditDrawer from '../discovery/editDrawer.vue' import EditDrawer from '../discovery/editDrawer.vue'
import AttrADTabs from './attrADTabs.vue'
export default { export default {
name: 'AttrAutoDiscovery', name: 'AttrAutoDiscovery',
components: { ADModal, AttrADTabpane, EditDrawer }, components: {
ADModal,
AttrADTabpane,
EditDrawer,
AttrADTabs
},
props: { props: {
CITypeId: { CITypeId: {
type: Number, type: Number,
@@ -86,7 +90,8 @@ export default {
return { return {
ciTypeAttributes: [], ciTypeAttributes: [],
adrList: [], adrList: [],
adCITypeList: [], serviceCITYpeList: [],
clientCITypeList: [],
currentTab: '', currentTab: '',
deletePlugin: false, deletePlugin: false,
} }
@@ -95,6 +100,13 @@ export default {
...mapState({ ...mapState({
windowHeight: (state) => state.windowHeight, windowHeight: (state) => state.windowHeight,
}), }),
currentADData() {
return this?.adCITypeList?.find((item) => item?.id === this?.currentTab) ?? {}
},
adCITypeList() {
const uniqueArray = _.differenceBy(this.clientCITypeList, this.serviceCITYpeList, 'id')
return [...this.serviceCITYpeList, ...uniqueArray]
}
}, },
provide() { provide() {
return { return {
@@ -106,7 +118,7 @@ export default {
handler() { handler() {
if (this.currentTab) { if (this.currentTab) {
this.$nextTick(() => { this.$nextTick(() => {
this.$refs[`attrAdTabpane_${this.currentTab}`][0].init() this.$refs[`attrAdTabpaneRef`].init()
}) })
} }
}, },
@@ -121,7 +133,7 @@ export default {
}) })
if (this.currentTab) { if (this.currentTab) {
this.$nextTick(() => { this.$nextTick(() => {
this.$refs[`attrAdTabpane_${this.currentTab}`][0].init() this.$refs[`attrAdTabpaneRef`].init()
}) })
} }
}) })
@@ -134,15 +146,34 @@ export default {
}, },
async getCITypeDiscovery(currentTab) { async getCITypeDiscovery(currentTab) {
await getCITypeDiscovery(this.CITypeId).then((res) => { await getCITypeDiscovery(this.CITypeId).then((res) => {
this.adCITypeList = res.filter((item) => item.adr_id) const serviceCITYpeList = res.filter((item) => item.adr_id)
if (this.adCITypeList && this.adCITypeList.length && !this.currentTab) { serviceCITYpeList.forEach((item) => {
this.currentTab = this.adCITypeList[0].id const _find = this.adrList.find((adr) => adr.id === item.adr_id)
} item.icon = _find?.option?.icon || {}
if (currentTab) { })
this.currentTab = currentTab
} this.serviceCITYpeList = serviceCITYpeList
this.$nextTick(() => {
if (this.adCITypeList && this.adCITypeList.length && !this.currentTab) {
this.currentTab = this.adCITypeList[0].id
}
if (currentTab) {
this.currentTab = currentTab
}
})
}) })
}, },
pushCITypeList(list) {
list.forEach((item) => {
const _find = this.adrList.find((adr) => adr.id === item.adr_id)
item.icon = _find?.option?.icon || {}
})
this.$set(this, 'clientCITypeList', [
...this.clientCITypeList,
...list
])
this.currentTab = list[0].id
},
getADCITypeParam(adr_id, params = 'name', isAll = false) { getADCITypeParam(adr_id, params = 'name', isAll = false) {
const _find = this.adrList.find((item) => item.id === adr_id) const _find = this.adrList.find((item) => item.id === adr_id)
if (_find) { if (_find) {
@@ -152,52 +183,119 @@ export default {
return _find[`${params}`] return _find[`${params}`]
} }
}, },
async deleteADT(e, item) { async deleteADT(item) {
e.preventDefault()
e.stopPropagation()
const that = this const that = this
const is_plugin = this.getADCITypeParam(item.adr_id, 'is_plugin')
this.$confirm({ this.$confirm({
title: that.$t('cmdb.ciType.confirmDeleteADT', { pluginName: `${item?.extra_option?.alias || this.getADCITypeParam(item.adr_id)}` }), title: that.$t('cmdb.ciType.confirmDeleteADT', { pluginName: `${item?.extra_option?.alias || this.getADCITypeParam(item.adr_id)}` }),
content: (h) => ( content: (h) => {
<div> if (!is_plugin) {
<a-checkbox v-model={that.deletePlugin}>{that.$t('cmdb.ciType.deletePlugin')}</a-checkbox> return ''
</div> }
), return (
onOk() { <div>
deleteCITypeDiscovery(item.id).then(async () => { <a-checkbox
if (that.currentTab === item.id) { v-model={that.deletePlugin}
that.currentTab = '' >
{that.$t('cmdb.ciType.deletePlugin')}
</a-checkbox>
</div>
)
},
onOk () {
if (item.isClient) {
const adtIndex = that.clientCITypeList.findIndex((listItem) => listItem.id === item.id)
if (adtIndex !== -1) {
that.clientCITypeList.splice(adtIndex, 1)
that.currentTab = that?.adCITypeList?.[0]?.id ?? ''
if (is_plugin && that.deletePlugin) {
that.deleteDiscovery(item.adr_id)
}
} }
that.$message.success(that.$t('deleteSuccess')) } else {
that.getCITypeDiscovery() deleteCITypeDiscovery(item.id).then(async () => {
if (that.deletePlugin) { if (that.currentTab === item.id) {
await deleteDiscovery(item.adr_id).finally(() => { that.currentTab = ''
that.deletePlugin = false }
}) that.$message.success(that.$t('deleteSuccess'))
} that.getCITypeDiscovery()
that.deletePlugin = false if (is_plugin && that.deletePlugin) {
}) that.deleteDiscovery(item.adr_id)
}
that.deletePlugin = false
})
}
}, },
onCancel() { onCancel() {
that.deletePlugin = false that.deletePlugin = false
}, },
}) })
}, },
deleteDiscovery(id) {
deleteDiscovery(id).finally(async () => {
this.deletePlugin = false
await this.getDiscovery()
})
},
openEditDrawer(data, type, adType) { openEditDrawer(data, type, adType) {
this.$refs.editDrawer.open(data, type, adType) this.$refs.editDrawer.open(data, type, adType)
}, },
async updateNotInner(adr) { async updateNotInner(adr) {
const _idx = this.adCITypeList.findIndex((item) => item.adr_id === adr.id) const _idx = this.adCITypeList.findIndex((item) => item.adr_id === adr.id)
let res
if (_idx < 0) {
res = await postCITypeDiscovery(this.CITypeId, { adr_id: adr.id, interval: 300 })
}
await this.getDiscovery() await this.getDiscovery()
await this.getCITypeDiscovery(res?.id ?? undefined) if (_idx < 0) {
const ciType = {
adr_id: adr.id,
id: new Date().getTime(),
extra_option: {
alias: ''
},
isClient: true,
}
this.pushCITypeList([ciType])
}
this.$nextTick(() => { this.$nextTick(() => {
this.$refs[`attrAdTabpane_${this.currentTab}`][0].init() this.$refs[`attrAdTabpaneRef`].init()
}) })
}, },
changeTab(id) {
console.log('changeTab', id)
this.currentTab = id
},
changeAlias({ id, value, isClient }) {
if (isClient) {
const adtIndex = this.clientCITypeList.findIndex((item) => item.id === id)
this.clientCITypeList[adtIndex].extra_option.alias = value
} else {
const adtIndex = this.adCITypeList.findIndex((item) => item.id === id)
const oldExtraOption = this.adCITypeList?.[adtIndex]?.extra_option
const params = {
extra_option: {
...(oldExtraOption || {}),
alias: value
}
}
putCITypeDiscovery(id, params).then(async () => {
this.$message.success(this.$t('saveSuccess'))
await this.getCITypeDiscovery()
})
}
},
saveTabpane(id) {
const adtIndex = this.clientCITypeList.findIndex((listItem) => listItem.id === this.currentTab)
if (adtIndex !== -1) {
this.clientCITypeList.splice(adtIndex, 1)
}
this.getCITypeDiscovery(id)
}
}, },
} }
</script> </script>
@@ -206,6 +304,7 @@ export default {
.attr-ad { .attr-ad {
position: relative; position: relative;
padding: 0 20px; padding: 0 20px;
.attr-ad-header { .attr-ad-header {
width: 100%; width: 100%;
display: inline-flex; display: inline-flex;
@@ -216,7 +315,13 @@ export default {
border-left: 4px solid @primary-color; border-left: 4px solid @primary-color;
font-size: 16px; font-size: 16px;
color: rgba(0, 0, 0, 0.75); color: rgba(0, 0, 0, 0.75);
margin-top: 30px;
} }
.attr-ad-header-margin {
margin-bottom: 0px;
}
.attr-ad-footer { .attr-ad-footer {
width: 60%; width: 60%;
text-align: right; text-align: right;

View File

@@ -1,11 +1,11 @@
<template> <template>
<div :style="{ height: `${windowHeight - 187}px`, overflow: 'auto', position: 'relative' }"> <div class="attr-ad-tab-pane" :style="{ height: `${windowHeight - 254}px` }">
<a <a
v-if="!adrIsInner" v-if="!adrIsInner"
:style="{ position: 'absolute', right: 0, top: 0 }" :style="{ position: 'absolute', right: 0, top: 0 }"
@click=" @click="
() => { () => {
$emit('openEditDrawer', currentAdr, 'edit', 'agent') $emit('openEditDrawer', currentAdr, 'edit', 'plugin')
} }
" "
> >
@@ -14,76 +14,44 @@
<span>{{ $t('edit') }}</span> <span>{{ $t('edit') }}</span>
</a-space> </a-space>
</a> </a>
<div>{{ $t('alias') }}<a-input v-model="alias" style="width:200px;" /></div>
<div class="attr-ad-header">{{ $t('cmdb.ciType.attributeMap') }}</div> <div class="attr-ad-header">{{ $t('cmdb.ciType.attributeMap') }}</div>
<vxe-table <div class="attr-ad-attributemap-main">
v-if="adrType === 'agent'" <AttrMapTable
ref="xTable" v-if="adrType === 'agent'"
:edit-config="{ trigger: 'click', mode: 'cell' }" ref="attrMapTable"
size="mini" :ruleType="adrType"
stripe :tableData="tableData"
class="ops-stripe-table" :ciTypeAttributes="ciTypeAttributes"
:data="tableData" :uniqueKey="uniqueKey"
:style="{ width: '700px', marginBottom: '20px' }" />
> <HttpSnmpAD
<vxe-colgroup :title="$t('cmdb.ciType.autoDiscovery')"> v-else
<vxe-column field="name" :title="$t('name')"> </vxe-column> :isEdit="true"
<vxe-column field="type" :title="$t('type')"> </vxe-column> ref="httpSnmpAd"
<vxe-column field="desc" :title="$t('desc')"> </vxe-column> :ruleType="adrType"
</vxe-colgroup> :ruleName="adrName"
<vxe-colgroup :title="$t('cmdb.ciType.attributes')"> :ciTypeAttributes="ciTypeAttributes"
<vxe-column field="attr" :title="$t('name')" :edit-render="{}"> :adCITypeList="adCITypeList"
<template #default="{row}"> :currentTab="adr_id"
{{ row.attr }} :uniqueKey="uniqueKey"
</template> :style="{ marginBottom: '20px' }"
<template #edit="{ row }"> />
<vxe-select </div>
filterable <template v-if="adrType === 'snmp'">
clearable <div class="attr-ad-header">{{ $t('cmdb.ciType.nodeConfig') }}</div>
v-model="row.attr" <a-form :form="form3" layout="inline" class="attr-ad-snmp-form">
type="text" <NodeSetting ref="nodeSetting" :initNodes="nodes" :form="form3" />
:options="ciTypeAttributes" </a-form>
transfer </template>
></vxe-select>
</template>
</vxe-column>
</vxe-colgroup>
</vxe-table>
<HttpSnmpAD
v-else
:isEdit="true"
ref="httpSnmpAd"
:ruleType="adrType"
:ruleName="adrName"
:ciTypeAttributes="ciTypeAttributes"
:adCITypeList="adCITypeList"
:currentTab="adr_id"
:style="{ marginBottom: '20px' }"
/>
<a-form-model
v-if="adrType === 'http'"
:model="form2"
:labelCol="{ span: 2 }"
:wrapperCol="{ span: 8 }"
:style="{ margin: '20px 0' }"
>
<a-form-model-item label="key">
<a-input-password v-model="form2.key" />
</a-form-model-item>
<a-form-model-item label="secret">
<a-input-password v-model="form2.secret" />
</a-form-model-item>
</a-form-model>
<a-form :form="form3" v-if="adrType === 'snmp'" class="attr-ad-snmp-form">
<a-col :span="24">
<a-form-item :label="$t('cmdb.ciType.node')" :labelCol="{ span: 2 }" :wrapperCol="{ span: 20 }">
<MonitorNodeSetting ref="monitorNodeSetting" :initNodes="nodes" :form="form3" />
</a-form-item>
</a-col>
</a-form>
<div class="attr-ad-header">{{ $t('cmdb.ciType.adExecConfig') }}</div> <div class="attr-ad-header">{{ $t('cmdb.ciType.adExecConfig') }}</div>
<a-form-model :model="form" :labelCol="{ span: 2 }" :wrapperCol="{ span: 20 }"> <a-form-model
<a-form-model-item :label="$t('cmdb.ciType.adExecTarget')"> :model="form"
:labelCol="labelCol"
labelAlign="left"
:wrapperCol="{ span: 14 }"
class="attr-ad-form"
>
<a-form-model-item :required="true" :label="$t('cmdb.ciType.adExecTarget')">
<CustomRadio v-model="agent_type" :radioList="agentTypeRadioList"> <CustomRadio v-model="agent_type" :radioList="agentTypeRadioList">
<a-input <a-input
:style="{ width: '300px' }" :style="{ width: '300px' }"
@@ -101,18 +69,101 @@
> >
<a @click="handleOpenCmdb" slot="suffix"><a-icon type="menu"/></a> <a @click="handleOpenCmdb" slot="suffix"><a-icon type="menu"/></a>
</a-input> </a-input>
<span
v-show="agent_type === 'master'"
slot="extra_master"
class="radio-master-tip"
>
{{ $t('cmdb.ciType.masterNodeTip') }}
</span>
</CustomRadio> </CustomRadio>
</a-form-model-item> </a-form-model-item>
<a-form-model-item :label="$t('cmdb.ciType.adAutoInLib')"> <a-form-model-item
:labelCol="labelCol"
:label="$t('cmdb.ciType.adAutoInLib')"
:extra="$t('cmdb.ciType.adAutoInLibTip')"
>
<a-switch v-model="form.auto_accept" /> <a-switch v-model="form.auto_accept" />
</a-form-model-item> </a-form-model-item>
<a-form-model-item
:labelCol="labelCol"
:wrapperCol="{ span: 6 }"
:label="$t('cmdb.ciType.adInterval')"
:required="true"
>
<el-popover v-model="cronVisible" trigger="click">
<template slot>
<Vcrontab
v-if="adrType"
ref="cronTab"
:hideComponent="['second', 'year']"
:expression="cron"
:hasFooter="true"
@fill="crontabFill"
@hide="hideCron"
></Vcrontab>
</template>
<a-input
v-model="cron"
slot="reference"
:placeholder="$t('cmdb.ciType.cronTips')"
/>
</el-popover>
</a-form-model-item>
</a-form-model> </a-form-model>
<div class="attr-ad-header">{{ $t('cmdb.ciType.adInterval') }}</div> <template v-if="adrType === 'http'">
<CustomRadio :radioList="radioList" v-model="interval"> <template v-if="isPrivateCloud">
<span v-show="interval === 'interval'" slot="extra_interval"> <template v-if="privateCloudName === PRIVATE_CLOUD_NAME.VCenter">
<a-input-number v-model="intervalValue" :min="1" /> {{ $t('seconds') }} <div class="attr-ad-header">{{ $t('cmdb.ciType.privateCloud') }}</div>
</span> <a-form-model
</CustomRadio> :model="privateCloudForm"
labelAlign="left"
:labelCol="labelCol"
:wrapperCol="{ span: 6 }"
class="attr-ad-form"
>
<a-form-model-item :required="true" :label="$t('cmdb.ciType.host')">
<a-input v-model="privateCloudForm.host" />
</a-form-model-item>
<a-form-model-item :required="true" :label="$t('cmdb.ciType.account')">
<a-input v-model="privateCloudForm.account" />
</a-form-model-item>
<a-form-model-item :required="true" :label="$t('cmdb.ciType.password')">
<a-input-password v-model="privateCloudForm.password" />
</a-form-model-item>
<a-form-model-item :label="$t('cmdb.ciType.insecure')">
<a-switch v-model="privateCloudForm.insecure" />
</a-form-model-item>
<a-form-model-item :label="$t('cmdb.ciType.vcenterName')">
<a-input v-model="privateCloudForm.vcenterName" />
</a-form-model-item>
</a-form-model>
</template>
</template>
<template v-else>
<div class="attr-ad-header">{{ $t('cmdb.ciType.cloudAccessKey') }}</div>
<!-- <div class="public-cloud-info">{{ $t('cmdb.ciType.cloudAccessKeyTip') }}</div> -->
<a-form-model
:model="form2"
labelAlign="left"
:labelCol="labelCol"
:wrapperCol="{ span: 6 }"
class="attr-ad-form"
>
<a-form-model-item :required="true" label="key">
<a-input-password v-model="form2.key" />
</a-form-model-item>
<a-form-model-item :required="true" label="secret">
<a-input-password v-model="form2.secret" />
</a-form-model-item>
</a-form-model>
</template>
</template>
<AttrADTest
:adtId="currentAdt.id"
/>
<div class="attr-ad-footer"> <div class="attr-ad-footer">
<a-button type="primary" @click="handleSave">{{ $t('save') }}</a-button> <a-button type="primary" @click="handleSave">{{ $t('save') }}</a-button>
@@ -125,14 +176,27 @@
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import { mapState } from 'vuex' import { mapState } from 'vuex'
import Vcrontab from '@/components/Crontab' import Vcrontab from '@/components/Crontab'
import { putCITypeDiscovery } from '../../api/discovery' import { putCITypeDiscovery, postCITypeDiscovery } from '../../api/discovery'
import { PRIVATE_CLOUD_NAME } from '@/modules/cmdb/views/discovery/constants.js'
import HttpSnmpAD from '../../components/httpSnmpAD' import HttpSnmpAD from '../../components/httpSnmpAD'
import AttrMapTable from '@/modules/cmdb/components/attrMapTable/index.vue'
import CMDBExprDrawer from '@/components/CMDBExprDrawer' import CMDBExprDrawer from '@/components/CMDBExprDrawer'
import MonitorNodeSetting from '@/components/MonitorNodeSetting' import NodeSetting from '@/modules/cmdb/components/nodeSetting/index.vue'
import AttrADTest from './attrADTest.vue'
import { Popover } from 'element-ui'
export default { export default {
name: 'AttrADTabpane', name: 'AttrADTabpane',
components: { Vcrontab, HttpSnmpAD, CMDBExprDrawer, MonitorNodeSetting }, components: {
Vcrontab,
HttpSnmpAD,
CMDBExprDrawer,
NodeSetting,
AttrMapTable,
AttrADTest,
ElPopover: Popover
},
props: { props: {
adr_id: { adr_id: {
type: Number, type: Number,
@@ -158,6 +222,10 @@ export default {
type: Array, type: Array,
default: () => [], default: () => [],
}, },
CITypeId: {
type: Number,
default: null,
},
}, },
data() { data() {
return { return {
@@ -171,7 +239,14 @@ export default {
key: '', key: '',
secret: '', secret: '',
}, },
interval: 'interval', // interval cron privateCloudForm: {
host: '',
account: '',
password: '',
insecure: false,
vcenterName: '',
},
interval: 'cron', // interval cron
cron: '', cron: '',
intervalValue: 3, intervalValue: 3,
agent_type: 'agent_id', agent_type: 'agent_id',
@@ -184,60 +259,101 @@ export default {
}, },
], ],
form3: this.$form.createForm(this, { name: 'snmp_form' }), form3: this.$form.createForm(this, { name: 'snmp_form' }),
alias: '', cronVisible: false,
uniqueKey: '',
isPrivateCloud: false,
privateCloudName: '',
PRIVATE_CLOUD_NAME
} }
}, },
computed: { computed: {
...mapState({ ...mapState({
windowHeight: (state) => state.windowHeight, windowHeight: (state) => state.windowHeight,
userRoles: (state) => state.user.roles, user: (state) => state.user,
}), }),
adrType() { adrType() {
return this.currentAdr.type return this.currentAdr?.type || ''
}, },
adrName() { adrName() {
return this.currentAdr.name return this?.currentAdr?.option?.en || this.currentAdr?.name || ''
}, },
adrIsInner() { adrIsInner() {
return this.currentAdr.is_inner return this.currentAdr?.is_inner || ''
}, },
agentTypeRadioList() { agentTypeRadioList() {
const { permissions = [] } = this.userRoles const radios = [
if ((permissions.includes('cmdb_admin') || permissions.includes('admin')) && this.adrType !== 'http') {
return [
{ value: 'all', label: this.$t('cmdb.ciType.allNodes') },
{ value: 'agent_id', label: this.$t('cmdb.ciType.specifyNodes') },
{ value: 'query_expr', label: this.$t('cmdb.ciType.selectFromCMDBTips') },
]
}
return [
{ value: 'agent_id', label: this.$t('cmdb.ciType.specifyNodes') }, { value: 'agent_id', label: this.$t('cmdb.ciType.specifyNodes') },
{ value: 'query_expr', label: this.$t('cmdb.ciType.selectFromCMDBTips') }, { value: 'query_expr', label: this.$t('cmdb.ciType.selectFromCMDBTips') },
] ]
const permissions = this?.user?.roles?.permissions
if ((permissions.includes('cmdb_admin') || permissions.includes('admin')) && this.adrType === 'agent') {
radios.unshift({ value: 'all', label: this.$t('cmdb.ciType.allNodes') })
}
if (this.adrType !== 'agent' || this?.currentAdr?.is_plugin) {
radios.unshift({ value: 'master', label: this.$t('cmdb.ciType.masterNode') })
}
return radios
}, },
radioList() { radioList() {
return [ return [
{ value: 'interval', label: this.$t('cmdb.ciType.byInterval') }, { value: 'interval', label: this.$t('cmdb.ciType.byInterval') },
// { value: 'cron', label: '按cron', layout: 'vertical' }, { value: 'cron', label: '按cron', layout: 'vertical' },
] ]
}, },
labelCol() {
const span = this.$i18n.locale === 'en' ? 5 : 3
return {
span
}
}
}, },
mounted() {}, mounted() {},
methods: { methods: {
init() { init() {
const _find = this.adrList.find((item) => Number(item.id) === Number(this.adr_id)) const _find = this.adrList.find((item) => Number(item.id) === Number(this.adr_id))
const _findADT = this.adCITypeList.find((item) => Number(item.id) === Number(this.currentAdt.id)) const _findADT = this.adCITypeList.find((item) => Number(item.id) === Number(this.currentAdt.id))
this.alias = _findADT?.extra_option?.alias ?? '' this.uniqueKey = _find?.unique_key ?? ''
if (this.adrType === 'http') { if (this.adrType === 'http') {
const { category = undefined, key = '', secret = '' } = _findADT?.extra_option ?? {} const {
this.form2 = { category = undefined,
key, key = '',
secret, secret = '',
host = '',
account = '',
password = '',
insecure = false,
vcenterName = ''
} = _findADT?.extra_option ?? {}
if (_find?.option?.category === 'private_cloud') {
this.isPrivateCloud = true
this.privateCloudName = _find?.option?.en || ''
if (this.privateCloudName === PRIVATE_CLOUD_NAME.VCenter) {
this.privateCloudForm = {
host,
account,
password,
insecure,
vcenterName,
}
}
} else {
this.isPrivateCloud = false
this.form2 = {
key,
secret,
}
} }
this.$refs.httpSnmpAd.setCurrentCate(category) this.$refs.httpSnmpAd.setCurrentCate(category)
} }
if (this.adrType === 'snmp') { if (this.adrType === 'snmp') {
this.nodes = _findADT?.extra_option?.nodes ?? [ this.nodes = _findADT?.extra_option?.nodes?.length ? _findADT?.extra_option?.nodes : [
{ {
id: uuidv4(), id: uuidv4(),
ip: '', ip: '',
@@ -246,9 +362,9 @@ export default {
}, },
] ]
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.monitorNodeSetting.initNodesFunc() this.$refs.nodeSetting.initNodesFunc()
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.monitorNodeSetting.setNodeField() this.$refs.nodeSetting.setNodeField()
}) })
}) })
} }
@@ -273,49 +389,57 @@ export default {
} }
this.form = { this.form = {
auto_accept: _findADT?.auto_accept || false, auto_accept: _findADT?.auto_accept || false,
agent_id: _findADT.agent_id || '', agent_id: _findADT?.agent_id && _findADT?.agent_id !== '0x0000' ? _findADT.agent_id : '',
query_expr: _findADT.query_expr || '', query_expr: _findADT.query_expr || '',
} }
if (_findADT.query_expr) { if (_findADT.query_expr) {
this.agent_type = 'query_expr' this.agent_type = 'query_expr'
} else if (_findADT.agent_id) { } else if (_findADT.agent_id) {
this.agent_type = 'agent_id' this.agent_type = _findADT.agent_id === '0x0000' ? 'master' : 'agent_id'
} else { } else {
this.agent_type = this.agentTypeRadioList[0].value this.agent_type = this.agentTypeRadioList[0].value
} }
if (_findADT.interval || (!_findADT.interval && !_findADT.cron)) {
this.interval = 'interval' this.interval = 'cron'
this.intervalValue = _findADT.interval || '' this.cron = _findADT?.cron || ''
} else {
this.interval = 'cron'
this.cron = `0 ${_findADT.cron}`
}
},
getAttrNameByAttrName(attrName) {
const _find = this.ciTypeAttributes.find((item) => item.name === attrName)
return _find?.alias || _find?.name || ''
}, },
crontabFill(cron) { crontabFill(cron) {
this.cron = cron this.cron = cron
}, },
handleSave() { handleSave() {
const { currentAdt, alias } = this const { currentAdt } = this
let params let params
const isError = this.validateForm()
if (isError) {
return
}
if (this.adrType === 'http') { if (this.adrType === 'http') {
let cloudOption = {}
if (this.isPrivateCloud) {
if (this.privateCloudName === PRIVATE_CLOUD_NAME.VCenter) {
cloudOption = this.privateCloudForm
}
} else {
cloudOption = this.form2
}
params = { params = {
extra_option: { extra_option: {
...this.form2, ...cloudOption,
category: this.$refs.httpSnmpAd.currentCate, category: this.$refs.httpSnmpAd.currentCate,
}, },
} }
} }
if (this.adrType === 'snmp') { if (this.adrType === 'snmp') {
params = { params = {
extra_option: { nodes: this.$refs.monitorNodeSetting?.getNodeValue() ?? [] }, extra_option: { nodes: this.$refs.nodeSetting?.getNodeValue() ?? [] },
} }
} }
if (this.adrType === 'agent') { if (this.adrType === 'agent') {
const $table = this.$refs.xTable const $table = this.$refs.attrMapTable
const { fullData: _tableData } = $table.getTableData() const { fullData: _tableData } = $table.getTableData()
const attributes = {} const attributes = {}
_tableData.forEach((td) => { _tableData.forEach((td) => {
@@ -340,17 +464,14 @@ export default {
attributes, attributes,
} }
} }
if (this.interval === 'cron') {
this.$refs.cronTab.submitFill()
}
params = { params = {
...params, ...params,
...this.form, ...this.form,
type_id: this.CITypeId,
adr_id: currentAdt.adr_id, adr_id: currentAdt.adr_id,
interval: this.interval === 'interval' ? this.intervalValue : null,
cron: this.interval === 'cron' ? this.cron : null, cron: this.interval === 'cron' ? this.cron : null,
} }
if (this.agent_type === 'agent_id' || this.agent_type === 'all') { if (this.agent_type === 'agent_id' || this.agent_type === 'all') {
params.query_expr = '' params.query_expr = ''
if (this.agent_type === 'agent_id' && !params.agent_id) { if (this.agent_type === 'agent_id' && !params.agent_id) {
@@ -358,6 +479,7 @@ export default {
return return
} }
} }
if (this.agent_type === 'query_expr' || this.agent_type === 'all') { if (this.agent_type === 'query_expr' || this.agent_type === 'all') {
params.agent_id = '' params.agent_id = ''
if (this.agent_type === 'query_expr' && !params.query_expr) { if (this.agent_type === 'query_expr' && !params.query_expr) {
@@ -365,17 +487,69 @@ export default {
return return
} }
} }
if (params.extra_option) {
params.extra_option.alias = alias if (this.agent_type === 'master') {
} else { params.agent_id = '0x0000'
params.extra_option = {} }
params.extra_option.alias = alias
if (!this.cron) {
this.$message.error(this.$t('cmdb.ciType.cronRequiredTip'))
return
}
if (currentAdt?.extra_option) {
params.extra_option = {
...(currentAdt?.extra_option || {}),
...(params?.extra_option || {})
}
}
if (currentAdt?.isClient) {
postCITypeDiscovery(this.CITypeId, params).then((res) => {
this.$message.success(this.$t('saveSuccess'))
this.$emit('handleSave', res.id)
})
} else {
putCITypeDiscovery(currentAdt.id, params).then((res) => {
this.$message.success(this.$t('saveSuccess'))
this.$emit('handleSave', res.id)
})
} }
putCITypeDiscovery(currentAdt.id, params).then((res) => {
this.$message.success(this.$t('saveSuccess'))
this.$emit('handleSave')
})
}, },
validateForm() {
let isError = false
if (this.adrType === 'http') {
if (this.isPrivateCloud) {
if (this.privateCloudName === PRIVATE_CLOUD_NAME.VCenter) {
const vcenterErros = {
'host': `${this.$t('placeholder1')} ${this.$t('cmdb.ciType.host')}`,
'account': `${this.$t('placeholder1')} ${this.$t('cmdb.ciType.account')}`,
'password': `${this.$t('placeholder1')} ${this.$t('cmdb.ciType.password')}`
}
const findError = Object.keys(this.privateCloudForm).find((key) => !this.privateCloudForm[key] && vcenterErros[key])
if (findError) {
isError = true
this.$message.error(this.$t(vcenterErros[findError]))
}
}
} else {
const publicCloudErros = {
'key': `${this.$t('placeholder1')} key`,
'secret': `${this.$t('placeholder1')} secret`
}
const findError = Object.keys(this.form2).find((key) => !this.form2[key] && publicCloudErros[key])
if (findError) {
isError = true
this.$message.error(this.$t(publicCloudErros[findError]))
}
}
}
return isError
},
handleOpenCmdb() { handleOpenCmdb() {
this.$refs.cmdbDrawer.open() this.$refs.cmdbDrawer.open()
}, },
@@ -385,11 +559,46 @@ export default {
query_expr: `${text}`, query_expr: `${text}`,
} }
}, },
hideCron() {
this.cronVisible = false
},
}, },
} }
</script> </script>
<style lang="less"> <style lang="less" scoped>
.attr-ad-tab-pane {
overflow-y: auto;
overflow-x: hidden;
position: relative;
.attr-ad-attributemap-main {
margin-left: 17px;
}
.attr-ad-form {
/deep/ .ant-form-item-label {
margin-left: 17px;
}
/deep/ .ant-form-item-control-wrapper {
// margin-left: -40px;
}
}
.public-cloud-info {
color: @text-color_3;
font-size: 12px;
font-weight: 400;
margin-left: 17px;
margin-bottom: 20px;
}
.radio-master-tip {
font-size: 12px;
color: #86909c;
}
}
.attr-ad-snmp-form { .attr-ad-snmp-form {
.ant-form-item { .ant-form-item {
margin-bottom: 0; margin-bottom: 0;

View File

@@ -0,0 +1,183 @@
<template>
<div class="attr-ad-tabs">
<div
v-for="item in adCITypeList"
:key="item.id"
:class="['attr-ad-tab', currentTab === item.id ? 'attr-ad-tab_active' : '']"
@click="changeTab(item.id)"
>
<img
v-if="item.icon.id && item.icon.url"
:src="`/api/common-setting/v1/file/${item.icon.url}`"
class="attr-ad-tab-icon"
/>
<ops-icon
v-else-if="item.icon.name"
:type="item.icon.name || 'caise-chajian'"
:style="{ color: item.icon.color }"
class="attr-ad-tab-icon"
/>
<a-input
v-if="nameEditId === item.id"
v-model="nameEditValue"
:ref="`name-edit-${item.id}`"
size="small"
:autofocus="true"
@blur="changeAlias(item.isClient || false)"
/>
<span v-else class="attr-ad-tab-name">
{{ item.extra_option && item.extra_option.alias ? item.extra_option.alias : getADCITypeParam(item.adr_id) }}
</span>
<a-icon
type="edit"
class="attr-ad-tab-edit"
@click="(e) => openNameEdit(e, item)"
/>
<a-icon
type="delete"
class="attr-ad-tab-delete"
@click="(e) => deleteADT(e, item)"
/>
</div>
<a-icon
type="plus-circle"
class="attr-ad-tabs-add"
@click="clickAdd"
></a-icon>
</div>
</template>
<script>
export default {
name: 'AttrADTabs',
props: {
currentTab: {
type: [String, Number],
default: ''
},
adCITypeList: {
type: Array,
default: () => [],
},
getADCITypeParam: {
type: Function,
default: () => ''
}
},
data() {
return {
nameEditId: '',
nameEditValue: '',
}
},
methods: {
changeTab(id) {
this.$emit('changeTab', id)
},
openNameEdit(e, item) {
e.preventDefault()
e.stopPropagation()
this.nameEditId = item.id
if (item?.extra_option?.alias) {
this.nameEditValue = item.extra_option.alias
}
this.$nextTick(() => {
if (this.$refs?.[`name-edit-${item.id}`]?.[0]) {
this.$refs[`name-edit-${item.id}`][0].focus()
}
})
},
changeAlias(isClient) {
this.$emit('changeAlias', {
id: this.nameEditId,
value: this.nameEditValue,
isClient
})
this.$nextTick(() => {
this.nameEditId = ''
this.nameEditValue = ''
})
},
deleteADT(e, item) {
e.preventDefault()
e.stopPropagation()
this.$emit('deleteADT', item)
},
clickAdd() {
this.$emit('clickAdd')
}
}
}
</script>
<style lang="less" scoepd>
.attr-ad-tabs {
display: flex;
align-items: center;
width: 100%;
overflow-x: auto;
padding-bottom: 10px;
.attr-ad-tab {
display: flex;
align-items: center;
justify-content: center;
padding: 8px 24px;
margin-right: 12px;
background-color: @primary-color_7;
cursor: pointer;
flex-shrink: 0;
&-name {
font-weight: 400;
font-size: 12px;
}
&-icon {
font-size: 12px;
width: 12px;
height: 12px;
margin-right: 4px;
}
&-edit {
display: none;
font-size: 10px;
color: @text-color_4;
margin-left: 4px;
}
&-delete {
display: none;
font-size: 10px;
color: @func-color_1;
margin-left: 6px;
}
&_active {
border: solid 1px @primary-color_8;
background-color: @primary-color_6;
.attr-ad-tab-name {
color: @primary-color;
}
}
&:hover {
.attr-ad-tab-edit {
display: inline-block;
}
.attr-ad-tab-delete {
display: inline-block;
}
}
}
&-add {
padding: 11px;
background-color: @primary-color_7;
font-size: 12px;
color: @text-color_4;
}
}
</style>

View File

@@ -0,0 +1,205 @@
<template>
<div>
<div class="attr-ad-header attr-ad-header-margin">{{ $t('cmdb.ciType.configCheckTitle') }}</div>
<div class="attr-ad-content">
<div class="ad-test-title-info">{{ $t('cmdb.ciType.checkTestTip') }}</div>
<div
class="ad-test-btn"
@click="showCheckModal"
>
{{ $t('cmdb.ciType.checkTestBtn') }}
</div>
<div class="ad-test-btn-info">{{ $t('cmdb.ciType.checkTestTip2') }}</div>
<!-- <div
class="ad-test-btn"
@click="showTestModal"
>
{{ $t('cmdb.ciType.checkTestBtn1') }}
</div>
<div class="ad-test-btn-info">{{ $t('cmdb.ciType.checkTestTip3') }}</div> -->
</div>
<a-modal
v-model="checkModalVisible"
:footer="null"
:width="900"
>
<div class="check-modal-title">{{ $t('cmdb.ciType.checkModalTitle') }}</div>
<div class="check-modal-info">{{ $t('cmdb.ciType.checkModalTip') }}</div>
<div class="check-modal-info">{{ $t('cmdb.ciType.checkModalTip1') }}</div>
<div class="check-modal-info">{{ $t('cmdb.ciType.checkModalTip2') }}</div>
<ops-table
size="mini"
:data="checkTableData"
:scroll-y="{ enabled: true }"
height="400"
class="check-modal-table"
>
<vxe-column field="oneagent_name" :title="$t('cmdb.ciType.checkModalColumn1')"></vxe-column>
<vxe-column field="oneagent_id" :title="$t('cmdb.ciType.checkModalColumn2')"></vxe-column>
<vxe-column
field="status"
:min-width="70"
:title="$t('cmdb.ciType.checkModalColumn3')"
>
<template #default="{ row }">
<div
:class="['check-modal-status', row.status ? 'check-modal-status-online' : 'check-modal-status-offline']"
>
{{ $t(`cmdb.ciType.${row.status ? 'checkModalColumnStatus1' : 'checkModalColumnStatus2'}`) }}
</div>
</template>
</vxe-column>
<vxe-column field="sync_at" :title="$t('cmdb.ciType.checkModalColumn4')"></vxe-column>
</ops-table>
</a-modal>
<a-modal
v-model="testModalVisible"
:footer="null"
:width="596"
>
<div class="check-modal-title">{{ $t('cmdb.ciType.testModalTitle') }}</div>
<p class="test-modal-text">{{ testResultText }}</p>
</a-modal>
</div>
</template>
<script>
import {
getAdtSyncHistories,
postAdtTest,
getAdtTestResult
} from '@/modules/cmdb/api/discovery.js'
import moment from 'moment'
export default {
name: 'AttrADTest',
props: {
adtId: {
type: Number,
default: 0,
}
},
data() {
return {
checkModalVisible: false,
checkTableData: [],
testModalVisible: false,
testResultText: '',
}
},
methods: {
async showCheckModal() {
await this.queryCheckTableData()
this.checkModalVisible = true
},
async queryCheckTableData() {
const res = await getAdtSyncHistories(this.adtId)
if (res?.result?.length) {
const newTableData = res.result
newTableData.forEach((item) => {
const syncTime = moment(item.sync_at).valueOf()
const nowTime = new Date().getTime()
item.status = nowTime - syncTime <= 10 * 60 * 1000
})
this.checkTableData = newTableData
} else {
this.checkTableData = []
}
},
async showTestModal() {
await this.queryTestResult()
this.testModalVisible = true
},
async queryTestResult() {
const res = await postAdtTest(this.adtId)
const exec_id = res?.exec_id
if (exec_id) {
const res = await getAdtTestResult(exec_id)
if (res?.stdout) {
this.testResultText = res.stdout
}
}
}
},
}
</script>
<style lang="less" scoped>
.attr-ad-content {
margin-left: 17px;
margin-bottom: 20px;
.ad-test-title-info {
color: @text-color_3;
font-size: 12px;
font-weight: 400;
}
.ad-test-btn {
margin-top: 30px;
padding: 5px 12px;
background-color: #F4F9FF;
border: solid 1px @primary-color_8;
display: inline-block;
cursor: pointer;
color: @link-color;
font-size: 12px;
font-weight: 400;
}
.ad-test-btn-info {
margin-top: 4px;
color: @text-color_3;
font-size: 12px;
font-weight: 400;
}
}
.check-modal-table {
margin-top: 14px;
}
.check-modal-title {
font-size: 14px;
font-weight: 500;
margin-bottom: 5px;
}
.check-modal-info {
color: @text-color_3;
font-size: 12px;
font-weight: 400;
}
.check-modal-status {
display: inline-block;
padding: 2px 11px;
font-size: 12px;
font-weight: 400;
&-online {
background-color: #E5F6DF;
color: #30AD2D;
}
&-offline {
background-color: #FFDADA;
color: #F14E4E;
}
}
.test-modal-text {
margin-top: 14px;
padding: 12px;
width: 100%;
height: 312px;
overflow: auto;
white-space: pre-wrap;
word-break: break-all;
border: solid 1px @border-color-base;
}
</style>

View File

@@ -180,6 +180,10 @@ export default {
label: this.$t('cmdb.ciType.isIndex'), label: this.$t('cmdb.ciType.isIndex'),
property: 'is_index', property: 'is_index',
}, },
{
label: this.$t('cmdb.ciType.isDynamic'),
property: 'is_dynamic',
},
] ]
}, },
inherited() { inherited() {
@@ -237,8 +241,8 @@ export default {
<style lang="less" scoped> <style lang="less" scoped>
.attribute-card { .attribute-card {
width: 182px; width: 172px;
height: 80px; height: 75px;
background: @primary-color_6; background: @primary-color_6;
border-radius: 2px; border-radius: 2px;
position: relative; position: relative;
@@ -304,7 +308,7 @@ export default {
} }
} }
.attribute-card-footer { .attribute-card-footer {
width: 182px; width: 172px;
height: 30px; height: 30px;
padding: 0 8px; padding: 0 8px;
position: absolute; position: absolute;

View File

@@ -157,33 +157,6 @@
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="6">
<a-form-item
:label-col="horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol"
:label="$t('required')"
>
<a-switch
@change="(checked) => onChange(checked, 'is_required')"
name="is_required"
v-decorator="['is_required', { rules: [], valuePropName: 'checked' }]"
/>
</a-form-item>
</a-col>
<a-col :span="6" v-if="currentValueType !== '6' && currentValueType !== '7'">
<a-form-item
:label-col="{ span: 8 }"
:wrapper-col="horizontalFormItemLayout.wrapperCol"
:label="$t('cmdb.ciType.unique')"
>
<a-switch
:disabled="isShowComputedArea"
@change="onChange"
name="is_unique"
v-decorator="['is_unique', { rules: [], valuePropName: 'checked' }]"
/>
</a-form-item>
</a-col>
<a-col :span="currentValueType === '2' ? 6 : 0" v-if="currentValueType !== '6'"> <a-col :span="currentValueType === '2' ? 6 : 0" v-if="currentValueType !== '6'">
<a-form-item <a-form-item
:hidden="currentValueType === '2' ? false : true" :hidden="currentValueType === '2' ? false : true"
@@ -196,7 +169,7 @@
>{{ $t('cmdb.ciType.index') }} >{{ $t('cmdb.ciType.index') }}
<a-tooltip :title="$t('cmdb.ciType.indexTips')"> <a-tooltip :title="$t('cmdb.ciType.indexTips')">
<a-icon <a-icon
style="position:absolute;top:3px;left:-17px;color:#2f54eb;" style="position:absolute;top:2px;left:-17px;color:#2f54eb;"
type="question-circle" type="question-circle"
theme="filled" theme="filled"
@click=" @click="
@@ -217,10 +190,37 @@
/> />
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="6"> <a-col :span="6" v-if="currentValueType !== '6' && currentValueType !== '7'">
<a-form-item <a-form-item
:label-col="currentValueType === '2' ? { span: 8 } : horizontalFormItemLayout.labelCol" :label-col="currentValueType === '2' ? { span: 8 } : horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol" :wrapper-col="horizontalFormItemLayout.wrapperCol"
:label="$t('cmdb.ciType.unique')"
>
<a-switch
:disabled="isShowComputedArea"
@change="onChange"
name="is_unique"
v-decorator="['is_unique', { rules: [], valuePropName: 'checked' }]"
/>
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item
:label-col="['2', '6', '7'].findIndex(i => currentValueType === i) === -1 ? { span: 8 } : horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol"
:label="$t('required')"
>
<a-switch
@change="(checked) => onChange(checked, 'is_required')"
name="is_required"
v-decorator="['is_required', { rules: [], valuePropName: 'checked' }]"
/>
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item
:label-col="currentValueType === '2' ? { span: 12 } : horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol"
> >
<template slot="label"> <template slot="label">
<span <span
@@ -228,7 +228,7 @@
>{{ $t('cmdb.ciType.defaultShow') }} >{{ $t('cmdb.ciType.defaultShow') }}
<a-tooltip :title="$t('cmdb.ciType.defaultShowTips')"> <a-tooltip :title="$t('cmdb.ciType.defaultShowTips')">
<a-icon <a-icon
style="position:absolute;top:3px;left:-17px;color:#2f54eb;" style="position:absolute;top:2px;left:-17px;color:#2f54eb;"
type="question-circle" type="question-circle"
theme="filled" theme="filled"
@click=" @click="
@@ -295,6 +295,37 @@
/> />
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col span="6">
<a-form-item
:label-col="['2', '6', '7'].findIndex(i => currentValueType === i) === -1 ? { span: 12 } : horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol"
>
<template slot="label">
<span
style="position:relative;white-space:pre;"
>{{ $t('cmdb.ciType.isDynamic') }}
<a-tooltip :title="$t('cmdb.ciType.dynamicTips')">
<a-icon
style="position:absolute;top:3px;left:-17px;color:#2f54eb;"
type="question-circle"
theme="filled"
@click="
(e) => {
e.stopPropagation()
e.preventDefault()
}
"
/>
</a-tooltip>
</span>
</template>
<a-switch
@change="(checked) => onChange(checked, 'is_dynamic')"
name="is_dynamic"
v-decorator="['is_dynamic', { rules: [], valuePropName: 'checked' }]"
/>
</a-form-item>
</a-col>
<a-divider style="font-size:14px;margin-top:6px;">{{ $t('cmdb.ciType.advancedSettings') }}</a-divider> <a-divider style="font-size:14px;margin-top:6px;">{{ $t('cmdb.ciType.advancedSettings') }}</a-divider>
<a-row> <a-row>
<a-col :span="24" v-if="!['6'].includes(currentValueType)"> <a-col :span="24" v-if="!['6'].includes(currentValueType)">
@@ -534,6 +565,7 @@ export default {
is_index: _record.is_index, is_index: _record.is_index,
is_sortable: _record.is_sortable, is_sortable: _record.is_sortable,
is_computed: _record.is_computed, is_computed: _record.is_computed,
is_dynamic: _record.is_dynamic,
}) })
} }
console.log(_record) console.log(_record)

View File

@@ -104,7 +104,7 @@
:filter="'.filter-empty'" :filter="'.filter-empty'"
:animation="300" :animation="300"
tag="div" tag="div"
style="width: 100%; display: flex;flex-flow: wrap" style="width: 100%; display: flex; flex-flow: wrap; column-gap: 10px;"
handle=".handle" handle=".handle"
> >
<AttributeCard <AttributeCard
@@ -146,7 +146,7 @@
} }
" "
:animation="300" :animation="300"
style="min-height: 2rem; width: 100%; display: flex; flex-flow: wrap" style="min-height: 2rem; width: 100%; display: flex; flex-flow: wrap; column-gap: 10px;"
handle=".handle" handle=".handle"
> >
<AttributeCard <AttributeCard

View File

@@ -150,42 +150,19 @@
</a-col> </a-col>
</a-row> </a-row>
<a-col :span="6"> <a-col :span="currentValueType === '2' ? 6 : 0" v-if="currentValueType !== '6'">
<a-form-item <a-form-item
:hidden="currentValueType === '2' ? false : true"
:label-col="horizontalFormItemLayout.labelCol" :label-col="horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol" :wrapper-col="horizontalFormItemLayout.wrapperCol"
:label="$t('required')"
> >
<a-switch
@change="(checked) => onChange(checked, 'is_required')"
name="is_required"
v-decorator="['is_required', { rules: [], valuePropName: 'checked' }]"
/>
</a-form-item>
</a-col>
<a-col :span="6" v-if="currentValueType !== '6' && currentValueType !== '7'">
<a-form-item
:label-col="{ span: 8 }"
:wrapper-col="horizontalFormItemLayout.wrapperCol"
:label="$t('cmdb.ciType.unique')"
>
<a-switch
:disabled="isShowComputedArea"
@change="onChange"
name="is_unique"
v-decorator="['is_unique', { rules: [], valuePropName: 'checked' }]"
/>
</a-form-item>
</a-col>
<a-col :span="6" v-if="currentValueType === '2'">
<a-form-item :label-col="horizontalFormItemLayout.labelCol" :wrapper-col="horizontalFormItemLayout.wrapperCol">
<template slot="label"> <template slot="label">
<span <span
style="position:relative;white-space:pre;" style="position:relative;white-space:pre;"
>{{ $t('cmdb.ciType.index') }} >{{ $t('cmdb.ciType.index') }}
<a-tooltip :title="$t('cmdb.ciType.indexTips')"> <a-tooltip :title="$t('cmdb.ciType.indexTips')">
<a-icon <a-icon
style="position:absolute;top:3px;left:-17px;color:#2f54eb;" style="position:absolute;top:2px;left:-17px;color:#2f54eb;"
type="question-circle" type="question-circle"
theme="filled" theme="filled"
@click=" @click="
@@ -206,10 +183,37 @@
/> />
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="6"> <a-col :span="6" v-if="currentValueType !== '6' && currentValueType !== '7'">
<a-form-item <a-form-item
:label-col="currentValueType === '2' ? { span: 8 } : horizontalFormItemLayout.labelCol" :label-col="currentValueType === '2' ? { span: 8 } : horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol" :wrapper-col="horizontalFormItemLayout.wrapperCol"
:label="$t('cmdb.ciType.unique')"
>
<a-switch
:disabled="isShowComputedArea"
@change="onChange"
name="is_unique"
v-decorator="['is_unique', { rules: [], valuePropName: 'checked' }]"
/>
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item
:label-col="['2', '6', '7'].findIndex(i => currentValueType === i) === -1 ? { span: 8 } : horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol"
:label="$t('required')"
>
<a-switch
@change="(checked) => onChange(checked, 'is_required')"
name="is_required"
v-decorator="['is_required', { rules: [], valuePropName: 'checked' }]"
/>
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item
:label-col="currentValueType === '2' ? { span: 12 } : horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol"
> >
<template slot="label"> <template slot="label">
<span <span
@@ -217,7 +221,7 @@
>{{ $t('cmdb.ciType.defaultShow') }} >{{ $t('cmdb.ciType.defaultShow') }}
<a-tooltip :title="$t('cmdb.ciType.defaultShowTips')"> <a-tooltip :title="$t('cmdb.ciType.defaultShowTips')">
<a-icon <a-icon
style="position:absolute;top:3px;left:-17px;color:#2f54eb;" style="position:absolute;top:2px;left:-17px;color:#2f54eb;"
type="question-circle" type="question-circle"
theme="filled" theme="filled"
@click=" @click="
@@ -284,6 +288,37 @@
/> />
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col span="6">
<a-form-item
:label-col="['2', '6', '7'].findIndex(i => currentValueType === i) === -1 ? { span: 12 } : horizontalFormItemLayout.labelCol"
:wrapper-col="horizontalFormItemLayout.wrapperCol"
>
<template slot="label">
<span
style="position:relative;white-space:pre;"
>{{ $t('cmdb.ciType.isDynamic') }}
<a-tooltip :title="$t('cmdb.ciType.dynamicTips')">
<a-icon
style="position:absolute;top:3px;left:-17px;color:#2f54eb;"
type="question-circle"
theme="filled"
@click="
(e) => {
e.stopPropagation()
e.preventDefault()
}
"
/>
</a-tooltip>
</span>
</template>
<a-switch
@change="(checked) => onChange(checked, 'is_dynamic')"
name="is_dynamic"
v-decorator="['is_dynamic', { rules: [], valuePropName: 'checked', initialValue: currentValueType === '6' ? true: false }]"
/>
</a-form-item>
</a-col>
<a-divider style="font-size:14px;margin-top:6px;">{{ $t('cmdb.ciType.advancedSettings') }}</a-divider> <a-divider style="font-size:14px;margin-top:6px;">{{ $t('cmdb.ciType.advancedSettings') }}</a-divider>
<a-row> <a-row>
<a-col :span="24" v-if="!['6'].includes(currentValueType)"> <a-col :span="24" v-if="!['6'].includes(currentValueType)">
@@ -404,8 +439,8 @@ export default {
} }
console.log(values) console.log(values)
const { is_required, default_show, default_value } = values const { is_required, default_show, default_value, is_dynamic } = values
const data = { is_required, default_show } const data = { is_required, default_show, is_dynamic }
delete values.is_required delete values.is_required
delete values.default_show delete values.default_show
if (values.value_type === '0' && default_value) { if (values.value_type === '0' && default_value) {

View File

@@ -5,21 +5,20 @@
<AttributesTable ref="attributesTable" :CITypeId="CITypeId" :CITypeName="CITypeName"></AttributesTable> <AttributesTable ref="attributesTable" :CITypeId="CITypeId" :CITypeName="CITypeName"></AttributesTable>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="2" :tab="$t('cmdb.ciType.relation')"> <a-tab-pane key="2" :tab="$t('cmdb.ciType.relation')">
<RelationTable :CITypeId="CITypeId" :CITypeName="CITypeName"></RelationTable> <RelationTable v-if="activeKey === '2'" :CITypeId="CITypeId" :CITypeName="CITypeName"></RelationTable>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="3" :tab="$t('cmdb.ciType.trigger')"> <a-tab-pane key="3" :tab="$t('cmdb.ciType.autoDiscoveryTab')">
<ADTab v-if="activeKey === '3'" :CITypeId="CITypeId"></ADTab>
</a-tab-pane>
<a-tab-pane key="5" :tab="$t('cmdb.ciType.trigger')">
<TriggerTable ref="triggerTable" :CITypeId="CITypeId"></TriggerTable> <TriggerTable ref="triggerTable" :CITypeId="CITypeId"></TriggerTable>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="4" :tab="$t('cmdb.ciType.attributeAD')">
<AttrAD :CITypeId="CITypeId"></AttrAD>
</a-tab-pane>
<a-tab-pane key="5" :tab="$t('cmdb.ciType.relationAD')">
<RelationAD :CITypeId="CITypeId"></RelationAD>
</a-tab-pane>
<a-tab-pane key="6" :tab="$t('cmdb.ciType.grant')"> <a-tab-pane key="6" :tab="$t('cmdb.ciType.grant')">
<GrantComp :CITypeId="CITypeId" resourceType="CIType" :resourceTypeName="CITypeName"></GrantComp> <template v-if="activeKey === '6'">
<div class="citype-detail-title">{{ $t('cmdb.components.relationGrant') }}</div> <GrantComp :CITypeId="CITypeId" resourceType="CIType" :resourceTypeName="CITypeName"></GrantComp>
<RelationTable isInGrantComp :CITypeId="CITypeId" :CITypeName="CITypeName"></RelationTable> <div class="citype-detail-title">{{ $t('cmdb.components.relationGrant') }}</div>
<RelationTable isInGrantComp :CITypeId="CITypeId" :CITypeName="CITypeName"></RelationTable>
</template>
</a-tab-pane> </a-tab-pane>
</a-tabs> </a-tabs>
</a-card> </a-card>
@@ -29,8 +28,7 @@
import AttributesTable from './attributesTable' import AttributesTable from './attributesTable'
import RelationTable from './relationTable' import RelationTable from './relationTable'
import TriggerTable from './triggerTable.vue' import TriggerTable from './triggerTable.vue'
import AttrAD from './attrAD.vue' import ADTab from './adTab.vue'
import RelationAD from './relationAD.vue'
import GrantComp from '../../components/cmdbGrant/grantComp.vue' import GrantComp from '../../components/cmdbGrant/grantComp.vue'
export default { export default {
@@ -39,8 +37,7 @@ export default {
AttributesTable, AttributesTable,
RelationTable, RelationTable,
TriggerTable, TriggerTable,
AttrAD, ADTab,
RelationAD,
GrantComp, GrantComp,
}, },
props: { props: {
@@ -67,7 +64,7 @@ export default {
if (activeKey === '1') { if (activeKey === '1') {
this.$refs.attributesTable.getCITypeGroupData() this.$refs.attributesTable.getCITypeGroupData()
} }
if (activeKey === '3') { if (activeKey === '5') {
this.$refs.triggerTable.getTableData() this.$refs.triggerTable.getTableData()
} }
}) })

View File

@@ -6,7 +6,12 @@
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="script" :disabled="!canDefineComputed"> <a-tab-pane key="script" :disabled="!canDefineComputed">
<span style="font-size:12px;" slot="tab">{{ $t('cmdb.ciType.code') }}</span> <span style="font-size:12px;" slot="tab">{{ $t('cmdb.ciType.code') }}</span>
<codemirror style="z-index: 9999" :options="cmOptions" v-model="compute_script"></codemirror> <codemirror
style="z-index: 9999"
:options="cmOptions"
v-model="compute_script"
@input="onCodeChange"
></codemirror>
</a-tab-pane> </a-tab-pane>
<template slot="tabBarExtraContent" v-if="showCalcComputed"> <template slot="tabBarExtraContent" v-if="showCalcComputed">
<a-button type="primary" size="small" @click="handleCalcComputed"> <a-button type="primary" size="small" @click="handleCalcComputed">
@@ -49,8 +54,26 @@ export default {
height: '200px', height: '200px',
theme: 'monokai', theme: 'monokai',
tabSize: 4, tabSize: 4,
lineWrapping: true, indentUnit: 4,
lineWrapping: false,
readOnly: !this.canDefineComputed, readOnly: !this.canDefineComputed,
extraKeys: {
Tab: (cm) => {
if (cm.somethingSelected()) {
cm.indentSelection('add')
} else {
cm.replaceSelection(Array(cm.getOption('indentUnit') + 1).join(' '), 'end', '+input')
}
},
'Shift-Tab': (cm) => {
if (cm.somethingSelected()) {
cm.indentSelection('subtract')
} else {
const cursor = cm.getCursor()
cm.setCursor({ line: cursor.line, ch: cursor.ch - 4 })
}
},
},
}, },
} }
}, },
@@ -83,6 +106,9 @@ export default {
}, },
}) })
}, },
onCodeChange(v) {
this.compute_script = v.replace('\t', ' ')
}
}, },
} }
</script> </script>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,200 @@
<template>
<a-modal
:visible="visible"
:title="$t('cmdb.ciType.modelExport')"
:width="560"
@cancel="handleCancel"
@ok="handleOK"
>
<a-form
:form="form"
:labelCol="{ span: 5 }"
:wrapperCol="{ span: 19 }"
>
<a-form-item
:label="$t('cmdb.ciType.filename')"
>
<a-input v-decorator="['name', { rules: [{ required: true, message: $t('cmdb.ciType.filenameInputTips') }], initialValue: 'cmdb_template' }]" />
</a-form-item>
<a-form-item
:label="$t('cmdb.ciType.selectModel')"
>
<a-transfer
class="model-export-transfer"
:dataSource="transferDataSource"
:targetKeys="targetKeys"
:render="item => item.title"
:titles="[$t('cmdb.ciType.unselectModel'), $t('cmdb.ciType.selectedModel')]"
:listStyle="{
width: '180px',
height: `262px`,
}"
@change="onChange"
>
<template
slot="children"
slot-scope="{ props: { direction, selectedKeys }, on: { itemSelect } }"
>
<a-tree
v-if="direction === 'left'"
blockNode
checkable
:checkedKeys="[...selectedKeys, ...targetKeys]"
:treeData="treeData"
:checkStrictly="true"
@check="
(_, props) => {
onChecked(_, props, [...selectedKeys, ...targetKeys], itemSelect);
}
"
@select="
(_, props) => {
onChecked(_, props, [...selectedKeys, ...targetKeys], itemSelect);
}
"
/>
</template>
</a-transfer>
</a-form-item>
</a-form>
</a-modal>
</template>
<script>
import _ from 'lodash'
import { exportCITypeGroups } from '@/modules/cmdb/api/ciTypeGroup'
export default {
name: 'ModelExport',
props: {
visible: {
type: Boolean,
default: false,
},
CITypeGroups: {
type: Array,
default: () => []
}
},
data() {
return {
form: this.$form.createForm(this, { name: 'model-export' }),
targetKeys: [],
btnLoading: false,
}
},
computed: {
transferDataSource() {
const dataSource = this.CITypeGroups.reduce((acc, item) => {
const types = _.cloneDeep(item?.ci_types || [])
types.forEach((item) => {
item.key = String(item.id)
item.title = item?.alias || item?.name || this.$t('other')
})
return acc.concat(types)
}, [])
return dataSource
},
treeData() {
const treeData = _.cloneDeep(this.CITypeGroups)
let newTreeData = treeData.map((item) => {
const childrenKeys = []
const children = (item.ci_types || []).map((child) => {
const key = String(child?.id)
const disabled = this.targetKeys.includes(key)
childrenKeys.push(key)
return {
key,
title: child?.alias || child?.name || this.$t('other'),
disabled,
checkable: true,
children: []
}
})
return {
key: String(item?.id),
title: item?.name || this.$t('other'),
children,
childrenKeys,
disabled: children.every((item) => item.disabled),
checkable: false,
selectable: false
}
})
console.log('treeData', newTreeData)
newTreeData = newTreeData.filter((item) => item.children.length > 0)
return newTreeData
}
},
methods: {
onChange(targetKeys, direction, moveKeys) {
this.targetKeys = targetKeys
},
onChecked(_, e, checkedKeys, itemSelect) {
const { eventKey } = e.node
const selected = checkedKeys.indexOf(eventKey) === -1
itemSelect(eventKey, selected)
},
handleCancel() {
this.$emit('cancel')
this.form.resetFields()
this.targetKeys = []
},
handleOK() {
this.form.validateFields(async (err, values) => {
if (err || !this.targetKeys.length || this.btnLoading) {
return
}
this.btnLoading = true
const hide = this.$message.loading(this.$t('loading'), 0)
try {
const typeIds = this.targetKeys.join(',')
const res = await exportCITypeGroups({
type_ids: typeIds
})
console.log('exportCITypeGroups res', res)
if (res) {
const jsonStr = JSON.stringify(res)
const blob = new Blob([jsonStr], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
const fileName = values.name
a.download = fileName
a.click()
URL.revokeObjectURL(url)
}
} catch (error) {
console.log('exportCITypeGroups fail', error)
hide()
this.btnLoading = false
}
hide()
this.btnLoading = false
})
}
}
}
</script>
<style lang="less" scoped>
.model-export-transfer {
/deep/ .ant-transfer-list-body {
overflow: auto;
}
/deep/ .ant-transfer-list-header-title {
color: @primary-color;
font-weight: 400;
font-size: 12px;
}
}
</style>

View File

@@ -1,95 +1,124 @@
<template> <template>
<div class="relation-ad" :style="{ height: `${windowHeight - 130}px` }"> <div class="relation-ad" :style="{ height: `${windowHeight - 200}px` }">
<div class="relation-ad-item" v-for="item in relationList" :key="item.id"> <div class="relation-ad-table-tip">
<treeselect <ops-icon class="relation-ad-table-tip-icon" type="cmdb-prompt" />
class="custom-treeselect" <span class="relation-ad-table-tip-text">1. {{ $t('cmdb.ciType.relationADTip') }}</span>
:style="{ width: '200px', marginRight: '10px', '--custom-height': '32px' }" <span class="relation-ad-table-tip-text">2. {{ $t('cmdb.ciType.relationADTip2') }}</span>
v-model="item.attrName" <span class="relation-ad-table-tip-text">3. {{ $t('cmdb.ciType.relationADTip3') }}</span>
:multiple="false"
:clearable="true"
searchable
:options="ciTypeADTAttributes"
value-consists-of="LEAF_PRIORITY"
:placeholder="$t('cmdb.ciType.selectAttributes')"
:normalizer="
(node) => {
return {
id: node.name,
label: node.name,
}
}
"
>
<div :title="node.label" slot="option-label" slot-scope="{ node }">
<div>{{ node.label }}</div>
<div :style="{ fontSize: '12px', color: '#cbcbcb', lineHeight: '12px' }">{{ node.raw.desc }}</div>
</div>
</treeselect>
<a><a-icon type="swap"/></a>
<treeselect
class="custom-treeselect"
:style="{ width: '200px', margin: '0 10px', '--custom-height': '32px' }"
v-model="item.type_name"
:multiple="false"
:clearable="true"
searchable
:options="ciTypeGroup"
value-consists-of="LEAF_PRIORITY"
:placeholder="$t('cmdb.ciType.selectCIType')"
:disableBranchNodes="true"
@select="changeType(item)"
:normalizer="
(node) => {
return {
id: node.name || $t('other'),
label: node.alias || node.name || $t('other'),
title: node.alias || node.name || $t('other'),
children: node.ci_types,
}
}
"
>
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
{{ node.label }}
</div>
</treeselect>
<treeselect
class="custom-treeselect"
:style="{ width: '200px', marginRight: '10px', '--custom-height': '32px' }"
v-model="item.attr_name"
:multiple="false"
:clearable="true"
searchable
:options="item.attributes"
value-consists-of="LEAF_PRIORITY"
:placeholder="$t('cmdb.ciType.selectAttributes')"
:normalizer="
(node) => {
return {
id: node.name,
label: node.alias || node.name,
title: node.alias || node.name,
}
}
"
>
<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>
<div class="relation-ad-footer"> <!-- <div class="relation-ad-tip">{{ $t('cmdb.ciType.relationADTip') }}</div> -->
<a-button type="primary" @click="handleSave">{{ $t('save') }}</a-button> <div class="relation-ad-header">
<div class="relation-ad-header-left">{{ $t('cmdb.ciType.relationADHeader1') }}</div>
<div class="relation-ad-header-left">{{ $t('cmdb.ciType.relationADHeader2') }}</div>
</div>
<div class="relation-ad-main">
<div class="relation-ad-item" v-for="item in relationList" :key="item.id">
<treeselect
class="custom-treeselect"
:style="{ width: '230px', '--custom-height': '32px' }"
v-model="item.ad_key"
:multiple="false"
:clearable="true"
searchable
:options="ciTypeADTAttributes"
value-consists-of="LEAF_PRIORITY"
:placeholder="$t('cmdb.ciType.relationADSelectAttr')"
:normalizer="
(node) => {
return {
id: node.value,
label: node.label,
}
}
"
>
<div :title="node.label" slot="option-label" slot-scope="{ node }">
<div>{{ node.label }}</div>
<!-- <div :style="{ fontSize: '12px', color: '#cbcbcb', lineHeight: '12px' }">{{ node.raw.desc }}</div> -->
</div>
</treeselect>
<div
class="relation-ad-item-link"
>
<div class="relation-ad-item-link-left"></div>
<div class="relation-ad-item-link-right"></div>
</div>
<treeselect
class="custom-treeselect"
:style="{ width: '230px', marginRight: '10px', '--custom-height': '32px' }"
v-model="item.peer_type_id"
:multiple="false"
:clearable="true"
searchable
:options="relationOptions"
value-consists-of="LEAF_PRIORITY"
:placeholder="$t('cmdb.ciType.relationADSelectCIType')"
:disableBranchNodes="true"
@select="changeType(item)"
:normalizer="
(node) => {
return {
id: node.value || $t('other'),
label: node.alias || node.name || $t('other'),
title: node.alias || node.name || $t('other'),
children: node.ci_types,
}
}
"
>
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
{{ node.label }}
</div>
</treeselect>
<treeselect
class="custom-treeselect"
:style="{ width: '230px', marginRight: '18px', '--custom-height': '32px' }"
v-model="item.peer_attr_id"
:multiple="false"
:clearable="true"
searchable
:options="item.attributes"
value-consists-of="LEAF_PRIORITY"
:placeholder="$t('cmdb.ciType.relationADSelectModelAttr')"
:normalizer="
(node) => {
return {
id: node.value,
label: node.alias || node.name,
title: node.alias || node.name,
}
}
"
>
<div
:title="node.label"
slot="option-label"
slot-scope="{ node }"
:style="{ width: '100%', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' }"
>
{{ node.label }}
</div>
</treeselect>
<div class="relation-ad-item-action">
<a @click="copyRelation(item)">
<a-icon type="copy" />
</a>
<a @click="deleteRelation(item)">
<a-icon type="minus-circle" />
</a>
<a @click="addRelation">
<a-icon type="plus-circle" />
</a>
</div>
</div>
<div class="relation-ad-footer">
<a-button type="primary" @click="handleSave">{{ $t('save') }}</a-button>
</div>
</div> </div>
</div> </div>
</template> </template>
@@ -99,9 +128,15 @@ import _ from 'lodash'
import { mapState } from 'vuex' import { mapState } from 'vuex'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
import Treeselect from '@riophae/vue-treeselect' import Treeselect from '@riophae/vue-treeselect'
import { getCITypeAttributesById } from '../../api/CITypeAttr' import {
import { getCITypeGroups } from '../../api/ciTypeGroup' getCITypeAttributes,
import { getDiscovery, getCITypeDiscovery, postCITypeDiscovery, putCITypeDiscovery } from '../../api/discovery' getCITypeRelations,
postCITypeRelations
} from '../../api/discovery'
import {
getCITypeChildren,
getCITypeParent
} from '../../api/CITypeRelation.js'
export default { export default {
name: 'RelationAutoDiscovery', name: 'RelationAutoDiscovery',
@@ -114,11 +149,11 @@ export default {
}, },
data() { data() {
return { return {
relationList: [], relationList: [], // 关系自动发现数据
ciTypeADTAttributes: [], ciTypeADTAttributes: [], // 自动发现 options
ciTypeGroup: [],
adt_id: null, adt_id: null,
adrList: [], adrList: [],
relationOptions: [],
} }
}, },
computed: { computed: {
@@ -126,64 +161,62 @@ export default {
windowHeight: (state) => state.windowHeight, windowHeight: (state) => state.windowHeight,
}), }),
}, },
created() {
getCITypeGroups({ need_other: true }).then((res) => {
this.ciTypeGroup = res
.filter((item) => item.ci_types && item.ci_types.length)
.map((item) => {
item.id = `parent_${item.id || -1}`
return { ..._.cloneDeep(item) }
})
})
},
async mounted() { async mounted() {
await this.getDiscovery() await this.getCITypeAttributes()
this.getCITypeDiscovery() await this.getCITypeRelationOptions()
this.getCITypeRelations()
}, },
methods: { methods: {
async getDiscovery() { async getCITypeAttributes() {
await getDiscovery().then((res) => { const res = await getCITypeAttributes(this.CITypeId)
this.adrList = res this.ciTypeADTAttributes = res.map((item) => {
return {
id: item,
value: item,
label: item
}
}) })
}, },
getCITypeDiscovery() { async getCITypeRelationOptions() {
getCITypeDiscovery(this.CITypeId).then(async (res) => { const childRes = await getCITypeChildren(this.CITypeId)
// Options for the first drop-down box const parentRes = await getCITypeParent(this.CITypeId)
const _ciTypeADTAttributes = [] const options = [...childRes.children, ...parentRes.parents]
res
.filter((adt) => adt.adr_id) options.forEach((item) => {
.forEach((adt) => { item.value = item.id
const _find = this.adrList.find((adr) => adr.id === adt.adr_id) item.label = item.alias || item.name
if (_find && _find.attributes) { const attributes = item?.attributes?.filter((attr) => !attr.is_password && !attr.is_list && attr.value_type !== '6')
_ciTypeADTAttributes.push(..._find.attributes) attributes.forEach((attr) => {
} attr.value = attr.id
}) attr.label = attr.alias || attr.name
console.log(_ciTypeADTAttributes) })
this.ciTypeADTAttributes = _.uniqBy(_ciTypeADTAttributes, 'name') item.attributes = attributes
// Options for the first drop-down box })
const _find = res.find((adt) => !adt.adr_id) this.relationOptions = options
if (_find) { },
this.adt_id = _find.id async getCITypeRelations() {
getCITypeRelations(this.CITypeId).then(async (res) => {
if (res?.length) {
// this.adt_id = _find.id
const _relationList = [] const _relationList = []
const keys = Object.keys(_find.relation) res.forEach((item) => {
for (let i = 0; i < keys.length; i++) { const attributes = this?.relationOptions?.find((option) => option?.value === item.peer_type_id)?.attributes || []
const { attributes } = await getCITypeAttributesById(_find.relation[`${keys[i]}`].type_name)
_relationList.push({ _relationList.push({
id: uuidv4(), id: uuidv4(),
attrName: keys[i], ad_key: item.ad_key,
type_name: _find.relation[`${keys[i]}`].type_name, peer_type_id: item.peer_type_id,
attr_name: _find.relation[`${keys[i]}`].attr_name, peer_attr_id: item.peer_attr_id,
attributes, attributes,
}) })
} })
this.relationList = _relationList.length this.relationList = _relationList.length
? _relationList ? _relationList
: [ : [
{ {
id: uuidv4(), id: uuidv4(),
attrName: undefined, ad_key: undefined,
type_name: undefined, peer_type_id: undefined,
attr_name: undefined, peer_attr_id: undefined,
attributes: [], attributes: [],
}, },
] ]
@@ -192,9 +225,9 @@ export default {
this.relationList = [ this.relationList = [
{ {
id: uuidv4(), id: uuidv4(),
attrName: undefined, ad_key: undefined,
type_name: undefined, peer_type_id: undefined,
attr_name: undefined, peer_attr_id: undefined,
attributes: [], attributes: [],
}, },
] ]
@@ -202,48 +235,57 @@ export default {
}) })
}, },
changeType(item) { changeType(item) {
console.log(item)
this.$nextTick(() => { this.$nextTick(() => {
getCITypeAttributesById(item.type_name).then((res) => { const peer_type_id = item.peer_type_id
item.attr_name = undefined const attributes = this?.relationOptions?.find((option) => option?.value === peer_type_id)?.attributes
item.attributes = res.attributes.map((item) => {
return { ...item, value: item.id, label: item.alias || item.name } item.attributes = attributes
}) item.peer_attr_id = undefined
})
}) })
}, },
addRelation() { addRelation() {
const _relationList = _.cloneDeep(this.relationList) const _relationList = _.cloneDeep(this.relationList)
_relationList.push({ _relationList.push({
id: uuidv4(), id: uuidv4(),
attrName: undefined, ad_key: undefined,
type_name: undefined, peer_type_id: undefined,
attr_name: undefined, peer_attr_id: undefined,
attributes: [], attributes: [],
}) })
this.relationList = _relationList this.relationList = _relationList
}, },
copyRelation(item) {
const _relationList = _.cloneDeep(this.relationList)
_relationList.push({
...item,
id: uuidv4()
})
this.relationList = _relationList
},
deleteRelation(item) { deleteRelation(item) {
if (this.relationList.length <= 1) {
this.$message.error(this.$t('cmdb.ciType.deleteRelationAdTip'))
return
}
const _idx = this.relationList.findIndex(({ id }) => item.id === id) const _idx = this.relationList.findIndex(({ id }) => item.id === id)
if (_idx > -1) { if (_idx > -1) {
this.relationList.splice(_idx, 1) this.relationList.splice(_idx, 1)
} }
}, },
async handleSave() { async handleSave() {
const _relation = {} const _relation = this.relationList.map(({ ad_key, peer_attr_id, peer_type_id }) => {
this.relationList.forEach(({ attrName, type_name, attr_name }) => { return {
if (attrName) { ad_key,
_relation[`${attrName}`] = { type_name, attr_name } peer_attr_id,
peer_type_id
} }
}) })
if (_relation) { if (_relation.length) {
if (this.adt_id) { await postCITypeRelations(this.CITypeId, { relations: _relation })
await putCITypeDiscovery(this.adt_id, { relation: _relation })
} else {
await postCITypeDiscovery(this.CITypeId, { relation: _relation })
}
this.$message.success(this.$t('saveSuccess')) this.$message.success(this.$t('saveSuccess'))
this.getCITypeDiscovery() this.getCITypeRelations()
} }
}, },
}, },
@@ -254,14 +296,103 @@ export default {
.relation-ad { .relation-ad {
overflow: auto; overflow: auto;
padding: 0 20px; padding: 0 20px;
&-tip {
color: @text-color_4;
font-size: 12px;
font-weight: 400;
line-height: 22px;
}
&-header {
margin-top: 20px;
display: flex;
align-items: center;
font-size: 14px;
font-weight: 700;
line-height: 22px;
&-left {
width: 230px;
margin-right: 63px;
}
}
&-main {
display: inline-block;
}
.relation-ad-item { .relation-ad-item {
display: inline-flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
margin: 10px 0; margin-top: 10px;
&-link {
position: relative;
height: 1px;
width: 63px;
background-color: @border-color-base;
&-left {
position: absolute;
top: -6px;
left: -6px;
z-index: 10;
width: 12px;
height: 12px;
background-color: @primary-color;
border: solid 3px #E2E7FC;
border-radius: 50%
}
&-right {
position: absolute;
z-index: 10;
top: -5px;
right: 0px;
width: 2px;
height: 10px;
border-radius: 1px 0px 0px 1px;
background-color: @primary-color;
}
}
&-action {
display: flex;
align-items: center;
gap: 12px;
}
} }
.relation-ad-footer {
width: 690px; &-table-tip {
display: inline-flex;
align-items: center;
padding: 3px 16px;
color: @text-color_2;
font-size: 14px;
font-weight: 400;
border: solid 1px @primary-color_8;
background-color: @primary-color_5;
border-radius: 2px;
&-icon {
font-size: 16px;
color: @primary-color;
margin-right: 8px;
}
&-text {
&:not(:last-child) {
padding-right: 10px;
margin-right: 10px;
border-right: solid 1px @primary-color_8;
}
}
}
&-footer {
// width: 690px;
text-align: right; text-align: right;
margin: 10px 0; margin: 10px 0;
} }

View File

@@ -1,5 +1,5 @@
<template> <template>
<div :style="{ padding: '0 20px 20px' }"> <div class="relation-table" :style="{ padding: '0 20px 20px' }">
<a-button <a-button
v-if="!isInGrantComp" v-if="!isInGrantComp"
style="margin-bottom: 10px" style="margin-bottom: 10px"
@@ -10,6 +10,7 @@
>{{ $t('cmdb.ciType.addRelation') }}</a-button >{{ $t('cmdb.ciType.addRelation') }}</a-button
> >
<vxe-table <vxe-table
ref="xTable"
stripe stripe
:data="tableData" :data="tableData"
size="small" size="small"
@@ -18,6 +19,7 @@
highlight-hover-row highlight-hover-row
keep-source keep-source
class="ops-stripe-table" class="ops-stripe-table"
min-height="500"
:row-class-name="rowClass" :row-class-name="rowClass"
:edit-config="{ trigger: 'dblclick', mode: 'cell', showIcon: false }" :edit-config="{ trigger: 'dblclick', mode: 'cell', showIcon: false }"
resizable resizable
@@ -43,7 +45,7 @@
<span v-else>{{ constraintMap[row.constraint] }}</span> <span v-else>{{ constraintMap[row.constraint] }}</span>
</template> </template>
</vxe-column> </vxe-column>
<vxe-column :width="250" field="attributeAssociation" :edit-render="{}"> <vxe-column :width="300" field="attributeAssociation" :edit-render="{}">
<template #header> <template #header>
<span> <span>
<a-tooltip :title="$t('cmdb.ciType.attributeAssociationTip1')"> <a-tooltip :title="$t('cmdb.ciType.attributeAssociationTip1')">
@@ -56,43 +58,73 @@
</span> </span>
</template> </template>
<template #default="{row}"> <template #default="{row}">
<span <template
v-if="row.parent_attr_id && row.child_attr_id" v-for="item in row.parentAndChildAttrList"
>{{ getAttrNameById(row.isParent ? row.attributes : attributes, row.parent_attr_id) }}=>
{{ getAttrNameById(row.isParent ? attributes : row.attributes, row.child_attr_id) }}</span
> >
<div
:key="item.id"
v-if="item.parentAttrId && item.childAttrId"
>
{{ getAttrNameById(row.isParent ? row.attributes : attributes, item.parentAttrId) }}=>
{{ getAttrNameById(row.isParent ? attributes : row.attributes, item.childAttrId) }}
</div>
</template>
</template> </template>
<template #edit="{ row }"> <template #edit="{ row }">
<div style="display:inline-flex;align-items:center;"> <div
v-for="item in tableAttrList"
:key="item.id"
class="table-attribute-row"
>
<a-select <a-select
allowClear allowClear
size="small" size="small"
v-model="parent_attr_id" v-model="item.parentAttrId"
:getPopupContainer="(trigger) => trigger.parentNode" :getPopupContainer="(trigger) => trigger.parentNode"
:style="{ width: '100px' }" :style="{ width: '100px' }"
show-search
optionFilterProp="title"
> >
<a-select-option <a-select-option
v-for="attr in filterAttributes(row.isParent ? row.attributes : attributes)" v-for="attr in filterAttributes(row.isParent ? row.attributes : attributes)"
:key="attr.id" :key="attr.id"
:value="attr.id"
:title="attr.alias || attr.name"
> >
{{ attr.alias || attr.name }} {{ attr.alias || attr.name }}
</a-select-option> </a-select-option>
</a-select> </a-select>
=> <span class="table-attribute-row-link">=></span>
<a-select <a-select
allowClear allowClear
size="small" size="small"
v-model="child_attr_id" v-model="item.childAttrId"
:getPopupContainer="(trigger) => trigger.parentNode" :getPopupContainer="(trigger) => trigger.parentNode"
:style="{ width: '100px' }" :style="{ width: '100px' }"
show-search
optionFilterProp="title"
> >
<a-select-option <a-select-option
v-for="attr in filterAttributes(row.isParent ? attributes : row.attributes)" v-for="attr in filterAttributes(row.isParent ? attributes : row.attributes)"
:key="attr.id" :key="attr.id"
:value="attr.id"
:title="attr.alias || attr.name"
> >
{{ attr.alias || attr.name }} {{ attr.alias || attr.name }}
</a-select-option> </a-select-option>
</a-select> </a-select>
<a
class="table-attribute-row-action"
@click="removeTableAttr(item.id)"
>
<a-icon type="minus-circle" />
</a>
<a
class="table-attribute-row-action"
@click="addTableAttr"
>
<a-icon type="plus-circle" />
</a>
</div> </div>
</template> </template>
</vxe-column> </vxe-column>
@@ -179,13 +211,16 @@
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item :label="$t('cmdb.ciType.attributeAssociation')"> <a-form-item :label="$t('cmdb.ciType.attributeAssociation')">
<a-row> <a-row
<a-col :span="11"> v-for="item in modalAttrList"
:key="item.id"
>
<a-col :span="10">
<a-form-item> <a-form-item>
<a-select <a-select
:placeholder="$t('cmdb.ciType.attributeAssociationTip4')" :placeholder="$t('cmdb.ciType.attributeAssociationTip4')"
allowClear allowClear
v-decorator="['parent_attr_id', { rules: [{ required: false }] }]" v-model="item.parentAttrId"
> >
<a-select-option v-for="attr in filterAttributes(attributes)" :key="attr.id"> <a-select-option v-for="attr in filterAttributes(attributes)" :key="attr.id">
{{ attr.alias || attr.name }} {{ attr.alias || attr.name }}
@@ -196,12 +231,12 @@
<a-col :span="2" :style="{ textAlign: 'center' }"> <a-col :span="2" :style="{ textAlign: 'center' }">
=> =>
</a-col> </a-col>
<a-col :span="11"> <a-col :span="9">
<a-form-item> <a-form-item>
<a-select <a-select
:placeholder="$t('cmdb.ciType.attributeAssociationTip5')" :placeholder="$t('cmdb.ciType.attributeAssociationTip5')"
allowClear allowClear
v-decorator="['child_attr_id', { rules: [{ required: false }] }]" v-model="item.childAttrId"
> >
<a-select-option v-for="attr in filterAttributes(modalChildAttributes)" :key="attr.id"> <a-select-option v-for="attr in filterAttributes(modalChildAttributes)" :key="attr.id">
{{ attr.alias || attr.name }} {{ attr.alias || attr.name }}
@@ -209,6 +244,20 @@
</a-select> </a-select>
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="3">
<a
class="modal-attribute-action"
@click="removeModalAttr(item.id)"
>
<a-icon type="minus-circle" />
</a>
<a
class="modal-attribute-action"
@click="addModalAttr"
>
<a-icon type="plus-circle" />
</a>
</a-col>
</a-row> </a-row>
</a-form-item> </a-form-item>
</a-form> </a-form>
@@ -227,6 +276,7 @@ import {
} from '@/modules/cmdb/api/CITypeRelation' } from '@/modules/cmdb/api/CITypeRelation'
import { getCITypes } from '@/modules/cmdb/api/CIType' import { getCITypes } from '@/modules/cmdb/api/CIType'
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr' import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
import { v4 as uuidv4 } from 'uuid'
import CMDBGrant from '../../components/cmdbGrant' import CMDBGrant from '../../components/cmdbGrant'
@@ -259,9 +309,11 @@ export default {
tableData: [], tableData: [],
parentTableData: [], parentTableData: [],
attributes: [], attributes: [],
parent_attr_id: undefined, tableAttrList: [],
child_attr_id: undefined, modalAttrList: [],
modalChildAttributes: [], modalChildAttributes: [],
currentEditData: null,
isContinueCloseEdit: false,
} }
}, },
computed: { computed: {
@@ -292,13 +344,16 @@ export default {
if (!this.isInGrantComp) { if (!this.isInGrantComp) {
await this.getCITypeParent() await this.getCITypeParent()
} }
this.getCITypeChildren() await this.getCITypeChildren()
}, },
async getCITypeParent() { async getCITypeParent() {
await getCITypeParent(this.CITypeId).then((res) => { await getCITypeParent(this.CITypeId).then((res) => {
this.parentTableData = res.parents.map((item) => { this.parentTableData = res.parents.map((item) => {
const parentAndChildAttrList = this.handleAttrList(item)
return { return {
...item, ...item,
parentAndChildAttrList,
source_ci_type_name: this.CITypeName, source_ci_type_name: this.CITypeName,
source_ci_type_id: this.CITypeId, source_ci_type_id: this.CITypeId,
isParent: true, isParent: true,
@@ -306,11 +361,14 @@ export default {
}) })
}) })
}, },
getCITypeChildren() { async getCITypeChildren() {
getCITypeChildren(this.CITypeId).then((res) => { await getCITypeChildren(this.CITypeId).then((res) => {
const data = res.children.map((obj) => { const data = res.children.map((obj) => {
const parentAndChildAttrList = this.handleAttrList(obj)
return { return {
...obj, ...obj,
parentAndChildAttrList,
source_ci_type_name: this.CITypeName, source_ci_type_name: this.CITypeName,
source_ci_type_id: this.CITypeId, source_ci_type_id: this.CITypeId,
} }
@@ -322,6 +380,20 @@ export default {
} }
}) })
}, },
handleAttrList(data) {
const length = Math.min(data?.parent_attr_ids?.length || 0, data.child_attr_ids?.length || 0)
const parentAndChildAttrList = []
for (let i = 0; i < length; i++) {
parentAndChildAttrList.push({
id: uuidv4(),
parentAttrId: data?.parent_attr_ids?.[i] ?? '',
childAttrId: data?.child_attr_ids?.[i] ?? ''
})
}
return parentAndChildAttrList
},
getCITypes() { getCITypes() {
getCITypes().then((res) => { getCITypes().then((res) => {
this.CITypes = res.ci_types this.CITypes = res.ci_types
@@ -342,6 +414,13 @@ export default {
handleCreate() { handleCreate() {
this.drawerTitle = this.$t('cmdb.ciType.addRelation') this.drawerTitle = this.$t('cmdb.ciType.addRelation')
this.visible = true this.visible = true
this.$set(this, 'modalAttrList', [
{
id: uuidv4(),
parentAttrId: undefined,
childAttrId: undefined
}
])
this.$nextTick(() => { this.$nextTick(() => {
this.form.setFieldsValue({ this.form.setFieldsValue({
source_ci_type_id: this.CITypeId, source_ci_type_id: this.CITypeId,
@@ -365,19 +444,22 @@ export default {
ci_type_id, ci_type_id,
relation_type_id, relation_type_id,
constraint, constraint,
parent_attr_id = undefined,
child_attr_id = undefined,
} = values } = values
if ((!parent_attr_id && child_attr_id) || (parent_attr_id && !child_attr_id)) { const {
this.$message.warning(this.$t('cmdb.ciType.attributeAssociationTip3')) parent_attr_ids,
child_attr_ids,
validate
} = this.handleValidateAttrList(this.modalAttrList)
if (!validate) {
return return
} }
createRelation(source_ci_type_id, ci_type_id, { createRelation(source_ci_type_id, ci_type_id, {
relation_type_id, relation_type_id,
constraint, constraint,
parent_attr_id, parent_attr_ids,
child_attr_id, child_attr_ids,
}).then((res) => { }).then((res) => {
this.$message.success(this.$t('addSuccess')) this.$message.success(this.$t('addSuccess'))
this.onClose() this.onClose()
@@ -386,6 +468,37 @@ export default {
} }
}) })
}, },
/**
* 校验属性列表
* @param {*} attrList
*/
handleValidateAttrList(attrList) {
const parent_attr_ids = []
const child_attr_ids = []
attrList.map((attr) => {
if (attr.parentAttrId) {
parent_attr_ids.push(attr.parentAttrId)
}
if (attr.childAttrId) {
child_attr_ids.push(attr.childAttrId)
}
})
if (parent_attr_ids.length !== child_attr_ids.length) {
this.$message.warning(this.$t('cmdb.ciType.attributeAssociationTip3'))
return {
validate: false
}
}
return {
validate: true,
parent_attr_ids,
child_attr_ids
}
},
handleOpenGrant(record) { handleOpenGrant(record) {
this.$refs.cmdbGrant.open({ this.$refs.cmdbGrant.open({
name: `${record.source_ci_type_name} -> ${record.name}`, name: `${record.source_ci_type_name} -> ${record.name}`,
@@ -401,25 +514,75 @@ export default {
if (row.isParent) return 'relation-table-parent' if (row.isParent) return 'relation-table-parent'
}, },
handleEditActived({ row }) { handleEditActived({ row }) {
this.parent_attr_id = row?.parent_attr_id ?? undefined this.$nextTick(async () => {
this.child_attr_id = row?.child_attr_id ?? undefined if (this.isContinueCloseEdit) {
const editRecord = this.$refs.xTable.getEditRecord()
const { row: editRow, column } = editRecord
this.currentEditData = {
row: editRow,
column
}
return
}
const tableAttrList = []
const length = Math.min(row?.parent_attr_ids?.length || 0, row.child_attr_ids?.length || 0)
if (length) {
for (let i = 0; i < length; i++) {
tableAttrList.push({
id: uuidv4(),
parentAttrId: row?.parent_attr_ids?.[i] ?? undefined,
childAttrId: row?.child_attr_ids?.[i] ?? undefined
})
}
} else {
tableAttrList.push({
id: uuidv4(),
parentAttrId: undefined,
childAttrId: undefined
})
}
this.$set(this, 'tableAttrList', tableAttrList)
})
}, },
async handleEditClose({ row }) { async handleEditClose({ row }) {
const { source_ci_type_id: parentId, id: childrenId, constraint, relation_type } = row if (this.currentEditData) {
const { parent_attr_id, child_attr_id } = this this.currentEditData = null
const _find = this.relationTypes.find((item) => item.name === relation_type)
const relation_type_id = _find?.id
if ((!parent_attr_id && child_attr_id) || (parent_attr_id && !child_attr_id)) {
this.$message.warning(this.$t('cmdb.ciType.attributeAssociationTip3'))
return return
} }
this.isContinueCloseEdit = true
const { source_ci_type_id: parentId, id: childrenId, constraint, relation_type } = row
const _find = this.relationTypes.find((item) => item.name === relation_type)
const relation_type_id = _find?.id
const {
parent_attr_ids,
child_attr_ids,
validate
} = this.handleValidateAttrList(this.tableAttrList)
if (!validate) {
this.isContinueCloseEdit = false
return
}
await createRelation(row.isParent ? childrenId : parentId, row.isParent ? parentId : childrenId, { await createRelation(row.isParent ? childrenId : parentId, row.isParent ? parentId : childrenId, {
relation_type_id, relation_type_id,
constraint, constraint,
parent_attr_id, parent_attr_ids,
child_attr_id, child_attr_ids,
}).finally(() => { }).finally(async () => {
this.getData() await this.getData()
this.isContinueCloseEdit = false
if (this.currentEditData) {
setTimeout(async () => {
const { fullData } = this.$refs.xTable.getTableData()
const findEdit = fullData.find((item) => item.id === this?.currentEditData?.row?.id)
await this.$refs.xTable.setEditRow(findEdit, 'attributeAssociation')
})
}
}) })
}, },
getAttrNameById(attributes, id) { getAttrNameById(attributes, id) {
@@ -427,7 +590,9 @@ export default {
return _find?.alias ?? _find?.name ?? id return _find?.alias ?? _find?.name ?? id
}, },
changeChild(value) { changeChild(value) {
this.form.setFieldsValue({ child_attr_id: undefined }) this.modalAttrList.forEach((item) => {
item.childAttrId = undefined
})
getCITypeAttributesById(value).then((res) => { getCITypeAttributesById(value).then((res) => {
this.modalChildAttributes = res?.attributes ?? [] this.modalChildAttributes = res?.attributes ?? []
}) })
@@ -436,10 +601,75 @@ export default {
// filter password/json/is_list // filter password/json/is_list
return attributes.filter((attr) => !attr.is_password && !attr.is_list && attr.value_type !== '6') return attributes.filter((attr) => !attr.is_password && !attr.is_list && attr.value_type !== '6')
}, },
addTableAttr() {
this.tableAttrList.push({
id: uuidv4(),
parentAttrId: undefined,
childAttrId: undefined
})
},
removeTableAttr(id) {
if (this.tableAttrList.length <= 1) {
this.$message.error(this.$t('cmdb.ciType.attributeAssociationTip6'))
return
}
const index = this.tableAttrList.findIndex((item) => item.id === id)
if (index !== -1) {
this.tableAttrList.splice(index, 1)
}
},
addModalAttr() {
this.modalAttrList.push({
id: uuidv4(),
parentAttrId: undefined,
childAttrId: undefined
})
},
removeModalAttr(id) {
if (this.modalAttrList.length <= 1) {
this.$message.error(this.$t('cmdb.ciType.attributeAssociationTip6'))
return
}
const index = this.modalAttrList.findIndex((item) => item.id === id)
if (index !== -1) {
this.modalAttrList.splice(index, 1)
}
}
}, },
} }
</script> </script>
<style lang="less" scoped>
.relation-table {
/deep/ .vxe-cell {
max-height: max-content !important;
}
}
.table-attribute-row {
display: inline-flex;
align-items: center;
margin-top: 5px;
&:last-child {
margin-bottom: 5px;
}
&-link {
margin: 0 5px;
}
&-action {
margin-left: 5px;
}
}
.modal-attribute-action {
margin-left: 5px;
}
</style>
<style lang="less"> <style lang="less">
.ops-stripe-table .vxe-body--row.row--stripe.relation-table-divider { .ops-stripe-table .vxe-body--row.row--stripe.relation-table-divider {
background-color: #b1b8d3 !important; background-color: #b1b8d3 !important;

View File

@@ -91,7 +91,7 @@
show-search show-search
> >
<a-select-option <a-select-option
v-for="attr in commonAttributes.filter((attr) => !attr.is_password)" v-for="attr in commonAttributes.filter((attr) => !attr.is_password && attr.value_type !== '6')"
:key="attr.id" :key="attr.id"
:value="attr.id" :value="attr.id"
>{{ attr.alias || attr.name }}</a-select-option >{{ attr.alias || attr.name }}</a-select-option

View File

@@ -0,0 +1,36 @@
<template>
<vxe-table
size="mini"
stripe
class="ops-stripe-table"
show-overflow
keep-source
ref="xTable"
max-height="100%"
:data="tableData"
:scroll-y="{enabled: true}"
>
<vxe-column field="name" :title="$t('name')">
<template #edit="{ row }">
<vxe-input v-model="row.name" type="text"></vxe-input>
</template>
</vxe-column>
<vxe-column field="type" :title="$t('type')"></vxe-column>
<vxe-column field="desc" :title="$t('desc')"></vxe-column>
</vxe-table>
</template>
<script>
export default {
name: 'AgentTable',
props: {
tableData: {
type: Array,
default: () => [],
},
}
}
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,12 @@
export const DISCOVERY_CATEGORY_TYPE = {
AGENT: 'agent',
SNMP: 'snmp',
HTTP: 'http',
PLUGIN: 'plugin',
COMPONENT: 'components',
PRIVATE_CLOUD: 'private_cloud'
}
export const PRIVATE_CLOUD_NAME = {
VCenter: 'vcenter'
}

View File

@@ -2,34 +2,82 @@
<div <div
:class="{ :class="{
'discovery-card': true, 'discovery-card': true,
'discovery-card-http': rule.type === DISCOVERY_CATEGORY_TYPE.HTTP,
'discovery-card-small': isSelected, 'discovery-card-small': isSelected,
'discovery-card-small-selected': isSelected && selectedIds().findIndex((item) => item.id === rule.id) > -1, 'discovery-card-small-selected': isSelected && selectedIds().findIndex((item) => item.id === rule.id) > -1,
}" }"
@click="clickCard" @click="clickCard"
> >
<div class="discovery-bottom"></div> <div
class="discovery-card-inner"
v-if="rule.is_inner"
>
<span class="discovery-card-inner-text">{{ $t('cmdb.ad.innerFlag') }}</span>
</div>
<div
class="discovery-background-top"
:style="{ background: borderTopColorMap[rule.name] || '' }"
></div>
<div class="discovery-top"> <div class="discovery-top">
<div class="discovery-header"> <div class="discovery-header">
<img <img
v-if="icon.id && icon.url" v-if="icon.id && icon.url"
class="discovery-header-icon"
:src="`/api/common-setting/v1/file/${icon.url}`" :src="`/api/common-setting/v1/file/${icon.url}`"
:style="{ maxHeight: '30px', maxWidth: '30px' }"
/> />
<ops-icon v-else :type="icon.name || 'caise-chajian'" :style="{ fontSize: '30px', color: icon.color }" /> <ops-icon
v-else
:type="icon.name || 'caise-chajian'"
:style="{ color: icon.color }"
class="discovery-header-icon"
/>
<span :title="rule.name">{{ rule.name }}</span> <span :title="rule.name">{{ rule.name }}</span>
</div> </div>
<template v-if="!isSelected"> <template v-if="!isSelected">
<a-divider :style="{ margin: '5px 0' }" /> <div
class="discovery-resources"
v-if="rule.type === DISCOVERY_CATEGORY_TYPE.HTTP && rule.resources.length"
>
<a-tooltip>
<template slot="title">
{{ $t('cmdb.ad.discoveryCardResoureTip') }}
</template>
<div class="discovery-resources-left">
<ops-icon class="discovery-resources-icon" type="cmdb-discovery_resources" />
<span class="discovery-resources-count">{{ rule.resources.length }}{{ $i18n.locale === 'zh' ? '' : '' }}</span>
</div>
</a-tooltip>
<div class="discovery-resources-right">
<template v-for="(item, index) in rule.resources">
<span
:key="index"
v-if="index < 2"
class="discovery-resources-item"
:style="{ maxWidth: rule.resources.length >= 2 ? '70px' : '160px' }"
>
{{ item }}
</span>
</template>
<span v-if="rule.resources.length > 2" class="discovery-resources-item">
<ops-icon type="veops-more" />
</span>
</div>
</div>
<a-divider
:style="{ margin: rule.type === DISCOVERY_CATEGORY_TYPE.HTTP ? '10px 0' : '7px 0' }"
/>
<div class="discovery-footer"> <div class="discovery-footer">
<a-space v-if="rule.type === 'agent'"> <a-space v-if="rule.type === 'agent' && rule.is_plugin">
<a @click="handleEdit"><ops-icon type="icon-xianxing-edit"/></a> <a @click="handleEdit">
<a-icon type="edit" />
</a>
<a <a
v-if="isDeletable" v-if="isDeletable"
@click="handleDelete" @click="handleDelete"
:style="{ color: 'red' }" :style="{ color: 'red' }"
><ops-icon >
type="icon-xianxing-delete" <a-icon type="delete" />
/></a> </a>
</a-space> </a-space>
<a v-else @click="handleEdit"><a-icon type="eye"/></a> <a v-else @click="handleEdit"><a-icon type="eye"/></a>
<span>{{ rule.is_plugin ? 'Plugin' : $t('cmdb.custom_dashboard.default') }}</span> <span>{{ rule.is_plugin ? 'Plugin' : $t('cmdb.custom_dashboard.default') }}</span>
@@ -40,6 +88,8 @@
</template> </template>
<script> <script>
import { DISCOVERY_CATEGORY_TYPE } from './constants.js'
export default { export default {
name: 'DiscoveryCard', name: 'DiscoveryCard',
props: { props: {
@@ -52,6 +102,17 @@ export default {
default: false, default: false,
}, },
}, },
data() {
return {
DISCOVERY_CATEGORY_TYPE,
borderTopColorMap: {
'阿里云': '#FFB287',
'腾讯云': '#87BEFF',
'华为云': '#FFB8B8',
'AWS': '#FFC187',
}
}
},
computed: { computed: {
icon() { icon() {
return this.rule?.option?.icon ?? { color: '', name: 'caise-wuliji' } return this.rule?.option?.icon ?? { color: '', name: 'caise-wuliji' }
@@ -95,7 +156,32 @@ export default {
position: relative; position: relative;
margin-bottom: 40px; margin-bottom: 40px;
margin-right: 40px; margin-right: 40px;
.discovery-bottom {
&-inner {
position: absolute;
top: 0;
right: 0;
z-index: 4;
width: 50px;
height: 30px;
border-left: 50px solid transparent;
border-top: 30px solid @primary-color_4;
&-text {
width: 30px;
position: absolute;
top: -28px;
right: 3px;
text-align: right;
color: @primary-color;
font-size: 10px;
font-weight: 400;
}
}
.discovery-background-top {
width: 100%; width: 100%;
height: 10px; height: 10px;
position: absolute; position: absolute;
@@ -105,6 +191,7 @@ export default {
background: linear-gradient(90.54deg, #879fff 1.32%, #a0ddff 99.13%); background: linear-gradient(90.54deg, #879fff 1.32%, #a0ddff 99.13%);
border-radius: 4px 4px 0 0; border-radius: 4px 4px 0 0;
} }
.discovery-top { .discovery-top {
width: 100%; width: 100%;
height: calc(100% - 5px); height: calc(100% - 5px);
@@ -118,7 +205,7 @@ export default {
padding: 12px; padding: 12px;
.discovery-header { .discovery-header {
width: 100%; width: 100%;
height: 50px; height: 45px;
display: flex; display: flex;
align-items: center; align-items: center;
> i { > i {
@@ -130,17 +217,73 @@ export default {
text-overflow: ellipsis; text-overflow: ellipsis;
color: #000; color: #000;
} }
&-icon {
font-size: 22px;
max-height: 22px;
max-width: 22px;
}
} }
.discovery-resources {
display: flex;
justify-content: space-between;
align-items: center;
&-count {
margin-left: 3px;
color: @text-color_3;
font-size: 12px;
font-weight: 400;
flex-shrink: 0;
}
&-right {
display: flex;
align-items: center;
}
&-item {
padding: 3px 6px;
border-radius: 12px;
background-color: @layout-content-background;
color: @text-color_3;
font-size: 11px;
font-weight: 400;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
&:not(:last-child) {
margin-right: 6px;
}
}
}
.discovery-footer { .discovery-footer {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
> span { > span {
color: #a5a9bc; color: #86909c;
background-color: #d8eaff; background-color: #f0f5ff;
border-radius: 2px; border-radius: 2px;
padding: 0 5px; padding: 2px 8px;
font-size: 12px; font-size: 11px;
}
}
}
&-http {
width: 263px;
height: 142px;
.discovery-header {
&-icon {
font-size: 30px !important;
max-height: 30px !important;
max-width: 30px !important;
} }
} }
} }
@@ -149,6 +292,11 @@ export default {
width: 170px; width: 170px;
height: 80px; height: 80px;
cursor: pointer; cursor: pointer;
// &:hover {
// .discovery-top {
// background-color: #f0f1f5;
// }
// }
} }
.discovery-card-small:hover, .discovery-card-small:hover,
.discovery-card-small-selected { .discovery-card-small-selected {

View File

@@ -1,6 +1,16 @@
<template> <template>
<CustomDrawer width="800px" :title="title" :visible="visible" @close="handleClose"> <CustomDrawer
<template v-if="adType === 'agent'"> width="800px"
:title="title"
:visible="visible"
:bodyStyle="{ height: 'calc(-108px + 100vh)' }"
@close="handleClose"
>
<AgentTable
v-if="adType === DISCOVERY_CATEGORY_TYPE.AGENT"
:tableData="tableData"
/>
<template v-else-if="adType === DISCOVERY_CATEGORY_TYPE.PLUGIN">
<a-form-model <a-form-model
ref="autoDiscoveryForm" ref="autoDiscoveryForm"
:model="form" :model="form"
@@ -47,8 +57,9 @@
icon="plus" icon="plus"
:style="{ marginBottom: '10px' }" :style="{ marginBottom: '10px' }"
@click="insertEvent(-1)" @click="insertEvent(-1)"
>{{ $t('new') }}</a-button
> >
{{ $t('new') }}
</a-button>
<vxe-table <vxe-table
size="mini" size="mini"
stripe stripe
@@ -77,7 +88,11 @@
<vxe-input v-model="row.desc" type="text"></vxe-input> <vxe-input v-model="row.desc" type="text"></vxe-input>
</template> </template>
</vxe-column> </vxe-column>
<vxe-column :title="$t('operation')" width="60" v-if="!form.is_plugin"> <vxe-column
:title="$t('operation')"
width="60"
v-if="!form.is_plugin"
>
<template #default="{ row }"> <template #default="{ row }">
<a-space v-if="$refs.xTable.isActiveByRow(row)"> <a-space v-if="$refs.xTable.isActiveByRow(row)">
<a @click="saveRowEvent(row)"><a-icon type="save"/></a> <a @click="saveRowEvent(row)"><a-icon type="save"/></a>
@@ -97,21 +112,30 @@
</div> </div>
</template> </template>
<template v-else> <template v-else>
<HttpSnmpAD ref="httpSnmpAd" :ruleType="adType" :ruleName="ruleData.name" /> <HttpSnmpAD ref="httpSnmpAd" :ruleType="adType" :ruleName="ruleName" />
</template> </template>
</CustomDrawer> </CustomDrawer>
</template> </template>
<script> <script>
import CustomIconSelect from '@/components/CustomIconSelect'
import { postDiscovery, putDiscovery } from '../../api/discovery' import { postDiscovery, putDiscovery } from '../../api/discovery'
import { DISCOVERY_CATEGORY_TYPE } from './constants.js'
import AgentTable from './agentTable.vue'
import CustomIconSelect from '@/components/CustomIconSelect'
import HttpSnmpAD from '../../components/httpSnmpAD' import HttpSnmpAD from '../../components/httpSnmpAD'
import CustomCodeMirror from '@/components/CustomCodeMirror' import CustomCodeMirror from '@/components/CustomCodeMirror'
import 'codemirror/lib/codemirror.css' import 'codemirror/lib/codemirror.css'
import 'codemirror/theme/monokai.css' import 'codemirror/theme/monokai.css'
export default { export default {
name: 'EditDrawer', name: 'EditDrawer',
components: { CustomIconSelect, CustomCodeMirror, HttpSnmpAD }, components: {
CustomIconSelect,
CustomCodeMirror,
HttpSnmpAD,
AgentTable
},
props: { props: {
is_inner: { is_inner: {
type: Boolean, type: Boolean,
@@ -142,11 +166,12 @@ export default {
tabSize: 4, tabSize: 4,
lineWrapping: true, lineWrapping: true,
}, },
DISCOVERY_CATEGORY_TYPE,
} }
}, },
computed: { computed: {
title() { title() {
if (this.adType === 'http' || this.adType === 'snmp') { if ([DISCOVERY_CATEGORY_TYPE.HTTP, DISCOVERY_CATEGORY_TYPE.SNMP, DISCOVERY_CATEGORY_TYPE.AGENT, DISCOVERY_CATEGORY_TYPE.PRIVATE_CLOUD, DISCOVERY_CATEGORY_TYPE.COMPONENT].includes(this.adType)) {
return this.ruleData.name return this.ruleData.name
} }
if (this.type === 'edit') { if (this.type === 'edit') {
@@ -154,6 +179,9 @@ export default {
} }
return this.$t('new') return this.$t('new')
}, },
ruleName() {
return this?.ruleData?.option?.en || this?.ruleData?.name || ''
}
}, },
inject: { inject: {
getDiscovery: { getDiscovery: {
@@ -173,10 +201,15 @@ export default {
is_plugin: true, is_plugin: true,
} }
} }
if (adType === 'http' || adType === 'snmp') { if (adType === DISCOVERY_CATEGORY_TYPE.HTTP || adType === DISCOVERY_CATEGORY_TYPE.SNMP) {
return return
} }
this.$nextTick(() => { this.$nextTick(() => {
if ([DISCOVERY_CATEGORY_TYPE.HTTP, DISCOVERY_CATEGORY_TYPE.SNMP, DISCOVERY_CATEGORY_TYPE.PRIVATE_CLOUD].includes(adType)) {
this.tableData = data?.attributes ?? []
return
}
if (this.type === 'edit') { if (this.type === 'edit') {
this.form = { this.form = {
name: data.name, name: data.name,
@@ -203,7 +236,7 @@ export default {
this.tableData = [] this.tableData = []
this.customIcon = { name: '', color: '' } this.customIcon = { name: '', color: '' }
this.form = { name: '', is_plugin: false } this.form = { name: '', is_plugin: false }
if (this.adType === 'agent') { if (this.adType === DISCOVERY_CATEGORY_TYPE.PLUGIN) {
this.$refs.autoDiscoveryForm.clearValidate() this.$refs.autoDiscoveryForm.clearValidate()
} else { } else {
// this.$refs.httpSnmpAd.currentCate = '' // this.$refs.httpSnmpAd.currentCate = ''
@@ -244,9 +277,10 @@ export default {
const $table = this.$refs.xTable const $table = this.$refs.xTable
const { fullData: _tableData } = $table.getTableData() const { fullData: _tableData } = $table.getTableData()
console.log(_tableData) console.log(_tableData)
const type = this.adType === DISCOVERY_CATEGORY_TYPE.PLUGIN ? DISCOVERY_CATEGORY_TYPE.AGENT : this.adType
const params = { const params = {
...this.form, ...this.form,
type: this.adType, type,
is_inner: this.is_inner, is_inner: this.is_inner,
option: { icon: this.customIcon }, option: { icon: this.customIcon },
attributes: this.form.is_plugin attributes: this.form.is_plugin

View File

@@ -1,39 +1,94 @@
<template> <template>
<div class="setting-discovery"> <div class="setting-discovery">
<div :style="{ textAlign: 'right' }"> <div v-if="!isSelected" class="setting-discovery-header">
<a-space v-if="!isSelected"> <a-input-search
<a-upload name="file" :multiple="false" accept=".json" :fileList="[]" :beforeUpload="beforeUpload"> class="setting-discovery-search"
<a><a-icon type="upload" />{{ $t('cmdb.ad.upload') }}</a> :placeholder="$t('cmdb.ad.pluginSearchTip')"
</a-upload> @search="onSearchDiscovery"
<a @click="download"><a-icon type="download" />{{ $t('cmdb.ad.download') }}</a> />
</a-space> <div class="setting-discovery-radio">
</div> <div
<div v-for="{ type, label } in typeCategory" :key="type"> v-for="{ type, label } in typeCategory"
<div class="type-header"> :key="type"
<div>{{ label }}</div> :class="['setting-discovery-radio-item', radioKey === type ? 'setting-discovery-radio-item_active' : '']"
<a-space v-if="!isSelected && type === 'agent'"> @click="changeRadio(type)"
<a @click="handleOpenEditDrawer(null, 'add', type)"><ops-icon type="icon-xianxing-tianjia"/></a> >
</a-space> {{ label }}
</div>
</div>
<div class="setting-discovery-header-action">
<a-upload
name="file"
:multiple="false"
accept=".json"
:fileList="[]"
:beforeUpload="beforeUpload"
>
<a class="setting-discovery-header-action-btn">
<a-icon type="upload" />
{{ $t('cmdb.ad.upload') }}
</a>
</a-upload>
<a
@click="download"
class="setting-discovery-header-action-btn"
>
<a-icon type="download" />
{{ $t('cmdb.ad.download') }}
</a>
</div>
</div>
<div
class="setting-discovery-body"
:style="{ height: !isSelected ? `${windowHeight - 155}px` : '' }"
>
<template v-if="!showNullData">
<div v-for="{ type, label } in typeCategory" :key="type">
<template v-if="filterCategoryChildren[type] && (filterCategoryChildren[type].children.length || (showAddPlugin && type === DISCOVERY_CATEGORY_TYPE.PLUGIN))">
<div class="type-header">
<div>{{ label }}</div>
</div>
<a-row type="flex" justify="start">
<DiscoveryCard
v-for="rule in filterCategoryChildren[type].children"
:key="rule.id"
:rule="rule"
:isSelected="isSelected"
@editRule="handleOpenEditDrawer(rule, 'edit', type)"
@deleteRule="deleteRule(rule)"
/>
<div
v-if="showAddPlugin && type === DISCOVERY_CATEGORY_TYPE.PLUGIN"
class="setting-discovery-add"
@click="handleOpenEditDrawer(null, 'add', DISCOVERY_CATEGORY_TYPE.PLUGIN)"
>
<a-icon type="plus-circle" theme="twoTone" />
<span class="setting-discovery-add-text">
{{ $t('cmdb.ad.addPlugin') }}
</span>
</div>
</a-row>
</template>
</div>
</template>
<div class="setting-discovery-empty" v-else>
<img class="setting-discovery-empty-img" :src="require(`@/assets/data_empty.png`)" />
<p class="setting-discovery-empty-text">{{ $t('noData') }}</p>
</div> </div>
<a-row type="flex" justify="start">
<DiscoveryCard
@editRule="handleOpenEditDrawer(rule, 'edit', type)"
@deleteRule="deleteRule(rule)"
v-for="rule in typeCategoryChildren[type]"
:key="rule.id"
:rule="rule"
:isSelected="isSelected"
/>
</a-row>
</div> </div>
<EditDrawer ref="editDrawer" /> <EditDrawer ref="editDrawer" />
</div> </div>
</template> </template>
<script> <script>
import { mapState } from 'vuex'
import _ from 'lodash'
import { getDiscovery, deleteDiscovery } from '../../api/discovery' import { getDiscovery, deleteDiscovery } from '../../api/discovery'
import { DISCOVERY_CATEGORY_TYPE } from './constants.js'
import DiscoveryCard from './discoveryCard.vue' import DiscoveryCard from './discoveryCard.vue'
import EditDrawer from './editDrawer.vue' import EditDrawer from './editDrawer.vue'
export default { export default {
name: 'AutoDiscovery', name: 'AutoDiscovery',
components: { DiscoveryCard, EditDrawer }, components: { DiscoveryCard, EditDrawer },
@@ -45,26 +100,67 @@ export default {
}, },
data() { data() {
return { return {
typeCategoryChildren: { agent: [], snmp: [], http: [] }, typeCategoryChildren: {},
DISCOVERY_CATEGORY_TYPE,
radioKey: '',
searchValue: '',
} }
}, },
computed: { computed: {
...mapState({
windowHeight: (state) => state.windowHeight,
}),
typeCategory() { typeCategory() {
return [ return [
{ {
type: 'agent', type: DISCOVERY_CATEGORY_TYPE.HTTP,
label: this.$t('cmdb.ad.http'),
},
{
type: DISCOVERY_CATEGORY_TYPE.PRIVATE_CLOUD,
label: this.$t('cmdb.ad.privateCloud'),
},
{
type: DISCOVERY_CATEGORY_TYPE.AGENT,
label: this.$t('cmdb.ad.agent'), label: this.$t('cmdb.ad.agent'),
}, },
{ {
type: 'snmp', type: DISCOVERY_CATEGORY_TYPE.COMPONENT,
label: this.$t('cmdb.ad.component'),
},
{
type: DISCOVERY_CATEGORY_TYPE.SNMP,
label: this.$t('cmdb.ad.snmp'), label: this.$t('cmdb.ad.snmp'),
}, },
{ {
type: 'http', type: DISCOVERY_CATEGORY_TYPE.PLUGIN,
label: this.$t('cmdb.ad.http'), label: this.$t('cmdb.ad.plugin'),
}, }
] ]
}, },
filterCategoryChildren() {
const _typeCategoryChildren = _.cloneDeep(this.typeCategoryChildren)
const _filterCategoryChildren = Object.values(_typeCategoryChildren).reduce((obj, category) => {
if (this.radioKey === '' || category.type === this.radioKey) {
category.children = category.children.filter((item) => {
return item?.name?.indexOf(this.searchValue) !== -1
})
obj[category.type] = category
}
return obj
}, {})
return _filterCategoryChildren
},
showNullData() {
const showCount = Object.values(this.filterCategoryChildren).reduce((acc, item) => {
return acc + (item?.children?.length || 0)
}, 0)
return showCount === 0
},
showAddPlugin() {
return !this.isSelected && this.searchValue === ''
}
}, },
provide() { provide() {
return { return {
@@ -76,13 +172,55 @@ export default {
}, },
methods: { methods: {
getDiscovery() { getDiscovery() {
const _typeCategoryChildren = { agent: [], snmp: [], http: [] } const _typeCategoryChildren = {
[DISCOVERY_CATEGORY_TYPE.HTTP]: {
type: DISCOVERY_CATEGORY_TYPE.HTTP,
children: []
},
[DISCOVERY_CATEGORY_TYPE.PRIVATE_CLOUD]: {
type: DISCOVERY_CATEGORY_TYPE.PRIVATE_CLOUD,
children: []
},
[DISCOVERY_CATEGORY_TYPE.AGENT]: {
type: DISCOVERY_CATEGORY_TYPE.AGENT,
children: []
},
[DISCOVERY_CATEGORY_TYPE.COMPONENT]: {
type: DISCOVERY_CATEGORY_TYPE.COMPONENT,
children: []
},
[DISCOVERY_CATEGORY_TYPE.SNMP]: {
type: DISCOVERY_CATEGORY_TYPE.SNMP,
children: []
},
[DISCOVERY_CATEGORY_TYPE.PLUGIN]: {
type: DISCOVERY_CATEGORY_TYPE.PLUGIN,
children: []
}
}
getDiscovery().then((res) => { getDiscovery().then((res) => {
this.typeCategory.forEach(({ type }) => { this.typeCategory.forEach(({ type }) => {
const _filterData = res.filter((list) => list.type === type && list.is_inner) let categoryChildren = []
_typeCategoryChildren[`${type}`] = _filterData switch (type) {
case DISCOVERY_CATEGORY_TYPE.PRIVATE_CLOUD:
categoryChildren = res.filter((list) => list?.option?.category === 'private_cloud' && list?.type === 'http')
break
case DISCOVERY_CATEGORY_TYPE.HTTP:
categoryChildren = res.filter((list) => list?.option?.category !== 'private_cloud' && list?.type === 'http')
break
case DISCOVERY_CATEGORY_TYPE.PLUGIN:
categoryChildren = res.filter((list) => list.is_plugin)
break
case DISCOVERY_CATEGORY_TYPE.AGENT:
categoryChildren = res.filter((list) => !list.is_plugin && list.type === type)
break
default:
categoryChildren = res.filter((list) => list.type === type)
break
}
_typeCategoryChildren[`${type}`]['children'] = categoryChildren
}) })
this.typeCategoryChildren = _typeCategoryChildren this.$set(this, 'typeCategoryChildren', _typeCategoryChildren)
}) })
}, },
handleOpenEditDrawer(data, type, autoType) { handleOpenEditDrawer(data, type, autoType) {
@@ -137,15 +275,111 @@ export default {
xhr.send(formData) xhr.send(formData)
return false return false
}, },
onSearchDiscovery(v) {
this.searchValue = v
},
changeRadio(key) {
this.radioKey = key === this.radioKey ? '' : key
}
}, },
} }
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.setting-discovery { .setting-discovery {
background-color: #fff; &-header {
padding: 20px; display: flex;
border-radius: @border-radius-box; align-items: center;
&-action {
margin-left: auto;
display: flex;
align-items: center;
gap: 14px;
flex-shrink: 0;
&-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 5px 12px;
border: solid 1px @primary-color_8;
background-color: #F4F9FF;
color: @link-color;
}
}
}
&-search {
width: 254px;
flex-shrink: 0;
}
&-radio {
display: flex;
align-items: center;
margin-left: 15px;
gap: 15px;
overflow: auto;
&-item {
padding: 4px 14px;
font-size: 14px;
font-weight: 400;
line-height: 24px;
cursor: pointer;
flex-shrink: 0;
&_active {
background-color: @primary-color_3;
color: @primary-color;
}
}
}
&-body {
background-color: #fff;
border-radius: @border-radius-box;
box-shadow: 0px 0px 4px 0px rgba(158, 171, 190, 0.25);
padding: 20px;
margin-top: 24px;
overflow: auto;
.setting-discovery-add {
height: 105px;
width: 180px;
border-radius: @border-radius-base;
border: 1px dashed @primary-color_8;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
&-text {
color: @text-color_3;
font-size: 12px;
font-weight: 400;
margin-top: 13px;
}
}
.setting-discovery-empty {
text-align: center;
padding: 20px 0;
&-text {
margin-top: 20px;
}
&-img {
width: 100px;
}
}
}
.type-header { .type-header {
width: 100%; width: 100%;
display: inline-flex; display: inline-flex;

View File

@@ -0,0 +1,234 @@
<template>
<div class="counter-wrap">
<div
v-for="(group, groupIndex) in counterData"
:key="groupIndex"
class="counter-group"
>
<div
v-for="(item, index) in group"
:key="index"
class="counter-item"
>
<div class="counter-item-header">
<ops-icon class="counter-item-icon" :type="item.icon" />
<span
class="counter-item-title"
:title="$t(item.title)"
>
{{ $t(item.title) }}
</span>
</div>
<div>
<span class="counter-item-number">{{ item.count }}</span>
<template v-if="item.percent !== undefined">
<span
v-if="item.percent !== -1"
:class="['counter-item-percent', 'counter-item-percent-' + (item.percentStatus ? 'up' : 'down')]"
>
<ops-icon class="counter-item-percent-icon" type="cmdb-arrow" />
<span
class="counter-item-percent-text"
>
{{ item.percent }}%
</span>
</span>
<span
v-else
class="counter-item-percent-null"
>
-
</span>
</template>
</div>
</div>
</div>
</div>
</template>
<script>
import _ from 'lodash'
import { getAdcCounter } from '@/modules/cmdb/api/discovery'
export default {
name: 'AdcCounter',
props: {
typeId: {
type: Number,
default: 0,
}
},
data() {
return {
counterDataTemplate: [
[
{
title: 'cmdb.ad.ruleCount',
icon: 'cmdb-rule',
type: 'ruleCount',
count: 0,
},
{
title: 'cmdb.ad.execMachine',
icon: 'cmdb-executing_machine',
type: 'execTargetCount',
count: 0,
},
],
[
{
title: 'cmdb.ad.resource',
icon: 'cmdb-resource',
type: 'resource',
count: 0,
},
{
title: 'cmdb.ad.autoInventory',
icon: 'cmdb-automatic_inventory',
type: 'autoInventory',
count: 0,
},
],
[
{
title: 'cmdb.ad.newThisWeek',
icon: 'cmdb-week_additions',
type: 'rule_count',
count: 0,
percentStatus: true,
percent: '',
},
{
title: 'cmdb.ad.newThisMonth',
icon: 'cmdb-month_additions',
type: 'rule_count',
count: 0,
percentStatus: true,
percent: '',
}
]
],
counterData: [],
}
},
watch: {
typeId: {
immediate: true,
handler(id) {
if (id) {
this.queryAdcCounter(id)
}
}
}
},
methods: {
async queryAdcCounter(id) {
const res = await getAdcCounter({
type_id: id
})
console.log('getAdcCounter res', res)
const _counterData = _.cloneDeep(this.counterDataTemplate)
_counterData[0][0]['count'] = res?.rule_count ?? 0
_counterData[0][1]['count'] = res?.exec_target_count ?? 0
_counterData[1][0]['count'] = res?.instance_count ?? 0
_counterData[1][1]['count'] = res?.accept_count ?? 0
const newWeekCount = Math.abs(res.this_week_count - res.last_week_count)
const newWeekPrecent = res.last_week_count ? Number((newWeekCount / res.last_week_count).toFixed(2)) * 100 : -1
_counterData[2][0]['count'] = res.this_week_count || 0
_counterData[2][0]['percent'] = newWeekPrecent
_counterData[2][0]['percentStatus'] = res.this_week_count >= res.last_week_count
const newMonthCount = Math.abs(res.this_month_count - res.last_month_count)
const newMonthPrecent = res.last_month_count ? Number((newMonthCount / res.last_month_count).toFixed(2)) * 100 : -1
_counterData[2][1]['count'] = res.this_month_count || 0
_counterData[2][1]['percent'] = newMonthPrecent
_counterData[2][1]['percentStatus'] = res.this_month_count >= res.last_month_count
this.counterData = _counterData
}
}
}
</script>
<style lang="less" scoped>
.counter-wrap {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
.counter-group {
display: flex;
align-items: center;
height: 82px;
border: solid 1px @border-color-base;
flex-grow: 0;
width: calc((100% - 30px) / 3);
.counter-item {
padding: 16px 18px;
flex-grow: 0;
width: 50%;
&-header {
width: 100%;
display: flex;
align-items: center;
}
&-icon {
font-size: 14px;
}
&-title {
font-size: 14px;
color: @text-color_2;
margin-left: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex-grow: 0;
}
&-number {
color: @primary-color;
font-size: 22px;
font-weight: 700;
}
&-percent {
margin-left: 5px;
&-icon {
font-size: 12px;
}
&-text {
font-size: 10px;
font-weight: 400;
}
&-up {
color: #00B42A;
}
&-down {
color: #FD4C6A;
.counter-item-percent-icon {
transform: rotate(180deg);
}
}
}
&-percent-null {
padding: 0 10px;
}
}
}
}
</style>

View File

@@ -0,0 +1,41 @@
<template>
<div>
<span v-if="!isShow">{{ showPassword }}</span>
<span v-else>{{ password }}</span>
<a
class="password-eye"
@click="switchPassword"
>
<a-icon :type="isShow ? 'eye-invisible' : 'eye'"/>
</a>
</div>
</template>
<script>
export default {
name: 'PasswordField',
props: {
password: {
type: String,
default: '',
}
},
data() {
return {
isShow: false,
showPassword: '******',
}
},
methods: {
switchPassword() {
this.isShow = !this.isShow
},
},
}
</script>
<style lang="less" scoped>
.password-eye {
margin-left: 10px
}
</style>

View File

@@ -7,42 +7,49 @@
><span :style="{ color: 'rgb(195, 205, 215)' }">({{ group.ci_types.length }})</span> ><span :style="{ color: 'rgb(195, 205, 215)' }">({{ group.ci_types.length }})</span>
</p> </p>
<div <div
:class="{ 'cmdb-adc-side-item': true, 'cmdb-adc-side-item-selected': currentType === type.id }" :class="{ 'cmdb-adc-side-item': true, 'cmdb-adc-side-item-selected': currentType === ciType.id }"
v-for="type in group.ci_types" v-for="ciType in group.ci_types"
:key="type.id" :key="ciType.id"
@click="clickSidebar(type.id)" @click="clickSidebar(ciType.id)"
> >
<span class="cmdb-adc-side-icon"> <span class="cmdb-adc-side-icon">
<template v-if="type.icon"> <template v-if="ciType.icon">
<img v-if="type.icon.split('$$')[2]" :src="`/api/common-setting/v1/file/${type.icon.split('$$')[3]}`" /> <img v-if="ciType.icon.split('$$')[2]" :src="`/api/common-setting/v1/file/${ciType.icon.split('$$')[3]}`" />
<ops-icon <ops-icon
v-else v-else
:style="{ :style="{
color: type.icon.split('$$')[1], color: ciType.icon.split('$$')[1],
fontSize: '14px', fontSize: '14px',
}" }"
:type="type.icon.split('$$')[0]" :type="ciType.icon.split('$$')[0]"
/> />
</template> </template>
<span :style="{ color: '#2f54eb' }" v-else>{{ type.name[0].toUpperCase() }}</span> <span :style="{ color: '#2f54eb' }" v-else>{{ ciType.name[0].toUpperCase() }}</span>
</span> </span>
<span :title="type.alias || type.name" class="cmdb-adc-side-name">{{ type.alias || type.name }}</span> <span :title="ciType.alias || ciType.name" class="cmdb-adc-side-name">{{ ciType.alias || ciType.name }}</span>
</div> </div>
</div> </div>
</template> </template>
<template #two> <template #two>
<div id="discovery-ci"> <div id="discovery-ci">
<a-input-search <AdcCounter :typeId="currentType" />
:placeholder="$t('cmdb.components.pleaseSearch')" <div class="discovery-ci-header">
:style="{ width: '200px', marginRight: '20px', marginBottom: '10px' }" <a-input-search
@search="handleSearch" :placeholder="$t('cmdb.components.pleaseSearch')"
allowClear :style="{ width: '200px', marginRight: '20px' }"
/> @search="handleSearch"
<div class="ops-list-batch-action" :style="{ marginBottom: '10px' }" v-show="!!selectedRowKeys.length"> allowClear
<span @click="batchAccept">{{ $t('cmdb.ad.accept') }}</span> />
<a-divider type="vertical" /> <span class="ops-list-batch-action" v-show="selectedCount">
<span @click="batchDelete">{{ $t('delete') }}</span> <span @click="batchAccept">{{ $t('cmdb.ad.accept') }}</span>
<span>{{ $t('cmdb.ci.selectRows', { rows: selectedRowKeys.length }) }}</span> <a-divider type="vertical" />
<span @click="batchDelete">{{ $t('delete') }}</span>
<span>{{ $t('cmdb.ci.selectRows', { rows: selectedCount }) }}</span>
</span>
<div @click="clickLog" class="discovery-ci-log">
<ops-icon type="a-cmdb-log1" />
<span>{{ $t('cmdb.ad.log') }}</span>
</div>
</div> </div>
<ops-table <ops-table
show-overflow show-overflow
@@ -51,7 +58,7 @@
ref="xTable" ref="xTable"
size="mini" size="mini"
stripe stripe
class="ops-stripe-table" class="ops-stripe-table checkbox-hover-table"
:data="filterTableData" :data="filterTableData"
:height="tableHeight" :height="tableHeight"
:scroll-y="{ enabled: true, gt: 50 }" :scroll-y="{ enabled: true, gt: 50 }"
@@ -62,56 +69,79 @@
:checkbox-config="{ reserve: true, highlight: true, range: true }" :checkbox-config="{ reserve: true, highlight: true, range: true }"
:sort-config="{ remote: false, trigger: 'cell' }" :sort-config="{ remote: false, trigger: 'cell' }"
> >
<vxe-column align="center" type="checkbox" width="60"></vxe-column>
<vxe-column <vxe-column
v-for="col in columns" align="center"
:key="col.field" type="checkbox"
width="60"
fixed="left"
>
<template #default="{row}">
{{ getRowSeq(row) }}
</template>
</vxe-column>
<vxe-column
v-for="(col, index) in columns"
:key="`${col.field}_${index}`"
:title="col.title" :title="col.title"
:field="col.field" :field="col.field"
:width="col.width" :width="col.width"
:sortable="col.sortable" :sortable="col.sortable"
> >
<template v-if="col.value_type === '6'" #default="{row}"> <template v-if="col.value_type === '6' || col.is_password" #default="{row}">
<span v-if="col.value_type === '6' && row[col.field]">{{ row[col.field] }}</span> <PasswordField
</template> v-if="col.is_password"
</vxe-column> :password="row[col.field]"
<vxe-column />
align="center" <span
field="is_accept" v-else-if="col.value_type === '6' && row[col.field]"
:title="$t('cmdb.ad.isAccept')" >
v-bind="columns.length ? { width: '100px' } : { minWidth: '100px' }" {{ row[col.field] }}
:filters="[ </span>
{ label: $t('yes'), value: true },
{ label: $t('no'), value: false },
]"
>
<template #default="{row}">
{{ row.is_accept ? $t('yes') : $t('no') }}
</template> </template>
</vxe-column> </vxe-column>
<vxe-column <vxe-column
field="accept_by" field="accept_by"
:title="$t('cmdb.ad.acceptBy')" :title="$t('cmdb.ad.acceptBy')"
v-bind="columns.length ? { width: '80px' } : { minWidth: '80px' }" v-bind="columns.length ? { width: '80px' } : { minWidth: '80px' }"
:filters="[]" :filters="acceptByFilters"
></vxe-column> ></vxe-column>
<vxe-column
align="center"
field="is_accept"
:title="$t('cmdb.ad.isAccept')"
v-bind="columns.length ? { width: '80px' } : { minWidth: '80px' }"
:filters="[
{ label: $t('yes'), value: true },
{ label: $t('no'), value: false },
]"
fixed="right"
>
<template #default="{row}">
<ops-icon
:type="row.is_accept ? 'cmdb-warehousing' : 'cmdb-not_warehousing'"
:style="{ color: row.is_accept ? '#00B42A' : '#A5A9BC' }"
/>
</template>
</vxe-column>
<vxe-column <vxe-column
field="accept_time" field="accept_time"
:title="$t('cmdb.ad.acceptTime')" :title="$t('cmdb.ad.acceptTime')"
sortable sortable
v-bind="columns.length ? { width: '130px' } : { minWidth: '130px' }" v-bind="columns.length ? { width: '150px' } : { minWidth: '150px' }"
fixed="right"
></vxe-column> ></vxe-column>
<vxe-column <vxe-column
:title="$t('operation')" :title="$t('operation')"
v-bind="columns.length ? { width: '60px' } : { minWidth: '60px' }" v-bind="columns.length ? { width: '60px' } : { minWidth: '60px' }"
align="center" align="center"
fixed="right"
> >
<template #default="{row}"> <template #default="{row}">
<a-space> <a-space>
<a-tooltip :title="$t('cmdb.ad.accept')"> <a-tooltip :title="$t('cmdb.ad.accept')">
<a v-if="!row.is_accept" @click="accept(row)"><ops-icon type="icon-xianxing-edit"/></a> <a v-if="!row.is_accept" @click="accept(row)"><ops-icon type="cmdb-manual_warehousing"/></a>
</a-tooltip> </a-tooltip>
<a :style="{ color: 'red' }" @click="deleteADC(row)"><ops-icon type="icon-xianxing-delete"/></a> <a :style="{ color: 'red' }" @click="deleteADC(row)"><a-icon type="delete"/></a>
</a-space> </a-space>
</template> </template>
</vxe-column> </vxe-column>
@@ -122,6 +152,23 @@
</div> </div>
</template> </template>
</ops-table> </ops-table>
<a-modal
v-model="logModalVisible"
:footer="null"
:width="596"
>
<div class="log-modal-title">{{ $t('cmdb.ad.log') }}</div>
<p ref="logModelText" class="log-modal-text">
<span
v-for="(item, index) in logTextArray"
:key="index"
class="log-modal-text-item"
>
{{ item }}
</span>
</p>
</a-modal>
</div> </div>
</template> </template>
</TwoColumnLayout> </TwoColumnLayout>
@@ -131,11 +178,26 @@
import _ from 'lodash' import _ from 'lodash'
import XEUtils from 'xe-utils' import XEUtils from 'xe-utils'
import TwoColumnLayout from '@/components/TwoColumnLayout' import TwoColumnLayout from '@/components/TwoColumnLayout'
import { getADCCiTypes, getAdc, updateADCAccept, getADCCiTypesAttrs, deleteAdc } from '../../api/discovery' import AdcCounter from './components/adcCounter.vue'
import PasswordField from './components/passwordField.vue'
import {
getADCCiTypes,
getAdc,
updateADCAccept,
getADCCiTypesAttrs,
deleteAdc,
getAdcExecHistories
} from '../../api/discovery'
import { getCITableColumns } from '../../utils/helper' import { getCITableColumns } from '../../utils/helper'
export default { export default {
name: 'DiscoveryCI', name: 'DiscoveryCI',
components: { TwoColumnLayout }, components: {
TwoColumnLayout,
AdcCounter,
PasswordField
},
data() { data() {
return { return {
ci_types_list: [], ci_types_list: [],
@@ -145,6 +207,10 @@ export default {
columns: [], columns: [],
selectedRowKeys: [], selectedRowKeys: [],
searchValue: '', searchValue: '',
logModalVisible: false,
logTextArray: [],
acceptByFilters: [],
selectedCount: 0,
} }
}, },
computed: { computed: {
@@ -152,7 +218,7 @@ export default {
return this.$store.state.windowHeight return this.$store.state.windowHeight
}, },
tableHeight() { tableHeight() {
return this.windowHeight - 140 return this.windowHeight - 240
}, },
filterTableData() { filterTableData() {
const { searchValue } = this const { searchValue } = this
@@ -169,7 +235,7 @@ export default {
return rest return rest
} }
return this.tableData return this.tableData
}, }
}, },
watch: { watch: {
currentType: { currentType: {
@@ -180,6 +246,13 @@ export default {
} }
}, },
}, },
selectedRowKeys: {
deep: true,
immediate: true,
handler(selectedRowKeys) {
this.selectedCount = selectedRowKeys.length
},
},
}, },
mounted() { mounted() {
getADCCiTypes({ need_other: true }).then((res) => { getADCCiTypes({ need_other: true }).then((res) => {
@@ -213,18 +286,20 @@ export default {
if ($table) { if ($table) {
const nameColumn = $table.getVxetableRef().getColumnByField('accept_by') const nameColumn = $table.getVxetableRef().getColumnByField('accept_by')
if (nameColumn) { if (nameColumn) {
const acceptByFilters = _.uniqBy(
res.result
.filter((item) => item.accept_by)
.map((item) => ({
value: item.accept_by,
label: item.accept_by,
})),
'value'
)
$table.getVxetableRef().setFilter( $table.getVxetableRef().setFilter(
nameColumn, nameColumn,
_.uniqBy( acceptByFilters
res.result
.filter((item) => item.accept_by)
.map((item) => ({
value: item.accept_by,
label: item.accept_by,
})),
'value'
)
) )
this.acceptByFilters = acceptByFilters
} }
} }
this.tableData = res.result.map((item) => ({ ..._.cloneDeep(item), ...item.instance })) this.tableData = res.result.map((item) => ({ ..._.cloneDeep(item), ...item.instance }))
@@ -302,12 +377,36 @@ export default {
onCancel() {}, onCancel() {},
}) })
}, },
onSelectChange({ records, checked, row }) { onSelectChange({ records, checked }) {
this.selectedRowKeys = records.map((item) => item.id) this.selectedRowKeys = records.map((item) => item.id)
}, },
handleSearch(value) { handleSearch(value) {
this.searchValue = value this.searchValue = value
}, },
async clickLog() {
this.logModalVisible = true
const logRes = await getAdcExecHistories({
type_id: this.currentType,
page_size: 1000
})
let logTextArray = []
if (logRes?.result?.length) {
logTextArray = logRes.result.map((log) => {
return `[${log.created_at}] ${log.stdout}`
})
}
this.logTextArray = logTextArray
this.$nextTick(() => {
const textEl = this.$refs.logModelText
if (textEl) {
textEl.scrollTop = textEl.scrollHeight
}
})
},
getRowSeq(row) {
return this.$refs.xTable.getVxetableRef().getRowSeq(row)
}
}, },
} }
</script> </script>
@@ -350,5 +449,78 @@ export default {
.ops_popover_item_selected(); .ops_popover_item_selected();
background-color: @primary-color_3; background-color: @primary-color_3;
} }
.discovery-ci-header {
display: flex;
align-items: center;
padding-top: 18px;
padding-bottom: 10px;
}
.discovery-ci-log {
cursor: pointer;
background-color: #F4F9FF;
border: solid 1px @primary-color_8;
color: @primary-color;
font-size: 12px;
padding: 5px 12px;
margin-left: auto;
display: flex;
align-items: center;
gap: 6px;
}
.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;
}
}
}
}
}
}
.log-modal-title {
font-size: 14px;
font-weight: 500;
}
.log-modal-text {
margin-top: 14px;
padding: 12px;
width: 100%;
height: 312px;
overflow: auto;
border: solid 1px @border-color-base;
background-color: #2f333d;
&-item {
color: #c5c8c6;
width: 100%;
display: block;
white-space: pre-wrap;
word-break: break-all;
}
} }
</style> </style>

View File

@@ -70,13 +70,16 @@
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item :label="$t('cmdb.ciType.attributeAssociation')"> <a-form-item :label="$t('cmdb.ciType.attributeAssociation')">
<a-row> <a-row
<a-col :span="11"> v-for="item in modalAttrList"
:key="item.id"
>
<a-col :span="10">
<a-form-item> <a-form-item>
<a-select <a-select
:placeholder="$t('cmdb.ciType.attributeAssociationTip4')" :placeholder="$t('cmdb.ciType.attributeAssociationTip4')"
allowClear allowClear
v-decorator="['parent_attr_id', { rules: [{ required: false }] }]" v-model="item.parentAttrId"
> >
<a-select-option v-for="attr in filterAttributes(modalParentAttributes)" :key="attr.id"> <a-select-option v-for="attr in filterAttributes(modalParentAttributes)" :key="attr.id">
{{ attr.alias || attr.name }} {{ attr.alias || attr.name }}
@@ -87,12 +90,12 @@
<a-col :span="2" :style="{ textAlign: 'center' }"> <a-col :span="2" :style="{ textAlign: 'center' }">
=> =>
</a-col> </a-col>
<a-col :span="11"> <a-col :span="9">
<a-form-item> <a-form-item>
<a-select <a-select
:placeholder="$t('cmdb.ciType.attributeAssociationTip5')" :placeholder="$t('cmdb.ciType.attributeAssociationTip5')"
allowClear allowClear
v-decorator="['child_attr_id', { rules: [{ required: false }] }]" v-model="item.childAttrId"
> >
<a-select-option v-for="attr in filterAttributes(modalChildAttributes)" :key="attr.id"> <a-select-option v-for="attr in filterAttributes(modalChildAttributes)" :key="attr.id">
{{ attr.alias || attr.name }} {{ attr.alias || attr.name }}
@@ -100,6 +103,20 @@
</a-select> </a-select>
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="3">
<a
class="modal-attribute-action"
@click="removeModalAttr(item.id)"
>
<a-icon type="minus-circle" />
</a>
<a
class="modal-attribute-action"
@click="addModalAttr"
>
<a-icon type="plus-circle" />
</a>
</a-col>
</a-row> </a-row>
</a-form-item> </a-form-item>
</a-form> </a-form>
@@ -114,6 +131,7 @@ import { getCITypeGroupsConfig } from '@/modules/cmdb/api/ciTypeGroup'
import { getCITypes } from '@/modules/cmdb/api/CIType' import { getCITypes } from '@/modules/cmdb/api/CIType'
import { createRelation, deleteRelation, getCITypeChildren, getRelationTypes } from '@/modules/cmdb/api/CITypeRelation' import { createRelation, deleteRelation, getCITypeChildren, getRelationTypes } from '@/modules/cmdb/api/CITypeRelation'
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr' import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
import { v4 as uuidv4 } from 'uuid'
export default { export default {
name: 'Index', name: 'Index',
@@ -139,6 +157,7 @@ export default {
modalParentAttributes: [], modalParentAttributes: [],
modalChildAttributes: [], modalChildAttributes: [],
modalAttrList: [],
} }
}, },
computed: { computed: {
@@ -228,6 +247,13 @@ export default {
handleCreate() { handleCreate() {
this.drawerTitle = this.$t('cmdb.ciType.addRelation') this.drawerTitle = this.$t('cmdb.ciType.addRelation')
this.visible = true this.visible = true
this.$set(this, 'modalAttrList', [
{
id: uuidv4(),
parentAttrId: undefined,
childAttrId: undefined
}
])
this.$nextTick(() => { this.$nextTick(() => {
this.form.setFieldsValue({ this.form.setFieldsValue({
source_ci_type_id: this.sourceCITypeId, source_ci_type_id: this.sourceCITypeId,
@@ -249,19 +275,22 @@ export default {
ci_type_id, ci_type_id,
relation_type_id, relation_type_id,
constraint, constraint,
parent_attr_id = undefined,
child_attr_id = undefined,
} = values } = values
if ((!parent_attr_id && child_attr_id) || (parent_attr_id && !child_attr_id)) { const {
this.$message.warning(this.$t('cmdb.ciType.attributeAssociationTip3')) parent_attr_ids,
child_attr_ids,
validate
} = this.handleValidateAttrList(this.modalAttrList)
if (!validate) {
return return
} }
createRelation(source_ci_type_id, ci_type_id, { createRelation(source_ci_type_id, ci_type_id, {
relation_type_id, relation_type_id,
constraint, constraint,
parent_attr_id, parent_attr_ids,
child_attr_id, child_attr_ids,
}).then((res) => { }).then((res) => {
this.$message.success(this.$t('addSuccess')) this.$message.success(this.$t('addSuccess'))
this.onClose() this.onClose()
@@ -272,6 +301,37 @@ export default {
this.sourceCITypeId = undefined this.sourceCITypeId = undefined
this.targetCITypeId = undefined this.targetCITypeId = undefined
}, },
/**
* 校验属性列表
* @param {*} attrList
*/
handleValidateAttrList(attrList) {
const parent_attr_ids = []
const child_attr_ids = []
attrList.map((attr) => {
if (attr.parentAttrId) {
parent_attr_ids.push(attr.parentAttrId)
}
if (attr.childAttrId) {
child_attr_ids.push(attr.childAttrId)
}
})
if (parent_attr_ids.length !== child_attr_ids.length) {
this.$message.warning(this.$t('cmdb.ciType.attributeAssociationTip3'))
return {
validate: false
}
}
return {
validate: true,
parent_attr_ids,
child_attr_ids
}
},
handleOk() { handleOk() {
this.$refs.table.refresh() this.$refs.table.refresh()
}, },
@@ -284,14 +344,18 @@ export default {
}, },
handleSourceTypeChange(value) { handleSourceTypeChange(value) {
this.sourceCITypeId = value this.sourceCITypeId = value
this.form.setFieldsValue({ parent_attr_id: undefined }) this.modalAttrList.forEach((item) => {
item.parentAttrId = undefined
})
getCITypeAttributesById(value).then((res) => { getCITypeAttributesById(value).then((res) => {
this.modalParentAttributes = res?.attributes ?? [] this.modalParentAttributes = res?.attributes ?? []
}) })
}, },
handleTargetTypeChange(value) { handleTargetTypeChange(value) {
this.targetCITypeId = value this.targetCITypeId = value
this.form.setFieldsValue({ child_attr_id: undefined }) this.modalAttrList.forEach((item) => {
item.childAttrId = undefined
})
getCITypeAttributesById(value).then((res) => { getCITypeAttributesById(value).then((res) => {
this.modalChildAttributes = res?.attributes ?? [] this.modalChildAttributes = res?.attributes ?? []
}) })
@@ -303,12 +367,30 @@ export default {
// filter password/json/is_list // filter password/json/is_list
return attributes.filter((attr) => !attr.is_password && !attr.is_list && attr.value_type !== '6') return attributes.filter((attr) => !attr.is_password && !attr.is_list && attr.value_type !== '6')
}, },
addModalAttr() {
this.modalAttrList.push({
id: uuidv4(),
parentAttrId: undefined,
childAttrId: undefined
})
},
removeModalAttr(id) {
if (this.modalAttrList.length <= 1) {
this.$message.error(this.$t('cmdb.ciType.attributeAssociationTip6'))
return
}
const index = this.modalAttrList.findIndex((item) => item.id === id)
if (index !== -1) {
this.modalAttrList.splice(index, 1)
}
}
}, },
} }
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.model-relation { .model-relation {
background-color: #fff; background-color: #fff;
border-radius: @border-radius-box; border-radius: @border-radius-box;
@@ -316,4 +398,8 @@ export default {
height: calc(100vh - 64px); height: calc(100vh - 64px);
margin-bottom: -24px; margin-bottom: -24px;
} }
.modal-attribute-action {
margin-left: 5px;
}
</style> </style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div> <div class="model-relation-table">
<vxe-table <vxe-table
ref="xTable" ref="xTable"
stripe stripe
@@ -7,6 +7,7 @@
show-header-overflow show-header-overflow
show-overflow show-overflow
resizable resizable
:scroll-y="{enabled: false}"
:height="`${windowHeight - 160}px`" :height="`${windowHeight - 160}px`"
:data="tableData" :data="tableData"
:sort-config="{ defaultSort: { field: 'created_at', order: 'desc' } }" :sort-config="{ defaultSort: { field: 'created_at', order: 'desc' } }"
@@ -34,7 +35,7 @@
{{ handleConstraint(row.constraint) }} {{ handleConstraint(row.constraint) }}
</template> </template>
</vxe-column> </vxe-column>
<vxe-column :width="250" field="attributeAssociation" :edit-render="{}"> <vxe-column :width="300" field="attributeAssociation" :edit-render="{}">
<template #header> <template #header>
<span> <span>
<a-tooltip :title="$t('cmdb.ciType.attributeAssociationTip1')"> <a-tooltip :title="$t('cmdb.ciType.attributeAssociationTip1')">
@@ -47,37 +48,73 @@
</span> </span>
</template> </template>
<template #default="{row}"> <template #default="{row}">
<span <template
v-if="row.parent_attr_id && row.child_attr_id" v-for="item in row.parentAndChildAttrList"
>{{ getAttrNameById(type2attributes[row.parent_id], row.parent_attr_id) }}=>
{{ getAttrNameById(type2attributes[row.child_id], row.child_attr_id) }}</span
> >
<div
:key="item.id"
v-if="item.parentAttrId && item.childAttrId"
>
{{ getAttrNameById(type2attributes[row.parent_id], item.parentAttrId) }}=>
{{ getAttrNameById(type2attributes[row.child_id], item.childAttrId) }}
</div>
</template>
</template> </template>
<template #edit="{ row }"> <template #edit="{ row }">
<div style="display:inline-flex;align-items:center;"> <div
v-for="item in tableAttrList"
:key="item.id"
class="table-attribute-row"
>
<a-select <a-select
allowClear allowClear
size="small" size="small"
v-model="parent_attr_id" v-model="item.parentAttrId"
:getPopupContainer="(trigger) => trigger.parentNode" :getPopupContainer="(trigger) => trigger.parentNode"
:style="{ width: '100px' }" :style="{ width: '100px' }"
show-search
optionFilterProp="title"
> >
<a-select-option v-for="attr in filterAttributes(type2attributes[row.parent_id])" :key="attr.id"> <a-select-option
v-for="attr in filterAttributes(type2attributes[row.parent_id])"
:key="attr.id"
:value="attr.id"
:title="attr.alias || attr.name"
>
{{ attr.alias || attr.name }} {{ attr.alias || attr.name }}
</a-select-option> </a-select-option>
</a-select> </a-select>
=> <span class="table-attribute-row-link">=></span>
<a-select <a-select
allowClear allowClear
size="small" size="small"
v-model="child_attr_id" v-model="item.childAttrId"
:getPopupContainer="(trigger) => trigger.parentNode" :getPopupContainer="(trigger) => trigger.parentNode"
:style="{ width: '100px' }" :style="{ width: '100px' }"
show-search
optionFilterProp="title"
> >
<a-select-option v-for="attr in filterAttributes(type2attributes[row.child_id])" :key="attr.id"> <a-select-option
v-for="attr in filterAttributes(type2attributes[row.child_id])"
:key="attr.id"
:value="attr.id"
:title="attr.alias || attr.name"
>
{{ attr.alias || attr.name }} {{ attr.alias || attr.name }}
</a-select-option> </a-select-option>
</a-select> </a-select>
<a
class="table-attribute-row-action"
@click="removeTableAttr(item.id)"
>
<a-icon type="minus-circle" />
</a>
<a
class="table-attribute-row-action"
@click="addTableAttr"
>
<a-icon type="plus-circle" />
</a>
</div> </div>
</template> </template>
</vxe-column> </vxe-column>
@@ -97,6 +134,7 @@
</template> </template>
<script> <script>
import { v4 as uuidv4 } from 'uuid'
import { getCITypeRelations, deleteRelation, createRelation } from '@/modules/cmdb/api/CITypeRelation' import { getCITypeRelations, deleteRelation, createRelation } from '@/modules/cmdb/api/CITypeRelation'
import { getRelationTypes } from '@/modules/cmdb/api/relationType' import { getRelationTypes } from '@/modules/cmdb/api/relationType'
import CMDBGrant from '../../../components/cmdbGrant' import CMDBGrant from '../../../components/cmdbGrant'
@@ -108,8 +146,7 @@ export default {
tableData: [], tableData: [],
relationTypeList: null, relationTypeList: null,
type2attributes: {}, type2attributes: {},
parent_attr_id: undefined, tableAttrList: [],
child_attr_id: undefined,
} }
}, },
components: { components: {
@@ -137,9 +174,29 @@ export default {
}, },
async getMainData() { async getMainData() {
const { relations, type2attributes } = await getCITypeRelations() const { relations, type2attributes } = await getCITypeRelations()
this.tableData = relations this.tableData = relations.map((item) => {
const parentAndChildAttrList = this.handleAttrList(item)
return {
...item,
parentAndChildAttrList
}
})
this.type2attributes = type2attributes this.type2attributes = type2attributes
}, },
handleAttrList(data) {
const length = Math.min(data?.parent_attr_ids?.length || 0, data.child_attr_ids?.length || 0)
const parentAndChildAttrList = []
for (let i = 0; i < length; i++) {
parentAndChildAttrList.push({
id: uuidv4(),
parentAttrId: data?.parent_attr_ids?.[i] ?? '',
childAttrId: data?.child_attr_ids?.[i] ?? ''
})
}
return parentAndChildAttrList
},
// 获取关系 // 获取关系
async getRelationTypes() { async getRelationTypes() {
const res = await getRelationTypes() const res = await getRelationTypes()
@@ -171,21 +228,75 @@ export default {
}) })
}, },
handleEditActived({ row }) { handleEditActived({ row }) {
this.parent_attr_id = row?.parent_attr_id ?? undefined const tableAttrList = []
this.child_attr_id = row?.child_attr_id ?? undefined
const length = Math.min(row?.parent_attr_ids?.length || 0, row.child_attr_ids?.length || 0)
if (length) {
for (let i = 0; i < length; i++) {
tableAttrList.push({
id: uuidv4(),
parentAttrId: row?.parent_attr_ids?.[i] ?? undefined,
childAttrId: row?.child_attr_ids?.[i] ?? undefined
})
}
} else {
tableAttrList.push({
id: uuidv4(),
parentAttrId: undefined,
childAttrId: undefined
})
}
console.log('handleEditActived', tableAttrList)
this.$set(this, 'tableAttrList', tableAttrList)
}, },
/**
* 校验属性列表
* @param {*} attrList
*/
handleValidateAttrList(attrList) {
const parent_attr_ids = []
const child_attr_ids = []
attrList.map((attr) => {
if (attr.parentAttrId) {
parent_attr_ids.push(attr.parentAttrId)
}
if (attr.childAttrId) {
child_attr_ids.push(attr.childAttrId)
}
})
if (parent_attr_ids.length !== child_attr_ids.length) {
this.$message.warning(this.$t('cmdb.ciType.attributeAssociationTip3'))
return {
validate: false
}
}
return {
validate: true,
parent_attr_ids,
child_attr_ids
}
},
async handleEditClose({ row }) { async handleEditClose({ row }) {
const { parent_id, child_id, constraint, relation_type_id } = row const { parent_id, child_id, constraint, relation_type_id } = row
const { parent_attr_id = undefined, child_attr_id = undefined } = this
if ((!parent_attr_id && child_attr_id) || (parent_attr_id && !child_attr_id)) { const {
this.$message.warning(this.$t('cmdb.ciType.attributeAssociationTip3')) parent_attr_ids,
child_attr_ids,
validate
} = this.handleValidateAttrList(this.tableAttrList)
if (!validate) {
return return
} }
await createRelation(parent_id, child_id, { await createRelation(parent_id, child_id, {
relation_type_id, relation_type_id,
constraint, constraint,
parent_attr_id, parent_attr_ids,
child_attr_id, child_attr_ids,
}).finally(() => { }).finally(() => {
this.getMainData() this.getMainData()
}) })
@@ -198,8 +309,49 @@ export default {
// filter password/json/is_list // filter password/json/is_list
return attributes.filter((attr) => !attr.is_password && !attr.is_list && attr.value_type !== '6') return attributes.filter((attr) => !attr.is_password && !attr.is_list && attr.value_type !== '6')
}, },
addTableAttr() {
this.tableAttrList.push({
id: uuidv4(),
parentAttrId: undefined,
childAttrId: undefined
})
},
removeTableAttr(id) {
if (this.tableAttrList.length <= 1) {
this.$message.error(this.$t('cmdb.ciType.attributeAssociationTip6'))
return
}
const index = this.tableAttrList.findIndex((item) => item.id === id)
if (index !== -1) {
this.tableAttrList.splice(index, 1)
}
},
}, },
} }
</script> </script>
<style></style> <style lang="less" scoped>
.relation-table {
/deep/ .vxe-cell {
max-height: max-content !important;
}
}
.table-attribute-row {
display: inline-flex;
align-items: center;
margin-top: 5px;
&:last-child {
margin-bottom: 5px;
}
&-link {
margin: 0 5px;
}
&-action {
margin-left: 5px;
}
}
</style>

View File

@@ -162,6 +162,9 @@ export default {
{ [this.$t('cmdb.history.deleteUniqueConstraint')]: 11 }, { [this.$t('cmdb.history.deleteUniqueConstraint')]: 11 },
{ [this.$t('cmdb.history.addRelation')]: 12 }, { [this.$t('cmdb.history.addRelation')]: 12 },
{ [this.$t('cmdb.history.deleteRelation')]: 13 }, { [this.$t('cmdb.history.deleteRelation')]: 13 },
{ [this.$t('cmdb.history.addReconciliation')]: 14 },
{ [this.$t('cmdb.history.updateReconciliation')]: 15 },
{ [this.$t('cmdb.history.deleteReconciliation')]: 16 },
], ],
}, },
], ],
@@ -198,6 +201,9 @@ export default {
['11', this.$t('cmdb.history.deleteUniqueConstraint')], ['11', this.$t('cmdb.history.deleteUniqueConstraint')],
['12', this.$t('cmdb.history.addRelation')], ['12', this.$t('cmdb.history.addRelation')],
['13', this.$t('cmdb.history.deleteRelation')], ['13', this.$t('cmdb.history.deleteRelation')],
['14', this.$t('cmdb.history.addReconciliation')],
['15', this.$t('cmdb.history.updateReconciliation')],
['16', this.$t('cmdb.history.deleteReconciliation')],
]) ])
}, },
}, },
@@ -312,27 +318,22 @@ export default {
// update CIType // update CIType
case '1': { case '1': {
item.changeArr = [] item.changeArr = []
for (const key in item.change.old) { const diffs = this.deepCompare({
const newVal = item.change.new[key] obj1: item?.change?.old,
const oldVal = item.change.old[key] obj2: item?.change?.new,
if (!_.isEqual(newVal, oldVal) && key !== 'updated_at') { ignoreKeys: ['updated_at']
if (oldVal === null) { })
const str = ` [ ${key} : ${newVal || '""'} ] ` for (const val of diffs) {
item.changeDescription += str const str = ` [ ${val.path} : ${val.value1} -> ${val.value2} ] `
item.changeArr.push(str) item.changeDescription += str
} else { item.changeArr.push(str)
const str = ` [ ${key} : ${oldVal || '""'} -> ${newVal || '""'} ] `
item.changeDescription += ` [ ${key} : ${oldVal || '""'} -> ${newVal || '""'} ] `
item.changeArr.push(str)
}
}
} }
if (!item.changeDescription) item.changeDescription = this.$t('cmdb.history.noModifications') if (!item.changeDescription) item.changeDescription = this.$t('cmdb.history.noModifications')
break break
} }
// delete CIType // delete CIType
case '2': { case '2': {
item.changeDescription = this.$t('cmdb.history.addCIType') + ': ' + `${item.change.alias}` item.changeDescription = this.$t('cmdb.history.deleteCIType') + ': ' + `${item.change.alias}`
break break
} }
// add Attribute // add Attribute
@@ -343,24 +344,15 @@ export default {
// update Attribute // update Attribute
case '4': { case '4': {
item.changeArr = [] item.changeArr = []
for (const key in item.change.old) { const diffs = this.deepCompare({
if (!_.isEqual(item.change.new[key], item.change.old[key]) && key !== 'updated_at') { obj1: item?.change?.old,
let newStr = item.change.new[key] obj2: item?.change?.new,
let oldStr = item.change.old[key] ignoreKeys: ['updated_at']
if (key === 'choice_value') { })
newStr = newStr ? newStr.map((item) => item[0]).join(',') : '' for (const val of diffs) {
oldStr = oldStr ? oldStr.map((item) => item[0]).join(',') : '' const str = ` [ ${val.path} : ${val.value1} -> ${val.value2} ] `
} item.changeDescription += str
if (Object.prototype.toString.call(newStr) === '[object Object]') { item.changeArr.push(str)
newStr = JSON.stringify(newStr)
}
if (Object.prototype.toString.call(oldStr) === '[object Object]') {
oldStr = JSON.stringify(oldStr)
}
const str = `${key} : ${oldStr ? ` ${oldStr || '""'} ` : ''} -> ${newStr || '""'}`
item.changeDescription += ` [ ${str} ] `
item.changeArr.push(str)
}
} }
if (!item.changeDescription) item.changeDescription = this.$t('cmdb.history.noModifications') if (!item.changeDescription) item.changeDescription = this.$t('cmdb.history.noModifications')
break break
@@ -372,39 +364,29 @@ export default {
} }
// add trigger // add trigger
case '6': { case '6': {
item.changeDescription = this.$t('cmdb.history.noModifications', { item.changeDescription = `${this.$t('cmdb.history.addTrigger')}${item?.change?.option?.name || ''}`
attr_id: item.change.attr_id,
before_days: item.change.option.before_days,
subject: item.change.option.subject,
body: item.change.option.body,
notify_at: item.change.option.notify_at,
})
break break
} }
// update trigger // update trigger
case '7': { case '7': {
item.changeArr = [] item.changeArr = []
for (const key in item.change.old.option) { const diffs = this.deepCompare({
const newVal = item.change.new.option[key] obj1: item?.change?.old,
const oldVal = item.change.old.option[key] obj2: item?.change?.new,
if (!_.isEqual(newVal, oldVal) && key !== 'updated_at') { directDeepKeys: ['notifies'],
const str = ` [ ${key} : ${oldVal} -> ${newVal} ] ` ignoreKeys: ['updated_at']
item.changeDescription += str })
item.changeArr.push(str) for (const val of diffs) {
} const str = ` [ ${val.path} : ${val.value1} -> ${val.value2} ] `
item.changeDescription += str
item.changeArr.push(str)
} }
if (!item.changeDescription) item.changeDescription = this.$t('cmdb.history.noModifications') if (!item.changeDescription) item.changeDescription = this.$t('cmdb.history.noModifications')
break break
} }
// delete trigger // delete trigger
case '8': { case '8': {
item.changeDescription = this.$t('cmdb.history.noModifications', { item.changeDescription = `${this.$t('cmdb.history.deleteTrigger')}${item?.change?.option?.name || ''}`
attr_id: item.change.attr_id,
before_days: item.change.option.before_days,
subject: item.change.option.subject,
body: item.change.option.body,
notify_at: item.change.option.notify_at,
})
break break
} }
// add unique constraint // add unique constraint
@@ -441,8 +423,77 @@ export default {
)} -> ${item.change.child.alias}` )} -> ${item.change.child.alias}`
break break
} }
case '14': {
item.changeDescription = this.$t('cmdb.history.addReconciliation') + ': ' + item.change.name || item.change.alias
break
}
case '15': {
item.changeArr = []
const diffs = this.deepCompare({
obj1: item?.change?.old,
obj2: item?.change?.new,
directDeepKeys: ['notifies'],
ignoreKeys: ['updated_at']
})
for (const val of diffs) {
const str = ` [ ${val.path} : ${val.value1} -> ${val.value2} ] `
item.changeDescription += str
item.changeArr.push(str)
}
if (!item.changeDescription) item.changeDescription = this.$t('cmdb.history.updateReconciliation')
break
}
case '16': {
item.changeDescription = this.$t('cmdb.history.deleteReconciliation') + ': ' + item.change.name || item.change.alias
break
}
} }
}, },
deepCompare({
obj1,
obj2,
directDeepKeys = [],
ignoreKeys = [],
}) {
const diffs = []
function compare(obj1, obj2, path = '') {
if (typeof obj1 !== 'object' || typeof obj2 !== 'object' || obj1 === null || obj2 === null) {
if (obj1 !== obj2) {
diffs.push({ path, value1: formatValue(obj1), value2: formatValue(obj2) })
}
return
}
const keys1 = new Set(Object.keys(obj1))
const keys2 = new Set(Object.keys(obj2))
const allKeys = new Set([...keys1, ...keys2])
allKeys.forEach(key => {
const newPath = path ? `${path}.${key}` : key
if (!ignoreKeys.includes(key)) {
if (directDeepKeys.includes(key) && !_.isEqual(obj1[key], obj2[key])) {
diffs.push({ path: newPath, value1: formatValue(obj1[key]), value2: formatValue(obj2[key]) })
} else if (!keys1.has(key)) {
diffs.push({ path: newPath, value1: undefined, value2: formatValue(obj2[key]) })
} else if (!keys2.has(key)) {
diffs.push({ path: newPath, value1: formatValue(obj1[key]), value2: undefined })
} else {
compare(obj1[key], obj2[key], newPath)
}
}
})
}
function formatValue(val) {
return _.isObject(val) ? JSON.stringify(val) : val
}
compare(obj1, obj2)
return diffs
},
filterOperate() { filterOperate() {
this.queryParams.page = 1 this.queryParams.page = 1
this.queryParams.page_size = 50 this.queryParams.page_size = 50

File diff suppressed because it is too large Load Diff

View File

@@ -32,8 +32,11 @@
ghost ghost
@click="handleClickAddGroup" @click="handleClickAddGroup"
class="ops-button-ghost" class="ops-button-ghost"
><ops-icon type="veops-increase" />{{ $t('cmdb.ciType.group') }}</a-button v-if="permissions.includes('admin') || permissions.includes('cmdb_admin')"
> >
<ops-icon type="veops-increase" />
{{ $t('cmdb.ciType.group') }}
</a-button>
</div> </div>
<draggable class="topo-left-content" :list="computedTopoGroups" @end="handleChangeGroups" filter=".undraggable"> <draggable class="topo-left-content" :list="computedTopoGroups" @end="handleChangeGroups" filter=".undraggable">
<div v-for="group in computedTopoGroups" :key="group.id || group.name"> <div v-for="group in computedTopoGroups" :key="group.id || group.name">
@@ -56,16 +59,16 @@
<a-space> <a-space>
<a-tooltip> <a-tooltip>
<template slot="title">{{ $t('cmdb.topo.addTopoViewInGroup') }}</template> <template slot="title">{{ $t('cmdb.topo.addTopoViewInGroup') }}</template>
<a><ops-icon type="veops-increase" @click="handleCreate(group)"/></a> <a v-if="permissions.includes('admin') || permissions.includes('cmdb_admin')"><ops-icon type="veops-increase" @click="handleCreate(group)"/></a>
</a-tooltip> </a-tooltip>
<template v-if="group.id"> <template v-if="group.id">
<a-tooltip > <a-tooltip >
<template slot="title">{{ $t('cmdb.ciType.editGroup') }}</template> <template slot="title">{{ $t('cmdb.ciType.editGroup') }}</template>
<a><a-icon type="edit" @click="handleEditGroup(group)"/></a> <a v-if="permissions.includes('admin') || permissions.includes('cmdb_admin')"><a-icon type="edit" @click="handleEditGroup(group)"/></a>
</a-tooltip> </a-tooltip>
<a-tooltip> <a-tooltip>
<template slot="title">{{ $t('cmdb.ciType.deleteGroup') }}</template> <template slot="title">{{ $t('cmdb.ciType.deleteGroup') }}</template>
<a :style="{color: 'red'}"><a-icon type="delete" @click="handleDeleteGroup(group)"/></a> <a v-if="permissions.includes('admin') || permissions.includes('cmdb_admin')" :style="{color: 'red'}"><a-icon type="delete" @click="handleDeleteGroup(group)"/></a>
</a-tooltip> </a-tooltip>
</template> </template>
</a-space> </a-space>
@@ -140,12 +143,35 @@
<div :style="{ height: `${windowHeight - 80}px` }" ref="rightTopoView"> <div :style="{ height: `${windowHeight - 80}px` }" ref="rightTopoView">
<RelationGraph ref="showTopoView" :options="graphOptions2" :on-node-click="showNodeTips"> <RelationGraph ref="showTopoView" :options="graphOptions2" :on-node-click="showNodeTips">
<template #node="{node}"> <template #node="{node}">
<div :style="{ lineHeight: '20px' }"> <div
<ops-icon type="caise-wuliji" /> :style="{ borderColor: node.data.btnType === 'more' ? '#A4B5E1' : nodeStyle[Math.abs(node.lot.level)] ? nodeStyle[Math.abs(node.lot.level)].backgroundColor : '#A4B5E1' }"
<span :style="{ marginLeft: '5px', textOverflow: 'ellipsis' }">{{ node.text }}aaa</span> class="relation-graph-node"
>
<template v-if="node.data.icon">
<img
v-if="node.data.icon.split('$$')[2]"
:src="`/api/common-setting/v1/file/${node.data.icon.split('$$')[3]}`"
class="relation-graph-node-image"
/>
<ops-icon
v-else
:style="{ color: node.data.icon.split('$$')[1] }"
:type="node.data.icon ? node.data.icon.split('$$')[0] : ''"
class="relation-graph-node-icon"
/>
</template>
<span class="relation-graph-node-text">
{{ node.data.btnType === 'more' ? $t('cmdb.topo.moreBtn', { count: node.text }) : node.text }}
</span>
</div> </div>
</template> </template>
<template #graph-plug> <template #graph-plug>
<a-input-search
class="relation-graph-search"
v-model="topoViewSearchValue"
:placeholder="$t('cmdb.topo.topoViewSearchPlaceholder')"
@search="handleSearchTopoView"
/>
<div v-if="(isShowNodeTipsPanel && currentNodeValues && currentNodeAttributes.length) || errorMessageShow" :style="nodeTipsPosition" class="node-tips"> <div v-if="(isShowNodeTipsPanel && currentNodeValues && currentNodeAttributes.length) || errorMessageShow" :style="nodeTipsPosition" class="node-tips">
<a-descriptions <a-descriptions
v-if="currentNodeValues" v-if="currentNodeValues"
@@ -236,12 +262,12 @@
</SeeksRelationGraph> </SeeksRelationGraph>
</div> </div>
</a-form-item> </a-form-item>
<a-form-item :style="{ display: 'none' }" :label="$t('cmdb.topo.aggregationCount')" prop="aggregation_count" :help="$t('cmdb.topo.aggreationCountTip')"> <a-form-item :label="$t('cmdb.topo.aggregationCount')" prop="aggregation_count" :help="$t('cmdb.topo.aggreationCountTip')">
<a-input-number <a-input-number
:style="{ width: '100%' }" :style="{ width: '100%' }"
:min="0"
v-decorator="['aggregation_count']" v-decorator="['aggregation_count']"
> >
<a @click="handleOpenCmdb" slot="suffix"><a-icon type="menu"/></a>
</a-input-number> </a-input-number>
</a-form-item> </a-form-item>
<div :class="{ 'chart-left-preview': true, 'chart-left-preview-empty': !isShowPreview }"> <div :class="{ 'chart-left-preview': true, 'chart-left-preview-empty': !isShowPreview }">
@@ -253,9 +279,27 @@
> >
<template v-if="isShowPreview"> <template v-if="isShowPreview">
<RelationGraph ref="previewTopoView" :options="graphOptionsPrivew"> <RelationGraph ref="previewTopoView" :options="graphOptionsPrivew">
<div slot="node" slot-scope="{ node }" :style="{ lineHeight: '20px' }"> <template #node="{node}">
<span :style="{ marginLeft: '5px' }">{{ node.text }}</span> <div
</div> :style="{ borderColor: nodeStyle[node.lot.level] ? nodeStyle[node.lot.level].backgroundColor : '#7F97FA' }"
class="relation-graph-node"
>
<template v-if="node.data.icon">
<img
v-if="node.data.icon.split('$$')[2]"
:src="`/api/common-setting/v1/file/${node.data.icon.split('$$')[3]}`"
class="relation-graph-node-image"
/>
<ops-icon
v-else
:style="{ color: node.data.icon.split('$$')[1] }"
:type="node.data.icon ? node.data.icon.split('$$')[0] : ''"
class="relation-graph-node-icon"
/>
</template>
<span class="relation-graph-node-text">{{ node.text }}</span>
</div>
</template>
</RelationGraph> </RelationGraph>
</template> </template>
</div> </div>
@@ -290,6 +334,7 @@ import { getSubscribeAttributes } from '@/modules/cmdb/api/preference'
import { searchCI } from '@/modules/cmdb/api/ci' import { searchCI } from '@/modules/cmdb/api/ci'
import { getTopoGroups, postTopoGroup, putTopoGroupByGId, putTopoGroupsOrder, deleteTopoGroup, getTopoView, addTopoView, updateTopoView, deleteTopoView, getRelationsByTypeId, previewTopoView, showTopoView } from '@/modules/cmdb/api/topology' import { getTopoGroups, postTopoGroup, putTopoGroupByGId, putTopoGroupsOrder, deleteTopoGroup, getTopoView, addTopoView, updateTopoView, deleteTopoView, getRelationsByTypeId, previewTopoView, showTopoView } from '@/modules/cmdb/api/topology'
import CMDBExprDrawer from '@/components/CMDBExprDrawer' import CMDBExprDrawer from '@/components/CMDBExprDrawer'
import { v4 as uuidv4 } from 'uuid'
const currentTopoKey = 'ops_cmdb_topo_currentId' const currentTopoKey = 'ops_cmdb_topo_currentId'
export default { export default {
@@ -326,6 +371,7 @@ export default {
const graphOptions2 = { const graphOptions2 = {
// ...defaultOptions, // ...defaultOptions,
backgrounImageNoRepeat: true, backgrounImageNoRepeat: true,
ovUseNodeSlot: true,
placeOtherGroup: true, placeOtherGroup: true,
moveToCenterWhenRefresh: true, moveToCenterWhenRefresh: true,
zoomToFitWhenRefresh: true, zoomToFitWhenRefresh: true,
@@ -339,10 +385,11 @@ export default {
max_per_width: 200, max_per_width: 200,
min_per_height: 40, min_per_height: 40,
max_per_height: undefined, max_per_height: undefined,
defaultLineColor: '#CACDD9', backgroundColor: '#FFFFFF',
defaultNodeColor: '#29AAE1', // defaultLineColor: '#CACDD9',
defaultNodeFontColor: '#ffffff', // defaultNodeColor: '#29AAE1',
defaultNodeBorderColor: '#b1c9ff', // defaultNodeFontColor: '#ffffff',
// defaultNodeBorderColor: '#b1c9ff',
defaultExpandHolderPosition: 'right', defaultExpandHolderPosition: 'right',
defaultJunctionPoint: 'lr', defaultJunctionPoint: 'lr',
layouts: [ layouts: [
@@ -350,7 +397,7 @@ export default {
layoutName: 'tree', layoutName: 'tree',
from: 'left', from: 'left',
layoutClassName: 'seeks-layout-center', layoutClassName: 'seeks-layout-center',
defaultExpandHolderPosition: 'hide', defaultExpandHolderPosition: 'right',
defaultJunctionPoint: 'border', defaultJunctionPoint: 'border',
}, },
], ],
@@ -411,6 +458,29 @@ export default {
errorMessageShow: false, errorMessageShow: false,
errorMessage: '', errorMessage: '',
nodeStyle: {
'0': {
backgroundColor: '#2F54EB'
},
'1': {
backgroundColor: '#29AAE1'
},
'2': {
backgroundColor: '#7F97FA'
},
'3': {
backgroundColor: '##75C5CA'
},
'4': {
backgroundColor: '#A699F6'
},
'5': {
backgroundColor: '#A4B5E1'
}
}, // 拓扑图节点分级别样式
topoViewJsonData: {}, // 拓扑图 JSON 数据
topoViewOption: {}, // 拓扑图配置数据 子节点分页
topoViewSearchValue: '', // 拓扑图搜索
} }
}, },
provide() { provide() {
@@ -485,6 +555,30 @@ export default {
: {} : {}
}, },
}, },
watch: {
'$i18n.locale': {
immediate: true,
handler(newVal) {
this.changeTopoViewToolbarLang(newVal)
},
},
isShowPreview: {
immediate: true,
handler(newVal) {
if (newVal) {
this.changeTopoViewToolbarLang(this.$i18n.locale)
}
},
},
drawerVisible: {
immediate: true,
handler(newVal) {
if (newVal) {
this.changeTopoViewToolbarLang(this.$i18n.locale)
}
},
}
},
methods: { methods: {
closeNodeTips(e) { closeNodeTips(e) {
e.preventDefault() e.preventDefault()
@@ -578,7 +672,7 @@ export default {
payload.view_ids = g.views.map(i => i.id) payload.view_ids = g.views.map(i => i.id)
} }
if (groupId) { if (groupId) {
putTopoGroupByGId(groupId, { view_ids: g.views.map((i) => i.id) }) putTopoGroupByGId(groupId, { view_ids: payload.view_ids })
.then(() => { .then(() => {
this.$message.success(that.$t('saveSuccess')) this.$message.success(that.$t('saveSuccess'))
}) })
@@ -729,12 +823,21 @@ export default {
disableDefaultClickEffect: true, disableDefaultClickEffect: true,
}) })
}) })
const type2meta = res?.type2meta
res.nodes.forEach(item => { res.nodes.forEach(item => {
const icon = type2meta?.[item?.type_id] || ''
nodes.push({ nodes.push({
id: `${item.id}`, id: `${item.id}`,
text: item.name, text: item.name,
nodeShape: 1, nodeShape: 1,
borderWidth: -1, borderWidth: -1,
color: 'transparent',
styleClass: {
padding: '0px'
},
data: {
icon
},
disableDefaultClickEffect: true, disableDefaultClickEffect: true,
}) })
}) })
@@ -752,11 +855,15 @@ export default {
} }
}) })
}, },
showTopoView(viewId) { async showTopoView(viewId) {
if (viewId === 'null' || !viewId) { if (viewId === 'null' || !viewId) {
return return
} }
showTopoView(viewId).then(res => { const topoViewRes = await getTopoView(viewId)
if (topoViewRes?.option) {
this.topoViewOption = topoViewRes.option
}
showTopoView(viewId).then(async res => {
const nodes = [] const nodes = []
const links = [] const links = []
this.currentNodes = res.nodes this.currentNodes = res.nodes
@@ -768,11 +875,21 @@ export default {
disableDefaultClickEffect: false, disableDefaultClickEffect: false,
}) })
}) })
const type2meta = res?.type2meta
res.nodes.forEach(item => { res.nodes.forEach(item => {
const icon = type2meta?.[item?.type_id] || ''
nodes.push({ nodes.push({
id: `${item.id}`, id: `${item.id}`,
text: item.name, text: item.name,
data: {}, color: 'transparent',
styleClass: {
padding: '0px'
},
data: {
icon
},
isHide: false,
opacity: 1,
}) })
}) })
const _graphJsonData = { const _graphJsonData = {
@@ -783,11 +900,132 @@ export default {
this.$message.error(this.$t('cmdb.topo.noData')) this.$message.error(this.$t('cmdb.topo.noData'))
return return
} }
// this.$nextTick(() => { this.$refs.showTopoView.setJsonData(_.cloneDeep(_graphJsonData), async () => {
this.$refs.showTopoView.setJsonData(_graphJsonData) this.topoViewSearchValue = ''
// })
// map 结构存储 节点
const nodeMap = _graphJsonData.nodes.reduce((map, node) => {
map.set(node.id, node)
return map
}, new Map())
_graphJsonData.nodes = nodeMap
if (this?.topoViewOption?.aggregation_count) {
const instance = this.$refs.showTopoView.getInstance()
const nodes = instance.getNodes()
const rootNodes = nodes.filter((node) => node.lot.level === 0)
rootNodes.forEach((node) => {
this.initMoreNodesData(node, _graphJsonData)
})
this.$refs.showTopoView.setJsonData(_.cloneDeep({
nodes: _graphJsonData.nodes.values(),
links: _graphJsonData.links
}))
}
this.topoViewJsonData = _graphJsonData
this.changeTopoViewToolbarLang(this.$i18n.locale)
})
}) })
}, },
/**
* 初始化子节点分页数据
*/
initMoreNodesData(node, jsonData) {
const childs = node.lot.childs
// 没有子节点 终止遍历
if (!childs?.length) {
return
}
// 子节点分页数量
const aggregation_count = this?.topoViewOption?.aggregation_count || 1
// 展示节点数量
let showNodeCount = 0
childs.forEach((childNode, index) => {
if (childNode?.data?.btnType !== 'more') {
if (showNodeCount >= aggregation_count) {
const originNode = jsonData?.nodes?.get(childNode.id)
if (originNode) {
originNode.isHide = true
}
} else if (!childNode.isHide) {
showNodeCount++
}
this.initMoreNodesData(childNode, jsonData)
}
})
if (childs.length - showNodeCount > 0) {
const id = uuidv4()
jsonData.nodes.set(id, {
id,
text: childs.length - showNodeCount,
data: {
btnType: 'more'
},
color: 'transparent',
styleClass: {
padding: '0px'
},
})
jsonData.links.push({
from: node.id,
to: id,
})
}
},
async clickMoreBtn(node) {
const childs = node?.lot?.parent?.lot?.childs
if (childs?.length) {
const topoViewJsonData = this.topoViewJsonData
let moreBtnNode = null
let showNodeCount = 0
let toggleNodeCount = 0
const aggregation_count = this?.topoViewOption?.aggregation_count || 1
childs.forEach((child) => {
if (!child.isHide) {
showNodeCount++
}
if (toggleNodeCount < aggregation_count && child.isHide) {
const childNode = topoViewJsonData?.nodes?.get(child.id)
if (childNode) {
childNode.isHide = false
toggleNodeCount++
showNodeCount++
}
}
if (child.data.btnType === 'more') {
moreBtnNode = topoViewJsonData?.nodes?.get(child.id)
}
})
if (moreBtnNode) {
if (showNodeCount === childs.length) {
moreBtnNode.isHide = true
} else {
moreBtnNode.text = childs.length - showNodeCount
}
}
const instance = this.$refs.showTopoView.getInstance()
instance.setJsonData(
{
links: topoViewJsonData.links,
nodes: topoViewJsonData.nodes.values()
},
false
)
this.topoViewJsonData = topoViewJsonData
}
},
handleOpenCmdb() { handleOpenCmdb() {
this.$refs.cmdbDrawer.open() this.$refs.cmdbDrawer.open()
}, },
@@ -921,9 +1159,16 @@ export default {
} }
}, },
async showNodeTips(nodeObject, $event) { async showNodeTips(nodeObject, $event) {
console.log('node click') console.log('node click', nodeObject)
$event.preventDefault() $event.preventDefault()
$event.stopPropagation() $event.stopPropagation()
const btnType = nodeObject?.data?.btnType
if (btnType === 'more') {
this.clickMoreBtn(nodeObject)
return
}
const _base_position = this.$refs.showTopoView.getInstance().options.fullscreen ? { x: 0, y: 0 } : this.$refs.rightTopoView.getBoundingClientRect() const _base_position = this.$refs.showTopoView.getInstance().options.fullscreen ? { x: 0, y: 0 } : this.$refs.rightTopoView.getBoundingClientRect()
if (this.currentNode !== nodeObject) { if (this.currentNode !== nodeObject) {
this.currentNodeValues = null this.currentNodeValues = null
@@ -934,25 +1179,16 @@ export default {
const [ attributes ] = await Promise.all([getSubscribeAttributes(rawNode.type_id)]) const [ attributes ] = await Promise.all([getSubscribeAttributes(rawNode.type_id)])
this.currentNodeAttributes = attributes?.attributes || [] this.currentNodeAttributes = attributes?.attributes || []
if (!this.currentNodeAttributes.length) { if (!this.currentNodeAttributes.length) {
this.errorMessage = this.$t('cmdb.topo.noPreferenceAttributes') this.handleNullNodeTips(this.$t('cmdb.topo.noPreferenceAttributes'))
this.errorMessageShow = true
this.currentNodeValues = null
this.isShowNodeTipsPanel = false
} }
await searchCI({ q: `_id:${rawNode.id}` }, false).then(res => { await searchCI({ q: `_id:${rawNode.id}` }, false).then(res => {
if (!res.result.length) { if (!res.result.length) {
this.errorMessage = this.$t('cmdb.topo.noInstancePerm') this.handleNullNodeTips(this.$t('cmdb.topo.noInstancePerm'))
this.errorMessageShow = true
this.currentNodeValues = null
this.isShowNodeTipsPanel = false
} else { } else {
this.currentNodeValues = res.result[0] this.currentNodeValues = res.result[0]
} }
}).catch(error => { }).catch(error => {
this.errorMessage = ((error.response || {}).data || {}).message this.handleNullNodeTips(((error.response || {}).data || {}).message)
this.errorMessageShow = true
this.currentNodeValues = null
this.isShowNodeTipsPanel = false
}) })
} }
} }
@@ -971,9 +1207,74 @@ export default {
this.isShowNodeTipsPanel = true this.isShowNodeTipsPanel = true
console.log(this.nodeTipsPosition) console.log(this.nodeTipsPosition)
}, },
handleNullNodeTips(errorMessage) {
this.errorMessage = errorMessage
this.errorMessageShow = true
this.currentNodeValues = null
this.isShowNodeTipsPanel = false
this.currentNode = {}
},
hideNodeTips(nodeObject, $event) { hideNodeTips(nodeObject, $event) {
this.isShowNodeTipsPanel = false this.isShowNodeTipsPanel = false
}, },
handleSearchTopoView(v) {
const topoViewJsonData = this.topoViewJsonData
topoViewJsonData.nodes.keys().forEach((key) => {
const node = topoViewJsonData?.nodes?.get(key)
if (node?.data?.btnType !== 'more') {
node.opacity = node?.text?.indexOf(v) !== -1 ? 1 : 0.1
}
})
const instance = this.$refs.showTopoView.getInstance()
instance.setJsonData(
{
links: topoViewJsonData.links,
nodes: topoViewJsonData.nodes.values()
},
false
)
this.topoViewJsonData = topoViewJsonData
},
changeTopoViewToolbarLang(lang) {
setTimeout(() => {
const toolbarElements = document.getElementsByClassName('rel-toolbar')
const zhlangMap = {
'全屏/退出全屏': 'Full Screen/Exit Full Screen',
'放大': 'zoom in',
'缩小': 'zoom out',
'刷新': 'refresh ',
'下载图片': 'download image'
}
const enlangMap = {
'Full Screen/Exit Full Screen': '全屏/退出全屏',
'zoom in': '放大',
'zoom out': '缩小',
'refresh': '刷新 ',
'download image': '下载图片'
}
const toolbarElementArray = Array.from(toolbarElements ?? [])
toolbarElementArray.forEach((toolbarElement) => {
const childArray = Array.from(toolbarElement?.children || [])
if (childArray?.length) {
childArray.forEach((node) => {
const oldTitle = node?.getAttribute('title')
if (oldTitle) {
const newTitle = lang === 'en' ? zhlangMap[oldTitle] : enlangMap[oldTitle]
if (newTitle) {
node.setAttribute('title', newTitle)
}
}
})
}
})
}, 300)
},
}, },
} }
</script> </script>
@@ -1098,6 +1399,14 @@ export default {
top: 40%; top: 40%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
} }
.relation-graph-search {
position: absolute;
z-index: 10;
top: 20px;
left: 20px;
width: 300px;
}
} }
.topo-left, .topo-left,
.topo-right { .topo-right {
@@ -1133,6 +1442,54 @@ export default {
border-radius: 8px; border-radius: 8px;
} }
} }
.relation-graph-node {
padding: 6px 3px;
border-radius: 2px;
border-width: 2px;
border-style: solid;
background-color: transparent;
position: relative !important;
display: flex;
justify-content: center;
align-items: center;
&-text {
color: #000000;
font-size: 12px;
font-weight: 400;
margin-left: 6px;
word-break: break-all;
}
&-icon {
font-size: 12px;
color: rgba(0, 0, 0, 0.65);
}
&-image {
max-height: 20px;
max-width: 20px;
}
}
/deep/ .relation-graph {
background-color: #FFFFFF;
.rel-node {
padding: 0px;
height: auto !important;
}
.rel-node-checked {
box-shadow: none;
}
.c-expanded {
background-color: rgb(64, 158, 255) !important;
}
.c-collapsed {
background-color: rgb(64, 158, 255) !important;
}
}
</style> </style>
<style lang="less"> <style lang="less">

View File

@@ -172,10 +172,14 @@
@edit-closed="handleEditClose" @edit-closed="handleEditClose"
@edit-actived="handleEditActived" @edit-actived="handleEditActived"
:edit-config="{ trigger: 'dblclick', mode: 'row', showIcon: false }" :edit-config="{ trigger: 'dblclick', mode: 'row', showIcon: false }"
class="ops-unstripe-table" class="ops-unstripe-table checkbox-hover-table"
:custom-config="{ storage: true }" :custom-config="{ storage: true }"
> >
<vxe-column align="center" type="checkbox" width="60" :fixed="isCheckboxFixed ? 'left' : ''"></vxe-column> <vxe-column align="center" type="checkbox" width="60" :fixed="isCheckboxFixed ? 'left' : ''">
<template #default="{row}">
{{ getRowSeq(row) }}
</template>
</vxe-column>
<vxe-table-column <vxe-table-column
v-for="(col, index) in columns" v-for="(col, index) in columns"
:key="`${col.field}_${index}`" :key="`${col.field}_${index}`"
@@ -1229,6 +1233,9 @@ export default {
} }
) )
}, },
getRowSeq(row) {
return this.$refs.xTable.getVxetableRef().getRowSeq(row)
}
}, },
} }
</script> </script>
@@ -1337,6 +1344,36 @@ export default {
overflow: auto; overflow: auto;
width: 100%; width: 100%;
border-radius: @border-radius-box; border-radius: @border-radius-box;
.checkbox-hover-table {
.vxe-table--body-wrapper {
.vxe-checkbox--label {
display: inline;
padding-left: 0px !important;
color: #bfbfbf;
}
.vxe-icon-checkbox-unchecked {
display: none;
}
.vxe-icon-checkbox-checked ~ .vxe-checkbox--label {
display: none;
}
.vxe-cell--checkbox {
&:hover {
.vxe-icon-checkbox-unchecked {
display: inline;
}
.vxe-checkbox--label {
display: none;
}
}
}
}
}
} }
} }
</style> </style>

View File

@@ -11,6 +11,8 @@
@primary-color_7: #f7f8fa; @primary-color_7: #f7f8fa;
@primary-color_8: #b1c9ff; @primary-color_8: #b1c9ff;
@link-color: @primary-color;
// Neutral color // Neutral color
@text-color_1: #1d2129; @text-color_1: #1d2129;
@text-color_2: #4e5969; @text-color_2: #4e5969;

View File

@@ -1,5 +1,3 @@
version: '2.19'
services: services:
cmdb-db: cmdb-db:
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-db:2.3 image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-db:2.3
@@ -43,7 +41,7 @@ services:
- redis - redis
cmdb-api: cmdb-api:
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.4.5 image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.4.7
container_name: cmdb-api container_name: cmdb-api
env_file: env_file:
- .env - .env
@@ -73,17 +71,24 @@ services:
flask cmdb-init-acl flask cmdb-init-acl
flask init-import-user-from-acl flask init-import-user-from-acl
flask init-department flask init-department
flask cmdb-patch -v 2.4.7
flask cmdb-counter > counter.log 2>&1 flask cmdb-counter > counter.log 2>&1
networks: networks:
new: new:
aliases: aliases:
- cmdb-api - cmdb-api
healthcheck:
timeout: 3s
interval: 5s
retries: 10
test: "ps aux|grep -v grep|grep -v '1 root'|grep gunicorn || exit 1"
cmdb-ui: cmdb-ui:
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.4.5 image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.4.7
container_name: cmdb-ui container_name: cmdb-ui
depends_on: depends_on:
- cmdb-api cmdb-api:
condition: service_healthy
environment: environment:
TZ: Asia/Shanghai TZ: Asia/Shanghai
CMDB_API_HOST: cmdb-api:5000 CMDB_API_HOST: cmdb-api:5000

25
docker/Dockerfile-API Normal file
View File

@@ -0,0 +1,25 @@
# ================================= API ================================
FROM python:3.8-alpine AS cmdb-api
LABEL description="Python3.8,cmdb"
COPY cmdb-api /data/apps/cmdb
WORKDIR /data/apps/cmdb
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
RUN apk add --no-cache tzdata gcc musl-dev libffi-dev openldap-dev python3-dev jpeg-dev zlib-dev build-base
ENV TZ=Asia/Shanghai
RUN pip install --no-cache-dir -r requirements.txt \
&& cp ./settings.example.py settings.py \
&& sed -i "s#{user}:{password}@127.0.0.1:3306/{db}#cmdb:123456@mysql:3306/cmdb#g" settings.py \
&& sed -i "s#redis://127.0.0.1#redis://redis#g" settings.py \
&& sed -i "s#CACHE_REDIS_HOST = '127.0.0.1'#CACHE_REDIS_HOST = 'redis'#g" settings.py
ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.7.3/wait /wait
RUN chmod +x /wait
CMD ["bash", "-c", "flask run"]

16
docker/Dockerfile-UI Normal file
View File

@@ -0,0 +1,16 @@
# ================================= UI ================================
FROM node:16.20 AS builder
LABEL description="cmdb-ui"
COPY cmdb-ui /data/apps/cmdb-ui
WORKDIR /data/apps/cmdb-ui
RUN sed -i "s#http://127.0.0.1:5000##g" .env && yarn install --ignore-engines --network-timeout 1000000 && yarn build
FROM nginx:alpine AS cmdb-ui
RUN mkdir /etc/nginx/html && rm -f /etc/nginx/conf.d/default.conf
COPY --from=builder /data/apps/cmdb-ui/dist /etc/nginx/html/

View File

@@ -1,48 +0,0 @@
# ================================= UI ================================
FROM node:16.0.0-alpine AS builder
LABEL description="cmdb-ui"
COPY ../cmdb-ui /data/apps/cmdb-ui
WORKDIR /data/apps/cmdb-ui
RUN sed -i "s#http://127.0.0.1:5000##g" .env && yarn install && yarn build
FROM nginx:alpine AS cmdb-ui
RUN mkdir /etc/nginx/html && rm -f /etc/nginx/conf.d/default.conf
COPY --from=builder /data/apps/cmdb-ui/dist /etc/nginx/html/
# ================================= API ================================
FROM python:3.8-alpine AS cmdb-api
LABEL description="Python3.8,cmdb"
COPY ../cmdb-api /data/apps/cmdb
WORKDIR /data/apps/cmdb
RUN apk add --no-cache tzdata gcc musl-dev libffi-dev openldap-dev python3-dev jpeg-dev zlib-dev build-base
ENV TZ=Asia/Shanghai
RUN pip install --no-cache-dir -r requirements.txt \
&& cp ./settings.example.py settings.py \
&& sed -i "s#{user}:{password}@127.0.0.1:3306/{db}#cmdb:123456@mysql:3306/cmdb#g" settings.py \
&& sed -i "s#redis://127.0.0.1#redis://redis#g" settings.py \
&& sed -i "s#CACHE_REDIS_HOST = '127.0.0.1'#CACHE_REDIS_HOST = 'redis'#g" settings.py
ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.7.3/wait /wait
RUN chmod +x /wait
CMD ["bash", "-c", "flask run"]
# ================================= Search ================================
FROM docker.elastic.co/elasticsearch/elasticsearch:7.4.2 AS cmdb-search
RUN yes | ./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.4.2/elasticsearch-analysis-ik-7.4.2.zip