mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-28 16:09:17 +00:00
feat(redis): implement pagination for Redis key scanning and enhance UI for loading more keys
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Search, RefreshCw, Key } from "lucide-vue-next";
|
||||
import { Search, RefreshCw, Key, Loader2 } from "lucide-vue-next";
|
||||
import { Splitpanes, Pane } from "splitpanes";
|
||||
import "splitpanes/dist/splitpanes.css";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -22,11 +22,34 @@ const keys = ref<RedisKeyInfo[]>([]);
|
||||
const loading = ref(false);
|
||||
const searchPattern = ref("*");
|
||||
const selectedKey = ref<string | null>(null);
|
||||
const cursor = ref(0);
|
||||
const hasMore = ref(false);
|
||||
|
||||
const PAGE_SIZE = 200;
|
||||
|
||||
async function loadKeys() {
|
||||
loading.value = true;
|
||||
try {
|
||||
keys.value = await api.redisScanKeys(props.connectionId, props.db, searchPattern.value, 1000);
|
||||
const result = await api.redisScanKeys(props.connectionId, props.db, 0, searchPattern.value, PAGE_SIZE);
|
||||
keys.value = result.keys;
|
||||
cursor.value = result.cursor;
|
||||
hasMore.value = result.cursor !== 0;
|
||||
selectedKey.value = null;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMoreKeys() {
|
||||
if (loading.value || !hasMore.value) return;
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const result = await api.redisScanKeys(props.connectionId, props.db, cursor.value, searchPattern.value, PAGE_SIZE);
|
||||
const existingKeys = new Set(keys.value.map((k) => k.key));
|
||||
keys.value = [...keys.value, ...result.keys.filter((k) => !existingKeys.has(k.key))];
|
||||
cursor.value = result.cursor;
|
||||
hasMore.value = result.cursor !== 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -72,13 +95,14 @@ onMounted(loadKeys);
|
||||
@keydown.enter="loadKeys"
|
||||
/>
|
||||
<Button variant="ghost" size="icon" class="h-6 w-6 shrink-0" @click="loadKeys">
|
||||
<RefreshCw class="h-3 w-3" />
|
||||
<Loader2 v-if="loading" class="h-3 w-3 animate-spin" />
|
||||
<RefreshCw v-else class="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Key count -->
|
||||
<div class="px-3 py-1 text-xs text-muted-foreground border-b shrink-0">
|
||||
{{ t('redis.keys', { count: keys.length }) }}
|
||||
{{ loading && keys.length === 0 ? t('redis.loadingKeys') : t('redis.keys', { count: keys.length }) }}
|
||||
</div>
|
||||
|
||||
<!-- Key list -->
|
||||
@@ -97,6 +121,16 @@ onMounted(loadKeys);
|
||||
<div v-if="keys.length === 0 && !loading" class="px-3 py-8 text-center text-muted-foreground text-xs">
|
||||
{{ t('redis.noKeys') }}
|
||||
</div>
|
||||
<div v-if="loading && keys.length === 0" class="px-3 py-8 flex items-center justify-center gap-2 text-muted-foreground text-xs">
|
||||
<Loader2 class="w-3.5 h-3.5 animate-spin" />
|
||||
<span>{{ t('redis.loadingKeys') }}</span>
|
||||
</div>
|
||||
<div v-if="hasMore || (loading && keys.length > 0)" class="p-2">
|
||||
<Button variant="outline" size="sm" class="w-full h-7 text-xs" :disabled="loading" @click="loadMoreKeys">
|
||||
<Loader2 v-if="loading" class="w-3 h-3 mr-1.5 animate-spin" />
|
||||
{{ t('redis.loadMoreKeys') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Pane>
|
||||
|
||||
@@ -101,6 +101,7 @@ async function toggle() {
|
||||
} else if (node.type === "redis-db" && node.connectionId && node.database) {
|
||||
const tabTitle = `${connectionStore.getConfig(node.connectionId)?.name || "Redis"}:db${node.database}`;
|
||||
queryStore.createTab(node.connectionId, node.database, tabTitle, "redis");
|
||||
await connectionStore.loadRedisKeys(node.connectionId, Number(node.database), node.id);
|
||||
} else if (node.type === "mongo-db" && node.connectionId && node.database) {
|
||||
await connectionStore.loadMongoCollections(node.connectionId, node.database);
|
||||
} else if (node.type === "mongo-collection" && node.connectionId && node.database) {
|
||||
@@ -179,6 +180,8 @@ async function refresh() {
|
||||
const node = props.node;
|
||||
node.isExpanded = false;
|
||||
node.children = [];
|
||||
node.scanCursor = undefined;
|
||||
node.hasMore = undefined;
|
||||
await toggle();
|
||||
}
|
||||
|
||||
@@ -229,15 +232,23 @@ const visibleChildren = computed(() => {
|
||||
});
|
||||
|
||||
const hasMoreChildren = computed(() =>
|
||||
(props.node.children?.length ?? 0) > displayLimit.value
|
||||
(props.node.children?.length ?? 0) > displayLimit.value || !!props.node.hasMore
|
||||
);
|
||||
|
||||
const remainingCount = computed(() =>
|
||||
(props.node.children?.length ?? 0) - displayLimit.value
|
||||
);
|
||||
|
||||
function showMore() {
|
||||
displayLimit.value += CHILDREN_PAGE_SIZE;
|
||||
async function showMore() {
|
||||
if ((props.node.children?.length ?? 0) > displayLimit.value) {
|
||||
displayLimit.value += CHILDREN_PAGE_SIZE;
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.node.type === "redis-db" && props.node.connectionId && props.node.database && props.node.hasMore) {
|
||||
await connectionStore.loadMoreRedisKeys(props.node.connectionId, Number(props.node.database), props.node.id);
|
||||
displayLimit.value += CHILDREN_PAGE_SIZE;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -269,7 +280,8 @@ function showMore() {
|
||||
:style="{ paddingLeft: `${(depth + 1) * 16 + 8}px` }"
|
||||
@click="showMore"
|
||||
>
|
||||
<span>{{ t('sidebar.showMore', { count: Math.min(CHILDREN_PAGE_SIZE, remainingCount) }) }}</span>
|
||||
<Loader2 v-if="node.isLoading" class="w-3 h-3 shrink-0 animate-spin" />
|
||||
<span>{{ node.hasMore && remainingCount <= 0 ? t('sidebar.loadMore') : t('sidebar.showMore', { count: Math.min(CHILDREN_PAGE_SIZE, remainingCount) }) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,7 @@ export default {
|
||||
import: "Import Connections",
|
||||
export: "Export Connections",
|
||||
showMore: "Show {count} more...",
|
||||
loadMore: "Load more...",
|
||||
},
|
||||
connection: {
|
||||
title: "New Connection",
|
||||
@@ -155,6 +156,8 @@ export default {
|
||||
noKeys: "No keys found",
|
||||
pattern: "pattern (e.g. user:*)",
|
||||
keys: "{count} keys",
|
||||
loadingKeys: "Loading keys...",
|
||||
loadMoreKeys: "Load more keys",
|
||||
items: "{count} items",
|
||||
fields: "{count} fields",
|
||||
members: "{count} members",
|
||||
|
||||
@@ -14,6 +14,7 @@ export default {
|
||||
import: "导入连接",
|
||||
export: "导出连接",
|
||||
showMore: "加载更多 ({count})...",
|
||||
loadMore: "继续加载...",
|
||||
},
|
||||
connection: {
|
||||
title: "新建连接",
|
||||
@@ -153,6 +154,8 @@ export default {
|
||||
noKeys: "未找到 key",
|
||||
pattern: "匹配模式 (如 user:*)",
|
||||
keys: "{count} 个 key",
|
||||
loadingKeys: "正在加载 key...",
|
||||
loadMoreKeys: "继续加载 key",
|
||||
items: "{count} 个元素",
|
||||
fields: "{count} 个字段",
|
||||
members: "{count} 个成员",
|
||||
|
||||
+7
-2
@@ -115,12 +115,17 @@ export interface RedisValue {
|
||||
value: any;
|
||||
}
|
||||
|
||||
export interface RedisScanResult {
|
||||
cursor: number;
|
||||
keys: RedisKeyInfo[];
|
||||
}
|
||||
|
||||
export async function redisListDatabases(connectionId: string): Promise<number[]> {
|
||||
return invoke("redis_list_databases", { connectionId });
|
||||
}
|
||||
|
||||
export async function redisScanKeys(connectionId: string, db: number, pattern: string, count: number): Promise<RedisKeyInfo[]> {
|
||||
return invoke("redis_scan_keys", { connectionId, db, pattern, count });
|
||||
export async function redisScanKeys(connectionId: string, db: number, cursor: number, pattern: string, count: number): Promise<RedisScanResult> {
|
||||
return invoke("redis_scan_keys", { connectionId, db, cursor, pattern, count });
|
||||
}
|
||||
|
||||
export async function redisGetValue(connectionId: string, key: string): Promise<RedisValue> {
|
||||
|
||||
@@ -145,8 +145,10 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
|
||||
node.isLoading = true;
|
||||
try {
|
||||
const keys = await api.redisScanKeys(connectionId, db, "*", 500);
|
||||
node.children = keys.map((k) => ({
|
||||
const result = await api.redisScanKeys(connectionId, db, 0, "*", 200);
|
||||
node.scanCursor = result.cursor;
|
||||
node.hasMore = result.cursor !== 0;
|
||||
node.children = result.keys.map((k) => ({
|
||||
id: `${nodeId}:${k.key}`,
|
||||
label: `${k.key} [${k.key_type}]${k.ttl > 0 ? ` TTL:${k.ttl}` : ""}`,
|
||||
type: "redis-key" as const,
|
||||
@@ -160,6 +162,33 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMoreRedisKeys(connectionId: string, db: number, nodeId: string) {
|
||||
const node = findNode(treeNodes.value, nodeId);
|
||||
if (!node || node.isLoading || !node.hasMore) return;
|
||||
|
||||
node.isLoading = true;
|
||||
try {
|
||||
const result = await api.redisScanKeys(connectionId, db, node.scanCursor ?? 0, "*", 200);
|
||||
const existingIds = new Set((node.children ?? []).map((child) => child.id));
|
||||
const moreChildren = result.keys
|
||||
.filter((k) => !existingIds.has(`${nodeId}:${k.key}`))
|
||||
.map((k) => ({
|
||||
id: `${nodeId}:${k.key}`,
|
||||
label: `${k.key} [${k.key_type}]${k.ttl > 0 ? ` TTL:${k.ttl}` : ""}`,
|
||||
type: "redis-key" as const,
|
||||
connectionId,
|
||||
database: String(db),
|
||||
isExpanded: false,
|
||||
}));
|
||||
|
||||
node.scanCursor = result.cursor;
|
||||
node.hasMore = result.cursor !== 0;
|
||||
node.children = [...(node.children ?? []), ...moreChildren];
|
||||
} finally {
|
||||
node.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMongoDatabases(connectionId: string) {
|
||||
const node = findNode(treeNodes.value, connectionId);
|
||||
if (!node) return;
|
||||
@@ -455,6 +484,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
loadDatabases,
|
||||
loadRedisDatabases,
|
||||
loadRedisKeys,
|
||||
loadMoreRedisKeys,
|
||||
loadMongoDatabases,
|
||||
loadMongoCollections,
|
||||
loadSchemas,
|
||||
|
||||
@@ -78,6 +78,8 @@ export interface TreeNode {
|
||||
children?: TreeNode[];
|
||||
isLoading?: boolean;
|
||||
isExpanded?: boolean;
|
||||
scanCursor?: number;
|
||||
hasMore?: boolean;
|
||||
connectionId?: string;
|
||||
database?: string;
|
||||
schema?: string;
|
||||
|
||||
Reference in New Issue
Block a user