Compare commits

...

26 Commits
2.3.3 ... 2.3.4

Author SHA1 Message Date
pycook
2e644233bc release 2.3.4 2023-09-27 11:37:18 +08:00
simontigers
d9b4082b46 feat: add api get_notice_by_ids (#184) 2023-09-27 09:54:30 +08:00
wang-liang0615
a07f984152 前端更新 (#183)
* fix:add package

* fix:notice_info为null的情况
2023-09-27 09:18:33 +08:00
pycook
4cab7ef6b0 feat: ci triggers 2023-09-26 21:18:34 +08:00
wang-liang0615
070c163de6 fix:add package (#182) 2023-09-26 21:12:10 +08:00
wang-liang0615
282a779fb1 Merge pull request #181 from veops/dev_ui
前端更新
2023-09-26 20:34:27 +08:00
wang-liang0615
cb6b51a84c Merge branch 'master' into dev_ui 2023-09-26 20:34:14 +08:00
wang-liang0615
34bd320e75 fix:topo图相同节点出现两次的bug 2023-09-26 20:13:12 +08:00
wang-liang0615
1eca5791f6 feat:wangeditor 注册自定义组件 2023-09-26 20:07:00 +08:00
wang-liang0615
13b1c9a30c delete:删除getwx 2023-09-26 20:04:38 +08:00
simontigers
b1a15a85d2 feat: common notice config (#180) 2023-09-26 19:44:20 +08:00
wang-liang0615
08e5a02caf feat: UI更新 触发器 (#179)
* feat:新增api&适配

* feat:触发器

* add packages & 注释代码

* feat: webhook tips
2023-09-26 18:25:04 +08:00
wang-liang0615
308827b8fc feat: webhook tips 2023-09-26 18:17:23 +08:00
wang-liang0615
dc4ccb22b9 add packages & 注释代码 2023-09-26 17:35:41 +08:00
wang-liang0615
c482e7ea43 feat:触发器 2023-09-26 17:01:31 +08:00
wang-liang0615
663c14f763 feat:新增api&适配 2023-09-26 16:26:25 +08:00
pycook
c6ee227bab fix: ci_cache 2023-09-25 15:46:07 +08:00
wang-liang0615
cb62cf2410 Merge pull request #178 from veops/dev_ui
前端更新:仪表盘优化
2023-09-25 14:52:09 +08:00
wang-liang0615
133f32a6b0 pref:仪表盘优化 2023-09-25 14:50:08 +08:00
wang-liang0615
45c48c86fe Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-09-25 14:43:34 +08:00
pycook
2321f17dae refactor: CI triggers 2023-09-22 17:39:54 +08:00
simontigers
ddb31a07a2 fix: icon svg support (#177) 2023-09-20 15:56:57 +08:00
pycook
b474914fbb fix date search 2023-09-18 18:15:02 +08:00
pycook
26099a3d69 fix dashboard compute 2023-09-18 13:04:50 +08:00
wang-liang0615
9f1b510cb3 Merge branch 'master' of github.com:veops/cmdb into dev_ui 2023-09-11 17:35:44 +08:00
wang-liang0615
61acb2483d 计算属性 触发计算 2023-09-11 17:34:51 +08:00
59 changed files with 5578 additions and 3030 deletions

View File

@@ -44,10 +44,12 @@ treelib = "==1.6.1"
flasgger = "==0.9.5"
Pillow = "==9.3.0"
# other
six = "==1.12.0"
six = "==1.16.0"
bs4 = ">=0.0.1"
toposort = ">=1.5"
requests = ">=2.22.0"
requests_oauthlib = "==1.3.1"
markdownify = "==0.11.6"
PyJWT = "==2.4.0"
elasticsearch = "==7.17.9"
future = "==0.18.3"

View File

@@ -15,7 +15,6 @@ import api.lib.cmdb.ci
from api.extensions import db
from api.extensions import rd
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.ci_type import CITypeTriggerManager
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import REDIS_PREFIX_CI
from api.lib.cmdb.const import REDIS_PREFIX_CI_RELATION
@@ -24,8 +23,8 @@ from api.lib.cmdb.const import RoleEnum
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.acl import UserCache
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
@@ -227,50 +226,60 @@ def cmdb_counter():
@with_appcontext
def cmdb_trigger():
"""
Trigger execution
Trigger execution for date attribute
"""
from api.lib.cmdb.ci import CITriggerManager
current_day = datetime.datetime.today().strftime("%Y-%m-%d")
trigger2cis = dict()
trigger2completed = dict()
i = 0
while True:
db.session.remove()
if datetime.datetime.today().strftime("%Y-%m-%d") != current_day:
trigger2cis = dict()
trigger2completed = dict()
current_day = datetime.datetime.today().strftime("%Y-%m-%d")
try:
db.session.remove()
if i == 360 or i == 0:
i = 0
try:
triggers = CITypeTrigger.get_by(to_dict=False)
if datetime.datetime.today().strftime("%Y-%m-%d") != current_day:
trigger2cis = dict()
trigger2completed = dict()
current_day = datetime.datetime.today().strftime("%Y-%m-%d")
if i == 3 or i == 0:
i = 0
triggers = CITypeTrigger.get_by(to_dict=False, __func_isnot__key_attr_id=None)
for trigger in triggers:
ready_cis = CITypeTriggerManager.waiting_cis(trigger)
try:
ready_cis = CITriggerManager.waiting_cis(trigger)
except Exception as e:
print(e)
continue
if trigger.id not in trigger2cis:
trigger2cis[trigger.id] = (trigger, ready_cis)
else:
cur = trigger2cis[trigger.id]
cur_ci_ids = {i.ci_id for i in cur[1]}
trigger2cis[trigger.id] = (trigger, cur[1] + [i for i in ready_cis if i.ci_id not in cur_ci_ids
and i.ci_id not in trigger2completed[trigger.id]])
trigger2cis[trigger.id] = (
trigger, cur[1] + [i for i in ready_cis if i.ci_id not in cur_ci_ids
and i.ci_id not in trigger2completed.get(trigger.id, {})])
except Exception as e:
print(e)
for tid in trigger2cis:
trigger, cis = trigger2cis[tid]
for ci in copy.deepcopy(cis):
if CITriggerManager.trigger_notify(trigger, ci):
trigger2completed.setdefault(trigger.id, set()).add(ci.ci_id)
for tid in trigger2cis:
trigger, cis = trigger2cis[tid]
for ci in copy.deepcopy(cis):
if CITypeTriggerManager.trigger_notify(trigger, ci):
trigger2completed.setdefault(trigger.id, set()).add(ci.ci_id)
for _ci in cis:
if _ci.ci_id == ci.ci_id:
cis.remove(_ci)
for _ci in cis:
if _ci.ci_id == ci.ci_id:
cis.remove(_ci)
i += 1
time.sleep(10)
i += 1
time.sleep(10)
except Exception as e:
import traceback
print(traceback.format_exc())
current_app.logger.error("cmdb trigger exception: {}".format(e))
time.sleep(60)
@click.command()

View File

@@ -230,3 +230,59 @@ def init_department():
cli.init_wide_company()
cli.create_acl_role_with_department()
cli.init_backend_resource()
@click.command()
@with_appcontext
def common_check_new_columns():
"""
add new columns to tables
"""
from api.extensions import db
from sqlalchemy import inspect, text
def get_model_by_table_name(table_name):
for model in db.Model.registry._class_registry.values():
if hasattr(model, '__tablename__') and model.__tablename__ == table_name:
return model
return None
def add_new_column(table_name, new_column):
column_type = new_column.type.compile(engine.dialect)
default_value = new_column.default.arg if new_column.default else None
sql = f"ALTER TABLE {table_name} ADD COLUMN {new_column.name} {column_type} "
if new_column.comment:
sql += f" comment '{new_column.comment}'"
if column_type == 'JSON':
pass
elif default_value:
if column_type.startswith('VAR') or column_type.startswith('Text'):
if default_value is None or len(default_value) == 0:
pass
else:
sql += f" DEFAULT {default_value}"
sql = text(sql)
db.session.execute(sql)
engine = db.get_engine()
inspector = inspect(engine)
table_names = inspector.get_table_names()
for table_name in table_names:
existed_columns = inspector.get_columns(table_name)
existed_column_name_list = [c['name'] for c in existed_columns]
model = get_model_by_table_name(table_name)
if model is None:
continue
model_columns = model.__table__.columns._all_columns
for column in model_columns:
if column.name not in existed_column_name_list:
try:
add_new_column(table_name, column)
current_app.logger.info(f"add new column [{column.name}] in table [{table_name}] success.")
except Exception as e:
current_app.logger.error(f"add new column [{column.name}] in table [{table_name}] err:")
current_app.logger.error(e)

View File

@@ -335,14 +335,20 @@ class CMDBCounterCache(object):
def attribute_counter(custom):
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
from api.lib.cmdb.utils import ValueTypeMap
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])))
try:
attr2value_type = [AttributeCache.get(i).value_type for i in attr_ids]
except AttributeError:
return
other_filter = custom['options'].get('filter')
other_filter = "({})".format(other_filter) if other_filter else ''
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)
@@ -365,7 +371,7 @@ class CMDBCounterCache(object):
current_app.logger.error(e)
return
for i in (list(facet.values()) or [[]])[0]:
result[i[0]] = i[1]
result[ValueTypeMap.serialize2[attr2value_type[0]](str(i[0]))] = i[1]
if len(attr_ids) == 1:
return result
@@ -380,7 +386,7 @@ class CMDBCounterCache(object):
return
result[v] = dict()
for i in (list(facet.values()) or [[]])[0]:
result[v][i[0]] = i[1]
result[v][ValueTypeMap.serialize2[attr2value_type[1]](str(i[0]))] = i[1]
if len(attr_ids) == 2:
return result
@@ -400,7 +406,7 @@ class CMDBCounterCache(object):
return
result[v1][v2] = dict()
for i in (list(facet.values()) or [[]])[0]:
result[v1][v2][i[0]] = i[1]
result[v1][v2][ValueTypeMap.serialize2[attr2value_type[2]](str(i[0]))] = i[1]
return result

View File

@@ -4,6 +4,7 @@
import copy
import datetime
import json
import threading
from flask import abort
from flask import current_app
@@ -24,29 +25,36 @@ from api.lib.cmdb.const import CMDB_QUEUE
from api.lib.cmdb.const import ConstraintEnum
from api.lib.cmdb.const import ExistPolicy
from api.lib.cmdb.const import OperateType
from api.lib.cmdb.const import PermEnum, ResourceTypeEnum
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import REDIS_PREFIX_CI
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RetKey
from api.lib.cmdb.history import AttributeHistoryManger
from api.lib.cmdb.history import CIRelationHistoryManager
from api.lib.cmdb.history import CITriggerHistoryManager
from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.cmdb.utils import TableMap
from api.lib.cmdb.utils import ValueTypeMap
from api.lib.cmdb.value import AttributeValueManager
from api.lib.decorator import kwargs_required
from api.lib.notify import notify_send
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import is_app_admin
from api.lib.perm.acl.acl import validate_permission
from api.lib.utils import Lock
from api.lib.utils import handle_arg_list
from api.lib.webhook import webhook_request
from api.models.cmdb import AttributeHistory
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.models.cmdb import CITypeTrigger
from api.tasks.cmdb import ci_cache
from api.tasks.cmdb import ci_delete
from api.tasks.cmdb import ci_delete_trigger
from api.tasks.cmdb import ci_relation_add
from api.tasks.cmdb import ci_relation_cache
from api.tasks.cmdb import ci_relation_delete
@@ -378,16 +386,17 @@ class CIManager(object):
key2attr = value_manager.valid_attr_value(ci_dict, ci_type.id, ci and ci.id,
ci_type_attrs_name, ci_type_attrs_alias, ci_attr2type_attr)
operate_type = OperateType.UPDATE if ci is not None else OperateType.ADD
try:
ci = ci or CI.create(type_id=ci_type.id, is_auto_discovery=is_auto_discovery)
record_id = value_manager.create_or_update_attr_value2(ci, ci_dict, key2attr)
record_id = value_manager.create_or_update_attr_value(ci, ci_dict, key2attr)
except BadRequest as e:
if existed is None:
cls.delete(ci.id)
raise e
if record_id: # has change
ci_cache.apply_async([ci.id], queue=CMDB_QUEUE)
ci_cache.apply_async(args=(ci.id, operate_type, record_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)
@@ -427,12 +436,12 @@ class CIManager(object):
return abort(403, ErrFormat.ci_filter_perm_attr_no_permission.format(k))
try:
record_id = value_manager.create_or_update_attr_value2(ci, ci_dict, key2attr)
record_id = value_manager.create_or_update_attr_value(ci, ci_dict, key2attr)
except BadRequest as e:
raise e
if record_id: # has change
ci_cache.apply_async([ci_id], queue=CMDB_QUEUE)
ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_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:
@@ -442,9 +451,10 @@ class CIManager(object):
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)))
AttributeValueManager().create_or_update_attr_value(unique_name, unique_value, ci)
key2attr = {unique_name: AttributeCache.get(unique_name)}
record_id = AttributeValueManager().create_or_update_attr_value(ci, {unique_name: unique_value}, key2attr)
ci_cache.apply_async([ci_id], queue=CMDB_QUEUE)
ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE)
@classmethod
def delete(cls, ci_id):
@@ -455,6 +465,17 @@ class CIManager(object):
ci_dict = cls.get_cis_by_ids([ci_id])
ci_dict = ci_dict and ci_dict[0]
triggers = CITriggerManager.get(ci_dict['_type'])
for trigger in triggers:
option = trigger['option']
if not option.get('enable') or option.get('action') != OperateType.DELETE:
continue
if option.get('filter') and not CITriggerManager.ci_filter(ci_dict.get('_id'), option['filter']):
continue
ci_delete_trigger.apply_async(args=(trigger, OperateType.DELETE, ci_dict), queue=CMDB_QUEUE)
attrs = CITypeAttribute.get_by(type_id=ci.type_id, to_dict=False)
attr_names = set([AttributeCache.get(attr.attr_id).name for attr in attrs])
for attr_name in attr_names:
@@ -479,7 +500,7 @@ class CIManager(object):
AttributeHistoryManger.add(None, ci_id, [(None, OperateType.DELETE, ci_dict, None)], ci.type_id)
ci_delete.apply_async([ci.id], queue=CMDB_QUEUE)
ci_delete.apply_async(args=(ci_id,), queue=CMDB_QUEUE)
return ci_id
@@ -896,3 +917,180 @@ class CIRelationManager(object):
for parent_id in parents:
for ci_id in ci_ids:
cls.delete_2(parent_id, ci_id)
class CITriggerManager(object):
@staticmethod
def get(type_id):
db.session.remove()
return CITypeTrigger.get_by(type_id=type_id, to_dict=True)
@staticmethod
def _update_old_attr_value(record_id, ci_dict):
attr_history = AttributeHistory.get_by(record_id=record_id, to_dict=False)
attr_dict = dict()
for attr_h in attr_history:
attr_dict['old_{}'.format(AttributeCache.get(attr_h.attr_id).name)] = attr_h.old
ci_dict.update({'old_{}'.format(k): ci_dict[k] for k in ci_dict})
ci_dict.update(attr_dict)
@classmethod
def _exec_webhook(cls, operate_type, webhook, ci_dict, trigger_id, trigger_name, record_id, ci_id=None, app=None):
app = app or current_app
with app.app_context():
if operate_type == OperateType.UPDATE:
cls._update_old_attr_value(record_id, ci_dict)
if ci_id is not None:
ci_dict = CIManager().get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
try:
response = webhook_request(webhook, ci_dict).text
is_ok = True
except Exception as e:
current_app.logger.warning("exec webhook failed: {}".format(e))
response = e
is_ok = False
CITriggerHistoryManager.add(operate_type,
record_id,
ci_dict.get('_id'),
trigger_id,
trigger_name,
is_ok=is_ok,
webhook=response)
return is_ok
@classmethod
def _exec_notify(cls, operate_type, notify, ci_dict, trigger_id, trigger_name, record_id, ci_id=None, app=None):
app = app or current_app
with app.app_context():
if ci_id is not None:
ci_dict = CIManager().get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
if operate_type == OperateType.UPDATE:
cls._update_old_attr_value(record_id, ci_dict)
is_ok = True
response = ''
for method in (notify.get('method') or []):
try:
res = notify_send(notify.get('subject'), notify.get('body'), [method],
notify.get('tos'), ci_dict)
response = "{}\n{}".format(response, res)
except Exception as e:
current_app.logger.warning("send notify failed: {}".format(e))
response = "{}\n{}".format(response, e)
is_ok = False
CITriggerHistoryManager.add(operate_type,
record_id,
ci_dict.get('_id'),
trigger_id,
trigger_name,
is_ok=is_ok,
notify=response.strip())
return is_ok
@staticmethod
def ci_filter(ci_id, other_filter):
from api.lib.cmdb.search import SearchError
from api.lib.cmdb.search.ci import search
query = "{},_id:{}".format(other_filter, ci_id)
try:
_, _, _, _, numfound, _ = search(query).search()
return numfound
except SearchError as e:
current_app.logger.warning("ci search failed: {}".format(e))
@classmethod
def fire(cls, operate_type, ci_dict, record_id):
type_id = ci_dict.get('_type')
triggers = cls.get(type_id) or []
for trigger in triggers:
option = trigger['option']
if not option.get('enable'):
continue
if option.get('filter') and not cls.ci_filter(ci_dict.get('_id'), option['filter']):
continue
if option.get('attr_ids') and isinstance(option['attr_ids'], list):
if not (set(option['attr_ids']) &
set([i.attr_id for i in AttributeHistory.get_by(record_id=record_id, to_dict=False)])):
continue
if option.get('action') == operate_type:
cls.fire_by_trigger(trigger, operate_type, ci_dict, record_id)
@classmethod
def fire_by_trigger(cls, trigger, operate_type, ci_dict, record_id=None):
option = trigger['option']
if option.get('webhooks'):
cls._exec_webhook(operate_type, option['webhooks'], ci_dict, trigger['id'],
option.get('name'), record_id)
elif option.get('notifies'):
cls._exec_notify(operate_type, option['notifies'], ci_dict, trigger['id'],
option.get('name'), record_id)
@classmethod
def waiting_cis(cls, trigger):
now = datetime.datetime.today()
config = trigger.option.get('notifies') or {}
delta_time = datetime.timedelta(days=(config.get('before_days', 0) or 0))
attr = AttributeCache.get(trigger.attr_id)
value_table = TableMap(attr=attr).table
values = value_table.get_by(attr_id=attr.id, to_dict=False)
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 trigger.option.get('filter') and not cls.ci_filter(v.ci_id, trigger.option['filter']):
continue
result.append(v)
return result
@classmethod
def trigger_notify(cls, trigger, ci):
"""
only for date attribute
:param trigger:
:param ci:
:return:
"""
if (trigger.option.get('notifies', {}).get('notify_at') == datetime.datetime.now().strftime("%H:%M") or
not trigger.option.get('notifies', {}).get('notify_at')):
if trigger.option.get('webhooks'):
threading.Thread(target=cls._exec_webhook, args=(
None, trigger.option['webhooks'], None, trigger.id, trigger.option.get('name'), None, ci.ci_id,
current_app._get_current_object())).start()
elif trigger.option.get('notifies'):
threading.Thread(target=cls._exec_notify, args=(
None, trigger.option['notifies'], None, trigger.id, trigger.option.get('name'), None, ci.ci_id,
current_app._get_current_object())).start()
return True
return False

View File

@@ -582,7 +582,8 @@ class CITypeRelationManager(object):
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]
if children:
result.setdefault(level + 1, []).extend([i.child.to_dict() for i in children])
for i in children:
if i.child_id != _id:
@@ -1165,16 +1166,18 @@ class CITypeUniqueConstraintManager(object):
class CITypeTriggerManager(object):
@staticmethod
def get(type_id):
return CITypeTrigger.get_by(type_id=type_id, to_dict=True)
def get(type_id, to_dict=True):
return CITypeTrigger.get_by(type_id=type_id, to_dict=to_dict)
@staticmethod
def add(type_id, attr_id, notify):
CITypeTrigger.get_by(type_id=type_id, attr_id=attr_id) and abort(400, ErrFormat.ci_type_trigger_duplicate)
def add(type_id, attr_id, option):
for i in CITypeTrigger.get_by(type_id=type_id, attr_id=attr_id, to_dict=False):
if i.option == option:
return abort(400, ErrFormat.ci_type_trigger_duplicate)
not isinstance(notify, dict) and abort(400, ErrFormat.argument_invalid.format("notify"))
not isinstance(option, dict) and abort(400, ErrFormat.argument_invalid.format("option"))
trigger = CITypeTrigger.create(type_id=type_id, attr_id=attr_id, notify=notify)
trigger = CITypeTrigger.create(type_id=type_id, attr_id=attr_id, option=option)
CITypeHistoryManager.add(CITypeOperateType.ADD_TRIGGER,
type_id,
@@ -1184,12 +1187,12 @@ class CITypeTriggerManager(object):
return trigger.to_dict()
@staticmethod
def update(_id, notify):
def update(_id, attr_id, option):
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)
new = existed.update(attr_id=attr_id or None, option=option, filter_none=False)
CITypeHistoryManager.add(CITypeOperateType.UPDATE_TRIGGER,
existed.type_id,
@@ -1209,35 +1212,3 @@ class CITypeTriggerManager(object):
existed.type_id,
trigger_id=_id,
change=existed.to_dict())
@staticmethod
def waiting_cis(trigger):
now = datetime.datetime.today()
delta_time = datetime.timedelta(days=(trigger.notify.get('before_days', 0) or 0))
attr = AttributeCache.get(trigger.attr_id)
value_table = TableMap(attr=attr).table
values = value_table.get_by(attr_id=attr.id, to_dict=False)
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")):
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')):
from api.tasks.cmdb import trigger_notify
trigger_notify.apply_async(args=(trigger.notify, ci.ci_id), queue=CMDB_QUEUE)
return True
return False

View File

@@ -16,6 +16,7 @@ from api.lib.perm.acl.cache import UserCache
from api.models.cmdb import Attribute
from api.models.cmdb import AttributeHistory
from api.models.cmdb import CIRelationHistory
from api.models.cmdb import CITriggerHistory
from api.models.cmdb import CITypeHistory
from api.models.cmdb import CITypeTrigger
from api.models.cmdb import CITypeUniqueConstraint
@@ -286,3 +287,68 @@ class CITypeHistoryManager(object):
change=change)
CITypeHistory.create(**payload)
class CITriggerHistoryManager(object):
@staticmethod
def get(page, page_size, type_id=None, trigger_id=None, operate_type=None):
query = CITriggerHistory.get_by(only_query=True)
if type_id:
query = query.filter(CITriggerHistory.type_id == type_id)
if trigger_id:
query = query.filter(CITriggerHistory.trigger_id == trigger_id)
if operate_type:
query = query.filter(CITriggerHistory.operate_type == operate_type)
numfound = query.count()
query = query.order_by(CITriggerHistory.id.desc())
result = query.offset((page - 1) * page_size).limit(page_size)
result = [i.to_dict() for i in result]
for res in result:
if res.get('trigger_id'):
trigger = CITypeTrigger.get_by_id(res['trigger_id'])
res['trigger'] = trigger and trigger.to_dict()
return numfound, result
@staticmethod
def get_by_ci_id(ci_id):
res = db.session.query(CITriggerHistory, CITypeTrigger).join(
CITypeTrigger, CITypeTrigger.id == CITriggerHistory.trigger_id).filter(
CITriggerHistory.ci_id == ci_id).order_by(CITriggerHistory.id.desc())
result = []
id2trigger = dict()
for i in res:
hist = i.CITriggerHistory
item = dict(is_ok=hist.is_ok,
operate_type=hist.operate_type,
notify=hist.notify,
trigger_id=hist.trigger_id,
trigger_name=hist.trigger_name,
webhook=hist.webhook,
created_at=hist.created_at.strftime('%Y-%m-%d %H:%M:%S'),
record_id=hist.record_id,
hid=hist.id
)
if i.CITypeTrigger.id not in id2trigger:
id2trigger[i.CITypeTrigger.id] = i.CITypeTrigger.to_dict()
result.append(item)
return dict(items=result, id2trigger=id2trigger)
@staticmethod
def add(operate_type, record_id, ci_id, trigger_id, trigger_name, is_ok=False, notify=None, webhook=None):
CITriggerHistory.create(operate_type=operate_type,
record_id=record_id,
ci_id=ci_id,
trigger_id=trigger_id,
trigger_name=trigger_name,
is_ok=is_ok,
notify=notify,
webhook=webhook)

View File

@@ -1,4 +1,4 @@
# -*- coding:utf-8 -*-
# -*- coding:utf-8 -*-
from __future__ import unicode_literals
@@ -141,6 +141,10 @@ class Search(object):
@staticmethod
def _in_query_handler(attr, v, is_not):
new_v = v[1:-1].split(";")
if attr.value_type == ValueTypeEnum.DATE:
new_v = ["{} 00:00:00".format(i) for i in new_v if len(i) == 10]
table_name = TableMap(attr=attr).table_name
in_query = " OR {0}.value ".format(table_name).join(['{0} "{1}"'.format(
"NOT LIKE" if is_not else "LIKE",
@@ -151,6 +155,11 @@ class Search(object):
@staticmethod
def _range_query_handler(attr, v, is_not):
start, end = [x.strip() for x in v[1:-1].split("_TO_")]
if attr.value_type == ValueTypeEnum.DATE:
start = "{} 00:00:00".format(start) if len(start) == 10 else start
end = "{} 00:00:00".format(end) if len(end) == 10 else end
table_name = TableMap(attr=attr).table_name
range_query = "{0} '{1}' AND '{2}'".format(
"NOT BETWEEN" if is_not else "BETWEEN",
@@ -162,8 +171,14 @@ class Search(object):
def _comparison_query_handler(attr, v):
table_name = TableMap(attr=attr).table_name
if v.startswith(">=") or v.startswith("<="):
if attr.value_type == ValueTypeEnum.DATE and len(v[2:]) == 10:
v = "{} 00:00:00".format(v)
comparison_query = "{0} '{1}'".format(v[:2], v[2:].replace("*", "%"))
else:
if attr.value_type == ValueTypeEnum.DATE and len(v[1:]) == 10:
v = "{} 00:00:00".format(v)
comparison_query = "{0} '{1}'".format(v[0], v[1:].replace("*", "%"))
_query_sql = QUERY_CI_BY_ATTR_NAME.format(table_name, attr.id, comparison_query)
return _query_sql
@@ -239,7 +254,7 @@ class Search(object):
attr_id = attr.id
table_name = TableMap(attr=attr).table_name
_v_query_sql = """SELECT {0}.ci_id, {1}.value
_v_query_sql = """SELECT {0}.ci_id, {1}.value
FROM ({2}) AS {0} INNER JOIN {1} ON {1}.ci_id = {0}.ci_id
WHERE {1}.attr_id = {3}""".format("ALIAS", table_name, query_sql, attr_id)
new_table = _v_query_sql
@@ -285,7 +300,7 @@ class Search(object):
query_sql = "SELECT * FROM ({0}) as {1} UNION ALL ({2})".format(query_sql, alias, _query_sql)
elif operator == "~":
query_sql = """SELECT * FROM ({0}) as {1} LEFT JOIN ({2}) as {3} USING(ci_id)
query_sql = """SELECT * FROM ({0}) as {1} LEFT JOIN ({2}) as {3} USING(ci_id)
WHERE {3}.ci_id is NULL""".format(query_sql, alias, _query_sql, alias + "A")
return query_sql
@@ -295,7 +310,7 @@ class Search(object):
start = time.time()
execute = db.session.execute
current_app.logger.debug(v_query_sql)
# current_app.logger.debug(v_query_sql)
res = execute(v_query_sql).fetchall()
end_time = time.time()
current_app.logger.debug("query ci ids time is: {0}".format(end_time - start))
@@ -391,6 +406,9 @@ class Search(object):
is_not = True if operator == "|~" else False
if field_type == ValueTypeEnum.DATE and len(v) == 10:
v = "{} 00:00:00".format(v)
# in query
if v.startswith("(") and v.endswith(")"):
_query_sql = self._in_query_handler(attr, v, is_not)
@@ -506,7 +524,7 @@ class Search(object):
if k:
table_name = TableMap(attr=attr).table_name
query_sql = FACET_QUERY.format(table_name, self.query_sql, attr.id)
# current_app.logger.debug(query_sql)
# current_app.logger.warning(query_sql)
result = db.session.execute(query_sql).fetchall()
facet[k] = result

View File

@@ -18,7 +18,6 @@ from api.extensions import db
from api.lib.cmdb.attribute import AttributeManager
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeAttributeCache
from api.lib.cmdb.const import ExistPolicy
from api.lib.cmdb.const import OperateType
from api.lib.cmdb.const import ValueTypeEnum
from api.lib.cmdb.history import AttributeHistoryManger
@@ -140,6 +139,7 @@ class AttributeValueManager(object):
try:
db.session.commit()
except Exception as e:
db.session.rollback()
current_app.logger.error("write change failed: {}".format(str(e)))
return record_id
@@ -235,7 +235,7 @@ class AttributeValueManager(object):
return key2attr
def create_or_update_attr_value2(self, ci, ci_dict, key2attr):
def create_or_update_attr_value(self, ci, ci_dict, key2attr):
"""
add or update attribute value, then write history
:param ci: instance object
@@ -288,66 +288,6 @@ class AttributeValueManager(object):
return self._write_change2(changed)
def create_or_update_attr_value(self, key, value, ci, _no_attribute_policy=ExistPolicy.IGNORE, record_id=None):
"""
add or update attribute value, then write history
:param key: id, name or alias
:param value:
:param ci: instance object
:param _no_attribute_policy: ignore or reject
:param record_id: op record
:return:
"""
attr = self._get_attr(key)
if attr is None:
if _no_attribute_policy == ExistPolicy.IGNORE:
return
if _no_attribute_policy == ExistPolicy.REJECT:
return abort(400, ErrFormat.attribute_not_found.format(key))
value_table = TableMap(attr=attr).table
try:
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, '')
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]
added = set(value_list) - set(existed_values)
deleted = set(existed_values) - set(value_list)
for v in added:
value_table.create(ci_id=ci.id, attr_id=attr.id, value=v)
record_id = self._write_change(ci.id, attr.id, OperateType.ADD, None, v, record_id, ci.type_id)
for v in deleted:
existed_attr = existed_attrs[existed_values.index(v)]
existed_attr.delete()
record_id = self._write_change(ci.id, attr.id, OperateType.DELETE, v, None, record_id, ci.type_id)
else:
value = self._validate(attr, value, value_table, ci)
existed_attr = value_table.get_by(attr_id=attr.id, ci_id=ci.id, first=True, to_dict=False)
existed_value = existed_attr and existed_attr.value
if existed_value is None and value is not None:
value_table.create(ci_id=ci.id, attr_id=attr.id, value=value)
record_id = self._write_change(ci.id, attr.id, OperateType.ADD, None, value, record_id, ci.type_id)
else:
if existed_value != value:
if value is None:
existed_attr.delete()
else:
existed_attr.update(value=value)
record_id = self._write_change(ci.id, attr.id, OperateType.UPDATE,
existed_value, value, record_id, ci.type_id)
return record_id
except Exception as e:
current_app.logger.warning(str(e))
return abort(400, ErrFormat.attribute_value_invalid2.format("{}({})".format(attr.alias, attr.name), value))
@staticmethod
def delete_attr_value(attr_id, ci_id):
attr = AttributeCache.get(attr_id)

View File

@@ -474,6 +474,29 @@ class EmployeeCRUD(object):
return [r.to_dict() for r in results]
@staticmethod
def get_employee_notice_by_ids(employee_ids):
criterion = [
Employee.employee_id.in_(employee_ids),
Employee.deleted == 0,
]
direct_columns = ['email', 'mobile']
employees = Employee.query.filter(
*criterion
).all()
results = []
for employee in employees:
d = employee.to_dict()
tmp = dict(
employee_id=employee.employee_id,
)
for column in direct_columns:
tmp[column] = d.get(column, '')
notice_info = d.get('notice_info', {})
tmp.update(**notice_info)
results.append(tmp)
return results
def get_user_map(key='uid', acl=None):
"""

View File

@@ -0,0 +1,94 @@
from api.models.common_setting import NoticeConfig
from wtforms import Form
from wtforms import StringField
from wtforms import validators
from flask import abort
import smtplib
from email.mime.text import MIMEText
from email.utils import formataddr
class NoticeConfigCRUD(object):
@staticmethod
def add_notice_config(**kwargs):
NoticeConfigCRUD.check_platform(kwargs.get('platform'))
try:
return NoticeConfig.create(
**kwargs
)
except Exception as e:
return abort(400, str(e))
@staticmethod
def check_platform(platform):
NoticeConfig.get_by(first=True, to_dict=False, platform=platform) and abort(400, f"{platform} 已存在!")
@staticmethod
def edit_notice_config(_id, **kwargs):
existed = NoticeConfigCRUD.get_notice_config_by_id(_id)
try:
return existed.update(**kwargs)
except Exception as e:
return abort(400, str(e))
@staticmethod
def get_notice_config_by_id(_id):
return NoticeConfig.get_by(first=True, to_dict=False, id=_id) or abort(400, f"{_id} 配置项不存在!")
@staticmethod
def get_all():
return NoticeConfig.get_by(to_dict=True)
@staticmethod
def test_send_email(receive_address, **kwargs):
# 设置发送方和接收方的电子邮件地址
sender_email = 'test@test.com'
sender_name = 'Test Sender'
recipient_email = receive_address
recipient_name = receive_address
subject = 'Test Email'
body = 'This is a test email'
message = MIMEText(body, 'plain', 'utf-8')
message['From'] = formataddr((sender_name, sender_email))
message['To'] = formataddr((recipient_name, recipient_email))
message['Subject'] = subject
smtp_server = kwargs.get('server')
smtp_port = kwargs.get('port')
smtp_username = kwargs.get('username')
smtp_password = kwargs.get('password')
if kwargs.get('mail_type') == 'SMTP':
smtp_connection = smtplib.SMTP(smtp_server, smtp_port)
else:
smtp_connection = smtplib.SMTP_SSL(smtp_server, smtp_port)
if kwargs.get('is_login'):
smtp_connection.login(smtp_username, smtp_password)
smtp_connection.sendmail(sender_email, recipient_email, message.as_string())
smtp_connection.quit()
return 1
class NoticeConfigForm(Form):
platform = StringField(validators=[
validators.DataRequired(message="平台 不能为空"),
validators.Length(max=255),
])
info = StringField(validators=[
validators.DataRequired(message="信息 不能为空"),
validators.Length(max=255),
])
class NoticeConfigUpdateForm(Form):
info = StringField(validators=[
validators.DataRequired(message="信息 不能为空"),
validators.Length(max=255),
])

View File

@@ -53,5 +53,6 @@ class ErrFormat(CommonErrFormat):
username_is_required = "username不能为空"
email_is_required = "邮箱不能为空"
email_format_error = "邮箱格式错误"
email_send_timeout = "邮件发送超时"
common_data_not_found = "ID {} 找不到记录"

View File

@@ -0,0 +1,55 @@
# -*- coding:utf-8 -*-
import json
import requests
from flask import current_app
from jinja2 import Template
from markdownify import markdownify as md
from api.lib.mail import send_mail
def _request_messenger(subject, body, tos, sender, payload):
params = dict(sender=sender, title=subject,
tos=[to[sender] for to in tos if to.get(sender)])
if not params['tos']:
raise Exception("no receivers")
params['tos'] = [Template(i).render(payload) for i in params['tos'] if i.strip()]
if sender == "email":
params['msgtype'] = 'text/html'
params['content'] = body
else:
params['msgtype'] = 'markdown'
try:
content = md("{}\n{}".format(subject or '', body or ''))
except Exception as e:
current_app.logger.warning("html2markdown failed: {}".format(e))
content = "{}\n{}".format(subject or '', body or '')
params['content'] = json.dumps(dict(content=content))
resp = requests.post(current_app.config.get('MESSENGER_URL'), json=params)
if resp.status_code != 200:
raise Exception(resp.text)
return resp.text
def notify_send(subject, body, methods, tos, payload=None):
payload = payload or {}
payload = {k: v or '' for k, v in payload.items()}
subject = Template(subject).render(payload)
body = Template(body).render(payload)
res = ''
for method in methods:
if method == "email" and not current_app.config.get('USE_MESSENGER', True):
send_mail(None, [Template(to.get('email')).render(payload) for to in tos], subject, body)
res += (_request_messenger(subject, body, tos, method, payload) + "\n")
return res

109
cmdb-api/api/lib/webhook.py Normal file
View File

@@ -0,0 +1,109 @@
# -*- coding:utf-8 -*-
import json
from functools import partial
import requests
from jinja2 import Template
from requests.auth import HTTPBasicAuth
from requests_oauthlib import OAuth2Session
class BearerAuth(requests.auth.AuthBase):
def __init__(self, token):
self.token = token
def __call__(self, r):
r.headers["authorization"] = "Bearer {}".format(self.token)
return r
def _wrap_auth(**kwargs):
auth_type = (kwargs.get('type') or "").lower()
if auth_type == "basicauth":
return HTTPBasicAuth(kwargs.get('username'), kwargs.get('password'))
elif auth_type == "bearer":
return BearerAuth(kwargs.get('token'))
elif auth_type == 'oauth2.0':
client_id = kwargs.get('client_id')
client_secret = kwargs.get('client_secret')
authorization_base_url = kwargs.get('authorization_base_url')
token_url = kwargs.get('token_url')
redirect_url = kwargs.get('redirect_url')
scope = kwargs.get('scope')
oauth2_session = OAuth2Session(client_id, scope=scope or None)
oauth2_session.authorization_url(authorization_base_url)
oauth2_session.fetch_token(token_url, client_secret=client_secret, authorization_response=redirect_url)
return oauth2_session
elif auth_type == "apikey":
return HTTPBasicAuth(kwargs.get('key'), kwargs.get('value'))
def webhook_request(webhook, payload):
"""
:param webhook:
{
"url": "https://veops.cn"
"method": "GET|POST|PUT|DELETE"
"body": {},
"headers": {
"Content-Type": "Application/json"
},
"parameters": {
"key": "value"
},
"authorization": {
"type": "BasicAuth|Bearer|OAuth2.0|APIKey",
"password": "mmmm", # BasicAuth
"username": "bbb", # BasicAuth
"token": "xxx", # Bearer
"key": "xxx", # APIKey
"value": "xxx", # APIKey
"client_id": "xxx", # OAuth2.0
"client_secret": "xxx", # OAuth2.0
"authorization_base_url": "xxx", # OAuth2.0
"token_url": "xxx", # OAuth2.0
"redirect_url": "xxx", # OAuth2.0
"scope": "xxx" # OAuth2.0
}
}
:param payload:
:return:
"""
assert webhook.get('url') is not None
payload = {k: v or '' for k, v in payload.items()}
url = Template(webhook['url']).render(payload)
params = webhook.get('parameters') or None
if isinstance(params, dict):
params = json.loads(Template(json.dumps(params)).render(payload))
headers = json.loads(Template(json.dumps(webhook.get('headers') or {})).render(payload))
data = Template(json.dumps(webhook.get('body', ''))).render(payload)
auth = _wrap_auth(**webhook.get('authorization', {}))
if (webhook.get('authorization', {}).get("type") or '').lower() == 'oauth2.0':
request = getattr(auth, webhook.get('method', 'GET').lower())
else:
request = partial(requests.request, webhook.get('method', 'GET'))
return request(
url,
params=params,
headers=headers or None,
data=data,
auth=auth
)

View File

@@ -125,16 +125,27 @@ class CITypeAttributeGroupItem(Model):
class CITypeTrigger(Model):
# __tablename__ = "c_ci_type_triggers"
__tablename__ = "c_c_t_t"
type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'), nullable=False)
attr_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id"), nullable=False)
notify = db.Column(db.JSON) # {subject: x, body: x, wx_to: [], mail_to: [], before_days: 0, notify_at: 08:00}
attr_id = db.Column(db.Integer, db.ForeignKey("c_attributes.id"))
option = db.Column('notify', db.JSON)
class CITriggerHistory(Model):
__tablename__ = "c_ci_trigger_histories"
operate_type = db.Column(db.Enum(*OperateType.all(), name="operate_type"))
record_id = db.Column(db.Integer, db.ForeignKey("c_records.id"))
ci_id = db.Column(db.Integer, index=True, nullable=False)
trigger_id = db.Column(db.Integer, db.ForeignKey("c_c_t_t.id"))
trigger_name = db.Column(db.String(64))
is_ok = db.Column(db.Boolean, default=False)
notify = db.Column(db.Text)
webhook = db.Column(db.Text)
class CITypeUniqueConstraint(Model):
# __tablename__ = "c_ci_type_unique_constraints"
__tablename__ = "c_c_t_u_c"
type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'), nullable=False)
@@ -363,7 +374,6 @@ class CITypeHistory(Model):
# preference
class PreferenceShowAttributes(Model):
# __tablename__ = "c_preference_show_attributes"
__tablename__ = "c_psa"
uid = db.Column(db.Integer, index=True, nullable=False)
@@ -377,7 +387,6 @@ class PreferenceShowAttributes(Model):
class PreferenceTreeView(Model):
# __tablename__ = "c_preference_tree_views"
__tablename__ = "c_ptv"
uid = db.Column(db.Integer, index=True, nullable=False)
@@ -386,7 +395,6 @@ class PreferenceTreeView(Model):
class PreferenceRelationView(Model):
# __tablename__ = "c_preference_relation_views"
__tablename__ = "c_prv"
uid = db.Column(db.Integer, index=True, nullable=False)

View File

@@ -47,6 +47,8 @@ class Employee(ModelWithoutPK):
last_login = db.Column(db.TIMESTAMP, nullable=True)
block = db.Column(db.Integer, default=0)
notice_info = db.Column(db.JSON, default={})
_department = db.relationship(
'Department', backref='common_employee.department_id',
lazy='joined'
@@ -87,3 +89,10 @@ class CommonData(Model):
data_type = db.Column(db.VARCHAR(255), default='')
data = db.Column(db.JSON)
class NoticeConfig(Model):
__tablename__ = "common_notice_config"
platform = db.Column(db.VARCHAR(255), nullable=False)
info = db.Column(db.JSON)

View File

@@ -4,8 +4,6 @@
import json
import time
import jinja2
import requests
from flask import current_app
from flask_login import login_user
@@ -18,7 +16,6 @@ from api.lib.cmdb.cache import CITypeAttributesCache
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
@@ -28,7 +25,9 @@ from api.models.cmdb import CITypeAttribute
@celery.task(name="cmdb.ci_cache", queue=CMDB_QUEUE)
def ci_cache(ci_id):
def ci_cache(ci_id, operate_type, record_id):
from api.lib.cmdb.ci import CITriggerManager
time.sleep(0.01)
db.session.remove()
@@ -42,9 +41,14 @@ def ci_cache(ci_id):
current_app.logger.info("{0} flush..........".format(ci_id))
current_app.test_request_context().push()
login_user(UserCache.get('worker'))
CITriggerManager.fire(operate_type, ci_dict, record_id)
@celery.task(name="cmdb.batch_ci_cache", queue=CMDB_QUEUE)
def batch_ci_cache(ci_ids):
def batch_ci_cache(ci_ids, ): # only for attribute change index
time.sleep(1)
db.session.remove()
@@ -72,6 +76,17 @@ def ci_delete(ci_id):
current_app.logger.info("{0} delete..........".format(ci_id))
@celery.task(name="cmdb.ci_delete_trigger", queue=CMDB_QUEUE)
def ci_delete_trigger(trigger, operate_type, ci_dict):
current_app.logger.info('delete ci {} trigger'.format(ci_dict['_id']))
from api.lib.cmdb.ci import CITriggerManager
current_app.test_request_context().push()
login_user(UserCache.get('worker'))
CITriggerManager.fire_by_trigger(trigger, operate_type, ci_dict)
@celery.task(name="cmdb.ci_relation_cache", queue=CMDB_QUEUE)
def ci_relation_cache(parent_id, child_id):
db.session.remove()
@@ -168,46 +183,6 @@ def ci_type_attribute_order_rebuild(type_id):
order += 1
@celery.task(name='cmdb.trigger_notify', queue=CMDB_QUEUE)
def trigger_notify(notify, ci_id):
from api.lib.perm.acl.cache import UserCache
def _wrap_mail(mail_to):
if "@" not in mail_to:
user = UserCache.get(mail_to)
if user:
return user.email
return mail_to
db.session.remove()
m = api.lib.cmdb.ci.CIManager()
ci_dict = m.get_ci_by_id_from_db(ci_id, need_children=False, use_master=False)
subject = jinja2.Template(notify.get('subject') or "").render(ci_dict)
body = jinja2.Template(notify.get('body') or "").render(ci_dict)
if notify.get('wx_to'):
to_user = jinja2.Template('|'.join(notify['wx_to'])).render(ci_dict)
url = current_app.config.get("WX_URI")
data = {"to_user": to_user, "content": subject}
try:
requests.post(url, data=data)
except Exception as e:
current_app.logger.error(str(e))
if notify.get('mail_to'):
try:
if len(subject) > 700:
subject = subject[:600] + "..." + subject[-100:]
send_mail("", [_wrap_mail(jinja2.Template(i).render(ci_dict))
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
@@ -217,7 +192,8 @@ def calc_computed_attribute(attr_id, uid):
current_app.test_request_context().push()
login_user(UserCache.get(uid))
cim = CIManager()
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, {})
cim.update(ci.id, {})

View File

@@ -185,8 +185,8 @@ class CIUnique(APIView):
@has_perm_from_args("ci_id", ResourceTypeEnum.CI, PermEnum.UPDATE, CIManager.get_type_name)
def put(self, ci_id):
params = request.values
unique_name = params.keys()[0]
unique_value = params.values()[0]
unique_name = list(params.keys())[0]
unique_value = list(params.values())[0]
CIManager.update_unique_value(ci_id, unique_name, unique_value)

View File

@@ -419,22 +419,22 @@ class CITypeTriggerView(APIView):
return self.jsonify(CITypeTriggerManager.get(type_id))
@has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id)
@args_required("attr_id")
@args_required("notify")
@args_required("option")
def post(self, type_id):
attr_id = request.values.get('attr_id')
notify = request.values.get('notify')
attr_id = request.values.get('attr_id') or None
option = request.values.get('option')
return self.jsonify(CITypeTriggerManager().add(type_id, attr_id, notify))
return self.jsonify(CITypeTriggerManager().add(type_id, attr_id, option))
@has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id)
@args_required("notify")
@args_required("option")
def put(self, type_id, _id):
assert type_id is not None
notify = request.values.get('notify')
option = request.values.get('option')
attr_id = request.values.get('attr_id')
return self.jsonify(CITypeTriggerManager().update(_id, notify))
return self.jsonify(CITypeTriggerManager().update(_id, attr_id, option))
@has_perm_from_args("type_id", ResourceTypeEnum.CI, PermEnum.CONFIG, CITypeManager.get_name_by_id)
def delete(self, type_id, _id):

View File

@@ -5,15 +5,18 @@ import datetime
from flask import abort
from flask import request
from flask import session
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 RoleEnum
from api.lib.cmdb.history import AttributeHistoryManger
from api.lib.cmdb.history import CITriggerHistoryManager
from api.lib.cmdb.history import CITypeHistoryManager
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.perm.acl.acl import has_perm_from_args
from api.lib.perm.acl.acl import is_app_admin
from api.lib.perm.acl.acl import role_required
from api.lib.utils import get_page
from api.lib.utils import get_page_size
@@ -76,6 +79,39 @@ class CIHistoryView(APIView):
return self.jsonify(result)
class CITriggerHistoryView(APIView):
url_prefix = ("/history/ci_triggers/<int:ci_id>", "/history/ci_triggers")
@has_perm_from_args("ci_id", ResourceTypeEnum.CI, PermEnum.READ, CIManager.get_type_name)
def get(self, ci_id=None):
if ci_id is not None:
result = CITriggerHistoryManager.get_by_ci_id(ci_id)
return self.jsonify(result)
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))
type_id = request.values.get("type_id")
trigger_id = request.values.get("trigger_id")
operate_type = request.values.get("operate_type")
page = get_page(request.values.get('page', 1))
page_size = get_page_size(request.values.get('page_size', 1))
numfound, result = CITriggerHistoryManager.get(page,
page_size,
type_id=type_id,
trigger_id=trigger_id,
operate_type=operate_type)
return self.jsonify(page=page,
page_size=page_size,
numfound=numfound,
total=len(result),
result=result)
class CITypeHistoryView(APIView):
url_prefix = "/history/ci_types"

View File

@@ -145,3 +145,14 @@ class EmployeePositionView(APIView):
result = EmployeeCRUD.get_all_position()
return self.jsonify(result)
class GetEmployeeNoticeByIds(APIView):
url_prefix = (f'{prefix}/get_notice_by_ids',)
def post(self):
employee_ids = request.json.get('employee_ids', [])
if not employee_ids:
result = []
else:
result = EmployeeCRUD.get_employee_notice_by_ids(employee_ids)
return self.jsonify(result)

View File

@@ -11,7 +11,7 @@ from api.resource import APIView
prefix = '/file'
ALLOWED_EXTENSIONS = {
'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'xls', 'xlsx', 'doc', 'docx', 'ppt', 'pptx', 'csv'
'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'xls', 'xlsx', 'doc', 'docx', 'ppt', 'pptx', 'csv', 'svg'
}

View File

@@ -0,0 +1,71 @@
from flask import request, abort, current_app
from werkzeug.datastructures import MultiDict
from api.lib.perm.auth import auth_with_app_token
from api.models.common_setting import NoticeConfig
from api.resource import APIView
from api.lib.common_setting.notice_config import NoticeConfigForm, NoticeConfigUpdateForm, NoticeConfigCRUD
from api.lib.decorator import args_required
from api.lib.common_setting.resp_format import ErrFormat
prefix = '/notice_config'
class NoticeConfigView(APIView):
url_prefix = (f'{prefix}',)
@args_required('platform')
@auth_with_app_token
def get(self):
platform = request.args.get('platform')
res = NoticeConfig.get_by(first=True, to_dict=True, platform=platform) or {}
return self.jsonify(res)
def post(self):
form = NoticeConfigForm(MultiDict(request.json))
if not form.validate():
abort(400, ','.join(['{}: {}'.format(filed, ','.join(msg)) for filed, msg in form.errors.items()]))
data = NoticeConfigCRUD.add_notice_config(**form.data)
return self.jsonify(data.to_dict())
class NoticeConfigUpdateView(APIView):
url_prefix = (f'{prefix}/<int:_id>',)
def put(self, _id):
form = NoticeConfigUpdateForm(MultiDict(request.json))
if not form.validate():
abort(400, ','.join(['{}: {}'.format(filed, ','.join(msg)) for filed, msg in form.errors.items()]))
data = NoticeConfigCRUD.edit_notice_config(_id, **form.data)
return self.jsonify(data.to_dict())
class CheckEmailServer(APIView):
url_prefix = (f'{prefix}/send_test_email',)
def post(self):
receive_address = request.args.get('receive_address')
info = request.values.get('info')
try:
result = NoticeConfigCRUD.test_send_email(receive_address, **info)
return self.jsonify(result=result)
except Exception as e:
current_app.logger.error('test_send_email err:')
current_app.logger.error(e)
if 'Timed Out' in str(e):
abort(400, ErrFormat.email_send_timeout)
abort(400, f"{str(e)}")
class NoticeConfigGetView(APIView):
method_decorators = []
url_prefix = (f'{prefix}/all',)
@auth_with_app_token
def get(self):
res = NoticeConfigCRUD.get_all()
return self.jsonify(res)

View File

@@ -36,11 +36,13 @@ python-ldap==3.4.0
PyYAML==6.0
redis==4.6.0
requests==2.31.0
six==1.12.0
requests_oauthlib==1.3.1
markdownify==0.11.6
six==1.16.0
SQLAlchemy==1.4.49
supervisor==4.0.3
timeout-decorator==0.5.0
toposort==1.10
treelib==1.6.1
Werkzeug==2.3.6
WTForms==3.0.0
WTForms==3.0.0

View File

@@ -94,3 +94,7 @@ ES_HOST = '127.0.0.1'
USE_ES = False
BOOL_TRUE = ['true', 'TRUE', 'True', True, '1', 1, "Yes", "YES", "yes", 'Y', 'y']
# # messenger
USE_MESSENGER = True
MESSENGER_URL = "http://{messenger_url}/v1/message"

View File

@@ -17,6 +17,8 @@
"@babel/plugin-syntax-import-meta": "^7.10.4",
"@riophae/vue-treeselect": "^0.4.0",
"@vue/composition-api": "^1.7.1",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^1.0.0",
"ant-design-vue": "^1.6.5",
"axios": "0.18.0",
"babel-eslint": "^8.2.2",
@@ -37,6 +39,7 @@
"moment": "^2.24.0",
"nprogress": "^0.2.0",
"relation-graph": "^1.1.0",
"snabbdom": "^3.5.1",
"sortablejs": "1.9.0",
"viser-vue": "^2.4.8",
"vue": "2.6.11",

View File

@@ -12,6 +12,9 @@ import zhCN from 'ant-design-vue/lib/locale-provider/zh_CN'
import { AppDeviceEnquire } from '@/utils/mixin'
import { debounce } from './utils/util'
import { h } from 'snabbdom'
import { DomEditor, Boot } from '@wangeditor/editor'
export default {
mixins: [AppDeviceEnquire],
provide() {
@@ -47,6 +50,134 @@ export default {
this.$store.dispatch('setWindowSize')
})
)
// 注册富文本自定义元素
const resume = {
type: 'attachment',
attachmentLabel: '',
attachmentValue: '',
children: [{ text: '' }], // void 元素必须有一个 children 其中只有一个空字符串重要
}
function withAttachment(editor) {
// JS 语法
const { isInline, isVoid } = editor
const newEditor = editor
newEditor.isInline = (elem) => {
const type = DomEditor.getNodeType(elem)
if (type === 'attachment') return true // 针对 type: attachment 设置为 inline
return isInline(elem)
}
newEditor.isVoid = (elem) => {
const type = DomEditor.getNodeType(elem)
if (type === 'attachment') return true // 针对 type: attachment 设置为 void
return isVoid(elem)
}
return newEditor // 返回 newEditor 重要
}
Boot.registerPlugin(withAttachment)
/**
* 渲染附件元素到编辑器
* @param elem 附件元素即上文的 myResume
* @param children 元素子节点void 元素可忽略
* @param editor 编辑器实例
* @returns vnode 节点通过 snabbdom.js h 函数生成
*/
function renderAttachment(elem, children, editor) {
// JS 语法
// 获取附件的数据参考上文 myResume 数据结构
const { attachmentLabel = '', attachmentValue = '' } = elem
// 附件元素 vnode
const attachVnode = h(
// HTML tag
'span',
// HTML 属性样式事件
{
props: { contentEditable: false }, // HTML 属性驼峰式写法
style: {
display: 'inline-block',
margin: '0 3px',
padding: '0 3px',
backgroundColor: '#e6f7ff',
border: '1px solid #91d5ff',
borderRadius: '2px',
color: '#1890ff',
}, // style 驼峰式写法
on: {
click() {
console.log('clicked', attachmentValue)
} /* 其他... */,
},
},
// 子节点
[attachmentLabel]
)
return attachVnode
}
const renderElemConf = {
type: 'attachment', // 新元素 type 重要
renderElem: renderAttachment,
}
Boot.registerRenderElem(renderElemConf)
/**
* 生成附件元素的 HTML
* @param elem 附件元素即上文的 myResume
* @param childrenHtml 子节点的 HTML 代码void 元素可忽略
* @returns 附件元素的 HTML 字符串
*/
function attachmentToHtml(elem, childrenHtml) {
// JS 语法
// 获取附件元素的数据
const { attachmentValue = '', attachmentLabel = '' } = elem
// 生成 HTML 代码
const html = `<span data-w-e-type="attachment" data-w-e-is-void data-w-e-is-inline data-attachmentValue="${attachmentValue}" data-attachmentLabel="${attachmentLabel}">${attachmentLabel}</span>`
return html
}
const elemToHtmlConf = {
type: 'attachment', // 新元素的 type 重要
elemToHtml: attachmentToHtml,
}
Boot.registerElemToHtml(elemToHtmlConf)
/**
* 解析 HTML 字符串生成附件元素
* @param domElem HTML 对应的 DOM Element
* @param children 子节点
* @param editor editor 实例
* @returns 附件元素如上文的 myResume
*/
function parseAttachmentHtml(domElem, children, editor) {
// JS 语法
// DOM element 中获取附件的信息
const attachmentValue = domElem.getAttribute('data-attachmentValue') || ''
const attachmentLabel = domElem.getAttribute('data-attachmentLabel') || ''
// 生成附件元素按照此前约定的数据结构
const myResume = {
type: 'attachment',
attachmentValue,
attachmentLabel,
children: [{ text: '' }], // void node 必须有 children 其中有一个空字符串重要
}
return myResume
}
const parseHtmlConf = {
selector: 'span[data-w-e-type="attachment"]', // CSS 选择器匹配特定的 HTML 标签
parseElemHtml: parseAttachmentHtml,
}
Boot.registerParseElemHtml(parseHtmlConf)
},
beforeDestroy() {
clearInterval(this.timer)

View File

@@ -117,3 +117,11 @@ export function getEmployeeListByFilter(data) {
data
})
}
export function getNoticeByEmployeeIds(data) {
return axios({
url: '/common-setting/v1/employee/get_notice_by_ids',
method: 'post',
data
})
}

View File

@@ -1,207 +1,215 @@
import { axios } from '@/utils/request'
/**
* 获取 所有的 ci_types
* @param parameter
* @returns {AxiosPromise}
*/
export function getCITypes(parameter) {
return axios({
url: '/v0.1/ci_types',
method: 'GET',
params: parameter
})
}
/**
* 获取 某个 ci_types
* @param CITypeName
* @param parameter
* @returns {AxiosPromise}
*/
export function getCIType(CITypeName, parameter) {
return axios({
url: `/v0.1/ci_types/${CITypeName}`,
method: 'GET',
params: parameter
})
}
/**
* 创建 ci_type
* @param data
* @returns {AxiosPromise}
*/
export function createCIType(data) {
return axios({
url: '/v0.1/ci_types',
method: 'POST',
data: data
})
}
/**
* 更新 ci_type
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function updateCIType(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}`,
method: 'PUT',
data: data
})
}
/**
* 删除 ci_type
* @param CITypeId
* @returns {AxiosPromise}
*/
export function deleteCIType(CITypeId) {
return axios({
url: `/v0.1/ci_types/${CITypeId}`,
method: 'DELETE'
})
}
/**
* 获取 某个 ci_type 的分组
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function getCITypeGroupById(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attribute_groups`,
method: 'GET',
params: data
})
}
/**
* 保存 某个 ci_type 的分组
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function createCITypeGroupById(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attribute_groups`,
method: 'POST',
data: data
})
}
/**
* 修改 某个 ci_type 的分组
* @param groupId
* @param data
* @returns {AxiosPromise}
*/
export function updateCITypeGroupById(groupId, data) {
return axios({
url: `/v0.1/ci_types/attribute_groups/${groupId}`,
method: 'PUT',
data: data
})
}
/**
* 删除 某个 ci_type 的分组
* @param groupId
* @param data
* @returns {AxiosPromise}
*/
export function deleteCITypeGroupById(groupId, data) {
return axios({
url: `/v0.1/ci_types/attribute_groups/${groupId}`,
method: 'delete',
data: data
})
}
export function getUniqueConstraintList(type_id) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint`,
method: 'get',
})
}
export function addUniqueConstraint(type_id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint`,
method: 'post',
data: data
})
}
export function updateUniqueConstraint(type_id, id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint/${id}`,
method: 'put',
data: data
})
}
export function deleteUniqueConstraint(type_id, id) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint/${id}`,
method: 'delete',
})
}
export function getTriggerList(type_id) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers`,
method: 'get',
})
}
export function addTrigger(type_id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers`,
method: 'post',
data: data
})
}
export function updateTrigger(type_id, id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers/${id}`,
method: 'put',
data: data
})
}
export function deleteTrigger(type_id, id) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers/${id}`,
method: 'delete',
})
}
// CMDB的模型和实例的授权接口
export function grantCiType(type_id, rid, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/roles/${rid}/grant`,
method: 'post',
data
})
}
// CMDB的模型和实例的删除授权接口
export function revokeCiType(type_id, rid, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/roles/${rid}/revoke`,
method: 'post',
data
})
}
// CMDB的模型和实例的过滤的权限
export function ciTypeFilterPermissions(type_id) {
return axios({
url: `/v0.1/ci_types/${type_id}/filters/permissions`,
method: 'get',
})
}
import { axios } from '@/utils/request'
/**
* 获取 所有的 ci_types
* @param parameter
* @returns {AxiosPromise}
*/
export function getCITypes(parameter) {
return axios({
url: '/v0.1/ci_types',
method: 'GET',
params: parameter
})
}
/**
* 获取 某个 ci_types
* @param CITypeName
* @param parameter
* @returns {AxiosPromise}
*/
export function getCIType(CITypeName, parameter) {
return axios({
url: `/v0.1/ci_types/${CITypeName}`,
method: 'GET',
params: parameter
})
}
/**
* 创建 ci_type
* @param data
* @returns {AxiosPromise}
*/
export function createCIType(data) {
return axios({
url: '/v0.1/ci_types',
method: 'POST',
data: data
})
}
/**
* 更新 ci_type
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function updateCIType(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}`,
method: 'PUT',
data: data
})
}
/**
* 删除 ci_type
* @param CITypeId
* @returns {AxiosPromise}
*/
export function deleteCIType(CITypeId) {
return axios({
url: `/v0.1/ci_types/${CITypeId}`,
method: 'DELETE'
})
}
/**
* 获取 某个 ci_type 的分组
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function getCITypeGroupById(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attribute_groups`,
method: 'GET',
params: data
})
}
/**
* 保存 某个 ci_type 的分组
* @param CITypeId
* @param data
* @returns {AxiosPromise}
*/
export function createCITypeGroupById(CITypeId, data) {
return axios({
url: `/v0.1/ci_types/${CITypeId}/attribute_groups`,
method: 'POST',
data: data
})
}
/**
* 修改 某个 ci_type 的分组
* @param groupId
* @param data
* @returns {AxiosPromise}
*/
export function updateCITypeGroupById(groupId, data) {
return axios({
url: `/v0.1/ci_types/attribute_groups/${groupId}`,
method: 'PUT',
data: data
})
}
/**
* 删除 某个 ci_type 的分组
* @param groupId
* @param data
* @returns {AxiosPromise}
*/
export function deleteCITypeGroupById(groupId, data) {
return axios({
url: `/v0.1/ci_types/attribute_groups/${groupId}`,
method: 'delete',
data: data
})
}
export function getUniqueConstraintList(type_id) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint`,
method: 'get',
})
}
export function addUniqueConstraint(type_id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint`,
method: 'post',
data: data
})
}
export function updateUniqueConstraint(type_id, id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint/${id}`,
method: 'put',
data: data
})
}
export function deleteUniqueConstraint(type_id, id) {
return axios({
url: `/v0.1/ci_types/${type_id}/unique_constraint/${id}`,
method: 'delete',
})
}
export function getTriggerList(type_id) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers`,
method: 'get',
})
}
export function addTrigger(type_id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers`,
method: 'post',
data: data
})
}
export function updateTrigger(type_id, id, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers/${id}`,
method: 'put',
data: data
})
}
export function deleteTrigger(type_id, id) {
return axios({
url: `/v0.1/ci_types/${type_id}/triggers/${id}`,
method: 'delete',
})
}
// CMDB的模型和实例的授权接口
export function grantCiType(type_id, rid, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/roles/${rid}/grant`,
method: 'post',
data
})
}
// CMDB的模型和实例的删除授权接口
export function revokeCiType(type_id, rid, data) {
return axios({
url: `/v0.1/ci_types/${type_id}/roles/${rid}/revoke`,
method: 'post',
data
})
}
// CMDB的模型和实例的过滤的权限
export function ciTypeFilterPermissions(type_id) {
return axios({
url: `/v0.1/ci_types/${type_id}/filters/permissions`,
method: 'get',
})
}
export function getAllDagsName(params) {
return axios({
url: '/v1/dag/all_names',
method: 'GET',
params: params
})
}

View File

@@ -1,40 +1,56 @@
import { axios } from '@/utils/request'
export function getCIHistory (ciId) {
return axios({
url: `/v0.1/history/ci/${ciId}`,
method: 'GET'
})
}
export function getCIHistoryTable (params) {
return axios({
url: `/v0.1/history/records/attribute`,
method: 'GET',
params: params
})
}
export function getRelationTable (params) {
return axios({
url: `/v0.1/history/records/relation`,
method: 'GET',
params: params
})
}
export function getCITypesTable (params) {
return axios({
url: `/v0.1/history/ci_types`,
method: 'GET',
params: params
})
}
export function getUsers (params) {
return axios({
url: `/v1/acl/users/employee`,
method: 'GET',
params: params
})
}
import { axios } from '@/utils/request'
export function getCIHistory(ciId) {
return axios({
url: `/v0.1/history/ci/${ciId}`,
method: 'GET'
})
}
export function getCIHistoryTable(params) {
return axios({
url: `/v0.1/history/records/attribute`,
method: 'GET',
params: params
})
}
export function getRelationTable(params) {
return axios({
url: `/v0.1/history/records/relation`,
method: 'GET',
params: params
})
}
export function getCITypesTable(params) {
return axios({
url: `/v0.1/history/ci_types`,
method: 'GET',
params: params
})
}
export function getUsers(params) {
return axios({
url: `/v1/acl/users/employee`,
method: 'GET',
params: params
})
}
export function getCiTriggers(params) {
return axios({
url: `/v0.1/history/ci_triggers`,
method: 'GET',
params: params
})
}
export function getCiTriggersByCiId(ci_id, params) {
return axios({
url: `/v0.1/history/ci_triggers/${ci_id}`,
method: 'GET',
params
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

View File

@@ -0,0 +1,2 @@
import NoticeContent from './index.vue'
export default NoticeContent

View File

@@ -0,0 +1,199 @@
<template>
<div class="notice-content">
<div class="notice-content-main">
<Toolbar
:editor="editor"
:defaultConfig="{
excludeKeys: [
'emotion',
'group-image',
'group-video',
'insertTable',
'codeBlock',
'blockquote',
'fullScreen',
],
}"
mode="default"
/>
<Editor class="notice-content-editor" :defaultConfig="editorConfig" mode="simple" @onCreated="onCreated" />
<div class="notice-content-sidebar">
<template v-if="needOld">
<div class="notice-content-sidebar-divider">变更前</div>
<div
@dblclick="dblclickSidebar(`old_${attr.name}`, attr.alias || attr.name)"
class="notice-content-sidebar-item"
v-for="attr in attrList"
:key="`old_${attr.id}`"
:title="attr.alias || attr.name"
>
{{ attr.alias || attr.name }}
</div>
<div class="notice-content-sidebar-divider">变更后</div>
</template>
<div
@dblclick="dblclickSidebar(attr.name, attr.alias || attr.name)"
class="notice-content-sidebar-item"
v-for="attr in attrList"
:key="attr.id"
:title="attr.alias || attr.name"
>
{{ attr.alias || attr.name }}
</div>
</div>
</div>
</div>
</template>
<script>
import _ from 'lodash'
import '@wangeditor/editor/dist/css/style.css'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
export default {
name: 'NoticeContent',
components: { Editor, Toolbar },
props: {
attrList: {
type: Array,
default: () => [],
},
needOld: {
type: Boolean,
default: false,
},
},
data() {
return {
editor: null,
editorConfig: { placeholder: '请输入通知内容', readOnly: this.readOnly },
content: '',
defaultParams: [],
value2LabelMap: {},
}
},
beforeDestroy() {
const editor = this.editor
if (editor == null) return
editor.destroy() // 组件销毁时及时销毁编辑器
},
methods: {
onCreated(editor) {
this.editor = Object.seal(editor) // 一定要用 Object.seal() 否则会报错
},
getContent() {
const html = _.cloneDeep(this.editor.getHtml())
const _html = html.replace(
/<span data-w-e-type="attachment" data-w-e-is-void data-w-e-is-inline.*?<\/span>/gm,
(value) => {
const _match = value.match(/(?<=data-attachmentValue=").*?(?=")/)
return `{{${_match}}}`
}
)
return { body_html: html, body: _html }
},
setContent(html) {
this.editor.setHtml(html)
},
dblclickSidebar(value, label) {
if (!this.readOnly) {
this.editor.restoreSelection()
const node = {
type: 'attachment',
attachmentValue: value,
attachmentLabel: `${label}`,
children: [{ text: '' }],
}
this.editor.insertNode(node)
}
},
},
}
</script>
<style lang="less" scoped>
@import '~@/style/static.less';
.notice-content {
width: 100%;
& &-main {
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-end;
position: relative;
.notice-content-editor {
height: 300px;
width: 75%;
border: 1px solid #e4e7ed;
border-top: none;
overflow: hidden;
}
.notice-content-sidebar {
width: 25%;
position: absolute;
height: 300px;
bottom: 0;
left: 0;
border: 1px solid #e4e7ed;
border-top: none;
border-right: none;
overflow: auto;
.notice-content-sidebar-divider {
position: sticky;
top: 0;
margin: 0;
font-size: 12px;
color: #afafaf;
background-color: #fff;
line-height: 20px;
padding-left: 12px;
&::before,
&::after {
content: '';
position: absolute;
border-top: 1px solid #d1d1d1;
top: 50%;
transition: translateY(-50%);
}
&::before {
left: 3px;
width: 5px;
}
&::after {
right: 3px;
width: 78px;
}
}
.notice-content-sidebar-item:first-child {
margin-top: 10px;
}
.notice-content-sidebar-item {
line-height: 1.5;
padding: 4px 12px;
cursor: pointer;
user-select: none;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&:hover {
background-color: #custom_colors[color_2];
color: #custom_colors[color_1];
}
}
}
}
}
</style>
<style lang="less">
@import '~@/style/static.less';
.notice-content {
.w-e-bar {
background-color: #custom_colors[color_2];
}
.w-e-text-placeholder {
line-height: 1.5;
}
}
</style>

View File

@@ -0,0 +1,144 @@
<template>
<div class="authorization-wrapper">
<div class="authorization-header">
<a-space>
<span>Authorization Type</span>
<a-select size="small" v-model="authorizationType" style="width: 200px" :showSearch="true">
<a-select-option value="none">
None
</a-select-option>
<a-select-option value="BasicAuth">
Basic Auth
</a-select-option>
<a-select-option value="Bearer">
Bearer
</a-select-option>
<a-select-option value="APIKey">
APIKey
</a-select-option>
<a-select-option value="OAuth2.0">
OAuth2.0
</a-select-option>
</a-select>
</a-space>
</div>
<div style="margin-top:10px">
<table v-if="authorizationType === 'BasicAuth'">
<tr>
<td><a-input class="authorization-input" v-model="BasicAuth.username" placeholder="用户名" /></td>
</tr>
<tr>
<td><a-input class="authorization-input" v-model="BasicAuth.password" placeholder="密码" /></td>
</tr>
</table>
<table v-else-if="authorizationType === 'Bearer'">
<tr>
<td><a-input class="authorization-input" v-model="Bearer.token" placeholder="token" /></td>
</tr>
</table>
<table v-else-if="authorizationType === 'APIKey'">
<tr>
<td><a-input class="authorization-input" v-model="APIKey.key" placeholder="key" /></td>
</tr>
<tr>
<td><a-input class="authorization-input" v-model="APIKey.value" placeholder="value" /></td>
</tr>
</table>
<table v-else-if="authorizationType === 'OAuth2.0'">
<tr>
<td><a-input class="authorization-input" v-model="OAuth2.client_id" placeholder="client_id" /></td>
</tr>
<tr>
<td>
<a-input class="authorization-input" v-model="OAuth2.client_secret" placeholder="client_secret" />
</td>
</tr>
<tr>
<td>
<a-input
class="authorization-input"
v-model="OAuth2.authorization_base_url"
placeholder="authorization_base_url"
/>
</td>
</tr>
<tr>
<td>
<a-input class="authorization-input" v-model="OAuth2.token_url" placeholder="token_url" />
</td>
</tr>
<tr>
<td><a-input class="authorization-input" v-model="OAuth2.redirect_url" placeholder="redirect_url" /></td>
</tr>
<tr>
<td>
<a-input class="authorization-input" v-model="OAuth2.scope" placeholder="scope" />
</td>
</tr>
</table>
<a-empty
v-else
:image-style="{
height: '60px',
}"
>
<img slot="image" :src="require('@/assets/data_empty.png')" />
<span slot="description"> 暂无请求认证 </span>
</a-empty>
</div>
</div>
</template>
<script>
export default {
name: 'Authorization',
data() {
return {
authorizationType: 'none',
BasicAuth: {
username: '',
password: '',
},
Bearer: {
token: '',
},
APIKey: {
key: '',
value: '',
},
OAuth2: {
client_id: '',
client_secret: '',
authorization_base_url: '',
token_url: '',
redirect_url: '',
scope: '',
},
}
},
}
</script>
<style lang="less" scoped>
.authorization-wrapper {
table {
width: 100%;
border-collapse: collapse;
}
table,
td,
th {
border: 1px solid #f3f4f6;
}
.authorization-input {
border: none;
&:focus {
box-shadow: none;
}
}
}
</style>

View File

@@ -0,0 +1,79 @@
<template>
<div class="body-wrapper">
<div class="body-header">
<!-- <a-space>
<span>Content Type</span>
<a-select size="small" v-model="contentType" style="width: 200px" :showSearch="true">
<a-select-option value="none">
None
</a-select-option>
<a-select-opt-group v-for="item in segmentedContentTypes" :key="item.title" :label="item.title">
<a-select-option v-for="ele in item.contentTypes" :key="ele" :value="ele">
{{ ele }}
</a-select-option>
</a-select-opt-group>
</a-select>
</a-space> -->
</div>
<div style="margin-top:10px">
<vue-json-editor v-model="jsonData" :showBtns="false" :mode="'text'" />
<!-- <a-empty
v-else
:image-style="{
height: '60px',
}"
>
<img slot="image" :src="require('@/assets/data_empty.png')" />
<span slot="description"> 暂无请求体 </span>
</a-empty> -->
</div>
</div>
</template>
<script>
import vueJsonEditor from 'vue-json-editor'
export default {
name: 'Body',
components: { vueJsonEditor },
data() {
const segmentedContentTypes = [
{
title: 'text',
contentTypes: [
'application/json',
'application/ld+json',
'application/hal+json',
'application/vnd.api+json',
'application/xml',
],
},
{
title: 'structured',
contentTypes: ['application/x-www-form-urlencoded', 'multipart/form-data'],
},
{
title: 'others',
contentTypes: ['text/html', 'text/plain'],
},
]
return {
segmentedContentTypes,
// contentType: 'none',
jsonData: {},
}
},
}
</script>
<style lang="less" scoped></style>
<style lang="less">
.body-wrapper {
div.jsoneditor-menu {
display: none;
}
div.jsoneditor {
border-color: #f3f4f6;
}
}
</style>

View File

@@ -0,0 +1,101 @@
<template>
<div>
<div class="headers-header">
<span>请求参数</span>
<a-space>
<a-tooltip title="清空">
<ops-icon
type="icon-xianxing-delete"
@click="
() => {
headers = [
{
id: uuidv4(),
key: '',
value: '',
},
]
}
"
/>
</a-tooltip>
<a-tooltip title="新增">
<a-icon type="plus" @click="add" />
</a-tooltip>
</a-space>
</div>
<div class="headers-box">
<table>
<tr v-for="(item, index) in headers" :key="item.id">
<td><a-input class="headers-input" v-model="item.key" :placeholder="`参数${index + 1}`" /></td>
<td><a-input class="headers-input" v-model="item.value" :placeholder="`值${index + 1}`" /></td>
<td>
<a style="color:red">
<ops-icon type="icon-xianxing-delete" @click="deleteParam(index)" />
</a>
</td>
</tr>
</table>
</div>
</div>
</template>
<script>
import { v4 as uuidv4 } from 'uuid'
export default {
name: 'Header',
data() {
return {
headers: [
{
id: uuidv4(),
key: '',
value: '',
},
],
}
},
methods: {
uuidv4,
add() {
this.headers.push({
id: uuidv4(),
key: '',
value: '',
})
},
deleteParam(index) {
this.headers.splice(index, 1)
},
},
}
</script>
<style lang="less" scoped>
.headers-header {
display: flex;
justify-content: space-between;
align-items: center;
i {
cursor: pointer;
}
}
.headers-box {
table {
width: 100%;
border-collapse: collapse;
}
table,
td,
th {
border: 1px solid #f3f4f6;
}
.headers-input {
border: none;
&:focus {
box-shadow: none;
}
}
}
</style>

View File

@@ -0,0 +1,2 @@
import Webhook from './index.vue'
export default Webhook

View File

@@ -0,0 +1,140 @@
<template>
<div>
<a-input-group compact>
<treeselect
:disable-branch-nodes="true"
class="custom-treeselect custom-treeselect-bgcAndBorder"
:style="{
'--custom-height': '30px',
lineHeight: '30px',
'--custom-bg-color': '#fff',
'--custom-border': '1px solid #d9d9d9',
display: 'inline-block',
width: '100px',
}"
v-model="method"
:multiple="false"
:clearable="false"
searchable
:options="methodList"
value-consists-of="LEAF_PRIORITY"
placeholder="请选择方式"
>
</treeselect>
<a-input :style="{ display: 'inline-block', width: 'calc(100% - 100px)' }" v-model="url" />
</a-input-group>
<a-tabs>
<a-tab-pane key="Parameters" tab="Parameters">
<Parameters ref="Parameters" />
</a-tab-pane>
<a-tab-pane key="Body" tab="Body" force-render>
<Body ref="Body" />
</a-tab-pane>
<a-tab-pane key="Headers" tab="Headers" force-render>
<Header ref="Header" />
</a-tab-pane>
<a-tab-pane key="Authorization" tab="Authorization" force-render>
<Authorization ref="Authorization" />
</a-tab-pane>
</a-tabs>
</div>
</template>
<script>
import _ from 'lodash'
import { v4 as uuidv4 } from 'uuid'
import Parameters from './paramaters.vue'
import Body from './body.vue'
import Header from './header.vue'
import Authorization from './authorization.vue'
export default {
name: 'Webhook',
components: { Parameters, Body, Header, Authorization },
data() {
const methodList = [
{
id: 'GET',
label: 'GET',
},
{
id: 'POST',
label: 'POST',
},
{
id: 'PUT',
label: 'PUT',
},
{
id: 'DELETE',
label: 'DELETE',
},
]
return {
methodList,
method: 'GET',
url: '',
}
},
methods: {
getParams() {
const parameters = {}
this.$refs.Parameters.parameters.forEach((item) => {
parameters[item.key] = item.value
})
const body = this.$refs.Body.jsonData
const headers = {}
this.$refs.Header.headers.forEach((item) => {
headers[item.key] = item.value
})
let authorization = {}
const type = this.$refs.Authorization.authorizationType
if (type !== 'none') {
if (type === 'OAuth2.0') {
authorization = { ...this.$refs.Authorization['OAuth2'], type }
} else {
authorization = { ...this.$refs.Authorization[type], type }
}
}
const { method, url } = this
return { method, url, parameters, body, headers, authorization }
},
setParams(params) {
console.log(2222, params)
const { method, url, parameters, body, headers, authorization = {} } = params ?? {}
this.method = method
this.url = url
this.$refs.Parameters.parameters =
Object.keys(parameters).map((key) => {
return {
id: uuidv4(),
key: key,
value: parameters[key],
}
}) || []
this.$refs.Body.jsonData = body
this.$refs.Header.headers =
Object.keys(headers).map((key) => {
return {
id: uuidv4(),
key: key,
value: headers[key],
}
}) || []
const { type = 'none' } = authorization
console.log(type)
this.$refs.Authorization.authorizationType = type
if (type !== 'none') {
const _authorization = _.cloneDeep(authorization)
delete _authorization.type
if (type === 'OAuth2.0') {
this.$refs.Authorization.OAuth2 = _authorization
} else {
this.$refs.Authorization[type] = _authorization
}
}
},
},
}
</script>
<style></style>

View File

@@ -0,0 +1,100 @@
<template>
<div>
<div class="parameters-header">
<span>请求参数</span>
<a-space>
<a-tooltip title="清空">
<ops-icon
type="icon-xianxing-delete"
@click="
() => {
parameters = []
}
"
/>
</a-tooltip>
<a-tooltip title="新增">
<a-icon type="plus" @click="add" />
</a-tooltip>
</a-space>
</div>
<div class="parameters-box" v-if="parameters && parameters.length">
<table>
<tr v-for="(item, index) in parameters" :key="item.id">
<td><a-input class="parameters-input" v-model="item.key" :placeholder="`参数${index + 1}`" /></td>
<td><a-input class="parameters-input" v-model="item.value" :placeholder="`值${index + 1}`" /></td>
<td>
<a style="color:red">
<ops-icon type="icon-xianxing-delete" @click="deleteParam(index)" />
</a>
</td>
</tr>
</table>
</div>
<a-empty
v-else
:image-style="{
height: '60px',
}"
>
<img slot="image" :src="require('@/assets/data_empty.png')" />
<span slot="description"> 暂无请求参数 </span>
<a-button @click="add" type="primary" size="small" icon="plus" class="ops-button-primary">
添加
</a-button>
</a-empty>
</div>
</template>
<script>
import { v4 as uuidv4 } from 'uuid'
export default {
name: 'Parameters',
data() {
return {
parameters: [],
}
},
methods: {
add() {
this.parameters.push({
id: uuidv4(),
key: '',
value: '',
})
},
deleteParam(index) {
this.parameters.splice(index, 1)
},
},
}
</script>
<style lang="less" scoped>
.parameters-header {
display: flex;
justify-content: space-between;
align-items: center;
i {
cursor: pointer;
}
}
.parameters-box {
table {
width: 100%;
border-collapse: collapse;
}
table,
td,
th {
border: 1px solid #f3f4f6;
}
.parameters-input {
border: none;
&:focus {
box-shadow: none;
}
}
}
</style>

View File

@@ -1,319 +1,327 @@
<template>
<CustomDrawer
width="80%"
placement="left"
@close="
() => {
visible = false
}
"
:visible="visible"
:hasTitle="false"
:hasFooter="false"
:bodyStyle="{ padding: 0, height: '100vh' }"
wrapClassName="ci-detail"
destroyOnClose
>
<a-tabs v-model="activeTabKey" @change="changeTab">
<a-tab-pane key="tab_1">
<span slot="tab"><a-icon type="book" />属性</span>
<div :style="{ maxHeight: `${windowHeight - 44}px`, overflow: 'auto', padding: '24px' }" class="ci-detail-attr">
<el-descriptions
:title="group.name || '其他'"
:key="group.name"
v-for="group in attributeGroups"
border
:column="3"
>
<el-descriptions-item
:label="`${attr.alias || attr.name}`"
:key="attr.name"
v-for="attr in group.attributes"
>
<CiDetailAttrContent :ci="ci" :attr="attr" @refresh="refresh" />
</el-descriptions-item>
</el-descriptions>
</div>
</a-tab-pane>
<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" />
</div>
</a-tab-pane>
<a-tab-pane key="tab_3">
<span slot="tab"><a-icon type="clock-circle" />操作历史</span>
<div :style="{ padding: '24px', height: 'calc(100vh - 44px)' }">
<vxe-table
ref="xTable"
:data="ciHistory"
size="small"
:max-height="`${windowHeight - 94}px`"
:span-method="mergeRowMethod"
border
:scroll-y="{ enabled: false }"
class="ops-stripe-table"
>
<vxe-table-column sortable field="created_at" title="时间"></vxe-table-column>
<vxe-table-column
field="username"
title="用户"
:filters="[]"
:filter-method="filterUsernameMethod"
></vxe-table-column>
<vxe-table-column
field="operate_type"
:filters="[
{ value: 0, label: '新增' },
{ value: 1, label: '删除' },
{ value: 3, label: '修改' },
]"
:filter-method="filterOperateMethod"
title="操作"
>
<template #default="{ row }">
{{ operateTypeMap[row.operate_type] }}
</template>
</vxe-table-column>
<vxe-table-column
field="attr_alias"
title="属性"
:filters="[]"
:filter-method="filterAttrMethod"
></vxe-table-column>
<vxe-table-column field="old" title=""></vxe-table-column>
<vxe-table-column field="new" title=""></vxe-table-column>
</vxe-table>
</div>
</a-tab-pane>
</a-tabs>
</CustomDrawer>
</template>
<script>
import { Descriptions, DescriptionsItem } from 'element-ui'
import { getCITypeGroupById, getCITypes } from '@/modules/cmdb/api/CIType'
import { getCIHistory } from '@/modules/cmdb/api/history'
import { getCIById } from '@/modules/cmdb/api/ci'
import CiDetailAttrContent from './ciDetailAttrContent.vue'
import CiDetailRelation from './ciDetailRelation.vue'
export default {
components: {
ElDescriptions: Descriptions,
ElDescriptionsItem: DescriptionsItem,
CiDetailAttrContent,
CiDetailRelation,
},
props: {
typeId: {
type: Number,
required: true,
},
treeViewsLevels: {
type: Array,
default: () => [],
},
},
data() {
const operateTypeMap = {
0: '新增',
1: '删除',
2: '修改',
}
return {
operateTypeMap,
visible: false,
ci: {},
attributeGroups: [],
activeTabKey: 'tab_1',
rowSpanMap: {},
ciHistory: [],
ciId: null,
ci_types: [],
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
},
provide() {
return {
ci_types: () => {
return this.ci_types
},
}
},
inject: ['reload', 'handleSearch', 'attrList'],
methods: {
create(ciId, activeTabKey = 'tab_1', ciDetailRelationKey = '1') {
this.visible = true
this.activeTabKey = activeTabKey
if (activeTabKey === 'tab_2') {
this.$nextTick(() => {
this.$refs.ciDetailRelation.activeKey = ciDetailRelationKey
})
}
this.ciId = ciId
this.getAttributes()
this.getCI()
this.getCIHistory()
getCITypes().then((res) => {
this.ci_types = res.ci_types
})
},
getAttributes() {
getCITypeGroupById(this.typeId, { need_other: 1 })
.then((res) => {
this.attributeGroups = res
})
.catch((e) => {})
},
getCI() {
getCIById(this.ciId)
.then((res) => {
// this.ci = res.ci
this.ci = res.result[0]
})
.catch((e) => {})
},
getCIHistory() {
getCIHistory(this.ciId)
.then((res) => {
this.ciHistory = res
const rowSpanMap = {}
let startIndex = 0
let startCount = 1
res.forEach((item, index) => {
if (index === 0) {
return
}
if (res[index].record_id === res[startIndex].record_id) {
startCount += 1
rowSpanMap[index] = 0
if (index === res.length - 1) {
rowSpanMap[startIndex] = startCount
}
} else {
rowSpanMap[startIndex] = startCount
startIndex = index
startCount = 1
if (index === res.length - 1) {
rowSpanMap[index] = 1
}
}
})
this.rowSpanMap = rowSpanMap
})
.catch((e) => {
console.log(e)
})
},
changeTab(key) {
this.activeTabKey = key
if (key === 'tab_3') {
this.$nextTick(() => {
const $table = this.$refs.xTable
if ($table) {
const usernameColumn = $table.getColumnByField('username')
const attrColumn = $table.getColumnByField('attr_alias')
if (usernameColumn) {
const usernameList = [...new Set(this.ciHistory.map((item) => item.username))]
$table.setFilter(
usernameColumn,
usernameList.map((item) => {
return {
value: item,
label: item,
}
})
)
}
if (attrColumn) {
$table.setFilter(
attrColumn,
this.attrList().map((attr) => {
return { value: attr.alias || attr.name, label: attr.alias || attr.name }
})
)
}
}
})
}
},
filterUsernameMethod({ value, row, column }) {
return row.username === value
},
filterOperateMethod({ value, row, column }) {
return Number(row.operate_type) === Number(value)
},
filterAttrMethod({ value, row, column }) {
return row.attr_alias === value
},
refresh(editAttrName) {
this.getCI()
const _find = this.treeViewsLevels.find((level) => level.name === editAttrName)
// 修改的字段为树形视图订阅的字段 则全部reload
setTimeout(() => {
if (_find) {
this.reload()
} else {
this.handleSearch()
}
}, 500)
},
mergeRowMethod({ row, _rowIndex, column, visibleData }) {
const fields = ['created_at', 'username']
const cellValue = row[column.property]
if (cellValue && fields.includes(column.property)) {
const prevRow = visibleData[_rowIndex - 1]
let nextRow = visibleData[_rowIndex + 1]
if (prevRow && prevRow[column.property] === cellValue) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow[column.property] === cellValue) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
},
},
}
</script>
<style lang="less" scoped></style>
<style lang="less">
.ci-detail {
.ant-tabs-bar {
margin: 0;
}
.ci-detail-attr {
.el-descriptions-item__content {
cursor: default;
&:hover a {
opacity: 1 !important;
}
}
.el-descriptions:first-child > .el-descriptions__header {
margin-top: 0;
}
.el-descriptions__header {
margin-bottom: 5px;
margin-top: 20px;
}
.ant-form-item {
margin-bottom: 0;
}
.ant-form-item-control {
line-height: 19px;
}
}
}
</style>
<template>
<CustomDrawer
width="80%"
placement="left"
@close="
() => {
visible = false
}
"
:visible="visible"
:hasTitle="false"
:hasFooter="false"
:bodyStyle="{ padding: 0, height: '100vh' }"
wrapClassName="ci-detail"
destroyOnClose
>
<a-tabs v-model="activeTabKey" @change="changeTab">
<a-tab-pane key="tab_1">
<span slot="tab"><a-icon type="book" />属性</span>
<div :style="{ maxHeight: `${windowHeight - 44}px`, overflow: 'auto', padding: '24px' }" class="ci-detail-attr">
<el-descriptions
:title="group.name || '其他'"
:key="group.name"
v-for="group in attributeGroups"
border
:column="3"
>
<el-descriptions-item
:label="`${attr.alias || attr.name}`"
:key="attr.name"
v-for="attr in group.attributes"
>
<CiDetailAttrContent :ci="ci" :attr="attr" @refresh="refresh" />
</el-descriptions-item>
</el-descriptions>
</div>
</a-tab-pane>
<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" />
</div>
</a-tab-pane>
<a-tab-pane key="tab_3">
<span slot="tab"><a-icon type="clock-circle" />操作历史</span>
<div :style="{ padding: '24px', height: 'calc(100vh - 44px)' }">
<vxe-table
ref="xTable"
:data="ciHistory"
size="small"
:max-height="`${windowHeight - 94}px`"
:span-method="mergeRowMethod"
border
:scroll-y="{ enabled: false }"
class="ops-stripe-table"
>
<vxe-table-column sortable field="created_at" title="时间"></vxe-table-column>
<vxe-table-column
field="username"
title="用户"
:filters="[]"
:filter-method="filterUsernameMethod"
></vxe-table-column>
<vxe-table-column
field="operate_type"
:filters="[
{ value: 0, label: '新增' },
{ value: 1, label: '删除' },
{ value: 3, label: '修改' },
]"
:filter-method="filterOperateMethod"
title="操作"
>
<template #default="{ row }">
{{ operateTypeMap[row.operate_type] }}
</template>
</vxe-table-column>
<vxe-table-column
field="attr_alias"
title="属性"
:filters="[]"
:filter-method="filterAttrMethod"
></vxe-table-column>
<vxe-table-column field="old" title=""></vxe-table-column>
<vxe-table-column field="new" title=""></vxe-table-column>
</vxe-table>
</div>
</a-tab-pane>
<a-tab-pane key="tab_4">
<span slot="tab"><ops-icon type="itsm_auto_trigger" />触发历史</span>
<div :style="{ padding: '24px', height: 'calc(100vh - 44px)' }">
<TriggerTable :ci_id="ci._id" />
</div>
</a-tab-pane>
</a-tabs>
</CustomDrawer>
</template>
<script>
import { Descriptions, DescriptionsItem } from 'element-ui'
import { getCITypeGroupById, getCITypes } from '@/modules/cmdb/api/CIType'
import { getCIHistory } from '@/modules/cmdb/api/history'
import { getCIById } from '@/modules/cmdb/api/ci'
import CiDetailAttrContent from './ciDetailAttrContent.vue'
import CiDetailRelation from './ciDetailRelation.vue'
import TriggerTable from '../../operation_history/modules/triggerTable.vue'
export default {
components: {
ElDescriptions: Descriptions,
ElDescriptionsItem: DescriptionsItem,
CiDetailAttrContent,
CiDetailRelation,
TriggerTable,
},
props: {
typeId: {
type: Number,
required: true,
},
treeViewsLevels: {
type: Array,
default: () => [],
},
},
data() {
const operateTypeMap = {
0: '新增',
1: '删除',
2: '修改',
}
return {
operateTypeMap,
visible: false,
ci: {},
attributeGroups: [],
activeTabKey: 'tab_1',
rowSpanMap: {},
ciHistory: [],
ciId: null,
ci_types: [],
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
},
provide() {
return {
ci_types: () => {
return this.ci_types
},
}
},
inject: ['reload', 'handleSearch', 'attrList'],
methods: {
create(ciId, activeTabKey = 'tab_1', ciDetailRelationKey = '1') {
this.visible = true
this.activeTabKey = activeTabKey
if (activeTabKey === 'tab_2') {
this.$nextTick(() => {
this.$refs.ciDetailRelation.activeKey = ciDetailRelationKey
})
}
this.ciId = ciId
this.getAttributes()
this.getCI()
this.getCIHistory()
getCITypes().then((res) => {
this.ci_types = res.ci_types
})
},
getAttributes() {
getCITypeGroupById(this.typeId, { need_other: 1 })
.then((res) => {
this.attributeGroups = res
})
.catch((e) => {})
},
getCI() {
getCIById(this.ciId)
.then((res) => {
// this.ci = res.ci
this.ci = res.result[0]
})
.catch((e) => {})
},
getCIHistory() {
getCIHistory(this.ciId)
.then((res) => {
this.ciHistory = res
const rowSpanMap = {}
let startIndex = 0
let startCount = 1
res.forEach((item, index) => {
if (index === 0) {
return
}
if (res[index].record_id === res[startIndex].record_id) {
startCount += 1
rowSpanMap[index] = 0
if (index === res.length - 1) {
rowSpanMap[startIndex] = startCount
}
} else {
rowSpanMap[startIndex] = startCount
startIndex = index
startCount = 1
if (index === res.length - 1) {
rowSpanMap[index] = 1
}
}
})
this.rowSpanMap = rowSpanMap
})
.catch((e) => {
console.log(e)
})
},
changeTab(key) {
this.activeTabKey = key
if (key === 'tab_3') {
this.$nextTick(() => {
const $table = this.$refs.xTable
if ($table) {
const usernameColumn = $table.getColumnByField('username')
const attrColumn = $table.getColumnByField('attr_alias')
if (usernameColumn) {
const usernameList = [...new Set(this.ciHistory.map((item) => item.username))]
$table.setFilter(
usernameColumn,
usernameList.map((item) => {
return {
value: item,
label: item,
}
})
)
}
if (attrColumn) {
$table.setFilter(
attrColumn,
this.attrList().map((attr) => {
return { value: attr.alias || attr.name, label: attr.alias || attr.name }
})
)
}
}
})
}
},
filterUsernameMethod({ value, row, column }) {
return row.username === value
},
filterOperateMethod({ value, row, column }) {
return Number(row.operate_type) === Number(value)
},
filterAttrMethod({ value, row, column }) {
return row.attr_alias === value
},
refresh(editAttrName) {
this.getCI()
const _find = this.treeViewsLevels.find((level) => level.name === editAttrName)
// 修改的字段为树形视图订阅的字段 则全部reload
setTimeout(() => {
if (_find) {
this.reload()
} else {
this.handleSearch()
}
}, 500)
},
mergeRowMethod({ row, _rowIndex, column, visibleData }) {
const fields = ['created_at', 'username']
const cellValue = row[column.property]
if (cellValue && fields.includes(column.property)) {
const prevRow = visibleData[_rowIndex - 1]
let nextRow = visibleData[_rowIndex + 1]
if (prevRow && prevRow[column.property] === cellValue) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow[column.property] === cellValue) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
},
},
}
</script>
<style lang="less" scoped></style>
<style lang="less">
.ci-detail {
.ant-tabs-bar {
margin: 0;
}
.ci-detail-attr {
.el-descriptions-item__content {
cursor: default;
&:hover a {
opacity: 1 !important;
}
}
.el-descriptions:first-child > .el-descriptions__header {
margin-top: 0;
}
.el-descriptions__header {
margin-bottom: 5px;
margin-top: 20px;
}
.ant-form-item {
margin-bottom: 0;
}
.ant-form-item-control {
line-height: 19px;
}
}
}
</style>

View File

@@ -1,163 +1,168 @@
<template>
<div
id="ci-detail-relation-topo"
class="ci-detail-relation-topo"
:style="{ width: '100%', marginTop: '20px', height: 'calc(100vh - 136px)' }"
></div>
</template>
<script>
import _ from 'lodash'
import { TreeCanvas } from 'butterfly-dag'
import { searchCIRelation } from '@/modules/cmdb/api/CIRelation'
import Node from './node.js'
import 'butterfly-dag/dist/index.css'
import './index.less'
export default {
name: 'CiDetailRelationTopo',
data() {
return {
topoData: {},
}
},
inject: ['ci_types'],
mounted() {},
methods: {
init() {
const root = document.getElementById('ci-detail-relation-topo')
this.canvas = new TreeCanvas({
root: root,
disLinkable: false, // 可删除连线
linkable: false, // 可连线
draggable: true, // 拖动
zoomable: true, // 放大
moveable: true, // 平移
theme: {
edge: {
shapeType: 'AdvancedBezier',
arrow: true,
arrowPosition: 1,
},
},
layout: {
type: 'mindmap',
options: {
direction: 'H',
getSide(d) {
return d.data.side || 'right'
},
getHeight(d) {
return 10
},
getWidth(d) {
return 40
},
getHGap(d) {
return 80
},
getVGap(d) {
return 40
},
},
},
})
this.canvas.setZoomable(true, true)
this.canvas.on('events', ({ type, data }) => {
const sourceNode = data?.id || null
if (type === 'custom:clickLeft') {
searchCIRelation(`root_id=${Number(sourceNode)}&&level=1&&reverse=1&&count=10000`).then((res) => {
this.redrawData(res, sourceNode, 'left')
})
}
if (type === 'custom:clickRight') {
searchCIRelation(`root_id=${Number(sourceNode)}&&level=1&&reverse=0&&count=10000`).then((res) => {
this.redrawData(res, sourceNode, 'right')
})
}
})
},
setTopoData(data) {
this.canvas = null
this.init()
this.topoData = _.cloneDeep(data)
this.canvas.draw(data, {}, () => {
this.canvas.focusCenterWithAnimate()
})
},
redrawData(res, sourceNode, side) {
const newNodes = []
const newEdges = []
if (!res.result.length) {
this.$message.info('无层级关系!')
return
}
const ci_types_list = this.ci_types()
res.result.forEach((r) => {
const _findCiType = ci_types_list.find((item) => item.id === r._type)
newNodes.push({
id: `${r._id}`,
Class: Node,
title: r.ci_type_alias || r.ci_type,
name: r.ci_type,
side: side,
unique_alias: r.unique_alias,
unique_name: r.unique,
unique_value: r[r.unique],
children: [],
icon: _findCiType?.icon || '',
endpoints: [
{
id: 'left',
orientation: [-1, 0],
pos: [0, 0.5],
},
{
id: 'right',
orientation: [1, 0],
pos: [0, 0.5],
},
],
})
newEdges.push({
id: `${r._id}`,
source: 'right',
target: 'left',
sourceNode: side === 'right' ? sourceNode : `${r._id}`,
targetNode: side === 'right' ? `${r._id}` : sourceNode,
type: 'endpoint',
})
})
const { nodes, edges } = this.canvas.getDataMap()
// 删除原节点和边
this.canvas.removeNodes(nodes.map((node) => node.id))
this.canvas.removeEdges(edges)
const _topoData = _.cloneDeep(this.topoData)
let result
const getTreeItem = (data, id) => {
for (let i = 0; i < data.length; i++) {
if (data[i].id === id) {
result = data[i] // 结果赋值
break
} else {
if (data[i].children && data[i].children.length) {
getTreeItem(data[i].children, id)
}
}
}
}
getTreeItem(_topoData.nodes.children, sourceNode)
result.children.push(...newNodes)
_topoData.edges.push(...newEdges)
this.topoData = _topoData
this.canvas.draw(_topoData, {}, () => {})
},
},
}
</script>
<style></style>
<template>
<div
id="ci-detail-relation-topo"
class="ci-detail-relation-topo"
:style="{ width: '100%', marginTop: '20px', height: 'calc(100vh - 136px)' }"
></div>
</template>
<script>
import _ from 'lodash'
import { TreeCanvas } from 'butterfly-dag'
import { searchCIRelation } from '@/modules/cmdb/api/CIRelation'
import Node from './node.js'
import 'butterfly-dag/dist/index.css'
import './index.less'
export default {
name: 'CiDetailRelationTopo',
data() {
return {
topoData: {},
exsited_ci: [],
}
},
inject: ['ci_types'],
mounted() {},
methods: {
init() {
const root = document.getElementById('ci-detail-relation-topo')
this.canvas = new TreeCanvas({
root: root,
disLinkable: false, // 删除连线
linkable: false, // 连线
draggable: true, // 拖动
zoomable: true, // 放大
moveable: true, // 可平移
theme: {
edge: {
shapeType: 'AdvancedBezier',
arrow: true,
arrowPosition: 1,
},
},
layout: {
type: 'mindmap',
options: {
direction: 'H',
getSide(d) {
return d.data.side || 'right'
},
getHeight(d) {
return 10
},
getWidth(d) {
return 40
},
getHGap(d) {
return 80
},
getVGap(d) {
return 40
},
},
},
})
this.canvas.setZoomable(true, true)
this.canvas.on('events', ({ type, data }) => {
const sourceNode = data?.id || null
if (type === 'custom:clickLeft') {
searchCIRelation(`root_id=${Number(sourceNode)}&&level=1&&reverse=1&&count=10000`).then((res) => {
this.redrawData(res, sourceNode, 'left')
})
}
if (type === 'custom:clickRight') {
searchCIRelation(`root_id=${Number(sourceNode)}&&level=1&&reverse=0&&count=10000`).then((res) => {
this.redrawData(res, sourceNode, 'right')
})
}
})
},
setTopoData(data) {
this.canvas = null
this.init()
this.topoData = _.cloneDeep(data)
this.canvas.draw(data, {}, () => {
this.canvas.focusCenterWithAnimate()
})
},
redrawData(res, sourceNode, side) {
const newNodes = []
const newEdges = []
if (!res.result.length) {
this.$message.info('无层级关系!')
return
}
const ci_types_list = this.ci_types()
res.result.forEach((r) => {
if (!this.exsited_ci.includes(r._id)) {
const _findCiType = ci_types_list.find((item) => item.id === r._type)
newNodes.push({
id: `${r._id}`,
Class: Node,
title: r.ci_type_alias || r.ci_type,
name: r.ci_type,
side: side,
unique_alias: r.unique_alias,
unique_name: r.unique,
unique_value: r[r.unique],
children: [],
icon: _findCiType?.icon || '',
endpoints: [
{
id: 'left',
orientation: [-1, 0],
pos: [0, 0.5],
},
{
id: 'right',
orientation: [1, 0],
pos: [0, 0.5],
},
],
})
}
newEdges.push({
id: `${r._id}`,
source: 'right',
target: 'left',
sourceNode: side === 'right' ? sourceNode : `${r._id}`,
targetNode: side === 'right' ? `${r._id}` : sourceNode,
type: 'endpoint',
})
})
const { nodes, edges } = this.canvas.getDataMap()
// 删除原节点和边
this.canvas.removeNodes(nodes.map((node) => node.id))
this.canvas.removeEdges(edges)
const _topoData = _.cloneDeep(this.topoData)
_topoData.edges.push(...newEdges)
let result
const getTreeItem = (data, id) => {
for (let i = 0; i < data.length; i++) {
if (data[i].id === id) {
result = data[i] // 结果赋值
result.edges = _topoData.edges
break
} else {
if (data[i].children && data[i].children.length) {
getTreeItem(data[i].children, id)
}
}
}
}
getTreeItem(_topoData.nodes.children, sourceNode)
result.children.push(...newNodes)
this.topoData = _topoData
this.canvas.draw(_topoData, {}, () => {})
this.exsited_ci = [...new Set([...this.exsited_ci, ...res.result.map((r) => r._id)])]
},
},
}
</script>
<style></style>

View File

@@ -1,56 +1,56 @@
/* eslint-disable no-useless-constructor */
import { TreeNode } from 'butterfly-dag'
import $ from 'jquery'
class BaseNode extends TreeNode {
constructor(opts) {
super(opts)
}
draw = (opts) => {
const container = $(`<div class="${opts.id.startsWith('Root') ? 'root' : ''} ci-detail-relation-topo-node"></div>`)
.css('top', opts.top)
.css('left', opts.left)
.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>`)
}
} else {
icon = $(`<span class="icon icon-default">${opts.options.name[0].toUpperCase()}</span>`)
}
const titleContent = $(`<div title=${opts.options.title} class="title">${opts.options.title}</div>`)
const uniqueDom = $(`<div class="unique">${opts.options.unique_alias || opts.options.unique_name}${opts.options.unique_value}<div>`)
container.append(icon)
container.append(titleContent)
container.append(uniqueDom)
if (opts.options.side && !opts.options.children.length) {
const addIcon = $(`<i aria-label="图标: plus-square" class="anticon anticon-plus-square add-icon-${opts.options.side}"><svg viewBox="64 64 896 896" data-icon="plus-square" width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false" class=""><path d="M328 544h152v152c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V544h152c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H544V328c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8v152H328c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8z"></path><path d="M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32zm-40 728H184V184h656v656z"></path></svg></i>`)
container.append(addIcon)
addIcon.on('click', () => {
if (opts.options.side === 'left') {
this.emit('events', {
type: 'custom:clickLeft',
data: { ...this }
})
}
if (opts.options.side === 'right') {
this.emit('events', {
type: 'custom:clickRight',
data: { ...this }
})
}
})
}
return container[0]
}
}
export default BaseNode
/* eslint-disable no-useless-constructor */
import { TreeNode } from 'butterfly-dag'
import $ from 'jquery'
class BaseNode extends TreeNode {
constructor(opts) {
super(opts)
}
draw = (opts) => {
const container = $(`<div class="${opts.id.startsWith('Root') ? 'root' : ''} ci-detail-relation-topo-node"></div>`)
.css('top', opts.top)
.css('left', opts.left)
.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>`)
}
} else {
icon = $(`<span class="icon icon-default">${opts.options.name[0].toUpperCase()}</span>`)
}
const titleContent = $(`<div title=${opts.options.title} class="title">${opts.options.title}</div>`)
const uniqueDom = $(`<div class="unique">${opts.options.unique_alias || opts.options.unique_name}${opts.options.unique_value}<div>`)
container.append(icon)
container.append(titleContent)
container.append(uniqueDom)
if (opts.options.side && (!opts.options.children.length && !(opts.options.edges && opts.options.edges.length && opts.options.edges.find(e => e.source === opts.options.side && e.sourceNode === opts.options.id)))) {
const addIcon = $(`<i aria-label="图标: plus-square" class="anticon anticon-plus-square add-icon-${opts.options.side}"><svg viewBox="64 64 896 896" data-icon="plus-square" width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false" class=""><path d="M328 544h152v152c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V544h152c4.4 0 8-3.6 8-8v-48c0-4.4-3.6-8-8-8H544V328c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8v152H328c-4.4 0-8 3.6-8 8v48c0 4.4 3.6 8 8 8z"></path><path d="M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32zm-40 728H184V184h656v656z"></path></svg></i>`)
container.append(addIcon)
addIcon.on('click', () => {
if (opts.options.side === 'left') {
this.emit('events', {
type: 'custom:clickLeft',
data: { ...this }
})
}
if (opts.options.side === 'right') {
this.emit('events', {
type: 'custom:clickRight',
data: { ...this }
})
}
})
}
return container[0]
}
}
export default BaseNode

View File

@@ -1,197 +1,559 @@
<template>
<a-modal :title="title" :visible="visible" @cancel="handleCancel" @ok="handleOk">
<a-space slot="footer">
<a-button type="primary" ghost @click="handleCancel">取消</a-button>
<a-button v-if="triggerId" type="danger" @click="handleDetele">删除</a-button>
<a-button @click="handleOk" type="primary">确定</a-button>
</a-space>
<a-form-model ref="triggerForm" :model="form" :rules="rules" :label-col="{ span: 6 }" :wrapper-col="{ span: 16 }">
<a-form-model-item label="属性" prop="attr_id" :hidden="!isCreateFromTriggerTable || triggerId">
<a-select v-model="form.attr_id">
<a-select-option v-for="attr in canAddTriggerAttr" :key="attr.id" :value="attr.id">{{
attr.alias || attr.name
}}</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item label="主题" prop="subject">
<a-input v-model="form.subject" />
</a-form-model-item>
<a-form-model-item label="内容" prop="body">
<a-textarea v-model="form.body" :rows="3" />
</a-form-model-item>
<a-form-model-item label="微信通知" prop="wx_to">
<a-select
mode="tags"
v-model="form.wx_to"
placeholder="选择微信通知人"
showSearch
:filter-option="false"
@search="filterChange"
>
<a-select-option v-for="item in filterWxUsers" :value="item['wx_id']" :key="item.id">
<span>{{ item['nickname'] }}</span>
<a-divider type="vertical" />
<span>{{ item['wx_id'].length > 12 ? item['wx_id'].slice(0, 10) + '...' : item['wx_id'] }}</span>
</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item label="邮箱通知" prop="mail_to">
<a-textarea v-model="form.mail_to" :rows="3" placeholder="多个邮箱用逗号分隔" />
</a-form-model-item>
<a-form-model-item label="提前" prop="before_days">
<a-input-number v-model="form.before_days" :min="0" />
</a-form-model-item>
<a-form-model-item label="发送时间" prop="notify_at">
<a-time-picker v-model="form.notify_at" format="HH:mm" valueFormat="HH:mm" />
</a-form-model-item>
</a-form-model>
</a-modal>
</template>
<script>
import { getWX } from '../../api/perm'
import { addTrigger, updateTrigger, deleteTrigger } from '../../api/CIType'
export default {
name: 'TriggerForm',
props: {
CITypeId: {
type: Number,
default: null,
},
},
data() {
return {
visible: false,
form: { attr_id: '', subject: '', body: '', wx_to: [], mail_to: '', before_days: 0, notify_at: '08:00' },
rules: {
attr_id: [{ required: true, message: '请选择属性' }],
subject: [{ required: true, message: '请填写主题' }],
body: [{ required: true, message: '请填写内容' }],
},
WxUsers: [],
filterValue: '',
triggerId: null,
attr_id: null,
canAddTriggerAttr: [],
isCreateFromTriggerTable: false,
title: '新增触发器',
}
},
computed: {
filterWxUsers() {
if (!this.filterValue) {
return this.WxUsers
}
return this.WxUsers.filter(
(user) =>
user.nickname.toLowerCase().indexOf(this.filterValue.toLowerCase()) >= 0 ||
user.username.toLowerCase().indexOf(this.filterValue.toLowerCase()) >= 0
)
},
},
inject: {
refresh: {
from: 'refresh',
default: null,
},
},
methods: {
createFromTriggerTable(canAddTriggerAttr) {
this.visible = true
this.getWxList()
this.canAddTriggerAttr = canAddTriggerAttr
this.triggerId = null
this.isCreateFromTriggerTable = true
this.title = '新增触发器'
this.form = {
attr_id: '',
subject: '',
body: '',
wx_to: [],
mail_to: '',
before_days: 0,
notify_at: '08:00',
}
},
open(property) {
this.visible = true
this.getWxList()
if (property.has_trigger) {
this.triggerId = property.trigger.id
this.title = `编辑触发器 ${property.alias || property.name}`
this.form = {
...property.trigger.notify,
attr_id: property.id,
mail_to: property.trigger.notify.mail_to ? property.trigger.notify.mail_to.join(',') : '',
}
} else {
this.title = `新增触发器 ${property.alias || property.name}`
this.triggerId = null
this.form = {
attr_id: property.id,
subject: '',
body: '',
wx_to: [],
mail_to: '',
before_days: 0,
notify_at: '08:00',
}
}
},
handleCancel() {
this.$refs.triggerForm.clearValidate()
this.$refs.triggerForm.resetFields()
this.filterValue = ''
this.visible = false
},
getWxList() {
getWX().then((res) => {
this.WxUsers = res.filter((item) => item.wx_id)
})
},
filterChange(value) {
this.filterValue = value
},
handleOk() {
this.$refs.triggerForm.validate(async (valid) => {
if (valid) {
const { mail_to, attr_id } = this.form
const params = {
attr_id,
notify: { ...this.form, mail_to: mail_to ? mail_to.split(',') : undefined },
}
delete params.notify.attr_id
if (this.triggerId) {
await updateTrigger(this.CITypeId, this.triggerId, params)
} else {
await addTrigger(this.CITypeId, params)
}
this.handleCancel()
if (this.refresh) {
this.refresh()
}
}
})
},
handleDetele() {
const that = this
this.$confirm({
title: '警告',
content: '确认删除该触发器吗?',
onOk() {
deleteTrigger(that.CITypeId, that.triggerId).then(() => {
that.$message.success('删除成功!')
that.handleCancel()
if (that.refresh) {
that.refresh()
}
})
},
})
},
},
}
</script>
<style></style>
<template>
<CustomDrawer
wrapClassName="trigger-form"
:width="700"
:title="title"
:visible="visible"
@close="handleCancel"
@ok="handleOk"
>
<div class="custom-drawer-bottom-action">
<a-button type="primary" ghost @click="handleCancel">取消</a-button>
<a-button v-if="triggerId" type="danger" @click="handleDetele">删除</a-button>
<a-button @click="handleOk" type="primary">确定</a-button>
</div>
<a-form-model ref="triggerForm" :model="form" :rules="rules" :label-col="{ span: 3 }" :wrapper-col="{ span: 18 }">
<p><strong>基本信息</strong></p>
<a-form-model-item label="名称" prop="name">
<a-input v-model="form.name" placeholder="请输入名称" />
</a-form-model-item>
<a-form-model-item label="类型">
<a-radio-group v-model="category">
<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 label="备注" prop="description">
<a-input v-model="form.description" placeholder="请输入备注" />
</a-form-model-item>
<a-form-model-item label="开启" prop="enable">
<a-switch v-model="form.enable" />
</a-form-model-item>
<template v-if="category === 1">
<p><strong>触发条件</strong></p>
<a-form-model-item label="事件" prop="action">
<a-radio-group v-model="form.action">
<a-radio value="0">
新增实例
</a-radio>
<a-radio value="1">
删除实例
</a-radio>
<a-radio value="2">
实例变更
</a-radio>
</a-radio-group>
</a-form-model-item>
<a-form-model-item v-if="form.action === '2'" label="属性" prop="attr_ids">
<a-select v-model="form.attr_ids" show-search mode="multiple" placeholder="请选择属性(多选)">
<a-select-option v-for="attr in attrList" :key="attr.id" :value="attr.id">{{
attr.alias || attr.name
}}</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item label="筛选" class="trigger-form-filter">
<FilterComp
ref="filterComp"
:isDropdown="false"
:canSearchPreferenceAttrList="attrList"
@setExpFromFilter="setExpFromFilter"
:expression="filterExp ? `q=${filterExp}` : ''"
/>
</a-form-model-item>
</template>
</a-form-model>
<template v-if="category === 2">
<p><strong>触发条件</strong></p>
<a-form-model
ref="dateForm"
:model="dateForm"
:rules="dateFormRules"
:label-col="{ span: 3 }"
:wrapper-col="{ span: 18 }"
>
<a-form-model-item label="属性" prop="attr_id">
<a-select v-model="dateForm.attr_id" placeholder="请选择属性(单选)">
<a-select-option v-for="attr in canAddTriggerAttr" :key="attr.id" :value="attr.id">{{
attr.alias || attr.name
}}</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item label="筛选" class="trigger-form-filter">
<FilterComp
ref="filterComp"
:isDropdown="false"
:canSearchPreferenceAttrList="attrList"
@setExpFromFilter="setExpFromFilter"
:expression="filterExp ? `q=${filterExp}` : ''"
/>
</a-form-model-item>
<a-form-model-item label="提前" prop="before_days">
<a-input-number v-model="dateForm.before_days" :min="0" />
</a-form-model-item>
<a-form-model-item label="发送时间" prop="notify_at">
<a-time-picker v-model="dateForm.notify_at" format="HH:mm" valueFormat="HH:mm" />
</a-form-model-item>
</a-form-model>
</template>
<p><strong>触发动作</strong></p>
<a-radio-group
v-model="triggerAction"
:style="{ width: '100%', display: 'flex', justifyContent: 'space-around', marginBottom: '10px' }"
>
<a-radio value="1">
通知
</a-radio>
<a-radio value="2">
Webhook
</a-radio>
<!-- <a-radio value="3">
DAG
</a-radio> -->
</a-radio-group>
<a-form-model
ref="notifiesForm"
:model="notifies"
:rules="notifiesRules"
:label-col="{ span: 3 }"
:wrapper-col="{ span: 18 }"
v-if="triggerAction === '1'"
>
<a-form-model-item label=" " :colon="false">
<span class="trigger-tips">{{ tips }}</span>
</a-form-model-item>
<a-form-model-item label="收件人" prop="employee_ids" class="trigger-form-employee">
<EmployeeTreeSelect multiple v-model="notifies.employee_ids" />
<div class="trigger-form-custom-email">
<a-textarea
v-if="showCustomEmail"
v-model="notifies.custom_email"
placeholder="请输入邮箱,多个邮箱用;分隔"
:rows="1"
/>
<a-button
@click="
() => {
showCustomEmail = !showCustomEmail
}
"
type="primary"
size="small"
class="ops-button-primary"
>{{ `${showCustomEmail ? '删除' : '添加'}自定义收件人` }}</a-button
>
</div>
</a-form-model-item>
<a-form-model-item label="通知标题" prop="subject">
<a-input v-model="notifies.subject" placeholder="请输入通知标题" />
</a-form-model-item>
<a-form-model-item label="内容" prop="body" :wrapper-col="{ span: 21 }">
<NoticeContent :needOld="category === 1 && form.action === '2'" :attrList="attrList" ref="noticeContent" />
</a-form-model-item>
<a-form-model-item label="通知方式" prop="method">
<a-checkbox-group v-model="notifies.method">
<a-checkbox value="wechatApp">
微信
</a-checkbox>
<a-checkbox value="email">
邮件
</a-checkbox>
</a-checkbox-group>
</a-form-model-item>
</a-form-model>
<div class="auto-complete-wrapper" v-if="triggerAction === '3'">
<a-input
id="auto-complete-wrapper-input"
ref="input"
v-model="searchValue"
@focus="focusOnInput"
@blur="handleBlurInput"
allowClear
>
</a-input>
<div id="auto-complete-wrapper-popover" class="auto-complete-wrapper-popover" v-if="isShow">
<div
class="auto-complete-wrapper-popover-item"
@click="handleClickSelect(item)"
v-for="item in filterList"
:key="item.id"
:title="item.label"
>
{{ item.label }}
</div>
</div>
</div>
<span v-if="triggerAction === '2'" class="trigger-tips">{{ webhookTips }}</span>
<Webhook ref="webhook" style="margin-top:10px" v-if="triggerAction === '2'" />
</CustomDrawer>
</template>
<script>
import _ from 'lodash'
import { addTrigger, updateTrigger, deleteTrigger, getAllDagsName } from '../../api/CIType'
import FilterComp from '@/components/CMDBFilterComp'
import EmployeeTreeSelect from '@/views/setting/components/employeeTreeSelect.vue'
import Webhook from '../../components/webhook'
import NoticeContent from '../../components/noticeContent'
import { getNoticeByEmployeeIds } from '@/api/employee'
export default {
name: 'TriggerForm',
components: { FilterComp, Webhook, EmployeeTreeSelect, NoticeContent },
props: {
CITypeId: {
type: Number,
default: null,
},
},
data() {
const defaultForm = {
name: '',
description: '',
enable: true,
action: '0',
attr_ids: [],
}
const defaultDateForm = {
attr_id: undefined,
before_days: 0,
notify_at: '08:00',
}
const defaultNotify = {
employee_ids: undefined,
custom_email: '',
subject: '',
body: '',
method: ['wechatApp'],
}
return {
defaultForm,
defaultDateForm,
defaultNotify,
tips: '标题、内容可以引用该模型的属性值,引用方法为: {{ attr_name }}',
webhookTips: '请求参数可以引用该模型的属性值,引用方法为: {{ attr_name }}',
visible: false,
category: 1,
form: _.cloneDeep(defaultForm),
rules: {
name: [{ required: true, message: '请填写名称' }],
},
dateForm: _.cloneDeep(defaultDateForm),
dateFormRules: {
attr_id: [{ required: true, message: '请选择属性' }],
},
notifies: _.cloneDeep(defaultNotify),
notifiesRules: {},
WxUsers: [],
filterValue: '',
triggerId: null,
title: '新增触发器',
attrList: [],
filterExp: '',
triggerAction: '1',
searchValue: '',
dags: [],
isShow: false,
dag_id: null,
showCustomEmail: false,
}
},
computed: {
canAddTriggerAttr() {
return this.attrList.filter((attr) => attr.value_type === '3' || attr.value_type === '4')
},
filterList() {
if (this.searchValue) {
return this.dags.filter((item) => item.label.toLowerCase().includes(this.searchValue.toLowerCase()))
}
return this.dags
},
},
inject: {
refresh: {
from: 'refresh',
default: null,
},
},
mounted() {},
methods: {
async getDags() {
await getAllDagsName().then((res) => {
this.dags = res.map((dag) => ({ id: dag[1], label: dag[0] }))
})
},
createFromTriggerTable(attrList) {
this.visible = true
// this.getDags()
this.attrList = attrList
this.triggerId = null
this.title = '新增触发器'
this.form = _.cloneDeep(this.defaultForm)
this.dateForm = _.cloneDeep(this.defaultDateForm)
this.notifies = _.cloneDeep(this.defaultNotify)
this.category = 1
this.triggerAction = '1'
this.filterExp = ''
this.$nextTick(() => {
this.$refs.filterComp.visibleChange(true, false)
setTimeout(() => {
this.$refs.noticeContent.setContent('')
}, 100)
})
},
async open(property, attrList) {
this.visible = true
// await this.getDags()
this.attrList = attrList
if (property.has_trigger) {
this.triggerId = property.trigger.id
this.title = `编辑触发器 ${property.alias || property.name}`
const { name, description, enable, action = '0', attr_ids, filter = '' } = property?.trigger?.option ?? {}
this.filterExp = filter
this.$nextTick(() => {
this.$refs.filterComp.visibleChange(true, false)
})
this.form = { name, description, enable, action, attr_ids }
const { attr_id } = property?.trigger ?? {}
if (attr_id) {
this.category = 2
const { before_days, notify_at } = property?.trigger?.option?.notifies ?? {}
this.dateForm = {
attr_id,
before_days,
notify_at,
}
} else {
this.category = 1
}
const { notifies = undefined, webhooks = undefined, dag_id = undefined } = property?.trigger?.option ?? {}
if (webhooks) {
this.triggerAction = '2'
this.$nextTick(() => {
this.$refs.webhook.setParams(webhooks)
})
} else if (dag_id) {
this.triggerAction = '3'
this.dag_id = dag_id
const _find = this.dags.find((item) => item.id === dag_id)
this.searchValue = _find?.label
} else if (notifies) {
this.triggerAction = '1'
const { tos = [], subject = '', body_html = '', method = ['wechatApp'] } =
property?.trigger?.option?.notifies ?? {}
const employee_ids = property?.trigger?.option?.employee_ids ?? undefined
const custom_email =
tos
.filter((t) => !t.employee_id)
.map((t) => t.email)
.join(';') ?? ''
if (custom_email) {
this.showCustomEmail = true
}
if (body_html) {
setTimeout(() => {
this.$refs.noticeContent.setContent(body_html)
}, 100)
}
this.notifies = { employee_ids, custom_email, subject, method }
}
} else {
this.title = `新增触发器 ${property.alias || property.name}`
this.triggerId = null
this.form = _.cloneDeep(this.defaultForm)
}
},
handleCancel() {
this.$refs.triggerForm.clearValidate()
this.$refs.triggerForm.resetFields()
this.filterValue = ''
this.form = _.cloneDeep(this.defaultForm)
this.dateForm = _.cloneDeep(this.defaultDateForm)
this.notifies = _.cloneDeep(this.defaultNotify)
this.category = 1
this.triggerAction = '1'
this.filterExp = ''
this.visible = false
},
filterChange(value) {
this.filterValue = value
},
handleOk() {
this.$refs.triggerForm.validate(async (valid) => {
if (valid) {
this.$refs.filterComp.handleSubmit()
const { name, description, enable, action, attr_ids } = this.form
const params = {
attr_id: '',
option: {
filter: this.filterExp,
name,
description,
enable,
},
}
switch (this.triggerAction) {
case '1':
const { employee_ids, custom_email, subject, method } = this.notifies
const { body, body_html } = this.$refs.noticeContent.getContent()
let tos = []
if (employee_ids && employee_ids.length) {
await getNoticeByEmployeeIds({ employee_ids: employee_ids.map((item) => item.split('-')[1]) }).then(
(res) => {
tos = tos.concat(res)
}
)
params.option.employee_ids = employee_ids
}
if (this.showCustomEmail) {
custom_email.split(';').forEach((email) => {
tos.push({ email })
})
}
if (this.category === 2) {
const { before_days, notify_at } = this.dateForm
params.option.notifies = { tos, subject, body, body_html, method, before_days, notify_at }
} else {
params.option.notifies = { tos, subject, body, body_html, method }
}
break
case '2':
const webhooks = this.$refs.webhook.getParams()
params.option.webhooks = webhooks
break
case '3':
params.option.dag_id = this.dag_id
break
}
if (this.category === 1) {
params.option.action = action
if (action === '2') {
params.option.attr_ids = attr_ids
}
}
if (this.category === 2) {
this.$refs.dateForm.validate((valid) => {
if (valid) {
const { attr_id, before_days, notify_at } = this.dateForm
params.attr_id = attr_id
params.option.notifies = { ..._.cloneDeep(params.option.notifies), before_days, notify_at }
} else {
throw Error()
}
})
}
if (this.triggerId) {
await updateTrigger(this.CITypeId, this.triggerId, params)
} else {
await addTrigger(this.CITypeId, params)
}
this.handleCancel()
if (this.refresh) {
this.refresh()
}
}
})
},
handleDetele() {
const that = this
this.$confirm({
title: '警告',
content: '确认删除该触发器吗?',
onOk() {
deleteTrigger(that.CITypeId, that.triggerId).then(() => {
that.$message.success('删除成功!')
that.handleCancel()
if (that.refresh) {
that.refresh()
}
})
},
})
},
setExpFromFilter(filterExp) {
if (filterExp) {
this.filterExp = `${filterExp}`
} else {
this.filterExp = ''
}
},
handleBlurInput() {
setTimeout(() => {
this.isShow = false
}, 100)
},
focusOnInput() {
this.isShow = true
},
handleClickSelect(item) {
this.searchValue = item.label
this.dag_id = item.id
},
},
}
</script>
<style lang="less">
.trigger-form {
.ant-form-item {
margin-bottom: 5px;
}
.trigger-form-employee,
.trigger-form-filter {
.ant-form-item-control {
line-height: 24px;
}
}
.trigger-form-filter {
.table-filter-add {
line-height: 40px;
}
}
}
</style>
<style lang="less" scoped>
@import '~@/style/static.less';
.auto-complete-wrapper {
position: relative;
margin-left: 25px;
width: 250px;
margin-top: 20px;
.auto-complete-wrapper-popover {
position: fixed;
width: 250px;
max-height: 200px;
overflow-y: auto;
overflow-x: hidden;
background-color: #fff;
z-index: 10;
box-shadow: 0 2px 8px #00000026;
.auto-complete-wrapper-popover-item {
.ops_popover_item();
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
.trigger-form-custom-email {
margin-top: 10px;
text-align: right;
}
.trigger-tips {
border: 1px solid #d4380d;
background-color: #fff2e8;
padding: 2px 10px;
border-radius: 4px;
color: #d4380d;
line-height: 1.5;
}
</style>

View File

@@ -1,144 +1,172 @@
<template>
<div class="ci-types-triggers">
<div style="margin-bottom: 10px">
<a-button
type="primary"
@click="handleAddTrigger"
size="small"
class="ops-button-primary"
icon="plus"
>新增触发器</a-button
>
<span class="trigger-tips">{{ tips }}</span>
</div>
<vxe-table
stripe
:data="tableData"
size="small"
show-overflow
highlight-hover-row
keep-source
:max-height="windowHeight - 180"
class="ops-stripe-table"
>
<vxe-column field="attr_name" title="属性名"></vxe-column>
<vxe-column field="notify.subject" title="主题"></vxe-column>
<vxe-column field="notify.body" title="内容"></vxe-column>
<vxe-column field="notify.wx_to" title="微信通知">
<template #default="{ row }">
<span v-for="(person, index) in row.notify.wx_to" :key="person + index">[{{ person }}]</span>
</template>
</vxe-column>
<vxe-column field="notify.mail_to" title="邮件通知">
<template #default="{ row }">
<span v-for="(email, index) in row.notify.mail_to" :key="email + index">[{{ email }}]</span>
</template>
</vxe-column>
<vxe-column field="notify.before_days" title="提前">
<template #default="{ row }">
<span v-if="row.notify.before_days">{{ row.notify.before_days }}</span>
</template>
</vxe-column>
<vxe-column field="notify.notify_at" title="发送时间"></vxe-column>
<vxe-column field="operation" title="操作" width="200px" align="center">
<template #default="{ row }">
<a-space>
<a @click="handleEdit(row)"><a-icon type="edit"/></a>
<a style="color:red;" @click="handleDetele(row.id)"><a-icon type="delete"/></a>
</a-space>
</template>
</vxe-column>
</vxe-table>
<TriggerForm ref="triggerForm" :CITypeId="CITypeId" />
</div>
</template>
<script>
import { getTriggerList, deleteTrigger } from '../../api/CIType'
import { getCITypeAttributesById } from '../../api/CITypeAttr'
import TriggerForm from './triggerForm.vue'
export default {
name: 'TriggerTable',
components: { TriggerForm },
props: {
CITypeId: {
type: Number,
default: null,
},
},
data() {
return {
tips: '主题、内容、微信通知和邮件通知都可以引用该模型的属性值,引用方法为: {{ attr_name }}',
tableData: [],
attrList: [],
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
canAddTriggerAttr() {
return this.attrList.filter((attr) => attr.value_type === '3' || attr.value_type === '4')
},
},
provide() {
return { refresh: this.getTableData }
},
mounted() {},
methods: {
async getTableData() {
const [triggerList, attrList] = await Promise.all([
getTriggerList(this.CITypeId),
getCITypeAttributesById(this.CITypeId),
])
triggerList.forEach((trigger) => {
const _find = attrList.attributes.find((attr) => attr.id === trigger.attr_id)
if (_find) {
trigger.attr_name = _find.alias || _find.name
}
})
this.tableData = triggerList
this.attrList = attrList.attributes
},
handleAddTrigger() {
this.$refs.triggerForm.createFromTriggerTable(this.canAddTriggerAttr)
},
handleDetele(id) {
const that = this
this.$confirm({
title: '警告',
content: '确认删除该触发器吗?',
onOk() {
deleteTrigger(that.CITypeId, id).then(() => {
that.$message.success('删除成功!')
that.getTableData()
})
},
})
},
handleEdit(row) {
const _find = this.attrList.find((attr) => attr.id === row.attr_id)
this.$refs.triggerForm.open({
id: row.attr_id,
alias: _find ? _find.alias || _find.name : '',
trigger: { id: row.id, notify: row.notify },
has_trigger: true,
})
},
},
}
</script>
<style lang="less" scoped>
.ci-types-triggers {
padding: 16px 24px 24px;
.trigger-tips {
border: 1px solid #d4380d;
background-color: #fff2e8;
padding: 2px 10px;
border-radius: 4px;
color: #d4380d;
float: right;
}
}
</style>
<template>
<div class="ci-types-triggers">
<div style="margin-bottom: 10px">
<a-button
type="primary"
@click="handleAddTrigger"
size="small"
class="ops-button-primary"
icon="plus"
>新增触发器</a-button
>
</div>
<vxe-table
stripe
:data="tableData"
size="small"
show-overflow
highlight-hover-row
keep-source
:max-height="windowHeight - 180"
class="ops-stripe-table"
>
<vxe-column field="option.name" title="名称"></vxe-column>
<vxe-column field="option.description" title="备注"></vxe-column>
<vxe-column field="type" title="类型">
<template #default="{ row }">
<span v-if="row.attr_id">日期属性</span>
<span v-else>数据变更</span>
</template>
</vxe-column>
<vxe-column field="option.enable" title="开启">
<template #default="{ row }">
<a-switch :checked="row.option.enable" @click="changeEnable(row)"></a-switch>
</template>
</vxe-column>
<!-- <vxe-column field="attr_name" title="属性名"></vxe-column>
<vxe-column field="option.subject" title="主题"></vxe-column>
<vxe-column field="option.body" title="内容"></vxe-column>
<vxe-column field="option.wx_to" title="微信通知">
<template #default="{ row }">
<span v-for="(person, index) in row.option.wx_to" :key="person + index">[{{ person }}]</span>
</template>
</vxe-column>
<vxe-column field="option.mail_to" title="邮件通知">
<template #default="{ row }">
<span v-for="(email, index) in row.option.mail_to" :key="email + index">[{{ email }}]</span>
</template>
</vxe-column>
<vxe-column field="option.before_days" title="提前">
<template #default="{ row }">
<span v-if="row.option.before_days">{{ row.option.before_days }}</span>
</template>
</vxe-column>
<vxe-column field="option.notify_at" title="发送时间"></vxe-column> -->
<vxe-column field="operation" title="操作" width="80px" align="center">
<template #default="{ row }">
<a-space>
<a @click="handleEdit(row)"><a-icon type="edit"/></a>
<a style="color:red;" @click="handleDetele(row.id)"><a-icon type="delete"/></a>
</a-space>
</template>
</vxe-column>
</vxe-table>
<TriggerForm ref="triggerForm" :CITypeId="CITypeId" />
</div>
</template>
<script>
import _ from 'lodash'
import { getTriggerList, deleteTrigger, updateTrigger } from '../../api/CIType'
import { getCITypeAttributesById } from '../../api/CITypeAttr'
import TriggerForm from './triggerForm.vue'
import { getAllDepAndEmployee } from '@/api/company'
export default {
name: 'TriggerTable',
components: { TriggerForm },
props: {
CITypeId: {
type: Number,
default: null,
},
},
data() {
return {
tableData: [],
attrList: [],
allTreeDepAndEmp: [],
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
},
provide() {
return {
refresh: this.getTableData,
provide_allTreeDepAndEmp: () => {
return this.allTreeDepAndEmp
},
}
},
mounted() {
this.getAllDepAndEmployee()
},
methods: {
getAllDepAndEmployee() {
getAllDepAndEmployee({ block: 0 }).then((res) => {
this.allTreeDepAndEmp = res
})
},
async getTableData() {
const [triggerList, attrList] = await Promise.all([
getTriggerList(this.CITypeId),
getCITypeAttributesById(this.CITypeId),
])
triggerList.forEach((trigger) => {
const _find = attrList.attributes.find((attr) => attr.id === trigger.attr_id)
if (_find) {
trigger.attr_name = _find.alias || _find.name
}
})
this.tableData = triggerList
this.attrList = attrList.attributes
},
handleAddTrigger() {
this.$refs.triggerForm.createFromTriggerTable(this.attrList)
},
handleDetele(id) {
const that = this
this.$confirm({
title: '警告',
content: '确认删除该触发器吗?',
onOk() {
deleteTrigger(that.CITypeId, id).then(() => {
that.$message.success('删除成功!')
that.getTableData()
})
},
})
},
handleEdit(row) {
this.$refs.triggerForm.open(
{
id: row.attr_id,
alias: row?.option?.name ?? '',
trigger: { id: row.id, attr_id: row.attr_id, option: row.option },
has_trigger: true,
},
this.attrList
)
},
changeEnable(row) {
const _row = _.cloneDeep(row)
delete _row.id
const enable = row?.option?.enable ?? true
_row.option.enable = !enable
updateTrigger(this.CITypeId, row.id, _row).then(() => {
this.getTableData()
})
},
},
}
</script>
<style lang="less" scoped>
.ci-types-triggers {
padding: 16px 24px 24px;
}
</style>

View File

@@ -32,19 +32,28 @@
v-if="options.chartType === 'table'"
:span-method="mergeRowMethod"
:border="!options.ret"
:show-header="!!options.ret"
show-overflow
show-header-overflow
>
<template v-if="options.ret">
<vxe-column v-for="col in columns" :key="col" :title="col" :field="col"></vxe-column>
<vxe-column v-for="col in columns" :key="col" :title="col" :field="col">
<template #default="{ row }">
<span>{{ row[col] }}</span>
</template>
</vxe-column>
</template>
<template v-else>
<vxe-column
v-for="(key, index) in Array(keyLength)"
:key="`key${index}`"
:title="`key${index}`"
:title="columnName[index]"
:field="`key${index}`"
></vxe-column>
<vxe-column field="value" title="value"></vxe-column>
>
<template #default="{ row }">
<span>{{ row[`key${index}`] }}</span>
</template>
</vxe-column>
<vxe-column field="value" title="数量"></vxe-column>
</template>
</vxe-table>
<div
@@ -66,6 +75,8 @@ import {
category_2_bar_options,
category_2_pie_options,
} from './chartOptions'
import { getCITypeAttributesByTypeIds } from '../../api/CITypeAttr'
export default {
name: 'Chart',
mixins: [mixin],
@@ -110,6 +121,8 @@ export default {
tableHeight: '',
tableData: [],
keyLength: 0,
attributes: [],
columnName: [],
}
},
computed: {
@@ -149,10 +162,19 @@ export default {
this.tableData = newValue
}
} else {
const _data = []
this.keyLength = this.options?.attr_ids?.length ?? 0
this.formatTableData(_data, this.data, {})
this.tableData = _data
getCITypeAttributesByTypeIds({ type_ids: this.options?.type_ids.join(',') }).then((res) => {
this.attributes = res.attributes
const _data = []
this.keyLength = this.options?.attr_ids?.length ?? 0
const _columnName = []
this.options.attr_ids.forEach((attr) => {
const _find = this.attributes.find((item) => item.id === attr)
_columnName.push(_find?.alias || _find?.name)
})
this.columnName = _columnName
this.formatTableData(_data, this.data, {})
this.tableData = _data
})
}
}
},
@@ -248,15 +270,15 @@ export default {
}
.cmdb-dashboard-grid-item-chart-icon {
> i {
font-size: 4vw;
font-size: 40px;
}
> img {
width: 4vw;
width: 40px;
}
> span {
display: inline-block;
width: 4vw;
height: 4vw;
width: 40px;
height: 40px;
font-size: 50px;
text-align: center;
line-height: 50px;

View File

@@ -82,7 +82,14 @@
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
:filter-option="filterOption"
@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>
@@ -116,7 +123,7 @@
</a-select-opt-group>
</a-select>
</a-form-model-item>
<div class="chart-left-preview">
<div :class="{ 'chart-left-preview': true, 'chart-left-preview-empty': !isShowPreview }">
<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">
@@ -170,6 +177,7 @@
type_ids: form.type_ids,
attr_ids: form.attr_ids,
isShadow: isShadow,
ret: form.tableCategory === 2 ? 'cis' : '',
}"
:editable="false"
:ci_types="ci_types"
@@ -464,10 +472,12 @@ export default {
changeCIType(value) {
this.form.attr_ids = []
this.commonAttributes = []
getCITypeAttributesByTypeIds({ type_ids: Array.isArray(value) ? value.join(',') : value }).then((res) => {
this.attributes = res.attributes
})
if (!Array.isArray(value)) {
if ((Array.isArray(value) && value.length) || (!Array.isArray(value) && value)) {
getCITypeAttributesByTypeIds({ type_ids: Array.isArray(value) ? value.join(',') : value }).then((res) => {
this.attributes = res.attributes
})
}
if (!Array.isArray(value) && value) {
getRecursive_level2children(value).then((res) => {
this.level2children = res
})
@@ -523,6 +533,7 @@ export default {
delete params.attr_ids
delete params.tableCategory
await putCustomDashboard(this.item.id, params)
this.$emit('refresh', this.item.id)
} else {
const { xLast, yLast, wLast } = getLastLayout(this.layout())
const w = this.width
@@ -581,6 +592,9 @@ export default {
// }
// },
changeChartType(t) {
if (!(['bar', 'line', 'pie'].includes(this.chartType) && ['bar', 'line', 'pie'].includes(t.value))) {
this.resetForm()
}
this.chartType = t.value
this.isShowPreview = false
if (t.value === 'count') {
@@ -588,7 +602,6 @@ export default {
} else {
this.form.category = 1
}
this.resetForm()
},
showPreview() {
this.$refs.chartForm.validate(async (valid) => {
@@ -673,6 +686,9 @@ export default {
}
this.form.level = level
},
filterOption(input, option) {
return option.componentOptions.children[0].text.toLowerCase().indexOf(input.toLowerCase()) >= 0
},
},
}
</script>
@@ -702,6 +718,13 @@ export default {
border-radius: 8px;
}
}
.chart-left-preview-empty {
background: url('../../assets/dashboard_empty.png');
background-size: contain;
background-repeat: no-repeat;
background-position-x: center;
background-position-y: center;
}
}
.chart-right {
width: 50%;

View File

@@ -23,6 +23,7 @@ export const category_1_bar_options = (data, options) => {
})
})
return {
color: options.chartColor.split(','),
grid: {
top: 15,
@@ -58,6 +59,7 @@ export const category_1_bar_options = (data, options) => {
data: xData
},
tooltip: {
appendToBody: true,
trigger: 'axis',
axisPointer: {
type: 'shadow'
@@ -65,7 +67,7 @@ export const category_1_bar_options = (data, options) => {
},
series: Object.keys(secondCategory).map(key => {
return {
name: key,
name: options.attr_ids.length === 1 ? '' : key,
type: 'bar',
stack: options?.barStack ?? 'total',
barGap: 0,
@@ -90,6 +92,7 @@ export const category_1_line_options = (data, options) => {
containLabel: true,
},
tooltip: {
appendToBody: true,
trigger: 'axis'
},
xAxis: {
@@ -137,6 +140,7 @@ export const category_1_pie_options = (data, options) => {
containLabel: true,
},
tooltip: {
appendToBody: true,
trigger: 'item'
},
legend: {
@@ -186,6 +190,7 @@ export const category_2_bar_options = (data, options, chartType) => {
containLabel: true,
},
tooltip: {
appendToBody: true,
trigger: 'axis',
axisPointer: {
type: 'shadow'
@@ -257,7 +262,6 @@ export const category_2_bar_options = (data, options, chartType) => {
}
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 => {
@@ -274,6 +278,7 @@ export const category_2_pie_options = (data, options) => {
containLabel: true,
},
tooltip: {
appendToBody: true,
trigger: 'item'
},
legend: {

View File

@@ -11,13 +11,12 @@
<template v-if="layout && layout.length">
<div v-if="editable">
<a-button
:style="{ marginLeft: '22px', marginTop: '20px' }"
:style="{ marginLeft: '22px', marginTop: '20px', backgroundColor: '#D6E9FF', boxShadow: 'none' }"
@click="openChartForm('add', { options: { w: 3 } })"
ghost
type="primary"
size="small"
icon="plus"
>新增</a-button
icon="plus-circle"
class="ops-button-primary"
>新增图表</a-button
>
</div>
<GridLayout
@@ -199,8 +198,14 @@ export default {
console.log(type, item)
this.$refs.chartForm.open(type, item)
},
refresh() {
this.getLayout()
refresh(id) {
if (id) {
setTimeout(() => {
this.$refs[`chart_${id}`][0].resizeChart()
}, 100)
} else {
this.getLayout()
}
},
deleteChart(item) {
const that = this
@@ -299,4 +304,7 @@ export default {
margin-right: 5px;
}
}
.ops-button-primary:hover {
background-color: #2f54eb !important;
}
</style>

View File

@@ -1,38 +1,43 @@
<template>
<div>
<a-card :bordered="false">
<a-tabs default-active-key="1">
<a-tab-pane key="1" tab="CI变更">
<ci-table></ci-table>
</a-tab-pane>
<a-tab-pane key="2" tab="关系变更">
<relation-table></relation-table>
</a-tab-pane>
<a-tab-pane key="3" tab="模型变更">
<type-table></type-table>
</a-tab-pane>
</a-tabs>
</a-card>
</div>
</template>
<script>
import CiTable from './modules/ciTable.vue'
import RelationTable from './modules/relation.vue'
import TypeTable from './modules/typeTable.vue'
export default {
name: 'Index',
data() {
return {
userList: []
}
},
components: {
CiTable,
RelationTable,
TypeTable
}
}
</script>
<style></style>
<template>
<div>
<a-card :bordered="false">
<a-tabs default-active-key="1">
<a-tab-pane key="1" tab="CI变更">
<ci-table></ci-table>
</a-tab-pane>
<a-tab-pane key="2" tab="关系变更">
<relation-table></relation-table>
</a-tab-pane>
<a-tab-pane key="3" tab="模型变更">
<type-table></type-table>
</a-tab-pane>
<a-tab-pane key="4" tab="触发历史">
<TriggerTable></TriggerTable>
</a-tab-pane>
</a-tabs>
</a-card>
</div>
</template>
<script>
import CiTable from './modules/ciTable.vue'
import RelationTable from './modules/relation.vue'
import TypeTable from './modules/typeTable.vue'
import TriggerTable from './modules/triggerTable.vue'
export default {
name: 'OperationHistory',
components: {
CiTable,
RelationTable,
TypeTable,
TriggerTable,
},
data() {
return {
userList: [],
}
},
}
</script>
<style></style>

View File

@@ -1,420 +1,421 @@
<template>
<div>
<search-form
ref="child"
:attrList="ciTableAttrList"
@expandChange="handleExpandChange"
@search="handleSearch"
@searchFormReset="searchFormReset"
@searchFormChange="searchFormChange"
></search-form>
<vxe-table
ref="xTable"
row-id="_XID"
:loading="loading"
border
size="small"
show-overflow="tooltip"
show-header-overflow="tooltip"
resizable
:data="tableData"
:max-height="`${windowHeight - windowHeightMinus}px`"
:span-method="mergeRowMethod"
:scroll-y="{enabled: false}"
>
<vxe-column field="created_at" width="159px" title="操作时间"></vxe-column>
<vxe-column field="user" width="100px" title="用户">
<template #header="{ column }">
<span>{{ column.title }}</span>
<a-popover trigger="click" placement="bottom">
<a-icon class="filter" type="filter" theme="filled"/>
<a slot="content">
<a-input placeholder="输入筛选用户名" size="small" v-model="queryParams.username" style="width: 200px" allowClear/>
<a-button type="link" class="filterButton" @click="filterUser">筛选</a-button>
<a-button type="link" class="filterResetButton" @click="filterUserReset">重置</a-button>
</a>
</a-popover>
</template>
</vxe-column>
<vxe-column field="type_id" width="100px" title="模型"></vxe-column>
<vxe-column field="operate_type" width="89px" title="操作">
<template #header="{ column }">
<span>{{ column.title }}</span>
<a-popover trigger="click" placement="bottom">
<a-icon class="filter" type="filter" theme="filled"/>
<a slot="content">
<a-select
v-model="queryParams.operate_type"
placeholder="选择筛选操作"
show-search
style="width: 200px"
:filter-option="filterOption"
allowClear
>
<a-select-option
:value="Object.values(choice)[0]"
:key="index"
v-for="(choice, index) in ciTableAttrList[4].choice_value"
>
{{ Object.keys(choice)[0] }}
</a-select-option
>
</a-select>
<a-button type="link" class="filterButton" @click="filterOperate">筛选</a-button>
<a-button type="link" class="filterResetButton" @click="filterOperateReset">重置</a-button>
</a>
</a-popover>
</template>
<template #default="{ row }">
<a-tag color="green" v-if="row.operate_type === '新增' ">
{{ row.operate_type }}
</a-tag>
<a-tag color="orange" v-else-if="row.operate_type === '修改' ">
{{ row.operate_type }}
</a-tag>
<a-tag color="red" v-else>
{{ row.operate_type }}
</a-tag>
</template>
</vxe-column>
<vxe-column field="attr_alias" title="属性"></vxe-column>
<vxe-column field="old" title=""></vxe-column>
<vxe-column field="new" title=""></vxe-column>
</vxe-table>
<pager
:current-page.sync="queryParams.page"
:page-size.sync="queryParams.page_size"
:page-sizes="[50,100,200]"
:total="total"
:isLoading="loading"
@change="onChange"
@showSizeChange="onShowSizeChange"
></pager>
</div>
</template>
<script>
import Pager from './pager.vue'
import SearchForm from './searchForm.vue'
import { getCIHistoryTable, getUsers } from '@/modules/cmdb/api/history'
import { getCITypes } from '@/modules/cmdb/api/CIType'
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
export default {
name: 'CiTable',
components: { SearchForm, Pager },
data() {
return {
typeId: undefined,
operateTypeMap: new Map([
['0', '新增'],
['1', '删除'],
['2', '修改'],
]),
loading: true,
typeList: null,
userList: [],
tableData: [],
total: 0,
isExpand: false,
queryParams: {
page: 1,
page_size: 50,
},
ciTableAttrList: [
{
alias: '日期',
is_choice: false,
name: 'datetime',
value_type: '3'
},
{
alias: '用户',
is_choice: true,
name: 'username',
value_type: '2',
choice_value: []
},
{
alias: '模型',
is_choice: true,
name: 'type_id',
value_type: '2',
choice_value: [],
},
{
alias: '属性',
is_choice: true,
name: 'attr_id',
value_type: '2',
choice_value: []
},
{
alias: '操作',
is_choice: true,
name: 'operate_type',
value_type: '2',
choice_value: [
{ '新增': 0 },
{ '删除': 1 },
{ '修改': 2 },
]
},
{
alias: 'CI_ID',
is_choice: false,
name: 'ci_id',
value_type: '2'
}
],
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
windowHeightMinus() {
return this.isExpand ? 396 : 331
}
},
async created() {
this.$watch(
function () {
return this.ciTableAttrList[3].choice_value
},
function () {
delete this.$refs.child.queryParams.attr_id
}
)
await Promise.all([
this.getUserList(),
this.getTypes()
])
await this.getTable(this.queryParams)
},
updated() {
this.$refs.xTable.$el.querySelector('.vxe-table--body-wrapper').scrollTop = 0
},
methods: {
// 获取表格数据
async getTable(queryParams) {
try {
this.loading = true
const res = await getCIHistoryTable(queryParams)
const tempArr = []
res.records.forEach(item => {
item[0].type_id = this.handleTypeId(item[0].type_id)
item[1].forEach((subItem) => {
subItem.operate_type = this.handleOperateType(subItem.operate_type)
const tempObj = Object.assign(subItem, item[0])
tempArr.push(tempObj)
})
})
this.tableData = tempArr
this.total = res.total
} finally {
this.loading = false
}
},
// 获取用户列表
async getUserList() {
const res = await getUsers()
this.userList = res.map(x => {
const username = x.nickname
const obj = {
[username]: username
}
return obj
})
this.ciTableAttrList[1].choice_value = this.userList
},
// 获取模型
async getTypes() {
const res = await getCITypes()
const typesArr = []
const typesMap = new Map()
res.ci_types.forEach(item => {
const tempObj = {}
tempObj[item.alias] = item.id
if (item.alias) {
typesArr.push(tempObj)
typesMap.set(item.id, item.alias)
}
})
this.typeList = typesMap
this.ciTableAttrList[2].choice_value = typesArr
},
// 获取模型对应属性
async getAttrs(type_id) {
if (!type_id) {
this.ciTableAttrList[3].choice_value = []
return
}
const res = await getCITypeAttributesById(type_id)
const attrsArr = []
res.attributes.forEach(item => {
const tempObj = {}
tempObj[item.alias] = item.id
if (item.alias) {
attrsArr.push(tempObj)
}
})
this.ciTableAttrList[3].choice_value = attrsArr
},
onShowSizeChange(size) {
this.queryParams.page_size = size
this.queryParams.page = 1
this.getTable(this.queryParams)
},
onChange(pageNum) {
this.queryParams.page = pageNum
this.getTable(this.queryParams)
},
handleExpandChange(expand) {
this.isExpand = expand
},
// 处理查询
handleSearch(queryParams) {
this.queryParams = queryParams
this.getTable(this.queryParams)
},
// 重置表单
searchFormReset() {
this.queryParams = {
page: 1,
page_size: 50,
start: '',
end: '',
username: '',
ci_id: undefined,
attr_id: undefined,
operate_type: undefined
}
// 将属性options重置
this.ciTableAttrList[3].choice_value = []
this.getTable(this.queryParams)
},
// 转换operate_type
handleOperateType(operate_type) {
return this.operateTypeMap.get(operate_type)
},
// 转换type_id
handleTypeId(type_id) {
return this.typeList.get(type_id) ? this.typeList.get(type_id) : type_id
},
// 表单改变重新获取属性列表
searchFormChange(queryParams) {
if (this.typeId !== queryParams.type_id) {
this.typeId = queryParams.type_id
this.getAttrs(queryParams.type_id)
}
if (queryParams.type_id === undefined) {
this.typeId = undefined
this.$refs.child.queryParams.attr_id = undefined
}
},
// 合并表格
mergeRowMethod ({ row, _rowIndex, column, visibleData }) {
const fields = ['created_at', 'user', 'type_id']
// 单元格值 = [.属性] 确定一格
const cellValue = row[column.property]
const created_at = row['created_at']
// 如果单元格值不为空且作用域包含当前列
if (column.property === 'created_at') {
if (cellValue && fields.includes(column.property)) {
// 前一行
const prevRow = visibleData[_rowIndex - 1]
// 下一行
let nextRow = visibleData[_rowIndex + 1]
// 如果前一行不为空且前一行单元格的值与cellValue相同
if (prevRow && prevRow[column.property] === cellValue) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow[column.property] === cellValue) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
} else if (column.property === 'user') {
if (cellValue && fields.includes(column.property)) {
// 前一行
const prevRow = visibleData[_rowIndex - 1]
// 下一行
let nextRow = visibleData[_rowIndex + 1]
// 如果前一行不为空且前一行单元格的值与cellValue相同
if (prevRow && prevRow[column.property] === cellValue && prevRow['created_at'] === created_at) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow[column.property] === cellValue && nextRow['created_at'] === created_at) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
} else if (column.property === 'type_id') {
if (cellValue && fields.includes(column.property)) {
// 前一行
const prevRow = visibleData[_rowIndex - 1]
// 下一行
let nextRow = visibleData[_rowIndex + 1]
// 如果前一行不为空且前一行单元格的值与cellValue相同
if (prevRow && prevRow[column.property] === cellValue && prevRow['created_at'] === created_at) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow[column.property] === cellValue && nextRow['created_at'] === created_at) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
}
},
filterUser() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.getTable(this.queryParams)
},
filterUserReset() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.queryParams.username = ''
this.getTable(this.queryParams)
},
filterOperate() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.getTable(this.queryParams)
},
filterOperateReset() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.queryParams.operate_type = undefined
this.getTable(this.queryParams)
},
filterOption(input, option) {
return (
option.componentOptions.children[0].text.indexOf(input) >= 0
)
}
}
}
</script>
<style lang="less" scoped>
.filter{
margin-left: 10px;
color: #c0c4cc;
cursor: pointer;
&:hover{
color: #606266;
}
}
</style>
<template>
<div>
<search-form
ref="child"
:attrList="ciTableAttrList"
@expandChange="handleExpandChange"
@search="handleSearch"
@searchFormReset="searchFormReset"
@searchFormChange="searchFormChange"
></search-form>
<vxe-table
ref="xTable"
row-id="_XID"
:loading="loading"
border
size="small"
show-overflow="tooltip"
show-header-overflow="tooltip"
resizable
:data="tableData"
:max-height="`${windowHeight - windowHeightMinus}px`"
:span-method="mergeRowMethod"
:scroll-y="{enabled: false}"
class="ops-unstripe-table"
>
<vxe-column field="created_at" width="159px" title="操作时间"></vxe-column>
<vxe-column field="user" width="100px" title="用户">
<template #header="{ column }">
<span>{{ column.title }}</span>
<a-popover trigger="click" placement="bottom">
<a-icon class="filter" type="filter" theme="filled"/>
<a slot="content">
<a-input placeholder="输入筛选用户名" size="small" v-model="queryParams.username" style="width: 200px" allowClear/>
<a-button type="link" class="filterButton" @click="filterUser">筛选</a-button>
<a-button type="link" class="filterResetButton" @click="filterUserReset">重置</a-button>
</a>
</a-popover>
</template>
</vxe-column>
<vxe-column field="type_id" width="100px" title="模型"></vxe-column>
<vxe-column field="operate_type" width="89px" title="操作">
<template #header="{ column }">
<span>{{ column.title }}</span>
<a-popover trigger="click" placement="bottom">
<a-icon class="filter" type="filter" theme="filled"/>
<a slot="content">
<a-select
v-model="queryParams.operate_type"
placeholder="选择筛选操作"
show-search
style="width: 200px"
:filter-option="filterOption"
allowClear
>
<a-select-option
:value="Object.values(choice)[0]"
:key="index"
v-for="(choice, index) in ciTableAttrList[4].choice_value"
>
{{ Object.keys(choice)[0] }}
</a-select-option
>
</a-select>
<a-button type="link" class="filterButton" @click="filterOperate">筛选</a-button>
<a-button type="link" class="filterResetButton" @click="filterOperateReset">重置</a-button>
</a>
</a-popover>
</template>
<template #default="{ row }">
<a-tag color="green" v-if="row.operate_type === '新增' ">
{{ row.operate_type }}
</a-tag>
<a-tag color="orange" v-else-if="row.operate_type === '修改' ">
{{ row.operate_type }}
</a-tag>
<a-tag color="red" v-else>
{{ row.operate_type }}
</a-tag>
</template>
</vxe-column>
<vxe-column field="attr_alias" title="属性"></vxe-column>
<vxe-column field="old" title=""></vxe-column>
<vxe-column field="new" title=""></vxe-column>
</vxe-table>
<pager
:current-page.sync="queryParams.page"
:page-size.sync="queryParams.page_size"
:page-sizes="[50,100,200]"
:total="total"
:isLoading="loading"
@change="onChange"
@showSizeChange="onShowSizeChange"
></pager>
</div>
</template>
<script>
import Pager from './pager.vue'
import SearchForm from './searchForm.vue'
import { getCIHistoryTable, getUsers } from '@/modules/cmdb/api/history'
import { getCITypes } from '@/modules/cmdb/api/CIType'
import { getCITypeAttributesById } from '@/modules/cmdb/api/CITypeAttr'
export default {
name: 'CiTable',
components: { SearchForm, Pager },
data() {
return {
typeId: undefined,
operateTypeMap: new Map([
['0', '新增'],
['1', '删除'],
['2', '修改'],
]),
loading: true,
typeList: null,
userList: [],
tableData: [],
total: 0,
isExpand: false,
queryParams: {
page: 1,
page_size: 50,
},
ciTableAttrList: [
{
alias: '日期',
is_choice: false,
name: 'datetime',
value_type: '3'
},
{
alias: '用户',
is_choice: true,
name: 'username',
value_type: '2',
choice_value: []
},
{
alias: '模型',
is_choice: true,
name: 'type_id',
value_type: '2',
choice_value: [],
},
{
alias: '属性',
is_choice: true,
name: 'attr_id',
value_type: '2',
choice_value: []
},
{
alias: '操作',
is_choice: true,
name: 'operate_type',
value_type: '2',
choice_value: [
{ '新增': 0 },
{ '删除': 1 },
{ '修改': 2 },
]
},
{
alias: 'CI_ID',
is_choice: false,
name: 'ci_id',
value_type: '2'
}
],
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
windowHeightMinus() {
return this.isExpand ? 396 : 331
}
},
async created() {
this.$watch(
function () {
return this.ciTableAttrList[3].choice_value
},
function () {
delete this.$refs.child.queryParams.attr_id
}
)
await Promise.all([
this.getUserList(),
this.getTypes()
])
await this.getTable(this.queryParams)
},
updated() {
this.$refs.xTable.$el.querySelector('.vxe-table--body-wrapper').scrollTop = 0
},
methods: {
// 获取表格数据
async getTable(queryParams) {
try {
this.loading = true
const res = await getCIHistoryTable(queryParams)
const tempArr = []
res.records.forEach(item => {
item[0].type_id = this.handleTypeId(item[0].type_id)
item[1].forEach((subItem) => {
subItem.operate_type = this.handleOperateType(subItem.operate_type)
const tempObj = Object.assign(subItem, item[0])
tempArr.push(tempObj)
})
})
this.tableData = tempArr
this.total = res.total
} finally {
this.loading = false
}
},
// 获取用户列表
async getUserList() {
const res = await getUsers()
this.userList = res.map(x => {
const username = x.nickname
const obj = {
[username]: username
}
return obj
})
this.ciTableAttrList[1].choice_value = this.userList
},
// 获取模型
async getTypes() {
const res = await getCITypes()
const typesArr = []
const typesMap = new Map()
res.ci_types.forEach(item => {
const tempObj = {}
tempObj[item.alias] = item.id
if (item.alias) {
typesArr.push(tempObj)
typesMap.set(item.id, item.alias)
}
})
this.typeList = typesMap
this.ciTableAttrList[2].choice_value = typesArr
},
// 获取模型对应属性
async getAttrs(type_id) {
if (!type_id) {
this.ciTableAttrList[3].choice_value = []
return
}
const res = await getCITypeAttributesById(type_id)
const attrsArr = []
res.attributes.forEach(item => {
const tempObj = {}
tempObj[item.alias] = item.id
if (item.alias) {
attrsArr.push(tempObj)
}
})
this.ciTableAttrList[3].choice_value = attrsArr
},
onShowSizeChange(size) {
this.queryParams.page_size = size
this.queryParams.page = 1
this.getTable(this.queryParams)
},
onChange(pageNum) {
this.queryParams.page = pageNum
this.getTable(this.queryParams)
},
handleExpandChange(expand) {
this.isExpand = expand
},
// 处理查询
handleSearch(queryParams) {
this.queryParams = queryParams
this.getTable(this.queryParams)
},
// 重置表单
searchFormReset() {
this.queryParams = {
page: 1,
page_size: 50,
start: '',
end: '',
username: '',
ci_id: undefined,
attr_id: undefined,
operate_type: undefined
}
// 将属性options重置
this.ciTableAttrList[3].choice_value = []
this.getTable(this.queryParams)
},
// 转换operate_type
handleOperateType(operate_type) {
return this.operateTypeMap.get(operate_type)
},
// 转换type_id
handleTypeId(type_id) {
return this.typeList.get(type_id) ? this.typeList.get(type_id) : type_id
},
// 表单改变重新获取属性列表
searchFormChange(queryParams) {
if (this.typeId !== queryParams.type_id) {
this.typeId = queryParams.type_id
this.getAttrs(queryParams.type_id)
}
if (queryParams.type_id === undefined) {
this.typeId = undefined
this.$refs.child.queryParams.attr_id = undefined
}
},
// 合并表格
mergeRowMethod ({ row, _rowIndex, column, visibleData }) {
const fields = ['created_at', 'user', 'type_id']
// 单元格值 = [.属性] 确定一格
const cellValue = row[column.property]
const created_at = row['created_at']
// 如果单元格值不为空且作用域包含当前列
if (column.property === 'created_at') {
if (cellValue && fields.includes(column.property)) {
// 前一行
const prevRow = visibleData[_rowIndex - 1]
// 下一行
let nextRow = visibleData[_rowIndex + 1]
// 如果前一行不为空且前一行单元格的值与cellValue相同
if (prevRow && prevRow[column.property] === cellValue) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow[column.property] === cellValue) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
} else if (column.property === 'user') {
if (cellValue && fields.includes(column.property)) {
// 前一行
const prevRow = visibleData[_rowIndex - 1]
// 下一行
let nextRow = visibleData[_rowIndex + 1]
// 如果前一行不为空且前一行单元格的值与cellValue相同
if (prevRow && prevRow[column.property] === cellValue && prevRow['created_at'] === created_at) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow[column.property] === cellValue && nextRow['created_at'] === created_at) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
} else if (column.property === 'type_id') {
if (cellValue && fields.includes(column.property)) {
// 前一行
const prevRow = visibleData[_rowIndex - 1]
// 下一行
let nextRow = visibleData[_rowIndex + 1]
// 如果前一行不为空且前一行单元格的值与cellValue相同
if (prevRow && prevRow[column.property] === cellValue && prevRow['created_at'] === created_at) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow[column.property] === cellValue && nextRow['created_at'] === created_at) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
}
},
filterUser() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.getTable(this.queryParams)
},
filterUserReset() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.queryParams.username = ''
this.getTable(this.queryParams)
},
filterOperate() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.getTable(this.queryParams)
},
filterOperateReset() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.queryParams.operate_type = undefined
this.getTable(this.queryParams)
},
filterOption(input, option) {
return (
option.componentOptions.children[0].text.indexOf(input) >= 0
)
}
}
}
</script>
<style lang="less" scoped>
.filter{
margin-left: 10px;
color: #c0c4cc;
cursor: pointer;
&:hover{
color: #606266;
}
}
</style>

View File

@@ -1,116 +1,116 @@
<template>
<div>
<a-row class="row" type="flex" justify="end">
<a-col>
<a-space align="end">
<a-button class="left-button" size="small" :disabled="prevIsDisabled" @click="prevPage"><a-icon type="left" /></a-button>
<a-button class="page-button" size="small" >{{ currentPage }}</a-button>
<a-button class="right-button" size="small" :disabled="nextIsDisabled" @click="nextPage"><a-icon type="right" /></a-button>
<a-dropdown class="dropdown" placement="topCenter" :trigger="['click']" :disabled="dropdownIsDisabled">
<a-menu slot="overlay">
<a-menu-item v-for="(size,index) in pageSizes" :key="index" @click="handleItemClick(size)">
{{ size }}/
</a-menu-item>
</a-menu>
<a-button size="small"> {{ pageSize }}/ <a-icon type="down" /> </a-button>
</a-dropdown>
</a-space>
</a-col>
</a-row>
</div>
</template>
<script>
export default {
props: {
currentPage: {
type: Number,
required: true
},
pageSize: {
type: Number,
required: true
},
pageSizes: {
type: Array,
required: true
},
total: {
type: Number,
required: true
},
isLoading: {
type: Boolean,
required: false
}
},
data() {
return {
dropdownIsDisabled: false,
prevIsDisabled: true,
}
},
computed: {
nextIsDisabled() {
return this.isLoading || this.total < this.pageSize
}
},
watch: {
isLoading: {
immediate: true,
handler: function (val) {
if (val === true) {
this.dropdownIsDisabled = true
this.prevIsDisabled = true
} else {
this.dropdownIsDisabled = false
if (this.currentPage === 1) {
this.prevIsDisabled = true
} else {
this.prevIsDisabled = false
}
}
}
},
currentPage: {
immediate: true,
handler: function (val) {
if (val === 1) {
this.prevIsDisabled = true
}
}
}
},
methods: {
handleItemClick(size) {
this.$emit('showSizeChange', size)
},
nextPage() {
const pageNum = this.currentPage + 1
this.$emit('change', pageNum)
},
prevPage() {
const pageNum = this.currentPage - 1
this.$emit('change', pageNum)
}
}
}
</script>
<style lang="less" scoped>
.row{
margin-top: 5px;
.left-button{
padding: 0;
width: 24px;
}
.right-button{
padding: 0;
width: 24px;
}
.page-button{
padding: 0;
width: 24px;
}
}
</style>
<template>
<div>
<a-row class="row" type="flex" justify="end">
<a-col>
<a-space align="end">
<a-button class="left-button" size="small" :disabled="prevIsDisabled" @click="prevPage"><a-icon type="left" /></a-button>
<a-button class="page-button" size="small" >{{ currentPage }}</a-button>
<a-button class="right-button" size="small" :disabled="nextIsDisabled" @click="nextPage"><a-icon type="right" /></a-button>
<a-dropdown class="dropdown" placement="topCenter" :trigger="['click']" :disabled="dropdownIsDisabled">
<a-menu slot="overlay">
<a-menu-item v-for="(size,index) in pageSizes" :key="index" @click="handleItemClick(size)">
{{ size }}/
</a-menu-item>
</a-menu>
<a-button size="small"> {{ pageSize }}/ <a-icon type="down" /> </a-button>
</a-dropdown>
</a-space>
</a-col>
</a-row>
</div>
</template>
<script>
export default {
props: {
currentPage: {
type: Number,
required: true
},
pageSize: {
type: Number,
required: true
},
pageSizes: {
type: Array,
required: true
},
total: {
type: Number,
required: true
},
isLoading: {
type: Boolean,
required: false
}
},
data() {
return {
dropdownIsDisabled: false,
prevIsDisabled: true,
}
},
computed: {
nextIsDisabled() {
return this.isLoading || this.total < this.pageSize
}
},
watch: {
isLoading: {
immediate: true,
handler: function (val) {
if (val === true) {
this.dropdownIsDisabled = true
this.prevIsDisabled = true
} else {
this.dropdownIsDisabled = false
if (this.currentPage === 1) {
this.prevIsDisabled = true
} else {
this.prevIsDisabled = false
}
}
}
},
currentPage: {
immediate: true,
handler: function (val) {
if (val === 1) {
this.prevIsDisabled = true
}
}
}
},
methods: {
handleItemClick(size) {
this.$emit('showSizeChange', size)
},
nextPage() {
const pageNum = this.currentPage + 1
this.$emit('change', pageNum)
},
prevPage() {
const pageNum = this.currentPage - 1
this.$emit('change', pageNum)
}
}
}
</script>
<style lang="less" scoped>
.row{
margin-top: 5px;
.left-button{
padding: 0;
width: 24px;
}
.right-button{
padding: 0;
width: 24px;
}
.page-button{
padding: 0;
width: 24px;
}
}
</style>

View File

@@ -1,403 +1,404 @@
<template>
<div>
<search-form
:attrList="relationTableAttrList"
@expandChange="handleExpandChange"
@search="handleSearch"
@searchFormReset="searchFormReset"
></search-form>
<vxe-table
ref="xTable"
:loading="loading"
border
size="small"
show-overflow="tooltip"
show-header-overflow="tooltip"
resizable
:data="tableData"
:max-height="`${windowHeight - windowHeightMinus}px`"
row-id="_XID"
:scroll-y="{ enabled: false }"
:span-method="mergeRowMethod"
>
<vxe-column field="created_at" width="159px" title="操作时间"></vxe-column>
<vxe-column field="user" width="100px" title="用户">
<template #header="{ column }">
<span>{{ column.title }}</span>
<a-popover trigger="click" placement="bottom">
<a-icon class="filter" type="filter" theme="filled" />
<a slot="content">
<a-input
placeholder="输入筛选用户名"
size="small"
v-model="queryParams.username"
style="width: 200px"
allowClear
/>
<a-button type="link" class="filterButton" @click="filterUser">筛选</a-button>
<a-button type="link" class="filterResetButton" @click="filterUserReset">重置</a-button>
</a>
</a-popover>
</template>
</vxe-column>
<vxe-column field="operate_type" width="89px" title="操作">
<template #header="{ column }">
<span>{{ column.title }}</span>
<a-popover trigger="click" placement="bottom">
<a-icon class="filter" type="filter" theme="filled" />
<a slot="content">
<a-select
v-model="queryParams.operate_type"
placeholder="选择筛选操作"
show-search
style="width: 200px"
:filter-option="filterOption"
allowClear
>
<a-select-option
:value="Object.values(choice)[0]"
:key="index"
v-for="(choice, index) in relationTableAttrList[4].choice_value"
>
{{ Object.keys(choice)[0] }}
</a-select-option>
</a-select>
<a-button type="link" class="filterButton" @click="filterOperate">筛选</a-button>
<a-button type="link" class="filterResetButton" @click="filterOperateReset">重置</a-button>
</a>
</a-popover>
</template>
<template #default="{ row }">
<a-tag color="green" v-if="row.operate_type.includes('新增')">
{{ row.operate_type }}
</a-tag>
<a-tag color="orange" v-else-if="row.operate_type.includes('修改')">
{{ row.operate_type }}
</a-tag>
<a-tag color="red" v-else>
{{ row.operate_type }}
</a-tag>
</template>
</vxe-column>
<vxe-column field="changeDescription" title="描述">
<template #default="{ row }">
<a-tag v-if="row && row.first">
{{
`${row.first.ci_type_alias}${
row.first.unique_alias && row.first[row.first.unique]
? `${row.first.unique_alias}${row.first[row.first.unique]}`
: ''
}`
}}
</a-tag>
<a-tag v-if="row.changeDescription === '没有修改'">
{{ row.relation_type_id }}
</a-tag>
<template v-else-if="row.operate_type.includes('修改')">
<a-tag :key="index" color="orange" v-for="(tag, index) in row.changeArr">
{{ tag }}
</a-tag>
</template>
<a-tag color="green" v-else-if="row.operate_type.includes('新增')" :style="{ fontWeight: 'bolder' }">
{{ row.relation_type_id }}
</a-tag>
<a-tag color="red" v-else-if="row.operate_type.includes('删除')">
{{ row.relation_type_id }}
</a-tag>
<a-tag v-if="row && row.second">
{{
`${row.second.ci_type_alias}${
row.second.unique_alias && row.second[row.second.unique]
? `${row.second.unique_alias}${row.second[row.second.unique]}`
: ''
}`
}}
</a-tag>
</template>
</vxe-column>
</vxe-table>
<pager
:current-page.sync="queryParams.page"
:page-size.sync="queryParams.page_size"
:page-sizes="[50, 100, 200]"
:total="total"
:isLoading="loading"
@change="onChange"
@showSizeChange="onShowSizeChange"
></pager>
</div>
</template>
<script>
import SearchForm from './searchForm'
import Pager from './pager.vue'
import { getCITypes } from '@/modules/cmdb/api/CIType'
import { getRelationTable, getUsers } from '@/modules/cmdb/api/history'
import { getRelationTypes } from '@/modules/cmdb/api/relationType'
export default {
name: 'RelationTable',
components: { SearchForm, Pager },
data() {
return {
visible: false,
loading: true,
isExpand: false,
tableData: [],
relationTypeList: null,
total: 0,
userList: [],
operateTypeMap: new Map([
['0', '新增'],
['1', '删除'],
['2', '修改'],
]),
queryParams: {
page: 1,
page_size: 50,
start: '',
end: '',
username: '',
first_ci_id: undefined,
second_ci_id: undefined,
operate_type: undefined,
},
relationTableAttrList: [
{
alias: '日期',
is_choice: false,
name: 'datetime',
value_type: '3',
},
{
alias: '用户',
is_choice: true,
name: 'username',
value_type: '2',
choice_value: [],
},
{
alias: 'FirstCI_ID',
is_choice: false,
name: 'first_ci_id',
value_type: '2',
choice_value: [],
},
{
alias: 'SecondCI_ID',
is_choice: false,
name: 'second_ci_id',
value_type: '2',
choice_value: [],
},
{
alias: '操作',
is_choice: true,
name: 'operate_type',
value_type: '2',
choice_value: [{ 新增: 0 }, { 删除: 1 }, { 修改: 2 }],
},
],
}
},
async created() {
await Promise.all([
this.getRelationTypes(),
this.getUserList(),
this.getTypes(),
])
await this.getTable(this.queryParams)
},
updated() {
this.$refs.xTable.$el.querySelector('.vxe-table--body-wrapper').scrollTop = 0
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
windowHeightMinus() {
return this.isExpand ? 396 : 331
},
},
methods: {
// 获取表格数据
async getTable(queryParams) {
try {
this.loading = true
const res = await getRelationTable(queryParams)
const tempArr = []
res.records.forEach((item) => {
item[1].forEach((subItem) => {
subItem.operate_type = this.handleOperateType(subItem.operate_type)
subItem.relation_type_id = this.handleRelationType(subItem.relation_type_id)
subItem.first = res.cis[String(subItem.first_ci_id)]
subItem.second = res.cis[String(subItem.second_ci_id)]
const tempObj = Object.assign(subItem, item[0])
tempArr.push(tempObj)
})
})
this.total = res.total
this.tableData = tempArr
} finally {
this.loading = false
}
},
// 获取用户列表
async getUserList() {
const res = await getUsers()
this.userList = res.map((x) => {
const username = x.nickname
const obj = {
[username]: username,
}
return obj
})
this.relationTableAttrList[1].choice_value = this.userList
},
// 获取模型
async getTypes() {
const res = await getCITypes()
const typesArr = []
res.ci_types.forEach((item) => {
const tempObj = {}
tempObj[item.alias] = item.id
if (item.alias) {
typesArr.push(tempObj)
}
})
this.relationTableAttrList[2].choice_value = typesArr
this.relationTableAttrList[3].choice_value = typesArr
},
// 获取关系
async getRelationTypes() {
const res = await getRelationTypes()
const relationTypeMap = new Map()
res.forEach((item) => {
relationTypeMap.set(item.id, item.name)
})
this.relationTypeList = relationTypeMap
},
onShowSizeChange(size) {
this.queryParams.page_size = size
this.queryParams.page = 1
this.getTable(this.queryParams)
},
onChange(pageNum) {
this.queryParams.page = pageNum
this.getTable(this.queryParams)
},
handleExpandChange(expand) {
this.isExpand = expand
},
// 处理查询
handleSearch(queryParams) {
this.queryParams = queryParams
this.getTable(queryParams)
},
// 重置表单
searchFormReset() {
this.queryParams = {
page: 1,
page_size: 50,
start: '',
end: '',
username: '',
first_ci_id: undefined,
second_ci_id: undefined,
operate_type: undefined,
}
this.getTable(this.queryParams)
},
// 转换operate_type
handleOperateType(operate_type) {
return this.operateTypeMap.get(operate_type)
},
// 转换relation_type_id
handleRelationType(relation_type_id) {
return this.relationTypeList.get(relation_type_id)
},
// 合并表格
mergeRowMethod({ row, _rowIndex, column, visibleData }) {
const fields = ['created_at', 'user']
// 单元格值 = [.属性] 确定一格
const cellValue = row[column.property]
const created_at = row['created_at']
// 如果单元格值不为空且作用域包含当前列
if (column.property === 'created_at') {
if (cellValue && fields.includes(column.property)) {
// 前一行
const prevRow = visibleData[_rowIndex - 1]
// 下一行
let nextRow = visibleData[_rowIndex + 1]
// 如果前一行不为空且前一行单元格的值与cellValue相同
if (prevRow && prevRow[column.property] === cellValue) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow[column.property] === cellValue) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
} else if (column.property === 'user') {
if (cellValue && fields.includes(column.property)) {
// 前一行
const prevRow = visibleData[_rowIndex - 1]
// 下一行
let nextRow = visibleData[_rowIndex + 1]
// 如果前一行不为空且前一行单元格的值与cellValue相同
if (prevRow && prevRow[column.property] === cellValue && prevRow['created_at'] === created_at) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow[column.property] === cellValue && nextRow['created_at'] === created_at) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
}
},
filterUser() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.getTable(this.queryParams)
},
filterUserReset() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.queryParams.username = ''
this.getTable(this.queryParams)
},
filterOperate() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.getTable(this.queryParams)
},
filterOperateReset() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.queryParams.operate_type = undefined
this.getTable(this.queryParams)
},
filterOption(input, option) {
return option.componentOptions.children[0].text.indexOf(input) >= 0
},
},
}
</script>
<style lang="less" scoped>
.filter {
margin-left: 10px;
color: #c0c4cc;
cursor: pointer;
&:hover {
color: #606266;
}
}
</style>
<template>
<div>
<search-form
:attrList="relationTableAttrList"
@expandChange="handleExpandChange"
@search="handleSearch"
@searchFormReset="searchFormReset"
></search-form>
<vxe-table
ref="xTable"
:loading="loading"
size="small"
show-overflow="tooltip"
show-header-overflow="tooltip"
resizable
:data="tableData"
:max-height="`${windowHeight - windowHeightMinus}px`"
row-id="_XID"
:scroll-y="{ enabled: false }"
:span-method="mergeRowMethod"
stripe
class="ops-stripe-table"
>
<vxe-column field="created_at" width="159px" title="操作时间"></vxe-column>
<vxe-column field="user" width="100px" title="用户">
<template #header="{ column }">
<span>{{ column.title }}</span>
<a-popover trigger="click" placement="bottom">
<a-icon class="filter" type="filter" theme="filled" />
<a slot="content">
<a-input
placeholder="输入筛选用户名"
size="small"
v-model="queryParams.username"
style="width: 200px"
allowClear
/>
<a-button type="link" class="filterButton" @click="filterUser">筛选</a-button>
<a-button type="link" class="filterResetButton" @click="filterUserReset">重置</a-button>
</a>
</a-popover>
</template>
</vxe-column>
<vxe-column field="operate_type" width="89px" title="操作">
<template #header="{ column }">
<span>{{ column.title }}</span>
<a-popover trigger="click" placement="bottom">
<a-icon class="filter" type="filter" theme="filled" />
<a slot="content">
<a-select
v-model="queryParams.operate_type"
placeholder="选择筛选操作"
show-search
style="width: 200px"
:filter-option="filterOption"
allowClear
>
<a-select-option
:value="Object.values(choice)[0]"
:key="index"
v-for="(choice, index) in relationTableAttrList[4].choice_value"
>
{{ Object.keys(choice)[0] }}
</a-select-option>
</a-select>
<a-button type="link" class="filterButton" @click="filterOperate">筛选</a-button>
<a-button type="link" class="filterResetButton" @click="filterOperateReset">重置</a-button>
</a>
</a-popover>
</template>
<template #default="{ row }">
<a-tag color="green" v-if="row.operate_type.includes('新增')">
{{ row.operate_type }}
</a-tag>
<a-tag color="orange" v-else-if="row.operate_type.includes('修改')">
{{ row.operate_type }}
</a-tag>
<a-tag color="red" v-else>
{{ row.operate_type }}
</a-tag>
</template>
</vxe-column>
<vxe-column field="changeDescription" title="描述">
<template #default="{ row }">
<a-tag v-if="row && row.first">
{{
`${row.first.ci_type_alias}${
row.first.unique_alias && row.first[row.first.unique]
? `${row.first.unique_alias}${row.first[row.first.unique]}`
: ''
}`
}}
</a-tag>
<a-tag v-if="row.changeDescription === '没有修改'">
{{ row.relation_type_id }}
</a-tag>
<template v-else-if="row.operate_type.includes('修改')">
<a-tag :key="index" color="orange" v-for="(tag, index) in row.changeArr">
{{ tag }}
</a-tag>
</template>
<a-tag color="green" v-else-if="row.operate_type.includes('新增')" :style="{ fontWeight: 'bolder' }">
{{ row.relation_type_id }}
</a-tag>
<a-tag color="red" v-else-if="row.operate_type.includes('删除')">
{{ row.relation_type_id }}
</a-tag>
<a-tag v-if="row && row.second">
{{
`${row.second.ci_type_alias}${
row.second.unique_alias && row.second[row.second.unique]
? `${row.second.unique_alias}${row.second[row.second.unique]}`
: ''
}`
}}
</a-tag>
</template>
</vxe-column>
</vxe-table>
<pager
:current-page.sync="queryParams.page"
:page-size.sync="queryParams.page_size"
:page-sizes="[50, 100, 200]"
:total="total"
:isLoading="loading"
@change="onChange"
@showSizeChange="onShowSizeChange"
></pager>
</div>
</template>
<script>
import SearchForm from './searchForm'
import Pager from './pager.vue'
import { getCITypes } from '@/modules/cmdb/api/CIType'
import { getRelationTable, getUsers } from '@/modules/cmdb/api/history'
import { getRelationTypes } from '@/modules/cmdb/api/relationType'
export default {
name: 'RelationTable',
components: { SearchForm, Pager },
data() {
return {
visible: false,
loading: true,
isExpand: false,
tableData: [],
relationTypeList: null,
total: 0,
userList: [],
operateTypeMap: new Map([
['0', '新增'],
['1', '删除'],
['2', '修改'],
]),
queryParams: {
page: 1,
page_size: 50,
start: '',
end: '',
username: '',
first_ci_id: undefined,
second_ci_id: undefined,
operate_type: undefined,
},
relationTableAttrList: [
{
alias: '日期',
is_choice: false,
name: 'datetime',
value_type: '3',
},
{
alias: '用户',
is_choice: true,
name: 'username',
value_type: '2',
choice_value: [],
},
{
alias: 'FirstCI_ID',
is_choice: false,
name: 'first_ci_id',
value_type: '2',
choice_value: [],
},
{
alias: 'SecondCI_ID',
is_choice: false,
name: 'second_ci_id',
value_type: '2',
choice_value: [],
},
{
alias: '操作',
is_choice: true,
name: 'operate_type',
value_type: '2',
choice_value: [{ 新增: 0 }, { 删除: 1 }, { 修改: 2 }],
},
],
}
},
async created() {
await Promise.all([
this.getRelationTypes(),
this.getUserList(),
this.getTypes(),
])
await this.getTable(this.queryParams)
},
updated() {
this.$refs.xTable.$el.querySelector('.vxe-table--body-wrapper').scrollTop = 0
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
windowHeightMinus() {
return this.isExpand ? 396 : 331
},
},
methods: {
// 获取表格数据
async getTable(queryParams) {
try {
this.loading = true
const res = await getRelationTable(queryParams)
const tempArr = []
res.records.forEach((item) => {
item[1].forEach((subItem) => {
subItem.operate_type = this.handleOperateType(subItem.operate_type)
subItem.relation_type_id = this.handleRelationType(subItem.relation_type_id)
subItem.first = res.cis[String(subItem.first_ci_id)]
subItem.second = res.cis[String(subItem.second_ci_id)]
const tempObj = Object.assign(subItem, item[0])
tempArr.push(tempObj)
})
})
this.total = res.total
this.tableData = tempArr
} finally {
this.loading = false
}
},
// 获取用户列表
async getUserList() {
const res = await getUsers()
this.userList = res.map((x) => {
const username = x.nickname
const obj = {
[username]: username,
}
return obj
})
this.relationTableAttrList[1].choice_value = this.userList
},
// 获取模型
async getTypes() {
const res = await getCITypes()
const typesArr = []
res.ci_types.forEach((item) => {
const tempObj = {}
tempObj[item.alias] = item.id
if (item.alias) {
typesArr.push(tempObj)
}
})
this.relationTableAttrList[2].choice_value = typesArr
this.relationTableAttrList[3].choice_value = typesArr
},
// 获取关系
async getRelationTypes() {
const res = await getRelationTypes()
const relationTypeMap = new Map()
res.forEach((item) => {
relationTypeMap.set(item.id, item.name)
})
this.relationTypeList = relationTypeMap
},
onShowSizeChange(size) {
this.queryParams.page_size = size
this.queryParams.page = 1
this.getTable(this.queryParams)
},
onChange(pageNum) {
this.queryParams.page = pageNum
this.getTable(this.queryParams)
},
handleExpandChange(expand) {
this.isExpand = expand
},
// 处理查询
handleSearch(queryParams) {
this.queryParams = queryParams
this.getTable(queryParams)
},
// 重置表单
searchFormReset() {
this.queryParams = {
page: 1,
page_size: 50,
start: '',
end: '',
username: '',
first_ci_id: undefined,
second_ci_id: undefined,
operate_type: undefined,
}
this.getTable(this.queryParams)
},
// 转换operate_type
handleOperateType(operate_type) {
return this.operateTypeMap.get(operate_type)
},
// 转换relation_type_id
handleRelationType(relation_type_id) {
return this.relationTypeList.get(relation_type_id)
},
// 合并表格
mergeRowMethod({ row, _rowIndex, column, visibleData }) {
const fields = ['created_at', 'user']
// 单元格值 = [.属性] 确定一格
const cellValue = row[column.property]
const created_at = row['created_at']
// 如果单元格值不为空且作用域包含当前列
if (column.property === 'created_at') {
if (cellValue && fields.includes(column.property)) {
// 前一行
const prevRow = visibleData[_rowIndex - 1]
// 下一行
let nextRow = visibleData[_rowIndex + 1]
// 如果前一行不为空且前一行单元格的值与cellValue相同
if (prevRow && prevRow[column.property] === cellValue) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow[column.property] === cellValue) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
} else if (column.property === 'user') {
if (cellValue && fields.includes(column.property)) {
// 前一行
const prevRow = visibleData[_rowIndex - 1]
// 下一行
let nextRow = visibleData[_rowIndex + 1]
// 如果前一行不为空且前一行单元格的值与cellValue相同
if (prevRow && prevRow[column.property] === cellValue && prevRow['created_at'] === created_at) {
return { rowspan: 0, colspan: 0 }
} else {
let countRowspan = 1
while (nextRow && nextRow[column.property] === cellValue && nextRow['created_at'] === created_at) {
nextRow = visibleData[++countRowspan + _rowIndex]
}
if (countRowspan > 1) {
return { rowspan: countRowspan, colspan: 1 }
}
}
}
}
},
filterUser() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.getTable(this.queryParams)
},
filterUserReset() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.queryParams.username = ''
this.getTable(this.queryParams)
},
filterOperate() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.getTable(this.queryParams)
},
filterOperateReset() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.queryParams.operate_type = undefined
this.getTable(this.queryParams)
},
filterOption(input, option) {
return option.componentOptions.children[0].text.indexOf(input) >= 0
},
},
}
</script>
<style lang="less" scoped>
.filter {
margin-left: 10px;
color: #c0c4cc;
cursor: pointer;
&:hover {
color: #606266;
}
}
</style>

View File

@@ -1,191 +1,191 @@
<template>
<div>
<a-form :colon="false">
<a-row :gutter="24">
<a-col
:sm="24"
:md="12"
:lg="12"
:xl="8"
v-for="attr in attrList.slice(0,3)"
:key="attr.name"
>
<a-form-item
:label="attr.alias || attr.name "
:labelCol="{span:4}"
:wrapperCol="{span:20}"
labelAlign="right"
>
<a-select
v-model="queryParams[attr.name]"
placeholder="请选择"
v-if="attr.is_choice"
show-search
:filter-option="filterOption"
allowClear
>
<a-select-option
:value="Object.values(choice)[0]"
v-for="(choice, index) in attr.choice_value"
:key="'Search_' + attr.name + index"
>
{{ Object.keys(choice)[0] }}
</a-select-option>
</a-select>
<a-range-picker
v-model="date"
@change="onChange"
:style="{width:'100%'}"
format="YYYY-MM-DD HH:mm:ss"
:placeholder="['开始时间', '结束时间']"
:show-time="{
hideDisabledOptions: true,
defaultValue: [moment('00:00:00', 'HH:mm:ss'), moment('23:59:59', 'HH:mm:ss')],
}"
v-else-if="valueTypeMap[attr.value_type] == 'date' || valueTypeMap[attr.value_type] == 'datetime'"
/>
<a-input v-model="queryParams[attr.name]" style="width: 100%" allowClear v-else />
</a-form-item>
</a-col>
<template v-if="expand && attrList.length >= 4">
<a-col
:sm="24"
:md="12"
:lg="8"
:xl="8"
:key="'expand_' + item.name"
v-for="item in attrList.slice(3)"
>
<a-form-item
:label="item.alias || item.name"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 20 }"
labelAlign="right"
>
<a-select
v-model="queryParams[item.name]"
placeholder="请选择"
v-if="item.is_choice"
show-search
:filter-option="filterOption"
allowClear
>
<a-select-option
:value="Object.values(choice)[0]"
:key="'Search_' + item.name + index"
v-for="(choice, index) in item.choice_value"
>
{{ Object.keys(choice)[0] }}
</a-select-option
>
</a-select>
<a-range-picker
:style="{width:'100%'}"
@change="onChange"
format="YYYY-MM-DD HH:mm"
:placeholder="['开始时间', '结束时间']"
v-else-if="valueTypeMap[item.value_type] == 'date' || valueTypeMap[item.value_type] == 'datetime'"
:show-time="{
hideDisabledOptions: true,
defaultValue: [moment('00:00:00', 'HH:mm:ss'), moment('23:59:59', 'HH:mm:ss')],
}"
/>
<a-input v-model="queryParams[item.name]" style="width: 100%" allowClear v-else/>
</a-form-item>
</a-col>
</template>
</a-row>
<a-row>
<a-col :span="24" :style="{ textAlign: 'right' , marginBottom: '10px' }">
<a-button type="primary" html-type="submit" @click="handleSearch">
查询
</a-button>
<a-button :style="{ marginLeft: '8px' }" @click="handleReset">
重置
</a-button>
<a :style="{ marginLeft: '8px', fontSize: '12px' }" @click="toggle" v-if="attrList.length >= 4">
{{ expand?'隐藏':'展开' }} <a-icon :type="expand ? 'up' : 'down'" />
</a>
</a-col>
</a-row>
</a-form>
</div>
</template>
<script>
import moment from 'moment'
import { valueTypeMap } from '../../../utils/const'
export default {
name: 'SearchForm',
props: {
attrList: {
type: Array,
required: true
}
},
data() {
return {
valueTypeMap,
expand: false,
queryParams: {
page: 1,
page_size: 50
},
date: undefined
}
},
watch: {
queryParams: {
deep: true,
handler: function (val) {
this.preProcessData()
this.$emit('searchFormChange', val)
}
},
},
methods: {
moment,
handleSearch() {
this.queryParams.page = 1
this.$emit('search', this.queryParams)
},
handleReset() {
this.queryParams = {
page: 1,
page_size: 50
}
this.date = undefined
this.$emit('searchFormReset')
},
toggle() {
this.expand = !this.expand
this.$emit('expandChange', this.expand)
},
onChange(date, dateString) {
this.queryParams.start = dateString[0]
this.queryParams.end = dateString[1]
},
filterOption(input, option) {
return (
option.componentOptions.children[0].text.indexOf(input) >= 0
)
},
preProcessData() {
Object.keys(this.queryParams).forEach(item => {
if (this.queryParams[item] === '' || this.queryParams[item] === undefined) {
delete this.queryParams[item]
}
})
return this.queryParams
},
},
}
</script>
<style>
</style>
<template>
<div>
<a-form :colon="false">
<a-row :gutter="24">
<a-col
:sm="24"
:md="12"
:lg="12"
:xl="8"
v-for="attr in attrList.slice(0,3)"
:key="attr.name"
>
<a-form-item
:label="attr.alias || attr.name "
:labelCol="{span:4}"
:wrapperCol="{span:20}"
labelAlign="right"
>
<a-select
v-model="queryParams[attr.name]"
placeholder="请选择"
v-if="attr.is_choice"
show-search
:filter-option="filterOption"
allowClear
>
<a-select-option
:value="Object.values(choice)[0]"
v-for="(choice, index) in attr.choice_value"
:key="'Search_' + attr.name + index"
>
{{ Object.keys(choice)[0] }}
</a-select-option>
</a-select>
<a-range-picker
v-model="date"
@change="onChange"
:style="{width:'100%'}"
format="YYYY-MM-DD HH:mm:ss"
:placeholder="['开始时间', '结束时间']"
:show-time="{
hideDisabledOptions: true,
defaultValue: [moment('00:00:00', 'HH:mm:ss'), moment('23:59:59', 'HH:mm:ss')],
}"
v-else-if="valueTypeMap[attr.value_type] == 'date' || valueTypeMap[attr.value_type] == 'datetime'"
/>
<a-input v-model="queryParams[attr.name]" style="width: 100%" allowClear v-else />
</a-form-item>
</a-col>
<template v-if="expand && attrList.length >= 4">
<a-col
:sm="24"
:md="12"
:lg="8"
:xl="8"
:key="'expand_' + item.name"
v-for="item in attrList.slice(3)"
>
<a-form-item
:label="item.alias || item.name"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 20 }"
labelAlign="right"
>
<a-select
v-model="queryParams[item.name]"
placeholder="请选择"
v-if="item.is_choice"
show-search
:filter-option="filterOption"
allowClear
>
<a-select-option
:value="Object.values(choice)[0]"
:key="'Search_' + item.name + index"
v-for="(choice, index) in item.choice_value"
>
{{ Object.keys(choice)[0] }}
</a-select-option
>
</a-select>
<a-range-picker
:style="{width:'100%'}"
@change="onChange"
format="YYYY-MM-DD HH:mm"
:placeholder="['开始时间', '结束时间']"
v-else-if="valueTypeMap[item.value_type] == 'date' || valueTypeMap[item.value_type] == 'datetime'"
:show-time="{
hideDisabledOptions: true,
defaultValue: [moment('00:00:00', 'HH:mm:ss'), moment('23:59:59', 'HH:mm:ss')],
}"
/>
<a-input v-model="queryParams[item.name]" style="width: 100%" allowClear v-else/>
</a-form-item>
</a-col>
</template>
</a-row>
<a-row>
<a-col :span="24" :style="{ textAlign: 'right' , marginBottom: '10px' }">
<a-button type="primary" html-type="submit" @click="handleSearch">
查询
</a-button>
<a-button :style="{ marginLeft: '8px' }" @click="handleReset">
重置
</a-button>
<a :style="{ marginLeft: '8px', fontSize: '12px' }" @click="toggle" v-if="attrList.length >= 4">
{{ expand?'隐藏':'展开' }} <a-icon :type="expand ? 'up' : 'down'" />
</a>
</a-col>
</a-row>
</a-form>
</div>
</template>
<script>
import moment from 'moment'
import { valueTypeMap } from '../../../utils/const'
export default {
name: 'SearchForm',
props: {
attrList: {
type: Array,
required: true
}
},
data() {
return {
valueTypeMap,
expand: false,
queryParams: {
page: 1,
page_size: 50
},
date: undefined
}
},
watch: {
queryParams: {
deep: true,
handler: function (val) {
this.preProcessData()
this.$emit('searchFormChange', val)
}
},
},
methods: {
moment,
handleSearch() {
this.queryParams.page = 1
this.$emit('search', this.queryParams)
},
handleReset() {
this.queryParams = {
page: 1,
page_size: 50
}
this.date = undefined
this.$emit('searchFormReset')
},
toggle() {
this.expand = !this.expand
this.$emit('expandChange', this.expand)
},
onChange(date, dateString) {
this.queryParams.start = dateString[0]
this.queryParams.end = dateString[1]
},
filterOption(input, option) {
return (
option.componentOptions.children[0].text.indexOf(input) >= 0
)
},
preProcessData() {
Object.keys(this.queryParams).forEach(item => {
if (this.queryParams[item] === '' || this.queryParams[item] === undefined) {
delete this.queryParams[item]
}
})
return this.queryParams
},
},
}
</script>
<style>
</style>

View File

@@ -0,0 +1,117 @@
<template>
<div>
<vxe-table
show-overflow
show-header-overflow
stripe
size="small"
class="ops-stripe-table"
:data="tableData"
v-bind="ci_id ? { maxHeight: `${windowHeight - 94}px` } : { height: `${windowHeight - 225}px` }"
>
<vxe-column field="trigger_name" title="触发器名称"> </vxe-column>
<vxe-column field="type" title="类型">
<template #default="{ row }">
<span v-if="row.trigger && row.trigger.attr_id">日期属性</span>
<span v-else-if="row.trigger && !row.trigger.attr_id">数据变更</span>
</template>
</vxe-column>
<vxe-column title="事件">
<template #default="{ row }">
<span v-if="row.operate_type === '0'">新增实例</span>
<span v-else-if="row.operate_type === '1'">删除实例</span>
<span v-else-if="row.operate_type === '2'">实例变更</span>
</template>
</vxe-column>
<vxe-column title="动作">
<template #default="{ row }">
<span v-if="row.webhook">Webhook</span>
<span v-else-if="row.notify">通知</span>
</template>
</vxe-column>
<vxe-column title="触发时间">
<template #default="{row}">
{{ row.updated_at || row.created_at }}
</template>
</vxe-column>
</vxe-table>
<div :style="{ textAlign: 'right' }" v-if="!ci_id">
<a-pagination
size="small"
show-size-changer
show-quick-jumper
:page-size-options="pageSizeOptions"
:current="tablePage.currentPage"
:total="tablePage.totalResult"
:show-total="(total, range) => `共 ${total} 条记录`"
:page-size="tablePage.pageSize"
:default-current="1"
@change="pageOrSizeChange"
@showSizeChange="pageOrSizeChange"
>
</a-pagination>
</div>
</div>
</template>
<script>
import { getCiTriggers, getCiTriggersByCiId } from '../../../api/history'
export default {
name: 'TriggerTable',
props: {
ci_id: {
type: Number,
default: null,
},
},
data() {
return {
tableData: [],
tablePage: {
currentPage: 1,
pageSize: 50,
totalResult: 0,
},
pageSizeOptions: ['50', '100', '200'],
}
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
},
mounted() {
this.updateTableData()
},
methods: {
updateTableData(currentPage = 1, pageSize = this.tablePage.pageSize) {
const params = { page: currentPage, page_size: pageSize }
if (this.ci_id) {
getCiTriggersByCiId(this.ci_id, params).then((res) => {
this.tableData = res.items.map((item) => {
return {
...item,
trigger: res.id2trigger[item.trigger_id],
}
})
})
} else {
getCiTriggers(params).then((res) => {
this.tableData = res?.result || []
this.tablePage = {
...this.tablePage,
currentPage: res.page,
pageSize: res.page_size,
totalResult: res.numfound,
}
})
}
},
pageOrSizeChange(currentPage, pageSize) {
this.updateTableData(currentPage, pageSize)
},
},
}
</script>
<style></style>

View File

@@ -1,477 +1,478 @@
<template>
<div>
<search-form
:attrList="typeTableAttrList"
@expandChange="handleExpandChange"
@search="handleSearch"
@searchFormReset="searchFormReset"
></search-form>
<vxe-table
ref="xTable"
:loading="loading"
border
resizable
:data="tableData"
:max-height="`${windowHeight - windowHeightMinus}px`"
row-id="_XID"
size="small"
:row-config="{isHover: true}"
>
<vxe-column field="created_at" width="159px" title="操作时间"></vxe-column>
<vxe-column field="user" width="116px" title="用户"></vxe-column>
<vxe-column field="operate_type" width="135px" title="操作">
<template #header="{ column }">
<span>{{ column.title }}</span>
<a-popover trigger="click" placement="bottom">
<a-icon class="filter" type="filter" theme="filled" />
<a slot="content">
<a-select
v-model="queryParams.operate_type"
placeholder="选择筛选操作"
show-search
style="width: 200px"
:filter-option="filterOption"
allowClear
>
<a-select-option
:value="Object.values(choice)[0]"
:key="index"
v-for="(choice, index) in typeTableAttrList[1].choice_value"
>
{{ Object.keys(choice)[0] }}
</a-select-option>
</a-select>
<a-button type="link" class="filterButton" @click="filterOperate">筛选</a-button>
<a-button type="link" class="filterResetButton" @click="filterOperateReset">重置</a-button>
</a>
</a-popover>
</template>
<template #default="{ row }">
<a-tag color="green" v-if="row.operate_type.includes('新增')">
{{ row.operate_type }}
</a-tag>
<a-tag color="orange" v-else-if="row.operate_type.includes('修改')">
{{ row.operate_type }}
</a-tag>
<a-tag color="red" v-else>
{{ row.operate_type }}
</a-tag>
</template>
</vxe-column>
<vxe-column field="type_id" title="模型" width="150px">
<template #default="{ row }">
{{ row.operate_type === '删除模型' ? row.change.alias : row.type_id }}
</template>
</vxe-column>
<vxe-column field="changeDescription" title="描述">
<template #default="{ row }">
<p style="color:rgba(0, 0, 0, 0.65);" v-if="row.changeDescription === '没有修改'">
{{ row.changeDescription }}
</p>
<template v-else-if="row.operate_type.includes('修改')">
<p :key="index" style="color:#fa8c16;" v-for="(tag, index) in row.changeArr">
{{ tag }}
</p>
</template>
<p class="more-tag" style="color:#52c41a;" v-else-if="row.operate_type.includes('新增')">
{{ row.changeDescription }}
</p>
<p style="color:#f5222d;" v-else-if="row.operate_type.includes('删除')">
{{ row.changeDescription }}
</p>
</template>
</vxe-column>
</vxe-table>
<a-row class="row" type="flex" justify="end">
<a-col>
<a-pagination
size="small"
v-model="current"
:page-size-options="pageSizeOptions"
:total="numfound"
show-size-changer
:page-size="pageSize"
@change="onChange"
@showSizeChange="onShowSizeChange"
:show-total="(total) => `共 ${total} 条记录`"
>
</a-pagination>
</a-col>
</a-row>
</div>
</template>
<script>
import _ from 'lodash'
import SearchForm from './searchForm'
import { getCITypesTable, getUsers } from '@/modules/cmdb/api/history'
import { getCITypes } from '@/modules/cmdb/api/CIType'
import { getRelationTypes } from '@/modules/cmdb/api/relationType'
export default {
name: 'TypeTable',
components: { SearchForm },
data() {
return {
loading: true,
relationTypeList: null,
operateTypeMap: new Map([
['0', '新增模型'],
['1', '修改模型'],
['2', '删除模型'],
['3', '新增属性'],
['4', '修改属性'],
['5', '删除属性'],
['6', '新增触发器'],
['7', '修改触发器'],
['8', '删除触发器'],
['9', '新增联合唯一'],
['10', '修改联合唯一'],
['11', '删除联合唯一'],
['12', '新增关系'],
['13', '删除关系'],
]),
typeList: null,
userList: [],
typeTableAttrList: [
{
alias: '模型',
is_choice: true,
name: 'type_id',
value_type: '2',
choice_value: [],
},
{
alias: '操作',
is_choice: true,
name: 'operate_type',
value_type: '2',
choice_value: [
{ 新增模型: 0 },
{ 修改模型: 1 },
{ 删除模型: 2 },
{ 新增属性: 3 },
{ 修改属性: 4 },
{ 删除属性: 5 },
{ 新增触发器: 6 },
{ 修改触发器: 7 },
{ 删除触发器: 8 },
{ 新增联合唯一: 9 },
{ 修改联合唯一: 10 },
{ 删除联合唯一: 11 },
{ 新增关系: 12 },
{ 删除关系: 13 },
],
},
],
pageSizeOptions: ['50', '100', '200'],
isExpand: false,
current: 1,
pageSize: 50,
total: 0,
numfound: 0,
tableData: [],
queryParams: {
page: 1,
page_size: 50,
type_id: undefined,
operate_type: undefined,
},
}
},
async created() {
await Promise.all([
this.getRelationTypes(),
this.getTypes(),
this.getUserList(),
])
await this.getTable(this.queryParams)
},
updated() {
this.$refs.xTable.$el.querySelector('.vxe-table--body-wrapper').scrollTop = 0
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
windowHeightMinus() {
return this.isExpand ? 396 : 331
},
},
watch: {
current(val) {
this.queryParams.page = val
},
pageSize(val) {
this.queryParams.page_size = val
},
},
methods: {
// 获取表格数据
async getTable(queryParams) {
try {
this.loading = true
const res = await getCITypesTable(queryParams)
res.result.forEach((item) => {
this.handleChangeDescription(item, item.operate_type)
item.operate_type = this.handleOperateType(item.operate_type)
item.type_id = this.handleTypeId(item.type_id)
item.uid = this.handleUID(item.uid)
})
this.tableData = res.result
this.pageSize = res.page_size
this.current = res.page
this.numfound = res.numfound
this.total = res.total
console.log(this.tableData)
} finally {
this.loading = false
}
},
// 获取模型
async getTypes() {
const res = await getCITypes()
const typesArr = []
const typesMap = new Map()
res.ci_types.forEach((item) => {
const tempObj = {}
tempObj[item.alias] = item.id
if (item.alias) {
typesMap.set(item.id, item.alias)
typesArr.push(tempObj)
}
})
this.typeList = typesMap
// 设置模型options选项
this.typeTableAttrList[0].choice_value = typesArr
},
// 获取用户列表
async getUserList() {
const res = await getUsers()
const userListMap = new Map()
res.forEach((item) => {
userListMap.set(item.uid, item.nickname)
})
this.userList = userListMap
},
// 获取关系
async getRelationTypes() {
const res = await getRelationTypes()
const relationTypeMap = new Map()
res.forEach((item) => {
relationTypeMap.set(item.id, item.name)
})
this.relationTypeList = relationTypeMap
},
onChange(current) {
this.current = current
this.getTable(this.queryParams)
},
onShowSizeChange(current, size) {
this.current = 1
this.pageSize = size
this.getTable(this.queryParams)
},
handleExpandChange(expand) {
this.isExpand = expand
},
// 处理查询
handleSearch(queryParams) {
this.current = 1
this.queryParams = queryParams
this.getTable(this.queryParams)
},
// 重置表单
searchFormReset() {
this.queryParams = {
page: 1,
page_size: 50,
type_id: undefined,
operate_type: undefined,
}
this.getTable(this.queryParams)
},
// 转换operate_type
handleOperateType(operate_type) {
return this.operateTypeMap.get(operate_type)
},
// 转换type_id
handleTypeId(type_id) {
return this.typeList.get(type_id) ? this.typeList.get(type_id) : type_id
},
// 转换uid
handleUID(uid) {
return this.userList.get(uid)
},
// 转换relation_type_id
handleRelationType(relation_type_id) {
return this.relationTypeList.get(relation_type_id)
},
// 处理改变描述
handleChangeDescription(item, operate_type) {
switch (operate_type) {
// 新增模型
case '0': {
item.changeDescription = '新增模型:' + item.change.alias
break
}
// 修改模型
case '1': {
item.changeArr = []
for (const key in item.change.old) {
const newVal = item.change.new[key]
const oldVal = item.change.old[key]
if (!_.isEqual(newVal, oldVal) && key !== 'updated_at') {
if (oldVal === null) {
const str = ` [ ${key} : 改为 ${newVal || '""'} ] `
item.changeDescription += str
item.changeArr.push(str)
} else {
const str = ` [ ${key} : ${oldVal || '""'} 改为 ${newVal || '""'} ] `
item.changeDescription += ` [ ${key} : ${oldVal || '""'} 改为 ${newVal || '""'} ] `
item.changeArr.push(str)
}
}
}
if (!item.changeDescription) item.changeDescription = '没有修改'
break
}
// 删除模型
case '2': {
item.changeDescription = `删除模型${item.change.alias}`
break
}
// 新增属性
case '3': {
item.changeDescription = `属性名${item.change.alias}`
break
}
// 修改属性
case '4': {
item.changeArr = []
for (const key in item.change.old) {
if (!_.isEqual(item.change.new[key], item.change.old[key]) && key !== 'updated_at') {
let newStr = item.change.new[key]
let oldStr = item.change.old[key]
if (key === 'choice_value') {
newStr = newStr ? newStr.map((item) => item[0]).join(',') : ''
oldStr = oldStr ? oldStr.map((item) => item[0]).join(',') : ''
}
if (Object.prototype.toString.call(newStr) === '[object Object]') {
newStr = JSON.stringify(newStr)
}
if (Object.prototype.toString.call(oldStr) === '[object Object]') {
oldStr = JSON.stringify(oldStr)
}
const str = `${key} : ${oldStr ? ` ${oldStr || '""'} ` : ''} 改为 ${newStr || '""'}`
item.changeDescription += ` [ ${str} ] `
item.changeArr.push(str)
}
}
if (!item.changeDescription) item.changeDescription = '没有修改'
break
}
// 删除属性
case '5': {
item.changeDescription = `删除${item.change.alias}`
break
}
// 新增触发器
case '6': {
item.changeDescription = `属性ID${item.change.attr_id}提前${item.change.notify.before_days}主题${item.change.notify.subject}\n内容${item.change.notify.body}\n通知时间${item.change.notify.notify_at}`
break
}
// 修改触发器
case '7': {
item.changeArr = []
for (const key in item.change.old.notify) {
const newVal = item.change.new.notify[key]
const oldVal = item.change.old.notify[key]
if (!_.isEqual(newVal, oldVal) && key !== 'updated_at') {
const str = ` [ ${key} : ${oldVal} 改为 ${newVal} ] `
item.changeDescription += str
item.changeArr.push(str)
}
}
if (!item.changeDescription) item.changeDescription = '没有修改'
break
}
// 删除触发器
case '8': {
item.changeDescription = `属性ID${item.change.attr_id}提前${item.change.notify.before_days}主题${item.change.notify.subject}\n内容${item.change.notify.body}\n通知时间${item.change.notify.notify_at}`
break
}
// 新增联合唯一
case '9': {
item.changeDescription = `属性id[${item.change.attr_ids}]`
break
}
// 修改联合唯一
case '10': {
item.changeArr = []
const oldVal = item.change.old.attr_ids
const newVal = item.change.new.attr_ids
const str = `属性id[${oldVal}] -> [${newVal}]`
item.changeDescription = str
item.changeArr.push(str)
break
}
// 删除联合唯一
case '11': {
item.changeDescription = `属性id[${item.change.attr_ids}]`
break
}
// 新增关系
case '12': {
item.changeDescription = `新增${item.change.parent.alias} -> ${this.handleRelationType(
item.change.relation_type_id
)} -> ${item.change.child.alias}`
break
}
// 删除关系
case '13': {
item.changeDescription = `删除${item.change.parent_id.alias} -> ${this.handleRelationType(
item.change.relation_type_id
)} -> ${item.change.child.alias}`
break
}
}
},
filterOperate() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.getTable(this.queryParams)
},
filterOperateReset() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.queryParams.operate_type = undefined
this.getTable(this.queryParams)
},
filterOption(input, option) {
return option.componentOptions.children[0].text.indexOf(input) >= 0
},
},
}
</script>
<style lang="less" scoped>
.row {
margin-top: 5px;
}
.filter {
margin-left: 10px;
color: #c0c4cc;
cursor: pointer;
&:hover {
color: #606266;
}
}
.more-tag {
max-width: 100%;
overflow: hidden;
text-overflow:ellipsis;
}
p {
margin-bottom: 0;
}
</style>
<template>
<div>
<search-form
:attrList="typeTableAttrList"
@expandChange="handleExpandChange"
@search="handleSearch"
@searchFormReset="searchFormReset"
></search-form>
<vxe-table
ref="xTable"
:loading="loading"
resizable
:data="tableData"
:max-height="`${windowHeight - windowHeightMinus}px`"
row-id="_XID"
size="small"
:row-config="{isHover: true}"
stripe
class="ops-stripe-table"
>
<vxe-column field="created_at" width="159px" title="操作时间"></vxe-column>
<vxe-column field="user" width="116px" title="用户"></vxe-column>
<vxe-column field="operate_type" width="135px" title="操作">
<template #header="{ column }">
<span>{{ column.title }}</span>
<a-popover trigger="click" placement="bottom">
<a-icon class="filter" type="filter" theme="filled" />
<a slot="content">
<a-select
v-model="queryParams.operate_type"
placeholder="选择筛选操作"
show-search
style="width: 200px"
:filter-option="filterOption"
allowClear
>
<a-select-option
:value="Object.values(choice)[0]"
:key="index"
v-for="(choice, index) in typeTableAttrList[1].choice_value"
>
{{ Object.keys(choice)[0] }}
</a-select-option>
</a-select>
<a-button type="link" class="filterButton" @click="filterOperate">筛选</a-button>
<a-button type="link" class="filterResetButton" @click="filterOperateReset">重置</a-button>
</a>
</a-popover>
</template>
<template #default="{ row }">
<a-tag color="green" v-if="row.operate_type.includes('新增')">
{{ row.operate_type }}
</a-tag>
<a-tag color="orange" v-else-if="row.operate_type.includes('修改')">
{{ row.operate_type }}
</a-tag>
<a-tag color="red" v-else>
{{ row.operate_type }}
</a-tag>
</template>
</vxe-column>
<vxe-column field="type_id" title="模型" width="150px">
<template #default="{ row }">
{{ row.operate_type === '删除模型' ? row.change.alias : row.type_id}}
</template>
</vxe-column>
<vxe-column field="changeDescription" title="描述">
<template #default="{ row }">
<p style="color:rgba(0, 0, 0, 0.65);" v-if="row.changeDescription === '没有修改'">
{{ row.changeDescription }}
</p>
<template v-else-if="row.operate_type.includes('修改')">
<p :key="index" style="color:#fa8c16;" v-for="(tag, index) in row.changeArr">
{{ tag }}
</p>
</template>
<p class="more-tag" style="color:#52c41a;" v-else-if="row.operate_type.includes('新增')">
{{ row.changeDescription }}
</p>
<p style="color:#f5222d;" v-else-if="row.operate_type.includes('删除')">
{{ row.changeDescription }}
</p>
</template>
</vxe-column>
</vxe-table>
<a-row class="row" type="flex" justify="end">
<a-col>
<a-pagination
size="small"
v-model="current"
:page-size-options="pageSizeOptions"
:total="numfound"
show-size-changer
:page-size="pageSize"
@change="onChange"
@showSizeChange="onShowSizeChange"
:show-total="(total) => `共 ${total} 条记录`"
>
</a-pagination>
</a-col>
</a-row>
</div>
</template>
<script>
import _ from 'lodash'
import SearchForm from './searchForm'
import { getCITypesTable, getUsers } from '@/modules/cmdb/api/history'
import { getCITypes } from '@/modules/cmdb/api/CIType'
import { getRelationTypes } from '@/modules/cmdb/api/relationType'
export default {
name: 'TypeTable',
components: { SearchForm },
data() {
return {
loading: true,
relationTypeList: null,
operateTypeMap: new Map([
['0', '新增模型'],
['1', '修改模型'],
['2', '删除模型'],
['3', '新增属性'],
['4', '修改属性'],
['5', '删除属性'],
['6', '新增触发器'],
['7', '修改触发器'],
['8', '删除触发器'],
['9', '新增联合唯一'],
['10', '修改联合唯一'],
['11', '删除联合唯一'],
['12', '新增关系'],
['13', '删除关系'],
]),
typeList: null,
userList: [],
typeTableAttrList: [
{
alias: '模型',
is_choice: true,
name: 'type_id',
value_type: '2',
choice_value: [],
},
{
alias: '操作',
is_choice: true,
name: 'operate_type',
value_type: '2',
choice_value: [
{ 新增模型: 0 },
{ 修改模型: 1 },
{ 删除模型: 2 },
{ 新增属性: 3 },
{ 修改属性: 4 },
{ 删除属性: 5 },
{ 新增触发器: 6 },
{ 修改触发器: 7 },
{ 删除触发器: 8 },
{ 新增联合唯一: 9 },
{ 修改联合唯一: 10 },
{ 删除联合唯一: 11 },
{ 新增关系: 12 },
{ 删除关系: 13 },
],
},
],
pageSizeOptions: ['50', '100', '200'],
isExpand: false,
current: 1,
pageSize: 50,
total: 0,
numfound: 0,
tableData: [],
queryParams: {
page: 1,
page_size: 50,
type_id: undefined,
operate_type: undefined,
},
}
},
async created() {
await Promise.all([
this.getRelationTypes(),
this.getTypes(),
this.getUserList(),
])
await this.getTable(this.queryParams)
},
updated() {
this.$refs.xTable.$el.querySelector('.vxe-table--body-wrapper').scrollTop = 0
},
computed: {
windowHeight() {
return this.$store.state.windowHeight
},
windowHeightMinus() {
return this.isExpand ? 396 : 335
},
},
watch: {
current(val) {
this.queryParams.page = val
},
pageSize(val) {
this.queryParams.page_size = val
},
},
methods: {
// 获取表格数据
async getTable(queryParams) {
try {
this.loading = true
const res = await getCITypesTable(queryParams)
res.result.forEach((item) => {
this.handleChangeDescription(item, item.operate_type)
item.operate_type = this.handleOperateType(item.operate_type)
item.type_id = this.handleTypeId(item.type_id)
item.uid = this.handleUID(item.uid)
})
this.tableData = res.result
this.pageSize = res.page_size
this.current = res.page
this.numfound = res.numfound
this.total = res.total
console.log(this.tableData)
} finally {
this.loading = false
}
},
// 获取模型
async getTypes() {
const res = await getCITypes()
const typesArr = []
const typesMap = new Map()
res.ci_types.forEach((item) => {
const tempObj = {}
tempObj[item.alias] = item.id
if (item.alias) {
typesMap.set(item.id, item.alias)
typesArr.push(tempObj)
}
})
this.typeList = typesMap
// 设置模型options选项
this.typeTableAttrList[0].choice_value = typesArr
},
// 获取用户列表
async getUserList() {
const res = await getUsers()
const userListMap = new Map()
res.forEach((item) => {
userListMap.set(item.uid, item.nickname)
})
this.userList = userListMap
},
// 获取关系
async getRelationTypes() {
const res = await getRelationTypes()
const relationTypeMap = new Map()
res.forEach((item) => {
relationTypeMap.set(item.id, item.name)
})
this.relationTypeList = relationTypeMap
},
onChange(current) {
this.current = current
this.getTable(this.queryParams)
},
onShowSizeChange(current, size) {
this.current = 1
this.pageSize = size
this.getTable(this.queryParams)
},
handleExpandChange(expand) {
this.isExpand = expand
},
// 处理查询
handleSearch(queryParams) {
this.current = 1
this.queryParams = queryParams
this.getTable(this.queryParams)
},
// 重置表单
searchFormReset() {
this.queryParams = {
page: 1,
page_size: 50,
type_id: undefined,
operate_type: undefined,
}
this.getTable(this.queryParams)
},
// 转换operate_type
handleOperateType(operate_type) {
return this.operateTypeMap.get(operate_type)
},
// 转换type_id
handleTypeId(type_id) {
return this.typeList.get(type_id) ? this.typeList.get(type_id) : type_id
},
// 转换uid
handleUID(uid) {
return this.userList.get(uid)
},
// 转换relation_type_id
handleRelationType(relation_type_id) {
return this.relationTypeList.get(relation_type_id)
},
// 处理改变描述
handleChangeDescription(item, operate_type) {
switch (operate_type) {
// 新增模型
case '0': {
item.changeDescription = '新增模型:' + item.change.alias
break
}
// 修改模型
case '1': {
item.changeArr = []
for (const key in item.change.old) {
const newVal = item.change.new[key]
const oldVal = item.change.old[key]
if (!_.isEqual(newVal, oldVal) && key !== 'updated_at') {
if (oldVal === null) {
const str = ` [ ${key} : 改为 ${newVal || '""'} ] `
item.changeDescription += str
item.changeArr.push(str)
} else {
const str = ` [ ${key} : ${oldVal || '""'} 改为 ${newVal || '""'} ] `
item.changeDescription += ` [ ${key} : ${oldVal || '""'} 改为 ${newVal || '""'} ] `
item.changeArr.push(str)
}
}
}
if (!item.changeDescription) item.changeDescription = '没有修改'
break
}
// 删除模型
case '2': {
item.changeDescription = `删除模型${item.change.alias}`
break
}
// 新增属性
case '3': {
item.changeDescription = `属性名${item.change.alias}`
break
}
// 修改属性
case '4': {
item.changeArr = []
for (const key in item.change.old) {
if (!_.isEqual(item.change.new[key], item.change.old[key]) && key !== 'updated_at') {
let newStr = item.change.new[key]
let oldStr = item.change.old[key]
if (key === 'choice_value') {
newStr = newStr ? newStr.map((item) => item[0]).join(',') : ''
oldStr = oldStr ? oldStr.map((item) => item[0]).join(',') : ''
}
if (Object.prototype.toString.call(newStr) === '[object Object]') {
newStr = JSON.stringify(newStr)
}
if (Object.prototype.toString.call(oldStr) === '[object Object]') {
oldStr = JSON.stringify(oldStr)
}
const str = `${key} : ${oldStr ? ` ${oldStr || '""'} ` : ''} 改为 ${newStr || '""'}`
item.changeDescription += ` [ ${str} ] `
item.changeArr.push(str)
}
}
if (!item.changeDescription) item.changeDescription = '没有修改'
break
}
// 删除属性
case '5': {
item.changeDescription = `删除${item.change.alias}`
break
}
// 新增触发器
case '6': {
item.changeDescription = `属性ID${item.change.attr_id}提前${item.change.option.before_days}主题${item.change.option.subject}\n内容${item.change.option.body}\n通知时间${item.change.option.notify_at}`
break
}
// 修改触发器
case '7': {
item.changeArr = []
for (const key in item.change.old.option) {
const newVal = item.change.new.option[key]
const oldVal = item.change.old.option[key]
if (!_.isEqual(newVal, oldVal) && key !== 'updated_at') {
const str = ` [ ${key} : ${oldVal} 改为 ${newVal} ] `
item.changeDescription += str
item.changeArr.push(str)
}
}
if (!item.changeDescription) item.changeDescription = '没有修改'
break
}
// 删除触发器
case '8': {
item.changeDescription = `属性ID${item.change.attr_id}提前${item.change.option.before_days}主题${item.change.option.subject}\n内容${item.change.option.body}\n通知时间${item.change.option.notify_at}`
break
}
// 新增联合唯一
case '9': {
item.changeDescription = `属性id[${item.change.attr_ids}]`
break
}
// 修改联合唯一
case '10': {
item.changeArr = []
const oldVal = item.change.old.attr_ids
const newVal = item.change.new.attr_ids
const str = `属性id[${oldVal}] -> [${newVal}]`
item.changeDescription = str
item.changeArr.push(str)
break
}
// 删除联合唯一
case '11': {
item.changeDescription = `属性id[${item.change.attr_ids}]`
break
}
// 新增关系
case '12': {
item.changeDescription = `新增${item.change.parent.alias} -> ${this.handleRelationType(
item.change.relation_type_id
)} -> ${item.change.child.alias}`
break
}
// 删除关系
case '13': {
item.changeDescription = `删除${item.change.parent_id.alias} -> ${this.handleRelationType(
item.change.relation_type_id
)} -> ${item.change.child.alias}`
break
}
}
},
filterOperate() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.getTable(this.queryParams)
},
filterOperateReset() {
this.queryParams.page = 1
this.queryParams.page_size = 50
this.queryParams.operate_type = undefined
this.getTable(this.queryParams)
},
filterOption(input, option) {
return option.componentOptions.children[0].text.indexOf(input) >= 0
},
},
}
</script>
<style lang="less" scoped>
.row {
margin-top: 5px;
}
.filter {
margin-left: 10px;
color: #c0c4cc;
cursor: pointer;
&:hover {
color: #606266;
}
}
.more-tag {
max-width: 100%;
overflow: hidden;
text-overflow:ellipsis;
}
p {
margin-bottom: 0;
}
</style>

View File

@@ -94,7 +94,7 @@
<ops-icon type="ops-setting-notice-wx" />
</div>
<div @click="handleBindWx" class="setting-person-bind-button">
{{ form.wx_id ? '重新绑定' : '绑定' }}
{{ form.notice_info && form.notice_info.wechatApp ? '重新绑定' : '绑定' }}
</div>
</a-space>
</a-form-model-item>

View File

@@ -1214,6 +1214,13 @@
dependencies:
regenerator-runtime "^0.13.11"
"@babel/runtime@^7.12.0":
version "7.23.1"
resolved "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.23.1.tgz#72741dc4d413338a91dcb044a86f3c0bc402646d"
integrity sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==
dependencies:
regenerator-runtime "^0.14.0"
"@babel/template@7.0.0-beta.44":
version "7.0.0-beta.44"
resolved "https://mirrors.huaweicloud.com/repository/npm/@babel/template/-/template-7.0.0-beta.44.tgz#f8832f4fdcee5d59bf515e595fc5106c529b394f"
@@ -1723,6 +1730,11 @@
resolved "https://mirrors.huaweicloud.com/repository/npm/@soda/get-current-script/-/get-current-script-1.0.2.tgz#a53515db25d8038374381b73af20bb4f2e508d87"
integrity sha512-T7VNNlYVM1SgQ+VsMYhnDkcGmWhQdL0bDyGm5TlQ3GBXnJscEClUUOKduWTmm2zCnvNLC1hc3JpuXjs/nFOc5w==
"@transloadit/prettier-bytes@0.0.7":
version "0.0.7"
resolved "https://registry.npmmirror.com/@transloadit/prettier-bytes/-/prettier-bytes-0.0.7.tgz#cdb5399f445fdd606ed833872fa0cabdbc51686b"
integrity sha512-VeJbUb0wEKbcwaSlj5n+LscBl9IPgLPkHVGBkh00cztv6X4L/TJXK58LzFuBKX7/GAfiGhIwH67YTLTlzvIzBA==
"@types/babel__core@^7.1.0":
version "7.20.1"
resolved "https://mirrors.huaweicloud.com/repository/npm/@types/babel__core/-/babel__core-7.20.1.tgz#916ecea274b0c776fec721e333e55762d3a9614b"
@@ -1805,6 +1817,11 @@
resolved "https://mirrors.huaweicloud.com/repository/npm/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194"
integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==
"@types/event-emitter@^0.3.3":
version "0.3.3"
resolved "https://registry.npmmirror.com/@types/event-emitter/-/event-emitter-0.3.3.tgz#727032a9fc67565f96bbd78b2e2809275c97d7e7"
integrity sha512-UfnOK1pIxO7P+EgPRZXD9jMpimd8QEFcEZ5R67R1UhGbv4zghU5+NE7U8M8G9H5Jc8FI51rqDWQs6FtUfq2e/Q==
"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33":
version "4.17.35"
resolved "https://mirrors.huaweicloud.com/repository/npm/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz#c95dd4424f0d32e525d23812aa8ab8e4d3906c4f"
@@ -2057,6 +2074,49 @@
dependencies:
"@types/yargs-parser" "*"
"@uppy/companion-client@^2.2.2":
version "2.2.2"
resolved "https://registry.npmmirror.com/@uppy/companion-client/-/companion-client-2.2.2.tgz#c70b42fdcca728ef88b3eebf7ee3e2fa04b4923b"
integrity sha512-5mTp2iq97/mYSisMaBtFRry6PTgZA6SIL7LePteOV5x0/DxKfrZW3DEiQERJmYpHzy7k8johpm2gHnEKto56Og==
dependencies:
"@uppy/utils" "^4.1.2"
namespace-emitter "^2.0.1"
"@uppy/core@^2.1.1":
version "2.3.4"
resolved "https://registry.npmmirror.com/@uppy/core/-/core-2.3.4.tgz#260b85b6bf3aa03cdc67da231f8c69cfbfdcc84a"
integrity sha512-iWAqppC8FD8mMVqewavCz+TNaet6HPXitmGXpGGREGrakZ4FeuWytVdrelydzTdXx6vVKkOmI2FLztGg73sENQ==
dependencies:
"@transloadit/prettier-bytes" "0.0.7"
"@uppy/store-default" "^2.1.1"
"@uppy/utils" "^4.1.3"
lodash.throttle "^4.1.1"
mime-match "^1.0.2"
namespace-emitter "^2.0.1"
nanoid "^3.1.25"
preact "^10.5.13"
"@uppy/store-default@^2.1.1":
version "2.1.1"
resolved "https://registry.npmmirror.com/@uppy/store-default/-/store-default-2.1.1.tgz#62a656a099bdaa012306e054d093754cb2d36e3e"
integrity sha512-xnpTxvot2SeAwGwbvmJ899ASk5tYXhmZzD/aCFsXePh/v8rNvR2pKlcQUH7cF/y4baUGq3FHO/daKCok/mpKqQ==
"@uppy/utils@^4.1.2", "@uppy/utils@^4.1.3":
version "4.1.3"
resolved "https://registry.npmmirror.com/@uppy/utils/-/utils-4.1.3.tgz#9d0be6ece4df25f228d30ef40be0f14208258ce3"
integrity sha512-nTuMvwWYobnJcytDO3t+D6IkVq/Qs4Xv3vyoEZ+Iaf8gegZP+rEyoaFT2CK5XLRMienPyqRqNbIfRuFaOWSIFw==
dependencies:
lodash.throttle "^4.1.1"
"@uppy/xhr-upload@^2.0.3":
version "2.1.3"
resolved "https://registry.npmmirror.com/@uppy/xhr-upload/-/xhr-upload-2.1.3.tgz#0d4e355332fe0c6eb372d7731315e04d02aeeb18"
integrity sha512-YWOQ6myBVPs+mhNjfdWsQyMRWUlrDLMoaG7nvf/G6Y3GKZf8AyjFDjvvJ49XWQ+DaZOftGkHmF1uh/DBeGivJQ==
dependencies:
"@uppy/companion-client" "^2.2.2"
"@uppy/utils" "^4.1.2"
nanoid "^3.1.25"
"@vue/babel-helper-vue-jsx-merge-props@^1.4.0":
version "1.4.0"
resolved "https://mirrors.huaweicloud.com/repository/npm/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.4.0.tgz#8d53a1e21347db8edbe54d339902583176de09f2"
@@ -2383,6 +2443,84 @@
resolved "https://mirrors.huaweicloud.com/repository/npm/@vue/web-component-wrapper/-/web-component-wrapper-1.3.0.tgz#b6b40a7625429d2bd7c2281ddba601ed05dc7f1a"
integrity sha512-Iu8Tbg3f+emIIMmI2ycSI8QcEuAUgPTgHwesDU1eKMLE4YC/c/sFbGc70QgMq31ijRftV0R7vCm9co6rldCeOA==
"@wangeditor/basic-modules@^1.1.7":
version "1.1.7"
resolved "https://registry.npmmirror.com/@wangeditor/basic-modules/-/basic-modules-1.1.7.tgz#a9c3ccf4ef53332f29550d59d3676e15f395946f"
integrity sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==
dependencies:
is-url "^1.2.4"
"@wangeditor/code-highlight@^1.0.3":
version "1.0.3"
resolved "https://registry.npmmirror.com/@wangeditor/code-highlight/-/code-highlight-1.0.3.tgz#90256857714d5c0cf83ac475aea64db7bf29a7cd"
integrity sha512-iazHwO14XpCuIWJNTQTikqUhGKyqj+dUNWJ9288Oym9M2xMVHvnsOmDU2sgUDWVy+pOLojReMPgXCsvvNlOOhw==
dependencies:
prismjs "^1.23.0"
"@wangeditor/core@^1.1.19":
version "1.1.19"
resolved "https://registry.npmmirror.com/@wangeditor/core/-/core-1.1.19.tgz#f9155f7fd92d03cb1982405b3b82e54c31f1c2b0"
integrity sha512-KevkB47+7GhVszyYF2pKGKtCSj/YzmClsD03C3zTt+9SR2XWT5T0e3yQqg8baZpcMvkjs1D8Dv4fk8ok/UaS2Q==
dependencies:
"@types/event-emitter" "^0.3.3"
event-emitter "^0.3.5"
html-void-elements "^2.0.0"
i18next "^20.4.0"
scroll-into-view-if-needed "^2.2.28"
slate-history "^0.66.0"
"@wangeditor/editor-for-vue@^1.0.0":
version "1.0.2"
resolved "https://registry.npmmirror.com/@wangeditor/editor-for-vue/-/editor-for-vue-1.0.2.tgz#62674d56354319ff8dcc83db5c62cec4437ee906"
integrity sha512-BOENvAXJVtVXlE2X50AAvjV82YlCUeu5cbeR0cvEQHQjYtiVnJtq7HSoj85r2kTgGouI5OrpJG9BBEjSjUSPyA==
"@wangeditor/editor@^5.1.23":
version "5.1.23"
resolved "https://registry.npmmirror.com/@wangeditor/editor/-/editor-5.1.23.tgz#c9d2007b7cb0ceef6b72692b4ee87b01ee2367b3"
integrity sha512-0RxfeVTuK1tktUaPROnCoFfaHVJpRAIE2zdS0mpP+vq1axVQpLjM8+fCvKzqYIkH0Pg+C+44hJpe3VVroSkEuQ==
dependencies:
"@uppy/core" "^2.1.1"
"@uppy/xhr-upload" "^2.0.3"
"@wangeditor/basic-modules" "^1.1.7"
"@wangeditor/code-highlight" "^1.0.3"
"@wangeditor/core" "^1.1.19"
"@wangeditor/list-module" "^1.0.5"
"@wangeditor/table-module" "^1.1.4"
"@wangeditor/upload-image-module" "^1.0.2"
"@wangeditor/video-module" "^1.1.4"
dom7 "^3.0.0"
is-hotkey "^0.2.0"
lodash.camelcase "^4.3.0"
lodash.clonedeep "^4.5.0"
lodash.debounce "^4.0.8"
lodash.foreach "^4.5.0"
lodash.isequal "^4.5.0"
lodash.throttle "^4.1.1"
lodash.toarray "^4.4.0"
nanoid "^3.2.0"
slate "^0.72.0"
snabbdom "^3.1.0"
"@wangeditor/list-module@^1.0.5":
version "1.0.5"
resolved "https://registry.npmmirror.com/@wangeditor/list-module/-/list-module-1.0.5.tgz#3fc0b167acddf885536b45fa0c127f9c6adaea33"
integrity sha512-uDuYTP6DVhcYf7mF1pTlmNn5jOb4QtcVhYwSSAkyg09zqxI1qBqsfUnveeDeDqIuptSJhkh81cyxi+MF8sEPOQ==
"@wangeditor/table-module@^1.1.4":
version "1.1.4"
resolved "https://registry.npmmirror.com/@wangeditor/table-module/-/table-module-1.1.4.tgz#757d4a5868b2b658041cd323854a4d707c8347e9"
integrity sha512-5saanU9xuEocxaemGdNi9t8MCDSucnykEC6jtuiT72kt+/Hhh4nERYx1J20OPsTCCdVr7hIyQenFD1iSRkIQ6w==
"@wangeditor/upload-image-module@^1.0.2":
version "1.0.2"
resolved "https://registry.npmmirror.com/@wangeditor/upload-image-module/-/upload-image-module-1.0.2.tgz#89e9b9467e10cbc6b11dc5748e08dd23aaebee30"
integrity sha512-z81lk/v71OwPDYeQDxj6cVr81aDP90aFuywb8nPD6eQeECtOymrqRODjpO6VGvCVxVck8nUxBHtbxKtjgcwyiA==
"@wangeditor/video-module@^1.1.4":
version "1.1.4"
resolved "https://registry.npmmirror.com/@wangeditor/video-module/-/video-module-1.1.4.tgz#b9df1b3ab2cd53f678b19b4d927e200774a6f532"
integrity sha512-ZdodDPqKQrgx3IwWu4ZiQmXI8EXZ3hm2/fM6E3t5dB8tCaIGWQZhmqd6P5knfkRAd3z2+YRSRbxOGfoRSp/rLg==
"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5":
version "1.11.6"
resolved "https://mirrors.huaweicloud.com/repository/npm/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24"
@@ -4398,6 +4536,11 @@ compression@^1.7.4:
safe-buffer "5.1.2"
vary "~1.1.2"
compute-scroll-into-view@^1.0.20:
version "1.0.20"
resolved "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz#1768b5522d1172754f5d0c9b02de3af6be506a43"
integrity sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==
concat-map@0.0.1:
version "0.0.1"
resolved "https://mirrors.huaweicloud.com/repository/npm/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@@ -5060,6 +5203,14 @@ d3-voronoi@^1.1.2:
resolved "https://mirrors.huaweicloud.com/repository/npm/d3-voronoi/-/d3-voronoi-1.1.4.tgz#dd3c78d7653d2bb359284ae478645d95944c8297"
integrity sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==
d@1, d@^1.0.1:
version "1.0.1"
resolved "https://registry.npmmirror.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a"
integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==
dependencies:
es5-ext "^0.10.50"
type "^1.0.1"
dagre@^0.8.2, dagre@~0.8.5:
version "0.8.5"
resolved "https://mirrors.huaweicloud.com/repository/npm/dagre/-/dagre-0.8.5.tgz#ba30b0055dac12b6c1fcc247817442777d06afee"
@@ -5401,6 +5552,13 @@ dom-to-image@~2.6.0:
resolved "https://mirrors.huaweicloud.com/repository/npm/dom-to-image/-/dom-to-image-2.6.0.tgz#8a503608088c87b1c22f9034ae032e1898955867"
integrity sha512-Dt0QdaHmLpjURjU7Tnu3AgYSF2LuOmksSGsUcE6ItvJoCWTBEmiMXcqBdNSAm9+QbbwD7JMoVsuuKX6ZVQv1qA==
dom7@^3.0.0:
version "3.0.0"
resolved "https://registry.npmmirror.com/dom7/-/dom7-3.0.0.tgz#b861ce5d67a6becd7aaa3ad02942ff14b1240331"
integrity sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==
dependencies:
ssr-window "^3.0.0-alpha.1"
domain-browser@^1.1.1:
version "1.2.0"
resolved "https://mirrors.huaweicloud.com/repository/npm/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
@@ -5743,6 +5901,32 @@ es-to-primitive@^1.2.1:
is-date-object "^1.0.1"
is-symbol "^1.0.2"
es5-ext@^0.10.35, es5-ext@^0.10.50, es5-ext@~0.10.14:
version "0.10.62"
resolved "https://registry.npmmirror.com/es5-ext/-/es5-ext-0.10.62.tgz#5e6adc19a6da524bf3d1e02bbc8960e5eb49a9a5"
integrity sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==
dependencies:
es6-iterator "^2.0.3"
es6-symbol "^3.1.3"
next-tick "^1.1.0"
es6-iterator@^2.0.3:
version "2.0.3"
resolved "https://registry.npmmirror.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7"
integrity sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==
dependencies:
d "1"
es5-ext "^0.10.35"
es6-symbol "^3.1.1"
es6-symbol@^3.1.1, es6-symbol@^3.1.3:
version "3.1.3"
resolved "https://registry.npmmirror.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18"
integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==
dependencies:
d "^1.0.1"
ext "^1.1.2"
escalade@^3.1.1:
version "3.1.1"
resolved "https://mirrors.huaweicloud.com/repository/npm/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
@@ -6007,6 +6191,14 @@ etag@~1.8.1:
resolved "https://mirrors.huaweicloud.com/repository/npm/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
event-emitter@^0.3.5:
version "0.3.5"
resolved "https://registry.npmmirror.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39"
integrity sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==
dependencies:
d "1"
es5-ext "~0.10.14"
event-pubsub@4.3.0:
version "4.3.0"
resolved "https://mirrors.huaweicloud.com/repository/npm/event-pubsub/-/event-pubsub-4.3.0.tgz#f68d816bc29f1ec02c539dc58c8dd40ce72cb36e"
@@ -6183,6 +6375,13 @@ express@^4.16.3, express@^4.17.1:
utils-merge "1.0.1"
vary "~1.1.2"
ext@^1.1.2:
version "1.7.0"
resolved "https://registry.npmmirror.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f"
integrity sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==
dependencies:
type "^2.7.2"
extend-shallow@^2.0.1:
version "2.0.1"
resolved "https://mirrors.huaweicloud.com/repository/npm/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
@@ -7090,6 +7289,11 @@ html-tags@^3.3.1:
resolved "https://mirrors.huaweicloud.com/repository/npm/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce"
integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==
html-void-elements@^2.0.0:
version "2.0.1"
resolved "https://registry.npmmirror.com/html-void-elements/-/html-void-elements-2.0.1.tgz#29459b8b05c200b6c5ee98743c41b979d577549f"
integrity sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==
html-webpack-plugin@^3.2.0:
version "3.2.0"
resolved "https://mirrors.huaweicloud.com/repository/npm/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz#b01abbd723acaaa7b37b6af4492ebda03d9dd37b"
@@ -7213,6 +7417,13 @@ human-signals@^1.1.1:
resolved "https://mirrors.huaweicloud.com/repository/npm/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
i18next@^20.4.0:
version "20.6.1"
resolved "https://registry.npmmirror.com/i18next/-/i18next-20.6.1.tgz#535e5f6e5baeb685c7d25df70db63bf3cc0aa345"
integrity sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==
dependencies:
"@babel/runtime" "^7.12.0"
iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.24:
version "0.4.24"
resolved "https://mirrors.huaweicloud.com/repository/npm/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
@@ -7262,6 +7473,11 @@ immediate@~3.0.5:
resolved "https://mirrors.huaweicloud.com/repository/npm/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==
immer@^9.0.6:
version "9.0.21"
resolved "https://registry.npmmirror.com/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176"
integrity sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==
import-cwd@^2.0.0:
version "2.1.0"
resolved "https://mirrors.huaweicloud.com/repository/npm/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9"
@@ -7687,6 +7903,11 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
dependencies:
is-extglob "^2.1.1"
is-hotkey@^0.2.0:
version "0.2.0"
resolved "https://registry.npmmirror.com/is-hotkey/-/is-hotkey-0.2.0.tgz#1835a68171a91e5c9460869d96336947c8340cef"
integrity sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==
is-mobile@^2.2.1:
version "2.2.2"
resolved "https://mirrors.huaweicloud.com/repository/npm/is-mobile/-/is-mobile-2.2.2.tgz#f6c9c5d50ee01254ce05e739bdd835f1ed4e9954"
@@ -7769,6 +7990,11 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4:
dependencies:
isobject "^3.0.1"
is-plain-object@^5.0.0:
version "5.0.0"
resolved "https://registry.npmmirror.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344"
integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==
is-posix-bracket@^0.1.0:
version "0.1.1"
resolved "https://mirrors.huaweicloud.com/repository/npm/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4"
@@ -7844,6 +8070,11 @@ is-typedarray@~1.0.0:
resolved "https://mirrors.huaweicloud.com/repository/npm/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==
is-url@^1.2.4:
version "1.2.4"
resolved "https://registry.npmmirror.com/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52"
integrity sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==
is-utf8@^0.2.0:
version "0.2.1"
resolved "https://mirrors.huaweicloud.com/repository/npm/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
@@ -8842,6 +9073,16 @@ locate-path@^5.0.0:
dependencies:
p-locate "^4.1.0"
lodash.camelcase@^4.3.0:
version "4.3.0"
resolved "https://registry.npmmirror.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==
lodash.clonedeep@^4.5.0:
version "4.5.0"
resolved "https://registry.npmmirror.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==
lodash.debounce@^4.0.8:
version "4.0.8"
resolved "https://mirrors.huaweicloud.com/repository/npm/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
@@ -8872,6 +9113,11 @@ lodash.flatten@^4.4.0:
resolved "https://mirrors.huaweicloud.com/repository/npm/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
integrity sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==
lodash.foreach@^4.5.0:
version "4.5.0"
resolved "https://registry.npmmirror.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53"
integrity sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==
lodash.get@^4.4.2:
version "4.4.2"
resolved "https://mirrors.huaweicloud.com/repository/npm/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
@@ -8937,6 +9183,16 @@ lodash.sortby@^4.7.0:
resolved "https://mirrors.huaweicloud.com/repository/npm/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==
lodash.throttle@^4.1.1:
version "4.1.1"
resolved "https://registry.npmmirror.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4"
integrity sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==
lodash.toarray@^4.4.0:
version "4.4.0"
resolved "https://registry.npmmirror.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561"
integrity sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw==
lodash.transform@^4.6.0:
version "4.6.0"
resolved "https://mirrors.huaweicloud.com/repository/npm/lodash.transform/-/lodash.transform-4.6.0.tgz#12306422f63324aed8483d3f38332b5f670547a0"
@@ -9185,6 +9441,13 @@ mime-db@1.52.0, "mime-db@>= 1.43.0 < 2":
resolved "https://mirrors.huaweicloud.com/repository/npm/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
mime-match@^1.0.2:
version "1.0.2"
resolved "https://registry.npmmirror.com/mime-match/-/mime-match-1.0.2.tgz#3f87c31e9af1a5fd485fb9db134428b23bbb7ba8"
integrity sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg==
dependencies:
wildcard "^1.1.0"
mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34:
version "2.1.35"
resolved "https://mirrors.huaweicloud.com/repository/npm/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
@@ -9389,14 +9652,19 @@ mz@^2.4.0:
object-assign "^4.0.1"
thenify-all "^1.0.0"
namespace-emitter@^2.0.1:
version "2.0.1"
resolved "https://registry.npmmirror.com/namespace-emitter/-/namespace-emitter-2.0.1.tgz#978d51361c61313b4e6b8cf6f3853d08dfa2b17c"
integrity sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g==
nan@^2.12.1:
version "2.17.0"
resolved "https://mirrors.huaweicloud.com/repository/npm/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb"
integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==
nanoid@^3.3.6:
nanoid@^3.1.25, nanoid@^3.2.0, nanoid@^3.3.6:
version "3.3.6"
resolved "https://mirrors.huaweicloud.com/repository/npm/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
nanomatch@^1.2.9:
@@ -9441,6 +9709,11 @@ neo-async@^2.5.0, neo-async@^2.6.0, neo-async@^2.6.1, neo-async@^2.6.2:
resolved "https://mirrors.huaweicloud.com/repository/npm/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
next-tick@^1.1.0:
version "1.1.0"
resolved "https://registry.npmmirror.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb"
integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==
nice-try@^1.0.4:
version "1.0.5"
resolved "https://mirrors.huaweicloud.com/repository/npm/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
@@ -10555,6 +10828,11 @@ postcss@^8.4.14:
picocolors "^1.0.0"
source-map-js "^1.0.2"
preact@^10.5.13:
version "10.17.1"
resolved "https://registry.npmmirror.com/preact/-/preact-10.17.1.tgz#0a1b3c658c019e759326b9648c62912cf5c2dde1"
integrity sha512-X9BODrvQ4Ekwv9GURm9AKAGaomqXmip7NQTZgY7gcNmr7XE83adOMJvd3N42id1tMFU7ojiynRsYnY6/BRFxLA==
prelude-ls@~1.1.2:
version "1.1.2"
resolved "https://mirrors.huaweicloud.com/repository/npm/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
@@ -10607,6 +10885,11 @@ printj@~1.1.0:
resolved "https://mirrors.huaweicloud.com/repository/npm/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222"
integrity sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==
prismjs@^1.23.0:
version "1.29.0"
resolved "https://registry.npmmirror.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12"
integrity sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==
process-nextick-args@~2.0.0:
version "2.0.1"
resolved "https://mirrors.huaweicloud.com/repository/npm/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
@@ -10921,6 +11204,11 @@ regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.4:
resolved "https://mirrors.huaweicloud.com/repository/npm/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
regenerator-runtime@^0.14.0:
version "0.14.0"
resolved "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45"
integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==
regenerator-transform@^0.15.1:
version "0.15.1"
resolved "https://mirrors.huaweicloud.com/repository/npm/regenerator-transform/-/regenerator-transform-0.15.1.tgz#f6c4e99fc1b4591f780db2586328e4d9a9d8dc56"
@@ -11355,6 +11643,13 @@ screenfull@^4.2.0:
resolved "https://mirrors.huaweicloud.com/repository/npm/screenfull/-/screenfull-4.2.1.tgz#3245b7bc73d2b7c9a15bd8caaf6965db7cbc7f04"
integrity sha512-PLSp6f5XdhvjCCCO8OjavRfzkSGL3Qmdm7P82bxyU8HDDDBhDV3UckRaYcRa/NDNTYt8YBpzjoLWHUAejmOjLg==
scroll-into-view-if-needed@^2.2.28:
version "2.2.31"
resolved "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz#d3c482959dc483e37962d1521254e3295d0d1587"
integrity sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==
dependencies:
compute-scroll-into-view "^1.0.20"
select-hose@^2.0.0:
version "2.0.0"
resolved "https://mirrors.huaweicloud.com/repository/npm/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
@@ -11571,6 +11866,22 @@ slash@^3.0.0:
resolved "https://mirrors.huaweicloud.com/repository/npm/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
slate-history@^0.66.0:
version "0.66.0"
resolved "https://registry.npmmirror.com/slate-history/-/slate-history-0.66.0.tgz#ac63fddb903098ceb4c944433e3f75fe63acf940"
integrity sha512-6MWpxGQZiMvSINlCbMW43E2YBSVMCMCIwQfBzGssjWw4kb0qfvj0pIdblWNRQZD0hR6WHP+dHHgGSeVdMWzfng==
dependencies:
is-plain-object "^5.0.0"
slate@^0.72.0:
version "0.72.8"
resolved "https://registry.npmmirror.com/slate/-/slate-0.72.8.tgz#5a018edf24e45448655293a68bfbcf563aa5ba81"
integrity sha512-/nJwTswQgnRurpK+bGJFH1oM7naD5qDmHd89JyiKNT2oOKD8marW0QSBtuFnwEbL5aGCS8AmrhXQgNOsn4osAw==
dependencies:
immer "^9.0.6"
is-plain-object "^5.0.0"
tiny-warning "^1.0.3"
slice-ansi@^2.1.0:
version "2.1.0"
resolved "https://mirrors.huaweicloud.com/repository/npm/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636"
@@ -11580,6 +11891,11 @@ slice-ansi@^2.1.0:
astral-regex "^1.0.0"
is-fullwidth-code-point "^2.0.0"
snabbdom@^3.1.0, snabbdom@^3.5.1:
version "3.5.1"
resolved "https://registry.npmmirror.com/snabbdom/-/snabbdom-3.5.1.tgz#25f80ef15b194baea703d9d5441892e369de18e1"
integrity sha512-wHMNIOjkm/YNE5EM3RCbr/+DVgPg6AqQAX1eOxO46zYNvCXjKP5Y865tqQj3EXnaMBjkxmQA5jFuDpDK/dbfiA==
snapdragon-node@^2.0.1:
version "2.1.1"
resolved "https://mirrors.huaweicloud.com/repository/npm/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
@@ -11800,6 +12116,11 @@ sshpk@^1.7.0:
safer-buffer "^2.0.2"
tweetnacl "~0.14.0"
ssr-window@^3.0.0-alpha.1:
version "3.0.0"
resolved "https://registry.npmmirror.com/ssr-window/-/ssr-window-3.0.0.tgz#fd5b82801638943e0cc704c4691801435af7ac37"
integrity sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA==
ssri@^6.0.1:
version "6.0.2"
resolved "https://mirrors.huaweicloud.com/repository/npm/ssri/-/ssri-6.0.2.tgz#157939134f20464e7301ddba3e90ffa8f7728ac5"
@@ -12313,6 +12634,11 @@ tiny-emitter@^2.0.0:
resolved "https://mirrors.huaweicloud.com/repository/npm/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423"
integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==
tiny-warning@^1.0.3:
version "1.0.3"
resolved "https://registry.npmmirror.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
tinycolor2@^1.4.1:
version "1.6.0"
resolved "https://mirrors.huaweicloud.com/repository/npm/tinycolor2/-/tinycolor2-1.6.0.tgz#f98007460169b0263b97072c5ae92484ce02d09e"
@@ -12543,6 +12869,16 @@ type-is@~1.6.18:
media-typer "0.3.0"
mime-types "~2.1.24"
type@^1.0.1:
version "1.2.0"
resolved "https://registry.npmmirror.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0"
integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==
type@^2.7.2:
version "2.7.2"
resolved "https://registry.npmmirror.com/type/-/type-2.7.2.tgz#2376a15a3a28b1efa0f5350dcf72d24df6ef98d0"
integrity sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==
typed-array-length@^1.0.4:
version "1.0.4"
resolved "https://mirrors.huaweicloud.com/repository/npm/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb"
@@ -13413,6 +13749,11 @@ which@^2.0.1:
dependencies:
isexe "^2.0.0"
wildcard@^1.1.0:
version "1.1.2"
resolved "https://registry.npmmirror.com/wildcard/-/wildcard-1.1.2.tgz#a7020453084d8cd2efe70ba9d3696263de1710a5"
integrity sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==
window-size@0.1.0:
version "0.1.0"
resolved "https://mirrors.huaweicloud.com/repository/npm/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"

View File

@@ -30,7 +30,7 @@ services:
- redis
cmdb-api:
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.3.3
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-api:2.3.4
# build:
# context: .
# target: cmdb-api
@@ -48,10 +48,11 @@ services:
gunicorn --workers=3 autoapp:app -b 0.0.0.0:5000 -D
flask cmdb-init-cache
flask cmdb-init-acl
nohup flask cmdb-trigger > trigger.log 2>&1 &
nohup flask cmdb-counter > counter.log 2>&1 &
celery -A celery_worker.celery worker -E -Q one_cmdb_async --concurrency=2 -D
celery -A celery_worker.celery worker -E -Q acl_async --concurrency=2
celery -A celery_worker.celery worker -E -Q one_cmdb_async --autoscale=5,2 --logfile=one_cmdb_async.log -D
celery -A celery_worker.celery worker -E -Q acl_async --logfile=one_acl_async.log --concurrency=2
depends_on:
- cmdb-db
- cmdb-cache
@@ -61,7 +62,7 @@ services:
- cmdb-api
cmdb-ui:
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.3.3
image: registry.cn-hangzhou.aliyuncs.com/veops/cmdb-ui:2.3.4
# build:
# context: .
# target: cmdb-ui

View File

@@ -843,7 +843,7 @@ CREATE TABLE `c_c_t_t` (
`updated_at` datetime DEFAULT NULL,
`id` int(11) NOT NULL AUTO_INCREMENT,
`type_id` int(11) NOT NULL,
`attr_id` int(11) NOT NULL,
`attr_id` int(11) DEFAULT NULL,
`notify` json DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `type_id` (`type_id`),
@@ -854,15 +854,36 @@ CREATE TABLE `c_c_t_t` (
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `c_c_t_t`
-- Table structure for table `c_ci_trigger_histories`
--
LOCK TABLES `c_c_t_t` WRITE;
/*!40000 ALTER TABLE `c_c_t_t` DISABLE KEYS */;
INSERT INTO `c_c_t_t` VALUES (NULL,0,'2023-01-09 14:53:47',NULL,1,4,51,'{\"body\": \"bbb\", \"wx_to\": [], \"subject\": \"aaa\", \"notify_at\": \"08:00\", \"before_days\": 1}');
/*!40000 ALTER TABLE `c_c_t_t` ENABLE KEYS */;
UNLOCK TABLES;
DROP TABLE IF EXISTS `c_ci_trigger_histories`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `c_ci_trigger_histories` (
`deleted_at` datetime DEFAULT NULL,
`deleted` tinyint(1) DEFAULT NULL,
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
`id` int(11) NOT NULL AUTO_INCREMENT,
`operate_type` enum('1','0','2') DEFAULT NULL,
`record_id` int(11) DEFAULT NULL,
`ci_id` int(11) NOT NULL,
`trigger_id` int(11) DEFAULT NULL,
`trigger_name` varchar(64) DEFAULT NULL,
`is_ok` tinyint(1) DEFAULT NULL,
`notify` text,
`webhook` text,
PRIMARY KEY (`id`),
KEY `record_id` (`record_id`),
KEY `trigger_id` (`trigger_id`),
KEY `ix_c_ci_trigger_histories_ci_id` (`ci_id`),
KEY `ix_c_ci_trigger_histories_deleted` (`deleted`),
CONSTRAINT `c_ci_trigger_histories_ibfk_1` FOREIGN KEY (`record_id`) REFERENCES `c_records` (`id`),
CONSTRAINT `c_ci_trigger_histories_ibfk_2` FOREIGN KEY (`trigger_id`) REFERENCES `c_c_t_t` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=19 DEFAULT CHARSET=utf8;
--
-- Table structure for table `c_c_t_u_c`
@@ -2098,6 +2119,7 @@ CREATE TABLE `common_employee` (
`acl_virtual_rid` int(11) DEFAULT NULL COMMENT 'ACL中虚拟角色rid',
`last_login` timestamp NULL DEFAULT NULL COMMENT '上次登录时间',
`block` int(11) DEFAULT NULL COMMENT '锁定状态',
`notice_info` json DEFAULT NULL,
PRIMARY KEY (`employee_id`),
KEY `department_id` (`department_id`),
KEY `ix_common_employee_deleted` (`deleted`),
@@ -2111,7 +2133,7 @@ CREATE TABLE `common_employee` (
LOCK TABLES `common_employee` WRITE;
/*!40000 ALTER TABLE `common_employee` DISABLE KEYS */;
INSERT INTO `common_employee` VALUES (NULL,0,'2023-07-11 16:28:25',NULL,1,'demo@veops.cn','demo','demo','','','','',0,0,46,0,0,'2023-07-11 08:28:24',0),(NULL,0,'2023-07-11 16:34:08',NULL,2,'admin@one-ops.com','admin','admin','','','','',0,0,1,0,0,'2023-07-11 08:34:08',0);
INSERT INTO `common_employee` VALUES (NULL,0,'2023-07-11 16:28:25',NULL,1,'demo@veops.cn','demo','demo','','','','',0,0,46,0,0,'2023-07-11 08:28:24',0, null),(NULL,0,'2023-07-11 16:34:08',NULL,2,'admin@one-ops.com','admin','admin','','','','',0,0,1,0,0,'2023-07-11 08:34:08',0, null);
/*!40000 ALTER TABLE `common_employee` ENABLE KEYS */;
UNLOCK TABLES;