mirror of
https://github.com/veops/cmdb.git
synced 2025-09-03 19:26:56 +08:00
Compare commits
26 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
2e644233bc | ||
|
d9b4082b46 | ||
|
a07f984152 | ||
|
4cab7ef6b0 | ||
|
070c163de6 | ||
|
282a779fb1 | ||
|
cb6b51a84c | ||
|
34bd320e75 | ||
|
1eca5791f6 | ||
|
13b1c9a30c | ||
|
b1a15a85d2 | ||
|
08e5a02caf | ||
|
308827b8fc | ||
|
dc4ccb22b9 | ||
|
c482e7ea43 | ||
|
663c14f763 | ||
|
c6ee227bab | ||
|
cb62cf2410 | ||
|
133f32a6b0 | ||
|
45c48c86fe | ||
|
2321f17dae | ||
|
ddb31a07a2 | ||
|
b474914fbb | ||
|
26099a3d69 | ||
|
9f1b510cb3 | ||
|
61acb2483d |
@@ -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"
|
||||
|
@@ -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()
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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)
|
||||
|
@@ -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):
|
||||
"""
|
||||
|
94
cmdb-api/api/lib/common_setting/notice_config.py
Normal file
94
cmdb-api/api/lib/common_setting/notice_config.py
Normal 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),
|
||||
])
|
@@ -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 {} 找不到记录"
|
||||
|
55
cmdb-api/api/lib/notify.py
Normal file
55
cmdb-api/api/lib/notify.py
Normal 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
109
cmdb-api/api/lib/webhook.py
Normal 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
|
||||
)
|
@@ -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)
|
||||
|
@@ -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)
|
||||
|
@@ -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, {})
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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):
|
||||
|
@@ -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"
|
||||
|
||||
|
@@ -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)
|
||||
|
@@ -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'
|
||||
}
|
||||
|
||||
|
||||
|
71
cmdb-api/api/views/common_setting/notice_config.py
Normal file
71
cmdb-api/api/views/common_setting/notice_config.py
Normal 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)
|
@@ -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
|
||||
|
@@ -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"
|
||||
|
@@ -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",
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
})
|
||||
}
|
||||
|
@@ -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
|
||||
})
|
||||
}
|
||||
|
@@ -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
|
||||
})
|
||||
}
|
||||
|
BIN
cmdb-ui/src/modules/cmdb/assets/dashboard_empty.png
Normal file
BIN
cmdb-ui/src/modules/cmdb/assets/dashboard_empty.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 85 KiB |
@@ -0,0 +1,2 @@
|
||||
import NoticeContent from './index.vue'
|
||||
export default NoticeContent
|
199
cmdb-ui/src/modules/cmdb/components/noticeContent/index.vue
Normal file
199
cmdb-ui/src/modules/cmdb/components/noticeContent/index.vue
Normal 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>
|
144
cmdb-ui/src/modules/cmdb/components/webhook/authorization.vue
Normal file
144
cmdb-ui/src/modules/cmdb/components/webhook/authorization.vue
Normal 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>
|
79
cmdb-ui/src/modules/cmdb/components/webhook/body.vue
Normal file
79
cmdb-ui/src/modules/cmdb/components/webhook/body.vue
Normal 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>
|
101
cmdb-ui/src/modules/cmdb/components/webhook/header.vue
Normal file
101
cmdb-ui/src/modules/cmdb/components/webhook/header.vue
Normal 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>
|
2
cmdb-ui/src/modules/cmdb/components/webhook/index.js
Normal file
2
cmdb-ui/src/modules/cmdb/components/webhook/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
import Webhook from './index.vue'
|
||||
export default Webhook
|
140
cmdb-ui/src/modules/cmdb/components/webhook/index.vue
Normal file
140
cmdb-ui/src/modules/cmdb/components/webhook/index.vue
Normal 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>
|
100
cmdb-ui/src/modules/cmdb/components/webhook/paramaters.vue
Normal file
100
cmdb-ui/src/modules/cmdb/components/webhook/paramaters.vue
Normal 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>
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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;
|
||||
|
@@ -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%;
|
||||
|
@@ -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: {
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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"
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
|
||||
|
Reference in New Issue
Block a user