mirror of https://github.com/veops/cmdb.git
feat(search): implement column search mode and enhance search input functionality (#658)
* chore: update .gitignore to include manage.sh and .env files * feat(api): add new SQL query for CI by no attribute in * feat(api): enhance search functionality with new IN clause support for queries * feat(lang): add new search tips and modes in English and Chinese language files * feat(search): implement column search mode and enhance search input functionality
This commit is contained in:
parent
0c57b2b83d
commit
ada23262bb
|
@ -79,3 +79,6 @@ cmdb-ui/yarn-debug.log*
|
|||
cmdb-ui/yarn-error.log*
|
||||
cmdb-ui/package-lock.json
|
||||
start.sh
|
||||
manage.sh
|
||||
.env
|
||||
cmdb-api/.env
|
||||
|
|
|
@ -107,3 +107,12 @@ FROM
|
|||
WHERE c_value_index_datetime.value LIKE "{0}") AS {1}
|
||||
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
|
||||
"""
|
|
@ -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_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_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_UNION_CI_ATTRIBUTE_IS_NULL
|
||||
from api.lib.cmdb.utils import TableMap
|
||||
|
@ -527,9 +528,14 @@ class Search(object):
|
|||
for q in queries:
|
||||
_query_sql = ""
|
||||
if isinstance(q, dict):
|
||||
current_app.logger.debug("Dict query content: queries=%s, operator=%s", q['queries'], q['operator'])
|
||||
if len(q['queries']) == 1 and ";" in q['queries'][0]:
|
||||
values = q['queries'][0].split(";")
|
||||
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)
|
||||
# current_app.logger.info(_query_sql)
|
||||
# current_app.logger.info((operator, is_first, alias))
|
||||
operator = q['operator']
|
||||
|
||||
elif ":" in q and not q.startswith("*"):
|
||||
|
|
|
@ -314,6 +314,9 @@ const cmdb_en = {
|
|||
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}}`,
|
||||
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',
|
||||
recentSearch: 'Recent Search',
|
||||
myCollection: 'My Collection',
|
||||
|
|
|
@ -314,6 +314,9 @@ const cmdb_zh = {
|
|||
enum: '枚举',
|
||||
ciGrantTip: `筛选条件可使用{{}}引用变量实现动态变化,目前支持用户变量,如{{user.uid}},{{user.username}},{{user.email}},{{user.nickname}}`,
|
||||
searchInputTip: '请搜索资源关键字',
|
||||
columnSearchInputTip: '192.168.1.1\n192.168.1.2\n192.168.1.3',
|
||||
rowSearchMode: '单行搜索',
|
||||
columnSearchMode: '多行搜索',
|
||||
resourceSearch: '资源搜索',
|
||||
recentSearch: '最近搜索',
|
||||
myCollection: '我的收藏',
|
||||
|
|
|
@ -180,7 +180,7 @@ export default {
|
|||
saveCondition(isSubmit) {
|
||||
this.$refs.conditionFilterRef.handleSubmit()
|
||||
this.$nextTick(() => {
|
||||
this.$emit('saveCondition', isSubmit)
|
||||
this.$emit('saveCondition', isSubmit, this.$parent.isColumnSearch ? 'column' : 'normal')
|
||||
this.visible = false
|
||||
})
|
||||
},
|
||||
|
|
|
@ -1,19 +1,40 @@
|
|||
<template>
|
||||
<div :class="['search-input', classType ? 'search-input-' + classType : '']">
|
||||
<div :class="['search-input', classType ? 'search-input-' + classType : '', { 'column-search-mode': isColumnSearch }]">
|
||||
<div class="search-area">
|
||||
<div v-show="!isColumnSearch" class="input-wrapper">
|
||||
<a-input
|
||||
:value="searchValue"
|
||||
class="search-input-component"
|
||||
:placeholder="$t('cmdb.ciType.searchInputTip')"
|
||||
@change="handleChangeSearchValue"
|
||||
@pressEnter="saveCondition(true)"
|
||||
>
|
||||
<a-icon
|
||||
class="search-input-component-icon"
|
||||
slot="prefix"
|
||||
type="search"
|
||||
@click="saveCondition(true)"
|
||||
@pressEnter="saveCondition(true, 'normal')"
|
||||
/>
|
||||
</a-input>
|
||||
<a-icon
|
||||
class="search-icon"
|
||||
type="search"
|
||||
@click="saveCondition(true, 'normal')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-show="isColumnSearch" class="textarea-wrapper">
|
||||
<div class="textarea-container">
|
||||
<a-textarea
|
||||
:value="searchValue"
|
||||
class="column-search-component"
|
||||
:rows="4"
|
||||
: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"
|
||||
|
@ -25,6 +46,15 @@
|
|||
@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">
|
||||
<span class="expression-display-text">{{ copyText }}</span>
|
||||
<a-icon
|
||||
|
@ -69,10 +99,11 @@ export default {
|
|||
classType: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
isColumnSearch: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
// 复制文字展示,与实际文本复制内容区别在于,未选择模型时不展示所有模型拼接数据
|
||||
|
@ -88,7 +119,14 @@ export default {
|
|||
textArray.push(exp)
|
||||
}
|
||||
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(',')}` : ''
|
||||
|
@ -98,8 +136,8 @@ export default {
|
|||
updateAllAttributesList(value) {
|
||||
this.$emit('updateAllAttributesList', value)
|
||||
},
|
||||
saveCondition(isSubmit) {
|
||||
this.$emit('saveCondition', isSubmit)
|
||||
saveCondition(isSubmit, searchType = 'normal') {
|
||||
this.$emit('saveCondition', isSubmit, searchType)
|
||||
},
|
||||
handleChangeSearchValue(e) {
|
||||
const value = e.target.value
|
||||
|
@ -125,7 +163,9 @@ export default {
|
|||
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)
|
||||
.then(() => {
|
||||
|
@ -134,6 +174,35 @@ export default {
|
|||
.catch(() => {
|
||||
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>
|
||||
.search-input {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.search-area {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
min-height: 48px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
|
||||
.search-input-component {
|
||||
height: 48px;
|
||||
width: 100%;
|
||||
background-color: #FFFFFF;
|
||||
border: 1px solid #d9d9d9;
|
||||
font-size: 14px;
|
||||
border-radius: 8px;
|
||||
|
||||
/deep/ input {
|
||||
height: 100%;
|
||||
padding-right: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #2F54EB;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.textarea-wrapper {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.operation-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 48px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
&-component {
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
background-color: #FFFFFF;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
border-radius: 48px;
|
||||
overflow: hidden;
|
||||
.column-search-btn {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 13px;
|
||||
cursor: pointer;
|
||||
|
||||
&-icon {
|
||||
color: #2F54EB;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/deep/ & > input {
|
||||
height: 100%;
|
||||
margin-left: 10px;
|
||||
border: solid 1px transparent;
|
||||
box-shadow: none;
|
||||
|
||||
&:focus {
|
||||
border-color: @primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-after {
|
||||
height: 38px;
|
||||
justify-content: flex-start;
|
||||
|
||||
.search-input-component {
|
||||
max-width: 524px;
|
||||
font-weight: 400;
|
||||
color: #2F54EB;
|
||||
margin-left: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -201,5 +334,18 @@ export default {
|
|||
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>
|
||||
|
|
|
@ -15,9 +15,11 @@
|
|||
:searchValue="searchValue"
|
||||
:selectCITypeIds="selectCITypeIds"
|
||||
:expression="expression"
|
||||
:isColumnSearch="currentSearchType === 'column'"
|
||||
@changeFilter="changeFilter"
|
||||
@updateAllAttributesList="updateAllAttributesList"
|
||||
@saveCondition="saveCondition"
|
||||
@toggleSearchMode="handleToggleSearchMode"
|
||||
/>
|
||||
<HistoryList
|
||||
:recentList="recentList"
|
||||
|
@ -46,9 +48,11 @@
|
|||
:searchValue="searchValue"
|
||||
:selectCITypeIds="selectCITypeIds"
|
||||
:expression="expression"
|
||||
:isColumnSearch="currentSearchType === 'column'"
|
||||
@changeFilter="changeFilter"
|
||||
@updateAllAttributesList="updateAllAttributesList"
|
||||
@saveCondition="saveCondition"
|
||||
@toggleSearchMode="handleToggleSearchMode"
|
||||
/>
|
||||
<HistoryList
|
||||
:recentList="recentList"
|
||||
|
@ -172,6 +176,7 @@ export default {
|
|||
showInstanceDetail: false,
|
||||
detailCIId: -1,
|
||||
detailCITypeId: -1,
|
||||
currentSearchType: 'normal',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -240,7 +245,9 @@ export default {
|
|||
}
|
||||
},
|
||||
|
||||
async saveCondition(isSubmit) {
|
||||
async saveCondition(isSubmit, searchType = 'normal') {
|
||||
this.currentSearchType = searchType
|
||||
|
||||
if (
|
||||
this.searchValue ||
|
||||
this.expression ||
|
||||
|
@ -253,7 +260,8 @@ export default {
|
|||
if (
|
||||
option.searchValue === this.searchValue &&
|
||||
option.expression === this.expression &&
|
||||
_.isEqual(option.ciTypeIds, this.selectCITypeIds)
|
||||
_.isEqual(option.ciTypeIds, this.selectCITypeIds) &&
|
||||
option.searchType === this.currentSearchType
|
||||
) {
|
||||
needDeleteList.push(item.id)
|
||||
} else {
|
||||
|
@ -279,7 +287,8 @@ export default {
|
|||
searchValue: this.searchValue,
|
||||
expression: this.expression,
|
||||
ciTypeIds: this.selectCITypeIds,
|
||||
ciTypeNames
|
||||
ciTypeNames,
|
||||
searchType: this.currentSearchType
|
||||
},
|
||||
name: '__recent__'
|
||||
})
|
||||
|
@ -290,7 +299,7 @@ export default {
|
|||
this.isSearch = true
|
||||
this.currentPage = 1
|
||||
this.hideDetail()
|
||||
this.loadInstance()
|
||||
this.loadInstance(this.currentSearchType)
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -307,11 +316,19 @@ export default {
|
|||
this.getRecentList()
|
||||
},
|
||||
|
||||
async loadInstance() {
|
||||
const { selectCITypeIds, expression, searchValue } = this
|
||||
async loadInstance(searchType = 'normal') {
|
||||
const { selectCITypeIds, expression } = this
|
||||
let { searchValue } = this
|
||||
const regQ = /(?<=q=).+(?=&)|(?<=q=).+$/g
|
||||
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]
|
||||
if (!ciTypeIds.length) {
|
||||
this.CITypeGroup.forEach((item) => {
|
||||
|
@ -322,7 +339,7 @@ export default {
|
|||
|
||||
const res = await searchCI({
|
||||
q: `${ciTypeIds?.length ? `_type:(${ciTypeIds.join(';')})` : ''}${exp ? `,${exp}` : ''}${
|
||||
searchValue ? `,*${searchValue}*` : ''
|
||||
searchValue ? `,${searchType === 'normal' ? '*' : ''}${searchValue}${searchType === 'normal' ? '*' : ''}` : ''
|
||||
}`,
|
||||
count: this.pageSize,
|
||||
page: this.currentPage,
|
||||
|
@ -389,7 +406,6 @@ export default {
|
|||
}
|
||||
this.ciTabList = ciTabList
|
||||
|
||||
// 处理引用属性
|
||||
const allAttr = []
|
||||
subscribedRes.map((item) => {
|
||||
allAttr.push(...item.attributes)
|
||||
|
@ -477,20 +493,21 @@ export default {
|
|||
this.searchValue = data?.searchValue || ''
|
||||
this.expression = data?.expression || ''
|
||||
this.selectCITypeIds = data?.ciTypeIds || []
|
||||
this.currentSearchType = data?.searchType || 'normal'
|
||||
|
||||
this.hideDetail()
|
||||
this.loadInstance()
|
||||
this.loadInstance(this.currentSearchType)
|
||||
},
|
||||
|
||||
handlePageSizeChange(_, pageSize) {
|
||||
this.pageSize = pageSize
|
||||
this.currentPage = 1
|
||||
this.loadInstance()
|
||||
this.loadInstance(this.currentSearchType)
|
||||
},
|
||||
|
||||
changePage(page) {
|
||||
this.currentPage = page
|
||||
this.loadInstance()
|
||||
this.loadInstance(this.currentSearchType)
|
||||
},
|
||||
|
||||
changeFilter(data) {
|
||||
|
@ -533,6 +550,10 @@ export default {
|
|||
clickFavor(data) {
|
||||
this.isSearch = true
|
||||
this.showDetail(data)
|
||||
},
|
||||
|
||||
handleToggleSearchMode(isColumn) {
|
||||
this.currentSearchType = isColumn ? 'column' : 'normal'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue