feat(redis): add hash field search
This commit is contained in:
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>
|
||||
|
||||
Reference in new issue
Block a user