Compare commits

..

2 Commits
2.3.3 ... 2.3.1

Author SHA1 Message Date
pycook
1f48b18e14 version: 2.3.1 2023-08-19 12:43:55 +08:00
pycook
b73822bf26 lint 2023-08-19 11:04:06 +08:00
112 changed files with 881 additions and 3656 deletions

1
.gitignore vendored
View File

@@ -39,7 +39,6 @@ pip-log.txt
nosetests.xml
.pytest_cache
cmdb-api/test-output
cmdb-api/api/uploaded_files
# Translations
*.mo

View File

@@ -3,7 +3,7 @@ FROM node:16.0.0-alpine AS builder
LABEL description="cmdb-ui"
COPY ../cmdb-ui /data/apps/cmdb-ui
COPY cmdb-ui /data/apps/cmdb-ui
WORKDIR /data/apps/cmdb-ui
@@ -22,7 +22,7 @@ FROM python:3.8-alpine AS cmdb-api
LABEL description="Python3.8,cmdb"
COPY ../cmdb-api /data/apps/cmdb
COPY cmdb-api /data/apps/cmdb
WORKDIR /data/apps/cmdb

View File

@@ -1,52 +1,37 @@
MYSQL_ROOT_PASSWORD ?= root
MYSQL_PORT ?= 3306
REDIS_PORT ?= 6379
.PHONY: env clean api ui worker
default: help
help: ## display this help
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
.PHONY: help
help:
@echo " env create a development environment using pipenv"
@echo " deps install dependencies using pip"
@echo " clean remove unwanted files like .pyc's"
@echo " lint check style with flake8"
@echo " api start api server"
@echo " ui start ui server"
@echo " worker start async tasks worker"
env: ## create a development environment using pipenv
env:
sudo easy_install pip && \
pip install pipenv -i https://pypi.douban.com/simple && \
npm install yarn && \
make deps
.PHONY: env
docker-mysql: ## deploy MySQL use docker
@docker run --name mysql -p ${MYSQL_PORT}:3306 -e MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD} -d mysql:latest
.PHONY: docker-mysql
docker-redis: ## deploy Redis use docker
@docker run --name redis -p ${REDIS_PORT}:6379 -d redis:latest
.PHONY: docker-redis
deps: ## install dependencies using pip
cd cmdb-api && \
deps:
pipenv install --dev && \
pipenv run flask db-setup && \
pipenv run flask cmdb-init-cache && \
cd .. && \
cd cmdb-ui && yarn install && cd ..
.PHONY: deps
api: ## start api server
api:
cd cmdb-api && pipenv run flask run -h 0.0.0.0
.PHONY: api
worker: ## start async tasks worker
worker:
cd cmdb-api && pipenv run celery -A celery_worker.celery worker -E -Q one_cmdb_async --concurrency=1 -D && pipenv run celery -A celery_worker.celery worker -E -Q acl_async --concurrency=1 -D
.PHONY: worker
ui: ## start ui server
ui:
cd cmdb-ui && yarn run serve
.PHONY: ui
clean: ## remove unwanted files like .pyc's
clean:
pipenv run flask clean
.PHONY: clean
lint: ## check style with flake8
lint:
flake8 --exclude=env .
.PHONY: lint

View File

@@ -4,8 +4,8 @@
[![UI](https://img.shields.io/badge/UI-Ant%20Design%20Pro%20Vue-brightgreen)](https://github.com/sendya/ant-design-pro-vue)
[![API](https://img.shields.io/badge/API-Flask-brightgreen)](https://github.com/pallets/flask)
[English](docs/README_en.md) / [中文](README.md)
- 产品文档https://veops.cn/docs/
[English](README_en.md) / [中文](README.md)
- 在线体验: <a href="https://cmdb.veops.cn" target="_blank">CMDB</a>
- username: demo 或者 admin
- password: 123456

View File

@@ -1,13 +1,13 @@
![维易CMDB](images/logo.png)
![维易CMDB](docs/images/logo.png)
[![License](https://img.shields.io/badge/License-AGPLv3-brightgreen)](https://github.com/veops/cmdb/blob/master/LICENSE)
[![UI](https://img.shields.io/badge/UI-Ant%20Design%20Pro%20Vue-brightgreen)](https://github.com/sendya/ant-design-pro-vue)
[![API](https://img.shields.io/badge/API-Flask-brightgreen)](https://github.com/pallets/flask)
[English](README_en.md) / [中文](../README.md)
[English](README_en.md) / [中文](README.md)
## DEMO ONLINE
- Product documenthttps://veops.cn/docs/
- Preview online: <a href="https://cmdb.veops.cn" target="_blank">CMDB</a>
- username: demo
- password: 123456
@@ -18,7 +18,7 @@
### Technical Architecture
<img src=images/view.jpg />
<img src=docs/images/view.jpg />
### Document
@@ -51,9 +51,9 @@
### System Overview
- Service Tree
![1](images/0.png "首页展示")
![1](docs/images/0.png "首页展示")
[View more screenshots](screenshot.md)
[View more screenshots](docs/screenshot.md)
### More Features
@@ -73,9 +73,9 @@
- password: 123456
### [Local Setup](local_en.md)
### [Local Setup](docs/local_en.md)
### [Installation with Makefile](makefile_en.md)
### [Installation with Makefile](docs/makefile_en.md)
## Contributing
@@ -89,4 +89,4 @@
_**Welcome to pay attention to our public account, click to contact us, join WeChat, QQ operation and maintenance group, and get more product and industry related information**_
![QQgroup](images/qrcode_for_gzh.jpg)
![QQgroup](docs/images/qrcode_for_gzh.jpg)

View File

@@ -6,14 +6,13 @@ import logging
import os
import sys
from inspect import getmembers
from flask.json.provider import DefaultJSONProvider
from logging.handlers import RotatingFileHandler
from flask import Flask
from flask import jsonify
from flask import make_response
from flask import jsonify, make_response
from flask.blueprints import Blueprint
from flask.cli import click
from flask.json.provider import DefaultJSONProvider
import api.views.entry
from api.extensions import (bcrypt, cache, celery, cors, db, es, login_manager, migrate, rd)
@@ -22,6 +21,7 @@ from api.models.acl import User
HERE = os.path.abspath(os.path.dirname(__file__))
PROJECT_ROOT = os.path.join(HERE, os.pardir)
API_PACKAGE = "api"
@login_manager.user_loader
@@ -174,8 +174,9 @@ def register_commands(app):
for root, _, files in os.walk(os.path.join(HERE, "commands")):
for filename in files:
if not filename.startswith("_") and filename.endswith("py"):
if root not in sys.path:
sys.path.insert(1, root)
module_path = os.path.join(API_PACKAGE, root[root.index("commands"):])
if module_path not in sys.path:
sys.path.insert(1, module_path)
command = __import__(os.path.splitext(filename)[0])
func_list = [o[0] for o in getmembers(command) if isinstance(o[1], click.core.Command)]
for func_name in func_list:

View File

@@ -9,7 +9,6 @@ import time
import click
from flask import current_app
from flask.cli import with_appcontext
from flask_login import login_user
import api.lib.cmdb.ci
from api.extensions import db
@@ -25,7 +24,6 @@ from api.lib.cmdb.const import ValueTypeEnum
from api.lib.exception import AbortException
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.cache import AppCache
from api.lib.perm.acl.cache import UserCache
from api.lib.perm.acl.resource import ResourceCRUD
from api.lib.perm.acl.resource import ResourceTypeCRUD
from api.lib.perm.acl.role import RoleCRUD
@@ -209,8 +207,6 @@ def cmdb_counter():
"""
from api.lib.cmdb.cache import CMDBCounterCache
current_app.test_request_context().push()
login_user(UserCache.get('worker'))
while True:
try:
db.session.remove()

View File

@@ -161,55 +161,6 @@ class InitDepartment(object):
info = f"update department acl_rid: {acl_rid}"
current_app.logger.info(info)
def init_backend_resource(self):
acl = self.check_app('backend')
resources_types = acl.get_all_resources_types()
results = list(filter(lambda t: t['name'] == '操作权限', resources_types['groups']))
if len(results) == 0:
payload = dict(
app_id=acl.app_name,
name='操作权限',
description='',
perms=['read', 'grant', 'delete', 'update']
)
resource_type = acl.create_resources_type(payload)
else:
resource_type = results[0]
for name in ['公司信息']:
payload = dict(
type_id=resource_type['id'],
app_id=acl.app_name,
name=name,
)
try:
acl.create_resource(payload)
except Exception as e:
if '已经存在' in str(e):
pass
else:
raise Exception(e)
def check_app(self, app_name):
acl = ACLManager(app_name)
payload = dict(
name=app_name,
description=app_name
)
try:
app = acl.validate_app()
if app:
return acl
acl.create_app(payload)
except Exception as e:
current_app.logger.error(e)
if '不存在' in str(e):
acl.create_app(payload)
return acl
raise Exception(e)
@click.command()
@with_appcontext
@@ -226,7 +177,5 @@ def init_department():
"""
Department initialization
"""
cli = InitDepartment()
cli.init_wide_company()
cli.create_acl_role_with_department()
cli.init_backend_resource()
InitDepartment().init()
InitDepartment().create_acl_role_with_department()

View File

@@ -8,14 +8,9 @@ from flask_login import current_user
from api.extensions import db
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeAttributesCache
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.const import BUILTIN_KEYWORDS
from api.lib.cmdb.const import CITypeOperateType
from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.const import PermEnum, ResourceTypeEnum, RoleEnum
from api.lib.cmdb.const import ValueTypeEnum
from api.lib.cmdb.history import CITypeHistoryManager
from api.lib.cmdb.resp_format import ErrFormat
@@ -45,7 +40,7 @@ class AttributeManager(object):
ret_key = choice_web_hook.get('ret_key')
headers = choice_web_hook.get('headers') or {}
payload = choice_web_hook.get('payload') or {}
method = (choice_web_hook.get('method') or 'GET').lower()
method = choice_web_hook.get('method', 'GET').lower()
try:
res = getattr(requests, method)(url, headers=headers, data=payload).json()
@@ -60,17 +55,15 @@ class AttributeManager(object):
return [[i, {}] for i in (res.get(ret_key_list[-1]) or [])]
except Exception as e:
current_app.logger.error("get choice values failed: {}".format(e))
current_app.logger.error(str(e))
return []
@classmethod
def get_choice_values(cls, attr_id, value_type, choice_web_hook, choice_web_hook_parse=True):
if choice_web_hook:
if choice_web_hook_parse:
if isinstance(choice_web_hook, dict):
return cls._get_choice_values_from_web_hook(choice_web_hook)
else:
return []
if choice_web_hook and isinstance(choice_web_hook, dict) and choice_web_hook_parse:
return cls._get_choice_values_from_web_hook(choice_web_hook)
elif choice_web_hook and not choice_web_hook_parse:
return []
choice_table = ValueTypeMap.choice.get(value_type)
choice_values = choice_table.get_by(fl=["value", "option"], attr_id=attr_id)
@@ -80,34 +73,34 @@ class AttributeManager(object):
@staticmethod
def add_choice_values(_id, value_type, choice_values):
choice_table = ValueTypeMap.choice.get(value_type)
if choice_table is None:
return
choice_table.get_by(attr_id=_id, only_query=True).delete()
db.session.query(choice_table).filter(choice_table.attr_id == _id).delete()
db.session.flush()
choice_values = choice_values
for v, option in choice_values:
choice_table.create(attr_id=_id, value=v, option=option, commit=False)
table = choice_table(attr_id=_id, value=v, option=option)
db.session.add(table)
try:
db.session.flush()
except Exception as e:
current_app.logger.warning("add choice values failed: {}".format(e))
except:
return abort(400, ErrFormat.invalid_choice_values)
@staticmethod
def _del_choice_values(_id, value_type):
choice_table = ValueTypeMap.choice.get(value_type)
choice_table and choice_table.get_by(attr_id=_id, only_query=True).delete()
db.session.query(choice_table).filter(choice_table.attr_id == _id).delete()
db.session.flush()
@classmethod
def search_attributes(cls, name=None, alias=None, page=1, page_size=None):
"""
:param name:
:param alias:
:param page:
:param page_size:
:param name:
:param alias:
:param page:
:param page_size:
:return: attribute, if name is None, then return all attributes
"""
if name is not None:
@@ -121,8 +114,8 @@ class AttributeManager(object):
attrs = attrs[(page - 1) * page_size:][:page_size]
res = list()
for attr in attrs:
attr["is_choice"] and attr.update(
dict(choice_value=cls.get_choice_values(attr["id"], attr["value_type"], attr["choice_web_hook"])))
attr["is_choice"] and attr.update(dict(choice_value=cls.get_choice_values(
attr["id"], attr["value_type"], attr["choice_web_hook"])))
attr['is_choice'] and attr.pop('choice_web_hook', None)
res.append(attr)
@@ -131,31 +124,30 @@ class AttributeManager(object):
def get_attribute_by_name(self, name):
attr = Attribute.get_by(name=name, first=True)
if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"], attr["choice_web_hook"])
if attr and attr["is_choice"]:
attr.update(dict(choice_value=self.get_choice_values(
attr["id"], attr["value_type"], attr["choice_web_hook"])))
return attr
def get_attribute_by_alias(self, alias):
attr = Attribute.get_by(alias=alias, first=True)
if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"], attr["choice_web_hook"])
if attr and attr["is_choice"]:
attr.update(dict(choice_value=self.get_choice_values(
attr["id"], attr["value_type"], attr["choice_web_hook"])))
return attr
def get_attribute_by_id(self, _id):
attr = Attribute.get_by_id(_id).to_dict()
if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values(attr["id"], attr["value_type"], attr["choice_web_hook"])
if attr and attr["is_choice"]:
attr.update(dict(choice_value=self.get_choice_values(
attr["id"], attr["value_type"], attr["choice_web_hook"])))
return attr
def get_attribute(self, key, choice_web_hook_parse=True):
attr = AttributeCache.get(key).to_dict()
if attr.get("is_choice"):
attr["choice_value"] = self.get_choice_values(
attr["id"], attr["value_type"], attr["choice_web_hook"], choice_web_hook_parse=choice_web_hook_parse)
if attr and attr["is_choice"]:
attr.update(dict(choice_value=self.get_choice_values(
attr["id"], attr["value_type"], attr["choice_web_hook"])), choice_web_hook_parse=choice_web_hook_parse)
return attr
@staticmethod
@@ -163,19 +155,6 @@ class AttributeManager(object):
if RoleEnum.CONFIG not in session.get("acl", {}).get("parentRoles", []) and not is_app_admin('cmdb'):
return abort(403, ErrFormat.role_required.format(RoleEnum.CONFIG))
@classmethod
def calc_computed_attribute(cls, attr_id):
"""
calculate computed attribute for all ci
:param attr_id:
:return:
"""
cls.can_create_computed_attribute()
from api.tasks.cmdb import calc_computed_attribute
calc_computed_attribute.apply_async(args=(attr_id, current_user.uid), queue=CMDB_QUEUE)
@classmethod
@kwargs_required("name")
def add(cls, **kwargs):
@@ -184,9 +163,8 @@ class AttributeManager(object):
is_choice = True if choice_value or kwargs.get('choice_web_hook') else False
name = kwargs.pop("name")
if name in BUILTIN_KEYWORDS:
if name in {'id', '_id', 'ci_id', 'type', '_type', 'ci_type'}:
return abort(400, ErrFormat.attribute_name_cannot_be_builtin)
alias = kwargs.pop("alias", "")
alias = name if not alias else alias
Attribute.get_by(name=name, first=True) and abort(400, ErrFormat.attribute_name_duplicate.format(name))
@@ -234,11 +212,6 @@ class AttributeManager(object):
return attr.id
@staticmethod
def _clean_ci_type_attributes_cache(attr_id):
for i in CITypeAttribute.get_by(attr_id=attr_id, to_dict=False):
CITypeAttributesCache.clean(i.type_id)
@staticmethod
def _change_index(attr, old, new):
from api.lib.cmdb.utils import TableMap
@@ -249,11 +222,11 @@ class AttributeManager(object):
new_table = TableMap(attr=attr, is_index=new).table
ci_ids = []
for i in old_table.get_by(attr_id=attr.id, to_dict=False):
for i in db.session.query(old_table).filter(getattr(old_table, 'attr_id') == attr.id):
new_table.create(ci_id=i.ci_id, attr_id=attr.id, value=i.value, flush=True)
ci_ids.append(i.ci_id)
old_table.get_by(attr_id=attr.id, only_query=True).delete()
db.session.query(old_table).filter(getattr(old_table, 'attr_id') == attr.id).delete()
try:
db.session.commit()
@@ -318,7 +291,7 @@ class AttributeManager(object):
if is_choice and choice_value:
self.add_choice_values(attr.id, attr.value_type, choice_value)
elif existed2['is_choice']:
elif is_choice:
self._del_choice_values(attr.id, attr.value_type)
try:
@@ -337,8 +310,6 @@ class AttributeManager(object):
AttributeCache.clean(attr)
self._clean_ci_type_attributes_cache(_id)
return attr.id
@staticmethod
@@ -352,25 +323,24 @@ class AttributeManager(object):
ref = CITypeAttribute.get_by(attr_id=_id, to_dict=False, first=True)
if ref is not None:
ci_type = CITypeCache.get(ref.type_id)
return abort(400, ErrFormat.attribute_is_ref_by_type.format(ci_type and ci_type.alias or ref.type_id))
return abort(400, ErrFormat.attribute_is_ref_by_type.format(ci_type.alias))
if attr.uid != current_user.uid and not is_app_admin('cmdb'):
return abort(403, ErrFormat.cannot_delete_attribute)
if attr.is_choice:
choice_table = ValueTypeMap.choice.get(attr.value_type)
choice_table.get_by(attr_id=_id, only_query=True).delete()
attr.soft_delete()
db.session.query(choice_table).filter(choice_table.attr_id == _id).delete() # FIXME: session conflict
db.session.flush()
AttributeCache.clean(attr)
attr.soft_delete()
for i in PreferenceShowAttributes.get_by(attr_id=_id, to_dict=False):
i.soft_delete(commit=False)
i.soft_delete()
for i in CITypeAttributeGroupItem.get_by(attr_id=_id, to_dict=False):
i.soft_delete(commit=False)
db.session.commit()
i.soft_delete()
return name

View File

@@ -240,10 +240,9 @@ class AutoDiscoveryCITypeCRUD(DBMixin):
try:
response, _, _, _, _, _ = s.search()
for i in response:
if (current_user.username not in (i.get('rd_duty') or []) and
current_user.username not in (i.get('op_duty') or []) and
current_user.nickname not in (i.get('rd_duty') or []) and
current_user.nickname not in (i.get('op_duty') or [])):
if current_user.username not in (i.get('rd_duty') or []) and current_user.username not in \
(i.get('op_duty') or []) and current_user.nickname not in (i.get('rd_duty') or []) and \
current_user.nickname not in (i.get('op_duty') or []):
return abort(403, ErrFormat.adt_target_expr_no_permission.format(
i.get("{}_name".format(i.get('ci_type')))))
except SearchError as e:
@@ -454,12 +453,10 @@ class AutoDiscoveryCICRUD(DBMixin):
relation_adts = AutoDiscoveryCIType.get_by(type_id=adt.type_id, adr_id=None, to_dict=False)
for r_adt in relation_adts:
if not r_adt.relation or ci_id is None:
continue
for ad_key in r_adt.relation:
if not adc.instance.get(ad_key):
continue
cmdb_key = r_adt.relation[ad_key]
if r_adt.relation and ci_id is not None:
ad_key, cmdb_key = None, {}
for ad_key in r_adt.relation:
cmdb_key = r_adt.relation[ad_key]
query = "_type:{},{}:{}".format(cmdb_key.get('type_name'), cmdb_key.get('attr_name'),
adc.instance.get(ad_key))
s = search(query)

View File

@@ -2,11 +2,14 @@
from __future__ import unicode_literals
import requests
from flask import current_app
from api.extensions import cache
from api.extensions import db
from api.lib.cmdb.custom_dashboard import CustomDashboardManager
from api.models.cmdb import Attribute
from api.models.cmdb import CI
from api.models.cmdb import CIType
from api.models.cmdb import CITypeAttribute
from api.models.cmdb import RelationType
@@ -31,7 +34,6 @@ class AttributeCache(object):
attr = attr or Attribute.get_by(alias=key, first=True, to_dict=False)
if attr is not None:
cls.set(attr)
return attr
@classmethod
@@ -65,7 +67,6 @@ class CITypeCache(object):
ct = ct or CIType.get_by(alias=key, first=True, to_dict=False)
if ct is not None:
cls.set(ct)
return ct
@classmethod
@@ -97,7 +98,6 @@ class RelationTypeCache(object):
ct = RelationType.get_by(name=key, first=True, to_dict=False) or RelationType.get_by_id(key)
if ct is not None:
cls.set(ct)
return ct
@classmethod
@@ -133,15 +133,12 @@ class CITypeAttributesCache(object):
attrs = attrs or cache.get(cls.PREFIX_ID.format(key))
if not attrs:
attrs = CITypeAttribute.get_by(type_id=key, to_dict=False)
if not attrs:
ci_type = CIType.get_by(name=key, first=True, to_dict=False)
if ci_type is not None:
attrs = CITypeAttribute.get_by(type_id=ci_type.id, to_dict=False)
if attrs is not None:
cls.set(key, attrs)
return attrs
@classmethod
@@ -158,16 +155,13 @@ class CITypeAttributesCache(object):
attrs = attrs or cache.get(cls.PREFIX_ID2.format(key))
if not attrs:
attrs = CITypeAttribute.get_by(type_id=key, to_dict=False)
if not attrs:
ci_type = CIType.get_by(name=key, first=True, to_dict=False)
if ci_type is not None:
attrs = CITypeAttribute.get_by(type_id=ci_type.id, to_dict=False)
if attrs is not None:
attrs = [(i, AttributeCache.get(i.attr_id)) for i in attrs]
cls.set2(key, attrs)
return attrs
@classmethod
@@ -207,13 +201,13 @@ class CITypeAttributeCache(object):
@classmethod
def get(cls, type_id, attr_id):
attr = cache.get(cls.PREFIX_ID.format(type_id, attr_id))
attr = attr or cache.get(cls.PREFIX_ID.format(type_id, attr_id))
attr = attr or CITypeAttribute.get_by(type_id=type_id, attr_id=attr_id, first=True, to_dict=False)
if attr is not None:
cls.set(type_id, attr_id, attr)
if not attr:
attr = CITypeAttribute.get_by(type_id=type_id, attr_id=attr_id, first=True, to_dict=False)
if attr is not None:
cls.set(type_id, attr_id, attr)
return attr
@classmethod
@@ -247,72 +241,53 @@ class CMDBCounterCache(object):
result = {}
for custom in customs:
if custom['category'] == 0:
res = cls.sum_counter(custom)
result[custom['id']] = cls.summary_counter(custom['type_id'])
elif custom['category'] == 1:
res = cls.attribute_counter(custom)
else:
res = cls.relation_counter(custom.get('type_id'),
custom.get('level'),
custom.get('options', {}).get('filter', ''),
custom.get('options', {}).get('type_ids', ''))
if res:
result[custom['id']] = res
result[custom['id']] = cls.attribute_counter(custom['type_id'], custom['attr_id'])
elif custom['category'] == 2:
result[custom['id']] = cls.relation_counter(custom['type_id'], custom['level'])
cls.set(result)
return result
@classmethod
def update(cls, custom, flush=True):
def update(cls, custom):
result = cache.get(cls.KEY) or {}
if not result:
result = cls.reset()
if custom['category'] == 0:
res = cls.sum_counter(custom)
result[custom['id']] = cls.summary_counter(custom['type_id'])
elif custom['category'] == 1:
res = cls.attribute_counter(custom)
else:
res = cls.relation_counter(custom.get('type_id'),
custom.get('level'),
custom.get('options', {}).get('filter', ''),
custom.get('options', {}).get('type_ids', ''))
result[custom['id']] = cls.attribute_counter(custom['type_id'], custom['attr_id'])
elif custom['category'] == 2:
result[custom['id']] = cls.relation_counter(custom['type_id'], custom['level'])
if res and flush:
result[custom['id']] = res
cls.set(result)
return res
cls.set(result)
@staticmethod
def relation_counter(type_id, level, other_filer, type_ids):
from api.lib.cmdb.search.ci_relation.search import Search as RelSearch
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
def summary_counter(type_id):
return db.session.query(CI.id).filter(CI.deleted.is_(False)).filter(CI.type_id == type_id).count()
query = "_type:{}".format(type_id)
s = search(query, count=1000000)
try:
type_names, _, _, _, _, _ = s.search()
except SearchError as e:
current_app.logger.error(e)
return
@staticmethod
def relation_counter(type_id, level):
uri = current_app.config.get('CMDB_API')
type_names = requests.get("{}/ci/s?q=_type:{}&count=10000".format(uri, type_id)).json().get('result')
type_id_names = [(str(i.get('_id')), i.get(i.get('unique'))) for i in type_names]
s = RelSearch([i[0] for i in type_id_names], level, other_filer or '')
try:
stats = s.statistics(type_ids)
except SearchError as e:
current_app.logger.error(e)
return
url = "{}/ci_relations/statistics?root_ids={}&level={}".format(
uri, ','.join([i[0] for i in type_id_names]), level)
stats = requests.get(url).json()
id2name = dict(type_id_names)
type_ids = set()
for i in (stats.get('detail') or []):
for j in stats['detail'][i]:
type_ids.add(j)
for type_id in type_ids:
_type = CITypeCache.get(type_id)
id2name[type_id] = _type and _type.alias
@@ -332,94 +307,9 @@ class CMDBCounterCache(object):
return result
@staticmethod
def attribute_counter(custom):
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
custom.setdefault('options', {})
type_id = custom.get('type_id')
attr_id = custom.get('attr_id')
type_ids = custom['options'].get('type_ids') or (type_id and [type_id])
attr_ids = list(map(str, custom['options'].get('attr_ids') or (attr_id and [attr_id])))
other_filter = custom['options'].get('filter')
other_filter = "({})".format(other_filter) if other_filter else ''
if custom['options'].get('ret') == 'cis':
query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter)
s = search(query, fl=attr_ids, ret_key='alias', count=100)
try:
cis, _, _, _, _, _ = s.search()
except SearchError as e:
current_app.logger.error(e)
return
return cis
result = dict()
# level = 1
query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter)
s = search(query, fl=attr_ids, facet=[attr_ids[0]], count=1)
try:
_, _, _, _, _, facet = s.search()
except SearchError as e:
current_app.logger.error(e)
return
for i in (list(facet.values()) or [[]])[0]:
result[i[0]] = i[1]
if len(attr_ids) == 1:
return result
# level = 2
for v in result:
query = "_type:({}),{},{}:{}".format(";".join(map(str, type_ids)), other_filter, attr_ids[0], v)
s = search(query, fl=attr_ids, facet=[attr_ids[1]], count=1)
try:
_, _, _, _, _, facet = s.search()
except SearchError as e:
current_app.logger.error(e)
return
result[v] = dict()
for i in (list(facet.values()) or [[]])[0]:
result[v][i[0]] = i[1]
if len(attr_ids) == 2:
return result
# level = 3
for v1 in result:
if not isinstance(result[v1], dict):
continue
for v2 in result[v1]:
query = "_type:({}),{},{}:{},{}:{}".format(";".join(map(str, type_ids)), other_filter,
attr_ids[0], v1, attr_ids[1], v2)
s = search(query, fl=attr_ids, facet=[attr_ids[2]], count=1)
try:
_, _, _, _, _, facet = s.search()
except SearchError as e:
current_app.logger.error(e)
return
result[v1][v2] = dict()
for i in (list(facet.values()) or [[]])[0]:
result[v1][v2][i[0]] = i[1]
return result
@staticmethod
def sum_counter(custom):
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
custom.setdefault('options', {})
type_id = custom.get('type_id')
type_ids = custom['options'].get('type_ids') or (type_id and [type_id])
other_filter = custom['options'].get('filter') or ''
query = "_type:({}),{}".format(";".join(map(str, type_ids)), other_filter)
s = search(query, count=1)
try:
_, _, _, _, numfound, _ = s.search()
except SearchError as e:
current_app.logger.error(e)
return
return numfound
def attribute_counter(type_id, attr_id):
uri = current_app.config.get('CMDB_API')
url = "{}/ci/s?q=_type:{}&fl={}&facet={}".format(uri, type_id, attr_id, attr_id)
res = requests.get(url).json()
if res.get('facet'):
return dict([i[:2] for i in list(res.get('facet').values())[0]])

View File

@@ -40,14 +40,12 @@ from api.lib.perm.acl.acl import is_app_admin
from api.lib.perm.acl.acl import validate_permission
from api.lib.utils import Lock
from api.lib.utils import handle_arg_list
from api.models.cmdb import AutoDiscoveryCI
from api.models.cmdb import CI
from api.models.cmdb import CIRelation
from api.models.cmdb import CITypeAttribute
from api.models.cmdb import CITypeRelation
from api.tasks.cmdb import ci_cache
from api.tasks.cmdb import ci_delete
from api.tasks.cmdb import ci_relation_add
from api.tasks.cmdb import ci_relation_cache
from api.tasks.cmdb import ci_relation_delete
@@ -68,13 +66,11 @@ class CIManager(object):
@staticmethod
def get_type_name(ci_id):
ci = CI.get_by_id(ci_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(ci_id)))
return CITypeCache.get(ci.type_id).name
@staticmethod
def get_type(ci_id):
ci = CI.get_by_id(ci_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(ci_id)))
return CITypeCache.get(ci.type_id)
@staticmethod
@@ -96,7 +92,9 @@ class CIManager(object):
res = dict()
need_children and res.update(CIRelationManager.get_children(ci_id, ret_key=ret_key)) # one floor
if need_children:
children = CIRelationManager.get_children(ci_id, ret_key=ret_key) # one floor
res.update(children)
ci_type = CITypeCache.get(ci.type_id)
res["ci_type"] = ci_type.name
@@ -163,11 +161,14 @@ class CIManager(object):
ci = CI.get_by_id(ci_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(ci_id)))
valid and cls.valid_ci_only_read(ci)
if valid:
cls.valid_ci_only_read(ci)
res = dict()
need_children and res.update(CIRelationManager.get_children(ci_id, ret_key=ret_key)) # one floor
if need_children:
children = CIRelationManager.get_children(ci_id, ret_key=ret_key) # one floor
res.update(children)
ci_type = CITypeCache.get(ci.type_id)
res["ci_type"] = ci_type.name
@@ -246,7 +247,7 @@ class CIManager(object):
for i in unique_constraints:
attr_ids.extend(i.attr_ids)
attrs = [AttributeCache.get(i) for i in set(attr_ids)]
attrs = [AttributeCache.get(i) for i in list(set(attr_ids))]
id2name = {i.id: i.name for i in attrs if i}
not_existed_fields = list(set(id2name.values()) - set(ci_dict.keys()))
if not_existed_fields and ci_id is not None:
@@ -291,7 +292,7 @@ class CIManager(object):
_is_admin=False,
**ci_dict):
"""
add ci
:param ci_type_name:
:param exist_policy: replace or reject or need
:param _no_attribute_policy: ignore or reject
@@ -306,7 +307,9 @@ class CIManager(object):
unique_key = AttributeCache.get(ci_type.unique_id) or abort(
400, ErrFormat.unique_value_not_found.format("unique_id={}".format(ci_type.unique_id)))
unique_value = ci_dict.get(unique_key.name) or ci_dict.get(unique_key.alias) or ci_dict.get(unique_key.id)
unique_value = ci_dict.get(unique_key.name)
unique_value = unique_value or ci_dict.get(unique_key.alias)
unique_value = unique_value or ci_dict.get(unique_key.id)
unique_value = unique_value or abort(400, ErrFormat.unique_key_required.format(unique_key.name))
attrs = CITypeAttributesCache.get2(ci_type_name)
@@ -329,6 +332,10 @@ class CIManager(object):
if exist_policy == ExistPolicy.NEED:
return abort(404, ErrFormat.ci_not_found.format("{}={}".format(unique_key.name, unique_value)))
from api.lib.cmdb.const import L_CI
if L_CI and len(CI.get_by(type_id=ci_type.id)) > L_CI * 2:
return abort(400, ErrFormat.limit_ci.format(L_CI * 2))
limit_attrs = cls._valid_ci_for_no_read(ci, ci_type) if not _is_admin else {}
if existed is None: # set default
@@ -359,18 +366,13 @@ class CIManager(object):
cls._valid_unique_constraint(ci_type.id, ci_dict, ci and ci.id)
ref_ci_dict = dict()
for k in ci_dict:
if k.startswith("$") and "." in k:
ref_ci_dict[k] = ci_dict[k]
continue
if k not in ci_type_attrs_name and (
k not in ci_type_attrs_alias and _no_attribute_policy == ExistPolicy.REJECT):
if k not in ci_type_attrs_name and k not in ci_type_attrs_alias and \
_no_attribute_policy == ExistPolicy.REJECT:
return abort(400, ErrFormat.attribute_not_found.format(k))
if limit_attrs and ci_type_attrs_name.get(k) not in limit_attrs and (
ci_type_attrs_alias.get(k) not in limit_attrs):
if limit_attrs and ci_type_attrs_name.get(k) not in limit_attrs and \
ci_type_attrs_alias.get(k) not in limit_attrs:
return abort(403, ErrFormat.ci_filter_perm_attr_no_permission.format(k))
ci_dict = {k: v for k, v in ci_dict.items() if k in ci_type_attrs_name or k in ci_type_attrs_alias}
@@ -389,9 +391,6 @@ class CIManager(object):
if record_id: # has change
ci_cache.apply_async([ci.id], queue=CMDB_QUEUE)
if ref_ci_dict: # add relations
ci_relation_add.apply_async(args=(ref_ci_dict, ci.id, current_user.uid), queue=CMDB_QUEUE)
return ci.id
def update(self, ci_id, _is_admin=False, **ci_dict):
@@ -434,10 +433,6 @@ class CIManager(object):
if record_id: # has change
ci_cache.apply_async([ci_id], queue=CMDB_QUEUE)
ref_ci_dict = {k: v for k, v in ci_dict.items() if k.startswith("$") and "." in k}
if ref_ci_dict:
ci_relation_add.apply_async(args=(ref_ci_dict, ci.id), queue=CMDB_QUEUE)
@staticmethod
def update_unique_value(ci_id, unique_name, unique_value):
ci = CI.get_by_id(ci_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(ci_id)))
@@ -460,22 +455,17 @@ class CIManager(object):
for attr_name in attr_names:
value_table = TableMap(attr_name=attr_name).table
for item in value_table.get_by(ci_id=ci_id, to_dict=False):
item.delete(commit=False)
item.delete()
for item in CIRelation.get_by(first_ci_id=ci_id, to_dict=False):
ci_relation_delete.apply_async(args=(item.first_ci_id, item.second_ci_id), queue=CMDB_QUEUE)
item.delete(commit=False)
item.delete()
for item in CIRelation.get_by(second_ci_id=ci_id, to_dict=False):
ci_relation_delete.apply_async(args=(item.first_ci_id, item.second_ci_id), queue=CMDB_QUEUE)
item.delete(commit=False)
item.delete()
ad_ci = AutoDiscoveryCI.get_by(ci_id=ci_id, to_dict=False, first=True)
ad_ci and ad_ci.update(is_accept=False, accept_by=None, accept_time=None, filter_none=False, commit=False)
ci.delete(commit=False) # TODO: soft delete
db.session.commit()
ci.delete() # TODO: soft delete
AttributeHistoryManger.add(None, ci_id, [(None, OperateType.DELETE, ci_dict, None)], ci.type_id)
@@ -490,8 +480,11 @@ class CIManager(object):
unique_key = AttributeCache.get(ci_type.unique_id)
value_table = TableMap(attr=unique_key).table
v = (value_table.get_by(attr_id=unique_key.id, value=unique_value, to_dict=False, first=True) or
abort(404, ErrFormat.not_found))
v = value_table.get_by(attr_id=unique_key.id,
value=unique_value,
to_dict=False,
first=True) \
or abort(404, ErrFormat.not_found)
ci = CI.get_by_id(v.ci_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(v.ci_id)))
@@ -537,7 +530,6 @@ class CIManager(object):
result = [(i.get("hostname"), i.get("private_ip")[0], i.get("ci_type"),
heartbeat_dict.get(i.get("_id"))) for i in res
if i.get("private_ip")]
return numfound, result
@staticmethod
@@ -657,7 +649,6 @@ class CIManager(object):
return res
current_app.logger.warning("cache not hit...............")
return cls._get_cis_from_db(ci_ids, ret_key, fields, value_tables, excludes=excludes)
@@ -683,7 +674,6 @@ class CIRelationManager(object):
ci_type = CITypeCache.get(type_id)
children = CIManager.get_cis_by_ids(list(map(str, ci_type2ci_ids[type_id])), ret_key=ret_key)
res[ci_type.name] = children
return res
@staticmethod
@@ -755,28 +745,17 @@ class CIRelationManager(object):
return ci_ids
@staticmethod
def _check_constraint(first_ci_id, first_type_id, second_ci_id, second_type_id, type_relation):
db.session.remove()
def _check_constraint(first_ci_id, second_ci_id, type_relation):
if type_relation.constraint == ConstraintEnum.Many2Many:
return
first_existed = CIRelation.get_by(first_ci_id=first_ci_id,
relation_type_id=type_relation.relation_type_id, to_dict=False)
second_existed = CIRelation.get_by(second_ci_id=second_ci_id,
relation_type_id=type_relation.relation_type_id, to_dict=False)
if type_relation.constraint == ConstraintEnum.One2One:
for i in first_existed:
if i.second_ci.type_id == second_type_id:
return abort(400, ErrFormat.relation_constraint.format("1-1"))
first_existed = CIRelation.get_by(first_ci_id=first_ci_id, relation_type_id=type_relation.relation_type_id)
second_existed = CIRelation.get_by(second_ci_id=second_ci_id, relation_type_id=type_relation.relation_type_id)
if type_relation.constraint == ConstraintEnum.One2One and (first_existed or second_existed):
return abort(400, ErrFormat.relation_constraint.format("1-1"))
for i in second_existed:
if i.first_ci.type_id == first_type_id:
return abort(400, ErrFormat.relation_constraint.format("1-1"))
if type_relation.constraint == ConstraintEnum.One2Many:
for i in second_existed:
if i.first_ci.type_id == first_type_id:
return abort(400, ErrFormat.relation_constraint.format("1-N"))
if type_relation.constraint == ConstraintEnum.One2Many and second_existed:
return abort(400, ErrFormat.relation_constraint.format("1-N"))
@classmethod
def add(cls, first_ci_id, second_ci_id, more=None, relation_type_id=None):
@@ -815,17 +794,15 @@ class CIRelationManager(object):
else:
type_relation = CITypeRelation.get_by_id(relation_type_id)
with Lock("ci_relation_add_{}_{}".format(first_ci.type_id, second_ci.type_id), need_lock=True):
cls._check_constraint(first_ci_id, second_ci_id, type_relation)
cls._check_constraint(first_ci_id, first_ci.type_id, second_ci_id, second_ci.type_id, type_relation)
existed = CIRelation.create(first_ci_id=first_ci_id,
second_ci_id=second_ci_id,
relation_type_id=relation_type_id)
existed = CIRelation.create(first_ci_id=first_ci_id,
second_ci_id=second_ci_id,
relation_type_id=relation_type_id)
CIRelationHistoryManager().add(existed, OperateType.ADD)
CIRelationHistoryManager().add(existed, OperateType.ADD)
ci_relation_cache.apply_async(args=(first_ci_id, second_ci_id), queue=CMDB_QUEUE)
ci_relation_cache.apply_async(args=(first_ci_id, second_ci_id), queue=CMDB_QUEUE)
if more is not None:
existed.upadte(more=more)
@@ -873,12 +850,12 @@ class CIRelationManager(object):
:param children:
:return:
"""
if isinstance(parents, list):
if parents is not None and isinstance(parents, list):
for parent_id in parents:
for ci_id in ci_ids:
cls.add(parent_id, ci_id)
if isinstance(children, list):
if children is not None and isinstance(children, list):
for child_id in children:
for ci_id in ci_ids:
cls.add(ci_id, child_id)
@@ -892,7 +869,7 @@ class CIRelationManager(object):
:return:
"""
if isinstance(parents, list):
if parents is not None and isinstance(parents, list):
for parent_id in parents:
for ci_id in ci_ids:
cls.delete_2(parent_id, ci_id)

View File

@@ -1,13 +1,11 @@
# -*- coding:utf-8 -*-
# -*- coding:utf-8 -*-
import copy
import datetime
import toposort
from flask import abort
from flask import current_app
from flask_login import current_user
from toposort import toposort_flatten
from api.extensions import db
from api.lib.cmdb.attribute import AttributeManager
@@ -18,9 +16,7 @@ from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.const import CITypeOperateType
from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.cmdb.const import ConstraintEnum
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.const import PermEnum, ResourceTypeEnum, RoleEnum
from api.lib.cmdb.const import ValueTypeEnum
from api.lib.cmdb.history import CITypeHistoryManager
from api.lib.cmdb.relation_type import RelationTypeManager
@@ -31,10 +27,7 @@ from api.lib.decorator import kwargs_required
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import is_app_admin
from api.models.cmdb import Attribute
from api.models.cmdb import AutoDiscoveryCI
from api.models.cmdb import AutoDiscoveryCIType
from api.models.cmdb import CI
from api.models.cmdb import CIFilterPerms
from api.models.cmdb import CIType
from api.models.cmdb import CITypeAttribute
from api.models.cmdb import CITypeAttributeGroup
@@ -44,9 +37,7 @@ from api.models.cmdb import CITypeGroupItem
from api.models.cmdb import CITypeRelation
from api.models.cmdb import CITypeTrigger
from api.models.cmdb import CITypeUniqueConstraint
from api.models.cmdb import CustomDashboard
from api.models.cmdb import PreferenceRelationView
from api.models.cmdb import PreferenceSearchOption
from api.models.cmdb import PreferenceShowAttributes
from api.models.cmdb import PreferenceTreeView
from api.models.cmdb import RelationType
@@ -64,7 +55,6 @@ class CITypeManager(object):
@staticmethod
def get_name_by_id(type_id):
ci_type = CITypeCache.get(type_id)
return ci_type and ci_type.name
@staticmethod
@@ -76,7 +66,7 @@ class CITypeManager(object):
@staticmethod
def get_ci_types(type_name=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():
resources = set([i.get('name') for i in ACLManager().get_resources("CIType")])
ci_types = CIType.get_by() if type_name is None else CIType.get_by_like(name=type_name)
@@ -115,8 +105,11 @@ class CITypeManager(object):
@classmethod
@kwargs_required("name")
def add(cls, **kwargs):
from api.lib.cmdb.const import L_TYPE
if L_TYPE and len(CIType.get_by()) > L_TYPE * 2:
return abort(400, ErrFormat.limit_ci_type.format(L_TYPE * 2))
unique_key = kwargs.pop("unique_key", None) or kwargs.pop("unique_id", None)
unique_key = kwargs.pop("unique_key", None)
unique_key = AttributeCache.get(unique_key) or abort(404, ErrFormat.unique_key_not_define)
kwargs["alias"] = kwargs["name"] if not kwargs.get("alias") else kwargs["alias"]
@@ -186,7 +179,6 @@ class CITypeManager(object):
def set_enabled(cls, type_id, enabled=True):
ci_type = cls.check_is_existed(type_id)
ci_type.update(enabled=enabled)
return type_id
@classmethod
@@ -206,21 +198,19 @@ class CITypeManager(object):
return abort(400, ErrFormat.ci_relation_view_exists_and_cannot_delete_type.format(rv.name))
for item in CITypeRelation.get_by(parent_id=type_id, to_dict=False):
item.soft_delete(commit=False)
item.soft_delete()
for item in CITypeRelation.get_by(child_id=type_id, to_dict=False):
item.soft_delete(commit=False)
item.soft_delete()
for table in [PreferenceTreeView, PreferenceShowAttributes, PreferenceSearchOption, CustomDashboard,
CITypeGroupItem, CITypeAttributeGroup, CITypeAttribute, CITypeUniqueConstraint, CITypeTrigger,
AutoDiscoveryCIType, CIFilterPerms]:
for item in table.get_by(type_id=type_id, to_dict=False):
item.soft_delete(commit=False)
for item in PreferenceTreeView.get_by(type_id=type_id, to_dict=False):
item.soft_delete()
for item in AutoDiscoveryCI.get_by(type_id=type_id, to_dict=False):
item.delete(commit=False)
for item in PreferenceShowAttributes.get_by(type_id=type_id, to_dict=False):
item.soft_delete()
db.session.commit()
for item in CITypeGroupItem.get_by(type_id=type_id, to_dict=False):
item.soft_delete()
ci_type.soft_delete()
@@ -271,17 +261,16 @@ class CITypeGroupManager(object):
@staticmethod
def add(name):
CITypeGroup.get_by(name=name, first=True) and abort(400, ErrFormat.ci_type_group_exists.format(name))
return CITypeGroup.create(name=name)
@staticmethod
def update(gid, name, type_ids):
"""
update part
:param gid:
:param name:
:param type_ids:
:return:
:param gid:
:param name:
:param type_ids:
:return:
"""
existed = CITypeGroup.get_by_id(gid) or abort(
404, ErrFormat.ci_type_group_not_found.format("id={}".format(gid)))
@@ -338,17 +327,6 @@ class CITypeAttributeManager(object):
def __init__(self):
pass
@staticmethod
def get_attr_name(ci_type_name, key):
ci_type = CITypeCache.get(ci_type_name)
if ci_type is None:
return
for i in CITypeAttributesCache.get(ci_type.id):
attr = AttributeCache.get(i.attr_id)
if attr and (attr.name == key or attr.alias == key):
return attr.name
@staticmethod
def get_attr_names_by_type_id(type_id):
return [AttributeCache.get(attr.attr_id).name for attr in CITypeAttributesCache.get(type_id)]
@@ -369,19 +347,8 @@ class CITypeAttributeManager(object):
attr_dict.pop('choice_web_hook', None)
result.append(attr_dict)
return result
@staticmethod
def get_common_attributes(type_ids):
result = CITypeAttribute.get_by(__func_in___key_type_id=list(map(int, type_ids)), to_dict=False)
attr2types = {}
for i in result:
attr2types.setdefault(i.attr_id, []).append(i.type_id)
return [AttributeCache.get(attr_id).to_dict() for attr_id in attr2types
if len(attr2types[attr_id]) == len(type_ids)]
@staticmethod
def _check(type_id, attr_ids):
ci_type = CITypeManager.check_is_existed(type_id)
@@ -398,10 +365,10 @@ class CITypeAttributeManager(object):
def add(cls, type_id, attr_ids=None, **kwargs):
"""
add attributes to CIType
:param type_id:
:param type_id:
:param attr_ids: list
:param kwargs:
:return:
:param kwargs:
:return:
"""
attr_ids = list(set(attr_ids))
@@ -428,9 +395,9 @@ class CITypeAttributeManager(object):
def update(cls, type_id, attributes):
"""
update attributes to CIType
:param type_id:
:param type_id:
:param attributes: list
:return:
:return:
"""
cls._check(type_id, [i.get('attr_id') for i in attributes])
@@ -458,9 +425,9 @@ class CITypeAttributeManager(object):
def delete(cls, type_id, attr_ids=None):
"""
delete attributes from CIType
:param type_id:
:param type_id:
:param attr_ids: list
:return:
:return:
"""
from api.tasks.cmdb import ci_cache
@@ -567,7 +534,6 @@ class CITypeRelationManager(object):
ci_type_dict["attributes"] = CITypeAttributeManager.get_attributes_by_type_id(ci_type_dict["id"])
ci_type_dict["relation_type"] = relation_inst.relation_type.name
ci_type_dict["constraint"] = relation_inst.constraint
return ci_type_dict
@classmethod
@@ -576,22 +542,6 @@ class CITypeRelationManager(object):
return [cls._wrap_relation_type_dict(child.child_id, child) for child in children]
@classmethod
def recursive_level2children(cls, parent_id):
result = dict()
def get_children(_id, level):
children = CITypeRelation.get_by(parent_id=_id, to_dict=False)
result[level + 1] = [i.child.to_dict() for i in children]
for i in children:
if i.child_id != _id:
get_children(i.child_id, level + 1)
get_children(parent_id, 0)
return result
@classmethod
def get_parents(cls, child_id):
parents = CITypeRelation.get_by(child_id=child_id, to_dict=False)
@@ -614,17 +564,6 @@ class CITypeRelationManager(object):
p = CITypeManager.check_is_existed(parent)
c = CITypeManager.check_is_existed(child)
rels = {}
for i in CITypeRelation.get_by(to_dict=False):
rels.setdefault(i.child_id, set()).add(i.parent_id)
rels.setdefault(c.id, set()).add(p.id)
try:
toposort_flatten(rels)
except toposort.CircularDependencyError as e:
current_app.logger.warning(str(e))
return abort(400, ErrFormat.circular_dependency_error)
existed = cls._get(p.id, c.id)
if existed is not None:
existed.update(relation_type_id=relation_type_id,
@@ -653,8 +592,8 @@ class CITypeRelationManager(object):
@classmethod
def delete(cls, _id):
ctr = (CITypeRelation.get_by_id(_id) or
abort(404, ErrFormat.ci_type_relation_not_found.format("id={}".format(_id))))
ctr = CITypeRelation.get_by_id(_id) or \
abort(404, ErrFormat.ci_type_relation_not_found.format("id={}".format(_id)))
ctr.soft_delete()
CITypeHistoryManager.add(CITypeOperateType.DELETE_RELATION, ctr.parent_id,
@@ -708,7 +647,6 @@ class CITypeAttributeGroupManager(object):
:param name:
:param group_order: group order
:param attr_order:
:param is_update:
:return:
"""
existed = CITypeAttributeGroup.get_by(type_id=type_id, name=name, first=True, to_dict=False)
@@ -749,8 +687,8 @@ class CITypeAttributeGroupManager(object):
@staticmethod
def delete(group_id):
group = (CITypeAttributeGroup.get_by_id(group_id) or
abort(404, ErrFormat.ci_type_attribute_group_not_found.format("id={}".format(group_id))))
group = CITypeAttributeGroup.get_by_id(group_id) \
or abort(404, ErrFormat.ci_type_attribute_group_not_found.format("id={}".format(group_id)))
group.soft_delete()
items = CITypeAttributeGroupItem.get_by(group_id=group_id, to_dict=False)
@@ -862,12 +800,6 @@ class CITypeTemplateManager(object):
for added_id in set(id2obj_dicts.keys()) - set(existed_ids):
if cls == CIType:
CITypeManager.add(**id2obj_dicts[added_id])
elif cls == CITypeRelation:
CITypeRelationManager.add(id2obj_dicts[added_id].get('parent_id'),
id2obj_dicts[added_id].get('child_id'),
id2obj_dicts[added_id].get('relation_type_id'),
id2obj_dicts[added_id].get('constraint'),
)
else:
cls.create(flush=True, **id2obj_dicts[added_id])
@@ -1025,8 +957,8 @@ class CITypeTemplateManager(object):
rule['uid'] = current_user.uid
try:
AutoDiscoveryCITypeCRUD.add(**rule)
except Exception as e:
current_app.logger.warning("import auto discovery rules failed: {}".format(e))
except:
pass
def import_template(self, tpt):
import time
@@ -1185,8 +1117,8 @@ class CITypeTriggerManager(object):
@staticmethod
def update(_id, notify):
existed = (CITypeTrigger.get_by_id(_id) or
abort(404, ErrFormat.ci_type_trigger_not_found.format("id={}".format(_id))))
existed = CITypeTrigger.get_by_id(_id) or \
abort(404, ErrFormat.ci_type_trigger_not_found.format("id={}".format(_id)))
existed2 = existed.to_dict()
new = existed.update(notify=notify)
@@ -1200,8 +1132,8 @@ class CITypeTriggerManager(object):
@staticmethod
def delete(_id):
existed = (CITypeTrigger.get_by_id(_id) or
abort(404, ErrFormat.ci_type_trigger_not_found.format("id={}".format(_id))))
existed = CITypeTrigger.get_by_id(_id) or \
abort(404, ErrFormat.ci_type_trigger_not_found.format("id={}".format(_id)))
existed.soft_delete()
@@ -1224,16 +1156,16 @@ class CITypeTriggerManager(object):
result = []
for v in values:
if (isinstance(v.value, (datetime.date, datetime.datetime)) and
(v.value - delta_time).strftime('%Y%m%d') == now.strftime("%Y%m%d")):
if isinstance(v.value, (datetime.date, datetime.datetime)) and \
(v.value - delta_time).strftime('%Y%m%d') == now.strftime("%Y%m%d"):
result.append(v)
return result
@staticmethod
def trigger_notify(trigger, ci):
if (trigger.notify.get('notify_at') == datetime.datetime.now().strftime("%H:%M") or
not trigger.notify.get('notify_at')):
if trigger.notify.get('notify_at') == datetime.datetime.now().strftime("%H:%M") or \
not trigger.notify.get('notify_at'):
from api.tasks.cmdb import trigger_notify
trigger_notify.apply_async(args=(trigger.notify, ci.ci_id), queue=CMDB_QUEUE)

View File

@@ -99,7 +99,5 @@ CMDB_QUEUE = "one_cmdb_async"
REDIS_PREFIX_CI = "ONE_CMDB"
REDIS_PREFIX_CI_RELATION = "CMDB_CI_RELATION"
BUILTIN_KEYWORDS = {'id', '_id', 'ci_id', 'type', '_type', 'ci_type'}
L_TYPE = None
L_CI = None

View File

@@ -14,14 +14,6 @@ class CustomDashboardManager(object):
def get():
return sorted(CustomDashboard.get_by(to_dict=True), key=lambda x: (x["category"], x['order']))
@staticmethod
def preview(**kwargs):
from api.lib.cmdb.cache import CMDBCounterCache
res = CMDBCounterCache.update(kwargs, flush=False)
return res
@staticmethod
def add(**kwargs):
from api.lib.cmdb.cache import CMDBCounterCache
@@ -31,9 +23,9 @@ class CustomDashboardManager(object):
new = CustomDashboard.create(**kwargs)
res = CMDBCounterCache.update(new.to_dict())
CMDBCounterCache.update(new.to_dict())
return new, res
return new
@staticmethod
def update(_id, **kwargs):
@@ -43,9 +35,9 @@ class CustomDashboardManager(object):
new = existed.update(**kwargs)
res = CMDBCounterCache.update(new.to_dict())
CMDBCounterCache.update(new.to_dict())
return new, res
return new
@staticmethod
def batch_update(id2options):

View File

@@ -176,8 +176,8 @@ class AttributeHistoryManger(object):
def get_record_detail(record_id):
from api.lib.cmdb.ci import CIManager
record = (OperationRecord.get_by_id(record_id) or
abort(404, ErrFormat.record_not_found.format("id={}".format(record_id))))
record = OperationRecord.get_by_id(record_id) or \
abort(404, ErrFormat.record_not_found.format("id={}".format(record_id)))
username = UserCache.get(record.uid).nickname or UserCache.get(record.uid).username
timestamp = record.created_at.strftime("%Y-%m-%d %H:%M:%S")

View File

@@ -37,12 +37,10 @@ class PreferenceManager(object):
def get_types(instance=False, tree=False):
types = db.session.query(PreferenceShowAttributes.type_id).filter(
PreferenceShowAttributes.uid == current_user.uid).filter(
PreferenceShowAttributes.deleted.is_(False)).group_by(
PreferenceShowAttributes.type_id).all() if instance else []
PreferenceShowAttributes.deleted.is_(False)).group_by(PreferenceShowAttributes.type_id).all() \
if instance else []
tree_types = PreferenceTreeView.get_by(uid=current_user.uid, to_dict=False) if tree else []
type_ids = set([i.type_id for i in types + tree_types])
type_ids = list(set([i.type_id for i in types + tree_types]))
return [CITypeCache.get(type_id).to_dict() for type_id in type_ids]
@staticmethod

View File

@@ -42,7 +42,7 @@ FACET_QUERY1 = """
FACET_QUERY = """
SELECT {0}.value,
count(distinct({0}.ci_id))
count({0}.ci_id)
FROM {0}
INNER JOIN ({1}) AS F ON F.ci_id={0}.ci_id
WHERE {0}.attr_id={2:d}

View File

@@ -24,21 +24,21 @@ class RelationTypeManager(object):
@staticmethod
def add(name):
RelationType.get_by(name=name, first=True, to_dict=False) and abort(
400, ErrFormat.relation_type_exists.format(name))
RelationType.get_by(name=name, first=True, to_dict=False) and \
abort(400, ErrFormat.relation_type_exists.format(name))
return RelationType.create(name=name)
@staticmethod
def update(rel_id, name):
existed = RelationType.get_by_id(rel_id) or abort(
404, ErrFormat.relation_type_not_found.format("id={}".format(rel_id)))
existed = RelationType.get_by_id(rel_id) or \
abort(404, ErrFormat.relation_type_not_found.format("id={}".format(rel_id)))
return existed.update(name=name)
@staticmethod
def delete(rel_id):
existed = RelationType.get_by_id(rel_id) or abort(
404, ErrFormat.relation_type_not_found.format("id={}".format(rel_id)))
existed = RelationType.get_by_id(rel_id) or \
abort(404, ErrFormat.relation_type_not_found.format("id={}".format(rel_id)))
existed.soft_delete()

View File

@@ -245,8 +245,10 @@ class Search(object):
new_table = _v_query_sql
if self.only_type_query or not self.type_id_list:
return ("SELECT SQL_CALC_FOUND_ROWS DISTINCT C.ci_id FROM ({0}) AS C ORDER BY C.value {2} "
"LIMIT {1:d}, {3};".format(new_table, (self.page - 1) * self.count, sort_type, self.count))
return "SELECT SQL_CALC_FOUND_ROWS DISTINCT C.ci_id " \
"FROM ({0}) AS C " \
"ORDER BY C.value {2} " \
"LIMIT {1:d}, {3};".format(new_table, (self.page - 1) * self.count, sort_type, self.count)
elif self.type_id_list:
self.query_sql = """SELECT C.ci_id

View File

@@ -297,8 +297,8 @@ class Search(object):
if not attr:
raise SearchError(ErrFormat.attribute_not_found.format(field))
sort_by = ("{0}.keyword".format(field)
if attr.value_type not in (ValueTypeEnum.INT, ValueTypeEnum.FLOAT) else field)
sort_by = "{0}.keyword".format(field) \
if attr.value_type not in (ValueTypeEnum.INT, ValueTypeEnum.FLOAT) else field
sorts.append({sort_by: {"order": sort_type}})
self.query.update(dict(sort=sorts))

View File

@@ -7,6 +7,7 @@ import json
import re
import six
from markupsafe import escape
import api.models.cmdb as model
from api.lib.cmdb.cache import AttributeCache
@@ -32,8 +33,8 @@ class ValueTypeMap(object):
deserialize = {
ValueTypeEnum.INT: string2int,
ValueTypeEnum.FLOAT: float,
ValueTypeEnum.TEXT: lambda x: x,
ValueTypeEnum.TIME: lambda x: TIME_RE.findall(x)[0],
ValueTypeEnum.TEXT: lambda x: escape(x).encode('utf-8').decode('utf-8'),
ValueTypeEnum.TIME: lambda x: TIME_RE.findall(escape(x).encode('utf-8').decode('utf-8'))[0],
ValueTypeEnum.DATETIME: str2datetime,
ValueTypeEnum.DATE: str2datetime,
ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x,

View File

@@ -80,10 +80,9 @@ class AttributeValueManager(object):
return res
@staticmethod
def _deserialize_value(value_type, value):
def __deserialize_value(value_type, value):
if not value:
return value
deserialize = ValueTypeMap.deserialize[value_type]
try:
v = deserialize(value)
@@ -92,13 +91,13 @@ class AttributeValueManager(object):
return abort(400, ErrFormat.attribute_value_invalid.format(value))
@staticmethod
def _check_is_choice(attr, value_type, value):
def __check_is_choice(attr, value_type, value):
choice_values = AttributeManager.get_choice_values(attr.id, value_type, attr.choice_web_hook)
if str(value) not in list(map(str, [i[0] for i in choice_values])):
return abort(400, ErrFormat.not_in_choice_values.format(value))
@staticmethod
def _check_is_unique(value_table, attr, ci_id, type_id, value):
def __check_is_unique(value_table, attr, ci_id, type_id, value):
existed = db.session.query(value_table.attr_id).join(CI, CI.id == value_table.ci_id).filter(
CI.type_id == type_id).filter(
value_table.attr_id == attr.id).filter(value_table.deleted.is_(False)).filter(
@@ -107,20 +106,20 @@ class AttributeValueManager(object):
existed and abort(400, ErrFormat.attribute_value_unique_required.format(attr.alias, value))
@staticmethod
def _check_is_required(type_id, attr, value, type_attr=None):
def __check_is_required(type_id, attr, value, type_attr=None):
type_attr = type_attr or CITypeAttributeCache.get(type_id, attr.id)
if type_attr and type_attr.is_required and not value and value != 0:
return abort(400, ErrFormat.attribute_value_required.format(attr.alias))
def _validate(self, attr, value, value_table, ci=None, type_id=None, ci_id=None, type_attr=None):
ci = ci or {}
v = self._deserialize_value(attr.value_type, value)
v = self.__deserialize_value(attr.value_type, value)
attr.is_choice and value and self._check_is_choice(attr, attr.value_type, v)
attr.is_unique and self._check_is_unique(
attr.is_choice and value and self.__check_is_choice(attr, attr.value_type, v)
attr.is_unique and self.__check_is_unique(
value_table, attr, ci and ci.id or ci_id, ci and ci.type_id or type_id, v)
self._check_is_required(ci and ci.type_id or type_id, attr, v, type_attr=type_attr)
self.__check_is_required(ci and ci.type_id or type_id, attr, v, type_attr=type_attr)
if v == "" and attr.value_type not in (ValueTypeEnum.TEXT,):
v = None
@@ -145,7 +144,7 @@ class AttributeValueManager(object):
return record_id
@staticmethod
def _compute_attr_value_from_expr(expr, ci_dict):
def __compute_attr_value_from_expr(expr, ci_dict):
t = jinja2.Template(expr).render(ci_dict)
try:
@@ -155,7 +154,7 @@ class AttributeValueManager(object):
return t
@staticmethod
def _compute_attr_value_from_script(script, ci_dict):
def __compute_attr_value_from_script(script, ci_dict):
script = jinja2.Template(script).render(ci_dict)
script_f = tempfile.NamedTemporaryFile(delete=False, suffix=".py")
@@ -184,22 +183,22 @@ class AttributeValueManager(object):
return [var for var in schema.get("properties")]
def _compute_attr_value(self, attr, payload, ci_id):
attrs = (self._jinja2_parse(attr['compute_expr']) if attr.get('compute_expr')
else self._jinja2_parse(attr['compute_script']))
def _compute_attr_value(self, attr, payload, ci):
attrs = self._jinja2_parse(attr['compute_expr']) if attr.get('compute_expr') else \
self._jinja2_parse(attr['compute_script'])
not_existed = [i for i in attrs if i not in payload]
if ci_id is not None:
payload.update(self.get_attr_values(not_existed, ci_id))
if ci is not None:
payload.update(self.get_attr_values(not_existed, ci.id))
if attr['compute_expr']:
return self._compute_attr_value_from_expr(attr['compute_expr'], payload)
return self.__compute_attr_value_from_expr(attr['compute_expr'], payload)
elif attr['compute_script']:
return self._compute_attr_value_from_script(attr['compute_script'], payload)
return self.__compute_attr_value_from_script(attr['compute_script'], payload)
def handle_ci_compute_attributes(self, ci_dict, computed_attrs, ci):
payload = copy.deepcopy(ci_dict)
for attr in computed_attrs:
computed_value = self._compute_attr_value(attr, payload, ci and ci.id)
computed_value = self._compute_attr_value(attr, payload, ci)
if computed_value is not None:
ci_dict[attr['name']] = computed_value
@@ -221,7 +220,7 @@ class AttributeValueManager(object):
for i in handle_arg_list(value)]
ci_dict[key] = value_list
if not value_list:
self._check_is_required(type_id, attr, '')
self.__check_is_required(type_id, attr, '')
else:
value = self._validate(attr, value, value_table, ci=None, type_id=type_id, ci_id=ci_id,
@@ -311,7 +310,7 @@ class AttributeValueManager(object):
if attr.is_list:
value_list = [self._validate(attr, i, value_table, ci) for i in handle_arg_list(value)]
if not value_list:
self._check_is_required(ci.type_id, attr, '')
self.__check_is_required(ci.type_id, attr, '')
existed_attrs = value_table.get_by(attr_id=attr.id, ci_id=ci.id, to_dict=False)
existed_values = [i.value for i in existed_attrs]

View File

@@ -6,7 +6,6 @@ from api.lib.common_setting.resp_format import ErrFormat
from api.lib.perm.acl.cache import RoleCache, AppCache
from api.lib.perm.acl.role import RoleCRUD, RoleRelationCRUD
from api.lib.perm.acl.user import UserCRUD
from api.lib.perm.acl.resource import ResourceTypeCRUD, ResourceCRUD
class ACLManager(object):
@@ -95,22 +94,3 @@ class ACLManager(object):
avatar=user_info.get('avatar'))
return result
def validate_app(self):
return AppCache.get(self.app_name)
def get_all_resources_types(self, q=None, page=1, page_size=999999):
app_id = self.validate_app().id
numfound, res, id2perms = ResourceTypeCRUD.search(q, app_id, page, page_size)
return dict(
numfound=numfound,
groups=[i.to_dict() for i in res],
id2perms=id2perms
)
def create_resource(self, payload):
payload['app_id'] = self.validate_app().id
resource = ResourceCRUD.add(**payload)
return resource.to_dict()

View File

@@ -1,46 +0,0 @@
from flask import abort
from api.extensions import db
from api.lib.common_setting.resp_format import ErrFormat
from api.models.common_setting import CommonData
class CommonDataCRUD(object):
@staticmethod
def get_data_by_type(data_type):
return CommonData.get_by(data_type=data_type)
@staticmethod
def get_data_by_id(_id, to_dict=True):
return CommonData.get_by(first=True, id=_id, to_dict=to_dict)
@staticmethod
def create_new_data(data_type, **kwargs):
try:
return CommonData.create(data_type=data_type, **kwargs)
except Exception as e:
db.session.rollback()
abort(400, str(e))
@staticmethod
def update_data(_id, **kwargs):
existed = CommonDataCRUD.get_data_by_id(_id, to_dict=False)
if not existed:
abort(404, ErrFormat.common_data_not_found.format(_id))
try:
return existed.update(**kwargs)
except Exception as e:
db.session.rollback()
abort(400, str(e))
@staticmethod
def delete(_id):
existed = CommonDataCRUD.get_data_by_id(_id, to_dict=False)
if not existed:
abort(404, ErrFormat.common_data_not_found.format(_id))
try:
existed.soft_delete()
except Exception as e:
db.session.rollback()
abort(400, str(e))

View File

@@ -54,4 +54,3 @@ class ErrFormat(CommonErrFormat):
email_is_required = "邮箱不能为空"
email_format_error = "邮箱格式错误"
common_data_not_found = "ID {} 找不到记录"

View File

@@ -4,7 +4,8 @@ from api.lib.common_setting.utils import get_cur_time_str
def allowed_file(filename, allowed_extensions):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in allowed_extensions
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in allowed_extensions
def generate_new_file_name(name):
@@ -12,5 +13,4 @@ def generate_new_file_name(name):
prev_name = ''.join(name.split(f".{ext}")[:-1])
uid = str(uuid.uuid4())
cur_str = get_cur_time_str('_')
return f"{prev_name}_{cur_str}_{uid}.{ext}"

View File

@@ -55,8 +55,8 @@ def args_validate(model_cls, exclude_args=None):
if exclude_args and arg in exclude_args:
continue
if attr.type.python_type == str and attr.type.length and (
len(request.values[arg] or '') > attr.type.length):
if attr.type.python_type == str and attr.type.length and \
len(request.values[arg] or '') > attr.type.length:
return abort(400, CommonErrFormat.argument_str_length_limit.format(arg, attr.type.length))
elif attr.type.python_type in (int, float) and request.values[arg]:

View File

@@ -19,7 +19,6 @@ def build_api_key(path, params):
_secret = "".join([path, secret, values]).encode("utf-8")
params["_secret"] = hashlib.sha1(_secret).hexdigest()
params["_key"] = key
return params

View File

@@ -5,10 +5,8 @@ import hashlib
import requests
import six
from flask import abort
from flask import current_app
from flask import request
from flask import session
from flask import abort, session
from flask import current_app, request
from flask_login import current_user
from api.extensions import cache
@@ -87,8 +85,8 @@ class ACLManager(object):
if user:
return Role.get_by(name=name, uid=user.uid, first=True, to_dict=False)
return (Role.get_by(name=name, app_id=self.app_id, first=True, to_dict=False) or
Role.get_by(name=name, first=True, to_dict=False))
return Role.get_by(name=name, app_id=self.app_id, first=True, to_dict=False) or \
Role.get_by(name=name, first=True, to_dict=False)
def add_resource(self, name, resource_type_name=None):
resource_type = ResourceType.get_by(name=resource_type_name, first=True, to_dict=False)

View File

@@ -8,9 +8,7 @@ from flask import abort
from flask import current_app
from api.extensions import db
from api.lib.perm.acl.audit import AuditCRUD
from api.lib.perm.acl.audit import AuditOperateType
from api.lib.perm.acl.audit import AuditScope
from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType, AuditScope
from api.lib.perm.acl.resp_format import ErrFormat
from api.models.acl import App

View File

@@ -4,21 +4,13 @@ import json
from enum import Enum
from typing import List
from flask import has_request_context, request
from flask import g, has_request_context, request
from flask_login import current_user
from sqlalchemy import func
from api.lib.perm.acl import AppCache
from api.models.acl import AuditPermissionLog
from api.models.acl import AuditResourceLog
from api.models.acl import AuditRoleLog
from api.models.acl import AuditTriggerLog
from api.models.acl import Permission
from api.models.acl import Resource
from api.models.acl import ResourceGroup
from api.models.acl import ResourceType
from api.models.acl import Role
from api.models.acl import RolePermission
from api.models.acl import AuditPermissionLog, AuditResourceLog, AuditRoleLog, AuditTriggerLog, Permission, Resource, \
ResourceGroup, ResourceType, Role, RolePermission
class AuditScope(str, Enum):
@@ -57,7 +49,7 @@ class AuditCRUD(object):
@staticmethod
def get_current_operate_uid(uid=None):
user_id = uid or (getattr(current_user, 'uid', None)) or getattr(current_user, 'user_id', None)
user_id = uid or getattr(current_user, 'uid', None)
if has_request_context() and request.headers.get('X-User-Id'):
_user_id = request.headers['X-User-Id']
@@ -99,8 +91,11 @@ class AuditCRUD(object):
criterion.append(AuditPermissionLog.operate_type == v)
records = AuditPermissionLog.query.filter(
AuditPermissionLog.deleted == 0, *criterion).order_by(
AuditPermissionLog.id.desc()).offset((page - 1) * page_size).limit(page_size).all()
AuditPermissionLog.deleted == 0,
*criterion) \
.order_by(AuditPermissionLog.id.desc()) \
.offset((page - 1) * page_size) \
.limit(page_size).all()
data = {
'data': [r.to_dict() for r in records],
@@ -163,8 +158,10 @@ class AuditCRUD(object):
elif k == 'operate_type':
criterion.append(AuditRoleLog.operate_type == v)
records = AuditRoleLog.query.filter(AuditRoleLog.deleted == 0, *criterion).order_by(
AuditRoleLog.id.desc()).offset((page - 1) * page_size).limit(page_size).all()
records = AuditRoleLog.query.filter(AuditRoleLog.deleted == 0, *criterion) \
.order_by(AuditRoleLog.id.desc()) \
.offset((page - 1) * page_size) \
.limit(page_size).all()
data = {
'data': [r.to_dict() for r in records],
@@ -226,8 +223,11 @@ class AuditCRUD(object):
criterion.append(AuditResourceLog.operate_type == v)
records = AuditResourceLog.query.filter(
AuditResourceLog.deleted == 0, *criterion).order_by(
AuditResourceLog.id.desc()).offset((page - 1) * page_size).limit(page_size).all()
AuditResourceLog.deleted == 0,
*criterion) \
.order_by(AuditResourceLog.id.desc()) \
.offset((page - 1) * page_size) \
.limit(page_size).all()
data = {
'data': [r.to_dict() for r in records],
@@ -257,8 +257,11 @@ class AuditCRUD(object):
criterion.append(AuditTriggerLog.operate_type == v)
records = AuditTriggerLog.query.filter(
AuditTriggerLog.deleted == 0, *criterion).order_by(
AuditTriggerLog.id.desc()).offset((page - 1) * page_size).limit(page_size).all()
AuditTriggerLog.deleted == 0,
*criterion) \
.order_by(AuditTriggerLog.id.desc()) \
.offset((page - 1) * page_size) \
.limit(page_size).all()
data = {
'data': [r.to_dict() for r in records],

View File

@@ -60,15 +60,15 @@ class UserCache(object):
@classmethod
def get(cls, key):
user = (cache.get(cls.PREFIX_ID.format(key)) or
cache.get(cls.PREFIX_NAME.format(key)) or
cache.get(cls.PREFIX_NICK.format(key)) or
cache.get(cls.PREFIX_WXID.format(key)))
user = cache.get(cls.PREFIX_ID.format(key)) or \
cache.get(cls.PREFIX_NAME.format(key)) or \
cache.get(cls.PREFIX_NICK.format(key)) or \
cache.get(cls.PREFIX_WXID.format(key))
if not user:
user = (User.query.get(key) or
User.query.get_by_username(key) or
User.query.get_by_nickname(key) or
User.query.get_by_wxid(key))
user = User.query.get(key) or \
User.query.get_by_username(key) or \
User.query.get_by_nickname(key) or \
User.query.get_by_wxid(key)
if user:
cls.set(user)

View File

@@ -4,9 +4,7 @@ import datetime
from flask import abort
from api.extensions import db
from api.lib.perm.acl.audit import AuditCRUD
from api.lib.perm.acl.audit import AuditOperateSource
from api.lib.perm.acl.audit import AuditOperateType
from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType, AuditOperateSource
from api.lib.perm.acl.cache import PermissionCache
from api.lib.perm.acl.cache import RoleCache
from api.lib.perm.acl.cache import UserCache
@@ -99,8 +97,8 @@ class PermissionCRUD(object):
elif group_id is not None:
from api.models.acl import ResourceGroup
group = ResourceGroup.get_by_id(group_id) or abort(
404, ErrFormat.resource_group_not_found.format("id={}".format(group_id)))
group = ResourceGroup.get_by_id(group_id) or \
abort(404, ErrFormat.resource_group_not_found.format("id={}".format(group_id)))
app_id = group.app_id
rt_id = group.resource_type_id
if not perms:
@@ -208,8 +206,8 @@ class PermissionCRUD(object):
if resource_id is not None:
from api.models.acl import Resource
resource = Resource.get_by_id(resource_id) or abort(
404, ErrFormat.resource_not_found.format("id={}".format(resource_id)))
resource = Resource.get_by_id(resource_id) or \
abort(404, ErrFormat.resource_not_found.format("id={}".format(resource_id)))
app_id = resource.app_id
rt_id = resource.resource_type_id
if not perms:
@@ -218,8 +216,8 @@ class PermissionCRUD(object):
elif group_id is not None:
from api.models.acl import ResourceGroup
group = ResourceGroup.get_by_id(group_id) or abort(
404, ErrFormat.resource_group_not_found.format("id={}".format(group_id)))
group = ResourceGroup.get_by_id(group_id) or \
abort(404, ErrFormat.resource_group_not_found.format("id={}".format(group_id)))
app_id = group.app_id
rt_id = group.resource_type_id

View File

@@ -5,9 +5,7 @@ from flask import abort
from flask import current_app
from api.extensions import db
from api.lib.perm.acl.audit import AuditCRUD
from api.lib.perm.acl.audit import AuditOperateType
from api.lib.perm.acl.audit import AuditScope
from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType, AuditScope
from api.lib.perm.acl.cache import ResourceCache
from api.lib.perm.acl.cache import ResourceGroupCache
from api.lib.perm.acl.cache import UserCache
@@ -104,8 +102,8 @@ class ResourceTypeCRUD(object):
@classmethod
def delete(cls, rt_id):
rt = ResourceType.get_by_id(rt_id) or abort(
404, ErrFormat.resource_type_not_found.format("id={}".format(rt_id)))
rt = ResourceType.get_by_id(rt_id) or \
abort(404, ErrFormat.resource_type_not_found.format("id={}".format(rt_id)))
Resource.get_by(resource_type_id=rt_id) and abort(400, ErrFormat.resource_type_cannot_delete)
@@ -167,8 +165,8 @@ class ResourceGroupCRUD(object):
@staticmethod
def add(name, type_id, app_id, uid=None):
ResourceGroup.get_by(name=name, resource_type_id=type_id, app_id=app_id) and abort(
400, ErrFormat.resource_group_exists.format(name))
ResourceGroup.get_by(name=name, resource_type_id=type_id, app_id=app_id) and \
abort(400, ErrFormat.resource_group_exists.format(name))
rg = ResourceGroup.create(name=name, resource_type_id=type_id, app_id=app_id, uid=uid)
AuditCRUD.add_resource_log(app_id, AuditOperateType.create,
@@ -177,8 +175,8 @@ class ResourceGroupCRUD(object):
@staticmethod
def update(rg_id, items):
rg = ResourceGroup.get_by_id(rg_id) or abort(
404, ErrFormat.resource_group_not_found.format("id={}".format(rg_id)))
rg = ResourceGroup.get_by_id(rg_id) or \
abort(404, ErrFormat.resource_group_not_found.format("id={}".format(rg_id)))
existed = ResourceGroupItems.get_by(group_id=rg_id, to_dict=False)
existed_ids = [i.resource_id for i in existed]
@@ -198,8 +196,8 @@ class ResourceGroupCRUD(object):
@staticmethod
def delete(rg_id):
rg = ResourceGroup.get_by_id(rg_id) or abort(
404, ErrFormat.resource_group_not_found.format("id={}".format(rg_id)))
rg = ResourceGroup.get_by_id(rg_id) or \
abort(404, ErrFormat.resource_group_not_found.format("id={}".format(rg_id)))
origin = rg.to_dict()
rg.soft_delete()
@@ -268,8 +266,8 @@ class ResourceCRUD(object):
def add(cls, name, type_id, app_id, uid=None):
type_id = cls._parse_resource_type_id(type_id, app_id)
Resource.get_by(name=name, resource_type_id=type_id, app_id=app_id) and abort(
400, ErrFormat.resource_exists.format(name))
Resource.get_by(name=name, resource_type_id=type_id, app_id=app_id) and \
abort(400, ErrFormat.resource_exists.format(name))
r = Resource.create(name=name, resource_type_id=type_id, app_id=app_id, uid=uid)

View File

@@ -10,9 +10,7 @@ from sqlalchemy import or_
from api.extensions import db
from api.lib.perm.acl.app import AppCRUD
from api.lib.perm.acl.audit import AuditCRUD
from api.lib.perm.acl.audit import AuditOperateType
from api.lib.perm.acl.audit import AuditScope
from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType, AuditScope
from api.lib.perm.acl.cache import AppCache
from api.lib.perm.acl.cache import HasResourceRoleCache
from api.lib.perm.acl.cache import RoleCache
@@ -71,16 +69,16 @@ class RoleRelationCRUD(object):
@staticmethod
def get_parent_ids(rid, app_id):
if app_id is not None:
return ([i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=app_id, to_dict=False)] +
[i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=None, to_dict=False)])
return [i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=app_id, to_dict=False)] + \
[i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=None, to_dict=False)]
else:
return [i.parent_id for i in RoleRelation.get_by(child_id=rid, app_id=app_id, to_dict=False)]
@staticmethod
def get_child_ids(rid, app_id):
if app_id is not None:
return ([i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=app_id, to_dict=False)] +
[i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=None, to_dict=False)])
return [i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=app_id, to_dict=False)] + \
[i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=None, to_dict=False)]
else:
return [i.child_id for i in RoleRelation.get_by(parent_id=rid, app_id=app_id, to_dict=False)]

View File

@@ -6,10 +6,9 @@ import json
import re
from fnmatch import fnmatch
from flask import abort
from flask import abort, current_app
from api.lib.perm.acl.audit import AuditCRUD
from api.lib.perm.acl.audit import AuditOperateType
from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType
from api.lib.perm.acl.cache import UserCache
from api.lib.perm.acl.const import ACL_QUEUE
from api.lib.perm.acl.resp_format import ErrFormat

View File

@@ -9,9 +9,7 @@ from flask import abort
from flask_login import current_user
from api.extensions import db
from api.lib.perm.acl.audit import AuditCRUD
from api.lib.perm.acl.audit import AuditOperateType
from api.lib.perm.acl.audit import AuditScope
from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType, AuditScope
from api.lib.perm.acl.cache import UserCache
from api.lib.perm.acl.resp_format import ErrFormat
from api.lib.perm.acl.role import RoleCRUD
@@ -51,9 +49,11 @@ class UserCRUD(object):
kwargs['block'] = 0
kwargs['key'], kwargs['secret'] = cls.gen_key_secret()
user_employee = db.session.query(User).filter(User.deleted.is_(False)).order_by(User.employee_id.desc()).first()
user_employee = db.session.query(User).filter(User.deleted.is_(False)).order_by(
User.employee_id.desc()).first()
biggest_employee_id = int(float(user_employee.employee_id)) if user_employee is not None else 0
biggest_employee_id = int(float(user_employee.employee_id)) \
if user_employee is not None else 0
kwargs['employee_id'] = '{0:04d}'.format(biggest_employee_id + 1)
user = User.create(**kwargs)

View File

@@ -9,8 +9,6 @@ class CommonErrFormat(object):
not_found = "不存在"
circular_dependency_error = "存在循环依赖!"
unknown_search_error = "未知搜索错误"
invalid_json = "json格式似乎不正确了, 请仔细确认一下!"

View File

@@ -1,6 +1,7 @@
# -*- coding:utf-8 -*-
import base64
import json
import sys
import time
from typing import Set
@@ -112,7 +113,7 @@ class RedisHandler(object):
try:
ret = self.r.hdel(prefix, key_id)
if not ret:
current_app.logger.warning("[{0}] is not in redis".format(key_id))
current_app.logger.warn("[{0}] is not in redis".format(key_id))
except Exception as e:
current_app.logger.error("delete redis key error, {0}".format(str(e)))
@@ -203,9 +204,9 @@ class ESHandler(object):
res = self.es.search(index=self.index, body=query, filter_path=filter_path)
if res['hits'].get('hits'):
return (res['hits']['total']['value'],
[i['_source'] for i in res['hits']['hits']],
res.get("aggregations", {}))
return res['hits']['total']['value'], \
[i['_source'] for i in res['hits']['hits']], \
res.get("aggregations", {})
else:
return 0, [], {}
@@ -256,10 +257,93 @@ class Lock(object):
self.release()
class Redis2Handler(object):
def __init__(self, flask_app=None, prefix=None):
self.flask_app = flask_app
self.prefix = prefix
self.r = None
def init_app(self, app):
self.flask_app = app
config = self.flask_app.config
try:
pool = redis.ConnectionPool(
max_connections=config.get("REDIS_MAX_CONN"),
host=config.get("ONEAGENT_REDIS_HOST"),
port=config.get("ONEAGENT_REDIS_PORT"),
db=config.get("ONEAGENT_REDIS_DB"),
password=config.get("ONEAGENT_REDIS_PASSWORD")
)
self.r = redis.Redis(connection_pool=pool)
except Exception as e:
current_app.logger.warning(str(e))
current_app.logger.error("init redis connection failed")
def get(self, key):
try:
value = json.loads(self.r.get(key))
except:
return
return value
def lrange(self, key, start=0, end=-1):
try:
value = "".join(map(redis_decode, self.r.lrange(key, start, end) or []))
except:
return
return value
def lrange2(self, key, start=0, end=-1):
try:
return list(map(redis_decode, self.r.lrange(key, start, end) or []))
except:
return []
def llen(self, key):
try:
return self.r.llen(key) or 0
except:
return 0
def hget(self, key, field):
try:
return self.r.hget(key, field)
except Exception as e:
current_app.logger.warning("hget redis failed, %s" % str(e))
return
def hset(self, key, field, value):
try:
self.r.hset(key, field, value)
except Exception as e:
current_app.logger.warning("hset redis failed, %s" % str(e))
return
def expire(self, key, timeout):
try:
self.r.expire(key, timeout)
except Exception as e:
current_app.logger.warning("expire redis failed, %s" % str(e))
return
def redis_decode(x):
try:
return x.decode()
except Exception as e:
print(x, e)
try:
return x.decode("gb18030")
except:
return "decode failed"
class AESCrypto(object):
BLOCK_SIZE = 16 # Bytes
pad = lambda s: s + ((AESCrypto.BLOCK_SIZE - len(s) % AESCrypto.BLOCK_SIZE) *
chr(AESCrypto.BLOCK_SIZE - len(s) % AESCrypto.BLOCK_SIZE))
pad = lambda s: s + (AESCrypto.BLOCK_SIZE - len(s) % AESCrypto.BLOCK_SIZE) * \
chr(AESCrypto.BLOCK_SIZE - len(s) % AESCrypto.BLOCK_SIZE)
unpad = lambda s: s[:-ord(s[len(s) - 1:])]
iv = '0102030405060708'
@@ -268,7 +352,7 @@ class AESCrypto(object):
def key():
key = current_app.config.get("SECRET_KEY")[:16]
if len(key) < 16:
key = "{}{}".format(key, (16 - len(key)) * "x")
key = "{}{}".format(key, (16 - len(key) * "x"))
return key.encode('utf8')

View File

@@ -80,10 +80,3 @@ class InternalMessage(Model):
category = db.Column(db.VARCHAR(128), nullable=False)
message_data = db.Column(db.JSON, nullable=True)
employee_id = db.Column(db.Integer, db.ForeignKey('common_employee.employee_id'), comment='ID')
class CommonData(Model):
__table_name__ = 'common_data'
data_type = db.Column(db.VARCHAR(255), default='')
data = db.Column(db.JSON)

View File

@@ -2,8 +2,7 @@
import os
import sys
from inspect import getmembers
from inspect import isclass
from inspect import getmembers, isclass
import six
from flask import jsonify
@@ -28,15 +27,16 @@ class APIView(Resource):
return send_file(*args, **kwargs)
API_PACKAGE = os.path.abspath(os.path.dirname(__file__))
API_PACKAGE = "api"
def register_resources(resource_path, rest_api):
for root, _, files in os.walk(os.path.join(resource_path)):
for filename in files:
if not filename.startswith("_") and filename.endswith("py"):
if root not in sys.path:
sys.path.insert(1, root)
module_path = os.path.join(API_PACKAGE, root[root.index("views"):])
if module_path not in sys.path:
sys.path.insert(1, module_path)
view = __import__(os.path.splitext(filename)[0])
resource_list = [o[0] for o in getmembers(view) if isclass(o[1]) and issubclass(o[1], Resource)]
resource_list = [i for i in resource_list if i != "APIView"]

View File

@@ -5,20 +5,17 @@ import re
from celery_once import QueueOnce
from flask import current_app
from werkzeug.exceptions import BadRequest
from werkzeug.exceptions import NotFound
from werkzeug.exceptions import BadRequest, NotFound
from api.extensions import celery
from api.extensions import db
from api.lib.perm.acl.audit import AuditCRUD
from api.lib.perm.acl.audit import AuditOperateSource
from api.lib.perm.acl.audit import AuditOperateType
from api.lib.perm.acl.cache import AppCache
from api.lib.perm.acl.cache import RoleCache
from api.lib.perm.acl.cache import RoleRelationCache
from api.lib.perm.acl.cache import UserCache
from api.lib.perm.acl.const import ACL_QUEUE
from api.lib.perm.acl.record import OperateRecordCRUD
from api.lib.perm.acl.audit import AuditCRUD, AuditOperateType, AuditOperateSource
from api.models.acl import Resource
from api.models.acl import Role
from api.models.acl import Trigger

View File

@@ -7,7 +7,6 @@ import time
import jinja2
import requests
from flask import current_app
from flask_login import login_user
import api.lib.cmdb.ci
from api.extensions import celery
@@ -19,12 +18,8 @@ from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.cmdb.const import REDIS_PREFIX_CI
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
from api.lib.mail import send_mail
from api.lib.perm.acl.cache import UserCache
from api.lib.utils import Lock
from api.lib.utils import handle_arg_list
from api.models.cmdb import CI
from api.models.cmdb import CIRelation
from api.models.cmdb import CITypeAttribute
@celery.task(name="cmdb.ci_cache", queue=CMDB_QUEUE)
@@ -89,51 +84,6 @@ def ci_relation_cache(parent_id, child_id):
current_app.logger.info("ADD ci relation cache: {0} -> {1}".format(parent_id, child_id))
@celery.task(name="cmdb.ci_relation_add", queue=CMDB_QUEUE)
def ci_relation_add(parent_dict, child_id, uid):
"""
:param parent_dict: key is '$parent_model.attr_name'
:param child_id:
:param uid:
:return:
"""
from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.ci_type import CITypeAttributeManager
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
current_app.test_request_context().push()
login_user(UserCache.get(uid))
db.session.remove()
for parent in parent_dict:
parent_ci_type_name, _attr_name = parent.strip()[1:].split('.', 1)
attr_name = CITypeAttributeManager.get_attr_name(parent_ci_type_name, _attr_name)
if attr_name is None:
current_app.logger.warning("attr name {} does not exist".format(_attr_name))
continue
parent_dict[parent] = handle_arg_list(parent_dict[parent])
for v in parent_dict[parent]:
query = "_type:{},{}:{}".format(parent_ci_type_name, attr_name, v)
s = search(query)
try:
response, _, _, _, _, _ = s.search()
except SearchError as e:
current_app.logger.error('ci relation add failed: {}'.format(e))
continue
for ci in response:
try:
CIRelationManager.add(ci['_id'], child_id)
ci_relation_cache(ci['_id'], child_id)
except Exception as e:
current_app.logger.warning(e)
finally:
db.session.remove()
@celery.task(name="cmdb.ci_relation_delete", queue=CMDB_QUEUE)
def ci_relation_delete(parent_id, child_id):
with Lock("CIRelation_{}".format(parent_id)):
@@ -206,18 +156,3 @@ def trigger_notify(notify, ci_id):
for i in notify['mail_to'] if i], subject, body)
except Exception as e:
current_app.logger.error("Send mail failed: {0}".format(str(e)))
@celery.task(name="cmdb.calc_computed_attribute", queue=CMDB_QUEUE)
def calc_computed_attribute(attr_id, uid):
from api.lib.cmdb.ci import CIManager
db.session.remove()
current_app.test_request_context().push()
login_user(UserCache.get(uid))
for i in CITypeAttribute.get_by(attr_id=attr_id, to_dict=False):
cis = CI.get_by(type_id=i.type_id, to_dict=False)
for ci in cis:
CIManager.update(ci.id, {})

View File

@@ -2,13 +2,12 @@
import datetime
import jwt
import six
import jwt
from flask import abort
from flask import current_app
from flask import request
from flask_login import login_user
from flask_login import logout_user
from flask_login import login_user, logout_user
from api.lib.decorator import args_required
from api.lib.perm.acl.cache import User

View File

@@ -1,5 +1,6 @@
# -*- coding:utf-8 -*-
from flask import g
from flask import request
from flask_login import current_user
@@ -103,7 +104,7 @@ class ResourceView(APIView):
type_id = request.values.get('type_id')
app_id = request.values.get('app_id')
uid = request.values.get('uid')
if not uid and hasattr(current_user, "uid"):
if not uid and hasattr(g, "user") and hasattr(current_user, "uid"):
uid = current_user.uid
resource = ResourceCRUD.add(name, type_id, app_id, uid)

View File

@@ -4,6 +4,7 @@
import requests
from flask import abort
from flask import current_app
from flask import g
from flask import request
from flask import session
from flask_login import current_user
@@ -160,7 +161,7 @@ class UserResetPasswordView(APIView):
if app.name not in ('cas-server', 'acl'):
return abort(403, ErrFormat.invalid_request)
elif hasattr(current_user, 'username'):
elif hasattr(g, 'user'):
if current_user.username != request.values['username']:
return abort(403, ErrFormat.invalid_request)

View File

@@ -33,8 +33,7 @@ class AttributeSearchView(APIView):
class AttributeView(APIView):
url_prefix = ("/attributes", "/attributes/<string:attr_name>", "/attributes/<int:attr_id>",
"/attributes/<int:attr_id>/calc_computed_attribute")
url_prefix = ("/attributes", "/attributes/<string:attr_name>", "/attributes/<int:attr_id>")
def get(self, attr_name=None, attr_id=None):
attr_manager = AttributeManager()
@@ -69,11 +68,6 @@ class AttributeView(APIView):
@args_validate(AttributeManager.cls)
def put(self, attr_id):
if request.url.endswith("/calc_computed_attribute"):
AttributeManager.calc_computed_attribute(attr_id)
return self.jsonify(attr_id=attr_id)
choice_value = handle_arg_list(request.values.get("choice_value"))
params = request.values
params["choice_value"] = choice_value

View File

@@ -11,8 +11,7 @@ from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.ci import CIRelationManager
from api.lib.cmdb.const import ExistPolicy
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import ResourceTypeEnum, PermEnum
from api.lib.cmdb.const import RetKey
from api.lib.cmdb.perms import has_perm_for_ci
from api.lib.cmdb.search import SearchError
@@ -107,7 +106,6 @@ class CIView(APIView):
_is_admin=request.values.pop('__is_admin', False),
**ci_dict)
else:
request.values.pop('exist_policy', None)
ci_id = manager.add(ci_type,
exist_policy=ExistPolicy.REPLACE,
_no_attribute_policy=_no_attribute_policy,

View File

@@ -1,4 +1,4 @@
# -*- coding:utf-8 -*-
# -*- coding:utf-8 -*-
import json
@@ -154,15 +154,9 @@ class EnableCITypeView(APIView):
class CITypeAttributeView(APIView):
url_prefix = ("/ci_types/<int:type_id>/attributes", "/ci_types/<string:type_name>/attributes",
"/ci_types/common_attributes")
url_prefix = ("/ci_types/<int:type_id>/attributes", "/ci_types/<string:type_name>/attributes")
def get(self, type_id=None, type_name=None):
if request.path.endswith("/common_attributes"):
type_ids = handle_arg_list(request.values.get('type_ids'))
return self.jsonify(attributes=CITypeAttributeManager.get_common_attributes(type_ids))
t = CITypeCache.get(type_id) or CITypeCache.get(type_name) or abort(404, ErrFormat.ci_type_not_found)
type_id = t.id
unique_id = t.unique_id
@@ -506,4 +500,3 @@ class CITypeFilterPermissionView(APIView):
@auth_with_app_token
def get(self, type_id):
return self.jsonify(CIFilterPermsCRUD().get(type_id))

View File

@@ -6,9 +6,7 @@ from flask import request
from api.lib.cmdb.ci_type import CITypeManager
from api.lib.cmdb.ci_type import CITypeRelationManager
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.const import PermEnum, ResourceTypeEnum, RoleEnum
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.decorator import args_required
from api.lib.perm.acl.acl import ACLManager
@@ -19,14 +17,9 @@ from api.resource import APIView
class GetChildrenView(APIView):
url_prefix = ("/ci_type_relations/<int:parent_id>/children",
"/ci_type_relations/<int:parent_id>/recursive_level2children",
)
url_prefix = "/ci_type_relations/<int:parent_id>/children"
def get(self, parent_id):
if request.url.endswith("recursive_level2children"):
return self.jsonify(CITypeRelationManager.recursive_level2children(parent_id))
return self.jsonify(children=CITypeRelationManager.get_children(parent_id))

View File

@@ -13,8 +13,7 @@ from api.resource import APIView
class CustomDashboardApiView(APIView):
url_prefix = ("/custom_dashboard", "/custom_dashboard/<int:_id>", "/custom_dashboard/batch",
"/custom_dashboard/preview")
url_prefix = ("/custom_dashboard", "/custom_dashboard/<int:_id>", "/custom_dashboard/batch")
def get(self):
return self.jsonify(CustomDashboardManager.get())
@@ -22,26 +21,17 @@ class CustomDashboardApiView(APIView):
@role_required(RoleEnum.CONFIG)
@args_validate(CustomDashboardManager.cls)
def post(self):
if request.url.endswith("/preview"):
return self.jsonify(counter=CustomDashboardManager.preview(**request.values))
cm = CustomDashboardManager.add(**request.values)
cm, counter = CustomDashboardManager.add(**request.values)
res = cm.to_dict()
res.update(counter=counter)
return self.jsonify(res)
return self.jsonify(cm.to_dict())
@role_required(RoleEnum.CONFIG)
@args_validate(CustomDashboardManager.cls)
def put(self, _id=None):
if _id is not None:
cm, counter = CustomDashboardManager.update(_id, **request.values)
cm = CustomDashboardManager.update(_id, **request.values)
res = cm.to_dict()
res.update(counter=counter)
return self.jsonify(res)
return self.jsonify(cm.to_dict())
CustomDashboardManager.batch_update(request.values.get("id2options"))

View File

@@ -7,8 +7,7 @@ from flask import abort
from flask import request
from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import ResourceTypeEnum, PermEnum
from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.history import AttributeHistoryManger
from api.lib.cmdb.history import CITypeHistoryManager

View File

@@ -5,9 +5,7 @@ from flask import abort
from flask import request
from api.lib.cmdb.ci_type import CITypeManager
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RoleEnum
from api.lib.cmdb.const import PermEnum, ResourceTypeEnum, RoleEnum
from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.cmdb.preference import PreferenceManager
from api.lib.cmdb.resp_format import ErrFormat

View File

@@ -1,35 +0,0 @@
from flask import request
from api.lib.common_setting.common_data import CommonDataCRUD
from api.resource import APIView
prefix = '/data'
class DataView(APIView):
url_prefix = (f'{prefix}/<string:data_type>',)
def get(self, data_type):
data_list = CommonDataCRUD.get_data_by_type(data_type)
return self.jsonify(data_list)
def post(self, data_type):
params = request.json
CommonDataCRUD.create_new_data(data_type, **params)
return self.jsonify(params)
class DataViewWithId(APIView):
url_prefix = (f'{prefix}/<string:data_type>/<int:_id>',)
def put(self, data_type, _id):
params = request.json
res = CommonDataCRUD.update_data(_id, **params)
return self.jsonify(res.to_dict())
def delete(self, data_type, _id):
CommonDataCRUD.delete(_id)
return self.jsonify({})

View File

@@ -16,16 +16,15 @@ class CompanyInfoView(APIView):
return self.jsonify(CompanyInfoCRUD.get())
def post(self):
info = CompanyInfoCRUD.get()
if info:
abort(400, ErrFormat.company_info_is_already_existed)
data = {
'info': {
**request.values
}
}
info = CompanyInfoCRUD.get()
if info:
d = CompanyInfoCRUD.update(info.get('id'), **data)
else:
d = CompanyInfoCRUD.create(**data)
d = CompanyInfoCRUD.create(**data)
res = d.to_dict()
return self.jsonify(res)

View File

@@ -6,9 +6,7 @@ from flask import Blueprint
from flask_restful import Api
from api.resource import register_resources
from .account import AuthWithKeyView
from .account import LoginView
from .account import LogoutView
from .account import LoginView, LogoutView, AuthWithKeyView
HERE = os.path.abspath(os.path.dirname(__file__))

View File

@@ -35,7 +35,6 @@ SQLALCHEMY_ENGINE_OPTIONS = {
CACHE_TYPE = "redis"
CACHE_REDIS_HOST = "127.0.0.1"
CACHE_REDIS_PORT = 6379
CACHE_REDIS_PASSWORD = ""
CACHE_KEY_PREFIX = "CMDB::"
CACHE_DEFAULT_TIMEOUT = 3000
@@ -87,10 +86,12 @@ DEFAULT_PAGE_COUNT = 50
# # permission
WHITE_LIST = ["127.0.0.1"]
USE_ACL = True
USE_ACL = False
# # elastic search
ES_HOST = '127.0.0.1'
USE_ES = False
BOOL_TRUE = ['true', 'TRUE', 'True', True, '1', 1, "Yes", "YES", "yes", 'Y', 'y']
CMDB_API = "http://127.0.0.1:5000/api/v0.1"

View File

@@ -1,11 +1,9 @@
# -*- coding: utf-8 -*-
"""provide some sample data in database"""
import random
import uuid
import random
from api.lib.cmdb.ci import CIManager, CIRelationManager
from api.lib.cmdb.ci_type import CITypeAttributeManager
from api.models.acl import User
from api.models.cmdb import (
Attribute,
CIType,
@@ -14,12 +12,16 @@ from api.models.cmdb import (
CITypeRelation,
RelationType
)
from api.models.acl import User
from api.lib.cmdb.ci_type import CITypeAttributeManager
from api.lib.cmdb.ci import CIManager, CIRelationManager
def force_add_user():
from flask_login import current_user, login_user
if not getattr(current_user, "username", None):
login_user(User.query.first())
from flask import g
if not getattr(g, "user", None):
g.user = User.query.first()
def init_attributes(num=1):
@@ -76,12 +78,12 @@ def init_relation_type(num=1):
def init_ci_type_relation(num=1):
result = []
ci_types = init_ci_types(num + 1)
ci_types = init_ci_types(num+1)
relation_types = init_relation_type(num)
for i in range(num):
result.append(CITypeRelation.create(
parent_id=ci_types[i].id,
child_id=ci_types[i + 1].id,
child_id=ci_types[i+1].id,
relation_type_id=relation_types[i].id
))
return result

View File

@@ -53,7 +53,7 @@
"vuedraggable": "^2.23.0",
"vuex": "^3.1.1",
"vxe-table": "3.6.9",
"vxe-table-plugin-export-xlsx": "2.0.0",
"vxe-table-plugin-export-xlsx": "^3.0.4",
"xe-utils": "3",
"xlsx": "0.15.0",
"xlsx-js-style": "^1.2.0"

View File

@@ -54,84 +54,6 @@
<div class="content unicode" style="display: block;">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont">&#xe886;</span>
<div class="name">cmdb-histogram</div>
<div class="code-name">&amp;#xe886;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe883;</span>
<div class="name">cmdb-index</div>
<div class="code-name">&amp;#xe883;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe884;</span>
<div class="name">cmdb-piechart</div>
<div class="code-name">&amp;#xe884;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe885;</span>
<div class="name">cmdb-line</div>
<div class="code-name">&amp;#xe885;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe882;</span>
<div class="name">cmdb-table</div>
<div class="code-name">&amp;#xe882;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe87f;</span>
<div class="name">itsm-all</div>
<div class="code-name">&amp;#xe87f;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe87e;</span>
<div class="name">itsm-reply</div>
<div class="code-name">&amp;#xe87e;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe880;</span>
<div class="name">itsm-information</div>
<div class="code-name">&amp;#xe880;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe881;</span>
<div class="name">itsm-contact</div>
<div class="code-name">&amp;#xe881;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe87d;</span>
<div class="name">itsm-my-processed</div>
<div class="code-name">&amp;#xe87d;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe87c;</span>
<div class="name">rule_7</div>
<div class="code-name">&amp;#xe87c;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe879;</span>
<div class="name">itsm-my-completed</div>
<div class="code-name">&amp;#xe879;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe87b;</span>
<div class="name">itsm-my-plan</div>
<div class="code-name">&amp;#xe87b;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe87a;</span>
<div class="name">rule_100</div>
@@ -3954,9 +3876,9 @@
<pre><code class="language-css"
>@font-face {
font-family: 'iconfont';
src: url('iconfont.woff2?t=1694508259411') format('woff2'),
url('iconfont.woff?t=1694508259411') format('woff'),
url('iconfont.ttf?t=1694508259411') format('truetype');
src: url('iconfont.woff2?t=1688550067963') format('woff2'),
url('iconfont.woff?t=1688550067963') format('woff'),
url('iconfont.ttf?t=1688550067963') format('truetype');
}
</code></pre>
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@@ -3982,123 +3904,6 @@
<div class="content font-class">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont cmdb-bar"></span>
<div class="name">
cmdb-histogram
</div>
<div class="code-name">.cmdb-bar
</div>
</li>
<li class="dib">
<span class="icon iconfont cmdb-count"></span>
<div class="name">
cmdb-index
</div>
<div class="code-name">.cmdb-count
</div>
</li>
<li class="dib">
<span class="icon iconfont cmdb-pie"></span>
<div class="name">
cmdb-piechart
</div>
<div class="code-name">.cmdb-pie
</div>
</li>
<li class="dib">
<span class="icon iconfont cmdb-line"></span>
<div class="name">
cmdb-line
</div>
<div class="code-name">.cmdb-line
</div>
</li>
<li class="dib">
<span class="icon iconfont cmdb-table"></span>
<div class="name">
cmdb-table
</div>
<div class="code-name">.cmdb-table
</div>
</li>
<li class="dib">
<span class="icon iconfont itsm-all"></span>
<div class="name">
itsm-all
</div>
<div class="code-name">.itsm-all
</div>
</li>
<li class="dib">
<span class="icon iconfont itsm-reply"></span>
<div class="name">
itsm-reply
</div>
<div class="code-name">.itsm-reply
</div>
</li>
<li class="dib">
<span class="icon iconfont itsm-information"></span>
<div class="name">
itsm-information
</div>
<div class="code-name">.itsm-information
</div>
</li>
<li class="dib">
<span class="icon iconfont itsm-contact"></span>
<div class="name">
itsm-contact
</div>
<div class="code-name">.itsm-contact
</div>
</li>
<li class="dib">
<span class="icon iconfont itsm-my-my_already_handle"></span>
<div class="name">
itsm-my-processed
</div>
<div class="code-name">.itsm-my-my_already_handle
</div>
</li>
<li class="dib">
<span class="icon iconfont rule_7"></span>
<div class="name">
rule_7
</div>
<div class="code-name">.rule_7
</div>
</li>
<li class="dib">
<span class="icon iconfont itsm-my-completed"></span>
<div class="name">
itsm-my-completed
</div>
<div class="code-name">.itsm-my-completed
</div>
</li>
<li class="dib">
<span class="icon iconfont itsm-my-plan"></span>
<div class="name">
itsm-my-plan
</div>
<div class="code-name">.itsm-my-plan
</div>
</li>
<li class="dib">
<span class="icon iconfont rule_100"></span>
<div class="name">
@@ -5954,11 +5759,11 @@
</li>
<li class="dib">
<span class="icon iconfont itsm-node-start"></span>
<span class="icon iconfont itsm-node-strat"></span>
<div class="name">
itsm-node-strat
</div>
<div class="code-name">.itsm-node-start
<div class="code-name">.itsm-node-strat
</div>
</li>
@@ -9832,110 +9637,6 @@
<div class="content symbol">
<ul class="icon_lists dib-box">
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#cmdb-bar"></use>
</svg>
<div class="name">cmdb-histogram</div>
<div class="code-name">#cmdb-bar</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#cmdb-count"></use>
</svg>
<div class="name">cmdb-index</div>
<div class="code-name">#cmdb-count</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#cmdb-pie"></use>
</svg>
<div class="name">cmdb-piechart</div>
<div class="code-name">#cmdb-pie</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#cmdb-line"></use>
</svg>
<div class="name">cmdb-line</div>
<div class="code-name">#cmdb-line</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#cmdb-table"></use>
</svg>
<div class="name">cmdb-table</div>
<div class="code-name">#cmdb-table</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-all"></use>
</svg>
<div class="name">itsm-all</div>
<div class="code-name">#itsm-all</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-reply"></use>
</svg>
<div class="name">itsm-reply</div>
<div class="code-name">#itsm-reply</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-information"></use>
</svg>
<div class="name">itsm-information</div>
<div class="code-name">#itsm-information</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-contact"></use>
</svg>
<div class="name">itsm-contact</div>
<div class="code-name">#itsm-contact</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-my-my_already_handle"></use>
</svg>
<div class="name">itsm-my-processed</div>
<div class="code-name">#itsm-my-my_already_handle</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#rule_7"></use>
</svg>
<div class="name">rule_7</div>
<div class="code-name">#rule_7</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-my-completed"></use>
</svg>
<div class="name">itsm-my-completed</div>
<div class="code-name">#itsm-my-completed</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-my-plan"></use>
</svg>
<div class="name">itsm-my-plan</div>
<div class="code-name">#itsm-my-plan</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#rule_100"></use>
@@ -11586,10 +11287,10 @@
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#itsm-node-start"></use>
<use xlink:href="#itsm-node-strat"></use>
</svg>
<div class="name">itsm-node-strat</div>
<div class="code-name">#itsm-node-start</div>
<div class="code-name">#itsm-node-strat</div>
</li>
<li class="dib">

View File

@@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 3857903 */
src: url('iconfont.woff2?t=1694508259411') format('woff2'),
url('iconfont.woff?t=1694508259411') format('woff'),
url('iconfont.ttf?t=1694508259411') format('truetype');
src: url('iconfont.woff2?t=1688550067963') format('woff2'),
url('iconfont.woff?t=1688550067963') format('woff'),
url('iconfont.ttf?t=1688550067963') format('truetype');
}
.iconfont {
@@ -13,58 +13,6 @@
-moz-osx-font-smoothing: grayscale;
}
.cmdb-bar:before {
content: "\e886";
}
.cmdb-count:before {
content: "\e883";
}
.cmdb-pie:before {
content: "\e884";
}
.cmdb-line:before {
content: "\e885";
}
.cmdb-table:before {
content: "\e882";
}
.itsm-all:before {
content: "\e87f";
}
.itsm-reply:before {
content: "\e87e";
}
.itsm-information:before {
content: "\e880";
}
.itsm-contact:before {
content: "\e881";
}
.itsm-my-my_already_handle:before {
content: "\e87d";
}
.rule_7:before {
content: "\e87c";
}
.itsm-my-completed:before {
content: "\e879";
}
.itsm-my-plan:before {
content: "\e87b";
}
.rule_100:before {
content: "\e87a";
}
@@ -889,7 +837,7 @@
content: "\e7ad";
}
.itsm-node-start:before {
.itsm-node-strat:before {
content: "\e7ae";
}

File diff suppressed because one or more lines are too long

View File

@@ -5,97 +5,6 @@
"css_prefix_text": "",
"description": "",
"glyphs": [
{
"icon_id": "37334642",
"name": "cmdb-histogram",
"font_class": "cmdb-bar",
"unicode": "e886",
"unicode_decimal": 59526
},
{
"icon_id": "37334651",
"name": "cmdb-index",
"font_class": "cmdb-count",
"unicode": "e883",
"unicode_decimal": 59523
},
{
"icon_id": "37334650",
"name": "cmdb-piechart",
"font_class": "cmdb-pie",
"unicode": "e884",
"unicode_decimal": 59524
},
{
"icon_id": "37334648",
"name": "cmdb-line",
"font_class": "cmdb-line",
"unicode": "e885",
"unicode_decimal": 59525
},
{
"icon_id": "37334627",
"name": "cmdb-table",
"font_class": "cmdb-table",
"unicode": "e882",
"unicode_decimal": 59522
},
{
"icon_id": "37310392",
"name": "itsm-all",
"font_class": "itsm-all",
"unicode": "e87f",
"unicode_decimal": 59519
},
{
"icon_id": "36998696",
"name": "itsm-reply",
"font_class": "itsm-reply",
"unicode": "e87e",
"unicode_decimal": 59518
},
{
"icon_id": "36639018",
"name": "itsm-information",
"font_class": "itsm-information",
"unicode": "e880",
"unicode_decimal": 59520
},
{
"icon_id": "36639017",
"name": "itsm-contact",
"font_class": "itsm-contact",
"unicode": "e881",
"unicode_decimal": 59521
},
{
"icon_id": "36557425",
"name": "itsm-my-processed",
"font_class": "itsm-my-my_already_handle",
"unicode": "e87d",
"unicode_decimal": 59517
},
{
"icon_id": "36488174",
"name": "rule_7",
"font_class": "rule_7",
"unicode": "e87c",
"unicode_decimal": 59516
},
{
"icon_id": "36380087",
"name": "itsm-my-completed",
"font_class": "itsm-my-completed",
"unicode": "e879",
"unicode_decimal": 59513
},
{
"icon_id": "36380096",
"name": "itsm-my-plan",
"font_class": "itsm-my-plan",
"unicode": "e87b",
"unicode_decimal": 59515
},
{
"icon_id": "36304673",
"name": "rule_100",
@@ -1541,7 +1450,7 @@
{
"icon_id": "35024980",
"name": "itsm-node-strat",
"font_class": "itsm-node-start",
"font_class": "itsm-node-strat",
"unicode": "e7ae",
"unicode_decimal": 59310
},

Binary file not shown.

View File

@@ -20,7 +20,13 @@ export function putCompanyInfo(id, parameter) {
data: parameter,
})
}
export function postImageFile(parameter) {
return axios({
url: '/common-setting/v1/file',
method: 'post',
data: parameter,
})
}
export function getDepartmentList(params) {
// ?department_parent_id=-1 查询第一级部门下面的id根据实际的传
return axios({

View File

@@ -1,31 +0,0 @@
import { axios } from '@/utils/request'
export function postImageFile(parameter) {
return axios({
url: '/common-setting/v1/file',
method: 'post',
data: parameter,
})
}
export function getFileData(data_type) {
return axios({
url: `/common-setting/v1/data/${data_type}`,
method: 'get',
})
}
export function addFileData(data_type, data) {
return axios({
url: `/common-setting/v1/data/${data_type}`,
method: 'post',
data,
})
}
export function deleteFileData(data_type, id) {
return axios({
url: `/common-setting/v1/data/${data_type}/${id}`,
method: 'delete',
})
}

View File

@@ -68,8 +68,7 @@ export default {
},
methods: {
visibleChange(open, isInitOne = true) {
// isInitOne 初始化exp为空时ruleList是否默认给一条
visibleChange(open) {
// const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
const exp = this.expression.match(new RegExp(this.regQ, 'g'))
? this.expression.match(new RegExp(this.regQ, 'g'))[0]
@@ -152,20 +151,15 @@ export default {
})
this.ruleList = [...expArray]
} else if (open) {
this.ruleList = isInitOne
? [
{
id: uuidv4(),
type: 'and',
property:
this.canSearchPreferenceAttrList && this.canSearchPreferenceAttrList.length
? this.canSearchPreferenceAttrList[0].name
: undefined,
exp: 'is',
value: null,
},
]
: []
this.ruleList = [
{
id: uuidv4(),
type: 'and',
property: this.canSearchPreferenceAttrList[0].name,
exp: 'is',
value: null,
},
]
}
},
handleClear() {

View File

@@ -15,117 +15,31 @@
>
{{ item.label }}
</div>
<div :class="`${currentIconType === '4' ? 'selected' : ''}`" @click="handleChangeIconType('4')">
自定义
</div>
<a-upload
slot="description"
name="avatar"
:before-upload="beforeUpload"
:show-upload-list="false"
accept=".svg,.png,.jpg,.jpeg"
v-if="currentIconType === '4'"
>
<a-button icon="plus" size="small" type="primary">添加</a-button>
</a-upload>
</div>
<div class="custom-icon-select-popover-content">
<template v-if="iconList && iconList.length">
<template v-if="currentIconType !== '4'">
<div v-for="category in iconList" :key="category.value">
<h4 class="category">{{ category.label }}</h4>
<div class="custom-icon-select-popover-content-wrapper">
<div
v-for="name in category.list"
:key="name.value"
:class="`custom-icon-select-popover-item ${value.name === name.value ? 'selected' : ''}`"
@click="clickIcon(name.value)"
>
<ops-icon :type="name.value" />
<span class="custom-icon-select-popover-item-label">{{ name.label }}</span>
</div>
</div>
</div>
</template>
<div class="custom-icon-select-popover-content-wrapper" :style="{ marginTop: '10px' }" v-else>
<div v-for="category in iconList" :key="category.value">
<h4 class="category">{{ category.label }}</h4>
<div class="custom-icon-select-popover-content-wrapper">
<div
v-for="icon in iconList"
:key="icon.id"
:class="`custom-icon-select-popover-item ${value.id === icon.id ? 'selected' : ''}`"
@click="clickCustomIcon(icon)"
v-for="name in category.list"
:key="name.value"
:class="`custom-icon-select-popover-item ${value.name === name.value ? 'selected' : ''}`"
@click="clickIcon(name.value)"
>
<div class="custom-icon-select-popover-content-img-box">
<img :src="`/api/common-setting/v1/file/${icon.data.url}`" />
<a-popconfirm
overlayClassName="custom-icon-select-confirm-popover"
:getPopupContainer="(trigger) => trigger.parentNode"
title="确认删除?"
@confirm="(e) => deleteIcon(e, icon)"
@cancel="
(e) => {
e.stopPropagation()
e.preventDefault()
}
"
>
<a-icon
type="close"
@click="
(e) => {
e.stopPropagation()
e.preventDefault()
}
"
/>
</a-popconfirm>
</div>
<span class="custom-icon-select-popover-item-label" :title="icon.data.name">{{ icon.data.name }}</span>
<ops-icon :type="name.value" />
<span class="custom-icon-select-popover-item-label">{{ name.label }}</span>
</div>
</div>
</template>
<a-empty v-else :style="{ marginTop: '15%' }">
<img slot="image" :src="require('@/assets/data_empty.png')" />
<a-upload
slot="description"
name="avatar"
:before-upload="beforeUpload"
:show-upload-list="false"
accept=".svg,.png,.jpg,.jpeg"
>
<a> 暂无自定义图标点击此处上传 </a>
</a-upload>
</a-empty>
</div>
</div>
<template v-if="!['0', '3', '4'].includes(currentIconType)">
<template v-if="currentIconType !== '0' && currentIconType !== '3'">
<a-divider :style="{ margin: '5px 0' }" />
<el-color-picker size="mini" v-model="value.color"> </el-color-picker>
</template>
<a-form class="custom-icon-select-form" :form="form" v-show="currentIconType === '4' && formVisible">
<a-form-item
label="名称"
:labelCol="{ span: 4 }"
:wrapperCol="{ span: 16 }"
><a-input
v-decorator="['name', { rules: [{ required: true, message: '请输入名称' }] }]"
/></a-form-item>
<a-form-item label="预览" :labelCol="{ span: 4 }">
<div class="custom-icon-select-form-img">
<img :src="formImg" />
</div>
</a-form-item>
<a-form-item label=" " :colon="false" :labelCol="{ span: 16 }">
<a-space>
<a-button size="small" @click="handleCancel">取消</a-button>
<a-button size="small" type="primary" @click="handleOk">确定</a-button>
</a-space>
</a-form-item>
</a-form>
</div>
<div class="custom-icon-select-block" id="custom-icon-select-block" @click="showSelect">
<img v-if="value.id && value.url" :src="`/api/common-setting/v1/file/${value.url}`" />
<ops-icon
v-else
:type="value.name"
:style="{ color: value.name && value.name.startsWith('icon-') ? value.color || '' : '' }"
/>
@@ -142,8 +56,6 @@ import {
fillIconList,
multicolorIconList,
} from './constants'
import { postImageFile, getFileData, addFileData, deleteFileData } from '@/api/file'
export default {
name: 'CustomIconSelect',
components: { ElColorPicker: ColorPicker },
@@ -165,18 +77,13 @@ export default {
},
data() {
return {
form: this.$form.createForm(this),
iconTypeList,
commonIconList,
linearIconList,
fillIconList,
multicolorIconList,
visible: false,
currentIconType: '3',
customIconList: [],
formVisible: false,
formImg: null,
file: null,
currentIconType: '1',
}
},
computed: {
@@ -190,30 +97,18 @@ export default {
return this.fillIconList
case '3': // 多色
return this.multicolorIconList
case '4': // 自定义
return this.customIconList
default:
return this.linearIconList
}
},
fileName() {
const splitFileName = this.file.name.split('.')
return splitFileName.splice(0, splitFileName.length - 1).join('')
},
},
mounted() {
document.addEventListener('click', this.eventListener)
this.getFileData()
},
beforeDestroy() {
document.removeEventListener('click', this.eventListener)
},
methods: {
getFileData() {
getFileData('ops-custom-icon').then((res) => {
this.customIconList = res
})
},
eventListener(e) {
if (this.visible) {
const dom = document.getElementById(`custom-icon-select-popover`)
@@ -242,87 +137,25 @@ export default {
})
}
},
clickCustomIcon(icon) {
if (icon.id === this.value.id) {
this.$emit('change', {
name: '',
color: '',
})
} else {
this.$emit('change', { name: icon.data.name, id: icon.id, url: icon.data.url })
}
},
showSelect() {
this.visible = true
console.log(this.value)
if (!this.value.name) {
this.currentIconType = '3'
this.currentIconType = '1'
return
}
// changyong已废弃
if (this.value.name.startsWith('changyong-')) {
this.currentIconType = '0'
} else if (this.value.name.startsWith('icon-xianxing')) {
this.currentIconType = '1'
} else if (this.value.name.startsWith('icon-shidi')) {
this.currentIconType = '2'
} else if (this.value.name.startsWith('caise')) {
this.currentIconType = '3'
} else {
this.currentIconType = '4'
this.currentIconType = '3'
}
},
handleChangeIconType(value) {
this.currentIconType = value
},
beforeUpload(file) {
const isLt2M = file.size / 1024 / 1024 < 2
if (!isLt2M) {
this.$message.error('图片大小不可超过2MB!')
return false
}
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onload = () => {
this.formVisible = true
this.$nextTick(() => {
this.file = file
this.formImg = reader.result
this.form.setFieldsValue({ name: this.fileName })
})
}
return false
},
handleCancel() {
this.formVisible = false
this.form.setFieldsValue({ name: '' })
this.formImg = null
},
handleOk() {
const fm = new FormData()
fm.append('file', this.file)
postImageFile(fm).then((res) => {
this.form.validateFields((err, values) => {
if (!err) {
addFileData('ops-custom-icon', { data: { name: values.name, url: res.file_name } }).then(() => {
this.$message.success('上传成功!')
this.handleCancel()
this.getFileData()
})
}
})
})
},
deleteIcon(e, icon) {
e.stopPropagation()
e.preventDefault()
deleteFileData('ops-custom-icon', icon.id).then(() => {
this.$message.success('删除成功!')
this.handleCancel()
this.getFileData()
})
},
},
}
</script>
@@ -343,7 +176,7 @@ export default {
padding: 4px 6px;
}
.custom-icon-select-popover-content {
height: 400px;
max-height: 400px;
overflow: auto;
.category {
font-size: 14px;
@@ -364,43 +197,12 @@ export default {
padding: 5px 5px 2px 5px;
margin: 0 2px 6px;
color: #666;
position: relative;
.custom-icon-select-popover-item-label {
margin-top: 6px;
font-size: 11px;
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
}
&:hover {
background-color: #eeeeee;
.custom-icon-select-popover-content-img-box > i {
display: inline;
}
}
.custom-icon-select-popover-content-img-box {
width: 26px;
height: 26px;
display: flex;
align-items: center;
justify-content: center;
> img {
max-width: 26px;
max-height: 26px;
}
> i {
display: none;
position: absolute;
top: 2px;
right: 2px;
font-size: 12px;
&:hover {
color: #2f54eb;
}
}
}
}
.selected {
@@ -410,8 +212,6 @@ export default {
}
.custom-icon-select-popover-icon-type {
display: inline-block;
width: 100%;
position: relative;
> div {
cursor: pointer;
display: inline-block;
@@ -424,16 +224,6 @@ export default {
.selected {
border-color: #2f54eb;
}
.ant-btn {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
}
}
.custom-icon-select-confirm-popover .ant-popover-inner-content {
width: 150px;
}
}
</style>
@@ -444,39 +234,15 @@ export default {
width: 28px;
height: 28px;
border-radius: 4px;
border: 1px solid #d9d9d9;
border: 1px solid #eeeeee;
display: inline-block;
cursor: pointer;
> i,
> img {
> i {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
> img {
max-width: 26px;
max-height: 26px;
}
> i {
font-size: 18px;
}
}
.custom-icon-select-form {
.custom-icon-select-form-img {
width: 28px;
height: 28px;
border-radius: 4px;
border: 1px solid #d9d9d9;
display: inline-flex;
margin-top: 5px;
justify-content: center;
align-items: center;
overflow: hidden;
img {
max-width: 26px;
max-height: 26px;
}
}
}
</style>

View File

@@ -222,9 +222,6 @@ export default {
renderIcon({ icon, selectedIcon, customIcon = undefined, name = undefined, typeId = undefined, routeName }) {
if (typeId) {
if (customIcon) {
if (customIcon.split('$$')[2]) {
return <img style={{ maxHeight: '14px', maxWidth: '14px', marginRight: '10px' }} src={`/api/common-setting/v1/file/${customIcon.split('$$')[3]}`}></img >
}
return <ops-icon
style={{
color: customIcon.split('$$')[1],

View File

@@ -77,14 +77,6 @@ export function getCITypeAttributesByTypeIds(params) {
})
}
export function getCITypeCommonAttributesByTypeIds(params) {
return axios({
url: `/v0.1/ci_types/common_attributes`,
method: 'get',
params: params
})
}
/**
* 删除属性
* @param attrId
@@ -161,10 +153,3 @@ export function canDefineComputed() {
method: 'HEAD',
})
}
export function calcComputedAttribute(attr_id) {
return axios({
url: `/v0.1/attributes/${attr_id}/calc_computed_attribute`,
method: 'PUT',
})
}

View File

@@ -61,10 +61,3 @@ export function revokeTypeRelation(first_type_id, second_type_id, rid, data) {
data
})
}
export function getRecursive_level2children(type_id) {
return axios({
url: `/v0.1/ci_type_relations/${type_id}/recursive_level2children`,
method: 'GET'
})
}

View File

@@ -37,11 +37,3 @@ export function batchUpdateCustomDashboard(data) {
data
})
}
export function postCustomDashboardPreview(data) {
return axios({
url: '/v0.1/custom_dashboard/preview',
method: 'post',
data
})
}

View File

@@ -1,7 +1,5 @@
/* eslint-disable */
import _ from 'lodash'
import XLSX from 'xlsx'
import XLSXS from 'xlsx-js-style'
export function sum(arr) {
if (!arr.length) {
return 0
@@ -151,26 +149,4 @@ export const toThousands = (num = 0) => {
return num.toString().replace(/\d+/, function (n) {
return n.replace(/(\d)(?=(?:\d{3})+$)/g, '$1,')
})
}
export const downloadExcel = (data, fileName = `${moment().format('YYYY-MM-DD HH:mm:ss')}.xls`) => {
// STEP 1: Create a new workbook
const wb = XLSXS.utils.book_new()
// STEP 2: Create data rows and styles
const rowArray = data
// STEP 3: Create worksheet with rows; Add worksheet to workbook
const ws = XLSXS.utils.aoa_to_sheet(rowArray)
XLSXS.utils.book_append_sheet(wb, ws, fileName)
let maxColumnNumber = 1 // 默认最大列数
rowArray.forEach(item => { if (item.length > maxColumnNumber) { maxColumnNumber = item.length } })
// 添加列宽
ws['!cols'] = (rowArray[0].map(item => {
return { width: 22 }
}))
// // 添加行高
// ws['!rows'] = [{ 'hpt': 80 }]
// STEP 4: Write Excel file to browser #导出
XLSXS.writeFile(wb, fileName + '.xlsx')
}
}

View File

@@ -14,83 +14,20 @@
}}</a-select-option>
</a-select>
<a-button
@click="openModal"
@click="downLoadExcel"
:disabled="!selectNum"
type="primary"
class="ops-button-primary"
icon="download"
>下载模板</a-button
>
<a-modal
:bodyStyle="{ paddingTop: 0 }"
width="800px"
:title="`${ciTypeName}`"
:visible="visible"
@cancel="handleCancel"
@ok="handleOk"
wrapClassName="ci-type-choice-modal"
>
<a-divider orientation="left">模型属性</a-divider>
<a-checkbox
@change="changeCheckAll"
:style="{ marginBottom: '20px' }"
:indeterminate="indeterminate"
:checked="checkAll"
>
全选
</a-checkbox>
<br />
<a-checkbox-group v-model="checkedAttrs">
<a-row>
<a-col :span="6" v-for="item in selectCiTypeAttrList.attributes" :key="item.alias || item.name">
<a-checkbox :disabled="item.name === selectCiTypeAttrList.unique" :value="item.alias || item.name">
{{ item.alias || item.name }}
<span style="color: red" v-if="item.name === selectCiTypeAttrList.unique">*</span>
</a-checkbox>
</a-col>
</a-row>
</a-checkbox-group>
<template v-if="parentsType && parentsType.length">
<a-divider orientation="left">模型关联</a-divider>
<a-row :gutter="[24, 24]" align="top" type="flex">
<a-col :style="{ display: 'inline-flex' }" :span="12" v-for="item in parentsType" :key="item.id">
<a-checkbox @click="clickParent(item)" :checked="checkedParents.includes(item.alias || item.name)">
</a-checkbox>
<span
:style="{
display: 'inline-block',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
width: '80px',
margin: '0 5px',
textAlign: 'right',
}"
:title="item.alias || item.name"
>{{ item.alias || item.name }}</span
>
<a-select :style="{ flex: 1 }" size="small" v-model="parentsForm[item.alias || item.name].attr">
<a-select-option
:title="attr.alias || attr.name"
v-for="attr in item.attributes"
:key="attr.alias || attr.name"
:value="attr.alias || attr.name"
>
{{ attr.alias || attr.name }}
</a-select-option>
</a-select>
</a-col>
</a-row>
</template>
</a-modal>
</a-space>
</template>
<script>
import { downloadExcel } from '../../../utils/helper'
import { getCITypes } from '@/modules/cmdb/api/CIType'
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
import { getCITypeParent } from '@/modules/cmdb/api/CITypeRelation'
import { writeExcel } from '@/modules/cmdb/api/batch'
export default {
name: 'CiTypeChoice',
@@ -100,13 +37,6 @@ export default {
ciTypeName: '',
selectNum: 0,
selectCiTypeAttrList: [],
visible: false,
checkedAttrs: [],
indeterminate: false,
checkAll: true,
parentsType: [],
parentsForm: {},
checkedParents: [],
}
},
created: function() {
@@ -114,18 +44,6 @@ export default {
this.ciTypeList = res.ci_types
})
},
watch: {
checkedAttrs() {
if (this.checkedAttrs.length < this.selectCiTypeAttrList.attributes.length) {
this.indeterminate = true
this.checkAll = false
}
if (this.checkedAttrs.length === this.selectCiTypeAttrList.attributes.length) {
this.indeterminate = false
this.checkAll = true
}
},
},
methods: {
selectCiType(el) {
// 当选择好模板类型时的回调函数
@@ -142,70 +60,24 @@ export default {
})
},
openModal() {
getCITypeParent(this.selectNum).then((res) => {
this.parentsType = res.parents
const _parentsForm = {}
res.parents.forEach((item) => {
const _find = item.attributes.find((attr) => attr.id === item.unique_id)
_parentsForm[item.alias || item.name] = { attr: _find?.alias || _find?.name, value: '' }
})
this.parentsForm = _parentsForm
this.checkedParents = []
this.visible = true
this.checkedAttrs = this.selectCiTypeAttrList.attributes.map((item) => item.alias || item.name)
downLoadExcel() {
const columns = []
this.selectCiTypeAttrList.attributes.forEach((item) => {
columns.push(item.alias)
})
const excel = writeExcel(columns, this.ciTypeName)
const tempLink = document.createElement('a')
tempLink.download = this.ciTypeName + '.xls'
tempLink.style.display = 'none'
const blob = new Blob([excel])
tempLink.href = URL.createObjectURL(blob)
document.body.appendChild(tempLink)
tempLink.click()
document.body.removeChild(tempLink)
},
filterOption(input, option) {
return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
},
handleCancel() {
this.visible = false
},
handleOk() {
const columns1 = this.checkedAttrs.map((item) => {
return {
v: item,
t: 's',
s: {
numFmt: 'string',
},
}
})
const columns2 = this.checkedParents.map((p) => {
return {
v: `$${p}.${this.parentsForm[p].attr}`,
t: 's',
s: {
font: {
color: {
rgb: 'FF0000',
},
},
},
}
})
downloadExcel([[...columns1, ...columns2]], this.ciTypeName)
this.handleCancel()
},
changeCheckAll(e) {
if (e.target.checked) {
this.checkedAttrs = this.selectCiTypeAttrList.attributes.map((item) => item.alias || item.name)
} else {
const _find = this.selectCiTypeAttrList.attributes.find(
(item) => item.name === this.selectCiTypeAttrList.unique
)
this.checkedAttrs = [_find?.alias || _find?.name]
}
},
clickParent(item) {
const _idx = this.checkedParents.findIndex((p) => p === (item.alias || item.name))
if (_idx > -1) {
this.checkedParents.splice(_idx, 1)
} else {
this.checkedParents.push(item.alias || item.name)
}
},
},
}
</script>
@@ -233,15 +105,3 @@ export default {
}
}
</style>
<style lang="less">
.ci-type-choice-modal {
.ant-checkbox-disabled .ant-checkbox-inner {
border-color: #2f54eb !important;
background-color: #2f54eb;
}
.ant-checkbox-disabled.ant-checkbox-checked .ant-checkbox-inner::after {
border-color: #fff;
}
}
</style>

View File

@@ -40,25 +40,15 @@ export default {
},
computed: {
columns() {
const _columns = []
if (this.ciTypeAttrs.attributes) {
_columns.push(
...this.ciTypeAttrs.attributes.map((item) => {
return {
title: item.alias || item.name,
field: item.alias || item.name,
}
})
)
}
if (this.uploadData && this.uploadData.length) {
Object.keys(this.uploadData[0]).forEach((key) => {
if (key.startsWith('$')) {
_columns.push({ title: key, field: key })
return this.ciTypeAttrs.attributes.map((item) => {
return {
title: item.alias || item.name,
field: item.alias || item.name,
}
})
}
return _columns
return []
},
dataSource() {
return _.cloneDeep(this.uploadData)

View File

@@ -4,13 +4,13 @@
ref="upload"
:multiple="false"
:customRequest="customRequest"
accept=".xls,.xlsx"
accept=".xls"
:showUploadList="false"
:fileList="fileList"
>
<img :style="{ width: '80px', height: '80px' }" src="@/assets/file_upload.png" />
<p class="ant-upload-text">点击或拖拽文件至此上传</p>
<p class="ant-upload-hint">支持文件类型xlsxlsx</p>
<p class="ant-upload-hint">支持文件类型xls</p>
</a-upload-dragger>
<div v-for="item in fileList" :key="item.name" class="cmdb-batch-upload-dragger-file">
<span><a-icon type="file" :style="{ color: '#2F54EB', marginRight: '5px' }" />{{ item.name }}</span>

View File

@@ -124,21 +124,12 @@
:key="'edit_' + col.field + idx"
v-for="(choice, idx) in col.filters"
>
<span
:style="{ ...(choice[1] ? choice[1].style : {}), display: 'inline-flex', alignItems: 'center' }"
>
<template v-if="choice[1] && choice[1].icon && choice[1].icon.name">
<img
v-if="choice[1].icon.id && choice[1].icon.url"
:src="`/api/common-setting/v1/file/${choice[1].icon.url}`"
:style="{ maxHeight: '13px', maxWidth: '13px', marginRight: '5px' }"
/>
<ops-icon
v-else
:style="{ color: choice[1].icon.color, marginRight: '5px' }"
:type="choice[1].icon.name"
/>
</template>
<span :style="choice[1] ? choice[1].style || {} : {}">
<ops-icon
:style="{ color: choice[1].icon.color }"
v-if="choice[1] && choice[1].icon && choice[1].icon.name"
:type="choice[1].icon.name"
/>
{{ choice[0] }}
</span>
</a-select-option>
@@ -161,18 +152,10 @@
padding: '1px 5px',
margin: '2px',
...getChoiceValueStyle(col, value),
display: 'inline-flex',
alignItems: 'center',
}"
>
<img
v-if="getChoiceValueIcon(col, value).id && getChoiceValueIcon(col, value).url"
:src="`/api/common-setting/v1/file/${getChoiceValueIcon(col, value).url}`"
:style="{ maxHeight: '13px', maxWidth: '13px', marginRight: '5px' }"
/>
<ops-icon
v-else
:style="{ color: getChoiceValueIcon(col, value).color, marginRight: '5px' }"
:style="{ color: getChoiceValueIcon(col, value).color }"
:type="getChoiceValueIcon(col, value).name"
/>{{ value }}
</span>
@@ -184,18 +167,10 @@
padding: '1px 5px',
margin: '2px 0',
...getChoiceValueStyle(col, row[col.field]),
display: 'inline-flex',
alignItems: 'center',
}"
>
<img
v-if="getChoiceValueIcon(col, row[col.field]).id && getChoiceValueIcon(col, row[col.field]).url"
:src="`/api/common-setting/v1/file/${getChoiceValueIcon(col, row[col.field]).url}`"
:style="{ maxHeight: '13px', maxWidth: '13px', marginRight: '5px' }"
/>
<ops-icon
v-else
:style="{ color: getChoiceValueIcon(col, row[col.field]).color, marginRight: '5px' }"
:style="{ color: getChoiceValueIcon(col, row[col.field]).color }"
:type="getChoiceValueIcon(col, row[col.field]).name"
/>
{{ row[col.field] }}</span
@@ -209,19 +184,13 @@
<EditAttrsPopover :typeId="typeId" class="operation-icon" @refresh="refreshAfterEditAttrs" />
</template>
<template #default="{ row }">
<a-space>
<a @click="$refs.detail.create(row.ci_id || row._id)">
<a-icon type="unordered-list" />
</a>
<a-tooltip title="添加关系">
<a @click="$refs.detail.create(row.ci_id || row._id, 'tab_2', '2')">
<a-icon type="retweet" />
</a>
</a-tooltip>
<a @click="deleteCI(row)" :style="{ color: 'red' }">
<a-icon type="delete" />
</a>
</a-space>
<a @click="$refs.detail.create(row.ci_id || row._id)">
<a-icon type="unordered-list" />
</a>
<a-divider type="vertical" />
<a @click="deleteCI(row)" :style="{ color: 'red' }">
<a-icon type="delete" />
</a>
</template>
</vxe-column>
<template #empty>

View File

@@ -38,7 +38,7 @@
<a-tab-pane key="tab_2">
<span slot="tab"><a-icon type="branches" />关系</span>
<div :style="{ padding: '24px' }">
<CiDetailRelation ref="ciDetailRelation" :ciId="ciId" :typeId="typeId" :ci="ci" />
<CiDetailRelation :ciId="ciId" :typeId="typeId" :ci="ci" />
</div>
</a-tab-pane>
<a-tab-pane key="tab_3">
@@ -147,14 +147,8 @@ export default {
},
inject: ['reload', 'handleSearch', 'attrList'],
methods: {
create(ciId, activeTabKey = 'tab_1', ciDetailRelationKey = '1') {
create(ciId) {
this.visible = true
this.activeTabKey = activeTabKey
if (activeTabKey === 'tab_2') {
this.$nextTick(() => {
this.$refs.ciDetailRelation.activeKey = ciDetailRelationKey
})
}
this.ciId = ciId
this.getAttributes()
this.getCI()

View File

@@ -23,30 +23,6 @@
:attributeList="attributeList"
/>
</template>
<template v-if="parentsType && parentsType.length">
<a-divider style="font-size:14px;margin:14px 0;font-weight:700;">模型关系</a-divider>
<a-form>
<a-row :gutter="24" align="top" type="flex">
<a-col :span="12" v-for="item in parentsType" :key="item.id">
<a-form-item :label="item.alias || item.name" :colon="false">
<a-input-group compact style="width: 100%">
<a-select v-model="parentsForm[item.name].attr">
<a-select-option
:title="attr.alias || attr.name"
v-for="attr in item.attributes"
:key="attr.name"
:value="attr.name"
>
{{ attr.alias || attr.name }}
</a-select-option>
</a-select>
<a-input placeholder="多个值使用,分割" v-model="parentsForm[item.name].value" style="width: 50%" />
</a-input-group>
</a-form-item>
</a-col>
</a-row>
</a-form>
</template>
</template>
<template v-if="action === 'update'">
<a-form :form="form">
@@ -134,7 +110,6 @@ import { addCI } from '@/modules/cmdb/api/ci'
import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue'
import { valueTypeMap } from '../../../utils/const'
import CreateInstanceFormByGroup from './createInstanceFormByGroup.vue'
import { getCITypeParent } from '@/modules/cmdb/api/CITypeRelation'
export default {
name: 'CreateInstanceForm',
@@ -163,8 +138,6 @@ export default {
batchUpdateLists: [],
editAttr: null,
attributesByGroup: [],
parentsType: [],
parentsForm: {},
}
},
computed: {
@@ -258,11 +231,6 @@ export default {
}
})
values.ci_type = _this.typeId
Object.keys(this.parentsForm).forEach((type) => {
if (this.parentsForm[type].value) {
values[`$${type}.${this.parentsForm[type].attr}`] = this.parentsForm[type].value
}
})
addCI(values).then((res) => {
_this.$message.success('新增成功!')
_this.visible = false
@@ -281,17 +249,6 @@ export default {
Promise.all([this.getCIType(), this.getAttributeList()]).then(() => {
this.batchUpdateLists = [{ name: this.attributeList[0].name }]
})
if (action === 'create') {
getCITypeParent(this.typeId).then((res) => {
this.parentsType = res.parents
const _parentsForm = {}
res.parents.forEach((item) => {
const _find = item.attributes.find((attr) => attr.id === item.unique_id)
_parentsForm[item.name] = { attr: _find.name, value: '' }
})
this.parentsForm = _parentsForm
})
}
})
},
getFieldType(name) {

View File

@@ -244,13 +244,14 @@ export default {
},
},
mounted() {
console.log(this.ci)
this.init(true)
},
methods: {
async init(isFirst) {
await Promise.all([this.getParentCITypes(), this.getChildCITypes()])
Promise.all([this.getFirstCIs(), this.getSecondCIs()]).then(() => {
if (isFirst && this.$refs.ciDetailRelationTopo) {
if (isFirst) {
this.$refs.ciDetailRelationTopo.setTopoData(this.topoData)
}
})
@@ -394,6 +395,12 @@ export default {
margin-top: 20px;
margin-bottom: 5px;
color: #303133;
> a {
display: none;
}
&:hover > a {
display: inline-block;
}
}
}
</style>

View File

@@ -29,14 +29,11 @@
overflow: hidden;
text-overflow: ellipsis;
}
.icon,
img {
.icon {
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 6px;
}
.icon {
width: 16px;
height: 16px;
}

View File

@@ -15,11 +15,7 @@ class BaseNode extends TreeNode {
.attr('id', opts.id)
let icon
if (opts.options.icon) {
if (opts.options.icon.split('$$')[2]) {
icon = $(`<img style="max-width:16px;max-height:16px;" src="/api/common-setting/v1/file/${opts.options.icon.split('$$')[3]}" />`)
} else {
icon = $(`<svg class="icon" style="color:${opts.options.icon.split('$$')[1]}" width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false" class=""><use data-v-5bd421da="" xlink:href="#${opts.options.icon.split('$$')[0]}"></use></svg>`)
}
icon = $(`<svg class="icon" style="color:${opts.options.icon.split('$$')[1]}" width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false" class=""><use data-v-5bd421da="" xlink:href="#${opts.options.icon.split('$$')[0]}"></use></svg>`)
} else {
icon = $(`<span class="icon icon-default">${opts.options.name[0].toUpperCase()}</span>`)
}

View File

@@ -1,6 +1,6 @@
<template>
<a-form :form="form">
<a-divider style="font-size: 14px; margin: 14px 0; font-weight: 700">{{ group.name || '其他' }}</a-divider>
<a-divider style="font-size:14px;margin:14px 0;font-weight:700;">{{ group.name || '其他' }}</a-divider>
<a-row :gutter="24" align="top" type="flex">
<a-col
:span="12"
@@ -37,19 +37,12 @@
:key="'New_' + attr.name + choice_idx"
v-for="(choice, choice_idx) in attr.choice_value"
>
<span :style="{ ...(choice[1] ? choice[1].style : {}), display: 'inline-flex', alignItems: 'center' }">
<template v-if="choice[1] && choice[1].icon && choice[1].icon.name">
<img
v-if="choice[1].icon.id && choice[1].icon.url"
:src="`/api/common-setting/v1/file/${choice[1].icon.url}`"
:style="{ maxHeight: '13px', maxWidth: '13px', marginRight: '5px' }"
/>
<ops-icon
v-else
:style="{ color: choice[1].icon.color, marginRight: '5px' }"
:type="choice[1].icon.name"
/>
</template>
<span :style="choice[1] ? choice[1].style || {} : {}">
<ops-icon
:style="{ color: choice[1].icon.color }"
v-if="choice[1] && choice[1].icon && choice[1].icon.name"
:type="choice[1].icon.name"
/>
{{ choice[0] }}
</span>
</a-select-option>

View File

@@ -51,9 +51,6 @@
<a-space class="attribute-card-operation">
<a v-if="!isStore"><a-icon type="edit" @click="handleEdit"/></a>
<a-tooltip title="所有CI触发计算">
<a v-if="!isStore && property.is_computed"><a-icon type="redo" @click="handleCalcComputed"/></a>
</a-tooltip>
<a style="color:red;"><a-icon type="delete" @click="handleDelete"/></a>
</a-space>
</div>
@@ -62,7 +59,7 @@
</template>
<script>
import { deleteCITypeAttributesById, deleteAttributesById, calcComputedAttribute } from '@/modules/cmdb/api/CITypeAttr'
import { deleteCITypeAttributesById, deleteAttributesById } from '@/modules/cmdb/api/CITypeAttr'
import ValueTypeIcon from '@/components/CMDBValueTypeMapIcon'
import {
ops_default_show,
@@ -168,18 +165,6 @@ export default {
openTrigger() {
this.$refs.triggerForm.open(this.property)
},
handleCalcComputed() {
const that = this
this.$confirm({
title: '警告',
content: `确认触发所有CI的计算`,
onOk() {
calcComputedAttribute(that.property.id).then(() => {
that.$message.success('触发成功!')
})
},
})
},
},
}
</script>

View File

@@ -10,7 +10,7 @@
:headerStyle="{ borderBottom: 'none' }"
wrapClassName="attribute-edit-form"
>
<a-form :form="form" :layout="formLayout">
<a-form :form="form" :layout="formLayout" @submit="handleSubmit">
<a-divider style="font-size:14px;margin-top:6px;">基础设置</a-divider>
<a-col :span="12">
<a-form-item :label-col="formItemLayout.labelCol" :wrapper-col="formItemLayout.wrapperCol" label="属性名(英文)">
@@ -343,13 +343,7 @@
name="is_password"
v-decorator="['is_computed', { rules: [], valuePropName: 'checked' }]"
/>
<ComputedArea
showCalcComputed
ref="computedArea"
v-show="isShowComputedArea"
@handleCalcComputed="handleCalcComputed"
:canDefineComputed="canDefineComputed"
/>
<ComputedArea ref="computedArea" v-show="isShowComputedArea" :canDefineComputed="canDefineComputed" />
</a-form-item>
</a-col>
</a-row>
@@ -359,7 +353,7 @@
</a-form-item>
<div class="custom-drawer-bottom-action">
<a-button @click="onClose">取消</a-button>
<a-button @click="handleSubmit(false)" type="primary">确定</a-button>
<a-button @click="handleSubmit" type="primary">确定</a-button>
</div>
</a-form>
</CustomDrawer>
@@ -372,7 +366,6 @@ import {
updateAttributeById,
updateCITypeAttributesById,
canDefineComputed,
calcComputedAttribute,
} from '@/modules/cmdb/api/CITypeAttr'
import { valueTypeMap } from '../../utils/const'
import ComputedArea from './computedArea.vue'
@@ -583,14 +576,15 @@ export default {
})
},
async handleSubmit(isCalcComputed = false) {
await this.form.validateFields(async (err, values) => {
handleSubmit(e) {
e.preventDefault()
this.form.validateFields((err, values) => {
if (!err) {
console.log('Received values of form: ', values)
if (this.record.is_required !== values.is_required || this.record.default_show !== values.default_show) {
console.log('changed is_required')
await updateCITypeAttributesById(this.CITypeId, {
updateCITypeAttributesById(this.CITypeId, {
attributes: [
{ attr_id: this.record.id, is_required: values.is_required, default_show: values.default_show },
],
@@ -636,21 +630,19 @@ export default {
const fontOptions = this.$refs.fontArea.getData()
if (values.id) {
await this.updateAttribute(values.id, { ...values, option: { fontOptions } }, isCalcComputed)
this.updateAttribute(values.id, { ...values, option: { fontOptions } })
} else {
// this.createAttribute(values)
}
}
})
},
async updateAttribute(attrId, data, isCalcComputed = false) {
await updateAttributeById(attrId, data)
if (isCalcComputed) {
await calcComputedAttribute(attrId)
}
this.$message.success(`更新成功`)
this.handleOk()
this.onClose()
updateAttribute(attrId, data) {
updateAttributeById(attrId, data).then((res) => {
this.$message.success(`更新成功`)
this.handleOk()
this.onClose()
})
},
handleOk() {
this.$emit('ok')
@@ -690,9 +682,6 @@ export default {
default_value: key,
})
},
async handleCalcComputed() {
await this.handleSubmit(true)
},
},
watch: {},
}

View File

@@ -8,14 +8,6 @@
<span style="font-size:12px;" slot="tab">代码</span>
<codemirror style="z-index: 9999" :options="cmOptions" v-model="compute_script"></codemirror>
</a-tab-pane>
<template slot="tabBarExtraContent" v-if="showCalcComputed">
<a-button type="primary" size="small" @click="handleCalcComputed">
应用
</a-button>
<a-tooltip title="所有CI触发计算">
<a-icon type="question-circle" style="margin-left:5px" />
</a-tooltip>
</template>
</a-tabs>
</template>
@@ -33,10 +25,6 @@ export default {
type: Boolean,
default: true,
},
showCalcComputed: {
type: Boolean,
default: false,
}
},
data() {
return {
@@ -74,16 +62,6 @@ export default {
this.activeKey = 'expr'
}
},
handleCalcComputed() {
const that = this
this.$confirm({
title: '警告',
content: `确认触发将保存当前配置及触发所有CI的计算`,
onOk() {
that.$emit('handleCalcComputed')
},
})
},
},
}
</script>

View File

@@ -113,20 +113,14 @@
style="width: 17px; height: 17px; display: none; position: absolute; left: 15px; top: 5px"
/>
<span class="ci-types-left-detail-icon">
<template v-if="ci.icon">
<img
v-if="ci.icon.split('$$')[2]"
:src="`/api/common-setting/v1/file/${ci.icon.split('$$')[3]}`"
/>
<ops-icon
v-else
:style="{
color: ci.icon.split('$$')[1],
fontSize: '14px',
}"
:type="ci.icon.split('$$')[0]"
/>
</template>
<ops-icon
:style="{
color: ci.icon.split('$$')[1],
fontSize: '14px',
}"
v-if="ci.icon"
:type="ci.icon.split('$$')[0]"
/>
<span :style="{ color: '#2f54eb' }" v-else>{{ ci.name[0].toUpperCase() }}</span>
</span>
</div>
@@ -210,7 +204,7 @@
:label="item.alias || item.name"
>
<span> {{ item.alias || item.name }}</span>
<span :title="item.name" style="font-size:10px;color:#afafaf;"> {{ item.name }}</span>
<span :title="item.name" style="font-size: 10px; color: #afafaf"> {{ item.name }}</span>
</el-option>
<a-divider :style="{ margin: '5px 0' }" />
<div :style="{ textAlign: 'right' }">
@@ -241,7 +235,7 @@
:label="item.alias || item.name"
>
<span> {{ item.alias || item.name }}</span>
<span :title="item.name" style="font-size:10px;color:#afafaf;"> {{ item.name }}</span>
<span :title="item.name" style="font-size: 10px; color: #afafaf"> {{ item.name }}</span>
</el-option>
</el-select>
<a-divider type="vertical" />
@@ -539,20 +533,21 @@ export default {
e.preventDefault()
this.form.validateFields(async (err, values) => {
if (!err) {
// eslint-disable-next-line no-console
console.log('Received values of form: ', values)
const icon = this.$refs.iconArea.getIcon()
this.loading = true
if (values.default_order_attr && this.default_order_asc === '2') {
values.default_order_attr = `-${values.default_order_attr}`
}
const _icon = this.$refs.iconArea.getIcon()
const icon =
_icon && _icon.name ? `${_icon.name}$$${_icon.color || ''}$$${_icon.id || ''}$$${_icon.url || ''}` : ''
if (values.id) {
await this.updateCIType(values.id, {
...values,
icon,
icon: icon && icon.name ? `${icon.name}$$${icon.color || ''}` : '',
})
} else {
await this.createCIType({ ...values, icon })
await this.createCIType({ ...values, icon: icon && icon.name ? `${icon.name}$$${icon.color || ''}` : '' })
// todo 把改ci 类型绑定在当前group下
}
}
})
@@ -736,8 +731,6 @@ export default {
? {
name: record.icon.split('$$')[0] || '',
color: record.icon.split('$$')[1] || '',
id: record.icon.split('$$')[2] ? Number(record.icon.split('$$')[2]) : null,
url: record.icon.split('$$')[3] || '',
}
: {}
)
@@ -835,7 +828,7 @@ export default {
margin-left: auto;
}
.ci-types-left-detail-icon {
display: flex;
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
@@ -844,10 +837,6 @@ export default {
box-shadow: 0px 1px 2px rgba(47, 84, 235, 0.2);
margin-right: 6px;
background-color: #fff;
img {
max-height: 20px;
max-width: 20px;
}
}
&:hover {
background-color: #e1efff;

View File

@@ -102,17 +102,8 @@
"
>
<span :style="{ cursor: disabled ? 'default' : 'move' }">
<img
v-if="icon.id && icon.url"
:src="`/api/common-setting/v1/file/${icon.url}`"
:style="{ maxHeight: '12px', maxWidth: '12px', marginRight: '5px' }"
/>
<ops-icon
v-else-if="icon.name"
:type="icon.name"
:style="{ marginRight: '5px', color: icon.color || '#595959' }"
/>
<span>{{ item[0] }}</span>
<ops-icon v-if="icon.name" :type="icon.name" :style="{ color: icon.color || '#595959' }" />
{{ item[0] }}
</span>
<a
class="pre-value-tag-dropdown"
@@ -249,10 +240,6 @@ export default {
border-radius: 4px;
font-size: 12px;
position: relative;
> span {
display: flex;
align-items: center;
}
&:hover .pre-value-tag-dropdown-icon {
display: inline !important;
}

View File

@@ -18,30 +18,18 @@
keep-source
:max-height="windowHeight - 180"
class="ops-stripe-table"
:row-class-name="rowClass"
>
<vxe-column field="source_ci_type_name" title="源模型英文名"></vxe-column>
<vxe-column field="relation_type" title="关联类型">
<template #default="{row}">
<span style="color:#2f54eb" v-if="row.isParent"></span>
{{ row.relation_type }}
</template>
</vxe-column>
<vxe-column field="relation_type" title="关联类型"></vxe-column>
<vxe-column field="alias" title="目标模型名"></vxe-column>
<vxe-column field="constraint" title="关系约束">
<template #default="{row}">
<span v-if="row.isParent && constraintMap[row.constraint]">{{
constraintMap[row.constraint]
.split('')
.reverse()
.join('')
}}</span>
<span v-else>{{ constraintMap[row.constraint] }}</span>
<span>{{ constraintMap[row.constraint] }}</span>
</template>
</vxe-column>
<vxe-column field="operation" title="操作" width="100">
<template #default="{row}">
<a-space v-if="!row.isParent && row.source_ci_type_id">
<a-space>
<a @click="handleOpenGrant(row)"><a-icon type="user-add"/></a>
<a-popconfirm title="确认删除?" @confirm="handleDelete(row)">
<a style="color: red;"><a-icon type="delete"/></a>
@@ -49,12 +37,6 @@
</a-space>
</template>
</vxe-column>
<template #empty>
<div>
<img :style="{ width: '100px' }" :src="require('@/assets/data_empty.png')" />
<div>暂无数据</div>
</div>
</template>
</vxe-table>
<a-modal
:closable="false"
@@ -113,13 +95,7 @@
</template>
<script>
import {
createRelation,
deleteRelation,
getCITypeChildren,
getCITypeParent,
getRelationTypes,
} from '@/modules/cmdb/api/CITypeRelation'
import { createRelation, deleteRelation, getCITypeChildren, getRelationTypes } from '@/modules/cmdb/api/CITypeRelation'
import { getCITypes } from '@/modules/cmdb/api/CIType'
import CMDBGrant from '../../components/cmdbGrant'
@@ -151,7 +127,6 @@ export default {
'2': '多对多',
},
tableData: [],
parentTableData: [],
}
},
computed: {
@@ -162,25 +137,12 @@ export default {
return this.$store.state.windowHeight
},
},
async mounted() {
mounted() {
this.getCITypes()
this.getRelationTypes()
await this.getCITypeParent()
this.getData()
},
methods: {
async getCITypeParent() {
await getCITypeParent(this.CITypeId).then((res) => {
this.parentTableData = res.parents.map((item) => {
return {
...item,
source_ci_type_name: this.CITypeName,
source_ci_type_id: this.CITypeId,
isParent: true,
}
})
})
},
getData() {
getCITypeChildren(this.CITypeId).then((res) => {
const data = res.children.map((obj) => {
@@ -190,11 +152,7 @@ export default {
source_ci_type_id: this.CITypeId,
}
})
if (this.parentTableData && this.parentTableData.length) {
this.tableData = [...data, { isDivider: true }, ...this.parentTableData]
} else {
this.tableData = data
}
this.tableData = data
})
},
getCITypes() {
@@ -259,25 +217,8 @@ export default {
filterOption(input, option) {
return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
},
rowClass({ row }) {
if (row.isDivider) return 'relation-table-divider'
if (row.isParent) return 'relation-table-parent'
},
},
}
</script>
<style lang="less">
.ops-stripe-table .vxe-body--row.row--stripe.relation-table-divider {
background-color: #b1b8d3 !important;
}
.ops-stripe-table .vxe-body--row.relation-table-parent {
background-color: #f5f8ff !important;
}
.relation-table-divider {
td {
height: 1px !important;
line-height: 1px !important;
}
}
</style>
<style lang="less" scoped></style>

View File

@@ -1,55 +1,11 @@
<template>
<div
:id="`cmdb-dashboard-${chartId}-${editable}-${isPreview}`"
:style="{ width: '100%', height: 'calc(100% - 2.2vw)' }"
>
<div
v-if="options.chartType === 'count'"
:style="{ color: options.fontColor || '#fff' }"
class="cmdb-dashboard-grid-item-chart"
>
<div class="cmdb-dashboard-grid-item-chart-icon" v-if="options.showIcon && ciType">
<template v-if="ciType.icon">
<img v-if="ciType.icon.split('$$')[2]" :src="`/api/common-setting/v1/file/${ciType.icon.split('$$')[3]}`" />
<ops-icon
v-else
:style="{
color: ciType.icon.split('$$')[1],
}"
:type="ciType.icon.split('$$')[0]"
/>
</template>
<span :style="{ color: '#2f54eb' }" v-else>{{ ciType.name[0].toUpperCase() }}</span>
</div>
<div :style="{ width: '100%', height: 'calc(100% - 2.2vw)' }">
<div v-if="category === 0" class="cmdb-dashboard-grid-item-chart">
<span :style="{ ...options.fontConfig }">{{ toThousands(data) }}</span>
</div>
<vxe-table
:max-height="tableHeight"
:data="tableData"
:stripe="!!options.ret"
size="mini"
class="ops-stripe-table"
v-if="options.chartType === 'table'"
:span-method="mergeRowMethod"
:border="!options.ret"
:show-header="!!options.ret"
>
<template v-if="options.ret">
<vxe-column v-for="col in columns" :key="col" :title="col" :field="col"></vxe-column>
</template>
<template v-else>
<vxe-column
v-for="(key, index) in Array(keyLength)"
:key="`key${index}`"
:title="`key${index}`"
:field="`key${index}`"
></vxe-column>
<vxe-column field="value" title="value"></vxe-column>
</template>
</vxe-table>
<div
:id="`cmdb-dashboard-${chartId}-${editable}`"
v-else-if="category === 1 || category === 2"
v-if="category === 1 || category === 2"
class="cmdb-dashboard-grid-item-chart"
></div>
</div>
@@ -59,27 +15,17 @@
import * as echarts from 'echarts'
import { mixin } from '@/utils/mixin'
import { toThousands } from '../../utils/helper'
import {
category_1_bar_options,
category_1_line_options,
category_1_pie_options,
category_2_bar_options,
category_2_pie_options,
} from './chartOptions'
import { category_1_bar_options, category_1_pie_options, category_2_bar_options } from './chartOptions'
export default {
name: 'Chart',
mixins: [mixin],
props: {
ci_types: {
type: Array,
default: () => [],
},
chartId: {
type: Number,
default: 0,
},
data: {
type: [Number, Object, Array],
type: [Number, Object],
default: 0,
},
category: {
@@ -94,65 +40,20 @@ export default {
type: Boolean,
default: false,
},
type_id: {
type: [Number, Array],
default: null,
},
isPreview: {
type: Boolean,
default: false,
},
},
data() {
return {
chart: null,
columns: [],
tableHeight: '',
tableData: [],
keyLength: 0,
}
},
computed: {
ciType() {
if (this.type_id || this.options?.type_ids) {
const _find = this.ci_types.find((item) => item.id === this.type_id || item.id === this.options?.type_ids[0])
return _find || null
}
return null
},
},
watch: {
data: {
immediate: true,
deep: true,
handler(newValue, oldValue) {
if (this.category === 1 || this.category === 2) {
if (this.options.chartType !== 'table' && Object.prototype.toString.call(newValue) === '[object Object]') {
if (this.isPreview) {
this.$nextTick(() => {
this.setChart()
})
} else {
this.setChart()
}
}
}
if (this.options.chartType === 'table') {
this.$nextTick(() => {
const dom = document.getElementById(`cmdb-dashboard-${this.chartId}-${this.editable}-${this.isPreview}`)
this.tableHeight = dom.offsetHeight
})
if (this.options.ret) {
const excludeKeys = ['_X_ROW_KEY', 'ci_type', 'ci_type_alias', 'unique', 'unique_alias', '_id', '_type']
if (newValue && newValue.length) {
this.columns = Object.keys(newValue[0]).filter((keys) => !excludeKeys.includes(keys))
this.tableData = newValue
}
} else {
const _data = []
this.keyLength = this.options?.attr_ids?.length ?? 0
this.formatTableData(_data, this.data, {})
this.tableData = _data
if (Object.prototype.toString.call(newValue) === '[object Object]') {
this.setChart()
}
}
},
@@ -180,19 +81,13 @@ export default {
this.chart = echarts.init(document.getElementById(`cmdb-dashboard-${this.chartId}-${this.editable}`))
}
if (this.category === 1 && this.options.chartType === 'bar') {
this.chart.setOption(category_1_bar_options(this.data, this.options), true)
}
if (this.category === 1 && this.options.chartType === 'line') {
this.chart.setOption(category_1_line_options(this.data, this.options), true)
this.chart.setOption(category_1_bar_options(this.data), true)
}
if (this.category === 1 && this.options.chartType === 'pie') {
this.chart.setOption(category_1_pie_options(this.data, this.options), true)
this.chart.setOption(category_1_pie_options(this.data), true)
}
if (this.category === 2 && ['bar', 'line'].includes(this.options.chartType)) {
this.chart.setOption(category_2_bar_options(this.data, this.options, this.options.chartType), true)
}
if (this.category === 2 && this.options.chartType === 'pie') {
this.chart.setOption(category_2_pie_options(this.data, this.options), true)
if (this.category === 2) {
this.chart.setOption(category_2_bar_options(this.data), true)
}
},
resizeChart() {
@@ -202,34 +97,6 @@ export default {
}
})
},
formatTableData(_data, data, obj) {
Object.keys(data).forEach((k) => {
if (typeof data[k] === 'number') {
_data.push({ ...obj, [`key${Object.keys(obj).length}`]: k, value: data[k] })
} else {
this.formatTableData(_data, data[k], { ...obj, [`key${Object.keys(obj).length}`]: k })
}
})
},
mergeRowMethod({ row, _rowIndex, column, visibleData }) {
const fields = ['key0', 'key1', 'key2']
const cellValue = row[column.field]
if (cellValue && fields.includes(column.field)) {
const prevRow = visibleData[_rowIndex - 1]
let nextRow = visibleData[_rowIndex + 1]
if (prevRow && prevRow[column.field] === cellValue) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow[column.field] === cellValue) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
},
},
}
</script>
@@ -239,28 +106,14 @@ export default {
width: 100%;
height: 100%;
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
> span {
font-size: 50px;
font-weight: 700;
}
.cmdb-dashboard-grid-item-chart-icon {
> i {
font-size: 4vw;
}
> img {
width: 4vw;
}
> span {
display: inline-block;
width: 4vw;
height: 4vw;
font-size: 50px;
text-align: center;
line-height: 50px;
}
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
</style>

View File

@@ -1,307 +1,65 @@
<template>
<a-modal
width="1100px"
:title="`${type === 'add' ? '新增' : '编辑'}图表`"
:visible="visible"
@cancel="handleclose"
@ok="handleok"
:bodyStyle="{ paddingTop: 0 }"
>
<div class="chart-wrapper">
<div class="chart-left">
<a-form-model ref="chartForm" :model="form" :rules="rules" :label-col="{ span: 4 }" :wrapper-col="{ span: 18 }">
<a-form-model-item label="标题" prop="name">
<a-input v-model="form.name" placeholder="请输入图表标题"></a-input>
</a-form-model-item>
<a-form-model-item label="类型" prop="category" v-if="chartType !== 'count' && chartType !== 'table'">
<a-radio-group
@change="
() => {
resetForm()
}
"
:default-value="1"
v-model="form.category"
>
<a-radio-button :value="Number(key)" :key="key" v-for="key in Object.keys(dashboardCategory)">
{{ dashboardCategory[key].label }}
</a-radio-button>
</a-radio-group>
</a-form-model-item>
<a-form-model-item label="类型" prop="tableCategory" v-if="chartType === 'table'">
<a-radio-group
@change="
() => {
resetForm()
}
"
:default-value="1"
v-model="form.tableCategory"
>
<a-radio-button :value="1">
计算指标
</a-radio-button>
<a-radio-button :value="2">
资源数据
</a-radio-button>
</a-radio-group>
</a-form-model-item>
<a-form-model-item
v-if="(chartType !== 'table' && form.category !== 2) || (chartType === 'table' && form.tableCategory === 1)"
label="模型"
prop="type_ids"
>
<a-select
show-search
optionFilterProp="children"
@change="changeCIType"
v-model="form.type_ids"
placeholder="请选择模型"
mode="multiple"
>
<a-select-option v-for="ci_type in ci_types" :key="ci_type.id" :value="ci_type.id">{{
ci_type.alias || ci_type.name
}}</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item v-else label="模型" prop="type_id">
<a-select
show-search
optionFilterProp="children"
@change="changeCIType"
v-model="form.type_id"
placeholder="请选择模型"
>
<a-select-option v-for="ci_type in ci_types" :key="ci_type.id" :value="ci_type.id">{{
ci_type.alias || ci_type.name
}}</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item
label="维度"
prop="attr_ids"
v-if="(['bar', 'line', 'pie'].includes(chartType) && form.category === 1) || chartType === 'table'"
>
<a-select @change="changeAttr" v-model="form.attr_ids" placeholder="请选择维度" mode="multiple" show-search>
<a-select-option v-for="attr in commonAttributes" :key="attr.id" :value="attr.id">{{
attr.alias || attr.name
}}</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item
prop="type_ids"
label="关系模型"
v-if="['bar', 'line', 'pie'].includes(chartType) && form.category === 2"
>
<a-select
show-search
optionFilterProp="children"
mode="multiple"
v-model="form.type_ids"
placeholder="请选择模型"
>
<a-select-opt-group
v-for="(key, index) in Object.keys(level2children)"
:key="key"
:label="`层级${index + 1}`"
>
<a-select-option
@click="(e) => clickLevel2children(e, citype, index + 1)"
v-for="citype in level2children[key]"
:key="citype.id"
:value="citype.id"
>
{{ citype.alias || citype.name }}
</a-select-option>
</a-select-opt-group>
</a-select>
</a-form-model-item>
<div class="chart-left-preview">
<span class="chart-left-preview-operation" @click="showPreview"><a-icon type="play-circle" /> 预览</span>
<template v-if="isShowPreview">
<div v-if="chartType !== 'count'" class="cmdb-dashboard-grid-item-title">
<template v-if="form.showIcon && ciType">
<template v-if="ciType.icon">
<img
v-if="ciType.icon.split('$$')[2]"
:src="`/api/common-setting/v1/file/${ciType.icon.split('$$')[3]}`"
/>
<ops-icon
v-else
:style="{
color: ciType.icon.split('$$')[1],
}"
:type="ciType.icon.split('$$')[0]"
/>
</template>
<span :style="{ color: '#2f54eb' }" v-else>{{ ciType.name[0].toUpperCase() }}</span>
</template>
<span :style="{ color: '#000' }"> {{ form.name }}</span>
</div>
<div
class="chart-left-preview-box"
:style="{
height: chartType === 'count' ? '120px' : '',
marginTop: chartType === 'count' ? '80px' : '',
background:
chartType === 'count'
? Array.isArray(bgColor)
? `linear-gradient(to bottom, ${bgColor[0]} 0%, ${bgColor[1]} 100%)`
: bgColor
: '#fafafa',
}"
>
<div :style="{ color: fontColor }">{{ form.name }}</div>
<Chart
:ref="`chart_${item.id}`"
:chartId="item.id"
:data="previewData"
:category="form.category"
:options="{
...item.options,
name: form.name,
fontColor: fontColor,
bgColor: bgColor,
chartType: chartType,
showIcon: form.showIcon,
barDirection: barDirection,
barStack: barStack,
chartColor: chartColor,
type_ids: form.type_ids,
attr_ids: form.attr_ids,
isShadow: isShadow,
}"
:editable="false"
:ci_types="ci_types"
:type_id="form.type_id || form.type_ids"
isPreview
/>
</div>
</template>
</div>
<a-form-model-item label="是否显示icon" prop="showIcon" :label-col="{ span: 5 }" :wrapper-col="{ span: 18 }">
<a-switch v-model="form.showIcon"></a-switch>
</a-form-model-item>
</a-form-model>
</div>
<div class="chart-right">
<h4>图表类型</h4>
<div class="chart-right-type">
<div
:class="{ 'chart-right-type-box': true, 'chart-right-type-box-selected': chartType === t.value }"
v-for="t in chartTypeList"
:key="t.value"
@click="changeChartType(t)"
>
<ops-icon :type="`cmdb-${t.value}`" />
<span>{{ t.label }}</span>
</div>
</div>
<h4>数据筛选</h4>
<FilterComp
ref="filterComp"
:isDropdown="false"
:canSearchPreferenceAttrList="attributes"
@setExpFromFilter="setExpFromFilter"
:expression="filterExp ? `q=${filterExp}` : ''"
/>
<h4>格式</h4>
<a-form-model :colon="false" :label-col="{ span: 4 }" :wrapper-col="{ span: 20 }">
<a-form-model-item label="字体颜色" v-if="chartType === 'count'">
<ColorPicker
v-model="fontColor"
:colorList="[
'#1D2129',
'#4E5969',
'#103C93',
'#86909C',
'#ffffff',
'#C9F2FF',
'#FFEAC0',
'#D6FFE6',
'#F2DEFF',
]"
/>
</a-form-model-item>
<a-form-model-item label="背景颜色" v-if="chartType === 'count'">
<ColorPicker
v-model="bgColor"
:colorList="[
['#6ABFFE', '#5375EB'],
['#C69EFF', '#A377F9'],
['#85EBC9', '#4AB8D8'],
['#FEB58B', '#DF6463'],
'#ffffff',
'#FFFBF0',
'#FFF1EC',
'#E5FFFE',
'#E5E7FF',
]"
/>
</a-form-model-item>
<a-form-model-item label="图表颜色" v-else-if="chartType !== 'table'">
<ColorListPicker v-model="chartColor" />
</a-form-model-item>
<a-form-model-item label="图表长度(%)">
<a-radio-group class="chart-width" style="width:100%;" v-model="width">
<a-radio-button :value="3">
25
</a-radio-button>
<a-radio-button :value="6">
50
</a-radio-button>
<a-radio-button :value="9">
75
</a-radio-button>
<a-radio-button :value="12">
100
</a-radio-button>
</a-radio-group>
</a-form-model-item>
<a-form-model-item label="柱状图类型" v-if="chartType === 'bar'">
<a-radio-group v-model="barStack">
<a-radio value="total">
堆积柱状图
</a-radio>
<a-radio value="">
多系列柱状图
</a-radio>
</a-radio-group>
</a-form-model-item>
<a-form-model-item label="方向" v-if="chartType === 'bar'">
<a-radio-group v-model="barDirection">
<a-radio value="x">
X轴
</a-radio>
<a-radio value="y">
y轴
</a-radio>
</a-radio-group>
</a-form-model-item>
<a-form-model-item label="下方阴影" v-if="chartType === 'line'">
<a-switch v-model="isShadow" />
</a-form-model-item>
</a-form-model>
</div>
</div>
<a-modal :title="`${type === 'add' ? '新增' : '编辑'}图表`" :visible="visible" @cancel="handleclose" @ok="handleok">
<a-form-model ref="chartForm" :model="form" :rules="rules" :label-col="{ span: 6 }" :wrapper-col="{ span: 14 }">
<a-form-model-item label="类型" prop="category">
<a-select v-model="form.category" @change="changeDashboardCategory">
<a-select-option v-for="cate in Object.keys(dashboardCategory)" :key="cate" :value="Number(cate)">{{
dashboardCategory[cate].label
}}</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item v-if="form.category !== 0" label="名称" prop="name">
<a-input v-model="form.name" placeholder="请输入图表名称"></a-input>
</a-form-model-item>
<a-form-model-item label="模型" prop="type_id">
<a-select
show-search
optionFilterProp="children"
@change="changeCIType"
v-model="form.type_id"
placeholder="请选择模型"
>
<a-select-option v-for="ci_type in ci_types" :key="ci_type.id" :value="ci_type.id">{{
ci_type.alias || ci_type.name
}}</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item v-if="form.category === 1" label="模型属性" prop="attr_id">
<a-select show-search optionFilterProp="children" v-model="form.attr_id" placeholder="请选择模型属性">
<a-select-option v-for="attr in attributes" :key="attr.id" :value="attr.id">{{
attr.alias || attr.name
}}</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item v-if="form.category === 1" label="图表类型" prop="chartType">
<a-radio-group v-model="chartType">
<a-radio value="bar">
柱状图
</a-radio>
<a-radio value="pie">
饼图
</a-radio>
</a-radio-group>
</a-form-model-item>
<a-form-model-item v-if="form.category === 2" label="关系层级" prop="level">
<a-input v-model="form.level" placeholder="请输入关系层级"></a-input>
</a-form-model-item>
<a-form-model-item v-if="form.category === 0" label="字体">
<FontConfig ref="fontConfig" />
</a-form-model-item>
</a-form-model>
</a-modal>
</template>
<script>
import Chart from './chart.vue'
import { dashboardCategory } from './constant'
import { postCustomDashboard, putCustomDashboard, postCustomDashboardPreview } from '../../api/customDashboard'
import { getCITypeAttributesByTypeIds, getCITypeCommonAttributesByTypeIds } from '../../api/CITypeAttr'
import { getRecursive_level2children } from '../../api/CITypeRelation'
import { postCustomDashboard, putCustomDashboard } from '../../api/customDashboard'
import { getCITypeAttributesById } from '../../api/CITypeAttr'
import { getLastLayout } from '../../utils/helper'
import FilterComp from '@/components/CMDBFilterComp'
import ColorPicker from './colorPicker.vue'
import ColorListPicker from './colorListPicker.vue'
import FontConfig from './fontConfig.vue'
export default {
name: 'ChartForm',
components: { Chart, FilterComp, ColorPicker, ColorListPicker },
components: { FontConfig },
props: {
ci_types: {
type: Array,
@@ -309,226 +67,100 @@ export default {
},
},
data() {
const chartTypeList = [
{
value: 'count',
label: '指标',
},
{
value: 'bar',
label: '柱状图',
},
{
value: 'line',
label: '折线图',
},
{
value: 'pie',
label: '饼状图',
},
{
value: 'table',
label: '表格',
},
]
return {
dashboardCategory,
chartTypeList,
visible: false,
attributes: [],
type: 'add',
form: {
category: 0,
tableCategory: 1,
name: undefined,
type_id: undefined,
type_ids: undefined,
attr_ids: undefined,
attr_id: undefined,
level: undefined,
showIcon: false,
},
rules: {
category: [{ required: true, trigger: 'change' }],
name: [{ required: true, message: '请输入图表名称' }],
type_id: [{ required: true, message: '请选择模型', trigger: 'change' }],
type_ids: [{ required: true, message: '请选择模型', trigger: 'change' }],
attr_ids: [{ required: true, message: '请选择模型属性', trigger: 'change' }],
attr_id: [{ required: true, message: '请选择模型属性', trigger: 'change' }],
level: [{ required: true, message: '请输入关系层级' }],
showIcon: [{ required: false }],
},
item: {},
chartType: 'count', // table,bar,line,pie,count
width: 3,
fontColor: '#ffffff',
bgColor: ['#6ABFFE', '#5375EB'],
chartColor: '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD', // 图表颜色
isShowPreview: false,
filterExp: undefined,
previewData: null,
barStack: 'total',
barDirection: 'y',
commonAttributes: [],
level2children: {},
isShadow: false,
chartType: 'bar',
}
},
computed: {
ciType() {
if (this.form.type_id || this.form.type_ids) {
const _find = this.ci_types.find((item) => item.id === this.form.type_id || item.id === this.form.type_ids[0])
return _find || null
}
return null
},
},
inject: ['layout'],
methods: {
async open(type, item = {}) {
open(type, item = {}) {
this.visible = true
this.type = type
this.item = item
const { category = 0, name, type_id, attr_id, level } = item
const chartType = (item.options || {}).chartType || 'count'
const fontColor = (item.options || {}).fontColor || '#ffffff'
const bgColor = (item.options || {}).bgColor || ['#6ABFFE', '#5375EB']
const width = (item.options || {}).w
const showIcon = (item.options || {}).showIcon
const type_ids = item?.options?.type_ids || []
const attr_ids = item?.options?.attr_ids || []
const ret = item?.options?.ret || ''
this.width = width
const chartType = (item.options || {}).chartType || 'bar'
this.chartType = chartType
this.filterExp = item?.options?.filter ?? ''
this.chartColor = item?.options?.chartColor ?? '#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD'
this.isShadow = item?.options?.isShadow ?? false
if (chartType === 'count') {
this.fontColor = fontColor
this.bgColor = bgColor
}
if (type_ids && type_ids.length) {
await getCITypeAttributesByTypeIds({ type_ids: type_ids.join(',') }).then((res) => {
if (type_id && attr_id) {
getCITypeAttributesById(type_id).then((res) => {
this.attributes = res.attributes
})
if ((['bar', 'line', 'pie'].includes(chartType) && category === 1) || chartType === 'table') {
this.barDirection = item?.options?.barDirection ?? 'y'
this.barStack = item?.options?.barStack ?? 'total'
await getCITypeCommonAttributesByTypeIds({
type_ids: type_ids.join(','),
}).then((res) => {
this.commonAttributes = res.attributes
})
}
}
if (type_id) {
getRecursive_level2children(type_id).then((res) => {
this.level2children = res
})
await getCITypeCommonAttributesByTypeIds({
type_ids: type_id,
}).then((res) => {
this.commonAttributes = res.attributes
})
}
this.$nextTick(() => {
this.$refs.filterComp.visibleChange(true, false)
})
const default_form = {
category: 0,
name: undefined,
type_id: undefined,
type_ids: undefined,
attr_ids: undefined,
attr_id: undefined,
level: undefined,
showIcon: false,
tableCategory: 1,
}
this.form = {
...default_form,
category,
name,
type_id,
type_ids,
attr_ids,
attr_id,
level,
showIcon,
tableCategory: ret === 'cis' ? 2 : 1,
}
if (category === 0) {
this.$nextTick(() => {
this.$refs.fontConfig.setConfig((item.options || {}).fontConfig)
})
}
},
handleclose() {
this.attributes = []
this.$refs.chartForm.clearValidate()
this.isShowPreview = false
this.visible = false
},
changeCIType(value) {
this.form.attr_ids = []
this.commonAttributes = []
getCITypeAttributesByTypeIds({ type_ids: Array.isArray(value) ? value.join(',') : value }).then((res) => {
getCITypeAttributesById(value).then((res) => {
this.attributes = res.attributes
this.form = {
...this.form,
attr_id: undefined,
}
})
if (!Array.isArray(value)) {
getRecursive_level2children(value).then((res) => {
this.level2children = res
})
}
if ((['bar', 'line', 'pie'].includes(this.chartType) && this.form.category === 1) || this.chartType === 'table') {
getCITypeCommonAttributesByTypeIds({ type_ids: Array.isArray(value) ? value.join(',') : value }).then((res) => {
this.commonAttributes = res.attributes
})
}
},
handleok() {
this.$refs.chartForm.validate(async (valid) => {
if (valid) {
const name = this.form.name
const { chartType, fontColor, bgColor } = this
this.$refs.filterComp.handleSubmit()
const fontConfig = this.form.category === 0 ? this.$refs.fontConfig.getConfig() : undefined
const _find = this.ci_types.find((attr) => attr.id === this.form.type_id)
const name = this.form.name || (_find || {}).alias || (_find || {}).name
if (this.item.id) {
const params = {
await putCustomDashboard(this.item.id, {
...this.form,
options: {
...this.item.options,
name,
w: this.width,
fontConfig,
chartType: this.chartType,
showIcon: this.form.showIcon,
type_ids: this.form.type_ids,
filter: this.filterExp,
isShadow: this.isShadow,
},
}
if (chartType === 'count') {
params.options.fontColor = fontColor
params.options.bgColor = bgColor
}
if (['bar', 'line', 'pie'].includes(chartType)) {
if (this.form.category === 1) {
params.options.attr_ids = this.form.attr_ids
}
params.options.chartColor = this.chartColor
}
if (chartType === 'bar') {
params.options.barDirection = this.barDirection
params.options.barStack = this.barStack
}
if (chartType === 'table') {
params.options.attr_ids = this.form.attr_ids
if (this.form.tableCategory === 2) {
params.options.ret = 'cis'
}
}
delete params.showIcon
delete params.type_ids
delete params.attr_ids
delete params.tableCategory
await putCustomDashboard(this.item.id, params)
})
} else {
const { xLast, yLast, wLast } = getLastLayout(this.layout())
const w = this.width
const w = 3
const x = xLast + wLast + w > 12 ? 0 : xLast + wLast
const y = xLast + wLast + w > 12 ? yLast + 1 : yLast
const params = {
await postCustomDashboard({
...this.form,
options: {
x,
@@ -537,216 +169,23 @@ export default {
h: this.form.category === 0 ? 3 : 5,
name,
chartType: this.chartType,
showIcon: this.form.showIcon,
type_ids: this.form.type_ids,
filter: this.filterExp,
isShadow: this.isShadow,
fontConfig,
},
}
if (chartType === 'count') {
params.options.fontColor = fontColor
params.options.bgColor = bgColor
}
if (['bar', 'line', 'pie'].includes(chartType)) {
if (this.form.category === 1) {
params.options.attr_ids = this.form.attr_ids
}
params.options.chartColor = this.chartColor
}
if (chartType === 'bar') {
params.options.barDirection = this.barDirection
params.options.barStack = this.barStack
}
if (chartType === 'table') {
params.options.attr_ids = this.form.attr_ids
if (this.form.tableCategory === 2) {
params.options.ret = 'cis'
}
}
delete params.showIcon
delete params.type_ids
delete params.attr_ids
delete params.tableCategory
await postCustomDashboard(params)
})
}
this.handleclose()
this.$emit('refresh')
}
})
},
// changeDashboardCategory(value) {
// this.$refs.chartForm.clearValidate()
// if (value === 1 && this.form.type_id) {
// this.changeCIType(this.form.type_id)
// }
// },
changeChartType(t) {
this.chartType = t.value
this.isShowPreview = false
if (t.value === 'count') {
this.form.category = 0
} else {
this.form.category = 1
}
this.resetForm()
},
showPreview() {
this.$refs.chartForm.validate(async (valid) => {
if (valid) {
this.isShowPreview = false
const name = this.form.name
const { chartType, fontColor, bgColor } = this
this.$refs.filterComp.handleSubmit()
const params = {
...this.form,
options: {
name,
chartType,
showIcon: this.form.showIcon,
type_ids: this.form.type_ids,
filter: this.filterExp,
isShadow: this.isShadow,
},
}
if (chartType === 'count') {
params.options.fontColor = fontColor
params.options.bgColor = bgColor
}
if (['bar', 'line', 'pie'].includes(chartType)) {
if (this.form.category === 1) {
params.options.attr_ids = this.form.attr_ids
}
params.options.chartColor = this.chartColor
}
if (chartType === 'bar') {
params.options.barDirection = this.barDirection
params.options.barStack = this.barStack
}
if (chartType === 'table') {
params.options.attr_ids = this.form.attr_ids
if (this.form.tableCategory === 2) {
params.options.ret = 'cis'
}
}
delete params.showIcon
delete params.type_ids
delete params.attr_ids
delete params.tableCategory
postCustomDashboardPreview(params).then((res) => {
this.isShowPreview = true
this.previewData = res.counter
})
}
})
},
setExpFromFilter(filterExp) {
if (filterExp) {
this.filterExp = `${filterExp}`
} else {
this.filterExp = undefined
}
},
resetForm() {
this.form.type_id = undefined
this.form.type_ids = []
this.form.attr_ids = []
changeDashboardCategory(value) {
this.$refs.chartForm.clearValidate()
},
changeAttr(value) {
if (value && value.length) {
if (['line', 'pie'].includes(this.chartType)) {
this.form.attr_ids = [value[value.length - 1]]
}
if (['bar'].includes(this.chartType) && value.length > 2) {
this.form.attr_ids = value.slice(value.length - 2, value.length)
}
if (['table'].includes(this.chartType) && value.length > 3) {
this.form.attr_ids = value.slice(value.length - 3, value.length)
}
if (value === 1 && this.form.type_id) {
this.changeCIType(this.form.type_id)
}
},
clickLevel2children(e, citype, level) {
if (this.form.level !== level) {
this.$nextTick(() => {
this.form.type_ids = [citype.id]
})
}
this.form.level = level
},
},
}
</script>
<style lang="less" scoped>
.chart-wrapper {
display: flex;
.chart-left {
width: 50%;
.chart-left-preview {
border: 1px solid #e4e7ed;
border-radius: 2px;
height: 280px;
width: 92%;
position: relative;
padding: 12px;
.chart-left-preview-operation {
color: #86909c;
position: absolute;
top: 12px;
right: 12px;
cursor: pointer;
}
.chart-left-preview-box {
padding: 6px 12px;
height: 250px;
border-radius: 8px;
}
}
}
.chart-right {
width: 50%;
h4 {
font-weight: 700;
color: #000;
}
.chart-right-type {
display: flex;
justify-content: space-between;
background-color: #f0f5ff;
padding: 6px 12px;
.chart-right-type-box {
cursor: pointer;
width: 70px;
height: 60px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
> i {
font-size: 32px;
}
> span {
font-size: 12px;
}
}
.chart-right-type-box-selected {
background-color: #e5f1ff;
}
}
.chart-width {
width: 100%;
> label {
width: 25%;
text-align: center;
}
}
}
}
</style>
<style lang="less">
.chart-wrapper {
.ant-form-item {
margin-bottom: 0;
}
}
</style>
<style></style>

View File

@@ -1,61 +1,23 @@
export const category_1_bar_options = (data, options) => {
// 计算一级分类
const xData = Object.keys(data)
// 计算共有多少二级分类
const secondCategory = {}
Object.keys(data).forEach(key => {
if (Object.prototype.toString.call(data[key]) === '[object Object]') {
Object.keys(data[key]).forEach(key1 => {
secondCategory[key1] = Array.from({ length: xData.length }).fill(0)
})
} else {
secondCategory['其他'] = Array.from({ length: xData.length }).fill(0)
}
})
Object.keys(secondCategory).forEach(key => {
xData.forEach((x, idx) => {
if (data[x][key]) {
secondCategory[key][idx] = data[x][key]
}
if (typeof data[x] === 'number') {
secondCategory['其他'][idx] = data[x]
}
})
})
export const colorList = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc']
export const category_1_bar_options = (data) => {
return {
color: options.chartColor.split(','),
grid: {
top: 15,
left: 'left',
right: 10,
bottom: 20,
right: 0,
bottom: 0,
containLabel: true,
},
legend: {
data: Object.keys(secondCategory),
bottom: 0,
type: 'scroll',
},
xAxis: options.barDirection === 'y' ? {
xAxis: {
type: 'category',
axisTick: { show: false },
data: xData
}
: {
type: 'value',
splitLine: {
show: false
}
},
yAxis: options.barDirection === 'y' ? {
data: Object.keys(data)
},
yAxis: {
type: 'value',
splitLine: {
show: false
}
} : {
type: 'category',
axisTick: { show: false },
data: xData
},
tooltip: {
trigger: 'axis',
@@ -63,76 +25,34 @@ export const category_1_bar_options = (data, options) => {
type: 'shadow'
}
},
series: Object.keys(secondCategory).map(key => {
return {
name: key,
type: 'bar',
stack: options?.barStack ?? 'total',
barGap: 0,
emphasis: {
focus: 'series'
},
data: secondCategory[key]
}
})
}
}
export const category_1_line_options = (data, options) => {
const xData = Object.keys(data)
return {
color: options.chartColor.split(','),
grid: {
top: 15,
left: 'left',
right: 10,
bottom: 20,
containLabel: true,
},
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: xData
},
yAxis: {
type: 'value'
},
series: [
{
data: xData.map(item => data[item]),
type: 'line',
smooth: true,
showSymbol: false,
areaStyle: options?.isShadow ? {
opacity: 0.5,
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [{
offset: 0, color: options.chartColor.split(',')[0] // 0% 处的颜色
}, {
offset: 1, color: '#ffffff' // 100% 处的颜色
}],
global: false // 缺省为 false
data: Object.keys(data).map((key, index) => {
return {
value: data[key],
itemStyle: { color: colorList[0] }
}
} : null
}),
type: 'bar',
label: {
show: true,
position: 'top',
fontSize: 10,
formatter(data) {
return `${data.value || ''}`
}
},
}
]
}
}
export const category_1_pie_options = (data, options) => {
export const category_1_pie_options = (data) => {
return {
color: options.chartColor.split(','),
grid: {
top: 10,
left: 'left',
right: 10,
right: 0,
bottom: 0,
containLabel: true,
},
@@ -169,7 +89,7 @@ export const category_1_pie_options = (data, options) => {
}
}
export const category_2_bar_options = (data, options, chartType) => {
export const category_2_bar_options = (data) => {
const xAxisData = Object.keys(data.detail)
const _legend = []
xAxisData.forEach(key => {
@@ -177,11 +97,10 @@ export const category_2_bar_options = (data, options, chartType) => {
})
const legend = [...new Set(_legend)]
return {
color: options.chartColor.split(','),
grid: {
top: 15,
left: 'left',
right: 10,
right: 0,
bottom: 20,
containLabel: true,
},
@@ -197,110 +116,41 @@ export const category_2_bar_options = (data, options, chartType) => {
type: 'scroll',
data: legend
},
xAxis: options.barDirection === 'y' || chartType === 'line' ? {
type: 'category',
axisTick: { show: false },
data: xAxisData
}
: {
xAxis: [
{
type: 'category',
axisTick: { show: false },
data: xAxisData
}
],
yAxis: [
{
type: 'value',
splitLine: {
show: false
}
},
yAxis: options.barDirection === 'y' || chartType === 'line' ? {
type: 'value',
splitLine: {
show: false
}
} : {
type: 'category',
axisTick: { show: false },
data: xAxisData
},
series: legend.map((le, index) => {
],
series: legend.map(le => {
return {
name: le,
type: chartType,
type: 'bar',
barGap: 0,
emphasis: {
focus: 'series'
},
stack: chartType === 'line' ? '' : options?.barStack ?? 'total',
data: xAxisData.map(x => {
return data.detail[x][le] || 0
}),
smooth: true,
showSymbol: false,
label: {
show: false,
},
areaStyle: chartType === 'line' && options?.isShadow ? {
opacity: 0.5,
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [{
offset: 0, color: options.chartColor.split(',')[index % 8] // 0% 处的颜色
}, {
offset: 1, color: '#ffffff' // 100% 处的颜色
}],
global: false // 缺省为 false
show: true,
position: 'top',
fontSize: 10,
formatter(data) {
return `${data.value || ''}`
}
} : null
},
}
})
}
}
export const category_2_pie_options = (data, options) => {
console.log(1111, options)
const _legend = []
Object.keys(data.detail).forEach(key => {
Object.keys(data.detail[key]).forEach(key2 => {
_legend.push({ value: data.detail[key][key2], name: `${key}-${key2}` })
})
})
return {
color: options.chartColor.split(','),
grid: {
top: 15,
left: 'left',
right: 10,
bottom: 20,
containLabel: true,
},
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left',
type: 'scroll',
formatter: function (name) {
const _find = _legend.find(item => item.name === name)
return `${name}${_find.value}`
}
},
series: [
{
type: 'pie',
radius: '90%',
data: _legend,
label: {
show: false,
},
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
}
}

View File

@@ -1,54 +0,0 @@
<template>
<a-select v-model="currenColor">
<a-select-option v-for="i in list" :value="i" :key="i">
<div>
<span :style="{ backgroundColor: color }" class="color-box" v-for="color in i.split(',')" :key="color"></span>
</div>
</a-select-option>
</a-select>
</template>
<script>
export default {
name: 'ColorListPicker',
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: [String, Array],
default: null,
},
},
data() {
return {
list: [
'#6592FD,#6EE3EB,#44C2FD,#5F59F7,#1A348F,#7D8FCF,#A6D1E5,#8E56DD',
'#C1A9DC,#E2B5CD,#EE8EBC,#8483C3,#4D66BD,#213764,#D9B6E9,#DD88EB',
'#6FC4DF,#9FE8CE,#16B4BE,#86E6FB,#1871A3,#E1BF8D,#ED8D8D,#DD88EB',
'#F8B751,#FC9054,#FFE380,#DF963F,#AB5200,#EA9387,#FFBB7C,#D27467',
],
}
},
computed: {
currenColor: {
get() {
return this.value
},
set(val) {
this.$emit('change', val)
return val
},
},
},
}
</script>
<style lang="less" scoped>
.color-box {
display: inline-block;
width: 40px;
height: 10px;
}
</style>

View File

@@ -1,79 +0,0 @@
<template>
<div class="color-picker">
<div
:style="{
background: Array.isArray(item) ? `linear-gradient(to bottom, ${item[0]} 0%, ${item[1]} 100%)` : item,
}"
:class="{ 'color-picker-box': true, 'color-picker-box-selected': isEqual(currenColor, item) }"
v-for="item in colorList"
:key="Array.isArray(item) ? item.join() : item"
@click="changeColor(item)"
></div>
</div>
</template>
<script>
import _ from 'lodash'
export default {
name: 'ColorPicker',
model: {
prop: 'value',
event: 'change',
},
props: {
value: {
type: [String, Array],
default: null,
},
colorList: {
type: Array,
default: () => [],
},
},
computed: {
currenColor: {
get() {
return this.value
},
set(val) {
this.$emit('change', val)
return val
},
},
},
methods: {
isEqual: _.isEqual,
changeColor(item) {
this.$emit('change', item)
},
},
}
</script>
<style lang="less" scoped>
.color-picker {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10px;
.color-picker-box {
width: 19px;
height: 19px;
border: 1px solid #dae2e7;
border-radius: 1px;
cursor: pointer;
}
.color-picker-box-selected {
position: relative;
&:after {
content: '';
position: absolute;
width: 24px;
height: 24px;
border: 1px solid #43bbff;
top: -3px;
left: -3px;
}
}
}
</style>

View File

@@ -1,4 +1,5 @@
export const dashboardCategory = {
1: { label: '默认' },
2: { label: '关系' }
0: { label: 'CI数统计' },
1: { label: '按属性值分类统计' },
2: { label: '关系统计' }
}

View File

@@ -11,8 +11,8 @@
<template v-if="layout && layout.length">
<div v-if="editable">
<a-button
:style="{ marginLeft: '22px', marginTop: '20px' }"
@click="openChartForm('add', { options: { w: 3 } })"
:style="{ marginLeft: '10px' }"
@click="openChartForm('add', {})"
ghost
type="primary"
size="small"
@@ -39,44 +39,11 @@
:h="item.h"
:i="item.i"
:key="item.i"
:style="{
background:
item.options.chartType === 'count'
? Array.isArray(item.options.bgColor)
? `linear-gradient(to bottom, ${item.options.bgColor[0]} 0%, ${item.options.bgColor[1]} 100%)`
: item.options.bgColor
: '#fafafa',
}"
:style="{ backgroundColor: '#fafafa' }"
>
<div class="cmdb-dashboard-grid-item-title">
<template v-if="item.options.chartType !== 'count' && item.options.showIcon && getCiType(item)">
<template v-if="getCiType(item).icon">
<img
v-if="getCiType(item).icon.split('$$')[2]"
:src="`/api/common-setting/v1/file/${getCiType(item).icon.split('$$')[3]}`"
/>
<ops-icon
v-else
:style="{
color: getCiType(item).icon.split('$$')[1],
}"
:type="getCiType(item).icon.split('$$')[0]"
/>
</template>
<span :style="{ color: '#2f54eb' }" v-else>{{ getCiType(item).name[0].toUpperCase() }}</span>
</template>
<span :style="{ color: item.options.chartType === 'count' ? item.options.fontColor : '#000' }">{{
item.options.name
}}</span>
</div>
<CardTitle>{{ item.options.name }}</CardTitle>
<a-dropdown v-if="editable">
<a
class="cmdb-dashboard-grid-item-operation"
:style="{
color: item.options.chartType === 'count' ? item.options.fontColor : '',
}"
><a-icon type="menu"></a-icon
></a>
<a class="cmdb-dashboard-grid-item-operation"><a-icon type="menu"></a-icon></a>
<a-menu slot="overlay">
<a-menu-item>
<a @click="() => openChartForm('edit', item)"><a-icon style="margin-right:5px" type="edit" />编辑</a>
@@ -86,13 +53,13 @@
</a-menu-item>
</a-menu>
</a-dropdown>
<!-- <a
<a
v-if="editable && item.category === 1"
class="cmdb-dashboard-grid-item-chart-type"
@click="changeChartType(item)"
><a-icon
:type="item.options.chartType === 'bar' ? 'bar-chart' : 'pie-chart'"
/></a> -->
/></a>
<Chart
:ref="`chart_${item.id}`"
:chartId="item.id"
@@ -100,26 +67,18 @@
:category="item.category"
:options="item.options"
:editable="editable"
:ci_types="ci_types"
:type_id="item.type_id"
/>
</GridItem>
</GridLayout>
</template>
<div v-else class="dashboard-empty">
<a-empty :image="emptyImage" description=""></a-empty>
<a-button
@click="openChartForm('add', { options: { w: 3 } })"
v-if="editable"
size="small"
type="primary"
icon="plus"
>
<a-button @click="openChartForm('add', {})" v-if="editable" size="small" type="primary" icon="plus">
定制仪表盘
</a-button>
<span v-else>管理员暂未定制仪表盘</span>
</div>
<ChartForm ref="chartForm" @refresh="refresh" :ci_types="ci_types" :totalData="totalData" />
<ChartForm ref="chartForm" @refresh="refresh" :ci_types="ci_types" />
</div>
</template>
@@ -168,14 +127,12 @@ export default {
},
}
},
created() {
mounted() {
this.getLayout()
getCITypes().then((res) => {
this.ci_types = res.ci_types
})
},
mounted() {
this.getLayout()
},
methods: {
async getLayout() {
const res = await getCustomDashboard()
@@ -239,13 +196,6 @@ export default {
})
}
},
getCiType(item) {
if (item.type_id || item.options?.type_ids) {
const _find = this.ci_types.find((type) => type.id === item.type_id || type.id === item.options?.type_ids[0])
return _find || null
}
return null
},
},
}
</script>
@@ -256,18 +206,15 @@ export default {
text-align: center;
}
.cmdb-dashboard-grid-item {
border-radius: 8px;
padding: 6px 12px;
border-radius: 15px;
.cmdb-dashboard-grid-item-title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 700;
color: #000000;
padding-left: 6px;
color: #000000bd;
}
.cmdb-dashboard-grid-item-operation {
position: absolute;
right: 12px;
right: 6px;
top: 6px;
}
.cmdb-dashboard-grid-item-chart-type {
@@ -277,26 +224,3 @@ export default {
}
}
</style>
<style lang="less">
.cmdb-dashboard-grid-item-title {
display: flex;
align-items: center;
> i {
font-size: 16px;
margin-right: 5px;
}
> img {
width: 16px;
margin-right: 5px;
}
> span:not(:last-child) {
display: inline-block;
width: 16px;
height: 16px;
font-size: 16px;
text-align: center;
margin-right: 5px;
}
}
</style>

View File

@@ -10,12 +10,7 @@
<div class="discovery-bottom"></div>
<div class="discovery-top">
<div class="discovery-header">
<img
v-if="icon.id && 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 :type="icon.name || 'caise-chajian'" :style="{ fontSize: '30px', color: icon.color }"></ops-icon>
<span :title="rule.name">{{ rule.name }}</span>
</div>
<template v-if="!isSelected">

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