diff --git a/backend/services/browser_service.go b/backend/services/browser_service.go index 2c1944f..66ff222 100644 --- a/backend/services/browser_service.go +++ b/backend/services/browser_service.go @@ -494,27 +494,68 @@ func (b *browserService) scanKeys(ctx context.Context, client redis.UniversalCli 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 -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) if err != nil { resp.Msg = err.Error() return } + if match == "*" { + exactMatch = false + } client, ctx, count := item.client, item.ctx, item.stepSize + var matchKeys []any + var maxKeys int64 cursor := item.cursor[db] - keys, cursor, err := b.scanKeys(ctx, client, match, keyType, cursor, count) - if err != nil { - resp.Msg = err.Error() - return + 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 { + resp.Msg = err.Error() + return + } + b.setClientCursor(server, db, cursor) + if fullScan { + maxKeys = b.loadDBSize(ctx, client) + } else { + maxKeys = int64(len(matchKeys)) + } } - b.setClientCursor(server, db, cursor) - maxKeys := b.loadDBSize(ctx, client) resp.Success = true resp.Data = map[string]any{ - "keys": keys, + "keys": matchKeys, "end": cursor == 0, "maxKeys": maxKeys, } @@ -522,7 +563,7 @@ func (b *browserService) LoadNextKeys(server string, db int, match, keyType stri } // 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) if err != nil { 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 - cursor := item.cursor[db] - keys, _, err := b.scanKeys(ctx, client, match, keyType, cursor, 0) - if err != nil { - resp.Msg = err.Error() - return + 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] + matchKeys, _, err = b.scanKeys(ctx, client, match, keyType, cursor, 0) + if err != nil { + resp.Msg = err.Error() + return + } + b.setClientCursor(server, db, 0) + if fullScan { + maxKeys = b.loadDBSize(ctx, client) + } else { + maxKeys = int64(len(matchKeys)) + } } - b.setClientCursor(server, db, 0) - maxKeys := b.loadDBSize(ctx, client) resp.Success = true resp.Data = map[string]any{ - "keys": keys, + "keys": matchKeys, "maxKeys": maxKeys, } return } // 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) if err != nil { 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 - keys, _, err := b.scanKeys(ctx, client, match, keyType, 0, 0) - if err != nil { - resp.Msg = err.Error() - return + 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 { + resp.Msg = err.Error() + return + } } resp.Success = true resp.Data = map[string]any{ - "keys": keys, + "keys": matchKeys, } return } diff --git a/backend/services/connection_service.go b/backend/services/connection_service.go index 51d3d1e..f57757f 100644 --- a/backend/services/connection_service.go +++ b/backend/services/connection_service.go @@ -86,7 +86,6 @@ func (c *connectionService) buildOption(config types.ConnectionConfig) (*redis.O if config.SSH.Enable { sshConfig = &ssh.ClientConfig{ User: config.SSH.Username, - Auth: []ssh.AuthMethod{ssh.Password(config.SSH.Password)}, HostKeyCallback: ssh.InsecureIgnoreHostKey(), Timeout: time.Duration(config.ConnTimeout) * time.Second, } diff --git a/frontend/src/components/common/IconButton.vue b/frontend/src/components/common/IconButton.vue index 5ff819f..959d583 100644 --- a/frontend/src/components/common/IconButton.vue +++ b/frontend/src/components/common/IconButton.vue @@ -1,9 +1,7 @@ @@ -65,7 +67,9 @@ const hasTooltip = computed(() => { - {{ props.tTooltip ? $t(props.tTooltip) : props.tooltip }} + + {{ props.tTooltip ? $t(props.tTooltip) : props.tooltip }} + -import { computed, reactive } from 'vue' +import { computed, nextTick, reactive } from 'vue' import { debounce, isEmpty, trim } from 'lodash' import { NButton, NInput } from 'naive-ui' import IconButton from '@/components/common/IconButton.vue' -import Help from '@/components/icons/Help.vue' +import SpellCheck from '@/components/icons/SpellCheck.vue' const props = defineProps({ fullSearchIcon: { @@ -22,17 +22,22 @@ const props = defineProps({ type: Boolean, 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({ match: '', filter: '', + exact: false, }) const hasMatch = computed(() => { @@ -43,26 +48,32 @@ const hasFilter = computed(() => { return !isEmpty(trim(inputData.filter)) }) +const onExactChecked = () => { + // update search search result + if (hasMatch.value) { + nextTick(() => onForceFullSearch()) + } +} + const onFullSearch = () => { inputData.filter = trim(inputData.filter) if (!isEmpty(inputData.filter)) { inputData.match = 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 = () => { - emit('filterChanged', inputData.filter) + emit('filterChanged', inputData.filter, inputData.exact) } const onInput = debounce(_onInput, props.debounceWait, { leading: true, trailing: true }) -const onKeyup = (evt) => { - if (evt.key === 'Enter') { - onFullSearch() - } -} - const onClearFilter = () => { inputData.filter = '' onClearMatch() @@ -77,9 +88,9 @@ const onClearMatch = () => { const changed = !isEmpty(inputData.match) inputData.match = '' if (changed) { - emit('matchChanged', inputData.match, inputData.filter) + emit('matchChanged', inputData.match, inputData.filter, inputData.exact) } else { - emit('filterChanged', inputData.filter) + emit('filterChanged', inputData.filter, inputData.exact) } } @@ -99,7 +110,7 @@ defineExpose({ clearable @clear="onClearFilter" @input="onInput" - @keyup.enter="onKeyup"> + @keyup.enter="onFullSearch">