Merge branch 'master' into fix_bug_1217

This commit is contained in:
pycook 2024-12-20 16:46:48 +08:00 committed by GitHub
commit c3ae1f3ffa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 371 additions and 100 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

@ -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

@ -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

@ -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

@ -107,3 +107,12 @@ FROM
WHERE c_value_index_datetime.value LIKE "{0}") AS {1} WHERE c_value_index_datetime.value LIKE "{0}") AS {1}
GROUP BY {1}.ci_id GROUP BY {1}.ci_id
""" """
QUERY_CI_BY_NO_ATTR_IN = """
SELECT *
FROM
(SELECT c_value_index_texts.ci_id
FROM c_value_index_texts
WHERE c_value_index_texts.value in ({0})) AS {1}
GROUP BY {1}.ci_id
"""

View File

@ -27,6 +27,7 @@ from api.lib.cmdb.search.ci.db.query_sql import FACET_QUERY
from api.lib.cmdb.search.ci.db.query_sql import QUERY_CI_BY_ATTR_NAME from api.lib.cmdb.search.ci.db.query_sql import QUERY_CI_BY_ATTR_NAME
from api.lib.cmdb.search.ci.db.query_sql import QUERY_CI_BY_ID from api.lib.cmdb.search.ci.db.query_sql import QUERY_CI_BY_ID
from api.lib.cmdb.search.ci.db.query_sql import QUERY_CI_BY_NO_ATTR from api.lib.cmdb.search.ci.db.query_sql import QUERY_CI_BY_NO_ATTR
from api.lib.cmdb.search.ci.db.query_sql import QUERY_CI_BY_NO_ATTR_IN
from api.lib.cmdb.search.ci.db.query_sql import QUERY_CI_BY_TYPE from api.lib.cmdb.search.ci.db.query_sql import QUERY_CI_BY_TYPE
from api.lib.cmdb.search.ci.db.query_sql import QUERY_UNION_CI_ATTRIBUTE_IS_NULL from api.lib.cmdb.search.ci.db.query_sql import QUERY_UNION_CI_ATTRIBUTE_IS_NULL
from api.lib.cmdb.utils import TableMap from api.lib.cmdb.utils import TableMap
@ -527,10 +528,15 @@ class Search(object):
for q in queries: for q in queries:
_query_sql = "" _query_sql = ""
if isinstance(q, dict): if isinstance(q, dict):
alias, _query_sql, operator = self.__query_build_by_field(q['queries'], True, True, alias, is_sub=True) current_app.logger.debug("Dict query content: queries=%s, operator=%s", q['queries'], q['operator'])
# current_app.logger.info(_query_sql) if len(q['queries']) == 1 and ";" in q['queries'][0]:
# current_app.logger.info((operator, is_first, alias)) values = q['queries'][0].split(";")
operator = q['operator'] in_values = ",".join("'{0}'".format(v) for v in values)
_query_sql = QUERY_CI_BY_NO_ATTR_IN.format(in_values, alias)
operator = q['operator']
else:
alias, _query_sql, operator = self.__query_build_by_field(q['queries'], True, True, alias, is_sub=True)
operator = q['operator']
elif ":" in q and not q.startswith("*"): elif ":" in q and not q.startswith("*"):
alias, _query_sql, operator = self.__query_by_attr(q, queries, alias, is_sub) alias, _query_sql, operator = self.__query_by_attr(q, queries, alias, is_sub)

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

@ -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

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

@ -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

@ -314,6 +314,9 @@ const cmdb_en = {
enum: 'Enum', enum: 'Enum',
ciGrantTip: `Filter conditions can be changed dynamically using {{}} referenced variables, currently user variables are supported, such as {{user.uid}},{{user.username}},{{user.email}},{{user.nickname}}`, ciGrantTip: `Filter conditions can be changed dynamically using {{}} referenced variables, currently user variables are supported, such as {{user.uid}},{{user.username}},{{user.email}},{{user.nickname}}`,
searchInputTip: 'Please search for resource keywords', searchInputTip: 'Please search for resource keywords',
columnSearchInputTip: '192.168.1.1\n192.168.1.2\n192.168.1.3',
rowSearchMode: 'Single Row Search',
columnSearchMode: 'Multi Row Search',
resourceSearch: 'Resource Search', resourceSearch: 'Resource Search',
recentSearch: 'Recent Search', recentSearch: 'Recent Search',
myCollection: 'My Collection', myCollection: 'My Collection',

View File

@ -314,6 +314,9 @@ const cmdb_zh = {
enum: '枚举', enum: '枚举',
ciGrantTip: `筛选条件可使用{{}}引用变量实现动态变化,目前支持用户变量,如{{user.uid}},{{user.username}},{{user.email}},{{user.nickname}}`, ciGrantTip: `筛选条件可使用{{}}引用变量实现动态变化,目前支持用户变量,如{{user.uid}},{{user.username}},{{user.email}},{{user.nickname}}`,
searchInputTip: '请搜索资源关键字', searchInputTip: '请搜索资源关键字',
columnSearchInputTip: '192.168.1.1\n192.168.1.2\n192.168.1.3',
rowSearchMode: '单行搜索',
columnSearchMode: '多行搜索',
resourceSearch: '资源搜索', resourceSearch: '资源搜索',
recentSearch: '最近搜索', recentSearch: '最近搜索',
myCollection: '我的收藏', myCollection: '我的收藏',

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

@ -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

@ -180,7 +180,7 @@ export default {
saveCondition(isSubmit) { saveCondition(isSubmit) {
this.$refs.conditionFilterRef.handleSubmit() this.$refs.conditionFilterRef.handleSubmit()
this.$nextTick(() => { this.$nextTick(() => {
this.$emit('saveCondition', isSubmit) this.$emit('saveCondition', isSubmit, this.$parent.isColumnSearch ? 'column' : 'normal')
this.visible = false this.visible = false
}) })
}, },

View File

@ -1,29 +1,59 @@
<template> <template>
<div :class="['search-input', classType ? 'search-input-' + classType : '']"> <div :class="['search-input', classType ? 'search-input-' + classType : '', { 'column-search-mode': isColumnSearch }]">
<a-input <div class="search-area">
:value="searchValue" <div v-show="!isColumnSearch" class="input-wrapper">
class="search-input-component" <a-input
:placeholder="$t('cmdb.ciType.searchInputTip')" :value="searchValue"
@change="handleChangeSearchValue" class="search-input-component"
@pressEnter="saveCondition(true)" :placeholder="$t('cmdb.ciType.searchInputTip')"
> @change="handleChangeSearchValue"
<a-icon @pressEnter="saveCondition(true, 'normal')"
class="search-input-component-icon" />
slot="prefix" <a-icon
type="search" class="search-icon"
@click="saveCondition(true)" type="search"
/> @click="saveCondition(true, 'normal')"
</a-input> />
<FilterPopover </div>
ref="filterPpoverRef"
:CITypeGroup="CITypeGroup" <div v-show="isColumnSearch" class="textarea-wrapper">
:allAttributesList="allAttributesList" <div class="textarea-container">
:expression="expression" <a-textarea
:selectCITypeIds="selectCITypeIds" :value="searchValue"
@changeFilter="changeFilter" class="column-search-component"
@updateAllAttributesList="updateAllAttributesList" :rows="4"
@saveCondition="saveCondition" :placeholder="$t('cmdb.ciType.columnSearchInputTip')"
/> @change="handleChangeColumnSearchValue"
@pressEnter="handlePressEnter"
/>
<a-icon
class="search-icon"
type="search"
@click="saveCondition(true, 'column')"
/>
</div>
</div>
<div class="operation-area">
<FilterPopover
ref="filterPpoverRef"
:CITypeGroup="CITypeGroup"
:allAttributesList="allAttributesList"
:expression="expression"
:selectCITypeIds="selectCITypeIds"
@changeFilter="changeFilter"
@updateAllAttributesList="updateAllAttributesList"
@saveCondition="saveCondition"
/>
<div class="column-search-btn" @click="toggleColumnSearch">
<a-icon class="column-search-btn-icon" type="menu" />
<span class="column-search-btn-title">
{{ isColumnSearch ? $t('cmdb.ciType.rowSearchMode') : $t('cmdb.ciType.columnSearchMode') }}
</span>
</div>
</div>
</div>
<div v-if="copyText" class="expression-display"> <div v-if="copyText" class="expression-display">
<span class="expression-display-text">{{ copyText }}</span> <span class="expression-display-text">{{ copyText }}</span>
@ -69,11 +99,12 @@ export default {
classType: { classType: {
type: String, type: String,
default: '' default: ''
},
isColumnSearch: {
type: Boolean,
default: false
} }
}, },
data() {
return {}
},
computed: { computed: {
// 复制文字展示与实际文本复制内容区别在于未选择模型时不展示所有模型拼接数据 // 复制文字展示与实际文本复制内容区别在于未选择模型时不展示所有模型拼接数据
copyText() { copyText() {
@ -88,7 +119,14 @@ export default {
textArray.push(exp) textArray.push(exp)
} }
if (this.searchValue) { if (this.searchValue) {
textArray.push(`*${this.searchValue}*`) let processedValue = this.searchValue
if (this.isColumnSearch) {
const values = this.searchValue.split('\n').filter(v => v.trim())
if (values.length) {
processedValue = `(${values.join(';')})`
}
}
textArray.push(`${!this.isColumnSearch ? '*' : ''}${processedValue}${!this.isColumnSearch ? '*' : ''}`)
} }
return textArray.length ? `q=${textArray.join(',')}` : '' return textArray.length ? `q=${textArray.join(',')}` : ''
@ -98,8 +136,8 @@ export default {
updateAllAttributesList(value) { updateAllAttributesList(value) {
this.$emit('updateAllAttributesList', value) this.$emit('updateAllAttributesList', value)
}, },
saveCondition(isSubmit) { saveCondition(isSubmit, searchType = 'normal') {
this.$emit('saveCondition', isSubmit) this.$emit('saveCondition', isSubmit, searchType)
}, },
handleChangeSearchValue(e) { handleChangeSearchValue(e) {
const value = e.target.value const value = e.target.value
@ -125,7 +163,9 @@ export default {
ciTypeIds.push(...ids) ciTypeIds.push(...ids)
}) })
} }
const copyText = `${ciTypeIds?.length ? `_type:(${ciTypeIds.join(';')})` : ''}${exp ? `,${exp}` : ''}${searchValue ? `,*${searchValue}*` : ''}` const copyText = `${ciTypeIds?.length ? `_type:(${ciTypeIds.join(';')})` : ''}${exp ? `,${exp}` : ''}${
searchValue ? `,${!this.isColumnSearch ? '*' : ''}${searchValue}${!this.isColumnSearch ? '*' : ''}` : ''
}`
this.$copyText(copyText) this.$copyText(copyText)
.then(() => { .then(() => {
@ -134,6 +174,35 @@ export default {
.catch(() => { .catch(() => {
this.$message.error(this.$t('cmdb.ci.copyFailed')) this.$message.error(this.$t('cmdb.ci.copyFailed'))
}) })
},
toggleColumnSearch() {
this.$emit('toggleSearchMode', !this.isColumnSearch)
this.saveCondition(false, !this.isColumnSearch ? 'column' : 'normal')
},
handleChangeColumnSearchValue(e) {
const value = e.target.value
this.changeFilter({
name: 'searchValue',
value
})
},
handlePressEnter(e) {
if (this.isColumnSearch) {
// 列搜索模式下按下 Enter 键时阻止默认行为并插入换行符
e.preventDefault()
const value = this.searchValue || ''
const cursorPosition = e.target.selectionStart
const newValue = value.slice(0, cursorPosition) + '\n' + value.slice(cursorPosition)
this.changeFilter({
name: 'searchValue',
value: newValue
})
} else {
this.saveCondition(true, 'normal')
}
} }
} }
} }
@ -142,43 +211,107 @@ export default {
<style lang="less" scoped> <style lang="less" scoped>
.search-input { .search-input {
width: 100%; width: 100%;
height: 48px;
display: flex; display: flex;
align-items: center; flex-direction: column;
justify-content: space-between; gap: 8px;
margin-bottom: 16px;
&-component { .search-area {
height: 100%; display: flex;
align-items: flex-start;
min-height: 48px;
width: 100%;
}
.input-wrapper {
position: relative;
flex-grow: 1; flex-grow: 1;
background-color: #FFFFFF;
border: none;
font-size: 14px;
border-radius: 48px;
overflow: hidden;
&-icon { .search-input-component {
color: #2F54EB; height: 48px;
width: 100%;
background-color: #FFFFFF;
border: 1px solid #d9d9d9;
font-size: 14px; font-size: 14px;
border-radius: 8px;
/deep/ input {
height: 100%;
padding-right: 40px;
}
} }
/deep/ & > input { .search-icon {
height: 100%; position: absolute;
margin-left: 10px; right: 12px;
border: solid 1px transparent; top: 50%;
box-shadow: none; transform: translateY(-50%);
color: #2F54EB;
font-size: 14px;
cursor: pointer;
}
}
&:focus { .textarea-wrapper {
border-color: @primary-color; flex-grow: 1;
.textarea-container {
position: relative;
width: 100%;
max-height: 200px;
.column-search-component {
width: 100%;
max-height: 200px;
background-color: #FFFFFF;
border: 1px solid #d9d9d9;
font-size: 14px;
border-radius: 8px;
padding-right: 35px;
resize: none;
transition: all 0.3s;
&:hover, &:focus {
border-color: #40a9ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
}
.search-icon {
position: absolute;
right: 12px;
top: 12px;
color: #2F54EB;
font-size: 14px;
cursor: pointer;
} }
} }
} }
&-after { .operation-area {
height: 38px; display: flex;
justify-content: flex-start; align-items: center;
height: 48px;
margin-left: 10px;
}
.search-input-component { .column-search-btn {
max-width: 524px; flex-shrink: 0;
display: flex;
align-items: center;
margin-left: 13px;
cursor: pointer;
&-icon {
color: #2F54EB;
font-size: 12px;
}
&-title {
font-size: 14px;
font-weight: 400;
color: #2F54EB;
margin-left: 3px;
} }
} }
@ -201,5 +334,18 @@ export default {
cursor: pointer; cursor: pointer;
} }
} }
.search-input-component,
.column-search-component {
&:hover {
border-color: #40a9ff;
}
&:focus {
border-color: #40a9ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
outline: none;
}
}
} }
</style> </style>

View File

@ -15,9 +15,11 @@
:searchValue="searchValue" :searchValue="searchValue"
:selectCITypeIds="selectCITypeIds" :selectCITypeIds="selectCITypeIds"
:expression="expression" :expression="expression"
:isColumnSearch="currentSearchType === 'column'"
@changeFilter="changeFilter" @changeFilter="changeFilter"
@updateAllAttributesList="updateAllAttributesList" @updateAllAttributesList="updateAllAttributesList"
@saveCondition="saveCondition" @saveCondition="saveCondition"
@toggleSearchMode="handleToggleSearchMode"
/> />
<HistoryList <HistoryList
:recentList="recentList" :recentList="recentList"
@ -46,9 +48,11 @@
:searchValue="searchValue" :searchValue="searchValue"
:selectCITypeIds="selectCITypeIds" :selectCITypeIds="selectCITypeIds"
:expression="expression" :expression="expression"
:isColumnSearch="currentSearchType === 'column'"
@changeFilter="changeFilter" @changeFilter="changeFilter"
@updateAllAttributesList="updateAllAttributesList" @updateAllAttributesList="updateAllAttributesList"
@saveCondition="saveCondition" @saveCondition="saveCondition"
@toggleSearchMode="handleToggleSearchMode"
/> />
<HistoryList <HistoryList
:recentList="recentList" :recentList="recentList"
@ -172,6 +176,7 @@ export default {
showInstanceDetail: false, showInstanceDetail: false,
detailCIId: -1, detailCIId: -1,
detailCITypeId: -1, detailCITypeId: -1,
currentSearchType: 'normal',
} }
}, },
computed: { computed: {
@ -240,7 +245,9 @@ export default {
} }
}, },
async saveCondition(isSubmit) { async saveCondition(isSubmit, searchType = 'normal') {
this.currentSearchType = searchType
if ( if (
this.searchValue || this.searchValue ||
this.expression || this.expression ||
@ -253,7 +260,8 @@ export default {
if ( if (
option.searchValue === this.searchValue && option.searchValue === this.searchValue &&
option.expression === this.expression && option.expression === this.expression &&
_.isEqual(option.ciTypeIds, this.selectCITypeIds) _.isEqual(option.ciTypeIds, this.selectCITypeIds) &&
option.searchType === this.currentSearchType
) { ) {
needDeleteList.push(item.id) needDeleteList.push(item.id)
} else { } else {
@ -279,7 +287,8 @@ export default {
searchValue: this.searchValue, searchValue: this.searchValue,
expression: this.expression, expression: this.expression,
ciTypeIds: this.selectCITypeIds, ciTypeIds: this.selectCITypeIds,
ciTypeNames ciTypeNames,
searchType: this.currentSearchType
}, },
name: '__recent__' name: '__recent__'
}) })
@ -290,7 +299,7 @@ export default {
this.isSearch = true this.isSearch = true
this.currentPage = 1 this.currentPage = 1
this.hideDetail() this.hideDetail()
this.loadInstance() this.loadInstance(this.currentSearchType)
} }
}, },
@ -307,11 +316,19 @@ export default {
this.getRecentList() this.getRecentList()
}, },
async loadInstance() { async loadInstance(searchType = 'normal') {
const { selectCITypeIds, expression, searchValue } = this const { selectCITypeIds, expression } = this
let { searchValue } = this
const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
const exp = expression.match(regQ) ? expression.match(regQ)[0] : null const exp = expression.match(regQ) ? expression.match(regQ)[0] : null
if (searchType === 'column' && searchValue) {
const values = searchValue.split('\n').filter(v => v.trim())
if (values.length) {
searchValue = `(${values.join(';')})`
}
}
const ciTypeIds = [...selectCITypeIds] const ciTypeIds = [...selectCITypeIds]
if (!ciTypeIds.length) { if (!ciTypeIds.length) {
this.CITypeGroup.forEach((item) => { this.CITypeGroup.forEach((item) => {
@ -322,7 +339,7 @@ export default {
const res = await searchCI({ const res = await searchCI({
q: `${ciTypeIds?.length ? `_type:(${ciTypeIds.join(';')})` : ''}${exp ? `,${exp}` : ''}${ q: `${ciTypeIds?.length ? `_type:(${ciTypeIds.join(';')})` : ''}${exp ? `,${exp}` : ''}${
searchValue ? `,*${searchValue}*` : '' searchValue ? `,${searchType === 'normal' ? '*' : ''}${searchValue}${searchType === 'normal' ? '*' : ''}` : ''
}`, }`,
count: this.pageSize, count: this.pageSize,
page: this.currentPage, page: this.currentPage,
@ -389,7 +406,6 @@ export default {
} }
this.ciTabList = ciTabList this.ciTabList = ciTabList
// 处理引用属性
const allAttr = [] const allAttr = []
subscribedRes.map((item) => { subscribedRes.map((item) => {
allAttr.push(...item.attributes) allAttr.push(...item.attributes)
@ -477,20 +493,21 @@ export default {
this.searchValue = data?.searchValue || '' this.searchValue = data?.searchValue || ''
this.expression = data?.expression || '' this.expression = data?.expression || ''
this.selectCITypeIds = data?.ciTypeIds || [] this.selectCITypeIds = data?.ciTypeIds || []
this.currentSearchType = data?.searchType || 'normal'
this.hideDetail() this.hideDetail()
this.loadInstance() this.loadInstance(this.currentSearchType)
}, },
handlePageSizeChange(_, pageSize) { handlePageSizeChange(_, pageSize) {
this.pageSize = pageSize this.pageSize = pageSize
this.currentPage = 1 this.currentPage = 1
this.loadInstance() this.loadInstance(this.currentSearchType)
}, },
changePage(page) { changePage(page) {
this.currentPage = page this.currentPage = page
this.loadInstance() this.loadInstance(this.currentSearchType)
}, },
changeFilter(data) { changeFilter(data) {
@ -533,6 +550,10 @@ export default {
clickFavor(data) { clickFavor(data) {
this.isSearch = true this.isSearch = true
this.showDetail(data) this.showDetail(data)
},
handleToggleSearchMode(isColumn) {
this.currentSearchType = isColumn ? 'column' : 'normal'
} }
} }
} }