feat: support exact match filter (#164)

This commit is contained in:
Lykin 2024-03-08 16:52:26 +08:00
parent fdf2c477f2
commit 2d2954d81c
13 changed files with 245 additions and 81 deletions

View File

@ -494,27 +494,68 @@ func (b *browserService) scanKeys(ctx context.Context, client redis.UniversalCli
return keys, cursor, nil return keys, cursor, nil
} }
// check if key exists
func (b *browserService) existsKey(ctx context.Context, client redis.UniversalClient, key, keyType string) bool {
var keyExists atomic.Bool
if cluster, ok := client.(*redis.ClusterClient); ok {
// cluster mode
cluster.ForEachMaster(ctx, func(ctx context.Context, cli *redis.Client) error {
if n := cli.Exists(ctx, key).Val(); n > 0 {
if len(keyType) <= 0 || strings.ToLower(keyType) == cli.Type(ctx, key).Val() {
keyExists.Store(true)
}
}
return nil
})
} else {
if n := client.Exists(ctx, key).Val(); n > 0 {
if len(keyType) <= 0 || strings.ToLower(keyType) == client.Type(ctx, key).Val() {
keyExists.Store(true)
}
}
}
return keyExists.Load()
}
// LoadNextKeys load next key from saved cursor // LoadNextKeys load next key from saved cursor
func (b *browserService) LoadNextKeys(server string, db int, match, keyType string) (resp types.JSResp) { func (b *browserService) LoadNextKeys(server string, db int, match, keyType string, exactMatch bool) (resp types.JSResp) {
item, err := b.getRedisClient(server, db) item, err := b.getRedisClient(server, db)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
if match == "*" {
exactMatch = false
}
client, ctx, count := item.client, item.ctx, item.stepSize client, ctx, count := item.client, item.ctx, item.stepSize
var matchKeys []any
var maxKeys int64
cursor := item.cursor[db] cursor := item.cursor[db]
keys, cursor, err := b.scanKeys(ctx, client, match, keyType, cursor, count) fullScan := match == "*" || match == ""
if exactMatch && !fullScan {
if b.existsKey(ctx, client, match, keyType) {
matchKeys = []any{match}
maxKeys = 1
}
b.setClientCursor(server, db, 0)
} else {
matchKeys, cursor, err = b.scanKeys(ctx, client, match, keyType, cursor, count)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
b.setClientCursor(server, db, cursor) b.setClientCursor(server, db, cursor)
maxKeys := b.loadDBSize(ctx, client) if fullScan {
maxKeys = b.loadDBSize(ctx, client)
} else {
maxKeys = int64(len(matchKeys))
}
}
resp.Success = true resp.Success = true
resp.Data = map[string]any{ resp.Data = map[string]any{
"keys": keys, "keys": matchKeys,
"end": cursor == 0, "end": cursor == 0,
"maxKeys": maxKeys, "maxKeys": maxKeys,
} }
@ -522,7 +563,7 @@ func (b *browserService) LoadNextKeys(server string, db int, match, keyType stri
} }
// LoadNextAllKeys load next all keys // LoadNextAllKeys load next all keys
func (b *browserService) LoadNextAllKeys(server string, db int, match, keyType string) (resp types.JSResp) { func (b *browserService) LoadNextAllKeys(server string, db int, match, keyType string, exactMatch bool) (resp types.JSResp) {
item, err := b.getRedisClient(server, db) item, err := b.getRedisClient(server, db)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
@ -530,25 +571,39 @@ func (b *browserService) LoadNextAllKeys(server string, db int, match, keyType s
} }
client, ctx := item.client, item.ctx client, ctx := item.client, item.ctx
var matchKeys []any
var maxKeys int64
fullScan := match == "*" || match == ""
if exactMatch && !fullScan {
if b.existsKey(ctx, client, match, keyType) {
matchKeys = []any{match}
maxKeys = 1
}
} else {
cursor := item.cursor[db] cursor := item.cursor[db]
keys, _, err := b.scanKeys(ctx, client, match, keyType, cursor, 0) matchKeys, _, err = b.scanKeys(ctx, client, match, keyType, cursor, 0)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
b.setClientCursor(server, db, 0) b.setClientCursor(server, db, 0)
maxKeys := b.loadDBSize(ctx, client) if fullScan {
maxKeys = b.loadDBSize(ctx, client)
} else {
maxKeys = int64(len(matchKeys))
}
}
resp.Success = true resp.Success = true
resp.Data = map[string]any{ resp.Data = map[string]any{
"keys": keys, "keys": matchKeys,
"maxKeys": maxKeys, "maxKeys": maxKeys,
} }
return return
} }
// LoadAllKeys load all keys // LoadAllKeys load all keys
func (b *browserService) LoadAllKeys(server string, db int, match, keyType string) (resp types.JSResp) { func (b *browserService) LoadAllKeys(server string, db int, match, keyType string, exactMatch bool) (resp types.JSResp) {
item, err := b.getRedisClient(server, db) item, err := b.getRedisClient(server, db)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
@ -556,15 +611,23 @@ func (b *browserService) LoadAllKeys(server string, db int, match, keyType strin
} }
client, ctx := item.client, item.ctx client, ctx := item.client, item.ctx
keys, _, err := b.scanKeys(ctx, client, match, keyType, 0, 0) var matchKeys []any
fullScan := match == "*" || match == ""
if exactMatch && !fullScan {
if b.existsKey(ctx, client, match, keyType) {
matchKeys = []any{match}
}
} else {
matchKeys, _, err = b.scanKeys(ctx, client, match, keyType, 0, 0)
if err != nil { if err != nil {
resp.Msg = err.Error() resp.Msg = err.Error()
return return
} }
}
resp.Success = true resp.Success = true
resp.Data = map[string]any{ resp.Data = map[string]any{
"keys": keys, "keys": matchKeys,
} }
return return
} }

View File

@ -86,7 +86,6 @@ func (c *connectionService) buildOption(config types.ConnectionConfig) (*redis.O
if config.SSH.Enable { if config.SSH.Enable {
sshConfig = &ssh.ClientConfig{ sshConfig = &ssh.ClientConfig{
User: config.SSH.Username, User: config.SSH.Username,
Auth: []ssh.AuthMethod{ssh.Password(config.SSH.Password)},
HostKeyCallback: ssh.InsecureIgnoreHostKey(), HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: time.Duration(config.ConnTimeout) * time.Second, Timeout: time.Duration(config.ConnTimeout) * time.Second,
} }

View File

@ -1,9 +1,7 @@
<script setup> <script setup>
import { computed } from 'vue' import { computed, useSlots } from 'vue'
import { NIcon } from 'naive-ui' import { NIcon } from 'naive-ui'
const emit = defineEmits(['click'])
const props = defineProps({ const props = defineProps({
tooltip: String, tooltip: String,
tTooltip: String, tTooltip: String,
@ -35,8 +33,12 @@ const props = defineProps({
tertiary: Boolean, tertiary: Boolean,
}) })
const emit = defineEmits(['click'])
const slots = useSlots()
const hasTooltip = computed(() => { const hasTooltip = computed(() => {
return props.tooltip || props.tTooltip return props.tooltip || props.tTooltip || slots.tooltip
}) })
</script> </script>
@ -65,7 +67,9 @@ const hasTooltip = computed(() => {
</template> </template>
</n-button> </n-button>
</template> </template>
<slot name="tooltip">
{{ props.tTooltip ? $t(props.tTooltip) : props.tooltip }} {{ props.tTooltip ? $t(props.tTooltip) : props.tooltip }}
</slot>
</n-tooltip> </n-tooltip>
<n-button <n-button
v-else v-else

View File

@ -1,9 +1,9 @@
<script setup> <script setup>
import { computed, reactive } from 'vue' import { computed, nextTick, reactive } from 'vue'
import { debounce, isEmpty, trim } from 'lodash' import { debounce, isEmpty, trim } from 'lodash'
import { NButton, NInput } from 'naive-ui' import { NButton, NInput } from 'naive-ui'
import IconButton from '@/components/common/IconButton.vue' import IconButton from '@/components/common/IconButton.vue'
import Help from '@/components/icons/Help.vue' import SpellCheck from '@/components/icons/SpellCheck.vue'
const props = defineProps({ const props = defineProps({
fullSearchIcon: { fullSearchIcon: {
@ -22,17 +22,22 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
exact: {
type: Boolean,
default: false,
},
}) })
const emit = defineEmits(['filterChanged', 'matchChanged']) const emit = defineEmits(['filterChanged', 'matchChanged', 'exactChanged'])
/** /**
* *
* @type {UnwrapNestedRefs<{filter: string, match: string}>} * @type {UnwrapNestedRefs<{filter: string, match: string, exact: boolean}>}
*/ */
const inputData = reactive({ const inputData = reactive({
match: '', match: '',
filter: '', filter: '',
exact: false,
}) })
const hasMatch = computed(() => { const hasMatch = computed(() => {
@ -43,26 +48,32 @@ const hasFilter = computed(() => {
return !isEmpty(trim(inputData.filter)) return !isEmpty(trim(inputData.filter))
}) })
const onExactChecked = () => {
// update search search result
if (hasMatch.value) {
nextTick(() => onForceFullSearch())
}
}
const onFullSearch = () => { const onFullSearch = () => {
inputData.filter = trim(inputData.filter) inputData.filter = trim(inputData.filter)
if (!isEmpty(inputData.filter)) { if (!isEmpty(inputData.filter)) {
inputData.match = inputData.filter inputData.match = inputData.filter
inputData.filter = '' inputData.filter = ''
emit('matchChanged', inputData.match, inputData.filter) emit('matchChanged', inputData.match, inputData.filter, inputData.exact)
} }
} }
const onForceFullSearch = () => {
inputData.filter = trim(inputData.filter)
emit('matchChanged', inputData.match, inputData.filter, inputData.exact)
}
const _onInput = () => { const _onInput = () => {
emit('filterChanged', inputData.filter) emit('filterChanged', inputData.filter, inputData.exact)
} }
const onInput = debounce(_onInput, props.debounceWait, { leading: true, trailing: true }) const onInput = debounce(_onInput, props.debounceWait, { leading: true, trailing: true })
const onKeyup = (evt) => {
if (evt.key === 'Enter') {
onFullSearch()
}
}
const onClearFilter = () => { const onClearFilter = () => {
inputData.filter = '' inputData.filter = ''
onClearMatch() onClearMatch()
@ -77,9 +88,9 @@ const onClearMatch = () => {
const changed = !isEmpty(inputData.match) const changed = !isEmpty(inputData.match)
inputData.match = '' inputData.match = ''
if (changed) { if (changed) {
emit('matchChanged', inputData.match, inputData.filter) emit('matchChanged', inputData.match, inputData.filter, inputData.exact)
} else { } else {
emit('filterChanged', inputData.filter) emit('filterChanged', inputData.filter, inputData.exact)
} }
} }
@ -99,7 +110,7 @@ defineExpose({
clearable clearable
@clear="onClearFilter" @clear="onClearFilter"
@input="onInput" @input="onInput"
@keyup.enter="onKeyup"> @keyup.enter="onFullSearch">
<template #prefix> <template #prefix>
<slot name="prefix" /> <slot name="prefix" />
<n-tooltip v-if="hasMatch" placement="bottom"> <n-tooltip v-if="hasMatch" placement="bottom">
@ -117,12 +128,23 @@ defineExpose({
</template> </template>
<template #suffix> <template #suffix>
<template v-if="props.useGlob"> <template v-if="props.useGlob">
<n-tooltip trigger="hover"> <n-tooltip placement="bottom" trigger="hover">
<template #trigger> <template #trigger>
<n-icon :component="Help" /> <n-tag
v-model:checked="inputData.exact"
:checkable="true"
:type="props.exact ? 'primary' : 'default'"
size="small"
strong
style="padding: 0 5px"
@updateChecked="onExactChecked">
<n-icon :size="14">
<spell-check :stroke-width="2" />
</n-icon>
</n-tag>
</template> </template>
<div class="text-block" style="max-width: 600px"> <div class="text-block" style="max-width: 600px">
{{ $t('dialogue.filter.filter_pattern_tip') }} {{ $t('dialogue.filter.exact_match_tip') }}
</div> </div>
</n-tooltip> </n-tooltip>
</template> </template>
@ -134,11 +156,17 @@ defineExpose({
:disabled="hasMatch && !hasFilter" :disabled="hasMatch && !hasFilter"
:icon="props.fullSearchIcon" :icon="props.fullSearchIcon"
:size="small ? 16 : 20" :size="small ? 16 : 20"
:tooltip-delay="1"
border border
small small
stroke-width="4" stroke-width="4"
t-tooltip="interface.full_search" @click="onFullSearch">
@click="onFullSearch" /> <template #tooltip>
<div class="text-block" style="max-width: 600px">
{{ $t('dialogue.filter.filter_pattern_tip') }}
</div>
</template>
</icon-button>
<n-button v-else :disabled="hasMatch && !hasFilter" :focusable="false" @click="onFullSearch"> <n-button v-else :disabled="hasMatch && !hasFilter" :focusable="false" @click="onFullSearch">
{{ $t('interface.full_search') }} {{ $t('interface.full_search') }}
</n-button> </n-button>

View File

@ -0,0 +1,26 @@
<script setup>
const props = defineProps({
strokeWidth: {
type: [Number, String],
default: 3,
},
})
</script>
<template>
<svg
:stroke-width="props.strokeWidth"
class="lucide lucide-spell-check"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path d="m6 16 6-12 6 12" />
<path d="M8 12h8" />
<path d="m16 20 2 2 4-4" />
</svg>
</template>
<style lang="scss" scoped></style>

View File

@ -50,7 +50,10 @@ const inCheckState = ref(false)
const dbSelectOptions = computed(() => { const dbSelectOptions = computed(() => {
const dblist = browserStore.getDBList(props.server) const dblist = browserStore.getDBList(props.server)
const hasPattern = !isEmpty(filterForm.pattern)
return map(dblist, ({ db, alias, keyCount, maxKeys }) => { return map(dblist, ({ db, alias, keyCount, maxKeys }) => {
keyCount = Math.max(0, keyCount)
maxKeys = Math.max(keyCount, maxKeys)
let label let label
if (!isEmpty(alias)) { if (!isEmpty(alias)) {
// has alias // has alias
@ -59,7 +62,11 @@ const dbSelectOptions = computed(() => {
label = `db${db}` label = `db${db}`
} }
if (props.db === db) { if (props.db === db) {
if (hasPattern) {
label += ` (${keyCount})`
} else {
label += ` (${keyCount}/${maxKeys})` label += ` (${keyCount}/${maxKeys})`
}
} else { } else {
label += ` (${maxKeys})` label += ` (${maxKeys})`
} }
@ -80,6 +87,11 @@ const moreOptions = [
] ]
const loadProgress = computed(() => { const loadProgress = computed(() => {
const hasPattern = !isEmpty(filterForm.pattern)
if (hasPattern) {
return 100
}
const db = browserStore.getDatabase(props.server, props.db) const db = browserStore.getDatabase(props.server, props.db)
if (db.maxKeys <= 0) { if (db.maxKeys <= 0) {
return 100 return 100
@ -111,6 +123,7 @@ const onReload = async () => {
browserStore.setKeyFilter(props.server, { browserStore.setKeyFilter(props.server, {
type: matchType, type: matchType,
pattern: unref(filterForm.pattern), pattern: unref(filterForm.pattern),
exact: unref(filterForm.exact) === true,
}) })
await browserStore.openDatabase(props.server, db) await browserStore.openDatabase(props.server, db)
fullyLoaded.value = await browserStore.loadMoreKeys(props.server, db) fullyLoaded.value = await browserStore.loadMoreKeys(props.server, db)
@ -210,6 +223,7 @@ const handleSelectDB = async (db) => {
const filterForm = reactive({ const filterForm = reactive({
type: '', type: '',
exact: false,
pattern: '', pattern: '',
filter: '', filter: '',
}) })
@ -217,13 +231,15 @@ const onSelectFilterType = (select) => {
onReload() onReload()
} }
const onFilterInput = (val) => { const onFilterInput = (val, exact) => {
filterForm.filter = val filterForm.filter = val
filterForm.exact = exact
} }
const onMatchInput = (matchVal, filterVal) => { const onMatchInput = (matchVal, filterVal, exact) => {
filterForm.pattern = matchVal filterForm.pattern = matchVal
filterForm.filter = filterVal filterForm.filter = filterVal
filterForm.exact = exact
onReload() onReload()
} }

View File

@ -168,8 +168,8 @@ const handleSelectContextMenu = (action) => {
dialogStore.openNewKeyDialog(redisKey, props.server, db) dialogStore.openNewKeyDialog(redisKey, props.server, db)
break break
case 'db_filter': case 'db_filter':
const { match: pattern, type } = browserStore.getKeyFilter(props.server) // const { match: pattern, type } = browserStore.getKeyFilter(props.server)
dialogStore.openKeyFilterDialog(props.server, db, pattern, type) // dialogStore.openKeyFilterDialog(props.server, db, pattern, type)
break break
case 'key_reload': case 'key_reload':
if (node != null && !!!node.loading) { if (node != null && !!!node.loading) {

View File

@ -325,7 +325,8 @@
"filter": { "filter": {
"set_key_filter": "Set Key Filter", "set_key_filter": "Set Key Filter",
"filter_pattern": "Pattern", "filter_pattern": "Pattern",
"filter_pattern_tip": "* matches 0 or more chars, e.g. 'key*' \n? matches single char, e.g. 'key?'\n[] matches range, e.g. 'key[1-3]'\n\\ escapes special chars" "filter_pattern_tip": "* matches 0 or more chars, e.g. 'key*' \n? matches single char, e.g. 'key?'\n[] matches range, e.g. 'key[1-3]'\n\\ escapes special chars",
"exact_match_tip": "Exact Match"
}, },
"export": { "export": {
"name": "Export Data", "name": "Export Data",

View File

@ -325,7 +325,8 @@
"filter": { "filter": {
"set_key_filter": "Definir Filtro de Chave", "set_key_filter": "Definir Filtro de Chave",
"filter_pattern": "Padrão", "filter_pattern": "Padrão",
"filter_pattern_tip": "* corresponde a 0 ou mais caracteres, ex: 'chave*'\n? corresponde a um único caractere, ex: 'chave?'\n[] corresponde a um intervalo, ex: 'chave[1-3]'\n\\ escapa caracteres especiais" "filter_pattern_tip": "* corresponde a 0 ou mais caracteres, ex: 'chave*'\n? corresponde a um único caractere, ex: 'chave?'\n[] corresponde a um intervalo, ex: 'chave[1-3]'\n\\ escapa caracteres especiais",
"exact_match_tip": "Correspondência Exata"
}, },
"export": { "export": {
"name": "Exportar Dados", "name": "Exportar Dados",

View File

@ -325,7 +325,8 @@
"filter": { "filter": {
"set_key_filter": "设置键过滤器", "set_key_filter": "设置键过滤器",
"filter_pattern": "过滤表达式", "filter_pattern": "过滤表达式",
"filter_pattern_tip": "*:匹配零个或多个字符。例如:\"key*\"匹配到以\"key\"开头的所有键\n?:匹配单个字符。例如:\"key?\"匹配\"key1\"、\"key2\"\n[ ]:匹配指定范围内的单个字符。例如:\"key[1-3]\"可以匹配类似于 \"key1\"、\"key2\"、\"key3\" 的键\n\\:转义字符。如果想要匹配 *、?、[、或],需要使用反斜杠\"\\\"进行转义" "filter_pattern_tip": "*:匹配零个或多个字符。例如:\"key*\"匹配到以\"key\"开头的所有键\n?:匹配单个字符。例如:\"key?\"匹配\"key1\"、\"key2\"\n[ ]:匹配指定范围内的单个字符。例如:\"key[1-3]\"可以匹配类似于 \"key1\"、\"key2\"、\"key3\" 的键\n\\:转义字符。如果想要匹配 *、?、[、或],需要使用反斜杠\"\\\"进行转义",
"exact_match_tip": "完全匹配"
}, },
"export": { "export": {
"name": "导出数据", "name": "导出数据",

View File

@ -325,7 +325,8 @@
"filter": { "filter": {
"set_key_filter": "設定鍵過濾器", "set_key_filter": "設定鍵過濾器",
"filter_pattern": "過濾表示式", "filter_pattern": "過濾表示式",
"filter_pattern_tip": "*:匹配零個或多個字元。例如:\"key*\"匹配到以\"key\"開頭的所有鍵\n?:匹配單個字元。例如:\"key?\"匹配\"key1\", \"key2\"\n[ ]:匹配指定範圍內的單個字元。例如:\"key[1-3]\"可以匹配類似於 \"key1\", \"key2\", \"key3\" 的鍵\n\\:轉義字元。如果想要匹配 *, ?, [, 或],需要使用反斜線\"\\\"進行轉義" "filter_pattern_tip": "*:匹配零個或多個字元。例如:\"key*\"匹配到以\"key\"開頭的所有鍵\n?:匹配單個字元。例如:\"key?\"匹配\"key1\", \"key2\"\n[ ]:匹配指定範圍內的單個字元。例如:\"key[1-3]\"可以匹配類似於 \"key1\", \"key2\", \"key3\" 的鍵\n\\:轉義字元。如果想要匹配 *, ?, [, 或],需要使用反斜線\"\\\"進行轉義",
"exact_match_tip": "精準匹配"
}, },
"export": { "export": {
"name": "匯出資料", "name": "匯出資料",

View File

@ -24,6 +24,7 @@ export class RedisServerState {
* @param {Object.<number, RedisDatabaseItem>} databases database list * @param {Object.<number, RedisDatabaseItem>} databases database list
* @param {string|null} patternFilter pattern filter * @param {string|null} patternFilter pattern filter
* @param {string|null} typeFilter redis type filter * @param {string|null} typeFilter redis type filter
* @param {boolean} exactFilter exact match filter keyword
* @param {LoadingState} loadingState all loading state in opened connections map by server and LoadingState * @param {LoadingState} loadingState all loading state in opened connections map by server and LoadingState
* @param {KeyViewType} viewType view type selection for all opened connections group by 'server' * @param {KeyViewType} viewType view type selection for all opened connections group by 'server'
* @param {Map<string, RedisNodeItem>} nodeMap map nodes by "type#key" * @param {Map<string, RedisNodeItem>} nodeMap map nodes by "type#key"
@ -35,6 +36,7 @@ export class RedisServerState {
databases = {}, databases = {},
patternFilter = null, patternFilter = null,
typeFilter = null, typeFilter = null,
exactFilter = false,
loadingState = {}, loadingState = {},
viewType = KeyViewType.Tree, viewType = KeyViewType.Tree,
nodeMap = new Map(), nodeMap = new Map(),
@ -46,6 +48,7 @@ export class RedisServerState {
this.databases = databases this.databases = databases
this.patternFilter = patternFilter this.patternFilter = patternFilter
this.typeFilter = typeFilter this.typeFilter = typeFilter
this.exactFilter = exactFilter
this.loadingState = loadingState this.loadingState = loadingState
this.viewType = viewType this.viewType = viewType
this.nodeMap = nodeMap this.nodeMap = nodeMap
@ -62,6 +65,7 @@ export class RedisServerState {
this.stats = {} this.stats = {}
this.patternFilter = null this.patternFilter = null
this.typeFilter = null this.typeFilter = null
this.exactFilter = false
this.nodeMap.clear() this.nodeMap.clear()
} }
@ -447,6 +451,7 @@ export class RedisServerState {
return { return {
match: pattern, match: pattern,
type: toUpper(this.typeFilter), type: toUpper(this.typeFilter),
exact: this.exactFilter === true,
} }
} }
@ -454,10 +459,12 @@ export class RedisServerState {
* set key filter * set key filter
* @param {string} [pattern] * @param {string} [pattern]
* @param {string} [type] * @param {string} [type]
* @param {boolean} [exact]
*/ */
setFilter({ pattern, type }) { setFilter({ pattern, type, exact = false }) {
this.patternFilter = pattern === null ? this.patternFilter : pattern this.patternFilter = pattern === null ? this.patternFilter : pattern
this.typeFilter = type === null ? this.typeFilter : type this.typeFilter = type === null ? this.typeFilter : type
this.exactFilter = exact === true
} }
/** /**

View File

@ -293,8 +293,7 @@ const useBrowserStore = defineStore('browser', {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async openDatabase(server, db) { async openDatabase(server, db) {
const { match: filterPattern, type: filterType } = this.getKeyFilter(server) const { data, success, msg } = await OpenDatabase(server, db)
const { data, success, msg } = await OpenDatabase(server, db, filterPattern, filterType)
if (!success) { if (!success) {
throw new Error(msg) throw new Error(msg)
} }
@ -565,22 +564,23 @@ const useBrowserStore = defineStore('browser', {
* @param {string} server * @param {string} server
* @param {number} db * @param {number} db
* @param {string} match * @param {string} match
* @param {boolean} exact
* @param {string} [matchType] * @param {string} [matchType]
* @param {number} [loadType] 0.load next; 1.load next full; 2.reload load all * @param {number} [loadType] 0.load next; 1.load next full; 2.reload load all
* @returns {Promise<{keys: string[], maxKeys: number, end: boolean}>} * @returns {Promise<{keys: string[], maxKeys: number, end: boolean}>}
*/ */
async scanKeys({ server, db, match = '*', matchType = '', loadType = 0 }) { async scanKeys({ server, db, match = '*', exact = false, matchType = '', loadType = 0 }) {
let resp let resp
switch (loadType) { switch (loadType) {
case 0: case 0:
default: default:
resp = await LoadNextKeys(server, db, match, matchType) resp = await LoadNextKeys(server, db, match, matchType, exact)
break break
case 1: case 1:
resp = await LoadNextAllKeys(server, db, match, matchType) resp = await LoadNextAllKeys(server, db, match, matchType, exact)
break break
case 2: case 2:
resp = await LoadAllKeys(server, db, match, matchType) resp = await LoadAllKeys(server, db, match, matchType, exact)
break break
} }
const { data, success, msg } = resp || {} const { data, success, msg } = resp || {}
@ -595,22 +595,24 @@ const useBrowserStore = defineStore('browser', {
* *
* @param {string} server * @param {string} server
* @param {number} db * @param {number} db
* @param {string|null} prefix * @param {string|null} match
* @param {boolean} exact
* @param {string|null} matchType * @param {string|null} matchType
* @param {boolean} [all] * @param {boolean} [all]
* @return {Promise<{keys: Array<string|number[]>, maxKeys: number, end: boolean}>} * @return {Promise<{keys: Array<string|number[]>, maxKeys: number, end: boolean}>}
* @private * @private
*/ */
async _loadKeys(server, db, prefix, matchType, all) { async _loadKeys({ server, db, match, exact, matchType, all }) {
let match = prefix
if (isEmpty(match)) { if (isEmpty(match)) {
match = '*' match = '*'
} else if (!isRedisGlob(match)) { }
if (!endsWith(prefix, '*')) {
match = prefix + '*' if (!isRedisGlob(match) && !exact) {
if (!endsWith(match, '*')) {
match = match + '*'
} }
} }
return this.scanKeys({ server, db, match, matchType, loadType: all ? 1 : 0 }) return this.scanKeys({ server, db, match, exact, matchType, loadType: all ? 1 : 0 })
}, },
/** /**
@ -620,8 +622,15 @@ const useBrowserStore = defineStore('browser', {
* @return {Promise<boolean>} * @return {Promise<boolean>}
*/ */
async loadMoreKeys(server, db) { async loadMoreKeys(server, db) {
const { match, type: keyType } = this.getKeyFilter(server) const { match, type: keyType, exact } = this.getKeyFilter(server)
const { keys, maxKeys, end } = await this._loadKeys(server, db, match, keyType, false) const { keys, maxKeys, end } = await this._loadKeys({
server,
db,
match,
exact,
matchType: keyType,
all: false,
})
/** @type RedisServerState **/ /** @type RedisServerState **/
const serverInst = this.servers[server] const serverInst = this.servers[server]
if (serverInst != null) { if (serverInst != null) {
@ -640,8 +649,8 @@ const useBrowserStore = defineStore('browser', {
* @return {Promise<void>} * @return {Promise<void>}
*/ */
async loadAllKeys(server, db) { async loadAllKeys(server, db) {
const { match, type: keyType } = this.getKeyFilter(server) const { match, type: keyType, exact } = this.getKeyFilter(server)
const { keys, maxKeys } = await this._loadKeys(server, db, match, keyType, true) const { keys, maxKeys } = await this._loadKeys({ server, db, match, exact, matchType: keyType, all: true })
/** @type RedisServerState **/ /** @type RedisServerState **/
const serverInst = this.servers[server] const serverInst = this.servers[server]
if (serverInst != null) { if (serverInst != null) {
@ -670,8 +679,15 @@ const useBrowserStore = defineStore('browser', {
match += '*' match += '*'
} }
// FIXME: ignore original match pattern due to redis not support combination matching // FIXME: ignore original match pattern due to redis not support combination matching
const { match: originMatch, type: keyType } = this.getKeyFilter(server) const { match: originMatch, type: keyType, exact } = this.getKeyFilter(server)
const { keys, maxKeys, success } = await this._loadKeys(server, db, match, keyType, true) const { keys, maxKeys, success } = await this._loadKeys({
server,
db,
match: originMatch,
exact: false,
matchType: keyType,
all: true,
})
if (!success) { if (!success) {
return return
} }
@ -1915,7 +1931,7 @@ const useBrowserStore = defineStore('browser', {
/** /**
* get key filter pattern and filter type * get key filter pattern and filter type
* @param {string} server * @param {string} server
* @returns {{match: string, type: string}} * @returns {{match: string, type: string, exact: boolean}}
*/ */
getKeyFilter(server) { getKeyFilter(server) {
let serverInst = this.servers[server] let serverInst = this.servers[server]
@ -1933,11 +1949,12 @@ const useBrowserStore = defineStore('browser', {
* @param {string} server * @param {string} server
* @param {string} [pattern] * @param {string} [pattern]
* @param {string} [type] * @param {string} [type]
* @param {boolean} [exact]
*/ */
setKeyFilter(server, { pattern, type }) { setKeyFilter(server, { pattern, type, exact = false }) {
const serverInst = this.servers[server] const serverInst = this.servers[server]
if (serverInst != null) { if (serverInst != null) {
serverInst.setFilter({ pattern, type }) serverInst.setFilter({ pattern, type, exact })
} }
}, },