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:
thexqn 2024-12-18 17:21:24 +08:00 committed by GitHub
parent 0c57b2b83d
commit ada23262bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 263 additions and 72 deletions

3
.gitignore vendored
View File

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

View File

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

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_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("*"):

View File

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

View File

@ -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: '我的收藏',

View File

@ -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
})
},

View File

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

View File

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