feat(redis): add hash field search

This commit is contained in:
lexmin0412 authored and GitHub committed 2026-07-05 19:37:49 +08:00
1 parent 2c33570351
commit d502b4ce6c
13 files changed
+176 -23

No files matched your search

@@ -3,7 +3,7 @@ import { computed, ref, nextTick, onBeforeUnmount, onMounted } from "vue";
import { useI18n } from "vue-i18n";
import { onClickOutside } from "@vueuse/core";
import { DynamicScroller, DynamicScrollerItem, RecycleScroller } from "vue-virtual-scroller";
import { Braces, Copy, Eye, FileText, Terminal, Trash2, Save, RefreshCw, Plus, Loader2, Pencil, WrapText, IndentIncrease, IndentDecrease, ArrowUp, ArrowDown, ArrowUpDown } from "@lucide/vue";
import { Braces, Copy, Eye, FileText, Terminal, Trash2, Save, RefreshCw, Plus, Loader2, Pencil, WrapText, IndentIncrease, IndentDecrease, ArrowUp, ArrowDown, ArrowUpDown, Search } from "@lucide/vue";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
@@ -78,6 +78,9 @@ const redisJsonWordWrap = ref(readRedisJsonWordWrap());
const redisJsonHighlighter = ref<RedisJsonHighlighter>();
const hashSortBy = ref<"field" | "value" | null>(null);
const hashSortDir = ref<"asc" | "desc">("asc");
const hashSearchQuery = ref("");
const activeHashSearchQuery = ref("");
const searchLoading = ref(false);
function toggleHashSort(column: "field" | "value") {
if (hashSortBy.value === column && hashSortDir.value === "desc") {
@@ -111,6 +114,55 @@ const hashCollectionRows = computed<RedisCollectionRow[]>(() =>
})),
);
function redisGlobEscape(s: string): string {
return s.replace(/[*?[\]\\]/g, "\\$&");
}
function hashSearchPattern(query: string): string | undefined {
return query ? `*${redisGlobEscape(query)}*` : undefined;
}
let hashSearchTimer: ReturnType<typeof setTimeout> | null = null;
let hashSearchRequestId = 0;
function onHashSearchInput() {
if (hashSearchTimer) clearTimeout(hashSearchTimer);
hashSearchTimer = setTimeout(() => void onHashSearch(), 400);
}
function onHashSearchKeydown(event: KeyboardEvent) {
if (event.key === "Enter") {
if (hashSearchTimer) clearTimeout(hashSearchTimer);
hashSearchTimer = null;
void onHashSearch();
return;
}
if (event.key === "Escape") {
if (hashSearchTimer) clearTimeout(hashSearchTimer);
hashSearchTimer = null;
hashSearchQuery.value = "";
void onHashSearch();
}
}
async function onHashSearch() {
const query = hashSearchQuery.value.trim();
if (!data.value) return;
const requestId = ++hashSearchRequestId;
searchLoading.value = true;
try {
const result = await api.redisLoadMore(props.connectionId, props.db, props.keyRaw, "hash", 0, 200, hashSearchPattern(query));
if (requestId !== hashSearchRequestId) return;
const items = Array.isArray(result.value) ? result.value : [];
activeHashSearchQuery.value = query;
collectionItems.value = items;
scanCursor.value = result.scan_cursor ?? undefined;
clearSelectedMember();
} finally {
if (requestId === hashSearchRequestId) searchLoading.value = false;
}
}
const selectedMemberDetail = computed(() => formatRedisMemberDetail(selectedMemberRaw.value));
const selectedMemberJsonDetail = computed(() => selectedMemberDetail.value.json ?? null);
const stringValueDetail = computed(() => (data.value?.key_type === "string" ? formatRedisMemberDetail(data.value.value) : null));
@@ -241,6 +293,12 @@ function collectionCountLabel(kind: "items" | "fields" | "members", loaded: numb
async function load(options: { selectDefaultMember?: boolean } = {}) {
const shouldSelectDefaultMember = options.selectDefaultMember ?? true;
if (hashSearchTimer) clearTimeout(hashSearchTimer);
hashSearchTimer = null;
hashSearchRequestId++;
hashSearchQuery.value = "";
activeHashSearchQuery.value = "";
searchLoading.value = false;
loading.value = true;
try {
const loadedValue = await api.redisGetValue(props.connectionId, props.db, props.keyRaw);
@@ -266,10 +324,14 @@ async function load(options: { selectDefaultMember?: boolean } = {}) {
}
async function loadMore() {
if (!data.value || !hasMore.value || loadingMore.value) return;
if (!data.value || !hasMore.value || loadingMore.value || (data.value.key_type === "hash" && searchLoading.value)) return;
const keyType = data.value.key_type;
const hashFilter = keyType === "hash" ? hashSearchPattern(activeHashSearchQuery.value) : undefined;
const requestId = hashSearchRequestId;
loadingMore.value = true;
try {
const result = await api.redisLoadMore(props.connectionId, props.db, props.keyRaw, data.value.key_type, scanCursor.value!, 200);
const result = await api.redisLoadMore(props.connectionId, props.db, props.keyRaw, keyType, scanCursor.value!, 200, hashFilter);
if (keyType === "hash" && requestId !== hashSearchRequestId) return;
const newItems = Array.isArray(result.value) ? result.value : [];
collectionItems.value = [...collectionItems.value, ...newItems];
scanCursor.value = result.scan_cursor ?? undefined;
@@ -803,6 +865,7 @@ onBeforeUnmount(() => {
stopResizeMemberSheet();
stopResizeHashColumns();
stopResizeZsetColumns();
if (hashSearchTimer) clearTimeout(hashSearchTimer);
});
</script>
@@ -969,7 +1032,11 @@ onBeforeUnmount(() => {
<!-- Hash -->
<div v-else-if="data.key_type === 'hash'" ref="hashTableRef" class="flex-1 flex flex-col overflow-hidden">
<div class="flex items-center gap-2 px-4 py-1.5 border-b shrink-0">
<span class="text-xs text-muted-foreground">{{ collectionCountLabel("fields", collectionItems.length, data.total) }}</span>
<span class="text-xs text-muted-foreground shrink-0">{{ collectionCountLabel("fields", collectionItems.length, activeHashSearchQuery ? null : data.total) }}</span>
<div class="relative flex-1 max-w-60">
<Search class="pointer-events-none absolute left-1.5 top-1/2 h-3 w-3 -translate-y-1/2 text-muted-foreground/80" />
<Input v-model="hashSearchQuery" class="h-6 w-full pl-5 pr-2 text-xs" :placeholder="t('redis.searchFields')" @input="onHashSearchInput" @keydown="onHashSearchKeydown" />
</div>
<span class="flex-1" />
<Input v-model="newField" class="h-6 w-24 text-xs" placeholder="field" />
<Input v-model="newValue" class="h-6 w-32 text-xs" placeholder="value" @keydown.enter="hashSet" />
@@ -1028,7 +1095,7 @@ onBeforeUnmount(() => {
</template>
<template #after>
<div v-if="hasMore" class="p-2">
<Button variant="outline" size="sm" class="w-full h-7 text-xs" :disabled="loadingMore" @click="loadMore">
<Button variant="outline" size="sm" class="w-full h-7 text-xs" :disabled="loadingMore || searchLoading" @click="loadMore">
<Loader2 v-if="loadingMore" class="w-3 h-3 mr-1.5 animate-spin" />
{{ t("redis.loadMoreKeys") }}
</Button>