Skip to content

Commit

Permalink
feat: support exact match filter (tiny-craft#164)
Browse files Browse the repository at this point in the history
  • Loading branch information
tiny-craft committed Mar 9, 2024
1 parent fdf2c47 commit 2d2954d
Show file tree
Hide file tree
Showing 13 changed files with 245 additions and 81 deletions.
109 changes: 86 additions & 23 deletions backend/services/browser_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -494,77 +494,140 @@ 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,
}
return
}

// 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()
return
}

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()
return
}

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
}
Expand Down
1 change: 0 additions & 1 deletion backend/services/connection_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down
14 changes: 9 additions & 5 deletions frontend/src/components/common/IconButton.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
<script setup>
import { computed } from 'vue'
import { computed, useSlots } from 'vue'
import { NIcon } from 'naive-ui'
const emit = defineEmits(['click'])
const props = defineProps({
tooltip: String,
tTooltip: String,
Expand Down Expand Up @@ -35,8 +33,12 @@ const props = defineProps({
tertiary: Boolean,
})
const emit = defineEmits(['click'])
const slots = useSlots()
const hasTooltip = computed(() => {
return props.tooltip || props.tTooltip
return props.tooltip || props.tTooltip || slots.tooltip
})
</script>

Expand Down Expand Up @@ -65,7 +67,9 @@ const hasTooltip = computed(() => {
</template>
</n-button>
</template>
{{ props.tTooltip ? $t(props.tTooltip) : props.tooltip }}
<slot name="tooltip">
{{ props.tTooltip ? $t(props.tTooltip) : props.tooltip }}
</slot>
</n-tooltip>
<n-button
v-else
Expand Down
68 changes: 48 additions & 20 deletions frontend/src/components/content_value/ContentSearchInput.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<script setup>
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: {
Expand All @@ -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(() => {
Expand All @@ -43,25 +48,31 @@ 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 _onInput = () => {
emit('filterChanged', inputData.filter)
const onForceFullSearch = () => {
inputData.filter = trim(inputData.filter)
emit('matchChanged', inputData.match, inputData.filter, inputData.exact)
}
const onInput = debounce(_onInput, props.debounceWait, { leading: true, trailing: true })
const onKeyup = (evt) => {
if (evt.key === 'Enter') {
onFullSearch()
}
const _onInput = () => {
emit('filterChanged', inputData.filter, inputData.exact)
}
const onInput = debounce(_onInput, props.debounceWait, { leading: true, trailing: true })
const onClearFilter = () => {
inputData.filter = ''
Expand All @@ -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)
}
}
Expand All @@ -99,7 +110,7 @@ defineExpose({
clearable
@clear="onClearFilter"
@input="onInput"
@keyup.enter="onKeyup">
@keyup.enter="onFullSearch">
<template #prefix>
<slot name="prefix" />
<n-tooltip v-if="hasMatch" placement="bottom">
Expand All @@ -117,12 +128,23 @@ defineExpose({
</template>
<template #suffix>
<template v-if="props.useGlob">
<n-tooltip trigger="hover">
<n-tooltip placement="bottom" trigger="hover">
<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>
<div class="text-block" style="max-width: 600px">
{{ $t('dialogue.filter.filter_pattern_tip') }}
{{ $t('dialogue.filter.exact_match_tip') }}
</div>
</n-tooltip>
</template>
Expand All @@ -134,11 +156,17 @@ defineExpose({
:disabled="hasMatch && !hasFilter"
:icon="props.fullSearchIcon"
:size="small ? 16 : 20"
:tooltip-delay="1"
border
small
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">
{{ $t('interface.full_search') }}
</n-button>
Expand Down
26 changes: 26 additions & 0 deletions frontend/src/components/icons/SpellCheck.vue
Original file line number Diff line number Diff line change
@@ -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>
Loading

0 comments on commit 2d2954d

Please sign in to comment.