Compare commits

...

11 Commits

Author SHA1 Message Date
simontigers
082724e7bd fix: ci_type find_path _graph 2024-12-18 16:42:03 +08:00
simontigers
510ea5dc2d fix: code linter 2024-12-18 06:24:38 +00:00
Leo Song
41ce5db1c7 Merge pull request #657 from veops/dev_ui_241217
fix(ui): ci - number type attr default value display error
2024-12-17 15:13:18 +08:00
songlh
c3aab86844 fix(ui): ci - number type attr default value display error 2024-12-17 15:12:44 +08:00
simontigers
d1e40b4e5e Merge pull request #654 from veops/fix_acl_date_joined_column
fix: acl user date_joined timezone
2024-12-16 13:58:46 +08:00
simontigers
ea4ea9d6b6 fix: acl user date_joined timezone 2024-12-16 13:58:20 +08:00
pycook
1c5d2c8e9e fix(api): date trigger 2024-12-13 16:57:24 +08:00
Leo Song
6bd3de8951 Merge pull request #652 from veops/dev_ui_241211
feat: update style
2024-12-11 15:48:23 +08:00
songlh
a0ff3d69cb feat: update style 2024-12-11 15:47:45 +08:00
pycook
fccf5db886 fix(ui): restore ui .env 2024-12-09 20:21:52 +08:00
pycook
95b55d2963 feat(api): set the default expire for redis lock 2024-12-09 19:50:04 +08:00
67 changed files with 1147 additions and 826 deletions

79
cmdb-api/.ruff.toml Normal file
View File

@@ -0,0 +1,79 @@
line-length = 120
cache-dir = ".ruff_cache"
target-version = "py310"
unsafe-fixes = true
show-fixes = true
[lint]
select = [
"E",
"F",
"I",
"TCH",
# W
"W505",
# PT
"PT018",
# SIM
"SIM101",
"SIM114",
# PGH
"PGH004",
# PL
"PLE1142",
# RUF
"RUF100",
# UP
"UP007"
]
preview = true
ignore = ["FURB101"]
[lint.flake8-pytest-style]
mark-parentheses = false
parametrize-names-type = "list"
parametrize-values-row-type = "list"
parametrize-values-type = "tuple"
[lint.flake8-unused-arguments]
ignore-variadic-names = true
[lint.isort]
lines-between-types = 1
order-by-type = true
[lint.per-file-ignores]
"**/api/v1/*.py" = ["TCH"]
"**/model/*.py" = ["TCH003"]
"**/models/__init__.py" = ["F401", "F403"]
"**/tests/*.py" = ["E402"]
"celery_worker.py" = ["F401"]
"api/views/entry.py" = ["I001"]
"migrations/*.py" = ["I001", "E402"]
"*.py" = ["I001"]
"api/views/common_setting/department.py" = ["F841"]
"api/lib/common_setting/upload_file.py" = ["F841"]
"api/lib/common_setting/acl.py" = ["F841"]
"**/__init__.py" = ["F822"]
"api/tasks/*.py" = ["E722"]
"api/views/cmdb/*.py" = ["E722"]
"api/views/acl/*.py" = ["E722"]
"api/lib/secrets/*.py" = ["E722", "F841"]
"api/lib/utils.py" = ["E722", "E731"]
"api/lib/perm/authentication/cas/*" = ["E113", "F841"]
"api/lib/perm/acl/*" = ["E722"]
"api/lib/*" = ["E721", "F722"]
"api/lib/cmdb/*" = ["F722", "E722"]
"api/lib/cmdb/search/ci/es/search.py" = ["F841", "SIM114"]
"api/lib/cmdb/search/ci/db/search.py" = ["F841"]
"api/lib/cmdb/value.py" = ["F841"]
"api/lib/cmdb/history.py" = ["E501"]
"api/commands/common.py" = ["E722"]
"api/commands/click_cmdb.py" = ["E722"]
"api/lib/perm/auth.py" = ["SIM114"]
[format]
preview = true
quote-style = "single"
docstring-code-format = true
skip-magic-trailing-comma = false

View File

@@ -256,10 +256,10 @@ def cmdb_trigger():
trigger2cis[trigger.id] = (trigger, ready_cis) trigger2cis[trigger.id] = (trigger, ready_cis)
else: else:
cur = trigger2cis[trigger.id] cur = trigger2cis[trigger.id]
cur_ci_ids = {i.ci_id for i in cur[1]} cur_ci_ids = {_ci.ci_id for _ci in cur[1]}
trigger2cis[trigger.id] = ( trigger2cis[trigger.id] = (
trigger, cur[1] + [i for i in ready_cis if i.ci_id not in cur_ci_ids trigger, cur[1] + [_ci for _ci in ready_cis if _ci.ci_id not in cur_ci_ids
and i.ci_id not in trigger2completed.get(trigger.id, {})]) and _ci.ci_id not in trigger2completed.get(trigger.id, {})])
for tid in trigger2cis: for tid in trigger2cis:
trigger, cis = trigger2cis[tid] trigger, cis = trigger2cis[tid]
@@ -346,7 +346,7 @@ def cmdb_inner_secrets_init(address):
if valid_address(address): if valid_address(address):
token = current_app.config.get("INNER_TRIGGER_TOKEN", "") if not token else token token = current_app.config.get("INNER_TRIGGER_TOKEN", "") if not token else token
if not token: if not token:
token = click.prompt(f'Enter root token', hide_input=True, confirmation_prompt=False) token = click.prompt('Enter root token', hide_input=True, confirmation_prompt=False)
assert token is not None assert token is not None
resp = requests.post("{}/api/v0.1/secrets/auto_seal".format(address.strip("/")), resp = requests.post("{}/api/v0.1/secrets/auto_seal".format(address.strip("/")),
headers={"Inner-Token": token}) headers={"Inner-Token": token})

View File

@@ -415,7 +415,7 @@ class AttributeManager(object):
db.session.rollback() db.session.rollback()
current_app.logger.error("update attribute error, {0}".format(str(e))) current_app.logger.error("update attribute error, {0}".format(str(e)))
return abort(400, ErrFormat.update_attribute_failed.format(("id=".format(_id)))) return abort(400, ErrFormat.update_attribute_failed.format(("id={}".format(_id))))
new = attr.to_dict() new = attr.to_dict()
if not new['choice_web_hook'] and new['is_choice']: if not new['choice_web_hook'] and new['is_choice']:

View File

@@ -295,7 +295,7 @@ class CIManager(object):
db.session.commit() db.session.commit()
value_table = TableMap(attr_name=attr.name).table value_table = TableMap(attr_name=attr.name).table
with redis_lock.Lock(rd.r, "auto_inc_id_{}".format(attr.name)): with redis_lock.Lock(rd.r, "auto_inc_id_{}".format(attr.name), expire=10):
max_v = value_table.get_by(attr_id=attr.id, only_query=True).order_by( max_v = value_table.get_by(attr_id=attr.id, only_query=True).order_by(
getattr(value_table, 'value').desc()).first() getattr(value_table, 'value').desc()).first()
if max_v is not None: if max_v is not None:
@@ -393,7 +393,7 @@ class CIManager(object):
ci = None ci = None
record_id = None record_id = None
password_dict = {} password_dict = {}
with redis_lock.Lock(rd.r, ci_type.name): with redis_lock.Lock(rd.r, ci_type.name, expire=10):
db.session.commit() db.session.commit()
if (unique_key.default and unique_key.default.get('default') == AttributeDefaultValueEnum.AUTO_INC_ID and if (unique_key.default and unique_key.default.get('default') == AttributeDefaultValueEnum.AUTO_INC_ID and
@@ -550,7 +550,7 @@ class CIManager(object):
limit_attrs = self._valid_ci_for_no_read(ci) if not _is_admin else {} limit_attrs = self._valid_ci_for_no_read(ci) if not _is_admin else {}
record_id = None record_id = None
with redis_lock.Lock(rd.r, ci_type.name): with redis_lock.Lock(rd.r, ci_type.name, expire=10):
db.session.commit() db.session.commit()
self._valid_unique_constraint(ci.type_id, ci_dict, ci_id) self._valid_unique_constraint(ci.type_id, ci_dict, ci_id)
@@ -1268,7 +1268,9 @@ class CIRelationManager(object):
else: else:
type_relation = CITypeRelation.get_by_id(relation_type_id) type_relation = CITypeRelation.get_by_id(relation_type_id)
with redis_lock.Lock(rd.r, "ci_relation_add_{}_{}".format(first_ci.type_id, second_ci.type_id)): with redis_lock.Lock(rd.r,
"ci_relation_add_{}_{}".format(first_ci.type_id, second_ci.type_id),
expire=10):
cls._check_constraint(first_ci_id, first_ci.type_id, second_ci_id, second_ci.type_id, type_relation) cls._check_constraint(first_ci_id, first_ci.type_id, second_ci_id, second_ci.type_id, type_relation)

View File

@@ -862,15 +862,15 @@ class CITypeRelationManager(object):
graph = nx.DiGraph() graph = nx.DiGraph()
def get_children(_id): def get_children(_id, _graph):
children = CITypeRelation.get_by(parent_id=_id, to_dict=False) children = CITypeRelation.get_by(parent_id=_id, to_dict=False)
for i in children: for i in children:
if i.child_id != _id: if i.child_id != _id:
graph.add_edge(i.parent_id, i.child_id) _graph.add_edge(i.parent_id, i.child_id)
get_children(i.child_id) get_children(i.child_id, _graph)
get_children(source_type_id) get_children(source_type_id, graph)
paths = list(nx.all_simple_paths(graph, source_type_id, target_type_ids)) paths = list(nx.all_simple_paths(graph, source_type_id, target_type_ids))

View File

@@ -122,7 +122,7 @@ class RackManager(DCIMBase):
CIManager().update(rack['_id'], **payload) CIManager().update(rack['_id'], **payload)
def add_device(self, rack_id, device_id, u_start, u_count=None): def add_device(self, rack_id, device_id, u_start, u_count=None):
with (redis_lock.Lock(rd.r, "DCIM_RACK_OPERATE_{}".format(rack_id))): with (redis_lock.Lock(rd.r, "DCIM_RACK_OPERATE_{}".format(rack_id), expire=10)):
self.calc_u_free_count(rack_id, device_id, u_start, u_count) self.calc_u_free_count(rack_id, device_id, u_start, u_count)
self.add_relation(rack_id, device_id) self.add_relation(rack_id, device_id)
@@ -139,7 +139,7 @@ class RackManager(DCIMBase):
OperateHistoryManager().add(operate_type=OperateTypeEnum.ADD_DEVICE, rack_id=rack_id, ci_id=device_id) OperateHistoryManager().add(operate_type=OperateTypeEnum.ADD_DEVICE, rack_id=rack_id, ci_id=device_id)
def remove_device(self, rack_id, device_id): def remove_device(self, rack_id, device_id):
with (redis_lock.Lock(rd.r, "DCIM_RACK_OPERATE_{}".format(rack_id))): with (redis_lock.Lock(rd.r, "DCIM_RACK_OPERATE_{}".format(rack_id), expire=10)):
CIRelationManager.delete_3(rack_id, device_id, apply_async=False, valid=False) CIRelationManager.delete_3(rack_id, device_id, apply_async=False, valid=False)
payload = {RackBuiltinAttributes.FREE_U_COUNT: self.calc_u_free_count(rack_id)} payload = {RackBuiltinAttributes.FREE_U_COUNT: self.calc_u_free_count(rack_id)}
@@ -151,7 +151,7 @@ class RackManager(DCIMBase):
OperateHistoryManager().add(operate_type=OperateTypeEnum.REMOVE_DEVICE, rack_id=rack_id, ci_id=device_id) OperateHistoryManager().add(operate_type=OperateTypeEnum.REMOVE_DEVICE, rack_id=rack_id, ci_id=device_id)
def move_device(self, rack_id, device_id, to_u_start): def move_device(self, rack_id, device_id, to_u_start):
with (redis_lock.Lock(rd.r, "DCIM_RACK_OPERATE_{}".format(rack_id))): with (redis_lock.Lock(rd.r, "DCIM_RACK_OPERATE_{}".format(rack_id), expire=10)):
payload = {RackBuiltinAttributes.FREE_U_COUNT: self.calc_u_free_count(rack_id, device_id, to_u_start)} payload = {RackBuiltinAttributes.FREE_U_COUNT: self.calc_u_free_count(rack_id, device_id, to_u_start)}
CIManager().update(rack_id, _sync=True, **payload) CIManager().update(rack_id, _sync=True, **payload)
@@ -160,7 +160,7 @@ class RackManager(DCIMBase):
OperateHistoryManager().add(operate_type=OperateTypeEnum.MOVE_DEVICE, rack_id=rack_id, ci_id=device_id) OperateHistoryManager().add(operate_type=OperateTypeEnum.MOVE_DEVICE, rack_id=rack_id, ci_id=device_id)
def migrate_device(self, rack_id, device_id, to_rack_id, to_u_start): def migrate_device(self, rack_id, device_id, to_rack_id, to_u_start):
with (redis_lock.Lock(rd.r, "DCIM_RACK_OPERATE_{}".format(rack_id))): with (redis_lock.Lock(rd.r, "DCIM_RACK_OPERATE_{}".format(rack_id), expire=10)):
self.calc_u_free_count(to_rack_id, device_id, to_u_start) self.calc_u_free_count(to_rack_id, device_id, to_u_start)
if rack_id != to_rack_id: if rack_id != to_rack_id:

View File

@@ -92,7 +92,7 @@ class IpAddressManager(object):
else: else:
return abort(400, ErrFormat.ipam_address_model_not_found) return abort(400, ErrFormat.ipam_address_model_not_found)
with (redis_lock.Lock(rd.r, "IPAM_ASSIGN_ADDRESS_{}".format(subnet_id))): with (redis_lock.Lock(rd.r, "IPAM_ASSIGN_ADDRESS_{}".format(subnet_id), expire=10)):
cis = self._get_cis(subnet_id, ips) cis = self._get_cis(subnet_id, ips)
ip2ci = {ci[IPAddressBuiltinAttributes.IP]: ci for ci in cis} ip2ci = {ci[IPAddressBuiltinAttributes.IP]: ci for ci in cis}

View File

@@ -163,7 +163,7 @@ class CIFilterPermsCRUD(DBMixin):
def add(self, **kwargs): def add(self, **kwargs):
kwargs = self._can_add(**kwargs) or kwargs kwargs = self._can_add(**kwargs) or kwargs
with redis_lock.Lock(rd.r, 'CMDB_FILTER_{}_{}'.format(kwargs['type_id'], kwargs['rid'])): with redis_lock.Lock(rd.r, 'CMDB_FILTER_{}_{}'.format(kwargs['type_id'], kwargs['rid']), expire=10):
request_id_filter = {} request_id_filter = {}
if kwargs.get('id_filter'): if kwargs.get('id_filter'):
obj = self.cls.get_by(type_id=kwargs.get('type_id'), obj = self.cls.get_by(type_id=kwargs.get('type_id'),
@@ -232,7 +232,7 @@ class CIFilterPermsCRUD(DBMixin):
pass pass
def delete(self, **kwargs): def delete(self, **kwargs):
with redis_lock.Lock(rd.r, 'CMDB_FILTER_{}_{}'.format(kwargs['type_id'], kwargs['rid'])): with redis_lock.Lock(rd.r, 'CMDB_FILTER_{}_{}'.format(kwargs['type_id'], kwargs['rid']), expire=10):
obj = self.cls.get_by(type_id=kwargs.get('type_id'), obj = self.cls.get_by(type_id=kwargs.get('type_id'),
rid=kwargs.get('rid'), rid=kwargs.get('rid'),
id_filter=None, id_filter=None,
@@ -249,7 +249,7 @@ class CIFilterPermsCRUD(DBMixin):
def delete2(self, **kwargs): def delete2(self, **kwargs):
with redis_lock.Lock(rd.r, 'CMDB_FILTER_{}_{}'.format(kwargs['type_id'], kwargs['rid'])): with redis_lock.Lock(rd.r, 'CMDB_FILTER_{}_{}'.format(kwargs['type_id'], kwargs['rid']), expire=10):
obj = self.cls.get_by(type_id=kwargs.get('type_id'), obj = self.cls.get_by(type_id=kwargs.get('type_id'),
rid=kwargs.get('rid'), rid=kwargs.get('rid'),
ci_filter=None, ci_filter=None,

View File

@@ -16,8 +16,9 @@ class ErrFormat(CommonErrFormat):
argument_file_not_found = _l("The file doesn't seem to be uploaded") # 文件似乎并未上传 argument_file_not_found = _l("The file doesn't seem to be uploaded") # 文件似乎并未上传
attribute_not_found = _l("Attribute {} does not exist!") # 属性 {} 不存在! attribute_not_found = _l("Attribute {} does not exist!") # 属性 {} 不存在!
# 该属性是模型的唯一标识,不能被删除!
attribute_is_unique_id = _l( attribute_is_unique_id = _l(
"This attribute is the unique identifier of the model and cannot be deleted!") # 该属性是模型的唯一标识,不能被删除! "This attribute is the unique identifier of the model and cannot be deleted!")
attribute_is_ref_by_type = _l( attribute_is_ref_by_type = _l(
"This attribute is referenced by model {} and cannot be deleted!") # 该属性被模型 {} 引用, 不能删除! "This attribute is referenced by model {} and cannot be deleted!") # 该属性被模型 {} 引用, 不能删除!
attribute_value_type_cannot_change = _l( attribute_value_type_cannot_change = _l(
@@ -129,7 +130,8 @@ class ErrFormat(CommonErrFormat):
adr_default_ref_once = _l("The default auto-discovery rule is already referenced by model {}!") adr_default_ref_once = _l("The default auto-discovery rule is already referenced by model {}!")
# unique_key方法必须返回非空字符串! # unique_key方法必须返回非空字符串!
adr_unique_key_required = _l("The unique_key method must return a non-empty string!") adr_unique_key_required = _l("The unique_key method must return a non-empty string!")
adr_plugin_attributes_list_required = _l("The attributes method must return a list") # attributes方法必须返回的是list # attributes方法必须返回的是list
adr_plugin_attributes_list_required = _l("The attributes method must return a list")
# attributes方法返回的list不能为空! # attributes方法返回的list不能为空!
adr_plugin_attributes_list_no_empty = _l("The list returned by the attributes method cannot be empty!") adr_plugin_attributes_list_no_empty = _l("The list returned by the attributes method cannot be empty!")
# 只有管理员才可以定义执行机器为: 所有节点! # 只有管理员才可以定义执行机器为: 所有节点!

View File

@@ -55,7 +55,6 @@ def str2datetime(x):
return datetime.datetime.strptime(x, "%Y-%m-%d %H:%M") return datetime.datetime.strptime(x, "%Y-%m-%d %H:%M")
class ValueTypeMap(object): class ValueTypeMap(object):
deserialize = { deserialize = {
ValueTypeEnum.INT: string2int, ValueTypeEnum.INT: string2int,

View File

@@ -1,6 +1,6 @@
import functools import functools
from flask import abort, session from flask import abort, session, current_app
from api.lib.common_setting.acl import ACLManager from api.lib.common_setting.acl import ACLManager
from api.lib.common_setting.resp_format import ErrFormat from api.lib.common_setting.resp_format import ErrFormat
from api.lib.perm.acl.acl import is_app_admin from api.lib.perm.acl.acl import is_app_admin
@@ -15,6 +15,7 @@ def perms_role_required(app_name, resource_type_name, resource_name, perm, role_
try: try:
has_perms = acl.role_has_perms(session["acl"]['rid'], resource_name, resource_type_name, perm) has_perms = acl.role_has_perms(session["acl"]['rid'], resource_name, resource_type_name, perm)
except Exception as e: except Exception as e:
current_app.logger.error(f"acl role_has_perms err: {e}")
# resource_type not exist, continue check role # resource_type not exist, continue check role
if role_name: if role_name:
if role_name not in session.get("acl", {}).get("parentRoles", []) and not is_app_admin(app_name): if role_name not in session.get("acl", {}).get("parentRoles", []) and not is_app_admin(app_name):

View File

@@ -476,7 +476,7 @@ class EditDepartmentInACL(object):
for employee in e_list: for employee in e_list:
employee_acl_rid = employee.get('e_acl_rid') employee_acl_rid = employee.get('e_acl_rid')
if employee_acl_rid == 0: if employee_acl_rid == 0:
result.append(f"employee_acl_rid == 0") result.append("employee_acl_rid == 0")
continue continue
cls.remove_single_employee_from_old_department(acl, employee, result) cls.remove_single_employee_from_old_department(acl, employee, result)
@@ -501,8 +501,8 @@ class EditDepartmentInACL(object):
acl.remove_user_from_role(employee.get('e_acl_rid'), payload) acl.remove_user_from_role(employee.get('e_acl_rid'), payload)
current_app.logger.info(f"remove {employee.get('e_acl_rid')} from {d_acl_rid}") current_app.logger.info(f"remove {employee.get('e_acl_rid')} from {d_acl_rid}")
except Exception as e: except Exception as e:
result.append( err = f"remove_user_from_role e_acl_rid: {employee.get('e_acl_rid')}, parent_id: {d_acl_rid}, err: {e}"
f"remove_user_from_role employee_acl_rid: {employee.get('e_acl_rid')}, parent_id: {d_acl_rid}, err: {e}") result.append(err)
return True return True
@@ -548,7 +548,7 @@ class EditDepartmentInACL(object):
for employee in e_list: for employee in e_list:
employee_acl_rid = employee.get('e_acl_rid') employee_acl_rid = employee.get('e_acl_rid')
if employee_acl_rid == 0: if employee_acl_rid == 0:
result.append(f"employee_acl_rid == 0") result.append("employee_acl_rid == 0")
continue continue
cls.remove_single_employee_from_old_department(acl, employee, result) cls.remove_single_employee_from_old_department(acl, employee, result)

View File

@@ -4,7 +4,7 @@ import traceback
from datetime import datetime from datetime import datetime
import requests import requests
from flask import abort from flask import abort, current_app
from flask_login import current_user from flask_login import current_user
from sqlalchemy import or_, literal_column, func, not_, and_ from sqlalchemy import or_, literal_column, func, not_, and_
from werkzeug.datastructures import MultiDict from werkzeug.datastructures import MultiDict
@@ -478,7 +478,7 @@ class EmployeeCRUD(object):
Employee.deleted == 0, Employee.deleted == 0,
Employee.block == block, Employee.block == block,
] ]
if type(department_id) == list: if isinstance(department_id, list):
if len(department_id) == 0: if len(department_id) == 0:
return [] return []
else: else:
@@ -702,6 +702,7 @@ class EmployeeCRUD(object):
try: try:
last_login = datetime.strptime(last_login, '%Y-%m-%d %H:%M:%S') last_login = datetime.strptime(last_login, '%Y-%m-%d %H:%M:%S')
except Exception as e: except Exception as e:
current_app.logger.error(f"strptime {last_login} err: {e}")
last_login = datetime.now() last_login = datetime.now()
else: else:
last_login = datetime.now() last_login = datetime.now()
@@ -712,6 +713,7 @@ class EmployeeCRUD(object):
) )
return last_login return last_login
except Exception as e: except Exception as e:
current_app.logger.error(f"update last_login err: {e}")
return return

View File

@@ -2,7 +2,7 @@ import requests
from api.lib.common_setting.const import BotNameMap from api.lib.common_setting.const import BotNameMap
from api.lib.common_setting.resp_format import ErrFormat from api.lib.common_setting.resp_format import ErrFormat
from api.models.common_setting import CompanyInfo, NoticeConfig from api.models.common_setting import NoticeConfig
from wtforms import Form from wtforms import Form
from wtforms import StringField from wtforms import StringField
from wtforms import validators from wtforms import validators

View File

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

View File

@@ -6,7 +6,7 @@ from functools import wraps
from flask import abort from flask import abort
from flask import request from flask import request
from api.lib.perm.acl.cache import AppCache, AppAccessTokenCache from api.lib.perm.acl.cache import AppCache
from api.lib.perm.acl.resp_format import ErrFormat from api.lib.perm.acl.resp_format import ErrFormat

View File

@@ -138,14 +138,14 @@ class HasResourceRoleCache(object):
@classmethod @classmethod
def add(cls, rid, app_id): def add(cls, rid, app_id):
with redis_lock.Lock(rd.r, 'HasResourceRoleCache'): with redis_lock.Lock(rd.r, 'HasResourceRoleCache', expire=10):
c = cls.get(app_id) c = cls.get(app_id)
c[rid] = 1 c[rid] = 1
cache.set(cls.PREFIX_KEY.format(app_id), c, timeout=0) cache.set(cls.PREFIX_KEY.format(app_id), c, timeout=0)
@classmethod @classmethod
def remove(cls, rid, app_id): def remove(cls, rid, app_id):
with redis_lock.Lock(rd.r, 'HasResourceRoleCache'): with redis_lock.Lock(rd.r, 'HasResourceRoleCache', expire=10):
c = cls.get(app_id) c = cls.get(app_id)
c.pop(rid, None) c.pop(rid, None)
cache.set(cls.PREFIX_KEY.format(app_id), c, timeout=0) cache.set(cls.PREFIX_KEY.format(app_id), c, timeout=0)

View File

@@ -1,8 +1,5 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
import time
import redis_lock import redis_lock
import six import six
from flask import abort from flask import abort
@@ -145,7 +142,7 @@ class RoleRelationCRUD(object):
@classmethod @classmethod
def add(cls, role, parent_id, child_ids, app_id): def add(cls, role, parent_id, child_ids, app_id):
with redis_lock.Lock(rd.r, "ROLE_RELATION_ADD"): with redis_lock.Lock(rd.r, "ROLE_RELATION_ADD", expire=10):
db.session.commit() db.session.commit()
result = [] result = []

View File

@@ -1,7 +1,6 @@
# -*- coding:utf-8 -*- # -*- coding:utf-8 -*-
import base64 import base64
from typing import Set
import elasticsearch import elasticsearch
import redis import redis

View File

@@ -105,8 +105,8 @@ class User(CRUDModel, SoftDeleteMixin):
_password = db.Column("password", db.String(80)) _password = db.Column("password", db.String(80))
key = db.Column(db.String(32), nullable=False) key = db.Column(db.String(32), nullable=False)
secret = db.Column(db.String(32), nullable=False) secret = db.Column(db.String(32), nullable=False)
date_joined = db.Column(db.DateTime, default=datetime.utcnow) date_joined = db.Column(db.DateTime, default=datetime.now)
last_login = db.Column(db.DateTime, default=datetime.utcnow) last_login = db.Column(db.DateTime, default=datetime.now)
block = db.Column(db.Boolean, default=False) block = db.Column(db.Boolean, default=False)
has_logined = db.Column(db.Boolean, default=False) has_logined = db.Column(db.Boolean, default=False)
wx_id = db.Column(db.String(32)) wx_id = db.Column(db.String(32))

View File

@@ -32,7 +32,7 @@ from api.models.acl import Trigger
def role_rebuild(rids, app_id): def role_rebuild(rids, app_id):
rids = rids if isinstance(rids, list) else [rids] rids = rids if isinstance(rids, list) else [rids]
for rid in rids: for rid in rids:
with redis_lock.Lock(rd.r, "ROLE_REBUILD_{}_{}".format(rid, app_id)): with redis_lock.Lock(rd.r, "ROLE_REBUILD_{}_{}".format(rid, app_id), expire=10):
RoleRelationCache.rebuild(rid, app_id) RoleRelationCache.rebuild(rid, app_id)
current_app.logger.info("Role {0} App {1} rebuild..........".format(rids, app_id)) current_app.logger.info("Role {0} App {1} rebuild..........".format(rids, app_id))

View File

@@ -145,7 +145,7 @@ def ci_delete_trigger(trigger, operate_type, ci_dict):
@flush_db @flush_db
@reconnect_db @reconnect_db
def ci_relation_cache(parent_id, child_id, ancestor_ids): def ci_relation_cache(parent_id, child_id, ancestor_ids):
with redis_lock.Lock(rd.r, "CIRelation_{}".format(parent_id)): with redis_lock.Lock(rd.r, "CIRelation_{}".format(parent_id), expire=10):
children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0] children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0]
children = json.loads(children) if children is not None else {} children = json.loads(children) if children is not None else {}
@@ -223,7 +223,7 @@ def ci_relation_add(parent_dict, child_id, uid):
@celery.task(name="cmdb.ci_relation_delete", queue=CMDB_QUEUE) @celery.task(name="cmdb.ci_relation_delete", queue=CMDB_QUEUE)
@reconnect_db @reconnect_db
def ci_relation_delete(parent_id, child_id, ancestor_ids): def ci_relation_delete(parent_id, child_id, ancestor_ids):
with redis_lock.Lock(rd.r, "CIRelation_{}".format(parent_id)): with redis_lock.Lock(rd.r, "CIRelation_{}".format(parent_id), expire=10):
children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0] children = rd.get([parent_id], REDIS_PREFIX_CI_RELATION)[0]
children = json.loads(children) if children is not None else {} children = json.loads(children) if children is not None else {}

View File

@@ -131,7 +131,7 @@ class EmployeeChangePasswordWithACLID(APIView):
if not password: if not password:
abort(400, ErrFormat.password_is_required) abort(400, ErrFormat.password_is_required)
data = EmployeeCRUD.change_password_by_uid(_uid, password) EmployeeCRUD.change_password_by_uid(_uid, password)
return self.jsonify(200) return self.jsonify(200)

View File

@@ -6,7 +6,7 @@ import magic
from api.lib.common_setting.const import MIMEExtMap from api.lib.common_setting.const import MIMEExtMap
from api.lib.common_setting.resp_format import ErrFormat from api.lib.common_setting.resp_format import ErrFormat
from api.lib.common_setting.upload_file import allowed_file, generate_new_file_name, CommonFileCRUD from api.lib.common_setting.upload_file import generate_new_file_name, CommonFileCRUD
from api.resource import APIView from api.resource import APIView
prefix = '/file' prefix = '/file'

View File

@@ -58,3 +58,4 @@ python-magic==0.4.27
jsonpath==0.82.2 jsonpath==0.82.2
networkx>=3.1 networkx>=3.1
ipaddress>=1.0.23 ipaddress>=1.0.23
ruff==0.8.3

View File

@@ -1,6 +1,6 @@
NODE_ENV=production NODE_ENV=production
VUE_APP_PREVIEW=false VUE_APP_PREVIEW=false
VUE_APP_API_BASE_URL=/api VUE_APP_API_BASE_URL=http://127.0.0.1:5000/api
VUE_APP_BUILD_PACKAGES="ticket,calendar,acl" VUE_APP_BUILD_PACKAGES="ticket,calendar,acl"
VUE_APP_IS_OUTER=true VUE_APP_IS_OUTER=true
VUE_APP_IS_OPEN_SOURCE=true VUE_APP_IS_OPEN_SOURCE=true

View File

@@ -11,10 +11,19 @@
.ant-input { .ant-input {
box-shadow: none; box-shadow: none;
border: none;
background-color: #F7F8FA;
height: 30px; height: 30px;
line-height: 30px; line-height: 30px;
border-radius: 30px;
&:focus {
border: solid 1px #B1C9FF;
}
}
.cmdb-side-menu-search-focused {
.ant-input {
border: solid 1px #B1C9FF;
}
} }
.ant-input-suffix { .ant-input-suffix {

View File

@@ -92,7 +92,7 @@ export default {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
this.$confirm({ this.$confirm({
title: this.$t('alert'), title: this.$t('warning'),
content: this.$t('cmdb.preference.confirmcancelSub2', { name: menu.meta.title }), content: this.$t('cmdb.preference.confirmcancelSub2', { name: menu.meta.title }),
onOk() { onOk() {
const citypeId = menu.meta.typeId const citypeId = menu.meta.typeId
@@ -313,10 +313,7 @@ export default {
<Item class={styles['cmdb-side-menu-search']}> <Item class={styles['cmdb-side-menu-search']}>
<a-input <a-input
ref="cmdbSideMenuSearchInputRef" ref="cmdbSideMenuSearchInputRef"
class={styles['cmdb-side-menu-search-input']} class={`ops-input ${this.$route.name === 'cmdb_resource_search' ? 'cmdb-side-menu-search-focused' : ''}`}
style={{
border: this.$route.name === 'cmdb_resource_search' ? 'solid 1px #B1C9FF' : ''
}}
placeholder={this.$t('cmdbSearch')} placeholder={this.$t('cmdbSearch')}
onPressEnter={(e) => { onPressEnter={(e) => {
this.jumpCMDBSearch(e.target.value) this.jumpCMDBSearch(e.target.value)

View File

@@ -1,121 +1,121 @@
<template> <template>
<vxe-table v-bind="$attrs" v-on="new$listeners" ref="xTable"> <vxe-table v-bind="$attrs" v-on="new$listeners" ref="xTable">
<slot></slot> <slot></slot>
<template #empty> <template #empty>
<slot name="empty"> <slot name="empty">
<div :style="{ paddingTop: '10px' }"> <div :style="{ paddingTop: '10px' }">
<img :style="{ width: '140px', height: '90px' }" :src="require('@/assets/data_empty.png')" /> <img :style="{ width: '140px', height: '120px' }" :src="require('@/assets/data_empty.png')" />
<div>{{ $t('noData') }}</div> <div>{{ $t('noData') }}</div>
</div> </div>
</slot> </slot>
</template> </template>
<template #loading> <template #loading>
<slot name="loading"></slot> <slot name="loading"></slot>
</template> </template>
</vxe-table> </vxe-table>
</template> </template>
<script> <script>
import _ from 'lodash' import _ from 'lodash'
// 该组件使用方法与vxe-table一致但调用它的方法时需先调用getVxetableRef()获取到vxe-table实体 // 该组件使用方法与vxe-table一致但调用它的方法时需先调用getVxetableRef()获取到vxe-table实体
export default { export default {
name: 'OpsTable', name: 'OpsTable',
data() { data() {
return { return {
// isShifting: false, // isShifting: false,
// lastIndex: -1, // lastIndex: -1,
lastSelected: [], lastSelected: [],
currentSelected: [], currentSelected: [],
} }
}, },
computed: { computed: {
new$listeners() { new$listeners() {
if (!Object.keys(this.$listeners).length) { if (!Object.keys(this.$listeners).length) {
return this.$listeners return this.$listeners
} }
return Object.assign(this.$listeners, { return Object.assign(this.$listeners, {
// 在这里覆盖原有的change事件 // 在这里覆盖原有的change事件
// 'checkbox-change': this.selectChangeEvent, // 'checkbox-change': this.selectChangeEvent,
'checkbox-range-change': this.checkboxRangeChange, 'checkbox-range-change': this.checkboxRangeChange,
'checkbox-range-start': this.checkboxRangeStart, 'checkbox-range-start': this.checkboxRangeStart,
'checkbox-range-end': this.checkboxRangeEnd, 'checkbox-range-end': this.checkboxRangeEnd,
}) })
}, },
}, },
mounted() { mounted() {
// window.onkeydown = (e) => { // window.onkeydown = (e) => {
// if (e.key === 'Shift') { // if (e.key === 'Shift') {
// this.isShifting = true // this.isShifting = true
// } // }
// } // }
// window.onkeyup = (e) => { // window.onkeyup = (e) => {
// if (e.key === 'Shift') { // if (e.key === 'Shift') {
// this.isShifting = false // this.isShifting = false
// this.lastIndex = -1 // this.lastIndex = -1
// } // }
// } // }
}, },
beforeDestroy() { beforeDestroy() {
// window.onkeydown = '' // window.onkeydown = ''
// window.onkeyup = '' // window.onkeyup = ''
}, },
methods: { methods: {
getVxetableRef() { getVxetableRef() {
return this.$refs.xTable return this.$refs.xTable
}, },
// selectChangeEvent(e) { // selectChangeEvent(e) {
// const xTable = this.$refs.xTable // const xTable = this.$refs.xTable
// const { lastIndex } = this // const { lastIndex } = this
// const currentIndex = e.rowIndex // const currentIndex = e.rowIndex
// const { tableData } = xTable.getTableData() // const { tableData } = xTable.getTableData()
// if (lastIndex > -1 && this.isShifting) { // if (lastIndex > -1 && this.isShifting) {
// let start = lastIndex // let start = lastIndex
// let end = currentIndex // let end = currentIndex
// if (lastIndex > currentIndex) { // if (lastIndex > currentIndex) {
// start = currentIndex // start = currentIndex
// end = lastIndex // end = lastIndex
// } // }
// const rangeData = tableData.slice(start, end + 1) // const rangeData = tableData.slice(start, end + 1)
// xTable.setCheckboxRow(rangeData, true) // xTable.setCheckboxRow(rangeData, true)
// } // }
// this.lastIndex = currentIndex // this.lastIndex = currentIndex
// this.$emit('checkbox-change', { ...e, records: xTable.getCheckboxRecords() }) // this.$emit('checkbox-change', { ...e, records: xTable.getCheckboxRecords() })
// }, // },
checkboxRangeStart(e) { checkboxRangeStart(e) {
const xTable = this.$refs.xTable const xTable = this.$refs.xTable
const lastSelected = xTable.getCheckboxRecords() const lastSelected = xTable.getCheckboxRecords()
const selectedReserve = xTable.getCheckboxReserveRecords() const selectedReserve = xTable.getCheckboxReserveRecords()
this.lastSelected = [...lastSelected, ...selectedReserve] this.lastSelected = [...lastSelected, ...selectedReserve]
this.$emit('checkbox-range-start', e) this.$emit('checkbox-range-start', e)
}, },
checkboxRangeChange(e) { checkboxRangeChange(e) {
const xTable = this.$refs.xTable const xTable = this.$refs.xTable
xTable.setCheckboxRow(this.lastSelected, true) xTable.setCheckboxRow(this.lastSelected, true)
this.currentSelected = e.records this.currentSelected = e.records
// this.lastSelected = [...new Set([...this.lastSelected, ...e.records])] // this.lastSelected = [...new Set([...this.lastSelected, ...e.records])]
this.$emit('checkbox-range-change', { this.$emit('checkbox-range-change', {
...e, ...e,
records: [...xTable.getCheckboxRecords(), ...xTable.getCheckboxReserveRecords()], records: [...xTable.getCheckboxRecords(), ...xTable.getCheckboxReserveRecords()],
}) })
}, },
checkboxRangeEnd(e) { checkboxRangeEnd(e) {
const xTable = this.$refs.xTable const xTable = this.$refs.xTable
const isAllSelected = this.currentSelected.every((item) => { const isAllSelected = this.currentSelected.every((item) => {
const _idx = this.lastSelected.findIndex((ele) => _.isEqual(ele, item)) const _idx = this.lastSelected.findIndex((ele) => _.isEqual(ele, item))
return _idx > -1 return _idx > -1
}) })
if (isAllSelected) { if (isAllSelected) {
xTable.setCheckboxRow(this.currentSelected, false) xTable.setCheckboxRow(this.currentSelected, false)
} }
this.currentSelected = [] this.currentSelected = []
this.lastSelected = [] this.lastSelected = []
this.$emit('checkbox-range-end', { this.$emit('checkbox-range-end', {
...e, ...e,
records: [...xTable.getCheckboxRecords(), ...xTable.getCheckboxReserveRecords()], records: [...xTable.getCheckboxRecords(), ...xTable.getCheckboxReserveRecords()],
}) })
}, },
}, },
} }
</script> </script>
<style lang="less"></style> <style lang="less"></style>

View File

@@ -195,11 +195,11 @@ export default {
background-color: #fff; background-color: #fff;
height: calc(100vh - 64px); height: calc(100vh - 64px);
margin-bottom: -24px; margin-bottom: -24px;
padding: 24px; padding: 20px;
.acl-resource-types-header { .acl-resource-types-header {
width: 100%; width: 100%;
display: inline-flex; display: inline-flex;
margin-bottom: 15px; margin-bottom: 20px;
align-items: center; align-items: center;
} }
} }

View File

@@ -358,11 +358,11 @@ export default {
background-color: #fff; background-color: #fff;
height: calc(100vh - 64px); height: calc(100vh - 64px);
margin-bottom: -24px; margin-bottom: -24px;
padding: 12px 24px 24px 24px; padding: 8px 20px 20px 20px;
.acl-resources-header { .acl-resources-header {
width: 100%; width: 100%;
display: inline-flex; display: inline-flex;
margin-bottom: 15px; margin-bottom: 20px;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
.ant-switch { .ant-switch {

View File

@@ -291,11 +291,11 @@ export default {
background-color: #fff; background-color: #fff;
height: calc(100vh - 64px); height: calc(100vh - 64px);
margin-bottom: -24px; margin-bottom: -24px;
padding: 24px; padding: 20px;
.acl-roles-header { .acl-roles-header {
width: 100%; width: 100%;
display: inline-flex; display: inline-flex;
margin-bottom: 15px; margin-bottom: 20px;
align-items: center; align-items: center;
.ant-checkbox-wrapper { .ant-checkbox-wrapper {
margin-left: auto; margin-left: auto;

View File

@@ -326,11 +326,11 @@ export default {
background-color: #fff; background-color: #fff;
height: calc(100vh - 64px); height: calc(100vh - 64px);
margin-bottom: -24px; margin-bottom: -24px;
padding: 24px; padding: 20px;
.acl-trigger-header { .acl-trigger-header {
width: 100%; width: 100%;
display: inline-flex; display: inline-flex;
margin-bottom: 15px; margin-bottom: 20px;
align-items: center; align-items: center;
} }
} }

View File

@@ -1,371 +1,370 @@
<template> <template>
<div class="cmdb-grant" :style="{ }"> <div class="cmdb-grant" :style="{ }">
<template v-if="cmdbGrantType.includes('ci_type')"> <template v-if="cmdbGrantType.includes('ci_type')">
<div class="cmdb-grant-title">{{ $t('cmdb.components.ciTypeGrant') }}</div> <div class="cmdb-grant-title">{{ $t('cmdb.components.ciTypeGrant') }}</div>
<CiTypeGrant <CiTypeGrant
:CITypeId="CITypeId" :CITypeId="CITypeId"
:tableData="tableData" :tableData="tableData"
grantType="ci_type" grantType="ci_type"
@grantDepart="grantDepart" @grantDepart="grantDepart"
@grantRole="grantRole" @grantRole="grantRole"
@getTableData="getTableData" @getTableData="getTableData"
ref="grant_ci_type" ref="grant_ci_type"
:addedRids="addedRids" :addedRids="addedRids"
/> />
</template> </template>
<template <template
v-if=" v-if="
cmdbGrantType.includes('ci_type,ci') || (cmdbGrantType.includes('ci') && !cmdbGrantType.includes('ci_type')) cmdbGrantType.includes('ci_type,ci') || (cmdbGrantType.includes('ci') && !cmdbGrantType.includes('ci_type'))
" "
> >
<div class="cmdb-grant-title">{{ $t('cmdb.components.ciGrant') }}</div> <div class="cmdb-grant-title">{{ $t('cmdb.components.ciGrant') }}</div>
<CiTypeGrant <CiTypeGrant
:CITypeId="CITypeId" :CITypeId="CITypeId"
:tableData="tableData" :tableData="tableData"
grantType="ci" grantType="ci"
@grantDepart="grantDepart" @grantDepart="grantDepart"
@grantRole="grantRole" @grantRole="grantRole"
@getTableData="getTableData" @getTableData="getTableData"
@openReadGrantModal="openReadGrantModal" @openReadGrantModal="openReadGrantModal"
ref="grant_ci" ref="grant_ci"
:addedRids="addedRids" :addedRids="addedRids"
/> />
</template> </template>
<template v-if="cmdbGrantType.includes('type_relation')"> <template v-if="cmdbGrantType.includes('type_relation')">
<div class="cmdb-grant-title">{{ $t('cmdb.components.relationGrant') }}</div> <div class="cmdb-grant-title">{{ $t('cmdb.components.relationGrant') }}</div>
<TypeRelationGrant <TypeRelationGrant
:typeRelationIds="typeRelationIds" :typeRelationIds="typeRelationIds"
:tableData="tableData" :tableData="tableData"
grantType="type_relation" grantType="type_relation"
@grantDepart="grantDepart" @grantDepart="grantDepart"
@grantRole="grantRole" @grantRole="grantRole"
@getTableData="getTableData" @getTableData="getTableData"
ref="grant_type_relation" ref="grant_type_relation"
:addedRids="addedRids" :addedRids="addedRids"
/> />
</template> </template>
<template v-if="cmdbGrantType.includes('relation_view')"> <template v-if="cmdbGrantType.includes('relation_view')">
<div class="cmdb-grant-title">{{ resourceTypeName }}{{ $t('cmdb.components.perm') }}</div> <div class="cmdb-grant-title">{{ resourceTypeName }}{{ $t('cmdb.components.perm') }}</div>
<RelationViewGrant <RelationViewGrant
:resourceTypeName="resourceTypeName" :resourceTypeName="resourceTypeName"
:tableData="tableData" :tableData="tableData"
grantType="relation_view" grantType="relation_view"
@grantDepart="grantDepart" @grantDepart="grantDepart"
@grantRole="grantRole" @grantRole="grantRole"
@getTableData="getTableData" @getTableData="getTableData"
ref="grant_relation_view" ref="grant_relation_view"
:addedRids="addedRids" :addedRids="addedRids"
/> />
</template> </template>
<template v-if="cmdbGrantType.includes('TopologyView')"> <template v-if="cmdbGrantType.includes('TopologyView')">
<div class="cmdb-grant-title">{{ resourceTypeName }}{{ $t('cmdb.components.perm') }}</div> <div class="cmdb-grant-title">{{ resourceTypeName }}{{ $t('cmdb.components.perm') }}</div>
<TopologyViewGrant <TopologyViewGrant
:resourceTypeName="resourceTypeName" :resourceTypeName="resourceTypeName"
:tableData="tableData" :tableData="tableData"
:viewId="CITypeId" :viewId="CITypeId"
grantType="TopologyView" grantType="TopologyView"
@grantDepart="grantDepart" @grantDepart="grantDepart"
@grantRole="grantRole" @grantRole="grantRole"
@getTableData="getTableData" @getTableData="getTableData"
ref="grantTopologyView" ref="grantTopologyView"
:addedRids="addedRids" :addedRids="addedRids"
/> />
</template> </template>
<GrantModal ref="grantModal" @handleOk="handleOk" /> <GrantModal ref="grantModal" @handleOk="handleOk" />
<ReadGrantModal ref="readGrantModal" :CITypeId="CITypeId" @updateTableDataRead="updateTableDataRead" /> <ReadGrantModal ref="readGrantModal" :CITypeId="CITypeId" @updateTableDataRead="updateTableDataRead" />
</div> </div>
</template> </template>
<script> <script>
import { mapState } from 'vuex' import { mapState } from 'vuex'
import CiTypeGrant from './ciTypeGrant.vue' import CiTypeGrant from './ciTypeGrant.vue'
import TypeRelationGrant from './typeRelationGrant.vue' import TypeRelationGrant from './typeRelationGrant.vue'
import { searchResource } from '@/modules/acl/api/resource' import { searchResource } from '@/modules/acl/api/resource'
import { getResourcePerms } from '@/modules/acl/api/permission' import { getResourcePerms } from '@/modules/acl/api/permission'
import GrantModal from './grantModal.vue' import GrantModal from './grantModal.vue'
import ReadGrantModal from './readGrantModal' import ReadGrantModal from './readGrantModal'
import RelationViewGrant from './relationViewGrant.vue' import RelationViewGrant from './relationViewGrant.vue'
import TopologyViewGrant from './topologyViewGrant.vue' import TopologyViewGrant from './topologyViewGrant.vue'
import { getCITypeGroupById, ciTypeFilterPermissions } from '../../api/CIType' import { getCITypeGroupById, ciTypeFilterPermissions } from '../../api/CIType'
export default { export default {
name: 'GrantComp', name: 'GrantComp',
components: { CiTypeGrant, TypeRelationGrant, RelationViewGrant, TopologyViewGrant, GrantModal, ReadGrantModal }, components: { CiTypeGrant, TypeRelationGrant, RelationViewGrant, TopologyViewGrant, GrantModal, ReadGrantModal },
props: { props: {
CITypeId: { CITypeId: {
type: Number, type: Number,
default: null, default: null,
}, },
resourceTypeName: { resourceTypeName: {
type: String, type: String,
default: '', default: '',
}, },
resourceType: { resourceType: {
type: String, type: String,
default: 'CIType', default: 'CIType',
}, },
app_id: { app_id: {
type: String, type: String,
default: 'cmdb', default: 'cmdb',
}, },
cmdbGrantType: { cmdbGrantType: {
type: String, type: String,
default: 'ci_type,ci', default: 'ci_type,ci',
}, },
typeRelationIds: { typeRelationIds: {
type: Array, type: Array,
default: null, default: null,
}, },
isModal: { isModal: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
}, },
inject: ['resource_type'], inject: ['resource_type'],
data() { data() {
return { return {
tableData: [], tableData: [],
grantType: '', grantType: '',
resource_id: null, resource_id: null,
attrGroup: [], attrGroup: [],
filerPerimissions: {}, filerPerimissions: {},
loading: false, loading: false,
addedRids: [], // added rid this time addedRids: [], // added rid this time
} }
}, },
computed: { computed: {
...mapState({ ...mapState({
allEmployees: (state) => state.user.allEmployees, allEmployees: (state) => state.user.allEmployees,
allDepartments: (state) => state.user.allDepartments, allDepartments: (state) => state.user.allDepartments,
}), }),
child_resource_type() { child_resource_type() {
return this.resource_type() return this.resource_type()
}, },
windowHeight() { windowHeight() {
return this.$store.state.windowHeight return this.$store.state.windowHeight
}, },
}, },
provide() { provide() {
return { return {
attrGroup: () => { attrGroup: () => {
return this.attrGroup return this.attrGroup
}, },
filerPerimissions: () => { filerPerimissions: () => {
return this.filerPerimissions return this.filerPerimissions
}, },
loading: () => { loading: () => {
return this.loading return this.loading
}, },
isModal: this.isModal, isModal: this.isModal,
} }
}, },
watch: { watch: {
resourceTypeName: { resourceTypeName: {
immediate: true, immediate: true,
handler() { handler() {
this.init() this.init()
}, },
}, },
CITypeId: { CITypeId: {
immediate: true, immediate: true,
handler() { handler() {
if (this.CITypeId && this.cmdbGrantType.includes('ci')) { if (this.CITypeId && this.cmdbGrantType.includes('ci')) {
this.getFilterPermissions() this.getFilterPermissions()
this.getAttrGroup() this.getAttrGroup()
} }
}, },
}, },
}, },
mounted() {}, mounted() {},
methods: { methods: {
getAttrGroup() { getAttrGroup() {
getCITypeGroupById(this.CITypeId, { need_other: true }).then((res) => { getCITypeGroupById(this.CITypeId, { need_other: true }).then((res) => {
this.attrGroup = res this.attrGroup = res
}) })
}, },
getFilterPermissions() { getFilterPermissions() {
ciTypeFilterPermissions(this.CITypeId).then((res) => { ciTypeFilterPermissions(this.CITypeId).then((res) => {
this.filerPerimissions = res this.filerPerimissions = res
}) })
}, },
async init() { async init() {
const _find = this.child_resource_type.groups.find((item) => item.name === this.resourceType) const _find = this.child_resource_type.groups.find((item) => item.name === this.resourceType)
const resource_type_id = _find?.id ?? 0 const resource_type_id = _find?.id ?? 0
const res = await searchResource({ const res = await searchResource({
app_id: this.app_id, app_id: this.app_id,
resource_type_id, resource_type_id,
page_size: 9999, page_size: 9999,
}) })
const _tempFind = res.resources.find((item) => item.name === this.resourceTypeName) const _tempFind = res.resources.find((item) => item.name === this.resourceTypeName)
console.log(this.resourceTypeName) console.log(this.resourceTypeName)
this.resource_id = _tempFind?.id || 0 this.resource_id = _tempFind?.id || 0
this.getTableData() this.getTableData()
}, },
async getTableData() { async getTableData() {
this.loading = true this.loading = true
const _tableData = await getResourcePerms(this.resource_id, { need_users: 0 }) const _tableData = await getResourcePerms(this.resource_id, { need_users: 0 })
const perms = [] const perms = []
for (const key in _tableData) { for (const key in _tableData) {
const obj = {} const obj = {}
obj.name = key obj.name = key
_tableData[key].perms.forEach((perm) => { _tableData[key].perms.forEach((perm) => {
obj[`${perm.name}`] = true obj[`${perm.name}`] = true
obj.rid = perm?.rid ?? null obj.rid = perm?.rid ?? null
}) })
perms.push(obj) perms.push(obj)
} }
this.tableData = perms this.tableData = perms
this.loading = false this.loading = false
}, },
// Grant the department in common-setting and get the roleid from it // Grant the department in common-setting and get the roleid from it
grantDepart(grantType) { grantDepart(grantType) {
this.$refs.grantModal.open('depart') this.$refs.grantModal.open('depart')
this.grantType = grantType this.grantType = grantType
}, },
// Grant the oldest role permissions // Grant the oldest role permissions
grantRole(grantType) { grantRole(grantType) {
this.$refs.grantModal.open('role') this.$refs.grantModal.open('role')
this.grantType = grantType this.grantType = grantType
}, },
handleOk(params, type) { handleOk(params, type) {
const { grantType } = this const { grantType } = this
let rids let rids
if (type === 'depart') { if (type === 'depart') {
rids = [ rids = [
...params.department.map((rid) => { ...params.department.map((rid) => {
const _find = this.allDepartments.find((dep) => dep.acl_rid === rid) const _find = this.allDepartments.find((dep) => dep.acl_rid === rid)
return { rid, name: _find?.department_name ?? rid } return { rid, name: _find?.department_name ?? rid }
}), }),
...params.user.map((rid) => { ...params.user.map((rid) => {
const _find = this.allEmployees.find((dep) => dep.acl_rid === rid) const _find = this.allEmployees.find((dep) => dep.acl_rid === rid)
return { rid, name: _find?.nickname ?? rid } return { rid, name: _find?.nickname ?? rid }
}), }),
] ]
} }
if (type === 'role') { if (type === 'role') {
rids = [ rids = [
...params.map((role) => { ...params.map((role) => {
return { rid: role.id, name: role.name } return { rid: role.id, name: role.name }
}), }),
] ]
} }
if (grantType === 'ci_type') { if (grantType === 'ci_type') {
this.tableData.unshift( this.tableData.unshift(
...rids.map(({ rid, name }) => { ...rids.map(({ rid, name }) => {
const _find = this.tableData.find((item) => item.rid === rid) const _find = this.tableData.find((item) => item.rid === rid)
return { return {
rid, rid,
name, name,
conifg: false, conifg: false,
grant: false, grant: false,
..._find, ..._find,
} }
}) })
) )
} }
if (grantType === 'ci') { if (grantType === 'ci') {
this.tableData.unshift( this.tableData.unshift(
...rids.map(({ rid, name }) => { ...rids.map(({ rid, name }) => {
const _find = this.tableData.find((item) => item.rid === rid) const _find = this.tableData.find((item) => item.rid === rid)
return { return {
rid, rid,
name, name,
read_attr: false, read_attr: false,
read_ci: false, read_ci: false,
create: false, create: false,
update: false, update: false,
delete: false, delete: false,
..._find, ..._find,
} }
}) })
) )
} }
if (grantType === 'type_relation') { if (grantType === 'type_relation') {
this.tableData.unshift( this.tableData.unshift(
...rids.map(({ rid, name }) => { ...rids.map(({ rid, name }) => {
return { return {
rid, rid,
name, name,
create: false, create: false,
grant: false, grant: false,
delete: false, delete: false,
} }
}) })
) )
} }
if (grantType === 'relation_view') { if (grantType === 'relation_view') {
this.tableData.unshift( this.tableData.unshift(
...rids.map(({ rid, name }) => { ...rids.map(({ rid, name }) => {
return { return {
rid, rid,
name, name,
read: false, read: false,
grant: false, grant: false,
} }
}) })
) )
} }
if (grantType === 'TopologyView') { if (grantType === 'TopologyView') {
this.tableData.unshift( this.tableData.unshift(
...rids.map(({ rid, name }) => { ...rids.map(({ rid, name }) => {
return { return {
rid, rid,
name, name,
read: false, read: false,
update: false, update: false,
delete: false, delete: false,
grant: false, grant: false,
} }
}) })
) )
} }
this.addedRids = rids this.addedRids = rids
this.$nextTick(() => { this.$nextTick(() => {
setTimeout(() => { setTimeout(() => {
this.$refs[`grant_${grantType}`].$refs.xTable.elemStore['main-body-wrapper'].scrollTo(0, 0) this.$refs[`grant_${grantType}`].$refs.xTable.elemStore['main-body-wrapper'].scrollTo(0, 0)
}, 300) }, 300)
}) })
}, },
openReadGrantModal(col, row) { openReadGrantModal(col, row) {
this.$refs.readGrantModal.open(col, row) this.$refs.readGrantModal.open(col, row)
}, },
updateTableDataRead(row, hasRead) { updateTableDataRead(row, hasRead) {
const _idx = this.tableData.findIndex((item) => item.rid === row.rid) const _idx = this.tableData.findIndex((item) => item.rid === row.rid)
this.$set(this.tableData, _idx, { ...this.tableData[_idx], read: hasRead }) this.$set(this.tableData, _idx, { ...this.tableData[_idx], read: hasRead })
this.getFilterPermissions() this.getFilterPermissions()
}, },
}, },
} }
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.cmdb-grant { .cmdb-grant {
position: relative; position: relative;
padding: 0 20px; padding: 0 20px;
overflow: auto; overflow: auto;
.cmdb-grant-title { .cmdb-grant-title {
border-left: 4px solid @primary-color; border-left: 4px solid @primary-color;
padding-left: 10px; padding-left: 10px;
} }
} }
</style> </style>
<style lang="less"> <style lang="less">
.cmdb-grant {
.cmdb-grant { .grant-button {
.grant-button { padding: 6px 8px;
padding: 6px 8px; color: @primary-color;
color: @primary-color; background-color: @primary-color_5;
background-color: @primary-color_5; border-radius: 2px;
border-radius: 2px; cursor: pointer;
cursor: pointer; margin: 15px 0;
margin: 15px 0; display: inline-block;
display: inline-block; transition: all 0.3s;
transition: all 0.3s; z-index: 1;
&:hover {
box-shadow: 2px 3px 4px @primary-color_5; .btn-wave-hover(@primary-color_4, -1);
} }
} }
} </style>
</style>

View File

@@ -1,122 +1,118 @@
<template> <template>
<a-modal :visible="visible" @cancel="handleCancel" @ok="handleOK" :title="$t('revoke')"> <a-modal :visible="visible" @cancel="handleCancel" @ok="handleOK" :title="$t('revoke')">
<a-form-model :model="form" :label-col="{ span: 4 }" :wrapper-col="{ span: 16 }"> <a-form-model :model="form" :label-col="{ span: 4 }" :wrapper-col="{ span: 16 }">
<a-form-model-item :label="$t('user')"> <a-form-model-item :label="$t('user')">
<EmployeeTreeSelect <EmployeeTreeSelect
class="custom-treeselect custom-treeselect-bgcAndBorder" class="custom-treeselect custom-treeselect-white"
:style="{ :style="{
'--custom-height': '32px', '--custom-height': '32px',
lineHeight: '32px', lineHeight: '32px',
'--custom-bg-color': '#fff', '--custom-multiple-lineHeight': '18px',
'--custom-border': '1px solid #d9d9d9', }"
'--custom-multiple-lineHeight': '18px', :multiple="true"
}" v-model="form.users"
:multiple="true" :placeholder="$t('cmdb.serviceTree.userPlaceholder')"
v-model="form.users" :idType="2"
:placeholder="$t('cmdb.serviceTree.userPlaceholder')" departmentKey="acl_rid"
:idType="2" employeeKey="acl_rid"
departmentKey="acl_rid" />
employeeKey="acl_rid" </a-form-model-item>
/> <a-form-model-item :label="$t('role')">
</a-form-model-item> <treeselect
<a-form-model-item :label="$t('role')"> v-model="form.roles"
<treeselect :multiple="true"
v-model="form.roles" :options="filterAllRoles"
:multiple="true" class="custom-treeselect custom-treeselect-white"
:options="filterAllRoles" :style="{
class="custom-treeselect custom-treeselect-bgcAndBorder" '--custom-height': '32px',
:style="{ lineHeight: '32px',
'--custom-height': '32px', '--custom-multiple-lineHeight': '18px',
lineHeight: '32px', }"
'--custom-bg-color': '#fff', :limit="10"
'--custom-border': '1px solid #d9d9d9', :limitText="(count) => `+ ${count}`"
'--custom-multiple-lineHeight': '18px', :normalizer="
}" (node) => {
:limit="10" return {
:limitText="(count) => `+ ${count}`" id: node.id,
:normalizer=" label: node.name,
(node) => { }
return { }
id: node.id, "
label: node.name, appendToBody
} zIndex="1050"
} :placeholder="$t('cmdb.serviceTree.rolePlaceholder')"
" @search-change="searchRole"
appendToBody />
zIndex="1050" </a-form-model-item>
:placeholder="$t('cmdb.serviceTree.rolePlaceholder')" </a-form-model>
@search-change="searchRole" </a-modal>
/> </template>
</a-form-model-item>
</a-form-model> <script>
</a-modal> import EmployeeTreeSelect from '@/views/setting/components/employeeTreeSelect.vue'
</template> import { getAllDepAndEmployee } from '@/api/company'
import { searchRole } from '@/modules/acl/api/role'
<script>
import EmployeeTreeSelect from '@/views/setting/components/employeeTreeSelect.vue' export default {
import { getAllDepAndEmployee } from '@/api/company' name: 'RevokeModal',
import { searchRole } from '@/modules/acl/api/role' components: { EmployeeTreeSelect },
data() {
export default { return {
name: 'RevokeModal', visible: false,
components: { EmployeeTreeSelect }, form: {
data() { users: undefined,
return { roles: undefined,
visible: false, },
form: { allTreeDepAndEmp: [],
users: undefined, allRoles: [],
roles: undefined, filterAllRoles: [],
}, }
allTreeDepAndEmp: [], },
allRoles: [], provide() {
filterAllRoles: [], return {
} provide_allTreeDepAndEmp: () => {
}, return this.allTreeDepAndEmp
provide() { },
return { }
provide_allTreeDepAndEmp: () => { },
return this.allTreeDepAndEmp mounted() {
}, this.getAllDepAndEmployee()
} this.loadRoles()
}, },
mounted() { methods: {
this.getAllDepAndEmployee() async loadRoles() {
this.loadRoles() const res = await searchRole({ page_size: 9999, app_id: 'cmdb', is_all: true })
}, this.allRoles = res.roles
methods: { this.filterAllRoles = this.allRoles.slice(0, 100)
async loadRoles() { },
const res = await searchRole({ page_size: 9999, app_id: 'cmdb', is_all: true }) getAllDepAndEmployee() {
this.allRoles = res.roles getAllDepAndEmployee({ block: 0 }).then((res) => {
this.filterAllRoles = this.allRoles.slice(0, 100) this.allTreeDepAndEmp = res
}, })
getAllDepAndEmployee() { },
getAllDepAndEmployee({ block: 0 }).then((res) => { open() {
this.allTreeDepAndEmp = res this.visible = true
}) this.$nextTick(() => {
}, this.form = {
open() { users: undefined,
this.visible = true roles: undefined,
this.$nextTick(() => { }
this.form = { })
users: undefined, },
roles: undefined, handleCancel() {
} this.visible = false
}) },
}, searchRole(searchQuery) {
handleCancel() { this.filterAllRoles = this.allRoles
this.visible = false .filter((item) => item.name.toLowerCase().includes(searchQuery.toLowerCase()))
}, .slice(0, 100)
searchRole(searchQuery) { },
this.filterAllRoles = this.allRoles handleOK() {
.filter((item) => item.name.toLowerCase().includes(searchQuery.toLowerCase())) this.$emit('handleRevoke', this.form)
.slice(0, 100) this.handleCancel()
}, },
handleOK() { },
this.$emit('handleRevoke', this.form) }
this.handleCancel() </script>
},
}, <style></style>
}
</script>
<style></style>

View File

@@ -3,12 +3,10 @@
:disabled="disabled" :disabled="disabled"
ref="cmdb_type_select" ref="cmdb_type_select"
:disable-branch-nodes="true" :disable-branch-nodes="true"
class="custom-treeselect custom-treeselect-bgcAndBorder" class="custom-treeselect custom-treeselect-white"
:style="{ :style="{
'--custom-height': '30px', '--custom-height': '30px',
lineHeight: '30px', lineHeight: '30px'
'--custom-bg-color': '#fff',
'--custom-border': '1px solid #d9d9d9',
}" }"
v-model="currenCiType" v-model="currenCiType"
:multiple="multiple" :multiple="multiple"

View File

@@ -35,8 +35,8 @@
:key="`${item.id}_${index}`" :key="`${item.id}_${index}`"
class="preference-search-tag" class="preference-search-tag"
:style="{ :style="{
backgroundColor: item.id === currentPreferenceSearch ? '#2f54eb' : '#fafafa', backgroundColor: item.id === currentPreferenceSearch ? '#2f54eb' : '',
color: item.id === currentPreferenceSearch ? '#fff' : '#000000a6', color: item.id === currentPreferenceSearch ? '#fff' : '',
}" }"
> >
<span @click="clickPreferenceSearch(item)">{{ item.name }}</span> <span @click="clickPreferenceSearch(item)">{{ item.name }}</span>
@@ -189,6 +189,10 @@ export default {
> i { > i {
font-size: 12px; font-size: 12px;
} }
&:hover {
color: @primary-color;
}
} }
.preference-search-delete { .preference-search-delete {
color: #a9a9a9; color: #a9a9a9;

View File

@@ -5,13 +5,11 @@
<a-space> <a-space>
<treeselect <treeselect
v-if="type === 'resourceSearch'" v-if="type === 'resourceSearch'"
class="custom-treeselect custom-treeselect-bgcAndBorder" class="custom-treeselect"
:style="{ :style="{
width: '200px', width: '200px',
marginRight: '10px', marginRight: '10px',
'--custom-height': '32px', '--custom-height': '32px',
'--custom-bg-color': '#fff',
'--custom-border': '1px solid #d9d9d9',
'--custom-multiple-lineHeight': '16px', '--custom-multiple-lineHeight': '16px',
}" }"
v-model="currenCiType" v-model="currenCiType"

View File

@@ -135,9 +135,15 @@ export default {
border: 1px solid #f3f4f6; border: 1px solid #f3f4f6;
} }
.authorization-input { .authorization-input {
border: none; border: 1px solid transparent;
&:focus { &:focus {
box-shadow: none; box-shadow: none;
border-color: @primary-color;
}
&:hover {
border-color: @primary-color;
} }
} }
} }

View File

@@ -29,7 +29,7 @@
<tr v-for="(item, index) in headers" :key="item.id"> <tr v-for="(item, index) in headers" :key="item.id">
<td><a-input class="headers-input" v-model="item.key" :placeholder="$t('cmdb.components.param', { param: `${index + 1}` })" /></td> <td><a-input class="headers-input" v-model="item.key" :placeholder="$t('cmdb.components.param', { param: `${index + 1}` })" /></td>
<td><a-input class="headers-input" v-model="item.value" :placeholder="$t('cmdb.components.value', { value: `${index + 1}` })" /></td> <td><a-input class="headers-input" v-model="item.value" :placeholder="$t('cmdb.components.value', { value: `${index + 1}` })" /></td>
<td> <td class="headers-delete">
<a style="color:red"> <a style="color:red">
<ops-icon type="icon-xianxing-delete" @click="deleteParam(index)" /> <ops-icon type="icon-xianxing-delete" @click="deleteParam(index)" />
</a> </a>
@@ -92,10 +92,20 @@ export default {
border: 1px solid #f3f4f6; border: 1px solid #f3f4f6;
} }
.headers-input { .headers-input {
border: none; border: 1px solid transparent;
&:focus { &:focus {
box-shadow: none; box-shadow: none;
border-color: @primary-color;
} }
&:hover {
border-color: @primary-color;
}
}
.headers-delete {
text-align: center;
} }
} }
</style> </style>

View File

@@ -3,12 +3,10 @@
<a-input-group compact> <a-input-group compact>
<treeselect <treeselect
:disable-branch-nodes="true" :disable-branch-nodes="true"
class="custom-treeselect custom-treeselect-bgcAndBorder" class="custom-treeselect custom-treeselect-white"
:style="{ :style="{
'--custom-height': '30px', '--custom-height': '30px',
lineHeight: '30px', lineHeight: '30px',
'--custom-bg-color': '#fff',
'--custom-border': '1px solid #d9d9d9',
display: 'inline-block', display: 'inline-block',
width: '100px', width: '100px',
}" }"

View File

@@ -23,7 +23,7 @@
<tr v-for="(item, index) in parameters" :key="item.id"> <tr v-for="(item, index) in parameters" :key="item.id">
<td><a-input class="parameters-input" v-model="item.key" :placeholder="$t('cmdb.components.param', { param: `${index + 1}` })" /></td> <td><a-input class="parameters-input" v-model="item.key" :placeholder="$t('cmdb.components.param', { param: `${index + 1}` })" /></td>
<td><a-input class="parameters-input" v-model="item.value" :placeholder="$t('cmdb.components.value', { value: `${index + 1}` })" /></td> <td><a-input class="parameters-input" v-model="item.value" :placeholder="$t('cmdb.components.value', { value: `${index + 1}` })" /></td>
<td> <td class="parameters-delete">
<a style="color:red"> <a style="color:red">
<ops-icon type="icon-xianxing-delete" @click="deleteParam(index)" /> <ops-icon type="icon-xianxing-delete" @click="deleteParam(index)" />
</a> </a>
@@ -91,10 +91,20 @@ export default {
border: 1px solid #f3f4f6; border: 1px solid #f3f4f6;
} }
.parameters-input { .parameters-input {
border: none; border: 1px solid transparent;
&:focus { &:focus {
box-shadow: none; box-shadow: none;
border-color: @primary-color;
} }
&:hover {
border-color: @primary-color;
}
}
.parameters-delete {
text-align: center;
} }
} }
</style> </style>

View File

@@ -104,6 +104,16 @@ export default {
.cmdb-batch-upload-tips { .cmdb-batch-upload-tips {
color: @primary-color; color: @primary-color;
} }
&:hover {
background: linear-gradient(90deg, @primary-color_2 50%, transparent 0) repeat-x,
linear-gradient(90deg, @primary-color_2 50%, transparent 0) repeat-x,
linear-gradient(0deg, @primary-color_2 50%, transparent 0) repeat-y,
linear-gradient(0deg, @primary-color_2 50%, transparent 0) repeat-y;
background-size: 15px 1px, 15px 1px, 1px 15px, 1px 15px;
background-position: 0 0, 0 100%, 0 0, 100% 0;
background-color: @primary-color_7;
}
} }
.ant-upload.ant-upload-drag .ant-upload-drag-container { .ant-upload.ant-upload-drag .ant-upload-drag-container {
vertical-align: baseline; vertical-align: baseline;

View File

@@ -9,22 +9,18 @@
" "
:title="$t('cmdb.ci.attributeDesc')" :title="$t('cmdb.ci.attributeDesc')"
width="72%" width="72%"
:bodyStyle="{ height: '100vh' }" :bodyStyle="{ height: '100vh', paddingTop: '16px' }"
> >
<vxe-toolbar> <a-input
<template #buttons> v-model="searchKey"
<a-input :style="{ display: 'inline-block', width: '244px', marginBottom: '16px' }"
v-model="searchKey" class="ops-input ops-input-radius"
:style="{ display: 'inline-block', width: '244px' }" type="search"
class="ops-input ops-input-radius" :placeholder="$t('cmdb.ci.tips5')"
type="search" @keyup="searchAttributes"
:placeholder="$t('cmdb.ci.tips5')" >
@keyup="searchAttributes" <a-icon type="search" slot="suffix" />
> </a-input>
<a-icon type="search" slot="suffix" />
</a-input>
</template>
</vxe-toolbar>
<a-spin :spinning="loading"> <a-spin :spinning="loading">
<vxe-table <vxe-table

View File

@@ -33,8 +33,8 @@
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="tab_3"> <a-tab-pane key="tab_3">
<span slot="tab"><a-icon type="clock-circle" />{{ $t('cmdb.ci.history') }}</span> <span slot="tab"><a-icon type="clock-circle" />{{ $t('cmdb.ci.history') }}</span>
<div :style="{ padding: '24px', height: '100%' }"> <div :style="{ padding: '16px 24px 24px', height: '100%' }">
<a-space :style="{ 'margin-bottom': '10px', display: 'flex' }"> <a-space :style="{ marginBottom: '16px', display: 'flex' }">
<a-button type="primary" class="ops-button-ghost" ghost @click="handleRollbackCI()"> <a-button type="primary" class="ops-button-ghost" ghost @click="handleRollbackCI()">
<ops-icon type="shishizhuangtai" />{{ $t('cmdb.ci.rollback') }} <ops-icon type="shishizhuangtai" />{{ $t('cmdb.ci.rollback') }}
</a-button> </a-button>
@@ -180,7 +180,7 @@ export default {
ci_types: [], ci_types: [],
hasPermission: true, hasPermission: true,
itsmInstalled: true, itsmInstalled: true,
tableHeight: this.attributeHistoryTableHeight || (this.$store.state.windowHeight - 120), tableHeight: this.attributeHistoryTableHeight || (this.$store.state.windowHeight - 130),
initQueryLoading: true, initQueryLoading: true,
} }
}, },

View File

@@ -95,7 +95,7 @@
attr.name, attr.name,
{ {
rules: [{ required: attr.is_required, message: $t('placeholder1') + `${attr.alias || attr.name}` }], rules: [{ required: attr.is_required, message: $t('placeholder1') + `${attr.alias || attr.name}` }],
initialValue: attr.default && attr.default.default ? attr.default.default : null, initialValue: attr.default && attr.default.default !== undefined && attr.default.default !== null ? attr.default.default : null,
}, },
]" ]"
style="width: 100%" style="width: 100%"
@@ -148,6 +148,7 @@
</template> </template>
<script> <script>
import _ from 'lodash'
import moment from 'moment' import moment from 'moment'
import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue' import JsonEditor from '../../../components/JsonEditor/jsonEditor.vue'
import CIReferenceAttr from '@/components/ciReferenceAttr/index.vue' import CIReferenceAttr from '@/components/ciReferenceAttr/index.vue'
@@ -210,7 +211,7 @@ export default {
}, },
getChoiceDefault(attr) { getChoiceDefault(attr) {
if (!attr?.default?.default) { if (_.isNil(attr?.default?.default)) {
return attr.is_list ? [] : null return attr.is_list ? [] : null
} }

View File

@@ -61,16 +61,20 @@ export default {
justify-content: center; justify-content: center;
font-size: 12px; font-size: 12px;
font-weight: 400; font-weight: 400;
color: #4E5969; color: @text-color_2;
background-color: #F7F8FA; background-color: @primary-color_7;
width: 105px; width: 105px;
height: 32px; height: 32px;
cursor: pointer; cursor: pointer;
&-active { &-active {
border: solid 1px #B1C9FF; border: solid 1px @primary-color_8;
background-color: #E1EFFF; background-color: @primary-color_4;
color: #2F54EB; color: @primary-color;
}
&:hover {
color: @primary-color;
} }
} }
} }

View File

@@ -154,6 +154,17 @@ export default {
margin-left: 6px; margin-left: 6px;
} }
&:hover {
background-color: @primary-color_5;
.attr-ad-tab-edit {
display: inline-block;
}
.attr-ad-tab-delete {
display: inline-block;
}
}
&_active { &_active {
border: solid 1px @primary-color_8; border: solid 1px @primary-color_8;
background-color: @primary-color_6; background-color: @primary-color_6;
@@ -161,14 +172,9 @@ export default {
.attr-ad-tab-name { .attr-ad-tab-name {
color: @primary-color; color: @primary-color;
} }
}
&:hover { &:hover {
.attr-ad-tab-edit { background-color: @primary-color_6;
display: inline-block;
}
.attr-ad-tab-delete {
display: inline-block;
} }
} }
} }
@@ -178,6 +184,11 @@ export default {
background-color: @primary-color_7; background-color: @primary-color_7;
font-size: 12px; font-size: 12px;
color: @text-color_4; color: @text-color_4;
&:hover {
background-color: @primary-color_5;
color: @primary-color;
}
} }
} }
</style> </style>

View File

@@ -3,12 +3,14 @@
<div class="attr-ad-header attr-ad-header-margin">{{ $t('cmdb.ciType.configCheckTitle') }}</div> <div class="attr-ad-header attr-ad-header-margin">{{ $t('cmdb.ciType.configCheckTitle') }}</div>
<div class="attr-ad-content"> <div class="attr-ad-content">
<div class="ad-test-title-info">{{ $t('cmdb.ciType.checkTestTip') }}</div> <div class="ad-test-title-info">{{ $t('cmdb.ciType.checkTestTip') }}</div>
<div <a-button
class="ad-test-btn" type="primary"
class="ops-button-ghost ad-test-btn"
ghost
@click="showCheckModal" @click="showCheckModal"
> >
{{ $t('cmdb.ciType.checkTestBtn') }} {{ $t('cmdb.ciType.checkTestBtn') }}
</div> </a-button>
<div class="ad-test-btn-info">{{ $t('cmdb.ciType.checkTestTip2') }}</div> <div class="ad-test-btn-info">{{ $t('cmdb.ciType.checkTestTip2') }}</div>
<!-- <div <!-- <div
class="ad-test-btn" class="ad-test-btn"
@@ -140,15 +142,6 @@ export default {
.ad-test-btn { .ad-test-btn {
margin-top: 30px; margin-top: 30px;
padding: 5px 12px;
background-color: #F4F9FF;
border: solid 1px @primary-color_8;
display: inline-block;
cursor: pointer;
color: @link-color;
font-size: 12px;
font-weight: 400;
} }
.ad-test-btn-info { .ad-test-btn-info {

View File

@@ -659,7 +659,7 @@ export default {
} else { } else {
this.$nextTick(() => { this.$nextTick(() => {
this.form.setFieldsValue({ this.form.setFieldsValue({
default_value: _record.default && _record.default.default ? _record.default.default : null, default_value: _record?.default?.default ?? null,
}) })
}) })
} }

View File

@@ -1194,7 +1194,7 @@ export default {
.selected { .selected {
background-color: @primary-color_3; background-color: @primary-color_3;
.ci-types-left-detail-title { .ci-types-left-detail-title {
font-weight: 700; color: @primary-color;
} }
} }
} }

View File

@@ -61,13 +61,11 @@
:disable-branch-nodes="true" :disable-branch-nodes="true"
:class="{ :class="{
'custom-treeselect': true, 'custom-treeselect': true,
'custom-treeselect-bgcAndBorder': true, 'custom-treeselect-white': true,
}" }"
:style="{ :style="{
'--custom-height': '32px', '--custom-height': '32px',
lineHeight: '32px', lineHeight: '32px',
'--custom-bg-color': '#fff',
'--custom-border': '1px solid #d9d9d9',
'--custom-multiple-lineHeight': '14px', '--custom-multiple-lineHeight': '14px',
}" }"
v-model="choice_other.type_ids" v-model="choice_other.type_ids"

View File

@@ -198,13 +198,11 @@
:disable-branch-nodes="true" :disable-branch-nodes="true"
:class="{ :class="{
'custom-treeselect': true, 'custom-treeselect': true,
'custom-treeselect-bgcAndBorder': true, 'custom-treeselect-white': true,
}" }"
:style="{ :style="{
'--custom-height': '32px', '--custom-height': '32px',
lineHeight: '32px', lineHeight: '32px',
'--custom-bg-color': '#fff',
'--custom-border': '1px solid #d9d9d9',
'--custom-multiple-lineHeight': '14px', '--custom-multiple-lineHeight': '14px',
}" }"
v-model="selectedBot" v-model="selectedBot"

View File

@@ -195,12 +195,14 @@
</template> </template>
</div> </div>
<a-form-model-item <a-form-model-item
:label="$t('cmdb.custom_dashboard.showIcon')"
prop="showIcon" prop="showIcon"
:label-col="{ span: 5 }" :label-col="{ span: 0 }"
:wrapper-col="{ span: 18 }" :wrapper-col="{ span: 23 }"
> >
<a-switch v-model="form.showIcon"></a-switch> <div class="chart-left-show-icon">
<span class="chart-left-show-icon-label">{{ $t('cmdb.custom_dashboard.showIcon') }}:</span>
<a-switch v-model="form.showIcon"></a-switch>
</div>
</a-form-model-item> </a-form-model-item>
</a-form-model> </a-form-model>
</div> </div>
@@ -733,6 +735,9 @@ export default {
width: 92%; width: 92%;
position: relative; position: relative;
padding: 12px; padding: 12px;
margin-top: 4px;
display: inline-block;
.chart-left-preview-operation { .chart-left-preview-operation {
color: #86909c; color: #86909c;
position: absolute; position: absolute;
@@ -753,12 +758,26 @@ export default {
background-position-x: center; background-position-x: center;
background-position-y: center; background-position-y: center;
} }
&-show-icon {
display: flex;
align-items: center;
&-label {
flex-shrink: 0;
margin-right: 8px;
}
}
} }
.chart-right { .chart-right {
width: 50%; width: 50%;
h4 { h4 {
font-weight: 700; font-weight: 700;
color: #000; color: #000;
&:not(:first-child) {
margin-top: 14px;
}
} }
.chart-right-type { .chart-right-type {
display: flex; display: flex;
@@ -797,7 +816,7 @@ export default {
<style lang="less"> <style lang="less">
.chart-wrapper { .chart-wrapper {
.ant-form-item { .ant-form-item {
margin-bottom: 0; margin-bottom: 8px;
} }
} }
</style> </style>

View File

@@ -170,6 +170,7 @@ export default {
position: relative; position: relative;
margin-bottom: 40px; margin-bottom: 40px;
margin-right: 40px; margin-right: 40px;
cursor: pointer;
&-inner { &-inner {
position: absolute; position: absolute;
@@ -294,6 +295,12 @@ export default {
} }
} }
&, &.discovery-card-small {
&:hover {
box-shadow: 0px 6px 20px 0px rgba(122, 140, 204, 0.30);
}
}
&-http { &-http {
width: 263px; width: 263px;
height: 142px; height: 142px;
@@ -305,6 +312,10 @@ export default {
max-width: 30px !important; max-width: 30px !important;
} }
} }
&:hover {
box-shadow: 0px 6px 28px 0px rgba(122, 140, 204, 0.30);
}
} }
} }
.discovery-card-small { .discovery-card-small {
@@ -312,7 +323,7 @@ export default {
height: 80px; height: 80px;
cursor: pointer; cursor: pointer;
} }
.discovery-card-small:hover,
.discovery-card-small-selected { .discovery-card-small-selected {
.discovery-top { .discovery-top {
background-color: #f0f1f5; background-color: #f0f1f5;

View File

@@ -25,18 +25,24 @@
:fileList="[]" :fileList="[]"
:beforeUpload="beforeUpload" :beforeUpload="beforeUpload"
> >
<a class="setting-discovery-header-action-btn"> <a-button
type="primary"
class="ops-button-ghost"
ghost
>
<a-icon type="upload" /> <a-icon type="upload" />
{{ $t('cmdb.ad.upload') }} {{ $t('cmdb.ad.upload') }}
</a> </a-button>
</a-upload> </a-upload>
<a <a-button
type="primary"
class="ops-button-ghost"
ghost
@click="download" @click="download"
class="setting-discovery-header-action-btn"
> >
<a-icon type="download" /> <a-icon type="download" />
{{ $t('cmdb.ad.download') }} {{ $t('cmdb.ad.download') }}
</a> </a-button>
</div> </div>
</div> </div>
<div <div

View File

@@ -46,10 +46,15 @@
<span @click="batchDelete">{{ $t('delete') }}</span> <span @click="batchDelete">{{ $t('delete') }}</span>
<span>{{ $t('cmdb.ci.selectRows', { rows: selectedCount }) }}</span> <span>{{ $t('cmdb.ci.selectRows', { rows: selectedCount }) }}</span>
</span> </span>
<div @click="clickLog" class="discovery-ci-log"> <a-button
type="primary"
ghost
class="ops-button-ghost discovery-ci-log"
@click="clickLog"
>
<ops-icon type="a-cmdb-log1" /> <ops-icon type="a-cmdb-log1" />
<span>{{ $t('cmdb.ad.log') }}</span> <span>{{ $t('cmdb.ad.log') }}</span>
</div> </a-button>
</div> </div>
<ops-table <ops-table
show-overflow show-overflow
@@ -458,16 +463,7 @@ export default {
} }
.discovery-ci-log { .discovery-ci-log {
cursor: pointer;
background-color: #F4F9FF;
border: solid 1px @primary-color_8;
color: @primary-color;
font-size: 12px;
padding: 5px 12px;
margin-left: auto; margin-left: auto;
display: flex;
align-items: center;
gap: 6px;
} }
.checkbox-hover-table { .checkbox-hover-table {

View File

@@ -1,5 +1,5 @@
<template> <template>
<div> <div class="operation-history">
<a-card :bordered="false"> <a-card :bordered="false">
<a-tabs default-active-key="1"> <a-tabs default-active-key="1">
<a-tab-pane key="1" :tab="$t('cmdb.history.ciChange')"> <a-tab-pane key="1" :tab="$t('cmdb.history.ciChange')">
@@ -40,4 +40,10 @@ export default {
} }
</script> </script>
<style></style> <style lang="less" scoped>
.operation-history {
/deep/ .ant-tabs-tab {
padding-top: 0px;
}
}
</style>

View File

@@ -1,6 +1,11 @@
<template> <template>
<div> <div>
<a-form :colon="false"> <a-form
:colon="false"
:labelCol="{ span:4 }"
:wrapperCol="{ span:20 }"
labelAlign="left"
>
<a-row :gutter="24"> <a-row :gutter="24">
<a-col <a-col
:sm="24" :sm="24"
@@ -10,12 +15,7 @@
v-for="attr in attrList.slice(0,3)" v-for="attr in attrList.slice(0,3)"
:key="attr.name" :key="attr.name"
> >
<a-form-item <a-form-item :label="attr.alias || attr.name">
:label="attr.alias || attr.name "
:labelCol="{span:4}"
:wrapperCol="{span:20}"
labelAlign="right"
>
<a-select <a-select
v-model="queryParams[attr.name]" v-model="queryParams[attr.name]"
:placeholder="$t('cmdb.history.pleaseSelect')" :placeholder="$t('cmdb.history.pleaseSelect')"
@@ -57,12 +57,7 @@
:key="'expand_' + item.name" :key="'expand_' + item.name"
v-for="item in attrList.slice(3)" v-for="item in attrList.slice(3)"
> >
<a-form-item <a-form-item :label="item.alias || item.name">
:label="item.alias || item.name"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 20 }"
labelAlign="right"
>
<a-select <a-select
v-model="queryParams[item.name]" v-model="queryParams[item.name]"
:placeholder="$t('cmdb.history.pleaseSelect')" :placeholder="$t('cmdb.history.pleaseSelect')"
@@ -97,7 +92,7 @@
</template> </template>
</a-row> </a-row>
<a-row> <a-row>
<a-col :span="24" :style="{ textAlign: 'right' , marginBottom: '10px' }"> <a-col :span="24" :style="{ textAlign: 'right', marginBottom: '20px', marginTop: '-4px'}">
<a-button type="primary" html-type="submit" @click="handleSearch"> <a-button type="primary" html-type="submit" @click="handleSearch">
{{ $t('query') }} {{ $t('query') }}
</a-button> </a-button>

View File

@@ -114,7 +114,11 @@
:placeholder="$t('cmdb.preference.searchPlaceholder')" :placeholder="$t('cmdb.preference.searchPlaceholder')"
/> />
<div v-for="group in filterCiTypeData" :key="group.id"> <div v-for="group in filterCiTypeData" :key="group.id">
<p @click="changeGroupExpand(group)" :style="{ display: 'inline-block', cursor: 'pointer' }"> <p
@click="changeGroupExpand(group)"
:style="{ display: 'inline-block', cursor: 'pointer' }"
class="cmdb-preference-right-group-title"
>
<a-icon :type="expandKeys.includes(group.id) ? 'caret-down' : 'caret-right'" />{{ group.name }}({{ <a-icon :type="expandKeys.includes(group.id) ? 'caret-down' : 'caret-right'" />{{ group.name }}({{
group.ci_types ? group.ci_types.length : 0 group.ci_types ? group.ci_types.length : 0
}}) }})
@@ -624,6 +628,16 @@ export default {
flex: 1; flex: 1;
height: 100%; height: 100%;
padding-top: 24px; padding-top: 24px;
&-group-title {
width: 300px;
margin-bottom: 20px;
&:hover {
color: @primary-color;
}
}
.cmdb-preference-content { .cmdb-preference-content {
display: flex; display: flex;
flex-direction: row; flex-direction: row;

View File

@@ -1697,6 +1697,10 @@ export default {
background-color: #fff; background-color: #fff;
padding: 20px; padding: 20px;
border-radius: @border-radius-box; border-radius: @border-radius-box;
.ant-tabs-tab {
padding-top: 0px;
}
} }
} }
</style> </style>

View File

@@ -19,13 +19,11 @@
> >
<treeselect <treeselect
:value="selectCITypeIds" :value="selectCITypeIds"
class="custom-treeselect custom-treeselect-bgcAndBorder filter-content-ciTypes" class="custom-treeselect custom-treeselect-white filter-content-ciTypes"
:style="{ :style="{
width: '400px', width: '400px',
zIndex: '1000', zIndex: '1000',
'--custom-height': '32px', '--custom-height': '32px',
'--custom-bg-color': '#FFF',
'--custom-border': '1px solid #d9d9d9',
'--custom-multiple-lineHeight': '32px', '--custom-multiple-lineHeight': '32px',
}" }"
:multiple="true" :multiple="true"

View File

@@ -78,21 +78,3 @@ export default {
} }
} }
</script> </script>
<style lang="less" scoped>
.save-condition-modal {
/deep/ .ant-modal-close-x {
width: 48px;
height: 48px;
line-height: 48px;
}
/deep/ .ant-modal-body {
padding: 24px 18px;
}
/deep/ .ant-modal-footer {
padding: 10px 18px 18px;
}
}
</style>

View File

@@ -512,15 +512,31 @@ export default {
padding: 14px 18px; padding: 14px 18px;
width: 500px; width: 500px;
} }
/deep/ .filter-content-ciTypes {
&:not(.vue-treeselect--disabled):not(.vue-treeselect--focused) {
.vue-treeselect__control {
border: solid 1px transparent;
&:hover {
border-color: @primary-color;
}
}
}
}
} }
&-input { &-input {
width: 100%; width: 100%;
/deep/ .ant-input { /deep/ .ant-input {
border: none; border: solid 1px transparent;
box-shadow: none; box-shadow: none;
cursor: pointer; cursor: pointer;
&:hover {
border-color: @primary-color;
}
} }
&-suffix { &-suffix {
@@ -532,8 +548,12 @@ export default {
width: 100%; width: 100%;
/deep/ .ant-select-selection { /deep/ .ant-select-selection {
border: none; border: solid 1px transparent;
box-shadow: none; box-shadow: none;
&:hover {
border-color: @primary-color;
}
} }
} }

View File

@@ -164,8 +164,12 @@ export default {
/deep/ & > input { /deep/ & > input {
height: 100%; height: 100%;
margin-left: 10px; margin-left: 10px;
border: none; border: solid 1px transparent;
box-shadow: none; box-shadow: none;
&:focus {
border-color: @primary-color;
}
} }
} }

View File

@@ -116,6 +116,10 @@ body {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
margin-right: 10px; margin-right: 10px;
&:hover {
color: @primary-color;
}
} }
.topmenu { .topmenu {
@@ -560,7 +564,7 @@ body {
top: 10px; top: 10px;
border-radius: 5px; border-radius: 5px;
&:hover { &:hover {
background: #c1bfbf; color: @primary-color;
} }
.anticon { .anticon {
margin-right: 0; margin-right: 0;
@@ -661,7 +665,7 @@ body {
margin-right: 5px !important; margin-right: 5px !important;
} }
&:hover { &:hover {
background-color: #e7e7e7; background-color: @primary-color_5;
} }
} }
} }
@@ -869,6 +873,10 @@ body {
align-items: center; align-items: center;
height: 30px; height: 30px;
} }
&:hover {
border-color: #597ef7 !important;
}
} }
.vue-treeselect__placeholder, .vue-treeselect__placeholder,
.vue-treeselect__single-value { .vue-treeselect__single-value {
@@ -885,15 +893,46 @@ body {
border-radius: 2px !important; border-radius: 2px !important;
border-color: #e4e7ed; border-color: #e4e7ed;
} }
&:hover {
border-color: #597ef7 !important;
}
} }
.custom-vue-treeselect__control(@bgColor:@primary-color_7,@border:none) { .custom-vue-treeselect__control(
background-color: @bgColor; @bgColor: @primary-color_7,
border: @border; @border: none,
} @hoverBgColor: none,
.custom-treeselect { @hoverBorderColor: none,
) {
.vue-treeselect__control {
background-color: @bgColor;
border: @border;
}
.mixin(@borderColor) when (iscolor(@borderColor)) {
border-color: @borderColor !important;
}
&:not(.vue-treeselect--disabled):not(.vue-treeselect--focused) {
.vue-treeselect__control {
&:hover {
background-color: if(iscolor(@hoverBgColor), @hoverBgColor, @bgColor);
.mixin(@hoverBorderColor)
}
}
}
}
.custom-treeselect {
.custom-vue-treeselect__control(
@primary-color_7,
none,
@primary-color_5,
none
);
.vue-treeselect__control { .vue-treeselect__control {
.custom-vue-treeselect__control();
height: var(--custom-height) !important; height: var(--custom-height) !important;
border-radius: 2px !important; border-radius: 2px !important;
} }
@@ -909,12 +948,34 @@ body {
.vue-treeselect__limit-tip-text { .vue-treeselect__limit-tip-text {
margin: 0; margin: 0;
} }
&.vue-treeselect--focused {
.vue-treeselect__control {
border: 1px solid @primary-color;
}
}
} }
// 自定义背景颜色和border // 自定义背景颜色和border
.custom-treeselect-bgcAndBorder .vue-treeselect__control { .custom-treeselect-bgcAndBorder {
.custom-vue-treeselect__control(var(--custom-bg-color), var(--custom-border)); .custom-vue-treeselect__control(
var(--custom-bg-color),
var(--custom-border),
var(--custom-hover-bg-color),
var(--custom-hover-border-color)
);
} }
// 白色背景
.custom-treeselect-white {
.custom-vue-treeselect__control(
#fff,
1px solid #d9d9d9,
none,
@primary-color
);
}
// 自定义背景颜色和border // 自定义背景颜色和border
.custom-treeselect.vue-treeselect--multi { .custom-treeselect.vue-treeselect--multi {
.vue-treeselect__multi-value, .vue-treeselect__multi-value,
@@ -1025,6 +1086,20 @@ body {
background-color: @primary-color_7; background-color: @primary-color_7;
border: none; border: none;
} }
&,
.ant-input,
.ant-time-picker-input {
&:not([disabled]) {
&:hover {
background-color: @primary-color_5;
}
&:focus {
background-color: @primary-color_7;
}
}
}
} }
.ops-input.ant-input { .ops-input.ant-input {
background-color: @primary-color_7; background-color: @primary-color_7;
@@ -1163,13 +1238,31 @@ body {
} }
// button // button
.ops-button-ghost.ant-btn-background-ghost.ant-btn-primary { .ops-button-ghost {
background-color: @primary-color_5!important; position: relative;
border-color: @primary-color_8; overflow: hidden;
&.ant-btn-background-ghost.ant-btn-primary {
border-color: @primary-color_8;
background-color: @primary-color_5 !important;
box-shadow: none;
.btn-wave-hover(@primary-color_4);
&[disabled] {
border-color: #d9d9d9;
background-color: @primary-color_7!important;
}
&:not([disabled]):hover {
color: #3F75FF;
border-color: transparent;
}
}
} }
.ops-button-ghost.ant-btn-background-ghost.ant-btn-primary[disabled] {
border-color: #d9d9d9; .ant-btn-primary:not(.ant-btn-background-ghost) {
background-color: @primary-color_7!important; .btn-wave-hover(#3F75FF);
} }
// button // button
@@ -1214,15 +1307,25 @@ body {
//modal //modal
.ant-modal-content { .ant-modal-content {
.ant-modal-close-x {
width: 56px;
height: 56px;
line-height: 56px;
}
.ant-modal-header { .ant-modal-header {
border-bottom: none; border-bottom: none;
padding: 22px 22px;
.ant-modal-title { .ant-modal-title {
padding-left: 10px; padding-left: 10px;
border-left: 4px solid @primary-color; border-left: 4px solid @primary-color;
} }
} }
.ant-modal-footer { .ant-modal-footer {
border-top: none; border-top: none;
padding: 16px 22px 22px;
} }
} }

View File

@@ -70,3 +70,46 @@
background-color: @primary-color_3; background-color: @primary-color_3;
color: @primary-color; color: @primary-color;
} }
.btn-wave-hover(
@hoverBgColor,
@bgZIndex: 0
) {
position: relative;
overflow: hidden;
& > * {
position: relative;
z-index: 1;
}
&:not([disabled])::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
z-index: @bgZIndex;
width: 100%;
aspect-ratio: 1/1;
border-radius: 50%;
background-color: @hoverBgColor;
-webkit-transform: scale(0) translate(-50%, -50%);
-ms-transform: scale(0) translate(-50%, -50%);
transform: scale(0) translate(-50%, -50%);
transform-origin: left top;
-webkit-transition: -webkit-transform 0.3s ease-out;
transition: -webkit-transform 0.3s ease-out;
transition: transform 0.3s ease-out;
transition: transform 0.3s ease-out, -webkit-transform 0.3s ease-out;
}
&:not([disabled]):hover {
&::after {
-webkit-transform: scale(2) translate(-50%, -50%);
-ms-transform: scale(2) translate(-50%, -50%);
transform: scale(2) translate(-50%, -50%);
}
}
}