feat(redis): add fuzzy key search toggle

This commit is contained in:
t8y2
2026-06-03 01:53:24 +08:00
parent 3c93554630
commit 6546cccd1e
7 changed files with 69 additions and 2 deletions
@@ -13,6 +13,7 @@ import {
Plus,
KeyRound,
TerminalSquare,
Asterisk,
} from "lucide-vue-next";
import { RecycleScroller } from "vue-virtual-scroller";
import "vue-virtual-scroller/dist/vue-virtual-scroller.css";
@@ -42,6 +43,7 @@ import { isRedisClearScreenCommand, nextRedisCommandDb, redisKeyTextToRaw } from
import { formatRedisCommandResult, formatRedisStringValue } from "@/lib/redisValuePresentation";
import { isCancelSearchShortcut } from "@/lib/keyboardShortcuts";
import { useEditorFontFamilyStyle } from "@/composables/useEditorFontFamilyStyle";
import { redisKeySearchPattern } from "@/lib/redisKeyPattern";
const { t } = useI18n();
const connectionStore = useConnectionStore();
@@ -72,6 +74,7 @@ const rootRef = ref<HTMLElement>();
const commandTerminalRef = ref<HTMLElement>();
const searchPattern = ref("");
const searchMode = ref<RedisSearchMode>("key");
const fuzzyKeySearch = ref(false);
const selectedKeyRaw = ref<string | null>(null);
const hasMore = ref(false);
const scanCursor = ref(0);
@@ -99,12 +102,18 @@ let redisBrowserIsActive = true;
let redisDbFlushedListenerRegistered = false;
const valueQuery = computed(() => searchPattern.value.trim());
const effectivePattern = computed(() => (searchMode.value === "key" ? searchPattern.value.trim() || "*" : "*"));
const effectivePattern = computed(() =>
searchMode.value === "key" ? redisKeySearchPattern(searchPattern.value, fuzzyKeySearch.value) : "*",
);
const isSearchMode = computed(() =>
searchMode.value === "key" ? effectivePattern.value !== "*" : valueQuery.value !== "",
);
const searchPlaceholder = computed(() =>
searchMode.value === "key" ? t("redis.pattern") : t("redis.valueSearchPlaceholder"),
searchMode.value === "key"
? fuzzyKeySearch.value
? t("redis.fuzzyPattern")
: t("redis.pattern")
: t("redis.valueSearchPlaceholder"),
);
const loadingEmptyText = computed(() =>
searchMode.value === "value" && valueQuery.value ? t("redis.searchingValues") : t("redis.loadingKeys"),
@@ -550,6 +559,11 @@ function setSearchMode(mode: RedisSearchMode) {
void loadKeys();
}
function toggleFuzzyKeySearch() {
fuzzyKeySearch.value = !fuzzyKeySearch.value;
if (searchMode.value === "key") void loadKeys();
}
function getSearchInput(): HTMLInputElement | null {
return rootRef.value?.querySelector<HTMLInputElement>("[data-redis-search-input]") ?? null;
}
@@ -674,6 +688,19 @@ defineExpose({ focusSearch });
@input="onSearchInput"
@keydown="onSearchKeydown"
/>
<Button
v-if="searchMode === 'key'"
variant="ghost"
size="sm"
class="h-6 shrink-0 px-2 text-xs"
:class="fuzzyKeySearch ? 'bg-accent text-accent-foreground' : 'text-muted-foreground'"
:title="t('redis.fuzzyMatchTitle')"
:aria-pressed="fuzzyKeySearch"
@click="toggleFuzzyKeySearch"
>
<Asterisk class="h-3 w-3 mr-1" />
{{ t("redis.fuzzyMatch") }}
</Button>
<Button variant="ghost" size="icon" class="h-6 w-6 shrink-0" @click="loadKeys">
<Loader2 v-if="loading" class="h-3 w-3 animate-spin" />
<RefreshCw v-else class="h-3 w-3" />
+3
View File
@@ -1206,6 +1206,9 @@ export default {
selectKey: "Select a key to view its value",
noKeys: "No keys found",
pattern: "pattern (e.g. user:*)",
fuzzyPattern: "key contains...",
fuzzyMatch: "Fuzzy",
fuzzyMatchTitle: "Fuzzy match: search keys by plain text contains",
valueSearchPlaceholder: "value contains...",
searchByKey: "Key",
searchByValue: "Value",
+3
View File
@@ -1098,6 +1098,9 @@ export default {
selectKey: "Selecciona una clave para ver su valor",
noKeys: "No se encontraron claves",
pattern: "patrón (p. ej. usuario:*)",
fuzzyPattern: "la clave contiene...",
fuzzyMatch: "Difusa",
fuzzyMatchTitle: "Coincidencia difusa: busca claves que contengan texto plano",
valueSearchPlaceholder: "el valor contiene...",
searchByKey: "Clave",
searchByValue: "Valor",
+3
View File
@@ -1182,6 +1182,9 @@ export default {
selectKey: "选择一个 key 查看值",
noKeys: "未找到 key",
pattern: "匹配模式 (如 user:*)",
fuzzyPattern: "输入关键字搜索 key",
fuzzyMatch: "模糊",
fuzzyMatchTitle: "模糊匹配:自动按包含关系搜索 key",
valueSearchPlaceholder: "按值内容搜索...",
searchByKey: "键",
searchByValue: "值",
+3
View File
@@ -1182,6 +1182,9 @@ export default {
selectKey: "選擇一個 key 檢視值",
noKeys: "未找到 key",
pattern: "模式 (例如 user:*)",
fuzzyPattern: "輸入關鍵字搜尋 key",
fuzzyMatch: "模糊",
fuzzyMatchTitle: "模糊匹配:自動按包含關係搜尋 key",
valueSearchPlaceholder: "值包含……",
searchByKey: "鍵",
searchByValue: "值",
+11
View File
@@ -0,0 +1,11 @@
const REDIS_GLOB_SPECIAL_CHARS = /[\\*?[\]]/g;
export function escapeRedisGlobText(value: string): string {
return value.replace(REDIS_GLOB_SPECIAL_CHARS, "\\$&");
}
export function redisKeySearchPattern(value: string, fuzzy: boolean): string {
const pattern = value.trim();
if (!pattern) return "*";
return fuzzy ? `*${escapeRedisGlobText(pattern)}*` : pattern;
}
@@ -0,0 +1,17 @@
import assert from "node:assert/strict";
import test from "node:test";
import { escapeRedisGlobText, redisKeySearchPattern } from "../../apps/desktop/src/lib/redisKeyPattern.ts";
test("builds Redis key fuzzy search patterns from plain text", () => {
assert.equal(redisKeySearchPattern("", true), "*");
assert.equal(redisKeySearchPattern(" user ", true), "*user*");
});
test("escapes Redis glob characters in fuzzy key search", () => {
assert.equal(escapeRedisGlobText(String.raw`user:*?[1]`), String.raw`user:\*\?\[1\]`);
assert.equal(redisKeySearchPattern(String.raw`user:*?[1]`, true), String.raw`*user:\*\?\[1\]*`);
});
test("keeps Redis key pattern syntax when fuzzy search is off", () => {
assert.equal(redisKeySearchPattern("user:*", false), "user:*");
});